Files
accounted/lib/reports/__tests__/financial-statement-pdf-template.test.ts
T
Jakob Wennberg cc7050f6cb fix(pdf): keep the minus sign on losses in standard-font PDFs (#1982) (#1987)
Intl sv-SE formats negatives with U+2212, which the bundled react-pdf Helvetica/Courier fonts cannot render, so a loss printed as a profit. lib/pdf/number-text.ts pdfNumberText maps U+2212 to an ASCII hyphen and prints negative zero unsigned; routed through financial-statement, kassaflodesanalys, reskontra, momsdeklaration, payslip (fmt, the literal Preliminar skatt sign and the calculation formula text) and operational-report templates, with content-stream regression tests. The K2/K3 arsredovisning templates keep main's formatPdfKronor from #2013.

Closes #1982
2026-08-30 11:22:05 +02:00

181 lines
5.7 KiB
TypeScript

import { describe, it, expect } from 'vitest'
import { renderToBuffer } from '@react-pdf/renderer'
import { FinancialStatementPDF } from '../financial-statement-pdf-template'
import type { CompanySettings } from '@/types'
import { pdfTextStrings } from '@/tests/pdf-text'
function fakeCompany(): CompanySettings {
return {
company_name: 'Gnubok',
org_number: '5566778899',
vat_number: 'SE556677889901',
address_line1: 'Kungsgatan 1',
postal_code: '11143',
city: 'Stockholm',
country: 'SE',
entity_type: 'aktiebolag',
} as unknown as CompanySettings
}
// Real @react-pdf/renderer layout is CPU-heavy; under a fully parallel
// test run these can exceed the 5s default on a saturated machine.
const RENDER_TIMEOUT = 30_000
describe('FinancialStatementPDF', () => {
it('renders a balance-sheet-shaped document to a PDF buffer', async () => {
const doc = FinancialStatementPDF({
title: 'Balansräkning',
groups: [
{
heading: 'Tillgångar',
sections: [
{
title: 'Kassa och bank',
rows: [
{ account_number: '1930', account_name: 'Företagskonto', amount: 125_432.5 },
],
subtotal: 125_432.5,
},
],
totalLabel: 'Summa tillgångar',
total: 125_432.5,
},
{
heading: 'Eget kapital och skulder',
sections: [
{
title: 'Eget kapital',
rows: [
{ account_number: '2010', account_name: 'Eget kapital', amount: 100_000 },
{ account_number: '2091', account_name: 'Balanserat resultat', amount: 25_432.5 },
],
subtotal: 125_432.5,
},
],
totalLabel: 'Summa eget kapital och skulder',
total: 125_432.5,
},
],
period: { start: '2026-01-01', end: '2026-12-31' },
company: fakeCompany(),
generatedAt: '2026-04-21T10:00:00Z',
})
const buffer = await renderToBuffer(doc)
expect(buffer).toBeInstanceOf(Buffer)
expect(buffer.length).toBeGreaterThan(1000)
// PDF files always start with "%PDF-"
expect(buffer.slice(0, 5).toString()).toBe('%PDF-')
}, RENDER_TIMEOUT)
it('renders an income-statement-shaped document with a summary block', async () => {
const doc = FinancialStatementPDF({
title: 'Resultaträkning',
groups: [
{
heading: 'Rörelseintäkter',
sections: [
{
title: 'Huvudintäkter',
rows: [
{ account_number: '3001', account_name: 'Försäljning 25%', amount: 500_000 },
],
subtotal: 500_000,
},
],
totalLabel: 'Summa rörelseintäkter',
total: 500_000,
},
{
heading: 'Rörelsekostnader',
sections: [
{
title: 'Lokalkostnader',
rows: [
{ account_number: '5010', account_name: 'Lokalhyra', amount: 120_000 },
],
subtotal: 120_000,
},
],
totalLabel: 'Summa rörelsekostnader',
total: 120_000,
negate: true,
},
],
summary: [
{ label: 'Rörelseresultat', amount: 380_000 },
{ label: 'Årets resultat', amount: 380_000, emphasis: true },
],
period: { start: '2026-01-01', end: '2026-12-31' },
company: fakeCompany(),
generatedAt: '2026-04-21T10:00:00Z',
})
const buffer = await renderToBuffer(doc)
expect(buffer).toBeInstanceOf(Buffer)
expect(buffer.slice(0, 5).toString()).toBe('%PDF-')
}, RENDER_TIMEOUT)
it('handles empty section groups gracefully', async () => {
const doc = FinancialStatementPDF({
title: 'Balansräkning',
groups: [
{
heading: 'Tillgångar',
sections: [],
totalLabel: 'Summa tillgångar',
total: 0,
},
{
heading: 'Eget kapital och skulder',
sections: [],
totalLabel: 'Summa eget kapital och skulder',
total: 0,
},
],
period: { start: '', end: '' },
company: fakeCompany(),
generatedAt: '2026-04-21T10:00:00Z',
})
const buffer = await renderToBuffer(doc)
expect(buffer.slice(0, 5).toString()).toBe('%PDF-')
}, RENDER_TIMEOUT)
it('prints a loss with its sign in the rendered bytes (issue #1982)', async () => {
// sv-SE formats negatives with U+2212, which the bundled Helvetica cannot
// draw: Årets resultat -4 684,24 used to print as 4 684,24 in both the
// resultaträkning summary and the balansräkning 2099 row.
const doc = FinancialStatementPDF({
title: 'Balansräkning',
groups: [
{
heading: 'Eget kapital och skulder',
sections: [
{
title: 'Eget kapital',
rows: [
{ account_number: '2081', account_name: 'Aktiekapital', amount: 25_000 },
{ account_number: '2099', account_name: 'Årets resultat', amount: -4684.24 },
],
subtotal: 20_315.76,
},
],
totalLabel: 'Summa eget kapital och skulder',
total: 20_315.76,
},
],
summary: [{ label: 'Årets resultat', amount: -4684.24, emphasis: true }],
period: { start: '2025-10-14', end: '2026-01-31' },
company: fakeCompany(),
generatedAt: '2026-08-27T10:00:00Z',
})
const text = pdfTextStrings(await renderToBuffer(doc)).join('\n')
expect(text).toContain('-4 684,24')
expect(text).not.toContain(String.fromCharCode(0x12))
expect(text).not.toContain('\u2212')
expect(text).toContain('20 315,76')
}, RENDER_TIMEOUT)
})