package main import ( "bufio" "fmt" "github.com/go-pdf/fpdf" "golang.org/x/text/language" "golang.org/x/text/message" "log" "os" "regexp" "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" const reportTitle = "Jahresrechnung - nbit Informatik GmbH" const defaultFontSize = 9 const smallFontSize = 7 const marginTop = 10 const smallLineSpacing = 4 const lineSpacing = 5 const tabstopLeft = 25 const tabstopRight = 100 const widthAmount = 28 const dashCorrectionXleft = 1.2 const dashCorrectionXright = -1.3 const dashCorrectionY = 3 //func round5rappen(f float64) float64 { // return (math.Round(f*20) / 20) //} func accountExists(s string) bool { _, ok := accounts[s] return ok } func addTransaction(document_number string, date string, text string, account_number_debit string, account_number_credit string, amount string, vat_code string) { //fmt.Printf("Calling addTransaction with amount: %s\n", amount) dateString := "02.01.2006" mydate, error := time.Parse(dateString, date) if error != nil { fmt.Println(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 = 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 _, 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],.*`, line) if !matched { fmt.Printf("WARNING: Line %v in Accountfile %s: line not four digits followed by a comma: %s and will be ignored.\n", line_number, filename, line) continue } token := strings.SplitN(line, ",", 2) account_number := token[0] account_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_description line_number++ } if err := scanner.Err(); err != nil { log.Fatal(err) } } func floatToString(f float64, sep string) string { p := message.NewPrinter(language.English) 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 (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 chars string switch section { case "assets": title = "AKTIVEN" chars = "1" case "liabilities": title = "PASSIVEN" chars = "2" case "expense": title = "AUFWAND" chars = "568" case "income": title = "ERTRAG" chars = "4" 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") yPos = yPos + smallLineSpacing pdf.SetFont("Dejavusans", "", smallFontSize) pdf.SetDashPattern([]float64{0.2, 0.2}, 0) for key, element := range account_balance { if strings.Contains(chars, key[0:1]) { writeText(tabstopLeft, yPos, 0, accounts[key]) writeText(tabstopRight, yPos, widthAmount, floatToString(element.balance_end, "'"), "TR") pdf.Line(tabstopLeft+dashCorrectionXleft, yPos+dashCorrectionY, tabstopRight+widthAmount+dashCorrectionXright, yPos+dashCorrectionY) total = total + element.balance_end yPos = yPos + smallLineSpacing } } pdf.SetFont("Dejavusans-Bold", "", smallFontSize) writeText(tabstopLeft, yPos, 0, "TOTAL "+title) writeText(tabstopRight, yPos, widthAmount, floatToString(total, "'"), "TR") yPos = yPos + smallLineSpacing + smallLineSpacing } func createBalanceSheet() { setupBalanceSheet() printPageHeader() printSection("assets") printSection("liabilities") printSection("expense") printSection("income") err := pdf.OutputFileAndClose("output.pdf") if err == nil { fmt.Printf("Successfully created Balance Sheet in file output.pdf\n") } else { fmt.Printf("Error: %v\n", err) } } 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]) //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() } }