fix(sie-import): accept tab as field separator (Bollbok exports) (#513)
* fix(sie-import): accept tab as field separator (Bollbok exports) The SIE 4 spec allows either space or tab between fields, but splitSIELine() only treated space (0x20) as a separator. Bollbok exports tab-separated lines for every record except #RAR, which silently swallowed all #IB / #UB / #KONTO / #KTYP / #VER / #TRANS records — imports appeared empty even though the file was well-formed. Also adds a parser-side diagnostic that emits a warning when raw #IB or #VER lines are present in the input but parsing produced none. The previous silent failure is how this bug stayed hidden; the warning gives the import preview something visible to surface next time. Verified against two real reproducer files (Sean / Erik Hellqvist): erik h 2025.SE (UTF-8): 166 accounts, 66 IB, 4 UB, 11 RES, 95 vouchers, 198 TRANS. erik h 2026.SE (CP437): 166 accounts, 66 IB, 4 UB, 0 vouchers. Both now parse with zero warnings/errors. Tests: + 8 Bollbok-shape tab-separated fixtures (2025 + 2026 quoting variants). + 4 silent-failure diagnostic-warning tests. All 74 sie-parser tests pass; 155/155 in lib/import; 64/64 downstream callers. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(sie-import): address PR #513 review — strip #KTYP quotes, suppress redundant aggregate warning Two non-blocking P2 findings from Greptile review on PR #513: 1. #KTYP handler stored fields[2] directly, so Bollbok 2026 exports (#KTYP\t1510\t"T") stored '"T"' with literal quotes instead of 'T'. Latent defect — accountType is unused downstream today, but my tab- separator fix made the quoted-value path reachable. Now routes through parseStringField so both Bollbok 2025 (unquoted T) and 2026 (quoted "T") land as 'T'. 2. The aggregate "kontrollera fältavskiljare och teckenkodning" warning fired alongside per-record 'error'-severity issues for malformed #IB / #VER records, producing a misleading hint when the parser had already pinpointed the structural problem. Now suppressed when an error-severity issue with the same tag already exists. Test coverage: + accountType asserted to be 'T' (not '"T"') in both 2025 + 2026 shapes. + VER aggregate-warning test now uses #VER lines without { } blocks (silent loss, no per-record error) — the canonical case the diagnostic is designed for. + New suppression test: bare #VER produces per-record errors AND the aggregate warning is absent. 75/75 sie-parser tests pass; 156/156 in lib/import. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
831920fede
commit
f829c96d8b
@@ -322,7 +322,10 @@ function splitSIELine(line: string): string[] {
|
||||
continue
|
||||
}
|
||||
|
||||
if (char === ' ' && !inQuotes && braceDepth === 0) {
|
||||
// SIE 4 spec allows either space or tab as field separator (programs like
|
||||
// Bollbok export tab-separated lines). Quoted strings and brace-bounded
|
||||
// dimension lists preserve any interior whitespace via the guards above.
|
||||
if ((char === ' ' || char === '\t') && !inQuotes && braceDepth === 0) {
|
||||
if (current) {
|
||||
fields.push(current)
|
||||
current = ''
|
||||
@@ -519,8 +522,10 @@ export function parseSIEFile(content: string): ParsedSIEFile {
|
||||
|
||||
case 'KTYP': {
|
||||
// #KTYP accountNumber type
|
||||
// Bollbok 2025 writes the type unquoted (T), Bollbok 2026 writes it
|
||||
// quoted ("T"). parseStringField strips the quotes in both cases.
|
||||
const accountNum = fields[1]
|
||||
const accountType = fields[2]
|
||||
const accountType = parseStringField(fields[2])
|
||||
const account = accounts.find((a) => a.number === accountNum)
|
||||
if (account) {
|
||||
account.accountType = accountType
|
||||
@@ -718,6 +723,38 @@ export function parseSIEFile(content: string): ParsedSIEFile {
|
||||
addIssue(issues, 'info', 0, `Account ${accountNumber} added from transaction data (not in #KONTO)`)
|
||||
}
|
||||
|
||||
// Silent-failure diagnostic: if the raw input declares #IB / #VER records
|
||||
// but parsing produced none, surface a warning instead of letting the file
|
||||
// look empty. Historically a tab-separator or encoding mismatch could swallow
|
||||
// all balance/voucher records without any visible signal.
|
||||
//
|
||||
// Suppressed when per-record 'error' issues already exist for the same tag —
|
||||
// in that case the parser already pinpointed the root cause (e.g. malformed
|
||||
// verification definition), so the generic "check separator/encoding" hint
|
||||
// would be misleading.
|
||||
const rawIBCount = lines.filter((l) => /^\s*#IB\b/.test(l)).length
|
||||
const rawVERCount = lines.filter((l) => /^\s*#VER\b/.test(l)).length
|
||||
const hasIBError = issues.some((i) => i.severity === 'error' && i.tag === 'IB')
|
||||
const hasVERError = issues.some((i) => i.severity === 'error' && i.tag === 'VER')
|
||||
if (rawIBCount > 0 && openingBalances.length === 0 && !hasIBError) {
|
||||
addIssue(
|
||||
issues,
|
||||
'warning',
|
||||
0,
|
||||
`${rawIBCount} #IB-rader hittades men inga ingående saldon kunde tolkas — kontrollera fältavskiljare och teckenkodning`,
|
||||
'IB'
|
||||
)
|
||||
}
|
||||
if (rawVERCount > 0 && vouchers.length === 0 && !hasVERError) {
|
||||
addIssue(
|
||||
issues,
|
||||
'warning',
|
||||
0,
|
||||
`${rawVERCount} #VER-rader hittades men inga verifikationer kunde tolkas — kontrollera fältavskiljare och teckenkodning`,
|
||||
'VER'
|
||||
)
|
||||
}
|
||||
|
||||
// Calculate statistics
|
||||
const currentFiscalYear = header.fiscalYears.find((fy) => fy.yearIndex === 0)
|
||||
const totalTransactionLines = vouchers.reduce((sum, v) => sum + v.lines.length, 0)
|
||||
|
||||
Reference in New Issue
Block a user