From 7b1d6a59e980747fe3f3b62594ced76b9c4aa1e2 Mon Sep 17 00:00:00 2001 From: renee-png Date: Fri, 19 Jun 2026 11:45:18 -0400 Subject: [PATCH] A/R reports: exclude void/paid/draft invoices from open balances The AR Aging, Invoice Summary by Customer, Homeowner Summary, and Delinquency reports computed open A/R as total - paid_amount across all invoices, which counted (1) voided invoices and (2) ledger-synced "paid" invoices whose paid_amount was left at 0. Both showed huge phantom past-due balances even when everything was paid. Add a shared invoiceOpen() helper that treats void/paid/draft invoices as zero open (status is the source of truth, since paid_amount is unreliable on synced invoices) and apply it to all A/R computation sites plus the reconciliation control. Also drop voided invoices from the flat Invoice Summary list. Co-Authored-By: Claude Opus 4.8 --- .../accounting/AccountingReportsPage.tsx | 29 +++++++++++++------ 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/src/pages/accounting/AccountingReportsPage.tsx b/src/pages/accounting/AccountingReportsPage.tsx index dc9c79b..f8fc7d3 100644 --- a/src/pages/accounting/AccountingReportsPage.tsx +++ b/src/pages/accounting/AccountingReportsPage.tsx @@ -125,6 +125,16 @@ async function fetchAllGLLines(cid: string, to: string, select: string, from?: s return out; } +// An invoice contributes to A/R only while it is an open receivable. `paid_amount` +// is unreliable on ledger-synced invoices (the owner_ledger→AR sync often leaves it +// at 0 even when status is "paid"), so the *status* is the source of truth here: +// void / paid / draft invoices never count as open, regardless of paid_amount. +const AR_CLOSED_STATUSES = new Set(["void", "paid", "draft"]); +export function invoiceOpen(inv: any): number { + if (AR_CLOSED_STATUSES.has(String(inv?.status))) return 0; + return Number(inv?.total ?? 0) - Number(inv?.paid_amount ?? 0); +} + // Shared fetch for the financial reports (also used by the report-batch engine). export async function fetchReportData(cid: string, from: string, to: string) { const ytdStart = new Date(new Date().getFullYear(), 0, 1).toISOString().slice(0,10); @@ -373,7 +383,8 @@ export default function AccountingReportsPage({ association }: { association?: { const { data } = await accounting .from("invoices") .select("id,customer_id,total,paid_amount,due_date,issue_date,status,number,customers(id,name)") - .eq("company_id", cid); + .eq("company_id", cid) + .neq("status", "void"); return data ?? []; }, }); @@ -412,7 +423,7 @@ export default function AccountingReportsPage({ association }: { association?: { type AR = { name: string; current: number; d30: number; d60: number; d90: number; d90p: number; total: number }; const byC = new Map(); for (const inv of arOpen as any[]) { - const open = Number(inv.total ?? 0) - Number(inv.paid_amount ?? 0); + const open = invoiceOpen(inv); if (open <= 0) continue; const cid2 = inv.customer_id; if (!cid2) continue; const name = inv.customers?.name ?? "—"; @@ -454,7 +465,7 @@ export default function AccountingReportsPage({ association }: { association?: { const customers = (data as any)?.customers ?? []; const openByC = new Map(); for (const inv of arOpen as any[]) { - const open = Number(inv.total ?? 0) - Number(inv.paid_amount ?? 0); + const open = invoiceOpen(inv); if (open > 0) { const e = openByC.get(inv.customer_id) ?? { open: 0, count: 0 }; e.open += open; e.count++; openByC.set(inv.customer_id, e); } } const rows = (customers as any[]) @@ -467,7 +478,7 @@ export default function AccountingReportsPage({ association }: { association?: { const customers = (data as any)?.customers ?? []; const overdue = new Map(); for (const inv of arOpen as any[]) { - const open = Number(inv.total ?? 0) - Number(inv.paid_amount ?? 0); + const open = invoiceOpen(inv); if (open <= 0) continue; const due = new Date(inv.due_date ?? inv.issue_date); const days = Math.floor((now.getTime() - due.getTime()) / 86400000); @@ -1296,7 +1307,7 @@ function buildReconChecks(d: any): RecCheck[] { account_id: l.account_id, date: String(l.journal_entries?.date ?? ""), debit: Number(l.debit || 0), credit: Number(l.credit || 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 openInv = ((d.allInvoices ?? []) as any[]).reduce((s, i) => s + invoiceOpen(i), 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 pl = buildPnL(d, undefined, false); const plNI = pl.rows.find((r) => r.kind === "grand" && /net income/i.test(r.label))?.amount; @@ -1887,7 +1898,7 @@ function buildFlat(id: ReportId, d: any, cur: string): Flat | null { case "invoice-summary": return { title: "Invoice Summary", columns: ["Invoice #", "Customer", "Date", "Status", "Amount"], - rows: d.invoices.map((i: any) => [i.number, i.customers?.name ?? "—", fmtDate(i.issue_date), i.status, m(Number(i.total))]), + rows: d.invoices.filter((i: any) => i.status !== "void").map((i: any) => [i.number, i.customers?.name ?? "—", fmtDate(i.issue_date), i.status, m(Number(i.total))]), }; case "customer-balances": return { @@ -2427,7 +2438,7 @@ function HomeownerSummaryTable({ customers, invoices, currency }: { customers: a for (const inv of invoices) { const cid = inv.customer_id; if (!cid) continue; - const open = Number(inv.total ?? 0) - Number(inv.paid_amount ?? 0); + const open = invoiceOpen(inv); const isPaid = inv.status === "paid"; const cur = m.get(cid) ?? { open: 0, count: 0, lastPaid: null }; if (open > 0) { cur.open += open; cur.count++; } @@ -2486,7 +2497,7 @@ function DelinquencyTable({ customers, invoices, currency }: { customers: any[]; const byCustomer = new Map(); for (const inv of invoices) { - const open = Number(inv.total ?? 0) - Number(inv.paid_amount ?? 0); + const open = invoiceOpen(inv); if (open <= 0) continue; const due = inv.due_date ? new Date(inv.due_date) : new Date(inv.issue_date ?? Date.now()); const days = Math.floor((now.getTime() - due.getTime()) / 86400000); @@ -2574,7 +2585,7 @@ function ARAgingTable({ rows, currency, detailed = false }: { rows: any[]; curre type AgingRow = { id: string; name: string; current: number; d30: number; d60: number; d90: number; d90p: number; total: number }; const byCustomer = new Map(); for (const inv of rows) { - const open = Number(inv.total ?? 0) - Number(inv.paid_amount ?? 0); + const open = invoiceOpen(inv); if (open <= 0) continue; const cid = inv.customer_id ?? inv.customers?.id; if (!cid) continue;