Skip to content

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-Variable zugferd am 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): BookingReportControllerOrderDocumentDataBuilder (JSON-Datensatz order-document) → ReportServicereport-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 = invoice inkl. aufgelöstem Statustitel und statusabhängiger Beschreibung,
  • supplier.* (Aussteller: Name, Anschrift, Steuernummer, USt-IdNr., Bank, Zahlungsbedingungen — aus company_*-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:

  1. buildXml(array $dataset, string $profile): string — CII-XML.
  2. embed(string $pdf, array $dataset, string $profile): string — Einbettung samt PDF/A-3-Container (3.2/3.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:

  1. Runner rendert das PDF (gewöhnlich, keine PDF/A-Stufe nötig),
  2. Overlay/Signaturen anwenden, bei der Freigabe zusätzlich deren Formatstufe,
  3. zuletzt factur-x.xml einbetten — dabei entsteht der PDF/A-3-Container,
  4. 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 (StkC62/H87, hHUR, 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 zugferd am Auftragsbeleg-Systembericht (SystemReportCatalog::ORDER_DOCUMENT, Grid booking, Ordner orders): 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

  1. Rechnungsnummern-Quelle: ✔ gelöst. Ist zugferd_invoice_number_field konfiguriert und das Feld leer, blockiert preflight() — kein stiller Rückfall auf die Auftragsnummer.
  2. 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).
  3. Rundung/Multi-Steuersatz + Auftragsrabatt: ✔ siehe §6, mit Unit-Tests festgenagelt.
  4. 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.
  5. Bibliotheks-Governance: horstoeko/zugferd ist auf ^1.0 gepinnt (MIT, aktiv gepflegt). Sie zieht 13 Pakete nach — darunter jms/serializer, symfony/validator, setasign/fpdi. Das ist der bewusst gezahlte Preis dafür, CII-Serialisierung und XMP-Einbettung nicht selbst zu pflegen; composer audit läuft im Pre-Commit-Hook.
  6. Schrifteinbettung (neu): PDF/A verlangt eingebettete Fonts, der PdfaProcessor stempelt 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), Template calServer-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