feat(assets): atomic asset disposal workflow (avyttring, utrangering, verksamhetsoverlatelse) (#1391)

* feat(assets): atomic asset disposal workflow (avyttring, utrangering, verksamhetsoverlatelse)

Disposal books depreciation to the disposal date, clears cost and
accumulated depreciation, books gain (3973) or loss (7973), applies
output VAT on third-party sales, honors the ML 5 kap. 38 §
verksamhetsoverlatelse exemption, and recalculates ML 15 kap. jamkning
server-side from tax years and original input VAT. The voucher, the
disposal-date depreciation schedule and the immutable register state
commit in one dedicated commit_asset_disposal RPC transaction that
delegates voucher numbering to commit_journal_entry.

Fixes #325

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

* fix(assets): harden disposal per review and pg-real findings

- commit_asset_disposal now uses the NULL-safe caller_is_company_member()
  guard (tenant-guard ratchet) and passes the allowed 'user_accept'
  commit_method instead of the unlisted 'asset_disposal' value
- disposal metadata invariants validated in the RPC (non-negative
  proceeds/VAT, VAT requires a treatment, VAT <= gross, scrap carries no
  proceeds) since the RPC is independently callable
- new FK and CHECK constraints added NOT VALID + VALIDATE CONSTRAINT so
  the migration never blocks writes on the hot journal_entries table
- disposeAsset paginates fiscal periods and depreciation schedules with
  fetchAllRows; jamkning_remaining_years keeps a valid 0 (?? not ||)
- engine imports shared AssetDisposalType/AssetJamkningDirection/
  VatTreatment unions; post-commit reload retries once and logs before
  surfacing, so a transient read cannot masquerade as a failed disposal
- dispose page parses Swedish-formatted amounts (125 000,50) and blocks
  submission on unparseable proceeds
- assets pg tests write disposal attributes in the disposal transition
  itself and gain a regression test that the register is frozen after

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-04 09:59:52 +02:00
committed by GitHub
co-authored by Claude Fable 5
parent 1c9d378df8
commit cb3ef45f14
22 changed files with 19644 additions and 1805 deletions
+56 -1
View File
@@ -836,7 +836,62 @@
"category_vehicle": "Vehicle",
"category_computer": "Computer",
"category_other_tangible": "Other tangible",
"count_footer": "{count, plural, one {1 asset} other {# assets}}"
"count_footer": "{count, plural, one {1 asset} other {# assets}}",
"disposal": {
"title": "Dispose of asset",
"load_failed_title": "Could not load the disposal",
"try_again": "Please try again.",
"submit_failed_title": "Disposal failed",
"success_title": "Asset disposed",
"success_description": "Depreciation, disposal, and the asset register were posted together.",
"not_found": "The asset could not be found.",
"already_disposed": "The asset was already disposed ({date}).",
"back": "Back",
"acquisition_cost": "Acquisition cost",
"acquired": "Acquired",
"bas_accounts": "Accounts (BAS)",
"details_title": "Disposal details",
"type_label": "Event",
"type_sale": "Sale",
"type_scrap": "Scrapping",
"type_business_transfer": "Transfer of a business",
"date_label": "Disposal date",
"period_label": "Fiscal period",
"period_placeholder": "Select period",
"locked": "locked",
"period_locked": "The selected period is locked or closed. Select an open period.",
"proceeds_label": "Amount received including VAT",
"consideration_label": "Consideration for the asset",
"proceeds_account_label": "Receiving account",
"vat_title": "VAT on the sale",
"vat_treatment_label": "VAT treatment",
"vat_standard_25": "Swedish VAT 25%",
"vat_reverse_charge": "Reverse charge",
"vat_export": "Export outside the EU",
"vat_exempt": "VAT-exempt sale",
"gross": "Gross",
"vat": "Output VAT",
"net": "Net",
"adjustment_title": "Input VAT adjustment (ML chapter 15)",
"within_adjustment_period": "Within the adjustment period: {years} years remain of {total}",
"outside_adjustment_period": "Outside the adjustment period",
"original_vat_label": "Total original input VAT",
"original_vat_hint": "Enter the full VAT amount, not only the deducted part. Threshold: SEK {threshold}.",
"original_percent_label": "Original deduction right (%)",
"adjustment_direction": "Direction",
"adjustment_amount": "Calculated adjustment",
"direction_increase": "Increased deduction",
"direction_decrease": "Decreased deduction",
"direction_none": "No adjustment",
"direction_transferred": "Transferred to the acquirer",
"adjustment_capped": "The amount was capped at 25% of the sale price excluding VAT.",
"business_transfer_confirm": "I confirm that the transfer covers an entire business or independent branch and meets the conditions in ML chapter 5, section 38.",
"adjustment_document_confirm": "I confirm that an adjustment document is prepared for the acquirer.",
"adjustment_data_required": "VAT basis data is required because the asset may be an investment good.",
"cancel": "Cancel",
"submit": "Post disposal",
"write_required": "Only users with write access can dispose of assets."
}
},
"employees": {
"title": "Employees",
+56 -1
View File
@@ -836,7 +836,62 @@
"category_vehicle": "Fordon",
"category_computer": "Dator",
"category_other_tangible": "Övriga materiella",
"count_footer": "{count, plural, one {1 tillgång} other {# tillgångar}}"
"count_footer": "{count, plural, one {1 tillgång} other {# tillgångar}}",
"disposal": {
"title": "Avyttra tillgång",
"load_failed_title": "Kunde inte ladda avyttringen",
"try_again": "Försök igen.",
"submit_failed_title": "Avyttringen misslyckades",
"success_title": "Tillgången är avyttrad",
"success_description": "Avskrivning, avyttring och tillgångsregister har bokförts tillsammans.",
"not_found": "Tillgången kunde inte hittas.",
"already_disposed": "Tillgången är redan avyttrad ({date}).",
"back": "Tillbaka",
"acquisition_cost": "Anskaffningsvärde",
"acquired": "Anskaffad",
"bas_accounts": "Konton (BAS)",
"details_title": "Avyttringsuppgifter",
"type_label": "Händelse",
"type_sale": "Försäljning",
"type_scrap": "Utrangering",
"type_business_transfer": "Verksamhetsöverlåtelse",
"date_label": "Avyttringsdatum",
"period_label": "Räkenskapsperiod",
"period_placeholder": "Välj period",
"locked": "låst",
"period_locked": "Vald period är låst eller stängd. Välj en öppen period.",
"proceeds_label": "Erhållet belopp inklusive moms",
"consideration_label": "Ersättning för tillgången",
"proceeds_account_label": "Mottagarkonto",
"vat_title": "Moms vid försäljning",
"vat_treatment_label": "Momsbehandling",
"vat_standard_25": "Svensk moms 25 %",
"vat_reverse_charge": "Omvänd betalningsskyldighet",
"vat_export": "Export utanför EU",
"vat_exempt": "Momsfri försäljning",
"gross": "Brutto",
"vat": "Utgående moms",
"net": "Netto",
"adjustment_title": "Justering av ingående moms (ML 15 kap.)",
"within_adjustment_period": "Inom justeringsperioden: {years} år kvar av {total}",
"outside_adjustment_period": "Utanför justeringsperioden",
"original_vat_label": "Total ursprunglig ingående moms",
"original_vat_hint": "Ange hela momsbeloppet, inte bara avdragen del. Tröskel: {threshold} kr.",
"original_percent_label": "Ursprunglig avdragsrätt (%)",
"adjustment_direction": "Riktning",
"adjustment_amount": "Beräknad justering",
"direction_increase": "Ökat avdrag",
"direction_decrease": "Minskat avdrag",
"direction_none": "Ingen justering",
"direction_transferred": "Överförs till förvärvaren",
"adjustment_capped": "Beloppet har begränsats till 25 % av försäljningspriset exklusive moms.",
"business_transfer_confirm": "Jag bekräftar att överlåtelsen omfattar en hel verksamhet eller självständig verksamhetsgren och uppfyller villkoren i ML 5 kap. 38 §.",
"adjustment_document_confirm": "Jag bekräftar att en justeringshandling upprättas för förvärvaren.",
"adjustment_data_required": "Momsunderlaget krävs eftersom tillgången kan vara en investeringsvara.",
"cancel": "Avbryt",
"submit": "Bokför avyttring",
"write_required": "Endast användare med skrivrättigheter kan avyttra tillgångar."
}
},
"employees": {
"title": "Anställda",