Files
accounted/supabase/migrations/20260610120000_gl_lines_for_matching.sql
T
Jakob WennbergandClaude Opus 4.8 094bd85e81 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>
2026-06-02 09:37:30 +02:00

102 lines
4.2 KiB
PL/PgSQL

-- 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';