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