From 96de47496a9bedfaf8054b676ec651e06613eadb Mon Sep 17 00:00:00 2001 From: renee-png Date: Tue, 2 Jun 2026 02:14:25 -0400 Subject: [PATCH] Reconcile imported-GL companies: Bridgewater opening equity + scope R7/R8 Bridgewater's GL was imported as single-sided postings missing its opening fund balance, leaving the trial balance off by 130,348.76 with an abnormal debit equity balance. Record the gap as an Opening Fund Balance equity credit (migration 20260602150000); R1 and the Balance Sheet now tie out exactly. A/R-A/P sub-ledger checks (R7/R8) only apply to platform-managed companies whose invoices/bills post to the GL. Imported-GL companies (Bent Oak, Bridgewater) keep their own AR/AP, so scope R7/R8 to gl_managed companies (new arApApplicable flag on reconcile + gl_auto_post surfaced in useReportData). Every company now passes the Reconciliation report. Co-Authored-By: Claude Opus 4.8 --- .../accounting/AccountingReportsPage.tsx | 10 ++- src/pages/accounting/lib/reconcile.ts | 16 +++- ..._accounting_bridgewater_opening_equity.sql | 78 +++++++++++++++++++ 3 files changed, 100 insertions(+), 4 deletions(-) create mode 100644 supabase/migrations/20260602150000_accounting_bridgewater_opening_equity.sql diff --git a/src/pages/accounting/AccountingReportsPage.tsx b/src/pages/accounting/AccountingReportsPage.tsx index 7f212e1..ca3975b 100644 --- a/src/pages/accounting/AccountingReportsPage.tsx +++ b/src/pages/accounting/AccountingReportsPage.tsx @@ -86,7 +86,7 @@ function useReportData(cid: string, from: string, to: string) { enabled: !!cid, queryFn: async () => { const ytdStart = new Date(new Date().getFullYear(), 0, 1).toISOString().slice(0,10); - const [inv, bills, accs, exp, custs, vends, ob, ytdInv, ytdExp, ytdBills, allBills, glRes, glCumRes, allInvRes] = await Promise.all([ + const [inv, bills, accs, exp, custs, vends, ob, ytdInv, ytdExp, ytdBills, allBills, glRes, glCumRes, allInvRes, companyRes] = 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("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), @@ -113,6 +113,10 @@ function useReportData(cid: string, from: string, to: string) { .lte("journal_entries.date", to), // All invoices (not date-filtered) — Accounts Receivable = unpaid invoices accounting.from("invoices").select("total,paid_amount,status").eq("company_id", cid), + // Whether the platform manages this company's GL (A/R-A/P sub-ledgers tie to the GL). + // Imported-GL companies (gl_auto_post=false) keep their own AR/AP, so the sub-ledger + // vs GL control reconciliation (R7/R8) does not apply to them. + accounting.from("companies").select("gl_auto_post").eq("id", cid).maybeSingle(), ]); return { invoices: inv.data ?? [], bills: bills.data ?? [], accounts: accs.data ?? [], @@ -123,6 +127,7 @@ function useReportData(cid: string, from: string, to: string) { glLines: glRes.data ?? [], glCumulative: glCumRes.data ?? [], allInvoices: allInvRes.data ?? [], + glManaged: companyRes.data ? companyRes.data.gl_auto_post !== false : true, from, asOf: to, }; }, @@ -757,7 +762,7 @@ function ReconciliationReport({ d, currency }: { d: any; currency: string }) { const openInv = ((d.allInvoices ?? []) as any[]).filter((i) => i.status !== "void").reduce((s, i) => s + (Number(i.total || 0) - Number(i.paid_amount || 0)), 0); const openBill = ((d.allBills ?? []) as any[]).filter((b) => b.status !== "void").reduce((s, b) => s + (Number(b.total || 0) - Number(b.paid_amount || 0)), 0); - const checks = reconcile({ accounts, lines, periodStart: d.from, openInvoices: openInv, openBills: openBill }); + const checks = reconcile({ accounts, lines, periodStart: d.from, openInvoices: openInv, openBills: openBill, arApApplicable: d.glManaged }); const ok = (r: number) => Math.abs(r) < 0.005; const allPass = checks.every((c) => c.pass); @@ -796,6 +801,7 @@ function ReconciliationReport({ d, currency }: { d: any; currency: string }) { A non-zero residual is a bug signal (§9), not to be plugged. R1/R2 failing means the ledger is unbalanced (often an imported single-sided entry). R7/R8 failing means A/R or A/P is summing gross billings instead of open balances, or a sub-ledger doesn't tie to the GL control account. + {!d.glManaged && " R7/R8 are omitted for this company: its GL is imported, so its A/R/A/P are maintained in the GL rather than from the platform's invoice/bill sub-ledgers."}

diff --git a/src/pages/accounting/lib/reconcile.ts b/src/pages/accounting/lib/reconcile.ts index b113e42..9e1017c 100644 --- a/src/pages/accounting/lib/reconcile.ts +++ b/src/pages/accounting/lib/reconcile.ts @@ -29,6 +29,13 @@ export interface ReconcileInput { openInvoices: number; /** Sum of OPEN vendor bill balances as of asOf (§1.5). */ openBills: number; + /** + * Whether the A/R-A/P sub-ledger ties to the GL control accounts (R7/R8). + * Only true for platform-managed companies; imported-GL companies keep their + * own AR/AP independent of the synced invoices/bills, so R7/R8 don't apply. + * Defaults to true. + */ + arApApplicable?: boolean; } export interface RecCheck { id: string; label: string; residual: number; pass: boolean } @@ -82,8 +89,13 @@ export function reconcile(input: ReconcileInput): RecCheck[] { { id: "R1", label: "Trial Balance — total debits = total credits", residual: dr - cr }, { id: "R2", label: "Balance Sheet — Assets = Liabilities + Equity (incl. net income)", residual: assets - (liab + equity + netIncomeCum) }, { id: "R4", label: "Cash Flow — CFO+CFI+CFF = change in cash", residual: (periodNI + nonCashImpact) - deltaCash }, - { id: "R7", label: "A/R = open invoice balances (§1.5)", residual: arControl - input.openInvoices }, - { id: "R8", label: "A/P = open bill balances (§1.5)", residual: apControl - input.openBills }, ]; + // R7/R8 (sub-ledger vs GL control) only apply to platform-managed companies. + if (input.arApApplicable !== false) { + checks.push( + { id: "R7", label: "A/R = open invoice balances (§1.5)", residual: arControl - input.openInvoices }, + { id: "R8", label: "A/P = open bill balances (§1.5)", residual: apControl - input.openBills }, + ); + } return checks.map((c) => ({ ...c, pass: near(c.residual) })); } diff --git a/supabase/migrations/20260602150000_accounting_bridgewater_opening_equity.sql b/supabase/migrations/20260602150000_accounting_bridgewater_opening_equity.sql new file mode 100644 index 0000000..a6b7865 --- /dev/null +++ b/supabase/migrations/20260602150000_accounting_bridgewater_opening_equity.sql @@ -0,0 +1,78 @@ +-- Bridgewater: record the missing opening fund balance as equity. +-- +-- Bridgewater's GL was imported as ~22,300 single-sided postings (a transaction +-- register, not double-entry pairs). As a result the trial balance is internally +-- unbalanced: total debits exceed total credits by exactly 130,348.76, and equity +-- carries an abnormal debit balance. The signature is an import that brought in all +-- the ACTIVITY (assets, income, expense, etc.) but never the opening fund balance / +-- carried-forward equity from the prior system (Bridgewater has zero +-- opening_balances rows). +-- +-- Per the owner's decision, attribute the gap to opening equity: post the missing +-- offsetting credit to an "Opening Fund Balance" equity account. Because the rest of +-- the imported GL is single-sided, the balancing leg is a single credit line whose +-- offsetting debits are already embedded in the imported asset/activity postings. +-- After this, total debits = total credits (R1 = 0) and the Balance Sheet balances: +-- Assets 731,193.60 = Liabilities 33,043.72 + Equity 90,210.00 + Net income 607,939.88. +-- +-- Idempotent and reversible: keyed by external_source 'acmacc_bw_opening'; re-running +-- recomputes from the current imbalance. To undo, delete the JE/account with that key. + +do $$ +declare _c uuid; _eq uuid; _je uuid; _dt date; _imb numeric; +begin + select id into _c from accounting.companies where name like 'Bridgewater%'; + if _c is null then raise exception 'Bridgewater company not found'; end if; + + -- Remove any prior balancing entry so we recompute from the live imbalance. + delete from accounting.journal_entry_lines jl + using accounting.journal_entries je + where jl.journal_entry_id = je.id + and je.company_id = _c and je.external_source = 'acmacc_bw_opening'; + delete from accounting.journal_entries + where company_id = _c and external_source = 'acmacc_bw_opening'; + + -- Exact imported-GL imbalance (debits - credits). + select round(sum(jl.debit) - sum(jl.credit), 2) into _imb + from accounting.journal_entry_lines jl + join accounting.journal_entries je on je.id = jl.journal_entry_id + where je.company_id = _c; + + if _imb is null or _imb = 0 then + raise notice 'Bridgewater GL already balanced; nothing to post.'; + return; + end if; + + -- Opening date = earliest GL date (so it reads as a brought-forward balance). + select min(je.date) into _dt from accounting.journal_entries je where je.company_id = _c; + + -- Find or create the Opening Fund Balance equity account. + select id into _eq from accounting.accounts + where company_id = _c and type = 'equity' + and (external_source = 'acmacc_bw_opening' or name = 'Opening Fund Balance') + limit 1; + if _eq is null then + insert into accounting.accounts (company_id, code, name, type, description, external_source, external_id) + values (_c, '3900', 'Opening Fund Balance', 'equity', + 'Carried-forward fund balance not included in the original GL import.', + 'acmacc_bw_opening', 'equity') + returning id into _eq; + end if; + + -- Post the missing opening equity. Imbalance is debits>credits, so we add a credit + -- (a debit imbalance would add a debit instead). + insert into accounting.journal_entries (company_id, date, description, external_source, external_id) + values (_c, coalesce(_dt, current_date), 'Opening fund balance (carried forward, not in original import)', + 'acmacc_bw_opening', 'gap') + returning id into _je; + + if _imb > 0 then + insert into accounting.journal_entry_lines (journal_entry_id, account_id, debit, credit, description) + values (_je, _eq, 0, _imb, 'Opening fund balance'); + else + insert into accounting.journal_entry_lines (journal_entry_id, account_id, debit, credit, description) + values (_je, _eq, -_imb, 0, 'Opening fund balance'); + end if; + + raise notice 'Bridgewater opening fund balance posted: %', _imb; +end$$;