# ledgerline

Plain-text double-entry accounting for people who want their money in version-controlled files. ledgerline reads a
strict, hledger-flavoured journal, refuses to accept anything that does not balance, points a caret at exactly what is
wrong when it fails, and turns the result into balance, register, income statement, balance sheet, cash flow and budget
reports with multi-commodity support, price-based valuation and Indian lakh digit grouping. Everything is
`Decimal` arithmetic; there is no database and no network access.

## Features

- **Strict journal format** with precise `file:line:col` errors, the offending line, a caret, and hints
  (did-you-mean for undeclared accounts, difference for failed assertions). All syntax errors are reported in one pass.
- **Multi-commodity balancing** with `@` / `@@` costs, one auto-balanced elided posting (producing one posting per
  commodity when needed), virtual postings `(A)` and `[A]`, tags, posting dates, `include` with globs.
- **Balance assertions** checked in date order, not file order.
- **Price database** from `P` directives plus implicit prices from costs; direct, inverse and one-hop lookup.
  `--value INR` at the report end date or historically (`--value-at then`), `-B` for cost basis.
- **Reports**: `bal` (tree/flat, depth, period columns), `reg` (running total), `is`, `bs` (with current earnings and
  unrealised gains), `cf` (operating/investing/financing/exchange), `budget` (periodic transactions, % used).
- **CSV importer** with TOML rules (column mapping, date format, debit/credit or signed amounts, regex rules),
  duplicate detection, and canonical output.
- **Canonical formatter** `fmt` (idempotent, `--check` for CI), **query language** (`payee:`, `tag:`, `amt:>1000`,
  `date:2026-01`, `not:`), `stats`, `accounts`, `payees`.
- **Output** as rich tables, `csv` or `json` for every report.
- Indian (`₹1,00,000.00`) and western (`$1,000,000.00`) grouping, driven by `commodity` directives.

## Install

Requires Python 3.12+.

```
git clone <this repo> && cd ledgerline
uv venv .venv && uv pip install -p .venv/bin/python -e '.[dev]'
.venv/bin/ledgerline --help
```

(`pip install -e .` in any virtualenv works equally well.)

## Quickstart

The repository ships a realistic year of finances in `examples/` (410 transactions, 12 monthly files, prices, an
import CSV with rules).

```
export LEDGERLINE_FILE=examples/main.journal
ledgerline check                          # parse + balance + assertions + strict accounts
ledgerline bal --depth 2                  # balances
ledgerline bal Expenses --quarterly       # spending by quarter
ledgerline is --monthly                   # income statement
ledgerline bs --value INR                 # net worth, USD holdings and stocks valued in rupees
ledgerline budget --yearly                # budget vs actual
ledgerline import examples/bank.csv --rules examples/rules.toml
```

Every command accepts `-f/--file` (or `$LEDGERLINE_FILE`), and `-o table|csv|json`.

## Usage

All output below was produced by running the commands against `examples/main.journal`.

### check

```
$ ledgerline check
OK: 410 transactions, 809 postings, 15 file(s)
```

Errors exit with status 1 and print one block per problem. Syntax errors (`examples/broken-syntax.journal`):

```
$ ledgerline check -f examples/broken-syntax.journal
error: invalid date '2026-02-30': 2026-02 has only 28 days
 --> examples/broken-syntax.journal:8:1
  |
8 | 2026-02-30 Impossible date
  | ^^^^^^^^^^

error: misplaced thousands separator in '1,00.50'
  --> examples/broken-syntax.journal:13:20
   |
13 |     Expenses:Food  1,00.50 INR
   |                    ^^^^^^^
   = hint: use 1,000 (western) or 1,00,000 (indian) grouping, or no commas

error: account name 'Expenses:Food 80 INR' contains an amount-like part
  --> examples/broken-syntax.journal:17:5
   |
17 |     Expenses:Food 80 INR
   |     ^^^^^^^^^^^^^^^^^^^^
   = hint: separate the account name from the amount with two or more spaces

3 errors found
```

Errors in the books themselves (`examples/broken-books.journal`, strict mode on):

```
$ ledgerline check -f examples/broken-books.journal
error: account 'Expenses:Fod' is not declared (strict mode)
  --> examples/broken-books.journal:15:5
   |
15 |     Expenses:Fod    250.00 INR
   |     ^^^^^^^^^^^^
   = hint: did you mean 'Expenses:Food'?

error: transaction does not balance: remaining 10.00 INR
  --> examples/broken-books.journal:19:1
   |
19 | 2026-01-10 Taxi
   | ^^^^^^^^^^^^^^^
   = hint: add the missing posting, or leave one amount out and it will be filled in automatically

2 errors found
```

A failed balance assertion (`examples/broken-assertion.journal`):

```
$ ledgerline check -f examples/broken-assertion.journal
error: balance assertion failed for Assets:Bank: expected 650.00 INR, actual 750.00 INR
  --> examples/broken-assertion.journal:17:32
   |
17 |     Assets:Bank       0.00 INR = 650.00 INR
   |                                ^^^^^^^^^^^^
   = hint: difference is -100.00 INR (assertions are checked in date order, as of 2026-01-31)

1 error found
```

### bal

Options: `QUERY...`, `--depth N`, `--flat/--tree`, `--empty`, `-b/-e DATE`, `--monthly/--quarterly/--yearly`,
`-H/--historical` (period columns show ending balances), `--value COMMODITY`, `--value-at end|then`, `-B/--cost`.

```
$ ledgerline bal --depth 2 --value INR
Balance

 Account                  Balance
 ────────────────────────────────
 Assets             ₹29,32,033.71
   Bank             ₹16,61,575.68
   Broker            ₹7,85,994.81
   Cash                ₹47,212.10
   Investments       ₹4,37,251.12
 Equity            -₹11,58,030.00
   Opening         -₹11,58,030.00
 Expenses            ₹6,88,473.66
   Entertainment       ₹10,116.00
   Food              ₹1,59,551.17
   Health               ₹9,168.16
   Insurance           ₹21,500.00
   Rent              ₹3,00,000.00
   Shopping            ₹34,460.39
   Transport           ₹46,140.95
   Travel              ₹55,650.50
   Utilities           ₹51,886.49
 Income            -₹23,81,364.97
   Bonus              -₹60,000.00
   Dividends           -₹4,108.53
   Interest            -₹7,256.44
   Salary          -₹23,10,000.00

 Total                 ₹81,112.40
```

The last line is the unrealised gain on the USD stock and mutual-fund holdings: assets are marked to market while
equity stays at cost (see [docs/design.md](docs/design.md)).

```
$ ledgerline bal Expenses --quarterly --depth 2
Balance

 Account                 2026Q1         2026Q2         2026Q3         2026Q4          Total
 ──────────────────────────────────────────────────────────────────────────────────────────
 Expenses          ₹2,06,050.91   ₹1,48,674.52   ₹1,74,430.97   ₹1,59,317.26   ₹6,88,473.66
   Entertainment      ₹2,529.00      ₹2,529.00      ₹2,529.00      ₹2,529.00     ₹10,116.00
   Food              ₹32,952.52     ₹41,218.30     ₹39,486.58     ₹45,893.77   ₹1,59,551.17
   Health             ₹2,612.50        ₹669.67      ₹1,115.53      ₹4,770.46      ₹9,168.16
   Insurance                  0              0     ₹21,500.00              0     ₹21,500.00
   Rent              ₹75,000.00     ₹75,000.00     ₹75,000.00     ₹75,000.00   ₹3,00,000.00
   Shopping          ₹10,163.49      ₹8,176.52      ₹9,462.31      ₹6,658.07     ₹34,460.39
   Transport         ₹13,868.57      ₹9,684.01     ₹11,257.98     ₹11,330.39     ₹46,140.95
   Travel            ₹55,650.50              0              0              0     ₹55,650.50
   Utilities         ₹13,274.33     ₹11,397.02     ₹14,079.57     ₹13,135.57     ₹51,886.49

 Total             ₹2,06,050.91   ₹1,48,674.52   ₹1,74,430.97   ₹1,59,317.26   ₹6,88,473.66
```

### reg

```
$ ledgerline reg tag:trip=goa
Register

 Date         Description                       Account                             Amount   Running total
 ─────────────────────────────────────────────────────────────────────────────────────────────────────────
 2026-03-09   IndiGo | Flights BLR-GOI return   Expenses:Travel:Flights         ₹18,400.00      ₹18,400.00
                                                Liabilities:CreditCard:ICICI   -₹18,400.00               0
 2026-03-09   Taj Holiday Village | 4 nights    Expenses:Travel:Hotels          ₹24,000.00      ₹24,000.00
                                                Liabilities:CreditCard:ICICI   -₹24,000.00               0
 2026-03-14   Fisherman's Wharf | Dining out    Expenses:Travel:Food             ₹3,150.00       ₹3,150.00
                                                Liabilities:CreditCard:ICICI    -₹3,150.00               0
 2026-03-14   Scooter rental                    Expenses:Travel:Transport        ₹2,400.00       ₹2,400.00
                                                Assets:Cash:Wallet              -₹2,400.00               0
 2026-03-15   Britto's | Dining out             Expenses:Travel:Food             ₹2,480.50       ₹2,480.50
                                                Liabilities:CreditCard:ICICI    -₹2,480.50               0
 2026-03-16   Gunpowder | Dining out            Expenses:Travel:Food             ₹2,890.00       ₹2,890.00
                                                Liabilities:CreditCard:ICICI    -₹2,890.00               0
 2026-03-16   Beach shack                       Expenses:Travel:Food             ₹1,350.00       ₹1,350.00
                                                Assets:Cash:Wallet              -₹1,350.00               0
 2026-03-17   Café Chocolatti | Dining out      Expenses:Travel:Food               ₹980.00         ₹980.00
                                                Liabilities:CreditCard:ICICI      -₹980.00               0
```

Queries combine: `ledgerline reg Expenses:Rent date:2026-03 amt:">1000"`.

```
$ ledgerline reg Expenses:Rent date:2026-03 amt:">1000"
Register

 Date         Description       Account             Amount   Running total
 ─────────────────────────────────────────────────────────────────────────
 2026-03-02   Landlord | Rent   Expenses:Rent   ₹25,000.00      ₹25,000.00
```

### is, bs, cf

```
$ ledgerline is --quarterly --depth 1
Income Statement

 Account                2026Q1         2026Q2         2026Q3         2026Q4           Total
 ──────────────────────────────────────────────────────────────────────────────────────────
 Revenues
 Income           ₹6,16,959.42   ₹5,86,786.25   ₹5,86,735.15   ₹5,86,775.62   ₹23,77,256.44
                        $13.52         $11.31         $11.47         $12.03          $48.33

 Total revenues   ₹6,16,959.42   ₹5,86,786.25   ₹5,86,735.15   ₹5,86,775.62   ₹23,77,256.44
                        $13.52         $11.31         $11.47         $12.03          $48.33

 Expenses
 Expenses         ₹2,06,050.91   ₹1,48,674.52   ₹1,74,430.97   ₹1,59,317.26    ₹6,88,473.66

 Total expenses   ₹2,06,050.91   ₹1,48,674.52   ₹1,74,430.97   ₹1,59,317.26    ₹6,88,473.66

 Net income       ₹4,10,908.51   ₹4,38,111.73   ₹4,12,304.18   ₹4,27,458.36   ₹16,88,782.78
                        $13.52         $11.31         $11.47         $12.03          $48.33
```

```
$ ledgerline bs --value INR --depth 2
Balance Sheet

 Account                                            total
 ────────────────────────────────────────────────────────
 Assets
 Bank                                       ₹16,61,575.68
 Broker                                      ₹7,85,994.81
 Cash                                          ₹47,212.10
 Investments                                 ₹4,37,251.12

 Total assets                               ₹29,32,033.71

 Liabilities

 Total liabilities                                      0

 Net worth                                  ₹29,32,033.71

 Equity
 Opening                                    ₹11,58,030.00
 Current earnings                           ₹16,92,891.31
 Unrealised gains / commodity differences      ₹81,112.40

 Total equity                               ₹29,32,033.71
```

`Current earnings` is the not-yet-closed profit; `Unrealised gains` closes the gap between market value and cost, so
`Net worth` always equals `Total equity`.

```
$ ledgerline cf --yearly --depth 1
Cash Flow

 Account                    2026
 ───────────────────────────────
 Operating
 Expenses          -₹4,39,344.61
 Income            ₹23,77,256.44
                          $48.33

 Total operating   ₹19,37,911.83
                          $48.33

 Investing
 Assets            -₹3,80,000.00
                      -$4,975.22

 Total investing   -₹3,80,000.00
                      -$4,975.22

 Financing
 Equity             ₹9,03,000.00
                       $3,000.00
 Liabilities       -₹2,49,129.05

 Total financing    ₹6,53,870.95
                       $3,000.00

 Exchange
 Exchange          -₹5,02,995.00
                       $6,000.00

 Total exchange    -₹5,02,995.00
                       $6,000.00

 Net cash flow     ₹17,08,787.78
                       $4,073.11
 Cash at start                 0
 Cash at end       ₹17,08,787.78
                       $4,073.11
```

### budget

`~ monthly` transactions in `main.journal` define the budget; actuals roll up from sub-accounts.

```
$ ledgerline budget --yearly
Budget (actual / budget, % used)

 Account                                                 2026
 ────────────────────────────────────────────────────────────
 Expenses:Entertainment         ₹10,116.00 / ₹10,800.00 (94%)
 Expenses:Food:Dining           ₹65,304.75 / ₹72,000.00 (91%)
 Expenses:Food:Groceries      ₹94,246.42 / ₹1,20,000.00 (79%)
 Expenses:Rent             ₹3,00,000.00 / ₹3,00,000.00 (100%)
 Expenses:Shopping              ₹34,460.39 / ₹48,000.00 (72%)
 Expenses:Transport             ₹46,140.95 / ₹48,000.00 (96%)
 Expenses:Utilities             ₹51,886.49 / ₹66,000.00 (79%)

 Total                      ₹6,02,155.00 / ₹6,64,800.00 (91%)
```

Cells over 100% are red, 80% and above yellow.

### import

```
$ ledgerline import examples/bank.csv --rules examples/rules.toml
15 new, 3 duplicate(s) skipped, 4 uncategorized (marked !)
2027-01-01 * Acme Corp | NEFT CR-ACME CORP-SALARY JAN  ; tag:salary, import:17f07fb9a69a
    Assets:Bank:HDFC  ₹1,95,000.00
    Income:Salary

2027-01-02 * Landlord | UPI-LANDLORD-RENT JAN  ; import:15ca623493ea
    Assets:Bank:HDFC  -₹25,000.00
    Expenses:Rent

2027-01-03 * UPI-ZOMATO-ORDER-88123  ; tag:food, import:8b25f29826ca
    Assets:Bank:HDFC      -₹486.00
    Expenses:Food:Dining

2027-01-04 * BESCOM | BESCOM ELECTRICITY BILL  ; tag:utilities, import:c0c3cfb816f8
    Assets:Bank:HDFC                -₹2,284.50
...
15 new, 3 duplicate(s) skipped, 4 uncategorized (marked !)
```

The three December rows that already exist in the journal (typed by hand with the bank's narration) were skipped.
Add `--append FILE` to write into a journal; re-running the same import afterwards reports `0 new`. Rows that no rule
matched go to `Expenses:Uncategorized` and are marked `!` for review. Rules format:

```toml
[csv]
account = "Assets:Bank:HDFC"
currency = "INR"
date_column = "Date"
date_format = "%d/%m/%y"
description_column = "Narration"
debit_column = "Withdrawal Amt."      # or: amount_column = "Amount" (+ invert = true)
credit_column = "Deposit Amt."
default_account = "Expenses:Uncategorized"

[[rule]]
match = "SWIGGY|ZOMATO"              # regex, case-insensitive, first match wins
account = "Expenses:Food:Dining"
payee = "Swiggy"                     # optional
status = "*"                         # optional ("!" to flag for review)
tags = "tag:food"                    # optional
```

### fmt, stats, accounts, payees

```
$ ledgerline fmt --check && echo canonical
canonical
```

`ledgerline fmt` prints the formatted root file; `-w` rewrites it and every included file; `--check` exits 1 if anything
would change.

```
$ ledgerline stats
Journal statistics

 Metric                                                           Value
 ──────────────────────────────────────────────────────────────────────
 Files                                                               15
 Transactions                                                       410
 Postings                                                           809
 First transaction                                           2026-01-01
 Last transaction                                            2026-12-31
 Span (days)                                                        365
 Accounts used                                                       30
 Accounts declared                                                   31
 Commodities                    AAPL, FLEXICAP, INR, MSFT, NIFTY50, USD
 Payees                                                              54
 Price directives                                                    84
 Periodic transactions                                                1
 Balance assertions                                                  16
 Pending (!) transactions                                             0
 Transactions per month (avg)                                      34.2
 Busiest month                                             2026-03 (43)
```

```
$ ledgerline accounts --tree --depth 2
Accounts

 Account           Postings   Declared
 ─────────────────────────────────────
 Assets                 312        yes
   Bank                 183        yes
   Broker                29        yes
   Cash                  75        yes
   Investments           25        yes
 Equity                   2        yes
   Opening                2        yes
 Expenses               300        yes
   Entertainment         36        yes
   Food                 103        yes
```

```
$ ledgerline payees
Payees

 Payee                   Transactions        First         Last
 ──────────────────────────────────────────────────────────────
 SIP                               24   2026-01-05   2026-12-05
 Uber                              24   2026-01-15   2026-12-27
 Chai Point                        23   2026-01-10   2026-12-22
 Local vegetables                  19   2026-02-04   2026-12-09
 Auto rickshaw                     18   2026-01-09   2026-12-09
```

### JSON and CSV output

Reports are emitted as tidy records (one row per account, period and commodity):

```
$ ledgerline bal Expenses --yearly --depth 2 -o csv
account,period,commodity,quantity
Expenses,2026,INR,688473.66
Expenses:Entertainment,2026,INR,10116.00
Expenses:Food,2026,INR,159551.17
Expenses:Health,2026,INR,9168.16
Expenses:Insurance,2026,INR,21500.00
Expenses:Rent,2026,INR,300000.00
Expenses:Shopping,2026,INR,34460.39
Expenses:Transport,2026,INR,46140.95
Expenses:Travel,2026,INR,55650.50
Expenses:Utilities,2026,INR,51886.49
Total,2026,INR,688473.66
```

### Query language

`Expenses:Food`, `payee:Uber`, `tag:trip=goa`, `amt:>1000`, `date:2026-03`, `status:!`, `not:Rent`, all ANDed.
Full reference: [docs/query-language.md](docs/query-language.md).

## Configuration

| Setting | Where |
|---|---|
| Journal file | `-f/--file`, or `$LEDGERLINE_FILE` |
| Strict accounts | `option strict` in the journal, or `--strict` |
| Commodity display | `commodity` directives (symbol, precision, grouping) |
| Import mapping and rules | `rules.toml` (see above) |
| Cash accounts for `cf` | `--cash REGEX` |

Dates on the command line are `YYYY`, `YYYY-MM` or `YYYY-MM-DD`; `-b` is inclusive and `-e` exclusive.
Exit codes: 0 ok, 1 journal/format errors (or `fmt --check` differences), 2 usage errors (bad query, missing `-f`).

The file syntax is documented in [docs/journal-format.md](docs/journal-format.md), the algorithms (balancing, assertion
ordering, price lookup, valuation modes, cash flow classification, import de-duplication) in
[docs/design.md](docs/design.md).

## Architecture

```
src/ledgerline/
  cli.py        click commands, option wiring, error -> exit code mapping
  parser.py     line-oriented parser -> Journal AST; collects errors with line/col
  models.py     Amount, Posting, Transaction, PeriodicTransaction, PriceDirective, Journal
  numfmt.py     commodity formats, lakh/western grouping, Decimal rendering
  booking.py    balancing, elision, strict accounts, date-ordered assertions
  prices.py     PriceDB: P directives + implicit prices, direct/inverse/one-hop lookup
  query.py      query language -> predicates over (transaction, posting)
  periods.py    partial dates, month/quarter/year columns, recurrence expansion
  reports.py    entries, valuation, aggregation, bal/reg/is/bs/cf/budget
  output.py     Report container; rich table, csv and json emitters
  importer.py   rules.toml, CSV reading, de-duplication, transaction building
  formatter.py  canonical transaction rendering and file formatting
  info.py       stats, accounts, payees
  errors.py     LedgerError (caret rendering), JournalError, UsageError
scripts/gen_examples.py   deterministic generator for examples/
```

## Tests

```
uv pip install -p .venv/bin/python -e '.[dev]'
.venv/bin/pytest
```

130 tests cover every directive and parser error with line/column, balancing (multi-commodity, costs, virtual
postings), assertion ordering, price chains, the query language, each report's numbers on a small fixture, import
de-duplication, formatter idempotency, and end-to-end CLI runs against `examples/`.

## Limitations

- Balancing is exact (no display-precision tolerance); use `@@` when a unit price does not multiply out.
- Price chains are limited to one intermediate commodity; amounts without a path stay in their own commodity.
- `bs` ignores `-b`; balances always accumulate from the start of the journal. There are no closing entries or
  period-end retained-earnings transfers; "current earnings" is computed.
- Cash-flow categories are derived from account types (credit-card payments appear as financing).
- Secondary dates (`DATE=DATE2`), lot tracking / capital-gains realisation, automated transactions and a
  `--forecast` view of periodic transactions are not implemented.
- The CSV importer handles one statement account per rules file and decimal-point numbers.
- Regexes in queries use Python syntax, matched case-insensitively.

## License

MIT, see [LICENSE](LICENSE).
