Alexander Avery
3 years ago
6 changed files with 172 additions and 2 deletions
@ -1,3 +1,9 @@ |
|||||
# lox |
# lox - ledger from ofx |
||||
|
|
||||
utility for generating ledger transactions from ofx |
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) |
||||
|
@ -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 |
||||
|
) |
@ -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= |
@ -0,0 +1,7 @@ |
|||||
|
[Assets] |
||||
|
XXXXXXXXX0 = Assets:Banks:Checking |
||||
|
XXXXXXXXX1 = Assets:Banks:Savings |
||||
|
|
||||
|
[Vendors] |
||||
|
TRADER JOE = Expenses:Food:Groceries |
||||
|
DUNKIN = Expenses:Food:Restaurants |
@ -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) |
||||
|
} |
||||
|
} |
@ -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] |
||||
|
} |
Loading…
Reference in new issue