* feat(reports): bokslutsbilagor, the pärm per räkenskapsår (Reko bilagor, PR 4) One bilaga per balance account as of the balansdag: IB, movement and UB from the trial balance, what it was reconciled against, the difference, the sign-off with who, when and note, and every attached file with its SHA-256; the closing checklist as the first page. JSON and PDF through /api/reports/bokslutsbilagor, in the reports library and on the Avstämning page, and written into every period folder of the full archive. Built from the attested rows, never by recomputing live status. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RvFveUpbdPBXdm7f5FEYoz * fix(reports): load the pärm renderer on demand in the full archive so PDF stubs elsewhere keep working Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RvFveUpbdPBXdm7f5FEYoz * fix(reconciliation): neutral rail dot for a manual account that is merely not attested yet An unsigned manual account without a system specification has nothing to compare against, so an amber dot read as a problem on every balance account of a freshly migrated company. Neutral until it is signed or a specification differs. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RvFveUpbdPBXdm7f5FEYoz --------- Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
81 lines
3.4 KiB
TypeScript
81 lines
3.4 KiB
TypeScript
import { describe, it, expect } from 'vitest'
|
|
import { renderToBuffer } from '@react-pdf/renderer'
|
|
import { BokslutsbilagorPDF } from '../bokslutsbilagor-pdf-template'
|
|
import type { BokslutsbilagorReport } from '../bokslutsbilagor-types'
|
|
|
|
function report(): BokslutsbilagorReport {
|
|
const account = (n: number): BokslutsbilagorReport['accounts'][number] => ({
|
|
account_key: `manual:${2300 + n}`,
|
|
kind: 'manual',
|
|
account_number: String(2300 + n),
|
|
name: `Skuld ${n} med ett ganska långt namn för radbrytning`,
|
|
opening_balance: -100000 - n,
|
|
movement: 5000,
|
|
closing_balance: -95000 - n,
|
|
external_label_sv: 'Saldo enligt underlag (angivet vid signering)',
|
|
external_label_en: 'Balance per supporting documents (stated at sign-off)',
|
|
external_balance: -95000 - n,
|
|
difference: 0,
|
|
signoff: {
|
|
id: `s${n}`,
|
|
through_date: '2026-12-31',
|
|
on_balansdag: n % 2 === 0,
|
|
external_balance: -95000 - n,
|
|
ledger_balance: -95000 - n,
|
|
unexplained_difference: 0,
|
|
note: n % 3 === 0 ? 'Enligt engagemangsbesked → kontrollerat' : null,
|
|
signed_by: 'u1',
|
|
signed_by_label: 'yasemin@example.se',
|
|
signed_at: '2027-01-10T08:00:00Z',
|
|
},
|
|
attachments: [
|
|
{
|
|
id: `a${n}`,
|
|
through_date: '2026-12-31',
|
|
file_name: `engagemangsbesked-${n}.pdf`,
|
|
mime_type: 'application/pdf',
|
|
size_bytes: 1234,
|
|
sha256: 'ab'.repeat(32),
|
|
note: null,
|
|
uploaded_by_label: 'yasemin@example.se',
|
|
uploaded_at: '2027-01-09T08:00:00Z',
|
|
removed_at: n === 1 ? '2027-01-09T09:00:00Z' : null,
|
|
removed_reason: n === 1 ? 'fel fil' : null,
|
|
},
|
|
],
|
|
})
|
|
return {
|
|
company: { name: 'Väla Redovisning AB', org_number: '5592383508' },
|
|
period: { id: 'fy-2026', name: 'Räkenskapsår 2026', start: '2026-01-01', end: '2026-12-31' },
|
|
generated_at: '2027-01-15T10:00:00Z',
|
|
app_version: '1.2.3',
|
|
checklist: {
|
|
items: [
|
|
{ key: 'bank_signed', group: 'avstamning', label_sv: 'Bankkonton avstämda', label_en: 'Bank reconciled', state: 'done', done_at: null, done_by_label: null, note: null },
|
|
{ key: 'inventory_valued', group: 'vardering', label_sv: 'Varulager inventerat', label_en: 'Inventory counted', state: 'not_applicable', done_at: '2027-01-06T08:00:00Z', done_by_label: 'yasemin@example.se', note: 'Inget lager' },
|
|
],
|
|
summary: { total: 2, done: 1, not_applicable: 1, open: 0 },
|
|
},
|
|
accounts: Array.from({ length: 40 }, (_, i) => account(i)),
|
|
summary: { accounts: 40, signed_on_balansdag: 20, signed_other_date: 20, unsigned: 0, attachments: 39 },
|
|
}
|
|
}
|
|
|
|
describe('BokslutsbilagorPDF', () => {
|
|
it('renders a multi-page pärm without deadlocking and with WinAnsi-safe text', async () => {
|
|
const pdf = await renderToBuffer(BokslutsbilagorPDF({ report: report() }))
|
|
expect(pdf.length).toBeGreaterThan(5000)
|
|
expect(pdf.subarray(0, 5).toString()).toBe('%PDF-')
|
|
// More than one page: the fixed header/footer repeat and no `break` was needed.
|
|
expect((pdf.toString('latin1').match(/\/Type\s*\/Page[^s]/g) ?? []).length).toBeGreaterThan(1)
|
|
}, 30000)
|
|
|
|
it('renders the empty pärm', async () => {
|
|
const r = report()
|
|
r.accounts = []
|
|
r.summary = { accounts: 0, signed_on_balansdag: 0, signed_other_date: 0, unsigned: 0, attachments: 0 }
|
|
const pdf = await renderToBuffer(BokslutsbilagorPDF({ report: r }))
|
|
expect(pdf.subarray(0, 5).toString()).toBe('%PDF-')
|
|
}, 30000)
|
|
})
|