Konzept: Optionale ZUGFeRD-Ausgabe für Auftragsrechnungen (V2)¶
Stand: 2026-09-07 · Status: umgesetzt (Phase 1 + 2) · Scope: Rechnungen aus der Auftragsverwaltung (Report-Contract
order-document,document.type = invoice)Kurzfassung: Rechnungen werden optional als ZUGFeRD 2.x im Profil EN 16931 (Factur-X-kompatibel) erzeugt — ein hybrides PDF/A-3b, in das die maschinenlesbare Rechnung als CII-XML (
factur-x.xml) eingebettet ist. Eingeschaltet wird das über die Report-Variablezugferdam Auftragsbeleg-Systembericht.Was sich gegenüber der Planung geändert hat, mit Begründung im ADR 2026-09-07:
- Der Auftragsbeleg ist ein Systembericht geworden. Das war die eigentlich offene Frage: Ohne feste Zeile gibt es keinen verlässlichen Ort für die Aktivierung und die Feldzuordnungen (§5).
- PDF/A-3 braucht gar keinen eigenen Schritt (§3.2). Die Bibliothek schreibt den Container beim Einbetten selbst. Der geplante Runner-Umbau war zwischenzeitlich gebaut und wurde zurückgenommen — er hätte Marker gestempelt, die eine Zeile später überschrieben werden.
- Der Datensatz steht bei Contract 1.8 (§4), nicht 1.3/1.4 — dazwischen kamen Sammelrechnung und Währung.
1. Ziel und Einordnung¶
- Was: Beim Generieren einer Auftrags-Rechnung (Statusgruppe Billing/Invoice/Rechnung) optional zusätzlich zur visuellen PDF-Rechnung die strukturierte E-Rechnung nach EN 16931 erzeugen und als ZUGFeRD-/Factur-X-Anhang in ein PDF/A-3 einbetten.
- Warum: Deutschland hat mit dem Wachstumschancengesetz die E-Rechnungspflicht im B2B eingeführt (Empfangspflicht seit 01.01.2025; Ausstellungspflicht gestaffelt bis 2028). ZUGFeRD ≥ 2.0.1 im Profil EN 16931 gilt als konformes Format. Kunden von calServer (Kalibrierlabore, Dienstleister) stellen B2B-Rechnungen — die Auftragsverwaltung ist der natürliche Ort dafür.
- Optional: Die Ausgabe wird pro Report-Einstellung aktiviert (Abschnitt 5). Ohne Aktivierung bleibt alles beim heutigen reinen PDF.
- Nicht-Ziel (vorerst): XRechnung als reines XML (B2G, Leitweg-ID), Gutschriften/Storno-Belege, Sammelrechnungs-Sonderfälle — als Ausbaustufen in Abschnitt 8 eingeplant.
2. Ausgangslage (Ist-Stand)¶
Reportpfad Rechnung (V2): BookingReportController →
OrderDocumentDataBuilder (JSON-Datensatz order-document) →
ReportService → report-runner (JasperReports 6.20.6, JSON-Datasource)
→ PDF → optional Overlay (ReportOverlayService), Signatur
(ReportSignatureService), DMS-Ablage, Mailversand, Freigabe.
Seit Contract v1.2 liefert der Datensatz die für EN 16931 zentralen Blöcke serverseitig:
document.type = invoiceinkl. aufgelöstem Statustitel und statusabhängiger Beschreibung,supplier.*(Aussteller: Name, Anschrift, Steuernummer, USt-IdNr., Bank, Zahlungsbedingungen — auscompany_*-Report-Variablen),recipient= Rechnungsadresse (mit V1-Fallback auf den Auftraggeber),positions[](Menge, Einheit, Einzelpreis, Rabatt, Steuersatz, Netto),statistics(Steuergruppen je Satz, Netto/USt./Brutto, Auftragsrabatt).
Damit ist die Datenfrage im Kern gelöst; offen sind XML-Serialisierung, PDF/A-3-Erzeugung und Einbettung.
3. Zielarchitektur¶
Grundsatz: eine Datenquelle. Das ZUGFeRD-XML wird aus demselben
order-document-Datensatz gebaut wie das visuelle PDF — kein zweiter
Datenpfad, keine Abweichungsgefahr zwischen Papierbild und XML.
sequenceDiagram
participant C as BookingReportController
participant B as OrderDocumentDataBuilder
participant R as report-runner (Jasper)
participant Z as ZugferdInvoiceService (neu)
C->>B: build(booking) — order-document v1.8
C->>R: generateReport(…, dataset)
R-->>C: PDF (visuelle Rechnung)
C->>C: Overlay + Signatur
C->>Z: embed(pdf, dataset)
Z->>Z: CII-XML (EN 16931) aus dataset
Z->>Z: factur-x.xml einbetten, XMP + OutputIntent (PDF/A-3b)
Z-->>C: hybrides ZUGFeRD-PDF
C-->>C: DMS/Versand/Download wie bisher
3.1 XML-Erzeugung (Laravel)¶
Neuer Service App\Services\Report\ZugferdInvoiceService mit zwei Belangen:
buildXml(array $dataset, string $profile): string— CII-XML.embed(string $pdf, array $dataset, string $profile): string— Einbettung samt PDF/A-3-Container (3.2/3.3).preflight(array $dataset): array— blockierende Befunde vor dem Bauen,warnings()für die nicht-blockierenden.
Bibliothek: horstoeko/zugferd
(PHP ≥ 8, MIT, aktiv gepflegt): ZugferdDocumentBuilder für das XML,
ZugferdDocumentPdfBuilder für die Einbettung inkl. korrekter
XMP-Metadaten und AFRelationship. Alternative (verworfen): eigene
CII-Serialisierung — hoher Pflegeaufwand, Schematron-Risiko.
Abgewogen: Java-Seite (Mustangproject im report-runner)? Mustang könnte XML + Einbettung im Runner erledigen. Dagegen spricht: die fachlichen Rechnungsdaten (Contract, Report-Variablen, W41xx-Konfiguration) leben in Laravel; der Runner bleibt bewusst ein generischer Render-Dienst. Empfehlung: Laravel.
3.2 PDF/A-3 (kommt aus der Bibliothek)¶
ZUGFeRD verlangt als Container PDF/A-3. Nicht als Formalie: PDF/A-1 und -2
verbieten eingebettete Dateien. Ein als PDF/A-1b markiertes PDF mit
factur-x.xml ist deshalb nicht „fast konform", sondern nachweislich unkonform.
Dieser Abschnitt war die falsche Annahme des Konzepts. Weder Jasper noch
der report-runner müssen etwas tun: ZugferdDocumentPdfBuilder schreibt beim
Einbetten sein eigenes XMP (pdfaid:part = 3) samt sRGB-OutputIntent und
überschreibt, was vorher an PDF/A-Auszeichnung im Dokument stand.
Nachgemessen an einem gewöhnlichen FPDI-PDF: vorher kein pdfaid-Marker,
nachher Teil 3, OutputIntent und factur-x.xml im Anhang. Festgehalten in
ZugferdInvoiceServiceTest::test_the_embedded_result_is_a_pdfa3_container().
Der geplante Runner-Umbau (Job-Flag pdfaConformance bzw. ein part-Parameter
am PDF/A-Endpunkt) war zwischenzeitlich gebaut und ist zurückgenommen: ein
Java-Eingriff plus ein HTTP-Round-Trip je Rechnung, dessen Ergebnis eine Zeile
später überschrieben wird. Dass die Freigabe vorher nach PDF/A-1b wandelt, ist
aus demselben Grund unschädlich.
Offen bleibt die Schrift-Einbettung. PDF/A verlangt eingebettete Fonts;
gestempelt wird der Marker, geprüft nichts. Die V2-Bundles deklarieren DejaVu
mit isPdfEmbedded="false" und verlassen sich auf die Runner-Font-Extension.
Das ist der Punkt, an dem ein veraPDF-Lauf zuerst anschlagen wird (Phase 2b);
dasselbe gilt für Transparenz in Logo-Overlays.
3.3 Einbettung & Nachbearbeitungs-Reihenfolge¶
Heutige PDF-Nachbearbeitung: Overlay (Briefpapier) und Signatur-Stamping. Beide verändern das PDF und dürfen die PDF/A-Konformität nicht nach der Einbettung brechen. Verbindliche Reihenfolge:
- Runner rendert das PDF (gewöhnlich, keine PDF/A-Stufe nötig),
- Overlay/Signaturen anwenden, bei der Freigabe zusätzlich deren Formatstufe,
- zuletzt
factur-x.xmleinbetten — dabei entsteht der PDF/A-3-Container, - ablegen/versenden (DMS, Mail, Download) — danach keine Mutation mehr.
4. Feldmapping EN 16931 → order-document¶
Pflicht-/Kernfelder (BT = Business Term, BG = Business Group):
| EN 16931 | Bedeutung | Quelle (v1.4) | Status |
|---|---|---|---|
| BT-1 | Rechnungsnummer | document.number — Achtung: Installationen mit W41xx-Rechnungsnummernfeld brauchen einen Konfig-Schlüssel (Abschnitt 5) |
◐ |
| BT-2 | Rechnungsdatum | document.date |
✔ |
| BT-3 | Rechnungstyp-Code | fix 380 (Rechnung); 381 Gutschrift → Ausbaustufe |
✔ |
| BT-5 | Währung | fehlt — Konfig-Variable company_currency (Default EUR) |
✖ |
| BT-9 | Fälligkeitsdatum | fehlt — aus Zahlungsziel ableiten (neue Variable company_payment_due_days) |
✖ |
| BT-20 | Zahlungsbedingungen | supplier.payment_terms |
✔ |
| BG-4 / BT-27/31/32 | Verkäufer, USt-IdNr., Steuernummer | supplier.* (name, vat_id, tax_number, Adresse) |
✔ |
| BT-40 | Verkäufer-Ländercode | supplier.country — muss ISO 3166-1 alpha-2 sein (Validierung/Normalisierung nötig) |
◐ |
| BG-7 / BT-44/46 | Käufer + Kennung | recipient (invoice-Rolle) + customer_number |
✔ |
| BT-10 | Käuferreferenz (B2G: Leitweg-ID) | fehlt — W41xx-Feld konfigurierbar mappen | ✖ |
| BT-72 | Tatsächliches Lieferdatum | fehlt — aktuell nur Hinweistext im PDF; konfigurierbares W41xx-/Belegfeld | ✖ |
| BG-13 | Lieferinformation | delivery-Rolle |
✔ |
| BG-16 / BT-84/85/86 | Zahlungsweg (Überweisung, IBAN/BIC) | supplier.iban, supplier.bic, supplier.bank_name |
✔ |
| BG-20 / BT-92 | Nachlass Dokumentebene | statistics.discount_amount (Prozentsatz in order_discount_percent), je Satz aufgeteilt in tax_groups[].allowance |
✔ |
| BG-21 / BT-99 | Zuschlag Dokumentebene | statistics.charge_total (Versandkosten des Auftrags), je Satz in tax_groups[].charge |
✔ |
| BG-22 / BT-106…112 | Summen | net_total (BT-106), discount_amount (BT-107), charge_total (BT-108), net_taxable (BT-109), vat_total (BT-110), gross_total (BT-112) |
✔ |
| BG-23 / BT-116…119 | USt.-Aufschlüsselung je Satz | statistics.tax_groups[] (net = Bemessungsgrundlage BT-116, vat = BT-117) + Kategorie-Code: S (Satz > 0), Z (0 %); E/AE/K-Fälle → Ausbaustufe |
◐ |
| BG-25 / BT-126…131, BT-146 | Positionen | positions[] (pos_number, name, quantity, price, discount, tax, line_net) |
✔ |
| BT-130 | Einheiten-Code (UN/ECE Rec 20) | positions[].unit ist Freitext („Stk") — Mapping-Tabelle nötig (Stk → C62/H87, h → HUR, Fallback C62) |
◐ |
Erledigt. Die vier Lücken sind geschlossen: die Währung mit Contract 1.6
(statistics.currency), Fälligkeit, Lieferdatum und Käuferreferenz mit
Contract 1.8, dazu Ländercodes (BT-40/BT-55) und Einheiten-Codes (BT-130).
Die mit ◐ markierten Punkte oben sind damit ebenfalls abgeräumt:
| Punkt | Lösung |
|---|---|
| BT-1 Rechnungsnummer aus W41xx | zugferd_invoice_number_field; konfiguriert-aber-leer ist ein Fehler, kein Rückfall |
| BT-40 Ländercode | App\Support\CountryCode (aus dem DCC-Generator extrahiert, eine Liste für beide Belege) |
| BT-130 Einheiten | App\Support\UnitCode — UN/ECE Rec 20, Rückfall C62, unbekannte Einheiten meldet warnings() |
| BG-23 Kategorie-Codes | S und Z umgesetzt; E/AE/K bleiben Ausbaustufe (§8.4) |
5. Aktivierung und Konfiguration¶
Analog zur bestehenden data_contract-Mechanik über Report-Variablen:
- Report-scoped Variable
zugferdam Auftragsbeleg-Systembericht (SystemReportCatalog::ORDER_DOCUMENT, Gridbooking, Ordnerorders):en16931(Empfehlung),basic,extended; nicht gesetzt = aus. Ein unbekannter Inhalt zählt ebenfalls als aus — ein Tippfehler darf keine Rechnung mit einem ungewählten Profil erzeugen.
Dass es diesen Systembericht gibt, ist die Voraussetzung der ganzen
Aktivierung. Vorher hätte die Variable an einer Zeile gehangen, die auf
einer frischen Installation gar nicht existiert und auf einem V1-System
mehrfach. Siehe
ADR 2026-09-07.
- Wirksam nur wenn document.type = invoice und Ausgabeformat pdf —
andere Dokumenttypen/Formate ignorieren die Variable stillschweigend.
- Stammdaten über die vorhandenen company_*-Variablen (v1.2), ergänzt um:
company_currency, company_payment_due_days, company_country_code
sowie optionale Mappings zugferd_invoice_number_field,
zugferd_buyer_reference_field, zugferd_delivery_date_field
(W41xx-api_names, da die Feldbelegung installationsspezifisch ist —
vgl. v1-ordermanagement-analysis.md §11).
- Greift in den vier Wegen, auf denen die Rechnung das Haus verlässt: Download
(generate), Mail (send), DMS (save-to-dms) und Freigabe (release).
Die Vorschau bleibt bewusst außen vor — sie zeigt das Layout, und ein
XML-Anhang ist daran nicht zu sehen; der Runner-Aufruf für die
PDF/A-3-Wandlung wäre reine Wartezeit.
- Prüfen vor dem Einschalten:
GET /bookings/{id}/reports/{reportId}/einvoice liefert das reine CII-XML
oder 422 mit der Liste der fehlenden Pflichtangaben. Antwortet auch ohne
gesetzte Variable im Standardprofil — er ist das Werkzeug, mit dem man
herausfindet, ob die Stammdaten reichen. Derselbe Endpunkt bedient
Empfänger, die reines XML nehmen.
6. Validierung und Tests¶
| Ebene | Werkzeug | Stand |
|---|---|---|
| Rundungs-/Summenregeln (BR-CO-10, BR-CO-13, BR-CO-15) | Unit-Tests, die das erzeugte XML zurücklesen und gegen OrderTotalsCalculator rechnen |
✔ tests/Unit/Services/Report/ZugferdInvoiceServiceTest.php |
| XML-Struktur (CII) | ZugferdDocumentReader liest das Erzeugte zurück; Felder werden einzeln geprüft |
✔ ebenda |
| Pflichtangaben vor dem Bauen | ZugferdInvoiceService::preflight() + Feature-Test am einvoice-Endpunkt |
✔ |
| Schematron (EN 16931 + ZUGFeRD-Profilregeln) | KoSIT-Validator bzw. Mustang-CLI --action validate |
offen — braucht ein externes Artefakt, gehört ins Dual-Run-Runbook |
| PDF/A-3-Konformität | veraPDF | offen — erster erwarteter Befund: Schrifteinbettung (§3.2) |
| Hybrid-Sichtprüfung | Quba-Viewer / Zoll-ZUGFeRD-Viewer | offen — Abnahme |
Die Unit-Tests lesen das erzeugte XML mit dem Reader derselben Bibliothek zurück. Das ist bewusst kein Ersatz für den Schematron-Lauf: es prüft, dass die richtigen Werte in den richtigen BT-Feldern stehen, nicht, dass das Profil in allen Regeln erfüllt ist.
Summenbildung: erledigt (Contract v1.4). Der Punkt stand hier als Blocker — die Summen mussten vor der ZUGFeRD-Aktivierung fixiert sein, sonst schlagen die BR-CO-Prüfungen fehl. Umgesetzt ist jetzt die EN-16931-Variante, nicht die V1-Arithmetik: der Auftragsrabatt mindert die Bemessungsgrundlage und wird dafür anteilig auf die Steuergruppen verteilt (BR-S-08), die Umsatzsteuer entsteht je Gruppe auf dem geminderten Netto. Der Rundungsrest der Verteilung geht auf die betragsgrößte Gruppe, damit BT-109 = BT-106 − BT-107 + BT-108 auf den Cent aufgeht (BR-CO-13).
Die V1-Formel (Statistics.jrxml: Steuer auf dem Netto vor Rabatt plus eine
Korrektur mit einem Steuersatz) ist damit bewusst nicht nachgebaut. Sie ist
bei gemischten Sätzen schon in sich nicht schlüssig und weist zu viel
Umsatzsteuer aus — bei 245,00 Auftragswert mit 10 Prozent Rabatt 4,11 Euro zu
viel. Beim Dual-Run ist die Abweichung deshalb erwartet und kein Befund.
Rechnung und Invarianten stehen in
laravel/app/Services/Booking/OrderTotalsCalculator.php, festgenagelt in
tests/Unit/Services/OrderTotalsCalculatorTest.php.
7. Phasenplan¶
| Phase | Inhalt | Stand |
|---|---|---|
| 0 — Datengrundlage | Contract v1.2 (supplier, recipient, document.type, Statusauflösung) |
✔ erledigt |
| 1 — XML-Builder | ZugferdInvoiceService::buildXml() auf horstoeko/zugferd, Feldlücken aus §4 (Contract v1.8, additiv), Einheiten-Mapping (App\Support\UnitCode), Ländercodes (App\Support\CountryCode), Unit-Tests inkl. BR-CO-Summenregeln |
✔ erledigt |
| 2 — Hybrid-PDF | embed() + Reihenfolge §3.3, zugferd-Variable am Systembericht, Verdrahtung von Download/Mail/DMS/Freigabe. Der PDF/A-3-Container kommt aus der Bibliothek — kein Runner-Eingriff noetig (§3.2) |
✔ erledigt |
| 2b — Abnahme | veraPDF gegen das erzeugte PDF/A-3b, KoSIT-/Schematron-Lauf gegen das XML, Sichtprüfung im Quba-Viewer. Braucht externe Artefakte und gehört ins Dual-Run-Runbook, nicht in die CI | offen |
| 3 — Ausbau | Gutschrift/Storno (BT-3 381), Steuer-Sonderfälle (§ 19 UStG, Reverse Charge AE, i.g. Lieferung K) samt Befreiungsgründen BT-120, XRechnung als eigenes Profil |
nach Bedarf |
XRechnung ist teilweise schon da: Der Endpunkt
GET /bookings/{id}/reports/{reportId}/einvoice liefert das reine CII-XML, und
BT-10 (Leitweg-ID) kommt über zugferd_buyer_reference_field. Was fehlt, ist
das XRechnung-Profil mit seinen zusätzlichen Schematron-Regeln — für B2G
reicht das reine EN-16931-XML nicht in jedem Fall.
8. Risiken und offene Punkte¶
- Rechnungsnummern-Quelle: ✔ gelöst. Ist
zugferd_invoice_number_fieldkonfiguriert und das Feld leer, blockiertpreflight()— kein stiller Rückfall auf die Auftragsnummer. - PDF/A-Bruch durch Nachbearbeitung: ✔ Reihenfolge erzwungen (§3.3): die Einbettung ist der letzte Schritt, nach Overlay, Signatur und der Freigabe-Wandlung. Der veraPDF-Nachweis steht noch aus (Phase 2b).
- Rundung/Multi-Steuersatz + Auftragsrabatt: ✔ siehe §6, mit Unit-Tests festgenagelt.
- Steuer-Sonderfälle: offen. §19 UStG (Kleinunternehmer), Reverse-Charge
(AE), innergemeinschaftliche Lieferung (K) brauchen Kategorie-Codes +
Pflichttexte — Phase 3, bis dahin dokumentierte Einschränkung auf S/Z.
taxCategory()rät bewusst nicht. - Bibliotheks-Governance: horstoeko/zugferd ist auf
^1.0gepinnt (MIT, aktiv gepflegt). Sie zieht 13 Pakete nach — darunterjms/serializer,symfony/validator,setasign/fpdi. Das ist der bewusst gezahlte Preis dafür, CII-Serialisierung und XMP-Einbettung nicht selbst zu pflegen;composer auditläuft im Pre-Commit-Hook. - Schrifteinbettung (neu): PDF/A verlangt eingebettete Fonts, der
PdfaProcessorstempelt aber nur Marker. Galt für PDF/A-1b schon vorher; mit Teil 3 wird es erstmals von außen prüfbar. Erster erwarteter veraPDF-Befund.
9. Referenzen¶
- Contract & Builder:
laravel/app/Services/Report/OrderDocumentDataBuilder.php(v1.2), TemplatecalServer-reports/ORDER-JSON-SAMPLE/ - Pipeline:
laravel/app/Http/Controllers/Api/V2/BookingReportController.php,laravel/app/Services/ReportService.php,report-runner/ - Runbook:
laravel/docs/v2/report-dual-run-acceptance.md - Analyse Auftragswesen:
v1-ordermanagement-analysis.md - Standards: EN 16931-1, ZUGFeRD 2.3 (FeRD), Factur-X 1.07, UN/ECE Rec 20, § 14 UStG