From 4feece9549cd2fe94956f5d527be8dd695fd6d13 Mon Sep 17 00:00:00 2001 From: Alexander Avery Date: Thu, 24 Feb 2022 21:39:26 -0500 Subject: [PATCH] generate simple transactions --- README.md | 10 ++++- go.mod | 10 +++++ go.sum | 11 ++++++ lox.cfg.example | 7 ++++ main.go | 99 +++++++++++++++++++++++++++++++++++++++++++++++++ transactions.go | 37 ++++++++++++++++++ 6 files changed, 172 insertions(+), 2 deletions(-) create mode 100644 go.mod create mode 100644 go.sum create mode 100644 lox.cfg.example create mode 100644 main.go create mode 100644 transactions.go diff --git a/README.md b/README.md index 9a8302c..7018436 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,9 @@ -# lox +# lox - ledger from ofx -utility for generating ledger transactions from ofx \ No newline at end of file +Simple command line utility to generate ledger transactions from OFX xml data. The generated ledger transactions are printed to stdout. The OFX data can come from other programs or the disk, lox does not support connecting to your bank or other financial institutions; it simply reads from the disk, or stdin. + +You can specify a configuration file to map vendor names and account numbers to specific account names in your ledger files. A basic example configuration is provided in this repository as `lox.cfg.example` + +Programs and libraries you can use with lox: +* [ofxtools](https://pypi.org/project/ofxtools/) - Python library and cli for downloading financial information +* [ofxgo](https://github.com/aclindsa/ofxgo) - Go library for downloading financial information (lox uses ofxgo to parse ofx data) diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..9cabd8a --- /dev/null +++ b/go.mod @@ -0,0 +1,10 @@ +module beetbox.io/lox + +go 1.17 + +require ( + github.com/aclindsa/ofxgo v0.1.3 // indirect + github.com/aclindsa/xml v0.0.0-20201125035057-bbd5c9ec99ac // indirect + github.com/ochinchina/go-ini v1.0.1 // indirect + golang.org/x/text v0.3.7 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..57852dc --- /dev/null +++ b/go.sum @@ -0,0 +1,11 @@ +github.com/aclindsa/ofxgo v0.1.3 h1:20Ckjpg5gG4rdGh2juGfa5I1gnWULMXGWxpseVLCVaM= +github.com/aclindsa/ofxgo v0.1.3/go.mod h1:q2mYxGiJr5X3rlyoQjQq+qqHAQ8cTLntPOtY0Dq0pzE= +github.com/aclindsa/xml v0.0.0-20201125035057-bbd5c9ec99ac h1:xCNSfPWpcx3Sdz/+aB/Re4L8oA6Y4kRRRuTh1CHCDEw= +github.com/aclindsa/xml v0.0.0-20201125035057-bbd5c9ec99ac/go.mod h1:GjqOUT8xlg5+T19lFv6yAGNrtMKkZ839Gt4e16mBXlY= +github.com/ochinchina/go-ini v1.0.1 h1:qrKGrgxJjY+4H8aV7B2HPohShzHGrymW+/X1Gx933zU= +github.com/ochinchina/go-ini v1.0.1/go.mod h1:Tqs5+JmccLSNMX1KXbbyG/B3ro4J9uXVYC5U5VOeRE8= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/lox.cfg.example b/lox.cfg.example new file mode 100644 index 0000000..42167e3 --- /dev/null +++ b/lox.cfg.example @@ -0,0 +1,7 @@ +[Assets] +XXXXXXXXX0 = Assets:Banks:Checking +XXXXXXXXX1 = Assets:Banks:Savings + +[Vendors] +TRADER JOE = Expenses:Food:Groceries +DUNKIN = Expenses:Food:Restaurants diff --git a/main.go b/main.go new file mode 100644 index 0000000..a1ac3ae --- /dev/null +++ b/main.go @@ -0,0 +1,99 @@ +package main + +import ( + "flag" + "os" + "bufio" + "log" + "strings" + "io" + "text/template" + "sort" + "github.com/aclindsa/ofxgo" + "github.com/ochinchina/go-ini" +) + +func matchVendor(keys []ini.Key, givenName string) string { + for _, k := range keys { + v := k.ValueWithDefault("") + if strings.Contains(givenName, k.Name()) { + return v + } + } + return "Expenses" +} + +func generateLedgerTransactions(transactions []transaction, w io.Writer) error { + t := template.New("ledger") + _, err := t.Parse(transactionTemplate) + if err != nil { + return err + } + + t.Execute(w, transactions) + return nil +} + +func main() { + var config = flag.String("config", "", "configuration file containing all your vendor and account mappings") + flag.Parse() + filename := flag.Arg(0) + + r, err := os.Open(filename) + if err != nil { + log.Fatal(err) + } + defer r.Close() + + c, err := os.Open(*config) + if err != nil { + log.Fatal(err) + } + defer c.Close() + + cfg := ini.NewIni() + cfg.LoadReader(c) + + assets, err := cfg.GetSection("Assets") + if err != nil { + log.Fatal(err) + } + + vendors, err := cfg.GetSection("Vendors") + if err != nil { + log.Fatal(err) + } + + resp, err := ofxgo.ParseResponse(r) + if err != nil { + log.Fatal(err) + } + + transactions := make([]transaction, 0) + for _, m := range resp.Bank { + if stmt, ok := m.(*ofxgo.StatementResponse); ok { + for _, t := range stmt.BankTranList.Transactions { + acctId := stmt.BankAcctFrom.AcctID.String() + tx := transaction{ + Date: t.DtPosted.Time, + TrnAmt: t.TrnAmt, + Asset: assets.GetValueWithDefault(acctId, acctId), + Vendor: matchVendor(vendors.Keys(), t.Name.String()), + VendorName: t.Name.String(), + } + transactions = append(transactions, tx) + } + } + } + + w := bufio.NewWriter(os.Stdout) + sort.Sort(byDate(transactions)) + err = generateLedgerTransactions(transactions, w) + if err != nil { + log.Fatal(err) + } + err = w.Flush() + if err != nil { + log.Fatal(err) + } +} diff --git a/transactions.go b/transactions.go new file mode 100644 index 0000000..8bc7a2b --- /dev/null +++ b/transactions.go @@ -0,0 +1,37 @@ +package main + +import ( + "time" + "github.com/aclindsa/ofxgo" +) + +var transactionTemplate = +` +{{ range . }} +{{ .Date.Format "2006-01-02" }} {{ .VendorName }} + {{ .Asset }} ${{ .TrnAmt }} + {{ .Vendor }} +{{ end }} +` + +type transaction struct { + Date time.Time + TrnAmt ofxgo.Amount + Asset string + Vendor string + VendorName string +} + +type byDate []transaction + +func (b byDate) Len() int { + return len(b) +} + +func (b byDate) Less(i, j int) bool { + return b[i].Date.Before(b[j].Date) +} + +func (b byDate) Swap(i, j int) { + b[i], b[j] = b[j], b[i] +}