-- §1.5 conformance: Accounts Receivable must be the OPEN balance (invoices net of -- payments applied), not gross invoiced. Previously invoices debited A/R but only -- payments_received credited it — so native invoices marked paid (paid_amount set, -- no payment row) left A/R overstated (recon R7 failed). -- -- Fix: settle A/R from invoice.paid_amount (the canonical "payments applied"): -- invoice -> Dr A/R / Cr income (acmacc_inv) -- invoice settled -> Dr Undeposited / Cr A/R (acmacc_invpay, = paid_amount) -- Payments are the cash sub-ledger only; they no longer post a separate A/R credit -- (that would double-count, since paid_amount already reflects applied payments). -- Net A/R control = total invoiced − total paid = open balance. Bills already -- settle (Dr A/P / Cr bank), so A/P was already correct. create or replace function accounting.post_invoice_gl(_invoice_id uuid) returns void language plpgsql security definer set search_path to 'public','accounting' as $$ declare i accounting.invoices%rowtype; _ar uuid; _inc uuid; _cash uuid; _je uuid; begin select * into i from accounting.invoices where id=_invoice_id; if not found then return; end if; perform accounting._gl_clear(i.company_id, 'acmacc_inv', i.id::text); perform accounting._gl_clear(i.company_id, 'acmacc_invpay', i.id::text); if not accounting.gl_managed(i.company_id) then return; end if; if coalesce(i.total,0) = 0 or i.status = 'void' then return; end if; _ar := accounting.coa_ar(i.company_id); _inc := accounting.coa_income_for(i.company_id, coalesce(nullif(i.notes,''), i.number)); -- Billing: Dr A/R / Cr income (gross) insert into accounting.journal_entries (company_id, date, description, reference, external_source, external_id) values (i.company_id, i.issue_date, coalesce(nullif(i.notes,''), 'Invoice ' || i.number), i.number, 'acmacc_inv', i.id::text) returning id into _je; insert into accounting.journal_entry_lines (journal_entry_id, account_id, debit, credit, description) values (_je, _ar, i.total, 0, 'Invoice ' || i.number), (_je, _inc, 0, i.total, 'Invoice ' || i.number); -- Settlement: Dr Undeposited / Cr A/R for the amount paid (open balance falls out) if coalesce(i.paid_amount,0) > 0 then _cash := accounting.coa_undeposited(i.company_id); insert into accounting.journal_entries (company_id, date, description, reference, external_source, external_id) values (i.company_id, coalesce(i.updated_at::date, i.issue_date), 'Payment on Invoice ' || i.number, i.number, 'acmacc_invpay', i.id::text) returning id into _je; insert into accounting.journal_entry_lines (journal_entry_id, account_id, debit, credit, description) values (_je, _cash, i.paid_amount, 0, 'Payment on Invoice ' || i.number), (_je, _ar, 0, i.paid_amount, 'Payment on Invoice ' || i.number); end if; end$$; -- Payments are the cash/undeposited sub-ledger only; A/R settlement is posted from -- invoice.paid_amount above. Drop any legacy payment JE to avoid double-crediting A/R. create or replace function accounting.post_payment_gl(_payment_id uuid) returns void language plpgsql security definer set search_path to 'public','accounting' as $$ declare p accounting.payments_received%rowtype; begin select * into p from accounting.payments_received where id=_payment_id; if not found then return; end if; perform accounting._gl_clear(p.company_id, 'acmacc_pay', p.id::text); end$$; -- Re-post managed companies: invoices gain the settlement leg; payments drop theirs. do $$ declare r record; begin for r in select i.id from accounting.invoices i join accounting.companies c on c.id=i.company_id where accounting.gl_managed(c.id) loop perform accounting.post_invoice_gl(r.id); end loop; for r in select p.id from accounting.payments_received p join accounting.companies c on c.id=p.company_id where accounting.gl_managed(c.id) loop perform accounting.post_payment_gl(r.id); end loop; end$$;