mirror of
https://github.com/renee-png/acmcc.git
synced 2026-06-21 01:40:01 +00:00
Sync owner ledger + payments into Accounting
DB triggers on public.owner_ledger_entries (migration applied to prod): charges (debit) -> accounting.invoices; payments (credit) -> accounting.payments_received (deposited=false, Undeposited Funds). Customer balance recomputed authoritatively from the source ledger; ledger payments FIFO-applied to ledger invoices. Keyed external_source='acmacc_ledger'. Backfilled 6,756 invoices + 4,253 payments; balances reconcile exactly. Frontend: customer Ledger tab now renders real payments_received credits (true dates/amounts); Make Deposit page surfaces undeposited payments_received alongside Undeposited Funds transactions and deposits both. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -65,7 +65,7 @@ export default function AccountingCustomerDetailPage() {
|
||||
queryFn: async () => {
|
||||
const { data } = await accounting
|
||||
.from("invoices")
|
||||
.select("id,number,issue_date,due_date,total,paid_amount,status,updated_at,notes")
|
||||
.select("id,number,issue_date,due_date,total,paid_amount,status,updated_at,notes,external_source")
|
||||
.eq("company_id", cid)
|
||||
.eq("customer_id", id)
|
||||
.order("issue_date", { ascending: true });
|
||||
@@ -73,6 +73,22 @@ export default function AccountingCustomerDetailPage() {
|
||||
},
|
||||
});
|
||||
|
||||
// Payments recorded against this homeowner (incl. those synced from the main
|
||||
// app's owner ledger). Shown as real ledger credits so dates/amounts are exact.
|
||||
const { data: payments = [] } = useQuery({
|
||||
queryKey: ["customer-payments", id],
|
||||
enabled: !!id && !!cid,
|
||||
queryFn: async () => {
|
||||
const { data } = await accounting
|
||||
.from("payments_received")
|
||||
.select("id,payment_date,amount,method,reference,memo,deposited")
|
||||
.eq("company_id", cid)
|
||||
.eq("customer_id", id)
|
||||
.order("payment_date", { ascending: true });
|
||||
return data ?? [];
|
||||
},
|
||||
});
|
||||
|
||||
const allRows = useMemo<LedgerRow[]>(() => {
|
||||
const rows: LedgerRow[] = [];
|
||||
for (const inv of invoices as any[]) {
|
||||
@@ -91,7 +107,10 @@ export default function AccountingCustomerDetailPage() {
|
||||
dueDate: inv.due_date,
|
||||
});
|
||||
}
|
||||
if (paid > 0) {
|
||||
// Manual invoices track payment via paid_amount (no payments_received row).
|
||||
// Synced ledger payments are rendered from payments_received below, so skip
|
||||
// their paid_amount here to avoid double-counting.
|
||||
if (paid > 0 && inv.external_source !== "acmacc_ledger") {
|
||||
rows.push({
|
||||
date: (inv.updated_at ?? inv.issue_date).slice(0, 10),
|
||||
type: "Payment",
|
||||
@@ -104,9 +123,21 @@ export default function AccountingCustomerDetailPage() {
|
||||
});
|
||||
}
|
||||
}
|
||||
for (const p of payments as any[]) {
|
||||
rows.push({
|
||||
date: p.payment_date,
|
||||
type: "Payment",
|
||||
ref: p.reference || "Payment",
|
||||
description: [p.method, p.memo].filter(Boolean).join(" · ") || "Payment received",
|
||||
debit: 0,
|
||||
credit: Number(p.amount ?? 0),
|
||||
sourceId: p.id,
|
||||
sourceKind: "payment",
|
||||
});
|
||||
}
|
||||
rows.sort((a, b) => a.date.localeCompare(b.date));
|
||||
return rows;
|
||||
}, [invoices]);
|
||||
}, [invoices, payments]);
|
||||
|
||||
const openingBalance = useMemo(
|
||||
() => allRows.filter((r) => r.date < from).reduce((s, r) => s + r.debit - r.credit, 0),
|
||||
|
||||
@@ -40,8 +40,11 @@ export default function AccountingDepositsPage() {
|
||||
(await accounting.from("accounts").select("id,name,code,balance").eq("company_id", cid).eq("is_bank", true).order("code")).data ?? [],
|
||||
});
|
||||
|
||||
const { data: pending = [] } = useQuery({
|
||||
queryKey: ["undeposited", cid, undepositedId],
|
||||
// Two sources of "awaiting deposit": transactions parked on the Undeposited
|
||||
// Funds account (banking flow) and payments_received not yet deposited (incl.
|
||||
// payments synced from the main app's owner ledger). Both are unified below.
|
||||
const { data: pendingTx = [] } = useQuery({
|
||||
queryKey: ["undeposited-tx", cid, undepositedId],
|
||||
enabled: !!cid && !!undepositedId,
|
||||
queryFn: async () => {
|
||||
const { data } = await accounting
|
||||
@@ -56,14 +59,45 @@ export default function AccountingDepositsPage() {
|
||||
},
|
||||
});
|
||||
|
||||
const { data: pendingPmt = [] } = useQuery({
|
||||
queryKey: ["undeposited-pmt", cid],
|
||||
enabled: !!cid,
|
||||
queryFn: async () => {
|
||||
const { data } = await accounting
|
||||
.from("payments_received")
|
||||
.select("id,payment_date,amount,method,reference,memo,customer_id")
|
||||
.eq("company_id", cid)
|
||||
.eq("deposited", false)
|
||||
.order("payment_date", { ascending: false });
|
||||
return data ?? [];
|
||||
},
|
||||
});
|
||||
|
||||
type PendingRow = { key: string; kind: "tx" | "pmt"; id: string; date: string; description: string; reference: string | null; amount: number };
|
||||
|
||||
const pending = useMemo<PendingRow[]>(() => {
|
||||
const rows: PendingRow[] = [
|
||||
...(pendingTx as any[]).map((t) => ({
|
||||
key: `tx:${t.id}`, kind: "tx" as const, id: t.id, date: t.date,
|
||||
description: t.description, reference: t.reference ?? null, amount: Number(t.amount),
|
||||
})),
|
||||
...(pendingPmt as any[]).map((p) => ({
|
||||
key: `pmt:${p.id}`, kind: "pmt" as const, id: p.id, date: p.payment_date,
|
||||
description: [p.method, p.memo].filter(Boolean).join(" · ") || "Customer payment",
|
||||
reference: p.reference ?? null, amount: Number(p.amount),
|
||||
})),
|
||||
];
|
||||
return rows.sort((a, b) => b.date.localeCompare(a.date));
|
||||
}, [pendingTx, pendingPmt]);
|
||||
|
||||
const selectedTotal = useMemo(
|
||||
() => (pending as any[]).filter((t) => selected.has(t.id)).reduce((s, t) => s + Number(t.amount), 0),
|
||||
() => pending.filter((r) => selected.has(r.key)).reduce((s, r) => s + r.amount, 0),
|
||||
[pending, selected]
|
||||
);
|
||||
|
||||
const toggleAll = () => {
|
||||
if (selected.size === (pending as any[]).length) setSelected(new Set());
|
||||
else setSelected(new Set((pending as any[]).map((t) => t.id)));
|
||||
if (selected.size === pending.length) setSelected(new Set());
|
||||
else setSelected(new Set(pending.map((r) => r.key)));
|
||||
};
|
||||
|
||||
const submitDeposit = async () => {
|
||||
@@ -72,7 +106,11 @@ export default function AccountingDepositsPage() {
|
||||
setSaving(true);
|
||||
try {
|
||||
const bank = (bankAccounts as any[]).find((a) => a.id === bankAccountId);
|
||||
const ids = Array.from(selected);
|
||||
const chosen = pending.filter((r) => selected.has(r.key));
|
||||
const txIds = chosen.filter((r) => r.kind === "tx").map((r) => r.id);
|
||||
const pmtIds = chosen.filter((r) => r.kind === "pmt").map((r) => r.id);
|
||||
const txTotal = chosen.filter((r) => r.kind === "tx").reduce((s, r) => s + r.amount, 0);
|
||||
const count = chosen.length;
|
||||
|
||||
// 1) Create deposit record
|
||||
const { data: dep, error: depErr } = await accounting
|
||||
@@ -81,49 +119,59 @@ export default function AccountingDepositsPage() {
|
||||
.select()
|
||||
.single();
|
||||
if (depErr || !dep) throw new Error(depErr?.message ?? "Failed to create deposit");
|
||||
const ref = `DEP-${dep.id.slice(0, 8).toUpperCase()}`;
|
||||
|
||||
// 2) Single debit on bank account
|
||||
// 2) Single debit on bank account for the full deposit
|
||||
await accounting.from("transactions").insert({
|
||||
company_id: cid,
|
||||
account_id: bankAccountId,
|
||||
date: depositDate,
|
||||
type: "debit",
|
||||
amount: selectedTotal,
|
||||
description: `Deposit · ${ids.length} payment${ids.length > 1 ? "s" : ""}${memo ? " · " + memo : ""}`,
|
||||
description: `Deposit · ${count} payment${count > 1 ? "s" : ""}${memo ? " · " + memo : ""}`,
|
||||
category: "Deposit",
|
||||
reference: `DEP-${dep.id.slice(0, 8).toUpperCase()}`,
|
||||
reference: ref,
|
||||
deposit_id: dep.id,
|
||||
});
|
||||
|
||||
// 3) Single offsetting credit on Undeposited Funds (clears the holding balance)
|
||||
// 3) Offsetting credit on Undeposited Funds — only for the portion actually
|
||||
// held there as transactions (payments_received aren't posted to it).
|
||||
if (txTotal > 0) {
|
||||
await accounting.from("transactions").insert({
|
||||
company_id: cid,
|
||||
account_id: undepositedId,
|
||||
date: depositDate,
|
||||
type: "credit",
|
||||
amount: selectedTotal,
|
||||
description: `Deposit cleared · ${ids.length} payment${ids.length > 1 ? "s" : ""}`,
|
||||
amount: txTotal,
|
||||
description: `Deposit cleared · ${txIds.length} payment${txIds.length > 1 ? "s" : ""}`,
|
||||
category: "Deposit",
|
||||
reference: `DEP-${dep.id.slice(0, 8).toUpperCase()}`,
|
||||
reference: ref,
|
||||
deposit_id: dep.id,
|
||||
});
|
||||
|
||||
// 4) Tag selected pending payments with deposit_id so they disappear from the queue
|
||||
await accounting.from("transactions").update({ deposit_id: dep.id }).in("id", ids);
|
||||
|
||||
// 5) Update account balances
|
||||
if (bank) {
|
||||
await accounting.from("accounts").update({ balance: Number(bank.balance) + selectedTotal }).eq("id", bank.id);
|
||||
}
|
||||
await accounting.from("transactions").update({ deposit_id: dep.id }).in("id", txIds);
|
||||
const { data: und } = await accounting.from("accounts").select("balance").eq("id", undepositedId).single();
|
||||
if (und) {
|
||||
await accounting.from("accounts").update({ balance: Number(und.balance) - selectedTotal }).eq("id", undepositedId);
|
||||
await accounting.from("accounts").update({ balance: Number(und.balance) - txTotal }).eq("id", undepositedId);
|
||||
}
|
||||
}
|
||||
|
||||
// 4) Mark selected payments_received as deposited so they leave the queue
|
||||
if (pmtIds.length) {
|
||||
await accounting.from("payments_received")
|
||||
.update({ deposited: true, deposit_id: dep.id, bank_account_id: bankAccountId })
|
||||
.in("id", pmtIds);
|
||||
}
|
||||
|
||||
// 5) Bank balance reflects the full deposit
|
||||
if (bank) {
|
||||
await accounting.from("accounts").update({ balance: Number(bank.balance) + selectedTotal }).eq("id", bank.id);
|
||||
}
|
||||
|
||||
toast.success(`Deposit of ${money(selectedTotal, cur)} recorded`);
|
||||
setSelected(new Set());
|
||||
setMemo("");
|
||||
qc.invalidateQueries({ queryKey: ["undeposited", cid] });
|
||||
qc.invalidateQueries({ queryKey: ["undeposited-tx", cid] });
|
||||
qc.invalidateQueries({ queryKey: ["undeposited-pmt", cid] });
|
||||
qc.invalidateQueries({ queryKey: ["bank-accounts", cid] });
|
||||
qc.invalidateQueries({ queryKey: ["accounts", cid] });
|
||||
qc.invalidateQueries({ queryKey: ["transactions", cid] });
|
||||
@@ -178,7 +226,7 @@ export default function AccountingDepositsPage() {
|
||||
<CardHeader className="flex flex-row items-center justify-between">
|
||||
<CardTitle className="text-base">Payments awaiting deposit</CardTitle>
|
||||
<div className="text-sm">
|
||||
Selected: <b>{money(selectedTotal, cur)}</b> ({selected.size} of {(pending as any[]).length})
|
||||
Selected: <b>{money(selectedTotal, cur)}</b> ({selected.size} of {pending.length})
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
@@ -188,7 +236,7 @@ export default function AccountingDepositsPage() {
|
||||
<TableHead className="w-10">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selected.size > 0 && selected.size === (pending as any[]).length}
|
||||
checked={selected.size > 0 && selected.size === pending.length}
|
||||
onChange={toggleAll}
|
||||
/>
|
||||
</TableHead>
|
||||
@@ -199,20 +247,20 @@ export default function AccountingDepositsPage() {
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{(pending as any[]).map((t) => (
|
||||
<TableRow key={t.id} className="cursor-pointer" onClick={() => {
|
||||
{pending.map((r) => (
|
||||
<TableRow key={r.key} className="cursor-pointer" onClick={() => {
|
||||
const s = new Set(selected);
|
||||
s.has(t.id) ? s.delete(t.id) : s.add(t.id);
|
||||
s.has(r.key) ? s.delete(r.key) : s.add(r.key);
|
||||
setSelected(s);
|
||||
}}>
|
||||
<TableCell><input type="checkbox" checked={selected.has(t.id)} readOnly /></TableCell>
|
||||
<TableCell>{fmtDate(t.date)}</TableCell>
|
||||
<TableCell>{t.description}</TableCell>
|
||||
<TableCell className="text-muted-foreground">{t.reference ?? "—"}</TableCell>
|
||||
<TableCell className="text-right tabular-nums">{money(t.amount, cur)}</TableCell>
|
||||
<TableCell><input type="checkbox" checked={selected.has(r.key)} readOnly /></TableCell>
|
||||
<TableCell>{fmtDate(r.date)}</TableCell>
|
||||
<TableCell>{r.description}</TableCell>
|
||||
<TableCell className="text-muted-foreground">{r.reference ?? "—"}</TableCell>
|
||||
<TableCell className="text-right tabular-nums">{money(r.amount, cur)}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
{(pending as any[]).length === 0 && (
|
||||
{pending.length === 0 && (
|
||||
<TableRow className="hover:bg-transparent">
|
||||
<TableCell colSpan={5} className="p-0">
|
||||
<EmptyState
|
||||
|
||||
@@ -0,0 +1,179 @@
|
||||
-- Sync the main-app owner ledger (public.owner_ledger_entries) into the
|
||||
-- Accounting platform per association:
|
||||
-- * a CHARGE (debit > 0) -> accounting.invoices (the customer's AR ledger)
|
||||
-- * a PAYMENT (credit > 0) -> accounting.payments_received, deposited=false
|
||||
-- (Undeposited Funds) so the funds flow into the "Make Deposit" page.
|
||||
-- The accounting customer's balance is recomputed authoritatively from the
|
||||
-- source ledger. Scoped to associations that have an accounting.companies row.
|
||||
-- Keyed by external_source='acmacc_ledger', external_id=owner_ledger_entries.id.
|
||||
|
||||
-- payments_received needs external linkage columns (invoices/bills/vendors already have them)
|
||||
alter table accounting.payments_received add column if not exists external_source text;
|
||||
alter table accounting.payments_received add column if not exists external_id text;
|
||||
create unique index if not exists ux_payments_received_ext
|
||||
on accounting.payments_received (company_id, external_source, external_id)
|
||||
where external_id is not null;
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Recompute a synced customer's balance from the source ledger and FIFO-apply
|
||||
-- ledger payments across that customer's ledger invoices (so paid_amount/status
|
||||
-- look right). Only touches acmacc_ledger rows — manual invoices/payments are
|
||||
-- left alone.
|
||||
-- ---------------------------------------------------------------------------
|
||||
create or replace function accounting.recompute_customer_from_ledger(
|
||||
_company_id uuid, _customer_id uuid, _unit_id uuid, _association_id uuid
|
||||
) returns void
|
||||
language plpgsql security definer set search_path to 'public','accounting'
|
||||
as $$
|
||||
declare
|
||||
_bal numeric;
|
||||
_pool numeric;
|
||||
_alloc numeric;
|
||||
inv record;
|
||||
begin
|
||||
if _customer_id is null or _unit_id is null then return; end if;
|
||||
|
||||
-- Authoritative AR balance = sum(debit - credit) of the unit's live ledger.
|
||||
select coalesce(sum(debit - credit), 0) into _bal
|
||||
from public.owner_ledger_entries
|
||||
where unit_id = _unit_id and association_id = _association_id
|
||||
and coalesce(is_archived, false) = false;
|
||||
update accounting.customers set balance = _bal where id = _customer_id;
|
||||
|
||||
-- FIFO-allocate ledger payments across ledger invoices.
|
||||
select coalesce(sum(amount), 0) into _pool
|
||||
from accounting.payments_received
|
||||
where customer_id = _customer_id and external_source = 'acmacc_ledger';
|
||||
|
||||
for inv in
|
||||
select id, total from accounting.invoices
|
||||
where customer_id = _customer_id and external_source = 'acmacc_ledger'
|
||||
order by issue_date asc, created_at asc
|
||||
loop
|
||||
_alloc := greatest(0, least(_pool, inv.total));
|
||||
update accounting.invoices set
|
||||
paid_amount = _alloc,
|
||||
status = case when _alloc >= inv.total and inv.total > 0
|
||||
then 'paid'::accounting.invoice_status
|
||||
else 'sent'::accounting.invoice_status end,
|
||||
updated_at = now()
|
||||
where id = inv.id;
|
||||
_pool := _pool - _alloc;
|
||||
end loop;
|
||||
end;
|
||||
$$;
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Sync one ledger entry into accounting (idempotent).
|
||||
-- ---------------------------------------------------------------------------
|
||||
create or replace function accounting.sync_owner_ledger_entry(_entry_id uuid)
|
||||
returns void
|
||||
language plpgsql security definer set search_path to 'public','accounting'
|
||||
as $$
|
||||
declare
|
||||
e public.owner_ledger_entries%rowtype;
|
||||
_company_id uuid;
|
||||
_customer_id uuid;
|
||||
begin
|
||||
select * into e from public.owner_ledger_entries where id = _entry_id;
|
||||
if not found then return; end if;
|
||||
|
||||
select id into _company_id from accounting.companies where association_id = e.association_id;
|
||||
if _company_id is null then return; end if;
|
||||
|
||||
select id into _customer_id from accounting.customers
|
||||
where company_id = _company_id and external_source = 'acmacc_unit'
|
||||
and external_id = e.unit_id::text;
|
||||
if _customer_id is null then return; end if; -- unit not synced; nothing to attach to
|
||||
|
||||
if coalesce(e.is_archived, false) then
|
||||
delete from accounting.invoices where company_id=_company_id and external_source='acmacc_ledger' and external_id=e.id::text;
|
||||
delete from accounting.payments_received where company_id=_company_id and external_source='acmacc_ledger' and external_id=e.id::text;
|
||||
|
||||
elsif coalesce(e.debit, 0) > 0 then
|
||||
-- charge -> invoice (drop any stale payment row for this entry)
|
||||
delete from accounting.payments_received where company_id=_company_id and external_source='acmacc_ledger' and external_id=e.id::text;
|
||||
insert into accounting.invoices
|
||||
(company_id, customer_id, number, issue_date, due_date, status, subtotal, tax, total, notes, external_source, external_id)
|
||||
values
|
||||
(_company_id, _customer_id,
|
||||
'AR-' || to_char(e.date, 'YYYYMMDD') || '-' || left(replace(e.id::text, '-', ''), 6),
|
||||
e.date, e.date, 'sent'::accounting.invoice_status,
|
||||
e.debit, 0, e.debit,
|
||||
coalesce(nullif(e.description, ''), initcap(e.transaction_type)),
|
||||
'acmacc_ledger', e.id::text)
|
||||
on conflict (company_id, external_source, external_id) where external_id is not null
|
||||
do update set customer_id=excluded.customer_id, issue_date=excluded.issue_date,
|
||||
due_date=excluded.due_date, subtotal=excluded.subtotal, total=excluded.total,
|
||||
notes=excluded.notes, number=excluded.number, updated_at=now();
|
||||
|
||||
elsif coalesce(e.credit, 0) > 0 then
|
||||
-- payment -> payments_received (Undeposited Funds); never reset deposited/deposit_id
|
||||
delete from accounting.invoices where company_id=_company_id and external_source='acmacc_ledger' and external_id=e.id::text;
|
||||
insert into accounting.payments_received
|
||||
(company_id, customer_id, payment_date, amount, method, reference, memo, deposited, external_source, external_id)
|
||||
values
|
||||
(_company_id, _customer_id, e.date, e.credit,
|
||||
coalesce(nullif(e.transaction_type, ''), 'payment'),
|
||||
e.reference_id, e.description, false, 'acmacc_ledger', e.id::text)
|
||||
on conflict (company_id, external_source, external_id) where external_id is not null
|
||||
do update set customer_id=excluded.customer_id, payment_date=excluded.payment_date,
|
||||
amount=excluded.amount, method=excluded.method, reference=excluded.reference,
|
||||
memo=excluded.memo, updated_at=now();
|
||||
|
||||
else
|
||||
delete from accounting.invoices where company_id=_company_id and external_source='acmacc_ledger' and external_id=e.id::text;
|
||||
delete from accounting.payments_received where company_id=_company_id and external_source='acmacc_ledger' and external_id=e.id::text;
|
||||
end if;
|
||||
|
||||
perform accounting.recompute_customer_from_ledger(_company_id, _customer_id, e.unit_id, e.association_id);
|
||||
end;
|
||||
$$;
|
||||
|
||||
-- Remove synced rows for a deleted/moved ledger entry, then recompute.
|
||||
create or replace function accounting.unsync_owner_ledger_entry(
|
||||
_entry_id uuid, _association_id uuid, _unit_id uuid
|
||||
) returns void
|
||||
language plpgsql security definer set search_path to 'public','accounting'
|
||||
as $$
|
||||
declare _company_id uuid; _customer_id uuid;
|
||||
begin
|
||||
select id into _company_id from accounting.companies where association_id=_association_id;
|
||||
if _company_id is null then return; end if;
|
||||
delete from accounting.invoices where company_id=_company_id and external_source='acmacc_ledger' and external_id=_entry_id::text;
|
||||
delete from accounting.payments_received where company_id=_company_id and external_source='acmacc_ledger' and external_id=_entry_id::text;
|
||||
select id into _customer_id from accounting.customers
|
||||
where company_id=_company_id and external_source='acmacc_unit' and external_id=_unit_id::text;
|
||||
perform accounting.recompute_customer_from_ledger(_company_id, _customer_id, _unit_id, _association_id);
|
||||
end;
|
||||
$$;
|
||||
|
||||
-- Trigger glue (error-swallowed so a sync hiccup never blocks a ledger write).
|
||||
create or replace function accounting.tg_owner_ledger_sync()
|
||||
returns trigger
|
||||
language plpgsql security definer set search_path to 'public','accounting'
|
||||
as $$
|
||||
begin
|
||||
begin
|
||||
if tg_op = 'DELETE' then
|
||||
perform accounting.unsync_owner_ledger_entry(old.id, old.association_id, old.unit_id);
|
||||
return old;
|
||||
end if;
|
||||
if tg_op = 'UPDATE'
|
||||
and (old.unit_id is distinct from new.unit_id
|
||||
or old.association_id is distinct from new.association_id) then
|
||||
perform accounting.unsync_owner_ledger_entry(old.id, old.association_id, old.unit_id);
|
||||
end if;
|
||||
perform accounting.sync_owner_ledger_entry(new.id);
|
||||
return new;
|
||||
exception when others then
|
||||
raise warning 'accounting: owner_ledger sync failed for %: %', coalesce(new.id, old.id), sqlerrm;
|
||||
return coalesce(new, old);
|
||||
end;
|
||||
end;
|
||||
$$;
|
||||
|
||||
drop trigger if exists trg_acct_sync_owner_ledger on public.owner_ledger_entries;
|
||||
create trigger trg_acct_sync_owner_ledger
|
||||
after insert or update or delete on public.owner_ledger_entries
|
||||
for each row execute function accounting.tg_owner_ledger_sync();
|
||||
Reference in New Issue
Block a user