bookkeeper/bookkeeper.go

521 lines
14 KiB
Go

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 <document number>,<date>,<text>,<account number debit>,<account number credit>,<amount>,<vat code> 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 <action> <accounts file> <transactions file>\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()
}
}