mirror of
https://github.com/renee-png/acmcc.git
synced 2026-06-21 01:40:01 +00:00
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:
@@ -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>
|
||||||
|
|||||||
@@ -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$$;
|
||||||
Reference in New Issue
Block a user