diff --git a/src/pages/accounting/AccountingReportsPage.tsx b/src/pages/accounting/AccountingReportsPage.tsx index 17c719a..095e535 100644 --- a/src/pages/accounting/AccountingReportsPage.tsx +++ b/src/pages/accounting/AccountingReportsPage.tsx @@ -125,7 +125,7 @@ async function fetchAllGLLines(cid: string, to: string, select: string, from?: s // 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); - const [inv, bills, accs, exp, custs, vends, ob, ytdInv, ytdExp, ytdBills, allBills, glRes, glCumRes, allInvRes, companyRes] = await Promise.all([ + const [inv, bills, accs, exp, custs, vends, ob, ytdInv, ytdExp, ytdBills, allBills, glRes, glCumRes, allInvRes, companyRes, periodBillItemsRes] = await Promise.all([ accounting.from("invoices").select("number,total,paid_amount,status,issue_date,customers(name)").eq("company_id", cid).gte("issue_date", from).lte("issue_date", to), accounting.from("bills").select("number,total,paid_amount,status,issue_date,due_date,vendors(name)").eq("company_id", cid).gte("issue_date", from).lte("issue_date", to), accounting.from("accounts").select("id,name,code,type,subtype,balance,is_bank").eq("company_id", cid).eq("is_archived", false), @@ -149,6 +149,9 @@ export async function fetchReportData(cid: string, from: string, to: string) { // Imported-GL companies (gl_auto_post=false) keep their own AR/AP, so the sub-ledger // vs GL control reconciliation (R7/R8) does not apply to them. accounting.from("companies").select("gl_auto_post").eq("id", cid).maybeSingle(), + // Bill items for bills issued in the period — lets the Expense Summary + // back out the expense of bills that aren't paid yet (cash-aware view). + accounting.from("bill_items").select("account_id,amount,bills!inner(total,paid_amount,status,issue_date,company_id)").eq("bills.company_id", cid).gte("bills.issue_date", from).lte("bills.issue_date", to).not("account_id", "is", null), ]); return { invoices: inv.data ?? [], bills: bills.data ?? [], accounts: accs.data ?? [], @@ -160,6 +163,7 @@ export async function fetchReportData(cid: string, from: string, to: string) { glCumulative: glCumRes ?? [], allInvoices: allInvRes.data ?? [], glManaged: companyRes.data ? companyRes.data.gl_auto_post !== false : true, + periodBillItems: periodBillItemsRes.data ?? [], from, asOf: to, }; } @@ -1795,10 +1799,12 @@ function buildFlat(id: ReportId, d: any, cur: string): Flat | null { rows: d.customers.map((c: any) => [c.name, m(Number(c.balance ?? 0))]), }; case "expense-summary": { - // GL-driven so it follows the same recognition rule as the P&L: a bill's - // expense counts on the bill date (Dr Expense / Cr A/P), and a vendor payment - // with no bill counts on the payment date (Dr Expense / Cr Bank). Reading the - // ledger avoids double-counting and never misses direct payments. + // GL-driven, billed-date recognition (a bill's expense counts on the bill + // date — Dr Expense / Cr A/P — and a direct vendor payment on the payment + // date — Dr Expense / Cr Bank), then we back out the expense of bills issued + // in the period that aren't paid yet, so the summary reflects amounts the + // association has actually paid. Direct payments are cash already, so they + // stay; only the unpaid portion of open bills is removed. const byAcct: Record = {}; for (const l of (d.glLines ?? []) as any[]) { const acc = l.accounts; @@ -1808,9 +1814,23 @@ function buildFlat(id: ReportId, d: any, cur: string): Flat | null { const name = acc.name ?? "Expense"; byAcct[name] = (byAcct[name] ?? 0) + amt; } - const rows = Object.entries(byAcct).sort((a, b) => b[1] - a[1]).map(([acct, amt]) => [acct, m(amt)]); - const total = Object.values(byAcct).reduce((s, v) => s + v, 0); - return { title: "Expense Summary (Accrual)", columns: ["Expense Account", "Amount"], rows: [...rows, ["TOTAL", m(total)]], boldRows: [rows.length] }; + // Remove the unpaid portion of period bills, prorated, keyed by account name. + const acctNameById = new Map((d.accounts ?? []).map((a: any) => [a.id, a.name])); + for (const bi of (d.periodBillItems ?? []) as any[]) { + const b = bi.bills; + if (!b || b.status === "void") continue; + const total = Number(b.total) || 0; + if (total <= 0) continue; + const unpaid = total - (Number(b.paid_amount) || 0); + if (unpaid <= 0) continue; + const name = acctNameById.get(bi.account_id); + if (!name || byAcct[name] === undefined) continue; + byAcct[name] -= Number(bi.amount) * (unpaid / total); + } + const kept = Object.entries(byAcct).filter(([, amt]) => Math.abs(amt) >= 0.005); + const rows = kept.sort((a, b) => b[1] - a[1]).map(([acct, amt]) => [acct, m(amt)]); + const total = kept.reduce((s, [, v]) => s + v, 0); + return { title: "Expense Summary (Paid)", columns: ["Expense Account", "Amount"], rows: [...rows, ["TOTAL", m(total)]], boldRows: [rows.length] }; } case "vendor-balances": { const byVendor: Record = {};