From 03cd7127a2f3b5a8553e0e7a440e6b5fc2fdc8c1 Mon Sep 17 00:00:00 2001 From: renee-png Date: Tue, 16 Jun 2026 14:26:32 -0400 Subject: [PATCH] Budget vs Actuals: show full per-period budget, no proration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Budget column overlap-weighted partial months, so a YTD range ending mid-month showed a fractional budget. Now include each budget period's full amount whenever the selected window touches it, for both the main and comparison windows — exactly as entered on the budget. Co-Authored-By: Claude Opus 4.8 --- .../accounting/AccountingReportsPage.tsx | 28 ++++++++----------- 1 file changed, 11 insertions(+), 17 deletions(-) diff --git a/src/pages/accounting/AccountingReportsPage.tsx b/src/pages/accounting/AccountingReportsPage.tsx index e7ff9eb..e4e7773 100644 --- a/src/pages/accounting/AccountingReportsPage.tsx +++ b/src/pages/accounting/AccountingReportsPage.tsx @@ -2000,16 +2000,16 @@ function BudgetVsActuals({ companyId, from, to, currency, companyName, rangeLabe 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). + // Budget for the selected actuals window: include each budget period's FULL + // amount, exactly as entered, for any period the window touches — no proration. + // period_index maps to months (monthly), quarters (quarterly), or the whole + // year (annual). E.g. Jan 1 – Jun 16 = full Jan…Jun monthly budgets. const budgetByAcct = useMemo(() => { const m: Record = {}; 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()]; @@ -2017,11 +2017,8 @@ function BudgetVsActuals({ companyId, from, to, currency, companyName, rangeLabe }; 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; + if (s > toT || en < fromT) continue; // period not touched by the window + m[e.account_id] = (m[e.account_id] ?? 0) + Number(e.amount); } return m; }, [entries, selectedBudget, actFrom, actTo]); @@ -2029,7 +2026,8 @@ function BudgetVsActuals({ companyId, from, to, currency, companyName, rangeLabe const actualByAcct = useMemo(() => computeBvaActuals(actualsData), [actualsData]); const cmpActualByAcct = useMemo(() => computeBvaActuals(cmpActualsData), [cmpActualsData]); - // Comparison-window budget (pro-rated like budgetByAcct, over [cmpFrom, cmpTo]). + // Comparison-window budget (full per-period amounts like budgetByAcct, over + // [cmpFrom, cmpTo]). No proration. const cmpBudgetByAcct = useMemo(() => { const m: Record = {}; if (!cmpOn || !cmpFrom || !cmpTo) return m; @@ -2037,7 +2035,6 @@ function BudgetVsActuals({ companyId, from, to, currency, companyName, rangeLabe const fy = Number(selectedBudget?.fiscal_year) || new Date(cmpFrom || cmpTo || Date.now()).getFullYear(); const fromT = new Date(cmpFrom).getTime(); const toT = new Date(cmpTo).getTime(); - 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()]; @@ -2045,11 +2042,8 @@ function BudgetVsActuals({ companyId, from, to, currency, companyName, rangeLabe }; 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; + if (s > toT || en < fromT) continue; // period not touched by the window + m[e.account_id] = (m[e.account_id] ?? 0) + Number(e.amount); } return m; }, [entries, selectedBudget, cmpOn, cmpFrom, cmpTo]); @@ -2194,7 +2188,7 @@ function BudgetVsActuals({ companyId, from, to, currency, companyName, rangeLabe
- Budget pro-rated to the selected period ({String((selectedBudget as any)?.period_type ?? "annual")} budget). + Budget shows the full {String((selectedBudget as any)?.period_type ?? "annual")} amounts for every period the selected range touches — exactly as entered, no proration.