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:
co-authored by
Claude Opus 4.6
parent
bf5a8d9195
commit
7a18d89c70
@@ -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(),
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
@@ -103,6 +103,7 @@ export function getVatRules(
|
||||
treatment: 'export',
|
||||
rate: 0,
|
||||
momsRuta: '40',
|
||||
reverseChargeText: 'Omsättning utanför EU, ML 10 kap.',
|
||||
}
|
||||
|
||||
default:
|
||||
|
||||
@@ -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')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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] : ''
|
||||
}
|
||||
|
||||
+696
-165
File diff suppressed because it is too large
Load Diff
+225
-101
@@ -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
@@ -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)',
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user