first commit
This commit is contained in:
commit
70fb02847f
|
|
@ -0,0 +1,3 @@
|
|||
bookkeeper
|
||||
testdata/*
|
||||
*.pdf
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
bookkeeper is a tool of nbit Information to replace Banana :-)
|
||||
|
||||
It is a simple CLI tool written in Golang by Joerg Lehmann
|
||||
|
||||
Usage:
|
||||
bookkeeper <action> <accounts file> <transactions file>
|
||||
|
||||
action can be one of the following:
|
||||
- check : checks data
|
||||
- balance : creates a balance sheet on STDOUT (Format: PDF)
|
||||
- journal : creates a journal on STDOUT (Format: Markdown)
|
||||
- mwst1 : create VAT report (1st to 4th quarter)
|
||||
mwst2 on STDOUT
|
||||
mwst3
|
||||
mwst4
|
||||
- new_year : writes current account balance values to STDOUT (to be used for
|
||||
new transaction file for next year)
|
||||
|
||||
<accounts file> is a text file with the following format (example line):
|
||||
|
||||
1020,Kontokorrent UBS 235-566236.01G
|
||||
|
||||
1020: Account number: 1XXX => Assets
|
||||
2XXX => Liabilities
|
||||
3XXX => Income
|
||||
4XXX => Expense
|
||||
5XXX => Personnel Expenses
|
||||
6XXX => Operating Expenses
|
||||
8XXX => Expense
|
||||
|
||||
Kontokorrent UBS 235-566236.01G: Account Text
|
||||
|
||||
<transactions file> is a CSV file with the following format (example line):
|
||||
|
||||
1,10.1.2023,Text,6574,1020,10.00,I77
|
||||
|
||||
1: document number, starts each year with 1 with incremening, there can be
|
||||
more than one transaction per document number
|
||||
10.1.2023: date (DD.MM.YYYY)
|
||||
Text: description of transaction (commas should be replaced with @)
|
||||
6574: account number debit (Soll)
|
||||
1020: account number credit (Haben)
|
||||
10.00: value
|
||||
I77: MwSt Code (starting with I or V, 77 means 7.7%)
|
||||
|
||||
the beginning of the transaction file can include the current balance of
|
||||
accounts in the following form:
|
||||
|
||||
1020:1234.56
|
||||
|
||||
1020: account
|
||||
1234.56: balance at start of year
|
||||
|
|
@ -0,0 +1,287 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"github.com/go-pdf/fpdf"
|
||||
"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
|
||||
}
|
||||
|
||||
/* 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]string)
|
||||
var yearEndDate = "YEARENDDATE"
|
||||
|
||||
const reportTitle = "Jahresrechnung - nbit Informatik GmbH"
|
||||
const defaultFontSize = 9
|
||||
const marginTop = 10
|
||||
const lineSpacing = 5
|
||||
const tabstopLeft = 20
|
||||
|
||||
//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
|
||||
}
|
||||
|
||||
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("Cannot convert Amount to Float64: %s\n", amount)
|
||||
}
|
||||
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("Line %v in Accountfile %s: line not four digits followed by a comma: %s.\n", line_number, filename, line)
|
||||
os.Exit(3)
|
||||
}
|
||||
|
||||
token := strings.SplitN(line, ",", 2)
|
||||
account_number := token[0]
|
||||
account_description := token[1]
|
||||
|
||||
if accountExists(account_number) {
|
||||
fmt.Printf("Line %v in Accountfile %s: Account Number %s already exists\n", line_number, filename, account_number)
|
||||
os.Exit(4)
|
||||
|
||||
}
|
||||
|
||||
accounts[account_number] = account_description
|
||||
|
||||
line_number++
|
||||
}
|
||||
|
||||
if err := scanner.Err(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
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("Line %v in Transactionfile %s: Account Number Debit %s does not exist\n", line_number, filename, account_number_debit)
|
||||
os.Exit(4)
|
||||
}
|
||||
if !accountExists(account_number_credit) {
|
||||
fmt.Printf("Line %v in Transactionfile %s: Account Number Credit %s does not exist\n", line_number, filename, account_number_credit)
|
||||
os.Exit(4)
|
||||
}
|
||||
|
||||
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("Line %v in Transactionfile %s: Account Number %s does not exist\n", line_number, filename, account_number)
|
||||
os.Exit(4)
|
||||
}
|
||||
|
||||
account_balance[account_number] = balance
|
||||
|
||||
} else {
|
||||
fmt.Printf("Line %v in Transactionfile %s is not of the form <document number>,<date>,<text>,<account number debit>,<account number credit>,<amount>,<vat code>.\n", line_number, filename, line)
|
||||
os.Exit(3)
|
||||
}
|
||||
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", "", defaultFontSize)
|
||||
writeText(tabstopLeft, yPos, 0, "per "+yearEndDate)
|
||||
}
|
||||
|
||||
func printAssets() {
|
||||
}
|
||||
|
||||
func printLiabilities() {
|
||||
}
|
||||
|
||||
func printExpenses() {
|
||||
}
|
||||
|
||||
func printIncome() {
|
||||
}
|
||||
|
||||
func createBalanceSheet() {
|
||||
setupBalanceSheet()
|
||||
printPageHeader()
|
||||
printAssets()
|
||||
printLiabilities()
|
||||
printExpenses()
|
||||
printIncome()
|
||||
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 <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])
|
||||
|
||||
//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()
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
{"Tp":"TrueType","Name":"DejaVuSans-Bold","Desc":{"Ascent":760,"Descent":-240,"CapHeight":760,"Flags":32,"FontBBox":{"Xmin":-1069,"Ymin":-415,"Xmax":1975,"Ymax":1175},"ItalicAngle":0,"StemV":120,"MissingWidth":600},"Up":-20,"Ut":44,"Cw":[600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,348,456,521,838,696,1002,872,306,457,457,523,838,380,415,380,365,696,696,696,696,696,696,696,696,696,696,400,400,838,838,838,580,1000,774,762,734,830,683,683,821,837,372,372,775,637,995,837,850,733,850,770,720,682,812,774,1103,771,724,725,457,365,457,838,500,500,675,716,593,716,678,435,716,712,343,343,665,343,1042,712,687,716,716,493,595,478,712,652,924,645,652,582,712,365,712,838,600,696,600,380,435,657,1000,500,500,500,1440,720,412,1167,600,725,600,600,380,380,657,657,639,500,1000,500,1000,595,412,1094,600,582,724,348,456,696,696,636,696,365,500,500,1000,564,646,838,415,1000,500,500,838,438,438,500,736,636,380,500,438,564,646,1035,1035,1035,580,774,774,774,774,774,774,1085,734,683,683,683,683,372,372,372,372,838,837,850,850,850,850,850,838,850,812,812,812,812,724,738,719,675,675,675,675,675,675,1048,593,678,678,678,678,343,343,343,343,687,712,687,687,687,687,687,838,687,712,712,712,712,652,716,652],"Enc":"cp1252","Diff":"","File":"DejaVuSans-Bold.z","Size1":0,"Size2":0,"OriginalSize":705684,"N":0,"DiffN":0}
|
||||
Binary file not shown.
|
|
@ -0,0 +1 @@
|
|||
{"Tp":"TrueType","Name":"DejaVuSans","Desc":{"Ascent":760,"Descent":-240,"CapHeight":760,"Flags":32,"FontBBox":{"Xmin":-1021,"Ymin":-463,"Xmax":1793,"Ymax":1232},"ItalicAngle":0,"StemV":70,"MissingWidth":600},"Up":-20,"Ut":44,"Cw":[600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,600,318,401,460,838,636,950,780,275,390,390,500,838,318,361,318,337,636,636,636,636,636,636,636,636,636,636,337,337,838,838,838,531,1000,684,686,698,770,632,575,775,752,295,295,656,557,863,748,787,603,787,695,635,611,732,684,989,685,611,685,390,337,390,838,500,500,613,635,550,635,615,352,635,634,278,278,579,278,974,634,612,635,635,411,521,392,634,592,818,592,592,525,636,337,636,838,600,636,600,318,352,518,1000,500,500,500,1342,635,400,1070,600,685,600,600,318,318,518,518,590,500,1000,500,1000,521,400,1023,600,525,611,318,401,636,636,636,636,337,500,500,1000,471,612,838,361,1000,500,500,838,401,401,500,636,636,318,500,401,471,612,969,969,969,531,684,684,684,684,684,684,974,698,632,632,632,632,295,295,295,295,775,748,787,787,787,787,787,838,787,732,732,732,732,611,605,630,613,613,613,613,613,613,982,550,615,615,615,615,278,278,278,278,612,634,612,612,612,612,612,838,612,634,634,634,634,592,635,592],"Enc":"cp1252","Diff":"","File":"DejaVuSans.z","Size1":0,"Size2":0,"OriginalSize":757076,"N":0,"DiffN":0}
|
||||
Binary file not shown.
|
|
@ -0,0 +1,7 @@
|
|||
module nbit.ch/bookkeeper
|
||||
|
||||
go 1.20
|
||||
|
||||
require (
|
||||
github.com/go-pdf/fpdf v0.9.0 // indirect
|
||||
)
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/go-pdf/fpdf v0.9.0 h1:PPvSaUuo1iMi9KkaAn90NuKi+P4gwMedWPHhj8YlJQw=
|
||||
github.com/go-pdf/fpdf v0.9.0/go.mod h1:oO8N111TkmKb9D7VvWGLvLJlaZUQVPM+6V42pp3iV4Y=
|
||||
github.com/jung-kurt/gofpdf v1.0.0/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes=
|
||||
github.com/jung-kurt/gofpdf v1.16.2 h1:jgbatWHfRlPYiK85qgevsZTHviWXKwB1TTiKdz5PtRc=
|
||||
github.com/jung-kurt/gofpdf v1.16.2/go.mod h1:1hl7y57EsiPAkLbOwzpzqgx1A30nQCk/YmFV8S2vmK0=
|
||||
github.com/phpdave11/gofpdi v1.0.7/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/ruudk/golang-pdf417 v0.0.0-20181029194003-1af4ab5afa58/go.mod h1:6lfFZQK844Gfx8o5WFuvpxWRwnSoipWe/p622j1v06w=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
golang.org/x/image v0.0.0-20190910094157-69e4b8554b2a/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
Loading…
Reference in New Issue