Files
boc/backend/handlers/sie4.go
T
Bernt 78b57273e2 security: Add proper authentication, RBAC, and tenant isolation
- 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
2026-08-10 12:52:48 +00:00

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",
}
}