mirror of
https://github.com/renee-png/acmcc.git
synced 2026-06-21 09:50: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