AB-PDF: auftragPdfDaten mit Kopf, Anschrift, Positionen und Prüfung der Pflichtangaben
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
b097fedf5a
commit
7ad9c12bca
3 files changed
+216
No files matched your search
@@ -1,5 +1,11 @@
|
||||
import { readFileSync } from 'node:fs'
|
||||
import * as fontkitModul from 'fontkit'
|
||||
import { euroDe } from '../finance/format.js'
|
||||
import { berlinDate, formatDe } from '../finance/dates.js'
|
||||
import { AuftragError } from './fehler.js'
|
||||
import { AUFTRAG_ARTEN, AUFTRAG_WEGE, ZAHLUNGSPLAN_ARTEN } from './konstanten.js'
|
||||
import { AB_TEXTE, ABSENDER } from './texte.js'
|
||||
import { aufwandText } from './auftragSicht.js'
|
||||
|
||||
/**
|
||||
* PDF der Auftragsbestätigung (A4, pdfkit): Schriften und Zeichenabdeckung, Daten (alle Texte fertig formatiert),
|
||||
@@ -76,3 +82,78 @@ export function bereinigeTexte(wert, hatGlyphFn = hatGlyph) {
|
||||
return wert
|
||||
}
|
||||
|
||||
// ===== 2. Daten =====
|
||||
|
||||
export const PDF_BESCHRIFTUNG = Object.freeze({
|
||||
nummer: 'Nummer', datum: 'Datum', kundennummer: 'Kundennummer', projekt: 'Projekt',
|
||||
leistung: 'Leistungsumfang', zahlung: 'Zahlung', termine: 'Termine', mitwirkung: 'Mitwirkung und Abnahme',
|
||||
spalten: Object.freeze(['Pos.', 'Beschreibung', 'Menge', 'Einzelpreis', 'Summe']), gesamt: 'Gesamtpreis',
|
||||
steuernummer: 'Steuernummer', telefon: 'Telefon', bank: 'Bankverbindung', kontakt: 'Kontakt', seite: 'Seite', von: 'von',
|
||||
entwurf: 'ENTWURF', entwurfHinweis: 'Entwurf – Vorschau, noch keine gültige Auftragsbestätigung',
|
||||
})
|
||||
|
||||
const fehlt = (was) => new AuftragError(409, `Entwurf unvollständig: ${was}`)
|
||||
const zeilen = (text) => String(text ?? '').split('\n').map((z) => z.trim()).filter(Boolean)
|
||||
const tag = (iso) => formatDe(berlinDate(new Date(iso)))
|
||||
|
||||
function pruefe(auftrag, vorschau) {
|
||||
if (!auftrag?.kunde || typeof auftrag.kunde !== 'object') throw fehlt('Kundendaten fehlen.')
|
||||
if (!vorschau && !auftrag.nummer) throw fehlt('Nummer fehlt.')
|
||||
if (!String(auftrag.titel ?? '').trim()) throw fehlt('Titel fehlt.')
|
||||
if (!/^\d{4}-\d{2}-\d{2}$/.test(auftrag.auftragVom ?? '')) throw fehlt('Datum der Absprache fehlt.')
|
||||
if (!Object.hasOwn(AUFTRAG_WEGE, auftrag.auftragWeg)) throw fehlt('Weg der Absprache fehlt.')
|
||||
if (!AUFTRAG_ARTEN.includes(auftrag.art)) throw fehlt('Art des Auftrags fehlt.')
|
||||
if (!ZAHLUNGSPLAN_ARTEN.includes(auftrag.zahlungsplan?.art)) throw fehlt('Zahlungsplan fehlt.')
|
||||
if (auftrag.art === 'festpreis') {
|
||||
if (!Array.isArray(auftrag.positionen) || auftrag.positionen.length === 0) throw fehlt('Positionen fehlen.')
|
||||
const summe = auftrag.positionen.reduce((s, p) => s + p.menge * p.einzelpreisCents, 0)
|
||||
if (summe !== auftrag.summeCents) throw new AuftragError(409, 'Die Summe der Positionen stimmt nicht mit der Auftragssumme überein.')
|
||||
} else if (!Number.isSafeInteger(auftrag.stundensatzCents) || auftrag.stundensatzCents <= 0) {
|
||||
throw fehlt('Stundensatz fehlt.')
|
||||
}
|
||||
}
|
||||
|
||||
function empfaengerZeilen(kunde) {
|
||||
const land = String(kunde.land ?? '').trim()
|
||||
const ausland = land && !/^(deutschland|germany|de)$/i.test(land)
|
||||
return [kunde.firma, kunde.name, kunde.strasse, [kunde.plz, kunde.ort].filter(Boolean).join(' '), ausland ? land : '']
|
||||
.map((z) => String(z ?? '').trim())
|
||||
.filter((z, i, alle) => z && alle.indexOf(z) === i)
|
||||
}
|
||||
|
||||
/** Alle Texte und Zahlen des AB-PDFs, fertig formatiert (reine Funktion). */
|
||||
export function auftragPdfDaten(auftrag, { settings = {}, vorschau = false, jetzt = new Date(), ersetzt = null } = {}) {
|
||||
pruefe(auftrag, vorschau)
|
||||
const b = PDF_BESCHRIFTUNG
|
||||
const steuernummer = String(settings.steuernummer ?? '').trim()
|
||||
const festpreis = auftrag.art === 'festpreis'
|
||||
return {
|
||||
vorschau,
|
||||
beschriftung: b,
|
||||
titel: AB_TEXTE.titel,
|
||||
nummer: auftrag.nummer || 'Entwurf',
|
||||
datum: tag(auftrag.gesendetAm || jetzt),
|
||||
kundennummer: String(auftrag.kunde.kundennummer ?? ''),
|
||||
projekt: auftrag.titel.trim(),
|
||||
absender: {
|
||||
zeilen: [ABSENDER.name, ABSENDER.strasse, ABSENDER.plzOrt, `${b.telefon} ${ABSENDER.telefon}`, ABSENDER.email, ABSENDER.web, ...(steuernummer ? [`${b.steuernummer} ${steuernummer}`] : [])],
|
||||
rueckzeile: `${ABSENDER.name} · ${ABSENDER.strasse} · ${ABSENDER.plzOrt}`,
|
||||
},
|
||||
empfaenger: empfaengerZeilen(auftrag.kunde),
|
||||
einleitung: AB_TEXTE.einleitung({ datum: formatDe(auftrag.auftragVom), weg: AUFTRAG_WEGE[auftrag.auftragWeg] }),
|
||||
leistung: festpreis
|
||||
? {
|
||||
art: 'festpreis',
|
||||
ueberschrift: b.leistung,
|
||||
positionen: auftrag.positionen.map((p, i) => ({
|
||||
nr: String(i + 1), beschreibung: p.beschreibung.trim(), menge: String(p.menge),
|
||||
einzelpreis: euroDe(p.einzelpreisCents), summe: euroDe(p.menge * p.einzelpreisCents),
|
||||
})),
|
||||
gesamt: { label: b.gesamt, betrag: euroDe(auftrag.summeCents) },
|
||||
aufwandText: null,
|
||||
}
|
||||
: { art: 'aufwand', ueberschrift: b.leistung, positionen: [], gesamt: null, aufwandText: aufwandText(auftrag) },
|
||||
kleinunternehmer: AB_TEXTE.kleinunternehmer,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user