From 0ba1db1c2137e0a6a8b84d24cf6c1efbe26fd8e2 Mon Sep 17 00:00:00 2001 From: Jakob Wennberg <149234542+jakobwennberg@users.noreply.github.com> Date: Fri, 20 Mar 2026 12:10:08 +0100 Subject: [PATCH] fix: skip BTRANS/RTRANS in SIE parser to prevent false balance errors (#63) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When Fortnox corrects a voucher, the SIE file contains #BTRANS (added) and #RTRANS (removed) lines alongside #TRANS (final state). The parser was summing all three, causing corrected vouchers to appear unbalanced. Only #TRANS represents the final voucher state — BTRANS/RTRANS are correction audit trail. Co-authored-by: Claude Opus 4.6 (1M context) --- lib/import/sie-parser.ts | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/lib/import/sie-parser.ts b/lib/import/sie-parser.ts index 1e6a6c3b..b1b35c60 100644 --- a/lib/import/sie-parser.ts +++ b/lib/import/sie-parser.ts @@ -574,16 +574,24 @@ export function parseSIEFile(content: string): ParsedSIEFile { case 'TRANS': case 'RTRANS': case 'BTRANS': { - // #TRANS/#RTRANS/#BTRANS accountNumber {objectList} amount [date] [description] [quantity] [signature] - // BTRANS = Added/corrected transaction lines (part of the voucher) - // RTRANS = Removed/reversed transaction lines (amounts already have correct sign) - // All three must be included for vouchers to balance correctly. - // Fortnox/Bokio/Visma only emit #TRANS — this is a no-op for those providers. + // #TRANS = final transaction lines (the current state of the voucher) + // #RTRANS = removed lines (correction audit trail — original lines that were undone) + // #BTRANS = added lines (correction audit trail — new lines that replaced removed ones) + // + // When a voucher has been corrected, Fortnox/Visma emit all three types. + // Only #TRANS represents the final voucher state; #RTRANS and #BTRANS are + // supplementary history. We skip RTRANS/BTRANS to avoid double-counting + // which would make balanced vouchers appear unbalanced. if (!currentVoucher) { addIssue(issues, 'error', lineNum, `${tag} outside of VER block`, tag) break } + // Skip RTRANS/BTRANS — they are correction audit trail, not final state + if (tag === 'RTRANS' || tag === 'BTRANS') { + break + } + // Parse account and skip object list (in braces) let fieldIndex = 1 const account = parseStringField(fields[fieldIndex++])