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 <noreply@anthropic.com>
This commit is contained in:
2026-06-08 18:11:01 -04:00
parent f3b81eaeeb
commit 7fd8ad2c52
2 changed files with 32 additions and 3 deletions
@@ -99,6 +99,28 @@ export default function AccountingChartOfAccountsPage() {
} }
}, [obSetup]); }, [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(() => { useEffect(() => {
const map: Record<string, { debit: string; credit: string }> = {}; const map: Record<string, { debit: string; credit: string }> = {};
for (const b of balances as any[]) { for (const b of balances as any[]) {
+10 -3
View File
@@ -1174,14 +1174,21 @@ function buildBalanceSheet(d: any, p?: any, useCompare?: boolean): StructuredRep
// A real "Retained Earnings" equity account is postable via journal entries. // A real "Retained Earnings" equity account is postable via journal entries.
// Fold its posted balance into the calculated "Retained Earnings (prior years)" // Fold its posted balance into the calculated "Retained Earnings (prior years)"
// line so manual JE adjustments show there instead of as a separate line. // 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 reAccts = equityAccs.filter((a) => /retained\s+earnings/i.test(String(a.name || "")));
const reIds = new Set(reAccts.map((a) => a.id)); const cyeAccts = equityAccs.filter((a) => /current\s*year\s*earnings/i.test(String(a.name || "")));
const otherEquity = equityAccs.filter((a) => !reIds.has(a.id)); 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 }); 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 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 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: "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 totalE = sumBal(equityAccs) + cur.rePrior + cur.cye;
const totalEP = prev ? (sumBalP(equityAccs)! + prev.rePrior + prev.cye) : undefined; const totalEP = prev ? (sumBalP(equityAccs)! + prev.rePrior + prev.cye) : undefined;
rows.push({ kind: "total", label: "Total Equity", amount: totalE, compare: cmp(totalEP) }); rows.push({ kind: "total", label: "Total Equity", amount: totalE, compare: cmp(totalEP) });