Reconciliation PDF: include outstanding items in prior-period reports; Balance Sheet: stop folding distinctly-named equity accounts

- Prior-reconciliation 'View Report' PDF was hard-coding empty uncleared lists,
  so outstanding items never appeared. It now reconstructs the items outstanding
  as of that statement (dated on/before the statement date, not voided, and not
  cleared in that or an earlier reconciliation) and includes them, with the book
  balance reflecting them.
- Balance Sheet equity folding matched any name containing 'retained earnings'
  (or 'current year earnings'), so a distinct account like 'Retained Earnings
  Savings' was swallowed into the calculated line and never shown. Now only the
  exact standard accounts fold; other equity accounts render on their own line.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-13 12:47:46 -04:00
parent 3ab016fc57
commit 9063e49389
2 changed files with 31 additions and 8 deletions
@@ -609,16 +609,35 @@ export default function AccountingReconcileDetailPage() {
</TableCell> </TableCell>
<TableCell className="text-right"> <TableCell className="text-right">
<Button size="sm" variant="ghost" onClick={async () => { <Button size="sm" variant="ghost" onClick={async () => {
const { data: rows } = await accounting const mapRow = (r: any) => ({ name: r.description ?? "", date: r.date, number: r.reference ?? "", memo: "", amount: Number(r.amount) });
// Items cleared IN this reconciliation
const { data: clearedRows } = await accounting
.from("transactions") .from("transactions")
.select("date,description,reference,amount,type") .select("date,description,reference,amount,type")
.eq("reconciliation_id", h.id); .eq("reconciliation_id", h.id);
const mapRow = (r: any) => ({ name: r.description ?? "", date: r.date, number: r.reference ?? "", memo: "", amount: Number(r.amount) }); // Outstanding AS OF this statement: dated on/before the
const clearedDeposits = (rows ?? []).filter((r: any) => r.type === "credit").map(mapRow); // statement date and not cleared in this or any earlier
const clearedWithdrawals = (rows ?? []).filter((r: any) => r.type === "debit").map(mapRow); // reconciliation — i.e. still uncleared, or cleared only
// in a LATER statement period. (Excludes voided.)
const laterReconIds = (history as any[])
.filter((r: any) => r.id !== h.id && String(r.statement_end_date) > String(h.statement_end_date))
.map((r: any) => r.id);
const { data: priorRows } = await accounting
.from("transactions")
.select("date,description,reference,amount,type,reconciliation_id,voided")
.eq("account_id", accountId)
.lte("date", h.statement_end_date);
const outstanding = (priorRows ?? []).filter((r: any) =>
!r.voided && r.reconciliation_id !== h.id &&
(r.reconciliation_id == null || laterReconIds.includes(r.reconciliation_id)));
const clearedDeposits = (clearedRows ?? []).filter((r: any) => r.type === "credit").map(mapRow);
const clearedWithdrawals = (clearedRows ?? []).filter((r: any) => r.type === "debit").map(mapRow);
const unclearedDeposits = outstanding.filter((r: any) => r.type === "credit").map(mapRow);
const unclearedWithdrawals = outstanding.filter((r: any) => r.type === "debit").map(mapRow);
const sumAmt = (rs: { amount: number }[]) => rs.reduce((s, r) => s + r.amount, 0); const sumAmt = (rs: { amount: number }[]) => rs.reduce((s, r) => s + r.amount, 0);
const begin = Number(h.opening_balance); const begin = Number(h.opening_balance);
const ending = begin + sumAmt(clearedDeposits) - sumAmt(clearedWithdrawals); const ending = begin + sumAmt(clearedDeposits) - sumAmt(clearedWithdrawals);
const book = ending + sumAmt(unclearedDeposits) - sumAmt(unclearedWithdrawals);
const acctLabel = (account as any)?.code ? `${(account as any).code} ${(account as any).name}` : ((account as any)?.name ?? "Account"); const acctLabel = (account as any)?.code ? `${(account as any).code} ${(account as any).name}` : ((account as any)?.name ?? "Account");
renderReconciliationPdf({ renderReconciliationPdf({
companyName: associationName ?? "Company", companyName: associationName ?? "Company",
@@ -626,8 +645,8 @@ export default function AccountingReconcileDetailPage() {
statementEndDate: h.statement_end_date, statementEndDate: h.statement_end_date,
beginningBalance: begin, beginningBalance: begin,
endingBalance: ending, endingBalance: ending,
bookBalance: ending, bookBalance: book,
clearedDeposits, clearedWithdrawals, unclearedDeposits: [], unclearedWithdrawals: [], clearedDeposits, clearedWithdrawals, unclearedDeposits, unclearedWithdrawals,
preparedBy: user?.email ?? "—", preparedBy: user?.email ?? "—",
currency: cur, currency: cur,
completedAt: h.completed_at ?? new Date().toISOString(), completedAt: h.completed_at ?? new Date().toISOString(),
@@ -1656,8 +1656,12 @@ function buildBalanceSheet(d: any, p?: any, useCompare?: boolean): StructuredRep
// postable via journal entries / opening balances. Fold their balances into the // postable via journal entries / opening balances. Fold their balances into the
// calculated prior-years and Net Income lines (instead of separate lines) so 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. // figures the user enters in Opening Balances show up where expected.
const reAccts = equityAccs.filter((a) => /retained\s+earnings/i.test(String(a.name || ""))); // Fold ONLY the standard "Retained Earnings" / "Current Year Earnings" accounts
const cyeAccts = equityAccs.filter((a) => /current\s*year\s*(earnings|income)/i.test(String(a.name || "")) || /^\s*net\s*income\s*$/i.test(String(a.name || ""))); // into the calculated lines. Distinctly-named equity accounts (e.g. "Retained
// Earnings Savings", a reserve-equity account) must NOT be swallowed — they
// render as their own equity line. Match the full name, not a substring.
const reAccts = equityAccs.filter((a) => /^\s*retained\s+earnings(\s*\(.*\))?\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 otherEquity) rows.push({ kind: "sub", label: a.name, code: a.code ?? undefined, amount: balOf(a), compare: cmp(balOfP(a)), accountId: a.id });