P&L monthly: span the fiscal year so columns aren't limited to one month

The monthly P&L was bound to the page Period, which defaults to the
current month — so 'Monthly columns' showed only a single column. When the
selected period is a single month, widen the start to the fiscal year
containing the end date (per company fiscal_year_start) so every month of
the FY-to-date gets a column with none missing. A multi-month selected
range is still respected as-is.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-19 02:36:57 -04:00
parent 2e99a9a7da
commit 7eb08ad29f
+26 -9
View File
@@ -676,7 +676,7 @@ export default function AccountingReportsPage({ association }: { association?: {
<BudgetVsActuals companyId={cid} from={from} to={to} currency={cur} companyName={associationName ?? "Company"} rangeLabel={rangeLabel} logoUrl={logoUrl} />
)}
{active === "pnl" && pnlMonthView && (
<IncomeStatementReport companyId={cid} companyName={associationName ?? "Company"} from={from} to={to} currency={cur} logoUrl={logoUrl} />
<IncomeStatementReport companyId={cid} companyName={associationName ?? "Company"} from={from} to={to} fiscalYearStart={fiscalYearStart} currency={cur} logoUrl={logoUrl} />
)}
{active === "trial-balance" && (
<TrialBalanceReport companyId={cid} companyName={associationName ?? ""} logoUrl={logoUrl} to={to} />
@@ -853,6 +853,14 @@ function isBuildPeriods(from: string, to: string, gran: ISGran): { key: string;
return out;
}
/** First day of the fiscal year that contains `to`, given an "MM-DD" FY start. */
function isFyStartFor(to: string, fyStart: string): string {
const [mm, dd] = (fyStart || "01-01").split("-").map(Number);
const ty = Number(to.slice(0, 4));
const candidate = `${ty}-${isPad2(mm || 1)}-${isPad2(dd || 1)}`;
return to >= candidate ? candidate : `${ty - 1}-${isPad2(mm || 1)}-${isPad2(dd || 1)}`;
}
/** Period key a given YYYY-MM-DD date falls into, for the chosen granularity. */
function isPeriodKey(date: string, gran: ISGran): string {
if (gran === "month") return date.slice(0, 7);
@@ -898,22 +906,31 @@ function isGroupAccts(accts: ISAcct[], periods: { key: string }[]): ISGroup[] {
return groups;
}
function IncomeStatementReport({ companyId, companyName, from, to, currency, logoUrl }: {
companyId: string; companyName: string; from: string; to: string; currency: string; logoUrl?: string | null;
function IncomeStatementReport({ companyId, companyName, from, to, fiscalYearStart, currency, logoUrl }: {
companyId: string; companyName: string; from: string; to: string; fiscalYearStart?: string; currency: string; logoUrl?: string | null;
}) {
const [gran, setGran] = useState<ISGran>("month");
// The point of the monthly P&L is a column per period. If the page Period is a
// single month (the default), there'd be just one column — so widen the start
// to the fiscal year containing `to`, giving every month of the FY-to-date.
// A multi-month selected range is respected as-is.
const effFrom = useMemo(
() => (from.slice(0, 7) === to.slice(0, 7) ? isFyStartFor(to, fiscalYearStart || "01-01") : from),
[from, to, fiscalYearStart],
);
const { data: glLines = [], isLoading } = useQuery({
queryKey: ["income-statement-gl", companyId, from, to],
queryKey: ["income-statement-gl", companyId, effFrom, to],
enabled: !!companyId,
queryFn: () => fetchAllGLLines(
companyId, to,
"id,debit,credit,accounts!inner(id,name,code,type,category),journal_entries!inner(company_id,date)",
from,
effFrom,
),
});
const periods = useMemo(() => isBuildPeriods(from, to, gran), [from, to, gran]);
const periods = useMemo(() => isBuildPeriods(effFrom, to, gran), [effFrom, to, gran]);
const model = useMemo(() => {
const accts = new Map<string, ISAcct>();
@@ -942,7 +959,7 @@ function IncomeStatementReport({ companyId, companyName, from, to, currency, log
return { incomeGroups, expenseGroups, incTot, expTot, net, hasRows: income.length > 0 || expense.length > 0 };
}, [glLines, periods, gran]);
const subtitle = `${fmtDate(from)} ${fmtDate(to)}, By ${gran[0].toUpperCase()}${gran.slice(1)}, Accrual basis`;
const subtitle = `${fmtDate(effFrom)} ${fmtDate(to)}, By ${gran[0].toUpperCase()}${gran.slice(1)}, Accrual basis`;
const { hasRows } = model;
const exportPdf = async () => {
@@ -995,7 +1012,7 @@ function IncomeStatementReport({ companyId, companyName, from, to, currency, log
},
});
drawBrandedFooter(doc);
doc.save(`profit-loss-${gran}-${from}-to-${to}.pdf`);
doc.save(`profit-loss-${gran}-${effFrom}-to-${to}.pdf`);
};
const exportCsv = () => {
@@ -1019,7 +1036,7 @@ function IncomeStatementReport({ companyId, companyName, from, to, currency, log
const blob = new Blob([lines.join("\n")], { type: "text/csv" });
const a = document.createElement("a");
a.href = URL.createObjectURL(blob);
a.download = `profit-loss-${gran}-${from}-to-${to}.csv`;
a.download = `profit-loss-${gran}-${effFrom}-to-${to}.csv`;
a.click();
URL.revokeObjectURL(a.href);
};