mirror of
https://github.com/renee-png/acmcc.git
synced 2026-06-21 01:40:01 +00:00
Accounting: two-way bill creation + mirror pending bills
- bill_should_mirror now includes 'pending', so approval bills appear in Payables immediately (still excludes draft/rejected/void/cancelled/denied). - New reverse-creation path: a bill created natively in the Accounting module (external_source NULL, non-auto, non-payment, non-void) now creates a matching public.bills row. The accounting row is pre-linked to the new public id so the forward sync adopts it (no duplicate mirror); vendor is mapped back to public.vendors and the line's GL is carried to expense_account_id. - Backfill: mirrored existing pending public bills and reverse-created the 8 eligible native accounting bills. Verified: 0 unlinked native, 0 duplicate mirrors, pending bills mirrored. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,153 @@
|
||||
-- Make bill creation truly two-way between the app (public.bills / bill_approvals)
|
||||
-- and the Accounting platform (accounting.bills), and mirror pending bills.
|
||||
--
|
||||
-- 1. bill_should_mirror now includes 'pending' so approval bills appear in
|
||||
-- Payables immediately (still excludes draft/rejected/void/cancelled/denied).
|
||||
-- 2. A bill created NATIVELY in the Accounting module (external_source IS NULL)
|
||||
-- now creates a matching public.bills row. The accounting row is pre-linked
|
||||
-- to the new public id so the existing forward sync adopts it (no duplicate),
|
||||
-- the vendor is mapped back to public.vendors, and the line's GL is carried.
|
||||
-- 3. Backfill: mirror existing pending public bills + reverse-create eligible
|
||||
-- native accounting bills.
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 1) Mirror pending bills too.
|
||||
-- ---------------------------------------------------------------------------
|
||||
create or replace function accounting.bill_should_mirror(_status text)
|
||||
returns boolean language sql immutable as $$
|
||||
select coalesce(lower(_status), '') not in
|
||||
('draft','rejected','void','voided','cancelled','denied');
|
||||
$$;
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 2) Reverse creation: native accounting bill -> public.bills (idempotent-ish;
|
||||
-- only acts on unlinked native rows).
|
||||
-- ---------------------------------------------------------------------------
|
||||
create or replace function accounting.create_public_from_accounting_bill(_acct_id uuid)
|
||||
returns uuid
|
||||
language plpgsql security definer set search_path to 'public','accounting'
|
||||
as $$
|
||||
declare
|
||||
ab accounting.bills%rowtype;
|
||||
_assoc uuid;
|
||||
_pub_vendor uuid;
|
||||
_avs text; _avext text; _avname text;
|
||||
_exp uuid;
|
||||
_new_id uuid;
|
||||
_pub_status text;
|
||||
begin
|
||||
select * into ab from accounting.bills where id=_acct_id;
|
||||
if not found then return null; end if;
|
||||
|
||||
-- only native, real, non-void bills
|
||||
if ab.external_source is not null then return null; end if;
|
||||
if coalesce(ab.auto_created,false) then return null; end if;
|
||||
if ab.source_payment_id is not null then return null; end if;
|
||||
if ab.status::text = 'void' then return null; end if;
|
||||
|
||||
select association_id into _assoc from accounting.companies where id=ab.company_id;
|
||||
if _assoc is null then return null; end if;
|
||||
|
||||
-- Map the vendor back to a single public.vendors roster (find-or-create).
|
||||
_pub_vendor := null;
|
||||
if ab.vendor_id is not null then
|
||||
select external_source, external_id, name into _avs, _avext, _avname
|
||||
from accounting.vendors where id=ab.vendor_id;
|
||||
if _avs='acmacc_vendor' and nullif(_avext,'') is not null then
|
||||
_pub_vendor := _avext::uuid;
|
||||
end if;
|
||||
if _pub_vendor is null or not exists (select 1 from public.vendors where id=_pub_vendor) then
|
||||
select id into _pub_vendor from public.vendors
|
||||
where (association_id=_assoc or _assoc = any(association_ids))
|
||||
and lower(trim(name))=lower(trim(coalesce(_avname,''))) limit 1;
|
||||
if _pub_vendor is null then
|
||||
insert into public.vendors (name, association_id, is_active)
|
||||
values (coalesce(nullif(_avname,''),'Vendor'), _assoc, true)
|
||||
returning id into _pub_vendor;
|
||||
end if;
|
||||
update accounting.vendors
|
||||
set external_source='acmacc_vendor', external_id=_pub_vendor::text, updated_at=now()
|
||||
where id=ab.vendor_id and nullif(external_id,'') is null;
|
||||
end if;
|
||||
end if;
|
||||
|
||||
-- Carry the first line's GL account if it is a valid public COA id.
|
||||
select account_id into _exp from accounting.bill_items
|
||||
where bill_id=ab.id and account_id is not null limit 1;
|
||||
if _exp is not null and not exists (select 1 from public.chart_of_accounts where id=_exp) then
|
||||
_exp := null;
|
||||
end if;
|
||||
|
||||
_pub_status := case ab.status::text when 'paid' then 'paid' else 'approved' end;
|
||||
_new_id := gen_random_uuid();
|
||||
|
||||
-- Pre-link the accounting row so the forward sync (fired by the insert below)
|
||||
-- updates THIS row on conflict instead of creating a duplicate mirror.
|
||||
update accounting.bills
|
||||
set external_source='acmacc_bill', external_id=_new_id::text, updated_at=now()
|
||||
where id=ab.id;
|
||||
|
||||
insert into public.bills
|
||||
(id, association_id, vendor_id, invoice_number, bill_date, due_date,
|
||||
amount, amount_paid, expense_account_id, description, status, attachment_url, approved_date)
|
||||
values
|
||||
(_new_id, _assoc, _pub_vendor, ab.number, coalesce(ab.issue_date, current_date), ab.due_date,
|
||||
coalesce(ab.total,0), coalesce(ab.paid_amount,0), _exp, ab.notes,
|
||||
_pub_status, ab.attachment_url,
|
||||
case when _pub_status in ('approved','paid') then current_date else null end);
|
||||
|
||||
return _new_id;
|
||||
end;
|
||||
$$;
|
||||
|
||||
create or replace function accounting.tg_accounting_bill_reverse_create()
|
||||
returns trigger
|
||||
language plpgsql security definer set search_path to 'public','accounting'
|
||||
as $$
|
||||
begin
|
||||
begin
|
||||
if new.external_source is null
|
||||
and not coalesce(new.auto_created,false)
|
||||
and new.source_payment_id is null
|
||||
and new.status::text <> 'void' then
|
||||
perform accounting.create_public_from_accounting_bill(new.id);
|
||||
end if;
|
||||
exception when others then
|
||||
raise warning 'accounting: reverse bill create failed for %: %', new.id, sqlerrm;
|
||||
end;
|
||||
return new;
|
||||
end;
|
||||
$$;
|
||||
|
||||
drop trigger if exists trg_acct_bill_reverse_create on accounting.bills;
|
||||
create trigger trg_acct_bill_reverse_create
|
||||
after insert on accounting.bills
|
||||
for each row execute function accounting.tg_accounting_bill_reverse_create();
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 3a) Backfill: mirror existing pending public bills into Payables now.
|
||||
-- ---------------------------------------------------------------------------
|
||||
do $$
|
||||
declare r record;
|
||||
begin
|
||||
for r in select id from public.bills where lower(coalesce(status,'')) = 'pending' loop
|
||||
perform accounting.sync_public_bill(r.id);
|
||||
end loop;
|
||||
end $$;
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 3b) Backfill: reverse-create eligible native accounting bills.
|
||||
-- ---------------------------------------------------------------------------
|
||||
do $$
|
||||
declare r record;
|
||||
begin
|
||||
for r in
|
||||
select id from accounting.bills
|
||||
where external_source is null
|
||||
and not coalesce(auto_created,false)
|
||||
and source_payment_id is null
|
||||
and status::text <> 'void'
|
||||
loop
|
||||
perform accounting.create_public_from_accounting_bill(r.id);
|
||||
end loop;
|
||||
end $$;
|
||||
Reference in New Issue
Block a user