feat(salary): repay utlägg with the salary as a tax-free payslip line (#2361)

* feat(salary): repay utlägg with the salary as a tax-free payslip line (#2331)

- expense_reimbursement line type: kostnadsersättning outside gross, tax,
  avgifter and the AGI. The engine adds tax-free reimbursements (utlägg,
  skattefritt traktamente, skattefri milersättning) to the net payout only.
- booking debits the claim's liability account (2820) on top of gross,
  never a 7xxx cost; a run that only repays utlägg posts 2820 D / 1930 K
  instead of being treated as a nollkörning
- salary_line_items.source_expense_claim_id (tenant-scoped FK, cascade,
  one payslip line per claim); settle_expense_claims_via_salary_run marks
  the claims paid with an expense_payout_batches row pointing at the
  salary verifikat, no second verifikat, idempotent on retry; wired into
  bookLoadedRun and the v1 book route with a pre-check before posting
- create_expense_payout_batch refuses claims scheduled on a payslip
  (ON_PAYSLIP); deleteExpenseClaim refuses once the run has left draft
- "Lägg till utlägg" on the employee row of a draft run; the payslip page
  labels and removes the lines
- pg-real: tests/pg/utlagg-via-lon.pg.test.ts + ON_PAYSLIP case

Claude-Session: https://claude.ai/code/session_01LvMaHcTnwAfxzgYD1fGYX1
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(salary): PR #2361 review: claim delete cannot cascade into a booked payslip; AGI excludes the utlägg line

- salary_line_items_source_expense_claim_fkey is ON DELETE RESTRICT (edited
  in the unmerged 20260906210300): the database refuses to delete a claim a
  payslip line still references, whichever path issues the DELETE
- deleteExpenseClaim removes the draft line first (before the storno) and
  keeps refusing with ON_PAYSLIP once the run has left draft
- pg-real: delete refused with 23503 on a booked and on a draft run; the
  app order (line, then claim) succeeds
- unit: AGI builder keeps FK011/FK001/FK487 and emits no benefit field for
  an expense_reimbursement line (FK011 derives from sre.gross_salary; only
  benefit_* types are read from line items)

Claude-Session: https://claude.ai/code/session_01LvMaHcTnwAfxzgYD1fGYX1
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

---------

Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Jakob Wennberg
2026-09-06 21:17:46 +02:00
committed by GitHub
co-authored by Claude Fable 5.1 Jakob Wennberg
parent 0069a3f59a
commit 4fce2d7b94
30 changed files with 2636 additions and 17 deletions
+11 -2
View File
@@ -7168,7 +7168,13 @@
"toast_payslips_downloaded_detail": "{count} files in a zip archive.",
"toast_zip_failed": "Could not create zip file",
"toast_agi_failed": "AGI file could not be generated",
"toast_agi_downloaded": "AGI file downloaded"
"toast_agi_downloaded": "AGI file downloaded",
"add_expense_claims": "Add {count, plural, one {# expense claim} other {# expense claims}} · {amount}",
"add_expense_claims_aria": "Add {name}'s open expense claims to the payslip",
"add_expense_claims_title": "Add open expense claims to the payslip: paid out tax-free with the salary",
"toast_expense_claims_added": "Expense claims added to the payslip",
"toast_expense_claims_added_detail": "{count, plural, one {# expense claim} other {# expense claims}} · {amount}. Calculate the run to update the net.",
"toast_expense_claims_failed": "Could not add expense claims"
},
"salary_agi": {
"title": "Arbetsgivardeklaration (AGI)",
@@ -7606,7 +7612,10 @@
"th_type": "Type",
"th_description": "Description",
"th_quantity": "Quantity",
"th_amount": "Amount"
"th_amount": "Amount",
"th_actions": "Actions",
"li_expense_reimbursement": "Expense reimbursement (tax-free)",
"remove_expense_claim_line_aria": "Remove the expense claim from the payslip"
},
"salary_tax_tables": {
"checking": "Checking tax tables…",
+11 -2
View File
@@ -7168,7 +7168,13 @@
"toast_payslips_downloaded_detail": "{count} stycken i zip-arkiv.",
"toast_zip_failed": "Kunde inte skapa zip-fil",
"toast_agi_failed": "AGI-fil kunde inte genereras",
"toast_agi_downloaded": "AGI-fil nedladdad"
"toast_agi_downloaded": "AGI-fil nedladdad",
"add_expense_claims": "Lägg till {count, plural, one {# utlägg} other {# utlägg}} · {amount}",
"add_expense_claims_aria": "Lägg till öppna utlägg för {name} på lönebeskedet",
"add_expense_claims_title": "Lägg till öppna utlägg på lönebeskedet: betalas ut skattefritt med lönen",
"toast_expense_claims_added": "Utlägg tillagda på lönebeskedet",
"toast_expense_claims_added_detail": "{count, plural, one {# utlägg} other {# utlägg}} · {amount}. Beräkna körningen för att uppdatera nettot.",
"toast_expense_claims_failed": "Kunde inte lägga till utlägg"
},
"salary_agi": {
"title": "Arbetsgivardeklaration (AGI)",
@@ -7606,7 +7612,10 @@
"th_type": "Typ",
"th_description": "Beskrivning",
"th_quantity": "Antal",
"th_amount": "Belopp"
"th_amount": "Belopp",
"th_actions": "Åtgärder",
"li_expense_reimbursement": "Utlägg (skattefritt)",
"remove_expense_claim_line_aria": "Ta bort utlägget från lönebeskedet"
},
"salary_tax_tables": {
"checking": "Kontrollerar skattetabeller…",