package vismaparse import ( "encoding/csv" "errors" "fmt" "log/slog" "os" "regexp" "strconv" "strings" "time" "kryddan.xyz/bookkeep/types" ) var ( ErrInvalidRow error = errors.New("invalid input row") ErrInvalidTable error = errors.New("invalid input table") ) type accountRange struct { High int Low int } func LoadSheet(path string) (types.Organization, error) { f, err := os.Open(path) if err != nil { return types.Organization{}, err } defer func() { // Close the spreadsheet. if err := f.Close(); err != nil { fmt.Println(err) } }() csvReader := csv.NewReader(f) csvReader.Comma = ';' csvReader.LazyQuotes = true output, err := csvReader.ReadAll() if err != nil { return types.Organization{}, err } trimmed := trimHeaders(output) ranges := accountRanges(trimmed) accounts := []types.BookkeepingAccount{} for _, r := range ranges { account, err := constructBookkeepingAccount(trimmed[r.Low:r.High]) if err != nil { slog.Error("construct bookeeping account", slog.Any("err", err)) continue } accounts = append(accounts, account) } return types.Organization{Accounts: accounts}, nil } func trimHeaders(input [][]string) [][]string { for i, line := range input { if strings.Contains(line[0], "Konto") { return input[i+1:] } } return input } func accountRanges(input [][]string) []accountRange { var low int ranges := []accountRange{} for i, line := range input { if line[0] != "" && low == 0 { low = i } else if line[0] != "" { ranges = append( ranges, accountRange{ High: i - 1, Low: low, }, ) low = i } } return ranges } func constructBookkeepingAccount(input [][]string) (types.BookkeepingAccount, error) { if len(input) < 2 || input[0][0] == "" { return types.BookkeepingAccount{}, ErrInvalidTable } splitAccountString := strings.SplitN(input[0][0], " ", 2) accountNumber, err := strconv.Atoi(splitAccountString[0]) if err != nil { return types.BookkeepingAccount{}, err } rows := make([]types.BookkeepingAccountRow, 0, len(input)) for _, line := range input[2:] { row, err := cosntructBookeepingAccountRow(line) if err != nil { if errors.Is(err, ErrInvalidRow) { continue } return types.BookkeepingAccount{}, err } rows = append(rows, row) } return types.BookkeepingAccount{ AccountNumber: accountNumber, Name: splitAccountString[1], Rows: rows, }, nil } const ( ACCOUNT_INDEX int32 = 0 VERIFICATION_INDEX int32 = 1 DATE_INDEX int32 = 2 DESCRIPTION_INDEX int32 = 3 PROJECT_INDEX int32 = 7 KOSTNADSSTALLE_INDEX int32 = 8 DEBET_INDEX int32 = 10 KREDIT_INDEX int32 = 11 ) // index : value // 0 : Account // 1 : Verification // 2 : Date // 3 : Description // 4 : Transaction text // 5-6: VAT // 7 : Project // 8 : Kostnadsställe // 9 : Motpart // 10: Debet // 11: Kredit // 12: Beloppp // 13: Saldo func cosntructBookeepingAccountRow(input []string) (types.BookkeepingAccountRow, error) { if len(input) <= 11 || input[VERIFICATION_INDEX] == "" { return types.BookkeepingAccountRow{}, ErrInvalidRow } verification, err := parseVerification(input[VERIFICATION_INDEX]) if err != nil { return types.BookkeepingAccountRow{}, err } debet, err := parseAmount(input[DEBET_INDEX]) if err != nil { return types.BookkeepingAccountRow{}, err } kredit, err := parseAmount(input[KREDIT_INDEX]) if err != nil { return types.BookkeepingAccountRow{}, err } date, err := time.Parse(time.DateOnly, input[DATE_INDEX]) if err != nil { return types.BookkeepingAccountRow{}, err } linkedVerifications, err := parseLinkedVerifications(input[DESCRIPTION_INDEX]) if err != nil { return types.BookkeepingAccountRow{}, err } return types.BookkeepingAccountRow{ Verification: verification, LinkedVerifications: linkedVerifications, Project: input[PROJECT_INDEX], Description: input[DESCRIPTION_INDEX], OrganizationUnit: &input[KOSTNADSSTALLE_INDEX], Debet: debet, Kredit: kredit, Date: date, }, nil } func parseVerification(input string) (types.Verification, error) { r := regexp.MustCompile(`(?P