fix(sie): selectable IB voucher series, smarter IB toggle on re-import, orphan-IB guard (#1896)
* fix(sie): selectable IB voucher series that never collides with the file's numbering The Ingående balanser voucher was hardcoded to series A and created before the file's vouchers, so it consumed the A series' next number and shifted every imported A voucher one number higher than in the source system (issue #1882). - IB voucher series is now selectable in the import wizard; the default is the first of M,O,P,Q,R,S,T,V,W,X,Y,Z not used by the file's #VER records (M matches the existing migration-adjustment series). - Plumbed end to end: wizard -> /api/import/sie/execute -> executeSIEImport, v1 REST options.openingBalanceSeries, MCP gnubok_import_sie opening_balance_series -> commitImportSie. - The wizard's 'Importera ingående balanser' toggle now defaults OFF when a posted IB voucher already exists inside the file's fiscal year, with a hint saying why. - Orphan-IB guard in executeSIEImport: replace_sie_import deletes only source_type='import' entries and clears the period's OB pointer, so a prior import's IB voucher survived every replace cycle and each re-import created another one (field report: five accumulated). The import now skips IB creation with a warning when a posted opening_balance entry already exists in the period. - MCP import_opening_balances default (false) vs web (true) documented as deliberate in the tool schema and DECISIONS.md. Fixes #1882 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(sie): harden IB series fix after skeptic review (relink orphan, exclude fallback series, type-check option) Skeptic findings on PR #1896, all four blocking items: - Orphan-IB guard now relinks a single surviving opening-balance voucher as the period's OB entry (permitted by the immutability trigger while the pointer is NULL): without it, reports showed IB 0, year-end's duplicate-IB blocker never armed, and the manual IB flow could double-book. It also diffs the survivor's lines against the file's IB and calls out stale amounts in the warning instead of keeping them silently; reverseEntry clears the pointer again for the storno-then-reimport path. - Series-less #VER records resolve to the transaction fallback series at import time, so the IB default picker now treats that series as used by the file (the same #1882 shift pattern through the fallback). The wizard recomputes its IB default with the effective transaction series once loaded. - openingBalanceSeries is type-checked on the web execute route, the MCP stage, and the staged-operation commit: a non-string falls back to the default instead of crashing mid-import after side effects. - The wizard's IB series select flags series used by the file and shows an attention line when the chosen series collides; the engine warns when an explicitly chosen series collides with the file's series (the choice is honored). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(sie): uppercase caller-chosen IB series before persisting Swedish accounting review on PR #1896: a lowercase series from v1 or MCP was persisted as-is, booking a case-distinct parallel series next to its uppercase sibling (BFL 5 kap requires one systematic series) and slipping past the file-collision warning. Normalize centrally in executeSIEImport, the single funnel for web, v1, and MCP. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
79013cf092
commit
c6f2bebab9
@@ -234,4 +234,57 @@ describe('gnubok_import_sie: update_account_names staging', () => {
|
||||
|
||||
expect((staged[0].params as Record<string, unknown>).update_account_names).toBe(false)
|
||||
})
|
||||
|
||||
it('passes opening_balance_series through to the staged params (issue #1882)', async () => {
|
||||
const { supabase, staged } = buildCapturingSupabase()
|
||||
|
||||
await importSie.execute(
|
||||
{
|
||||
file_content: VALID_SIE,
|
||||
filename: 'bok.se',
|
||||
mappings: COVER_VALID_SIE,
|
||||
import_opening_balances: true,
|
||||
opening_balance_series: 'K',
|
||||
},
|
||||
'company-1',
|
||||
'user-1',
|
||||
supabase as never,
|
||||
{ type: 'api_key' },
|
||||
)
|
||||
|
||||
expect((staged[0].params as Record<string, unknown>).opening_balance_series).toBe('K')
|
||||
})
|
||||
|
||||
it('drops a non-string opening_balance_series instead of staging it (hosts do not always enforce inputSchema)', async () => {
|
||||
const { supabase, staged } = buildCapturingSupabase()
|
||||
|
||||
await importSie.execute(
|
||||
{
|
||||
file_content: VALID_SIE,
|
||||
filename: 'bok.se',
|
||||
mappings: COVER_VALID_SIE,
|
||||
opening_balance_series: 123,
|
||||
},
|
||||
'company-1',
|
||||
'user-1',
|
||||
supabase as never,
|
||||
{ type: 'api_key' },
|
||||
)
|
||||
|
||||
expect((staged[0].params as Record<string, unknown>).opening_balance_series).toBeUndefined()
|
||||
})
|
||||
|
||||
it('leaves opening_balance_series unset when omitted so the engine picks a non-colliding default', async () => {
|
||||
const { supabase, staged } = buildCapturingSupabase()
|
||||
|
||||
await importSie.execute(
|
||||
{ file_content: VALID_SIE, filename: 'bok.se', mappings: COVER_VALID_SIE },
|
||||
'company-1',
|
||||
'user-1',
|
||||
supabase as never,
|
||||
{ type: 'api_key' },
|
||||
)
|
||||
|
||||
expect((staged[0].params as Record<string, unknown>).opening_balance_series).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -15862,9 +15862,10 @@ export const tools: McpTool[] = [
|
||||
items: { type: 'object' },
|
||||
},
|
||||
create_fiscal_period: { type: 'boolean' },
|
||||
import_opening_balances: { type: 'boolean' },
|
||||
import_opening_balances: { type: 'boolean', description: 'Default false (web wizard defaults true)' },
|
||||
import_transactions: { type: 'boolean' },
|
||||
voucher_series: { type: 'string', description: 'Override voucher series for imported vouchers' },
|
||||
opening_balance_series: { type: 'string', description: 'Series for the IB voucher; default avoids series used by the file' },
|
||||
update_account_names: { type: 'boolean', description: 'Use #KONTO names from the file for created and existing accounts (default true). Set false to keep BAS default names.' },
|
||||
},
|
||||
required: ['file_content', 'filename', 'mappings'],
|
||||
@@ -15944,6 +15945,12 @@ export const tools: McpTool[] = [
|
||||
import_opening_balances: Boolean(args.import_opening_balances),
|
||||
import_transactions: Boolean(args.import_transactions),
|
||||
voucher_series: args.voucher_series,
|
||||
// Hosts do not always enforce inputSchema: a non-string must fall
|
||||
// back to the engine default, not crash at commit time.
|
||||
opening_balance_series:
|
||||
typeof args.opening_balance_series === 'string'
|
||||
? args.opening_balance_series
|
||||
: undefined,
|
||||
// Default true: Boolean(undefined) would silently flip it off.
|
||||
update_account_names:
|
||||
args.update_account_names === undefined ? true : Boolean(args.update_account_names),
|
||||
|
||||
Reference in New Issue
Block a user