mirror of
https://github.com/renee-png/acmcc.git
synced 2026-06-21 01:40:01 +00:00
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:
@@ -24,7 +24,7 @@ import {
|
|||||||
renderReportPdf, fmtAmount,
|
renderReportPdf, fmtAmount,
|
||||||
type StructuredReport, type StructuredRow,
|
type StructuredReport, type StructuredRow,
|
||||||
} from "./lib/reportPdf";
|
} from "./lib/reportPdf";
|
||||||
import { reconcile, type RecAccount, type RecLine } from "./lib/reconcile";
|
import { reconcile, type RecAccount, type RecLine, type RecCheck } from "./lib/reconcile";
|
||||||
import {
|
import {
|
||||||
computePnL, computeMargins, toMinor, fromMinor, PnlValidationError,
|
computePnL, computeMargins, toMinor, fromMinor, PnlValidationError,
|
||||||
type PnlAccount, type PnlClassification, type Posting as PnlPosting, type PnlResult,
|
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 m = (n: number) => money(n, cur);
|
||||||
const now = new Date();
|
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") {
|
if (active === "ar-aging" || active === "customer-balances") {
|
||||||
type AR = { name: string; current: number; d30: number; d60: number; d90: number; d90p: number; total: number };
|
type AR = { name: string; current: number; d30: number; d60: number; d90: number; d90p: number; total: number };
|
||||||
const byC = new Map<string, AR>();
|
const byC = new Map<string, AR>();
|
||||||
@@ -1251,18 +1262,12 @@ function PreviewSheet({ report, companyName, rangeLabel, showCodes, showCompare,
|
|||||||
// ---------- Financial report builders (structured) ----------
|
// ---------- Financial report builders (structured) ----------
|
||||||
|
|
||||||
// Reconciliation matrix (§9) surfaced as visible residuals — never plug a residual.
|
// Reconciliation matrix (§9) surfaced as visible residuals — never plug a residual.
|
||||||
function ReconciliationReport({ d, currency }: { d: any; currency: string }) {
|
// Shared so both the on-screen report and the PDF/CSV export run identical checks.
|
||||||
if (!d) return <div className="text-sm text-muted-foreground">Loading…</div>;
|
function buildReconChecks(d: any): RecCheck[] {
|
||||||
const accounts: RecAccount[] = ((d.accounts ?? []) as any[]).map((a) => ({
|
const accounts: RecAccount[] = ((d.accounts ?? []) as any[]).map((a) => ({
|
||||||
id: a.id, type: a.type, name: a.name,
|
id: a.id, type: a.type, name: a.name,
|
||||||
is_cash: !!a.is_bank || /cash|undeposited/i.test(String(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));
|
const knownAcctIds = new Set(accounts.map((a) => a.id));
|
||||||
for (const l of (d.glCumulative ?? []) as any[]) {
|
for (const l of (d.glCumulative ?? []) as any[]) {
|
||||||
const meta = l.accounts; const id = l.account_id;
|
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 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 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 pl = buildPnL(d, undefined, false);
|
||||||
const plNI = pl.rows.find((r) => r.kind === "grand" && /net income/i.test(r.label))?.amount;
|
const plNI = pl.rows.find((r) => r.kind === "grand" && /net income/i.test(r.label))?.amount;
|
||||||
const bs = buildBalanceSheet(d);
|
const bs = buildBalanceSheet(d);
|
||||||
const bsEquity = bs.rows.find((r) => r.kind === "total" && /total equity/i.test(r.label))?.amount;
|
const bsEquity = bs.rows.find((r) => r.kind === "total" && /total equity/i.test(r.label))?.amount;
|
||||||
const sce = buildMovementOfEquity(d, undefined, false);
|
const sce = buildMovementOfEquity(d, undefined, false);
|
||||||
const sceEnding = sce.rows.find((r) => r.kind === "grand" && /closing equity/i.test(r.label))?.amount;
|
const sceEnding = sce.rows.find((r) => r.kind === "grand" && /closing equity/i.test(r.label))?.amount;
|
||||||
|
return reconcile({
|
||||||
const checks = reconcile({
|
|
||||||
accounts, lines, periodStart: d.from, openInvoices: openInv, openBills: openBill,
|
accounts, lines, periodStart: d.from, openInvoices: openInv, openBills: openBill,
|
||||||
arApApplicable: d.glManaged,
|
arApApplicable: d.glManaged,
|
||||||
reportPLNetIncome: plNI, sceEndingEquity: sceEnding, bsTotalEquity: bsEquity,
|
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 ok = (r: number) => Math.abs(r) < 0.005;
|
||||||
const allPass = checks.every((c) => c.pass);
|
const allPass = checks.every((c) => c.pass);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user