A package for plaintext accounting named after the famous copper merchant Ea-nāṣir.
This package does not handle clay tablets, but sees value in the ideas of plaintext accounting and aims to make them available in typst.
As such this package allows for the creation of accounts and transactions and to group these in a ledger.
The beancount format can be used to read accounts and transactions from a text file.
Usage example
Say you’ve got a beancount file like ea-nasir.beancount.
You can read it into a ledger like this:
#import "@preview/ea-nasir:0.1.0": parseLedger, reportLedger
#let ledger = parseLedger(read("./ea-nasir.beancount"))
Which gives you a ledger holding all the accounts and transactions defined in that file.
You can then create a report on it like this:
#let report = reportLedger(ledger)
giving you a detailed report of the ledger.
#report.prefixes then holds data like this:
#{
(
assets: (
EUR: (
total:
decimal("1"),
plus: decimal("7"),
minus: decimal("-6"),
),
),
copper: (
XCP: (
total:
decimal("0"),
plus: decimal("8"),
minus: decimal("-8"),
),
),
expenses: (
EUR: (
total:
decimal("6"),
plus: decimal("6"),
minus: decimal("0"),
),
),
income: (
EUR: (
total:
decimal("-7"),
plus: 0,
minus: decimal("-7"),
),
),
)
}
Beancount syntax
The syntax used by this package is (currently) only a subset of what other tools support. This section describes what is supported.
Example
2026-08-01 open expenses:copper EUR ; Money spent on copper
2026-08-01 open expenses:travel EUR ; Travel costs
2026-08-01 open assets:money EUR ; Money kept by us
2026-08-01 open copper:ea-nasir XCP ; Copper held by Ea-nāṣir
2026-08-01 open copper:warehouse XCP ; Copper in our warehouse
2026-08-02 * "Buy copper" "We buy some sub-par copper from ea-nasir"
assets:money ; Inferred to -6 EUR
expenses:copper 5 EUR
expenses:travel 1 EUR
copper:ea-nasir -4 XCP
copper:warehouse 4 XCP
Empty lines
Empty lines may contain arbitrary whitespace.
Comments
Comments start with ; and are generally allowed on their own lines as well as at the end of other lines.
Dates
Dates generally have the Form yyyy-MM-dd - the date portion of an ISO 8601 string.
When comparing dates this package only cares about the day, never the time of day of account openings and transactions.
So a transaction may happen on the same day as an account is opened or later, but never before.
Account opening
Account opening statements have the form of
<date> open <account name> <currency>.
They can have a trailing comment that will be interpreted as the description for the account, like so: <date> open <account name> <currency> ; <description>.
Monotone
Parsing config will decide on the monotone behaviour of an account: some accounts may only receive (positive) or only loose (negative) money.
The default here is to treat accounts where the name starts with expenses: as monotone positive, and ones where the name starts with income: as monotone negative.
Transactions
Transactions generally start with a headline followed by a number of deltas.
A transaction headline has the form of <date> * "<name>" "<narration>".
Like with the account opening statement we accept a trailing comment as description: <date> * "<name>" "<description>".
Both name and narration are not allowed to contain " - because escaping just isn’t implemented.
Deltas generally have this form: <account name> <amount> <currency>.
It is allowed for a delta to just be the account name, if that account is the only one of unspecified amount and currency so that the amount can be inferred from the other deltas, and the currency can be looked up from the ledger.
Recall our example:
2026-08-02 * "Buy copper" "We buy some sub-par copper from ea-nasir"
assets:money ; Inferred to -6 EUR
expenses:copper 5 EUR
expenses:travel 1 EUR
copper:ea-nasir -4 XCP
copper:warehouse 4 XCP
Concepts
Currency
- A currency is a string.
- Same string means same currency.
- Typical currencies are USD, EUR.
- We also allow 🍿, “milk” or whatever else.
Date
- Dates are generally of type datetime
- We parse strings of the form yyyy-mm-dd into datetimes.
Account
- A (hierarchical) name
- An opening date
- A currency (USD, EUR, 🍿)
- A starting amount - missing will default to 0.
- An optional description
- An optional rule called monotone
- positive: amount may only grow.
- negative: amount may only shrink.
- If a monotone rule is given all deltas must obey it.
- A sorted log of deltas and times. We expect to append only.
- deltas are always decimals.
- times are always ?
Account naming
We understand the : in an account name to express hierarchy.
If we have accounts Expenses:Foo and Expenses:Bar
we can compute sums for both accounts as well as for Expenses.
Transaction
- A date
- A name
- A narration
- An optional description to expand on name and narration.
- A list of account deltas
- Triplets of account, amount, currency
- All deltas of the same currency must sum up to 0.
Ledger
- A collection of accounts and transactions.
Reports
- A report is calculated over an account or over a ledger.
- A report can be constrained to a date range.
- A report adds up all sums (of a date range) for an account.
- They are returned as (total, plus, minus).
- If the opening time of the account is in the date range, the initial amount is included in the sum.
Account reports
The report for an account consists of the following fields:
- The
namefor the account the report was computed on - The
currencyused for the account - The
from,tofields for the date range of the report - The
totalsum - The
plussum (sum of all positive additions to the account) - The
minussum (sum of all negative additions to the account)
Ledger reports
The report for a ledger consists of the following fields:
- A dictionary of
accountsthat maps account names to the report for that account - A dictionary of
prefixesthat map prefixes for account names (as separated by:) to currencySums- Where each currencySum maps a currency string to (total, plus, minus)
- An
alldictionary, that is the union ofaccountsandprefixes. - The
from,tofields for the date range of the report
API
This package exports the following functions:
Beancount
The central function to parse beancount text is parseLedger.
It takes a second, optional argument config, which defaults to parseAccountConfig, but can be overwritten accordingly.
Report
Two reporting functions exist:
reportLedger: create a report for a ledger over an optional timeframe.reportAccount: create a report for an account over an optional timeframe.
Ledger
A ledger is generally handled as immutable. This means the following functions don’t modify the ledger, but create a new one instead.
mkLedger: creates a new, empty ledgeraddAccount: adds an account to a ledgeraddTransaction: applies a transaction to a ledger
Transaction
The package exports the following functions for the transaction concept:
mkTransaction: it is how transactions are created.mkDelta: a helper functions to create deltas for a transaction.validateDelta: a helper function to validate the structure of deltas.
Account
The package exports two functions working on the account concept:
mkAccount, which creates new accounts.log, which takes an account and some data to add to the log of an account. It handles the account as immutable.
Date
We have two helper functions for date handling:
toDate, which converts a date of the formyyyy-MM-ddto adatetime.sameDayOrLater, which compares two dates checking that the second argument is on the same day or later than the first argument.