package finance import ( "encoding/csv" "fmt" "log" "os" "path" "strconv" "strings" ) const ( BillingCode = iota Date InvoiceNumber AccountName BillingClass Amount ) const ( DotRune = 65533 expenseAccountCutoff = 50000 ) var ( TrialBalanceHeaders = []string{"Account Name", "Amount", "Period", "Account Type"} ) type ( BillingLine struct { BillingCode string Date string InvoiceNumber string AccountName string Class string Amount string } TrialBalanceLine struct { Amount float64 AccountName string Period string AccountType string } ) func ProcessTrialBalances(pathlikeIn string, pathlikeOut string) { paths := enumFiles(pathlikeIn) table := make([]*[]*TrialBalanceLine, 0, 100) for _, p := range *paths { table = append(table, processTrialBalance(p, true)) } writeTable(&table, pathlikeOut) } func writeTable(table *[]*[]*TrialBalanceLine, pathlikeOut string) { writer := createCsvWriter(pathlikeOut) err := writer.Write(TrialBalanceHeaders) if err != nil { log.Panic(err) } for _, sheet := range *table { for _, line := range *sheet { writer.Write(line.toRow()) } } writer.Flush() } func formatDate(period string) string { month := period[0:2] date := period[2:4] year := period[4:6] return fmt.Sprintf("20%s-%s-%s", year, month, date) } func processTrialBalance(pathlike *string, skipHeaders bool) *[]*TrialBalanceLine { ret := make([]*TrialBalanceLine, 0, 50) file, err := os.OpenFile(*pathlike, os.O_RDONLY, 0755) if err != nil { log.Panic(err) } defer file.Close() reader := csv.NewReader(file) table, err := reader.ReadAll() if err != nil { log.Panic(err) } period := getDateFromFileName(pathlike) skipped := false buff := make([]rune, 500) for _, row := range table { if !skipped && skipHeaders { skipped = true continue } rowBuffer, err := rowBufferToBalanceLine(&row, &buff, period) if err != nil { log.Printf("error found in %s\n", *pathlike) panic(err) } ret = append(ret, rowBuffer) } return &ret } func getDateFromFileName(pathlike *string) string { fileName := path.Base(*pathlike) date := strings.Split(fileName, " ")[0] return date } func getAccountTypeFromName(accountName string) (string, error) { parts := strings.Split(accountName, " ") if parts[0] != "" { } number, err := strconv.Atoi(parts[0]) if err != nil { return "", err } if number < expenseAccountCutoff { return "income", nil } else { return "expense", nil } } func rowBufferToBalanceLine(row *[]string, buffer *[]rune, date string) (*TrialBalanceLine, error) { if strings.Index((*row)[0], ":") > -1 { return revenueRowBufferToBalanceLine(row, buffer, date), nil } else { row, err := expenseRowBufferToBalanceLine(row, buffer, date) if err != nil { return nil, err } return row, nil } } func revenueRowBufferToBalanceLine(row *[]string, buffer *[]rune, date string) *TrialBalanceLine { balance := TrialBalanceLine{} target := strings.Split((*row)[0], ":")[1] dotIdx := strings.IndexRune(target, DotRune) buff := *buffer if dotIdx > -1 { src := []rune(target) copy(buff[:dotIdx-1], src[:dotIdx-1]) copy(buff[dotIdx-1:len(src)-2], src[dotIdx+1:]) target = string(buff[:len(src)-2]) } amount, err := strconv.ParseFloat((*row)[2], 64) if err != nil { log.Println("Error in revenueRowBufferToBalanceLine") log.Println(err) amount = 0.0 } balance.AccountName = target balance.Amount = amount balance.Period = formatDate(date) balance.AccountType, err = getAccountTypeFromName(target) if err != nil { panic(err) } return &balance } func expenseRowBufferToBalanceLine(row *[]string, buffer *[]rune, date string) (*TrialBalanceLine, error) { balance := TrialBalanceLine{} target := (*row)[0] dotIdx := strings.IndexRune(target, DotRune) buff := *buffer if dotIdx > -1 { src := []rune(target) copy(buff[:dotIdx-1], src[:dotIdx-1]) copy(buff[dotIdx-1:len(src)-2], src[dotIdx+1:]) target = string(buff[:len(src)-2]) } //carve-out for the revenue parent account amountIdx := 1 if target == "40000 Revenue" { amountIdx = 2 } amount, err := strconv.ParseFloat((*row)[amountIdx], 64) if err != nil { log.Println("Error in expenseRowBufferToBalanceLine") log.Println(err) amount = 0.0 } balance.AccountName = target balance.Amount = amount balance.Period = formatDate(date) balance.AccountType, err = getAccountTypeFromName(target) if err != nil { return nil, err } return &balance, nil } func ProcessCSVsFromPath(pathlikeIn string, pathlikeOut string) { paths := enumFiles(pathlikeIn) writer := createCsvWriter(pathlikeOut) headers := []string{"Billing Code", "Date", "Invoice Number", "Account Name", "Class", "Amount"} err := writer.Write(headers) if err != nil { log.Panic(err) } for _, pptr := range *paths { lines := processFile(pptr) for _, line := range *lines { writer.Write(line.toRow()) } } writer.Flush() } func processFile(pathlike *string) *[]*BillingLine { ret := make([]*BillingLine, 0, 500000) content, err := os.OpenFile(*pathlike, os.O_RDONLY, 0755) if err != nil { log.Panic(err) } csvReader := csv.NewReader(content) records, err := csvReader.ReadAll() if err != nil { log.Panic(err) } currentBillingCode := "" for idx, recordRow := range records { serviceName := recordRow[BillingCode] if idx < 2 || strings.ToLower(serviceName) == "total" { continue //every ignorable row starts with "TOTAL" } else if len(serviceName) > 0 { // this will set the currentBillingCode to "smear" down the column to fill in the fields that QB refuses currentBillingCode = serviceName continue } lineItem := BillingLine{ BillingCode: currentBillingCode, Date: convertDateToSqlDate(recordRow[Date]), InvoiceNumber: recordRow[InvoiceNumber], AccountName: recordRow[AccountName], Class: recordRow[BillingClass], Amount: recordRow[Amount], } if err != nil { log.Printf("%#v\n", recordRow) log.Panic(err) } ret = append(ret, &lineItem) } return &ret } func enumFiles(pathlikebase string) *[]*string { files, err := os.ReadDir(pathlikebase) ret := make([]*string, 0, 100) if err != nil { panic(err) } for _, file := range files { subPath := fmt.Sprintf("%s%s", pathlikebase, file.Name()) ret = append(ret, &subPath) } return &ret } func (b *BillingLine) toRow() []string { return []string{b.BillingCode, b.Date, b.InvoiceNumber, b.AccountName, b.Class, b.Amount} } func (t *TrialBalanceLine) toRow() []string { return []string{t.AccountName, strconv.FormatFloat(t.Amount, 'f', 2, 64), t.Period, t.AccountType} } // /*====================================================================================== util ======================================================================================*/ func convertDateToSqlDate(datelike string) string { parts := strings.Split(datelike, "/") return fmt.Sprintf("%s-%s-%s", parts[2], parts[0], parts[1]) } func readCsv(pathlike string) *[][]string { file, err := os.OpenFile(pathlike, os.O_RDONLY, 0755) if err != nil { log.Panic(err) } defer file.Close() reader := csv.NewReader(file) contents, err := reader.ReadAll() if err != nil { log.Panic(err) } return &contents } func createCsvWriter(pathlike string) *csv.Writer { _, err := os.Stat(pathlike) var file *os.File if os.IsNotExist(err) { file, err = os.Create(pathlike) if err != nil { log.Panic(err) } } else { os.Remove(pathlike) file, err = os.Create(pathlike) if err != nil { log.Panic(err) } } writer := csv.NewWriter(file) return writer } //