package main import ( "bufio" "fmt" "github.com/go-pdf/fpdf" "golang.org/x/text/language" "golang.org/x/text/message" "log" "math" "os" "regexp" "sort" "strconv" "strings" "time" ) /* own types */ type TransactionItem struct { Description string Debit string Credit string Amount float64 Vat_code string Amount_Vat float64 } type Transaction struct { Date time.Time Items []TransactionItem } type Balance struct { balance_start float64 balance_end float64 } /* global variable declaration */ var pdf *fpdf.Fpdf var yPos float64 var accounts = make(map[string]string) var transactions = make(map[uint16]Transaction) var account_balance = make(map[string]Balance) var balanceYear = "UNDEFINED" var profit float64 = 0.0 const reportTitle = "Jahresrechnung - nbit Informatik GmbH" const MWST_ACCOUNT = "2201" const defaultFontSize = 12 const smallFontSize = 8 const marginTop = 10 const smallLineSpacing = 5 const lineSpacing = 6 const tabstopLeft = 35 const tabstopRight = 160 const widthAmount = 28 const dashCorrectionXleft = 1.2 const dashCorrectionXright = -1.3 const dashCorrectionY = 3.5 func roundRappen(f float64) float64 { return (math.Round(f*100) / 100) } func accountExists(s string) bool { _, ok := accounts[s] return ok } func accountType(s string) string { return accounts[s][0:1] } func accountDescription(s string) string { return accounts[s][2:] } func accountBalanceExists(s string) bool { _, ok := account_balance[s] return ok } func calculateVAT(vat_code string, amount float64) float64 { if vat_code == "" { return 0.0 } vat_type := string(vat_code[0]) vat_perc, err := strconv.Atoi(vat_code[1:]) if err != nil { fmt.Printf("ERROR: %v\n", err) return 0.0 } vat_perc_f64 := float64(vat_perc) if vat_type == "V" { return roundRappen(0 - (amount / (1000.0 + vat_perc_f64) * vat_perc_f64)) } else if vat_type == "I" { return roundRappen(amount / (1000.0 + vat_perc_f64) * vat_perc_f64) } else { fmt.Printf("WARNING: Invalid Vat Type: %s\n", vat_type) return 0.0 } } func addTransaction(document_number string, date string, text string, account_number_debit string, account_number_credit string, amount string, vat_code string) { dateString := "02.01.2006" mydate, error := time.Parse(dateString, date) if error != nil { fmt.Printf("ERROR: %v\n", error) return } // we use the first transaction to set the year, all transactions not in this year will be invalid if balanceYear == "UNDEFINED" { balanceYear = date[6:10] } else { if date[6:10] != balanceYear { fmt.Printf("WARNING: transaction with Document Number %s is not in same year as first transaction: %s and will be ignored\n", document_number, balanceYear) return } } var myItem TransactionItem document_number_i, err := strconv.ParseInt(document_number, 0, 16) if err != nil { log.Fatal(err) } myItem.Description = text myItem.Debit = account_number_debit myItem.Credit = account_number_credit if s, err := strconv.ParseFloat(amount, 64); err == nil { myItem.Amount = roundRappen(s) } else { fmt.Printf("WARNING: Document %s, cannot convert Amount to Float64: %s and will be ignored\n", document_number, amount) return } myItem.Vat_code = vat_code myItem.Amount_Vat = calculateVAT(vat_code, myItem.Amount) // adjust balance values var myBalance Balance myBalance = account_balance[account_number_debit] account_type := accountType(account_number_debit) if (account_type == "A") || (account_type == "E") || (account_type == "L") { myBalance.balance_end = myBalance.balance_end + myItem.Amount } else { myBalance.balance_end = myBalance.balance_end - myItem.Amount } if myItem.Amount_Vat > 0 { myBalance.balance_end = myBalance.balance_end - myItem.Amount_Vat } account_balance[account_number_debit] = myBalance myBalance = account_balance[account_number_credit] account_type2 := accountType(account_number_credit) if (account_type2 == "A") || (account_type2 == "E") || (account_type2 == "L") { myBalance.balance_end = myBalance.balance_end - myItem.Amount } else { myBalance.balance_end = myBalance.balance_end + myItem.Amount } if myItem.Amount_Vat < 0 { myBalance.balance_end = myBalance.balance_end + myItem.Amount_Vat } account_balance[account_number_credit] = myBalance myBalance = account_balance[MWST_ACCOUNT] myBalance.balance_end = myBalance.balance_end + myItem.Amount_Vat account_balance[MWST_ACCOUNT] = myBalance _, ok := transactions[uint16(document_number_i)] if ok { newtransactions := transactions[uint16(document_number_i)] newtransactions.Items = append(newtransactions.Items, myItem) transactions[uint16(document_number_i)] = newtransactions } else { var t Transaction t.Date = mydate t.Items = append(t.Items, myItem) transactions[uint16(document_number_i)] = t } } func readAccountData(filename string) { file, err := os.Open(filename) if err != nil { log.Fatal(err) } defer file.Close() scanner := bufio.NewScanner(file) line_number := 1 for scanner.Scan() { line := scanner.Text() matched, _ := regexp.MatchString(`^[0-9][0-9][0-9][0-9],[ALEI]:.*`, line) if !matched { fmt.Printf("WARNING: Line %v in Accountfile %s: line not four digits followed by a comma followed by ALEI (one of those), Colon and description: %s and will be ignored.\n", line_number, filename, line) continue } token := strings.SplitN(line, ",", 2) account_number := token[0] account_type_and_description := token[1] if accountExists(account_number) { fmt.Printf("WARNING: Line %v in Accountfile %s: Account Number %s already exists and will be ignored\n", line_number, filename, account_number) continue } accounts[account_number] = account_type_and_description line_number++ } if err := scanner.Err(); err != nil { log.Fatal(err) } } func floatToString(f float64, sep string) string { var s string p := message.NewPrinter(language.English) if roundRappen(f) == 0.00 { s = "-.-" } else { s = strings.ReplaceAll(p.Sprintf("%.2f", f), ",", sep) //fmt.Printf("--- s: @%s@\n", s) } return s } func str2float64(f string) float64 { if s, err := strconv.ParseFloat(f, 64); err == nil { return roundRappen(s) } else { fmt.Printf("WARNING: cannot convert Amount to Float64: %s and will be ignored\n", f) return 0 } } func readTransactionData(filename string) { file, err := os.Open(filename) if err != nil { log.Fatal(err) } defer file.Close() scanner := bufio.NewScanner(file) line_number := 1 for scanner.Scan() { line := scanner.Text() matched, _ := regexp.MatchString(`^[0-9]+,[0-9][0-9]\.[0-9][0-9]\.[0-9][0-9][0-9][0-9],.*,[0-9][0-9][0-9][0-9],[0-9][0-9][0-9][0-9],-?[0-9]+\.[0-9][0-9],.*`, line) if matched { token := strings.Split(line, ",") document_number := token[0] date := token[1] text := strings.Replace(token[2], "@", ",", -1) account_number_debit := token[3] account_number_credit := token[4] amount := token[5] vat_code := token[6] if !accountExists(account_number_debit) { fmt.Printf("WARNING: Line %v in Transactionfile %s: Account Number Debit %s does not exist, and will be ignored\n", line_number, filename, account_number_debit) continue } if !accountExists(account_number_credit) { fmt.Printf("WARNING: Line %v in Transactionfile %s: Account Number Credit %s does not exist and will be ignored\n", line_number, filename, account_number_credit) continue } addTransaction(document_number, date, text, account_number_debit, account_number_credit, amount, vat_code) } else { matched2, _ := regexp.MatchString(`^[0-9][0-9][0-9][0-9]:-?[0-9]+\.[0-9][0-9]$`, line) if matched2 { token := strings.Split(line, ":") account_number := token[0] balance := token[1] if !accountExists(account_number) { fmt.Printf("WARNING: Line %v in Transactionfile %s: Account Number %s does not exist and will be ignored\n", line_number, filename, account_number) continue } f := str2float64(balance) var myBalance Balance myBalance.balance_start = f myBalance.balance_end = f account_balance[account_number] = myBalance } else { fmt.Printf("WARNING: Line %v in Transactionfile %s is not of the form ,,,,,, and will be ignored.\n", line_number, filename, line) continue } line_number++ } } if err := scanner.Err(); err != nil { log.Fatal(err) } } func writeText(x float64, y float64, w float64, text string, alignStr ...string) { tr := pdf.UnicodeTranslatorFromDescriptor("") align := "LT" if len(alignStr) > 0 { align = alignStr[0] } pdf.SetXY(x, y) pdf.CellFormat(w, 11, tr(text), "", 0, align, false, 0, "") } func setupBalanceSheet() { pdf = fpdf.New("P", "mm", "A4", "") pdf.SetMargins(0, 0, 0) pdf.SetAutoPageBreak(false, 0) pdf.SetFontLocation("fonts") pdf.AddFont("Dejavusans", "", "DejaVuSans.json") pdf.AddFont("Dejavusans-Bold", "", "DejaVuSans-Bold.json") pdf.SetFont("Dejavusans", "", defaultFontSize) } func printPageHeader() { pdf.AddPage() yPos = marginTop pdf.SetFont("Dejavusans-Bold", "", defaultFontSize) writeText(tabstopLeft, yPos, 0, reportTitle) yPos = yPos + lineSpacing pdf.SetFont("Dejavusans", "", smallFontSize) writeText(tabstopLeft, yPos, 0, "per 31.12."+balanceYear) yPos = yPos + lineSpacing + smallLineSpacing } func printSection(section string) { var title string var change_sign bool = false switch section { // Assets case "A": title = "AKTIVEN" // Liabilities case "L": title = "PASSIVEN" change_sign = true // Expense case "E": title = "AUFWAND" // Income case "I": title = "ERTRAG" default: fmt.Printf("WARNING: invalid section: %s\n", section) return } var total float64 = 0.0 pdf.SetFont("Dejavusans-Bold", "", smallFontSize) writeText(tabstopLeft, yPos, 0, title) writeText(tabstopRight, yPos, widthAmount, "31.12."+balanceYear, "TR") pdf.SetDashPattern([]float64{}, 0) pdf.Line(tabstopLeft+dashCorrectionXleft, yPos+dashCorrectionY, tabstopRight+widthAmount+dashCorrectionXright, yPos+dashCorrectionY) yPos = yPos + smallLineSpacing pdf.SetFont("Dejavusans", "", smallFontSize) pdf.SetDashPattern([]float64{0.2, 0.2}, 0) // Extract keys from map keys := make([]string, 0, len(account_balance)) for k := range account_balance { keys = append(keys, k) } // Sort keys sort.Strings(keys) for _, key := range keys { if accountType(key) == section { writeText(tabstopLeft, yPos, 0, key+": "+accountDescription(key)) balance_end := account_balance[key].balance_end if change_sign { balance_end = 0 - balance_end } writeText(tabstopRight, yPos, widthAmount, floatToString(balance_end, "'"), "TR") pdf.Line(tabstopLeft+dashCorrectionXleft, yPos+dashCorrectionY, tabstopRight+widthAmount+dashCorrectionXright, yPos+dashCorrectionY) total = total + balance_end yPos = yPos + smallLineSpacing } } switch section { case "A": if profit < 0 { writeText(tabstopLeft, yPos, 0, "Verlust") writeText(tabstopRight, yPos, widthAmount, floatToString(0-profit, "'"), "TR") pdf.Line(tabstopLeft+dashCorrectionXleft, yPos+dashCorrectionY, tabstopRight+widthAmount+dashCorrectionXright, yPos+dashCorrectionY) total = total - profit yPos = yPos + smallLineSpacing } case "L": if profit >= 0 { writeText(tabstopLeft, yPos, 0, "Gewinn") writeText(tabstopRight, yPos, widthAmount, floatToString(profit, "'"), "TR") pdf.Line(tabstopLeft+dashCorrectionXleft, yPos+dashCorrectionY, tabstopRight+widthAmount+dashCorrectionXright, yPos+dashCorrectionY) total = total + profit yPos = yPos + smallLineSpacing } } pdf.SetFont("Dejavusans-Bold", "", smallFontSize) writeText(tabstopLeft, yPos, 0, "TOTAL "+title) writeText(tabstopRight, yPos, widthAmount, floatToString(total, "'"), "TR") pdf.SetDashPattern([]float64{}, 0) pdf.Line(tabstopLeft+dashCorrectionXleft, yPos+dashCorrectionY, tabstopRight+widthAmount+dashCorrectionXright, yPos+dashCorrectionY) yPos = yPos + smallLineSpacing pdf.SetDashPattern([]float64{0.2, 0.2}, 0) pdf.SetFont("Dejavusans", "", smallFontSize) switch section { case "E": if profit < 0 { writeText(tabstopLeft, yPos, 0, "Verlust") writeText(tabstopRight, yPos, widthAmount, floatToString(0-profit, "'"), "TR") pdf.Line(tabstopLeft+dashCorrectionXleft, yPos+dashCorrectionY, tabstopRight+widthAmount+dashCorrectionXright, yPos+dashCorrectionY) total = total - profit yPos = yPos + smallLineSpacing } case "I": if profit >= 0 { writeText(tabstopLeft, yPos, 0, "Gewinn") writeText(tabstopRight, yPos, widthAmount, floatToString(profit, "'"), "TR") pdf.Line(tabstopLeft+dashCorrectionXleft, yPos+dashCorrectionY, tabstopRight+widthAmount+dashCorrectionXright, yPos+dashCorrectionY) total = total + profit yPos = yPos + smallLineSpacing } } yPos = yPos + smallLineSpacing } func createBalanceSheet() { setupBalanceSheet() printPageHeader() printSection("A") printSection("L") printPageHeader() printSection("E") printSection("I") err := pdf.OutputFileAndClose("output.pdf") if err == nil { fmt.Printf("INFO: Successfully created Balance Sheet in file output.pdf\n") } else { fmt.Printf("ERROR: %v\n", err) } } func calculateProfit() float64 { var res float64 = 0.0 for key := range account_balance { if accountType(key) == "I" { res = res + account_balance[key].balance_end } if accountType(key) == "E" { res = res - account_balance[key].balance_end } } res = roundRappen(res) fmt.Printf("INFO: Calculated Profit: %v\n", res) return res } func usage() { fmt.Printf("usage: bookkeeper \n") fmt.Printf("\n") fmt.Printf("Valid actions: check, balance, journal, mwst1, mwst2, mwst3, mwst4, new_year\n") os.Exit(1) } func main() { if len(os.Args) != 4 { usage() } readAccountData(os.Args[2]) readTransactionData(os.Args[3]) profit = calculateProfit() //fmt.Printf("accounts: %#v\n", accounts) //fmt.Printf("transactions: %#v\n", transactions) //fmt.Printf("account_balance: %#v\n", account_balance) switch action := os.Args[1]; action { case "check": fmt.Println("Check Data") case "balance": fmt.Println("Create Balance Sheet") createBalanceSheet() case "journal": fmt.Println("Create Journal") case "mwst1": fmt.Println("Create Mwst1") case "mstw2": fmt.Println("Create Mwst2") case "mwst3": fmt.Println("Create Mwst3") case "mwst4": fmt.Println("Create Mwst4") case "new_year": fmt.Println("Create New Year") default: usage() } }