mirror of
https://github.com/renee-png/acmcc.git
synced 2026-06-21 01:40:01 +00:00
Merge pull request #14 from renee-png/bs-include-archived-accounts-with-balance
Reports: keep archived accounts with a balance on the financial statements
This commit is contained in:
@@ -101,17 +101,20 @@ function shiftBack(from: string, to: string) {
|
|||||||
// falls past row 1000 silently read as $0). Order by a stable key (id) so the
|
// falls past row 1000 silently read as $0). Order by a stable key (id) so the
|
||||||
// pages don't overlap or skip rows.
|
// pages don't overlap or skip rows.
|
||||||
const GL_PAGE = 1000;
|
const GL_PAGE = 1000;
|
||||||
async function fetchAllGLLines(cid: string, to: string, select: string, from?: string): Promise<any[]> {
|
async function fetchAllGLLines(cid: string, to: string, select: string, from?: string, includeArchived = false): Promise<any[]> {
|
||||||
const out: any[] = [];
|
const out: any[] = [];
|
||||||
for (let offset = 0; ; offset += GL_PAGE) {
|
for (let offset = 0; ; offset += GL_PAGE) {
|
||||||
let q = accounting
|
let q = accounting
|
||||||
.from("journal_entry_lines")
|
.from("journal_entry_lines")
|
||||||
.select(select)
|
.select(select)
|
||||||
.eq("journal_entries.company_id", cid)
|
.eq("journal_entries.company_id", cid)
|
||||||
// Archived accounts stay off the financial statements (their history
|
|
||||||
// remains visible in the General Ledger report, which queries directly).
|
|
||||||
.eq("accounts.is_archived", false)
|
|
||||||
.lte("journal_entries.date", to);
|
.lte("journal_entries.date", to);
|
||||||
|
// Archived accounts are normally kept off the financial statements, BUT an
|
||||||
|
// archived account that still holds a balance MUST stay on them — otherwise
|
||||||
|
// its balance is silently dropped and the Balance Sheet goes out of balance
|
||||||
|
// by exactly that amount. Including archived accounts here is safe: this
|
||||||
|
// queries journal lines, so archived accounts with no activity never appear.
|
||||||
|
if (!includeArchived) q = q.eq("accounts.is_archived", false);
|
||||||
if (from) q = q.gte("journal_entries.date", from);
|
if (from) q = q.gte("journal_entries.date", from);
|
||||||
const { data, error } = await q.order("id", { ascending: true }).range(offset, offset + GL_PAGE - 1);
|
const { data, error } = await q.order("id", { ascending: true }).range(offset, offset + GL_PAGE - 1);
|
||||||
if (error) throw error;
|
if (error) throw error;
|
||||||
@@ -140,9 +143,9 @@ export async function fetchReportData(cid: string, from: string, to: string) {
|
|||||||
// All bills (not date-filtered) for AP aging
|
// All bills (not date-filtered) for AP aging
|
||||||
accounting.from("bills").select("id,vendor_id,total,paid_amount,status,due_date,issue_date,vendors(id,name)").eq("company_id", cid),
|
accounting.from("bills").select("id,vendor_id,total,paid_amount,status,due_date,issue_date,vendors(id,name)").eq("company_id", cid),
|
||||||
// General-ledger lines in period — P&L is built from these, grouped by account
|
// General-ledger lines in period — P&L is built from these, grouped by account
|
||||||
fetchAllGLLines(cid, to, "id,debit,credit,accounts!inner(id,name,code,type,parent_account_id),journal_entries!inner(company_id,date)", from),
|
fetchAllGLLines(cid, to, "id,debit,credit,accounts!inner(id,name,code,type,parent_account_id),journal_entries!inner(company_id,date)", from, true),
|
||||||
// Cumulative GL through `to` — Balance Sheet is built from these (as-of balances)
|
// Cumulative GL through `to` — Balance Sheet is built from these (as-of balances)
|
||||||
fetchAllGLLines(cid, to, "id,debit,credit,account_id,accounts!inner(type),journal_entries!inner(company_id,date)"),
|
fetchAllGLLines(cid, to, "id,debit,credit,account_id,accounts!inner(id,name,code,type),journal_entries!inner(company_id,date)", undefined, true),
|
||||||
// All invoices (not date-filtered) — Accounts Receivable = unpaid invoices
|
// All invoices (not date-filtered) — Accounts Receivable = unpaid invoices
|
||||||
accounting.from("invoices").select("total,paid_amount,status").eq("company_id", cid),
|
accounting.from("invoices").select("total,paid_amount,status").eq("company_id", cid),
|
||||||
// Whether the platform manages this company's GL (A/R-A/P sub-ledgers tie to the GL).
|
// Whether the platform manages this company's GL (A/R-A/P sub-ledgers tie to the GL).
|
||||||
@@ -1629,10 +1632,25 @@ function buildBalanceSheet(d: any, p?: any, useCompare?: boolean): StructuredRep
|
|||||||
const accounts = (d.accounts ?? []) as any[];
|
const accounts = (d.accounts ?? []) as any[];
|
||||||
const cur = bsBalances(d);
|
const cur = bsBalances(d);
|
||||||
const prev = useCompare && p ? bsBalances(p) : undefined;
|
const prev = useCompare && p ? bsBalances(p) : undefined;
|
||||||
|
// Surface accounts missing from the active COA list (i.e. archived) that still
|
||||||
|
// carry a balance, so their balance lands on the statement instead of being
|
||||||
|
// dropped (which would unbalance the Balance Sheet). Archived income/expense
|
||||||
|
// accounts already flow into Net Income via bsBalances; asset/liability/equity
|
||||||
|
// ones must appear as line items here. Zero-balance accounts are not added.
|
||||||
|
const knownIds = new Set(accounts.map((a) => a.id));
|
||||||
|
const extraAccounts: any[] = [];
|
||||||
|
const seenExtra = new Set<string>();
|
||||||
|
for (const l of (d.glCumulative ?? []) as any[]) {
|
||||||
|
const id = l.account_id; const meta = l.accounts;
|
||||||
|
if (!meta || !id || knownIds.has(id) || seenExtra.has(id)) continue;
|
||||||
|
seenExtra.add(id);
|
||||||
|
if (Math.abs(cur.glByAcct.get(id) ?? 0) > 0.005) extraAccounts.push({ id, name: meta.name, code: meta.code, type: meta.type });
|
||||||
|
}
|
||||||
|
const allAccounts = extraAccounts.length ? [...accounts, ...extraAccounts] : accounts;
|
||||||
const balOf = (a: any) => (cur.glByAcct.get(a.id) ?? 0);
|
const balOf = (a: any) => (cur.glByAcct.get(a.id) ?? 0);
|
||||||
const balOfP = (a: any) => (prev ? (prev.glByAcct.get(a.id) ?? 0) : undefined);
|
const balOfP = (a: any) => (prev ? (prev.glByAcct.get(a.id) ?? 0) : undefined);
|
||||||
const cmp = (v: number | undefined) => (prev ? v : undefined);
|
const cmp = (v: number | undefined) => (prev ? v : undefined);
|
||||||
const byType = (t: string) => accounts.filter((a) => a.type === t);
|
const byType = (t: string) => allAccounts.filter((a) => a.type === t);
|
||||||
const sumBal = (rows: any[]) => rows.reduce((s, a) => s + balOf(a), 0);
|
const sumBal = (rows: any[]) => rows.reduce((s, a) => s + balOf(a), 0);
|
||||||
const sumBalP = (rows: any[]) => (prev ? rows.reduce((s, a) => s + (prev.glByAcct.get(a.id) ?? 0), 0) : undefined);
|
const sumBalP = (rows: any[]) => (prev ? rows.reduce((s, a) => s + (prev.glByAcct.get(a.id) ?? 0), 0) : undefined);
|
||||||
|
|
||||||
|
|||||||
@@ -526,6 +526,17 @@ Deno.serve(async (req) => {
|
|||||||
} else if (unmappedGl.size > 0) {
|
} else if (unmappedGl.size > 0) {
|
||||||
companyResult.watermark_held = true;
|
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 };
|
companyResult.window = { since, until };
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
companyResult.errors.push(e?.message || String(e));
|
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