mirror of
https://github.com/renee-png/acmcc.git
synced 2026-06-21 09:50:01 +00:00
21224e400d
- post_transaction_gl counter priority fixed: customer->A/R, then coa->that account (direct categorized expense/income), then uncategorized vendor->A/P. Previously any vendor payment went to A/P, driving A/P negative and hiding the real expense account. The 38 direct vendor payments now post to their expense accounts; A/P went from -5,895.79 to +3,099.29 (real open payables). - sync_public_bill now maps the public bill's expense_account_id to the matching accounting expense account (by name) on the bill line item, so synced bills stop defaulting to "Administrative". Expenses now spread across Attorneys Fees, Electric, Lawn Service, Management Fees, Water, etc. - Verified managed companies: R1=0, R7=0. R8 surfaces a real $1,029.17 residual (bills marked paid whose payments were entered as direct expenses, not A/P settlements) — surfaced in the Reconciliation report, not plugged. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
124 lines
6.2 KiB
PL/PgSQL
124 lines
6.2 KiB
PL/PgSQL
-- A5 (expense classification) + A/P overpayment fix.
|
|
--
|
|
-- 1. post_transaction_gl counter priority was customer→AR, vendor→AP, coa→coa.
|
|
-- But direct vendor payments carry an expense category (coa_account_id); mapping
|
|
-- them to A/P drove A/P negative AND hid the real expense account. New priority:
|
|
-- customer→A/R (a receipt always settles A/R), then coa→that account (direct
|
|
-- categorized expense/income), then vendor with no category→A/P (bill settlement).
|
|
--
|
|
-- 2. Synced bills created a single bill_item with no account, so post_bill_gl lumped
|
|
-- every bill into the default "Administrative" expense. Map the public bill's
|
|
-- expense_account_id (public.chart_of_accounts) to the matching accounting expense
|
|
-- account by name and set it on the bill_item.
|
|
|
|
create or replace function accounting.post_transaction_gl(_txn_id uuid) returns void
|
|
language plpgsql security definer set search_path to 'public','accounting' as $$
|
|
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 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) -- receipt settles A/R
|
|
when t.coa_account_id is not null then t.coa_account_id -- direct categorized expense/income
|
|
when t.vendor_id is not null then accounting.coa_ap(t.company_id) -- uncategorized vendor payment = bill settlement
|
|
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 -- money in
|
|
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 -- money out
|
|
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$$;
|
|
|
|
create or replace function accounting.sync_public_bill(_bill_id uuid)
|
|
returns void
|
|
language plpgsql security definer set search_path to 'public','accounting'
|
|
as $$
|
|
declare
|
|
b public.bills%rowtype;
|
|
_company_id uuid; _vendor_id uuid; _status accounting.bill_status;
|
|
_paid numeric; _tot numeric; _acct_bill_id uuid; _exp_acct uuid; _exp_name text;
|
|
begin
|
|
select * into b from public.bills where id=_bill_id;
|
|
if not found then return; end if;
|
|
select id into _company_id from accounting.companies where association_id=b.association_id;
|
|
if _company_id is null then return; end if;
|
|
|
|
if not accounting.bill_should_mirror(b.status) then
|
|
delete from accounting.bills where company_id=_company_id and external_source='acmacc_bill' and external_id=b.id::text;
|
|
return;
|
|
end if;
|
|
|
|
_vendor_id := accounting.ensure_vendor_for_public(_company_id, b.vendor_id);
|
|
_paid := coalesce(b.amount_paid, 0);
|
|
_tot := coalesce(b.amount, 0);
|
|
_status := (case when _paid >= _tot and _tot > 0 then 'paid'
|
|
when _paid > 0 then 'partially_paid'
|
|
else 'open' end)::accounting.bill_status;
|
|
|
|
insert into accounting.bills
|
|
(company_id, vendor_id, number, issue_date, due_date, status, subtotal, tax, total,
|
|
notes, paid_amount, attachment_url, external_source, external_id)
|
|
values
|
|
(_company_id, _vendor_id,
|
|
coalesce(nullif(b.invoice_number,''), 'BILL-' || left(replace(b.id::text,'-',''),8)),
|
|
b.bill_date, b.due_date, _status, _tot, 0, _tot,
|
|
b.description, _paid, b.attachment_url, 'acmacc_bill', b.id::text)
|
|
on conflict (company_id, external_source, external_id) where external_id is not null
|
|
do update set vendor_id=excluded.vendor_id, number=excluded.number, issue_date=excluded.issue_date,
|
|
due_date=excluded.due_date, status=excluded.status, subtotal=excluded.subtotal,
|
|
total=excluded.total, notes=excluded.notes, paid_amount=excluded.paid_amount,
|
|
attachment_url=excluded.attachment_url, updated_at=now()
|
|
returning id into _acct_bill_id;
|
|
|
|
if _acct_bill_id is null then
|
|
select id into _acct_bill_id from accounting.bills
|
|
where company_id=_company_id and external_source='acmacc_bill' and external_id=b.id::text;
|
|
end if;
|
|
|
|
-- Map the bill's expense account (public COA name -> accounting expense account).
|
|
if b.expense_account_id is not null then
|
|
select account_name into _exp_name from public.chart_of_accounts where id=b.expense_account_id;
|
|
if _exp_name is not null then
|
|
_exp_acct := accounting.coa_get_or_create(_company_id, array[_exp_name], null, _exp_name, 'expense'::accounting.account_type);
|
|
end if;
|
|
end if;
|
|
|
|
delete from accounting.bill_items where bill_id=_acct_bill_id;
|
|
insert into accounting.bill_items (bill_id, description, quantity, rate, amount, account_id)
|
|
values (_acct_bill_id, coalesce(nullif(b.description,''), 'Bill ' || coalesce(b.invoice_number,'')), 1, _tot, _tot, _exp_acct);
|
|
|
|
-- Re-post the bill to the GL now that the line item's expense account is set.
|
|
perform accounting.post_bill_gl(_acct_bill_id);
|
|
end;
|
|
$$;
|
|
|
|
-- Backfill managed companies
|
|
do $$
|
|
declare r record;
|
|
begin
|
|
for r in select b.id from public.bills b
|
|
join accounting.companies c on c.association_id=b.association_id
|
|
where accounting.gl_managed(c.id) loop
|
|
perform accounting.sync_public_bill(r.id);
|
|
end loop;
|
|
for r in select t.id from accounting.transactions t
|
|
join accounting.companies c on c.id=t.company_id
|
|
where accounting.gl_managed(c.id) and t.transfer_id is null and t.deposit_id is null loop
|
|
perform accounting.post_transaction_gl(r.id);
|
|
end loop;
|
|
end$$; |