Buchungskalender: PDF-Writer und Rechnungs-PDF
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
d7b5b98187
commit
118be44847
3 files changed
+413
No files matched your search
@@ -0,0 +1,110 @@
|
||||
import { test } from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { PdfDoc } from '../server/services/unterkunft/pdf.js'
|
||||
import { rechnungPdf } from '../server/services/unterkunft/rechnungPdf.js'
|
||||
|
||||
// Buchungskalender Task 19: PDF-Writer und Rechnungs-PDF
|
||||
const latin = (b) => b.toString('latin1')
|
||||
|
||||
function inhalt(extra = {}) {
|
||||
return {
|
||||
aussteller: { firma: 'Haus Seeblick', inhaber: 'Anna Muster', strasse: 'Seestr. 1', plz: '88662', ort: 'Ueberlingen', email: 'a@x.de', telefon: '0123', steuernummer: '12/345/67890', ustId: '', iban: 'DE02 1203 0000 0000 2020 51', bic: 'BYLADEM1001', bank: 'DKB' },
|
||||
empfaenger: { name: 'Max Gast', firma: '', strasse: 'Weg 2', plz: '10115', ort: 'Berlin', land: 'Deutschland' },
|
||||
unterkunft: { name: 'Ferienwohnung Süd' },
|
||||
von: '2026-11-12', bis: '2026-11-15', naechte: 3, personen: 2,
|
||||
posten: [{ text: 'Übernachtung (Frühstück), 3 Nächte', menge: 3, einzelCent: 9000, ustProzent: 7 }, { text: 'Endreinigung', menge: 1, einzelCent: 5000, ustProzent: 19 }],
|
||||
summen: { bruttoCent: 32000, nettoCent: 29000, ust: [{ satz: 7, nettoCent: 25234, steuerCent: 1766 }, { satz: 19, nettoCent: 4202, steuerCent: 798 }], steuerart: 'regel' },
|
||||
steuerart: 'regel', zahlart: 'vorort', bezugNummer: '', anfrageNummer: 'A-1', ...extra,
|
||||
}
|
||||
}
|
||||
const kleinInhalt = () => inhalt({
|
||||
steuerart: 'kleinunternehmer',
|
||||
posten: [{ text: 'Übernachtung, 3 Nächte', menge: 3, einzelCent: 9000, ustProzent: 0 }],
|
||||
summen: { bruttoCent: 27000, nettoCent: 27000, ust: [], steuerart: 'kleinunternehmer' },
|
||||
})
|
||||
|
||||
test('PdfDoc: Struktur, xref, Kopf und Ende', () => {
|
||||
const d = new PdfDoc()
|
||||
d.text(50, 50, 'Hallo')
|
||||
const b = d.toBuffer()
|
||||
const s = latin(b)
|
||||
assert.ok(s.startsWith('%PDF-1.4'))
|
||||
assert.ok(s.endsWith('%%EOF\n'))
|
||||
const start = Number(/startxref\n(\d+)\n/.exec(s)[1])
|
||||
assert.equal(s.slice(start, start + 4), 'xref')
|
||||
const eintraege = s.slice(start).split('\n').filter((z) => /^\d{10} 00000 n $/.test(z))
|
||||
assert.ok(eintraege.length >= 6)
|
||||
eintraege.forEach((z, k) => {
|
||||
const off = Number(z.slice(0, 10))
|
||||
assert.ok(s.slice(off).startsWith(`${k + 1} 0 obj`), `Objekt ${k + 1}`)
|
||||
})
|
||||
})
|
||||
|
||||
test('PdfDoc: CP1252, Euro, Escaping, Minus', () => {
|
||||
const d = new PdfDoc()
|
||||
d.text(10, 10, 'Übernachtung ß 5 € −3 (a) \\ ☃')
|
||||
const s = latin(d.toBuffer())
|
||||
assert.ok(s.includes('\xdcbernachtung \xdf 5 \x80 -3 \\(a\\) \\\\ ?'))
|
||||
assert.ok(s.includes('/Encoding /WinAnsiEncoding'))
|
||||
})
|
||||
|
||||
test('PdfDoc: Breiten und Umbruch', () => {
|
||||
const d = new PdfDoc()
|
||||
assert.equal(Math.round(d.breite('Hello', 10) * 100), 2278)
|
||||
assert.ok(d.breite('Hello', 10, true) > d.breite('Hello', 10))
|
||||
const z = d.umbrechen('eins zwei drei vier fünf sechs', 60, 10)
|
||||
assert.ok(z.length > 1)
|
||||
z.forEach((x) => assert.ok(d.breite(x, 10) <= 60))
|
||||
assert.ok(d.umbrechen('Donaudampfschifffahrtsgesellschaft', 50, 10).length > 1)
|
||||
assert.ok(d.breite('x', 10) > 0 && d.breite('€', 10) > 0)
|
||||
})
|
||||
|
||||
test('Rechnung: Regelbesteuerung', () => {
|
||||
const s = latin(rechnungPdf(inhalt(), { art: 'rechnung', nummer: 'RE-2026-0001', datum: '2026-11-01' }))
|
||||
assert.ok(s.includes('RE-2026-0001'))
|
||||
assert.ok(s.includes('Enthaltene USt 7 %'))
|
||||
assert.ok(s.includes('Nettobetrag 19 %'))
|
||||
assert.ok(s.includes('12.11.2026'))
|
||||
assert.ok(s.includes('Beherbergung Ferienwohnung S\xfcd'))
|
||||
assert.ok(s.includes('Zahlung vor Ort'))
|
||||
assert.ok(!s.includes('Gem\xe4\xdf \xa7 19 UStG'))
|
||||
assert.ok(s.includes('Steuernummer 12/345/67890'))
|
||||
assert.ok(s.includes('IBAN'))
|
||||
})
|
||||
|
||||
test('Rechnung: Kleinunternehmer und Online-Zahlung', () => {
|
||||
const s = latin(rechnungPdf({ ...kleinInhalt(), zahlart: 'online' }, { art: 'rechnung', nummer: 'RE-2026-0002', datum: '2026-11-01' }))
|
||||
assert.ok(s.includes('Gem\xe4\xdf \xa7 19 UStG wird keine Umsatzsteuer berechnet.'))
|
||||
assert.ok(s.includes('Online-Zahlung'))
|
||||
assert.ok(!s.includes('Enthaltene USt'))
|
||||
})
|
||||
|
||||
test('Storno: Titel mit Bezug, negative Betraege', () => {
|
||||
const neg = kleinInhalt()
|
||||
neg.posten = neg.posten.map((p) => ({ ...p, einzelCent: -p.einzelCent }))
|
||||
neg.summen = { ...neg.summen, bruttoCent: -27000, nettoCent: -27000 }
|
||||
neg.bezugNummer = 'RE-2026-0002'
|
||||
const s = latin(rechnungPdf(neg, { art: 'storno', nummer: 'RE-2026-0003', datum: '2026-11-02' }))
|
||||
assert.ok(s.includes('Stornorechnung zu RE-2026-0002'))
|
||||
assert.ok(s.includes('-270,00 \x80'))
|
||||
})
|
||||
|
||||
test('Viele Posten: Seitenumbruch', () => {
|
||||
const posten = Array.from({ length: 40 }, (_, k) => ({ text: `Posten ${k + 1}`, menge: 1, einzelCent: 1000, ustProzent: 0 }))
|
||||
const s = latin(rechnungPdf({ ...kleinInhalt(), posten }, { art: 'rechnung', nummer: 'RE-2026-0004', datum: '2026-11-01' }))
|
||||
assert.ok(s.includes('/Count 2'))
|
||||
assert.ok(s.includes('Seite 2 von 2'))
|
||||
})
|
||||
|
||||
test('Deterministisch', () => {
|
||||
const o = { art: 'rechnung', nummer: 'RE-2026-0001', datum: '2026-11-01' }
|
||||
assert.ok(rechnungPdf(inhalt(), o).equals(rechnungPdf(inhalt(), o)))
|
||||
})
|
||||
|
||||
test('Beispiel-PDF schreiben (nur wenn Zielordner existiert)', async () => {
|
||||
const { mkdirSync, writeFileSync } = await import('node:fs')
|
||||
const dir = '/root/reports/buchungskalender-2026-10-07'
|
||||
if (process.env.UK_BEISPIEL_PDF !== '1') return
|
||||
mkdirSync(dir, { recursive: true })
|
||||
writeFileSync(`${dir}/beispiel-rechnung.pdf`, rechnungPdf(inhalt(), { art: 'rechnung', nummer: 'RE-2026-0001', datum: '2026-11-01' }))
|
||||
})
|
||||
Reference in new issue
Block a user