Accounting: Sales Receipts, COA sync to dashboard, vendor-expense recognition

- Add Sales Receipts page (dashboard/accounting/sales-receipts): records a
  cash sale (name, address, income account, price, qty) — deposits and books
  income in one step via a transaction. New accounting.sales_receipts table.
- Sync chart of accounts to the accounting dashboard: mirror accounting.accounts
  into public.chart_of_accounts for platform associations (one-way, same id) so
  Bill Approvals and every COA consumer use the dashboard's accounts. Legacy
  rows hidden; Bill Approvals made system-aware.
- Vendor-expense recognition: a vendor payment with no bill now books the
  expense directly (Dr Expense / Cr Bank) on the payment date instead of going
  to A/P; payments against open bills still clear A/P (applied FIFO). Backfill
  reclassifies unbilled payments stuck in A/P. Expense Summary report made
  GL-driven so it follows the same rule.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-04 10:01:18 -04:00
parent bd5caf5415
commit d82466f826
14 changed files with 688 additions and 24 deletions
+14 -5
View File
@@ -172,8 +172,8 @@ export default function BillApprovalsPage({ boardAssociationIds }: { boardAssoci
setBills([]);
setApprovalsByBill({});
const [aRes2, coaRes2, vRes2] = await Promise.all([
supabase.from("associations").select("id, name").eq("status", "active").order("name"),
supabase.from("chart_of_accounts").select("id, account_name, account_number, account_type").eq("account_type", "expense").order("account_number"),
supabase.from("associations").select("id, name, accounting_system").eq("status", "active").order("name"),
supabase.from("chart_of_accounts").select("id, account_name, account_number, account_type, accounting_system, association_id").eq("account_type", "expense").eq("is_active", true).order("account_number"),
supabase.from("vendors").select("id, name, address, association_id, association_ids").eq("is_active", true).order("name"),
]);
setAssociations(aRes2.data || []);
@@ -201,8 +201,8 @@ export default function BillApprovalsPage({ boardAssociationIds }: { boardAssoci
if (data.length < PAGE) break;
}
const [aRes, coaRes, vRes] = await Promise.all([
supabase.from("associations").select("id, name").eq("status", "active").order("name"),
supabase.from("chart_of_accounts").select("id, account_name, account_number, account_type").eq("account_type", "expense").order("account_number"),
supabase.from("associations").select("id, name, accounting_system").eq("status", "active").order("name"),
supabase.from("chart_of_accounts").select("id, account_name, account_number, account_type, accounting_system, association_id").eq("account_type", "expense").eq("is_active", true).order("account_number"),
supabase.from("vendors").select("id, name, address, association_id, association_ids").eq("is_active", true).order("name"),
]);
@@ -604,8 +604,17 @@ export default function BillApprovalsPage({ boardAssociationIds }: { boardAssoci
return bill.status;
};
const selectedSystem = associations.find((a: any) => a.id === form.association_id)?.accounting_system ?? null;
const filteredAccounts = form.association_id
? accounts.filter((a: any) => !a.association_id || a.association_id === form.association_id)
? accounts.filter((a: any) => {
// Platform associations use the accounts managed in the accounting
// dashboard (synced into chart_of_accounts as accounting_system 'platform').
if (selectedSystem === "platform") {
return a.accounting_system === "platform" && a.association_id === form.association_id;
}
// Buildium / Zoho keep their existing scoping; never surface platform-only rows.
return a.accounting_system !== "platform" && (!a.association_id || a.association_id === form.association_id);
})
: [];
const filteredVendors = form.association_id