diff --git a/src/pages/accounting/AccountingReportsPage.tsx b/src/pages/accounting/AccountingReportsPage.tsx index 5ac1048..1992a62 100644 --- a/src/pages/accounting/AccountingReportsPage.tsx +++ b/src/pages/accounting/AccountingReportsPage.tsx @@ -1139,80 +1139,82 @@ function buildBalanceSheet(d: any): StructuredReport { // Indirect-method cash flow built from the GL (§5). It ties to the change in the // Balance Sheet cash accounts by construction (R4): because every entry balances, // the cash impact of net income + all non-cash balance movements equals ΔCash. -function buildCashFlow(d: any, _p: any | undefined, _useCompare: boolean): StructuredReport { +type CashFlowCalc = { + netIncome: number; operating: { label: string; amount: number }[]; + cfo: number; cfi: number; cff: number; netChange: number; + beginCash: number; endCash: number; residual: number; +}; + +function computeCashFlow(d: any): CashFlowCalc { const from: string = d.from; const acctById = new Map((d.accounts ?? []).map((a: any) => [a.id, a])); const isCash = (a: any) => !!a && (a.is_bank || /cash|undeposited/i.test(String(a.name || ""))); - - // Beginning (date < from) and ending (<= asOf) raw balances (debit − credit). const beginRaw = new Map(); const endRaw = new Map(); for (const l of (d.glCumulative ?? []) as any[]) { const raw = Number(l.debit || 0) - Number(l.credit || 0); endRaw.set(l.account_id, (endRaw.get(l.account_id) ?? 0) + raw); - if (String(l.journal_entries?.date ?? "") < from) { - beginRaw.set(l.account_id, (beginRaw.get(l.account_id) ?? 0) + raw); - } + if (String(l.journal_entries?.date ?? "") < from) beginRaw.set(l.account_id, (beginRaw.get(l.account_id) ?? 0) + raw); } const ids = new Set([...endRaw.keys(), ...beginRaw.keys()]); const deltaRaw = (id: string) => (endRaw.get(id) ?? 0) - (beginRaw.get(id) ?? 0); - let beginCash = 0, endCash = 0, revenue = 0, expense = 0; + let beginCash = 0, endCash = 0, revenue = 0, expense = 0, cfi = 0, cff = 0; const operating: { label: string; amount: number }[] = []; - let cfi = 0, cff = 0; - for (const id of ids) { const a = acctById.get(id); if (!a) continue; if (isCash(a)) { beginCash += beginRaw.get(id) ?? 0; endCash += endRaw.get(id) ?? 0; continue; } - if (a.type === "income") { revenue += -deltaRaw(id); continue; } // natural = −raw - if (a.type === "expense") { expense += deltaRaw(id); continue; } // natural = raw - - // Non-cash balance-sheet account: cash impact of its movement = −Δraw. + if (a.type === "income") { revenue += -deltaRaw(id); continue; } + if (a.type === "expense") { expense += deltaRaw(id); continue; } const impact = -deltaRaw(id); if (Math.abs(impact) < 0.005) continue; const name = String(a.name || "").toLowerCase(); if (a.type === "asset") { - const naturalUp = deltaRaw(id) > 0; // asset natural = raw + const naturalUp = deltaRaw(id) > 0; if (/investment|property|equipment|fixed|capital asset/.test(name)) cfi += impact; else operating.push({ label: `${naturalUp ? "Increase" : "Decrease"} in ${a.name}`, amount: impact }); } else if (a.type === "liability") { - const naturalUp = -deltaRaw(id) > 0; // liability natural = −raw + const naturalUp = -deltaRaw(id) > 0; if (/loan|note|mortgage|debt|bond/.test(name)) cff += impact; else operating.push({ label: `${naturalUp ? "Increase" : "Decrease"} in ${a.name}`, amount: impact }); - } else if (a.type === "equity") { - cff += impact; // contributions / distributions / opening equity - } + } else if (a.type === "equity") { cff += impact; } } - const netIncome = revenue - expense; const cfo = netIncome + operating.reduce((s, r) => s + r.amount, 0); const netChange = cfo + cfi + cff; - const deltaCash = endCash - beginCash; - const residual = netChange - deltaCash; + return { netIncome, operating, cfo, cfi, cff, netChange, beginCash, endCash, residual: netChange - (endCash - beginCash) }; +} + +function buildCashFlow(d: any, p: any | undefined, useCompare: boolean): StructuredReport { + const cur = computeCashFlow(d); + const prev = useCompare && p ? computeCashFlow(p) : undefined; + const cmp = (v: number | undefined) => (useCompare && prev ? v : undefined); + // Match prior-period operating line items by label for the compare column. + const prevByLabel = new Map((prev?.operating ?? []).map((r) => [r.label, r.amount])); const rows: StructuredRow[] = []; rows.push({ kind: "section", label: "Operating Activities" }); - rows.push({ kind: "sub", label: "Net Income", amount: netIncome }); - for (const r of operating) rows.push({ kind: "sub", label: r.label, amount: r.amount }); - rows.push({ kind: "total", label: "Net Cash from Operating Activities", amount: cfo }); + rows.push({ kind: "sub", label: "Net Income", amount: cur.netIncome, compare: cmp(prev?.netIncome) }); + for (const r of cur.operating) rows.push({ kind: "sub", label: r.label, amount: r.amount, compare: cmp(prevByLabel.get(r.label) ?? 0) }); + rows.push({ kind: "total", label: "Net Cash from Operating Activities", amount: cur.cfo, compare: cmp(prev?.cfo) }); rows.push({ kind: "spacer", label: "" }); rows.push({ kind: "section", label: "Investing Activities" }); - rows.push({ kind: "total", label: "Net Cash from Investing Activities", amount: cfi }); + rows.push({ kind: "total", label: "Net Cash from Investing Activities", amount: cur.cfi, compare: cmp(prev?.cfi) }); rows.push({ kind: "spacer", label: "" }); rows.push({ kind: "section", label: "Financing Activities" }); - rows.push({ kind: "total", label: "Net Cash from Financing Activities", amount: cff }); + rows.push({ kind: "total", label: "Net Cash from Financing Activities", amount: cur.cff, compare: cmp(prev?.cff) }); rows.push({ kind: "spacer", label: "" }); - rows.push({ kind: "sub", label: "Beginning Cash", amount: beginCash }); - rows.push({ kind: "sub", label: "Ending Cash", amount: endCash }); - if (Math.abs(residual) >= 0.005) { - rows.push({ kind: "total", label: "⚠ Out of balance — R4 residual (CFO+CFI+CFF − ΔCash)", amount: residual }); + rows.push({ kind: "sub", label: "Beginning Cash", amount: cur.beginCash, compare: cmp(prev?.beginCash) }); + rows.push({ kind: "sub", label: "Ending Cash", amount: cur.endCash, compare: cmp(prev?.endCash) }); + if (Math.abs(cur.residual) >= 0.005) { + rows.push({ kind: "total", label: "⚠ Out of balance — R4 residual (CFO+CFI+CFF − ΔCash)", amount: cur.residual }); } return { title: "Cash Flow Statement", rows, - cashHighlight: { label: "Net Change in Cash", amount: netChange }, + cashHighlight: { label: "Net Change in Cash", amount: cur.netChange }, }; } @@ -1369,11 +1371,33 @@ function BudgetVsActuals({ companyId, from, to, currency, companyName, rangeLabe expense: orderAccountsHierarchically(grouped.expense ?? []), } as Record), [grouped]); + const selectedBudget = useMemo(() => (budgets as any[]).find((b) => b.id === budgetId), [budgets, budgetId]); + + // Budget pro-rated to the selected actuals window (B2): sum each budget period + // weighted by how much of it overlaps [actFrom, actTo]. period_index maps to + // months (monthly), quarters (quarterly), or the whole year (annual). const budgetByAcct = useMemo(() => { const m: Record = {}; - for (const e of (entries as any[])) m[e.account_id] = (m[e.account_id] ?? 0) + Number(e.amount); + const pt = String(selectedBudget?.period_type ?? "annual"); + const fy = Number(selectedBudget?.fiscal_year) || new Date(actFrom || actTo || Date.now()).getFullYear(); + const fromT = actFrom ? new Date(actFrom).getTime() : -Infinity; + const toT = actTo ? new Date(actTo).getTime() : Infinity; + const DAY = 86400000; + const span = (idx: number): [number, number] => { + if (pt === "monthly") return [new Date(fy, idx, 1).getTime(), new Date(fy, idx + 1, 0).getTime()]; + if (pt === "quarterly") return [new Date(fy, idx * 3, 1).getTime(), new Date(fy, idx * 3 + 3, 0).getTime()]; + return [new Date(fy, 0, 1).getTime(), new Date(fy, 11, 31).getTime()]; + }; + for (const e of (entries as any[])) { + const [s, en] = span(Number(e.period_index) || 0); + const overlap = Math.max(0, Math.min(en, toT) - Math.max(s, fromT)); + const full = en - s; + const weight = full > 0 ? Math.min(1, (overlap + DAY) / (full + DAY)) : 1; + if (weight <= 0) continue; + m[e.account_id] = (m[e.account_id] ?? 0) + Number(e.amount) * weight; + } return m; - }, [entries]); + }, [entries, selectedBudget, actFrom, actTo]); const actualByAcct = useMemo(() => { const m: Record = {}; @@ -1477,7 +1501,7 @@ function BudgetVsActuals({ companyId, from, to, currency, companyName, rangeLabe doc.setFont("helvetica", "bold"); doc.setFontSize(14); doc.setTextColor(33, 37, 41); doc.text("Budget vs Actuals", 40, 50); doc.setFont("helvetica", "normal"); doc.setFontSize(9); doc.setTextColor(110, 116, 122); - doc.text(`${companyName} · ${actualsLabel}`, 40, 66); + doc.text(`${companyName} · ${actualsLabel} · Budget pro-rated to period`, 40, 66); autoTable(doc, { startY: 80, head: [["Account", "Budget", "Actual", "Variance", "Variance %"]], @@ -1519,6 +1543,9 @@ function BudgetVsActuals({ companyId, from, to, currency, companyName, rangeLabe +
+ Budget pro-rated to the selected period ({String((selectedBudget as any)?.period_type ?? "annual")} budget). +