The best proof of cash software proves the bank row first, because every reconciling item in the four columns is measured against it and a bank row with a misread figure poisons all four. That was the first test when we ranked 8 proof of cash tools on how much of the bank side each one does. This post builds the four columns in Python so you can see exactly which part is automatable and which part isn't.

We make ZeraBooks and rank it first there. It does the bank side: reading the statement and proving it. The books side belongs to the accountant, or gets built into the firm's workflow.

What are the four columns?

Beginning balance, receipts, disbursements, ending balance. Each row has to read across (beginning plus receipts minus disbursements equals ending), and each column has to add down to the books. A plain bank reconciliation only checks the last column. The proof of cash also checks the two middle ones, which is where a receipt that never hit the bank would hide.

Where does the bank row come from?

Straight off the statement. Here's checking ••4417 for March 2025 from Matter 25-117, our fictional matter: opening 14,902.35, money in 8,120.00, money out 4,781.80, closing 18,240.55.

import csv from dataclasses import dataclass from decimal import Decimal as D @dataclass class Row: label: str beginning: D = D("0") receipts: D = D("0") disbursements: D = D("0") ending: D = D("0") def crossfoots(self): return self.beginning + self.receipts - self.disbursements == self.ending # The bank row, straight from the March statement for checking 4417. bank = Row("Per bank", D("14902.35"), D("8120.00"), D("4781.80"), D("18240.55")) assert bank.crossfoots(), "statement does not tie; fix extraction first" 

That assert is the line that matters. If the extraction misread one deposit, the script stops here, before a single reconciling item gets blamed for an error that was really in the PDF.

How do the reconciling items go in?

They come from the books, not the bank. March has two: a 500.00 deposit recorded on March 31 that the bank hadn't credited yet, and check 3290 for 312.40, written but not cleared.

items = [ Row("Deposit in transit (Mar 31)", receipts=D("500.00"), ending=D("500.00")), Row("Outstanding check 3290", disbursements=D("312.40"), ending=D("-312.40")), ] def total(rows, label): return Row(label, sum((r.beginning for r in rows), D("0")), sum((r.receipts for r in rows), D("0")), sum((r.disbursements for r in rows), D("0")), sum((r.ending for r in rows), D("0"))) adjusted = total([bank, *items], "Adjusted bank") books = Row("Per books", D("14902.35"), D("8620.00"), D("5094.20"), D("18428.15")) 

Does every column foot?

problems = [r.label for r in [*items, adjusted, books] if not r.crossfoots()] for col in ("beginning", "receipts", "disbursements", "ending"): gap = getattr(books, col) - getattr(adjusted, col) if gap: problems.append(f"{col} off by {gap}") print("proof holds" if not problems else problems) # proof holds 

Check it by hand once. Receipts: 8,120.00 + 500.00 = 8,620.00. Disbursements: 4,781.80 + 312.40 = 5,094.20. Ending: 18,240.55 + 500.00 − 312.40 = 18,428.15. Beginning is 14,902.35 on both sides.

How do you hand it to a reviewer?

Write it out as the workpaper rows, in order:

with open("proof_of_cash_4417_2025-03.csv", "w", newline="") as f: w = csv.writer(f) w.writerow(["", "Beginning", "Receipts", "Disbursements", "Ending"]) for r in [bank, *items, adjusted, books]: w.writerow([r.label, r.beginning, r.receipts, r.disbursements, r.ending]) 
Beginning Receipts Disbursements Ending
Per bank 14,902.35 8,120.00 4,781.80 18,240.55
Deposit in transit 500.00 500.00
Outstanding check 3290 312.40 −312.40
Per books 14,902.35 8,620.00 5,094.20 18,428.15

What can't the script confirm?

Here's the part I find most useful about this example. The deposit in transit should clear in April. In this matter, April's statement for ••4417 was never produced: March closes at 18,240.55 and May opens at 21,905.10. So the bank can't confirm the 500.00 ever landed, and the proof for April can't be built at all.

The script above would happily build March and never mention it. That's why the bank side needs a month-to-month chain as well as a tie-out. ZeraBooks flags a month whose opening doesn't match the prior closing, which is how the April hole surfaces before anyone starts the April proof. The matter-level view, with check images and duplicate uploads, is at zerabooks.com/forensic-accounting-software.

What's worth automating, and what isn't?

  • Automate the bank row completely. It's arithmetic against printed numbers.
  • Automate the footing and crossfooting. Nobody should add columns by hand in 2026.
  • Automate the chain check across months, so a missing statement stops the next proof.
  • Keep the reconciling items with the person who knows the books. A deposit in transit is a judgment about timing, not a parsing problem.

Prove the bank row. The other three rows only mean something after that.