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:
co-authored by
Claude Opus 4.6
parent
e46654ab25
commit
24466c6e94
@@ -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', () => {
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user