Eine Factur-X- oder ZUGFeRD-Rechnung besteht aus zwei Dokumenten unter einem Dateinamen. Das äußere Dokument ist ein PDF/A-3-Container, den ein Archivierungsprogramm die nächsten zehn Jahre lang akzeptieren muss. Das innere Dokument ist eine XML-Rechnung, die das Buchhaltungssystem eines Käufers gemäß EN 16931 parsen muss. Der Fehler, der defekte Rechnungen in die Produktion bringt, ist der Glaube, dass man das zweite Dokument automatisch geschenkt bekommt, wenn man das erste richtig macht. Dem ist nicht so. Eine Datei kann ein makelloses PDF/A-3 sein und trotzdem XML enthalten, das keine Steuerbehörde akzeptieren wird, und sie kann lehrbuchmäßiges EN 16931-XML in einem Container enthalten, der die Archivvalidierung nicht besteht. Die beiden Schichten werden von zwei unterschiedlichen Tools validiert, die nichts voneinander wissen, und eine echte Pipeline muss beide zufriedenstellen
Zwei Validatoren, zwei unterschiedliche Fragen
veraPDF ist die Referenzimplementierung für PDF/A. Richten Sie es auf eine Rechnung und es beantwortet genau eine Frage: Ist dies eine konforme PDF/A-3-Datei? Es prüft die Dinge, auf die ISO 19005-3 Wert legt. Ist jede Schriftart eingebettet? Gibt es ein OutputIntent? Deklarieren die XMP-Metadaten den richtigen Teil und Konformitätsgrad? Bei einer E-Rechnung prüft es außerdem die Infrastruktur für zugehörige Dateien (Associated Files), die PDF/A-3 erfordert, da das XML als eingebettete Datei mit einem /AFRelationship und einem Eintrag im /AF-Array des Dokumentenkatalogs mitreist. veraPDF sagt absolut nichts darüber aus, ob die Rechnungssumme stimmt, denn das fällt nicht in seinen Zuständigkeitsbereich
Mustang ist der Open-Source-Validator des Mustangprojects. Er stellt die orthogonale Frage: Ist das eingebettete XML eine gültige Rechnung? Er prüft das XML gegen das Schema für das deklarierte Profil und wendet dann die Geschäftsregeln der EN 16931 sowie die darauf liegenden länderspezifischen Regelwerke an, darunter auch die CIUS der XRechnung. Er prüft, ob die Umsatzsteuer-Identifikationsnummer (VAT Identifier) des Verkäufers vorhanden ist, wenn die Summen dies erfordern, ob sich Nachlässe und Zuschläge mit dem Gesamtdokumentenbetrag in Einklang bringen lassen und ob die Profil-URN im XML mit dem übereinstimmt, was die Datei vorgibt zu sein. Mustang interessiert sich nicht dafür, ob das umgebende PDF seine Schriftarten einbettet, denn das ist die Aufgabe von veraPDF
Keines der Tools ist eine Obermenge des anderen. veraPDF lässt einen strukturell perfekten Container rund um unsinniges XML passieren. Mustang lässt perfektes XML passieren, das in einem Container mit fehlendem OutputIntent verpackt ist. Jeder Validator fängt genau die Klasse von Fehlern ab, für die der andere blind ist, und das ist der einzige Grund, warum eine ernsthafte Validierungsumgebung (Harness) beide ausführt und eine Datei nur dann als auslieferbar behandelt, wenn beide zustimmen
Die Validierungsmatrix
Um zu beweisen, dass die Bibliothek Dateien produziert, die beide Tore unbeschadet passieren, baut die Testumgebung eine Matrix auf. Sechs Rechnungsprofile decken die Bandbreite ab, auf die eine europäische Pipeline in der Praxis trifft: Factur-X EN 16931, Factur-X BASIC, die Variante Factur-X EXTENDED France B2B, XRechnung 3.0, ZUGFeRD 1.0 COMFORT und ZUGFeRD 2.0 BASIC. Jedes Profil wird gegen zwei PDF/A-Unterkonformitätsstufen (Sub-Conformance Levels), 3b und 3u, generiert, da die Anforderungen der Stufen B und U in Bezug auf das Unicode-Mapping voneinander abweichen und eine Datei, die die eine Stufe besteht, bei der anderen durchfallen kann. Sechs Profile mal zwei Stufen ergibt zwölf Dateien, von denen jede einzelne „Headless“ (ohne Benutzeroberfläche) über denselben Code-Pfad erstellt wird, den auch das GUI-Beispiel mitliefert. Die getesteten Artefakte sind also nicht für den Test handoptimiert
Der Generator schreibt alle zwölf Dateien und ein Skript füttert jede davon in beide Validatoren. Beim ersten vollständigen Durchlauf ließ veraPDF alle zwölf durchgehen. Die Container-Infrastruktur war durchweg korrekt: Zugehörige Dateien waren registriert, XMP-Konformität deklariert, Output Intents vorhanden. Mustang ließ acht passieren. Vier Rechnungen waren strukturell gültige PDF/A-3-Dateien, trugen aber XML in sich, das der Geschäftsregel-Validator ablehnte. Dies ist genau die Aufteilung (Split), die der Zwei-Tool-Ansatz ans Tageslicht bringen soll. Hätte die Testumgebung nur veraPDF vertraut, hätten diese vier Dokumente wie fertig ausgesehen
Die zwei Fixes, die die Lücke geschlossen haben
Die vier Mustang-Fehlschläge ergaben sich aus zwei unterschiedlichen Ursachen, und die Lösung für jede davon ist ein Detail, das Sie kennen sollten, bevor Sie diese Profile selbst generieren
Die erste Ursache betraf das Profil Factur-X EXTENDED France B2B. Der ursprüngliche Generator übergab ein internes Label als Konformitätsgrad und eine interne URN als Richtlinie (Guideline). Mustang wies die Datei mit einem Invalid-Conformance-Value-Fehler zurück, gefolgt von einem Unsupported-Profile-Type-Fehler. Der Grund dafür ist, dass das XMP-Feld fx:ConformanceLevel kein Freitextfeld für Ihre eigene Profilbenennung ist. Factur-X definiert dafür exakt fünf Standardwerte: MINIMUM, BASIC WL, BASIC, EN 16931 und EXTENDED. Eine Frankreich-spezifische B2B-Rechnung ist, was die XMP-Metadaten betrifft, immer noch ein Dokument mit EXTENDED-Profil. Der französische Charakter der Rechnung wird nicht durch die Erfindung eines sechsten Konformitätswertes ausgedrückt. Er wird durch den Ländercode FR und durch den Richtlinien-Identifikator im XML ausgedrückt, der das Präfix urn:cen.eu:en16931:2017#conformant# tragen muss, das eine zur EN 16931 konforme CIUS kennzeichnet. Die Übergabe des Standardwertes EXTENDED mit FR als Ländercode und der korrekten Richtlinien-URN machte die Datei konform
In der Bibliotheks-API ist das ein Aufruf von AddFacturXAssociatedFileFromString, bei dem Konformität, Land und Richtlinie aufeinander abgestimmt sind. Das Argument für den Konformitätsgrad trägt das Standard-Token, das Argument für den Ländercode trägt FR, und die Richtlinien-URN befindet sich in den XML-Bytes, die Sie übergeben
var
FileID: Integer;
begin
PDF.SetPDFAMode(5); // PDF/A-3b
PDF.NewDocument;
// ... draw the human-readable invoice page ...
// ExtendedXML carries an EN 16931 guideline URN of the form
// urn:cen.eu:en16931:2017#conformant#urn:factur-x.eu:1p0:extended
FileID := PDF.AddFacturXAssociatedFileFromString(
ExtendedXML,
'EXTENDED', // standard fx:ConformanceLevel, not an internal label
'factur-x.xml',
'Factur-X EXTENDED invoice',
'Alternative', // /AFRelationship
'1.0',
'FR'); // France B2B marked by country code, not by conformance
if FileID = 0 then
raise Exception.Create('Factur-X attachment rejected');
PDF.SaveToFile('02_Factur-X-EXTENDED-FR_PDFA-3b.pdf');
end;
Die zweite Ursache war das Profil ZUGFeRD 1.0 COMFORT, und das hatte überhaupt nichts mit Metadaten zu tun. ZUGFeRD 1.0 wird gegen das :1p0-XSD validiert, das in Bezug auf Kardinalitäten strenger ist, als die Zusammenfassungen in Textform vermuten lassen. Das XSD erfordert, dass die Summenbildung für die Abrechnung im Header, ram:SpecifiedTradeSettlementMonetarySummation, die Felder ram:ChargeTotalAmount und ram:AllowanceTotalAmount jeweils genau einmal enthält. Das generierte XML ließ beide weg, also meldete Mustang, dass die Elemente exakt einmal vorkommen müssen. Sie sind nicht optional, wenn das Schema besagt, dass minOccurs eins ist. Die Ausgabe beider Elemente in der XSD-Sequenzreihenfolge, unmittelbar nach ram:LineTotalAmount, mit einem Wert von 0.00, wenn es keine Zu- oder Abschläge gibt, genügte dem Schema. Eine Null ist ein vorhandenes Element; ein fehlendes Element ist eine Schema-Verletzung. Mit diesen beiden Korrekturen erreichte die Matrix zwölf von zwölf bei Mustang und blieb gleichzeitig bei zwölf von zwölf bei veraPDF
Die XRechnung-Felder, die aus ungültig gültig machen
Die XRechnung verdient eine eigene Anmerkung, da ihre deutsche CIUS Geschäftsregeln hinzufügt, die im Basis-Regelwerk der EN 16931 fehlen, und sie schlagen auf eine Art und Weise fehl, die auf den ersten Blick so aussieht, als wäre mit dem Dokument alles in Ordnung. Zwei davon betreffen elektronische Adressen. BT-34 ist die elektronische Adresse des Verkäufers und BT-49 ist die elektronische Adresse des Käufers, also die Routing-Endpunkte, die ein deutsches Portal für den öffentlichen Sektor nutzt, um die Rechnung zuzustellen und zu bestätigen. Das Basis-Modell der EN 16931 behandelt sie als optional. Die XRechnung tut das nicht. Lassen Sie eines von beiden weg, und die Rechnung ist wohlgeformt, schema-gültig und abgelehnt
Die dritte ist die Regel BR-DE-6, die vorschreibt, dass die Telefonnummer des Verkäuferkontakts vorhanden sein muss. Das ist die Art von Feld, die ein Entwickler gerne mal weglässt, weil es sich eher nach Präsentation als nach Daten anfühlt. Das Fehlen dieses Feldes erzeugt einen Validierungsfehler, der eher auf die Verkäuferkontaktgruppe verweist als auf etwas offensichtlich Fehlendes. Das Bereitstellen von BT-34, BT-49 und der Telefonnummer des Verkäufers ist es, was eine XRechnung-Datei unter Mustang von ungültig zu gültig verschiebt, und nichts davon ändert irgendetwas an dem, was veraPDF sieht, da alle drei im XML leben
Verdrahtung der Bibliotheksausgabe mit einem Validator
Der architektonische Gedanke hinter der Testumgebung (Harness) lässt sich auf jedes Business-System verallgemeinern. Die PDF-Bibliothek schreibt einen konformen Container und bettet das XML ein. Sie versucht nicht – und sollte es auch nicht versuchen –, die Autorität für die Geschäftsregeln der EN 16931 zu sein. ValidateFacturXInvoice in der Bibliothek prüft die Konsistenz des Containers: ob das /AF-Array im Katalog, der Name-Tree der eingebetteten Dateien, der XMP-DocumentFileName, das Profil, die Guideline und die /AFRelationship übereinstimmen. Sie validiert jedoch keine Steuercodes und gleicht keine Beträge ab. Die richtige Arbeitsteilung besteht darin, dass das Business-System das XML extrahiert und es einem dedizierten Rechnungsvalidator übergibt, genau so, wie das Harness es an Mustang übergibt
Wenn Sie die Datei wieder einlesen, erfahren Sie, was tatsächlich geschrieben wurde. DetectFacturXInvoice meldet, ob eine Rechnung erkannt wurde, und GetFacturXInvoiceInfo liest die Metadatenfelder nach Tag aus: Tag 1 ist der Name der eingebetteten Datei, Tag 2 der XMP-DocumentFileName, Tag 5 der Konformitätsgrad, Tag 6 der Guideline-Identifikator und Tag 7 die /AFRelationship. Die Bestätigung, dass der zurückgelesene Konformitätsgrad das Standard-Token und nicht ein internes Label ist, ist der kostengünstigste Weg, um den EXTENDED-Fehler abzufangen, bevor eine Datei Ihren Build-Prozess verlässt
function ExtractAndInspect(const PdfPath: string): AnsiString;
var
Profile, Guideline: WideString;
begin
Result := '';
PDF.LoadFromFile(PdfPath);
if PDF.DetectFacturXInvoice = 1 then
begin
Profile := PDF.GetFacturXInvoiceInfo(5); // fx:ConformanceLevel
Guideline := PDF.GetFacturXInvoiceInfo(6); // XML guideline ID
Writeln('Profile: ', Profile);
Writeln('Guideline: ', Guideline);
// Hand the raw XML to a dedicated EN 16931 / Mustang validator.
Result := PDF.ExtractFacturXXMLToString;
end;
end;
ExtractFacturXXMLToString gibt die rohen XML-Bytes als AnsiString zurück, bereit, um in eine Datei geschrieben oder an einen Validator-Prozess gestreamt zu werden. In der Testumgebung ist dieses Ziel Mustang, aufgerufen über sein Command-Line-Jar, wobei veraPDF im selben Durchlauf über dieselbe Datei ausgeführt wird. Die Verdrahtung ist gering: Ein Konsolen-Generator, EInvoiceValidation.dpr, schreibt die zwölf Dateien unter Verwendung des gemeinsamen Rechnungsmodells aus dem Beispielprogramm, und ein Skript, run-validation.ps1, steuert beide Validatoren über das Ausgabeverzeichnis und druckt eine Pass/Fail-Tabelle. Dasselbe zweistufige Konstrukt – Generieren mit der Bibliothek und Verifizieren mit externen Validatoren – sollte ein Continuous-Integration-Job bei jeder Änderung an der Rechnungsgenerierung ausführen, denn der einzige Weg, um zu wissen, dass eine Datei beide Schichten zufriedenstellt, besteht darin, beide Tools zu fragen
Wenn Ihre Pipeline den Container vor dem Signieren auch Zertifizieren muss, wird die Preflight-Seite dieser Arbeit in unserem Walkthrough zu PDF/A- und PDF/UA-Preflight in Delphi behandelt, und der umfassendere Zertifizieren-dann-Signieren-Workflow wird in der Compliance- und Signatur-Workbench beschrieben. Beide bauen auf demselben Generierungspfad auf, der als Teil der Delphi-PDF-Bibliothek für Delphi und C++Builder ausgeliefert wird, zusammen mit den hier verwendeten APIs für PDF/A, zugehörige Dateien (Associated Files) und Metadaten