@@ -723,12 +731,21 @@ export default function NewInvoicePage() {
{formatCurrency(subtotal, watchCurrency)}
{Array.from(vatByRate.entries())
- .filter(([, group]) => group.vat > 0)
.sort(([a], [b]) => b - a)
.map(([rate, group]) => (
-
-
Moms {rate}%
-
{formatCurrency(group.vat, watchCurrency)}
+
+ {vatByRate.size > 1 && (
+
+ Netto {rate}%
+ {formatCurrency(group.base, watchCurrency)}
+
+ )}
+ {group.vat > 0 && (
+
+ Moms {rate}%
+ {formatCurrency(group.vat, watchCurrency)}
+
+ )}
))}
{vatByRate.size === 0 && (
diff --git a/app/api/invoices/preview-pdf/route.ts b/app/api/invoices/preview-pdf/route.ts
index cb9f64d0..65785f23 100644
--- a/app/api/invoices/preview-pdf/route.ts
+++ b/app/api/invoices/preview-pdf/route.ts
@@ -24,7 +24,7 @@ export async function POST(request: Request) {
const companyId = await requireCompanyId(supabase, user.id)
const body = await request.json()
- const { customer_id, invoice_date, due_date, currency, items, your_reference, our_reference, notes, document_type } = body
+ const { customer_id, invoice_date, due_date, delivery_date, currency, items, your_reference, our_reference, notes, document_type } = body
if (!customer_id || !items || items.length === 0) {
return NextResponse.json({ error: 'Kunduppgifter och rader krävs' }, { status: 400 })
@@ -93,6 +93,7 @@ export async function POST(request: Request) {
invoice_number: 'FÖRHANDSGRANSKNING',
invoice_date: invoice_date || new Date().toISOString().split('T')[0],
due_date: due_date || new Date().toISOString().split('T')[0],
+ delivery_date: delivery_date || null,
status: 'draft',
currency: currency || 'SEK',
exchange_rate: null,
diff --git a/app/api/invoices/route.ts b/app/api/invoices/route.ts
index 8e2512e6..61a75939 100644
--- a/app/api/invoices/route.ts
+++ b/app/api/invoices/route.ts
@@ -188,6 +188,7 @@ export async function POST(request: Request) {
invoice_number: invoiceNumber,
invoice_date: invoiceInput.invoice_date,
due_date: invoiceInput.due_date,
+ delivery_date: invoiceInput.delivery_date ?? null,
currency: invoiceInput.currency,
exchange_rate: exchangeRate,
exchange_rate_date: exchangeRateDate,
@@ -312,6 +313,7 @@ async function createCreditNote(
invoice_number: creditNoteNumber,
invoice_date: new Date().toISOString().split('T')[0],
due_date: new Date().toISOString().split('T')[0],
+ delivery_date: originalInvoice.delivery_date ?? null,
currency: originalInvoice.currency,
exchange_rate: originalInvoice.exchange_rate,
exchange_rate_date: originalInvoice.exchange_rate_date,
diff --git a/app/api/reports/ink2/route.ts b/app/api/reports/ink2/route.ts
index 42af7e57..a9a76139 100644
--- a/app/api/reports/ink2/route.ts
+++ b/app/api/reports/ink2/route.ts
@@ -2,11 +2,11 @@ import { createClient } from '@/lib/supabase/server'
import { NextResponse } from 'next/server'
import { generateINK2Declaration } from '@/lib/reports/ink2/ink2-engine'
import {
- generateSRUFile,
- sruFileToString,
- getSRUFilename,
+ generateSRUSubmission,
+ getZipFilename,
} from '@/lib/reports/ink2/sru-generator'
import { requireCompanyId } from '@/lib/company/context'
+import JSZip from 'jszip'
/**
* GET /api/reports/ink2
@@ -15,7 +15,7 @@ import { requireCompanyId } from '@/lib/company/context'
*
* Query parameters:
* - period_id: Fiscal period ID (required)
- * - format: 'json' (default) or 'sru' for SRU file download
+ * - format: 'json' (default) or 'sru' for SRU file download (ZIP with INFO.SRU + BLANKETTER.SRU)
*/
export async function GET(request: Request) {
const supabase = await createClient()
@@ -42,14 +42,24 @@ export async function GET(request: Request) {
const declaration = await generateINK2Declaration(supabase, companyId, periodId)
if (format === 'sru') {
- const sruFile = generateSRUFile(declaration)
- const sruContent = sruFileToString(sruFile)
- const filename = getSRUFilename(declaration)
+ const submission = generateSRUSubmission(declaration)
- return new NextResponse(sruContent, {
+ // Encode both files as ISO 8859-1 (Latin-1) — required by Skatteverket
+ const infoBytes = encodeISO88591(submission.infoSru)
+ const blanketterBytes = encodeISO88591(submission.blanketterSru)
+
+ // Create ZIP with both files
+ const zip = new JSZip()
+ zip.file('INFO.SRU', infoBytes)
+ zip.file('BLANKETTER.SRU', blanketterBytes)
+
+ const zipArrayBuffer = await zip.generateAsync({ type: 'arraybuffer' })
+ const filename = getZipFilename(declaration)
+
+ return new NextResponse(zipArrayBuffer, {
status: 200,
headers: {
- 'Content-Type': 'text/plain; charset=utf-8',
+ 'Content-Type': 'application/zip',
'Content-Disposition': `attachment; filename="${filename}"`,
},
})
@@ -64,3 +74,16 @@ export async function GET(request: Request) {
)
}
}
+
+/**
+ * Encode a string as ISO 8859-1 (Latin-1) bytes.
+ * Characters outside the Latin-1 range are replaced with '?'.
+ */
+function encodeISO88591(str: string): Uint8Array {
+ const bytes = new Uint8Array(str.length)
+ for (let i = 0; i < str.length; i++) {
+ const code = str.charCodeAt(i)
+ bytes[i] = code <= 0xFF ? code : 0x3F // '?' for unmappable chars
+ }
+ return bytes
+}
diff --git a/components/reports/INK2DeclarationView.tsx b/components/reports/INK2DeclarationView.tsx
index 0142d53f..08571a9b 100644
--- a/components/reports/INK2DeclarationView.tsx
+++ b/components/reports/INK2DeclarationView.tsx
@@ -7,17 +7,18 @@ import { Badge } from '@/components/ui/badge'
import { Download, AlertCircle, Info } from 'lucide-react'
import { AccountNumber } from '@/components/ui/account-number'
import { formatCurrency } from '@/lib/utils'
-import type { INK2Declaration, INK2SRUCode } from '@/lib/reports/ink2/types'
+import type { INK2Declaration, INK2RSRUCode } from '@/lib/reports/ink2/types'
import {
- INK2_RUTA_LABELS,
- INK2_ASSET_CODES,
- INK2_EQUITY_LIABILITY_CODES,
- INK2_INCOME_STATEMENT_CODES,
+ INK2R_RUTA_LABELS,
+ INK2R_ASSET_CODES,
+ INK2R_EQUITY_LIABILITY_CODES,
+ INK2R_INCOME_CODES,
} from '@/lib/reports/ink2/types'
export function INK2DeclarationView({ periodId }: { periodId: string }) {
const [data, setData] = useState
(null)
const [loading, setLoading] = useState(false)
+ const [downloading, setDownloading] = useState(false)
const [error, setError] = useState(null)
const fetchDeclaration = async () => {
@@ -38,8 +39,24 @@ export function INK2DeclarationView({ periodId }: { periodId: string }) {
}
}
- const downloadSRU = () => {
- window.open(`/api/reports/ink2?period_id=${periodId}&format=sru`, '_blank')
+ const downloadSRU = async () => {
+ setDownloading(true)
+ try {
+ const res = await fetch(`/api/reports/ink2?period_id=${periodId}&format=sru`)
+ if (!res.ok) throw new Error('Download failed')
+ const blob = await res.blob()
+ const filename = res.headers.get('Content-Disposition')?.match(/filename="(.+)"/)?.[1] || 'INK2_SRU.zip'
+ const url = URL.createObjectURL(blob)
+ const a = document.createElement('a')
+ a.href = url
+ a.download = filename
+ a.click()
+ URL.revokeObjectURL(url)
+ } catch {
+ setError('Kunde inte ladda ner SRU-filer')
+ } finally {
+ setDownloading(false)
+ }
}
return (
@@ -52,20 +69,34 @@ export function INK2DeclarationView({ periodId }: { periodId: string }) {
-
- INK2 visar det bokföringsmässiga resultatet baserat på din bokföring.
- Skattemässiga justeringar (ej avdragsgilla kostnader, periodiseringsfonder m.m.)
- hanteras av din revisor/redovisningskonsult.
-
+
+
+ INK2 visar det bokföringsmässiga resultatet baserat på din bokföring.
+ Skattemässiga justeringar (ej avdragsgilla kostnader, periodiseringsfonder m.m.)
+ hanteras av din revisor/redovisningskonsult.
+
+
+ SRU-filen laddas ner som en ZIP med INFO.SRU och BLANKETTER.SRU.
+ Ladda upp båda filerna till{' '}
+
+ Skatteverkets filöverföringstjänst
+ .
+
+
{loading ? 'Laddar...' : 'Hämta INK2'}
{data && (
-
+
- Ladda ner SRU-fil
+ {downloading ? 'Laddar ner...' : 'Ladda ner SRU-filer'}
)}
@@ -126,12 +157,12 @@ export function INK2DeclarationView({ periodId }: { periodId: string }) {
- {INK2_ASSET_CODES.map((code) => (
+ {INK2R_ASSET_CODES.map((code) => (
))}
@@ -156,12 +187,12 @@ export function INK2DeclarationView({ periodId }: { periodId: string }) {
- {INK2_EQUITY_LIABILITY_CODES.map((code) => (
+ {INK2R_EQUITY_LIABILITY_CODES.map((code) => (
))}
@@ -186,19 +217,15 @@ export function INK2DeclarationView({ periodId }: { periodId: string }) {
- {INK2_INCOME_STATEMENT_CODES.map((code) => {
- const isExpense = code !== '7310' && code !== '7370' && code !== '7380'
- return (
-
- )
- })}
+ {INK2R_INCOME_CODES.map((code) => (
+
+ ))}
@@ -208,7 +235,7 @@ export function INK2DeclarationView({ periodId }: { periodId: string }) {
- Resultat efter finansiella poster
+ Årets resultat
= 0 ? 'text-success' : 'text-destructive'}`}>
{formatCurrency(data.totals.resultAfterFinancial)}
@@ -217,6 +244,57 @@ export function INK2DeclarationView({ periodId }: { periodId: string }) {
+
+ {/* INK2S summary */}
+
+
+ INK2S — Skattemässiga justeringar
+
+
+
+
+
+ Grundläggande justeringar beräknas automatiskt. Manuella justeringar
+ (periodiseringsfonder, koncernbidrag m.m.) hanteras av din redovisningskonsult.
+
+
+
+
+
+
+ 4.1
+ Årets resultat (vinst)
+
+ {formatCurrency(data.ink2s['7650'])}
+
+
+
+ 4.2
+ Årets resultat (förlust)
+
+ {formatCurrency(data.ink2s['7750'])}
+
+
+
+ 4.3a
+ Skatt på årets resultat (ej avdragsgill)
+
+ {formatCurrency(data.ink2s['7651'])}
+
+
+
+
+
+ {data.ink2s['8020'] > 0 ? 'Överskott (punkt 1.1)' : 'Underskott (punkt 1.2)'}
+
+ 0 ? 'text-success' : 'text-destructive'}`}>
+ {formatCurrency(data.ink2s['8020'] > 0 ? data.ink2s['8020'] : data.ink2s['8021'])}
+
+
+
+
+
+
>
)}
@@ -236,13 +314,11 @@ function INK2DeclarationRow({
label,
amount,
accounts,
- isExpense,
}: {
- code: INK2SRUCode
+ code: INK2RSRUCode
label: string
amount: number
accounts: Array<{ accountNumber: string; accountName: string; amount: number }>
- isExpense?: boolean
}) {
const [expanded, setExpanded] = useState(false)
@@ -263,8 +339,8 @@ function INK2DeclarationRow({
)}
-
- {isExpense && amount > 0 ? '-' : ''}{formatCurrency(Math.abs(amount))}
+
+ {formatCurrency(amount)}
{expanded && accounts.length > 0 && (
@@ -278,8 +354,8 @@ function INK2DeclarationRow({
{acc.accountName}
-
- {isExpense && acc.amount > 0 ? '-' : ''}{formatCurrency(Math.abs(acc.amount))}
+
+ {formatCurrency(acc.amount)}
))}
diff --git a/lib/api/schemas.ts b/lib/api/schemas.ts
index 0530b3f2..5919d25a 100644
--- a/lib/api/schemas.ts
+++ b/lib/api/schemas.ts
@@ -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(),
diff --git a/lib/invoices/pdf-template.tsx b/lib/invoices/pdf-template.tsx
index 2c229182..2a6a6cb6 100644
--- a/lib/invoices/pdf-template.tsx
+++ b/lib/invoices/pdf-template.tsx
@@ -351,6 +351,12 @@ export function InvoicePDF({ invoice, customer, items, company, originalInvoiceN
Förfallodatum:
{formatDate(invoice.due_date)}
+ {invoice.delivery_date && invoice.delivery_date !== invoice.invoice_date && (
+
+ Leveransdatum:
+ {formatDate(invoice.delivery_date)}
+
+ )}
{invoice.your_reference && (
Er referens:
@@ -447,12 +453,19 @@ export function InvoicePDF({ invoice, customer, items, company, originalInvoiceN
{vatByRate.size > 1 ? (
Array.from(vatByRate.entries())
- .filter(([, group]) => group.vat > 0)
.sort(([a], [b]) => b - a)
.map(([rate, group]) => (
-
- Moms {rate}%:
- {formatCurrency(group.vat, invoice.currency)}
+
+
+ Netto {rate}%:
+ {formatCurrency(group.base, invoice.currency)}
+
+ {group.vat > 0 && (
+
+ Moms {rate}%:
+ {formatCurrency(group.vat, invoice.currency)}
+
+ )}
))
) : (
@@ -477,9 +490,17 @@ export function InvoicePDF({ invoice, customer, items, company, originalInvoiceN
)}
{invoice.currency !== 'SEK' && invoice.total_sek && (
-
- I SEK (kurs {invoice.exchange_rate}):
- {formatCurrency(invoice.total_sek, 'SEK')}
+
+ {invoice.vat_amount_sek != null && invoice.vat_amount_sek !== 0 && (
+
+ Moms i SEK (kurs {invoice.exchange_rate}):
+ {formatCurrency(invoice.vat_amount_sek, 'SEK')}
+
+ )}
+
+ Totalt i SEK:
+ {formatCurrency(invoice.total_sek, 'SEK')}
+
)}
@@ -549,12 +570,17 @@ export function InvoicePDF({ invoice, customer, items, company, originalInvoiceN
)}
- {/* Reverse charge notice */}
+ {/* Reverse charge / export / exempt notice */}
{invoice.reverse_charge_text && (
{invoice.reverse_charge_text}
)}
+ {invoice.vat_treatment === 'exempt' && !invoice.reverse_charge_text && (
+
+ Undantag från skatteplikt, ML 3 kap.
+
+ )}
{/* Notes */}
{invoice.notes && (
diff --git a/lib/invoices/vat-rules.ts b/lib/invoices/vat-rules.ts
index ae8b1e56..5185ac34 100644
--- a/lib/invoices/vat-rules.ts
+++ b/lib/invoices/vat-rules.ts
@@ -103,6 +103,7 @@ export function getVatRules(
treatment: 'export',
rate: 0,
momsRuta: '40',
+ reverseChargeText: 'Omsättning utanför EU, ML 10 kap.',
}
default:
diff --git a/lib/reports/ink2/__tests__/ink2-engine.test.ts b/lib/reports/ink2/__tests__/ink2-engine.test.ts
index 29c7f46a..724251cc 100644
--- a/lib/reports/ink2/__tests__/ink2-engine.test.ts
+++ b/lib/reports/ink2/__tests__/ink2-engine.test.ts
@@ -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')
})
})
})
diff --git a/lib/reports/ink2/__tests__/sru-generator.test.ts b/lib/reports/ink2/__tests__/sru-generator.test.ts
index 06374b1a..7adb25ab 100644
--- a/lib/reports/ink2/__tests__/sru-generator.test.ts
+++ b/lib/reports/ink2/__tests__/sru-generator.test.ts
@@ -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 {
+ 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
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
}
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] : ''
+}
diff --git a/lib/reports/ink2/ink2-engine.ts b/lib/reports/ink2/ink2-engine.ts
index a775f9ba..95467dd9 100644
--- a/lib/reports/ink2/ink2-engine.ts
+++ b/lib/reports/ink2/ink2-engine.ts
@@ -7,169 +7,606 @@ import type {
} from '@/types'
import type {
INK2Declaration,
- INK2DeclarationRutor,
+ INK2RRutor,
+ INK2Rutor,
+ INK2SRutor,
INK2AccountMapping,
- INK2SRUCode,
+ INK2RSRUCode,
} from './types'
/**
- * INK2 (Aktiebolag / Limited Company Declaration)
+ * INK2 Declaration Engine
*
- * Maps BAS account balances to INK2 declaration fields (SRU 7201-7380)
- * for tax reporting to Skatteverket.
+ * Generates INK2 (huvudblankett), INK2R (räkenskapsschema), and INK2S
+ * (skattemässiga justeringar) for aktiebolag tax reporting.
*
- * This generates the bokföringsmässigt resultat (accounting result).
- * Skattemässiga justeringar (INK2S) are handled by the accountant.
+ * Account mappings follow the official BAS-to-SRU mapping from
+ * bas.se/kontoplaner/sru/ and Skatteverket field code spec.
*
- * Account mappings use engine-internal range-based logic, NOT the DB
- * sru_code column, because the DB column is NE-biased for class 3-8.
+ * INK2R contains the full balance sheet + income statement.
+ * INK2S auto-derives basic fields (result + tax → taxable result).
+ * Manual tax adjustments (periodiseringsfonder etc.) are handled by the accountant.
*/
/**
- * Account mapping configuration for INK2 declaration
+ * BAS-to-SRU account mappings for INK2R
+ * Source: bas.se/kontoplaner/sru/ (stable since 2017)
*/
-export const INK2_ACCOUNT_MAPPINGS: INK2AccountMapping[] = [
- // Balance sheet - Assets
+export const INK2R_ACCOUNT_MAPPINGS: INK2AccountMapping[] = [
+ // ---- Balance sheet: Assets ----
{
sruCode: '7201',
- description: 'Immateriella anläggningstillgångar',
+ description: 'Koncessioner, patent, licenser, varumärken, goodwill',
section: 'assets',
normalBalance: 'debit',
- accountRanges: [{ start: '1000', end: '1099' }],
- },
- {
- sruCode: '7202',
- description: 'Materiella anläggningstillgångar',
- section: 'assets',
- normalBalance: 'debit',
- accountRanges: [{ start: '1100', end: '1299' }],
- },
- {
- sruCode: '7203',
- description: 'Finansiella anläggningstillgångar',
- section: 'assets',
- normalBalance: 'debit',
- accountRanges: [{ start: '1300', end: '1399' }],
- },
- {
- sruCode: '7210',
- description: 'Varulager m.m.',
- section: 'assets',
- normalBalance: 'debit',
- accountRanges: [{ start: '1400', end: '1499' }],
- },
- {
- sruCode: '7211',
- description: 'Kundfordringar',
- section: 'assets',
- normalBalance: 'debit',
- accountRanges: [{ start: '1500', end: '1599' }],
- },
- {
- sruCode: '7212',
- description: 'Övriga omsättningstillgångar',
- section: 'assets',
- normalBalance: 'debit',
- accountRanges: [{ start: '1600', end: '1999' }],
- },
-
- // Balance sheet - Equity & Liabilities
- {
- sruCode: '7220',
- description: 'Aktiekapital',
- section: 'equity_liabilities',
- normalBalance: 'credit',
- accountRanges: [{ start: '2081', end: '2081' }],
- },
- {
- sruCode: '7221',
- description: 'Övrigt eget kapital',
- section: 'equity_liabilities',
- normalBalance: 'credit',
accountRanges: [
- { start: '2000', end: '2080' },
- { start: '2082', end: '2098' },
+ { start: '1010', end: '1079' },
+ { start: '1090', end: '1099' },
],
},
{
- sruCode: '7222',
- description: 'Årets resultat',
- section: 'equity_liabilities',
- normalBalance: 'credit',
- accountRanges: [{ start: '2099', end: '2099' }],
+ sruCode: '7202',
+ description: 'Förskott immateriella anläggningstillgångar',
+ section: 'assets',
+ normalBalance: 'debit',
+ accountRanges: [{ start: '1080', end: '1089' }],
+ },
+ {
+ sruCode: '7214',
+ description: 'Byggnader och mark',
+ section: 'assets',
+ normalBalance: 'debit',
+ accountRanges: [
+ { start: '1100', end: '1119' },
+ { start: '1130', end: '1179' },
+ { start: '1190', end: '1199' },
+ ],
+ },
+ {
+ sruCode: '7215',
+ description: 'Maskiner, inventarier, övriga materiella',
+ section: 'assets',
+ normalBalance: 'debit',
+ accountRanges: [{ start: '1200', end: '1299' }],
+ },
+ {
+ sruCode: '7216',
+ description: 'Förbättringsutgifter på annans fastighet',
+ section: 'assets',
+ normalBalance: 'debit',
+ accountRanges: [{ start: '1120', end: '1129' }],
+ },
+ {
+ sruCode: '7217',
+ description: 'Pågående nyanläggningar, förskott materiella',
+ section: 'assets',
+ normalBalance: 'debit',
+ accountRanges: [{ start: '1180', end: '1189' }],
},
{
sruCode: '7230',
- description: 'Obeskattade reserver, avsättningar och skulder',
- section: 'equity_liabilities',
- normalBalance: 'credit',
- accountRanges: [{ start: '2100', end: '2499' }],
+ description: 'Andelar i koncernföretag',
+ section: 'assets',
+ normalBalance: 'debit',
+ accountRanges: [{ start: '1311', end: '1316' }],
},
{
sruCode: '7231',
- description: 'Övriga skulder',
- section: 'equity_liabilities',
- normalBalance: 'credit',
- accountRanges: [{ start: '2500', end: '2999' }],
+ description: 'Andelar i intresseföretag',
+ section: 'assets',
+ normalBalance: 'debit',
+ accountRanges: [{ start: '1330', end: '1338' }],
+ },
+ {
+ sruCode: '7233',
+ description: 'Ägarintressen övriga företag + långfristiga värdepapper',
+ section: 'assets',
+ normalBalance: 'debit',
+ accountRanges: [
+ { start: '1350', end: '1359' },
+ { start: '1380', end: '1389' },
+ ],
+ },
+ {
+ sruCode: '7232',
+ description: 'Fordringar koncern/intresse',
+ section: 'assets',
+ normalBalance: 'debit',
+ accountRanges: [
+ { start: '1320', end: '1329' },
+ { start: '1340', end: '1349' },
+ ],
+ },
+ {
+ sruCode: '7234',
+ description: 'Lån till delägare eller närstående',
+ section: 'assets',
+ normalBalance: 'debit',
+ accountRanges: [{ start: '1360', end: '1369' }],
+ },
+ {
+ sruCode: '7235',
+ description: 'Övriga långfristiga fordringar',
+ section: 'assets',
+ normalBalance: 'debit',
+ accountRanges: [
+ { start: '1370', end: '1379' },
+ { start: '1390', end: '1399' },
+ ],
+ },
+ {
+ sruCode: '7241',
+ description: 'Råvaror och förnödenheter',
+ section: 'assets',
+ normalBalance: 'debit',
+ accountRanges: [{ start: '1410', end: '1419' }],
+ },
+ {
+ sruCode: '7242',
+ description: 'Varor under tillverkning',
+ section: 'assets',
+ normalBalance: 'debit',
+ accountRanges: [{ start: '1440', end: '1449' }],
+ },
+ {
+ sruCode: '7243',
+ description: 'Färdiga varor och handelsvaror',
+ section: 'assets',
+ normalBalance: 'debit',
+ accountRanges: [{ start: '1450', end: '1469' }],
+ },
+ {
+ sruCode: '7244',
+ description: 'Övriga lagertillgångar',
+ section: 'assets',
+ normalBalance: 'debit',
+ accountRanges: [{ start: '1470', end: '1489' }],
+ },
+ {
+ sruCode: '7245',
+ description: 'Pågående arbeten för annans räkning',
+ section: 'assets',
+ normalBalance: 'debit',
+ accountRanges: [{ start: '1490', end: '1499' }],
+ },
+ {
+ sruCode: '7246',
+ description: 'Förskott till leverantörer',
+ section: 'assets',
+ normalBalance: 'debit',
+ accountRanges: [{ start: '1400', end: '1409' }],
+ },
+ {
+ sruCode: '7251',
+ description: 'Kundfordringar',
+ section: 'assets',
+ normalBalance: 'debit',
+ accountRanges: [{ start: '1500', end: '1519' }],
+ },
+ {
+ sruCode: '7252',
+ description: 'Fordringar koncern/intresse (kortfristiga)',
+ section: 'assets',
+ normalBalance: 'debit',
+ accountRanges: [{ start: '1560', end: '1579' }],
+ },
+ {
+ sruCode: '7261',
+ description: 'Övriga fordringar',
+ section: 'assets',
+ normalBalance: 'debit',
+ accountRanges: [
+ { start: '1520', end: '1559' },
+ { start: '1580', end: '1599' },
+ { start: '1600', end: '1619' },
+ { start: '1621', end: '1699' },
+ ],
+ },
+ {
+ sruCode: '7262',
+ description: 'Upparbetad men ej fakturerad intäkt',
+ section: 'assets',
+ normalBalance: 'debit',
+ accountRanges: [{ start: '1620', end: '1620' }],
+ },
+ {
+ sruCode: '7263',
+ description: 'Förutbetalda kostnader och upplupna intäkter',
+ section: 'assets',
+ normalBalance: 'debit',
+ accountRanges: [{ start: '1700', end: '1799' }],
+ },
+ {
+ sruCode: '7270',
+ description: 'Andelar i koncernföretag (kortfristiga)',
+ section: 'assets',
+ normalBalance: 'debit',
+ accountRanges: [{ start: '1860', end: '1869' }],
+ },
+ {
+ sruCode: '7271',
+ description: 'Övriga kortfristiga placeringar',
+ section: 'assets',
+ normalBalance: 'debit',
+ accountRanges: [
+ { start: '1800', end: '1859' },
+ { start: '1870', end: '1899' },
+ ],
+ },
+ {
+ sruCode: '7281',
+ description: 'Kassa, bank och redovisningsmedel',
+ section: 'assets',
+ normalBalance: 'debit',
+ accountRanges: [{ start: '1900', end: '1999' }],
},
- // Income statement
+ // ---- Balance sheet: Equity & Liabilities ----
{
- sruCode: '7310',
+ sruCode: '7301',
+ description: 'Bundet eget kapital',
+ section: 'equity_liabilities',
+ normalBalance: 'credit',
+ accountRanges: [{ start: '2010', end: '2089' }],
+ },
+ {
+ sruCode: '7302',
+ description: 'Fritt eget kapital',
+ section: 'equity_liabilities',
+ normalBalance: 'credit',
+ accountRanges: [{ start: '2090', end: '2099' }],
+ },
+ {
+ sruCode: '7321',
+ description: 'Periodiseringsfonder',
+ section: 'equity_liabilities',
+ normalBalance: 'credit',
+ accountRanges: [
+ { start: '2100', end: '2109' },
+ { start: '2110', end: '2129' },
+ ],
+ },
+ {
+ sruCode: '7322',
+ description: 'Ackumulerade överavskrivningar',
+ section: 'equity_liabilities',
+ normalBalance: 'credit',
+ accountRanges: [{ start: '2150', end: '2159' }],
+ },
+ {
+ sruCode: '7323',
+ description: 'Övriga obeskattade reserver',
+ section: 'equity_liabilities',
+ normalBalance: 'credit',
+ accountRanges: [
+ { start: '2130', end: '2149' },
+ { start: '2160', end: '2199' },
+ ],
+ },
+ {
+ sruCode: '7331',
+ description: 'Pensionsavsättningar tryggandelagen',
+ section: 'equity_liabilities',
+ normalBalance: 'credit',
+ accountRanges: [{ start: '2210', end: '2219' }],
+ },
+ {
+ sruCode: '7332',
+ description: 'Övriga pensionsavsättningar',
+ section: 'equity_liabilities',
+ normalBalance: 'credit',
+ accountRanges: [{ start: '2220', end: '2229' }],
+ },
+ {
+ sruCode: '7333',
+ description: 'Övriga avsättningar',
+ section: 'equity_liabilities',
+ normalBalance: 'credit',
+ accountRanges: [{ start: '2230', end: '2299' }],
+ },
+ {
+ sruCode: '7350',
+ description: 'Obligationslån',
+ section: 'equity_liabilities',
+ normalBalance: 'credit',
+ accountRanges: [
+ { start: '2300', end: '2319' },
+ { start: '2320', end: '2329' },
+ ],
+ },
+ {
+ sruCode: '7351',
+ description: 'Checkräkningskredit (långfristig)',
+ section: 'equity_liabilities',
+ normalBalance: 'credit',
+ accountRanges: [{ start: '2330', end: '2339' }],
+ },
+ {
+ sruCode: '7352',
+ description: 'Övriga skulder kreditinstitut (långfristiga)',
+ section: 'equity_liabilities',
+ normalBalance: 'credit',
+ accountRanges: [{ start: '2340', end: '2359' }],
+ },
+ {
+ sruCode: '7353',
+ description: 'Skulder koncern/intresse (långfristiga)',
+ section: 'equity_liabilities',
+ normalBalance: 'credit',
+ accountRanges: [{ start: '2360', end: '2379' }],
+ },
+ {
+ sruCode: '7354',
+ description: 'Övriga skulder (långfristiga)',
+ section: 'equity_liabilities',
+ normalBalance: 'credit',
+ accountRanges: [{ start: '2380', end: '2399' }],
+ },
+ {
+ sruCode: '7360',
+ description: 'Checkräkningskredit (kortfristig)',
+ section: 'equity_liabilities',
+ normalBalance: 'credit',
+ accountRanges: [{ start: '2410', end: '2419' }],
+ },
+ {
+ sruCode: '7361',
+ description: 'Övriga skulder kreditinstitut (kortfristiga)',
+ section: 'equity_liabilities',
+ normalBalance: 'credit',
+ accountRanges: [{ start: '2420', end: '2439' }],
+ },
+ {
+ sruCode: '7362',
+ description: 'Förskott från kunder',
+ section: 'equity_liabilities',
+ normalBalance: 'credit',
+ accountRanges: [{ start: '2400', end: '2409' }],
+ },
+ {
+ sruCode: '7363',
+ description: 'Pågående arbeten (skuldsida)',
+ section: 'equity_liabilities',
+ normalBalance: 'credit',
+ accountRanges: [{ start: '2450', end: '2459' }],
+ },
+ {
+ sruCode: '7364',
+ description: 'Fakturerad men ej upparbetad intäkt',
+ section: 'equity_liabilities',
+ normalBalance: 'credit',
+ accountRanges: [{ start: '2460', end: '2469' }],
+ },
+ {
+ sruCode: '7365',
+ description: 'Leverantörsskulder',
+ section: 'equity_liabilities',
+ normalBalance: 'credit',
+ accountRanges: [{ start: '2440', end: '2449' }],
+ },
+ {
+ sruCode: '7366',
+ description: 'Växelskulder',
+ section: 'equity_liabilities',
+ normalBalance: 'credit',
+ accountRanges: [{ start: '2490', end: '2490' }],
+ },
+ {
+ sruCode: '7367',
+ description: 'Skulder koncern/intresse (kortfristiga)',
+ section: 'equity_liabilities',
+ normalBalance: 'credit',
+ accountRanges: [{ start: '2470', end: '2479' }],
+ },
+ {
+ sruCode: '7369',
+ description: 'Övriga skulder (kortfristiga)',
+ section: 'equity_liabilities',
+ normalBalance: 'credit',
+ accountRanges: [
+ { start: '2480', end: '2489' },
+ { start: '2491', end: '2499' },
+ { start: '2600', end: '2799' },
+ { start: '2800', end: '2899' },
+ ],
+ },
+ {
+ sruCode: '7368',
+ description: 'Skatteskulder',
+ section: 'equity_liabilities',
+ normalBalance: 'credit',
+ accountRanges: [{ start: '2500', end: '2599' }],
+ },
+ {
+ sruCode: '7370',
+ description: 'Upplupna kostnader och förutbetalda intäkter',
+ section: 'equity_liabilities',
+ normalBalance: 'credit',
+ accountRanges: [{ start: '2900', end: '2999' }],
+ },
+
+ // ---- Income statement ----
+ {
+ sruCode: '7410',
description: 'Nettoomsättning',
section: 'income_statement',
normalBalance: 'credit',
- accountRanges: [{ start: '3000', end: '3999' }],
+ accountRanges: [{ start: '3000', end: '3799' }],
},
{
- sruCode: '7320',
- description: 'Varuinköp/direkta kostnader',
+ sruCode: '7412',
+ description: 'Aktiverat arbete för egen räkning',
+ section: 'income_statement',
+ normalBalance: 'credit',
+ accountRanges: [{ start: '3800', end: '3899' }],
+ },
+ {
+ sruCode: '7413',
+ description: 'Övriga rörelseintäkter',
+ section: 'income_statement',
+ normalBalance: 'credit',
+ accountRanges: [{ start: '3900', end: '3999' }],
+ },
+ {
+ sruCode: '7411',
+ description: 'Förändring av lager',
+ section: 'income_statement',
+ normalBalance: 'net',
+ accountRanges: [{ start: '4900', end: '4999' }],
+ },
+ {
+ sruCode: '7511',
+ description: 'Råvaror och förnödenheter',
section: 'income_statement',
normalBalance: 'debit',
- accountRanges: [{ start: '4000', end: '4999' }],
+ accountRanges: [
+ { start: '4000', end: '4499' },
+ { start: '4500', end: '4599' },
+ { start: '4700', end: '4899' },
+ ],
},
{
- sruCode: '7330',
+ sruCode: '7512',
+ description: 'Handelsvaror',
+ section: 'income_statement',
+ normalBalance: 'debit',
+ accountRanges: [{ start: '4600', end: '4699' }],
+ },
+ // CRITICAL: BAS 5000-6999 ALL map to SRU 7513
+ {
+ sruCode: '7513',
description: 'Övriga externa kostnader',
section: 'income_statement',
normalBalance: 'debit',
accountRanges: [{ start: '5000', end: '6999' }],
},
{
- sruCode: '7340',
+ sruCode: '7514',
description: 'Personalkostnader',
section: 'income_statement',
normalBalance: 'debit',
accountRanges: [{ start: '7000', end: '7699' }],
},
{
- sruCode: '7350',
- description: 'Avskrivningar',
+ sruCode: '7515',
+ description: 'Av- och nedskrivningar materiella/immateriella',
section: 'income_statement',
normalBalance: 'debit',
- accountRanges: [{ start: '7700', end: '7899' }],
+ accountRanges: [{ start: '7800', end: '7899' }],
},
{
- sruCode: '7360',
+ sruCode: '7516',
+ description: 'Nedskrivningar omsättningstillgångar',
+ section: 'income_statement',
+ normalBalance: 'debit',
+ accountRanges: [{ start: '7700', end: '7799' }],
+ },
+ {
+ sruCode: '7517',
description: 'Övriga rörelsekostnader',
section: 'income_statement',
normalBalance: 'debit',
accountRanges: [{ start: '7900', end: '7999' }],
},
{
- sruCode: '7370',
- description: 'Finansiella poster (netto)',
+ sruCode: '7414',
+ description: 'Resultat från andelar i koncernföretag',
section: 'income_statement',
normalBalance: 'net',
- accountRanges: [{ start: '8000', end: '8499' }],
+ accountRanges: [{ start: '8000', end: '8099' }],
},
{
- sruCode: '7380',
- description: 'Extraordinära poster (netto)',
+ sruCode: '7415',
+ description: 'Resultat från andelar i intresseföretag',
section: 'income_statement',
normalBalance: 'net',
- accountRanges: [{ start: '8500', end: '8999' }],
+ accountRanges: [{ start: '8100', end: '8199' }],
},
+ {
+ sruCode: '7423',
+ description: 'Resultat från övriga företag med ägarintresse',
+ section: 'income_statement',
+ normalBalance: 'net',
+ accountRanges: [{ start: '8200', end: '8269' }],
+ },
+ {
+ sruCode: '7416',
+ description: 'Resultat från övriga finansiella anläggningstillgångar',
+ section: 'income_statement',
+ normalBalance: 'net',
+ accountRanges: [{ start: '8270', end: '8299' }],
+ },
+ {
+ sruCode: '7417',
+ description: 'Övriga ränteintäkter och liknande',
+ section: 'income_statement',
+ normalBalance: 'credit',
+ accountRanges: [{ start: '8300', end: '8399' }],
+ },
+ {
+ sruCode: '7522',
+ description: 'Räntekostnader och liknande',
+ section: 'income_statement',
+ normalBalance: 'debit',
+ accountRanges: [{ start: '8400', end: '8499' }],
+ },
+ {
+ sruCode: '7521',
+ description: 'Nedskrivningar finansiella anläggningstillgångar',
+ section: 'income_statement',
+ normalBalance: 'debit',
+ accountRanges: [{ start: '8500', end: '8599' }],
+ },
+ {
+ sruCode: '7524',
+ description: 'Lämnade koncernbidrag',
+ section: 'income_statement',
+ normalBalance: 'debit',
+ accountRanges: [{ start: '8810', end: '8810' }],
+ },
+ {
+ sruCode: '7419',
+ description: 'Mottagna koncernbidrag',
+ section: 'income_statement',
+ normalBalance: 'credit',
+ accountRanges: [{ start: '8820', end: '8820' }],
+ },
+ {
+ sruCode: '7420',
+ description: 'Återföring av periodiseringsfond',
+ section: 'income_statement',
+ normalBalance: 'credit',
+ accountRanges: [{ start: '8830', end: '8830' }],
+ },
+ {
+ sruCode: '7525',
+ description: 'Avsättning till periodiseringsfond',
+ section: 'income_statement',
+ normalBalance: 'debit',
+ accountRanges: [{ start: '8840', end: '8840' }],
+ },
+ {
+ sruCode: '7421',
+ description: 'Förändring av överavskrivningar',
+ section: 'income_statement',
+ normalBalance: 'net',
+ accountRanges: [{ start: '8850', end: '8850' }],
+ },
+ {
+ sruCode: '7422',
+ description: 'Övriga bokslutsdispositioner',
+ section: 'income_statement',
+ normalBalance: 'net',
+ accountRanges: [{ start: '8860', end: '8899' }],
+ },
+ {
+ sruCode: '7528',
+ description: 'Skatt på årets resultat',
+ section: 'income_statement',
+ normalBalance: 'debit',
+ accountRanges: [{ start: '8900', end: '8989' }],
+ },
+ // 7450/7550 (årets resultat vinst/förlust) are calculated, not mapped from accounts
]
/**
@@ -188,16 +625,14 @@ export function isAccountInMapping(accountNumber: string, mapping: INK2AccountMa
}
/**
- * Round to nearest krona (whole number) for INK2 declaration
+ * Truncate to nearest krona (drop öre) per SFL 22 kap. 1 §
*/
-function roundToKrona(value: number): number {
- return Math.round(value)
+function truncateToKrona(value: number): number {
+ return value >= 0 ? Math.floor(value) : Math.ceil(value)
}
/**
* Check if the balance sheet totals differ beyond the expected rounding tolerance.
- * Each ruta is independently rounded to whole kronor for SRU output, so with 11+
- * rutor the accumulated rounding can produce a 1-2 kr difference.
*/
export function checkBalanceWarning(totalAssets: number, totalEquityLiabilities: number): string | null {
const balanceDiff = Math.abs(totalAssets - totalEquityLiabilities)
@@ -208,6 +643,49 @@ export function checkBalanceWarning(totalAssets: number, totalEquityLiabilities:
return null
}
+/** Create zero-initialized INK2R rutor */
+function createEmptyINK2RRutor(): INK2RRutor {
+ return {
+ '7201': 0, '7202': 0, '7214': 0, '7215': 0, '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': 0, '7252': 0, '7261': 0, '7262': 0, '7263': 0,
+ '7270': 0, '7271': 0, '7281': 0,
+ '7301': 0, '7302': 0,
+ '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': 0, '7366': 0, '7367': 0, '7369': 0, '7368': 0, '7370': 0,
+ '7410': 0, '7411': 0, '7412': 0, '7413': 0,
+ '7511': 0, '7512': 0, '7513': 0, '7514': 0, '7515': 0, '7516': 0, '7517': 0,
+ '7414': 0, '7415': 0, '7423': 0, '7416': 0, '7417': 0,
+ '7521': 0, '7522': 0,
+ '7524': 0, '7419': 0, '7420': 0, '7525': 0, '7421': 0, '7422': 0,
+ '7528': 0,
+ '7450': 0, '7550': 0,
+ }
+}
+
+/** All INK2R asset codes for summing */
+const ASSET_CODES: INK2RSRUCode[] = [
+ '7201', '7202', '7214', '7215', '7216', '7217',
+ '7230', '7231', '7233', '7232', '7234', '7235',
+ '7241', '7242', '7243', '7244', '7245', '7246',
+ '7251', '7252', '7261', '7262', '7263',
+ '7270', '7271', '7281',
+]
+
+/** All INK2R equity/liability codes for summing */
+const EQUITY_LIABILITY_CODES: INK2RSRUCode[] = [
+ '7301', '7302',
+ '7321', '7322', '7323',
+ '7331', '7332', '7333',
+ '7350', '7351', '7352', '7353', '7354',
+ '7360', '7361', '7362', '7363', '7364', '7365', '7366', '7367', '7369', '7368',
+ '7370',
+]
+
/**
* Generate INK2 declaration for a fiscal period
*/
@@ -232,7 +710,7 @@ export async function generateINK2Declaration(
// Fetch company settings
const { data: settings } = await supabase
.from('company_settings')
- .select('company_name, org_number, entity_type')
+ .select('company_name, org_number, entity_type, address_line1, postal_code, city, email')
.eq('company_id', companyId)
.single()
@@ -279,14 +757,9 @@ export async function generateINK2Declaration(
}
}
- // Initialize rutor
- const rutor: INK2DeclarationRutor = {
- '7201': 0, '7202': 0, '7203': 0, '7210': 0, '7211': 0, '7212': 0,
- '7220': 0, '7221': 0, '7222': 0, '7230': 0, '7231': 0,
- '7310': 0, '7320': 0, '7330': 0, '7340': 0, '7350': 0, '7360': 0, '7370': 0, '7380': 0,
- }
-
- const allCodes: INK2SRUCode[] = Object.keys(rutor) as INK2SRUCode[]
+ // Initialize INK2R rutor and breakdown
+ const ink2r = createEmptyINK2RRutor()
+ const allCodes = Object.keys(ink2r) as INK2RSRUCode[]
const breakdown = {} as INK2Declaration['breakdown']
for (const code of allCodes) {
breakdown[code] = { accounts: [], total: 0 }
@@ -294,83 +767,147 @@ export async function generateINK2Declaration(
const warnings: string[] = []
- // Process each account balance
+ // Process each account balance against INK2R mappings
for (const [accountNumber, balance] of accountBalances) {
if (Math.abs(balance) < 0.01) continue
- for (const mapping of INK2_ACCOUNT_MAPPINGS) {
+ // Skip account 8999 — årets resultat is calculated
+ if (accountNumber === '8999') continue
+
+ let mapped = false
+ for (const mapping of INK2R_ACCOUNT_MAPPINGS) {
if (isAccountInMapping(accountNumber, mapping)) {
let amount: number
- if (mapping.normalBalance === 'debit') {
- // Asset/expense accounts: debit normal, balance is already positive for debit
- amount = balance
- } else if (mapping.normalBalance === 'credit') {
- // Equity/liability/revenue accounts: credit normal, negate to show as positive
- amount = -balance
+ if (mapping.section === 'income_statement') {
+ // Income statement: supply the signed value as-is from accounting
+ // Revenue (credit normal): balance is negative (credit > debit), negate for positive revenue
+ // Cost (debit normal): balance is positive (debit > credit), keep as-is for negative cost
+ // Net: negate so positive = income, negative = cost
+ if (mapping.normalBalance === 'credit') {
+ amount = -balance
+ } else if (mapping.normalBalance === 'debit') {
+ // Costs: debit balance is positive in ledger, but on the form they appear negative
+ amount = -balance
+ } else {
+ // Net: negate to match accounting convention
+ amount = -balance
+ }
} else {
- // Net fields (7370, 7380): negate so positive = net income, negative = net cost
- amount = -balance
+ // Balance sheet: all amounts reported as positive
+ if (mapping.normalBalance === 'debit') {
+ amount = balance
+ } else {
+ amount = -balance
+ }
}
- rutor[mapping.sruCode] += amount
+ ink2r[mapping.sruCode] += amount
breakdown[mapping.sruCode].accounts.push({
accountNumber,
accountName: accountNameMap.get(accountNumber) || `Konto ${accountNumber}`,
- amount: roundToKrona(amount),
+ amount: truncateToKrona(amount),
})
+ mapped = true
break
}
}
+
+ if (!mapped) {
+ // BAS accounts 4500-4599, 4700-4899, and 1300-1310 have no standard SRU mapping
+ // These are unusual and may indicate custom accounts
+ const classChar = accountNumber.charAt(0)
+ if (classChar >= '1' && classChar <= '8') {
+ // Only warn for standard BAS range accounts that weren't mapped
+ warnings.push(`Konto ${accountNumber} (${accountNameMap.get(accountNumber) || 'okänt'}) kunde inte mappas till ett SRU-fält.`)
+ }
+ }
}
- // Round all rutor to whole krona
+ // Truncate all INK2R rutor to whole kronor
for (const code of allCodes) {
- rutor[code] = roundToKrona(rutor[code])
- breakdown[code].total = rutor[code]
+ ink2r[code] = truncateToKrona(ink2r[code])
+ breakdown[code].total = ink2r[code]
}
- // Calculate derived totals
- const totalAssets = rutor['7201'] + rutor['7202'] + rutor['7203'] +
- rutor['7210'] + rutor['7211'] + rutor['7212']
+ // Calculate totals
+ const totalAssets = ASSET_CODES.reduce((sum, code) => sum + ink2r[code], 0)
+ const totalEquityLiabilities = EQUITY_LIABILITY_CODES.reduce((sum, code) => sum + ink2r[code], 0)
- // Operating result = revenue - operating costs
- const operatingResult = rutor['7310'] -
- rutor['7320'] - rutor['7330'] - rutor['7340'] -
- rutor['7350'] - rutor['7360']
+ // Operating result: revenue + costs (costs are already negative)
+ const operatingResult =
+ ink2r['7410'] + ink2r['7411'] + ink2r['7412'] + ink2r['7413'] +
+ ink2r['7511'] + ink2r['7512'] + ink2r['7513'] + ink2r['7514'] +
+ ink2r['7515'] + ink2r['7516'] + ink2r['7517']
- // Result after financial items
- const resultAfterFinancial = operatingResult + rutor['7370'] + rutor['7380']
+ // Financial items
+ const financialItems =
+ ink2r['7414'] + ink2r['7415'] + ink2r['7423'] + ink2r['7416'] + ink2r['7417'] +
+ ink2r['7521'] + ink2r['7522']
- // Årets resultat (7222): During an open fiscal year, account 2099 has no balance —
- // the profit only exists as the net of income statement accounts (class 3-8).
- // After year-end closing, 2099 has the balance and income accounts are zeroed.
- // Adding resultAfterFinancial handles both cases correctly (0 + profit, or profit + 0).
- rutor['7222'] += roundToKrona(resultAfterFinancial)
- breakdown['7222'].total = rutor['7222']
- if (resultAfterFinancial !== 0) {
- breakdown['7222'].accounts.push({
- accountNumber: 'calc',
- accountName: 'Beräknat resultat från resultaträkningen',
- amount: roundToKrona(resultAfterFinancial),
- })
+ // Bokslutsdispositioner
+ const bokslutsdispositioner =
+ ink2r['7524'] + ink2r['7419'] + ink2r['7420'] + ink2r['7525'] + ink2r['7421'] + ink2r['7422']
+
+ // Result before tax
+ const resultBeforeTax = operatingResult + financialItems + bokslutsdispositioner
+
+ // Result after tax
+ const resultAfterFinancial = resultBeforeTax + ink2r['7528']
+
+ // Set årets resultat: vinst (7450) or förlust (7550)
+ if (resultAfterFinancial >= 0) {
+ ink2r['7450'] = resultAfterFinancial
+ ink2r['7550'] = 0
+ } else {
+ ink2r['7450'] = 0
+ ink2r['7550'] = Math.abs(resultAfterFinancial)
}
- const totalEquityLiabilities = rutor['7220'] + rutor['7221'] + rutor['7222'] +
- rutor['7230'] + rutor['7231']
+ // Add calculated result to fritt eget kapital for balance
+ // During open fiscal year, 2099 may have no balance — the result only exists
+ // as net of income statement accounts. Adding it here handles both cases.
+ const adjustedEquityLiabilities = totalEquityLiabilities + resultAfterFinancial
+
+ // Fiscal year dates as YYYYMMDD
+ const fyStart = (period.period_start as string).replace(/-/g, '')
+ const fyEnd = (period.period_end as string).replace(/-/g, '')
+
+ // Build INK2 (huvudblankett)
+ // Auto-derive from INK2S result (simplified: result + non-deductible tax)
+ const taxAmount = Math.abs(ink2r['7528'])
+ const taxableResult = resultAfterFinancial + taxAmount
+
+ const ink2: INK2Rutor = {
+ '7011': fyStart,
+ '7012': fyEnd,
+ '7113': taxableResult >= 0 ? taxableResult : 0,
+ '7114': taxableResult < 0 ? Math.abs(taxableResult) : 0,
+ }
+
+ // Build INK2S (skattemässiga justeringar — auto-derived basics only)
+ const ink2s: INK2SRutor = {
+ '7011': fyStart,
+ '7012': fyEnd,
+ '7650': resultAfterFinancial >= 0 ? resultAfterFinancial : 0,
+ '7750': resultAfterFinancial < 0 ? Math.abs(resultAfterFinancial) : 0,
+ '7651': taxAmount, // Skatt (ej avdragsgill)
+ '8020': taxableResult >= 0 ? taxableResult : 0,
+ '8021': taxableResult < 0 ? Math.abs(taxableResult) : 0,
+ }
// Add warnings
if (!(period as FiscalPeriod).is_closed) {
warnings.push('Räkenskapsåret är inte stängt — deklarationen kan genereras, men siffrorna kan ändras om fler bokföringar görs.')
}
- if (totalAssets === 0 && totalEquityLiabilities === 0 && rutor['7310'] === 0) {
+ if (totalAssets === 0 && totalEquityLiabilities === 0 && ink2r['7410'] === 0) {
warnings.push('Inga bokförda transaktioner hittades för perioden.')
}
- const balanceWarning = checkBalanceWarning(totalAssets, totalEquityLiabilities)
+ const balanceWarning = checkBalanceWarning(totalAssets, adjustedEquityLiabilities)
if (balanceWarning) {
warnings.push(balanceWarning)
}
@@ -383,30 +920,24 @@ export async function generateINK2Declaration(
end: period.period_end,
isClosed: period.is_closed,
},
- rutor,
+ ink2,
+ ink2r,
+ ink2s,
breakdown,
totals: {
totalAssets,
- totalEquityLiabilities,
+ totalEquityLiabilities: adjustedEquityLiabilities,
operatingResult,
resultAfterFinancial,
},
companyInfo: {
companyName: settings?.company_name || 'Okänt företag',
orgNumber: settings?.org_number || null,
+ addressLine1: settings?.address_line1 || null,
+ postalCode: settings?.postal_code || null,
+ city: settings?.city || null,
+ email: settings?.email || null,
},
warnings,
}
}
-
-/**
- * Get totals for display
- */
-export function getINK2DeclarationTotals(declaration: INK2Declaration): {
- totalAssets: number
- totalEquityLiabilities: number
- operatingResult: number
- resultAfterFinancial: number
-} {
- return declaration.totals
-}
diff --git a/lib/reports/ink2/sru-generator.ts b/lib/reports/ink2/sru-generator.ts
index 20abe82f..3acc3ce9 100644
--- a/lib/reports/ink2/sru-generator.ts
+++ b/lib/reports/ink2/sru-generator.ts
@@ -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`
}
diff --git a/lib/reports/ink2/types.ts b/lib/reports/ink2/types.ts
index c271ac7d..d8381251 100644
--- a/lib/reports/ink2/types.ts
+++ b/lib/reports/ink2/types.ts
@@ -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 = {
- '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 = {
+ // 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)',
+}
diff --git a/supabase/migrations/20260409120000_add_invoice_delivery_date.sql b/supabase/migrations/20260409120000_add_invoice_delivery_date.sql
new file mode 100644
index 00000000..250e6636
--- /dev/null
+++ b/supabase/migrations/20260409120000_add_invoice_delivery_date.sql
@@ -0,0 +1,2 @@
+-- ML 17:24 p.7: leveransdatum when different from fakturadatum
+ALTER TABLE public.invoices ADD COLUMN IF NOT EXISTS delivery_date date;
diff --git a/tests/helpers.ts b/tests/helpers.ts
index 545e04f9..f002d88b 100644
--- a/tests/helpers.ts
+++ b/tests/helpers.ts
@@ -328,6 +328,7 @@ export function makeInvoice(overrides: Partial = {}): Invoice {
invoice_number: 'F-2024001',
invoice_date: '2024-06-15',
due_date: '2024-07-15',
+ delivery_date: null,
status: 'draft',
currency: 'SEK',
exchange_rate: null,
diff --git a/types/index.ts b/types/index.ts
index 4f14fd83..20f9dd55 100644
--- a/types/index.ts
+++ b/types/index.ts
@@ -546,6 +546,7 @@ export interface Invoice {
// Dates
invoice_date: string
due_date: string
+ delivery_date: string | null
// Status
status: InvoiceStatus