Since 2017, I have been using open-source double-entry bookkeeping tools to manage personal finances – initially GnuCash, and from 2019, the plaintext accounting tool ledger. Ledger, and plaintext accounting generally, sported a number of appealing features – the software is lightweight and performant, ledger workflows incorporate efficient automation such as importing transactions from bank statements, and it is compelling that data is stored in a non-proprietary human-readable flat plaintext file.

As time progressed, however, ledger alone grew increasingly inadequate for my needs. There are 2 root causes which I discuss below.

Accounting tools should embrace accounting principles

Firstly, ledger in its minimalism functions as a general double-entry ledger keeping tool. It simply tracks the balances of ledger accounts, and is agnostic to specific accounting principles, such as whether those accounts are assets, liabilities, etc.1 The first manifestation of this is that ledger does not have built-in functionality to generate standard accounting reports, such as a balance sheet or income statement. Users must write their own scripts to filter for the relevant balance sheet or income statement accounts.2 Even with such a filter, the standard ledger workflow presents assets with positive balances but liabilities with negative balances, since ledger represents debits and credits with signed numbers, which can lead to confusion.3

Another manifestation of this problem is that ledger does not handle mixing commodities well by default, which I discussed in a previous post. There is no good way to instruct ledger to value assets at market value and income/expenses at historical cost, because ledger does not differentiate between asset accounts and income/expense accounts.

To address these limitations, in 2020 I created ledger-pyreport, a wrapper around ledger which used its output to create standard accounting reports. Initially, this simply called ledger bal and performed some sign conversions and formatting; however, as more functionality was added (such as tracking different commodities), this morphed into using ledger simply as a parsing utility for ledger files, while ledger-pyreport increasingly became a reimplementation of core accounting functionality.

Compare with commercial heavyweight accounting packages, such as Xero, which begin with establishing a chart of accounts and assigning account types (asset, liability, etc.) to ledger accounts. With this information, a wide range of accounting reports can be automatically generated from the ledger, applying special treatment to different accounts as required.

Financial information is relational, not flat

Secondly, ledger's data structure of a flat text file with sequential transactions increasingly became a poor fit for financial information. In the standard accounting paradigm, we begin with various types of source data (bank statements, cheque stubs, invoices, sales journals, etc.), from which ledger transactions are derived. Contrast this with ledger's approach, which treats the ledger itself as central.

In practice, the most difficulty emerged when importing bank statements containing transfers between two accounts. When importing bank statements for a single account, ledger avoids duplicating transactions by assigning each transaction a UUID based on the statement data. However, this model breaks when the same transaction is included on two bank statements – the source account and the destination account in a transfer between accounts.4 In other words, bank transactions have a many-to-one relationship with ledger transactions, not a one-to-one relationship. Financial information is relational, not flat.

Compare again with the commercial product Xero, which recognises this relationship. In Xero, importing a bank statement does not touch the ledger. Rather, once the bank statement is imported, statement lines can be reconciled to ledger accounts, which then creates a transaction on the ledger. A ledger transaction can correspond with multiple statement lines, in the case of a transfer, or vice versa in applicable cases.

Other desirable features in an accounting tool

When evaluating my options for moving away from ledger, a number of other features were desirable:

  • The software should be performant. My utility tool, ledger-pyreport, was written in Python, and with it implementing increasingly more functionality, performance was poor when the number of transactions was large. Another plaintext accounting tool written in Python, beancount, is also encountering this problem, leading to a rewrite in C++ that is currently in progress.

  • The software should be extensible. For example, a useful feature would be to use the transaction data to automatically generate a summary for income tax purposes, and to track income tax liability on the balance sheet and income statement over the course of the year. However, this would be very specific to the tax jurisdiction, and it would be useful if other users could disable this module or replace it with their own.

  • The software should make exploring financial information easy. For example, if an income statement report shows an unexpectedly large expense account in one year, it should take little effort to break down the expenses by month, or to list the individual transactions. Easily exposing a large number of degrees of freedom in user interaction in this way necessitates a GUI rather than CLI.

DrCr, a solution 3 years in the making

In 2022, I began developing DrCr as a solution to these issues. DrCr, an open-source double-entry bookkeeping tool, would be built from the ground up with specific accounting applications in mind, with a relational model of source data and ledger transactions. It would adopt a GUI-first approach to presenting financial information, including standard financial reports. DrCr remains alpha-quality software, but is now ready for a public release, and I discuss it in more detail here.

Footnotes

  1. A memorable application of this is the use of ledger as a flight logbook

  2. Some plaintext accounting tools do recognise the different account types, most notably beancount; however, I evaluated beancount as unsuitable for my needs for different reasons, as mentioned later in this post. 

  3. One obvious manifestation of confusion is the frequently repeated misconception that signed-number accounting is in some way fundamentally different to debit-credit accounting. 

  4. Workarounds for this in ledger are possible. For example, both bank statements could be imported, and the transaction reconciled to a third, temporary account. The net effect is of a transfer effected between the two bank accounts, and the temporary account is left with a zero balance. However, this leads to problems down the track since no relationship is enforced between the two transactions. For example, if one transaction is modified but not the other, the temporary account will be left with a nonzero balance. While it can be detected with a balance assertion that this has occurred, it is nontrivial using ledger to detect when and where it has occurred.