c6f2bebab9
* 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>
64 lines
2.6 KiB
TypeScript
64 lines
2.6 KiB
TypeScript
/**
|
|
* Defaults for the SIE-import opening-balance (Ingående balanser) voucher.
|
|
*
|
|
* Pure module: imported by both the import engine (server) and the import
|
|
* wizard (client), so it must stay free of Supabase/engine dependencies.
|
|
*
|
|
* Issue #1882: the IB voucher used to be hardcoded to series A and created
|
|
* BEFORE the file's vouchers, so it consumed the A series' next number and
|
|
* shifted every A voucher one number higher than in the source system. The
|
|
* default series must therefore never collide with the series the file's
|
|
* own vouchers use.
|
|
*/
|
|
|
|
/**
|
|
* Default series for the IB voucher. 'M' matches the series the import
|
|
* engine already uses for its other system voucher (the migration
|
|
* adjustment / omföringsverifikation in sie-import.ts) and is not part of
|
|
* the common Swedish source-system conventions (A huvudserie, B automat,
|
|
* F kundfakturor, I inbetalningar, J bokslut, L leverantörsfakturor,
|
|
* N löner, U utbetalningar).
|
|
*/
|
|
export const DEFAULT_OPENING_BALANCE_SERIES = 'M'
|
|
|
|
/**
|
|
* Candidate series tried in order when the file's own vouchers already use
|
|
* the preferred default. Letters with a conventional meaning in Swedish
|
|
* bookkeeping (A, B, F, I, J, L, N, U) are deliberately excluded so the IB
|
|
* voucher never lands in a series a migrated company recognizes as
|
|
* something else.
|
|
*/
|
|
const SERIES_CANDIDATES = ['M', 'O', 'P', 'Q', 'R', 'S', 'T', 'V', 'W', 'X', 'Y', 'Z'] as const
|
|
|
|
/**
|
|
* Pick the default IB-voucher series: the first candidate not used by the
|
|
* file's own vouchers. Falls back to 'M' in the (practically impossible)
|
|
* case where a file uses every candidate; the user can still override in
|
|
* the wizard.
|
|
*/
|
|
export function defaultOpeningBalanceSeries(seriesInFile: Iterable<string>): string {
|
|
const used = new Set<string>()
|
|
for (const s of seriesInFile) {
|
|
const normalized = typeof s === 'string' ? s.trim().toUpperCase() : ''
|
|
if (normalized) used.add(normalized)
|
|
}
|
|
for (const candidate of SERIES_CANDIDATES) {
|
|
if (!used.has(candidate)) return candidate
|
|
}
|
|
return DEFAULT_OPENING_BALANCE_SERIES
|
|
}
|
|
|
|
/**
|
|
* Smart default for the wizard's "Importera ingående balanser" toggle.
|
|
* OFF when the file carries no IB, and OFF on re-import when the fiscal
|
|
* year already has a posted opening-balance voucher: importing again would
|
|
* create a duplicate "Ingående balanser" verifikat (the field report behind
|
|
* issue #1882 had five accumulated ones).
|
|
*/
|
|
export function defaultImportOpeningBalancesOn(args: {
|
|
hasOpeningBalances: boolean
|
|
existingIbEntryCount: number
|
|
}): boolean {
|
|
return args.hasOpeningBalances && args.existingIbEntryCount === 0
|
|
}
|