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$$;