Last active
November 4, 2018 19:01
-
-
Save fergusq/48b87457968d6f6eb98a1dfba72a53f4 to your computer and use it in GitHub Desktop.
A script that creates a report with pie charts and transactions from an Aktia bank statement.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* Copyright (c) 2018 Iikka Hauhio | |
* Do whatever you want with this. | |
* | |
* This Röda script will process an Aktia bank statement and | |
* create a report with nice pie charts. It relies on the | |
* pgf-pie LaTeX package. After creating pictures using LaTeX | |
* it creates the report using Troff (Groff) and the ms | |
* preprocessor. | |
* | |
* Example usage: | |
* $ pdftotext tammikuun-tiliote.pdf | |
* $ röda abstr.röd "Tammikuu" tammikuun-tiliote.txt tammikuun-raportti.pdf | |
*/ | |
{ | |
j2r := require("json").jsonToRödaObj | |
logExec := require("fileutil").logExec | |
PARTIES := new map | |
current_day := "" | |
} | |
loadParties { | |
readLines "osapuolet.json" | concat | json | j2r(_) | pull partylist | |
for party in partylist do | |
PARTIES[party["name"]] = party["category"] | |
done | |
} | |
saveParties { | |
{ | |
push "[" | |
push([keys(PARTIES) | for party do | |
push `{"name":"$party","category":"${PARTIES[party]}"}` | |
done]&",") | |
push "]" | |
} | writeStrings "osapuolet.json" | |
} | |
record Transaction(date, party, cat, val) { | |
date : string = date | |
party : string = party | |
category : string = cat | |
val : integer = val | |
function asString { | |
return party.."("..val/100..")" | |
} | |
function askCategoryIfNeeded { | |
return if [ self.category != "" ] | |
unless [ PARTIES[party]? or self.category != "" ] do | |
STDOUT.push "Kategoria: ", party, "? " | |
STDIN.pull category | |
PARTIES[party] = category | |
done | |
self.category = PARTIES[party] | |
} | |
} | |
loadTransactionsJson file { | |
readLines file | concat | json | j2r(_) | pull transactionList | |
return [push(new Transaction(t["date"], t["party"], t["category"], parseInteger(t["val"]))) for t in transactionList] | |
} | |
saveTransactionsJson file, transactions { | |
{ | |
push "[\n" | |
push([transactions() | for t do | |
push `{"date":"${t.date}","party":"${t.party}","category":"${t.category}","val":"${t.val}"}` | |
done]&",\n") | |
push "]" | |
} | writeStrings file | |
} | |
createCharts expense_cat_sums, income_cat_sums { | |
createChart income_cat_sums, 1, "tulot.ps" | |
createChart expense_cat_sums, -1, "menot.ps" | |
} | |
createChart cat_sums, sign, out_file { | |
{ | |
total := 0 | |
keys cat_sums | total += sign*cat_sums[key]/100 for key | |
push `\documentclass{minimal}\usepackage[utf8]{inputenc}\usepackage[T1]{fontenc}\usepackage{pgf-pie}\begin{document}\begin{tikzpicture}\pie[text=legend,after number=e, sum=$total]{` | |
i := 0 | |
sortedByValue cat_sums | for val, key do | |
push ", " if [ i > 0 ] | |
push `${sign*val/100}/$key` | |
i ++ | |
done | |
push `}\end{tikzpicture}\end{document}` | |
} | writeStrings "tmp.tex" | |
logExec "pdflatex", "tmp.tex" | |
logExec "pdfcrop", "tmp.pdf" | |
logExec "pdftops", "tmp-crop.pdf" | |
logExec "mv", "tmp-crop.ps", out_file | |
logExec "zsh", "-c", "rm tmp*" | |
} | |
createReport time, transactions, expense_cat_sums, income_cat_sums, party_sums, total, out_file { | |
expense_total := transactions() | min([ _.val, 0 ]) | sum | |
income_total := transactions() | max([ _.val, 0 ]) | sum | |
{ | |
print `.nr PO 0.65in` | |
print `.nr LL 7in` | |
print `.pl 11.7in` | |
print `.nr PI 0.3in` | |
print `.nr HM 0.5in` | |
print `.2C` | |
print ".LG\n", time | |
print ".NH 1\nTiivistelmä" | |
print `.LP` | |
print `.TS` | |
print `expand;` | |
print `lb rb rb` | |
print `l r r.` | |
print "Kategoria\t%\tSumma\n_" | |
sortedByValue expense_cat_sums | for val, key if [ val != 0 ] do | |
print key, "\t", | |
formatMoney(val/expense_total*10000), "\t", | |
formatMoney(val) | |
done | |
print `_` | |
print "MENOT YHTEENSÄ\t\t", formatMoney(expense_total) | |
print `=` | |
sortedByValue income_cat_sums | for val, key if [ val != 0 ] do | |
print key, "\t", | |
formatMoney(val/income_total*10000), "\t", | |
formatMoney(val) | |
done | |
print `_` | |
print "TULOT YHTEENSÄ\t\t", formatMoney(income_total) | |
print `=` | |
print "YLIJÄÄMÄ (ALIJÄÄMÄ)\t\t", formatMoney(total) | |
print `.TE` | |
print ".NH 2\nTulot" | |
print `.PSPIC -L tulot.ps` | |
print ".NH 2\nMenot" | |
print `.PSPIC -L menot.ps` | |
print ".NH 1\nYksityiskohtainen listaus" | |
print ".NH 2\nOsapuolet" | |
print `.LP` | |
print `.TS` | |
print `expand;` | |
print `lb rb` | |
print `l r.` | |
print "Osapuoli\tSumma\n_" | |
sortedByValue party_sums | for val, key if [ val != 0 ] do | |
print key, "\t", formatMoney(val) | |
done | |
print `=` | |
print "YLIJÄÄMÄ (ALIJÄÄMÄ)\t", formatMoney(total) | |
print `.TE` | |
print ".NH 2\nTilitapahtumat" | |
print `.LP` | |
print `.TS` | |
print `expand;` | |
print `lb lb rb` | |
print `l l r.` | |
print "Päiväys\tOsapuoli\tSumma\n_" | |
for t in transactions do | |
print t.date, "\t", t.party[:min([#t.party, 18])], "\t", formatMoney(t.val) | |
done | |
print `=` | |
print "YLIJÄÄMÄ\t(ALIJÄÄMÄ)\t", formatMoney(total) | |
print `.TE` | |
} | exec "groff", "-p", "-t", "-m", "ms", "-Kutf8", "-Tps", "-dpaper=a4l", "-P-pa4" | writeStrings "tmp.ps" | |
logExec "ps2pdf", "tmp.ps", out_file | |
logExec "zsh", "-c", "rm tmp*" | |
} | |
main time, in_file, out_file, flags... { | |
if [ "--jsonista" in flags ] do | |
loadTransactionsJson in_file | |
else | |
loadTransactionsTxt in_file | |
done | pull transactions | |
if [ "--jsoniksi" in flags ] do | |
saveTransactionsJson out_file, transactions | |
else | |
processTransactions transactions, time, out_file | |
done | |
} | |
loadTransactionsTxt in_file { | |
loadParties | |
transactions := [] | |
readLines in_file | fixSpaces | { | |
skipHeader | |
while true do | |
break unless skipToRecord | |
readLine line | |
if [ fieldNum(line) = 4 ] do | |
parseFields line, code1, code2, n, val | |
readLine code3 | |
readLine party | |
readLine location | |
readLine code4 | |
readLine code5 | |
transactions += new Transaction(current_day, party, "", parseMoney(val)) | |
else | |
parseFields line, code1, code2, party, n, val | |
readLine code3 | |
readLine message | |
transactions += new Transaction(current_day, party, "", parseMoney(val)) | |
done | |
done | |
} | |
t.askCategoryIfNeeded for t in transactions | |
saveParties | |
return transactions | |
} | |
processTransactions transactions, time, out_file { | |
s := 0 | |
expense_cat_sums := new map | |
income_cat_sums := new map | |
party_sums := new map | |
for t in transactions do | |
expense_cat_sums[t.category] = 0 if [ t.val < 0 ] | |
income_cat_sums[t.category] = 0 if [ t.val > 0 ] | |
party_sums[t.party] = 0 | |
done | |
for t in transactions do | |
expense_cat_sums[t.category] += t.val if [ t.val < 0 ] | |
income_cat_sums[t.category] += t.val if [ t.val > 0 ] | |
party_sums[t.party] += t.val | |
s += t.val | |
done | |
expense_total := transactions() | min([ _.val, 0 ]) | sum | |
income_total := transactions() | max([ _.val, 0 ]) | sum | |
expense_divider := keys(expense_cat_sums) | abs(expense_cat_sums[_]) | max() / 120 | |
income_divider := keys(income_cat_sums) | abs(income_cat_sums[_]) | max() / 120 | |
{ | |
push(["Kategoria","%","€",""]) | |
push(["","","",""]) | |
sortedByValue expense_cat_sums | for csum, cat if [ csum != 0 ] do | |
push([cat, csum/expense_total*100//1, csum/100, "-"*(-csum//expense_divider)]) | |
done | |
sortedByValue income_cat_sums | for csum, cat if [ csum != 0 ] do | |
push([cat, csum/income_total*100//1, csum/100, "+"*(csum//income_divider)]) | |
done | |
push(["","","",""]) | |
push(["TOTAL","",s/100,""]) | |
} | tasaa | |
createCharts expense_cat_sums, income_cat_sums | |
createReport time, transactions, expense_cat_sums, income_cat_sums, party_sums, s, out_file | |
} | |
fixSpaces { | |
replace `([AK]) (\d{4})`, "$1 $2 " | |
} | |
skipHeader { | |
for line do | |
break if [ line =~ "Kirj.pvm.*" ] | |
done | |
} | |
skipToRecord { | |
for line do | |
if [ line =~ "KIRJAUSPÄIVÄ .*" ] do | |
current_day = line[#"KIRJAUSPÄIVÄ ":] | |
done | |
if [ line =~ "FT.*" ] do | |
unpull line | |
true | |
return | |
done | |
done | |
false | |
} | |
fieldNum line { | |
return #[split(line, sep=" +")] | |
} | |
parseFields line, &vars... { | |
vars <> split line, sep=" +" | for var, field do | |
var := field | |
done | |
} | |
readLine &var { | |
pull var | |
while [ var =~ "\\s*SIIRTO\\s*" ] do | |
pull var | |
skipHeader | |
done | |
while [ var =~ "" ] do | |
pull var | |
done | |
var ~= "^\\s*|\\s*$", "" | |
} | |
parseMoney val { | |
val ~= "\\.", "", "(\\d+) (.)", "$2$1" | |
parseInteger val | |
} | |
formatMoney val { | |
val /= 100 | |
val = match("(-?)(\\d+)\\.(\\d+)", `$val`) | |
ans := "" | |
ans .= "– " if [ val[1] = "-" ] | |
ans .= val[2] | |
ans .= "," | |
ans .= val[3] if [ #val[3] = 2 ] | |
ans .= val[3][:2] if [ #val[3] > 2 ] | |
ans .= val[3].."0" if [ #val[3] < 2 ] | |
return ans | |
} | |
tasaa { | |
kaikki := [identity()] | |
pituudet := [interleave(*kaikki) | while open do | |
head(#kaikki) | [ #`$_` ] | max() | |
done] | |
kaikki | for rivi do | |
pituudet <> rivi <> seq(1, #rivi) | for pituus, arvo, i do | |
push(arvo, " "*(pituus-#`$arvo`)) | |
push(" | ") unless [ i = #rivi ] | |
done | |
print("") | |
done | |
} | |
sortedByValue kv { | |
keys kv | [[kv[_1], _1]] | sort | _ | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment