Technisch artikel

Native EN 16931-factuurvalidatie in Delphi, zonder Java

PDFlibPas, de losLab Delphi PDF Library, controleert de zakelijke regels van EN 16931, XRechnung 3.0.2 en Peppol BIS Billing 3.0 native in Pascal. TPDFlib.ValidateInvoiceBusinessRulesBuiltIn leest de XML die in een Factur-X- of ZUGFeRD-PDF zit en geeft een JSON-rapport terug met de officiële rule-IDs, zonder Java-runtime, Saxon of een Schematron-toolchain. Het rapport noemt zichzelf een subset, en juist dat label is het belangrijkste veld erin

Pipeline van de ingebouwde EN 16931-factuurvalidatie in de PDF-bibliotheek PDFlibPas voor Delphi: Factur-X-detectie, schema- en schematron-stadia en een JSON-rapport met gedekte subset
de ingebouwde validator draait de schema- en schematron-stadia in-process en meldt precies welke regels de subset dekt en welke niet

De druk achter dit alles komt vooral uit Duitsland. Sinds januari 2025 moet elk binnenlands bedrijf in Duitsland gestructureerde B2B-e-facturen kunnen ontvangen, en de verzendplicht volgt in fasen. Plotseling moet de ontvangende kant van een Delphi-boekhoud- of ERP-applicatie een vraag beantwoorden die ze zich nooit eerder stelde: is de XML in deze PDF wel een geldige EN 16931-factuur? Het officiële antwoord komt uit Schematron-artefacten die naar XSLT 2.0 zijn gecompileerd, of uit de KoSIT-validator voor XRechnung, en beide draaien op de JVM. Bij elke desktopinstallatie een JRE meeleveren en bijhouden om alleen maar een foute factuur af te wijzen, is een kost waar veel teams geen zin in hebben

Wat controleert de ingebouwde validator eigenlijk?

De native validator vertaalt de factuur-XML naar de zakelijke begrippen van EN 16931 en draait een gedocumenteerde subset van de regels uit de EN 16931-validatieartefacten 1.3.16, waarbij elke bevinding onder zijn officiële rule-ID wordt gemeld. Beide syntaxen worden ondersteund: UN/CEFACT CII D16B, wat Factur-X en ZUGFeRD insluiten, en OASIS UBL 2.1 Invoice en CreditNote. De gedekte gebieden zijn:

  • Verplichte termen: specificatie-identificatie, factuurnummer, factuurdatum, typecode, valuta, namen van verkoper en koper, adressen, documenttotalen, btw-specificatie en regels (BR-01 tot BR-16, BR-21 tot BR-28, BR-CO-04, BR-CO-18, BR-CO-26, BR-45 tot BR-48)
  • Berekeningen: totalen voor regel, korting, toeslag, belasting en te betalen bedrag, plus het belastbare en het btw-bedrag van elke btw-categorie (BR-CO-10 tot BR-CO-17, BR-xx-08, BR-xx-09)
  • Btw-categorieregels voor S, Z, E, AE, K, G, O, L en M: tarieven per regel, vrijstellingsredenen en de btw-identificaties die elke categorie vereist
  • Alle codelijsten BR-CL-01 tot BR-CL-26, inclusief de ruim 2100 UN/ECE Rec 20/21-eenheidscodes achter BR-CL-23, en de regels voor twee decimalen BR-DEC-01, -05, -09 tot -20 en -23
  • Profielregels: XRechnung 3.0.2 (BR-DE-1 tot BR-DE-31, BR-DEX-01 tot BR-DEX-14) op CII en UBL, en de core-, common-, codelijst- en nationale regels van Peppol BIS Billing 3.0.20 op UBL

De dekking wordt gerapporteerd, niet geïmpliceerd. Zonder een schemabestand bevestigt de schema-stage alleen de welgevormdheid en het rootelement. Elke stage zet de regels buiten de subset in een notCovered-veld, en GetBuiltInInvoiceRuleCatalog geeft de hele afbeelding als JSON terug, zodat je UI een gebruiker precies kan laten zien wat een groen resultaat dekt

Nog een grens die makkelijk wordt gemist. Factur-X MINIMUM- en BASIC WL-documenten zijn geen EN 16931-facturen, dus de validator past er geen EN 16931-regels op toe. In plaats daarvan voegt hij een informatieve BuiltIn.Coverage.Profile-bevinding toe die dat zegt, wat het eerlijke antwoord is en tegelijk een hint: volgens de Duitse regels tellen die twee profielen toch niet als e-factuur

De ingesloten XML van een hybride PDF valideren

ValidateInvoiceBusinessRulesBuiltIn krijgt één argument mee, de naam van het ingesloten bestand, en een lege string is de normale keuze. PDFlibPas pakt dan de bijlage die de Factur-X-detectie aanwijst; bij een PDF zonder Factur-X-metagegevens valt hij terug op de enige .xml-bijlage. Meerdere kandidaat-.xml-bijlagen, of twee bijlagen met de gevraagde naam, leveren een Invoice.AttachmentAmbiguous-fout op in plaats van een gok, het decoderen is begrensd op 64 MB en een ontbrekende factuur zet LastErrorCode op 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]);
    // Lege naam: valideer de gedetecteerde Factur-X / ZUGFeRD-bijlage
    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 nooit True
    finally
      Report.Free;
    end;
  finally
    PDF.Free;
  end;
end;

Het bovenste niveau van het rapport draagt standard, version, profile, complete, valid, fullConformance, coverage, een schema- en een schematron-stage en een package-object dat in built-in-modus null is. Elke stage meldt een status, de engine, het aantal uitgevoerde regels, zijn dekking en een issues-array. Elke bevinding heeft ruleID, fieldPath, severity, category, line en message

Bevindingen lezen op categorie in plaats van op meldingstekst

Het category-veld is wat het rapport bruikbaar maakt in een echte inbox, want het vertelt je wie het probleem moet oplossen. De waarden zijn syntax, schema, cardinality, calculation, vat, codelist, decimal, profile, coverage, processing en integrity. Een calculation-bevinding wijst meestal op de afrondingscode van de verzender, een codelist-bevinding op stamgegevens en een profile-bevinding op een XRechnung-veld zoals de koperreferentie

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;  // coveragenotities zoals 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;

Draai dezelfde lus ook over schema.issues, want welgevormdheids- en rootelementfouten belanden daar en de zakelijke-regelstage ziet nooit een document dat hij niet kon parsen

Waarom kan dezelfde factuur in CII slagen en in UBL falen?

De officiële artefacten passen in beide syntaxen niet dezelfde numerieke tolerantie toe, en PDFlibPas kopieert elke regel uit de XSLT van zijn eigen syntaxis in plaats van ze te harmoniseren. BR-CO-17 is het duidelijkste geval. Die controleert dat het btw-bedrag van de categorie gelijk is aan belastbaar bedrag keer tarief, afgerond op twee decimalen, en beide syntaxen staan een verschil van één valuta-eenheid toe. Het CII-artefact schrijft dat als <= en >=; het UBL-artefact gebruikt abs met een strenge <

Neem een belastbaar bedrag van 100.00 tegen 19 procent, dus een verwacht btw-bedrag van 19.00, en een factuur die 20.00 opgeeft. In CII is 20.00 min 1 gelijk aan 19.00, wat aan <= voldoet, en BR-CO-17 gaat erdoor. In UBL is dezelfde vergelijking streng, 19.00 is niet kleiner dan 19.00, en BR-CO-17 faalt. Niemand verstuurt zo'n factuur met opzet, maar generators met een afrondingsbug zitten precies op zulke grenzen, en een testsuite die maar één syntaxis dekt ziet het niet

BR-CO-17-tolerantievergelijking in de factuurvalidatie van PDFlibPas voor Delphi: dezelfde factuur met een afrondingsverschil van één eenheid slaagt in CII en faalt in UBL
één afrondingseenheid zit precies op de BR-CO-17-grens, dus dezelfde factuur slaagt in CII en faalt in UBL

De verschillen gaan verder dan BR-CO-17. BR-xx-08 eist in CII exacte gelijkheid na afronding op twee decimalen, terwijl UBL ±1 toestaat en alleen afgaat wanneer de categorie daadwerkelijk regels, kortingen of toeslagen heeft. BR-O-08 is in beide exact. Het CII-artefact definieert BR-AF-09 en BR-AG-09 als true(), dus die falen daar nooit, terwijl UBL ze met ±1 controleert. En als een CII-btw-specificatie helemaal geen tarief heeft, gaan de regels voor niet-O-categorieën leeg door, omdat XPath every $rate over een lege reeks waar is. De native code moet ze op dezelfde manier overslaan, anders meldt hij fouten op officiële voorbeeldbestanden

Een tweede valkuil betreft de rule-IDs. Het categorieprefix van een rule is niet de btw-categoriecode: K hoort bij BR-IC, L bij BR-AF en M bij BR-AG. De ID afleiden uit de categoriecode levert BR-K-09 en BR-K-10 op, IDs die niet bestaan in EN 16931. Als je applicatie rule-IDs aan helpteksten koppelt, neem de ID dan uit het rapport en bouw hem nooit vanuit de btw-categoriecode

Hoe worden de XRechnung- en Peppol-regels aangezet?

De ingebouwde validator kiest de profielregels op basis van de specificatie-identificatie (BT-24). Een identifier met xrechnung draait de CIUS-regels van XRechnung 3.0.2 op CII of UBL; de extensie-identifier urn:xeinkauf.de:kosit:extension:xrechnung_3.0 zet daarbovenop de BR-DEX-regels aan. De controles op het subtotaal van factuurregels en btw (BR-DEX-02, -03 en -09 tot -14) draaien alleen op UBL; op CII meldt BR-DEX-15 dat factuursubregels niet beschikbaar zijn. Een identifier met peppol draait de regels van Peppol BIS Billing 3.0.20, die Peppol alleen voor UBL definieert. Een CII-document dat een Peppol-customization claimt, krijgt een informatieve BuiltIn.Coverage.Profile-notitie dat die regels niet zijn gecontroleerd, in plaats van een stille doorgang. Voor andere CIUS-profielen: gebruik de officiële artefacten

Nog een UBL-detail om te weten: BR-06 en BR-07 accepteren alleen PartyLegalEntity/RegistrationName als naam van verkoper of koper. Terugvallen op PartyName/Name ziet er toegeeflijk uit maar verhult echte BR-06-falen, en daarom is de controle streng

Wanneer heb je de officiële Schematron nog nodig?

Gebruik de officiële artefacten — de EN 16931 Schematron via Saxon of de KoSIT-validator voor XRechnung — zodra een resultaat juridisch of contractueel gewicht draagt: een leveranciersfactuur afwijzen, je eigen generator certificeren of een geschil beslechten. Het ingebouwde rapport zet fullConformance nooit op true. Die vlag vereist een geverifieerd rulepackage, beide stadia geslaagd, beide stadia gedraaid door een engine die de eigen regels van het package uitvoert, en niets over in notCovered

ValidateInvoiceBusinessRulesWithPackage loopt dat pad. Een rulepackage is een map met een rulepackage.xml-manifest, geschreven door CreateInvoiceRulePackage, dat het normtype, de versie, het schema en de Schematron-stylesheet benoemt en de SHA-256 van elk bestand vastlegt. PDFlibPas controleert elke digest bij het laden van het package en nogmaals vlak voordat een engine het bestand leest, wat het venster voor een verwisseling na verificatie verkleint. Paden mogen de package-map niet uit en de native XSD-loader weigert imports die het manifest niet noemt. Met xmllint geconfigureerd leest xmllint zelf de geïmporteerde schema's, dus alleen de controle bij het laden beschermt ze

const
  // SHA-256 van de rulepackage.xml die je hebt beoordeeld en goedgekeurd
  ApprovedManifestSHA256 = '<64 hex digits from your release review>';
var
  Report, Pkg: TJSONObject;
begin
  // PDF: een TPDFlib waarin de factuur al geladen is, zoals in het eerste voorbeeld
  Report := TJSONObject.ParseJSONValue(
    PDF.ValidateInvoiceBusinessRulesWithPackage('',
      'C:\Rules\EN16931-CII-1.3.16', ApprovedManifestSHA256,
      '',                          // leeg: native XSD-stage
      'C:\Java\bin\java.exe', 'C:\Saxon\saxon-he.jar',
      0)) as TJSONObject;          // 0: 30000 ms per 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;

De vastgezette digest is geen optionele versiering. Bestandsdigests bewijzen dat de bestanden matchen met het manifest, maar niets in een package bewijst dat het manifest zelf echt is: een aanvaller die de regels kan herschrijven, kan het manifest mee herschrijven. Alleen een digest die de aanroeper vastlegt, in code of in ondertekende configuratie, dicht dat gat, en het rapport houdt pinned en verified uit elkaar zodat je kunt zien welke garantie je werkelijk hebt

Integriteitsstroom voor een invoice-rulepackage van PDFlibPas in Delphi: SHA-256-digests van het manifest opnieuw gecontroleerd bij het laden en vóór elke read, vastgezet door de aanroeper
bestandsdigests beschermen de package-inhoud en alleen een digest die de aanroeper vastzet beschermt het manifest zelf

Geef je een lege JavaExecutable mee, dan valideert de schema-stage nog steeds native tegen de package-XSD, inclusief facets en identity constraints sinds v3.672.0, maar de zakelijke regels vallen terug op de ingebouwde subset en fullConformance blijft false. Schema-constructies die de native loader niet ondersteunt, zoals substitution groups, laten het laden mislukken met XSD.Unsupported in plaats van ze over te slaan

Waar veraPDF en Mustang passen

Geen van deze functies kijkt naar de PDF-container. PDF/A-3-conformiteit onder ISO 19005-3, het Factur-X XMP-extensieschema en de associated-file-relatie zijn aparte vragen, en veraPDF en Mustang blijven daarvoor de referentietools, zoals beschreven in e-facturen valideren met veraPDF en Mustang. De producerende kant, van het toevoegen van de XML tot het opgeven van het profiel, is behandeld in Factur-X- en ZUGFeRD-hybride facturen bouwen in Delphi en PDF/A-3-extensieschema's voor Factur-X-metagegevens

Een verstandige verdeling draait de ingebouwde controle in-process op elke binnenkomende factuur, waar het milliseconden kost: een UBL-factuur van circa 1 MB met base64-bijlagen valideert in 15 ms, of 79 ms met de UBL 2.1 XSD. De Saxon-run met package gaat naar CI en naar het afwijzingspad, en veraPDF dekt de container. Wat het ingebouwde resultaat ook zegt, het beschrijft de gedekte subset, niet de norm, en precies daarvoor is het coverage-veld er om je aan te herinneren

De functies voor factuurvalidatie, de rulecatalogus en de rulepackage-tools worden meegeleverd met de Delphi PDF Library voor Delphi en C++Builder, samen met de hierboven gebruikte API's voor Factur-X, PDF/A-3 en ingesloten bestanden