feat: INK2 declaration improvements, invoice delivery date & Swedish compliance skills (#204)

* feat: add INK2 declaration improvements, invoice delivery date, and Swedish compliance skills

Expand INK2 engine with full INK2S/INK2R support and improved SRU generation.
Add delivery_date field to invoices and corresponding PDF/migration support.
Add Claude skills for Swedish asset accounting, invoice compliance, SIE import/export, SRU filing, and tax planning.

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

* fix: address PR review — map BAS 4500–4899, strip CRLF in SRU, document P3

- Map BAS accounts 4500–4599 (legoarbeten), 4700–4899 (diverse
  varuinköpskostnader) to SRU 7512 so they are not silently dropped
  from INK2R declarations
- Strip \r\n in sanitizeString to prevent CRLF injection in SRU fields
- Document P3 period suffix limitation for brutet räkenskapsår

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

* fix: correct BAS 4500-4599, 4700-4899 mapping from 7512 to 7511

Per the official BAS-to-SRU mapping, these account ranges are cost of
goods (legoarbeten, inkurans, svinn) and belong under 7511 (Råvaror
och förnödenheter), not 7512 (Handelsvaror). 7512 remains 4600-4699.

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-09 13:16:10 +02:00
committed by GitHub
co-authored by Claude Opus 4.6
parent bf5a8d9195
commit 7a18d89c70
37 changed files with 5481 additions and 658 deletions
+1
View File
@@ -157,6 +157,7 @@ export const CreateInvoiceSchema = z.object({
customer_id: uuid,
invoice_date: isoDate,
due_date: isoDate,
delivery_date: isoDate.optional(),
currency: CurrencySchema,
document_type: InvoiceDocumentTypeSchema.optional(),
your_reference: z.string().optional(),
+34 -8
View File
@@ -351,6 +351,12 @@ export function InvoicePDF({ invoice, customer, items, company, originalInvoiceN
<Text style={styles.label}>Förfallodatum:</Text>
<Text style={styles.value}>{formatDate(invoice.due_date)}</Text>
</View>
{invoice.delivery_date && invoice.delivery_date !== invoice.invoice_date && (
<View style={styles.row}>
<Text style={styles.label}>Leveransdatum:</Text>
<Text style={styles.value}>{formatDate(invoice.delivery_date)}</Text>
</View>
)}
{invoice.your_reference && (
<View style={{ marginBottom: 4 }}>
<Text style={styles.label}>Er referens:</Text>
@@ -447,12 +453,19 @@ export function InvoicePDF({ invoice, customer, items, company, originalInvoiceN
</View>
{vatByRate.size > 1 ? (
Array.from(vatByRate.entries())
.filter(([, group]) => group.vat > 0)
.sort(([a], [b]) => b - a)
.map(([rate, group]) => (
<View key={rate} style={styles.totalRow}>
<Text style={styles.totalLabel}>Moms {rate}%:</Text>
<Text style={styles.totalValue}>{formatCurrency(group.vat, invoice.currency)}</Text>
<View key={rate}>
<View style={styles.totalRow}>
<Text style={styles.totalLabel}>Netto {rate}%:</Text>
<Text style={styles.totalValue}>{formatCurrency(group.base, invoice.currency)}</Text>
</View>
{group.vat > 0 && (
<View style={styles.totalRow}>
<Text style={styles.totalLabel}>Moms {rate}%:</Text>
<Text style={styles.totalValue}>{formatCurrency(group.vat, invoice.currency)}</Text>
</View>
)}
</View>
))
) : (
@@ -477,9 +490,17 @@ export function InvoicePDF({ invoice, customer, items, company, originalInvoiceN
</View>
)}
{invoice.currency !== 'SEK' && invoice.total_sek && (
<View style={[styles.totalRow, { marginTop: 8 }]}>
<Text style={[styles.totalLabel, { fontSize: 9 }]}>I SEK (kurs {invoice.exchange_rate}):</Text>
<Text style={[styles.totalValue, { fontSize: 9 }]}>{formatCurrency(invoice.total_sek, 'SEK')}</Text>
<View style={{ marginTop: 8 }}>
{invoice.vat_amount_sek != null && invoice.vat_amount_sek !== 0 && (
<View style={styles.totalRow}>
<Text style={[styles.totalLabel, { fontSize: 9 }]}>Moms i SEK (kurs {invoice.exchange_rate}):</Text>
<Text style={[styles.totalValue, { fontSize: 9 }]}>{formatCurrency(invoice.vat_amount_sek, 'SEK')}</Text>
</View>
)}
<View style={styles.totalRow}>
<Text style={[styles.totalLabel, { fontSize: 9 }]}>Totalt i SEK:</Text>
<Text style={[styles.totalValue, { fontSize: 9 }]}>{formatCurrency(invoice.total_sek, 'SEK')}</Text>
</View>
</View>
)}
</View>
@@ -549,12 +570,17 @@ export function InvoicePDF({ invoice, customer, items, company, originalInvoiceN
</View>
)}
{/* Reverse charge notice */}
{/* Reverse charge / export / exempt notice */}
{invoice.reverse_charge_text && (
<View style={styles.reverseChargeBox}>
<Text style={styles.reverseChargeText}>{invoice.reverse_charge_text}</Text>
</View>
)}
{invoice.vat_treatment === 'exempt' && !invoice.reverse_charge_text && (
<View style={styles.reverseChargeBox}>
<Text style={styles.reverseChargeText}>Undantag från skatteplikt, ML 3 kap.</Text>
</View>
)}
{/* Notes */}
{invoice.notes && (
+1
View File
@@ -103,6 +103,7 @@ export function getVatRules(
treatment: 'export',
rate: 0,
momsRuta: '40',
reverseChargeText: 'Omsättning utanför EU, ML 10 kap.',
}
default:
+191 -158
View File
@@ -1,12 +1,12 @@
import { describe, it, expect } from 'vitest'
import { INK2_ACCOUNT_MAPPINGS, isAccountInMapping, checkBalanceWarning } from '../ink2-engine'
import type { INK2SRUCode } from '../types'
import { INK2R_ACCOUNT_MAPPINGS, isAccountInMapping, checkBalanceWarning } from '../ink2-engine'
import type { INK2RSRUCode } from '../types'
/**
* Helper to find which SRU code an account maps to
*/
function findSRUCodeForAccount(accountNumber: string): INK2SRUCode | null {
for (const mapping of INK2_ACCOUNT_MAPPINGS) {
function findSRUCodeForAccount(accountNumber: string): INK2RSRUCode | null {
for (const mapping of INK2R_ACCOUNT_MAPPINGS) {
if (isAccountInMapping(accountNumber, mapping)) {
return mapping.sruCode
}
@@ -14,244 +14,277 @@ function findSRUCodeForAccount(accountNumber: string): INK2SRUCode | null {
return null
}
describe('INK2 Account Mappings', () => {
describe('INK2R Account Mappings', () => {
describe('completeness', () => {
it('has 19 mappings covering all INK2 fields', () => {
expect(INK2_ACCOUNT_MAPPINGS).toHaveLength(19)
it('has mappings covering all INK2R balance sheet and income statement fields', () => {
// 26 asset + 21 equity/liability + 20 income statement = 67 mappings
expect(INK2R_ACCOUNT_MAPPINGS.length).toBeGreaterThanOrEqual(60)
})
it('covers all SRU codes', () => {
const codes = INK2_ACCOUNT_MAPPINGS.map(m => m.sruCode)
const expectedCodes: INK2SRUCode[] = [
'7201', '7202', '7203', '7210', '7211', '7212',
'7220', '7221', '7222', '7230', '7231',
'7310', '7320', '7330', '7340', '7350', '7360', '7370', '7380',
]
expect(codes).toEqual(expectedCodes)
it('covers all expected SRU codes', () => {
const codes = new Set(INK2R_ACCOUNT_MAPPINGS.map(m => m.sruCode))
// Balance sheet asset codes
for (const code of ['7201', '7202', '7214', '7215', '7216', '7217', '7281']) {
expect(codes.has(code as INK2RSRUCode)).toBe(true)
}
// Equity/liability codes
for (const code of ['7301', '7302', '7321', '7322', '7365', '7368', '7370']) {
expect(codes.has(code as INK2RSRUCode)).toBe(true)
}
// Income statement codes
for (const code of ['7410', '7513', '7514', '7515', '7522', '7528']) {
expect(codes.has(code as INK2RSRUCode)).toBe(true)
}
})
})
describe('Balance sheet - Assets', () => {
it('1000-1099 -> 7201 (Immateriella AT)', () => {
expect(findSRUCodeForAccount('1000')).toBe('7201')
describe('Balance sheet - Assets (per bas.se/kontoplaner/sru/)', () => {
it('1010-1079, 1090-1099 -> 7201 (Immateriella AT excl. förskott)', () => {
expect(findSRUCodeForAccount('1010')).toBe('7201')
expect(findSRUCodeForAccount('1050')).toBe('7201')
expect(findSRUCodeForAccount('1079')).toBe('7201')
expect(findSRUCodeForAccount('1090')).toBe('7201')
expect(findSRUCodeForAccount('1099')).toBe('7201')
})
it('1100-1299 -> 7202 (Materiella AT)', () => {
expect(findSRUCodeForAccount('1100')).toBe('7202')
expect(findSRUCodeForAccount('1210')).toBe('7202')
expect(findSRUCodeForAccount('1299')).toBe('7202')
it('1080-1089 -> 7202 (Förskott immateriella)', () => {
expect(findSRUCodeForAccount('1080')).toBe('7202')
expect(findSRUCodeForAccount('1089')).toBe('7202')
})
it('1300-1399 -> 7203 (Finansiella AT)', () => {
expect(findSRUCodeForAccount('1300')).toBe('7203')
expect(findSRUCodeForAccount('1350')).toBe('7203')
expect(findSRUCodeForAccount('1399')).toBe('7203')
it('1100-1119, 1130-1179, 1190-1199 -> 7214 (Byggnader och mark)', () => {
expect(findSRUCodeForAccount('1100')).toBe('7214')
expect(findSRUCodeForAccount('1110')).toBe('7214')
expect(findSRUCodeForAccount('1130')).toBe('7214')
expect(findSRUCodeForAccount('1190')).toBe('7214')
})
it('1400-1499 -> 7210 (Varulager)', () => {
expect(findSRUCodeForAccount('1400')).toBe('7210')
expect(findSRUCodeForAccount('1460')).toBe('7210')
expect(findSRUCodeForAccount('1499')).toBe('7210')
it('1120-1129 -> 7216 (Förbättringsutgifter annans fastighet)', () => {
expect(findSRUCodeForAccount('1120')).toBe('7216')
expect(findSRUCodeForAccount('1129')).toBe('7216')
})
it('1500-1599 -> 7211 (Kundfordringar)', () => {
expect(findSRUCodeForAccount('1500')).toBe('7211')
expect(findSRUCodeForAccount('1510')).toBe('7211')
expect(findSRUCodeForAccount('1599')).toBe('7211')
it('1180-1189 -> 7217 (Pågående nyanläggningar)', () => {
expect(findSRUCodeForAccount('1180')).toBe('7217')
expect(findSRUCodeForAccount('1189')).toBe('7217')
})
it('1600-1999 -> 7212 (Övriga OT)', () => {
expect(findSRUCodeForAccount('1600')).toBe('7212')
expect(findSRUCodeForAccount('1930')).toBe('7212')
expect(findSRUCodeForAccount('1999')).toBe('7212')
it('1200-1299 -> 7215 (Maskiner och inventarier)', () => {
expect(findSRUCodeForAccount('1200')).toBe('7215')
expect(findSRUCodeForAccount('1250')).toBe('7215')
expect(findSRUCodeForAccount('1299')).toBe('7215')
})
it('1500-1519 -> 7251 (Kundfordringar)', () => {
expect(findSRUCodeForAccount('1500')).toBe('7251')
expect(findSRUCodeForAccount('1510')).toBe('7251')
expect(findSRUCodeForAccount('1519')).toBe('7251')
})
it('1520-1559 -> 7261 (Övriga fordringar, not 7251)', () => {
expect(findSRUCodeForAccount('1520')).toBe('7261')
expect(findSRUCodeForAccount('1550')).toBe('7261')
})
it('1700-1799 -> 7263 (Förutbetalda kostnader)', () => {
expect(findSRUCodeForAccount('1700')).toBe('7263')
expect(findSRUCodeForAccount('1790')).toBe('7263')
})
it('1900-1999 -> 7281 (Kassa och bank)', () => {
expect(findSRUCodeForAccount('1900')).toBe('7281')
expect(findSRUCodeForAccount('1930')).toBe('7281')
expect(findSRUCodeForAccount('1999')).toBe('7281')
})
})
describe('Balance sheet - Equity & Liabilities', () => {
it('2081 -> 7220 (Aktiekapital)', () => {
expect(findSRUCodeForAccount('2081')).toBe('7220')
it('2010-2089 -> 7301 (Bundet EK)', () => {
expect(findSRUCodeForAccount('2010')).toBe('7301')
expect(findSRUCodeForAccount('2081')).toBe('7301')
expect(findSRUCodeForAccount('2089')).toBe('7301')
})
it('2081 does NOT go to 7221', () => {
expect(findSRUCodeForAccount('2081')).not.toBe('7221')
it('2090-2099 -> 7302 (Fritt EK)', () => {
expect(findSRUCodeForAccount('2090')).toBe('7302')
expect(findSRUCodeForAccount('2099')).toBe('7302')
})
it('2000-2080 -> 7221 (Övrigt EK)', () => {
expect(findSRUCodeForAccount('2000')).toBe('7221')
expect(findSRUCodeForAccount('2010')).toBe('7221')
expect(findSRUCodeForAccount('2080')).toBe('7221')
it('2110-2129 -> 7321 (Periodiseringsfonder)', () => {
expect(findSRUCodeForAccount('2110')).toBe('7321')
expect(findSRUCodeForAccount('2120')).toBe('7321')
})
it('2082-2098 -> 7221 (Övrigt EK)', () => {
expect(findSRUCodeForAccount('2082')).toBe('7221')
expect(findSRUCodeForAccount('2090')).toBe('7221')
expect(findSRUCodeForAccount('2098')).toBe('7221')
it('2150-2159 -> 7322 (Ackumulerade överavskrivningar)', () => {
expect(findSRUCodeForAccount('2150')).toBe('7322')
expect(findSRUCodeForAccount('2159')).toBe('7322')
})
it('2099 -> 7222 (Årets resultat)', () => {
expect(findSRUCodeForAccount('2099')).toBe('7222')
it('2440-2449 -> 7365 (Leverantörsskulder)', () => {
expect(findSRUCodeForAccount('2440')).toBe('7365')
expect(findSRUCodeForAccount('2449')).toBe('7365')
})
it('2099 does NOT go to 7221', () => {
expect(findSRUCodeForAccount('2099')).not.toBe('7221')
it('2500-2599 -> 7368 (Skatteskulder)', () => {
expect(findSRUCodeForAccount('2500')).toBe('7368')
expect(findSRUCodeForAccount('2510')).toBe('7368')
})
it('2100-2499 -> 7230 (Obeskattade reserver, avsättningar, skulder)', () => {
expect(findSRUCodeForAccount('2100')).toBe('7230')
expect(findSRUCodeForAccount('2150')).toBe('7230') // Obeskattade reserver
expect(findSRUCodeForAccount('2250')).toBe('7230') // Avsättningar
expect(findSRUCodeForAccount('2440')).toBe('7230') // Leverantörsskulder
expect(findSRUCodeForAccount('2499')).toBe('7230')
it('2600-2799 -> 7369 (Övriga skulder kortfristiga, e.g. moms)', () => {
expect(findSRUCodeForAccount('2611')).toBe('7369')
expect(findSRUCodeForAccount('2710')).toBe('7369')
})
it('2500-2999 -> 7231 (Övriga skulder)', () => {
expect(findSRUCodeForAccount('2500')).toBe('7231')
expect(findSRUCodeForAccount('2611')).toBe('7231') // Utgående moms
expect(findSRUCodeForAccount('2710')).toBe('7231') // Personalens källskatt
expect(findSRUCodeForAccount('2999')).toBe('7231')
it('2900-2999 -> 7370 (Upplupna kostnader)', () => {
expect(findSRUCodeForAccount('2900')).toBe('7370')
expect(findSRUCodeForAccount('2999')).toBe('7370')
})
})
describe('Income statement', () => {
it('3000-3999 -> 7310 (Nettoomsättning)', () => {
expect(findSRUCodeForAccount('3000')).toBe('7310')
expect(findSRUCodeForAccount('3001')).toBe('7310')
expect(findSRUCodeForAccount('3100')).toBe('7310')
expect(findSRUCodeForAccount('3999')).toBe('7310')
describe('Income statement (per bas.se — CRITICAL: 5000-6999 ALL → 7513)', () => {
it('3000-3799 -> 7410 (Nettoomsättning)', () => {
expect(findSRUCodeForAccount('3000')).toBe('7410')
expect(findSRUCodeForAccount('3001')).toBe('7410')
expect(findSRUCodeForAccount('3100')).toBe('7410')
expect(findSRUCodeForAccount('3799')).toBe('7410')
})
it('4000-4999 -> 7320 (Varuinköp)', () => {
expect(findSRUCodeForAccount('4000')).toBe('7320')
expect(findSRUCodeForAccount('4010')).toBe('7320')
expect(findSRUCodeForAccount('4999')).toBe('7320')
it('3900-3999 -> 7413 (Övriga rörelseintäkter)', () => {
expect(findSRUCodeForAccount('3900')).toBe('7413')
expect(findSRUCodeForAccount('3999')).toBe('7413')
})
it('5000-6999 -> 7330 (Övriga externa kostnader)', () => {
expect(findSRUCodeForAccount('5000')).toBe('7330')
expect(findSRUCodeForAccount('5460')).toBe('7330')
expect(findSRUCodeForAccount('6200')).toBe('7330')
expect(findSRUCodeForAccount('6999')).toBe('7330')
it('4000-4499 -> 7511 (Råvaror)', () => {
expect(findSRUCodeForAccount('4000')).toBe('7511')
expect(findSRUCodeForAccount('4010')).toBe('7511')
expect(findSRUCodeForAccount('4499')).toBe('7511')
})
it('7000-7699 -> 7340 (Personalkostnader)', () => {
expect(findSRUCodeForAccount('7000')).toBe('7340')
expect(findSRUCodeForAccount('7210')).toBe('7340')
expect(findSRUCodeForAccount('7699')).toBe('7340')
it('4600-4699 -> 7512 (Handelsvaror)', () => {
expect(findSRUCodeForAccount('4600')).toBe('7512')
expect(findSRUCodeForAccount('4699')).toBe('7512')
})
it('7700-7899 -> 7350 (Avskrivningar)', () => {
expect(findSRUCodeForAccount('7700')).toBe('7350')
expect(findSRUCodeForAccount('7820')).toBe('7350')
expect(findSRUCodeForAccount('7899')).toBe('7350')
it('5000-6999 ALL -> 7513 (Övriga externa kostnader)', () => {
expect(findSRUCodeForAccount('5000')).toBe('7513')
expect(findSRUCodeForAccount('5460')).toBe('7513')
expect(findSRUCodeForAccount('6200')).toBe('7513')
expect(findSRUCodeForAccount('6999')).toBe('7513')
})
it('7900-7999 -> 7360 (Övriga rörelsekostnader)', () => {
expect(findSRUCodeForAccount('7900')).toBe('7360')
expect(findSRUCodeForAccount('7970')).toBe('7360')
expect(findSRUCodeForAccount('7999')).toBe('7360')
it('7000-7699 -> 7514 (Personalkostnader)', () => {
expect(findSRUCodeForAccount('7000')).toBe('7514')
expect(findSRUCodeForAccount('7210')).toBe('7514')
expect(findSRUCodeForAccount('7699')).toBe('7514')
})
it('8000-8499 -> 7370 (Finansiella poster)', () => {
expect(findSRUCodeForAccount('8000')).toBe('7370')
expect(findSRUCodeForAccount('8310')).toBe('7370') // Ränteintäkter
expect(findSRUCodeForAccount('8400')).toBe('7370') // Räntekostnader
expect(findSRUCodeForAccount('8499')).toBe('7370')
it('7800-7899 -> 7515 (Avskrivningar)', () => {
expect(findSRUCodeForAccount('7800')).toBe('7515')
expect(findSRUCodeForAccount('7820')).toBe('7515')
expect(findSRUCodeForAccount('7899')).toBe('7515')
})
it('8500-8999 -> 7380 (Extraordinära poster)', () => {
expect(findSRUCodeForAccount('8500')).toBe('7380')
expect(findSRUCodeForAccount('8910')).toBe('7380') // Skatt
expect(findSRUCodeForAccount('8999')).toBe('7380')
it('7700-7799 -> 7516 (Nedskrivningar OT)', () => {
expect(findSRUCodeForAccount('7700')).toBe('7516')
expect(findSRUCodeForAccount('7799')).toBe('7516')
})
it('7900-7999 -> 7517 (Övriga rörelsekostnader)', () => {
expect(findSRUCodeForAccount('7900')).toBe('7517')
expect(findSRUCodeForAccount('7999')).toBe('7517')
})
it('8300-8399 -> 7417 (Ränteintäkter)', () => {
expect(findSRUCodeForAccount('8300')).toBe('7417')
expect(findSRUCodeForAccount('8310')).toBe('7417')
})
it('8400-8499 -> 7522 (Räntekostnader)', () => {
expect(findSRUCodeForAccount('8400')).toBe('7522')
expect(findSRUCodeForAccount('8499')).toBe('7522')
})
it('8900-8989 -> 7528 (Skatt)', () => {
expect(findSRUCodeForAccount('8900')).toBe('7528')
expect(findSRUCodeForAccount('8910')).toBe('7528')
})
})
describe('no overlap between mappings', () => {
it('each account matches exactly one mapping', () => {
// Test a representative sample across boundaries
it('representative boundary accounts match exactly one mapping', () => {
const testAccounts = [
'1099', '1100', // 7201/7202 boundary
'1299', '1300', // 7202/7203 boundary
'1399', '1400', // 7203/7210 boundary
'1499', '1500', // 7210/7211 boundary
'1599', '1600', // 7211/7212 boundary
'1999', '2000', // 7212/7221 boundary
'2080', '2081', '2082', // 7221/7220/7221
'2098', '2099', '2100', // 7221/7222/7230 boundary
'2499', '2500', // 7230/7231 boundary
'2999', '3000', // 7231/7310 boundary
'3999', '4000', // 7310/7320 boundary
'4999', '5000', // 7320/7330 boundary
'6999', '7000', // 7330/7340 boundary
'7699', '7700', // 7340/7350 boundary
'7899', '7900', // 7350/7360 boundary
'7999', '8000', // 7360/7370 boundary
'8499', '8500', // 7370/7380 boundary
'1079', '1080', // 7201/7202 boundary
'1089', '1090', // 7202/7201 boundary
'1099', '1100', // 7201/7214 boundary
'1119', '1120', // 7214/7216 boundary
'1129', '1130', // 7216/7214 boundary
'1199', '1200', // 7214/7215 boundary
'1299', '1311', // 7215/7230 boundary
'1519', '1520', // 7251/7261 boundary
'1559', '1560', // 7261/7252 boundary
'1930', '1999', // 7281 bank accounts
'2089', '2090', // 7301/7302 boundary
'2099', '2110', // 7302/7321 boundary
'2439', '2440', // 7361/7365 boundary
'2449', '2450', // 7365/7363 boundary
'2499', '2500', // 7369/7368 boundary
'2599', '2600', // 7368/7369 boundary
'2899', '2900', // 7369/7370 boundary
'3799', '3800', // 7410/7412 boundary
'3899', '3900', // 7412/7413 boundary
'3999', '4000', // 7413/7511 boundary
'4499', '4600', // 7511/7512 boundary (4500-4599 unmapped)
'5000', '6999', // 7513 (övriga externa)
'7699', '7700', // 7514/7516 boundary
'7799', '7800', // 7516/7515 boundary
'7899', '7900', // 7515/7517 boundary
'8399', '8400', // 7417/7522 boundary
]
for (const account of testAccounts) {
let matchCount = 0
for (const mapping of INK2_ACCOUNT_MAPPINGS) {
for (const mapping of INK2R_ACCOUNT_MAPPINGS) {
if (isAccountInMapping(account, mapping)) {
matchCount++
}
}
expect(matchCount).toBe(1)
expect(matchCount, `Account ${account} should match exactly one mapping, got ${matchCount}`).toBe(1)
}
})
})
describe('section assignments', () => {
it('asset mappings have section "assets"', () => {
const assetMappings = INK2_ACCOUNT_MAPPINGS.filter(m => m.section === 'assets')
expect(assetMappings.map(m => m.sruCode)).toEqual(['7201', '7202', '7203', '7210', '7211', '7212'])
})
it('equity/liability mappings have section "equity_liabilities"', () => {
const eqMappings = INK2_ACCOUNT_MAPPINGS.filter(m => m.section === 'equity_liabilities')
expect(eqMappings.map(m => m.sruCode)).toEqual(['7220', '7221', '7222', '7230', '7231'])
})
it('income statement mappings have section "income_statement"', () => {
const isMappings = INK2_ACCOUNT_MAPPINGS.filter(m => m.section === 'income_statement')
expect(isMappings.map(m => m.sruCode)).toEqual(['7310', '7320', '7330', '7340', '7350', '7360', '7370', '7380'])
})
})
describe('normal balance assignments', () => {
it('asset accounts are debit-normal', () => {
const assetMappings = INK2_ACCOUNT_MAPPINGS.filter(m => m.section === 'assets')
it('all asset mappings have section "assets"', () => {
const assetMappings = INK2R_ACCOUNT_MAPPINGS.filter(m => m.section === 'assets')
expect(assetMappings.length).toBe(26)
for (const m of assetMappings) {
expect(m.normalBalance).toBe('debit')
}
})
it('equity/liability accounts are credit-normal', () => {
const eqMappings = INK2_ACCOUNT_MAPPINGS.filter(m => m.section === 'equity_liabilities')
it('all equity/liability mappings have section "equity_liabilities"', () => {
const eqMappings = INK2R_ACCOUNT_MAPPINGS.filter(m => m.section === 'equity_liabilities')
expect(eqMappings.length).toBe(24)
for (const m of eqMappings) {
expect(m.normalBalance).toBe('credit')
}
})
it('revenue (7310) is credit-normal', () => {
const revenue = INK2_ACCOUNT_MAPPINGS.find(m => m.sruCode === '7310')
it('income statement mappings have correct normal balance types', () => {
const isMappings = INK2R_ACCOUNT_MAPPINGS.filter(m => m.section === 'income_statement')
expect(isMappings.length).toBeGreaterThanOrEqual(20)
// Revenue accounts are credit-normal
const revenue = isMappings.find(m => m.sruCode === '7410')
expect(revenue?.normalBalance).toBe('credit')
})
it('expense accounts (7320-7360) are debit-normal', () => {
const expenseCodes: INK2SRUCode[] = ['7320', '7330', '7340', '7350', '7360']
for (const code of expenseCodes) {
const mapping = INK2_ACCOUNT_MAPPINGS.find(m => m.sruCode === code)
expect(mapping?.normalBalance).toBe('debit')
}
})
// Cost accounts are debit-normal
const costs = isMappings.find(m => m.sruCode === '7513')
expect(costs?.normalBalance).toBe('debit')
it('financial and extraordinary items (7370, 7380) are net', () => {
const financial = INK2_ACCOUNT_MAPPINGS.find(m => m.sruCode === '7370')
const extraordinary = INK2_ACCOUNT_MAPPINGS.find(m => m.sruCode === '7380')
expect(financial?.normalBalance).toBe('net')
expect(extraordinary?.normalBalance).toBe('net')
// Net items
const net = isMappings.find(m => m.sruCode === '7414')
expect(net?.normalBalance).toBe('net')
})
})
})
+257 -109
View File
@@ -1,8 +1,33 @@
import { describe, it, expect } from 'vitest'
import { generateSRUFile, sruFileToString, validateSRUFile, getSRUFilename } from '../sru-generator'
import {
generateSRUSubmission,
validateBlanketterSru,
getZipFilename,
} from '../sru-generator'
import type { INK2Declaration } from '../types'
function makeDeclaration(overrides?: Partial<INK2Declaration>): INK2Declaration {
const defaultInk2r = {
'7201': 0, '7202': 0, '7214': 0, '7215': 50000, '7216': 0, '7217': 0,
'7230': 0, '7231': 0, '7233': 0, '7232': 0, '7234': 0, '7235': 0,
'7241': 0, '7242': 0, '7243': 0, '7244': 0, '7245': 0, '7246': 0,
'7251': 25000, '7252': 0, '7261': 0, '7262': 0, '7263': 0,
'7270': 0, '7271': 0, '7281': 100000,
'7301': 50000, '7302': 20000,
'7321': 0, '7322': 0, '7323': 0,
'7331': 0, '7332': 0, '7333': 0,
'7350': 0, '7351': 0, '7352': 0, '7353': 0, '7354': 0,
'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,
'7414': 0, '7415': 0, '7423': 0, '7416': 0, '7417': 0,
'7521': 0, '7522': -3000,
'7524': 0, '7419': 0, '7420': 0, '7525': 0, '7421': 0, '7422': 0,
'7528': 0,
'7450': 302000, '7550': 0,
} as INK2Declaration['ink2r']
return {
fiscalYear: {
id: 'period-1',
@@ -11,25 +36,36 @@ function makeDeclaration(overrides?: Partial<INK2Declaration>): INK2Declaration
end: '2025-12-31',
isClosed: true,
},
rutor: {
'7201': 0, '7202': 50000, '7203': 0,
'7210': 10000, '7211': 25000, '7212': 100000,
'7220': 50000, '7221': 20000, '7222': 15000,
'7230': 30000, '7231': 70000,
'7310': 500000, '7320': 200000, '7330': 100000,
'7340': 80000, '7350': 10000, '7360': 5000,
'7370': -3000, '7380': 0,
ink2: {
'7011': '20250101',
'7012': '20251231',
'7113': 302000,
'7114': 0,
},
ink2r: defaultInk2r,
ink2s: {
'7011': '20250101',
'7012': '20251231',
'7650': 302000,
'7750': 0,
'7651': 0,
'8020': 302000,
'8021': 0,
},
breakdown: {} as INK2Declaration['breakdown'],
totals: {
totalAssets: 185000,
totalEquityLiabilities: 185000,
operatingResult: 105000,
resultAfterFinancial: 102000,
totalAssets: 175000,
totalEquityLiabilities: 175000,
operatingResult: 305000,
resultAfterFinancial: 302000,
},
companyInfo: {
companyName: 'Test AB',
orgNumber: '556677-8899',
addressLine1: 'Testgatan 1',
postalCode: '11122',
city: 'Stockholm',
email: 'test@example.com',
},
warnings: [],
...overrides,
@@ -37,138 +73,250 @@ function makeDeclaration(overrides?: Partial<INK2Declaration>): INK2Declaration
}
describe('INK2 SRU Generator', () => {
describe('generateSRUFile', () => {
it('produces valid SRU file structure', () => {
describe('generateSRUSubmission', () => {
it('produces valid INFO.SRU', () => {
const declaration = makeDeclaration()
const sruFile = generateSRUFile(declaration)
const validation = validateSRUFile(sruFile)
const submission = generateSRUSubmission(declaration)
expect(submission.infoSru).toContain('#DATABESKRIVNING_START')
expect(submission.infoSru).toContain('#PRODUKT SRU')
expect(submission.infoSru).toContain('#FILNAMN BLANKETTER.SRU')
expect(submission.infoSru).toContain('#DATABESKRIVNING_SLUT')
expect(submission.infoSru).toContain('#MEDIELEV_START')
expect(submission.infoSru).toContain('#ORGNR 165566778899')
expect(submission.infoSru).toContain('#NAMN Test AB')
expect(submission.infoSru).toContain('#POSTNR 11122')
expect(submission.infoSru).toContain('#POSTORT Stockholm')
expect(submission.infoSru).toContain('#MEDIELEV_SLUT')
})
it('formats org number as 12-digit with century prefix 16', () => {
const declaration = makeDeclaration()
const submission = generateSRUSubmission(declaration)
// In INFO.SRU
expect(submission.infoSru).toContain('#ORGNR 165566778899')
// In BLANKETTER.SRU
expect(submission.blanketterSru).toContain('#IDENTITET 165566778899')
})
it('handles org number already in 12-digit format', () => {
const declaration = makeDeclaration({
companyInfo: {
companyName: 'Test AB',
orgNumber: '165566778899',
addressLine1: null,
postalCode: '11122',
city: 'Stockholm',
email: null,
},
})
const submission = generateSRUSubmission(declaration)
expect(submission.infoSru).toContain('#ORGNR 165566778899')
})
it('produces three blankett blocks in BLANKETTER.SRU', () => {
const declaration = makeDeclaration()
const submission = generateSRUSubmission(declaration)
expect(submission.blanketterSru).toContain('#BLANKETT INK2-2025P4')
expect(submission.blanketterSru).toContain('#BLANKETT INK2R-2025P4')
expect(submission.blanketterSru).toContain('#BLANKETT INK2S-2025P4')
expect(submission.blanketterSru).toContain('#FIL_SLUT')
})
it('validates the generated BLANKETTER.SRU', () => {
const declaration = makeDeclaration()
const submission = generateSRUSubmission(declaration)
const validation = validateBlanketterSru(submission.blanketterSru)
expect(validation.isValid).toBe(true)
expect(validation.errors).toEqual([])
})
it('uses #BLANKETT INK2', () => {
it('uses correct period suffix for calendar year', () => {
const declaration = makeDeclaration()
const sruFile = generateSRUFile(declaration)
const blankettRecord = sruFile.records.find(r => r.fieldCode === 'BLANKETT')
expect(blankettRecord?.value).toBe('INK2')
const submission = generateSRUSubmission(declaration)
// Dec = P4
expect(submission.blanketterSru).toContain('INK2-2025P4')
})
it('includes only non-zero field values', () => {
const declaration = makeDeclaration()
const sruFile = generateSRUFile(declaration)
const uppgiftRecords = sruFile.records.filter(r => r.fieldCode === 'UPPGIFT')
// 7000 (fiscal year) + non-zero rutor
// Zero rutor: 7201, 7203, 7380 = 3 zero fields
// Non-zero: 16 fields
// Total UPPGIFT records: 1 (fiscal year) + 16 (non-zero values)
expect(uppgiftRecords).toHaveLength(17)
// Verify zero fields are excluded
const fieldCodes = uppgiftRecords.map(r => String(r.value).split(' ')[0])
expect(fieldCodes).not.toContain('7201')
expect(fieldCodes).not.toContain('7203')
expect(fieldCodes).not.toContain('7380')
it('uses P1 suffix for fiscal year ending in Jan-Apr', () => {
const declaration = makeDeclaration({
fiscalYear: {
id: 'p1',
name: 'FY',
start: '2024-05-01',
end: '2025-04-30',
isClosed: true,
},
})
const submission = generateSRUSubmission(declaration)
expect(submission.blanketterSru).toContain('INK2-2025P1')
})
it('includes fiscal year as field 7000', () => {
const declaration = makeDeclaration()
const sruFile = generateSRUFile(declaration)
const fiscalYearRecord = sruFile.records.find(
r => r.fieldCode === 'UPPGIFT' && String(r.value).startsWith('7000')
)
expect(fiscalYearRecord).toBeDefined()
expect(fiscalYearRecord?.value).toBe('7000 20250101-20251231')
it('uses P2 suffix for fiscal year ending in May-Aug', () => {
const declaration = makeDeclaration({
fiscalYear: {
id: 'p2',
name: 'FY',
start: '2024-09-01',
end: '2025-06-30',
isClosed: true,
},
})
const submission = generateSRUSubmission(declaration)
expect(submission.blanketterSru).toContain('INK2-2025P2')
})
it('handles negative values (financial items)', () => {
it('excludes zero-value #UPPGIFT lines', () => {
const declaration = makeDeclaration()
const sruFile = generateSRUFile(declaration)
const financialRecord = sruFile.records.find(
r => r.fieldCode === 'UPPGIFT' && String(r.value).startsWith('7370')
)
expect(financialRecord?.value).toBe('7370 -3000')
const submission = generateSRUSubmission(declaration)
// 7201 is 0, should not appear
const ink2rBlock = extractBlock(submission.blanketterSru, 'INK2R')
expect(ink2rBlock).not.toContain('#UPPGIFT 7201')
// 7215 is 50000, should appear
expect(ink2rBlock).toContain('#UPPGIFT 7215 50000')
})
it('strips dashes from org number', () => {
it('includes fiscal year fields 7011 and 7012 in each block', () => {
const declaration = makeDeclaration()
const sruFile = generateSRUFile(declaration)
const identityRecord = sruFile.records.find(r => r.fieldCode === 'IDENTITET')
expect(identityRecord?.value).toBe('5566778899')
const submission = generateSRUSubmission(declaration)
// Each block should have 7011 and 7012
const blocks = [
extractBlock(submission.blanketterSru, 'INK2-'),
extractBlock(submission.blanketterSru, 'INK2R'),
extractBlock(submission.blanketterSru, 'INK2S'),
]
for (const block of blocks) {
expect(block).toContain('#UPPGIFT 7011 20250101')
expect(block).toContain('#UPPGIFT 7012 20251231')
}
})
})
describe('sruFileToString', () => {
it('formats records as #FIELD value lines', () => {
it('each blankett block has #IDENTITET and #NAMN', () => {
const declaration = makeDeclaration()
const sruFile = generateSRUFile(declaration)
const content = sruFileToString(sruFile)
const submission = generateSRUSubmission(declaration)
expect(content).toContain('#BLANKETT INK2')
expect(content).toContain('#IDENTITET 5566778899')
expect(content).toContain('#BLANKETTSLUT')
const blocks = submission.blanketterSru.split('#BLANKETT ').slice(1)
expect(blocks).toHaveLength(3)
for (const block of blocks) {
expect(block).toContain('#IDENTITET 165566778899')
expect(block).toContain('#NAMN Test AB')
expect(block).toContain('#BLANKETTSLUT')
}
})
it('handles negative values correctly', () => {
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')
})
it('includes INK2S with överskott/underskott', () => {
const declaration = makeDeclaration()
const submission = generateSRUSubmission(declaration)
const ink2sBlock = extractBlock(submission.blanketterSru, 'INK2S')
expect(ink2sBlock).toContain('#UPPGIFT 7650 302000')
expect(ink2sBlock).toContain('#UPPGIFT 8020 302000')
// 7750 and 8021 are 0, should not appear
expect(ink2sBlock).not.toContain('#UPPGIFT 7750')
expect(ink2sBlock).not.toContain('#UPPGIFT 8021')
})
it('INK2 block includes överskott', () => {
const declaration = makeDeclaration()
const submission = generateSRUSubmission(declaration)
const ink2Block = extractBlock(submission.blanketterSru, 'INK2-')
expect(ink2Block).toContain('#UPPGIFT 7113 302000')
// 7114 (underskott) is 0, should not appear
expect(ink2Block).not.toContain('#UPPGIFT 7114')
})
it('uses CRLF line endings', () => {
const declaration = makeDeclaration()
const sruFile = generateSRUFile(declaration)
const content = sruFileToString(sruFile)
expect(content).toContain('\r\n')
const submission = generateSRUSubmission(declaration)
expect(submission.infoSru).toContain('\r\n')
expect(submission.blanketterSru).toContain('\r\n')
})
it('ends with newline', () => {
const declaration = makeDeclaration()
const sruFile = generateSRUFile(declaration)
const content = sruFileToString(sruFile)
expect(content.endsWith('\r\n')).toBe(true)
it('sanitizes # from company name', () => {
const declaration = makeDeclaration({
companyInfo: {
companyName: 'Test #1 AB',
orgNumber: '556677-8899',
addressLine1: null,
postalCode: '11122',
city: 'Stockholm',
email: null,
},
})
const submission = generateSRUSubmission(declaration)
expect(submission.infoSru).toContain('#NAMN Test 1 AB')
})
})
describe('getSRUFilename', () => {
describe('validateBlanketterSru', () => {
it('validates a correct BLANKETTER.SRU', () => {
const declaration = makeDeclaration()
const submission = generateSRUSubmission(declaration)
const result = validateBlanketterSru(submission.blanketterSru)
expect(result.isValid).toBe(true)
})
it('detects missing INK2R block', () => {
const result = validateBlanketterSru(
'#BLANKETT INK2-2025P4\r\n#IDENTITET 165566778899 20250101 100000\r\n#NAMN Test\r\n#BLANKETTSLUT\r\n' +
'#BLANKETT INK2S-2025P4\r\n#IDENTITET 165566778899 20250101 100002\r\n#NAMN Test\r\n#BLANKETTSLUT\r\n' +
'#FIL_SLUT\r\n'
)
expect(result.isValid).toBe(false)
expect(result.errors).toContain('Missing INK2R blankett block')
})
it('detects missing #FIL_SLUT', () => {
const result = validateBlanketterSru(
'#BLANKETT INK2-2025P4\r\n#IDENTITET x\r\n#NAMN T\r\n#BLANKETTSLUT\r\n' +
'#BLANKETT INK2R-2025P4\r\n#IDENTITET x\r\n#NAMN T\r\n#BLANKETTSLUT\r\n' +
'#BLANKETT INK2S-2025P4\r\n#IDENTITET x\r\n#NAMN T\r\n#BLANKETTSLUT\r\n'
)
expect(result.isValid).toBe(false)
expect(result.errors).toContain('Missing #FIL_SLUT terminator')
})
})
describe('getZipFilename', () => {
it('returns correct filename format', () => {
const declaration = makeDeclaration()
expect(getSRUFilename(declaration)).toBe('INK2_5566778899_2025.sru')
expect(getZipFilename(declaration)).toBe('INK2_SRU_5566778899_2025.zip')
})
it('handles missing org number', () => {
const declaration = makeDeclaration({
companyInfo: { companyName: 'Test AB', orgNumber: null },
companyInfo: {
companyName: 'Test AB',
orgNumber: null,
addressLine1: null,
postalCode: null,
city: null,
email: null,
},
})
expect(getSRUFilename(declaration)).toBe('INK2_unknown_2025.sru')
})
})
describe('validateSRUFile', () => {
it('validates a correct SRU file', () => {
const declaration = makeDeclaration()
const sruFile = generateSRUFile(declaration)
const result = validateSRUFile(sruFile)
expect(result.isValid).toBe(true)
})
it('detects missing PRODUKT header', () => {
const result = validateSRUFile({
records: [
{ fieldCode: 'BLANKETT', value: 'INK2' },
{ fieldCode: 'BLANKETTSLUT', value: '' },
],
generatedAt: new Date().toISOString(),
})
expect(result.isValid).toBe(false)
expect(result.errors).toContain('Missing PRODUKT header')
})
it('detects wrong blankett type', () => {
const result = validateSRUFile({
records: [
{ fieldCode: 'PRODUKT', value: 'KONTROLLUPPGIFTER' },
{ fieldCode: 'BLANKETT', value: 'NE' },
{ fieldCode: 'BLANKETTSLUT', value: '' },
],
generatedAt: new Date().toISOString(),
})
expect(result.isValid).toBe(false)
expect(result.errors).toContain('Expected BLANKETT INK2, got NE')
expect(getZipFilename(declaration)).toBe('INK2_SRU_unknown_2025.zip')
})
})
})
/** Extract a specific blankett block from BLANKETTER.SRU content */
function extractBlock(content: string, blockPrefix: string): string {
const regex = new RegExp(`#BLANKETT ${blockPrefix}[^\\r\\n]*[\\s\\S]*?#BLANKETTSLUT`)
const match = content.match(regex)
return match ? match[0] : ''
}
File diff suppressed because it is too large Load Diff
+225 -101
View File
@@ -1,97 +1,67 @@
import type { INK2Declaration, INK2SRUCode, SRUFile, SRURecord } from './types'
import type {
INK2Declaration,
INK2RSRUCode,
INK2SRutor,
SRUSubmission,
} from './types'
/**
* SRU File Generator for INK2
* SRU File Generator for INK2 (Aktiebolag)
*
* Generates SRU (Standardiserat Räkenskapsutdrag) files for electronic
* submission to Skatteverket. The SRU format is used for tax declarations.
* Generates a Skatteverket-compliant SRU submission consisting of:
* - INFO.SRU: submitter metadata
* - BLANKETTER.SRU: three blankett blocks (INK2, INK2R, INK2S)
*
* INK2 field codes are the SRU codes directly (7201-7380).
* Encoding: ISO 8859-1 (handled by the API route when writing the response)
* Line endings: CRLF
* Amounts: integers in hela kronor, no decimals, no thousands separators
* Org number: 12 digits with century prefix 16 for juridisk person
*/
/** All INK2 SRU field codes in order */
const INK2_FIELD_CODES: INK2SRUCode[] = [
'7201', '7202', '7203', '7210', '7211', '7212',
'7220', '7221', '7222', '7230', '7231',
'7310', '7320', '7330', '7340', '7350', '7360', '7370', '7380',
]
const CRLF = '\r\n'
const PROGRAM_NAME = 'gnubok'
const PROGRAM_VERSION = '1.0'
/**
* Generate SRU file content from INK2 declaration
* Compute the period suffix for blankett type strings.
* Based on which month the fiscal year ENDS in:
* P1 = Jan-Apr, P2 = May-Aug, P3 = special, P4 = Sep-Dec
*/
export function generateSRUFile(declaration: INK2Declaration): SRUFile {
const records: SRURecord[] = []
const now = new Date()
// File header
records.push({ fieldCode: 'PRODUKT', value: 'KONTROLLUPPGIFTER' })
records.push({ fieldCode: 'SESSION', value: '1' })
records.push({ fieldCode: 'PROGRAMNAMN', value: 'ERPBase' })
records.push({ fieldCode: 'PROGRAMVERSION', value: '1.0' })
records.push({
fieldCode: 'SKAPAT',
value: formatSRUDate(now),
})
// Form declaration
records.push({ fieldCode: 'BLANKETT', value: 'INK2' })
// Company identification
if (declaration.companyInfo.orgNumber) {
const cleanOrgNumber = declaration.companyInfo.orgNumber.replace(/-/g, '')
records.push({
fieldCode: 'IDENTITET',
value: cleanOrgNumber,
})
}
// Fiscal year
records.push({
fieldCode: 'UPPGIFT',
value: `7000 ${formatSRUDateRange(declaration.fiscalYear.start, declaration.fiscalYear.end)}`,
})
// INK2 field values
for (const code of INK2_FIELD_CODES) {
const value = declaration.rutor[code]
if (value !== 0) {
records.push({
fieldCode: 'UPPGIFT',
value: `${code} ${formatSRUAmount(value)}`,
})
}
}
// End of form
records.push({ fieldCode: 'BLANKETTSLUT', value: '' })
return {
records,
generatedAt: now.toISOString(),
}
function computePeriodSuffix(fiscalYearEnd: string): string {
const endMonth = parseInt(fiscalYearEnd.substring(5, 7), 10)
if (endMonth >= 1 && endMonth <= 4) return 'P1'
if (endMonth >= 5 && endMonth <= 8) return 'P2'
// P4 covers Sep-Dec (most common: calendar year companies)
// NOTE: P3 (first/short fiscal year) cannot be derived from end month alone.
// Callers must handle P3 manually for brutet räkenskapsår.
return 'P4'
}
/**
* Convert SRU file to string content
* Get the income year from the fiscal year end date.
* The year in the blankett type string is the income year.
*/
export function sruFileToString(sruFile: SRUFile): string {
const lines: string[] = []
for (const record of sruFile.records) {
if (record.value === '') {
lines.push(`#${record.fieldCode}`)
} else {
lines.push(`#${record.fieldCode} ${record.value}`)
}
}
return lines.join('\r\n') + '\r\n'
function getIncomeYear(fiscalYearEnd: string): string {
return fiscalYearEnd.substring(0, 4)
}
/**
* Format date for SRU: YYYYMMDD
* Format org number as 12-digit with century prefix.
* Swedish juridiska personer use century prefix "16".
* Input: "556677-8899" or "5566778899"
* Output: "165566778899"
*/
function formatSRUDate(date: Date): string {
function formatOrgNumber12(orgNumber: string): string {
const clean = orgNumber.replace(/-/g, '')
if (clean.length === 12) return clean
if (clean.length === 10) return `16${clean}`
return `16${clean}`
}
/**
* Format a Date as YYYYMMDD
*/
function formatDate(date: Date): string {
const y = date.getFullYear()
const m = String(date.getMonth() + 1).padStart(2, '0')
const d = String(date.getDate()).padStart(2, '0')
@@ -99,47 +69,201 @@ function formatSRUDate(date: Date): string {
}
/**
* Format date string (YYYY-MM-DD) to SRU format (YYYYMMDD)
* Format a Date as HHMMSS
*/
function dateStringToSRU(dateStr: string): string {
return dateStr.replace(/-/g, '')
function formatTime(date: Date): string {
const h = String(date.getHours()).padStart(2, '0')
const m = String(date.getMinutes()).padStart(2, '0')
const s = String(date.getSeconds()).padStart(2, '0')
return `${h}${m}${s}`
}
/**
* Format fiscal year date range for SRU
* Format integer amount for SRU. No decimals, no thousands separator.
* Truncated to hela kronor by the engine.
*/
function formatSRUDateRange(startDate: string, endDate: string): string {
return `${dateStringToSRU(startDate)}-${dateStringToSRU(endDate)}`
function formatAmount(amount: number): string {
return Math.trunc(amount).toString()
}
/**
* Format amount for SRU: whole numbers, no thousands separator, negative with minus
* Generate the INFO.SRU file content
*/
function formatSRUAmount(amount: number): string {
return Math.round(amount).toString()
function generateInfoSru(declaration: INK2Declaration, now: Date): string {
const lines: string[] = []
const orgNumber12 = declaration.companyInfo.orgNumber
? formatOrgNumber12(declaration.companyInfo.orgNumber)
: '000000000000'
// DATABESKRIVNING block (required order)
lines.push('#DATABESKRIVNING_START')
lines.push('#PRODUKT SRU')
lines.push(`#SKAPAD ${formatDate(now)} ${formatTime(now)}`)
lines.push(`#PROGRAM ${PROGRAM_NAME} ${PROGRAM_VERSION}`)
lines.push('#FILNAMN BLANKETTER.SRU')
lines.push('#DATABESKRIVNING_SLUT')
// MEDIELEV block
lines.push('#MEDIELEV_START')
lines.push(`#ORGNR ${orgNumber12}`)
lines.push(`#NAMN ${sanitizeString(declaration.companyInfo.companyName)}`)
if (declaration.companyInfo.addressLine1) {
lines.push(`#ADRESS ${sanitizeString(declaration.companyInfo.addressLine1)}`)
}
lines.push(`#POSTNR ${declaration.companyInfo.postalCode || '00000'}`)
lines.push(`#POSTORT ${sanitizeString(declaration.companyInfo.city || 'Okänd')}`)
if (declaration.companyInfo.email) {
lines.push(`#EMAIL ${declaration.companyInfo.email}`)
}
lines.push('#MEDIELEV_SLUT')
return lines.join(CRLF) + CRLF
}
/**
* Validate SRU file content
* Generate the BLANKETTER.SRU file content with three blankett blocks
*/
export function validateSRUFile(sruFile: SRUFile): {
function generateBlanketterSru(declaration: INK2Declaration, now: Date): string {
const lines: string[] = []
const orgNumber12 = declaration.companyInfo.orgNumber
? formatOrgNumber12(declaration.companyInfo.orgNumber)
: '000000000000'
const incomeYear = getIncomeYear(declaration.fiscalYear.end)
const periodSuffix = computePeriodSuffix(declaration.fiscalYear.end)
const companyName = sanitizeString(declaration.companyInfo.companyName)
const dateStr = formatDate(now)
// Each blankett gets a unique timestamp (increment seconds)
const time0 = formatTime(now)
const time1 = formatTime(new Date(now.getTime() + 1000))
const time2 = formatTime(new Date(now.getTime() + 2000))
// ---- Block 1: INK2 (huvudblankett) ----
lines.push(`#BLANKETT INK2-${incomeYear}${periodSuffix}`)
lines.push(`#IDENTITET ${orgNumber12} ${dateStr} ${time0}`)
lines.push(`#NAMN ${companyName}`)
// Fiscal year dates
lines.push(`#UPPGIFT 7011 ${declaration.ink2['7011']}`)
lines.push(`#UPPGIFT 7012 ${declaration.ink2['7012']}`)
// Överskott/underskott
if (declaration.ink2['7113'] > 0) {
lines.push(`#UPPGIFT 7113 ${formatAmount(declaration.ink2['7113'])}`)
}
if (declaration.ink2['7114'] > 0) {
lines.push(`#UPPGIFT 7114 ${formatAmount(declaration.ink2['7114'])}`)
}
lines.push('#BLANKETTSLUT')
// ---- Block 2: INK2R (räkenskapsschema) ----
lines.push(`#BLANKETT INK2R-${incomeYear}${periodSuffix}`)
lines.push(`#IDENTITET ${orgNumber12} ${dateStr} ${time1}`)
lines.push(`#NAMN ${companyName}`)
// Fiscal year dates
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[]
for (const code of ink2rCodes) {
const value = declaration.ink2r[code]
if (value !== 0) {
lines.push(`#UPPGIFT ${code} ${formatAmount(value)}`)
}
}
lines.push('#BLANKETTSLUT')
// ---- Block 3: INK2S (skattemässiga justeringar) ----
lines.push(`#BLANKETT INK2S-${incomeYear}${periodSuffix}`)
lines.push(`#IDENTITET ${orgNumber12} ${dateStr} ${time2}`)
lines.push(`#NAMN ${companyName}`)
// Fiscal year dates
lines.push(`#UPPGIFT 7011 ${declaration.ink2s['7011']}`)
lines.push(`#UPPGIFT 7012 ${declaration.ink2s['7012']}`)
// INK2S numeric fields — emit non-zero values only
const ink2sNumericFields: (keyof INK2SRutor)[] = ['7650', '7750', '7651', '8020', '8021']
for (const code of ink2sNumericFields) {
const value = declaration.ink2s[code]
if (typeof value === 'number' && value !== 0) {
lines.push(`#UPPGIFT ${code} ${formatAmount(value)}`)
}
}
lines.push('#BLANKETTSLUT')
// Required terminator
lines.push('#FIL_SLUT')
return lines.join(CRLF) + CRLF
}
/**
* Sanitize string for SRU: remove # characters (reserved), limit to 250 chars
*/
function sanitizeString(str: string): string {
return str.replace(/#/g, '').replace(/[\r\n]/g, ' ').substring(0, 250)
}
/**
* Generate complete SRU submission (INFO.SRU + BLANKETTER.SRU)
*/
export function generateSRUSubmission(declaration: INK2Declaration): SRUSubmission {
const now = new Date()
return {
infoSru: generateInfoSru(declaration, now),
blanketterSru: generateBlanketterSru(declaration, now),
generatedAt: now.toISOString(),
}
}
/**
* Validate the generated BLANKETTER.SRU content
*/
export function validateBlanketterSru(content: string): {
isValid: boolean
errors: string[]
} {
const errors: string[] = []
const hasHeader = sruFile.records.some(r => r.fieldCode === 'PRODUKT')
const hasBlankett = sruFile.records.some(r => r.fieldCode === 'BLANKETT')
const hasBlankettslut = sruFile.records.some(r => r.fieldCode === 'BLANKETTSLUT')
// Check for required blankett blocks
const hasINK2 = /^#BLANKETT INK2-/m.test(content)
const hasINK2R = /^#BLANKETT INK2R-/m.test(content)
const hasINK2S = /^#BLANKETT INK2S-/m.test(content)
const hasFilSlut = /^#FIL_SLUT/m.test(content)
if (!hasHeader) errors.push('Missing PRODUKT header')
if (!hasBlankett) errors.push('Missing BLANKETT declaration')
if (!hasBlankettslut) errors.push('Missing BLANKETTSLUT')
if (!hasINK2) errors.push('Missing INK2 blankett block')
if (!hasINK2R) errors.push('Missing INK2R blankett block')
if (!hasINK2S) errors.push('Missing INK2S blankett block')
if (!hasFilSlut) errors.push('Missing #FIL_SLUT terminator')
// Verify it's INK2
const blankettRecord = sruFile.records.find(r => r.fieldCode === 'BLANKETT')
if (blankettRecord && blankettRecord.value !== 'INK2') {
errors.push(`Expected BLANKETT INK2, got ${blankettRecord.value}`)
// Count BLANKETTSLUT — should be exactly 3
const blankettslutCount = (content.match(/^#BLANKETTSLUT/gm) || []).length
if (blankettslutCount !== 3) {
errors.push(`Expected 3 BLANKETTSLUT, found ${blankettslutCount}`)
}
// Check that each blankett has #IDENTITET
const blankettBlocks = content.split(/^#BLANKETT /m).slice(1)
for (const block of blankettBlocks) {
if (!block.includes('#IDENTITET')) {
const type = block.split('\n')[0]?.split('\r')[0] || 'unknown'
errors.push(`Blankett ${type} missing #IDENTITET`)
}
if (!block.includes('#NAMN')) {
const type = block.split('\n')[0]?.split('\r')[0] || 'unknown'
errors.push(`Blankett ${type} missing #NAMN`)
}
}
return {
@@ -149,10 +273,10 @@ export function validateSRUFile(sruFile: SRUFile): {
}
/**
* Get filename for SRU file download
* Get ZIP filename for download
*/
export function getSRUFilename(declaration: INK2Declaration): string {
export function getZipFilename(declaration: INK2Declaration): string {
const year = declaration.fiscalYear.start.substring(0, 4)
const orgNumber = declaration.companyInfo.orgNumber?.replace(/-/g, '') || 'unknown'
return `INK2_${orgNumber}_${year}.sru`
return `INK2_SRU_${orgNumber}_${year}.zip`
}
+245 -62
View File
@@ -1,36 +1,121 @@
// INK2 declaration rutor (fields) keyed by SRU code
export interface INK2DeclarationRutor {
// Balance sheet - Assets
'7201': number // Immateriella anläggningstillgångar
'7202': number // Materiella anläggningstillgångar
'7203': number // Finansiella anläggningstillgångar
'7210': number // Varulager m.m.
'7211': number // Kundfordringar
'7212': number // Övriga omsättningstillgångar
/**
* INK2R — Räkenskapsschema (balance sheet + income statement)
* Field codes per Skatteverket spec and bas.se/kontoplaner/sru/
*/
export interface INK2RRutor {
// Balance sheet - Assets (Tillgångar)
'7201': number // 2.1 Koncessioner, patent, licenser, varumärken, hyresrätter, goodwill
'7202': number // 2.2 Förskott avs. immateriella anläggningstillgångar
'7214': number // 2.3 Byggnader och mark
'7215': number // 2.4 Maskiner, inventarier, övriga materiella anläggningstillgångar
'7216': number // 2.5 Förbättringsutgifter på annans fastighet
'7217': number // 2.6 Pågående nyanläggningar, förskott materiella anläggningstillgångar
'7230': number // 2.7 Andelar i koncernföretag
'7231': number // 2.8 Andelar i intresseföretag och gemensamt styrda företag
'7233': number // 2.9 Ägarintressen i övriga företag + andra långfristiga värdepapper
'7232': number // 2.10 Fordringar hos koncern-/intresse-/gemensamt styrda företag
'7234': number // 2.11 Lån till delägare eller närstående
'7235': number // 2.12 Fordringar hos övriga + andra långfristiga fordringar
'7241': number // 2.13 Råvaror och förnödenheter
'7242': number // 2.14 Varor under tillverkning
'7243': number // 2.15 Färdiga varor och handelsvaror
'7244': number // 2.16 Övriga lagertillgångar
'7245': number // 2.17 Pågående arbeten för annans räkning
'7246': number // 2.18 Förskott till leverantörer
'7251': number // 2.19 Kundfordringar
'7252': number // 2.20 Fordringar koncern/intresse (kortfristiga)
'7261': number // 2.21 Fordringar övriga + övriga fordringar
'7262': number // 2.22 Upparbetad men ej fakturerad intäkt
'7263': number // 2.23 Förutbetalda kostnader och upplupna intäkter
'7270': number // 2.24 Andelar i koncernföretag (kortfristiga)
'7271': number // 2.25 Övriga kortfristiga placeringar
'7281': number // 2.26 Kassa, bank och redovisningsmedel
// Balance sheet - Equity & Liabilities
'7220': number // Aktiekapital
'7221': number // Övrigt eget kapital
'7222': number // Årets resultat
'7230': number // Obeskattade reserver, avsättningar och skulder
'7231': number // Övriga skulder
// Balance sheet - Equity & Liabilities (Eget kapital och skulder)
'7301': number // 2.27 Bundet eget kapital
'7302': number // 2.28 Fritt eget kapital
'7321': number // 2.29 Periodiseringsfonder
'7322': number // 2.30 Ackumulerade överavskrivningar
'7323': number // 2.31 Övriga obeskattade reserver
'7331': number // 2.32 Avsättningar för pensioner enl. tryggandelagen
'7332': number // 2.33 Övriga avsättningar för pensioner
'7333': number // 2.34 Övriga avsättningar
'7350': number // 2.35 Obligationslån
'7351': number // 2.36 Checkräkningskredit (långfristig)
'7352': number // 2.37 Övriga skulder till kreditinstitut (långfristiga)
'7353': number // 2.38 Skulder koncern/intresse (långfristiga)
'7354': number // 2.39 Övriga skulder (långfristiga)
'7360': number // 2.40 Checkräkningskredit (kortfristig)
'7361': number // 2.41 Övriga skulder till kreditinstitut (kortfristiga)
'7362': number // 2.42 Förskott från kunder
'7363': number // 2.43 Pågående arbeten (skuldsida)
'7364': number // 2.44 Fakturerad men ej upparbetad intäkt
'7365': number // 2.45 Leverantörsskulder
'7366': number // 2.46 Växelskulder
'7367': number // 2.47 Skulder koncern/intresse (kortfristiga)
'7369': number // 2.48 Övriga skulder (kortfristiga)
'7368': number // 2.49 Skatteskulder
'7370': number // 2.50 Upplupna kostnader och förutbetalda intäkter
// Income statement
'7310': number // Nettoomsättning
'7320': number // Varuinköp/direkta kostnader
'7330': number // Övriga externa kostnader
'7340': number // Personalkostnader
'7350': number // Avskrivningar
'7360': number // Övriga rörelsekostnader
'7370': number // Finansiella poster (netto)
'7380': number // Extraordinära poster (netto)
// Income statement (Resultaträkning)
'7410': number // 3.1 Nettoomsättning
'7411': number // 3.2 Förändring av lager
'7412': number // 3.3 Aktiverat arbete för egen räkning
'7413': number // 3.4 Övriga rörelseintäkter
'7511': number // 3.5 Råvaror och förnödenheter
'7512': number // 3.6 Handelsvaror
'7513': number // 3.7 Övriga externa kostnader
'7514': number // 3.8 Personalkostnader
'7515': number // 3.9 Av- och nedskrivningar materiella/immateriella
'7516': number // 3.10 Nedskrivningar omsättningstillgångar
'7517': number // 3.11 Övriga rörelsekostnader
'7414': number // 3.12 Resultat från andelar i koncernföretag
'7415': number // 3.13 Resultat från andelar i intresseföretag
'7423': number // 3.14 Resultat från övriga företag med ägarintresse
'7416': number // 3.15 Resultat från övriga finansiella anläggningstillgångar
'7417': number // 3.16 Övriga ränteintäkter och liknande
'7521': number // 3.17 Nedskrivningar finansiella anläggningstillgångar
'7522': number // 3.18 Räntekostnader och liknande
'7524': number // 3.19 Lämnade koncernbidrag
'7419': number // 3.20 Mottagna koncernbidrag
'7420': number // 3.21 Återföring av periodiseringsfond
'7525': number // 3.22 Avsättning till periodiseringsfond
'7421': number // 3.23 Förändring av överavskrivningar
'7422': number // 3.24 Övriga bokslutsdispositioner
'7528': number // 3.25 Skatt på årets resultat
'7450': number // 3.26 Årets resultat, vinst (positive)
'7550': number // 3.27 Årets resultat, förlust (positive = loss)
}
export type INK2SRUCode = keyof INK2DeclarationRutor
export type INK2RSRUCode = keyof INK2RRutor
// Account mapping configuration for INK2 declaration
/**
* INK2 — Huvudblankett (main declaration, page 1)
*/
export interface INK2Rutor {
'7011': string // Räkenskapsår fr.o.m. (YYYYMMDD)
'7012': string // Räkenskapsår t.o.m. (YYYYMMDD)
'7113': number // 1.1 Överskott av näringsverksamhet
'7114': number // 1.2 Underskott av näringsverksamhet
}
/**
* INK2S — Skattemässiga justeringar (page 4)
* Auto-derived fields only. Manual tax adjustments are handled by the accountant.
*/
export interface INK2SRutor {
'7011': string // Räkenskapsår fr.o.m. (YYYYMMDD)
'7012': string // Räkenskapsår t.o.m. (YYYYMMDD)
'7650': number // 4.1 Årets resultat, vinst
'7750': number // 4.2 Årets resultat, förlust
'7651': number // 4.3a Skatt på årets resultat (ej avdragsgill)
'8020': number // 4.15 Överskott → punkt 1.1
'8021': number // 4.16 Underskott → punkt 1.2
}
// Account mapping configuration for INK2R
export interface INK2AccountMapping {
sruCode: INK2SRUCode
sruCode: INK2RSRUCode
description: string
section: 'assets' | 'equity_liabilities' | 'income_statement'
normalBalance: 'debit' | 'credit' | 'net'
@@ -41,7 +126,17 @@ export interface INK2AccountMapping {
}>
}
// INK2 declaration response
// Company info for SRU file generation
export interface INK2CompanyInfo {
companyName: string
orgNumber: string | null
addressLine1: string | null
postalCode: string | null
city: string | null
email: string | null
}
// INK2 declaration response (includes all three blankett sections)
export interface INK2Declaration {
fiscalYear: {
id: string
@@ -50,8 +145,10 @@ export interface INK2Declaration {
end: string
isClosed: boolean
}
rutor: INK2DeclarationRutor
breakdown: Record<INK2SRUCode, {
ink2: INK2Rutor
ink2r: INK2RRutor
ink2s: INK2SRutor
breakdown: Record<INK2RSRUCode, {
accounts: Array<{
accountNumber: string
accountName: string
@@ -65,40 +162,126 @@ export interface INK2Declaration {
operatingResult: number
resultAfterFinancial: number
}
companyInfo: {
companyName: string
orgNumber: string | null
}
companyInfo: INK2CompanyInfo
warnings: string[]
}
// Reuse SRU file types from NE-bilaga
export type { SRURecord, SRUFile } from '@/lib/reports/ne-bilaga/types'
// Labels for INK2 rutor
export const INK2_RUTA_LABELS: Record<INK2SRUCode, string> = {
'7201': 'Immateriella anläggningstillgångar',
'7202': 'Materiella anläggningstillgångar',
'7203': 'Finansiella anläggningstillgångar',
'7210': 'Varulager m.m.',
'7211': 'Kundfordringar',
'7212': 'Övriga omsättningstillgångar',
'7220': 'Aktiekapital',
'7221': 'Övrigt eget kapital',
'7222': 'Årets resultat',
'7230': 'Obeskattade reserver, avsättningar och skulder',
'7231': 'Övriga skulder',
'7310': 'Nettoomsättning',
'7320': 'Varuinköp/direkta kostnader',
'7330': 'Övriga externa kostnader',
'7340': 'Personalkostnader',
'7350': 'Avskrivningar',
'7360': 'Övriga rörelsekostnader',
'7370': 'Finansiella poster (netto)',
'7380': 'Extraordinära poster (netto)',
// SRU file types — no longer shared with NE-bilaga since the structure
// is fundamentally different (INFO.SRU + BLANKETTER.SRU two-file format)
export interface SRUSubmission {
infoSru: string
blanketterSru: string
generatedAt: string
}
// Section groupings for UI display
export const INK2_ASSET_CODES: INK2SRUCode[] = ['7201', '7202', '7203', '7210', '7211', '7212']
export const INK2_EQUITY_LIABILITY_CODES: INK2SRUCode[] = ['7220', '7221', '7222', '7230', '7231']
export const INK2_INCOME_STATEMENT_CODES: INK2SRUCode[] = ['7310', '7320', '7330', '7340', '7350', '7360', '7370', '7380']
// ---- UI display helpers ----
export const INK2R_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',
]
export const INK2R_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',
]
export const INK2R_INCOME_CODES: INK2RSRUCode[] = [
'7410', '7411', '7412', '7413',
'7511', '7512', '7513', '7514', '7515', '7516', '7517',
'7414', '7415', '7423', '7416', '7417',
'7521', '7522',
'7524', '7419', '7420', '7525', '7421', '7422',
'7528',
'7450', '7550',
]
export const INK2R_RUTA_LABELS: Record<INK2RSRUCode, string> = {
// Assets
'7201': 'Koncessioner, patent, licenser, varumärken, goodwill',
'7202': 'Förskott immateriella anläggningstillgångar',
'7214': 'Byggnader och mark',
'7215': 'Maskiner och inventarier',
'7216': 'Förbättringsutgifter på annans fastighet',
'7217': 'Pågående nyanläggningar och förskott',
'7230': 'Andelar i koncernföretag',
'7231': 'Andelar i intresseföretag',
'7233': 'Ägarintressen i övriga företag',
'7232': 'Fordringar koncern-/intresseföretag',
'7234': 'Lån till delägare eller närstående',
'7235': 'Övriga långfristiga fordringar',
'7241': 'Råvaror och förnödenheter',
'7242': 'Varor under tillverkning',
'7243': 'Färdiga varor och handelsvaror',
'7244': 'Övriga lagertillgångar',
'7245': 'Pågående arbeten för annans räkning',
'7246': 'Förskott till leverantörer',
'7251': 'Kundfordringar',
'7252': 'Fordringar koncern/intresse (kortfristiga)',
'7261': 'Övriga fordringar',
'7262': 'Upparbetad men ej fakturerad intäkt',
'7263': 'Förutbetalda kostnader och upplupna intäkter',
'7270': 'Andelar i koncernföretag (kortfristiga)',
'7271': 'Övriga kortfristiga placeringar',
'7281': 'Kassa, bank och redovisningsmedel',
// Equity & Liabilities
'7301': 'Bundet eget kapital',
'7302': 'Fritt eget kapital',
'7321': 'Periodiseringsfonder',
'7322': 'Ackumulerade överavskrivningar',
'7323': 'Övriga obeskattade reserver',
'7331': 'Pensionsavsättningar (tryggandelagen)',
'7332': 'Övriga pensionsavsättningar',
'7333': 'Övriga avsättningar',
'7350': 'Obligationslån',
'7351': 'Checkräkningskredit (långfristig)',
'7352': 'Övriga skulder kreditinstitut (långfristiga)',
'7353': 'Skulder koncern/intresse (långfristiga)',
'7354': 'Övriga skulder (långfristiga)',
'7360': 'Checkräkningskredit (kortfristig)',
'7361': 'Övriga skulder kreditinstitut (kortfristiga)',
'7362': 'Förskott från kunder',
'7363': 'Pågående arbeten (skuldsida)',
'7364': 'Fakturerad men ej upparbetad intäkt',
'7365': 'Leverantörsskulder',
'7366': 'Växelskulder',
'7367': 'Skulder koncern/intresse (kortfristiga)',
'7369': 'Övriga skulder (kortfristiga)',
'7368': 'Skatteskulder',
'7370': 'Upplupna kostnader och förutbetalda intäkter',
// Income statement
'7410': 'Nettoomsättning',
'7411': 'Förändring av lager',
'7412': 'Aktiverat arbete för egen räkning',
'7413': 'Övriga rörelseintäkter',
'7511': 'Råvaror och förnödenheter',
'7512': 'Handelsvaror',
'7513': 'Övriga externa kostnader',
'7514': 'Personalkostnader',
'7515': 'Av- och nedskrivningar',
'7516': 'Nedskrivningar omsättningstillgångar',
'7517': 'Övriga rörelsekostnader',
'7414': 'Resultat andelar koncernföretag',
'7415': 'Resultat andelar intresseföretag',
'7423': 'Resultat övriga ägarintresse',
'7416': 'Övriga finansiella anläggningstillgångar',
'7417': 'Ränteintäkter',
'7521': 'Nedskrivningar finansiella anläggningstillgångar',
'7522': 'Räntekostnader',
'7524': 'Lämnade koncernbidrag',
'7419': 'Mottagna koncernbidrag',
'7420': 'Återföring av periodiseringsfond',
'7525': 'Avsättning till periodiseringsfond',
'7421': 'Förändring av överavskrivningar',
'7422': 'Övriga bokslutsdispositioner',
'7528': 'Skatt på årets resultat',
'7450': 'Årets resultat (vinst)',
'7550': 'Årets resultat (förlust)',
}