Files
acmcc/supabase/migrations/20260613150000_transactions_void_support.sql
T
admin 6bf9da5482 Accounting: prior-period reconcile items, void txns, unified report filters, accrual-only
1. Reconciliation now shows ALL outstanding (unreconciled) items on/before the
   statement date, including ones from prior periods — removed the prior-recon
   date floor. Finalized items still drop off (they carry a reconciliation_id).

2. Void transactions in Banking and Reconciliation. New accounting.transactions
   .voided flag (+ voided_at/by); voided rows stay visible (strikethrough + VOID
   badge) but are excluded from the running balance, register totals, cached
   account balance, and reconciliation. post_transaction_gl reverses the GL for
   gl_managed companies; un-void supported from Banking.

3. Unified report filters: the single Period bar on the Reports page now drives
   every report. General Ledger, Trial Balance, AR Aging (Property), Pre-Paid
   Homeowners, Cash Disbursement, and Reserve Fund no longer have their own date
   pickers — they consume the shared from/to (range) or to (as-of).

4. Accrual only: removed the cash-basis toggle from Trial Balance and General
   Ledger (the data was always accrual GL anyway; the cash label was misleading).
   All income/expense reports recognize on billed/issue date.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 10:51:03 -04:00

95 lines
5.6 KiB
PL/PgSQL

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