feat: stage kontantmetod year-end cutoff (#1586)
* feat: stage kontantmetod year-end cutoff * fix: keep cutoff tool payload searchable * fix: trim year-end tool metadata
This commit is contained in:
@@ -0,0 +1,161 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
vi.mock('@/lib/core/bookkeeping/period-service', async () => {
|
||||
const actual = await vi.importActual<typeof import('@/lib/core/bookkeeping/period-service')>(
|
||||
'@/lib/core/bookkeeping/period-service',
|
||||
)
|
||||
return { ...actual, findNextPeriod: vi.fn() }
|
||||
})
|
||||
|
||||
vi.mock('@/lib/core/bookkeeping/kontantmetod-cutoff', async () => {
|
||||
const actual = await vi.importActual<
|
||||
typeof import('@/lib/core/bookkeeping/kontantmetod-cutoff')
|
||||
>('@/lib/core/bookkeeping/kontantmetod-cutoff')
|
||||
return { ...actual, assessKontantmetodCutoff: vi.fn() }
|
||||
})
|
||||
|
||||
import { tools, deriveToolMeta } from '../server'
|
||||
import { TOOL_SCOPE_MAP } from '@/lib/auth/api-keys'
|
||||
import { findNextPeriod } from '@/lib/core/bookkeeping/period-service'
|
||||
import {
|
||||
assessKontantmetodCutoff,
|
||||
buildCutoffLines,
|
||||
} from '@/lib/core/bookkeeping/kontantmetod-cutoff'
|
||||
|
||||
const tool = tools.find((candidate) => candidate.name === 'gnubok_post_kontantmetod_cutoff')!
|
||||
|
||||
function makeSupabase(settings: Record<string, unknown> = {
|
||||
accounting_method: 'cash', entity_type: 'aktiebolag',
|
||||
}) {
|
||||
const inserts: unknown[] = []
|
||||
const rows: Record<string, unknown> = {
|
||||
fiscal_periods: {
|
||||
id: 'fp-1', name: '2026', period_start: '2026-01-01', period_end: '2026-12-31',
|
||||
is_closed: false, locked_at: null,
|
||||
},
|
||||
company_settings: settings,
|
||||
pending_operations: { id: 'op-1' },
|
||||
}
|
||||
const from = vi.fn((table: string) => {
|
||||
const chain: Record<string, unknown> = {}
|
||||
for (const name of ['select', 'eq', 'in', 'gte', 'lte', 'order', 'limit']) {
|
||||
chain[name] = () => chain
|
||||
}
|
||||
chain.insert = (value: unknown) => {
|
||||
inserts.push(value)
|
||||
return chain
|
||||
}
|
||||
chain.maybeSingle = async () => ({ data: rows[table] ?? null, error: null })
|
||||
chain.single = async () => ({ data: rows[table] ?? null, error: null })
|
||||
chain.then = (resolve: (value: unknown) => unknown) =>
|
||||
resolve({ data: rows[table] ?? null, error: null })
|
||||
return chain
|
||||
})
|
||||
return { auth: {}, from, inserts }
|
||||
}
|
||||
|
||||
const collection = {
|
||||
receivables: [{
|
||||
id: 'inv-1', reference: 'F-1', vatTreatment: 'standard_25' as const,
|
||||
outstanding: 1250, vat: 250,
|
||||
}],
|
||||
payables: [{
|
||||
id: 'si-1', reference: 'L-1', outstanding: 625, vat: 125,
|
||||
netByAccount: [{ account: '5410', amount: 500 }],
|
||||
}],
|
||||
unknownVatTreatment: [],
|
||||
strayVatOnZeroRate: [],
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
vi.mocked(findNextPeriod).mockResolvedValue({
|
||||
id: 'fp-2', period_start: '2027-01-01', period_end: '2027-12-31',
|
||||
is_closed: false, locked_at: null,
|
||||
} as never)
|
||||
vi.mocked(assessKontantmetodCutoff).mockResolvedValue({
|
||||
collection,
|
||||
lines: buildCutoffLines(collection.receivables, collection.payables),
|
||||
postings: {
|
||||
complete: false, hasAny: false, receivableEntryId: null,
|
||||
receivableReversalId: null, payableEntryId: null, payableReversalId: null,
|
||||
missing: ['receivable', 'receivable_reversal', 'payable', 'payable_reversal'],
|
||||
duplicates: [],
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
describe('gnubok_post_kontantmetod_cutoff', () => {
|
||||
it('is a discoverable high-risk staged bookkeeping write with readiness preflight', () => {
|
||||
expect(tool).toBeDefined()
|
||||
expect(tool.catalogVisibility).toBe('search')
|
||||
expect(tool.annotations).toMatchObject({ readOnlyHint: false, destructiveHint: true })
|
||||
expect(TOOL_SCOPE_MAP.gnubok_post_kontantmetod_cutoff).toBe('bookkeeping:write')
|
||||
expect(deriveToolMeta(tool)).toMatchObject({
|
||||
requires_approval: true,
|
||||
approve_tool: 'gnubok_approve_pending_operation',
|
||||
preflight: 'gnubok_year_end_readiness',
|
||||
})
|
||||
})
|
||||
|
||||
it('stages the exact two cut-offs and two day-one reversals without posting', async () => {
|
||||
const supabase = makeSupabase()
|
||||
const result = (await tool.execute(
|
||||
{ fiscal_period_id: 'fp-1' },
|
||||
'company-1', 'user-1', supabase as never, { type: 'api_key' },
|
||||
)) as { staged: boolean; risk_level: string; preview: { entries: Array<Record<string, unknown>> } }
|
||||
|
||||
expect(result.staged).toBe(true)
|
||||
expect(result.risk_level).toBe('high')
|
||||
expect(result.preview.entries).toHaveLength(4)
|
||||
expect(result.preview.entries.map((entry) => entry.entry_date)).toEqual([
|
||||
'2026-12-31', '2027-01-01', '2026-12-31', '2027-01-01',
|
||||
])
|
||||
expect(result.preview.entries[0]?.lines).toEqual(buildCutoffLines(collection.receivables, []).receivableLines)
|
||||
expect(supabase.inserts).toHaveLength(1)
|
||||
expect(supabase.inserts[0]).toMatchObject({
|
||||
operation_type: 'post_kontantmetod_cutoff',
|
||||
risk_level: 'high',
|
||||
params: { fiscal_period_id: 'fp-1', next_fiscal_period_id: 'fp-2', collection },
|
||||
})
|
||||
})
|
||||
|
||||
it('refuses accrual companies, missing next periods, invalid VAT, duplicates, and empty previews', async () => {
|
||||
const accrual = makeSupabase({ accounting_method: 'accrual', entity_type: 'aktiebolag' })
|
||||
await expect(tool.execute(
|
||||
{ fiscal_period_id: 'fp-1' }, 'company-1', 'user-1', accrual as never,
|
||||
)).rejects.toThrow(/inte kontantmetoden/i)
|
||||
|
||||
vi.mocked(findNextPeriod).mockResolvedValueOnce(null)
|
||||
await expect(tool.execute(
|
||||
{ fiscal_period_id: 'fp-1' }, 'company-1', 'user-1', makeSupabase() as never,
|
||||
)).rejects.toThrow(/nästa räkenskapsår/i)
|
||||
|
||||
vi.mocked(assessKontantmetodCutoff).mockResolvedValueOnce({
|
||||
collection: { ...collection, unknownVatTreatment: ['F-9'] },
|
||||
lines: buildCutoffLines([], []),
|
||||
postings: { complete: false, hasAny: false, receivableEntryId: null, receivableReversalId: null, payableEntryId: null, payableReversalId: null, missing: [], duplicates: [] },
|
||||
})
|
||||
await expect(tool.execute(
|
||||
{ fiscal_period_id: 'fp-1' }, 'company-1', 'user-1', makeSupabase() as never,
|
||||
)).rejects.toThrow(/saknar momsinställning/i)
|
||||
|
||||
vi.mocked(assessKontantmetodCutoff).mockResolvedValueOnce({
|
||||
collection,
|
||||
lines: buildCutoffLines(collection.receivables, collection.payables),
|
||||
postings: { complete: true, hasAny: true, receivableEntryId: 'je-1', receivableReversalId: 'je-2', payableEntryId: 'je-3', payableReversalId: 'je-4', missing: [], duplicates: [] },
|
||||
})
|
||||
await expect(tool.execute(
|
||||
{ fiscal_period_id: 'fp-1' }, 'company-1', 'user-1', makeSupabase() as never,
|
||||
)).rejects.toThrow(/redan bokförd/i)
|
||||
|
||||
vi.mocked(assessKontantmetodCutoff).mockResolvedValueOnce({
|
||||
collection: { receivables: [], payables: [], unknownVatTreatment: [], strayVatOnZeroRate: [] },
|
||||
lines: buildCutoffLines([], []),
|
||||
postings: { complete: true, hasAny: false, receivableEntryId: null, receivableReversalId: null, payableEntryId: null, payableReversalId: null, missing: [], duplicates: [] },
|
||||
})
|
||||
await expect(tool.execute(
|
||||
{ fiscal_period_id: 'fp-1' }, 'company-1', 'user-1', makeSupabase() as never,
|
||||
)).rejects.toThrow(/Inga obetalda/i)
|
||||
})
|
||||
})
|
||||
@@ -67,7 +67,7 @@ describe('gnubok_year_end_readiness: registration', () => {
|
||||
)
|
||||
// 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)
|
||||
expect(actionable.length).toBe(8)
|
||||
for (const kind of actionable) {
|
||||
expect(tool.description, `blocker kind ${kind} missing from description`).toContain(kind)
|
||||
}
|
||||
|
||||
@@ -156,8 +156,15 @@ import {
|
||||
validateVoucherForSupplierInvoiceLink,
|
||||
} from '@/lib/invoices/supplier-voucher-matching'
|
||||
import { findFiscalPeriod, reverseEntry, validateBalance } from '@/lib/bookkeeping/engine'
|
||||
import { closePeriod, countUnbookedInPeriod, lockPeriod, resolvePeriodStatusForDate, type PeriodStatusForDate } from '@/lib/core/bookkeeping/period-service'
|
||||
import { closePeriod, countUnbookedInPeriod, findNextPeriod, lockPeriod, resolvePeriodStatusForDate, type PeriodStatusForDate } from '@/lib/core/bookkeeping/period-service'
|
||||
import { validateYearEndReadiness, previewYearEndClosing } from '@/lib/core/bookkeeping/year-end-service'
|
||||
import {
|
||||
assessKontantmetodCutoff,
|
||||
hasIncompleteKontantmetodCutoffPair,
|
||||
KONTANTMETOD_CUTOFF_DESCRIPTIONS,
|
||||
nextDay,
|
||||
reverseLines,
|
||||
} from '@/lib/core/bookkeeping/kontantmetod-cutoff'
|
||||
import { generateSIEExport } from '@/lib/reports/sie-export'
|
||||
import { generateFullArchive, estimateArchiveSize } from '@/lib/reports/full-archive-export'
|
||||
import { bookkeepingErrorResponse } from '@/lib/bookkeeping/errors'
|
||||
@@ -1144,6 +1151,7 @@ const STAGED_OPERATION_SCHEMA = {
|
||||
*/
|
||||
const TOOL_PREFLIGHT_MAP: Record<string, string> = {
|
||||
gnubok_run_year_end: 'gnubok_year_end_readiness',
|
||||
gnubok_post_kontantmetod_cutoff: 'gnubok_year_end_readiness',
|
||||
gnubok_vat_declaration_submit: 'gnubok_vat_declaration_validate',
|
||||
gnubok_post_annual_depreciation: 'gnubok_propose_annual_depreciation',
|
||||
gnubok_book_salary_run: 'gnubok_get_salary_run',
|
||||
@@ -1761,6 +1769,8 @@ export const YEAR_END_BLOCKER_KIND: Record<YearEndBlockerCode, string> = {
|
||||
TRIAL_BALANCE_UNBALANCED: 'trial_balance_unbalanced',
|
||||
CONTINUITY_MISMATCH: 'opening_balance_continuity',
|
||||
NEXT_PERIOD_HAS_IB: 'next_period_ib_posted',
|
||||
KONTANTMETOD_CUTOFF_REQUIRED: 'kontantmetod_cutoff_required',
|
||||
KONTANTMETOD_CUTOFF_CHECK_FAILED: 'kontantmetod_cutoff_required',
|
||||
UNBOOKED_TRANSACTIONS: 'unbooked_transactions',
|
||||
UNBOOKED_CHECK_FAILED: 'unbooked_transactions',
|
||||
}
|
||||
@@ -13132,7 +13142,7 @@ export const tools: McpTool[] = [
|
||||
// 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.",
|
||||
description: 'Year-end check. Blockers: kontantmetod_cutoff_required, 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,
|
||||
@@ -13236,6 +13246,189 @@ export const tools: McpTool[] = [
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: 'gnubok_post_kontantmetod_cutoff',
|
||||
title: 'Post Cash-Method Year-End Cut-Off',
|
||||
description: 'Stage the exact year-end receivable/payable cut-off and next-period reversals required for kontantmetoden. Review all proposed lines, then approve with confirmed=true.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
fiscal_period_id: {
|
||||
type: 'string',
|
||||
description: 'UUID of the cash-method fiscal period being closed',
|
||||
},
|
||||
idempotency_key: {
|
||||
type: 'string',
|
||||
description: 'Optional UUID to dedupe retries of the same preview and staged operation',
|
||||
},
|
||||
},
|
||||
required: ['fiscal_period_id'],
|
||||
},
|
||||
outputSchema: STAGED_OPERATION_SCHEMA,
|
||||
annotations: {
|
||||
readOnlyHint: false,
|
||||
destructiveHint: true,
|
||||
idempotentHint: false,
|
||||
openWorldHint: false,
|
||||
},
|
||||
// Specialized cash-method year-end step. The year-end readiness blocker
|
||||
// and year-end skill name it exactly, while search-only visibility avoids
|
||||
// charging every MCP session for a schema most companies never need.
|
||||
catalogVisibility: 'search',
|
||||
async execute(args, companyId, userId, supabase, actor) {
|
||||
const fiscalPeriodId = args.fiscal_period_id as string
|
||||
if (!fiscalPeriodId) throw new Error('fiscal_period_id is required')
|
||||
|
||||
const [{ data: period }, { data: settings }] = await Promise.all([
|
||||
supabase
|
||||
.from('fiscal_periods')
|
||||
.select('id, name, period_start, period_end, is_closed, locked_at')
|
||||
.eq('id', fiscalPeriodId)
|
||||
.eq('company_id', companyId)
|
||||
.maybeSingle(),
|
||||
supabase
|
||||
.from('company_settings')
|
||||
.select('accounting_method, entity_type')
|
||||
.eq('company_id', companyId)
|
||||
.maybeSingle(),
|
||||
])
|
||||
|
||||
if (!period) throw new Error('Fiscal period not found')
|
||||
if (period.is_closed || period.locked_at) {
|
||||
throw new Error('Räkenskapsperioden är stängd eller låst')
|
||||
}
|
||||
if (settings?.accounting_method !== 'cash') {
|
||||
throw new Error('Företaget använder inte kontantmetoden')
|
||||
}
|
||||
|
||||
const nextPeriod = await findNextPeriod(supabase, companyId, fiscalPeriodId)
|
||||
if (!nextPeriod) {
|
||||
throw new Error(
|
||||
'Kontantmetodens bokslutsavgränsning kräver att nästa räkenskapsår är upplagt: vändningarna bokas första dagen på det nya året.',
|
||||
)
|
||||
}
|
||||
if (nextPeriod.is_closed || nextPeriod.locked_at) {
|
||||
throw new Error(
|
||||
'Nästa räkenskapsår är stängt eller låst: vändningarna kan inte bokföras. Lås upp perioden och försök igen.',
|
||||
)
|
||||
}
|
||||
|
||||
const assessment = await assessKontantmetodCutoff(
|
||||
supabase,
|
||||
companyId,
|
||||
period,
|
||||
nextPeriod.id,
|
||||
(settings.entity_type ?? 'aktiebolag') as EntityType,
|
||||
)
|
||||
if (assessment.collection.unknownVatTreatment.length > 0) {
|
||||
throw new Error(
|
||||
`${assessment.collection.unknownVatTreatment.length} fakturor saknar momsinställning: ` +
|
||||
`${assessment.collection.unknownVatTreatment.slice(0, 10).join(', ')}. Komplettera dem och försök igen.`,
|
||||
)
|
||||
}
|
||||
if (assessment.collection.strayVatOnZeroRate.length > 0) {
|
||||
throw new Error(
|
||||
`${assessment.collection.strayVatOnZeroRate.length} fakturor har moms trots en momsfri momsinställning: ` +
|
||||
`${assessment.collection.strayVatOnZeroRate.slice(0, 10).join(', ')}. Rätta dem och försök igen.`,
|
||||
)
|
||||
}
|
||||
if (
|
||||
assessment.lines.receivableLines.length === 0 &&
|
||||
assessment.lines.payableLines.length === 0
|
||||
) {
|
||||
throw new Error('Inga obetalda kund- eller leverantörsfakturor finns vid periodens slut')
|
||||
}
|
||||
if (
|
||||
assessment.postings.complete ||
|
||||
hasIncompleteKontantmetodCutoffPair(assessment.postings, assessment.lines)
|
||||
) {
|
||||
throw new Error(
|
||||
'Kontantmetodens bokslutsavgränsning är redan bokförd eller delvis bokförd för perioden. Kontrollera verifikaten innan du försöker igen.',
|
||||
)
|
||||
}
|
||||
|
||||
const reversalDate = nextDay(period.period_end)
|
||||
const entries = [
|
||||
...(assessment.lines.receivableLines.length > 0 &&
|
||||
!assessment.postings.receivableEntryId &&
|
||||
!assessment.postings.receivableReversalId
|
||||
? [
|
||||
{
|
||||
kind: 'receivable_cutoff',
|
||||
fiscal_period_id: fiscalPeriodId,
|
||||
entry_date: period.period_end,
|
||||
description: KONTANTMETOD_CUTOFF_DESCRIPTIONS.receivable,
|
||||
total: assessment.lines.receivableTotal,
|
||||
lines: assessment.lines.receivableLines,
|
||||
},
|
||||
{
|
||||
kind: 'receivable_reversal',
|
||||
fiscal_period_id: nextPeriod.id,
|
||||
entry_date: reversalDate,
|
||||
description: KONTANTMETOD_CUTOFF_DESCRIPTIONS.receivableReversal,
|
||||
total: assessment.lines.receivableTotal,
|
||||
lines: reverseLines(assessment.lines.receivableLines),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(assessment.lines.payableLines.length > 0 &&
|
||||
!assessment.postings.payableEntryId &&
|
||||
!assessment.postings.payableReversalId
|
||||
? [
|
||||
{
|
||||
kind: 'payable_cutoff',
|
||||
fiscal_period_id: fiscalPeriodId,
|
||||
entry_date: period.period_end,
|
||||
description: KONTANTMETOD_CUTOFF_DESCRIPTIONS.payable,
|
||||
total: assessment.lines.payableTotal,
|
||||
lines: assessment.lines.payableLines,
|
||||
},
|
||||
{
|
||||
kind: 'payable_reversal',
|
||||
fiscal_period_id: nextPeriod.id,
|
||||
entry_date: reversalDate,
|
||||
description: KONTANTMETOD_CUTOFF_DESCRIPTIONS.payableReversal,
|
||||
total: assessment.lines.payableTotal,
|
||||
lines: reverseLines(assessment.lines.payableLines),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
]
|
||||
|
||||
return stagePendingOperation(
|
||||
supabase,
|
||||
companyId,
|
||||
userId,
|
||||
'post_kontantmetod_cutoff',
|
||||
`Kontantmetodens bokslutsavgränsning: ${period.name}`,
|
||||
{
|
||||
fiscal_period_id: fiscalPeriodId,
|
||||
next_fiscal_period_id: nextPeriod.id,
|
||||
collection: assessment.collection,
|
||||
},
|
||||
{
|
||||
fiscal_period_id: fiscalPeriodId,
|
||||
next_fiscal_period_id: nextPeriod.id,
|
||||
receivable_count: assessment.collection.receivables.length,
|
||||
payable_count: assessment.collection.payables.length,
|
||||
entries,
|
||||
},
|
||||
actor,
|
||||
{
|
||||
description: 'Re-run year-end readiness after the cut-off and all reversals are posted.',
|
||||
tool: 'gnubok_year_end_readiness',
|
||||
args: { fiscal_period_id: fiscalPeriodId },
|
||||
},
|
||||
{
|
||||
idempotencyKey:
|
||||
typeof args.idempotency_key === 'string' ? args.idempotency_key : undefined,
|
||||
dateForPeriodCheck: period.period_end,
|
||||
},
|
||||
)
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: 'gnubok_run_year_end',
|
||||
title: 'Run Year-End Closing (Bokslut)',
|
||||
@@ -16800,7 +16993,7 @@ export async function handleMcpRequest(request: Request): Promise<Response> {
|
||||
'• VAT: gnubok_get_vat_report(period_type, year, period). Ruta49 = VAT to pay (positive) or refund (negative). Pass render_ui=true to open the momsdeklaration review widget (claude.ai / Desktop). gnubok_vat_close_check reports filing-readiness blockers.',
|
||||
'• Reporting: gnubok_get_trial_balance / _income_statement / _balance_sheet / _kpi_report / _ar_ledger / _supplier_ledger: all default to the most recent fiscal period. For account roll-ups use gnubok_get_general_ledger; for ad-hoc line queries (free-text, amount/date/source filters) use gnubok_query_journal.',
|
||||
'• Interactive review UIs (claude.ai / Claude Desktop only): gnubok_get_vat_report(render_ui=true) renders the VAT widget, gnubok_receipt_matcher opens the receipt↔transaction matcher, and gnubok_list_pending_operations(render_ui=true) opens the approval queue where the user approves/rejects with a click. All also return structured data; other clients ignore the UI and use the data.',
|
||||
'• Year-end: gnubok_lock_period → gnubok_run_year_end → gnubok_set_opening_balances → gnubok_close_period. Each stages for human approval; closing is irreversible per BFL.',
|
||||
'• Year-end: run gnubok_year_end_readiness first. For kontantmetoden, resolve kontantmetod_cutoff_required with the searchable gnubok_post_kontantmetod_cutoff tool. Then gnubok_run_year_end → gnubok_set_opening_balances → gnubok_close_period. Each write stages for human approval; closing is irreversible per BFL.',
|
||||
'• Payroll: gnubok_create_salary_run → gnubok_calculate_salary_run → gnubok_book_salary_run → gnubok_generate_agi.',
|
||||
'• Reviewing & approving staged operations: gnubok_list_pending_operations shows the queue. When the user explicitly authorises a specific operation_id in chat, call gnubok_approve_pending_operation to commit. Use gnubok_reject_pending_operation to discard.',
|
||||
'',
|
||||
|
||||
@@ -28,15 +28,23 @@ Before running year-end, post any year-end adjusting entries via the web app:
|
||||
|
||||
These are not staged via MCP today: direct in web UI. The skill is to remind the user.
|
||||
|
||||
### Step 2: Currency revaluation (if multi-currency)
|
||||
### Step 2: Cash-method cut-off (kontantmetoden only)
|
||||
|
||||
For a company using kontantmetoden, first run
|
||||
|
||||
\`gnubok_post_kontantmetod_cutoff({ fiscal_period_id })\`.
|
||||
|
||||
It stages the exact customer-receivable and supplier-payable entries dated on the fiscal year end, plus their reversals on day one of the next period. Review every proposed line and approve with \`confirmed=true\`. The next fiscal period must already exist and be open. Re-run \`gnubok_year_end_readiness\` after approval: BFL 5 kap 2 § makes this a blocker, not an optional reminder.
|
||||
|
||||
### Step 3: Currency revaluation (if multi-currency)
|
||||
|
||||
If the company has open foreign-currency receivables/payables (1510/2440 in EUR/USD/etc.), revalue to closing-date FX rate via \`gnubok_run_currency_revaluation({ fiscal_period_id, closing_date })\`. Posts to **3960** (kursvinster) and **7960** (kursförluster). One revaluation per period.
|
||||
|
||||
### Step 3: Lock the period
|
||||
### Step 4: Lock the period
|
||||
|
||||
\`gnubok_lock_period(fiscal_period_id)\`. Required before year-end. Refuses if business transactions are unbooked.
|
||||
|
||||
### Step 4: Run year-end
|
||||
### Step 5: Run year-end
|
||||
|
||||
\`gnubok_run_year_end(fiscal_period_id)\`: stages a high-risk operation. After approval:
|
||||
|
||||
@@ -44,11 +52,11 @@ If the company has open foreign-currency receivables/payables (1510/2440 in EUR/
|
||||
- Period flagged \`is_year_end_complete\`
|
||||
- Next period created automatically
|
||||
|
||||
### Step 5: Set opening balances
|
||||
### Step 6: Set opening balances
|
||||
|
||||
\`gnubok_set_opening_balances({ closed_period_id, next_period_id })\`. Copies class 1-2 closing balances into the next period as opening balances. Stage → approve.
|
||||
|
||||
### Step 6: Close (final, irreversible)
|
||||
### Step 7: Close (final, irreversible)
|
||||
|
||||
\`gnubok_close_period(fiscal_period_id)\`. Once approved, the period is sealed forever. **No more entries possible, not even via storno.**
|
||||
|
||||
@@ -85,6 +93,7 @@ These compute with \`gnubok_get_kpi_report\` for inputs but the actual tax JE is
|
||||
## Tools
|
||||
|
||||
- \`gnubok_lock_period\`: pre-flight before year-end
|
||||
- \`gnubok_post_kontantmetod_cutoff\`: stage the mandatory cash-method cut-off and reversals
|
||||
- \`gnubok_run_year_end\`: zero result accounts
|
||||
- \`gnubok_set_opening_balances\`: seed next period
|
||||
- \`gnubok_run_currency_revaluation\`: FX revaluation
|
||||
|
||||
Reference in New Issue
Block a user