The most important part to remember now is that plaintext accounting (especially for personal finance) is not for everyone. As Duarte O.Carmo said, this article is meant to be a showcase and not really a tutorial or how to. I really want to show that “not too much” work is necessary to get going.
Introduction
I started to track my personal finances in 2017, when I was a PhD student also interested in competitive Magic The Gathering (which can be a pretty expensive hobby especially when playing a still expenrsive rotating format). Back then I was looking for a solution for budgeting properly.
I started with Skrooge, which was really great with its importers, but the binary data format scared me a little bit, especially as I almost lost data due to corruption while upgrading twice in 3 years. At that point I looked into plain text accounting, and I settled on Beancount mostly because the file format looked simpler, and less features (that I don’t use anyway) mean less complexity.
At that point the main issue was to get to a workflow where updating my accounting data wasn’t so long that I lost the habit. Actually the criterium was, “I know I’ll miss some habits, so catching up should not be too painful”.
This article is just a snapshot of my current flow
Fetch data
Fetching the data from my various bank accounts is made really easy thanks to the recently renamed Woob project. Its bank backends can be used to fetch the data from multiple sources and make CSV data out of it that I can process later.
That means that fetching data is really only 2 small steps.
Having configuration for the backends
# Generated and handled in ~/.config/woob/backends
[bank_module]
_module = bank_module
login = `kwallet-query -f "Woob Bank" -r bank_user kdewallet`
password = `kwallet-query -f "Woob Bank" -r bank_pass kdewallet`
And then a single command that will non-interactively fetch the data and put it in csv form.
woob bank history ID@BACKEND -f csv -n 140 \
--no-header -s id,date,rdate,raw,label,amount \
> card.csv
history
fetches the operations that already happened and coming
is useful
for the current month of expenses on the card. And all that csv data is then
available for review or anything else. Next step is to make this data go into
formatted transaction for the beancount file.
Process transactions
Once all reports are fetched from the bank, I use a small python script to
transform it into Beancount transactions. Beancount offers importers
theoretically, and I can probably, eventually, integrate parts of this python
script into a real importer but for now it does the job : ./csv2beancount.py FILE TYPE
will print to stdout all the records found in the file in correct
transaction format. The `TYPE` is mostly use to separate types during
interpretation, this is a very “quick and dirty” script that’s included
(sanitized) below to show it’s really just a pipe for structured text data.
#!/usr/bin/env python3
import sys
import csv
import dataclasses
PAYEE_CATEGORY = {
"75 MONOPRIX": "Expenses:Food:Supermarché",
"75 RATP": "Expenses:Transports:Transports-en-commun",
# And others...
}
PAYEE_NAMES = {
"75 MONOPRIX": "MONOPRIX",
"75 RATP": "RATP",
# And others...
}
TYPE_TO_ACCOUNT = {
"card": "Liabilities:Credit-Card",
"checking": "Assets:Bank:Checking-Account",
}
@dataclasses.dataclass
class BeancountLine:
date: str
payee: str
amount: float
account: str
expense_type: str
def print(self):
print(f'{self.date} * "{self.payee}" ""')
print(f" {self.account} {self.amount} EUR")
print(f" {self.expense_type} {self.amount * -1} EUR")
def csv2beancount(csv_file, type):
csv_reader = csv.DictReader(csv_file, delimiter=";")
line_count = 0
records = []
for row in csv_reader:
if line_count == 0:
print(f'Column names are {", ".join(row)}')
line_count += 1
else:
if type == "card":
line = BeancountLine(
date=row["rdate"],
account=TYPE_TO_ACCOUNT[type],
payee=PAYEE_NAMES.get(
row["label"], row["label"]
),
amount=float(row["amount"]),
expense_type=PAYEE_CATEGORY.get(
row["label"], "Expenses"
),
)
records.append(line)
line_count += 1
continue
if type == "checking":
line = BeancountLine(
date=row["rdate"],
account=TYPE_TO_ACCOUNT[type],
payee=PAYEE_NAMES.get(
row["label"], row["label"]
),
amount=float(row["amount"]),
expense_type=PAYEE_CATEGORY.get(
row["label"], "Expenses"
),
)
records.append(line)
line_count += 1
continue
print(f"Processed {line_count} lines.")
records.sort(key=lambda x: x.date)
for line in records:
line.print()
print("")
if __name__ == "__main__":
filename = sys.argv[1]
type = sys.argv[2]
with open(filename, "r") as file:
csv2beancount(file, type)
After this is done, I get a list for formatted transactions, and I just have to adjust the category/payee for the new entries each time. If I know something is recurring (like rent, or maybe a nice bakery that I go often to), I add it to the maps so that it get automatically tagged the next time.
Once this is done I paste the results in Emacs (but any text editor will do), and I adjust transactions, as well as add “balance assertions” that make sure that I didn’t miss anything while importing (I assert that the sum of operations match the current amount on my accounts, and that validates everything for me).
Last time I ran the scripts on 3 months worth of data, and had only around a dozen of transactions to manually adjust, and the balance assertion ran without issue.
Generate/Analyze reports
Once all this data is properly input, this is considered immutable, and all the rest is tooling that both asserted that it is correct (at the last step, checking that the operations sum to the correct state), and that analyze data to produce reports and visualize expenses.
Beancount project hosts Fava, which is a webserver using my ledger.beancount
file to make a nice web UI to explore my data. You can see a Fava example
instance to get a preview. I mostly use it to estimate and adjust how much I can
save, and try to get a longer-term view of my infrequent expenses (like sport
shoes, books, laptops…).
Hopefully later I will also add actual budgeting to control my expenses better, but currently I update my beancount file once per 4/6 weeks, so it’s not really going to help with monthly budgets. Until I upgrade my processing flow to be so good that I run it weekly at least, I won’t try too hard to go into monthly budgeting. Burning out of personal projects is not fun, but I tend to do that a lot, so let’s go one step at a time.
Have fun !