mirror of
https://github.com/renee-png/acmcc.git
synced 2026-06-21 01:40:01 +00:00
Accounting: guard import-mode register rows against GL double-counting
post_transaction_gl now skips posting a register row whose exact bank movement (company + bank account + date + amount + direction) is already in buildium_gl, so categorizing an ad-hoc-materialized register row can no longer duplicate the Buildium GL pull. Also freezes (exclude_from_gl) any remaining unguarded buildium-overlapping register rows across all import-mode companies. Genuinely-missing manual items (no GL match) still post normally. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
-- Import-mode register double-count guard.
|
||||
--
|
||||
-- Background: import-mode companies (gl_auto_post=false) get their GL from the
|
||||
-- Buildium GL pull (external_source='buildium_gl'). buildium-gl-sync materializes
|
||||
-- a bank-register row per GL bank line WITH journal_entry_line_id set, so those
|
||||
-- are recognized as "already in GL" and never re-post. But register rows created
|
||||
-- by an ad-hoc materialization (e.g. Village Woods' finishing step) were left
|
||||
-- UNLINKED (journal_entry_line_id null, exclude_from_gl false). They sat harmless
|
||||
-- until the "categorize register transactions from Buildium GL lines" op
|
||||
-- (buildium-payee-backfill partymap) assigned them a vendor/customer/coa — which
|
||||
-- gave post_transaction_gl a counter account and made it post them, DUPLICATING
|
||||
-- the Buildium GL pull (A/P, A/R and bank all double-counted).
|
||||
--
|
||||
-- This migration:
|
||||
-- #1 Freezes any remaining unguarded register row whose exact bank movement
|
||||
-- already exists in buildium_gl, across ALL import-mode companies. Rows that
|
||||
-- are genuinely missing from Buildium (intentionally-posted manual items)
|
||||
-- have no matching buildium_gl line and are left postable.
|
||||
-- #2 Adds a backstop inside post_transaction_gl: it never posts a register row
|
||||
-- whose exact bank movement (bank account + date + amount + direction) is
|
||||
-- already recorded in buildium_gl — regardless of what a later categorization
|
||||
-- run assigns. This enforces the rule at the GL choke point (the ad-hoc
|
||||
-- apply step that writes party/coa onto the register has no committed home).
|
||||
--
|
||||
-- Match key = (company, bank account_id, date, amount, direction). For a register
|
||||
-- row, type 'credit' = money in = bank debit; otherwise = money out = bank credit.
|
||||
-- Genuinely-missing manual items don't match (verified against Bent Oak's
|
||||
-- insurance/electric/water and Bridgewater's misc-income donations), so they keep
|
||||
-- posting. A coincidental same-account/date/amount/direction collision with an
|
||||
-- unrelated buildium_gl movement is rare and resolves conservatively (no double
|
||||
-- count); such a row can still be surfaced and posted as an explicit manual JE.
|
||||
|
||||
create or replace function accounting.post_transaction_gl(_txn_id uuid)
|
||||
returns void language plpgsql security definer set search_path to 'public', 'accounting'
|
||||
as $function$
|
||||
declare t accounting.transactions%rowtype; _counter uuid; _je uuid; _amt numeric;
|
||||
begin
|
||||
select * into t from accounting.transactions where id=_txn_id;
|
||||
if not found then return; end if;
|
||||
perform accounting._gl_clear(t.company_id, 'acmacc_txn', t.id::text);
|
||||
if coalesce(t.voided,false) then return; end if;
|
||||
if t.journal_entry_line_id is not null then return; end if; -- already in GL via Buildium pull
|
||||
if coalesce(t.exclude_from_gl,false) then return; end if; -- frozen legacy/duplicate register row
|
||||
if t.transfer_id is not null or t.deposit_id is not null then return; end if;
|
||||
if t.account_id is null then return; end if;
|
||||
_amt := coalesce(t.amount,0);
|
||||
if _amt = 0 then return; end if;
|
||||
|
||||
-- Backstop: if buildium_gl already records this exact bank movement, this
|
||||
-- register row is a duplicate of the Buildium GL pull. Never double-post it,
|
||||
-- no matter what party/category a categorization run later assigns.
|
||||
if exists (
|
||||
select 1
|
||||
from accounting.journal_entries je
|
||||
join accounting.journal_entry_lines l on l.journal_entry_id = je.id
|
||||
where je.company_id = t.company_id
|
||||
and je.external_source = 'buildium_gl'
|
||||
and je.date = t.date
|
||||
and l.account_id = t.account_id
|
||||
and ((t.type = 'credit' and l.debit = _amt) or (t.type <> 'credit' and l.credit = _amt))
|
||||
) then
|
||||
return;
|
||||
end if;
|
||||
|
||||
_counter := case
|
||||
when t.customer_id is not null then accounting.coa_ar(t.company_id)
|
||||
when t.coa_account_id is not null then t.coa_account_id
|
||||
when t.vendor_id is not null then accounting.coa_ap(t.company_id)
|
||||
else null end;
|
||||
if _counter is null then return; end if;
|
||||
|
||||
insert into accounting.journal_entries (company_id, date, description, reference, external_source, external_id)
|
||||
values (t.company_id, t.date, coalesce(nullif(t.description,''), 'Bank transaction'), t.reference, 'acmacc_txn', t.id::text)
|
||||
returning id into _je;
|
||||
if t.type = 'credit' then
|
||||
insert into accounting.journal_entry_lines (journal_entry_id, account_id, debit, credit, description) values
|
||||
(_je, t.account_id, _amt, 0, t.description), (_je, _counter, 0, _amt, t.description);
|
||||
else
|
||||
insert into accounting.journal_entry_lines (journal_entry_id, account_id, debit, credit, description) values
|
||||
(_je, _counter, _amt, 0, t.description), (_je, t.account_id, 0, _amt, t.description);
|
||||
end if;
|
||||
end$function$;
|
||||
|
||||
-- One-time cleanup: freeze remaining unguarded buildium-overlapping register rows
|
||||
-- across all import-mode companies. The UPDATE fires trg_acct_txn_gl, which clears
|
||||
-- any duplicate acmacc_txn JE these rows had posted. Idempotent (already-frozen and
|
||||
-- already-linked rows are skipped; intentionally-posted manual items don't match).
|
||||
update accounting.transactions t
|
||||
set exclude_from_gl = true
|
||||
from accounting.companies c
|
||||
where t.company_id = c.id
|
||||
and coalesce(c.gl_auto_post, true) = false
|
||||
and t.journal_entry_line_id is null
|
||||
and coalesce(t.exclude_from_gl, false) = false
|
||||
and t.deposit_id is null and t.transfer_id is null
|
||||
and t.account_id is not null
|
||||
and coalesce(t.voided, false) = false
|
||||
and exists (
|
||||
select 1 from accounting.journal_entries je
|
||||
join accounting.journal_entry_lines l on l.journal_entry_id = je.id
|
||||
where je.company_id = t.company_id and je.external_source = 'buildium_gl' and je.date = t.date
|
||||
and l.account_id = t.account_id
|
||||
and ((t.type = 'credit' and l.debit = t.amount) or (t.type <> 'credit' and l.credit = t.amount))
|
||||
);
|
||||
Reference in New Issue
Block a user