|
diff --git a/components/reports/SkatteverketPanel.tsx b/components/reports/SkatteverketPanel.tsx
index f948ae75..df26116e 100644
--- a/components/reports/SkatteverketPanel.tsx
+++ b/components/reports/SkatteverketPanel.tsx
@@ -36,6 +36,22 @@ interface SkatteverketStatus {
expiresAt?: string
}
+/**
+ * Codes from /lib/api-client.ts's SkatteverketAuthError that mean "the user
+ * needs to reconnect with BankID before this action can succeed". When the API
+ * returns one of these codes we flip the local status.expired flag so the
+ * "Session utgången" badge + "Förnya session" button surface, even if the
+ * upstream /status endpoint hasn't reflected the change yet.
+ */
+const AUTH_RECONNECT_CODES = new Set([
+ 'NOT_CONNECTED',
+ 'SESSION_EXPIRED',
+ 'REFRESH_EXHAUSTED',
+ 'TOKEN_REVOKED',
+ 'TOKEN_CORRUPTED',
+ 'MISSING_SCOPE',
+])
+
// Shape per Skatteverket Momsdeklaration v1.0.24 RAML
// (kontrollResultat.resultat[].{kod, status, beskrivning})
interface KontrollResult {
@@ -90,6 +106,21 @@ function SkatteverketPanelInner({ periodType, year, period, hasData, rutor }: Sk
const localErrors = localChecks.filter((c) => c.status === 'ERROR')
const localBlocked = localErrors.length > 0
+ /**
+ * Apply an API JSON error result. When the error indicates the SKV session
+ * has expired/been revoked/lost scope, immediately reflect that in the
+ * local status so the "Förnya session" CTA appears next to the message —
+ * the user shouldn't have to wait for /status to catch up.
+ */
+ const applyApiError = useCallback((result: { error?: string; code?: string } | null) => {
+ if (!result?.error) return false
+ setError(result.error)
+ if (result.code && AUTH_RECONNECT_CODES.has(result.code)) {
+ setStatus((prev) => prev ? { ...prev, expired: true } : prev)
+ }
+ return true
+ }, [])
+
const fetchStatus = useCallback(async () => {
try {
const res = await fetch('/api/extensions/ext/skatteverket/status')
@@ -169,8 +200,8 @@ function SkatteverketPanelInner({ periodType, year, period, hasData, rutor }: Sk
body: JSON.stringify({ periodType, year, period }),
})
const result = await res.json()
- if (result.error) {
- setError(result.error)
+ if (applyApiError(result)) {
+ // surfaced + status updated; nothing more to do
} else {
const controls: KontrollResult[] = result.data?.kontrollResultat?.resultat || []
setKontroller(controls)
@@ -215,8 +246,8 @@ function SkatteverketPanelInner({ periodType, year, period, hasData, rutor }: Sk
body: JSON.stringify({ periodType, year, period }),
})
const result = await res.json()
- if (result.error) {
- setError(result.error)
+ if (applyApiError(result)) {
+ // surfaced + status updated; nothing more to do
} else {
const controls: KontrollResult[] = result.data?.kontrollResultat?.resultat || []
setKontroller(controls)
@@ -245,8 +276,8 @@ function SkatteverketPanelInner({ periodType, year, period, hasData, rutor }: Sk
{ method: 'PUT' }
)
const result = await res.json()
- if (result.error) {
- setError(result.error)
+ if (applyApiError(result)) {
+ // surfaced + status updated; nothing more to do
} else if (result.data?.signeringsLank) {
setSigneringslank(result.data.signeringsLank)
setSuccess('Utkastet är låst. Öppna signeringslänken för att signera med BankID.')
@@ -269,8 +300,8 @@ function SkatteverketPanelInner({ periodType, year, period, hasData, rutor }: Sk
{ method: 'DELETE' }
)
const result = await res.json()
- if (result.error) {
- setError(result.error)
+ if (applyApiError(result)) {
+ // surfaced + status updated; nothing more to do
} else {
setSigneringslank(null)
setSuccess('Utkastet har låsts upp')
@@ -292,8 +323,8 @@ function SkatteverketPanelInner({ periodType, year, period, hasData, rutor }: Sk
)}&redovisningsperiod=${getRedovisningsperiod()}`
)
const result = await res.json()
- if (result.error) {
- setError(result.error)
+ if (applyApiError(result)) {
+ // surfaced + status updated; nothing more to do
} else if (result.data) {
setSubmitted(result.data)
setSuccess('Deklarationen har lämnats in')
@@ -323,7 +354,9 @@ function SkatteverketPanelInner({ periodType, year, period, hasData, rutor }: Sk
setSuccess('Utkastet har raderats från Eget utrymme')
} else {
const result = await res.json().catch(() => ({}))
- setError(result.error || `Kunde inte radera utkast (${res.status})`)
+ if (!applyApiError(result)) {
+ setError(`Kunde inte radera utkast (${res.status})`)
+ }
}
} catch {
setError('Kunde inte radera utkast')
@@ -342,8 +375,8 @@ function SkatteverketPanelInner({ periodType, year, period, hasData, rutor }: Sk
)}&redovisningsperiod=${getRedovisningsperiod()}`
)
const result = await res.json()
- if (result.error) {
- setError(result.error)
+ if (applyApiError(result)) {
+ // surfaced + status updated; nothing more to do
} else if (!result.data) {
setSuccess('Inget sparat utkast hittades för perioden')
} else {
@@ -369,8 +402,8 @@ function SkatteverketPanelInner({ periodType, year, period, hasData, rutor }: Sk
)}&redovisningsperiod=${getRedovisningsperiod()}`
)
const result = await res.json()
- if (result.error) {
- setError(result.error)
+ if (applyApiError(result)) {
+ // surfaced + status updated; nothing more to do
} else if (!result.data) {
setSuccess('Inget beslut hittades för perioden')
} else {
@@ -455,10 +488,21 @@ function SkatteverketPanelInner({ periodType, year, period, hasData, rutor }: Sk
Ansluten
{status.expired && (
-
-
- Session utgången
-
+ <>
+
+
+ Session utgången
+
+
+ >
)}
diff --git a/components/transactions/JournalEntryPreview.tsx b/components/transactions/JournalEntryPreview.tsx
index 99cd8e53..94ba055b 100644
--- a/components/transactions/JournalEntryPreview.tsx
+++ b/components/transactions/JournalEntryPreview.tsx
@@ -24,6 +24,8 @@ interface JournalEntryPreviewProps {
templateDebitAccount?: string
templateCreditAccount?: string
templateVatRate?: number
+ templateVatTreatment?: VatTreatment | null
+ templateSupplierType?: 'eu_business' | 'non_eu_business' | 'swedish_business'
/** For multi-line counterparty template bookings */
linePattern?: LinePatternEntry[]
settlementAccount?: string
@@ -39,6 +41,8 @@ export default function JournalEntryPreview({
templateDebitAccount,
templateCreditAccount,
templateVatRate,
+ templateVatTreatment,
+ templateSupplierType,
linePattern,
settlementAccount = '1930',
}: JournalEntryPreviewProps) {
@@ -91,6 +95,7 @@ export default function JournalEntryPreview({
const vatAmt = extractVatAmount(absAmount, vatRate)
const netAmt = extractNetAmount(absAmount, vatRate)
const isIncome = amount > 0
+ const isReverseCharge = templateVatTreatment === 'reverse_charge' && !isIncome
if (isIncome) {
// Income: debit bank gross, credit revenue net, credit output VAT
@@ -101,6 +106,32 @@ export default function JournalEntryPreview({
const outputVatAccount = vatRate === 0.06 ? '2631' : vatRate === 0.12 ? '2621' : '2611'
result.push({ side: 'kredit', account: outputVatAccount, amount: vatAmt })
}
+ } else if (isReverseCharge) {
+ // Expense with reverse charge: full reverse-charge verifikation
+ // (must match engine output in buildMappingResultFromTemplate).
+ const rcRate = 0.25
+ const rcVatAmt = Math.round(absAmount * rcRate * 100) / 100
+ const supplierType = templateSupplierType ?? 'eu_business'
+ const isDomestic = supplierType === 'swedish_business'
+
+ // Expense gross + bank
+ result.push({ side: 'debet', account: templateDebitAccount, amount: absAmount })
+ result.push({ side: 'kredit', account: templateCreditAccount, amount: absAmount })
+
+ // Fiktiv moms pair: 2645 (or 2647 domestic) / 2614
+ result.push({ side: 'debet', account: isDomestic ? '2647' : '2645', amount: rcVatAmt })
+ result.push({ side: 'kredit', account: '2614', amount: rcVatAmt })
+
+ // Basbelopp pair: 44xx|45xx / 4598 — populates rutor 20–24.
+ // Skip if the debit account is already a basis account.
+ if (!/^4[45]\d{2}$/.test(templateDebitAccount)) {
+ const basisAccount =
+ supplierType === 'eu_business' ? '4535'
+ : supplierType === 'non_eu_business' ? '4531'
+ : '4425'
+ result.push({ side: 'debet', account: basisAccount, amount: absAmount })
+ result.push({ side: 'kredit', account: '4598', amount: absAmount })
+ }
} else {
// Expense: debit expense net + input VAT, credit bank gross
result.push({ side: 'debet', account: templateDebitAccount, amount: netAmt })
@@ -150,7 +181,7 @@ export default function JournalEntryPreview({
}
return result
- }, [amount, category, vatTreatment, accountOverride, entityType, templateDebitAccount, templateCreditAccount, templateVatRate, linePattern, settlementAccount])
+ }, [amount, category, vatTreatment, accountOverride, entityType, templateDebitAccount, templateCreditAccount, templateVatRate, templateVatTreatment, templateSupplierType, linePattern, settlementAccount])
if (lines.length === 0) return null
diff --git a/components/transactions/QuickReviewDialog.tsx b/components/transactions/QuickReviewDialog.tsx
index e6bb0241..032e8ea2 100644
--- a/components/transactions/QuickReviewDialog.tsx
+++ b/components/transactions/QuickReviewDialog.tsx
@@ -172,7 +172,7 @@ export default function QuickReviewDialog({
}
onOpenChange(o)
}}>
-
+
Granska bokföring
@@ -264,7 +264,13 @@ export default function QuickReviewDialog({
{...(isCounterpartyTemplate
? { linePattern: counterpartyLinePattern ?? undefined }
: templateId && template
- ? { templateDebitAccount: template.debit_account, templateCreditAccount: template.credit_account, templateVatRate: template.vat_rate }
+ ? {
+ templateDebitAccount: template.debit_account,
+ templateCreditAccount: template.credit_account,
+ templateVatRate: template.vat_rate,
+ templateVatTreatment: template.vat_treatment,
+ templateSupplierType: template.reverse_charge_supplier_type,
+ }
: { category, vatTreatment: isLiabilityAccount ? 'none' : vatTreatment, accountOverride, entityType }
)}
/>
diff --git a/lib/bookkeeping/__tests__/booking-templates.test.ts b/lib/bookkeeping/__tests__/booking-templates.test.ts
index 391be003..e18c8cd3 100644
--- a/lib/bookkeeping/__tests__/booking-templates.test.ts
+++ b/lib/bookkeeping/__tests__/booking-templates.test.ts
@@ -316,18 +316,80 @@ describe('buildMappingResultFromTemplate', () => {
expect(result.vat_lines[0].debit_amount).toBe(30) // 530 * 0.06 / 1.06 = 30
})
- it('produces reverse charge lines for EU purchases', () => {
+ it('produces reverse charge lines for EU purchases (fiktiv moms + basbelopp)', () => {
const template = getTemplate('it_saas_eu')
const tx = makeTransaction({ amount: -1000 })
const result = buildMappingResultFromTemplate(template, tx, 'enskild_firma')
- expect(result.vat_lines).toHaveLength(2)
- // Fiktiv ingående moms
+ // Four lines: fiktiv-moms pair + basbelopp pair. Without the basbelopp
+ // pair the deklaration is rejected with FK004 (ruta 30-32 without 20-24).
+ expect(result.vat_lines).toHaveLength(4)
+ // Fiktiv ingående moms (EU: 2645)
expect(result.vat_lines[0].account_number).toBe('2645')
expect(result.vat_lines[0].debit_amount).toBe(250)
- // Fiktiv utgående moms
+ // Fiktiv utgående moms (25%: 2614)
expect(result.vat_lines[1].account_number).toBe('2614')
expect(result.vat_lines[1].credit_amount).toBe(250)
+ // Basbelopp EU services 25% → ruta 21
+ expect(result.vat_lines[2].account_number).toBe('4535')
+ expect(result.vat_lines[2].debit_amount).toBe(1000)
+ // Motkonto basbelopp
+ expect(result.vat_lines[3].account_number).toBe('4598')
+ expect(result.vat_lines[3].credit_amount).toBe(1000)
+ })
+
+ it('defaults to eu_business supplier type when not set on template', () => {
+ // it_cloud_hosting has no explicit reverse_charge_supplier_type
+ const template = getTemplate('it_cloud_hosting')
+ const tx = makeTransaction({ amount: -800 })
+ const result = buildMappingResultFromTemplate(template, tx, 'enskild_firma')
+
+ expect(result.vat_lines).toHaveLength(4)
+ // Defaults to EU services → 4535
+ expect(result.vat_lines[2].account_number).toBe('4535')
+ expect(result.vat_lines[2].debit_amount).toBe(800)
+ })
+
+ it('uses 4531 basbelopp for non-EU supplier type', () => {
+ const template: BookingTemplate = {
+ ...getTemplate('it_cloud_hosting'),
+ reverse_charge_supplier_type: 'non_eu_business',
+ }
+ const tx = makeTransaction({ amount: -1000 })
+ const result = buildMappingResultFromTemplate(template, tx, 'enskild_firma')
+
+ expect(result.vat_lines).toHaveLength(4)
+ expect(result.vat_lines[0].account_number).toBe('2645') // non-EU still uses 2645
+ expect(result.vat_lines[2].account_number).toBe('4531') // non-EU services → ruta 22
+ })
+
+ it('uses 4425 basbelopp and 2647 for domestic (swedish) reverse charge', () => {
+ const template: BookingTemplate = {
+ ...getTemplate('it_cloud_hosting'),
+ reverse_charge_supplier_type: 'swedish_business',
+ }
+ const tx = makeTransaction({ amount: -1000 })
+ const result = buildMappingResultFromTemplate(template, tx, 'enskild_firma')
+
+ expect(result.vat_lines).toHaveLength(4)
+ // Domestic RC uses 2647 (ML 16 kap) for the input pair
+ expect(result.vat_lines[0].account_number).toBe('2647')
+ // Domestic services (byggtjänster) → 4425, ruta 24
+ expect(result.vat_lines[2].account_number).toBe('4425')
+ })
+
+ it('skips basbelopp emission when template already debits a basis account', () => {
+ const template: BookingTemplate = {
+ ...getTemplate('it_cloud_hosting'),
+ debit_account: '4535', // user-customized template that books directly to basis
+ }
+ const tx = makeTransaction({ amount: -1000 })
+ const result = buildMappingResultFromTemplate(template, tx, 'enskild_firma')
+
+ // Only the fiktiv-moms pair — basbelopp would double-count.
+ expect(result.vat_lines).toHaveLength(2)
+ expect(result.vat_lines[0].account_number).toBe('2645')
+ expect(result.vat_lines[1].account_number).toBe('2614')
})
it('produces no VAT lines for exempt expenses', () => {
diff --git a/lib/bookkeeping/__tests__/mapping-engine.test.ts b/lib/bookkeeping/__tests__/mapping-engine.test.ts
index 1e6863e1..bf992172 100644
--- a/lib/bookkeeping/__tests__/mapping-engine.test.ts
+++ b/lib/bookkeeping/__tests__/mapping-engine.test.ts
@@ -341,5 +341,114 @@ describe('mapping-engine', () => {
expect(result.credit_account).toBe('1930')
expect(result.confidence).toBe(0.95)
})
+
+ it('emits both fiktiv-moms and basbelopp lines for reverse_charge rules', async () => {
+ const { evaluateMappingRules } = await import('../mapping-engine')
+
+ const tx = makeTransaction({
+ amount: -1000,
+ merchant_name: 'AWS',
+ description: 'AWS EU-WEST-1',
+ })
+
+ mockResult({
+ data: [
+ {
+ id: 'rule-rc',
+ user_id: 'user-1',
+ rule_name: 'AWS reverse charge',
+ rule_type: 'merchant_name',
+ priority: 10,
+ mcc_codes: null,
+ merchant_pattern: 'AWS',
+ description_pattern: null,
+ amount_min: null,
+ amount_max: null,
+ debit_account: '5421',
+ credit_account: '1930',
+ vat_treatment: 'reverse_charge',
+ vat_debit_account: null,
+ vat_credit_account: null,
+ risk_level: 'LOW',
+ default_private: false,
+ requires_review: false,
+ confidence_score: 0.9,
+ capitalization_threshold: null,
+ capitalized_debit_account: null,
+ is_active: true,
+ source: 'system',
+ user_description: null,
+ template_id: null,
+ created_at: '2024-01-01',
+ updated_at: '2024-01-01',
+ },
+ ],
+ error: null,
+ })
+
+ const result = await evaluateMappingRules(mockSupabase as never, 'user-1', tx)
+
+ // Fiktiv-moms pair + basbelopp pair = 4 lines (FK004 guard)
+ expect(result.vat_lines).toHaveLength(4)
+ expect(result.vat_lines[0].account_number).toBe('2645')
+ expect(result.vat_lines[0].debit_amount).toBe(250)
+ expect(result.vat_lines[1].account_number).toBe('2614')
+ expect(result.vat_lines[1].credit_amount).toBe(250)
+ expect(result.vat_lines[2].account_number).toBe('4535')
+ expect(result.vat_lines[2].debit_amount).toBe(1000)
+ expect(result.vat_lines[3].account_number).toBe('4598')
+ expect(result.vat_lines[3].credit_amount).toBe(1000)
+ })
+
+ it('skips basbelopp emission when rule already debits a basis account', async () => {
+ const { evaluateMappingRules } = await import('../mapping-engine')
+
+ const tx = makeTransaction({
+ amount: -1000,
+ merchant_name: 'AWS',
+ })
+
+ mockResult({
+ data: [
+ {
+ id: 'rule-rc-basis',
+ user_id: 'user-1',
+ rule_name: 'AWS RC to basis',
+ rule_type: 'merchant_name',
+ priority: 10,
+ mcc_codes: null,
+ merchant_pattern: 'AWS',
+ description_pattern: null,
+ amount_min: null,
+ amount_max: null,
+ debit_account: '4535',
+ credit_account: '1930',
+ vat_treatment: 'reverse_charge',
+ vat_debit_account: null,
+ vat_credit_account: null,
+ risk_level: 'LOW',
+ default_private: false,
+ requires_review: false,
+ confidence_score: 0.9,
+ capitalization_threshold: null,
+ capitalized_debit_account: null,
+ is_active: true,
+ source: 'system',
+ user_description: null,
+ template_id: null,
+ created_at: '2024-01-01',
+ updated_at: '2024-01-01',
+ },
+ ],
+ error: null,
+ })
+
+ const result = await evaluateMappingRules(mockSupabase as never, 'user-1', tx)
+
+ // Only fiktiv-moms pair — basbelopp already covered by the expense line
+ expect(result.vat_lines).toHaveLength(2)
+ expect(result.vat_lines[0].account_number).toBe('2645')
+ expect(result.vat_lines[1].account_number).toBe('2614')
+ })
})
})
diff --git a/lib/bookkeeping/booking-templates.ts b/lib/bookkeeping/booking-templates.ts
index fbf94b62..0f5c881d 100644
--- a/lib/bookkeeping/booking-templates.ts
+++ b/lib/bookkeeping/booking-templates.ts
@@ -7,7 +7,12 @@ import type {
VatTreatment,
RiskLevel,
} from '@/types'
-import { getVatRate, generateReverseChargeLines, generateInputVatLine } from './vat-entries'
+import {
+ getVatRate,
+ generateReverseChargeLines,
+ generateReverseChargeBasisLines,
+ generateInputVatLine,
+} from './vat-entries'
// ============================================================
// Types
@@ -59,6 +64,13 @@ export interface BookingTemplate {
description_sv: string
common: boolean
requires_vat_registration_data?: boolean
+ /**
+ * Supplier-type hint for reverse-charge bookings. Determines which 44xx/45xx
+ * basbelopp account is emitted alongside the 2645/2614 fiktiv-moms pair so
+ * Skatteverket's momsdeklaration rutor 20–24 line up with rutor 30–32
+ * (felkod FK004 if absent). Default 'eu_business' when unset.
+ */
+ reverse_charge_supplier_type?: 'eu_business' | 'non_eu_business' | 'swedish_business'
}
export interface TemplateGroupInfo {
@@ -1536,6 +1548,15 @@ export function findMatchingTemplates(
.slice(0, 10)
}
+/**
+ * Whether an account number sits in the reverse-charge basbelopp range
+ * (44xx/45xx series — ruta 20–24 inputs). Used to skip redundant basis
+ * emission when the template already books to such an account.
+ */
+function isBasisAccount(account: string): boolean {
+ return /^4[45]\d{2}$/.test(account)
+}
+
/**
* Convert a booking template into a MappingResult.
* Follows the same pattern as buildMappingResultFromCategory in category-mapping.ts.
@@ -1562,9 +1583,17 @@ export function buildMappingResultFromTemplate(
const vatRate = getVatRate(template.vat_treatment)
if (template.vat_treatment === 'reverse_charge' && isExpense) {
- // EU reverse charge: fiktiv moms (offsetting entries)
+ // EU/non-EU/domestic reverse charge: emit BOTH the fiktiv-moms pair
+ // (2645|2647 / 2614) AND the basbelopp pair (44xx|45xx / 4598). The
+ // basbelopp pair populates momsdeklaration rutor 20–24; without it
+ // Skatteverket rejects with FK004 ("ruta 30-32 utan motsvarande
+ // basbelopp i 20-24" — ML 13 kap kräver båda sidor).
const absAmount = Math.abs(transaction.amount)
- const rcLines = generateReverseChargeLines(absAmount)
+ const supplierType = template.reverse_charge_supplier_type ?? 'eu_business'
+ const isDomestic = supplierType === 'swedish_business'
+ const rcRate = 0.25 // fiktiv moms rate; current templates are 25%
+
+ const rcLines = generateReverseChargeLines(absAmount, rcRate, isDomestic)
for (const rcl of rcLines) {
vatLines.push({
account_number: rcl.account_number,
@@ -1573,6 +1602,20 @@ export function buildMappingResultFromTemplate(
description: rcl.line_description || '',
})
}
+
+ // Skip basbelopp emission if the template already books the expense
+ // directly to a basis account (44xx/45xx series) — would double-count.
+ if (!isBasisAccount(debitAccount)) {
+ const basisLines = generateReverseChargeBasisLines(absAmount, rcRate, supplierType)
+ for (const bl of basisLines) {
+ vatLines.push({
+ account_number: bl.account_number,
+ debit_amount: bl.debit_amount,
+ credit_amount: bl.credit_amount,
+ description: bl.line_description || '',
+ })
+ }
+ }
} else if (vatRate > 0 && isExpense) {
// Input VAT deduction
const absAmount = Math.abs(transaction.amount)
diff --git a/lib/bookkeeping/mapping-engine.ts b/lib/bookkeeping/mapping-engine.ts
index 3d4860a2..270499a8 100644
--- a/lib/bookkeeping/mapping-engine.ts
+++ b/lib/bookkeeping/mapping-engine.ts
@@ -2,6 +2,7 @@ import type { SupabaseClient } from '@supabase/supabase-js'
import {
generateInputVatLine,
generateReverseChargeLines,
+ generateReverseChargeBasisLines,
} from './vat-entries'
import { findMatchingTemplates, buildMappingResultFromTemplate } from './booking-templates'
import {
@@ -221,8 +222,13 @@ function buildResult(rule: MappingRule, transaction: Transaction, entityType?: E
const vatLines: VatJournalLine[] = []
if (isExpense && !rule.default_private && rule.vat_treatment) {
if (rule.vat_treatment === 'reverse_charge') {
- // EU reverse charge: fiktiv moms (offsetting entries)
- const rcLines = generateReverseChargeLines(absAmount)
+ // Reverse charge: emit BOTH the fiktiv-moms pair (2645/2614) AND the
+ // basbelopp pair (44xx|45xx / 4598). The basbelopp pair populates
+ // momsdeklaration rutor 20–24; without it Skatteverket rejects with
+ // FK004. Mapping rules don't carry supplier-country today, so we
+ // default to EU services — the most common reverse-charge scenario.
+ const rcRate = 0.25
+ const rcLines = generateReverseChargeLines(absAmount, rcRate, false)
for (const rcl of rcLines) {
vatLines.push({
account_number: rcl.account_number,
@@ -231,6 +237,19 @@ function buildResult(rule: MappingRule, transaction: Transaction, entityType?: E
description: rcl.line_description || '',
})
}
+
+ // Skip basbelopp emission if the rule already books to a basis account.
+ if (!/^4[45]\d{2}$/.test(debitAccount)) {
+ const basisLines = generateReverseChargeBasisLines(absAmount, rcRate, 'eu_business')
+ for (const bl of basisLines) {
+ vatLines.push({
+ account_number: bl.account_number,
+ debit_amount: bl.debit_amount,
+ credit_amount: bl.credit_amount,
+ description: bl.line_description || '',
+ })
+ }
+ }
} else if (rule.vat_treatment === 'standard_25' || rule.vat_treatment === 'reduced_12' || rule.vat_treatment === 'reduced_6') {
const vatRate =
rule.vat_treatment === 'standard_25' ? 0.25
diff --git a/lib/reports/__tests__/vat-declaration-checks.test.ts b/lib/reports/__tests__/vat-declaration-checks.test.ts
index 96649d1f..43e05a51 100644
--- a/lib/reports/__tests__/vat-declaration-checks.test.ts
+++ b/lib/reports/__tests__/vat-declaration-checks.test.ts
@@ -101,4 +101,107 @@ describe('runVatDeclarationChecks', () => {
const findings = runVatDeclarationChecks(rutor)
expect(findings.find((f) => f.code === 'SUMMA_MOMS_DRIFT')).toBeUndefined()
})
+
+ // SKV §4.1.1.4 rule 1 — taxable sales base without output VAT.
+ it('flags ERROR when taxable sales (ruta 05) booked without output VAT', () => {
+ const rutor: VatDeclarationRutor = {
+ ...emptyRutor,
+ ruta05: 10000,
+ // ruta 10/11/12 all zero — SKV rule 1 violation
+ ruta49: 0,
+ }
+ const findings = runVatDeclarationChecks(rutor)
+ const finding = findings.find((f) => f.code === 'TAXABLE_SALES_WITHOUT_OUTPUT')
+ expect(finding?.status).toBe('ERROR')
+ expect(finding?.message).toMatch(/försäljning/)
+ expect(finding?.message).toMatch(/utgående moms/)
+ })
+
+ it('flags ERROR for ruta 06 (uttag) without output VAT', () => {
+ const rutor: VatDeclarationRutor = {
+ ...emptyRutor,
+ ruta06: 5000,
+ ruta49: 0,
+ }
+ const findings = runVatDeclarationChecks(rutor)
+ expect(findings.find((f) => f.code === 'TAXABLE_SALES_WITHOUT_OUTPUT')?.status).toBe('ERROR')
+ })
+
+ it('does not flag taxable sales without output VAT when output VAT is present', () => {
+ const rutor: VatDeclarationRutor = {
+ ...emptyRutor,
+ ruta05: 10000,
+ ruta10: 2500,
+ ruta49: 2500,
+ }
+ const findings = runVatDeclarationChecks(rutor)
+ expect(findings.find((f) => f.code === 'TAXABLE_SALES_WITHOUT_OUTPUT')).toBeUndefined()
+ })
+
+ // Mirror: output VAT without taxable sales base.
+ it('flags ERROR when output VAT booked without taxable sales base', () => {
+ const rutor: VatDeclarationRutor = {
+ ...emptyRutor,
+ // No ruta 05/06/07/08
+ ruta10: 2500,
+ ruta49: 2500,
+ }
+ const findings = runVatDeclarationChecks(rutor)
+ expect(findings.find((f) => f.code === 'OUTPUT_VAT_WITHOUT_SALES_BASE')?.status).toBe('ERROR')
+ })
+
+ // SKV §4.1.1.4 rule 5 — import base without import output VAT.
+ it('flags ERROR when import base (ruta 50) without import output VAT', () => {
+ const rutor: VatDeclarationRutor = {
+ ...emptyRutor,
+ ruta50: 10000,
+ // ruta 60/61/62 all zero
+ ruta48: 0,
+ ruta49: 0,
+ }
+ const findings = runVatDeclarationChecks(rutor)
+ expect(findings.find((f) => f.code === 'IMPORT_BASE_WITHOUT_OUTPUT')?.status).toBe('ERROR')
+ })
+
+ // SKV §4.1.1.4 rule 6 — import output VAT without import base.
+ it('flags ERROR when import output VAT (ruta 60) without ruta 50', () => {
+ const rutor: VatDeclarationRutor = {
+ ...emptyRutor,
+ ruta60: 2500,
+ ruta48: 2500,
+ ruta49: 0,
+ }
+ const findings = runVatDeclarationChecks(rutor)
+ expect(findings.find((f) => f.code === 'IMPORT_OUTPUT_WITHOUT_BASE')?.status).toBe('ERROR')
+ })
+
+ it('does not flag import checks when both base and output VAT are present', () => {
+ const rutor: VatDeclarationRutor = {
+ ...emptyRutor,
+ ruta50: 10000,
+ ruta60: 2500,
+ ruta48: 2500,
+ ruta49: 0,
+ }
+ const findings = runVatDeclarationChecks(rutor)
+ expect(findings.find((f) => f.code === 'IMPORT_BASE_WITHOUT_OUTPUT')).toBeUndefined()
+ expect(findings.find((f) => f.code === 'IMPORT_OUTPUT_WITHOUT_BASE')).toBeUndefined()
+ })
+
+ // Multiple findings should surface together so the user sees the whole picture.
+ it('reports multiple distinct findings for a deeply broken declaration', () => {
+ const rutor: VatDeclarationRutor = {
+ ...emptyRutor,
+ ruta05: 10000, // taxable sales but no output VAT
+ ruta30: 2500, // RC output but no RC basis
+ ruta50: 5000, // import base but no import output
+ ruta48: 0,
+ ruta49: 2500,
+ }
+ const findings = runVatDeclarationChecks(rutor)
+ const codes = findings.map((f) => f.code).sort()
+ expect(codes).toContain('TAXABLE_SALES_WITHOUT_OUTPUT')
+ expect(codes).toContain('RC_BASIS_MISSING')
+ expect(codes).toContain('IMPORT_BASE_WITHOUT_OUTPUT')
+ })
})
diff --git a/lib/reports/vat-declaration-checks.ts b/lib/reports/vat-declaration-checks.ts
index b0829d6d..afb4b8fd 100644
--- a/lib/reports/vat-declaration-checks.ts
+++ b/lib/reports/vat-declaration-checks.ts
@@ -43,6 +43,10 @@ export interface VatDeclarationCheck {
| 'RC_OUTPUT_MISSING'
| 'RC_INPUT_VAT_MISMATCH'
| 'SUMMA_MOMS_DRIFT'
+ | 'TAXABLE_SALES_WITHOUT_OUTPUT'
+ | 'IMPORT_BASE_WITHOUT_OUTPUT'
+ | 'IMPORT_OUTPUT_WITHOUT_BASE'
+ | 'OUTPUT_VAT_WITHOUT_SALES_BASE'
status: VatDeclarationCheckStatus
/** Swedish user-facing message; safe to render directly in the UI. */
message: string
@@ -116,6 +120,71 @@ export function runVatDeclarationChecks(rutor: VatDeclarationRutor): VatDeclarat
})
}
+ // SKV §4.1.1.4 rule 1 — taxable sales base requires output VAT.
+ // If user has booked revenue (3001-3003, uttag, VMB, frivillig uthyrning)
+ // without any output VAT (2611-2638), the declaration will be rejected.
+ // Common cause: revenue posted but VAT line forgotten, or revenue on a
+ // zero-rated account that should have been ruta 35/36/39/40.
+ const taxableSalesBase = rutor.ruta05 + rutor.ruta06 + rutor.ruta07 + rutor.ruta08
+ const taxableSalesOutput = rutor.ruta10 + rutor.ruta11 + rutor.ruta12
+ if (taxableSalesBase > eps && taxableSalesOutput <= eps) {
+ findings.push({
+ code: 'TAXABLE_SALES_WITHOUT_OUTPUT',
+ status: 'ERROR',
+ message:
+ 'Du har redovisat momspliktig försäljning (ruta 05-08) men ingen ' +
+ 'utgående moms (ruta 10-12). Skatteverket kräver att momspliktig ' +
+ 'försäljning kombineras med utgående moms. Kontrollera att VAT-rader ' +
+ 'är bokförda på 2611/2621/2631 — eller flytta intäkterna till rätt ' +
+ 'momsfri ruta (35/36/39/40) om de inte är momspliktiga.',
+ rutor: ['ruta05', 'ruta06', 'ruta07', 'ruta08', 'ruta10', 'ruta11', 'ruta12'],
+ })
+ }
+
+ // Mirror — output VAT without taxable sales base. Output VAT booked
+ // standalone (e.g. manual correction without matching revenue posting)
+ // would also fail SKV's contract.
+ if (taxableSalesOutput > eps && taxableSalesBase <= eps) {
+ findings.push({
+ code: 'OUTPUT_VAT_WITHOUT_SALES_BASE',
+ status: 'ERROR',
+ message:
+ 'Du har redovisat utgående moms (ruta 10-12) men ingen momspliktig ' +
+ 'försäljning (ruta 05-08). Skatteverket kräver att utgående moms ' +
+ 'matchas med ett försäljningsunderlag. Kontrollera att intäktskonton ' +
+ '(3001/3002/3003) är bokförda för varje VAT-rad.',
+ rutor: ['ruta05', 'ruta06', 'ruta07', 'ruta08', 'ruta10', 'ruta11', 'ruta12'],
+ })
+ }
+
+ // SKV §4.1.1.4 rule 5 — import base requires import output VAT.
+ const importOutput = rutor.ruta60 + rutor.ruta61 + rutor.ruta62
+ if (rutor.ruta50 > eps && importOutput <= eps) {
+ findings.push({
+ code: 'IMPORT_BASE_WITHOUT_OUTPUT',
+ status: 'ERROR',
+ message:
+ 'Du har redovisat importunderlag (ruta 50) men ingen utgående ' +
+ 'importmoms (ruta 60-62). Skatteverket kräver båda. Kontrollera ' +
+ 'att importmoms är bokförd på 2615/2625/2635.',
+ rutor: ['ruta50', 'ruta60', 'ruta61', 'ruta62'],
+ })
+ }
+
+ // SKV §4.1.1.4 rule 6 — import output VAT requires import base.
+ // This was the canary that the Phase 1b ruta50 wiring fixed.
+ if (importOutput > eps && rutor.ruta50 <= eps) {
+ findings.push({
+ code: 'IMPORT_OUTPUT_WITHOUT_BASE',
+ status: 'ERROR',
+ message:
+ 'Du har redovisat utgående importmoms (ruta 60-62) men inget ' +
+ 'importunderlag (ruta 50). Skatteverket kräver att importmoms ' +
+ 'kombineras med tullvärdesunderlag på 4545/4546/4547.',
+ rutor: ['ruta50', 'ruta60', 'ruta61', 'ruta62'],
+ })
+ }
+
// SummaMoms drift — sanity check that our local ruta49 matches what the
// mapper will send. If this fires, the calculator and mapper disagree
// and we'd hit SKV's FK009.
diff --git a/lib/reports/vat-declaration.ts b/lib/reports/vat-declaration.ts
index 12a90a1e..c9a17a65 100644
--- a/lib/reports/vat-declaration.ts
+++ b/lib/reports/vat-declaration.ts
@@ -47,7 +47,7 @@ import type {
* 4425/4426/4427 (domestic services reverse charge) → ruta 24
* 4545/4546/4547 (import) → ruta 50
*/
-const ACCOUNT_RUTA: Record = {
+export const ACCOUNT_RUTA: Record = {
// Output VAT 25% → ruta 10
'2610': { box: 'ruta10', side: 'credit' }, // Utgående moms 25% (summary/parent)
'2611': { box: 'ruta10', side: 'credit' }, // Försäljning inom Sverige
diff --git a/lib/vat/__tests__/moms-box-mapping.test.ts b/lib/vat/__tests__/moms-box-mapping.test.ts
new file mode 100644
index 00000000..db7472b1
--- /dev/null
+++ b/lib/vat/__tests__/moms-box-mapping.test.ts
@@ -0,0 +1,155 @@
+import { describe, it, expect } from 'vitest'
+import {
+ ACCOUNT_TO_BOX,
+ BOX_LABELS,
+ getBoxForAccount,
+ getBoxLabel,
+ type MomsBox,
+} from '../moms-box-mapping'
+import { ACCOUNT_RUTA } from '@/lib/reports/vat-declaration'
+
+describe('ACCOUNT_TO_BOX', () => {
+ it('has a label for every box ID used in the map', () => {
+ const usedBoxes = new Set(Object.values(ACCOUNT_TO_BOX))
+ for (const box of usedBoxes) {
+ expect(BOX_LABELS[box]).toBeTruthy()
+ }
+ })
+
+ it('maps all known revenue accounts to a sales box', () => {
+ expect(ACCOUNT_TO_BOX['3001']).toBe('05')
+ expect(ACCOUNT_TO_BOX['3002']).toBe('05')
+ expect(ACCOUNT_TO_BOX['3003']).toBe('05')
+ expect(ACCOUNT_TO_BOX['3108']).toBe('35')
+ expect(ACCOUNT_TO_BOX['3308']).toBe('39')
+ expect(ACCOUNT_TO_BOX['3105']).toBe('36')
+ expect(ACCOUNT_TO_BOX['3305']).toBe('40')
+ })
+
+ it('maps all output VAT accounts including parent/summary and vilande', () => {
+ expect(ACCOUNT_TO_BOX['2610']).toBe('10')
+ expect(ACCOUNT_TO_BOX['2611']).toBe('10')
+ expect(ACCOUNT_TO_BOX['2618']).toBe('10')
+ expect(ACCOUNT_TO_BOX['2620']).toBe('11')
+ expect(ACCOUNT_TO_BOX['2630']).toBe('12')
+ expect(ACCOUNT_TO_BOX['2614']).toBe('30')
+ expect(ACCOUNT_TO_BOX['2624']).toBe('31')
+ expect(ACCOUNT_TO_BOX['2634']).toBe('32')
+ })
+
+ it('maps all input VAT accounts including parent and domestic RC', () => {
+ expect(ACCOUNT_TO_BOX['2640']).toBe('48')
+ expect(ACCOUNT_TO_BOX['2641']).toBe('48')
+ expect(ACCOUNT_TO_BOX['2645']).toBe('48')
+ expect(ACCOUNT_TO_BOX['2647']).toBe('48')
+ expect(ACCOUNT_TO_BOX['2649']).toBe('48')
+ })
+
+ it('maps reverse-charge basis accounts to the correct ruta', () => {
+ // EU goods → ruta 20
+ expect(ACCOUNT_TO_BOX['4515']).toBe('20')
+ expect(ACCOUNT_TO_BOX['4516']).toBe('20')
+ expect(ACCOUNT_TO_BOX['4517']).toBe('20')
+ // EU services → ruta 21
+ expect(ACCOUNT_TO_BOX['4535']).toBe('21')
+ expect(ACCOUNT_TO_BOX['4536']).toBe('21')
+ expect(ACCOUNT_TO_BOX['4537']).toBe('21')
+ // Non-EU services → ruta 22
+ expect(ACCOUNT_TO_BOX['4531']).toBe('22')
+ expect(ACCOUNT_TO_BOX['4532']).toBe('22')
+ expect(ACCOUNT_TO_BOX['4533']).toBe('22')
+ // Domestic goods RC → ruta 23
+ expect(ACCOUNT_TO_BOX['4415']).toBe('23')
+ expect(ACCOUNT_TO_BOX['4416']).toBe('23')
+ expect(ACCOUNT_TO_BOX['4417']).toBe('23')
+ // Domestic services RC → ruta 24
+ expect(ACCOUNT_TO_BOX['4425']).toBe('24')
+ expect(ACCOUNT_TO_BOX['4426']).toBe('24')
+ expect(ACCOUNT_TO_BOX['4427']).toBe('24')
+ })
+
+ it('maps import beskattningsunderlag accounts to ruta 50', () => {
+ expect(ACCOUNT_TO_BOX['4545']).toBe('50')
+ expect(ACCOUNT_TO_BOX['4546']).toBe('50')
+ expect(ACCOUNT_TO_BOX['4547']).toBe('50')
+ })
+
+ it('maps import output VAT accounts to ruta 60/61/62', () => {
+ expect(ACCOUNT_TO_BOX['2615']).toBe('60')
+ expect(ACCOUNT_TO_BOX['2625']).toBe('61')
+ expect(ACCOUNT_TO_BOX['2635']).toBe('62')
+ })
+
+ it('maps momspliktiga uttag accounts to ruta 06', () => {
+ expect(ACCOUNT_TO_BOX['3401']).toBe('06')
+ expect(ACCOUNT_TO_BOX['3402']).toBe('06')
+ expect(ACCOUNT_TO_BOX['3403']).toBe('06')
+ })
+})
+
+describe('getBoxForAccount', () => {
+ it('returns the box for known accounts', () => {
+ expect(getBoxForAccount('2611')).toBe('10')
+ expect(getBoxForAccount('4535')).toBe('21')
+ })
+
+ it('returns undefined for unknown accounts', () => {
+ expect(getBoxForAccount('9999')).toBeUndefined()
+ expect(getBoxForAccount('1930')).toBeUndefined() // bank account, not VAT-related
+ })
+})
+
+describe('getBoxLabel', () => {
+ it('returns Swedish labels for every box', () => {
+ expect(getBoxLabel('10')).toMatch(/Utgående moms 25%/)
+ expect(getBoxLabel('30')).toMatch(/inköp 25%/)
+ expect(getBoxLabel('48')).toMatch(/Ingående moms/)
+ expect(getBoxLabel('49')).toMatch(/Moms att betala/)
+ })
+})
+
+// Regression guard: ACCOUNT_TO_BOX must stay aligned with the source-of-truth
+// mapping in vat-declaration.ts. If a new account is added to one map without
+// the other, the calculation and the cross-validation labels drift apart.
+describe('ACCOUNT_TO_BOX ↔ ACCOUNT_RUTA alignment', () => {
+ const RUTA_TO_BOX: Record = {
+ ruta05: '05', ruta06: '06', ruta07: '07', ruta08: '08',
+ ruta10: '10', ruta11: '11', ruta12: '12',
+ ruta20: '20', ruta21: '21', ruta22: '22', ruta23: '23', ruta24: '24',
+ ruta30: '30', ruta31: '31', ruta32: '32',
+ ruta35: '35', ruta36: '36', ruta37: '37', ruta38: '38',
+ ruta39: '39', ruta40: '40', ruta41: '41', ruta42: '42',
+ ruta48: '48', ruta49: '49',
+ ruta50: '50', ruta60: '60', ruta61: '61', ruta62: '62',
+ }
+
+ it('every account in ACCOUNT_RUTA exists in ACCOUNT_TO_BOX with the matching box', () => {
+ const drift: string[] = []
+ for (const [account, mapping] of Object.entries(ACCOUNT_RUTA)) {
+ const expectedBox = RUTA_TO_BOX[mapping.box]
+ const actualBox = ACCOUNT_TO_BOX[account]
+ if (!actualBox) {
+ drift.push(`missing in ACCOUNT_TO_BOX: ${account} (should be box ${expectedBox})`)
+ } else if (actualBox !== expectedBox) {
+ drift.push(`mismatched box for ${account}: ACCOUNT_TO_BOX=${actualBox}, ACCOUNT_RUTA=${expectedBox}`)
+ }
+ }
+ expect(drift).toEqual([])
+ })
+
+ it('every account in ACCOUNT_TO_BOX exists in ACCOUNT_RUTA (or is an extra cross-validation hint)', () => {
+ // Allowed extras: accounts in ACCOUNT_TO_BOX that don't feed the declaration
+ // but are useful for the Export VAT Monitor / EU Sales List. Currently these
+ // are the frakter accounts that follow goods treatment.
+ const allowedExtras = new Set(['3521', '3522', '3109'])
+
+ const drift: string[] = []
+ for (const account of Object.keys(ACCOUNT_TO_BOX)) {
+ if (allowedExtras.has(account)) continue
+ if (!ACCOUNT_RUTA[account]) {
+ drift.push(`extra in ACCOUNT_TO_BOX: ${account} (not in ACCOUNT_RUTA — consider adding to declaration mapping or to allowedExtras)`)
+ }
+ }
+ expect(drift).toEqual([])
+ })
+})
diff --git a/lib/vat/moms-box-mapping.ts b/lib/vat/moms-box-mapping.ts
index c031d8cd..5d092b19 100644
--- a/lib/vat/moms-box-mapping.ts
+++ b/lib/vat/moms-box-mapping.ts
@@ -42,13 +42,28 @@ export type MomsBox =
| '61' // Importmoms 12%
| '62' // Importmoms 6%
-/** Map BAS account to momsdeklaration box */
+/**
+ * Map BAS account to momsdeklaration box.
+ *
+ * Source of truth for "which moms box does this BAS account contribute to?"
+ * Used for cross-validation (Export VAT Monitor, EU Sales List) and any
+ * UI that needs to label a journal line by its declaration ruta.
+ *
+ * Must stay aligned with `ACCOUNT_RUTA` in `lib/reports/vat-declaration.ts`
+ * — a regression test asserts that every account mapped here points at the
+ * matching ruta and vice versa.
+ */
export const ACCOUNT_TO_BOX: Record = {
// Domestic revenue (taxable) → Box 05
'3001': '05', // Försäljning varor/tjänster 25%
'3002': '05', // Försäljning varor/tjänster 12%
'3003': '05', // Försäljning varor/tjänster 6%
+ // Momspliktiga uttag → Box 06
+ '3401': '06',
+ '3402': '06',
+ '3403': '06',
+
// EU goods (reverse charge, VAT-free) → Box 35
'3108': '35', // Försäljning varor till annat EU-land
'3521': '35', // Fakturerade frakter EU (follows goods treatment)
@@ -69,22 +84,31 @@ export const ACCOUNT_TO_BOX: Record = {
// VAT-exempt sales → Box 42
'3004': '42', // Momsfri försäljning (AB)
'3100': '42', // Momsfria intäkter (EF)
+ '3404': '42', // Momsfria uttag
+ '3980': '42', // Erhållna offentliga stöd m.m.
+ '3994': '42', // Övriga rörelseintäkter momsfria
// Output VAT 25% → Box 10
+ '2610': '10', // Utgående moms 25% (summary/parent)
'2611': '10', // Försäljning inom Sverige
'2612': '10', // Egna uttag
'2613': '10', // Uthyrning (frivillig skattskyldighet)
'2616': '10', // Vinstmarginalbeskattning
+ '2618': '10', // Vilande utgående moms 25%
// Output VAT 12% → Box 11
+ '2620': '11', // Utgående moms 12% (summary/parent)
'2621': '11',
'2622': '11', // Egna uttag
'2623': '11', // Uthyrning
'2626': '11', // VMB
+ '2628': '11', // Vilande utgående moms 12%
// Output VAT 6% → Box 12
+ '2630': '12', // Utgående moms 6% (summary/parent)
'2631': '12',
'2632': '12', // Egna uttag
'2633': '12', // Uthyrning
'2636': '12', // VMB
+ '2638': '12', // Vilande utgående moms 6%
// Reverse charge output VAT → Boxes 30, 31, 32
'2614': '30',
@@ -97,12 +121,35 @@ export const ACCOUNT_TO_BOX: Record = {
'2635': '62', // Import 6%
// Input VAT → Box 48
+ '2640': '48', // Ingående moms (summary/parent)
'2641': '48', // Debiterad ingående moms
'2642': '48', // Frivillig skattskyldighet
'2645': '48', // Beräknad ingående moms (EU/non-EU förvärv)
'2646': '48', // Uthyrning
'2647': '48', // Omvänd skattskyldighet i Sverige
'2649': '48', // Blandad verksamhet
+
+ // Reverse-charge purchase bases (debit on cost accounts) → Boxes 20-24
+ '4515': '20', // Inköp varor EU 25%
+ '4516': '20', // Inköp varor EU 12%
+ '4517': '20', // Inköp varor EU 6%
+ '4535': '21', // Inköp tjänster EU 25% (huvudregeln)
+ '4536': '21', // Inköp tjänster EU 12%
+ '4537': '21', // Inköp tjänster EU 6%
+ '4531': '22', // Inköp tjänster utanför EU 25%
+ '4532': '22', // Inköp tjänster utanför EU 12%
+ '4533': '22', // Inköp tjänster utanför EU 6%
+ '4415': '23', // Inköp varor SE omvänd skattskyldighet 25%
+ '4416': '23', // Inköp varor SE omvänd skattskyldighet 12%
+ '4417': '23', // Inköp varor SE omvänd skattskyldighet 6%
+ '4425': '24', // Inköp tjänster SE omvänd skattskyldighet 25%
+ '4426': '24', // Inköp tjänster SE omvänd skattskyldighet 12%
+ '4427': '24', // Inköp tjänster SE omvänd skattskyldighet 6%
+
+ // Import beskattningsunderlag → Box 50
+ '4545': '50', // Import 25%
+ '4546': '50', // Import 12%
+ '4547': '50', // Import 6%
}
/** Swedish labels for each momsdeklaration box */
|