mirror of
https://github.com/renee-png/acmcc.git
synced 2026-06-21 09:50:01 +00:00
Conform reports to financial spec: A/R open-balance + reconciliation checks
Per the Financial Reports Master Spec: - §1.5 A/R fix: invoice settlements now post to the GL (Dr Undeposited / Cr A/R from invoice.paid_amount); payments are the cash sub-ledger only and no longer separately credit A/R (avoids double-count). A/R control = open balance, so recon R7 passes for managed companies (Ashley Manor 39,248 -> 0). Bills already settled (R8 ok). Migration applied + backfilled managed companies. - §9/§10: add a "Reconciliation Checks" report that surfaces R1/R2/R7/R8 residuals (never plugged) so imbalances are visible — e.g. Bridgewater's imported GL is unbalanced (R1) and its sub-ledgers don't tie (R7/R8). Imported companies (Bridgewater/Bent Oak) left untouched per decision; their residuals now surface in the Reconciliation report. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -34,7 +34,7 @@ type ReportId =
|
||||
| "pnl" | "balance-sheet" | "cash-flow" | "movement-of-equity" | "budget-vs-actuals"
|
||||
| "trial-balance" | "general-ledger"
|
||||
| "invoice-summary" | "customer-balances" | "ar-aging" | "homeowner-summary" | "delinquency"
|
||||
| "expense-summary" | "vendor-balances" | "ap-aging";
|
||||
| "expense-summary" | "vendor-balances" | "ap-aging" | "reconciliation";
|
||||
|
||||
const APP_NAME = "Cozy Books";
|
||||
const FINANCIAL: ReportId[] = ["pnl", "balance-sheet", "cash-flow", "movement-of-equity"];
|
||||
@@ -61,6 +61,9 @@ const GROUPS = [
|
||||
{ id: "expense-summary" as ReportId, name: "Expense Summary" },
|
||||
{ id: "vendor-balances" as ReportId, name: "Vendor Balance Summary" },
|
||||
]},
|
||||
{ name: "Audit", reports: [
|
||||
{ id: "reconciliation" as ReportId, name: "Reconciliation Checks" },
|
||||
]},
|
||||
];
|
||||
|
||||
const TZ_ET = "America/New_York";
|
||||
@@ -525,7 +528,10 @@ export default function AccountingReportsPage() {
|
||||
{active === "general-ledger" && (
|
||||
<GeneralLedgerReport companyId={cid} companyName={associationName ?? ""} />
|
||||
)}
|
||||
{!isFinancial && active !== "budget-vs-actuals" && active !== "trial-balance" && active !== "general-ledger" && (
|
||||
{active === "reconciliation" && (
|
||||
<ReconciliationReport d={data} currency={cur} />
|
||||
)}
|
||||
{!isFinancial && active !== "budget-vs-actuals" && active !== "trial-balance" && active !== "general-ledger" && active !== "reconciliation" && (
|
||||
<Card>
|
||||
<CardContent>
|
||||
{!data ? (
|
||||
@@ -726,6 +732,77 @@ 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>;
|
||||
const acctById = new Map((d.accounts ?? []).map((a: any) => [a.id, a]));
|
||||
let dr = 0, cr = 0, assets = 0, liab = 0, equity = 0, income = 0, expense = 0, arControl = 0, apControl = 0;
|
||||
for (const l of (d.glCumulative ?? []) as any[]) {
|
||||
const debit = Number(l.debit || 0), credit = Number(l.credit || 0);
|
||||
dr += debit; cr += credit;
|
||||
const a: any = acctById.get(l.account_id) || {};
|
||||
const t = l.accounts?.type || a.type;
|
||||
const name = String(a.name || "").toLowerCase();
|
||||
if (t === "asset") { assets += debit - credit; if (name.includes("receivable")) arControl += debit - credit; }
|
||||
else if (t === "liability") { liab += credit - debit; if (name.includes("payable")) apControl += credit - debit; }
|
||||
else if (t === "equity") equity += credit - debit;
|
||||
else if (t === "income") income += credit - debit;
|
||||
else if (t === "expense") expense += debit - credit;
|
||||
}
|
||||
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);
|
||||
const netIncome = income - expense;
|
||||
|
||||
const checks = [
|
||||
{ id: "R1", label: "Trial Balance — total debits = total credits", residual: dr - cr },
|
||||
{ id: "R2", label: "Balance Sheet — Assets = Liabilities + Equity (incl. net income)", residual: assets - (liab + equity + netIncome) },
|
||||
{ id: "R7", label: "A/R = open invoice balances (§1.5, gross vs open)", residual: arControl - openInv },
|
||||
{ id: "R8", label: "A/P = open bill balances (§1.5)", residual: apControl - openBill },
|
||||
];
|
||||
const ok = (r: number) => Math.abs(r) < 0.005;
|
||||
const allPass = checks.every((c) => ok(c.residual));
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base flex items-center gap-2">
|
||||
Reconciliation Checks
|
||||
<span className={`text-xs rounded px-2 py-0.5 ${allPass ? "bg-emerald-100 text-emerald-700" : "bg-red-100 text-red-700"}`}>
|
||||
{allPass ? "All passing" : "Residuals present"}
|
||||
</span>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="p-0">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-16">Check</TableHead>
|
||||
<TableHead>Assertion</TableHead>
|
||||
<TableHead className="text-right">Residual</TableHead>
|
||||
<TableHead className="text-right">Status</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{checks.map((c) => (
|
||||
<TableRow key={c.id}>
|
||||
<TableCell className="font-mono">{c.id}</TableCell>
|
||||
<TableCell>{c.label}</TableCell>
|
||||
<TableCell className={`text-right tabular-nums ${ok(c.residual) ? "" : "text-red-600 font-semibold"}`}>{money(c.residual, currency)}</TableCell>
|
||||
<TableCell className="text-right">{ok(c.residual) ? "✓" : "✗"}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
<p className="text-xs text-muted-foreground p-4">
|
||||
A non-zero residual is a bug signal (§9), not to be plugged. R1/R2 failing means the ledger is
|
||||
unbalanced (often an imported single-sided entry). R7/R8 failing means A/R or A/P is summing gross
|
||||
billings instead of open balances, or a sub-ledger doesn't tie to the GL control account.
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
function buildFinancial(id: ReportId, d: any, p: any | undefined, useCompare: boolean): StructuredReport {
|
||||
if (id === "pnl") return buildPnL(d, p, useCompare);
|
||||
if (id === "balance-sheet") return buildBalanceSheet(d);
|
||||
|
||||
Reference in New Issue
Block a user