Files
accounted/supabase/migrations/20260727180000_reanchor_floating_supplier_invoice_documents.sql
Jakob Wennberg 69c537fd1f fix(documents): anchor floating supplier-invoice underlag instead of nagging (#1248)
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>
2026-07-27 18:04:15 +02:00

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