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 !