Files
Webklar-Kundenbereich/server/services/unterkunft/rechnungPdf.js
T
2026-10-07 14:06:11 +00:00

135 lines
5.5 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Rechnungs-PDF aus dem unveraenderlichen Schnappschuss ukRechnungen.inhalt (N2). Deterministisch, A4.
import { PdfDoc } from './pdf.js'
import { formatEuro } from './preis.js'
import { formatTag } from './datum.js'
const L = 56
const R = 595.28 - 56
const UNTEN = 745 // Inhalt endet hier, darunter Fusszeile
const tag = (t) => (/^\d{4}-\d{2}-\d{2}$/.test(String(t ?? '')) ? formatTag(t) : String(t ?? ''))
const nz = (s) => String(s ?? '').trim()
const ortZeile = (o) => [nz(o?.plz), nz(o?.ort)].filter(Boolean).join(' ')
export function rechnungPdf(inhalt, { art = 'rechnung', nummer = '', datum = '' } = {}) {
const i = inhalt || {}
const a = i.aussteller || {}
const e = i.empfaenger || {}
const storno = art === 'storno'
const klein = (i.steuerart || i.summen?.steuerart) === 'kleinunternehmer'
const doc = new PdfDoc()
let y
// Absenderzeile (klein)
const absender = [nz(a.firma), nz(a.strasse), ortZeile(a)].filter(Boolean).join(' · ')
doc.text(L, 60, absender, { groesse: 7.5 })
doc.linie(L, 64, L + 250, 64, { dicke: 0.3 })
// Empfaenger
const adr = [nz(e.firma), nz(e.name), nz(e.strasse), ortZeile(e), nz(e.land)].filter(Boolean)
adr.forEach((z, k) => doc.text(L, 82 + k * 13, z, { groesse: 10 }))
// Rechts: Nummer, Datum, Leistungszeitraum
const kopf = [
[storno ? 'Stornonummer' : 'Rechnungsnummer', nz(nummer)],
['Datum', tag(datum)],
['Leistungszeitraum', `${tag(i.von)} – ${tag(i.bis)}`],
]
if (storno && i.bezugNummer) kopf.splice(1, 0, ['Zu Rechnung', nz(i.bezugNummer)])
kopf.forEach(([k, v], n) => {
doc.text(350, 82 + n * 13, k, { groesse: 9 })
doc.text(R, 82 + n * 13, v, { groesse: 9, fett: true, rechts: true })
})
// Titel
y = 190
doc.text(L, y, storno ? `Stornorechnung zu ${nz(i.bezugNummer)}` : 'Rechnung', { groesse: 16, fett: true })
y += 22
const leistung = `Beherbergung ${nz(i.unterkunft?.name)}, ${tag(i.von)}–${tag(i.bis)} (${i.naechte} ${i.naechte === 1 ? 'Nacht' : 'Nächte'}, ${i.personen} ${i.personen === 1 ? 'Person' : 'Personen'})`
for (const z of doc.umbrechen(leistung, R - L, 10)) {
doc.text(L, y, z, { groesse: 10 })
y += 13
}
y += 10
// Posten-Tabelle
const cMenge = 318
const cUst = 362
const cEinzel = 450
const cBetrag = R
const textBreite = (klein ? cMenge - 30 : cUst - 40) - L - 20
const kopfzeile = () => {
doc.text(L, y, 'Leistung', { groesse: 9, fett: true })
doc.text(cMenge, y, 'Menge', { groesse: 9, fett: true, rechts: true })
if (!klein) doc.text(cUst, y, 'USt', { groesse: 9, fett: true, rechts: true })
doc.text(cEinzel, y, 'Einzelpreis', { groesse: 9, fett: true, rechts: true })
doc.text(cBetrag, y, 'Betrag', { groesse: 9, fett: true, rechts: true })
doc.linie(L, y + 4, R, y + 4)
y += 18
}
kopfzeile()
for (const p of i.posten || []) {
const zeilen = doc.umbrechen(nz(p.text), textBreite, 10)
if (y + zeilen.length * 12 > UNTEN) {
doc.seite()
y = 70
kopfzeile()
}
const betrag = Math.round(p.menge * p.einzelCent)
zeilen.forEach((z, k) => doc.text(L, y + k * 12, z, { groesse: 10 }))
doc.text(cMenge, y, String(p.menge), { groesse: 10, rechts: true })
if (!klein) doc.text(cUst, y, `${p.ustProzent} %`, { groesse: 10, rechts: true })
doc.text(cEinzel, y, formatEuro(p.einzelCent), { groesse: 10, rechts: true })
doc.text(cBetrag, y, formatEuro(betrag), { groesse: 10, rechts: true })
y += zeilen.length * 12 + 4
}
doc.linie(L, y - 2, R, y - 2)
y += 12
// Summen (zusammenhaengend auf einer Seite)
const s = i.summen || {}
const zeilenSumme = []
if (klein) {
zeilenSumme.push(['Gesamtbetrag', formatEuro(s.bruttoCent ?? 0), true])
} else {
for (const u of s.ust || []) {
zeilenSumme.push([`Nettobetrag ${u.satz} %`, formatEuro(u.nettoCent), false])
zeilenSumme.push([`Enthaltene USt ${u.satz} %`, formatEuro(u.steuerCent), false])
}
zeilenSumme.push(['Gesamtbetrag (brutto)', formatEuro(s.bruttoCent ?? 0), true])
}
const hinweis = klein ? doc.umbrechen('Gemäß § 19 UStG wird keine Umsatzsteuer berechnet.', R - L, 9) : []
const zahlText = i.zahlart === 'online' ? 'Zahlart: Online-Zahlung' : i.zahlart === 'vorort' ? 'Zahlart: Zahlung vor Ort' : ''
const noetig = zeilenSumme.length * 14 + hinweis.length * 12 + 40
if (y + noetig > UNTEN) {
doc.seite()
y = 70
}
for (const [k, v, fett] of zeilenSumme) {
doc.text(cEinzel, y, k, { groesse: 10, fett, rechts: true })
doc.text(cBetrag, y, v, { groesse: 10, fett, rechts: true })
y += 14
}
y += 8
for (const z of hinweis) {
doc.text(L, y, z, { groesse: 9 })
y += 12
}
if (zahlText) {
y += 4
doc.text(L, y, zahlText, { groesse: 9 })
}
// Fusszeile auf jeder Seite
const fuss1 = [nz(a.firma), nz(a.inhaber) && `Inh. ${nz(a.inhaber)}`, nz(a.strasse), ortZeile(a)].filter(Boolean).join(' · ')
const fuss2 = [nz(a.steuernummer) && `Steuernummer ${nz(a.steuernummer)}`, nz(a.ustId) && `USt-IdNr. ${nz(a.ustId)}`, nz(a.email), nz(a.telefon)].filter(Boolean).join(' · ')
const fuss3 = [nz(a.bank), nz(a.iban) && `IBAN ${nz(a.iban)}`, nz(a.bic) && `BIC ${nz(a.bic)}`].filter(Boolean).join(' · ')
const n = doc.seitenzahl
for (let p = 0; p < n; p++) {
doc.aufSeite(p)
doc.linie(L, 790, R, 790, { dicke: 0.3 })
;[fuss1, fuss2, fuss3].filter(Boolean).forEach((z, k) => doc.text(L, 802 + k * 10, z, { groesse: 7.5 }))
if (n > 1) doc.text(R, 826, `Seite ${p + 1} von ${n}`, { groesse: 7.5, rechts: true })
}
return doc.toBuffer()
}