Files
accounted/lib/import/opening-balance-defaults.ts
T
Mattsson c6f2bebab9 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>
2026-08-25 15:14:59 +02:00

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
}