fix(copy): make K3 and year-end claims match what the code actually does (#1431)

* fix(copy): make K3, leasing and year-end claims match what the code does

Follow-up to the batch that removed the uppskjuten-skatt posting on
obeskattade reserver (K3 29.37 gross in juridisk person) and added the K2
asset-account gate. Six user-facing strings still described the old
behaviour or made claims the code cannot support.

1. Arsredovisning page: the K3 explainer promised an uppskjuten skatt-not
   and a materiella anlaggningstillgangar-not in every K3 document. Both
   are conditional (a 2240/8940 balance, assets in the register) and the
   first is now absent in the normal case. The kassaflodesanalys is
   dropped with a warning when it cannot be generated, so it is named
   only when the document actually carries one.

2. Regelverk settings: kassaflodesanalys was presented as following from
   K3. It follows from being ett storre foretag
   (swedish-year-end-closing/references/reporting-and-filing.md:10,
   legal-framework.md:42); the copy now says the product includes one and
   states the storre-foretag rule separately. Komponentavskrivning was
   presented as optional under K3; it is mandatory where component useful
   lives differ materially (k2-vs-k3.md:5, asset-accounting
   references/depreciation.md:33).

3. Note 1 and the Uppskjutna skatter-not no longer claim the 2240 balance
   is hanforlig till obeskattade reserver. deriveLatentTaxMovement reads
   the 2240/8940 balances only, and under K3 that account carries deferred
   tax on all temporary differences (k2-vs-k3.md:11-13).

4. The deferredTax 'unknown' branch emitted the gross-reserve statement,
   which is the denial phrased positively: the same affirmative claim
   about books that could not be read. It now emits no deferred-tax
   paragraph at all; build-data already warns on that path.

5. Capitalized-lease detection looked at 1260/1269 only. On the shipped
   BAS 2026 chart 1260 is a free inventarier account and 1269 is ack.
   avskrivningar pa datorer, so owned computers were reported as leased,
   while 1217/1227 (finansiellt leasade) were missed. Detection now reads
   the company's own account names in kontogrupp 12, which is where BAS
   keeps capitalized leases (leasing-and-disposal.md:28) and which owned
   inventarier on 1220 never matches. 1720 forutbetalda leasingavgifter
   stays out: that is the operational treatment.

6a. gnubok_year_end_readiness listed FX revaluation as a blocker (it is a
   warning) and omitted UNBOOKED_TRANSACTIONS, the common one. The
   description now names every actionable blocker kind, within the
   280-char budget, and a test pins it against YEAR_END_BLOCKER_KIND.

6b. companies.accounting_framework defaults to 'k2', so every enskild
   firma hit the K2 asset gate and was handed a BFNAR 2016:10 punkt 10.4
   citation plus a K3 remedy it cannot take: a sole trader prepares ett
   forenklat arsbokslut, not an arsredovisning (legal-framework.md:29,
   :48). entity_type now rides along on the companies read the routes
   already do, and non-AB entities get wording with no citation and no
   K3, keeping the 1090 remedy. The K1 counterpart of punkt 10.4 is not
   sourced in the repo skills, so nothing was invented in its place.

* fix(copy): close the review findings on the copy-truth sweep

Three follow-ups from the source and code reviews. (1) The K2/K3 help text had upgraded a vague sentence into a definite boundary claim ('gransen gar vid <trosklar>'), which excludes the other routes into mandatory K3 that are live right now for this control's audience: noterade vardepapper, and from fiscal years starting after 2025-12-31 also utlandsk filial, kryptotillgangar, aktierelaterade ersattningar and fastighetsbolag. An AB in one of those categories would have read the sentence and stayed on a regelverk it may no longer use. (2) hasCapitalizedLeaseAsset compared per-side cumulative totals, so a lease acquired earlier and disposed this year still claimed the balance sheet carries a leased asset; it now compares the net balance. (3) The K3 warning enumerated a kassaflodesanalys the document may not contain, contradicting the newly conditional page copy on the same screen.

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:
Jakob Wennberg
2026-08-06 11:54:57 +02:00
committed by GitHub
parent a5c10e441a
commit 5b0ca3d874
14 changed files with 518 additions and 84 deletions
@@ -530,10 +530,20 @@ export default function ArsredovisningPage() {
{data.accounting_framework === 'k3' && (
<div className="px-1 text-sm">
<p className="font-medium">Årsredovisning enligt K3 (BFNAR 2012:1)</p>
{/* Enumerate only what buildK3Noter and buildArsredovisningData
actually emit. The uppskjuten skatt-noten needs a 2240/8940
balance (which the engine no longer creates: K3 29.37 gross in
juridisk person), the anläggningsnoter need assets in the
register, and the kassaflödesanalys is dropped with a warning
when it cannot be generated. Promising any of them
unconditionally is false in the normal case. */}
<p className="mt-1 text-xs leading-5 text-muted-foreground">
Dokumentet innehåller kassaflödesanalys, förändring av eget kapital och
utökade noter (uppskjuten skatt, redovisningsprinciper, materiella
anläggningstillgångar): krav som följer K3 men inte K2.
{data.kassaflodesanalys
? 'Dokumentet följer K3-mallen: kassaflödesanalys, förändring av eget kapital som egen räkning och noter enligt K3.'
: 'Dokumentet följer K3-mallen: förändring av eget kapital som egen räkning och noter enligt K3. Kassaflödesanalysen kunde inte beräknas för perioden: se varningarna längre ner.'}{' '}
Vilka noter som kommer med styrs av bokföringen: noten Uppskjutna skatter tas
med först när konto 2240 eller 8940 har ett saldo, och anläggningsnoterna när
det finns tillgångar i anläggningsregistret.
</p>
</div>
)}
+5 -2
View File
@@ -160,7 +160,10 @@ export const PATCH = withRouteContext(
const [{ data: company }, existing] = await Promise.all([
supabase
.from('companies')
.select('accounting_framework')
// entity_type rides along on the same fetch: the rejection wording
// must not cite BFNAR 2016:10 at an enskild firma, which prepares no
// årsredovisning under K2. See lib/bokslut/assets/k2-account-guard.ts.
.select('accounting_framework, entity_type')
.eq('id', companyId)
.single(),
getAsset(supabase, companyId, id),
@@ -187,7 +190,7 @@ export const PATCH = withRouteContext(
(categoryDefaultsApply ? defaults.accumulated : existing.bas_accumulated_account),
])
if (excluded) {
const messages = k2ExcludedAccountMessages(excluded)
const messages = k2ExcludedAccountMessages(excluded, company?.entity_type)
return NextResponse.json(
{
error: {
+45 -4
View File
@@ -167,7 +167,7 @@ describe('POST /api/assets', () => {
])(
'rejects a K2 company explicitly overriding onto %s with 422',
async (assetAccount, accumulatedAccount) => {
enqueue({ data: { accounting_framework: 'k2' } })
enqueue({ data: { accounting_framework: 'k2', entity_type: 'aktiebolag' } })
const { status, body } = await parseJsonResponse<{
error: { code: string; message: string }
@@ -189,12 +189,52 @@ describe('POST /api/assets', () => {
expect(status).toBe(422)
expect(body.error.code).toBe('K2_EXCLUDED_ACCOUNT')
expect(body.error.message).toContain(assetAccount)
// All six are kontogrupp 10, so the punkt 10.4 citation applies.
// All six are kontogrupp 10 and the company is an AB preparing an
// årsredovisning, so the punkt 10.4 citation applies.
expect(body.error.message).toContain('BFNAR 2016:10 punkt 10.4')
expect(mockCreateAsset).not.toHaveBeenCalled()
},
)
// companies.accounting_framework is NOT NULL DEFAULT 'k2', so an enskild
// firma runs into the same gate. It prepares a förenklat årsbokslut, not an
// årsredovisning under BFNAR 2016:10, so the K2 citation would be a false
// legal claim and "use K3 instead" an impossible remedy. The block stands;
// the wording drops both and keeps the actionable 1090 remedy.
it('rejects an enskild firma on 1010 without citing BFNAR 2016:10 or K3', async () => {
enqueue({ data: { accounting_framework: 'k2', entity_type: 'enskild_firma' } })
const { status, body } = await parseJsonResponse<{
error: { code: string; message: string; message_en: string }
}>(
await POST(createMockRequest('/api/assets', {
method: 'POST',
body: {
name: 'Utvecklingsprojekt',
category: 'immaterial',
acquisition_date: '2025-01-01',
acquisition_cost: 50_000,
useful_life_months: 60,
bas_asset_account: '1010',
bas_accumulated_account: '1039',
},
}))
)
expect(status).toBe(422)
expect(body.error.code).toBe('K2_EXCLUDED_ACCOUNT')
expect(body.error.message).toContain('1010')
expect(body.error.message).not.toContain('BFNAR 2016:10')
expect(body.error.message).not.toContain('10.4')
expect(body.error.message).not.toContain('K3')
expect(body.error.message_en).not.toContain('BFNAR 2016:10')
expect(body.error.message_en).not.toContain('K3')
// Still actionable: the lawful account, and what the system did.
expect(body.error.message).toContain('1090')
expect(body.error.message).toContain('anläggningsregistret')
expect(mockCreateAsset).not.toHaveBeenCalled()
})
it('rejects a K2 company overriding the ACCUMULATED account onto 1019 with 422', async () => {
enqueue({ data: { accounting_framework: 'k2' } })
@@ -395,7 +435,7 @@ describe('PATCH /api/assets/[id]', () => {
})
it('rejects a K2 company patching the asset account onto 1010 with 422', async () => {
enqueue({ data: { accounting_framework: 'k2' } })
enqueue({ data: { accounting_framework: 'k2', entity_type: 'aktiebolag' } })
mockGetAsset.mockResolvedValue({
id: 'asset-1',
category: 'immaterial',
@@ -416,6 +456,7 @@ describe('PATCH /api/assets/[id]', () => {
expect(status).toBe(422)
expect(body.error.code).toBe('K2_EXCLUDED_ACCOUNT')
expect(body.error.message).toContain('1010')
// AB on the companies row, so the K2 citation is the right one here.
expect(body.error.message).toContain('BFNAR 2016:10 punkt 10.4')
expect(mockUpdateAsset).not.toHaveBeenCalled()
})
@@ -457,7 +498,7 @@ describe('PATCH /api/assets/[id]', () => {
// override outside the category range hits this gate before updateAsset()
// raises its range error. Those rejections must NOT claim punkt 10.4.
it('rejects a K2 company patching onto 1370 without citing the intangible rule', async () => {
enqueue({ data: { accounting_framework: 'k2' } })
enqueue({ data: { accounting_framework: 'k2', entity_type: 'aktiebolag' } })
mockGetAsset.mockResolvedValue({
id: 'asset-1',
category: 'immaterial',
+4 -3
View File
@@ -215,10 +215,11 @@ export const POST = withRouteContext(
// immaterial default is the acquired pair 1090/1099, which is lawful,
// so only a deliberate override can trip this). The guard supplies the
// message: the egenupparbetade group cites BFNAR 2016:10 punkt 10.4,
// other Ej K2 accounts do not.
// other Ej K2 accounts do not, and an enskild firma gets neither, since
// K2 is not its regelverk. entity_type rides along on the same fetch.
const { data: company } = await supabase
.from('companies')
.select('accounting_framework')
.select('accounting_framework, entity_type')
.eq('id', companyId)
.single()
const isK3Company = company?.accounting_framework === 'k3'
@@ -247,7 +248,7 @@ export const POST = withRouteContext(
validation.data.bas_accumulated_account ?? defaults.accumulated,
])
if (excluded) {
const messages = k2ExcludedAccountMessages(excluded)
const messages = k2ExcludedAccountMessages(excluded, company?.entity_type)
return NextResponse.json(
{
error: {
@@ -34,8 +34,16 @@ interface AccountingFrameworkFormProps {
* UX rules (regulatory area: kept in Swedish):
* - Default is K2 (matches the column default and BFNAR 2016:10 baseline).
* - Switching in either direction fires a confirmation dialog. K2 → K3
* warns about the added obligations; the recommendation per BFN is that
* the choice is permanent once made, surfaced as a warning, not a block.
* names what the system then does (K3-mallen for the årsredovisning,
* komponentuppdelning in the asset register) and the one obligation the
* choice itself carries: komponentavskrivning is mandatory under K3 where
* component useful lives differ materially (punkt 17.4,
* .claude/skills/swedish-year-end-closing/references/k2-vs-k3.md:5).
* Kassaflödesanalys is NOT a consequence of K3: it follows from being a
* större företag (references/reporting-and-filing.md:10), so the copy
* says the product includes one, it does not blame the regelverk.
* The recommendation per BFN is that the choice is permanent once made,
* surfaced as a warning, not a block.
* K3 → K2 warns about what the system does NOT do: uppskjuten skatt
* (2240/8940) balances and komponentavskrivningar are not unwound
* automatically, and the K3 årsredovisning content stops applying.
@@ -103,10 +111,18 @@ export function AccountingFrameworkForm({ current, onSaved }: AccountingFramewor
htmlFor="accounting_framework"
help={
<>
K2 är standard för mindre bolag och innebär förenklade regler. K3 krävs när
bolaget når två av tre tröskelvärden (nettoomsättning &gt; 80 MSEK, tillgångar
&gt; 40 MSEK, eller fler än 50 anställda). K3 ställer högre krav: kassaflödesanalys,
komponentavskrivning materiella anläggningstillgångar och mer omfattande noter.
K2 är standard för mindre bolag och innebär förenklade regler. Större företag
ska tillämpa K3 och upprätta kassaflödesanalys: dit räknas bland annat bolag
som överskrider mer än ett av tre tröskelvärden (nettoomsättning &gt; 80 MSEK,
tillgångar &gt; 40 MSEK, fler än 50 anställda) under vart och ett av de två
senaste räkenskapsåren, och bolag med noterade värdepapper. För räkenskapsår
som börjar efter 2025-12-31 är K2 dessutom stängt för bolag med utländsk
filial, kryptotillgångar eller aktierelaterade ersättningar, och för bolag där
byggnader ger minst 75 % av nettoomsättningen. Med K3
valt bygger Accounted årsredovisningen enligt K3-mallen: kassaflödesanalys,
förändring av eget kapital som egen räkning och utökade noter.
Anläggningsregistret tar emot komponentuppdelning först med K3, som kräver
komponentavskrivning när komponenterna har väsentligt olika nyttjandeperioder.
Obeskattade reserver redovisas brutto i juridisk person enligt K3 punkt 29.37.
</>
}
@@ -153,9 +169,15 @@ export function AccountingFrameworkForm({ current, onSaved }: AccountingFramewor
) : (
<>
<span className="block">
K3 medför löpande att kassaflödesanalys upprättas, att
komponentavskrivning kan användas och att årsredovisningen får ett
utökat notinnehåll.
Årsredovisningen byggs enligt K3-mallen: kassaflödesanalys,
förändring av eget kapital som egen räkning och utökade noter.
Kassaflödesanalys är i sig ett krav för större företag, inte en följd
av regelverksvalet.
</span>
<span className="block">
Komponentavskrivning blir obligatorisk för tillgångar vars komponenter
har väsentligt olika nyttjandeperioder (K3 punkt 17.4).
Anläggningsregistret tar emot komponentuppdelning först när K3 är valt.
</span>
<span className="block">
Bytet är permanent enligt rekommendation. Fortsätt?
@@ -7,7 +7,7 @@
* lib/core/bookkeeping tests + the manual MCP smoke test.
*/
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { tools } from '../server'
import { tools, YEAR_END_BLOCKER_KIND } from '../server'
import { TOOL_SCOPE_MAP } from '@/lib/auth/api-keys'
vi.mock('@/lib/core/bookkeeping/year-end-service', () => ({
@@ -47,6 +47,48 @@ describe('gnubok_year_end_readiness: registration', () => {
it('is mapped to reports:read scope', () => {
expect(TOOL_SCOPE_MAP.gnubok_year_end_readiness).toBe('reports:read')
})
// The description is the only thing an agent reads before deciding what to
// pre-check, and the 280-char budget does not fit all eleven kinds. It must
// therefore name every kind the caller can DO something about ahead of time;
// the four period-state kinds are summarized, since nothing can be
// pre-checked about them.
const PERIOD_STATE_KINDS = new Set([
'period_not_found',
'period_not_ended',
'period_already_closed',
'closing_entry_exists',
])
it('names every actionable blocker kind the tool can emit', () => {
const tool = tools.find((t) => t.name === 'gnubok_year_end_readiness')!
const actionable = [...new Set(Object.values(YEAR_END_BLOCKER_KIND))].filter(
(kind) => !PERIOD_STATE_KINDS.has(kind),
)
// Guards the summarizing itself: if a kind stops being period-state, or a
// new one appears, it has to show up in the description.
expect(actionable.length).toBe(7)
for (const kind of actionable) {
expect(tool.description, `blocker kind ${kind} missing from description`).toContain(kind)
}
expect(tool.description).toContain('period-state')
})
it('names unbooked transactions as the most common blocker', () => {
// The omission that sent agents pre-checking the wrong things: this is the
// blocker a real close trips on, and the description never mentioned it.
const tool = tools.find((t) => t.name === 'gnubok_year_end_readiness')!
expect(tool.description).toMatch(/unbooked_transactions \(most common\)/)
})
it('does not present open foreign-currency items as a blocker', () => {
// validateYearEndReadiness pushes those onto `warnings`, never `blockers`:
// executeYearEndClosing runs the revaluation itself, so escalating would
// block a close that the very next step performs.
const tool = tools.find((t) => t.name === 'gnubok_year_end_readiness')!
expect(tool.description).toMatch(/FX = warning, never blocker/)
expect(tool.description).not.toMatch(/[Bb]lockers[^.]*revaluation/)
})
})
function makeMockSupabase(period: Record<string, unknown> | null) {
+14 -2
View File
@@ -1722,8 +1722,13 @@ const RC_COMPLETENESS_CODES = new Set<VatDeclarationCheck['code']>([
* UNBOOKED_CHECK_FAILED shares 'unbooked_transactions' with the real count:
* the fail-closed variant means "we could not tell", and an agent should react
* to it the same way (go look at the transactions, then re-run readiness).
*
* Exported so the tool-description test can assert that every kind an agent
* can receive is actually named in the description it plans against: the
* description drifted once already (it advertised FX revaluation, a WARNING,
* as a blocker and never mentioned unbooked transactions, the common one).
*/
const YEAR_END_BLOCKER_KIND: Record<YearEndBlockerCode, string> = {
export const YEAR_END_BLOCKER_KIND: Record<YearEndBlockerCode, string> = {
PERIOD_NOT_FOUND: 'period_not_found',
PERIOD_NOT_ENDED: 'period_not_ended',
PERIOD_ALREADY_CLOSED: 'period_already_closed',
@@ -12780,7 +12785,14 @@ export const tools: McpTool[] = [
{
name: 'gnubok_year_end_readiness',
title: 'Year-End Readiness Check',
description: "Pre-flight before irreversible gnubok_run_year_end. Returns ready (bool) + ordered blockers (drafts, voucher gaps, sequence mismatches, unbalanced TB, FX revaluation) + optional closing-entry preview.",
// Budget: 280 chars (output-schema.test.ts). Spend it on the blockers an
// agent can act on BEFORE calling, in likelihood order. The four
// period-state kinds (period_not_found / _not_ended / _already_closed /
// closing_entry_exists) collapse into "period-state": nothing to pre-check
// there, the period either is closable or is not. Open items in foreign
// currency are warnings, never blockers, because executeYearEndClosing
// revalues them in step 2 (lib/core/bookkeeping/year-end-service.ts).
description: "Pre-flight for irreversible gnubok_run_year_end. Blockers: unbooked_transactions (most common), draft_entries, unexplained_voucher_gap, sequence_mismatch, trial_balance_unbalanced, opening_balance_continuity, next_period_ib_posted, period-state. FX = warning, never blocker.",
inputSchema: {
type: 'object',
additionalProperties: false,
@@ -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 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."
"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 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": "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"
"body": "Posten avser uppskjuten skatteskuld 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,
@@ -413,6 +413,122 @@ describe('buildArsredovisningData: K3', () => {
expect(principles.body).not.toContain('i balansräkningen')
})
// ── Leasing paragraph: which balances contradict "all leases operational" ──
//
// The detection reads the company's OWN account names in kontogrupp 12, not
// a hardcoded 1260/1269 pair. On the shipped BAS 2026 chart those two are
// "(Fritt konto för Inventarier, verktyg och installationer)" and "Ack.
// avskrivningar på datorer", so the number-based rule both missed real lease
// accounts and flagged owned computers.
function plantTbRow(row: {
account_number: string
account_name: string
closing_debit?: number
closing_credit?: number
}) {
mockedTrialBalance.mockResolvedValue({
rows: [
{
account_number: row.account_number,
account_name: row.account_name,
account_class: 1,
opening_debit: 0,
opening_credit: 0,
period_debit: 0,
period_credit: 0,
closing_debit: row.closing_debit ?? 0,
closing_credit: row.closing_credit ?? 0,
},
],
totalDebit: row.closing_debit ?? 0,
totalCredit: row.closing_credit ?? 0,
isBalanced: true,
})
}
async function leasingParagraph(): Promise<string> {
const supabase = makeSupabase({ accountingFramework: 'k3' })
// @ts-expect-error: chainable mock isn't fully typed as SupabaseClient
const data = await buildArsredovisningData(supabase, 'co1', 'fp1')
return data.noter.find((n) => n.title.startsWith('Redovisnings'))!.body
}
it('drops the blanket operational claim for a finance-leased asset on 1227', async () => {
// 1227 Finansiellt leasade inventarier: a capitalized lease the old
// 1260/1269 rule never saw, while the K2 mapper folds it into the same BR
// post as ordinary inventarier.
plantTbRow({
account_number: '1227',
account_name: 'Finansiellt leasade inventarier',
closing_debit: 180_000,
})
const body = await leasingParagraph()
expect(body).not.toMatch(/Samtliga leasingavtal/)
expect(body).not.toMatch(/20\.29/)
expect(body).toContain('leasade tillgångar')
})
it('keeps the blanket operational claim for owned inventarier on 1220', async () => {
plantTbRow({
account_number: '1220',
account_name: 'Inventarier, verktyg och installationer',
closing_debit: 180_000,
})
const body = await leasingParagraph()
expect(body).toContain('Samtliga leasingavtal redovisas som operationella')
expect(body).toContain('K3 punkt 20.29')
})
it('does not read owned datorer on 1269 as a leased asset', async () => {
// Regression on the old rule: on this chart 1269 is "Ack. avskrivningar på
// datorer", so an owned laptop used to flip the paragraph and put a leased
// asset in a signed årsredovisning that has none.
plantTbRow({
account_number: '1269',
account_name: 'Ack. avskrivningar på datorer',
closing_credit: 40_000,
})
const body = await leasingParagraph()
expect(body).toContain('Samtliga leasingavtal redovisas som operationella')
expect(body).not.toContain('Balansräkningen innehåller leasade tillgångar')
})
it('does not claim a leased asset for a lease disposed during the year', async () => {
// closing_debit/closing_credit are cumulative per-side totals, not a net
// balance: a lease acquired in an earlier year and disposed this year has
// both sides non-zero while the balansräkning carries nothing. Asserting
// "Balansräkningen innehåller leasade tillgångar" about an empty balance
// sheet is the same defect class this sweep exists to remove.
plantTbRow({
account_number: '1217',
account_name: 'Finansiellt leasade maskiner',
closing_debit: 180_000,
closing_credit: 180_000,
})
const body = await leasingParagraph()
expect(body).toContain('Samtliga leasingavtal redovisas som operationella')
expect(body).not.toContain('Balansräkningen innehåller leasade tillgångar')
})
it('ignores a zero-balance lease account', async () => {
plantTbRow({
account_number: '1217',
account_name: 'Finansiellt leasade maskiner',
})
const body = await leasingParagraph()
expect(body).toContain('Samtliga leasingavtal redovisas som operationella')
})
it('ignores förutbetalda leasingavgifter on 1720 (the operational treatment)', async () => {
plantTbRow({
account_number: '1720',
account_name: 'Förutbetalda leasingavgifter',
closing_debit: 24_000,
})
const body = await leasingParagraph()
expect(body).toContain('Samtliga leasingavtal redovisas som operationella')
})
it('emits an Eventualförpliktelser note', async () => {
const supabase = makeSupabase({ accountingFramework: 'k3' })
// @ts-expect-error: chainable mock isn't fully typed as SupabaseClient
@@ -62,6 +62,16 @@ describe('buildK3RedovisningsPrinciper', () => {
expect(note.body).not.toMatch(/beräknad/)
})
it('does not attribute the recognised 2240 balance to obeskattade reserver', () => {
// deriveLatentTaxMovement reads the 2240 and 8940 balances and nothing
// else. Under K3 that account carries deferred tax on ALL temporary
// differences (k2-vs-k3.md:11-13), so where the balance came from is
// exactly what this builder cannot establish.
const note = principles({ deferredTax: 'recognized' })
expect(note.body).toContain('Uppskjuten skatteskuld redovisas på konto 2240')
expect(note.body).not.toContain('hänförlig till obeskattade reserver')
})
it('describes leasing as expensed operational leases under the K3 20.29 juridisk-person exemption', () => {
const note = principles()
expect(note.body).toContain('operationella leasingavtal')
@@ -81,22 +91,32 @@ describe('buildK3RedovisningsPrinciper', () => {
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.
// A balansrakning carrying an anlaggningskonto the company's own chart
// names as leased 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', () => {
it('prints NO deferred-tax paragraph when the 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')
// Every available wording is a statement about how THIS document reports
// the reserves, and on the read-failure path there are no figures to state
// it from: the gross wording is the denial phrased positively, not a
// weaker claim. So the paragraph is omitted entirely; build-data has
// already pushed a warning that the note could not be computed.
expect(note.body).not.toMatch(/Uppskjuten skatt/)
expect(note.body).not.toMatch(/29\.37/)
expect(note.body).not.toMatch(/uppskjuten skatteskuld/)
expect(note.body).not.toMatch(/2240/)
// The rest of the policy note is unaffected.
expect(note.body).toContain('BFNAR 2012:1')
expect(note.body).toContain('Intäktsredovisning')
expect(note.body).toContain('Leasing')
expect(note.body).toContain('Finansiella instrument')
})
it('OMITS the komponentavskrivning paragraph when no asset has components', () => {
@@ -132,11 +152,13 @@ describe('K3 noter: the two deferred-tax notes never contradict each other', ()
latentTaxChange: 20_600,
latentTaxClosing: 70_600,
})
// Both notes recognise the same liability on the same account.
// Both notes recognise the same liability on the same account, and
// neither says where the balance came from: the caller sees 2240/8940
// only, and K3 puts every temporary difference on 2240.
expect(policy.body).toContain('konto 2240')
expect(movement.body).toContain('konto 2240')
expect(policy.body).toContain('obeskattade reserver')
expect(movement.body).toContain('obeskattade reserver')
expect(policy.body).not.toContain('hänförlig till obeskattade reserver')
expect(movement.body).not.toContain('hänförlig till 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')
@@ -191,7 +213,7 @@ describe('buildUppskjutenSkattNot', () => {
expect(note.body).toMatch(/Utgående saldo.*60/)
})
it('reports the balance without claiming a rate it cannot verify', () => {
it('reports the balance without claiming a rate or an origin it cannot verify', () => {
const note = buildUppskjutenSkattNot({
noteNumber: 1,
latentTaxOpening: 50_000,
@@ -199,12 +221,14 @@ describe('buildUppskjutenSkattNot', () => {
latentTaxClosing: 50_000,
})
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/)
// Same for provenance: 2240 is K3's account for every temporary
// difference (k2-vs-k3.md:11-13), and the caller reads only the balance.
expect(note.body).not.toContain('obeskattade reserver')
})
})
+52 -9
View File
@@ -335,8 +335,14 @@ export async function buildArsredovisningData(
// K3-compliant; we keep a soft notice here so the filer remembers to
// verify the document against their specific obligations before sending
// to Bolagsverket.
// The enumeration is conditional on what was actually produced: when
// generateKassaflodesanalys failed we already pushed "kunde inte
// genereras" above, and claiming the PDF contains one in the very next
// warning would contradict it on the same screen.
warnings.push(
'Bolaget redovisar enligt K3 (BFNAR 2012:1). Soliditeten är beräknad med 79,4 % av obeskattade reserver inräknat i eget kapital. PDF:en innehåller kassaflödesanalys, förändring av eget kapital och utökade noter: granska innehållet mot er specifika redovisning innan inlämning.',
'Bolaget redovisar enligt K3 (BFNAR 2012:1). Soliditeten är beräknad med 79,4 % av obeskattade reserver inräknat i eget kapital. PDF:en innehåller '
+ (kassaflodesanalys ? 'kassaflödesanalys, förändring av eget kapital och utökade noter' : 'förändring av eget kapital och utökade noter')
+ ': granska innehållet mot er specifika redovisning innan inlämning.',
)
}
if (entityType === 'unknown') {
@@ -867,6 +873,47 @@ function deriveLatentTaxMovement(
}
}
/**
* True when the balansräkning carries an anläggningstillgång that the
* company's OWN chart of accounts names as leased.
*
* The previous rule tested for account 1260 or 1269, which was wrong in both
* directions. It missed every other account a finance-leased asset can land
* on (the K2 mapper folds 1220-1279 into one BR post, and the shipped BAS
* chart carries 1217 Finansiellt leasade maskiner and 1227 Finansiellt
* leasade inventarier), and on that same chart 1260 is "(Fritt konto för
* Inventarier, verktyg och installationer)" and 1269 is "Ack. avskrivningar
* på datorer": an owned laptop would have been reported as a leased asset.
*
* Numbers cannot separate an owned 1220 inventarie from a leased one, so ask
* the names instead. A kontogrupp-12 account (BAS keeps capitalized leases
* there: .claude/skills/swedish-asset-accounting/references/
* leasing-and-disposal.md:28) whose name contains "leas" is a leased asset or
* its accumulated depreciation; "Inventarier, verktyg och installationer"
* never matches. Names are the right source because they travel with the
* balance: the asset register cannot even reach 1260-1279 (its per-category
* ranges stop at 1259 and resume at 1280), so such a balance always arrives
* by SIE import or a manual voucher, carrying the originating chart's name.
*
* The scan stops at 1299 on purpose: 1720 Förutbetalda leasingavgifter is
* part of the OPERATIONAL treatment (leasing-and-disposal.md:14) and must not
* flip the paragraph.
*/
function hasCapitalizedLeaseAsset(tbFullRows: TrialBalanceRow[]): boolean {
return tbFullRows.some(
(r) =>
r.account_number >= '1200' &&
r.account_number < '1300' &&
/leas/i.test(r.account_name ?? '') &&
// closing_debit/closing_credit are cumulative per-side totals, not a
// net balance, so a leased asset acquired earlier and disposed this
// year has both sides non-zero while the balansrakning carries
// nothing. Compare the NET balance so a disposed lease stops
// triggering the paragraph.
Math.abs((r.closing_debit || 0) - (r.closing_credit || 0)) > 0.005,
)
}
/**
* Build the K3 note set (BFNAR 2012:1). Differs from K2 in:
* - Verbose redovisningsprinciper covering all K3 measurement principles
@@ -990,14 +1037,10 @@ async function buildK3Noter(
)
}
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),
)
// Leased assets on the balance sheet 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 = hasCapitalizedLeaseAsset(tbFullRows)
notes.push(
buildK3RedovisningsPrinciper({
hasComponents,
+32 -18
View File
@@ -41,12 +41,15 @@ import type {
* 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.
* every possible wording is an affirmative claim about figures we could not
* read, so NO deferred-tax paragraph is emitted at all. The caller derives
* this once (build-data.ts) and warns the user on that path.
* @param hasCapitalizedLease: true when the balance sheet carries an
* anlaggningskonto the company's own chart names as leased (build-data
* decides; see hasCapitalizedLeaseAsset there). 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(params: {
hasComponents: boolean
@@ -80,18 +83,25 @@ export function buildK3RedovisningsPrinciper(params: {
// 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.
// - 'unknown': the figures could not be read, so NO paragraph is emitted.
// Both of the other wordings state how this document reports the
// reserves, which is a claim about books we just failed to read: the
// gross wording is the same affirmative statement as the denial, only
// phrased positively. Silence is the one honest option, and the caller
// has already raised a warning telling the user the note could not be
// computed.
// No branch claims HOW the balance was measured or WHERE it came from:
// under K3 the 2240 account carries deferred tax on ALL temporary
// differences (.claude/skills/swedish-year-end-closing/references/
// k2-vs-k3.md:11-13), and deriveLatentTaxMovement reads only the 2240/8940
// balances, so an attribution to obeskattade reserver is not something this
// builder can establish. An imported provision may also have been booked at
// 22 or 21.4 percent by another system.
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.'
? 'Uppskjuten skatt: Uppskjuten skatteskuld 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.'
? null
: '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
@@ -102,8 +112,8 @@ export function buildK3RedovisningsPrinciper(params: {
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.'
if (deferredTaxParagraph) paragraphs.push(deferredTaxParagraph)
paragraphs.push(
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.',
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.',
@@ -131,7 +141,11 @@ export function buildK3RedovisningsPrinciper(params: {
* 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.
* system, and the balance is disclosed here exactly as it stands. It also
* does not attribute the balance to obeskattade reserver: under K3 the 2240
* account holds deferred tax on every temporary difference
* (.claude/skills/swedish-year-end-closing/references/k2-vs-k3.md:11-13) and
* the caller derives these figures from the 2240/8940 balances alone.
*
* Whenever this note is emitted, buildK3RedovisningsPrinciper MUST be built
* with hasRecognizedDeferredTax: true, or note 1 would deny the very split
@@ -150,7 +164,7 @@ export function buildUppskjutenSkattNot(params: {
const fmt = (n: number) =>
Math.round(n).toLocaleString('sv-SE')
const lines: string[] = [
'Posten avser uppskjuten skatteskuld hänförlig till obeskattade reserver, redovisad på konto 2240.',
'Posten avser uppskjuten skatteskuld redovisad på konto 2240.',
'',
`Ingående saldo (2240): ${fmt(latentTaxOpening)} kr`,
`Årets förändring (8940): ${fmt(latentTaxChange)} kr`,
@@ -4,6 +4,12 @@
* the rejection text cites BFNAR 2016:10 punkt 10.4 only for the
* egenupparbetade immateriella group and stays generic everywhere else, so a
* deferred-tax or fair-value account never gets a wrong legal citation.
*
* The second axis is entity_type. companies.accounting_framework is NOT NULL
* DEFAULT 'k2', so an enskild firma hits the same gate although K2 is the
* regelverk for an ÅRSREDOVISNING, which it does not prepare
* (legal-framework.md:29, :48). It may therefore get neither the citation nor
* a K3 remedy.
*/
import { describe, it, expect } from 'vitest'
import {
@@ -11,11 +17,12 @@ import {
k2ExcludedAccountMessages,
} from '@/lib/bokslut/assets/k2-account-guard'
import { BAS_REFERENCE, getBASReference } from '@/lib/bookkeeping/bas-reference'
import type { EntityType } from '@/types'
function messagesFor(accountNumber: string) {
function messagesFor(accountNumber: string, entityType: EntityType | null = 'aktiebolag') {
const account = getBASReference(accountNumber)
expect(account, `${accountNumber} missing from the BAS reference`).toBeTruthy()
return k2ExcludedAccountMessages(account!)
return k2ExcludedAccountMessages(account!, entityType)
}
describe('findK2ExcludedAccount', () => {
@@ -77,12 +84,14 @@ describe('k2ExcludedAccountMessages', () => {
// rewrites the whole årsredovisning: it is never the remedy for one
// misdirected account. No message may suggest it, on any Ej K2 account.
it('never tells the user to change the accounting framework', () => {
for (const account of BAS_REFERENCE.filter((a) => a.k2_excluded)) {
const { message_sv, message_en } = k2ExcludedAccountMessages(account)
expect(message_sv).not.toContain('Byt regelverk')
expect(message_sv).not.toContain('Inställningar')
expect(message_en).not.toContain('Switch the accounting framework')
expect(message_en).not.toContain('Settings')
for (const entityType of ['aktiebolag', 'enskild_firma', null] as const) {
for (const account of BAS_REFERENCE.filter((a) => a.k2_excluded)) {
const { message_sv, message_en } = k2ExcludedAccountMessages(account, entityType)
expect(message_sv).not.toContain('Byt regelverk')
expect(message_sv).not.toContain('Inställningar')
expect(message_en).not.toContain('Switch the accounting framework')
expect(message_en).not.toContain('Settings')
}
}
})
@@ -91,12 +100,14 @@ describe('k2ExcludedAccountMessages', () => {
// company. The text must therefore describe the ACCOUNT, never claim which
// framework this company applies.
it('never asserts which framework the company applies', () => {
for (const account of BAS_REFERENCE.filter((a) => a.k2_excluded)) {
const { message_sv, message_en } = k2ExcludedAccountMessages(account)
expect(message_sv).not.toContain('företaget tillämpar')
expect(message_sv).not.toContain('ert företag')
expect(message_en).not.toContain('the company applies')
expect(message_en).not.toContain('your company')
for (const entityType of ['aktiebolag', 'enskild_firma', null] as const) {
for (const account of BAS_REFERENCE.filter((a) => a.k2_excluded)) {
const { message_sv, message_en } = k2ExcludedAccountMessages(account, entityType)
expect(message_sv).not.toContain('företaget tillämpar')
expect(message_sv).not.toContain('ert företag')
expect(message_en).not.toContain('the company applies')
expect(message_en).not.toContain('your company')
}
}
})
@@ -107,7 +118,7 @@ describe('k2ExcludedAccountMessages', () => {
// 1370, 1518, 2089, 2092, 2096, 2240, 2448, 3940, 7940, 82xx-84xx, 8940.
expect(others.length).toBeGreaterThan(0)
for (const account of others) {
const { message_sv, message_en } = k2ExcludedAccountMessages(account)
const { message_sv, message_en } = k2ExcludedAccountMessages(account, 'aktiebolag')
expect(message_sv).toContain(account.account_number)
expect(message_sv).toContain('Ej K2')
expect(message_sv).toContain('K3')
@@ -128,3 +139,49 @@ describe('k2ExcludedAccountMessages', () => {
expect(message_sv).toContain('1370 (Uppskjuten skattefordran)')
})
})
describe('k2ExcludedAccountMessages: enskild firma', () => {
// An enskild firma prepares ett förenklat årsbokslut (K1) or ett årsbokslut
// (BFNAR 2017:3), never an årsredovisning under BFNAR 2016:10
// (.claude/skills/swedish-year-end-closing/references/legal-framework.md:29,
// :48). Citing the K2 regelverk at it is a wrong legal claim, and "apply K3
// instead" points at a document it does not produce. The K1 counterpart of
// punkt 10.4 is not sourced in the repo skills, so nothing replaces it: the
// message states the chart flag, what the system did, and the way forward.
it.each(['1010', '1011', '1012', '1018', '1019', '1081'])(
'drops the BFNAR 2016:10 citation on %s but keeps the 1090 remedy',
(accountNumber) => {
const { message_sv, message_en } = messagesFor(accountNumber, 'enskild_firma')
expect(message_sv).toContain(accountNumber)
expect(message_sv).not.toContain('BFNAR 2016:10')
expect(message_sv).not.toContain('10.4')
expect(message_sv).not.toContain('K3')
expect(message_sv).toContain('Ej K2')
expect(message_sv).toContain('anläggningsregistret')
expect(message_sv).toContain('1090')
expect(message_en).not.toContain('BFNAR 2016:10')
expect(message_en).not.toContain('K3')
expect(message_en).toContain('1090')
},
)
it('drops the "presumes K3" line for the other Ej K2 accounts', () => {
const { message_sv, message_en } = messagesFor('1370', 'enskild_firma')
expect(message_sv).toContain('1370 (Uppskjuten skattefordran)')
expect(message_sv).toContain('Ej K2')
expect(message_sv).not.toContain('K3')
// No 1090: that answer belongs to a misdirected intangible only.
expect(message_sv).not.toContain('1090')
expect(message_en).not.toContain('K3')
expect(message_en).not.toContain('1090')
})
// A failed companies read leaves entity_type undefined. An unknown entity
// may not be handed a legal citation on a guess, so it gets the same
// neutral wording as an enskild firma.
it('treats an unknown entity type like an enskild firma', () => {
const known = messagesFor('1010', 'enskild_firma')
const unknown = k2ExcludedAccountMessages(getBASReference('1010')!)
expect(unknown).toEqual(known)
})
})
+51 -2
View File
@@ -36,7 +36,24 @@
* intangible belongs on 1090, which K2 permits
* (.claude/skills/swedish-year-end-closing/references/k2-vs-k3.md:24,
* "Only acquired intangibles may be recognized").
*
* The K2 framing only holds for an entity that prepares an ÅRSREDOVISNING.
* companies.accounting_framework is NOT NULL DEFAULT 'k2', so an enskild
* firma trips this gate too, and BFNAR 2016:10 is not its regelverk: K2 is
* for mindre aktiebolag och ekonomiska föreningar, while a sole trader
* prepares ett förenklat årsbokslut (K1) or ett årsbokslut (BFNAR 2017:3)
* (.claude/skills/swedish-year-end-closing/references/legal-framework.md:29,
* :31, :48). Citing punkt 10.4 at them is a wrong legal claim, and "apply K3
* instead" is not a remedy the ordinary sole trader has (the same source, :50,
* has it reaching an årsredovisning only by meeting the större-företag
* criteria, which it calls extremely rare). The K1 counterpart of punkt 10.4
* is not sourced in the repo skills, so nothing is cited in its place: the
* enskild-firma wording states what the BAS chart marks and what the asset
* register does, plus the same 1090 remedy. `entityType` is optional, and
* when it is missing the neutral wording is used: an unknown entity may not
* be handed a legal citation on a guess.
*/
import type { EntityType } from '@/types'
import { getBASReference, type BASReferenceAccount } from '@/lib/bookkeeping/bas-reference'
/** Swedish and English rejection text, mirroring the structured-errors registry shape. */
@@ -78,15 +95,36 @@ export function findK2ExcludedAccount(
/**
* User-facing text for the K2_EXCLUDED_ACCOUNT rejection, in both languages.
* The legal citation is conditional on what actually triggered the gate: see
* the file header.
* The legal citation is conditional on what actually triggered the gate AND on
* the entity preparing an årsredovisning at all: see the file header.
*
* @param entityType companies.entity_type for the company being written to.
* Only 'aktiebolag' gets the K2/K3 regelverk framing; anything else
* (enskild firma, or an entity we could not read) gets wording that is true
* without it.
*/
export function k2ExcludedAccountMessages(
account: BASReferenceAccount,
entityType?: EntityType | null,
): K2ExcludedAccountMessages {
const label = `${account.account_number} (${account.account_name})`
const preparesArsredovisning = entityType === 'aktiebolag'
if (isEgenupparbetadImmateriell(account)) {
if (!preparesArsredovisning) {
return {
message_sv:
`Konto ${label} är i BAS-kontoplanen reserverat för egenupparbetade ` +
`utvecklingsutgifter och markerat Ej K2: anläggningsregistret tar inte emot det. ` +
`En förvärvad immateriell tillgång bokförs på 1090 ` +
`(Övriga immateriella anläggningstillgångar).`,
message_en:
`Account ${label} is reserved in the BAS chart of accounts for internally generated ` +
`development expenditure and marked Ej K2: the asset register does not accept it. ` +
`An acquired intangible asset belongs on 1090 ` +
`(Övriga immateriella anläggningstillgångar).`,
}
}
return {
message_sv:
`Konto ${label} är reserverat för egenupparbetade utvecklingsutgifter, som bara får ` +
@@ -99,6 +137,17 @@ export function k2ExcludedAccountMessages(
}
}
if (!preparesArsredovisning) {
return {
message_sv:
`Konto ${label} är markerat Ej K2 i BAS-kontoplanen: anläggningsregistret tar inte ` +
`emot det. Välj ett annat konto för tillgången.`,
message_en:
`Account ${label} is marked Ej K2 in the BAS chart of accounts: the asset register ` +
`does not accept it. Choose a different account for the asset.`,
}
}
return {
message_sv: `Konto ${label} är markerat Ej K2 i BAS-kontoplanen och förutsätter K3.`,
message_en: