135 lines
5.5 KiB
JavaScript
135 lines
5.5 KiB
JavaScript
// 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()
|
||
}
|