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 <noreply@anthropic.com>
This commit is contained in:
2026-06-02 02:14:25 -04:00
parent f25a778230
commit 96de47496a
3 changed files with 100 additions and 4 deletions
@@ -86,7 +86,7 @@ function useReportData(cid: string, from: string, to: string) {
enabled: !!cid, enabled: !!cid,
queryFn: async () => { queryFn: async () => {
const ytdStart = new Date(new Date().getFullYear(), 0, 1).toISOString().slice(0,10); 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("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), 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), .lte("journal_entries.date", to),
// All invoices (not date-filtered) — Accounts Receivable = unpaid invoices // All invoices (not date-filtered) — Accounts Receivable = unpaid invoices
accounting.from("invoices").select("total,paid_amount,status").eq("company_id", cid), 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 { return {
invoices: inv.data ?? [], bills: bills.data ?? [], accounts: accs.data ?? [], invoices: inv.data ?? [], bills: bills.data ?? [], accounts: accs.data ?? [],
@@ -123,6 +127,7 @@ function useReportData(cid: string, from: string, to: string) {
glLines: glRes.data ?? [], glLines: glRes.data ?? [],
glCumulative: glCumRes.data ?? [], glCumulative: glCumRes.data ?? [],
allInvoices: allInvRes.data ?? [], allInvoices: allInvRes.data ?? [],
glManaged: companyRes.data ? companyRes.data.gl_auto_post !== false : true,
from, asOf: to, 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 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 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 ok = (r: number) => Math.abs(r) < 0.005;
const allPass = checks.every((c) => c.pass); 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 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 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. 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."}
</p> </p>
</CardContent> </CardContent>
</Card> </Card>
+14 -2
View File
@@ -29,6 +29,13 @@ export interface ReconcileInput {
openInvoices: number; openInvoices: number;
/** Sum of OPEN vendor bill balances as of asOf (§1.5). */ /** Sum of OPEN vendor bill balances as of asOf (§1.5). */
openBills: number; 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 } 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: "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: "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: "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) })); return checks.map((c) => ({ ...c, pass: near(c.residual) }));
} }
@@ -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$$;