fix(salary): book net deductions as settlement lines so salary entries balance (#1374)

* fix(salary): book net deductions as settlement lines so salary entries balance

Net deduction line items were skipped entirely in createSalaryEntry, so the
credit side (2710 tax + 1930 net) fell short of the gross debit by exactly
the deducted amount and the balance trigger rejected the voucher.

Net deductions now book on their mapped settlement account (1613 advance
repayment, 2794 union fee, 7385 benefit co-payment, 2799 other; explicit
account_number overrides), aggregated and undimensioned like the other
settlement legs. The default mapping in account-mapping.ts moves off 7210
so payslip lines, the booking preview and the voucher all agree.

Fixes #316

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

* fix(salary): address compliance review on net-deduction accounts

Label 7385 with its BAS 2026 name (Kostnader för fri bil) instead of the
benefit-generic Bilförmån, document why the single benefit-payment item
type defaults to 7385 with per-line override for other benefit kinds, and
add a repayment-direction test (positive net deduction books as debit).

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-03 18:51:53 +02:00
committed by GitHub
co-authored by Claude Fable 5
parent 8443062b1f
commit 0ef3c03904
4 changed files with 178 additions and 6 deletions
+22 -1
View File
@@ -2,6 +2,7 @@ import { NextResponse } from 'next/server'
import { ensureInitialized } from '@/lib/init'
import { withRouteContext } from '@/lib/api/with-route-context'
import { SALARY_ACCOUNTS, getLineItemAccount } from '@/lib/salary/account-mapping'
import { roundOre } from '@/lib/money'
import type { CreateJournalEntryLineInput } from '@/types'
ensureInitialized()
@@ -43,10 +44,19 @@ export const GET = withRouteContext<{ params: Promise<{ id: string }> }>(
// Build salary entry preview
const salaryLines: CreateJournalEntryLineInput[] = []
const expenseByAccount = new Map<string, number>()
// Net deductions book as settlement lines on their mapped liability or
// receivable account, mirroring createSalaryEntry; they must not merge
// into the 7xxx expense buckets.
const netDeductionByAccount = new Map<string, number>()
for (const sre of employees) {
for (const li of sre.line_items || []) {
if (li.is_net_deduction || li.is_gross_deduction) continue
if (li.is_net_deduction) {
const account = li.account_number || getLineItemAccount(li.item_type, sre.employee?.employment_type || 'employee')
netDeductionByAccount.set(account, (netDeductionByAccount.get(account) || 0) + li.amount)
continue
}
if (li.is_gross_deduction) continue
const account = li.account_number || getLineItemAccount(li.item_type, sre.employee?.employment_type || 'employee')
expenseByAccount.set(account, (expenseByAccount.get(account) || 0) + li.amount)
}
@@ -62,6 +72,17 @@ export const GET = withRouteContext<{ params: Promise<{ id: string }> }>(
})
}
for (const [account, amount] of netDeductionByAccount) {
const rounded = roundOre(Math.abs(amount))
if (rounded === 0) continue
salaryLines.push({
account_number: account,
debit_amount: amount > 0 ? rounded : 0,
credit_amount: amount < 0 ? rounded : 0,
line_description: `${desc}`,
})
}
const totalTax = employees.reduce((sum, e) => sum + e.tax_withheld, 0)
if (totalTax > 0) {
salaryLines.push({