fix(bokslut): derive preview netResult from the 2099/2010 closing amount (#1045)
The Arets resultat summary card on the bokslut preview step read its figure from generateIncomeStatement, which excludes entries tagged source_type='year_end'. Bokslut-flow entries (annual depreciation, bokslutsdispositioner) carry that tag, so the card showed the pre-depreciation result while the bokslutsverifikation table below it (built from the unfiltered trial balance) included depreciation in the 2099 balancing line. previewYearEndClosing now derives netResult from the closing-lines totals before the balancing line is appended: it equals, by construction, the signed amount transferred to 2099 (AB) or 2010 (EF); positive = credit = vinst, negative = debit = forlust. The posted verifikat is unchanged: executeYearEndClosing only consumes preview.closingLines, never netResult. Fixes #766 Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
88f53350de
commit
03fd1b60b7
@@ -190,3 +190,4 @@ One line per decision: `[YYYY-MM-DD] <decision>: <why>`. Appended by agents and
|
||||
[2026-07-17] Settings PUT keeps update(body) without a pick() allow-list: UpdateSettingsSchema is the write boundary (z.object strips unknown keys; company_id/onboarding_complete are not in the schema, org_number is deleted post-onboarding), so the mass-assignment finding does not apply.
|
||||
[2026-07-17] Turning vat_registered off (or vat_has_eu_trade off) now coerces the dependent flags (vat_taxable_base_over_40m, vat_has_eu_trade, periodisk_sammanstallning_enabled) to false server-side instead of 400-ing on the stale stored combination; explicitly enabling PS without registration or EU trade still 400s. PS period/filing-method preferences are deliberately preserved while PS is disabled (they are inert until re-enabled).
|
||||
[2026-07-17] Kept the cron's cross-tenant company_settings/deadlines scans on the service client: a daily all-company repair job is inherently cross-tenant, is cron-secret-gated, and per-company scoping would turn one paginated query into N queries; the per-company writes remain scoped by company_id inside the generator.
|
||||
[2026-07-17] YearEndPreview.netResult now derived from the closing-lines totals (the signed 2099/2010 transfer) instead of generateIncomeStatement: the income statement excludes source_type='year_end' entries, so bokslut-flow depreciation/dispositioner were missing from the preview summary card while the bokslutsverifikation table included them (issue #766); patching the income statement instead was rejected because its exclusion is load-bearing (post-closing RR would collapse to zero) and executeYearEndClosing never reads netResult.
|
||||
|
||||
@@ -407,8 +407,10 @@ describe('previewYearEndClosing', () => {
|
||||
{ data: { period_end: '2024-12-31' }, error: null },
|
||||
]
|
||||
|
||||
// Deliberately different from the trial-balance-derived result: netResult
|
||||
// must NOT come from the income statement anymore (issue #766).
|
||||
vi.mocked(generateIncomeStatement).mockResolvedValue({
|
||||
net_result: 150000,
|
||||
net_result: 999,
|
||||
} as never)
|
||||
|
||||
vi.mocked(generateTrialBalance).mockResolvedValue({
|
||||
@@ -426,10 +428,62 @@ describe('previewYearEndClosing', () => {
|
||||
const preview = await previewYearEndClosing(supabase as never, 'company-1', 'user-1', 'fp-1')
|
||||
|
||||
expect(preview.netResult).toBe(150000)
|
||||
expect(generateIncomeStatement).not.toHaveBeenCalled()
|
||||
expect(preview.closingAccount).toBe('2099')
|
||||
expect(preview.closingAccountName).toBe('Årets resultat')
|
||||
expect(preview.closingLines.length).toBeGreaterThanOrEqual(3)
|
||||
expect(preview.resultAccountSummary).toHaveLength(3)
|
||||
|
||||
// Profit: the 2099 line is a credit equal to netResult.
|
||||
const closingLine2099 = preview.closingLines.find((l) => l.account_number === '2099')
|
||||
expect(closingLine2099).toBeDefined()
|
||||
expect(closingLine2099?.debit_amount).toBe(0)
|
||||
expect(closingLine2099?.credit_amount).toBe(preview.netResult)
|
||||
})
|
||||
|
||||
it('includes year_end-tagged depreciation in netResult and matches the 2099 line (issue #766)', async () => {
|
||||
results = [
|
||||
// 0: fetch company_settings (.single)
|
||||
{ data: { entity_type: 'aktiebolag' }, error: null },
|
||||
// 1: fetch fiscal period for closing date (.single)
|
||||
{ data: { period_end: '2024-12-31' }, error: null },
|
||||
]
|
||||
|
||||
// Old behavior took netResult from the income statement, which excludes
|
||||
// source_type='year_end' entries: it would have reported the
|
||||
// pre-depreciation loss of 10 000. The mock returns that stale value to
|
||||
// prove the service no longer uses it.
|
||||
vi.mocked(generateIncomeStatement).mockResolvedValue({
|
||||
net_result: -10000,
|
||||
} as never)
|
||||
|
||||
// Trial balance WITHOUT excludeYearEndClosing sees the bokslut-flow
|
||||
// depreciation verifikat (78xx, source_type='year_end').
|
||||
vi.mocked(generateTrialBalance).mockResolvedValue({
|
||||
rows: [
|
||||
{ account_number: '3001', account_name: 'Tjänsteintäkter', account_class: 3, closing_debit: 0, closing_credit: 90000 },
|
||||
{ account_number: '5010', account_name: 'Lokalhyra', account_class: 5, closing_debit: 100000, closing_credit: 0 },
|
||||
{ account_number: '7832', account_name: 'Avskrivningar inventarier', account_class: 7, closing_debit: 2000, closing_credit: 0 },
|
||||
],
|
||||
isBalanced: true,
|
||||
totalDebit: 102000,
|
||||
totalCredit: 90000,
|
||||
} as never)
|
||||
|
||||
const supabase = makeClient()
|
||||
const preview = await previewYearEndClosing(supabase as never, 'company-1', 'user-1', 'fp-1')
|
||||
|
||||
// Loss including depreciation: 90 000 - 100 000 - 2 000 = -12 000,
|
||||
// not the pre-depreciation -10 000.
|
||||
expect(preview.netResult).toBe(-12000)
|
||||
|
||||
// The summary figure equals the signed amount on the 2099 balancing line:
|
||||
// a loss is a debit to 2099.
|
||||
const closingLine2099 = preview.closingLines.find((l) => l.account_number === '2099')
|
||||
expect(closingLine2099).toBeDefined()
|
||||
expect(closingLine2099?.debit_amount).toBe(12000)
|
||||
expect(closingLine2099?.credit_amount).toBe(0)
|
||||
expect(preview.netResult).toBe(-(closingLine2099?.debit_amount ?? NaN))
|
||||
})
|
||||
|
||||
it('uses 2010 for EF entity type', async () => {
|
||||
|
||||
@@ -6,7 +6,6 @@ import { createLogger } from '@/lib/logger'
|
||||
|
||||
const log = createLogger('year-end-service')
|
||||
import { generateTrialBalance } from '@/lib/reports/trial-balance'
|
||||
import { generateIncomeStatement } from '@/lib/reports/income-statement'
|
||||
import { lockPeriod, closePeriod, createNextPeriod, findNextPeriod } from './period-service'
|
||||
import { generateResultAppropriation } from './result-appropriation-service'
|
||||
import {
|
||||
@@ -310,10 +309,6 @@ export async function previewYearEndClosing(
|
||||
? 'Eget kapital'
|
||||
: 'Årets resultat'
|
||||
|
||||
// Get income statement for net result
|
||||
const incomeStatement = await generateIncomeStatement(supabase, companyId, fiscalPeriodId)
|
||||
const netResult = incomeStatement.net_result
|
||||
|
||||
// Get trial balance for individual account balances in class 3-8
|
||||
const { rows } = await generateTrialBalance(supabase, companyId, fiscalPeriodId)
|
||||
const resultAccounts = rows.filter(
|
||||
@@ -361,6 +356,16 @@ export async function previewYearEndClosing(
|
||||
// If negative (loss): debit to equity (2099/2010)
|
||||
const totalClosingDebit = closingLines.reduce((sum, l) => sum + l.debit_amount, 0)
|
||||
const totalClosingCredit = closingLines.reduce((sum, l) => sum + l.credit_amount, 0)
|
||||
|
||||
// netResult must equal, by construction, the signed amount transferred to the
|
||||
// closing account (2099/2010) by the balancing line below: positive = credit
|
||||
// = vinst, negative = debit = forlust. It is deliberately NOT taken from
|
||||
// generateIncomeStatement: that report excludes source_type='year_end'
|
||||
// entries, and bokslut-flow entries (annual depreciation, dispositioner)
|
||||
// carry that tag, so the income-statement figure misses them and the
|
||||
// summary card would mismatch the bokslutsverifikation table (issue #766).
|
||||
const netResult = roundOre(totalClosingDebit - totalClosingCredit)
|
||||
|
||||
const balancingAmount = roundOre(Math.abs(totalClosingDebit - totalClosingCredit))
|
||||
|
||||
if (balancingAmount > ORE_TOLERANCE) {
|
||||
|
||||
Reference in New Issue
Block a user