For those who don’t know, Beancount is one of the few tools that exist to do the so-called “plaintext accounting”. Plaintext accounting is a way to perform usual accounting tasks with tools that work on plaintext files, and in some occasions (Beancount being one example) using command line tools.
Some advantages include extreme simplicity in keeping/moving/exporting/transfering/converting the data, since it’s just text in a file. It’s also very easy (and fast) to manage years worth of data and it’s much easier to build additional tooling around such systems, as they do expose some level of API (for Beancount there is a SQL-like interface) and when they don’t, it’s just a matter of manipulating strings.
Overall, I like to have my entire accounting history one command away in my terminal, I like to be able to easily merge ‘stuff’ from different sources (for example banks) and I am used to edit files efficiently in Vim, so plaintext accounting sounded the right choice for me.
How Beancount works
The official Beancount documentation makes for sure a better job than I can do to introduce the tool, but I will try to give just a general idea.
All it is needed to have beancount working is a ‘ledger’ file, this is the file where pretty much everything is: the account definition, the transactions, price updates etc.
As Beancount is a double-entry bookeeping system, we have accounts for everything, for example your salary might ‘come’ from an account called
income:mycompany:salary while the money you spend in plastic dinosaurs will go to
There are 5 types of accounts:
assets. For details on what each account type represents it’s much better to consult the related documentation.
Now, in our ledger we will have to:
- Declare some options (such as the ledger name, the default currency, etc.)
- Open the accounts
- Create a baseline (usually using
equities) for the current state of the assets
- Add transactions
A new ledger might look like the following:
* Options ; Beancount options option "title" "Test Ledger" option "operating_currency" "EUR" ; Accounts 2022-01-01 open Assets:Bank:Personal:Checking EUR 2022-01-01 open Assets:Bank:Shared:Checking EUR 2022-01-01 open Assets:Bank:Personal:Saving EUR 2022-01-01 open Assets:Bank:Personal:Investing EUR 2022-01-01 open Assets:Cash EUR 2022-01-01 open Liabilities:Bank:HouseLoan EUR 2022-01-01 open Income:Salary EUR 2022-01-01 open Income:Bank:Interests EUR 2022-01-01 open Income:Gifts EUR 2022-01-01 open Expenses:Trip EUR 2022-01-01 open Expenses:Food:Groceries EUR 2022-01-01 open Expenses:House EUR 2022-01-01 open Expenses:House:Utilities EUR 2022-01-01 open Expenses:Bank:DebitCardFee EUR 2022-01-01 open Equity:Opening-Balances 2022-01-01 * "Opening Balances" Assets:Bank:Personal:Checking 0 EUR Assets:Bank:Personal:Saving 1000 EUR Assets:Bank:Shared:Checking 300 EUR Liabilities:Bank:HouseLoan -100000 EUR Equity:Opening-Balances
The content should be self-explainatory, but first some options are declared (the name and currency), then there are a few statements that ‘open’ the accounts we want to use, and finally an initial transaction that adds money to our accounts (the last leg of the transaction is to say ‘everytihing comes from equity’).
From this point on, every transaction will need to balance, money should come from one account (income accounts in general) and should go to other accounts.
The Struggle of Keeping it in sync
Let’s now talk about the problems. Most of the banks offer minimal, if any, tools to export transaction data from their websites. Usually the export is some heavily formatted CSV or some PDF. The problem with keeping a separate ledger then is that every transaction needs to be ‘backported’ to it, which sorts of encourage you to think ‘the hell with it, I will just use the bank’s website’. The problem comes when you start having multiple accounts in possibly multiple banks, some financial institutions (for example some stock broker) etc. If this is the case, then it’s much harder to keep the overview of where money are going (usually we do know where they come from).
I once read someone writing that ‘inserting transactions should be painful or boring’, the idea being that having to enter every transaction individually in an accounting system helps building awareness of how the money are being spent. I do agree with this to a certain degree, but I want to stress the ‘awareness’ part reducing the ‘I have to type a lot’ part as well.
Living in Estonia, cash is non-existent for me, which means that every single purchase I do, online or in a store, big or small, is done via a debit/credit card. This translates very concretely in a lot of transactions, which means a lot of typing, which means a lot of temptation to keep the ledger out of sync and use the bank’s website.
How do I manage to have an up-to-date ledger (for 2.5 years and counting!) then? Well, part of it is just habit and will, but making the maintance a bit easier helped quite a lot.
Before dealing with everything else, I built some automation. My main account(s) is in Swedbank, one of the major banks in Estonia, and to address the problems described earlier, I wanted the following:
- Minimal configuration
- Ability to digest the CSV as it is downloaded from Swedbank site
- Ability to automatically associate certain transactions with Beancount accounts
- Necessity to still enter the transactions somewhat individually to be aware of the expenses
For this, I built a simple (very simple) tool called swed2beancount, which does…well, exactly what I wanted to do.
Using this tool, it takes approximately 5 minutes to import data from Swedbank accounts into my ledger, and on average I have to specify the account for only 20-30% of the transactions.
In the final section I will describe what my update routine looks like.
This is trivial, but updating the ledger at intervals very distant from each other will have two main effects for me:
- I will rely solely on automation, too much to check individually
- More probability of making mistakes and having to debug
For me, 2 weeks is the perfect period in between updates. It is short enough that the amount of transactions is usually manageable, but long enough that I don’t need to ‘waste’ a session for just few transactions.
Use the Assert Function
Initially I was not even aware of this functionality, and when I became aware of it, I didn’t understand the use of it. That was before a mistake of a sign sent me to a debugging journey into more than year of transactions.
An assert transaction is a very simple statement such as:
2022-01-05 balance Assets:Bank:Checking 112.01 EUR
All it does is stating that at a given date an account has a specific balance. After every update, I always end my ledger with an assertion for the balance of all the bank accounts. This way, whatever happens next, I know that I have to check for errors in at most 2 weeks (or as much time passed from the last update) worth of data.
The only problem is that sometimes there are transactions that were pending the day that the update was done, or other similar cases. Assertion are always done ‘at the beginning of the day’, so if there are 2 transactions on 5th of January, and 3 the next day, an assertion on 6th of January will count only the first 2 transactions, while an assertion on 5th of January will not count any of those. This is a feature, not a problem, but needs to be taken into account when using assertions.
Granularity of Accounts
When starting with plaintext accounting, it’s easy to go deep into the rabbit hole:
2022-01-01 open Expenses:Trip:Italy EUR
2022-01-01 open Expenses:Trip:Italy EUR 2022-01-01 open Expenses:Trip:Italy:Driving:Car EUR 2022-01-01 open Expenses:Trip:Italy:Driving:Fuel EUR 2022-01-01 open Expenses:Trip:Italy:Driving:Insurance EUR 2022-01-01 open Expenses:Trip:Italy:Activities:Museums EUR 2022-01-01 open Expenses:Trip:Italy:Activities:Fun EUR 2022-01-01 open Expenses:Trip:Italy:Eating:Restaurants EUR 2022-01-01 open Expenses:Trip:Italy:Eating:IceCreams EUR 2022-01-01 open Expenses:Trip:Italy:... EUR
Basically since the system allows for fine-grain accounts, we want order in our lives and we start creating way more details than are needed. I am guilty of this and I have learned my lesson.
With this, I am not saying that it is wrong to add details, but that you should add details when you need them. In my case, for example, I had absolutely no use to know that I spent X for restaurants and Y for activities, as it’s not something that can help me plan for the future. Similarly, it’s enough to use transaction descriptions to understand the cost of the car (Rental + fuel + etc.) to decide if next time I would use the train or choose a different car/company/etc.
The important bit is understanding the tradeoff: more details lead to potentially deeper analysis, but have a higher maintaince costs. Usually the expenses need to be categorized manually, sometimes they will need to be divided into multiple legs etc. If the additional maintance overhead is not giving you any benefit, then it’s clearly not worth.
What an Update Session Looks Like
To conclude this post I want to show what one of my update sessions looks like. As I have mentioned, I will update my ledger every 2 weeks usually.
My folder structure looks like this:
├── importer │ ├── mappings.yaml │ ├── account1 │ │ └── config.yaml │ └── account2 │ └── config.yaml └── ledger.beancount
- First, I go to the Swedbank site, and dump the simple CSV for every account I have (usually I take the current year, whatever is faster).
- Then I move the downloaded CSVs in the respective folders in the
- I then edit the
config.yamlfile with the dates I am interested in. In general I import from a couple of days before the last import until the present day (leaving it blank works). The configuration file looks like the following:
ledger: "../../ledger.beancount" mappings_file: "../mappings.yaml" output_file: "import.beancount" from_date: "2022-04-01"
- Finally, in each account directory I run a command such as
swed2beancount -a "Assets:Bank:Checking"This command basically is saying to process a given CSV and assume that one leg of the transaction is always the one specified.
The previous command generates an output file (with the name specified), with only the transactions related to the importing period. If any of the transactions has
UNCATEGORIZED as account, it means that there was no mapping defined for the amount/date/description/payee. In general, if I know that a similar transaction will happen again in the future, I add a matching rule in the
mappings.yaml file, otherwise, I just edit the account manually.
Once the transactions are all complete (all legs are existing accounts), I append this file to the main ledger, I add a comment (such as “Updated on DD/MM/YYYY”) and an assertion.
Visualizing the Result
After any update (or whenever I want) I also run fava on my main ledger. This is a web interface for beancount that makes exploring the data quite convenient and visually pleasing. Fava can automatically update the data when the ledger is modified, so it’s possible to have it running continuously and simply syincing the ledger (through a repository, with a script, etc.), if you want to have your personal dashboard always active.
Beancount, and in general plaintext accounting, allows full flexibility in how you want to manage your finances. Building tooling and automation around open formats is much easier compared to usual banks, and it’s possible to manipulate the data in countless ways. Personally, using Beancount has increased my awareness on my expenses and has greatly helped in financial planning and cost saving. Plus, you get the pleasure of Vim-editing your ledger, how cool is that (and of course, a plugin exist)!