SuperAPI AI Directory

data-reconcile

Office & productivity

View repository

Updated 10-16 01:19

About this skill

Finance Reconciliation

Reconcile A (ours) against B (theirs) and — the point of the skill — triage the differences: every unreconciled item is classified, not just listed, so finance can act on the few that matter. Produces a reconciliation working paper for a qualified person to review and sign off.

Working paper, not a posting. It matches and flags; it does not adjust the ledger, post a correction, or write to any system. Unmatched items stay flagged — never force-fitted into a match. Matching and triage are computed on your machine (the engine makes no external calls) — though the AI agent driving it does send whatever it reads into its context to your AI provider; see ../../PRINCIPLES.md#data-handling--pii-policy.

Workflow

import sys; sys.path.insert(0, "scripts")
from reconcile import (reconcile_files, propose_aggregations, apply_aggregations,
                       finalize, render_proposals, write_workpaper, render_report, PRESETS)

Run from the skill directory (skills/data-reconcile/). The scripts path resolves to this skill's scripts/ subdirectory where reconcile.py lives.

1. Intent first — what are we reconciling, and against what?

Establish A (e.g. our invoice tracker / cashbook / internal records) and B (e.g. the ledger / bank statement / fund administrator). Pick the preset if it's a recurring one (python scripts/reconcile.py --catalogue), else go generic.

2. Pick the match strategy (per run — it's a mix)

  • Key — a shared reference exists on both sides (invoice no, payment ref): --mode key --key <col>.
  • Amount + date — no clean key (typical for bank vs cashbook): --mode amount_date — matches on amount within tolerance, nearest date within the date window (--date-window N, default ±5 days). A small in-window gap is a timing difference (likely in-transit), not a true exception. An equal-amount pair outside the window is not reconciled — it's flagged ambiguous_match for you to confirm, so transactions weeks apart aren't passed off as timing.
    • Always pass --date <col> in this mode — the date window is the safety rail. With no resolvable date column the window can't apply, so equal-amount pairs are held as ambiguous_match ("matched on amount alone — confirm") and the run warns rather than matching on amount alone.
    • Never ship a run flagged ⛔ UNRELIABLE. When almost nothing reconciles although the two sides share many exactly-equal amounts, the engine says so in the headline and leads the warnings with UNRELIABLE RESULT (summary["unreliable"]). At thousands of rows no reviewer can see by eye that virtually every item should have matched, so this is checked mechanically. It nearly always means a misconfigured run, not a broken ledger — re-check the sign convention (--flip-b), the --debit/--credit mapping and --date, then re-run. Do not rationalise a 0%-matched working paper.
    • A second pass over the one-sided residue recovers duplicate, sign_flip and amount_mismatch (e.g. a double-entered cashbook line, a debit booked as a credit, a net-vs-gross GST gap) that key mode gets from the key — so they're classified, not left as bare missing_in_A/B. Conservative and confirm-first (see references/triage-taxonomy.md).

Amounts are parsed as exact Decimal — ties don't break on binary-float dust. Currencies are compared, so 100 USD never matches 100 SGD: name a currency column with --currency <col> (else the code is read from the amount cell's symbol — US$/S$/£/…; a bare $ is treated as ambiguous). A key match whose currencies differ is flagged currency_mismatch; in amount_date mode incompatible currencies simply don't match. For audit/finance work add --strict-currency (strict_currency=True): an unknown currency on either side won't match either — it's routed to currency_unknown rather than assumed compatible (default mode stays permissive). Multi-tab .xlsx? Pass sheet_a= / sheet_b= (CLI --sheet-a/--sheet-b); otherwise ingest auto-picks the single data sheet, or asks you to choose.

Day-to-day bank/GL conveniences (column names resolve case-insensitively, so amount finds a bank CSV's Amount):

  • Separate Debit / Credit columns (the usual bank/GL export layout): --debit <col> --credit <col> — signed amount = debit − credit; a side without those columns just uses its amount column, so a debit/credit file reconciles against a signed-amount file.
  • Opposite sign conventions (bank statement vs the GL cash account): --flip-b negates B's amounts before comparing.
  • Statement completeness: --opening-a/--closing-a (and -b) checks opening + net movement = stated closing — catching a truncated or filtered export before it silently "reconciles". Result is reported in the header of the working paper.
  • Ageing: --as-of <date> ages every one-sided exception with a parsed date (age_days), so stale unbanked receipts / uncleared payments rank visibly.
  • An amount_mismatch whose gap is a common GST/VAT/WHT rate of the smaller side gets an advisory net-vs-gross hint in its probable cause (never a category change).

Messy inputs? Tidy them with data-tidy first, then reconcile the clean outputs.

3. Run it

res, exceptions, summary, _ = reconcile_files(
    "tracker.xlsx", "ledger.xlsx", preset="invoice_tracker_vs_ledger",
    material=1000, escalate=10000)            # materiality thresholds (set to your policy)
write_workpaper(res, exceptions, summary, "reconciliation_[name]_[date].xlsx",
                a_label="Tracker", b_label="Ledger")

Or one-shot from the CLI: python scripts/reconcile.py A B --preset bank_vs_ledger --mode amount_date --out wp.xlsx

3a. (Optional) Aggregation — sum-to-one / sum-to-sum, confirm-first

When one item ties to a sum of items on the other side (a bank receipt = several invoices), or a batch on each side ties total-to-total, run a second pass over what's still unmatched. It is heuristic, so proposals are never auto-accepted — present them, get the user's confirmation, then apply only the confirmed ones. Bound the search by a shared key + date window + counterparty (a subset-size cap applies underneath) to stay accurate.

res, exceptions, summary, proposals = reconcile_files(
    "bank.xlsx", "ledger.xlsx", preset="bank_vs_ledger", aggregate=True,
    group_col="batch", party_col="counterparty", date_window=7)   # the constraints
print(render_proposals(proposals, a_label="Bank", b_label="Cashbook"))   # show the user
# ... the user confirms which to accept, e.g. [0, 2] ...
apply_aggregations(res, proposals, accepted=[0, 2])
exceptions, summary = finalize(res, a_label="Bank", b_label="Cashbook", material=1000)

Confirmed proposals become matched (aggregated); the rest stay as exceptions. Never confirm on the user's behalf — surface the proposed groupings (constituents + how they tie) and let them decide. (CLI: add --aggregate --group-col … --party-col … --date-window N; it prints the proposals for review — --auto-confirm exists for headless/testing only, not normal use.)

4. Read the triage, act on the reds

The working paper leads with the answer (RAG, % reconciled, value matched vs in exception), then the exceptions sorted by materiality (escalate → material → immaterial) within category. Each carries a probable cause and a suggested action — but the action is for a person to take; the skill proposes, it doesn't post.

Discrepancy Triage (the categories)

Full taxonomy + the materiality bands in references/triage-taxonomy.md (the artifact for finance to red-line). In short:

Category Means Typical action
missing_in_B in A, not B (e.g. invoiced-not-booked; approved-not-paid) investigate / suggest posting / chase
missing_in_A in B, not A (e.g. booked-not-tracked; paid-without-approval) investigate / record / query
amount_mismatch matched, amounts differ investigate the difference
rounding differ within the rounding / FX tolerance accept (note)
sign_flip equal magnitude, opposite sign correct the sign/side
duplicate appears more than once on one side confirm + remove
timing_difference matches on amount, dates differ but within the window — in-transit / cut-off monitor — clears next period
ambiguous_match equal amount, dates differ beyond the window — possible match, possibly coincidental confirm same item before reconciling
currency_mismatch matched item, but the currencies differ — amounts not comparable as-is investigate; convert at rate / fix mis-booked currency
currency_unknown amounts tie but a side's currency is unknown (strict mode only) establish the currency both sides, then re-run

Materiality grades each by value: within tolerance → immaterial → material → escalate (thresholds you set per run).

Files

  • scripts/reconcile.py — engine. Pure match() / triage() (work on parsed rows — offline, testable); aggregation: propose_aggregations() (read-only; sum-to-one + sum-to-sum, bounded by key/date/party), apply_aggregations() (applies only confirmed proposals), render_proposals(), finalize() (triage + summarise after confirmation); reconcile_files() (reads A and B in any format via the shared ingest/dataclean engine), summarise(), render_report(), write_workpaper() (.xlsx — Summary/Exceptions/Matched/Aggregations), redact() (mask parties/amounts before any artefact leaves entitled use), catalogue_md(), PRESETS. --self-test runs offline; --catalogue lists the presets.
  • references/triage-taxonomy.md — the discrepancy taxonomy, materiality bands and presets — read this; it's the artifact for finance to confirm against your own policy.
  • examples/ — a worked invoice-tracker vs ledger pair (fictional) + the reconciliation output.

Boundaries

  • Reconciliation only — records ↔ records. Not budget/variance (no headroom or over-budget analysis), and not deal/asset analysis (development monitoring, capex returns → out of scope). It won't depend on another toolkit.
  • Drafts, not advice; never invents. An unmatched or ambiguous item is flagged, never guessed into a match. The output is a first-pass working paper for a qualified person.

Principles

Behavioural charter: ../../PRINCIPLES.md — drafts not advice, never invent, honesty and calibration, plain speech, action boundary.

Data handling

Matching and triage are computed on your machine — A, B and the working paper stay in your synced or shared file store and nothing is sent to any external tool by the engine. (The AI agent driving the skill does send whatever it reads into its context to your AI provider.) Reconciliations routinely touch bank details and sensitive account data (esp. the fund-administrator preset) — keep it local, and redact() parties/amounts before any reconciliation artefact leaves its entitled use. Full rule: ../../PRINCIPLES.md#data-handling--pii-policy.

Feedback

Found a bug or an improvement (a new preset, a triage category)? Capture it with the toolkit's shared format — ../../CONTRIBUTING.md#skill-feedback-format — and save as feedback_data-reconcile_[date].txt.

Requirements & mode

Pre-screen: ../../README.md#mode--environment-compatibility + python ../../scripts/envcheck.py. Python + openpyxl (for the .xlsx working paper and to read .xlsx sources); reads CSV/PDF/.docx/.msg via the shared engine (same optional deps as data-tidy). Portable — the code needs no network, no MS Office, no credentials, no connector.