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
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
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
Ü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