diff --git a/src/pages/accounting/AccountingBankingPage.tsx b/src/pages/accounting/AccountingBankingPage.tsx index 3ccf99e..d4b5b6e 100644 --- a/src/pages/accounting/AccountingBankingPage.tsx +++ b/src/pages/accounting/AccountingBankingPage.tsx @@ -13,7 +13,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, SelectGr import { Table, TableHeader, TableRow, TableHead, TableBody, TableCell } from "@/components/ui/table"; import { Switch } from "@/components/ui/switch"; import { Checkbox } from "@/components/ui/checkbox"; -import { Plus, Trash2, Pencil, ArrowLeftRight, Printer, Link2, RefreshCw, Unlink, FileUp, Download, Loader2 } from "lucide-react"; +import { Plus, Trash2, Pencil, ArrowLeftRight, Printer, Link2, RefreshCw, Unlink, FileUp, Download, Loader2, Ban } from "lucide-react"; import { toast } from "sonner"; import { money, fmtDate } from "./lib/format"; import { PeriodPicker, periodRange, type PeriodPreset } from "./components/PeriodPicker"; @@ -206,8 +206,11 @@ export default function AccountingBankingPage() { let bal = 0; return (txs as any[]).map((tx) => { const amt = Number(tx.amount ?? 0); - if (tx.type === "credit") bal += amt; - else bal -= amt; + // Voided rows stay visible for audit but don't move the running balance. + if (!tx.voided) { + if (tx.type === "credit") bal += amt; + else bal -= amt; + } return { ...tx, running: bal }; }); }, [txs]); @@ -482,6 +485,21 @@ export default function AccountingBankingPage() { qc.invalidateQueries({ queryKey: ["accounts", cid] }); }; + const toggleVoidTx = async (tx: any) => { + if (tx.reconciliation_id) return toast.error("Reconciled transactions can't be voided"); + const next = !tx.voided; + if (next && !confirm("Void this transaction? It stays in the register for audit but is removed from the balance and can't be reconciled.")) return; + const patch = next + ? { voided: true, voided_at: new Date().toISOString(), voided_by: user?.id ?? null } + : { voided: false, voided_at: null, voided_by: null }; + const q = accounting.from("transactions").update(patch); + const { error } = tx.transfer_id ? await q.eq("transfer_id", tx.transfer_id) : await q.eq("id", tx.id); + if (error) return toast.error(error.message); + toast.success(next ? "Transaction voided" : "Void removed"); + qc.invalidateQueries({ queryKey: ["transactions", cid] }); + qc.invalidateQueries({ queryKey: ["accounts", cid] }); + }; + const openEdit = (tx: any) => { if (tx.reconciliation_id) return toast.error("Reconciled transactions can't be edited"); setEditId(tx.id); @@ -653,8 +671,8 @@ export default function AccountingBankingPage() { ? "Edit transaction" : (txForm.type === "debit" ? "New payment" : "New transaction"); - const totalDebits = filteredRegister.reduce((s, r) => s + (r.type === "debit" ? Number(r.amount) : 0), 0); - const totalCredits = filteredRegister.reduce((s, r) => s + (r.type === "credit" ? Number(r.amount) : 0), 0); + const totalDebits = filteredRegister.reduce((s, r) => s + (!r.voided && r.type === "debit" ? Number(r.amount) : 0), 0); + const totalCredits = filteredRegister.reduce((s, r) => s + (!r.voided && r.type === "credit" ? Number(r.amount) : 0), 0); if (!associationId) return

Select an association.

; if (companyLoading) return
; @@ -849,14 +867,17 @@ export default function AccountingBankingPage() { {row.reconciliation_id && ( R )} - {row.description} + {row.voided && ( + VOID + )} + {row.description} {row.category ?? "—"} - + {row.type === "debit" ? money(row.amount, cur) : ""} - + {row.type === "credit" ? money(row.amount, cur) : ""} @@ -864,9 +885,17 @@ export default function AccountingBankingPage() {
- + {!row.voided && ( + + )} + {!row.reconciliation_id && ( + + )} diff --git a/src/pages/accounting/AccountingReconcileDetailPage.tsx b/src/pages/accounting/AccountingReconcileDetailPage.tsx index ed907b1..684ff2b 100644 --- a/src/pages/accounting/AccountingReconcileDetailPage.tsx +++ b/src/pages/accounting/AccountingReconcileDetailPage.tsx @@ -18,7 +18,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { Table, TableHeader, TableRow, TableHead, TableBody, TableCell } from "@/components/ui/table"; -import { ArrowLeft, CheckCircle2, AlertTriangle, FileDown, Search, Loader2, ArrowUp, ArrowDown, ChevronsUpDown, Plus } from "lucide-react"; +import { ArrowLeft, CheckCircle2, AlertTriangle, FileDown, Search, Loader2, ArrowUp, ArrowDown, ChevronsUpDown, Plus, Ban } from "lucide-react"; import { cn } from "@/lib/utils"; import { toast } from "sonner"; import { money, fmtDate } from "./lib/format"; @@ -52,6 +52,7 @@ type Tx = { type: "debit" | "credit"; cleared: boolean; reconciliation_id: string | null; + voided?: boolean; }; export default function AccountingReconcileDetailPage() { @@ -139,15 +140,18 @@ export default function AccountingReconcileDetailPage() { queryKey: ["recon-txs", accountId, active?.statement_end_date, priorReconDate], enabled: !!accountId && !!active, queryFn: async () => { - let q = accounting + // Show every item not yet tied to a completed reconciliation, on/before the + // statement date — INCLUDING outstanding items from prior periods. Items + // that were finalized get a reconciliation_id and drop off; uncleared ones + // carry forward until they clear. Voided items are excluded entirely. + const { data } = await accounting .from("transactions") - .select("id,date,description,reference,amount,type,cleared,reconciliation_id") + .select("id,date,description,reference,amount,type,cleared,reconciliation_id,voided") .eq("account_id", accountId) .is("reconciliation_id", null) - .lte("date", active!.statement_end_date); - // Only this period: skip anything already covered by the last reconciliation. - if (priorReconDate) q = q.gt("date", priorReconDate); - const { data } = await q.order("date"); + .eq("voided", false) + .lte("date", active!.statement_end_date) + .order("date"); return (data ?? []) as Tx[]; }, }); @@ -378,6 +382,19 @@ export default function AccountingReconcileDetailPage() { } }; + const voidTx = async (t: Tx) => { + if (t.reconciliation_id) return toast.error("Reconciled transactions can't be voided"); + if (!confirm(`Void this ${t.type === "credit" ? "deposit" : "withdrawal"} of ${money(t.amount, cur)}? It will be removed from the register and this reconciliation.`)) return; + const { error } = await accounting.from("transactions") + .update({ voided: true, voided_at: new Date().toISOString(), voided_by: user?.id ?? null }) + .eq("id", t.id); + if (error) return toast.error(error.message); + setChecked((prev) => { const n = new Set(prev); n.delete(t.id); return n; }); + toast.success("Transaction voided"); + qc.invalidateQueries({ queryKey: ["recon-txs", accountId] }); + qc.invalidateQueries({ queryKey: ["transactions", cid] }); + }; + if (!associationId) return

Select an association.

; if (companyLoading) return
; if (companyError || !companyId) return

{companyError || "Accounting setup is not ready."}

; @@ -454,6 +471,7 @@ export default function AccountingReconcileDetailPage() { + Void @@ -485,11 +503,17 @@ export default function AccountingReconcileDetailPage() { {t.type === "debit" ? money(t.amount, cur) : ""} + + + ); })} {filtered.length === 0 && ( - + No unreconciled transactions in this period. )} diff --git a/src/pages/accounting/AccountingReportsPage.tsx b/src/pages/accounting/AccountingReportsPage.tsx index 817a53c..bbf3b61 100644 --- a/src/pages/accounting/AccountingReportsPage.tsx +++ b/src/pages/accounting/AccountingReportsPage.tsx @@ -657,22 +657,22 @@ export default function AccountingReportsPage({ association }: { association?: { )} {active === "trial-balance" && ( - + )} {active === "general-ledger" && ( - + )} {active === "reserve-fund" && ( - + )} {active === "ar-aging-property" && ( - + )} {active === "prepaid-homeowners" && ( - + )} {active === "cash-disbursement" && ( - + )} {active === "reconciliation" && ( diff --git a/src/pages/accounting/components/ARAgingPropertyReport.tsx b/src/pages/accounting/components/ARAgingPropertyReport.tsx index fc38403..99073a2 100644 --- a/src/pages/accounting/components/ARAgingPropertyReport.tsx +++ b/src/pages/accounting/components/ARAgingPropertyReport.tsx @@ -1,4 +1,4 @@ -import { useMemo, useState } from "react"; +import { useEffect, useMemo, useState } from "react"; import { useQuery } from "@tanstack/react-query"; import { supabase } from "@/integrations/supabase/client"; import { Card, CardContent } from "@/components/ui/card"; @@ -56,8 +56,9 @@ const dash = (n: number) => (n ? money(n) : "-"); * collection status, summary and distribution. Payments and credits apply to * charges oldest-first (FIFO), so only genuinely open charge amounts age. */ -export function ARAgingPropertyReport({ companyId, companyName, logoUrl }: { companyId: string; companyName: string; logoUrl?: string | null }) { - const [asOf, setAsOf] = useState(() => new Date().toISOString().slice(0, 10)); +export function ARAgingPropertyReport({ companyId, companyName, logoUrl, to: propTo }: { companyId: string; companyName: string; logoUrl?: string | null; to?: string }) { + const [asOf, setAsOf] = useState(() => propTo ?? new Date().toISOString().slice(0, 10)); + useEffect(() => { if (propTo) setAsOf(propTo); }, [propTo]); const { data, isLoading } = useQuery({ queryKey: ["ar-aging-property", companyId, asOf], @@ -305,7 +306,7 @@ export function ARAgingPropertyReport({ companyId, companyName, logoUrl }: { com
- setAsOf(e.target.value || asOf)} className="w-44 mt-1" /> +
{asOfLabel}
{report && report.rows.length > 0 && (
diff --git a/src/pages/accounting/components/CashDisbursementReport.tsx b/src/pages/accounting/components/CashDisbursementReport.tsx index 7cee68e..15a2e0b 100644 --- a/src/pages/accounting/components/CashDisbursementReport.tsx +++ b/src/pages/accounting/components/CashDisbursementReport.tsx @@ -1,4 +1,4 @@ -import { useMemo, useState } from "react"; +import { useEffect, useMemo, useState } from "react"; import { useQuery } from "@tanstack/react-query"; import { accounting } from "@/lib/accountingClient"; import { Card, CardContent } from "@/components/ui/card"; @@ -48,9 +48,10 @@ const acctLabel = (a: GLAccount | undefined) => (a ? `${a.code ? a.code + " - " * platform-managed and Buildium-imported companies. Platform entries are * enriched with check #, vendor and bill info from the banking register. */ -export function CashDisbursementReport({ companyId, companyName, logoUrl }: { companyId: string; companyName: string; logoUrl?: string | null }) { - const [from, setFrom] = useState(monthStart()); - const [to, setTo] = useState(today()); +export function CashDisbursementReport({ companyId, companyName, logoUrl, from: propFrom, to: propTo }: { companyId: string; companyName: string; logoUrl?: string | null; from?: string; to?: string }) { + const [from, setFrom] = useState(propFrom ?? monthStart()); + const [to, setTo] = useState(propTo ?? today()); + useEffect(() => { if (propFrom) setFrom(propFrom); if (propTo) setTo(propTo); }, [propFrom, propTo]); const { data, isLoading } = useQuery({ queryKey: ["cash-disbursement", companyId, from, to], @@ -249,12 +250,8 @@ export function CashDisbursementReport({ companyId, companyName, logoUrl }: { co
- - setFrom(e.target.value || from)} className="w-44 mt-1" /> -
-
- - setTo(e.target.value || to)} className="w-44 mt-1" /> + +
{fmtDate(from)} – {fmtDate(to)}
{hasData && (
diff --git a/src/pages/accounting/components/GeneralLedgerReport.tsx b/src/pages/accounting/components/GeneralLedgerReport.tsx index 8ec2335..3365e29 100644 --- a/src/pages/accounting/components/GeneralLedgerReport.tsx +++ b/src/pages/accounting/components/GeneralLedgerReport.tsx @@ -38,10 +38,11 @@ type Txn = { debit: number; credit: number; balance: number; abnormal?: boolean; }; -export function GeneralLedgerReport({ companyId, companyName, logoUrl, initialAccountId }: { companyId: string; companyName: string; logoUrl?: string | null; initialAccountId?: string | null }) { +export function GeneralLedgerReport({ companyId, companyName, logoUrl, initialAccountId, from: propFrom, to: propTo }: { companyId: string; companyName: string; logoUrl?: string | null; initialAccountId?: string | null; from?: string; to?: string }) { const [preset, setPreset] = useState("month"); - const [from, setFrom] = useState(() => periodRange("month").from); - const [to, setTo] = useState(() => periodRange("month").to); + const [from, setFrom] = useState(() => propFrom ?? periodRange("month").from); + const [to, setTo] = useState(() => propTo ?? periodRange("month").to); + useEffect(() => { if (propFrom) setFrom(propFrom); if (propTo) setTo(propTo); }, [propFrom, propTo]); const [selectedAccounts, setSelectedAccounts] = useState(initialAccountId ? [initialAccountId] : []); // When opened via a report drill-down, focus the chosen account. @@ -287,10 +288,10 @@ export function GeneralLedgerReport({ companyId, companyName, logoUrl, initialAc
- { setPreset(n.preset); setFrom(n.from); setTo(n.to); }} - /> +
+ +
{fmtDate(from)} – {fmtDate(to)}
+
@@ -323,11 +324,7 @@ export function GeneralLedgerReport({ companyId, companyName, logoUrl, initialAc
-
- Accrual - setBasis(v ? "cash" : "accrual")} /> - Cash -
+
Accrual
diff --git a/src/pages/accounting/components/PrepaidHomeownersReport.tsx b/src/pages/accounting/components/PrepaidHomeownersReport.tsx index 7b168d5..8d43906 100644 --- a/src/pages/accounting/components/PrepaidHomeownersReport.tsx +++ b/src/pages/accounting/components/PrepaidHomeownersReport.tsx @@ -1,4 +1,4 @@ -import { useMemo, useState } from "react"; +import { useEffect, useMemo, useState } from "react"; import { useQuery } from "@tanstack/react-query"; import { Card, CardContent } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; @@ -22,8 +22,9 @@ type Row = { account: string; property: string; ownerName: string; credit: numbe * Buildium-style "Pre Paid Homeowners": every unit whose owner ledger nets to * a credit (payments exceed charges) as of the chosen date, with the credit amount. */ -export function PrepaidHomeownersReport({ companyId, companyName, logoUrl }: { companyId: string; companyName: string; logoUrl?: string | null }) { - const [asOf, setAsOf] = useState(() => new Date().toISOString().slice(0, 10)); +export function PrepaidHomeownersReport({ companyId, companyName, logoUrl, to: propTo }: { companyId: string; companyName: string; logoUrl?: string | null; to?: string }) { + const [asOf, setAsOf] = useState(() => propTo ?? new Date().toISOString().slice(0, 10)); + useEffect(() => { if (propTo) setAsOf(propTo); }, [propTo]); const { data, isLoading } = useQuery({ queryKey: ["prepaid-homeowners", companyId, asOf], @@ -126,7 +127,7 @@ export function PrepaidHomeownersReport({ companyId, companyName, logoUrl }: { c
- setAsOf(e.target.value || asOf)} className="w-44 mt-1" /> +
{asOfLabel}
{rows.length > 0 && (
diff --git a/src/pages/accounting/components/ReserveFundReport.tsx b/src/pages/accounting/components/ReserveFundReport.tsx index 2e51c71..b70f3ff 100644 --- a/src/pages/accounting/components/ReserveFundReport.tsx +++ b/src/pages/accounting/components/ReserveFundReport.tsx @@ -1,4 +1,4 @@ -import { useMemo, useState } from "react"; +import { useEffect, useMemo, useState } from "react"; import { useQuery } from "@tanstack/react-query"; import { accounting } from "@/lib/accountingClient"; import { Card, CardContent } from "@/components/ui/card"; @@ -61,13 +61,16 @@ export function ReserveFundReport({ companyName, fiscalYearStart = "01-01", logoUrl, + to: propTo, }: { companyId: string; companyName: string; fiscalYearStart?: string; logoUrl?: string | null; + to?: string; }) { - const [asOfMonth, setAsOfMonth] = useState(() => new Date().toISOString().slice(0, 7)); + const [asOfMonth, setAsOfMonth] = useState(() => (propTo ?? new Date().toISOString().slice(0, 10)).slice(0, 7)); + useEffect(() => { if (propTo) setAsOfMonth(propTo.slice(0, 7)); }, [propTo]); const asOf = monthEnd(asOfMonth); const monthStart = monthStartOf(asOfMonth); @@ -270,7 +273,7 @@ export function ReserveFundReport({
- setAsOfMonth(e.target.value || asOfMonth)} className="w-44 mt-1" /> +
{asOfLabel}
Fiscal year from {new Date(fyStart + "T00:00:00").toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" })} diff --git a/src/pages/accounting/components/TrialBalanceReport.tsx b/src/pages/accounting/components/TrialBalanceReport.tsx index 8be90bf..2131840 100644 --- a/src/pages/accounting/components/TrialBalanceReport.tsx +++ b/src/pages/accounting/components/TrialBalanceReport.tsx @@ -1,4 +1,4 @@ -import { useMemo, useState } from "react"; +import { useEffect, useMemo, useState } from "react"; import { useQuery } from "@tanstack/react-query"; import { accounting } from "@/lib/accountingClient"; import { Card, CardContent } from "@/components/ui/card"; @@ -52,8 +52,9 @@ function splitDebitCredit(a: Account): { debit: number; credit: number } { return bal >= 0 ? { debit: 0, credit: bal } : { debit: -bal, credit: 0 }; } -export function TrialBalanceReport({ companyId, companyName, logoUrl }: { companyId: string; companyName: string; logoUrl?: string | null }) { - const [asOf, setAsOf] = useState(() => new Date().toISOString().slice(0, 10)); +export function TrialBalanceReport({ companyId, companyName, logoUrl, to: propTo }: { companyId: string; companyName: string; logoUrl?: string | null; to?: string }) { + const [asOf, setAsOf] = useState(() => propTo ?? new Date().toISOString().slice(0, 10)); + useEffect(() => { if (propTo) setAsOf(propTo); }, [propTo]); const [basis, setBasis] = useState<"accrual" | "cash">("accrual"); const [showZero, setShowZero] = useState(false); const [typeFilter, setTypeFilter] = useState<"all" | Account["type"]>("all"); @@ -210,17 +211,11 @@ export function TrialBalanceReport({ companyId, companyName, logoUrl }: { compan
- setAsOf(e.target.value)} className="w-44 mt-1" /> +
{fmtDate(asOf)}
- +
Accrual
diff --git a/supabase/migrations/20260613150000_transactions_void_support.sql b/supabase/migrations/20260613150000_transactions_void_support.sql new file mode 100644 index 0000000..9eb9101 --- /dev/null +++ b/supabase/migrations/20260613150000_transactions_void_support.sql @@ -0,0 +1,94 @@ +-- Void support for bank-register transactions. Voided rows stay for audit but +-- are excluded from the cached account balance and, for gl_managed companies, +-- reversed out of the GL (post_transaction_gl clears + posts nothing when voided). +alter table accounting.transactions + add column if not exists voided boolean not null default false, + add column if not exists voided_at timestamptz, + add column if not exists voided_by uuid; + +-- post_transaction_gl: a voided txn clears its prior GL entry and posts nothing. +create or replace function accounting.post_transaction_gl(_txn_id uuid) + returns void language plpgsql security definer set search_path to 'public', 'accounting' +as $function$ +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 coalesce(t.voided, false) then return; end if; + 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) + when t.coa_account_id is not null then t.coa_account_id + when t.vendor_id is not null then accounting.coa_ap(t.company_id) + 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 + 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 + 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$function$; + +-- sync_account_balance: exclude voided transactions from every cached-balance sum. +-- (Full body re-created so each SUM filters COALESCE(voided,false)=false.) +create or replace function accounting.sync_account_balance() + returns trigger language plpgsql security definer set search_path to 'public' +as $function$ +DECLARE v_computed numeric; +BEGIN + IF TG_OP IN ('INSERT','UPDATE') THEN + IF NEW.account_id IS NOT NULL THEN + SELECT COALESCE(SUM(CASE WHEN type='credit' THEN amount ELSE -amount END),0) INTO v_computed + FROM accounting.transactions WHERE account_id=NEW.account_id AND COALESCE(voided,false)=false; + UPDATE accounting.accounts SET balance=v_computed, updated_at=now() WHERE id=NEW.account_id; + END IF; + IF NEW.coa_account_id IS NOT NULL THEN + SELECT COALESCE(SUM(CASE WHEN a.type IN ('expense','asset') THEN CASE WHEN t.type='debit' THEN t.amount ELSE -t.amount END + ELSE CASE WHEN t.type='credit' THEN t.amount ELSE -t.amount END END),0) INTO v_computed + FROM accounting.transactions t JOIN accounting.accounts a ON a.id=t.coa_account_id + WHERE t.coa_account_id=NEW.coa_account_id AND COALESCE(t.voided,false)=false; + UPDATE accounting.accounts SET balance=v_computed, updated_at=now() WHERE id=NEW.coa_account_id; + END IF; + END IF; + IF TG_OP='UPDATE' THEN + IF OLD.account_id IS NOT NULL AND OLD.account_id IS DISTINCT FROM NEW.account_id THEN + SELECT COALESCE(SUM(CASE WHEN type='credit' THEN amount ELSE -amount END),0) INTO v_computed + FROM accounting.transactions WHERE account_id=OLD.account_id AND COALESCE(voided,false)=false; + UPDATE accounting.accounts SET balance=v_computed, updated_at=now() WHERE id=OLD.account_id; + END IF; + IF OLD.coa_account_id IS NOT NULL AND OLD.coa_account_id IS DISTINCT FROM NEW.coa_account_id THEN + SELECT COALESCE(SUM(CASE WHEN a.type IN ('expense','asset') THEN CASE WHEN t.type='debit' THEN t.amount ELSE -t.amount END + ELSE CASE WHEN t.type='credit' THEN t.amount ELSE -t.amount END END),0) INTO v_computed + FROM accounting.transactions t JOIN accounting.accounts a ON a.id=t.coa_account_id + WHERE t.coa_account_id=OLD.coa_account_id AND COALESCE(t.voided,false)=false; + UPDATE accounting.accounts SET balance=v_computed, updated_at=now() WHERE id=OLD.coa_account_id; + END IF; + END IF; + IF TG_OP='DELETE' THEN + IF OLD.account_id IS NOT NULL THEN + SELECT COALESCE(SUM(CASE WHEN type='credit' THEN amount ELSE -amount END),0) INTO v_computed + FROM accounting.transactions WHERE account_id=OLD.account_id AND COALESCE(voided,false)=false; + UPDATE accounting.accounts SET balance=v_computed, updated_at=now() WHERE id=OLD.account_id; + END IF; + IF OLD.coa_account_id IS NOT NULL THEN + SELECT COALESCE(SUM(CASE WHEN a.type IN ('expense','asset') THEN CASE WHEN t.type='debit' THEN t.amount ELSE -t.amount END + ELSE CASE WHEN t.type='credit' THEN t.amount ELSE -t.amount END END),0) INTO v_computed + FROM accounting.transactions t JOIN accounting.accounts a ON a.id=t.coa_account_id + WHERE t.coa_account_id=OLD.coa_account_id AND COALESCE(t.voided,false)=false; + UPDATE accounting.accounts SET balance=v_computed, updated_at=now() WHERE id=OLD.coa_account_id; + END IF; + RETURN OLD; + END IF; + RETURN NEW; +END; +$function$;