fix(reconciliation): secondary-account scoping, dialog clipping, and N:1 matching (#624)
* fix(reconciliation): secondary-account scoping, dialog clipping, and N:1 matching Three follow-ups to per-account bank reconciliation (PR #623): - Secondary same-currency accounts (e.g. a 1931 savings account) double-counted the company's unassigned (NULL cash_account_id) transactions, inflating their bank total and showing a large bogus difference while 1930 still reconciled. Only the primary cash account now claims NULL rows; every other account scopes strictly to its own id. `includeUnassigned` is threaded through all status/run/list call sites from cash_accounts.is_primary. - The "Matcha mot befintlig verifikation" picker's dropdown was absolutely positioned inside the dialog's overflow-y-auto container and got clipped. Add an `inline` mode that renders the candidate list in normal flow; the dialog uses it, the reconciliation view keeps the compact overlay. - N:1 matching: several bank transactions can now settle one verifikat (a salary run paid in multiple transfers, an invoice paid in instalments). New get_account_gl_lines_for_matching RPC surfaces already-matched vouchers with a linked_transaction_count behind a "Visa även matchade verifikationer" toggle; manualLink's 1:1 guard is relaxed (the aggregate difference still catches mis-links). Tests: extended bank-reconciliation unit tests (strict scope + N:1), rewrote the cash_account_id isolation pg test to prove NULL rows land on the primary account only, and added a pg test for the new RPC. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(reconciliation): address PR review — accurate "att matcha mot" count - BankReconciliationView: the "N verifikationer att matcha mot" hint counted glLines (which includes already-matched vouchers when "Visa matchade" is on), overcounting the vouchers that still need a transaction. Use unmatchedGlLines so the label is correct regardless of the toggle (matches the table below). - MatchVerifikationPicker: document that `open` is overlay-only; the setOpen() writes are intentional no-ops in inline mode. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
b5c3c3ec04
commit
094bd85e81
@@ -0,0 +1,101 @@
|
||||
-- Migration: get_account_gl_lines_for_matching — GL lines on a settlement
|
||||
-- account as MATCH CANDIDATES, optionally including already-matched ones.
|
||||
--
|
||||
-- Why: get_unlinked_gl_lines (20260605130000) returns only lines with NO bank
|
||||
-- transaction linked. That is correct for the reconciliation status count and
|
||||
-- the default picker, but it makes a legitimate N:1 case impossible: a single
|
||||
-- verifikat settled by SEVERAL bank transactions — e.g. a salary run booked as
|
||||
-- one voucher (1930 credit = total net pay) but paid out as multiple transfers,
|
||||
-- or a supplier invoice paid in instalments. Once the first transaction links,
|
||||
-- the voucher vanishes from the candidate list and the user can't attach the
|
||||
-- rest ("kan inte välja matchade verifikat → kan inte lägga på flera").
|
||||
--
|
||||
-- This RPC mirrors get_unlinked_gl_lines exactly (same posted-only filter, same
|
||||
-- opening_balance / storno / correction exclusions, same date window) and adds:
|
||||
-- * linked_transaction_count — how many transactions already point at the
|
||||
-- entry, so the UI can mark a candidate "Redan matchad" and the user opts in
|
||||
-- consciously.
|
||||
-- * p_include_matched — when false (default) the result is IDENTICAL to
|
||||
-- get_unlinked_gl_lines (count = 0 only); when true, already-matched lines
|
||||
-- are included too.
|
||||
--
|
||||
-- The aggregate reconciliation math stays correct under N:1: the GL line is
|
||||
-- counted ONCE in the period movement regardless of how many transactions point
|
||||
-- at it, while each transaction sums on the bank side — so linking transactions
|
||||
-- whose amounts sum to the voucher's bank line nets to zero difference, and any
|
||||
-- mis-link surfaces immediately as a non-zero difference on the status card.
|
||||
--
|
||||
-- A separate function (not a parameter added to get_unlinked_gl_lines) so the
|
||||
-- status-count path and its existing coverage are untouched, and so the extra
|
||||
-- column never leaks into callers that don't expect it.
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.get_account_gl_lines_for_matching(
|
||||
p_company_id UUID,
|
||||
p_account_number TEXT DEFAULT '1930',
|
||||
p_date_from DATE DEFAULT NULL,
|
||||
p_date_to DATE DEFAULT NULL,
|
||||
p_include_matched BOOLEAN DEFAULT false
|
||||
)
|
||||
RETURNS TABLE (
|
||||
line_id UUID,
|
||||
journal_entry_id UUID,
|
||||
debit_amount NUMERIC,
|
||||
credit_amount NUMERIC,
|
||||
line_description TEXT,
|
||||
entry_date DATE,
|
||||
voucher_number INT,
|
||||
voucher_series TEXT,
|
||||
entry_description TEXT,
|
||||
source_type TEXT,
|
||||
linked_transaction_count INT
|
||||
)
|
||||
LANGUAGE sql
|
||||
STABLE
|
||||
SECURITY DEFINER
|
||||
SET search_path = public
|
||||
AS $$
|
||||
SELECT
|
||||
jel.id AS line_id,
|
||||
je.id AS journal_entry_id,
|
||||
jel.debit_amount,
|
||||
jel.credit_amount,
|
||||
jel.line_description,
|
||||
je.entry_date,
|
||||
je.voucher_number,
|
||||
je.voucher_series,
|
||||
je.description AS entry_description,
|
||||
je.source_type,
|
||||
(
|
||||
SELECT count(*)
|
||||
FROM public.transactions t
|
||||
WHERE t.journal_entry_id = je.id
|
||||
AND t.company_id = p_company_id
|
||||
)::int AS linked_transaction_count
|
||||
FROM public.journal_entry_lines jel
|
||||
JOIN public.journal_entries je ON je.id = jel.journal_entry_id
|
||||
WHERE jel.account_number = p_account_number
|
||||
AND je.company_id = p_company_id
|
||||
AND je.status = 'posted'
|
||||
-- Same no-bank-counterpart exclusions as get_unlinked_gl_lines: IB, and the
|
||||
-- book-only storno/correction vouchers can never be a bank-transaction target.
|
||||
AND je.source_type IS DISTINCT FROM 'opening_balance'
|
||||
AND je.source_type IS DISTINCT FROM 'storno'
|
||||
AND je.source_type IS DISTINCT FROM 'correction'
|
||||
AND (p_date_from IS NULL OR je.entry_date >= p_date_from)
|
||||
AND (p_date_to IS NULL OR je.entry_date <= p_date_to)
|
||||
-- Default: only unmatched lines (parity with get_unlinked_gl_lines). When
|
||||
-- p_include_matched is true, already-matched lines are returned too so a
|
||||
-- second/third transaction can be attached to the same verifikat.
|
||||
AND (
|
||||
p_include_matched
|
||||
OR NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM public.transactions t
|
||||
WHERE t.journal_entry_id = je.id
|
||||
AND t.company_id = p_company_id
|
||||
)
|
||||
)
|
||||
ORDER BY je.entry_date, je.voucher_number;
|
||||
$$;
|
||||
|
||||
NOTIFY pgrst, 'reload schema';
|
||||
Reference in New Issue
Block a user