fix(reports): always emit compulsory #FORMAT PC8 in SIE export (#1466)

#FORMAT is a compulsory record in every SIE type and PC8 is its only
legal value. We only emitted it when the caller opted into cp437 byte
encoding, so the default UTF-8 download had no #FORMAT line and strict
importers (Visma Spiris) rejected the file with 'Etiketten #FORMAT
saknas i filen'. Cloud exporters (Fortnox, Bokio) ship UTF-8 bytes with
#FORMAT PC8 and importers detect the real encoding from the bytes, so
the tag is now unconditional.

Also formats #ORGNR as nnnnnn-nnnn per spec; company_settings stores
the org number without a hyphen.

Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Jakob Wennberg
2026-08-08 15:04:07 +02:00
committed by GitHub
co-authored by Jakob Wennberg Claude Fable 5
parent 799fa1246a
commit ddbe9b1379
6 changed files with 36 additions and 14 deletions
+26 -7
View File
@@ -97,13 +97,32 @@ describe('generateSIEExport', () => {
const lines = output.split('\r\n')
expect(lines[0]).toBe('#FLAGGA 0')
expect(lines[1]).toBe('#SIETYP 4')
expect(lines[2]).toMatch(/^#PROGRAM "ERPBase" "1\.0"$/)
expect(lines[3]).toMatch(/^#GEN \d{8}$/)
expect(lines[4]).toBe('#ORGNR 556677-8899')
expect(lines[5]).toBe('#FNAMN "Test AB"')
expect(lines[6]).toBe('#RAR 0 20240101 20241231')
expect(output).not.toContain('#FORMAT PC8')
expect(lines[1]).toBe('#FORMAT PC8')
expect(lines[2]).toBe('#SIETYP 4')
expect(lines[3]).toMatch(/^#PROGRAM "ERPBase" "1\.0"$/)
expect(lines[4]).toMatch(/^#GEN \d{8}$/)
expect(lines[5]).toBe('#ORGNR 556677-8899')
expect(lines[6]).toBe('#FNAMN "Test AB"')
expect(lines[7]).toBe('#RAR 0 20240101 20241231')
})
it('hyphenates a bare 10-digit org_number in #ORGNR', async () => {
results = [
{ data: { id: 'period-1', period_start: '2024-01-01', period_end: '2024-12-31' }, error: null },
{ data: null, error: null }, // prevPeriod
{ data: [], error: null }, // accounts
{ data: [], error: null }, // journal_entries (no entries -> line fetch skipped)
{ data: [], error: null }, // dimensions
{ data: [], error: null }, // dimension_values
{ data: [], error: null }, // opening balances RPC
]
const output = await generateSIEExport(supabase, 'company-1', {
...baseOptions,
org_number: '5566778899',
})
expect(output).toContain('#ORGNR 556677-8899')
})
it('omits #ORGNR when org_number is null', async () => {
+8 -2
View File
@@ -2,6 +2,7 @@ import type { SupabaseClient } from '@supabase/supabase-js'
import { fetchAllRows } from '@/lib/supabase/fetch-all'
import { fetchLinesByEntryIds } from '@/lib/bookkeeping/entry-lines'
import { getBranding } from '@/lib/branding/service'
import { formatOrgNumber } from '@/lib/utils'
import { createLogger } from '@/lib/logger'
import { getOpeningBalances } from './opening-balances'
import type { SIEExportOptions, JournalEntry, JournalEntryLine, BASAccount } from '@/types'
@@ -158,14 +159,19 @@ export async function generateSIEExport(
// === Header ===
lines.push('#FLAGGA 0')
if (options.emit_format_pc8) lines.push('#FORMAT PC8')
// #FORMAT is compulsory in every SIE type and PC8 is its only legal value.
// Strict importers (e.g. Visma Spiris) reject files without it, and they
// detect the actual byte encoding themselves, so it is emitted even when
// the output is served as UTF-8.
lines.push('#FORMAT PC8')
lines.push('#SIETYP 4')
const programName = sanitizeProgramName(options.program_name || getBranding().appName)
lines.push(`#PROGRAM "${programName}" "1.0"`)
lines.push(`#GEN ${formatSIEDate(now)}`)
if (options.org_number) {
lines.push(`#ORGNR ${options.org_number}`)
// Spec format is nnnnnn-nnnn; company_settings may store it without the hyphen.
lines.push(`#ORGNR ${formatOrgNumber(options.org_number)}`)
}
lines.push(`#FNAMN "${escapeQuotes(options.company_name)}"`)