69c537fd1f
A verifikat booked from a supplier invoice showed the invoice PDF when opened while the list kept warning "Underlag saknas" on the same row. Both surfaces behaved as written: every missing-underlag surface only accepts a referenced supplier-invoice document when it is ANCHORED to a journal entry (only anchored docs sit behind block_document_deletion), while the verifikat view's reference resolver displayed the document regardless. The document was floating because delete_last_voucher clears journal_entry_id on everything attached to the voucher it tears down (the FK is ON DELETE RESTRICT, so it must). Deleting a rättelse the invoice PDF had been relinked onto therefore orphaned it while the payment verifikat stayed posted, and nothing ever anchored it again: the warning was unresolvable by design. Same class one surface over: v1 mark-paid never linked the document at all, dashboard mark-paid only did so for the cash entry, and both match-supplier-invoice routes propagated the transaction's document but not the invoice's own. Four of the five affected prod rows come from those paths, not from a deleted voucher. - lib/core/documents/supplier-invoice-underlag.ts: anchor a floating document to the invoice's own posted verifikat (registration, then payment, then partial payments; open unlocked periods only). Never moves an anchored doc, never throws. - Called after delete_last_voucher and from all four payment paths. - getJournalEntryUnderlagReferences withholds an unanchored document so the verifikat view and the warning can no longer contradict each other. - Migration 20260727180000 backfills the rows already in this state. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
97 lines
4.2 KiB
SQL
97 lines
4.2 KiB
SQL
-- Re-anchor supplier-invoice source documents that are floating.
|
|
--
|
|
-- Problem (support case 2026-07-27, MGS Sweden): a verifikat booked from a
|
|
-- supplier invoice showed the invoice PDF when opened, while the verifikat list
|
|
-- kept warning "Underlag saknas" on the same row. Both surfaces were behaving
|
|
-- as written:
|
|
--
|
|
-- * every missing-underlag surface (verifikat_without_documents /
|
|
-- transactions_without_documents, /api/documents/counts, the transactions
|
|
-- list) only accepts a referenced supplier-invoice document as underlag
|
|
-- when it is ANCHORED (document_attachments.journal_entry_id IS NOT NULL),
|
|
-- because only anchored docs sit behind the WORM deletion guards
|
|
-- (block_document_deletion keys on journal_entry_id);
|
|
-- * the verifikat view's reference resolver did not check the anchor and
|
|
-- displayed the document regardless.
|
|
--
|
|
-- The resolver is aligned in the same change. This migration fixes the data
|
|
-- half: documents that ended up floating even though the invoice still has a
|
|
-- posted verifikat to hang on. Two production causes, both observed:
|
|
--
|
|
-- 1. delete_last_voucher clears journal_entry_id on every document attached
|
|
-- to the voucher it tears down (the FK is ON DELETE RESTRICT, so it has
|
|
-- no alternative). Deleting a rättelse the invoice's PDF had been
|
|
-- relinked onto therefore orphans the PDF while the invoice's payment
|
|
-- verifikat stays posted.
|
|
-- 2. Payment/cash verifikat booked for an invoice whose document was never
|
|
-- anchored at registration (attached after the fact, or booked through
|
|
-- the API-key/MCP mark-paid path, which did not link it at all).
|
|
--
|
|
-- Anchoring is strictly protective: it moves the document behind the deletion
|
|
-- guard, and NULL -> uuid is explicitly permitted by
|
|
-- enforce_document_journal_entry_immutability (it returns early when
|
|
-- OLD.journal_entry_id IS NULL). Scoped like the 20260724090000 §4 backfill:
|
|
-- only currently-unlinked current-version documents (never steal a doc that
|
|
-- already serves a verifikat), only into open, unlocked periods
|
|
-- (enforce_period_lock_documents raises otherwise), and the target verifikat
|
|
-- must be posted and belong to the same company as the document.
|
|
--
|
|
-- Preference order matches lib/core/documents/supplier-invoice-underlag.ts:
|
|
-- the registration booking is the primary booking of the affärshändelse, the
|
|
-- payment booking is the fallback (and the only booking under kontantmetoden),
|
|
-- then partial-payment verifikat, oldest first.
|
|
|
|
DO $$
|
|
DECLARE
|
|
v_updated integer;
|
|
BEGIN
|
|
WITH candidate AS (
|
|
SELECT
|
|
si.document_id,
|
|
si.company_id,
|
|
je.id AS journal_entry_id,
|
|
ROW_NUMBER() OVER (
|
|
PARTITION BY si.document_id
|
|
ORDER BY rank_source, coalesce(sip.payment_date, je.entry_date), je.id
|
|
) AS pick
|
|
FROM supplier_invoices si
|
|
JOIN document_attachments d
|
|
ON d.id = si.document_id
|
|
AND d.company_id = si.company_id
|
|
AND d.journal_entry_id IS NULL
|
|
AND d.is_current_version = true
|
|
CROSS JOIN LATERAL (
|
|
SELECT si.registration_journal_entry_id AS entry_id, 1 AS rank_source, NULL::uuid AS payment_id
|
|
UNION ALL
|
|
SELECT si.payment_journal_entry_id, 2, NULL::uuid
|
|
UNION ALL
|
|
SELECT p.journal_entry_id, 3, p.id
|
|
FROM supplier_invoice_payments p
|
|
WHERE p.supplier_invoice_id = si.id
|
|
AND p.company_id = si.company_id
|
|
AND p.journal_entry_id IS NOT NULL
|
|
) AS src(entry_id, rank_source, payment_id)
|
|
LEFT JOIN supplier_invoice_payments sip ON sip.id = src.payment_id
|
|
JOIN journal_entries je
|
|
ON je.id = src.entry_id
|
|
AND je.company_id = si.company_id
|
|
AND je.status = 'posted'
|
|
JOIN fiscal_periods fp
|
|
ON fp.id = je.fiscal_period_id
|
|
AND fp.is_closed = false
|
|
AND fp.locked_at IS NULL
|
|
)
|
|
UPDATE document_attachments d
|
|
SET journal_entry_id = candidate.journal_entry_id
|
|
FROM candidate
|
|
WHERE candidate.pick = 1
|
|
AND d.id = candidate.document_id
|
|
AND d.company_id = candidate.company_id
|
|
AND d.journal_entry_id IS NULL
|
|
AND d.is_current_version = true;
|
|
|
|
GET DIAGNOSTICS v_updated = ROW_COUNT;
|
|
RAISE NOTICE 're-anchored % floating supplier-invoice documents to a posted verifikat', v_updated;
|
|
END;
|
|
$$;
|