feat(bookkeeping): guarded fiscal-year reset + findable Angra import (#1883) (#1897)

* feat(bookkeeping): guarded fiscal-year reset + findable Angra import (#1883)

Two deliverables from the community report where a bad SIE test import
left no way out short of deleting the company:

A) Discoverability: the voucher list shows one attn line linking to
   /import?history=sie whenever the page contains import-sourced
   vouchers, and /import?history=sie deep-links straight into the
   fold-open SIE import history where per-import Angra already lives.

B) Reset of an UNLOCKED fiscal year regardless of how the entries
   arrived: new reset_fiscal_year RPC (same gnubok.allow_delete escape
   hatch as undo_sie_import; no enforcement trigger touched) behind
   GET/POST /api/bookkeeping/fiscal-periods/[id]/reset and a typed
   type-the-year-name confirmation dialog on the fiscal years settings
   list. Refuses on: locked/closed year, company lock date over any part
   of the year, executed year-end, arsredovisning state, later year
   depending on this year's UB, VAT-declared evidence (vat_settlement
   verifikat, SKV lock/submit audit rows, extension workflow keys, fail
   closed) and AGI-declared months. Entries referenced by RESTRICT/NO
   ACTION FKs abort the whole reset (all-or-nothing). Documents are
   detached, never deleted (BFL 7 kap); every delete is audit-logged
   plus one behandlingshistorik summary row.

Fixes #1883

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(bookkeeping): harden fiscal-year reset after skeptic review (#1883)

Blocking skeptic findings on PR #1897, one consolidated pass:

- New snapshot blocker cross_year_reference: an entry outside the year whose
  correction_of_id / reverses_id / reversed_by_id points into the year made
  the delete crash with an uncaught P0001 (immutability trigger refusing the
  ON DELETE SET NULL referential UPDATE) after an eligible:true preview, and
  silently severed draft chains. 12 such chains exist in prod today.
- New snapshot blocker rot_rut_state: a begaran om utbetalning that reached
  Skatteverket (submitted/paid/partially_paid/rejected) was silently
  unlinked via SET NULL, erasing the bokforing behind a filed and possibly
  decided myndighetsarende.
- Rakenskapsinformation preservation (BFL 7 kap): line-level trigger audit
  rows carry no company_id and header rows no amounts, so a reset destroyed
  konton/belopp with no company-readable trace. The RPC now archives the
  full content of every verifikat in company-scoped RESET_SNAPSHOT audit
  rows before deleting (action added to audit_log_action_check, NOT VALID),
  and behandlingshistorik renders them.
- Dimension registry lockstep on reset (mirrors undo_sie_import): flipped
  imports can never be undone again, so their dimensions/values would have
  been orphaned forever.
- EXCEPTION WHEN raise_exception now returns a typed
  FISCAL_YEAR_RESET_LINKED_ENTRIES envelope instead of a bare 500;
  gnubok.allow_delete is cleared before leaving the guarded block.
- Voucher-list attn line fires only for source_type 'import':
  opening_balance is also written by year-end closing and the manual IB
  flows, which mislabelled every year-2+ company as SIE-imported.
- /import?history=sie now scrolls the SIE history into view.
- Reset dialog copy (sv+en) discloses that linked invoices, payments and
  bank transactions become unbooked; new blocker strings in both locales.
- pg fixture fix: document_attachments seeded without company_id (23502);
  new pg tests for both blockers, RESET_SNAPSHOT rows and the lockstep.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Mattsson
2026-08-25 14:36:18 +02:00
committed by GitHub
co-authored by Claude Fable 5
parent 77cacdcf34
commit 79013cf092
17 changed files with 2359 additions and 2 deletions
+31
View File
@@ -1958,6 +1958,35 @@
"fy_lock_success": "Fiscal year locked",
"fy_unlock_success": "Fiscal year unlocked",
"fy_action_error": "The action could not be completed",
"fy_action_reset": "Reset",
"fy_reset_dialog_title": "Reset fiscal year {name}?",
"fy_reset_dialog_description": "All vouchers in the fiscal year are permanently deleted, regardless of how they were created. Documents are never deleted, only detached. Invoices, payments and bank transactions that were linked to the vouchers become unbooked and need to be booked again. This cannot be undone and is recorded in the audit log.",
"fy_reset_checking": "Checking whether the fiscal year can be reset...",
"fy_reset_load_failed": "Could not check the fiscal year. Try again.",
"fy_reset_blocked_title": "The fiscal year cannot be reset",
"fy_reset_blocker_closed": "The fiscal year is closed.",
"fy_reset_blocker_locked": "The fiscal year is locked. Unlock it first.",
"fy_reset_blocker_lock_date": "The company bookkeeping lock date ({date}) covers part of the fiscal year.",
"fy_reset_blocker_year_end": "A year-end closing has been executed for the fiscal year.",
"fy_reset_blocker_arsredovisning": "An annual report has been started or filed for the fiscal year.",
"fy_reset_blocker_next_year": "A later fiscal year depends on this year's closing balances.",
"fy_reset_blocker_vat": "A VAT declaration has been booked or filed for a period in the fiscal year.",
"fy_reset_blocker_agi": "An employer declaration (AGI) has been filed for a month in the fiscal year.",
"fy_reset_blocker_rot_rut": "A ROT or RUT payout request filed with Skatteverket relies on vouchers in the fiscal year.",
"fy_reset_blocker_cross_year": "Vouchers in another fiscal year correct or reverse vouchers in this year.",
"fy_reset_blocker_other": "The fiscal year is in a state that cannot be reset.",
"fy_reset_summary_heading": "This will be deleted",
"fy_reset_summary_year": "Fiscal year",
"fy_reset_summary_vouchers": "Vouchers to delete",
"fy_reset_summary_documents": "Document links to remove",
"fy_reset_documents_note": "Documents stay in the archive (7-year retention); they are only detached from the vouchers.",
"fy_reset_confirm_name": "Type <strong>{name}</strong> to confirm",
"fy_reset_submit": "Reset fiscal year",
"fy_reset_resetting": "Resetting...",
"fy_reset_success_title": "Fiscal year reset",
"fy_reset_success_description": "{count} vouchers were deleted.",
"fy_reset_failed_title": "The fiscal year could not be reset",
"fy_reset_failed_default": "Something went wrong. No changes were saved.",
"related_heading": "Related",
"related_chart_of_accounts": "Chart of accounts (BAS)",
"periodisering_label": "Accruals",
@@ -4881,6 +4910,8 @@
"send_failed_fallback": "Please try again."
},
"journal_list": {
"import_attn": "This list contains vouchers from an SIE import. If something went wrong, the whole import can be undone.",
"import_attn_action": "Show import history",
"mode_vouchers": "Vouchers",
"mode_drafts": "Drafts",
"density_compact": "Compact view",
+31
View File
@@ -1958,6 +1958,35 @@
"fy_lock_success": "Räkenskapsåret är låst",
"fy_unlock_success": "Räkenskapsåret är upplåst",
"fy_action_error": "Åtgärden kunde inte slutföras",
"fy_action_reset": "Nollställ",
"fy_reset_dialog_title": "Nollställ räkenskapsåret {name}?",
"fy_reset_dialog_description": "Alla verifikat i räkenskapsåret raderas permanent, oavsett hur de skapades. Underlag och dokument raderas aldrig, de kopplas bara loss. Fakturor, betalningar och banktransaktioner som var kopplade till verifikaten blir obokförda och behöver bokföras om. Åtgärden kan inte ångras och loggas i behandlingshistoriken.",
"fy_reset_checking": "Kontrollerar om räkenskapsåret kan nollställas...",
"fy_reset_load_failed": "Kunde inte kontrollera räkenskapsåret. Försök igen.",
"fy_reset_blocked_title": "Räkenskapsåret kan inte nollställas",
"fy_reset_blocker_closed": "Räkenskapsåret är stängt.",
"fy_reset_blocker_locked": "Räkenskapsåret är låst. Lås upp det först.",
"fy_reset_blocker_lock_date": "Företagets bokföringslås ({date}) täcker en del av räkenskapsåret.",
"fy_reset_blocker_year_end": "Ett bokslut är verkställt för räkenskapsåret.",
"fy_reset_blocker_arsredovisning": "En årsredovisning har påbörjats eller lämnats in för räkenskapsåret.",
"fy_reset_blocker_next_year": "Ett senare räkenskapsår bygger på det här årets utgående balanser.",
"fy_reset_blocker_vat": "En momsdeklaration är bokförd eller inlämnad för en period i räkenskapsåret.",
"fy_reset_blocker_agi": "En arbetsgivardeklaration är inlämnad för en månad i räkenskapsåret.",
"fy_reset_blocker_rot_rut": "En rot- eller rutbegäran hos Skatteverket bygger på verifikat i räkenskapsåret.",
"fy_reset_blocker_cross_year": "Verifikat i ett annat räkenskapsår rättar eller vänder verifikat i det här året.",
"fy_reset_blocker_other": "Räkenskapsåret är i ett läge som inte kan nollställas.",
"fy_reset_summary_heading": "Detta raderas",
"fy_reset_summary_year": "Räkenskapsår",
"fy_reset_summary_vouchers": "Verifikat som raderas",
"fy_reset_summary_documents": "Dokumentkopplingar som tas bort",
"fy_reset_documents_note": "Dokumenten och underlagen ligger kvar i arkivet (7 års arkiveringsplikt), de kopplas bara loss från verifikaten.",
"fy_reset_confirm_name": "Skriv <strong>{name}</strong> för att bekräfta",
"fy_reset_submit": "Nollställ räkenskapsåret",
"fy_reset_resetting": "Nollställer...",
"fy_reset_success_title": "Räkenskapsåret är nollställt",
"fy_reset_success_description": "{count} verifikat raderades.",
"fy_reset_failed_title": "Räkenskapsåret kunde inte nollställas",
"fy_reset_failed_default": "Något gick fel. Inga ändringar har sparats.",
"related_heading": "Relaterat",
"related_chart_of_accounts": "Kontoplan (BAS)",
"periodisering_label": "Periodisering",
@@ -4881,6 +4910,8 @@
"send_failed_fallback": "Försök igen."
},
"journal_list": {
"import_attn": "Listan innehåller verifikat från en SIE-import. Blev något fel kan hela importen ångras.",
"import_attn_action": "Visa importhistorik",
"mode_vouchers": "Verifikat",
"mode_drafts": "Utkast",
"density_compact": "Kompakt visning",