mirror of
https://github.com/renee-png/acmcc.git
synced 2026-06-21 01:40:01 +00:00
Accounting: auto-create record-only bills for no-bill vendor payments (import mode)
Buildium often records vendor payments as direct checks (Dr Expense/Cr Bank) with no bill. For import-mode companies post_bill_gl no-ops, so a bill is a record only. New fn accounting.autocreate_nobill_vendor_bills() creates a paid record-only bill for each such buildium_gl payment (vendor resolved by payee, idempotent by external_id); buildium-gl-sync calls it per import-mode company after each pull. No GL impact, no double-count. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -526,6 +526,17 @@ Deno.serve(async (req) => {
|
||||
} else if (unmappedGl.size > 0) {
|
||||
companyResult.watermark_held = true;
|
||||
}
|
||||
|
||||
// Import-mode: surface no-bill vendor payments (Buildium direct checks that
|
||||
// hit an expense account with no A/P leg) as record-only bills. post_bill_gl
|
||||
// no-ops for gl_auto_post=false companies, so this never touches the GL.
|
||||
if (!dryRun && company.gl_auto_post === false) {
|
||||
const { data: madeBills, error: billErr } =
|
||||
await supabase.schema("accounting").rpc("autocreate_nobill_vendor_bills", { _company_id: company.id });
|
||||
if (billErr) companyResult.errors.push(`autocreate bills: ${billErr.message}`);
|
||||
else if (madeBills) companyResult.autobills_created = madeBills;
|
||||
}
|
||||
|
||||
companyResult.window = { since, until };
|
||||
} catch (e: any) {
|
||||
companyResult.errors.push(e?.message || String(e));
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
-- Auto-create record-only bills for no-bill vendor payments in IMPORT-MODE companies.
|
||||
--
|
||||
-- Buildium often records a vendor payment as a direct check (Dr Expense / Cr Bank)
|
||||
-- with no Bill behind it. For import-mode companies (gl_auto_post=false) the GL is
|
||||
-- the Buildium pull, and post_bill_gl no-ops, so a bill is a RECORD ONLY — creating
|
||||
-- one for such a payment surfaces it on the Bills page WITHOUT touching the GL (the
|
||||
-- expense is already booked once via the check). This makes A/P / vendor history
|
||||
-- complete without any double-count.
|
||||
--
|
||||
-- A "no-bill vendor payment" = a buildium_gl journal entry that: looks like a check
|
||||
-- /payment (description), debits >=1 expense account, credits >=1 asset account (the
|
||||
-- bank), and has NO accounts-payable line. Idempotent: skips entries that already
|
||||
-- have an acmacc_autobill_nobill bill. Vendor is resolved from the payee (text after
|
||||
-- the last "·" in the description) by exact name match; unmatched => bill with no
|
||||
-- vendor (still correct on the GL, fixable later). Safe to call after every GL pull.
|
||||
|
||||
create or replace function accounting.autocreate_nobill_vendor_bills(_company_id uuid)
|
||||
returns integer
|
||||
language plpgsql security definer set search_path to 'public','accounting'
|
||||
as $function$
|
||||
declare _n int := 0; je record; _bill uuid; _vendor uuid; _payee text;
|
||||
begin
|
||||
-- Import-mode only: in gl_managed companies bills POST (post_bill_gl) and would
|
||||
-- double-count; those use the direct-expense rule instead.
|
||||
if accounting.gl_managed(_company_id) then return 0; end if;
|
||||
|
||||
for je in
|
||||
select j.id, j.date, j.description,
|
||||
(select sum(l.debit) from accounting.journal_entry_lines l
|
||||
join accounting.accounts a on a.id=l.account_id
|
||||
where l.journal_entry_id=j.id and a.type='expense') as total
|
||||
from accounting.journal_entries j
|
||||
where j.company_id = _company_id
|
||||
and j.external_source = 'buildium_gl'
|
||||
and (j.description ilike 'Check%' or j.description ilike '%payment%')
|
||||
and exists (select 1 from accounting.journal_entry_lines l join accounting.accounts a on a.id=l.account_id
|
||||
where l.journal_entry_id=j.id and a.type='expense' and l.debit>0)
|
||||
and exists (select 1 from accounting.journal_entry_lines l2 join accounting.accounts a2 on a2.id=l2.account_id
|
||||
where l2.journal_entry_id=j.id and a2.type='asset' and l2.credit>0)
|
||||
and not exists (select 1 from accounting.journal_entry_lines l3 join accounting.accounts a3 on a3.id=l3.account_id
|
||||
where l3.journal_entry_id=j.id and a3.type='liability' and a3.name ilike '%payable%')
|
||||
and not exists (select 1 from accounting.bills b
|
||||
where b.company_id=_company_id and b.external_source='acmacc_autobill_nobill' and b.external_id=j.id::text)
|
||||
loop
|
||||
_payee := nullif(trim(regexp_replace(je.description, '^.*·\s*', '')), '');
|
||||
select id into _vendor from accounting.vendors
|
||||
where company_id=_company_id and _payee is not null and lower(name)=lower(_payee) limit 1;
|
||||
|
||||
insert into accounting.bills
|
||||
(company_id, vendor_id, number, issue_date, due_date, status, subtotal, tax, total, paid_amount,
|
||||
auto_created, external_source, external_id, notes)
|
||||
values
|
||||
(_company_id, _vendor, 'AUTOBILL-'||left(replace(je.id::text,'-',''),8), je.date, je.date, 'paid',
|
||||
coalesce(je.total,0), 0, coalesce(je.total,0), coalesce(je.total,0),
|
||||
true, 'acmacc_autobill_nobill', je.id::text, je.description)
|
||||
returning id into _bill;
|
||||
|
||||
insert into accounting.bill_items (bill_id, description, quantity, rate, amount, account_id)
|
||||
select _bill, a.name, 1, l.debit, l.debit, l.account_id
|
||||
from accounting.journal_entry_lines l join accounting.accounts a on a.id=l.account_id
|
||||
where l.journal_entry_id=je.id and a.type='expense' and l.debit>0;
|
||||
|
||||
_n := _n + 1;
|
||||
end loop;
|
||||
|
||||
return _n;
|
||||
end$function$;
|
||||
Reference in New Issue
Block a user