From 7fd8ad2c522ab538644bd8db9f11dcf4180df6ed Mon Sep 17 00:00:00 2001 From: renee-png Date: Mon, 8 Jun 2026 18:11:01 -0400 Subject: [PATCH] Opening Balances: allow entering Retained Earnings + Current Year Earnings Auto-provision "Retained Earnings" and "Current Year Earnings" equity accounts per company so they appear as inputtable rows in the Chart of Accounts Opening Balances grid. The Balance Sheet folds the posted "Current Year Earnings" account into the Net Income line (already did this for Retained Earnings), so a mid-year import can seed both equity figures without entering income/expense detail, and Total Equity stays balanced. Co-Authored-By: Claude Opus 4.8 --- .../AccountingChartOfAccountsPage.tsx | 22 +++++++++++++++++++ .../accounting/AccountingReportsPage.tsx | 13 ++++++++--- 2 files changed, 32 insertions(+), 3 deletions(-) diff --git a/src/pages/accounting/AccountingChartOfAccountsPage.tsx b/src/pages/accounting/AccountingChartOfAccountsPage.tsx index b5f188c..af40e29 100644 --- a/src/pages/accounting/AccountingChartOfAccountsPage.tsx +++ b/src/pages/accounting/AccountingChartOfAccountsPage.tsx @@ -99,6 +99,28 @@ export default function AccountingChartOfAccountsPage() { } }, [obSetup]); + // Ensure "Retained Earnings" and "Current Year Earnings" equity accounts exist + // so they can be entered directly in Opening Balances. The Balance Sheet folds + // their balances into the "Retained Earnings (prior years)" and "Net Income" + // lines, so a mid-year import can seed both without entering income/expense detail. + useEffect(() => { + if (!cid) return; + const list = accounts as any[]; + if (list.length === 0) return; // wait for accounts to load before deciding + const want = [ + { name: "Retained Earnings", re: /retained\s*earnings/i }, + { name: "Current Year Earnings", re: /current\s*year\s*earnings/i }, + ]; + const missing = want.filter((w) => !list.some((a) => a.type === "equity" && w.re.test(String(a.name || "")))); + if (missing.length === 0) return; + (async () => { + const { error } = await accounting.from("accounts").insert( + missing.map((w) => ({ company_id: cid, name: w.name, type: "equity" })) + ); + if (!error) qc.invalidateQueries({ queryKey: ["accounts", cid] }); + })(); + }, [accounts, cid, qc]); + useEffect(() => { const map: Record = {}; for (const b of balances as any[]) { diff --git a/src/pages/accounting/AccountingReportsPage.tsx b/src/pages/accounting/AccountingReportsPage.tsx index abc8635..ccb6444 100644 --- a/src/pages/accounting/AccountingReportsPage.tsx +++ b/src/pages/accounting/AccountingReportsPage.tsx @@ -1174,14 +1174,21 @@ function buildBalanceSheet(d: any, p?: any, useCompare?: boolean): StructuredRep // A real "Retained Earnings" equity account is postable via journal entries. // Fold its posted balance into the calculated "Retained Earnings (prior years)" // line so manual JE adjustments show there instead of as a separate line. + // Posted "Retained Earnings" and "Current Year Earnings" equity accounts are + // postable via journal entries / opening balances. Fold their balances into the + // calculated prior-years and Net Income lines (instead of separate lines) so the + // figures the user enters in Opening Balances show up where expected. const reAccts = equityAccs.filter((a) => /retained\s+earnings/i.test(String(a.name || ""))); - const reIds = new Set(reAccts.map((a) => a.id)); - const otherEquity = equityAccs.filter((a) => !reIds.has(a.id)); + const cyeAccts = equityAccs.filter((a) => /current\s*year\s*earnings/i.test(String(a.name || ""))); + const foldedIds = new Set([...reAccts, ...cyeAccts].map((a) => a.id)); + const otherEquity = equityAccs.filter((a) => !foldedIds.has(a.id)); for (const a of otherEquity) rows.push({ kind: "sub", label: a.name, code: a.code ?? undefined, amount: balOf(a), compare: cmp(balOfP(a)), accountId: a.id }); const rePosted = reAccts.reduce((s, a) => s + balOf(a), 0); const rePostedP = prev ? reAccts.reduce((s, a) => s + (prev.glByAcct.get(a.id) ?? 0), 0) : undefined; + const cyePosted = cyeAccts.reduce((s, a) => s + balOf(a), 0); + const cyePostedP = prev ? cyeAccts.reduce((s, a) => s + (prev.glByAcct.get(a.id) ?? 0), 0) : undefined; rows.push({ kind: "sub", label: "Retained Earnings (prior years)", amount: cur.rePrior + rePosted, compare: cmp(prev ? prev.rePrior + (rePostedP ?? 0) : undefined) }); - rows.push({ kind: "sub", label: "Net Income", amount: cur.cye, compare: cmp(prev?.cye) }); + rows.push({ kind: "sub", label: "Net Income", amount: cur.cye + cyePosted, compare: cmp(prev ? prev.cye + (cyePostedP ?? 0) : undefined) }); const totalE = sumBal(equityAccs) + cur.rePrior + cur.cye; const totalEP = prev ? (sumBalP(equityAccs)! + prev.rePrior + prev.cye) : undefined; rows.push({ kind: "total", label: "Total Equity", amount: totalE, compare: cmp(totalEP) });