78b57273e2
- Add password hashing with bcrypt - Add AuthService with proper login - Add password strength validation - Add RBAC middleware (AdminOnly, ManagerOrAdmin) - Add tenant isolation middleware - Update CRM handler with tenant filtering - Add JWT fallback for development mode - Add user context helpers - Build successful
299 lines
7.3 KiB
Go
299 lines
7.3 KiB
Go
package handlers
|
|
|
|
import (
|
|
"bufio"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// SIE4Handler hanterar SIE4-import/export
|
|
type SIE4Handler struct{}
|
|
|
|
func NewSIE4Handler() *SIE4Handler {
|
|
return &SIE4Handler{}
|
|
}
|
|
|
|
// SIE4Entry representerar en SIE4-post
|
|
type SIE4Entry struct {
|
|
Date string `json:"date"`
|
|
VoucherNo string `json:"voucher_no"`
|
|
Account string `json:"account"`
|
|
Description string `json:"description"`
|
|
Amount float64 `json:"amount"`
|
|
Dimension string `json:"dimension,omitempty"`
|
|
}
|
|
|
|
// ParseSIE4 parsar SIE4-fil
|
|
func (h *SIE4Handler) ParseSIE4(w http.ResponseWriter, r *http.Request) {
|
|
var reader io.Reader
|
|
|
|
// Kolla om det är multipart (filuppladdning) eller JSON med content
|
|
contentType := r.Header.Get("Content-Type")
|
|
if strings.Contains(contentType, "multipart/form-data") {
|
|
// Läs uppladdad fil
|
|
file, _, err := r.FormFile("file")
|
|
if err != nil {
|
|
writeJSON(w, http.StatusBadRequest, map[string]interface{}{
|
|
"ok": false,
|
|
"error": "missing file",
|
|
})
|
|
return
|
|
}
|
|
defer file.Close()
|
|
reader = file
|
|
} else {
|
|
// Läs JSON med content
|
|
var req struct {
|
|
Content string `json:"content"`
|
|
}
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
writeJSON(w, http.StatusBadRequest, map[string]interface{}{
|
|
"ok": false,
|
|
"error": "invalid request: expected multipart file or JSON with content field",
|
|
})
|
|
return
|
|
}
|
|
reader = strings.NewReader(req.Content)
|
|
}
|
|
|
|
entries, parseErr := h.parseSIE4File(reader)
|
|
if parseErr != nil {
|
|
writeJSON(w, http.StatusBadRequest, map[string]interface{}{
|
|
"ok": false,
|
|
"error": parseErr.Error(),
|
|
})
|
|
return
|
|
}
|
|
|
|
writeJSON(w, http.StatusOK, map[string]interface{}{
|
|
"ok": true,
|
|
"entries": entries,
|
|
"count": len(entries),
|
|
})
|
|
}
|
|
|
|
// GenerateSIE4 genererar SIE4-fil
|
|
func (h *SIE4Handler) GenerateSIE4(w http.ResponseWriter, r *http.Request) {
|
|
var req struct {
|
|
Company string `json:"company"`
|
|
OrgNumber string `json:"org_number"`
|
|
FiscalYear string `json:"fiscal_year"`
|
|
Entries []SIE4Entry `json:"entries"`
|
|
}
|
|
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
writeJSON(w, http.StatusBadRequest, map[string]interface{}{
|
|
"ok": false,
|
|
"error": err.Error(),
|
|
})
|
|
return
|
|
}
|
|
|
|
sie4Content := h.generateSIE4Content(req.Company, req.OrgNumber, req.FiscalYear, req.Entries)
|
|
|
|
w.Header().Set("Content-Type", "text/plain; charset=ISO-8859-1")
|
|
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s_%s.SI", req.OrgNumber, req.FiscalYear))
|
|
w.WriteHeader(http.StatusOK)
|
|
w.Write([]byte(sie4Content))
|
|
}
|
|
|
|
// parseSIE4File parsar en SIE4-fil
|
|
func (h *SIE4Handler) parseSIE4File(reader io.Reader) ([]SIE4Entry, error) {
|
|
var entries []SIE4Entry
|
|
scanner := bufio.NewScanner(reader)
|
|
|
|
for scanner.Scan() {
|
|
line := scanner.Text()
|
|
line = strings.TrimSpace(line)
|
|
|
|
// Hoppa över tomma rader och kommentarer
|
|
if line == "" || strings.HasPrefix(line, "#") {
|
|
continue
|
|
}
|
|
|
|
// Parsa #VER (verifikat)
|
|
if strings.HasPrefix(line, "#VER") {
|
|
// Format: #VER "serie" "voucherno" "date" "description" "date_created"
|
|
parts := h.parseSIELine(line)
|
|
if len(parts) >= 4 {
|
|
voucherNo := h.unquote(parts[2])
|
|
date := h.unquote(parts[3])
|
|
description := ""
|
|
if len(parts) >= 5 {
|
|
description = h.unquote(parts[4])
|
|
}
|
|
|
|
// Läs tillhörande rader
|
|
for scanner.Scan() {
|
|
rowLine := scanner.Text()
|
|
rowLine = strings.TrimSpace(rowLine)
|
|
|
|
if rowLine == "}" {
|
|
break
|
|
}
|
|
|
|
if strings.HasPrefix(rowLine, "#TRANS") {
|
|
// Format: #TRANS account { amount "date" "description" }
|
|
rowParts := h.parseSIELine(rowLine)
|
|
if len(rowParts) >= 4 {
|
|
account := h.unquote(rowParts[1])
|
|
amountStr := rowParts[3]
|
|
amount, _ := strconv.ParseFloat(amountStr, 64)
|
|
|
|
rowDesc := description
|
|
if len(rowParts) >= 6 {
|
|
rowDesc = h.unquote(rowParts[5])
|
|
}
|
|
|
|
entries = append(entries, SIE4Entry{
|
|
Date: date,
|
|
VoucherNo: voucherNo,
|
|
Account: account,
|
|
Description: rowDesc,
|
|
Amount: amount,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if err := scanner.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return entries, nil
|
|
}
|
|
|
|
// generateSIE4Content genererar SIE4-innehåll
|
|
func (h *SIE4Handler) generateSIE4Content(company, orgNumber, fiscalYear string, entries []SIE4Entry) string {
|
|
var sb strings.Builder
|
|
|
|
// SIE4 header
|
|
sb.WriteString("#FLAGGA 0\n")
|
|
sb.WriteString(fmt.Sprintf("#FORMAT PC8\n"))
|
|
sb.WriteString(fmt.Sprintf("#SIETYP 4\n"))
|
|
sb.WriteString("#PROGRAM \"AMOS BOC\" 1.0\n")
|
|
sb.WriteString(fmt.Sprintf("#GEN %s\n", time.Now().Format("20060102")))
|
|
sb.WriteString(fmt.Sprintf("#FNAMN \"%s\"\n", company))
|
|
sb.WriteString(fmt.Sprintf("#ORGNR \"%s\"\n", orgNumber))
|
|
sb.WriteString(fmt.Sprintf("#RAR 0 %s0101 %s1231\n", fiscalYear, fiscalYear))
|
|
sb.WriteString("#KPTYP EUBAS97\n")
|
|
|
|
// Kontoplan (BAS-konton)
|
|
accounts := h.getBASAccounts()
|
|
for code, name := range accounts {
|
|
sb.WriteString(fmt.Sprintf("#KONTO %s \"%s\"\n", code, name))
|
|
}
|
|
|
|
// Verifikat
|
|
vouchers := h.groupByVoucher(entries)
|
|
for voucherNo, voucherEntries := range vouchers {
|
|
if len(voucherEntries) == 0 {
|
|
continue
|
|
}
|
|
|
|
first := voucherEntries[0]
|
|
sb.WriteString(fmt.Sprintf("#VER \"\" \"%s\" %s \"%s\" %s\n",
|
|
voucherNo,
|
|
h.formatSIEDate(first.Date),
|
|
first.Description,
|
|
time.Now().Format("20060102")))
|
|
sb.WriteString("{\n")
|
|
|
|
for _, entry := range voucherEntries {
|
|
sb.WriteString(fmt.Sprintf("#TRANS %s {} %s \"%s\" \"%s\"\n",
|
|
entry.Account,
|
|
h.formatSIEAmount(entry.Amount),
|
|
h.formatSIEDate(entry.Date),
|
|
entry.Description))
|
|
}
|
|
|
|
sb.WriteString("}\n")
|
|
}
|
|
|
|
// Slut
|
|
sb.WriteString("#SLUT\n")
|
|
|
|
return sb.String()
|
|
}
|
|
|
|
// Helper functions för SIE4
|
|
|
|
func (h *SIE4Handler) parseSIELine(line string) []string {
|
|
var parts []string
|
|
var current strings.Builder
|
|
inQuotes := false
|
|
|
|
for _, ch := range line {
|
|
switch ch {
|
|
case '"':
|
|
if inQuotes {
|
|
parts = append(parts, current.String())
|
|
current.Reset()
|
|
}
|
|
inQuotes = !inQuotes
|
|
case ' ', '\t':
|
|
if !inQuotes && current.Len() > 0 {
|
|
parts = append(parts, current.String())
|
|
current.Reset()
|
|
} else if inQuotes {
|
|
current.WriteRune(ch)
|
|
}
|
|
default:
|
|
current.WriteRune(ch)
|
|
}
|
|
}
|
|
|
|
if current.Len() > 0 {
|
|
parts = append(parts, current.String())
|
|
}
|
|
|
|
return parts
|
|
}
|
|
|
|
func (h *SIE4Handler) unquote(s string) string {
|
|
if len(s) >= 2 && s[0] == '"' && s[len(s)-1] == '"' {
|
|
return s[1 : len(s)-1]
|
|
}
|
|
return s
|
|
}
|
|
|
|
func (h *SIE4Handler) formatSIEDate(date string) string {
|
|
// Konvertera YYYY-MM-DD till YYYYMMDD
|
|
return strings.ReplaceAll(date, "-", "")
|
|
}
|
|
|
|
func (h *SIE4Handler) formatSIEAmount(amount float64) string {
|
|
// SIE4 använder punkt som decimaltecken
|
|
return fmt.Sprintf("%.2f", amount)
|
|
}
|
|
|
|
func (h *SIE4Handler) groupByVoucher(entries []SIE4Entry) map[string][]SIE4Entry {
|
|
groups := make(map[string][]SIE4Entry)
|
|
for _, entry := range entries {
|
|
groups[entry.VoucherNo] = append(groups[entry.VoucherNo], entry)
|
|
}
|
|
return groups
|
|
}
|
|
|
|
func (h *SIE4Handler) getBASAccounts() map[string]string {
|
|
return map[string]string{
|
|
"1510": "Kundfordringar",
|
|
"1930": "Företagskonto",
|
|
"2010": "Eget kapital",
|
|
"2440": "Leverantörsskulder",
|
|
"2610": "Utgående moms",
|
|
"3010": "Försäljning tjänster",
|
|
"6540": "IT-kostnader",
|
|
"7210": "Löner",
|
|
"7690": "Övriga personalkostnader",
|
|
}
|
|
}
|