Plaintext accounting as a scripting language

Plaintext accounting as DSL for finance

Plaintext accounting is a whole philosophy which tries to get the best of both simple paper ledgers (for maximum flexibility and portability) and the capabilities of modern computers (for maximum error-avoidance). Some interesting software in this category is ledger, hledger (similar to ledger, but implemented in haskell) and beancount (similar to the ones above, implemented in python).

I think all the above programs have some important flaws (ledger is actually not very good, hledger seems to be a bit inplexible with imports and beancount makes it very hard to import transaction data whithout looking at the source and writing custom python scripts). I actually like the flexibility of beancount, but I’ve decided to go a little further and implement a ledger as a DSL on top of Elixir.

The idea is very simple: a ledger is a sequence of elixir function calls (which use the process dictionary for shared mutable state, which is safe because everything runs in order) which build a ledger datastructure in the background. This allows us to have an unambiguous grammar with some goodies such as Elixir sigils.

For example, this very simple ledger which features the opening of accounts, a 3-way transaction and a balance check (the numbers are random, nothing actually checks). The functions in this file only generate the ledger, they don’t actually check anything, that is left for an as of yet unimplemented module.

open ~D[1990-04-25], account("Expenses::Groceries"), currency: "EUR", note: "track all groceries"
open ~D[1990-04-24], account("Expenses::Eating out"), currency: "EUR"
open ~D[1990-04-24], account("Expenses::Going out (other)"), currency: "EUR"

open ~D[2010-04-24], account("Assets::Bank::Checking"), currency: "EUR"
open ~D[1990-04-24], account("Assets::Cash"), currency: "EUR"

open ~D[2010-04-24], account("Income::Salary"), currency: "EUR"
open ~D[2010-04-24], account("Income::Studio rent"), currency: "EUR"

txn ~D[2025-07-09], notes: "account", accounts: %{
  account("Income::Studio rent") => ~A[-800 EUR],
  account("Liabilities::Tax::Tax owed on rent") => ~A[224 EUR],
  account("Assets::Bank::Checking") => ~A[576 EUR]
}

balance ~D[2025-07-09], account("Assets::Bank::CGD::Checking"), ~A[200 EUR]

Has anyone here ever tried something like this?

I think this doens’t attempt to sandbox the elixir execution, so a maliciously constructed ledger could do bad things to your computer. However, I believe, it might be easy to sanbox it if one disallows the use of qualified functions.

3 Likes

Double-entry book-keeping is something that I’m interested in. I played around with Tigerbeetle many moons ago and found it impressive, but also overkill for my needs.

At some point I’ll build something using this: GitHub - coinjar/ex_double_entry: An Elixir double-entry library inspired by Ruby's DoubleEntry. Brought to you by CoinJar.

It might be useful for you too.

1 Like

Sorry, but an accounting nit which admittedly is besides the point of your post. The transaction below…

… looks wrong to me. You’re showing that you’re paying a liability, the amount that you owe someone, without ever having booked an expense for which that liability was incurred. Typically, for an incurred tax liability you’d have two transactions.

When the income is earned and on which the taxes are levied:

txn ~D[2025-07-09], notes: "account", accounts: %{
  account("Income::Studio rent") => ~A[-800 EUR],
  account("Expense::Tax::Taxes on rent") => ~A[224 EUR],
  account("Liabilities::Tax::Taxes owed") => ~A[-224 EUR],
  account("Assets::Bank::Checking") => ~A[800 EUR]
}

Later when the taxes are paid:

txn ~D[2025-08-01], notes: "account", accounts: %{
  account("Liabilities::Tax::Taxes owed") => ~A[224 EUR],
  account("Assets::Bank::Checking") => ~A[-224 EUR]
}

This represents that between earning the income which causes the taxes and the actual payment of the taxes, the money was sitting in your checking account. Naturally, if the taxes are paid straight-away, you’d just book the expense and never go to the liability account at all. There’s more to say about this… but this is a forum for Elixir and not accounting… so I’ve already said too much :slight_smile:. Also, I’m not an accountant, but am forced to play one in my daily work with uncomfortable regularity… so I could be completely wrong!

4 Likes

Thank you for the links, but I think the goal of the projects you’ve linked is very different from my own goal. My own goal is to have the elixir script define a ledger (which is of course further processed by code that checks everything is fine). There is no database because the script is the database.