* feat(bokslut): year-end wizard with bokslutsdispositioner + asset register
Ships the first user-visible bokslut surface for K2 aktiebolag. The year-end
engine, INK2/INK2R/INK2S generator, and reconciliation reports already existed
in lib/core/bookkeeping/ and lib/reports/; this work wires them into a real
multi-step UI, adds the missing dispositioner calculators (bolagsskatt,
periodiseringsfond, överavskrivningar, SLP), and introduces a fixed-asset
register that feeds planenliga avskrivningar into the same flow.
PHASE 1 — Wizard around the existing year-end engine
- Replaces the "Kommer snart" stub at /bookkeeping/year-end with a 4-step
wizard (Kontroll → Dispositioner → Förhandsgranska → Verkställ) plus a
Klart result view
- New aggregator lib/bokslut/readiness-aggregator.ts composes
validateYearEndReadiness with bank-reconciliation status and entity-typed
reminders into one fetch backing the preflight step
- New endpoint GET /api/bookkeeping/fiscal-periods/[id]/bokslut-readiness
PHASE 2 — Bokslutsdispositioner calculators
- lib/bokslut/tax-provision/{bolagsskatt,sarskild-loneskatt}-calculator.ts —
20.6 % on taxable result → 8910/2512 (with non-deductible / non-taxable
manual adjustments and schablonintäkt pass-through) and 24.26 % SLP on
posted pension costs → 7533/2514
- lib/bokslut/reserves/periodiseringsfond-service.ts — proposeAvsattning
(25 % cap, BAS 212X cohort accounts) + proposeAteforing (FIFO 6-year
mandatory reversal with schablonintäkt computation) + balance lookup
- lib/bokslut/reserves/overavskrivningar-service.ts — 30-rule + 20-rule
helpers + proposeOveravskrivningar (8853/2153)
- New endpoint /api/bookkeeping/fiscal-periods/[id]/bokslutsdispositioner
(GET ordered proposals, POST commits user-chosen ones as separate
year_end vouchers via the journal engine)
- New DispositionsStep UI: per-card accept/skip + editable amount where
meaningful; mandatory p-fond reversals can't be skipped
- INK2 bug fix: ink2-engine.ts SRU mapping ranges previously pointed at
accounts BAS doesn't seed (8810/8830/8840). Corrected to 8811 (avsättning),
8819 (återföring), 8830 (lämnade koncernbidrag) so calculator output now
flows into INK2 correctly. Regression-locked with 6 new mapping tests.
PHASE 3 — Anläggningsregister + depreciation engine
- New migration 20260516120000_assets_and_depreciation.sql: assets table
(category, BAS-triple, K3 components JSONB reserved) and
depreciation_schedules (asset+period+journal_entry link). RLS via
user_company_ids(), immutability triggers after disposal/posting.
- lib/bokslut/assets/asset-service.ts — CRUD + disposal that posts a proper
gain/loss entry against 3973/7973
- lib/bokslut/assets/depreciation-engine.ts — computeAnnualDepreciation
(linear, pro-rata at acquisition/disposal/end-of-life) +
proposeAnnualPostings + commitAnnualPostings (one entry per asset)
- New endpoints /api/assets (CRUD + dispose) and
/api/bookkeeping/fiscal-periods/[id]/depreciation (preview + commit)
- /assets list+create page with K2 schablon defaults (3y datorer,
5y inventarier, 25y byggnader); sidebar entry added
- DepreciationPanel mounted at the top of DispositionsStep; posting
refreshes dispositions so bolagsskatt picks up the new result
Out of scope (per the agreed plan): K3 framework, iXBRL filing to
Bolagsverket (manual export only for now — regulatory risk flagged for
FY2026 closings), inventory module, koncernredovisning, revisor workflow.
Verification
- 116 unit tests pass across lib/bokslut/, lib/reports/ink2/, and the
existing lib/core/bookkeeping/year-end-service suite
- Zero lint or typecheck errors in any new file
- Migration applied successfully via Supabase MCP
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(bokslut): address PR #508 review — P1 correctness + P2 conventions
P1 — stale reminders in readiness-aggregator (greptile)
Remove the depreciation_manual / bolagsskatt_manual / periodiseringsfond_manual
nudges. After Phase 3, the wizard handles all three automatically via
DepreciationPanel and the dispositioner calculators — surfacing them as
manual reminders told users to repeat work the page already did. Only the
accruals_manual reminder (Phase 4 hasn't shipped) and the EF-only
ef_skatt_via_ne reminder remain.
P1 — netBookValueAfter ignored prior accumulated depreciation
proposeAnnualPostings now fetches all prior posted depreciation_schedules
for the company (excluding the current period) and sums them per asset, so
the displayed restvärde reflects every previously-booked year of avskrivning
instead of only this year. Without the fix, a 5-year asset in year 3 would
have shown 48 000 instead of the correct 24 000 net book value.
P1 — ordering bug in dispositioner POST handler
The 25 % p-fond avsättning cap derives from the current trial balance, so
mandatory återföring entries must post first. Added a server-side sort by
canonical bokslut order (återföring → överavskrivningar → avsättning → SLP
→ bolagsskatt) regardless of the client array order. The cap can no longer
be evaluated against a stale pre-återföring net result.
P2 — depreciation_schedules missing updated_at
New migration 20260516140000_depreciation_schedules_updated_at.sql adds the
column + trigger via update_updated_at_column(). Per CLAUDE.md migration
conventions, never modified the original migration. DepreciationSchedule
type updated.
P2 — addMonths end-of-month overflow
Replaced setUTCMonth (which overflows: Jan 31 + 1 month → Mar 3) with a
day-clamping implementation that produces Feb 28/29. Without the fix,
lifeEndExclusive landed one day too late and slightly over-depreciated.
New regression test asserts Jan 31 + 12 months stays in January.
P2 — pg-real tests for new triggers and RLS
tests/pg/assets.pg.test.ts (13 tests) covers:
- enforce_asset_post_disposal_immutability blocks every financial field
after disposal, allows notes/name through
- assets_disposal_atomic CHECK requires both disposed columns set together
- enforce_depreciation_schedule_immutability blocks edits after
journal_entry_id is linked, allows them before
- depreciation_schedules delete RLS policy filters out posted rows
- assets + depreciation_schedules RLS isolates across companies
Verification
- 117 unit tests pass (was 116, +1 for the addMonths regression)
- New pg-real suite syntactically + type-correct; will execute in CI
- Zero lint or typecheck errors in any touched file
- Migration 20260516140000 applied to remote Supabase via MCP
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(bokslut): address compliance-swarm + swedish-review findings
Real bugs surfaced by the bots on the first push that weren't covered by
greptile's inline P1/P2 set:
- Immaterial asset disposal hit the wrong BAS accounts. disposeAsset always
posted gain/loss to 3973/7973 regardless of category. For category
'immaterial' it must use 3013 (vinst) / 7813 (förlust) per BAS — using the
tangible accounts misclassifies in INK2R. Now branches on category. Two
new regression tests pin each branch.
- acquisition_cost CHECK was too loose. CreateAssetSchema accepted 0 (just
nonnegative). Tightened to z.number().positive() — a zero-cost asset
creates a no-op depreciation row and a balance sheet line that nothing
reconciles against.
- UpdateAssetSchema let users remap BAS accounts arbitrarily. Bot flagged
this as a defense-in-depth gap (V4.5). Added BAS_RANGES_BY_CATEGORY
validation at both the schema layer (Create) and the service layer
(Update) so user-supplied account overrides must stay inside the
category's expected BAS range. INK2R mappings and the depreciation
engine's category-driven defaults now can't drift.
Swedish accounting review:
- Building/markanläggning defaults — clarified UI copy. The 25-year
schablon is K2-redovisning, not the IL skattemässig rate. New helper text
spells this out. Markanläggning default lowered from 20→10 years
(Skatteverket guidance allows 10 % rate; 20 was on the upper bound
without justification).
- createAsset doesn't post the acquisition entry by design — that gap
wasn't called out anywhere in the UI. Added a tip box in
CreateAssetDialog explaining that the acquisition must already be in the
books; the register only drives depreciation.
- Disposal VAT (ML 3:3 / 7:3) not handled — sale of a deduct-eligible
anläggningstillgång is in principle 25 % momspliktig. Documented this as
a known limitation in the disposeAsset docstring so any future UI
surfacing the disposal endpoint warns the user.
Documented (not fixed yet) — bot was right but wider-scope work:
- SOC 2 PI1.3: dispositioner POST loop is not transactional. A failure
midway leaves partial postings. Added a code comment explaining the
recovery path (re-POST omitting committed kinds — each calculator
re-derives from current TB). Real atomicity via an RPC wrapper is Phase
5+ work.
False positives intentionally not changed:
- 4× OWASP V8.2.1 cross-tenant findings — service functions already filter
by company_id; the bot can't see past the route handler.
- V2.3 client-supplied amount clamping — proposeAvsattning and
proposeAteforing both clamp via Math.min already.
- A.8.15 audit events — withRouteContext already logs completion.
- Schablonintäkt journal entry — per IL 30:6a it's a skattemässig
justering, never booked. Current implementation is correct.
- Voucher series 'A' — matches existing executeYearEndClosing convention;
not changing here in isolation.
Verification
- 119 tests pass (was 117, +2 for the immaterial-disposal branches)
- Zero lint or typecheck errors on any touched file
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(bokslut): address compliance round 4 — BAS account overlap + method gate
Two real bugs the compliance bot caught after my round-3 fixes (both new on
its re-run, not in the original report):
- BAS account overlap not prevented: my BAS_RANGES_BY_CATEGORY uses the same
class range for asset and accumulated (e.g. immaterial: both 1010–1099,
building: both 1100–1199). Nothing stopped a user from picking the same
account for both, which would silently net acquisition cost against
accumulated depreciation in one bucket and corrupt INK2R 720x mappings.
CreateAssetSchema now rejects bas_asset_account === bas_accumulated_account
in a superRefine cross-field check; updateAsset enforces the same invariant
by reading the existing asset and validating the merged result.
- declining-balance methods silently fell back to linear. The DB enum allowed
declining_balance_30 / declining_balance_20, but the engine's
computeAnnualDepreciation only implements linear math. A determined caller
(MCP, curl, future UI) could create an asset labelled as räkenskapsenlig
avskrivning and get linear charges — silently wrong numbers under a
misleading method. Both CreateAssetSchema and UpdateAssetSchema now refine
the depreciation_method enum to require 'linear'. The DB enum stays open
for a future phase to add proper support. Stale comment in
depreciation-engine.ts updated to reflect the new invariant.
False positives I'm explicitly not chasing further on this round:
- 3× repeated OWASP V8.2.1 cross-tenant — services already filter by
company_id; bot can't see past the route handler. Round 3 already added
service-layer tests and inline reasoning.
- V2.3 atomicity upgrade to high — bot now flags it harder *because* I
documented it in round 3. The existing executeYearEndClosing has the same
non-transactional sequential-write pattern; wrapping just this endpoint
in an RPC while leaving the rest inconsistent is worse than the doc
comment. Real atomicity is Phase 5+.
- Disposal VAT user-facing warning — no UI surfaces dispose yet; docstring
in the service is sufficient until the UI ships.
Verification
- 119 tests pass
- Zero lint or typecheck errors on any touched file
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(bokslut): address compliance round 5 — disposal integrity + öavskr accounts
Round-5 bot feedback after my round-4 push (the sticky reports re-edited
themselves with two new genuine findings; bot count: 18 → 14 active).
P1 — accumulated_depreciation was client-supplied (OWASP V8.2.1, swedish
compliance review): the dispose endpoint accepted accumulated_depreciation
as a request-body number. A malicious or buggy caller could inflate it to
manipulate the book-value calculation and pocket a phantom gain. Now:
- DisposeAssetSchema no longer accepts accumulated_depreciation
- disposeAsset sums planned_depreciation from depreciation_schedules where
journal_entry_id IS NOT NULL for the asset, server-side
- New regression test "server-derives accumulated_depreciation — caller
cannot inflate gain" pins the server-derivation against the prior attack
- Limitation: manual avskrivningsverifikationer posted outside the engine
aren't captured. Phase 5+ can swap this for a trial-balance scan on
bas_accumulated_account if that gap matters.
P2 — överavskrivningar hardcoded 8853/2153 regardless of asset category
(swedish-asset-accounting): for buildings BAS uses 8852/2152 and for
immateriella tillgångar 8851/2151. Edge case for K2 SME (öavskr on
buildings is rare; on immateriella rarer still) but worth not lying about
the accounts. Now:
- New OVERAVSKRIVNING_ACCOUNTS table maps category → expense/accumulated
pair (machinery_equipment, building, immaterial, group)
- proposeOveravskrivningar accepts optional category, defaults to
machinery_equipment (the dominant K2 case — no behaviour change for
existing callers)
- POST handler item schema accepts optional category
- Label + description strings now name the actual accounts used
- 3 new tests cover the building, immaterial, and default branches
False positives I'm still declining to chase (already covered in prior
commit messages):
- 3× repeated OWASP V8.2.1 cross-tenant — services scope by company_id;
bot can't see past route handler
- V2.3 atomicity — existing executeYearEndClosing has the same pattern;
wrapping just this endpoint is inconsistent; real fix is Phase 5+ RPC
- Disposal VAT user-facing warning — no UI surfaces dispose yet
Verification
- 123 tests pass (was 119, +3 for öavskr category branches and +1 for the
server-derivation regression test, with one prior test rewritten to use
the new server-supplied accumulated path)
- Zero lint or typecheck errors on any touched file
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(bokslut): pre-merge polish — building disposal accounts, SLR-2026 rate
Last polish round before merge. All three from the round-5 sticky
Swedish-accounting-review update:
- Building / markanläggning disposal posted gain/loss to 3973/7973. BAS
2026 routes those to 3971/7971 (the SRU mapping points them at a
different INK2R field, so the existing accounts misclassify). Extended
the existing immaterial branch (3013/7813) into a three-way:
immaterial → 3013 / 7813
building / land_imprv → 3971 / 7971
other tangible → 3973 / 7973
Two new regression tests pin the building and land_improvement branches.
- DEFAULT_SCHABLONINTAKT_RATE was 0.03, based on SLR 2024-11-30 (1.96 %).
For closings of inkomstår 2026 the rate is SLR 2025-11-30 (2.55 %) + 1 pe
= 3.55 %. The wrong rate under-taxes the schablonintäkt, which feeds into
bolagsskatt. Updated to 0.0355 and rewrote the doc comment to track both
years so the next bump is obvious.
- Jämkning of input VAT for buildings / markanläggning disposed within the
10-year jämkningsperiod (ML 9 kap 8–11 §§) is out of scope for this PR
but should not be silently absent — added a KNOWN LIMITATION block to the
disposeAsset docstring so any future UI surfacing disposal checks the
10-year window and warns the user.
After this push the PR has 125 passing tests, all CI green, no merge
conflicts, and the only remaining bot complaints are repeat false
positives or Phase 5+ scope (RPC atomicity, full asset disposal UI,
K3 component depreciation).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
955 lines
28 KiB
TypeScript
955 lines
28 KiB
TypeScript
import type { SupabaseClient } from '@supabase/supabase-js'
|
|
import { fetchAllRows } from '@/lib/supabase/fetch-all'
|
|
import type {
|
|
FiscalPeriod,
|
|
JournalEntry,
|
|
JournalEntryLine,
|
|
} from '@/types'
|
|
import type {
|
|
INK2Declaration,
|
|
INK2RRutor,
|
|
INK2Rutor,
|
|
INK2SRutor,
|
|
INK2AccountMapping,
|
|
INK2RSRUCode,
|
|
} from './types'
|
|
import {
|
|
INK2R_ASSET_CODES,
|
|
INK2R_EQUITY_LIABILITY_CODES,
|
|
} from './types'
|
|
|
|
/**
|
|
* INK2 Declaration Engine
|
|
*
|
|
* Generates INK2 (huvudblankett), INK2R (räkenskapsschema), and INK2S
|
|
* (skattemässiga justeringar) for aktiebolag tax reporting.
|
|
*
|
|
* Account mappings follow the official BAS-to-SRU mapping from
|
|
* bas.se/kontoplaner/sru/ and Skatteverket field code spec.
|
|
*
|
|
* INK2R contains the full balance sheet + income statement.
|
|
* INK2S auto-derives basic fields (result + tax → taxable result), as well as
|
|
* periodiseringsfond and överavskrivningar when those have been posted via the
|
|
* bokslut-dispositions calculators in lib/bokslut/.
|
|
*/
|
|
|
|
/**
|
|
* BAS-to-SRU account mappings for INK2R
|
|
* Source: bas.se/kontoplaner/sru/ (stable since 2017)
|
|
*/
|
|
export const INK2R_ACCOUNT_MAPPINGS: INK2AccountMapping[] = [
|
|
// ---- Balance sheet: Assets ----
|
|
{
|
|
sruCode: '7201',
|
|
description: 'Koncessioner, patent, licenser, varumärken, goodwill',
|
|
section: 'assets',
|
|
normalBalance: 'debit',
|
|
accountRanges: [
|
|
{ start: '1010', end: '1079' },
|
|
{ start: '1090', end: '1099' },
|
|
],
|
|
},
|
|
{
|
|
sruCode: '7202',
|
|
description: 'Förskott immateriella anläggningstillgångar',
|
|
section: 'assets',
|
|
normalBalance: 'debit',
|
|
accountRanges: [{ start: '1080', end: '1089' }],
|
|
},
|
|
{
|
|
sruCode: '7214',
|
|
description: 'Byggnader och mark',
|
|
section: 'assets',
|
|
normalBalance: 'debit',
|
|
accountRanges: [
|
|
{ start: '1100', end: '1119' },
|
|
{ start: '1130', end: '1179' },
|
|
{ start: '1190', end: '1199' },
|
|
],
|
|
},
|
|
{
|
|
sruCode: '7215',
|
|
description: 'Maskiner, inventarier, övriga materiella',
|
|
section: 'assets',
|
|
normalBalance: 'debit',
|
|
accountRanges: [{ start: '1200', end: '1299' }],
|
|
},
|
|
{
|
|
sruCode: '7216',
|
|
description: 'Förbättringsutgifter på annans fastighet',
|
|
section: 'assets',
|
|
normalBalance: 'debit',
|
|
accountRanges: [{ start: '1120', end: '1129' }],
|
|
},
|
|
{
|
|
sruCode: '7217',
|
|
description: 'Pågående nyanläggningar, förskott materiella',
|
|
section: 'assets',
|
|
normalBalance: 'debit',
|
|
accountRanges: [{ start: '1180', end: '1189' }],
|
|
},
|
|
{
|
|
sruCode: '7230',
|
|
description: 'Andelar i koncernföretag',
|
|
section: 'assets',
|
|
normalBalance: 'debit',
|
|
accountRanges: [{ start: '1311', end: '1316' }],
|
|
},
|
|
{
|
|
sruCode: '7231',
|
|
description: 'Andelar i intresseföretag',
|
|
section: 'assets',
|
|
normalBalance: 'debit',
|
|
accountRanges: [{ start: '1330', end: '1338' }],
|
|
},
|
|
{
|
|
sruCode: '7233',
|
|
description: 'Ägarintressen övriga företag + långfristiga värdepapper',
|
|
section: 'assets',
|
|
normalBalance: 'debit',
|
|
accountRanges: [
|
|
{ start: '1350', end: '1359' },
|
|
{ start: '1380', end: '1389' },
|
|
],
|
|
},
|
|
{
|
|
sruCode: '7232',
|
|
description: 'Fordringar koncern/intresse',
|
|
section: 'assets',
|
|
normalBalance: 'debit',
|
|
accountRanges: [
|
|
{ start: '1320', end: '1329' },
|
|
{ start: '1340', end: '1349' },
|
|
],
|
|
},
|
|
{
|
|
sruCode: '7234',
|
|
description: 'Lån till delägare eller närstående',
|
|
section: 'assets',
|
|
normalBalance: 'debit',
|
|
accountRanges: [{ start: '1360', end: '1369' }],
|
|
},
|
|
{
|
|
sruCode: '7235',
|
|
description: 'Övriga långfristiga fordringar',
|
|
section: 'assets',
|
|
normalBalance: 'debit',
|
|
accountRanges: [
|
|
{ start: '1370', end: '1379' },
|
|
{ start: '1390', end: '1399' },
|
|
],
|
|
},
|
|
{
|
|
sruCode: '7241',
|
|
description: 'Råvaror och förnödenheter',
|
|
section: 'assets',
|
|
normalBalance: 'debit',
|
|
accountRanges: [{ start: '1410', end: '1419' }],
|
|
},
|
|
{
|
|
sruCode: '7242',
|
|
description: 'Varor under tillverkning',
|
|
section: 'assets',
|
|
normalBalance: 'debit',
|
|
accountRanges: [{ start: '1440', end: '1449' }],
|
|
},
|
|
{
|
|
sruCode: '7243',
|
|
description: 'Färdiga varor och handelsvaror',
|
|
section: 'assets',
|
|
normalBalance: 'debit',
|
|
accountRanges: [{ start: '1450', end: '1469' }],
|
|
},
|
|
{
|
|
sruCode: '7244',
|
|
description: 'Övriga lagertillgångar',
|
|
section: 'assets',
|
|
normalBalance: 'debit',
|
|
accountRanges: [{ start: '1470', end: '1489' }],
|
|
},
|
|
{
|
|
sruCode: '7245',
|
|
description: 'Pågående arbeten för annans räkning',
|
|
section: 'assets',
|
|
normalBalance: 'debit',
|
|
accountRanges: [{ start: '1490', end: '1499' }],
|
|
},
|
|
{
|
|
sruCode: '7246',
|
|
description: 'Förskott till leverantörer',
|
|
section: 'assets',
|
|
normalBalance: 'debit',
|
|
accountRanges: [{ start: '1400', end: '1409' }],
|
|
},
|
|
{
|
|
sruCode: '7251',
|
|
description: 'Kundfordringar',
|
|
section: 'assets',
|
|
normalBalance: 'debit',
|
|
accountRanges: [{ start: '1500', end: '1519' }],
|
|
},
|
|
{
|
|
sruCode: '7252',
|
|
description: 'Fordringar koncern/intresse (kortfristiga)',
|
|
section: 'assets',
|
|
normalBalance: 'debit',
|
|
accountRanges: [{ start: '1560', end: '1579' }],
|
|
},
|
|
{
|
|
sruCode: '7261',
|
|
description: 'Övriga fordringar',
|
|
section: 'assets',
|
|
normalBalance: 'debit',
|
|
accountRanges: [
|
|
{ start: '1520', end: '1559' },
|
|
{ start: '1580', end: '1599' },
|
|
{ start: '1600', end: '1619' },
|
|
{ start: '1621', end: '1699' },
|
|
],
|
|
},
|
|
{
|
|
sruCode: '7262',
|
|
description: 'Upparbetad men ej fakturerad intäkt',
|
|
section: 'assets',
|
|
normalBalance: 'debit',
|
|
accountRanges: [{ start: '1620', end: '1620' }],
|
|
},
|
|
{
|
|
sruCode: '7263',
|
|
description: 'Förutbetalda kostnader och upplupna intäkter',
|
|
section: 'assets',
|
|
normalBalance: 'debit',
|
|
accountRanges: [{ start: '1700', end: '1799' }],
|
|
},
|
|
{
|
|
sruCode: '7270',
|
|
description: 'Andelar i koncernföretag (kortfristiga)',
|
|
section: 'assets',
|
|
normalBalance: 'debit',
|
|
accountRanges: [{ start: '1860', end: '1869' }],
|
|
},
|
|
{
|
|
sruCode: '7271',
|
|
description: 'Övriga kortfristiga placeringar',
|
|
section: 'assets',
|
|
normalBalance: 'debit',
|
|
accountRanges: [
|
|
{ start: '1800', end: '1859' },
|
|
{ start: '1870', end: '1899' },
|
|
],
|
|
},
|
|
{
|
|
sruCode: '7281',
|
|
description: 'Kassa, bank och redovisningsmedel',
|
|
section: 'assets',
|
|
normalBalance: 'debit',
|
|
accountRanges: [{ start: '1900', end: '1999' }],
|
|
},
|
|
|
|
// ---- Balance sheet: Equity & Liabilities ----
|
|
{
|
|
sruCode: '7301',
|
|
description: 'Bundet eget kapital',
|
|
section: 'equity_liabilities',
|
|
normalBalance: 'credit',
|
|
accountRanges: [{ start: '2010', end: '2089' }],
|
|
},
|
|
{
|
|
sruCode: '7302',
|
|
description: 'Fritt eget kapital',
|
|
section: 'equity_liabilities',
|
|
normalBalance: 'credit',
|
|
accountRanges: [{ start: '2090', end: '2099' }],
|
|
},
|
|
{
|
|
sruCode: '7321',
|
|
description: 'Periodiseringsfonder',
|
|
section: 'equity_liabilities',
|
|
normalBalance: 'credit',
|
|
accountRanges: [
|
|
{ start: '2100', end: '2109' },
|
|
{ start: '2110', end: '2129' },
|
|
],
|
|
},
|
|
{
|
|
sruCode: '7322',
|
|
description: 'Ackumulerade överavskrivningar',
|
|
section: 'equity_liabilities',
|
|
normalBalance: 'credit',
|
|
accountRanges: [{ start: '2150', end: '2159' }],
|
|
},
|
|
{
|
|
sruCode: '7323',
|
|
description: 'Övriga obeskattade reserver',
|
|
section: 'equity_liabilities',
|
|
normalBalance: 'credit',
|
|
accountRanges: [
|
|
{ start: '2130', end: '2149' },
|
|
{ start: '2160', end: '2199' },
|
|
],
|
|
},
|
|
{
|
|
sruCode: '7331',
|
|
description: 'Pensionsavsättningar tryggandelagen',
|
|
section: 'equity_liabilities',
|
|
normalBalance: 'credit',
|
|
accountRanges: [{ start: '2210', end: '2219' }],
|
|
},
|
|
{
|
|
sruCode: '7332',
|
|
description: 'Övriga pensionsavsättningar',
|
|
section: 'equity_liabilities',
|
|
normalBalance: 'credit',
|
|
accountRanges: [{ start: '2220', end: '2229' }],
|
|
},
|
|
{
|
|
sruCode: '7333',
|
|
description: 'Övriga avsättningar',
|
|
section: 'equity_liabilities',
|
|
normalBalance: 'credit',
|
|
accountRanges: [{ start: '2230', end: '2299' }],
|
|
},
|
|
{
|
|
sruCode: '7350',
|
|
description: 'Obligationslån',
|
|
section: 'equity_liabilities',
|
|
normalBalance: 'credit',
|
|
accountRanges: [
|
|
{ start: '2300', end: '2319' },
|
|
{ start: '2320', end: '2329' },
|
|
],
|
|
},
|
|
{
|
|
sruCode: '7351',
|
|
description: 'Checkräkningskredit (långfristig)',
|
|
section: 'equity_liabilities',
|
|
normalBalance: 'credit',
|
|
accountRanges: [{ start: '2330', end: '2339' }],
|
|
},
|
|
{
|
|
sruCode: '7352',
|
|
description: 'Övriga skulder kreditinstitut (långfristiga)',
|
|
section: 'equity_liabilities',
|
|
normalBalance: 'credit',
|
|
accountRanges: [{ start: '2340', end: '2359' }],
|
|
},
|
|
{
|
|
sruCode: '7353',
|
|
description: 'Skulder koncern/intresse (långfristiga)',
|
|
section: 'equity_liabilities',
|
|
normalBalance: 'credit',
|
|
accountRanges: [{ start: '2360', end: '2379' }],
|
|
},
|
|
{
|
|
sruCode: '7354',
|
|
description: 'Övriga skulder (långfristiga)',
|
|
section: 'equity_liabilities',
|
|
normalBalance: 'credit',
|
|
accountRanges: [{ start: '2380', end: '2399' }],
|
|
},
|
|
{
|
|
sruCode: '7360',
|
|
description: 'Checkräkningskredit (kortfristig)',
|
|
section: 'equity_liabilities',
|
|
normalBalance: 'credit',
|
|
accountRanges: [{ start: '2410', end: '2419' }],
|
|
},
|
|
{
|
|
sruCode: '7361',
|
|
description: 'Övriga skulder kreditinstitut (kortfristiga)',
|
|
section: 'equity_liabilities',
|
|
normalBalance: 'credit',
|
|
accountRanges: [{ start: '2420', end: '2439' }],
|
|
},
|
|
{
|
|
sruCode: '7362',
|
|
description: 'Förskott från kunder',
|
|
section: 'equity_liabilities',
|
|
normalBalance: 'credit',
|
|
accountRanges: [{ start: '2400', end: '2409' }],
|
|
},
|
|
{
|
|
sruCode: '7363',
|
|
description: 'Pågående arbeten (skuldsida)',
|
|
section: 'equity_liabilities',
|
|
normalBalance: 'credit',
|
|
accountRanges: [{ start: '2450', end: '2459' }],
|
|
},
|
|
{
|
|
sruCode: '7364',
|
|
description: 'Fakturerad men ej upparbetad intäkt',
|
|
section: 'equity_liabilities',
|
|
normalBalance: 'credit',
|
|
accountRanges: [{ start: '2460', end: '2469' }],
|
|
},
|
|
{
|
|
sruCode: '7365',
|
|
description: 'Leverantörsskulder',
|
|
section: 'equity_liabilities',
|
|
normalBalance: 'credit',
|
|
accountRanges: [{ start: '2440', end: '2449' }],
|
|
},
|
|
{
|
|
sruCode: '7366',
|
|
description: 'Växelskulder',
|
|
section: 'equity_liabilities',
|
|
normalBalance: 'credit',
|
|
accountRanges: [{ start: '2490', end: '2490' }],
|
|
},
|
|
{
|
|
sruCode: '7367',
|
|
description: 'Skulder koncern/intresse (kortfristiga)',
|
|
section: 'equity_liabilities',
|
|
normalBalance: 'credit',
|
|
accountRanges: [{ start: '2470', end: '2479' }],
|
|
},
|
|
{
|
|
sruCode: '7369',
|
|
description: 'Övriga skulder (kortfristiga)',
|
|
section: 'equity_liabilities',
|
|
normalBalance: 'credit',
|
|
accountRanges: [
|
|
{ start: '2480', end: '2489' },
|
|
{ start: '2491', end: '2499' },
|
|
{ start: '2600', end: '2799' },
|
|
{ start: '2800', end: '2899' },
|
|
],
|
|
},
|
|
{
|
|
sruCode: '7368',
|
|
description: 'Skatteskulder',
|
|
section: 'equity_liabilities',
|
|
normalBalance: 'credit',
|
|
accountRanges: [{ start: '2500', end: '2599' }],
|
|
},
|
|
{
|
|
sruCode: '7370',
|
|
description: 'Upplupna kostnader och förutbetalda intäkter',
|
|
section: 'equity_liabilities',
|
|
normalBalance: 'credit',
|
|
accountRanges: [{ start: '2900', end: '2999' }],
|
|
},
|
|
|
|
// ---- Income statement ----
|
|
{
|
|
sruCode: '7410',
|
|
description: 'Nettoomsättning',
|
|
section: 'income_statement',
|
|
normalBalance: 'credit',
|
|
accountRanges: [{ start: '3000', end: '3799' }],
|
|
},
|
|
{
|
|
sruCode: '7412',
|
|
description: 'Aktiverat arbete för egen räkning',
|
|
section: 'income_statement',
|
|
normalBalance: 'credit',
|
|
accountRanges: [{ start: '3800', end: '3899' }],
|
|
},
|
|
{
|
|
sruCode: '7413',
|
|
description: 'Övriga rörelseintäkter',
|
|
section: 'income_statement',
|
|
normalBalance: 'credit',
|
|
accountRanges: [{ start: '3900', end: '3999' }],
|
|
},
|
|
{
|
|
sruCode: '7411',
|
|
description: 'Förändring av lager',
|
|
section: 'income_statement',
|
|
normalBalance: 'net',
|
|
accountRanges: [{ start: '4900', end: '4999' }],
|
|
},
|
|
{
|
|
sruCode: '7511',
|
|
description: 'Råvaror och förnödenheter',
|
|
section: 'income_statement',
|
|
normalBalance: 'debit',
|
|
accountRanges: [
|
|
{ start: '4000', end: '4499' },
|
|
{ start: '4500', end: '4599' },
|
|
{ start: '4700', end: '4899' },
|
|
],
|
|
},
|
|
{
|
|
sruCode: '7512',
|
|
description: 'Handelsvaror',
|
|
section: 'income_statement',
|
|
normalBalance: 'debit',
|
|
accountRanges: [{ start: '4600', end: '4699' }],
|
|
},
|
|
// CRITICAL: BAS 5000-6999 ALL map to SRU 7513
|
|
{
|
|
sruCode: '7513',
|
|
description: 'Övriga externa kostnader',
|
|
section: 'income_statement',
|
|
normalBalance: 'debit',
|
|
accountRanges: [{ start: '5000', end: '6999' }],
|
|
},
|
|
{
|
|
sruCode: '7514',
|
|
description: 'Personalkostnader',
|
|
section: 'income_statement',
|
|
normalBalance: 'debit',
|
|
accountRanges: [{ start: '7000', end: '7699' }],
|
|
},
|
|
{
|
|
sruCode: '7515',
|
|
description: 'Av- och nedskrivningar materiella/immateriella',
|
|
section: 'income_statement',
|
|
normalBalance: 'debit',
|
|
accountRanges: [{ start: '7800', end: '7899' }],
|
|
},
|
|
{
|
|
sruCode: '7516',
|
|
description: 'Nedskrivningar omsättningstillgångar',
|
|
section: 'income_statement',
|
|
normalBalance: 'debit',
|
|
accountRanges: [{ start: '7700', end: '7799' }],
|
|
},
|
|
{
|
|
sruCode: '7517',
|
|
description: 'Övriga rörelsekostnader',
|
|
section: 'income_statement',
|
|
normalBalance: 'debit',
|
|
accountRanges: [{ start: '7900', end: '7999' }],
|
|
},
|
|
{
|
|
sruCode: '7414',
|
|
description: 'Resultat från andelar i koncernföretag',
|
|
section: 'income_statement',
|
|
normalBalance: 'net',
|
|
accountRanges: [{ start: '8000', end: '8099' }],
|
|
},
|
|
{
|
|
sruCode: '7415',
|
|
description: 'Resultat från andelar i intresseföretag',
|
|
section: 'income_statement',
|
|
normalBalance: 'net',
|
|
accountRanges: [{ start: '8100', end: '8199' }],
|
|
},
|
|
{
|
|
sruCode: '7423',
|
|
description: 'Resultat från övriga företag med ägarintresse',
|
|
section: 'income_statement',
|
|
normalBalance: 'net',
|
|
accountRanges: [{ start: '8200', end: '8269' }],
|
|
},
|
|
{
|
|
sruCode: '7416',
|
|
description: 'Resultat från övriga finansiella anläggningstillgångar',
|
|
section: 'income_statement',
|
|
normalBalance: 'net',
|
|
accountRanges: [{ start: '8270', end: '8299' }],
|
|
},
|
|
{
|
|
sruCode: '7417',
|
|
description: 'Övriga ränteintäkter och liknande',
|
|
section: 'income_statement',
|
|
normalBalance: 'credit',
|
|
accountRanges: [{ start: '8300', end: '8399' }],
|
|
},
|
|
{
|
|
sruCode: '7522',
|
|
description: 'Räntekostnader och liknande',
|
|
section: 'income_statement',
|
|
normalBalance: 'debit',
|
|
accountRanges: [{ start: '8400', end: '8499' }],
|
|
},
|
|
{
|
|
sruCode: '7521',
|
|
description: 'Nedskrivningar finansiella anläggningstillgångar',
|
|
section: 'income_statement',
|
|
normalBalance: 'debit',
|
|
accountRanges: [{ start: '8500', end: '8599' }],
|
|
},
|
|
// Bokslutsdispositioner — account numbers per BAS 2020 (verified against
|
|
// lib/bookkeeping/bas-data/class-8-financial.ts).
|
|
{
|
|
sruCode: '7525',
|
|
description: 'Avsättning till periodiseringsfond',
|
|
section: 'income_statement',
|
|
normalBalance: 'debit',
|
|
accountRanges: [{ start: '8811', end: '8811' }],
|
|
},
|
|
{
|
|
sruCode: '7420',
|
|
description: 'Återföring av periodiseringsfond',
|
|
section: 'income_statement',
|
|
normalBalance: 'credit',
|
|
accountRanges: [{ start: '8819', end: '8819' }],
|
|
},
|
|
{
|
|
sruCode: '7419',
|
|
description: 'Mottagna koncernbidrag',
|
|
section: 'income_statement',
|
|
normalBalance: 'credit',
|
|
accountRanges: [{ start: '8820', end: '8820' }],
|
|
},
|
|
{
|
|
sruCode: '7524',
|
|
description: 'Lämnade koncernbidrag',
|
|
section: 'income_statement',
|
|
normalBalance: 'debit',
|
|
accountRanges: [{ start: '8830', end: '8830' }],
|
|
},
|
|
{
|
|
sruCode: '7421',
|
|
description: 'Förändring av överavskrivningar',
|
|
section: 'income_statement',
|
|
normalBalance: 'net',
|
|
// 8850 = grupp, 8851-8853 = per kategori (immateriella, byggnader, M&I)
|
|
accountRanges: [{ start: '8850', end: '8859' }],
|
|
},
|
|
{
|
|
sruCode: '7422',
|
|
description: 'Övriga bokslutsdispositioner',
|
|
section: 'income_statement',
|
|
normalBalance: 'net',
|
|
// 8840 = Lämnade gottgörelser, 8860-8899 = övriga
|
|
accountRanges: [
|
|
{ start: '8840', end: '8840' },
|
|
{ start: '8860', end: '8899' },
|
|
],
|
|
},
|
|
{
|
|
sruCode: '7528',
|
|
description: 'Skatt på årets resultat',
|
|
section: 'income_statement',
|
|
normalBalance: 'debit',
|
|
accountRanges: [{ start: '8900', end: '8989' }],
|
|
},
|
|
// 7450/7550 (årets resultat vinst/förlust) are calculated, not mapped from accounts
|
|
]
|
|
|
|
/**
|
|
* Check if an account number falls within a mapping's ranges
|
|
*/
|
|
export function isAccountInMapping(accountNumber: string, mapping: INK2AccountMapping): boolean {
|
|
for (const range of mapping.accountRanges) {
|
|
if (accountNumber >= range.start && accountNumber <= range.end) {
|
|
if (range.exclude && range.exclude.includes(accountNumber)) {
|
|
continue
|
|
}
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
/**
|
|
* Truncate to nearest krona (drop öre) per SFL 22 kap. 1 §
|
|
*/
|
|
function truncateToKrona(value: number): number {
|
|
return value >= 0 ? Math.floor(value) : Math.ceil(value)
|
|
}
|
|
|
|
/**
|
|
* Check if the balance sheet totals differ beyond the expected rounding tolerance.
|
|
*/
|
|
export function checkBalanceWarning(totalAssets: number, totalEquityLiabilities: number): string | null {
|
|
const balanceDiff = Math.abs(totalAssets - totalEquityLiabilities)
|
|
const ROUNDING_TOLERANCE_KR = 2
|
|
if (balanceDiff > ROUNDING_TOLERANCE_KR && (totalAssets > 0 || totalEquityLiabilities > 0)) {
|
|
return `Balansräkningen är inte i balans. Tillgångar: ${totalAssets} kr, Eget kapital och skulder: ${totalEquityLiabilities} kr (differens: ${balanceDiff} kr).`
|
|
}
|
|
return null
|
|
}
|
|
|
|
/** Create zero-initialized INK2R rutor */
|
|
function createEmptyINK2RRutor(): INK2RRutor {
|
|
return {
|
|
'7201': 0, '7202': 0, '7214': 0, '7215': 0, '7216': 0, '7217': 0,
|
|
'7230': 0, '7231': 0, '7233': 0, '7232': 0, '7234': 0, '7235': 0,
|
|
'7241': 0, '7242': 0, '7243': 0, '7244': 0, '7245': 0, '7246': 0,
|
|
'7251': 0, '7252': 0, '7261': 0, '7262': 0, '7263': 0,
|
|
'7270': 0, '7271': 0, '7281': 0,
|
|
'7301': 0, '7302': 0,
|
|
'7321': 0, '7322': 0, '7323': 0,
|
|
'7331': 0, '7332': 0, '7333': 0,
|
|
'7350': 0, '7351': 0, '7352': 0, '7353': 0, '7354': 0,
|
|
'7360': 0, '7361': 0, '7362': 0, '7363': 0, '7364': 0,
|
|
'7365': 0, '7366': 0, '7367': 0, '7369': 0, '7368': 0, '7370': 0,
|
|
'7410': 0, '7411': 0, '7412': 0, '7413': 0,
|
|
'7511': 0, '7512': 0, '7513': 0, '7514': 0, '7515': 0, '7516': 0, '7517': 0,
|
|
'7414': 0, '7415': 0, '7423': 0, '7416': 0, '7417': 0,
|
|
'7521': 0, '7522': 0,
|
|
'7524': 0, '7419': 0, '7420': 0, '7525': 0, '7421': 0, '7422': 0,
|
|
'7528': 0,
|
|
'7450': 0, '7550': 0,
|
|
}
|
|
}
|
|
|
|
// Reuse canonical code arrays from types.ts (single source of truth)
|
|
const ASSET_CODES = INK2R_ASSET_CODES
|
|
const EQUITY_LIABILITY_CODES = INK2R_EQUITY_LIABILITY_CODES
|
|
|
|
/**
|
|
* Generate INK2 declaration for a fiscal period
|
|
*/
|
|
export async function generateINK2Declaration(
|
|
supabase: SupabaseClient,
|
|
companyId: string,
|
|
fiscalPeriodId: string
|
|
): Promise<INK2Declaration> {
|
|
|
|
// Fetch fiscal period
|
|
const { data: period, error: periodError } = await supabase
|
|
.from('fiscal_periods')
|
|
.select('*')
|
|
.eq('id', fiscalPeriodId)
|
|
.eq('company_id', companyId)
|
|
.single()
|
|
|
|
if (periodError || !period) {
|
|
throw new Error('Fiscal period not found')
|
|
}
|
|
|
|
// Fetch company settings
|
|
const { data: settings } = await supabase
|
|
.from('company_settings')
|
|
.select('company_name, org_number, entity_type, address_line1, postal_code, city, email')
|
|
.eq('company_id', companyId)
|
|
.single()
|
|
|
|
// Resolve entity_type: prefer company_settings, fall back to companies table (NOT NULL, always reliable)
|
|
let entityType = settings?.entity_type
|
|
if (!entityType) {
|
|
const { data: company, error: companyError } = await supabase
|
|
.from('companies')
|
|
.select('entity_type')
|
|
.eq('id', companyId)
|
|
.single()
|
|
if (companyError) throw new Error(`Failed to resolve entity type: ${companyError.message}`)
|
|
entityType = company?.entity_type
|
|
}
|
|
|
|
if (entityType !== 'aktiebolag') {
|
|
throw new Error('INK2 declaration is only for aktiebolag (limited company)')
|
|
}
|
|
|
|
// Fetch all posted journal entries with lines for this period
|
|
const { data: entries, error: entriesError } = await supabase
|
|
.from('journal_entries')
|
|
.select('*, lines:journal_entry_lines(*)')
|
|
.eq('company_id', companyId)
|
|
.eq('fiscal_period_id', fiscalPeriodId)
|
|
.in('status', ['posted', 'reversed'])
|
|
|
|
if (entriesError) {
|
|
throw new Error(`Failed to fetch journal entries: ${entriesError.message}`)
|
|
}
|
|
|
|
// Fetch chart of accounts for account names
|
|
const accounts = await fetchAllRows<{ account_number: string; account_name: string }>(({ from, to }) =>
|
|
supabase
|
|
.from('chart_of_accounts')
|
|
.select('account_number, account_name')
|
|
.eq('company_id', companyId)
|
|
.range(from, to)
|
|
)
|
|
|
|
const accountNameMap = new Map<string, string>()
|
|
for (const acc of accounts) {
|
|
accountNameMap.set(acc.account_number, acc.account_name)
|
|
}
|
|
|
|
// Calculate balances per account (debit - credit)
|
|
const accountBalances = new Map<string, number>()
|
|
|
|
for (const entry of (entries as JournalEntry[]) || []) {
|
|
const lines = (entry.lines as JournalEntryLine[]) || []
|
|
for (const line of lines) {
|
|
const current = accountBalances.get(line.account_number) || 0
|
|
const netAmount = (Number(line.debit_amount) || 0) - (Number(line.credit_amount) || 0)
|
|
accountBalances.set(line.account_number, current + netAmount)
|
|
}
|
|
}
|
|
|
|
// Initialize INK2R rutor and breakdown
|
|
const ink2r = createEmptyINK2RRutor()
|
|
const allCodes = Object.keys(ink2r) as INK2RSRUCode[]
|
|
const breakdown = {} as INK2Declaration['breakdown']
|
|
for (const code of allCodes) {
|
|
breakdown[code] = { accounts: [], total: 0 }
|
|
}
|
|
|
|
const warnings: string[] = []
|
|
|
|
// Process each account balance against INK2R mappings
|
|
for (const [accountNumber, balance] of accountBalances) {
|
|
if (Math.abs(balance) < 0.01) continue
|
|
|
|
// Skip account 8999 — årets resultat is calculated
|
|
if (accountNumber === '8999') continue
|
|
|
|
let mapped = false
|
|
for (const mapping of INK2R_ACCOUNT_MAPPINGS) {
|
|
if (isAccountInMapping(accountNumber, mapping)) {
|
|
let amount: number
|
|
|
|
if (mapping.section === 'income_statement') {
|
|
// Income statement sign convention per Skatteverket INK2R:
|
|
// All amounts are reported as positive values on the form.
|
|
// Revenue (credit normal): balance is negative in ledger, negate → positive
|
|
// Cost (debit normal): balance is positive in ledger, keep → positive
|
|
// Net: negate so positive = income, negative = cost
|
|
if (mapping.normalBalance === 'credit') {
|
|
amount = -balance
|
|
} else if (mapping.normalBalance === 'debit') {
|
|
// Costs: debit balance is positive in ledger, keep positive (Skatteverket convention)
|
|
amount = balance
|
|
} else {
|
|
// Net: negate to match accounting convention
|
|
amount = -balance
|
|
}
|
|
} else {
|
|
// Balance sheet: all amounts reported as positive
|
|
if (mapping.normalBalance === 'debit') {
|
|
amount = balance
|
|
} else {
|
|
amount = -balance
|
|
}
|
|
}
|
|
|
|
ink2r[mapping.sruCode] += amount
|
|
|
|
breakdown[mapping.sruCode].accounts.push({
|
|
accountNumber,
|
|
accountName: accountNameMap.get(accountNumber) || `Konto ${accountNumber}`,
|
|
amount: truncateToKrona(amount),
|
|
})
|
|
|
|
mapped = true
|
|
break
|
|
}
|
|
}
|
|
|
|
if (!mapped) {
|
|
// BAS accounts 4500-4599, 4700-4899, and 1300-1310 have no standard SRU mapping
|
|
// These are unusual and may indicate custom accounts
|
|
const classChar = accountNumber.charAt(0)
|
|
if (classChar >= '1' && classChar <= '8') {
|
|
// Only warn for standard BAS range accounts that weren't mapped
|
|
warnings.push(`Konto ${accountNumber} (${accountNameMap.get(accountNumber) || 'okänt'}) kunde inte mappas till ett SRU-fält.`)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Truncate all INK2R rutor to whole kronor
|
|
for (const code of allCodes) {
|
|
ink2r[code] = truncateToKrona(ink2r[code])
|
|
breakdown[code].total = ink2r[code]
|
|
}
|
|
|
|
// Calculate totals
|
|
const totalAssets = ASSET_CODES.reduce((sum, code) => sum + ink2r[code], 0)
|
|
const totalEquityLiabilities = EQUITY_LIABILITY_CODES.reduce((sum, code) => sum + ink2r[code], 0)
|
|
|
|
// Operating result: revenue minus costs (costs are positive per Skatteverket convention)
|
|
const operatingResult =
|
|
ink2r['7410'] + ink2r['7411'] + ink2r['7412'] + ink2r['7413']
|
|
- ink2r['7511'] - ink2r['7512'] - ink2r['7513'] - ink2r['7514']
|
|
- ink2r['7515'] - ink2r['7516'] - ink2r['7517']
|
|
|
|
// Financial items: income minus costs
|
|
const financialItems =
|
|
ink2r['7414'] + ink2r['7415'] + ink2r['7423'] + ink2r['7416'] + ink2r['7417']
|
|
- ink2r['7521'] - ink2r['7522']
|
|
|
|
// Bokslutsdispositioner: subtract debit-normal, add credit-normal and net
|
|
const bokslutsdispositioner =
|
|
- ink2r['7524'] + ink2r['7419'] + ink2r['7420'] - ink2r['7525']
|
|
+ ink2r['7421'] + ink2r['7422']
|
|
|
|
// Result before tax
|
|
const resultBeforeTax = operatingResult + financialItems + bokslutsdispositioner
|
|
|
|
// Result after tax (7528 is positive, subtract it)
|
|
const resultAfterFinancial = resultBeforeTax - ink2r['7528']
|
|
|
|
// Set årets resultat: vinst (7450) or förlust (7550)
|
|
if (resultAfterFinancial >= 0) {
|
|
ink2r['7450'] = resultAfterFinancial
|
|
ink2r['7550'] = 0
|
|
} else {
|
|
ink2r['7450'] = 0
|
|
ink2r['7550'] = Math.abs(resultAfterFinancial)
|
|
}
|
|
|
|
// Add calculated result to fritt eget kapital for balance
|
|
// During open fiscal year, 2099 may have no balance — the result only exists
|
|
// as net of income statement accounts. Adding it here handles both cases.
|
|
const adjustedEquityLiabilities = totalEquityLiabilities + resultAfterFinancial
|
|
|
|
// Fiscal year dates as YYYYMMDD
|
|
const fyStart = (period.period_start as string).replace(/-/g, '')
|
|
const fyEnd = (period.period_end as string).replace(/-/g, '')
|
|
|
|
// Build INK2 (huvudblankett)
|
|
// Auto-derive from INK2S result (simplified: result + non-deductible tax)
|
|
// 7528 is already positive per Skatteverket convention
|
|
const taxAmount = ink2r['7528']
|
|
const taxableResult = resultAfterFinancial + taxAmount
|
|
|
|
const ink2: INK2Rutor = {
|
|
'7011': fyStart,
|
|
'7012': fyEnd,
|
|
'7113': taxableResult >= 0 ? taxableResult : 0,
|
|
'7114': taxableResult < 0 ? Math.abs(taxableResult) : 0,
|
|
}
|
|
|
|
// Build INK2S (skattemässiga justeringar — auto-derived basics only)
|
|
const ink2s: INK2SRutor = {
|
|
'7011': fyStart,
|
|
'7012': fyEnd,
|
|
'7650': resultAfterFinancial >= 0 ? resultAfterFinancial : 0,
|
|
'7750': resultAfterFinancial < 0 ? Math.abs(resultAfterFinancial) : 0,
|
|
'7651': taxAmount, // Skatt (ej avdragsgill)
|
|
'8020': taxableResult >= 0 ? taxableResult : 0,
|
|
'8021': taxableResult < 0 ? Math.abs(taxableResult) : 0,
|
|
}
|
|
|
|
// Add warnings
|
|
if (!(period as FiscalPeriod).is_closed) {
|
|
warnings.push('Räkenskapsåret är inte stängt — deklarationen kan genereras, men siffrorna kan ändras om fler bokföringar görs.')
|
|
}
|
|
|
|
if (totalAssets === 0 && totalEquityLiabilities === 0 && ink2r['7410'] === 0) {
|
|
warnings.push('Inga bokförda transaktioner hittades för perioden.')
|
|
}
|
|
|
|
const balanceWarning = checkBalanceWarning(totalAssets, adjustedEquityLiabilities)
|
|
if (balanceWarning) {
|
|
warnings.push(balanceWarning)
|
|
}
|
|
|
|
return {
|
|
fiscalYear: {
|
|
id: period.id,
|
|
name: period.name,
|
|
start: period.period_start,
|
|
end: period.period_end,
|
|
isClosed: period.is_closed,
|
|
},
|
|
ink2,
|
|
ink2r,
|
|
ink2s,
|
|
breakdown,
|
|
totals: {
|
|
totalAssets,
|
|
totalEquityLiabilities: adjustedEquityLiabilities,
|
|
operatingResult,
|
|
resultAfterFinancial,
|
|
},
|
|
companyInfo: {
|
|
companyName: settings?.company_name || 'Okänt företag',
|
|
orgNumber: settings?.org_number || null,
|
|
addressLine1: settings?.address_line1 || null,
|
|
postalCode: settings?.postal_code || null,
|
|
city: settings?.city || null,
|
|
email: settings?.email || null,
|
|
},
|
|
warnings,
|
|
}
|
|
}
|