Balance Sheet: show account numbers + parent categories (Buildium style)

B/S line items now render as '3011 SIRS Reserves - 3066 Painting/
Waterproofing' — leaf code+name prefixed by parent code+name when the
account has a parent, sorted so siblings group under their parent. Adds
parent_account_id to the reports accounts fetch. Codes always show
(baked into the label). Budget vs Actuals already shows codes + parent
hierarchy.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-18 22:16:42 -04:00
parent fa4ee3e215
commit af9e092cbd
+19 -4
View File
@@ -131,7 +131,7 @@ export async function fetchReportData(cid: string, from: string, to: string) {
const [inv, bills, accs, exp, custs, vends, ob, ytdInv, ytdExp, ytdBills, allBills, glRes, glCumRes, allInvRes, companyRes, periodBillItemsRes] = await Promise.all([ const [inv, bills, accs, exp, custs, vends, ob, ytdInv, ytdExp, ytdBills, allBills, glRes, glCumRes, allInvRes, companyRes, periodBillItemsRes] = await Promise.all([
accounting.from("invoices").select("number,total,paid_amount,status,issue_date,customers(name)").eq("company_id", cid).gte("issue_date", from).lte("issue_date", to), accounting.from("invoices").select("number,total,paid_amount,status,issue_date,customers(name)").eq("company_id", cid).gte("issue_date", from).lte("issue_date", to),
accounting.from("bills").select("number,total,paid_amount,status,issue_date,due_date,vendors(name)").eq("company_id", cid).gte("issue_date", from).lte("issue_date", to), accounting.from("bills").select("number,total,paid_amount,status,issue_date,due_date,vendors(name)").eq("company_id", cid).gte("issue_date", from).lte("issue_date", to),
accounting.from("accounts").select("id,name,code,type,subtype,balance,is_bank").eq("company_id", cid).eq("is_archived", false), accounting.from("accounts").select("id,name,code,type,subtype,balance,is_bank,parent_account_id").eq("company_id", cid).eq("is_archived", false),
accounting.from("expenses").select("date,category,amount,vendor_name,vendors(name)").eq("company_id", cid).gte("date", from).lte("date", to), accounting.from("expenses").select("date,category,amount,vendor_name,vendors(name)").eq("company_id", cid).gte("date", from).lte("date", to),
accounting.from("customers").select("id,name,balance,email,phone,property_address,lot_number").eq("company_id", cid).order("name"), accounting.from("customers").select("id,name,balance,email,phone,property_address,lot_number").eq("company_id", cid).order("name"),
accounting.from("vendors").select("id,name").eq("company_id", cid), accounting.from("vendors").select("id,name").eq("company_id", cid),
@@ -1667,12 +1667,27 @@ function buildBalanceSheet(d: any, p?: any, useCompare?: boolean): StructuredRep
const sumBal = (rows: any[]) => rows.reduce((s, a) => s + balOf(a), 0); const sumBal = (rows: any[]) => rows.reduce((s, a) => s + balOf(a), 0);
const sumBalP = (rows: any[]) => (prev ? rows.reduce((s, a) => s + (prev.glByAcct.get(a.id) ?? 0), 0) : undefined); const sumBalP = (rows: any[]) => (prev ? rows.reduce((s, a) => s + (prev.glByAcct.get(a.id) ?? 0), 0) : undefined);
// Account-number + parent-category labels (Buildium style:
// "3011 SIRS Reserves - 3066 Painting/Waterproofing"). Codes are baked into the
// label so they always show, and rows are sorted so siblings group by parent.
const acctMeta = new Map<string, { code: any; name: string }>(allAccounts.map((a: any) => [a.id, { code: a.code ?? null, name: a.name }]));
const codeName = (code: any, name: any) => `${code != null && String(code).trim() !== "" ? String(code) + " " : ""}${name ?? ""}`.trim();
const bsLabel = (a: any) => {
const p = a.parent_account_id ? acctMeta.get(a.parent_account_id) : null;
return p ? `${codeName(p.code, p.name)} - ${codeName(a.code, a.name)}` : codeName(a.code, a.name);
};
const bsKey = (a: any) => {
const p = a.parent_account_id ? acctMeta.get(a.parent_account_id) : null;
return `${p ? String(p.code ?? "") : String(a.code ?? "")}|${String(a.code ?? "")}`;
};
const bsSort = (arr: any[]) => [...arr].sort((x, y) => bsKey(x).localeCompare(bsKey(y), undefined, { numeric: true }));
const rows: StructuredRow[] = []; const rows: StructuredRow[] = [];
// Assets // Assets
rows.push({ kind: "section", label: "Assets" }); rows.push({ kind: "section", label: "Assets" });
const assets = byType("asset"); const assets = byType("asset");
for (const a of assets) 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 bsSort(assets)) rows.push({ kind: "sub", label: bsLabel(a), amount: balOf(a), compare: cmp(balOfP(a)), accountId: a.id });
const totalA = sumBal(assets); const totalA = sumBal(assets);
rows.push({ kind: "grand", label: "TOTAL ASSETS", amount: totalA, compare: cmp(sumBalP(assets)) }); rows.push({ kind: "grand", label: "TOTAL ASSETS", amount: totalA, compare: cmp(sumBalP(assets)) });
rows.push({ kind: "spacer", label: "" }); rows.push({ kind: "spacer", label: "" });
@@ -1680,7 +1695,7 @@ function buildBalanceSheet(d: any, p?: any, useCompare?: boolean): StructuredRep
// Liabilities // Liabilities
rows.push({ kind: "section", label: "Liabilities" }); rows.push({ kind: "section", label: "Liabilities" });
const liabs = byType("liability"); const liabs = byType("liability");
for (const a of liabs) 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 bsSort(liabs)) rows.push({ kind: "sub", label: bsLabel(a), amount: balOf(a), compare: cmp(balOfP(a)), accountId: a.id });
const totalL = sumBal(liabs); const totalL = sumBal(liabs);
rows.push({ kind: "total", label: "Total Liabilities", amount: totalL, compare: cmp(sumBalP(liabs)) }); rows.push({ kind: "total", label: "Total Liabilities", amount: totalL, compare: cmp(sumBalP(liabs)) });
rows.push({ kind: "spacer", label: "" }); rows.push({ kind: "spacer", label: "" });
@@ -1703,7 +1718,7 @@ function buildBalanceSheet(d: any, p?: any, useCompare?: boolean): StructuredRep
const cyeAccts = equityAccs.filter((a) => /^\s*current\s*year\s*(earnings|income)(\s*\(.*\))?\s*$/i.test(String(a.name || "")) || /^\s*net\s*income\s*$/i.test(String(a.name || ""))); const cyeAccts = equityAccs.filter((a) => /^\s*current\s*year\s*(earnings|income)(\s*\(.*\))?\s*$/i.test(String(a.name || "")) || /^\s*net\s*income\s*$/i.test(String(a.name || "")));
const foldedIds = new Set([...reAccts, ...cyeAccts].map((a) => a.id)); const foldedIds = new Set([...reAccts, ...cyeAccts].map((a) => a.id));
const otherEquity = equityAccs.filter((a) => !foldedIds.has(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 bsSort(otherEquity)) rows.push({ kind: "sub", label: bsLabel(a), 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 cyePosted = cyeAccts.reduce((s, a) => s + balOf(a), 0);