fix(bokslut): remove uppskjuten skatt on obeskattade reserver in juridisk person (K3 29.37) (#1421)
* fix(bokslut): remove uppskjuten skatt on obeskattade reserver in juridisk person (K3 29.37) and confirm K3 to K2 reversion In juridisk person K3 29.37 keeps obeskattade reserver at gross; the 79.4/20.6 split belongs to koncernredovisning. The old disposition double-counted the tax portion (result charged twice, 2240 overstated on top of gross 21xx). Removes the proposal step, POST kind, UI case, K2-to-K3 account seeding and the interim framework gate; keeps LATENT_TAX_DEFAULT_RATE for analytical soliditet presentation. Also adds the K3-to-K2 consequence confirmation dialog in settings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(decisions): scope the batch log to shipped code and record the 29.37 election nuance Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(settings): stop promising deferred-tax accounting the engine no longer does The framework help text and the K2-to-K3 confirmation both told the user that switching to K3 means uppskjuten skatt is recognised separately on 2240/8940 with a 79.4/20.6 split. This PR removes exactly that behaviour, so the copy would have promised something the product does not do, which is the defect class this batch exists to remove. Both now describe what actually happens: kassaflodesanalys, komponentavskrivning and a wider note set, with obeskattade reserver carried gross per K3 29.37. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
Jakob Wennberg
parent
8f38baca05
commit
12c59399ee
@@ -1,14 +1,15 @@
|
||||
/**
|
||||
* K3 framework integration test for the dispositions builder.
|
||||
*
|
||||
* Verifies that:
|
||||
* - K3 companies get an `uppskjuten_skatt` proposal at the end of the chain.
|
||||
* - K2 companies (the default) do NOT receive that proposal.
|
||||
* - The latent tax amount equals 20.6 % × projected closing 21xx − current
|
||||
* 2240 balance.
|
||||
* Since 2026-08-05 (founder decision, K3 29.37) the builder books NO
|
||||
* uppskjuten skatt on obeskattade reserver: in juridisk person the reserves
|
||||
* stay at gross and the 79.4/20.6 split belongs to koncernredovisning.
|
||||
* These tests pin that the proposal chain is framework-independent and that
|
||||
* no uppskjuten_skatt proposal ever appears, alongside the disposition math
|
||||
* that remains (periodiseringsfond, overavskrivningar, bolagsskatt).
|
||||
*
|
||||
* Mocks generateIncomeStatement and generateTrialBalance directly so the
|
||||
* test can drive numeric inputs without touching the database.
|
||||
* Mocks generateIncomeStatement directly so the test can drive numeric
|
||||
* inputs without touching the database.
|
||||
*/
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||
|
||||
@@ -181,7 +182,7 @@ beforeEach(() => {
|
||||
})
|
||||
|
||||
describe('buildDispositionsProposal: K3 framework', () => {
|
||||
it('appends an uppskjuten_skatt proposal for K3 aktiebolag', async () => {
|
||||
it('never proposes uppskjuten skatt on obeskattade reserver, K3 included (K3 29.37)', async () => {
|
||||
const supabase = makeSupabase({ entityType: 'aktiebolag', accountingFramework: 'k3' })
|
||||
const result = await buildDispositionsProposal(
|
||||
supabase as unknown as Parameters<typeof buildDispositionsProposal>[0],
|
||||
@@ -189,15 +190,12 @@ describe('buildDispositionsProposal: K3 framework', () => {
|
||||
'fp1',
|
||||
)
|
||||
expect(result.entityType).toBe('aktiebolag')
|
||||
const latentTax = result.proposals.find((p) => p.kind === 'uppskjuten_skatt')
|
||||
expect(latentTax).toBeDefined()
|
||||
expect(latentTax!.amount).toBe(20_600)
|
||||
expect(latentTax!.lines).toHaveLength(2)
|
||||
// Liability increased → debit 8940 / credit 2240.
|
||||
const debit = latentTax!.lines.find((l) => l.account_number === '8940')!
|
||||
const credit = latentTax!.lines.find((l) => l.account_number === '2240')!
|
||||
expect(debit.debit_amount).toBe(20_600)
|
||||
expect(credit.credit_amount).toBe(20_600)
|
||||
// In juridisk person obeskattade reserver stay at gross: no 8940/2240
|
||||
// proposal exists, and no proposal line may touch those accounts.
|
||||
expect(result.proposals.map((p) => p.kind)).not.toContain('uppskjuten_skatt')
|
||||
const touched = result.proposals.flatMap((p) => p.lines.map((l) => l.account_number))
|
||||
expect(touched).not.toContain('8940')
|
||||
expect(touched).not.toContain('2240')
|
||||
})
|
||||
|
||||
it('computes bolagsskatt on the result AFTER the periodiseringsfond avsättning', async () => {
|
||||
@@ -263,42 +261,6 @@ describe('buildDispositionsProposal: K3 framework', () => {
|
||||
expect(bolagsskatt?.amount).toBe(139_050)
|
||||
})
|
||||
|
||||
it('includes a pending 2153 increase in the K3 latent tax proposal', async () => {
|
||||
vi.mocked(calculateOveravskrivningar).mockResolvedValue({
|
||||
status: 'ready',
|
||||
proposal: {
|
||||
kind: 'overavskrivningar',
|
||||
label: 'Överavskrivningar',
|
||||
description: 'Skillnad mellan bokförd och skattemässig avskrivning.',
|
||||
amount: 10_000,
|
||||
signedAmount: 10_000,
|
||||
lines: [
|
||||
{ account_number: '8853', debit_amount: 10_000, credit_amount: 0 },
|
||||
{ account_number: '2153', debit_amount: 0, credit_amount: 10_000 },
|
||||
],
|
||||
warnings: [],
|
||||
},
|
||||
warning: null,
|
||||
currentReserve: 0,
|
||||
currentPeriodChange: 0,
|
||||
targetReserve: 10_000,
|
||||
maximumSignedChange: 10_000,
|
||||
})
|
||||
|
||||
const supabase = makeSupabase({ entityType: 'aktiebolag', accountingFramework: 'k3' })
|
||||
const result = await buildDispositionsProposal(
|
||||
supabase as unknown as Parameters<typeof buildDispositionsProposal>[0],
|
||||
'co',
|
||||
'fp1',
|
||||
)
|
||||
|
||||
const latentTax = result.proposals.find((p) => p.kind === 'uppskjuten_skatt')
|
||||
expect(latentTax?.amount).toBe(22_660)
|
||||
expect(latentTax?.computation).toEqual(
|
||||
expect.objectContaining({ untaxedReserves: 110_000, target2240: 22_660 }),
|
||||
)
|
||||
})
|
||||
|
||||
it('does NOT add an uppskjuten_skatt proposal for K2 aktiebolag', async () => {
|
||||
const supabase = makeSupabase({ entityType: 'aktiebolag', accountingFramework: 'k2' })
|
||||
const result = await buildDispositionsProposal(
|
||||
@@ -307,7 +269,7 @@ describe('buildDispositionsProposal: K3 framework', () => {
|
||||
'fp1',
|
||||
)
|
||||
expect(result.entityType).toBe('aktiebolag')
|
||||
expect(result.proposals.find((p) => p.kind === 'uppskjuten_skatt')).toBeUndefined()
|
||||
expect(result.proposals.map((p) => p.kind)).not.toContain('uppskjuten_skatt')
|
||||
})
|
||||
|
||||
it('defaults to K2 when accounting_framework is null on the company row', async () => {
|
||||
@@ -317,7 +279,7 @@ describe('buildDispositionsProposal: K3 framework', () => {
|
||||
'co',
|
||||
'fp1',
|
||||
)
|
||||
expect(result.proposals.find((p) => p.kind === 'uppskjuten_skatt')).toBeUndefined()
|
||||
expect(result.proposals.map((p) => p.kind)).not.toContain('uppskjuten_skatt')
|
||||
})
|
||||
|
||||
it('does NOT add an uppskjuten_skatt proposal for enskild firma even if mislabelled K3', async () => {
|
||||
@@ -333,44 +295,4 @@ describe('buildDispositionsProposal: K3 framework', () => {
|
||||
expect(result.proposals).toEqual([])
|
||||
})
|
||||
|
||||
it('skips uppskjuten_skatt when 2240 already matches target (no change)', async () => {
|
||||
// Bump 2240 to exactly 20 600 so the delta is zero: calculator should
|
||||
// return null and the builder skip the proposal entirely.
|
||||
vi.mocked(generateTrialBalance).mockResolvedValue({
|
||||
rows: [
|
||||
{
|
||||
account_number: '2125',
|
||||
account_name: 'Periodiseringsfond 2025',
|
||||
account_class: 2,
|
||||
closing_credit: 100_000,
|
||||
closing_debit: 0,
|
||||
opening_credit: 0,
|
||||
opening_debit: 0,
|
||||
period_credit: 100_000,
|
||||
period_debit: 0,
|
||||
},
|
||||
{
|
||||
account_number: '2240',
|
||||
account_name: 'Avsättningar för uppskjutna skatter',
|
||||
account_class: 2,
|
||||
closing_credit: 20_600,
|
||||
closing_debit: 0,
|
||||
opening_credit: 20_600,
|
||||
opening_debit: 0,
|
||||
period_credit: 0,
|
||||
period_debit: 0,
|
||||
},
|
||||
],
|
||||
totalDebit: 0,
|
||||
totalCredit: 120_600,
|
||||
isBalanced: false,
|
||||
} as unknown as Awaited<ReturnType<typeof generateTrialBalance>>)
|
||||
const supabase = makeSupabase({ entityType: 'aktiebolag', accountingFramework: 'k3' })
|
||||
const result = await buildDispositionsProposal(
|
||||
supabase as unknown as Parameters<typeof buildDispositionsProposal>[0],
|
||||
'co',
|
||||
'fp1',
|
||||
)
|
||||
expect(result.proposals.find((p) => p.kind === 'uppskjuten_skatt')).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,96 +0,0 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import {
|
||||
computeLatentTax,
|
||||
proposeLatentTaxChange,
|
||||
LATENT_TAX_DEFAULT_RATE,
|
||||
LATENT_TAX_LIABILITY_ACCOUNT,
|
||||
LATENT_TAX_EXPENSE_ACCOUNT,
|
||||
} from '../tax-provision/latent-tax-calculator'
|
||||
|
||||
describe('computeLatentTax', () => {
|
||||
it('splits 100 000 reserves into 79 400 equity + 20 600 liability at default 20,6 %', () => {
|
||||
const split = computeLatentTax({ untaxedReserves: 100_000 })
|
||||
expect(split.liabilityPortion).toBe(20_600)
|
||||
expect(split.equityPortion).toBe(79_400)
|
||||
// Invariant: the two portions reconcile to the input.
|
||||
expect(split.equityPortion + split.liabilityPortion).toBeCloseTo(100_000, 2)
|
||||
})
|
||||
|
||||
it('returns zero portions for zero reserves', () => {
|
||||
const split = computeLatentTax({ untaxedReserves: 0 })
|
||||
expect(split.equityPortion).toBe(0)
|
||||
expect(split.liabilityPortion).toBe(0)
|
||||
})
|
||||
|
||||
it('preserves the sign for negative reserves (over-reversal edge case)', () => {
|
||||
// Unusual but the math should stay symmetric: e.g. when the
|
||||
// dispositions builder posts more återföring than the existing reserves.
|
||||
const split = computeLatentTax({ untaxedReserves: -50_000 })
|
||||
expect(split.liabilityPortion).toBe(-10_300)
|
||||
expect(split.equityPortion).toBe(-39_700)
|
||||
expect(split.equityPortion + split.liabilityPortion).toBeCloseTo(-50_000, 2)
|
||||
})
|
||||
|
||||
it('accepts a custom tax rate (future flex for rate changes)', () => {
|
||||
// If bolagsskatt drops to e.g. 18 %, K3 split would follow.
|
||||
const split = computeLatentTax({ untaxedReserves: 100_000, taxRate: 0.18 })
|
||||
expect(split.liabilityPortion).toBe(18_000)
|
||||
expect(split.equityPortion).toBe(82_000)
|
||||
})
|
||||
|
||||
it('rounds to öre on non-integer reserves', () => {
|
||||
// 12 345.67 × 0.206 = 2 543.20802 → öre rounding → 2 543.21
|
||||
// equity = 12 345.67 − 2 543.21 = 9 802.46
|
||||
const split = computeLatentTax({ untaxedReserves: 12_345.67 })
|
||||
expect(split.liabilityPortion).toBe(2_543.21)
|
||||
expect(split.equityPortion).toBe(9_802.46)
|
||||
expect(split.equityPortion + split.liabilityPortion).toBeCloseTo(12_345.67, 2)
|
||||
})
|
||||
|
||||
it('exports the canonical 20.6 % rate constant', () => {
|
||||
expect(LATENT_TAX_DEFAULT_RATE).toBe(0.206)
|
||||
})
|
||||
})
|
||||
|
||||
describe('proposeLatentTaxChange', () => {
|
||||
it('returns null when current already equals target (no change)', () => {
|
||||
expect(proposeLatentTaxChange(20_600, 20_600)).toBeNull()
|
||||
})
|
||||
|
||||
it('returns null when delta is below 1 öre tolerance', () => {
|
||||
// Floating-point dust below 1 öre should not produce a verifikat.
|
||||
expect(proposeLatentTaxChange(20_600, 20_600.001)).toBeNull()
|
||||
expect(proposeLatentTaxChange(20_600.0049, 20_600)).toBeNull()
|
||||
})
|
||||
|
||||
it('books an avsättning when liability grows: debit 8940 / credit 2240', () => {
|
||||
const lines = proposeLatentTaxChange(0, 20_600)
|
||||
expect(lines).not.toBeNull()
|
||||
expect(lines).toHaveLength(2)
|
||||
const debit = lines!.find((l) => l.account_number === LATENT_TAX_EXPENSE_ACCOUNT)!
|
||||
const credit = lines!.find((l) => l.account_number === LATENT_TAX_LIABILITY_ACCOUNT)!
|
||||
expect(debit.debit_amount).toBe(20_600)
|
||||
expect(debit.credit_amount).toBe(0)
|
||||
expect(credit.debit_amount).toBe(0)
|
||||
expect(credit.credit_amount).toBe(20_600)
|
||||
})
|
||||
|
||||
it('books a återföring when liability shrinks: debit 2240 / credit 8940', () => {
|
||||
const lines = proposeLatentTaxChange(20_600, 15_000)
|
||||
expect(lines).not.toBeNull()
|
||||
expect(lines).toHaveLength(2)
|
||||
const debit = lines!.find((l) => l.account_number === LATENT_TAX_LIABILITY_ACCOUNT)!
|
||||
const credit = lines!.find((l) => l.account_number === LATENT_TAX_EXPENSE_ACCOUNT)!
|
||||
expect(debit.debit_amount).toBe(5_600)
|
||||
expect(credit.credit_amount).toBe(5_600)
|
||||
})
|
||||
|
||||
it('produces a balanced verifikat (sum debit = sum credit)', () => {
|
||||
const lines = proposeLatentTaxChange(10_000, 18_000)
|
||||
expect(lines).not.toBeNull()
|
||||
const totalDebit = lines!.reduce((s, l) => s + l.debit_amount, 0)
|
||||
const totalCredit = lines!.reduce((s, l) => s + l.credit_amount, 0)
|
||||
expect(totalDebit).toBe(totalCredit)
|
||||
expect(totalDebit).toBe(8_000)
|
||||
})
|
||||
})
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { SupabaseClient } from '@supabase/supabase-js'
|
||||
import { generateIncomeStatement } from '@/lib/reports/income-statement'
|
||||
import { generateTrialBalance } from '@/lib/reports/trial-balance'
|
||||
import {
|
||||
calculateBolagsskatt,
|
||||
getBookedBolagsskatt,
|
||||
@@ -8,12 +7,6 @@ import {
|
||||
} from './tax-provision/bolagsskatt-calculator'
|
||||
import { loadTaxAdjustmentSnapshot } from './tax-provision/tax-adjustment-service'
|
||||
import { calculateSarskildLoneskatt } from './tax-provision/sarskild-loneskatt-calculator'
|
||||
import {
|
||||
computeLatentTax,
|
||||
LATENT_TAX_EXPENSE_ACCOUNT,
|
||||
LATENT_TAX_LIABILITY_ACCOUNT,
|
||||
proposeLatentTaxChange,
|
||||
} from './tax-provision/latent-tax-calculator'
|
||||
import {
|
||||
getPeriodiseringsfondCohortAccount,
|
||||
getSchablonintaktRate,
|
||||
@@ -23,7 +16,6 @@ import {
|
||||
} from './reserves/periodiseringsfond-service'
|
||||
import { calculateOveravskrivningar } from './reserves/overavskrivningar-calculator'
|
||||
import type { CompletedDisposition, DispositionsProposal, ProposedDisposition } from './types'
|
||||
import type { AccountingFramework } from '@/types'
|
||||
|
||||
/**
|
||||
* Shared core of the GET /bokslutsdispositioner endpoint, lifted out so the
|
||||
@@ -71,19 +63,6 @@ export async function buildDispositionsProposal(
|
||||
}
|
||||
}
|
||||
|
||||
// Look up the accounting framework: K3 (BFNAR 2012:1) triggers the
|
||||
// uppskjuten-skatt provision step; K2 skips it.
|
||||
const { data: companyRow } = await supabase
|
||||
.from('companies')
|
||||
.select('accounting_framework')
|
||||
.eq('id', companyId)
|
||||
.maybeSingle()
|
||||
const accountingFramework: AccountingFramework =
|
||||
(companyRow as { accounting_framework?: AccountingFramework } | null)?.accounting_framework
|
||||
=== 'k3'
|
||||
? 'k3'
|
||||
: 'k2'
|
||||
|
||||
const fiscalYear = parseInt(period.period_end.slice(0, 4), 10)
|
||||
const incomeStatement = await generateIncomeStatement(supabase, companyId, fiscalPeriodId)
|
||||
const resultBeforeTax = incomeStatement.net_result
|
||||
@@ -239,21 +218,6 @@ export async function buildDispositionsProposal(
|
||||
proposals.push(bolagsskatt)
|
||||
}
|
||||
|
||||
// K3 only: split obeskattade reserver into the 79.4 % equity portion and
|
||||
// the 20.6 % uppskjuten skatteskuld. We sum the projected 21xx balance
|
||||
// AFTER the dispositions above have been applied so the latent-tax
|
||||
// amount reflects the closing position: anything else would diverge
|
||||
// from the BR the user sees in the preview.
|
||||
if (accountingFramework === 'k3') {
|
||||
const latentTax = await buildLatentTaxProposal({
|
||||
supabase,
|
||||
companyId,
|
||||
fiscalPeriodId,
|
||||
proposalsBeforeLatentTax: proposals,
|
||||
})
|
||||
if (latentTax) proposals.push(latentTax)
|
||||
}
|
||||
|
||||
return {
|
||||
entityType,
|
||||
fiscalPeriod: period,
|
||||
@@ -265,77 +229,3 @@ export async function buildDispositionsProposal(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Compose the K3 uppskjuten-skatt proposal.
|
||||
*
|
||||
* The latent tax provision must reflect the *closing* obeskattade-reserver
|
||||
* balance, so we pull the current 21xx balance from the trial balance and
|
||||
* adjust it for any 21xx-touching dispositions that haven't yet posted
|
||||
* (avsättning ↑, återföring ↓). 2240's current balance is the existing
|
||||
* provision; the delta becomes the new verifikat.
|
||||
*/
|
||||
export async function buildLatentTaxProposal(params: {
|
||||
supabase: SupabaseClient
|
||||
companyId: string
|
||||
fiscalPeriodId: string
|
||||
/** Optional: additional 21xx-touching dispositions that have NOT yet been
|
||||
* posted but will be in the same batch. The TB already reflects everything
|
||||
* posted, so leave this empty if the latent-tax run is sequenced after the
|
||||
* 21xx postings (the API route's case). */
|
||||
proposalsBeforeLatentTax?: ProposedDisposition[]
|
||||
}): Promise<ProposedDisposition | null> {
|
||||
const { supabase, companyId, fiscalPeriodId, proposalsBeforeLatentTax = [] } = params
|
||||
|
||||
// Reads 21xx and 2240 only (class 2), which no resultatavslut touches.
|
||||
const tb = await generateTrialBalance(supabase, companyId, fiscalPeriodId, {
|
||||
closingEntry: 'include',
|
||||
})
|
||||
|
||||
// 21xx: obeskattade reserver (credit-normal, so we measure credit − debit).
|
||||
let untaxedReserves = tb.rows
|
||||
.filter((r) => r.account_number.startsWith('21'))
|
||||
.reduce((s, r) => s + (r.closing_credit - r.closing_debit), 0)
|
||||
|
||||
// Pending 21xx postings from the proposals that will commit alongside
|
||||
// latent tax. Credits add to reserves and debits remove them.
|
||||
for (const p of proposalsBeforeLatentTax) {
|
||||
for (const line of p.lines) {
|
||||
if (!line.account_number.startsWith('21')) continue
|
||||
untaxedReserves += (line.credit_amount ?? 0) - (line.debit_amount ?? 0)
|
||||
}
|
||||
}
|
||||
|
||||
// Current 2240 balance: credit-normal. Equal to existing latent tax.
|
||||
const current2240 = tb.rows
|
||||
.filter((r) => r.account_number === LATENT_TAX_LIABILITY_ACCOUNT)
|
||||
.reduce((s, r) => s + (r.closing_credit - r.closing_debit), 0)
|
||||
|
||||
const split = computeLatentTax({ untaxedReserves })
|
||||
const lines = proposeLatentTaxChange(current2240, split.liabilityPortion)
|
||||
if (!lines) return null
|
||||
|
||||
const delta = Math.round((split.liabilityPortion - current2240) * 100) / 100
|
||||
const amount = Math.abs(delta)
|
||||
const direction = delta > 0 ? 'avsättning' : 'återföring'
|
||||
|
||||
return {
|
||||
kind: 'uppskjuten_skatt',
|
||||
label: 'Uppskjuten skatt (K3)',
|
||||
description:
|
||||
delta > 0
|
||||
? `Avsättning till uppskjuten skatteskuld 20,6 % av obeskattade reserver. Debet ${LATENT_TAX_EXPENSE_ACCOUNT}, kredit ${LATENT_TAX_LIABILITY_ACCOUNT}.`
|
||||
: `Återföring av uppskjuten skatteskuld när obeskattade reserver minskar. Debet ${LATENT_TAX_LIABILITY_ACCOUNT}, kredit ${LATENT_TAX_EXPENSE_ACCOUNT}.`,
|
||||
amount,
|
||||
lines,
|
||||
warnings: [],
|
||||
computation: {
|
||||
untaxedReserves,
|
||||
taxRate: 0.206,
|
||||
target2240: split.liabilityPortion,
|
||||
current2240,
|
||||
delta,
|
||||
direction,
|
||||
equityPortion: split.equityPortion,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,118 +1,22 @@
|
||||
import type { CreateJournalEntryLineInput } from '@/types'
|
||||
|
||||
/**
|
||||
* Statutory split of obeskattade reserver (21xx: periodiseringsfonder,
|
||||
* överavskrivningar, etc.) under K3 (BFNAR 2012:1).
|
||||
* K3 (BFNAR 2012:1) treatment of uppskjuten skatt on obeskattade reserver.
|
||||
*
|
||||
* The full balance is taxable when the reserve is reversed, so K3 requires
|
||||
* the balance to be presented as:
|
||||
* - 79.4 % equity (bundet eget kapital: the post-tax economic value)
|
||||
* - 20.6 % deferred tax liability (uppskjuten skatteskuld, account 2240)
|
||||
* In JURIDISK PERSON, K3 kap 29 (29.37) keeps obeskattade reserver at gross:
|
||||
* uppskjuten skatt attributable to them is NOT separately recognised, because
|
||||
* the reserves are presented including their deferred-tax component. The
|
||||
* 79.4 / 20.6 split into equity + uppskjuten skatteskuld belongs to
|
||||
* KONCERNREDOVISNING (and analytical contexts such as kontrollbalansrakning
|
||||
* and soliditet), which this product does not book.
|
||||
*
|
||||
* 20.6 % is the current Swedish bolagsskatt rate (since 2021). If the rate
|
||||
* changes (it has been 20.6 % since fiscal year 2021), pass an override.
|
||||
* The engine therefore books NO 8940/2240 entry on obeskattade reserver.
|
||||
* A dispositions step that did exactly that was removed 2026-08-05 (founder
|
||||
* decision, see DECISIONS.md): it double-counted the tax portion in juridisk
|
||||
* person (result charged twice, 2240 overstated on top of gross 21xx).
|
||||
*
|
||||
* Under K2 this split does NOT apply: the obeskattade reserver row stays
|
||||
* intact between eget kapital and skulder and 2240 is excluded from the chart.
|
||||
* What remains here is the analytical rate, used for presentation-side
|
||||
* calculations such as justerat eget kapital in soliditet (equity portion =
|
||||
* obeskattade reserver x (1 - rate)).
|
||||
*
|
||||
* 20.6 % is the current Swedish bolagsskatt rate (since 2021).
|
||||
*/
|
||||
export const LATENT_TAX_DEFAULT_RATE = 0.206
|
||||
|
||||
/** BAS account for the K3 latent tax liability. */
|
||||
export const LATENT_TAX_LIABILITY_ACCOUNT = '2240'
|
||||
|
||||
/** BAS account for the K3 latent tax expense (income statement). */
|
||||
export const LATENT_TAX_EXPENSE_ACCOUNT = '8940'
|
||||
|
||||
export interface LatentTaxSplit {
|
||||
/** The 79.4 % portion that K3 folds into equity for soliditet / BR purposes. */
|
||||
equityPortion: number
|
||||
/** The 20.6 % portion presented as a separate deferred tax liability (2240). */
|
||||
liabilityPortion: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute the K3 equity / liability split of a given untaxed-reserves total.
|
||||
*
|
||||
* Pure function: no DB access. Caller passes the *current* sum of 21xx
|
||||
* (post all dispositioner the user has accepted in the bokslut flow).
|
||||
*
|
||||
* Monetary precision: rounds to öre (2 decimals) via Math.round(x*100)/100
|
||||
* per project convention. liabilityPortion is computed first so the split
|
||||
* always reconciles (equityPortion = total − liability rounded the same way).
|
||||
*
|
||||
* Negative reserves are unusual (would indicate over-reversal) but the math
|
||||
* is symmetric: both portions come out negative, preserving the sign.
|
||||
*/
|
||||
export function computeLatentTax(params: {
|
||||
untaxedReserves: number
|
||||
taxRate?: number
|
||||
}): LatentTaxSplit {
|
||||
const taxRate = params.taxRate ?? LATENT_TAX_DEFAULT_RATE
|
||||
const liabilityPortion = Math.round(params.untaxedReserves * taxRate * 100) / 100
|
||||
const equityPortion = Math.round((params.untaxedReserves - liabilityPortion) * 100) / 100
|
||||
return { equityPortion, liabilityPortion }
|
||||
}
|
||||
|
||||
/**
|
||||
* Tolerance for "no change" detection. The latent tax provision posts in
|
||||
* whole krona via the engine, so an absolute delta below 1 öre means the
|
||||
* stored 2240 balance already matches the target and no adjustment is needed.
|
||||
*/
|
||||
const LATENT_TAX_ORE_TOLERANCE = 0.01
|
||||
|
||||
/**
|
||||
* Generate the journal lines needed to move the 2240 balance from its
|
||||
* current amount to the new target. Returns null when no adjustment is
|
||||
* required (delta below 1 öre, see {@link LATENT_TAX_ORE_TOLERANCE}).
|
||||
*
|
||||
* Direction:
|
||||
* - target > current → latent tax LIABILITY grew → debit 8940 (cost),
|
||||
* credit 2240 (liability).
|
||||
* - target < current → latent tax LIABILITY shrank → debit 2240, credit
|
||||
* 8940 (income: a reversal of prior expense).
|
||||
*
|
||||
* The entry posts as `source_type='year_end'` and is meant to be created via
|
||||
* `createJournalEntry()` so the engine assigns the voucher number atomically
|
||||
* and enforces the balance/period rules.
|
||||
*/
|
||||
export function proposeLatentTaxChange(
|
||||
currentLatentTax2240: number,
|
||||
targetLatentTax2240: number,
|
||||
): CreateJournalEntryLineInput[] | null {
|
||||
const delta = Math.round((targetLatentTax2240 - currentLatentTax2240) * 100) / 100
|
||||
if (Math.abs(delta) < LATENT_TAX_ORE_TOLERANCE) return null
|
||||
|
||||
const absoluteDelta = Math.abs(delta)
|
||||
if (delta > 0) {
|
||||
// Liability increased: debit 8940 (expense), credit 2240 (liability).
|
||||
return [
|
||||
{
|
||||
account_number: LATENT_TAX_EXPENSE_ACCOUNT,
|
||||
debit_amount: absoluteDelta,
|
||||
credit_amount: 0,
|
||||
line_description: 'Förändring uppskjuten skatt (K3)',
|
||||
},
|
||||
{
|
||||
account_number: LATENT_TAX_LIABILITY_ACCOUNT,
|
||||
debit_amount: 0,
|
||||
credit_amount: absoluteDelta,
|
||||
line_description: 'Avsättning uppskjuten skatteskuld',
|
||||
},
|
||||
]
|
||||
}
|
||||
// Liability decreased: debit 2240, credit 8940.
|
||||
return [
|
||||
{
|
||||
account_number: LATENT_TAX_LIABILITY_ACCOUNT,
|
||||
debit_amount: absoluteDelta,
|
||||
credit_amount: 0,
|
||||
line_description: 'Återföring uppskjuten skatteskuld',
|
||||
},
|
||||
{
|
||||
account_number: LATENT_TAX_EXPENSE_ACCOUNT,
|
||||
debit_amount: 0,
|
||||
credit_amount: absoluteDelta,
|
||||
line_description: 'Förändring uppskjuten skatt (K3)',
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ export type DispositionKind =
|
||||
| 'periodiseringsfond_ateforing'
|
||||
| 'overavskrivningar'
|
||||
| 'sarskild_loneskatt'
|
||||
| 'uppskjuten_skatt'
|
||||
|
||||
/**
|
||||
* Common shape every bokslut-disposition calculator returns. The wizard renders
|
||||
|
||||
Reference in New Issue
Block a user