Reconciliation Checks: enable PDF/CSV download

Extracted the check computation into buildReconChecks() and exposed it
to the report exporter (exportFlat), so the Reconciliation Checks report
now downloads as a branded PDF/CSV (Check · Residual · Status) like the
other reports.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-19 00:01:39 -04:00
parent ad74072061
commit 4ffe17cd7f
+20 -14
View File
@@ -24,7 +24,7 @@ import {
renderReportPdf, fmtAmount,
type StructuredReport, type StructuredRow,
} from "./lib/reportPdf";
import { reconcile, type RecAccount, type RecLine } from "./lib/reconcile";
import { reconcile, type RecAccount, type RecLine, type RecCheck } from "./lib/reconcile";
import {
computePnL, computeMargins, toMinor, fromMinor, PnlValidationError,
type PnlAccount, type PnlClassification, type Posting as PnlPosting, type PnlResult,
@@ -397,6 +397,17 @@ export default function AccountingReportsPage({ association }: { association?: {
const m = (n: number) => money(n, cur);
const now = new Date();
if (active === "reconciliation") {
if (!data) return null;
const checks = buildReconChecks(data);
return {
title: "Reconciliation Checks",
columns: ["Check", "Residual", "Status"],
rows: checks.map((c) => [`${c.id} ${c.label}`, m(c.residual), c.pass ? "Pass" : "FAIL"]),
boldRows: [],
};
}
if (active === "ar-aging" || active === "customer-balances") {
type AR = { name: string; current: number; d30: number; d60: number; d90: number; d90p: number; total: number };
const byC = new Map<string, AR>();
@@ -1251,18 +1262,12 @@ function PreviewSheet({ report, companyName, rangeLabel, showCodes, showCompare,
// ---------- Financial report builders (structured) ----------
// Reconciliation matrix (§9) surfaced as visible residuals — never plug a residual.
function ReconciliationReport({ d, currency }: { d: any; currency: string }) {
if (!d) return <div className="text-sm text-muted-foreground">Loading</div>;
// Shared so both the on-screen report and the PDF/CSV export run identical checks.
function buildReconChecks(d: any): RecCheck[] {
const accounts: RecAccount[] = ((d.accounts ?? []) as any[]).map((a) => ({
id: a.id, type: a.type, name: a.name,
is_cash: !!a.is_bank || /cash|undeposited/i.test(String(a.name || "")),
}));
// Archived accounts are excluded from d.accounts, but their GL lines remain in
// glCumulative. Without their type, reconcile() drops those lines (if (!a) continue)
// and the Balance Sheet check (R2) goes out of balance by exactly the dropped
// amount — e.g. an archived income account that still holds a balance. Add any
// GL-referenced account missing from the active list, using the joined meta, so
// reconcile classifies them just like the Balance Sheet builder does.
const knownAcctIds = new Set(accounts.map((a) => a.id));
for (const l of (d.glCumulative ?? []) as any[]) {
const meta = l.accounts; const id = l.account_id;
@@ -1276,21 +1281,22 @@ function ReconciliationReport({ d, currency }: { d: any; currency: string }) {
}));
const openInv = ((d.allInvoices ?? []) as any[]).filter((i) => i.status !== "void").reduce((s, i) => s + (Number(i.total || 0) - Number(i.paid_amount || 0)), 0);
const openBill = ((d.allBills ?? []) as any[]).filter((b) => b.status !== "void").reduce((s, b) => s + (Number(b.total || 0) - Number(b.paid_amount || 0)), 0);
// Cross-path figures from the report builders so R3/R5 verify the builders agree
// with the raw GL. (P&L net income vs GL; Movement-of-Equity ending vs Balance Sheet.)
const pl = buildPnL(d, undefined, false);
const plNI = pl.rows.find((r) => r.kind === "grand" && /net income/i.test(r.label))?.amount;
const bs = buildBalanceSheet(d);
const bsEquity = bs.rows.find((r) => r.kind === "total" && /total equity/i.test(r.label))?.amount;
const sce = buildMovementOfEquity(d, undefined, false);
const sceEnding = sce.rows.find((r) => r.kind === "grand" && /closing equity/i.test(r.label))?.amount;
const checks = reconcile({
return reconcile({
accounts, lines, periodStart: d.from, openInvoices: openInv, openBills: openBill,
arApApplicable: d.glManaged,
reportPLNetIncome: plNI, sceEndingEquity: sceEnding, bsTotalEquity: bsEquity,
});
}
function ReconciliationReport({ d, currency }: { d: any; currency: string }) {
if (!d) return <div className="text-sm text-muted-foreground">Loading</div>;
const checks = buildReconChecks(d);
const ok = (r: number) => Math.abs(r) < 0.005;
const allPass = checks.every((c) => c.pass);