fix(arsredovisning): losses lost their minus sign in the PDF (#2013)
* fix(arsredovisning): losses lost their minus sign in the PDF
toLocaleString('sv-SE') formats negatives with U+2212 MINUS SIGN, which
react-pdf's built-in Helvetica (WinAnsi encoding) has no glyph for, so the
sign was silently dropped: a loss rendered as a profit on 'Arets resultat'
and 'Summa fritt eget kapital' while all totals stayed correct. Reported by
a user whose -4 684,24 kr loss displayed as +4 684 kr.
Add formatPdfKronor (absolute value + ASCII hyphen-minus) and route the K2
and K3 PDF templates and the PDF-bound note builders through it. Display
only; no ledger or iXBRL changes (the iXBRL writer already handles signs
via the sign attribute and its own presentational minus).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012CBPBCdBr5P75MQCt4qcnE
* fix(arsredovisning): pin PDF group separator to U+00A0 across ICU versions
Newer CLDR data groups sv-SE thousands with U+202F NARROW NO-BREAK SPACE,
which WinAnsi Helvetica also lacks: digits would silently run together the
same way the minus sign was dropped. Normalize the separator to U+00A0 in
formatPdfKronor so the rendering does not depend on the runtime's ICU.
Addresses the PR Agent reviewer-guide finding on #2013.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012CBPBCdBr5P75MQCt4qcnE
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
b4b11c33ca
commit
89ce837947
@@ -0,0 +1,43 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { formatPdfKronor } from '../pdf-format'
|
||||
|
||||
// Build the special characters from code points so the assertions cannot be
|
||||
// corrupted by editor/encoding round-trips: U+00A0 is the sv-SE thousands
|
||||
// separator, U+2212 is the locale minus that WinAnsi Helvetica cannot render.
|
||||
const NBSP = String.fromCharCode(0x00a0)
|
||||
const U2212 = String.fromCharCode(0x2212)
|
||||
|
||||
describe('formatPdfKronor', () => {
|
||||
it('formats a loss with an ASCII hyphen-minus, never U+2212', () => {
|
||||
const out = formatPdfKronor(-4684.24)
|
||||
expect(out).toBe(`-4${NBSP}684`)
|
||||
expect(out).not.toContain(U2212)
|
||||
})
|
||||
|
||||
it('keeps sv-SE thousands grouping for positive amounts', () => {
|
||||
expect(formatPdfKronor(1234567.89)).toBe(`1${NBSP}234${NBSP}568`)
|
||||
})
|
||||
|
||||
it('pins the group separator to U+00A0 regardless of the ICU CLDR choice', () => {
|
||||
// Newer CLDR data groups sv-SE with U+202F NARROW NO-BREAK SPACE, which
|
||||
// is also missing from WinAnsi; the formatter must normalize to U+00A0.
|
||||
const U202F = String.fromCharCode(0x202f)
|
||||
const out = formatPdfKronor(-9876543)
|
||||
expect(out).toBe(`-9${NBSP}876${NBSP}543`)
|
||||
expect(out).not.toContain(U202F)
|
||||
})
|
||||
|
||||
it('rounds to whole kronor and never renders "-0"', () => {
|
||||
expect(formatPdfKronor(-0.4)).toBe('0')
|
||||
expect(formatPdfKronor(-0.6)).toBe('-1')
|
||||
expect(formatPdfKronor(0)).toBe('0')
|
||||
})
|
||||
|
||||
it('always uses ASCII hyphen for negative amounts', () => {
|
||||
for (const n of [-1, -999, -1000, -4684.24, -1_000_000.5]) {
|
||||
const out = formatPdfKronor(n)
|
||||
expect(out).not.toContain(U2212)
|
||||
expect(out.startsWith('-')).toBe(true)
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -24,6 +24,7 @@
|
||||
|
||||
import type { NoteEntry } from './types'
|
||||
import type { AssetDepreciationFigures } from './asset-note-figures'
|
||||
import { formatPdfKronor } from './pdf-format'
|
||||
|
||||
export interface AnlaggningAsset {
|
||||
category: string
|
||||
@@ -161,7 +162,9 @@ export function computeRollforwardTotals(
|
||||
}
|
||||
}
|
||||
|
||||
const fmt = (n: number) => Math.round(n).toLocaleString('sv-SE')
|
||||
// ASCII hyphen for negatives: the note body renders in the ÅR PDF with
|
||||
// Helvetica/WinAnsi, which has no U+2212 glyph.
|
||||
const fmt = (n: number) => formatPdfKronor(n)
|
||||
|
||||
/**
|
||||
* Build the anläggningstillgångar roll-forward note. Returns null when no
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Document, Page, Text, View, StyleSheet } from '@react-pdf/renderer'
|
||||
import type { ArsredovisningData, StatementRow } from './types'
|
||||
import { formatPdfKronor } from './pdf-format'
|
||||
|
||||
/**
|
||||
* K3 årsredovisning PDF template (BFNAR 2012:1).
|
||||
@@ -160,7 +161,8 @@ const styles = StyleSheet.create({
|
||||
})
|
||||
|
||||
function fmt(amount: number): string {
|
||||
return Math.round(amount).toLocaleString('sv-SE')
|
||||
// ASCII hyphen for negatives: Helvetica/WinAnsi has no U+2212 glyph.
|
||||
return formatPdfKronor(amount)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Document, Page, Text, View, StyleSheet } from '@react-pdf/renderer'
|
||||
import type { ArsredovisningData, StatementRow } from './types'
|
||||
import { formatPdfKronor } from './pdf-format'
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
page: {
|
||||
@@ -100,7 +101,8 @@ const styles = StyleSheet.create({
|
||||
|
||||
function fmt(amount: number): string {
|
||||
// sv-SE thousands grouping, no decimals: typical for K2 ÅR.
|
||||
return Math.round(amount).toLocaleString('sv-SE')
|
||||
// ASCII hyphen for negatives: Helvetica/WinAnsi has no U+2212 glyph.
|
||||
return formatPdfKronor(amount)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -2,6 +2,7 @@ import type {
|
||||
EgenKapitalRow,
|
||||
NoteEntry,
|
||||
} from './types'
|
||||
import { formatPdfKronor } from './pdf-format'
|
||||
|
||||
/**
|
||||
* K3 noter builder (BFNAR 2012:1).
|
||||
@@ -161,8 +162,8 @@ export function buildUppskjutenSkattNot(params: {
|
||||
const { noteNumber, latentTaxOpening, latentTaxChange, latentTaxClosing } =
|
||||
params
|
||||
// sv-SE thousand separator, no decimals: typical for ÅR notes.
|
||||
const fmt = (n: number) =>
|
||||
Math.round(n).toLocaleString('sv-SE')
|
||||
// ASCII hyphen for negatives: PDF Helvetica/WinAnsi has no U+2212 glyph.
|
||||
const fmt = (n: number) => formatPdfKronor(n)
|
||||
const lines: string[] = [
|
||||
'Posten avser uppskjuten skatteskuld redovisad på konto 2240.',
|
||||
'',
|
||||
@@ -367,7 +368,7 @@ export function buildMateriellaAnlaggningsNot(params: {
|
||||
}
|
||||
|
||||
// If any asset has components, render a per-component breakdown per asset.
|
||||
const fmt = (n: number) => Math.round(n).toLocaleString('sv-SE')
|
||||
const fmt = (n: number) => formatPdfKronor(n)
|
||||
const withComponents = active.filter((a) => isComponentArray(a.k3_components))
|
||||
if (withComponents.length > 0) {
|
||||
linesOut.push('', 'Komponentuppdelning per tillgång:')
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
/**
|
||||
* Whole-krona amount formatting for text that react-pdf renders with the
|
||||
* built-in Helvetica font (the K2/K3 arsredovisning PDFs and their notes).
|
||||
*
|
||||
* `toLocaleString('sv-SE')` formats negative numbers with U+2212 MINUS SIGN,
|
||||
* which WinAnsi-encoded standard fonts cannot encode; react-pdf drops the
|
||||
* glyph silently, so a loss like -4 684 printed as "−4 684" loses its
|
||||
* sign and reads as a profit. Format the absolute value and prepend an ASCII
|
||||
* hyphen-minus instead. The sv-SE thousands separator (U+00A0 NBSP) is in
|
||||
* WinAnsi and renders fine.
|
||||
*/
|
||||
const NBSP = String.fromCharCode(0x00a0)
|
||||
|
||||
export function formatPdfKronor(amount: number): string {
|
||||
const rounded = Math.round(amount)
|
||||
// Pin the group separator to U+00A0: depending on the Node/ICU CLDR
|
||||
// version, sv-SE groups with U+00A0 or U+202F NARROW NO-BREAK SPACE, and
|
||||
// U+202F is missing from WinAnsi too (digits would silently run together).
|
||||
const grouped = Math.abs(rounded)
|
||||
.toLocaleString('sv-SE')
|
||||
.replace(/\s/g, NBSP)
|
||||
return rounded < 0 ? `-${grouped}` : grouped
|
||||
}
|
||||
Reference in New Issue
Block a user