CLI · commit e841098

bankrecon

Bank reconciliation engine: statements vs ledger with many-to-many matching

Download bankrecon.zip
Quickstart
unzip bankrecon.zip && cd bankrecon
uv venv .venv && uv pip install -p .venv/bin/python -e '.[dev]'
.venv/bin/python -m pytest -q

bankrecon

Bank reconciliation for finance teams. bankrecon imports a bank statement (HDFC / ICICI / SBI style CSV or XLSX, MT940, or ISO 20022 CAMT.053) and the cash book, matches them with explainable rules (one-to-one, one-to-many, many-to-one and small many-to-many groups), classifies unbooked bank items (charges, GST, interest, TDS, returns) with suggested journal entries, produces the Bank Reconciliation Statement (BRS) with a hard tie-out check, and carries outstanding items forward to the next period with aging.

Features

Install

uv venv .venv && uv pip install -p .venv/bin/python -e '.[dev]'
source .venv/bin/activate        # or call .venv/bin/bankrecon directly

Requires Python 3.12+. Dependencies: click, rich, openpyxl.

Quickstart

The repository ships a realistic two-month data set in examples/ (regenerate it with bankrecon sample --out examples). September 2026 has 500 bank lines and 480 cash book entries; October continues from it. The same September statement is provided as bank_hdfc.csv, bank_icici.csv, bank_sbi.csv, bank_hdfc.xlsx, bank.mt940 and bank.camt053.xml.

export BANKRECON_DB=/tmp/demo.db      # default is ./bankrecon.db

# September: first period, so give the opening cash book balance and the items outstanding at 31 Aug
bankrecon run --bank examples/2026-09/bank.mt940 --ledger examples/2026-09/cashbook.csv \
    --period 2026-09 --opening-book 4498444.95 \
    --prior-outstanding examples/2026-09/prior_outstanding.csv --xlsx out/2026-09.xlsx

bankrecon review                # accept / reject suggestions, match by hand
bankrecon brs                   # Bank Reconciliation Statement
bankrecon explain M-0440        # why were these items matched?

# October: openings and outstanding items come from the database
bankrecon run --bank examples/2026-10/bank.camt053.xml --ledger examples/2026-10/cashbook.csv --period 2026-10
bankrecon outstanding --aging

Usage with real output

bankrecon run

$ bankrecon run --bank examples/2026-09/bank.mt940 --ledger examples/2026-09/cashbook.csv \
      --period 2026-09 --opening-book 4498444.95 --prior-outstanding examples/2026-09/prior_outstanding.csv
Period 2026-09: 500 bank lines, 480 ledger lines, 9 carried-forward items
              Matching results               
┏━━━━━━━━━━━━━━━┳━━━━━━━━━┳━━━━━━━━━━━━━━━━━┓
┃ Rule          ┃ Matches ┃ Awaiting review ┃
┡━━━━━━━━━━━━━━━╇━━━━━━━━━╇━━━━━━━━━━━━━━━━━┩
│ reference     │ 328     │ 0               │
│ cheque        │ 86      │ 0               │
│ amount_date   │ 23      │ 0               │
│ fuzzy         │ 2       │ 2               │
│ one_to_many   │ 6       │ 0               │
│ many_to_one   │ 5       │ 0               │
│ many_to_many  │ 2       │ 2               │
│ bank_reversal │ 1       │ 0               │
└───────────────┴─────────┴─────────────────┘
Unmatched: 38 bank, 17 book. Suggested journal entries: 38.
BRS: TIED
4 suggested match(es) need review: `bankrecon review --period 2026-09`

Options: --format auto|csv|xlsx|mt940|camt053 (auto looks at the extension), --profile hdfc|icici|sbi|<custom>, --opening-bank, --opening-book, --closing-book, --prior-outstanding FILE, --xlsx, --csv-dir, --json, --strict (exit status 2 if the BRS does not tie). Opening bank balance is taken from the flag, else the statement (MT940 :60F:, CAMT OPBD, or the first CSV balance minus the first movement), else the previous period. Opening book balance comes from the flag or the previous period.

bankrecon review

Run without flags it walks through each suggested match, then offers manual matching:

$ bankrecon review --user akshay        # (a)ccept (r)eject (n)ote (s)kip (q)uit

One-shot flags for scripting: --accept M-0438, --reject M-0452 --note "credit note still pending", --match B-0249=L-0265 --note "confirmed with sales", --note-on L-0480 --note "...", --show-audit.

$ bankrecon review --show-audit
┏━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
┃ When                ┃ User   ┃ Action       ┃ Target  ┃ Detail
┡━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
│ 2026-10-16T21:23:04 │ konkan │ run          │ 2026-09 │ {'matches': 453, 'carried': 9}
│ 2026-10-16T21:23:05 │ akshay │ accept       │ M-0438  │ {'note': ''}
│ 2026-10-16T21:23:05 │ akshay │ accept       │ M-0439  │ {'note': ''}
│ 2026-10-16T21:23:05 │ akshay │ accept       │ M-0451  │ {'note': ''}
│ 2026-10-16T21:23:05 │ akshay │ reject       │ M-0452  │ {'note': 'Credit note still pending', 'was': 'suggested', 'bank':
│                     │        │              │         │ ['B-0223', 'B-0234'], 'book': ['L-0223', 'L-0213']}

Rejecting a match releases its items (they appear as unmatched and carry forward); manual matches must balance to the paisa.

bankrecon brs

Prints the statement (excerpt below, after the review above) and exits with status 2 if it does not tie; --format json and the output flags work here too.

$ bankrecon brs
BANK RECONCILIATION STATEMENT - 2026-09
========================================================================================================================
Balance as per cash book                                                                                    64,52,046.84
Add: cheques issued but not yet presented                                                                   22,73,353.78
    L-0038 02-Sep PV/26-27/0028 Cheque 450327 issued to Bhavani Transport Co against B     3,22,110.95
    L-0088 04-Sep PV/26-27/0051 Cheque 450326 issued to Omkar Raw Materials against BI     1,09,024.98
    L-0236 14-Sep PV/26-27/0118 Cheque 450336 issued to Uday Fabricators against BILL-     1,66,496.73
    ...
Add: bank credits not yet booked in the cash book                                                            3,46,900.46
    B-0011 01-Sep NEFT CR-HDFC0009999-UNKNOWN REMITTER-SBINR5202609019032792 [unclassi       24,887.77
    B-0012 01-Sep NEFT CR-ICIC0009999-UNKNOWN REMITTER-UTIBR5202609016527997 [unclassi       17,856.26
    B-0028 02-Sep NEFT CR-SBIN0009999-UNKNOWN REMITTER-UTIBR5202609028735259 [unclassi        9,693.89
    B-0030 02-Sep NEFT CR-PUNB0009999-UNKNOWN REMITTER-SBINR5202609027706513 [unclassi       24,115.93
    B-0107 07-Sep NEFT RETURN-SBINR5202609043405436-ACCOUNT CLOSED [returned_payment]        48,504.58
    ...
Less: deposits booked but not yet credited by the bank                                                        -73,429.49
    L-0213 11-Sep RV/26-27/0117 Receipt from Bharat Polymers Ltd - invoice INV-26/1145       21,564.81
    L-0223 12-Sep RV/26-27/0121 Receipt from Bharat Polymers Ltd - invoice INV-26/1150       21,976.62
    L-0266 15-Sep RV/26-27/0143 Counter receipt 2                                            29,888.06
Less: book receipts with no bank credit (possible duplicate entries)                                          -77,242.65
    L-0358 18-Sep RV/26-27/0194 Receipt from Evergreen Organics against INV-26/1166          77,242.65
Less: bank debits not yet booked in the cash book                                                           -1,59,328.60
    B-0029 02-Sep ATM CASH WDL SELF 0209 [cash_withdrawal]                                    5,966.31
    ...
------------------------------------------------------------------------------------------------------------------------
Balance as per bank (computed)                                                                              87,62,300.34
Balance as per bank statement                                                                               87,62,300.34
Difference                                                                                                          0.00
========================================================================================================================
STATUS: TIED
wrote out/2026-09-final.xlsx

bankrecon explain

$ bankrecon explain M-0440
╭──────────────────────────────────────────────────────────── Match ─────────────────────────────────────────────────────────────╮
│ M-0440  rule: one_to_many  status: auto  confidence: 0.85                                                                      │
│ one bank line equals the sum of several book entries                                                                           │
╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
  * bank item B-0106 for 1,17,011.39 equals the sum of 5 book items (12,421.70 + 36,715.56 + 37,916.03 + 16,741.19 + 13,216.91 = 
1,17,011.39)
  * Group dates within ±10 days of 2026-09-07; 22 candidates searched, smallest group size 5 (max 6)
  * Exactly one combination of this size fits
┏━━━━━━━━┳━━━━━━┳━━━━━━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━┓
┃ Id     ┃ Side ┃ Date       ┃ Amount      ┃ Narration                                  ┃ References ┃
┡━━━━━━━━╇━━━━━━╇━━━━━━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━┩
│ B-0106 │ bank │ 2026-09-07 │ 1,17,011.39 │ BY CASH DEPOSIT-CDM BRANCH 0123            │            │
│ L-0102 │ book │ 2026-09-06 │ 12,421.70   │ RV/26-27/0058 Cash sales receipt counter 5 │            │
│ L-0101 │ book │ 2026-09-05 │ 36,715.56   │ RV/26-27/0057 Cash sales receipt counter 4 │            │
│ L-0092 │ book │ 2026-09-04 │ 37,916.03   │ RV/26-27/0049 Cash sales receipt counter 3 │            │
│ L-0060 │ book │ 2026-09-03 │ 16,741.19   │ RV/26-27/0031 Cash sales receipt counter 2 │            │
│ L-0042 │ book │ 2026-09-02 │ 13,216.91   │ RV/26-27/0022 Cash sales receipt counter 1 │            │
└────────┴──────┴────────────┴─────────────┴────────────────────────────────────────────┴────────────┘
Bank total 1,17,011.39  Book total 1,17,011.39

explain also accepts a bank/book item id and tells you whether it is matched or why it is outstanding.

Carry-forward across periods and aging

$ bankrecon run --bank examples/2026-10/bank.camt053.xml --ledger examples/2026-10/cashbook.csv --period 2026-10
Period 2026-10: 371 bank lines, 361 ledger lines, 57 carried-forward items
              Matching results               
┏━━━━━━━━━━━━━━━┳━━━━━━━━━┳━━━━━━━━━━━━━━━━━┓
┃ Rule          ┃ Matches ┃ Awaiting review ┃
┡━━━━━━━━━━━━━━━╇━━━━━━━━━╇━━━━━━━━━━━━━━━━━┩
│ reference     │ 198     │ 0               │
│ cheque        │ 85      │ 0               │
│ amount_date   │ 35      │ 0               │
│ fuzzy         │ 2       │ 2               │
│ one_to_many   │ 6       │ 0               │
│ many_to_one   │ 5       │ 0               │
│ many_to_many  │ 3       │ 3               │
│ bank_reversal │ 1       │ 0               │
└───────────────┴─────────┴─────────────────┘
Unmatched: 65 bank, 25 book. Suggested journal entries: 65.
BRS: TIED
5 suggested match(es) need review: `bankrecon review --period 2026-10`

$ bankrecon outstanding --aging
    Outstanding at end of 2026-10     
┏━━━━━━━━━━━━┳━━━━━━━┳━━━━━━━━━━━━━━━┓
┃ Age (days) ┃ Items ┃ Net amount    ┃
┡━━━━━━━━━━━━╇━━━━━━━╇━━━━━━━━━━━━━━━┩
│ 0-30       │ 58    │ -24,38,491.27 │
│ 31-60      │ 31    │ -5,09,400.77  │
│ 61-90      │ 0     │ 0.00          │
│ 90+        │ 1     │ -10,498.12    │
└────────────┴───────┴───────────────┘
...

The 57 carried-forward items are the September outstanding items (cheques not yet presented, deposits in transit, unbooked bank charges, and anything left unmatched). Cheques that cleared in October and the charges booked on 1-3 October were matched against them; the opening balances were taken from the September closing balances and the BRS still ties. The one cheque dated 12 May is flagged stale.

Other commands

Input formats

Input Notes
Bank CSV/XLSX Header row is auto-detected, so preamble lines (SBI) and footers are fine. Profile = column names + date format.
MT940 :60F:/:62F: balances, :61: with value and entry date, C/D/RC/RD, :86: free text or ?NN structured. The entry date is used when present.
CAMT.053 Any namespace version. CdtDbtInd is read as the effect on the account (also for RvslInd=true). One Ntry is one line even if it carries several TxDtls.
Cash book CSV Columns Date, Voucher No, Narration, Cheque No, Debit, Credit, Party (case/spacing flexible). Debit = money in.
Prior outstanding side (bank/book), date, reference, narration, cheque_no, amount (money in positive, so unpresented cheques are negative).

Configuration

A TOML file passed with --config overrides pass order, windows, group-size bounds, the invoice regex, the account names used in journal entries, classification rules and custom bank profiles. See docs/configuration.md and the annotated examples/bankrecon.toml.

Architecture

src/bankrecon/
  models.py        Txn, Match, RawLine (signed amounts: money in is positive on both sides)
  normalize.py     dates, amounts, reference extraction, party names, token-set similarity, INR formatting
  config.py        defaults + TOML loading
  importers/       tabular.py (CSV/XLSX profiles), mt940.py, camt053.py, ledger.py
  loaders.py       RawLine/BookLine -> Txn, prior-outstanding file, period helpers
  engine.py        the matching passes and the pruned subset-sum search
  classify.py      rule-based classification of bank-only items -> journal entries
  recon.py         derived state: unmatched sets, categories, aging, BRS assembly
  brs.py           BRS model and tie-out diagnostics
  store.py         SQLite: periods, txns, matches, notes, audit log
  pipeline.py      run_period: import -> carry forward -> match -> store
  review.py        accept / reject / manual match / notes with audit logging
  exporters.py     Excel, CSV, JSON, text BRS
  cli.py           click commands
  samplegen.py, sampleio.py   synthetic data with planted scenarios, writers for all formats
docs/              matching.md, brs.md, configuration.md
examples/          two months of generated data (+ truth.json describing the planted scenarios)

Tests

.venv/bin/pytest -q

107 tests cover MT940/CAMT.053 parsing, reference regexes, each matching pass (including subset-sum bounds and ambiguity), the BRS tie-out identity as a property over generated data (also after random reviewer rejections), carry-forward across two periods, classification and journal entries, Excel/CSV/JSON output, explain, and the review workflow (flags and interactive prompts). A ground-truth test checks that every planted scenario in the September example is matched correctly and that the deliberately ambiguous pair is left alone.

Limitations