fix: correct INK2R SRU sign convention and field ordering (#249)

* fix: correct INK2R SRU sign convention and field ordering

Cost fields (7511-7528) were negated to negative values, but Skatteverket
expects positive values on the INK2R form. Updated the engine to keep
debit-normal income statement balances positive and adjusted all result
calculations accordingly. Also switched SRU field output from Object.keys()
to canonical Skatteverket ordering arrays.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: address Greptile review — deduplicate code arrays, add disposition test

Remove duplicate ASSET_CODES/EQUITY_LIABILITY_CODES arrays from the engine
and import the canonical arrays from types.ts. Add test coverage for
bokslutsdispositioner fields (7524, 7525, 7419) to exercise the new
subtraction sign path.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jakob Wennberg
2026-04-15 16:07:03 +02:00
committed by GitHub
co-authored by Claude Opus 4.6
parent e46654ab25
commit 24466c6e94
3 changed files with 61 additions and 42 deletions
@@ -20,9 +20,9 @@ function makeDeclaration(overrides?: Partial<INK2Declaration>): INK2Declaration
'7360': 0, '7361': 0, '7362': 0, '7363': 0, '7364': 0,
'7365': 30000, '7366': 0, '7367': 0, '7369': 70000, '7368': 0, '7370': 0,
'7410': 500000, '7411': 0, '7412': 0, '7413': 0,
'7511': 0, '7512': 0, '7513': -100000, '7514': -80000, '7515': -10000, '7516': 0, '7517': -5000,
'7511': 0, '7512': 0, '7513': 100000, '7514': 80000, '7515': 10000, '7516': 0, '7517': 5000,
'7414': 0, '7415': 0, '7423': 0, '7416': 0, '7417': 0,
'7521': 0, '7522': -3000,
'7521': 0, '7522': 3000,
'7524': 0, '7419': 0, '7420': 0, '7525': 0, '7421': 0, '7422': 0,
'7528': 0,
'7450': 302000, '7550': 0,
@@ -209,13 +209,31 @@ describe('INK2 SRU Generator', () => {
}
})
it('handles negative values correctly', () => {
it('reports cost fields as positive values per Skatteverket convention', () => {
const declaration = makeDeclaration()
const submission = generateSRUSubmission(declaration)
const ink2rBlock = extractBlock(submission.blanketterSru, 'INK2R')
expect(ink2rBlock).toContain('#UPPGIFT 7513 -100000')
expect(ink2rBlock).toContain('#UPPGIFT 7522 -3000')
expect(ink2rBlock).toContain('#UPPGIFT 7513 100000')
expect(ink2rBlock).toContain('#UPPGIFT 7522 3000')
})
it('handles bokslutsdispositioner fields with correct sign', () => {
const declaration = makeDeclaration({
ink2r: {
...makeDeclaration().ink2r,
'7524': 50000, // Lämnade koncernbidrag (debit-normal cost, positive)
'7525': 30000, // Avsättning periodiseringsfond (debit-normal cost, positive)
'7419': 20000, // Mottagna koncernbidrag (credit-normal income, positive)
},
})
const submission = generateSRUSubmission(declaration)
const ink2rBlock = extractBlock(submission.blanketterSru, 'INK2R')
// All bokslutsdispositioner fields are positive in the SRU output
expect(ink2rBlock).toContain('#UPPGIFT 7524 50000')
expect(ink2rBlock).toContain('#UPPGIFT 7525 30000')
expect(ink2rBlock).toContain('#UPPGIFT 7419 20000')
})
it('includes INK2S with överskott/underskott', () => {
+27 -35
View File
@@ -13,6 +13,10 @@ import type {
INK2AccountMapping,
INK2RSRUCode,
} from './types'
import {
INK2R_ASSET_CODES,
INK2R_EQUITY_LIABILITY_CODES,
} from './types'
/**
* INK2 Declaration Engine
@@ -667,24 +671,9 @@ function createEmptyINK2RRutor(): INK2RRutor {
}
}
/** All INK2R asset codes for summing */
const ASSET_CODES: INK2RSRUCode[] = [
'7201', '7202', '7214', '7215', '7216', '7217',
'7230', '7231', '7233', '7232', '7234', '7235',
'7241', '7242', '7243', '7244', '7245', '7246',
'7251', '7252', '7261', '7262', '7263',
'7270', '7271', '7281',
]
/** All INK2R equity/liability codes for summing */
const EQUITY_LIABILITY_CODES: INK2RSRUCode[] = [
'7301', '7302',
'7321', '7322', '7323',
'7331', '7332', '7333',
'7350', '7351', '7352', '7353', '7354',
'7360', '7361', '7362', '7363', '7364', '7365', '7366', '7367', '7369', '7368',
'7370',
]
// Reuse canonical code arrays from types.ts (single source of truth)
const ASSET_CODES = INK2R_ASSET_CODES
const EQUITY_LIABILITY_CODES = INK2R_EQUITY_LIABILITY_CODES
/**
* Generate INK2 declaration for a fiscal period
@@ -791,15 +780,16 @@ export async function generateINK2Declaration(
let amount: number
if (mapping.section === 'income_statement') {
// Income statement: supply the signed value as-is from accounting
// Revenue (credit normal): balance is negative (credit > debit), negate for positive revenue
// Cost (debit normal): balance is positive (debit > credit), keep as-is for negative cost
// Income statement sign convention per Skatteverket INK2R:
// All amounts are reported as positive values on the form.
// Revenue (credit normal): balance is negative in ledger, negate → positive
// Cost (debit normal): balance is positive in ledger, keep → positive
// Net: negate so positive = income, negative = cost
if (mapping.normalBalance === 'credit') {
amount = -balance
} else if (mapping.normalBalance === 'debit') {
// Costs: debit balance is positive in ledger, but on the form they appear negative
amount = -balance
// Costs: debit balance is positive in ledger, keep positive (Skatteverket convention)
amount = balance
} else {
// Net: negate to match accounting convention
amount = -balance
@@ -847,26 +837,27 @@ export async function generateINK2Declaration(
const totalAssets = ASSET_CODES.reduce((sum, code) => sum + ink2r[code], 0)
const totalEquityLiabilities = EQUITY_LIABILITY_CODES.reduce((sum, code) => sum + ink2r[code], 0)
// Operating result: revenue + costs (costs are already negative)
// Operating result: revenue minus costs (costs are positive per Skatteverket convention)
const operatingResult =
ink2r['7410'] + ink2r['7411'] + ink2r['7412'] + ink2r['7413'] +
ink2r['7511'] + ink2r['7512'] + ink2r['7513'] + ink2r['7514'] +
ink2r['7515'] + ink2r['7516'] + ink2r['7517']
ink2r['7410'] + ink2r['7411'] + ink2r['7412'] + ink2r['7413']
- ink2r['7511'] - ink2r['7512'] - ink2r['7513'] - ink2r['7514']
- ink2r['7515'] - ink2r['7516'] - ink2r['7517']
// Financial items
// Financial items: income minus costs
const financialItems =
ink2r['7414'] + ink2r['7415'] + ink2r['7423'] + ink2r['7416'] + ink2r['7417'] +
ink2r['7521'] + ink2r['7522']
ink2r['7414'] + ink2r['7415'] + ink2r['7423'] + ink2r['7416'] + ink2r['7417']
- ink2r['7521'] - ink2r['7522']
// Bokslutsdispositioner
// Bokslutsdispositioner: subtract debit-normal, add credit-normal and net
const bokslutsdispositioner =
ink2r['7524'] + ink2r['7419'] + ink2r['7420'] + ink2r['7525'] + ink2r['7421'] + ink2r['7422']
- ink2r['7524'] + ink2r['7419'] + ink2r['7420'] - ink2r['7525']
+ ink2r['7421'] + ink2r['7422']
// Result before tax
const resultBeforeTax = operatingResult + financialItems + bokslutsdispositioner
// Result after tax
const resultAfterFinancial = resultBeforeTax + ink2r['7528']
// Result after tax (7528 is positive, subtract it)
const resultAfterFinancial = resultBeforeTax - ink2r['7528']
// Set årets resultat: vinst (7450) or förlust (7550)
if (resultAfterFinancial >= 0) {
@@ -888,7 +879,8 @@ export async function generateINK2Declaration(
// Build INK2 (huvudblankett)
// Auto-derive from INK2S result (simplified: result + non-deductible tax)
const taxAmount = Math.abs(ink2r['7528'])
// 7528 is already positive per Skatteverket convention
const taxAmount = ink2r['7528']
const taxableResult = resultAfterFinancial + taxAmount
const ink2: INK2Rutor = {
+11 -2
View File
@@ -4,6 +4,11 @@ import type {
INK2SRutor,
SRUSubmission,
} from './types'
import {
INK2R_ASSET_CODES,
INK2R_EQUITY_LIABILITY_CODES,
INK2R_INCOME_CODES,
} from './types'
/**
* SRU File Generator for INK2 (Aktiebolag)
@@ -170,8 +175,12 @@ function generateBlanketterSru(declaration: INK2Declaration, now: Date): string
lines.push(`#UPPGIFT 7011 ${declaration.ink2['7011']}`)
lines.push(`#UPPGIFT 7012 ${declaration.ink2['7012']}`)
// All INK2R fields — emit non-zero values only
const ink2rCodes: INK2RSRUCode[] = Object.keys(declaration.ink2r) as INK2RSRUCode[]
// All INK2R fields in canonical Skatteverket order — emit non-zero values only
const ink2rCodes: INK2RSRUCode[] = [
...INK2R_ASSET_CODES,
...INK2R_EQUITY_LIABILITY_CODES,
...INK2R_INCOME_CODES,
]
for (const code of ink2rCodes) {
const value = declaration.ink2r[code]
if (value !== 0) {