mirror of
https://github.com/renee-png/acmcc.git
synced 2026-06-21 01:40:01 +00:00
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 <noreply@anthropic.com>
This commit is contained in:
@@ -125,6 +125,16 @@ async function fetchAllGLLines(cid: string, to: string, select: string, from?: s
|
|||||||
return out;
|
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).
|
// Shared fetch for the financial reports (also used by the report-batch engine).
|
||||||
export async function fetchReportData(cid: string, from: string, to: string) {
|
export async function fetchReportData(cid: string, from: string, to: string) {
|
||||||
const ytdStart = new Date(new Date().getFullYear(), 0, 1).toISOString().slice(0,10);
|
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
|
const { data } = await accounting
|
||||||
.from("invoices")
|
.from("invoices")
|
||||||
.select("id,customer_id,total,paid_amount,due_date,issue_date,status,number,customers(id,name)")
|
.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 ?? [];
|
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 };
|
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>();
|
||||||
for (const inv of arOpen as any[]) {
|
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;
|
if (open <= 0) continue;
|
||||||
const cid2 = inv.customer_id; if (!cid2) continue;
|
const cid2 = inv.customer_id; if (!cid2) continue;
|
||||||
const name = inv.customers?.name ?? "—";
|
const name = inv.customers?.name ?? "—";
|
||||||
@@ -454,7 +465,7 @@ export default function AccountingReportsPage({ association }: { association?: {
|
|||||||
const customers = (data as any)?.customers ?? [];
|
const customers = (data as any)?.customers ?? [];
|
||||||
const openByC = new Map<string, { open: number; count: number }>();
|
const openByC = new Map<string, { open: number; count: number }>();
|
||||||
for (const inv of arOpen as any[]) {
|
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); }
|
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[])
|
const rows = (customers as any[])
|
||||||
@@ -467,7 +478,7 @@ export default function AccountingReportsPage({ association }: { association?: {
|
|||||||
const customers = (data as any)?.customers ?? [];
|
const customers = (data as any)?.customers ?? [];
|
||||||
const overdue = new Map<string, { name: string; property: string; email: string; phone: string; amount: number; oldest: number }>();
|
const overdue = new Map<string, { name: string; property: string; email: string; phone: string; amount: number; oldest: number }>();
|
||||||
for (const inv of arOpen as any[]) {
|
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;
|
if (open <= 0) continue;
|
||||||
const due = new Date(inv.due_date ?? inv.issue_date);
|
const due = new Date(inv.due_date ?? inv.issue_date);
|
||||||
const days = Math.floor((now.getTime() - due.getTime()) / 86400000);
|
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 ?? ""),
|
account_id: l.account_id, date: String(l.journal_entries?.date ?? ""),
|
||||||
debit: Number(l.debit || 0), credit: Number(l.credit || 0),
|
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 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 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;
|
||||||
@@ -1887,7 +1898,7 @@ function buildFlat(id: ReportId, d: any, cur: string): Flat | null {
|
|||||||
case "invoice-summary":
|
case "invoice-summary":
|
||||||
return {
|
return {
|
||||||
title: "Invoice Summary", columns: ["Invoice #", "Customer", "Date", "Status", "Amount"],
|
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":
|
case "customer-balances":
|
||||||
return {
|
return {
|
||||||
@@ -2427,7 +2438,7 @@ function HomeownerSummaryTable({ customers, invoices, currency }: { customers: a
|
|||||||
for (const inv of invoices) {
|
for (const inv of invoices) {
|
||||||
const cid = inv.customer_id;
|
const cid = inv.customer_id;
|
||||||
if (!cid) continue;
|
if (!cid) continue;
|
||||||
const open = Number(inv.total ?? 0) - Number(inv.paid_amount ?? 0);
|
const open = invoiceOpen(inv);
|
||||||
const isPaid = inv.status === "paid";
|
const isPaid = inv.status === "paid";
|
||||||
const cur = m.get(cid) ?? { open: 0, count: 0, lastPaid: null };
|
const cur = m.get(cid) ?? { open: 0, count: 0, lastPaid: null };
|
||||||
if (open > 0) { cur.open += open; cur.count++; }
|
if (open > 0) { cur.open += open; cur.count++; }
|
||||||
@@ -2486,7 +2497,7 @@ function DelinquencyTable({ customers, invoices, currency }: { customers: any[];
|
|||||||
const byCustomer = new Map<string, DelRow>();
|
const byCustomer = new Map<string, DelRow>();
|
||||||
|
|
||||||
for (const inv of invoices) {
|
for (const inv of invoices) {
|
||||||
const open = Number(inv.total ?? 0) - Number(inv.paid_amount ?? 0);
|
const open = invoiceOpen(inv);
|
||||||
if (open <= 0) continue;
|
if (open <= 0) continue;
|
||||||
const due = inv.due_date ? new Date(inv.due_date) : new Date(inv.issue_date ?? Date.now());
|
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);
|
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 };
|
type AgingRow = { id: string; name: string; current: number; d30: number; d60: number; d90: number; d90p: number; total: number };
|
||||||
const byCustomer = new Map<string, AgingRow>();
|
const byCustomer = new Map<string, AgingRow>();
|
||||||
for (const inv of rows) {
|
for (const inv of rows) {
|
||||||
const open = Number(inv.total ?? 0) - Number(inv.paid_amount ?? 0);
|
const open = invoiceOpen(inv);
|
||||||
if (open <= 0) continue;
|
if (open <= 0) continue;
|
||||||
const cid = inv.customer_id ?? inv.customers?.id;
|
const cid = inv.customer_id ?? inv.customers?.id;
|
||||||
if (!cid) continue;
|
if (!cid) continue;
|
||||||
|
|||||||
Reference in New Issue
Block a user