I recently started using Beancount to implement plain text accounting. This post describes my approach around it and my nvim setup.

A scene of double-entry bookkeeping reimagined in a vibrant cyberpunk setting
-- DALL·E

📒 Understanding why Beancount might be useful requires making sense of double-entry bookkeping.

Rather than writing a half-baked explanation about it, I encourage you to skim through Beancount’s getting started guide if this sounds unfamiliar to you.

Go on, I’ll wait here. ✌️

# My approach to Beancount

Beancount can shine only after we bear the pain of populating it with recent transactions and then figure out a way to periodically keep track of new ones. The easiest way I found to do this is by writing a few automated importers for bank statements, exports from operations, and so on. Each importer will parse a file (e.g., a CSV from your bank, or – sigh – a PDF) and output one or more Beancount transactions.

I have spread the effort of writing the importers and processing their output over a few months, starting with more important accounts first and then slowly adding other things. Depending on how you decide to structure transactions (e.g., dividing expenses by type, payee, etc.), adding new entries will require some manual post-processing to assign things to the right accounts. This tool can make things easier after some initial manual work: it trains an SVC classifier that tries to predict the right accounts for each transaction.

After a few iterations I ended up with a small Python library1 that can scan a directory for new files, extract their statements, and then archive them. By filing them in the appropriate directory2, Beancount will automatically match documents to the accounts they relate to (e.g., an invoice to its expenses account).

Overall, the process works well-enough, modulo that parsing some PDF statements to extract balances is more art than science and will inevitably require some evolution as the document structure will change.

I keep the full ledger within a single file3, divided in sections (e.g., one per institution, year, etc.) through nvim folds (more below). Being all text (plus the occasional PDF statement), I use git to keep track of changes (both for the library and the journal). Backups go through the same methods I use for the rest of the system (e.g., Time Machine on macOS).

# Setting up nvim

The community has built awesome tools to manage Beancount files. My weapon of choice for editing plain text files is nvim, and after experimenting with a few things I settled on:

  • A Tree-sitter parser for indent and highlight.
  • The built-in bean-format to consistently format the journal. I integrate it with nvim through the efm-langserver.
  • An nvim-cmp source to auto-complete accounts. The source enumerate accounts at startup and caches the result. As a result, it refused to auto-complete new accounts created after opening the file. I hacked together a lua function to manually refresh the cache when needed.
  • A custom compiler that runs bean-check through poetry, based on this plugin’s compiler. I quickly run it with m<cr>.
  • A few file-type options to tell nvim how to deal with comments, formatting, and correctly split keywords:

    setlocal formatprg=bean-format
    
    setlocal comments=b:;
    setlocal commentstring=;%s
    
    setlocal iskeyword+=:
    
  • vim’s default fold marker {{{ to split the file in sections. I write folds in comments, number them (so there’s no need to close them) and nest them, e.g. ; --- Bank A {{{2.
  • Last, I extended4 universal-ctags to support Beancount. This lets me easily jump to the opening directive of an account or to the definition of a commodity using C-] in nvim. The resulting beancount.ctags is here.

# Results

I am pretty happy with the overall setup.

After completing the costly first-time import, Beancount is useful to keep track of financial assets, expenses, and book-keep in general. At first my understanding about it was pretty limited: I used it to track monthly expense against their budgets, or forecast them for incoming periods.

But Beancount can do a lot more! For instance, it generates comprehensive financial reports and supports an SQL-like query language that will be pretty useful for the next tax-reporting period – replacing a bunch of hacky iPython notebooks I had built over the years.

Reading Financial Intelligence for Entrepreneurs then gave it the finishing touch. The authors do a great job at helping those, like me, unfamiliar with accounting making sense of balance sheets and income statements. With a little practice I found it easy to apply their teaching to my statements and found lot of value in the balance sheet and the income statement that Beancount (or fava, its web interface) automatically generates.

And you, how do you use Beancount? If you have any feedback or comment, please get in touch, I’d love to hear them.

Thank you for reading! ‘til next time! 👋

# Footnotes

  1. poetry makes is easy to manage dependencies while poetry2nix ensures a reproducible environment through nix

  2. In my case, it looks as follows: docs/<year>/<account>, where <account> builds a directory tree from Beancount’s account, turning Assets:Checking to Assets/Checking

  3. As long as things remain manageable this way. I suspect that at some point the file length will make it complicated for nvim to handle things efficiently. 

  4. Adding a Beancount ctags parser was easier than expected, but took me longer than it should have because the manual states:

    The regular expression, <line_pattern>, defines an extended regular expression (roughly that used by egrep(1)), …

    I tried for a while to use \d (supported by egrep but not working in ctags) then went for [0-9]