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]);
// 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<string, { debit: string; credit: string }> = {};
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.
// 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) });