fix(arsredovisning): truthful K3 policy note and GRANSKNINGSUTKAST watermark (#1423)
* fix(arsredovisning): truthful K3 policy note and GRANSKNINGSUTKAST watermark The K3 redovisningsprinciper note asserted policies the engine does not follow: full balansrakningsmetoden deferred tax and finance lease capitalization. The note now states actual practice: deferred tax only on obeskattade reserver at 20,6 percent, all leases expensed as operational. Every K3 report is classified AR-K3-DRAFT-ONLY, so the K3 PDF now carries a fixed rotated GRANSKNINGSUTKAST watermark on every page, and the arsredovisning studio shows an always visible AttnLine for K3 explaining that the document cannot be locked or filed via Accounted yet. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(arsredovisning): stop the two K3 deferred-tax notes contradicting each other The redovisningsprinciper note said deferred tax on obeskattade reserver is not separately recognised in juridisk person, while the "Uppskjutna skatter" note right after it disclosed exactly that split plus the 8940 movement. A signed arsredovisning cannot say both. Both notes are now driven by one derivation (deriveLatentTaxMovement in build-data.ts, computed before note 1 is built): with no 2240/8940 activity the policy paragraph states the gross treatment K3 29.37 permits and no movement note is emitted; with a 2240 balance the paragraph discloses the recognised liability and points at the movement note. Neither text claims a measurement rate or an origin the builder cannot verify, since a balance can come from the K3 bokslutsdisposition, a legacy posting or an SIE import booked at 22 or 21,4 percent. The leasing paragraph now states the basis for its blanket treatment: the document is always a single juridisk person with no koncernredovisning, so the K3 punkt 20.29 exemption applies. Without that qualifier the paragraph asserted a treatment that is not the general K3 rule. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(arsredovisning): never assert a K3 policy the books can contradict Three cases where the principles note still made claims it could not back: a read failure on the 2240/8940 figures printed the affirmative denial anyway, the leasing paragraph asserted a fact about the entity's group obligations that a Koncernforhallanden note in the same document can contradict, and the blanket operational-lease claim survived even when the balance sheet carries leased assets on 1260/1269. Deferred tax becomes a tri-state (none/recognized/unknown) and leasing is driven by the same trial balance. 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
12c59399ee
commit
ab712f829a
@@ -3,6 +3,7 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { useTranslations } from 'next-intl'
|
||||
import { AlertCircle, CheckCircle2, FileClock, Loader2, LockKeyhole, Save } from 'lucide-react'
|
||||
import { AttnLine } from '@/components/ui/attn-line'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
@@ -315,6 +316,7 @@ export function AnnualReportStudio({
|
||||
<Badge variant="warning">{t('blocker_count', { count: blockingIssues.length })}</Badge>
|
||||
)}
|
||||
</div>
|
||||
{framework === 'k3' && <AttnLine>{t('k3_draft_notice')}</AttnLine>}
|
||||
<div className="grid gap-2 sm:grid-cols-4">
|
||||
{[
|
||||
[
|
||||
|
||||
@@ -29,12 +29,12 @@
|
||||
{
|
||||
"number": 1,
|
||||
"title": "Redovisnings- och värderingsprinciper",
|
||||
"body": "Årsredovisningen är upprättad i enlighet med Årsredovisningslagen (1995:1554) och Bokföringsnämndens allmänna råd BFNAR 2012:1 Årsredovisning och koncernredovisning (K3).\n\nVärderingsprinciper: Tillgångar och skulder värderas till anskaffningsvärde om inget annat anges. Materiella anläggningstillgångar redovisas till anskaffningsvärde med avdrag för ackumulerade avskrivningar och eventuella nedskrivningar. Avskrivning sker linjärt över tillgångens bedömda nyttjandeperiod.\n\nUppskjuten skatt: Uppskjuten skatt redovisas enligt balansräkningsmetoden för temporära skillnader mellan redovisade och skattemässiga värden på tillgångar och skulder. Uppskjuten skatt värderas till nominellt belopp utan diskontering och beräknas utifrån den skattesats som är beslutad på balansdagen.\n\nIntäktsredovisning: Intäkter redovisas till det verkliga värdet av det som erhållits eller kommer att erhållas och redovisas när väsentliga risker och förmåner har överförts till köparen, beloppet kan mätas tillförlitligt och det är sannolikt att de ekonomiska fördelarna tillfaller företaget.\n\nLeasing: Leasingavtal klassificeras som finansiell eller operationell leasing. Operationella leasingavgifter redovisas linjärt i resultaträkningen under leasingperioden. Finansiella leasingavtal redovisas som anläggningstillgång med motsvarande skuld i balansräkningen.\n\nFinansiella instrument: Finansiella instrument redovisas initialt till anskaffningsvärde inklusive transaktionskostnader. Kundfordringar värderas till det belopp som beräknas inflyta. Övriga finansiella tillgångar och skulder redovisas till upplupet anskaffningsvärde."
|
||||
"body": "Årsredovisningen är upprättad i enlighet med Årsredovisningslagen (1995:1554) och Bokföringsnämndens allmänna råd BFNAR 2012:1 Årsredovisning och koncernredovisning (K3).\n\nVärderingsprinciper: Tillgångar och skulder värderas till anskaffningsvärde om inget annat anges. Materiella anläggningstillgångar redovisas till anskaffningsvärde med avdrag för ackumulerade avskrivningar och eventuella nedskrivningar. Avskrivning sker linjärt över tillgångens bedömda nyttjandeperiod.\n\nUppskjuten skatt: Uppskjuten skatteskuld hänförlig till obeskattade reserver redovisas på konto 2240. Ingående saldo, årets förändring och utgående saldo framgår av noten Uppskjutna skatter.\n\nIntäktsredovisning: Intäkter redovisas till det verkliga värdet av det som erhållits eller kommer att erhållas och redovisas när väsentliga risker och förmåner har överförts till köparen, beloppet kan mätas tillförlitligt och det är sannolikt att de ekonomiska fördelarna tillfaller företaget.\n\nLeasing: Samtliga leasingavtal redovisas som operationella leasingavtal med stöd av undantaget i K3 punkt 20.29, som medger att även finansiella leasingavtal redovisas som operationella i juridisk person. Leasingavgifterna kostnadsförs linjärt i resultaträkningen över leasingperioden.\n\nFinansiella instrument: Finansiella instrument redovisas initialt till anskaffningsvärde inklusive transaktionskostnader. Kundfordringar värderas till det belopp som beräknas inflyta. Övriga finansiella tillgångar och skulder redovisas till upplupet anskaffningsvärde."
|
||||
},
|
||||
{
|
||||
"number": 2,
|
||||
"title": "Uppskjutna skatter",
|
||||
"body": "Uppskjuten skatteskuld avser i huvudsak temporära skillnader på obeskattade reserver (periodiseringsfonder och överavskrivningar), beräknad med skattesatsen 20,6 %.\n\nIngående saldo (2240): 50 000 kr\nÅrets förändring (8940): 20 600 kr\nUtgående saldo (2240): 70 600 kr"
|
||||
"body": "Posten avser uppskjuten skatteskuld hänförlig till obeskattade reserver, redovisad på konto 2240.\n\nIngående saldo (2240): 50 000 kr\nÅrets förändring (8940): 20 600 kr\nUtgående saldo (2240): 70 600 kr"
|
||||
},
|
||||
{
|
||||
"number": 3,
|
||||
|
||||
@@ -169,6 +169,40 @@ function makeMinimalK3Data(): ArsredovisningData {
|
||||
}
|
||||
}
|
||||
|
||||
interface WatermarkElement {
|
||||
type: unknown
|
||||
props: { children?: unknown; fixed?: boolean }
|
||||
}
|
||||
|
||||
function isReactElement(node: unknown): node is WatermarkElement {
|
||||
return (
|
||||
typeof node === 'object' &&
|
||||
node !== null &&
|
||||
'type' in node &&
|
||||
'props' in node
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Walks the unrendered element tree, expanding plain function components
|
||||
* (react-pdf primitives are strings like 'TEXT', so only local helpers such
|
||||
* as PageChrome get invoked; none of them use hooks), and collects every
|
||||
* Text element whose child is the GRANSKNINGSUTKAST watermark string.
|
||||
*/
|
||||
function findWatermarkNodes(node: unknown, found: WatermarkElement[]): void {
|
||||
if (Array.isArray(node)) {
|
||||
for (const child of node) findWatermarkNodes(child, found)
|
||||
return
|
||||
}
|
||||
if (!isReactElement(node)) return
|
||||
if (typeof node.type === 'function') {
|
||||
findWatermarkNodes((node.type as (props: unknown) => unknown)(node.props), found)
|
||||
return
|
||||
}
|
||||
if (node.props.children === 'GRANSKNINGSUTKAST') found.push(node)
|
||||
findWatermarkNodes(node.props.children, found)
|
||||
}
|
||||
|
||||
describe('ArsredovisningK3PDF', () => {
|
||||
it('renders without throwing against a minimal K3 fixture', async () => {
|
||||
const doc = ArsredovisningK3PDF({ data: makeMinimalK3Data() })
|
||||
@@ -219,6 +253,30 @@ describe('ArsredovisningK3PDF', () => {
|
||||
expect(buffer.length).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it('carries a fixed GRANSKNINGSUTKAST watermark in every page chrome', () => {
|
||||
const doc = ArsredovisningK3PDF({ data: makeMinimalK3Data() })
|
||||
const found: WatermarkElement[] = []
|
||||
findWatermarkNodes(doc, found)
|
||||
// 9 Page elements with the full fixture: cover, forvaltningsberattelse,
|
||||
// resultatrakning, balansrakning, kassaflodesanalys, equity changes,
|
||||
// noter, underskrifter, faststallelseintyg. One watermark per PageChrome;
|
||||
// the `fixed` prop repeats it on wrap-generated continuation pages.
|
||||
expect(found.length).toBe(9)
|
||||
for (const node of found) {
|
||||
expect(node.props.fixed).toBe(true)
|
||||
}
|
||||
})
|
||||
|
||||
it('does NOT watermark the K2 template', () => {
|
||||
const data = makeMinimalK3Data()
|
||||
data.accounting_framework = 'k2'
|
||||
delete data.kassaflodesanalys
|
||||
delete data.equity_changes_statement
|
||||
const found: WatermarkElement[] = []
|
||||
findWatermarkNodes(ArsredovisningPDF({ data }), found)
|
||||
expect(found.length).toBe(0)
|
||||
})
|
||||
|
||||
it('renders with multiple signatures', async () => {
|
||||
const data = makeMinimalK3Data()
|
||||
data.signatures = [
|
||||
|
||||
@@ -333,6 +333,86 @@ describe('buildArsredovisningData: K3', () => {
|
||||
expect(uppskjuten!.body).toMatch(/Utgående saldo.*70/)
|
||||
})
|
||||
|
||||
it('makes the principles note acknowledge the 2240 balance the movement note discloses', async () => {
|
||||
// The planted trial balance carries a legacy 2240/8940 pair, so the
|
||||
// document contains BOTH notes. They must tell one story: the policy
|
||||
// paragraph may not deny a separately recognised deferred tax on
|
||||
// obeskattade reserver while the next note discloses exactly that.
|
||||
const supabase = makeSupabase({ accountingFramework: 'k3' })
|
||||
// @ts-expect-error: chainable mock isn't fully typed as SupabaseClient
|
||||
const data = await buildArsredovisningData(supabase, 'co1', 'fp1')
|
||||
const principles = data.noter.find((n) => n.title.startsWith('Redovisnings'))!
|
||||
const uppskjuten = data.noter.find((n) => n.title === 'Uppskjutna skatter')!
|
||||
expect(uppskjuten).toBeDefined()
|
||||
expect(principles.body).toContain('konto 2240')
|
||||
expect(principles.body).toContain('Uppskjutna skatter')
|
||||
expect(principles.body).not.toContain('särredovisas inte')
|
||||
})
|
||||
|
||||
it('denies the split in the principles note when no 2240/8940 activity exists', async () => {
|
||||
// What the engine produces today: no deferred tax is booked, so there is
|
||||
// no movement note and the policy paragraph states the gross treatment.
|
||||
mockedTrialBalance.mockResolvedValue({
|
||||
rows: [],
|
||||
totalDebit: 0,
|
||||
totalCredit: 0,
|
||||
isBalanced: true,
|
||||
})
|
||||
const supabase = makeSupabase({ accountingFramework: 'k3' })
|
||||
// @ts-expect-error: chainable mock isn't fully typed as SupabaseClient
|
||||
const data = await buildArsredovisningData(supabase, 'co1', 'fp1')
|
||||
expect(data.noter.find((n) => n.title === 'Uppskjutna skatter')).toBeUndefined()
|
||||
const principles = data.noter.find((n) => n.title.startsWith('Redovisnings'))!
|
||||
expect(principles.body).toContain(
|
||||
'Uppskjuten skatt hänförlig till obeskattade reserver särredovisas inte i juridisk person',
|
||||
)
|
||||
expect(principles.body).not.toMatch(/2240/)
|
||||
})
|
||||
|
||||
it('keeps the pair consistent when the provision is fully reversed to a zero closing balance', async () => {
|
||||
// Opening 50 000, reversed in full: the movement note is still emitted,
|
||||
// so the principles paragraph must stay on the "recognised" branch and
|
||||
// must not assert a closing balance that no longer exists.
|
||||
mockedTrialBalance.mockResolvedValue({
|
||||
rows: [
|
||||
{
|
||||
account_number: '2240',
|
||||
account_name: 'Uppskjuten skatteskuld',
|
||||
account_class: 2,
|
||||
opening_debit: 0,
|
||||
opening_credit: 50_000,
|
||||
period_debit: 50_000,
|
||||
period_credit: 0,
|
||||
closing_debit: 0,
|
||||
closing_credit: 0,
|
||||
},
|
||||
{
|
||||
account_number: '8940',
|
||||
account_name: 'Uppskjuten skatt',
|
||||
account_class: 8,
|
||||
opening_debit: 0,
|
||||
opening_credit: 0,
|
||||
period_debit: 0,
|
||||
period_credit: 50_000,
|
||||
closing_debit: 0,
|
||||
closing_credit: 50_000,
|
||||
},
|
||||
],
|
||||
totalDebit: 50_000,
|
||||
totalCredit: 50_000,
|
||||
isBalanced: true,
|
||||
})
|
||||
const supabase = makeSupabase({ accountingFramework: 'k3' })
|
||||
// @ts-expect-error: chainable mock isn't fully typed as SupabaseClient
|
||||
const data = await buildArsredovisningData(supabase, 'co1', 'fp1')
|
||||
const uppskjuten = data.noter.find((n) => n.title === 'Uppskjutna skatter')!
|
||||
expect(uppskjuten).toBeDefined()
|
||||
expect(uppskjuten.body).toMatch(/Utgående saldo \(2240\): 0 kr/)
|
||||
const principles = data.noter.find((n) => n.title.startsWith('Redovisnings'))!
|
||||
expect(principles.body).not.toContain('särredovisas inte')
|
||||
expect(principles.body).not.toContain('i balansräkningen')
|
||||
})
|
||||
|
||||
it('emits an Eventualförpliktelser note', async () => {
|
||||
const supabase = makeSupabase({ accountingFramework: 'k3' })
|
||||
// @ts-expect-error: chainable mock isn't fully typed as SupabaseClient
|
||||
|
||||
@@ -7,9 +7,25 @@ import {
|
||||
buildUppskjutenSkattNot,
|
||||
} from '../k3-noter-builder'
|
||||
|
||||
/** Default: no komponentavskrivning, no legacy 2240 balance. */
|
||||
function principles(
|
||||
overrides: Partial<{
|
||||
hasComponents: boolean
|
||||
deferredTax: 'none' | 'recognized' | 'unknown'
|
||||
hasCapitalizedLease: boolean
|
||||
}> = {},
|
||||
) {
|
||||
return buildK3RedovisningsPrinciper({
|
||||
hasComponents: false,
|
||||
deferredTax: 'none',
|
||||
hasCapitalizedLease: false,
|
||||
...overrides,
|
||||
})
|
||||
}
|
||||
|
||||
describe('buildK3RedovisningsPrinciper', () => {
|
||||
it('always includes the K3 framework citation and the standard policy paragraphs', () => {
|
||||
const note = buildK3RedovisningsPrinciper(false)
|
||||
const note = principles()
|
||||
expect(note.number).toBe(1)
|
||||
expect(note.title).toBe('Redovisnings- och värderingsprinciper')
|
||||
expect(note.body).toContain('BFNAR 2012:1')
|
||||
@@ -19,18 +35,121 @@ describe('buildK3RedovisningsPrinciper', () => {
|
||||
expect(note.body).toContain('Finansiella instrument')
|
||||
})
|
||||
|
||||
it('describes uppskjuten skatt per K3 29.37: not separately recognised in juridisk person', () => {
|
||||
const note = principles()
|
||||
// The engine books NO deferred tax on obeskattade reserver (K3 29.37:
|
||||
// gross in juridisk person; the split belongs to koncernredovisning).
|
||||
// The note states that policy and must not claim balansrakningsmetoden
|
||||
// or any recognised uppskjuten skatteskuld on the reserves.
|
||||
expect(note.body).toContain('särredovisas inte i juridisk person')
|
||||
expect(note.body).toContain('inklusive uppskjuten skatteskuld')
|
||||
expect(note.body).not.toMatch(/balansräkningsmetoden/)
|
||||
expect(note.body).not.toMatch(/temporära skillnader mellan redovisade och skattemässiga värden/)
|
||||
// With no 2240 balance the note must not reference one.
|
||||
expect(note.body).not.toMatch(/2240/)
|
||||
})
|
||||
|
||||
it('discloses the recognised 2240 liability instead when the books carry one', () => {
|
||||
const note = principles({ deferredTax: 'recognized' })
|
||||
expect(note.body).toContain('konto 2240')
|
||||
expect(note.body).toContain('Uppskjutna skatter')
|
||||
// The gross-reserve claim is the OTHER branch: keeping it here would
|
||||
// deny the liability disclosed two notes down.
|
||||
expect(note.body).not.toContain('särredovisas inte')
|
||||
// The builder cannot know how an imported or legacy provision was
|
||||
// measured, so the paragraph asserts no rate and no origin.
|
||||
expect(note.body).not.toMatch(/20,6/)
|
||||
expect(note.body).not.toMatch(/beräknad/)
|
||||
})
|
||||
|
||||
it('describes leasing as expensed operational leases under the K3 20.29 juridisk-person exemption', () => {
|
||||
const note = principles()
|
||||
expect(note.body).toContain('operationella leasingavtal')
|
||||
expect(note.body).toContain('kostnadsförs linjärt')
|
||||
// The blanket operational treatment is only available to a juridisk
|
||||
// person (K3 punkt 20.29); it is not the general K3 rule, so the note
|
||||
// must state the basis it relies on.
|
||||
expect(note.body).toContain('juridisk person')
|
||||
expect(note.body).toContain('K3 punkt 20.29')
|
||||
// It must NOT assert anything about the entity's group obligations: the
|
||||
// same document emits a Koncernforhallanden note when a parent is set.
|
||||
expect(note.body).not.toMatch(/koncernredovisning upprättas inte/)
|
||||
// No code capitalizes leases, so the note must not assert that finance
|
||||
// leases are recognized as assets with a corresponding liability.
|
||||
expect(note.body).not.toMatch(/anläggningstillgång med motsvarande skuld/)
|
||||
})
|
||||
|
||||
it('drops the blanket operational claim when the books carry leased assets', () => {
|
||||
const note = principles({ hasCapitalizedLease: true })
|
||||
// A balansrakning with 1260/1269 contradicts "samtliga leasingavtal
|
||||
// redovisas som operationella", so that claim and the 20.29 basis for it
|
||||
// must both disappear; the paragraph describes what is booked instead.
|
||||
expect(note.body).not.toMatch(/Samtliga leasingavtal/)
|
||||
expect(note.body).not.toMatch(/20\.29/)
|
||||
expect(note.body).toContain('leasade tillgångar')
|
||||
expect(note.body).toContain('kostnadsförs linjärt')
|
||||
})
|
||||
|
||||
it('never prints an affirmative denial when the deferred-tax figures could not be read', () => {
|
||||
const note = principles({ deferredTax: 'unknown' })
|
||||
// The denial is a statement about the books; on the read-failure path we
|
||||
// have no books to state it from, so only the going-forward policy shows.
|
||||
expect(note.body).not.toMatch(/särredovisas inte/)
|
||||
expect(note.body).toContain('K3 punkt 29.37')
|
||||
expect(note.body).toContain('inklusive uppskjuten skatteskuld')
|
||||
})
|
||||
|
||||
it('OMITS the komponentavskrivning paragraph when no asset has components', () => {
|
||||
const note = buildK3RedovisningsPrinciper(false)
|
||||
const note = principles()
|
||||
expect(note.body).not.toMatch(/Komponentavskrivning/)
|
||||
})
|
||||
|
||||
it('INCLUDES the komponentavskrivning paragraph when an asset has components', () => {
|
||||
const note = buildK3RedovisningsPrinciper(true)
|
||||
const note = principles({ hasComponents: true })
|
||||
expect(note.body).toMatch(/Komponentavskrivning/)
|
||||
expect(note.body).toMatch(/betydande komponenter/)
|
||||
})
|
||||
})
|
||||
|
||||
describe('K3 noter: the two deferred-tax notes never contradict each other', () => {
|
||||
it('company WITHOUT a legacy 2240 balance: policy denies the split and no movement note exists', () => {
|
||||
// build-data omits buildUppskjutenSkattNot entirely when the trial
|
||||
// balance has no 2240/8940 activity, so the only deferred-tax statement
|
||||
// in the document is the policy paragraph.
|
||||
const note = principles({ deferredTax: 'none' })
|
||||
expect(note.body).toContain(
|
||||
'Uppskjuten skatt hänförlig till obeskattade reserver särredovisas inte i juridisk person',
|
||||
)
|
||||
expect(note.body).not.toMatch(/2240/)
|
||||
expect(note.body).not.toMatch(/8940/)
|
||||
})
|
||||
|
||||
it('company WITH a 2240 balance: policy paragraph and movement note tell one story', () => {
|
||||
const policy = principles({ deferredTax: 'recognized' })
|
||||
const movement = buildUppskjutenSkattNot({
|
||||
noteNumber: 4,
|
||||
latentTaxOpening: 50_000,
|
||||
latentTaxChange: 20_600,
|
||||
latentTaxClosing: 70_600,
|
||||
})
|
||||
// Both notes recognise the same liability on the same account.
|
||||
expect(policy.body).toContain('konto 2240')
|
||||
expect(movement.body).toContain('konto 2240')
|
||||
expect(policy.body).toContain('obeskattade reserver')
|
||||
expect(movement.body).toContain('obeskattade reserver')
|
||||
// The policy paragraph must NOT carry the denial while the movement note
|
||||
// discloses exactly that split: that is the contradiction this pins.
|
||||
expect(policy.body).not.toContain('särredovisas inte')
|
||||
expect(policy.body).not.toContain(
|
||||
'obeskattade reserver redovisas inklusive uppskjuten skatteskuld',
|
||||
)
|
||||
// Neither note asserts a measurement rate for a balance whose origin
|
||||
// (K3 disposition, legacy posting, SIE import) the builder cannot see.
|
||||
expect(policy.body).not.toMatch(/20,6/)
|
||||
expect(movement.body).not.toMatch(/20,6/)
|
||||
})
|
||||
})
|
||||
|
||||
describe('buildUppskjutenSkattNot', () => {
|
||||
it('renders opening + change + closing line in the body', () => {
|
||||
const note = buildUppskjutenSkattNot({
|
||||
@@ -72,14 +191,20 @@ describe('buildUppskjutenSkattNot', () => {
|
||||
expect(note.body).toMatch(/Utgående saldo.*60/)
|
||||
})
|
||||
|
||||
it('mentions the 20.6% latent tax rate so readers understand the figures', () => {
|
||||
it('reports the balance without claiming a rate it cannot verify', () => {
|
||||
const note = buildUppskjutenSkattNot({
|
||||
noteNumber: 1,
|
||||
latentTaxOpening: 0,
|
||||
latentTaxOpening: 50_000,
|
||||
latentTaxChange: 0,
|
||||
latentTaxClosing: 0,
|
||||
latentTaxClosing: 50_000,
|
||||
})
|
||||
expect(note.body).toMatch(/20,6/)
|
||||
expect(note.body).toContain('konto 2240')
|
||||
expect(note.body).toContain('obeskattade reserver')
|
||||
// A balance can come from the K3 disposition, a legacy posting or an SIE
|
||||
// import measured at 22 or 21,4 percent: asserting 20,6 percent would be
|
||||
// a claim about numbers this builder did not produce.
|
||||
expect(note.body).not.toMatch(/20,6/)
|
||||
expect(note.body).not.toMatch(/skattesats/)
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -140,6 +140,23 @@ const styles = StyleSheet.create({
|
||||
borderWidth: 0.5,
|
||||
borderColor: '#888',
|
||||
},
|
||||
// Draft watermark: completeness.ts classifies every K3 report as
|
||||
// AR-K3-DRAFT-ONLY (it can never be finalized), so the rendered PDF must
|
||||
// say so on every page, not just in a cover banner. Rendered with the
|
||||
// `fixed` prop inside PageChrome so it repeats on wrap-generated
|
||||
// continuation pages too.
|
||||
draftWatermark: {
|
||||
position: 'absolute',
|
||||
top: '45%',
|
||||
left: 0,
|
||||
right: 0,
|
||||
textAlign: 'center',
|
||||
fontSize: 48,
|
||||
fontFamily: 'Helvetica-Bold',
|
||||
color: '#000',
|
||||
opacity: 0.1,
|
||||
transform: 'rotate(-45deg)',
|
||||
},
|
||||
})
|
||||
|
||||
function fmt(amount: number): string {
|
||||
@@ -208,6 +225,9 @@ function PageChrome({
|
||||
<Text style={styles.pageFooter} fixed>
|
||||
{pageLabel ?? ''}
|
||||
</Text>
|
||||
<Text style={styles.draftWatermark} fixed>
|
||||
GRANSKNINGSUTKAST
|
||||
</Text>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -232,7 +252,7 @@ export function ArsredovisningK3PDF({ data }: { data: ArsredovisningData }) {
|
||||
<View>
|
||||
<Text style={styles.title}>Årsredovisning</Text>
|
||||
<Text style={styles.subtitle}>
|
||||
för räkenskapsåret {data.fiscal_period.period_start}: {data.fiscal_period.period_end}
|
||||
för räkenskapsåret {data.fiscal_period.period_start} till {data.fiscal_period.period_end}
|
||||
</Text>
|
||||
<Text style={styles.k3Banner}>Upprättad enligt K3 (BFNAR 2012:1)</Text>
|
||||
<Text style={styles.paragraph}>{data.company.name}</Text>
|
||||
|
||||
@@ -807,6 +807,66 @@ async function buildK2Noter(
|
||||
return { notes, warnings }
|
||||
}
|
||||
|
||||
interface LatentTaxMovement {
|
||||
/** Opening balance on 2240 (credit-normal, so positive = liability). */
|
||||
opening: number
|
||||
/** Year movement, taken from 8940 when the account was used. */
|
||||
change: number
|
||||
/** Closing balance on 2240. */
|
||||
closing: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Derive the uppskjuten-skatt movement from the current-period full trial
|
||||
* balance: 2240 (latent tax liability) and 8940 (latent tax expense).
|
||||
*
|
||||
* Returns `movement: null` when the company has no such activity at all,
|
||||
* which is the normal case: a 2240 balance only appears when the K3
|
||||
* uppskjuten-skatt disposition was posted, or from legacy postings and
|
||||
* imported history. Both the redovisningsprinciper paragraph and the
|
||||
* "Uppskjutna skatter" note are driven by this one result, so they can never
|
||||
* disagree about whether a deferred tax on obeskattade reserver is
|
||||
* separately recognized.
|
||||
*
|
||||
* `ok: false` means the figures could not be read at all; the caller warns
|
||||
* rather than blocking the document.
|
||||
*/
|
||||
function deriveLatentTaxMovement(
|
||||
tbFullRows: TrialBalanceRow[],
|
||||
): { ok: true; movement: LatentTaxMovement | null } | { ok: false } {
|
||||
try {
|
||||
const row2240 = tbFullRows.find((r) => r.account_number === '2240')
|
||||
const row8940 = tbFullRows.find((r) => r.account_number === '8940')
|
||||
// 2240 is credit-normal liability: opening = opening_credit - opening_debit
|
||||
const opening2240 = row2240
|
||||
? (row2240.opening_credit || 0) - (row2240.opening_debit || 0)
|
||||
: 0
|
||||
const closing2240 = row2240
|
||||
? (row2240.closing_credit || 0) - (row2240.closing_debit || 0)
|
||||
: 0
|
||||
// 8940 is an expense (debit-normal): movement = period_debit - period_credit
|
||||
// A positive movement = additional avsättning (cost incurred = liability
|
||||
// grew). The 2240 balance moves by the same magnitude (with opposite
|
||||
// sign convention since 2240 is on the credit side).
|
||||
const change8940 = row8940
|
||||
? (row8940.period_debit || 0) - (row8940.period_credit || 0)
|
||||
: closing2240 - opening2240
|
||||
if (opening2240 === 0 && closing2240 === 0 && change8940 === 0) {
|
||||
return { ok: true, movement: null }
|
||||
}
|
||||
return {
|
||||
ok: true,
|
||||
movement: {
|
||||
opening: opening2240,
|
||||
change: change8940,
|
||||
closing: closing2240,
|
||||
},
|
||||
}
|
||||
} catch {
|
||||
return { ok: false }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the K3 note set (BFNAR 2012:1). Differs from K2 in:
|
||||
* - Verbose redovisningsprinciper covering all K3 measurement principles
|
||||
@@ -918,7 +978,39 @@ async function buildK3Noter(
|
||||
})
|
||||
const adaptedAssets = assets.map(adaptAsset)
|
||||
const hasComponents = anyAssetHasComponents(adaptedAssets)
|
||||
notes.push(buildK3RedovisningsPrinciper(hasComponents))
|
||||
// The deferred-tax figures are derived BEFORE note 1 is built, even though
|
||||
// the "Uppskjutna skatter" note is emitted further down as note 4: the
|
||||
// redovisningsprinciper paragraph has to describe the same reality that
|
||||
// note discloses. One derivation feeds both, so no code path can produce a
|
||||
// policy paragraph denying a split the following note then discloses.
|
||||
const latentTax = deriveLatentTaxMovement(tbFullRows)
|
||||
if (!latentTax.ok) {
|
||||
warnings.push(
|
||||
'Uppskjutna skatter-noten kunde inte beräknas automatiskt. Kontrollera kontot 2240 och kör om bokslutet.',
|
||||
)
|
||||
}
|
||||
const latentTaxMovement = latentTax.ok ? latentTax.movement : null
|
||||
// Leased assets on the balance sheet (1260/1269) contradict the blanket
|
||||
// "all leases are operational" simplification, so the same trial balance
|
||||
// that decides the deferred-tax wording also decides the leasing wording.
|
||||
const hasCapitalizedLease = tbFullRows.some(
|
||||
(r) =>
|
||||
(r.account_number === '1260' || r.account_number === '1269')
|
||||
&& ((r.closing_debit || 0) !== 0 || (r.closing_credit || 0) !== 0),
|
||||
)
|
||||
notes.push(
|
||||
buildK3RedovisningsPrinciper({
|
||||
hasComponents,
|
||||
// A read failure may not print an affirmative denial: it degrades to
|
||||
// the going-forward policy, and the warning above tells the user.
|
||||
deferredTax: !latentTax.ok
|
||||
? 'unknown'
|
||||
: latentTaxMovement !== null
|
||||
? 'recognized'
|
||||
: 'none',
|
||||
hasCapitalizedLease,
|
||||
}),
|
||||
)
|
||||
|
||||
// 2. Aktiekapital (shared with K2 logic: K3 punkt 18.x mandates the same
|
||||
// disclosure for AB).
|
||||
@@ -992,42 +1084,18 @@ async function buildK3Noter(
|
||||
}
|
||||
|
||||
// 4. Uppskjutna skatter. K3 ch.29 requires disclosure of opening,
|
||||
// movement, and closing balance of uppskjuten skatteskuld. We derive
|
||||
// these from the current-period full trial balance (passed in by the
|
||||
// caller, which already fetched it for the statements) for 2240 (latent
|
||||
// tax liability) and 8940 (latent tax expense).
|
||||
try {
|
||||
const rows = tbFullRows
|
||||
const row2240 = rows.find((r) => r.account_number === '2240')
|
||||
const row8940 = rows.find((r) => r.account_number === '8940')
|
||||
// 2240 is credit-normal liability: opening = opening_credit - opening_debit
|
||||
const opening2240 = row2240
|
||||
? (row2240.opening_credit || 0) - (row2240.opening_debit || 0)
|
||||
: 0
|
||||
const closing2240 = row2240
|
||||
? (row2240.closing_credit || 0) - (row2240.closing_debit || 0)
|
||||
: 0
|
||||
// 8940 is an expense (debit-normal): movement = period_debit - period_credit
|
||||
// A positive movement = additional avsättning (cost incurred = liability
|
||||
// grew). The 2240 balance moves by the same magnitude (with opposite
|
||||
// sign convention since 2240 is on the credit side).
|
||||
const change8940 = row8940
|
||||
? (row8940.period_debit || 0) - (row8940.period_credit || 0)
|
||||
: closing2240 - opening2240
|
||||
if (opening2240 !== 0 || closing2240 !== 0 || change8940 !== 0) {
|
||||
notes.push(
|
||||
buildUppskjutenSkattNot({
|
||||
noteNumber: notes.length + 1,
|
||||
latentTaxOpening: opening2240,
|
||||
latentTaxChange: change8940,
|
||||
latentTaxClosing: closing2240,
|
||||
}),
|
||||
)
|
||||
}
|
||||
} catch {
|
||||
// Trial-balance failure should not block the document; flag as warning.
|
||||
warnings.push(
|
||||
'Uppskjutna skatter-noten kunde inte beräknas automatiskt. Kontrollera kontot 2240 och kör om bokslutet.',
|
||||
// movement, and closing balance of uppskjuten skatteskuld. The figures
|
||||
// were derived above (deriveLatentTaxMovement) so that note 1 and this
|
||||
// note tell the same story; a null movement means no 2240/8940 activity
|
||||
// exists and the note is omitted.
|
||||
if (latentTaxMovement) {
|
||||
notes.push(
|
||||
buildUppskjutenSkattNot({
|
||||
noteNumber: notes.length + 1,
|
||||
latentTaxOpening: latentTaxMovement.opening,
|
||||
latentTaxChange: latentTaxMovement.change,
|
||||
latentTaxClosing: latentTaxMovement.closing,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -36,10 +36,24 @@ import type {
|
||||
* paragraph. K3 ch.17.4 makes component depreciation mandatory when the
|
||||
* components have meaningfully different useful lives; otherwise the
|
||||
* paragraph would be misleading and is omitted.
|
||||
* @param deferredTax: what the trial balance says about 2240/8940.
|
||||
* 'recognized' is exactly when buildUppskjutenSkattNot is emitted; the two
|
||||
* notes MUST be driven by the same signal, since a policy paragraph denying
|
||||
* any separately recognized deferred tax cannot sit in the same document as
|
||||
* a note disclosing that very balance. 'unknown' is the read-failure path:
|
||||
* an affirmative denial is itself a claim, so figures we could not read may
|
||||
* not produce one. The caller derives this once (build-data.ts).
|
||||
* @param hasCapitalizedLease: true when the balance sheet carries leased
|
||||
* assets (1260/1269). The blanket operational treatment is then contradicted
|
||||
* by the company's own balansrakning, so the paragraph describes what is
|
||||
* booked instead of asserting the 20.29 simplification.
|
||||
*/
|
||||
export function buildK3RedovisningsPrinciper(
|
||||
hasComponents: boolean,
|
||||
): NoteEntry {
|
||||
export function buildK3RedovisningsPrinciper(params: {
|
||||
hasComponents: boolean
|
||||
deferredTax: 'none' | 'recognized' | 'unknown'
|
||||
hasCapitalizedLease: boolean
|
||||
}): NoteEntry {
|
||||
const { hasComponents, deferredTax, hasCapitalizedLease } = params
|
||||
const paragraphs: string[] = [
|
||||
'Årsredovisningen är upprättad i enlighet med Årsredovisningslagen (1995:1554) och Bokföringsnämndens allmänna råd BFNAR 2012:1 Årsredovisning och koncernredovisning (K3).',
|
||||
'Värderingsprinciper: Tillgångar och skulder värderas till anskaffningsvärde om inget annat anges. Materiella anläggningstillgångar redovisas till anskaffningsvärde med avdrag för ackumulerade avskrivningar och eventuella nedskrivningar. Avskrivning sker linjärt över tillgångens bedömda nyttjandeperiod.',
|
||||
@@ -49,10 +63,49 @@ export function buildK3RedovisningsPrinciper(
|
||||
'Komponentavskrivning: Materiella anläggningstillgångar med betydande komponenter som har väsentligt olika nyttjandeperioder delas upp och varje komponent skrivs av separat. Anskaffningsvärdet fördelas på komponenterna baserat på relativ andel av tillgångens värde.',
|
||||
)
|
||||
}
|
||||
// The uppskjuten skatt and leasing paragraphs below describe what the
|
||||
// BOOKS show, never a policy the document cannot back up. No code
|
||||
// capitalizes leases, so every lease is expensed as operational under the
|
||||
// K3 20.29 exemption available to a juridisk person. Do not restore
|
||||
// broader policy claims (balansrakningsmetoden over all temporary
|
||||
// differences, finance-lease capitalization) unless the engine actually
|
||||
// implements them: a signed AR must not assert policies the books do not
|
||||
// follow.
|
||||
//
|
||||
// Deferred tax has three cases, decided by the same signal that decides
|
||||
// whether buildUppskjutenSkattNot is emitted:
|
||||
// - 'none': no 2240/8940 activity. The reserves are carried gross, which
|
||||
// is what K3 29.37 permits in juridisk person, and the paragraph says so.
|
||||
// - 'recognized': a 2240 balance exists (legacy postings or imported
|
||||
// history). Denying the split would contradict both the balansrakning
|
||||
// and the "Uppskjutna skatter" note, so the paragraph discloses the
|
||||
// recognized liability and points at that note.
|
||||
// - 'unknown': the figures could not be read. A denial is an affirmative
|
||||
// statement about the books, so it may not be printed on data we do not
|
||||
// have; the paragraph states only the policy applied going forward. The
|
||||
// caller has already raised a warning on this path.
|
||||
// No branch claims HOW the balance was measured or where it came from: an
|
||||
// imported provision may have been booked at 22 or 21.4 percent by another
|
||||
// system, and this builder cannot tell the cases apart.
|
||||
const deferredTaxParagraph =
|
||||
deferredTax === 'recognized'
|
||||
? 'Uppskjuten skatt: Uppskjuten skatteskuld hänförlig till obeskattade reserver redovisas på konto 2240. Ingående saldo, årets förändring och utgående saldo framgår av noten Uppskjutna skatter.'
|
||||
: deferredTax === 'unknown'
|
||||
? 'Uppskjuten skatt: Obeskattade reserver redovisas inklusive uppskjuten skatteskuld i enlighet med K3 punkt 29.37, som medger att de redovisas brutto i juridisk person.'
|
||||
: 'Uppskjuten skatt: Uppskjuten skatt hänförlig till obeskattade reserver särredovisas inte i juridisk person, utan obeskattade reserver redovisas inklusive uppskjuten skatteskuld.'
|
||||
// Leasing follows the same rule as deferred tax: describe the books, do not
|
||||
// assert a simplification the balansrakning contradicts. The 20.29 wording
|
||||
// states the basis for the treatment without claiming anything about the
|
||||
// entity's group obligations, which this builder cannot know (build-data
|
||||
// emits a Koncernforhallanden note whenever a parent company is declared).
|
||||
// Source: .claude/skills/swedish-asset-accounting/references/leasing-and-disposal.md:32.
|
||||
const leasingParagraph = hasCapitalizedLease
|
||||
? 'Leasing: Leasingavgifter för operationella leasingavtal kostnadsförs linjärt i resultaträkningen över leasingperioden. Balansräkningen innehåller leasade tillgångar som redovisas som anläggningstillgång med tillhörande avskrivningar.'
|
||||
: 'Leasing: Samtliga leasingavtal redovisas som operationella leasingavtal med stöd av undantaget i K3 punkt 20.29, som medger att även finansiella leasingavtal redovisas som operationella i juridisk person. Leasingavgifterna kostnadsförs linjärt i resultaträkningen över leasingperioden.'
|
||||
paragraphs.push(
|
||||
'Uppskjuten skatt: Uppskjuten skatt redovisas enligt balansräkningsmetoden för temporära skillnader mellan redovisade och skattemässiga värden på tillgångar och skulder. Uppskjuten skatt värderas till nominellt belopp utan diskontering och beräknas utifrån den skattesats som är beslutad på balansdagen.',
|
||||
deferredTaxParagraph,
|
||||
'Intäktsredovisning: Intäkter redovisas till det verkliga värdet av det som erhållits eller kommer att erhållas och redovisas när väsentliga risker och förmåner har överförts till köparen, beloppet kan mätas tillförlitligt och det är sannolikt att de ekonomiska fördelarna tillfaller företaget.',
|
||||
'Leasing: Leasingavtal klassificeras som finansiell eller operationell leasing. Operationella leasingavgifter redovisas linjärt i resultaträkningen under leasingperioden. Finansiella leasingavtal redovisas som anläggningstillgång med motsvarande skuld i balansräkningen.',
|
||||
leasingParagraph,
|
||||
'Finansiella instrument: Finansiella instrument redovisas initialt till anskaffningsvärde inklusive transaktionskostnader. Kundfordringar värderas till det belopp som beräknas inflyta. Övriga finansiella tillgångar och skulder redovisas till upplupet anskaffningsvärde.',
|
||||
)
|
||||
return {
|
||||
@@ -73,6 +126,17 @@ export function buildK3RedovisningsPrinciper(
|
||||
*
|
||||
* The closing must equal opening + change. Caller passes raw figures; the
|
||||
* note formats them with thousand-separators and the appropriate sign.
|
||||
*
|
||||
* Emitted only when such a balance actually exists (K3 bokslutsdisposition,
|
||||
* legacy postings or imported history). The body reports the figures and
|
||||
* does NOT claim they were measured at the current 20.6 percent rate: an
|
||||
* imported provision may have been booked at 21.4 or 22 percent by another
|
||||
* system, and the balance is disclosed here exactly as it stands.
|
||||
*
|
||||
* Whenever this note is emitted, buildK3RedovisningsPrinciper MUST be built
|
||||
* with hasRecognizedDeferredTax: true, or note 1 would deny the very split
|
||||
* this note discloses. build-data.ts derives both from one call to
|
||||
* deriveLatentTaxMovement so the pair cannot drift.
|
||||
*/
|
||||
export function buildUppskjutenSkattNot(params: {
|
||||
noteNumber: number
|
||||
@@ -86,7 +150,7 @@ export function buildUppskjutenSkattNot(params: {
|
||||
const fmt = (n: number) =>
|
||||
Math.round(n).toLocaleString('sv-SE')
|
||||
const lines: string[] = [
|
||||
'Uppskjuten skatteskuld avser i huvudsak temporära skillnader på obeskattade reserver (periodiseringsfonder och överavskrivningar), beräknad med skattesatsen 20,6 %.',
|
||||
'Posten avser uppskjuten skatteskuld hänförlig till obeskattade reserver, redovisad på konto 2240.',
|
||||
'',
|
||||
`Ingående saldo (2240): ${fmt(latentTaxOpening)} kr`,
|
||||
`Årets förändring (8940): ${fmt(latentTaxChange)} kr`,
|
||||
|
||||
+2
-1
@@ -6417,7 +6417,8 @@
|
||||
"remove_signer": "Ta bort undertecknare",
|
||||
"signer_removed": "Undertecknaren har tagits bort",
|
||||
"signer_remove_error": "Kunde inte ta bort undertecknaren",
|
||||
"no_digital_version": "Ingen undertecknad version är godkänd för digital inlämning. Kontrollera versionens blockerare i årsredovisningsstudion eller använd pappersflödet."
|
||||
"no_digital_version": "Ingen undertecknad version är godkänd för digital inlämning. Kontrollera versionens blockerare i årsredovisningsstudion eller använd pappersflödet.",
|
||||
"k3_draft_notice": "K3-dokumentet är ännu ett granskningsutkast och kan inte låsas eller lämnas in via Accounted. Upprätta och lämna in årsredovisningen på papper tills hela upplysningsmatrisen är implementerad och granskad."
|
||||
},
|
||||
"empty": {
|
||||
"invoices_title": "No invoices yet",
|
||||
|
||||
+2
-1
@@ -6417,7 +6417,8 @@
|
||||
"remove_signer": "Ta bort undertecknare",
|
||||
"signer_removed": "Undertecknaren har tagits bort",
|
||||
"signer_remove_error": "Kunde inte ta bort undertecknaren",
|
||||
"no_digital_version": "Ingen undertecknad version är godkänd för digital inlämning. Kontrollera versionens blockerare i årsredovisningsstudion eller använd pappersflödet."
|
||||
"no_digital_version": "Ingen undertecknad version är godkänd för digital inlämning. Kontrollera versionens blockerare i årsredovisningsstudion eller använd pappersflödet.",
|
||||
"k3_draft_notice": "K3-dokumentet är ännu ett granskningsutkast och kan inte låsas eller lämnas in via Accounted. Upprätta och lämna in årsredovisningen på papper tills hela upplysningsmatrisen är implementerad och granskad."
|
||||
},
|
||||
"empty": {
|
||||
"invoices_title": "Inga fakturor ännu",
|
||||
|
||||
Reference in New Issue
Block a user