Technischer Artikel

EN 16931, XRechnung und Peppol nativ in Delphi prüfen

PDFlibPas, die losLab Delphi PDF Library, prüft die Geschäftsregeln von EN 16931, XRechnung 3.0.2 und Peppol BIS Billing 3.0 nativ in Pascal. TPDFlib.ValidateInvoiceBusinessRulesBuiltIn liest das in einem Factur-X- oder ZUGFeRD-PDF eingebettete XML und liefert einen JSON-Report mit offiziellen Regel-IDs, ohne Java-Laufzeit, ohne Saxon und ohne Schematron-Toolchain. Der Report nennt sich selbst eine Teilmenge, und genau dieses Label ist das wichtigste Feld darin

Pipeline der eingebauten EN-16931-Rechnungsvalidierung in der PDFlibPas-PDF-Bibliothek für Delphi: Factur-X-Erkennung, Schema- und Schematron-Stufen und ein JSON-Report zur abgedeckten Teilmenge
der eingebaute Validator fährt Schema- und Schematron-Stufen in-process und meldet exakt, welche Regeln die Teilmenge abdeckt und welche nicht

Der Druck dahinter ist größtenteils deutsch. Seit Januar 2025 muss jeder inländische Betrieb in Deutschland strukturierte B2B-E-Rechnungen empfangen können, und die Ausstellungspflicht folgt in Etappen. Plötzlich muss die Empfangsseite einer Delphi-Finanzbuchhaltungs- oder ERP-Anwendung eine Frage beantworten, die sie sich nie vorher gestellt hat: Ist das XML in diesem PDF überhaupt eine gültige EN-16931-Rechnung? Die offizielle Antwort kommt aus Schematron-Artefakten, kompiliert nach XSLT 2.0, oder aus dem KoSIT-Validator für XRechnung, und beide laufen auf der JVM. Eine JRE neben jeder Desktop-Installation auszuliefern und zu patchen, nur um eine schlechte Rechnung abzulehnen, ist ein Kostenblock, den viele Teams lieber nicht schleppen

Was prüft der eingebaute Validator tatsächlich?

Der native Validator bildet das Rechnungs-XML auf die EN-16931-Geschäftssterme ab und wendet einen dokumentierten Teilbestand der Regeln aus den EN-16931-Validierungsartefakten 1.3.16 an, wobei jeder Befund unter seiner offiziellen Regel-ID gemeldet wird. Beide Syntaxen werden unterstützt: UN/CEFACT CII D16B, das Format, das Factur-X und ZUGFeRD einbetten, und OASIS UBL 2.1 Invoice und CreditNote. Die abgedeckten Bereiche sind:

  • Pflichtangaben: Spezifikationskennung, Rechnungsnummer, Ausstellungsdatum, Typcode, Währung, Verkäufer- und Käufername, Adressen, Dokumentsummen, USt-Aufschlüsselung und Positionen (BR-01 bis BR-16, BR-21 bis BR-28, BR-CO-04, BR-CO-18, BR-CO-26, BR-45 bis BR-48)
  • Berechnungen: Summen für Positionen, Nachlässe, Zuschläge, Steuern und Zahlbetrag, dazu Steuergrundlage und Steuerbetrag je USt-Kategorie (BR-CO-10 bis BR-CO-17, BR-xx-08, BR-xx-09)
  • USt-Kategorieregeln für S, Z, E, AE, K, G, O, L und M: Positionssätze, Steuerbefreiungsgründe und die USt-Kennnummern, die jede Kategorie verlangt
  • Alle Codelisten BR-CL-01 bis BR-CL-26, inklusive der über 2100 UN/ECE Rec 20/21-Einheitencodes hinter BR-CL-23, und die Zweidekimalstellen-Regeln BR-DEC-01, -05, -09 bis -20 und -23
  • Profilregeln: XRechnung 3.0.2 (BR-DE-1 bis BR-DE-31, BR-DEX-01 bis BR-DEX-14) auf CII und UBL, dazu die Peppol-BIS-Billing-3.0.20-Core-, Common-, Codelisten- und nationalen Regeln auf UBL

Die Abdeckung wird gemeldet, nicht impliziert. Ohne Schema-Datei bestätigt die Schema-Stufe Wohlgeformtheit und Wurzelelement. Jede Stufe listet die Regeln außerhalb der Teilmenge in einem notCovered-Feld auf, und GetBuiltInInvoiceRuleCatalog liefert die ganze Zuordnung als JSON, damit die UI dem Anwender genau zeigen kann, was ein grünes Ergebnis abdeckt

Eine weitere Grenze wird leicht übersehen. Factur-X-MINIMUM- und BASIC-WL-Dokumente sind keine EN-16931-Rechnungen, also wendet der Validator keine EN-16931-Regeln auf sie an. Stattdessen fügt er einen informativen BuiltIn.Coverage.Profile-Befund hinzu, der genau das sagt, was die ehrliche Antwort ist und zugleich ein Hinweis: Nach den deutschen Regeln zählen diese beiden Profile ohnehin nicht als E-Rechnungen

Das eingebettete XML eines hybriden PDFs validieren

ValidateInvoiceBusinessRulesBuiltIn nimmt ein Argument entgegen, den Namen der eingebetteten Datei, und ein leerer String ist die normale Wahl. PDFlibPas wählt dann den Anhang, den die Factur-X-Erkennung identifiziert; bei einem PDF ohne Factur-X-Metadaten fällt es auf den einzigen .xml-Anhang zurück. Mehrere .xml-Anhangskandidaten oder zwei Anhänge mit dem angefragten Namen erzeugen einen Invoice.AttachmentAmbiguous-Fehler statt einer Vermutung, die Dekodierung ist auf 64 MB begrenzt, und eine fehlende Rechnung setzt LastErrorCode auf 401

uses
  System.SysUtils, System.JSON, PDFlibrary;

procedure CheckInboundInvoice(const FileName: string);
var
  PDF: TPDFlib;
  Report: TJSONObject;
begin
  PDF := TPDFlib.Create;
  try
    if PDF.LoadFromFile(FileName, '') <> 1 then
      raise Exception.CreateFmt('Cannot open %s (error %d)',
        [FileName, PDF.LastErrorCode]);
    // Leerer Name: den erkannten Factur-X-/ZUGFeRD-Anhang validieren
    Report := TJSONObject.ParseJSONValue(
      PDF.ValidateInvoiceBusinessRulesBuiltIn('')) as TJSONObject;
    try
      Writeln('profile:  ', Report.GetValue<string>('profile'));
      Writeln('valid:    ', Report.GetValue<Boolean>('valid'));
      Writeln('coverage: ', Report.GetValue<string>('coverage'));        // "subset"
      Writeln('full:     ', Report.GetValue<Boolean>('fullConformance')); // hier nie True
    finally
      Report.Free;
    end;
  finally
    PDF.Free;
  end;
end;

Die oberste Ebene des Reports trägt standard, version, profile, complete, valid, fullConformance, coverage, eine schema- und eine schematron-Stufe sowie ein package-Objekt, das im Built-in-Modus null ist. Jede Stufe meldet einen Status, die Engine, die Anzahl der ausgeführten Regeln, ihre Abdeckung und ein issues-Array. Jeder Befund hat ruleID, fieldPath, severity, category, line und message

Befunde nach Kategorie lesen statt nach Meldung

Das category-Feld macht den Report in einem echten Posteingang brauchbar, denn es sagt dir, wer das Problem beheben muss. Die Werte sind syntax, schema, cardinality, calculation, vat, codelist, decimal, profile, coverage, processing und integrity. Ein calculation-Befund zeigt meist auf den Rundungscode des Absenders, ein codelist-Befund auf die Stammdaten, und ein profile-Befund auf ein XRechnung-Feld wie die Käuferreferenz

procedure ListIssues(Report: TJSONObject);
var
  Item: TJSONValue;
  Issue: TJSONObject;
begin
  for Item in Report.GetValue<TJSONArray>('schematron.issues') do
  begin
    Issue := Item as TJSONObject;
    if Issue.GetValue<string>('severity') = 'info' then
      Continue;  // Abdeckungshinweise wie BuiltIn.Coverage.Profile
    Writeln(Format('[%s] %s at %s: %s', [
      Issue.GetValue<string>('category'),
      Issue.GetValue<string>('ruleID'),
      Issue.GetValue<string>('fieldPath'),
      Issue.GetValue<string>('message')]));
  end;
end;

Lass dieselbe Schleife auch über schema.issues laufen, denn Fehler bei Wohlgeformtheit und Wurzelelement landen dort, und die Geschäftsregel-Stufe sieht nie ein Dokument, das sie nicht hätte parsen können

Warum kann dieselbe Rechnung in CII bestehen und in UBL scheitern?

Die offiziellen Artefakte wenden in beiden Syntaxen nicht dieselbe numerische Toleranz an, und PDFlibPas übernimmt jede Regel aus dem XSLT ihrer eigenen Syntax, statt sie zu vereinheitlichen. BR-CO-17 ist der klarste Fall. Er prüft, ob der Steuerbetrag der USt-Kategorie der Steuergrundlage mal Satz entspricht, gerundet auf zwei Dezimalstellen, und beide Syntaxen erlauben eine Abweichung von einer Währungseinheit. Das CII-Artefakt schreibt das als <= und >=; das UBL-Artefakt nutzt abs mit striktem <

Nimm eine Steuergrundlage von 100,00 bei 19 Prozent, erwartete Steuer also 19,00, und eine Rechnung, die 20,00 angibt. In CII ergibt 20,00 minus 1 genau 19,00, das erfüllt <=, und BR-CO-17 besteht. In UBL ist derselbe Vergleich strikt, 19,00 ist nicht kleiner als 19,00, und BR-CO-17 scheitert. Niemand schickt so eine Rechnung absichtlich, aber Generatoren mit einem Rundungsbug sitzen exakt auf solchen Grenzen, und eine Testsuite, die nur eine Syntax abdeckt, merkt es nicht

BR-CO-17-Toleranzvergleich in der PDFlibPas-Rechnungsvalidierung für Delphi: dieselbe Rechnung mit einer Rundungsabweichung von einer Einheit besteht in CII und scheitert in UBL
eine Rundungseinheit sitzt exakt auf der BR-CO-17-Grenze, dieselbe Rechnung besteht also in CII und scheitert in UBL

Die Unterschiede gehen über BR-CO-17 hinaus. BR-xx-08 verlangt in CII exakte Gleichheit nach Rundung auf zwei Dezimalstellen, während UBL ±1 erlaubt und nur zuschlägt, wenn die Kategorie tatsächlich Positionen, Nachlässe oder Zuschläge hat. BR-O-08 ist in beiden exakt. Das CII-Artefakt definiert BR-AF-09 und BR-AG-09 als true(), dort scheitern sie also nie, während UBL sie mit ±1 prüft. Und wenn eine CII-USt-Aufschlüsselung gar keinen Satz hat, gehen die Nicht-O-Kategorieregeln leer durch, weil XPath every $rate über eine leere Sequenz wahr ist. Der native Code muss sie auf dieselbe Weise überspringen, sonst meldet er Fehler auf offiziellen Beispieldateien

Eine zweite Falle betrifft die Regel-IDs. Das Regelkategorie-Präfix ist nicht der USt-Kategoriecode: K bildet auf BR-IC ab, L auf BR-AF und M auf BR-AG. Die ID aus dem Kategoriecode abzuleiten ergibt BR-K-09 und BR-K-10, IDs, die es in EN 16931 nicht gibt. Wenn deine Anwendung Regel-IDs auf Hilfetexte abbildet, nimm die ID aus dem Report und baue sie nie aus dem Steuerkategoriecode

Wie werden die XRechnung- und Peppol-Regeln eingeschaltet?

Der eingebaute Validator wählt die Profilregeln anhand der Spezifikationskennung (BT-24). Eine Kennung mit xrechnung startet die XRechnung-3.0.2-CIUS-Regeln auf CII oder UBL; die Erweiterungskennung urn:xeinkauf.de:kosit:extension:xrechnung_3.0 schaltet zusätzlich die BR-DEX-Regeln frei. Die Prüfungen zu Positionsbeträgen untergeordneter Rechnungen und USt (BR-DEX-02, -03 und -09 bis -14) laufen nur auf UBL; auf CII meldet BR-DEX-15, dass untergeordnete Rechnungspositionen nicht verfügbar sind. Eine Kennung mit peppol startet die Peppol-BIS-Billing-3.0.20-Regeln, die Peppol nur für UBL definiert. Ein CII-Dokument, das eine Peppol-Customization beansprucht, bekommt einen informativen BuiltIn.Coverage.Profile-Hinweis, dass diese Regeln nicht geprüft wurden, statt eines stillen Durchwinkens. Für andere CIUS-Profile nimm die offiziellen Artefakte

Ein UBL-Detail ist gut zu wissen: BR-06 und BR-07 akzeptieren nur PartyLegalEntity/RegistrationName als Verkäufer- oder Käufername. Auf PartyName/Name zurückzufallen wirkt kulant, versteckt aber echte BR-06-Fehler, weshalb die Prüfung strikt ist

Wann brauchst du weiterhin das offizielle Schematron?

Nimm die offiziellen Artefakte, das EN-16931-Schematron über Saxon oder den KoSIT-Validator für XRechnung, wann immer ein Ergebnis rechtlich oder vertraglich zählt: beim Abweisen einer Lieferantenrechnung, beim Zertifizieren des eigenen Generators oder beim Klären eines Streits. Der eingebaute Report setzt fullConformance nie auf true. Dieses Flag verlangt ein verifiziertes Regelpaket, beide bestandenen Stufen, beide Stufen von einer Engine ausgeführt, die die eigenen Regeln des Pakets ausführt, und nichts Übriggebliebenes in notCovered

ValidateInvoiceBusinessRulesWithPackage fährt diesen Pfad. Ein Regelpaket ist ein Verzeichnis mit einem rulepackage.xml-Manifest, geschrieben von CreateInvoiceRulePackage, das Standard, Version, Schema und Schematron-Stylesheet benennt und die SHA-256 jeder Datei festhält. PDFlibPas prüft jede Prüfsumme beim Laden des Pakets und noch einmal unmittelbar bevor eine Engine die Datei liest, was das Fenster für einen Austausch nach der Verifizierung verkleinert. Pfade dürfen das Paketverzeichnis nicht verlassen, und der native XSD-Loader lehnt Importe ab, die das Manifest nicht listet. Bei konfiguriertem xmllint liest xmllint die importierten Schemata selbst, dann schützt nur die Prüfung zur Ladezeit

const
  // SHA-256 der rulepackage.xml, die du geprüft und freigegeben hast
  ApprovedManifestSHA256 = '<64 hex digits from your release review>';
var
  Report, Pkg: TJSONObject;
begin
  // PDF: ein TPDFlib mit bereits geladener Rechnung, wie im ersten Beispiel
  Report := TJSONObject.ParseJSONValue(
    PDF.ValidateInvoiceBusinessRulesWithPackage('',
      'C:\Rules\EN16931-CII-1.3.16', ApprovedManifestSHA256,
      '',                          // leer: native XSD-Stufe
      'C:\Java\bin\java.exe', 'C:\Saxon\saxon-he.jar',
      0)) as TJSONObject;          // 0: 30000 ms pro Tool
  try
    if not (Report.FindValue('package') is TJSONObject) then
      raise Exception.Create('No rule package in the report');
    Pkg := Report.GetValue<TJSONObject>('package');
    if not (Pkg.GetValue<Boolean>('pinned') and Pkg.GetValue<Boolean>('verified')) then
      raise Exception.Create('Rule package failed its integrity check');
    if Report.GetValue<Boolean>('fullConformance') then
      Writeln('Conforms to the complete rule package');
  finally
    Report.Free;
  end;
end;

Die festgepinnte Prüfsumme ist kein optionaler Schmuck. Datei-Prüfsummen belegen, dass die Dateien zum Manifest passen, aber nichts innerhalb eines Pakets belegt, dass das Manifest selbst echt ist: Ein Angreifer, der die Regeln umschreiben kann, kann das Manifest gleich mit umschreiben. Nur eine vom Aufrufer festgelegte Prüfsumme, im Code oder in signierter Konfiguration, schließt diese Lücke, und der Report hält pinned und verified getrennt, damit du erkennst, welche Garantie du tatsächlich hast

Integritätsfluss für ein PDFlibPas-Rechnungsregelpaket in Delphi: SHA-256-Prüfsummen des Manifests, erneut geprüft beim Laden und vor jedem Lesen, festgepinnt vom Aufrufer
Datei-Prüfsummen schützen den Paketinhalt, und nur eine vom Aufrufer gepinnte Prüfsumme schützt das Manifest selbst

Übergibst du einen leeren JavaExecutable, validiert die Schema-Stufe weiterhin nativ gegen die Paket-XSD, inklusive Facetten und Identity Constraints seit v3.672.0, aber die Geschäftsregeln fallen auf die eingebaute Teilmenge zurück, und fullConformance bleibt false. Schemakonstrukte, die der native Loader nicht unterstützt, etwa Substitution Groups, lassen das Laden mit XSD.Unsupported scheitern, statt sie zu überspringen

Wo veraPDF und Mustang dazugehören

Keine dieser Funktionen betrachtet den PDF-Container. PDF/A-3-Konformität nach ISO 19005-3, das Factur-X-XMP-Extension-Schema und die Associated-File-Beziehung sind eigene Fragen, und veraPDF und Mustang bleiben dafür die Referenzwerkzeuge, wie in E-Rechnungen mit veraPDF und Mustang validieren beschrieben. Die Erzeugerseite, vom Anhängen des XML bis zur Profil-Deklaration, behandeln Factur-X- und ZUGFeRD-Hybridrechnungen in Delphi bauen und PDF/A-3-Extension-Schemas für Factur-X-Metadaten

Eine sinnvolle Aufteilung lässt die eingebaute Prüfung in-process auf jeder eingehenden Rechnung laufen, wo sie Millisekunden kostet: Eine UBL-Rechnung von rund 1 MB mit Base64-Anhängen validiert in 15 ms, mit der UBL-2.1-XSD in 79 ms. Der paketierte Saxon-Lauf kommt in die CI und in den Ablehnungspfad, und veraPDF übernimmt den Container. Was auch immer das eingebaute Ergebnis sagt, es beschreibt die abgedeckte Teilmenge, nicht den Standard, und genau daran soll dich das coverage-Feld erinnern

Die Rechnungsvalidierungsfunktionen, der Regelkatalog und die Regelpaket-Werkzeuge kommen mit der Delphi PDF Library für Delphi und C++Builder, zusammen mit den oben verwendeten Factur-X-, PDF/A-3- und Embedded-File-APIs