Technischer Artikel

Validierung von E-Rechnungen: veraPDF und Mustang in Delphi

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

PDF Library for Delphi: Schichtdiagramm einer Factur-X-E-Rechnungsdatei: der äußere PDF/A-3-Archivcontainer mit /AFRelationship- und /AF-Verrohrung, geprüft von veraPDF, und das innere factur-x.xml EN-16931-Rechnungs-XML, geprüft von Mustang, wobei keines der Werkzeuge die andere Schicht abdeckt
Die äußere PDF/A-3-Ebene antwortet gegenüber veraPDF, während Mustang das eingebettete EN 16931-XML beurteilt; keine der beiden Prüfungen impliziert die andere

Eine Validierungsmatrix aufbauen

Zu belegen, dass erzeugte Dateien beide Tore überstehen, erfordert eine Matrix. 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 generiert, 3b und 3u, denn die Anforderungen der Stufen B und U weichen beim Unicode-Mapping voneinander ab, und eine Datei, die die eine Stufe besteht, kann bei der anderen durchfallen. Sechs Profile mal zwei Stufen ergeben zwölf Dateien, und jede einzelne sollte headless über denselben Code-Pfad erzeugt werden, den auch Ihre Anwendung verwendet, damit die Testartefakte nicht handoptimiert für den Test sind

Jede Datei an beide Validatoren verfüttern, und die Aufspaltung wird sichtbar. Durchweg korrekte Container-Verdrahtung — registrierte zugehörige Dateien, deklarierte XMP-Konformität, vorhandene Output Intents — passiert veraPDF bei jeder Datei, während eine strukturell gültige PDF/A-3 weiterhin XML tragen kann, das der Geschäftsregel-Validator ablehnt. Genau diese Aufspaltung ist es, die der Zwei-Tool-Ansatz ans Tageslicht bringen soll: Eine Testumgebung, die nur veraPDF vertraut, würde diese Dateien als fertig buchen

PDF Library for Delphi: Zusammenfassung der Validierungsmatrix: sechs Rechnungsprofile von Factur-X EN 16931 bis ZUGFeRD 2.0 BASIC, erzeugt auf Stufen 3b und 3u, ergeben zwölf Dateien; veraPDF besteht zwölf von zwölf, Mustang nur acht, und zwei Korrekturen am Conformance-Token und den Pflicht-Summenelementen schließen die Lücke
Sechs Profile auf zwei Konformitätsstufen ergeben zwölf Dateien: alle zwölf grün bei veraPDF und nur acht grün bei Mustang

Zwei Fallen, die veraPDF bestehen und an Mustang scheitern

Beides sind Details, die man kennen sollte, bevor man diese Profile selbst generiert

Die erste ist das Profil Factur-X EXTENDED France B2B. Übergibt man ein internes Label als Konformitätsgrad und eine interne URN als Richtlinie, weist Mustang die Datei mit einem Invalid-Conformance-Value-Fehler zurück, gefolgt von einem Unsupported-Profile-Type-Fehler. Der Grund: Das XMP-Feld fx:ConformanceLevel ist kein Freitextfeld für Ihre eigene Profilbenennung. 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. Der Standardwert EXTENDED mit FR als Ländercode und der korrekten Richtlinien-URN macht 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;
  // ... die menschenlesbare Rechnungsseite zeichnen ...
  // ExtendedXML trägt eine EN-16931-Guideline-URN der Form
  //   urn:cen.eu:en16931:2017#conformant#urn:factur-x.eu:1p0:extended
  FileID := PDF.AddFacturXAssociatedFileFromString(
    ExtendedXML,
    'EXTENDED',          // Standard-fx:ConformanceLevel, kein internes Label
    'factur-x.xml',
    'Factur-X EXTENDED invoice',
    'Alternative',       // /AFRelationship
    '1.0',
    'FR');               // Frankreich-B2B wird über den Ländercode markiert, nicht über die 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 ist das Profil ZUGFeRD 1.0 COMFORT, und das hat ü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 verlangt, dass die Summenbildung für die Abrechnung im Header, ram:SpecifiedTradeSettlementMonetarySummation, ram:ChargeTotalAmount und ram:AllowanceTotalAmount jeweils genau einmal enthält. Lässt man beide weg, meldet Mustang, dass die Elemente exakt einmal vorkommen müssen. Sie sind nicht optional, wenn das Schema besagt, dass minOccurs eins ist. Beide Elemente in der XSD-Sequenzreihenfolge auszugeben, unmittelbar nach ram:LineTotalAmount, mit dem Wert 0.00, wenn es keine Zu- oder Abschläge gibt, genügt dem Schema. Eine Null ist ein vorhandenes Element; ein fehlendes Element ist eine Schema-Verletzung

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

PDF Library for Delphi-Pipelinediagramm, das die Bibliotheksausgabe an externe Validatoren anbindet: der Generator schreibt invoice.pdf headless, veraPDF prüft den Container, ExtractFacturXXMLToString füttert Mustang mit den EN-16931-Geschäftsregeln, und das Release-Tor endet nur mit null, wenn beide Validatoren bestehen
Die Pipeline erzeugt headless, übergibt dasselbe Artefakt an beide Validatoren und liefert erst aus, wenn ihr gemeinsames Urteil grün ist
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);
    // Das rohe XML an einen dedizierten EN 16931 / Mustang Validator übergeben
    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