You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

337 lines
7.6 KiB
Go

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}
}
//<editor-fold name="util">
/*======================================================================================
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
}
//</editor-fold>