diff --git a/app/(dashboard)/bookkeeping/year-end/arsredovisning/page.tsx b/app/(dashboard)/bookkeeping/year-end/arsredovisning/page.tsx
index d52ba053..f8efe2df 100644
--- a/app/(dashboard)/bookkeeping/year-end/arsredovisning/page.tsx
+++ b/app/(dashboard)/bookkeeping/year-end/arsredovisning/page.tsx
@@ -530,10 +530,20 @@ export default function ArsredovisningPage() {
{data.accounting_framework === 'k3' && (
Årsredovisning enligt K3 (BFNAR 2012:1)
+ {/* 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. */}
- 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.
)}
diff --git a/app/api/assets/[id]/route.ts b/app/api/assets/[id]/route.ts
index 56537936..ef1119ec 100644
--- a/app/api/assets/[id]/route.ts
+++ b/app/api/assets/[id]/route.ts
@@ -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: {
diff --git a/app/api/assets/__tests__/id.test.ts b/app/api/assets/__tests__/id.test.ts
index 7f49fbe0..12e89833 100644
--- a/app/api/assets/__tests__/id.test.ts
+++ b/app/api/assets/__tests__/id.test.ts
@@ -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',
diff --git a/app/api/assets/route.ts b/app/api/assets/route.ts
index 27688cdf..60ac5788 100644
--- a/app/api/assets/route.ts
+++ b/app/api/assets/route.ts
@@ -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: {
diff --git a/components/settings/AccountingFrameworkForm.tsx b/components/settings/AccountingFrameworkForm.tsx
index f5f31a83..4f31f3ef 100644
--- a/components/settings/AccountingFrameworkForm.tsx
+++ b/components/settings/AccountingFrameworkForm.tsx
@@ -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 > 80 MSEK, tillgångar
- > 40 MSEK, eller fler än 50 anställda). K3 ställer högre krav: kassaflödesanalys,
- komponentavskrivning på 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 > 80 MSEK,
+ tillgångar > 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
) : (
<>