feat(salary): öresavrundning of net pay to whole kronor (#1609)
* feat(salary): öresavrundning of net pay to whole kronor Some banks reject salary payment files whose amounts carry öre. New company_settings.salary_net_rounding toggle (off by default): the engine rounds each net payout up to the next whole krona, never down, and emits a derived oresavrundning line item (semesterersattning pattern) that debits 3740 Öres- och kronutjämning so the salary entry stays balanced. Gross, tax and avgifter are untouched, so AGI/KU are unaffected. Payment files (pain.001 + Bankgirot LB) get whole-krona amounts via the rounded net_salary. Toggle in salary settings; payslip and run detail show the line item. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(salary): keep employer cost on the shared definition; block manual rounding lines Skeptic findings on the öresavrundning commit: (1) the engine included netRounding in totalEmployerCost while payslip summary, KPI cards and lönejournal recompute the figure from stored columns, printing two different totals on the same payslip; employer cost now stays on the shared definition and the öre cost is carried by the 3740 ledger line. (2) 'oresavrundning' is excluded from the line-item create/update schemas: it is the only item type the booking keeps out of the gross reconciliation, so a manually created row would structurally unbalance the salary verifikat. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(salary): add the item_type CHECK as NOT VALID, validate separately Compliance-swarm finding (SOC 2 CC8.1): the CHECK re-add scanned salary_line_items under the ADD's ACCESS EXCLUSIVE lock. Split per the house pattern (DECISIONS.md 2026-07-13): 20260813143000 re-adds the constraint NOT VALID, new 20260813143001 validates it under SHARE UPDATE EXCLUSIVE in its own transaction. The list is a strict superset of the previous CHECK, so validation cannot fail. Both files are branch-only, so editing in place is within the never-modify-shipped rule. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
4a9fa5e6c5
commit
4bb0655e4a
@@ -14,6 +14,7 @@ import {
|
||||
SettingsSelect,
|
||||
} from '@/components/settings/SettingsRows'
|
||||
import { TaxTableStatus } from '@/components/salary/TaxTableStatus'
|
||||
import { Switch } from '@/components/ui/switch'
|
||||
import { useSettings } from '@/components/settings/useSettings'
|
||||
import { resolveDefaultSeriesForSource } from '@/lib/bookkeeping/voucher-series-resolver'
|
||||
import type { CompanySettings } from '@/types'
|
||||
@@ -36,11 +37,15 @@ export function SalarySettingsContent() {
|
||||
const { settings, isLoading, updateSettings, refetch } = useSettings()
|
||||
// Controlled so the LB sunset note reacts to the selection before save.
|
||||
const [format, setFormat] = useState<'bg_lb' | 'pain001' | null>(null)
|
||||
// Controlled: the Radix Switch is not a form element, so its value rides
|
||||
// along in handleSave instead of FormData.
|
||||
const [netRounding, setNetRounding] = useState<boolean | null>(null)
|
||||
|
||||
if (isLoading) return <SettingsLoadingSkeleton />
|
||||
if (!settings) return <SettingsLoadError onRetry={refetch} />
|
||||
|
||||
const effectiveFormat = format ?? settings.preferred_payment_format ?? 'pain001'
|
||||
const effectiveNetRounding = netRounding ?? settings.salary_net_rounding ?? false
|
||||
const currentSeries = resolveDefaultSeriesForSource(settings, 'salary_payment')
|
||||
|
||||
function handleSave(formData: FormData) {
|
||||
@@ -54,6 +59,7 @@ export function SalarySettingsContent() {
|
||||
salary_pay_day: payDay,
|
||||
preferred_payment_format: paymentFormat,
|
||||
salary_default_bank: bank === 'none' ? null : bank,
|
||||
salary_net_rounding: effectiveNetRounding,
|
||||
}
|
||||
|
||||
// The booking engine resolves the series from the per-source-type map;
|
||||
@@ -132,6 +138,16 @@ export function SalarySettingsContent() {
|
||||
<option value="other">{t('bank_other')}</option>
|
||||
</SettingsSelect>
|
||||
</SettingsRow>
|
||||
<SettingsRow label={t('net_rounding_label')} help={t('net_rounding_help')}>
|
||||
<Switch
|
||||
id="salary_net_rounding"
|
||||
checked={effectiveNetRounding}
|
||||
onCheckedChange={(next) => setNetRounding(next)}
|
||||
/>
|
||||
<label htmlFor="salary_net_rounding" className="cursor-pointer text-sm">
|
||||
{t('net_rounding_toggle')}
|
||||
</label>
|
||||
</SettingsRow>
|
||||
</SettingsGroup>
|
||||
|
||||
<SettingsGroup label={t('accounting_heading')}>
|
||||
|
||||
Reference in New Issue
Block a user