-- Void support for bank-register transactions. Voided rows stay for audit but -- are excluded from the cached account balance and, for gl_managed companies, -- reversed out of the GL (post_transaction_gl clears + posts nothing when voided). alter table accounting.transactions add column if not exists voided boolean not null default false, add column if not exists voided_at timestamptz, add column if not exists voided_by uuid; -- post_transaction_gl: a voided txn clears its prior GL entry and posts nothing. 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 not accounting.gl_managed(t.company_id) then return; end if; 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; _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$; -- sync_account_balance: exclude voided transactions from every cached-balance sum. -- (Full body re-created so each SUM filters COALESCE(voided,false)=false.) create or replace function accounting.sync_account_balance() returns trigger language plpgsql security definer set search_path to 'public' as $function$ DECLARE v_computed numeric; BEGIN IF TG_OP IN ('INSERT','UPDATE') THEN IF NEW.account_id IS NOT NULL THEN SELECT COALESCE(SUM(CASE WHEN type='credit' THEN amount ELSE -amount END),0) INTO v_computed FROM accounting.transactions WHERE account_id=NEW.account_id AND COALESCE(voided,false)=false; UPDATE accounting.accounts SET balance=v_computed, updated_at=now() WHERE id=NEW.account_id; END IF; IF NEW.coa_account_id IS NOT NULL THEN SELECT COALESCE(SUM(CASE WHEN a.type IN ('expense','asset') THEN CASE WHEN t.type='debit' THEN t.amount ELSE -t.amount END ELSE CASE WHEN t.type='credit' THEN t.amount ELSE -t.amount END END),0) INTO v_computed FROM accounting.transactions t JOIN accounting.accounts a ON a.id=t.coa_account_id WHERE t.coa_account_id=NEW.coa_account_id AND COALESCE(t.voided,false)=false; UPDATE accounting.accounts SET balance=v_computed, updated_at=now() WHERE id=NEW.coa_account_id; END IF; END IF; IF TG_OP='UPDATE' THEN IF OLD.account_id IS NOT NULL AND OLD.account_id IS DISTINCT FROM NEW.account_id THEN SELECT COALESCE(SUM(CASE WHEN type='credit' THEN amount ELSE -amount END),0) INTO v_computed FROM accounting.transactions WHERE account_id=OLD.account_id AND COALESCE(voided,false)=false; UPDATE accounting.accounts SET balance=v_computed, updated_at=now() WHERE id=OLD.account_id; END IF; IF OLD.coa_account_id IS NOT NULL AND OLD.coa_account_id IS DISTINCT FROM NEW.coa_account_id THEN SELECT COALESCE(SUM(CASE WHEN a.type IN ('expense','asset') THEN CASE WHEN t.type='debit' THEN t.amount ELSE -t.amount END ELSE CASE WHEN t.type='credit' THEN t.amount ELSE -t.amount END END),0) INTO v_computed FROM accounting.transactions t JOIN accounting.accounts a ON a.id=t.coa_account_id WHERE t.coa_account_id=OLD.coa_account_id AND COALESCE(t.voided,false)=false; UPDATE accounting.accounts SET balance=v_computed, updated_at=now() WHERE id=OLD.coa_account_id; END IF; END IF; IF TG_OP='DELETE' THEN IF OLD.account_id IS NOT NULL THEN SELECT COALESCE(SUM(CASE WHEN type='credit' THEN amount ELSE -amount END),0) INTO v_computed FROM accounting.transactions WHERE account_id=OLD.account_id AND COALESCE(voided,false)=false; UPDATE accounting.accounts SET balance=v_computed, updated_at=now() WHERE id=OLD.account_id; END IF; IF OLD.coa_account_id IS NOT NULL THEN SELECT COALESCE(SUM(CASE WHEN a.type IN ('expense','asset') THEN CASE WHEN t.type='debit' THEN t.amount ELSE -t.amount END ELSE CASE WHEN t.type='credit' THEN t.amount ELSE -t.amount END END),0) INTO v_computed FROM accounting.transactions t JOIN accounting.accounts a ON a.id=t.coa_account_id WHERE t.coa_account_id=OLD.coa_account_id AND COALESCE(t.voided,false)=false; UPDATE accounting.accounts SET balance=v_computed, updated_at=now() WHERE id=OLD.coa_account_id; END IF; RETURN OLD; END IF; RETURN NEW; END; $function$;