Article technique

Validation EN 16931 native des factures en Delphi sans Java

PDFlibPas, la Delphi PDF Library de losLab, contrôle nativement en Pascal les règles métier EN 16931, XRechnung 3.0.2 et Peppol BIS Billing 3.0. TPDFlib.ValidateInvoiceBusinessRulesBuiltIn lit le XML embarqué dans un PDF Factur-X ou ZUGFeRD et renvoie un rapport JSON avec les IDs de règles officiels, sans runtime Java, sans Saxon ni chaîne d'outils Schematron. Le rapport se déclare lui-même comme un sous-ensemble, et cette étiquette est son champ le plus important

Pipeline de validation de factures EN 16931 intégrée dans la PDFlibPas PDF library pour Delphi : détection Factur-X, étapes schema et schematron, et rapport JSON de sous-ensemble couvert
le validateur intégré exécute les étapes schema et schematron dans le processus et dit exactement quelles règles le sous-ensemble couvre et lesquelles il ne couvre pas

La pression derrière tout ça est surtout allemande. Depuis janvier 2025, chaque entreprise domestique en Allemagne doit pouvoir recevoir des e-factures B2B structurées, et l'obligation d'émission suit par étapes. Du coup, le côté réception d'une application Delphi de comptabilité ou d'ERP doit répondre à une question qu'elle ne s'était jamais posée : le XML dans ce PDF est-il réellement une facture EN 16931 valide ? La réponse officielle vient d'artefacts Schematron compilés en XSLT 2.0, ou du validateur KoSIT pour XRechnung, et les deux tournent sur la JVM. Livrer et patcher une JRE à côté de chaque installation de bureau juste pour rejeter une facture fautive, c'est un coût que beaucoup d'équipes se passeraient volontiers de porter

Que contrôle réellement le validateur intégré ?

Le validateur natif projette le XML de la facture sur les termes métier de l'EN 16931 et exécute un sous-ensemble documenté des règles des artefacts de validation EN 16931 1.3.16, en rapportant chaque constat sous son ID de règle officiel. Les deux syntaxes sont supportées : UN/CEFACT CII D16B, celle qu'embarquent Factur-X et ZUGFeRD, et OASIS UBL 2.1 Invoice et CreditNote. Les zones couvertes sont :

  • Termes obligatoires : identifiant de spécification, numéro de facture, date d'émission, code de type, devise, noms du vendeur et de l'acheteur, adresses, totaux du document, ventilation de TVA et lignes (BR-01 à BR-16, BR-21 à BR-28, BR-CO-04, BR-CO-18, BR-CO-26, BR-45 à BR-48)
  • Calculs : totaux de ligne, de remise, de frais, de taxe et à payer, plus la base et le montant de TVA de chaque catégorie (BR-CO-10 à BR-CO-17, BR-xx-08, BR-xx-09)
  • Règles de catégories de TVA pour S, Z, E, AE, K, G, O, L et M : taux de ligne, motifs d'exonération et identifiants de TVA exigés par chaque catégorie
  • Toutes les listes de codes BR-CL-01 à BR-CL-26, dont les plus de 2 100 codes d'unités UN/ECE Rec 20/21 derrière BR-CL-23, et les règles à deux décimales BR-DEC-01, -05, -09 à -20 et -23
  • Règles de profils : XRechnung 3.0.2 (BR-DE-1 à BR-DE-31, BR-DEX-01 à BR-DEX-14) sur CII et UBL, et règles core, common, code list et nationales de Peppol BIS Billing 3.0.20 sur UBL

Le périmètre est rapporté, pas sous-entendu. Sans fichier schema, l'étape schema confirme la bonne formation et l'élément racine. Chaque étape liste les règles hors sous-ensemble dans un champ notCovered, et GetBuiltInInvoiceRuleCatalog renvoie toute la carte en JSON, pour que votre UI puisse montrer à un utilisateur exactement ce que couvre un résultat vert

Une autre frontière est facile à manquer. Les documents Factur-X MINIMUM et BASIC WL ne sont pas des factures EN 16931, donc le validateur ne leur applique pas les règles EN 16931. Il ajoute à la place un constat informatif BuiltIn.Coverage.Profile qui le dit, ce qui est la réponse honnête et aussi un indice : selon les règles allemandes, ces deux profils ne comptent de toute façon pas comme e-factures

Valider le XML embarqué d'un PDF hybride

ValidateInvoiceBusinessRulesBuiltIn prend un argument, le nom du fichier embarqué, et la chaîne vide est le choix normal. PDFlibPas choisit alors la pièce jointe que la détection Factur-X identifie ; pour un PDF sans métadonnées Factur-X, il retombe sur l'unique pièce jointe .xml. Plusieurs pièces jointes .xml candidates, ou deux pièces jointes portant le nom demandé, produisent une erreur Invoice.AttachmentAmbiguous plutôt qu'une devinette, le décodage est plafonné à 64 MB, et une facture absente met LastErrorCode à 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]);
    // Nom vide : valider la pièce jointe Factur-X / ZUGFeRD détectée
    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')); // jamais True ici
    finally
      Report.Free;
    end;
  finally
    PDF.Free;
  end;
end;

Le haut niveau du rapport porte standard, version, profile, complete, valid, fullConformance, coverage, une étape schema et une étape schematron, plus un objet package qui est null en mode intégré. Chaque étape rapporte un statut, le moteur, le nombre de règles exécutées, sa couverture et un tableau issues. Chaque constat porte ruleID, fieldPath, severity, category, line et message

Lire les constats par catégorie plutôt que par message

Le champ category est ce qui rend le rapport exploitable dans une vraie boîte de réception, car il vous dit qui doit corriger le problème. Les valeurs sont syntax, schema, cardinality, calculation, vat, codelist, decimal, profile, coverage, processing et integrity. Un constat calculation pointe en général vers le code d'arrondi de l'expéditeur, un constat codelist vers les données de référence, et un constat profile vers un champ XRechnung comme la référence acheteur

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;  // notes de couverture comme 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;

Faites tourner la même boucle sur schema.issues aussi, car les échecs de bonne formation et d'élément racine atterrissent là, et l'étape des règles métier ne voit jamais un document qu'elle n'a pas pu parser

Pourquoi la même facture peut-elle passer en CII et échouer en UBL ?

Les artefacts officiels n'appliquent pas la même tolérance numérique dans les deux syntaxes, et PDFlibPas copie chaque règle depuis le XSLT de sa propre syntaxe plutôt que de les harmoniser. BR-CO-17 est le cas le plus limpide. Il vérifie que le montant de TVA de la catégorie égale la base multipliée par le taux, arrondi à deux décimales, et les deux syntaxes tolèrent un écart d'une unité monétaire. L'artefact CII écrit cela avec <= et >= ; l'artefact UBL utilise abs avec un < strict

Prenons une base de 100.00 à 19 pour cent, donc une taxe attendue de 19.00, et une facture qui déclare 20.00. En CII, 20.00 moins 1 égale 19.00, ce qui satisfait <=, et BR-CO-17 passe. En UBL, la même comparaison est stricte, 19.00 n'est pas inférieur à 19.00, et BR-CO-17 échoue. Personne n'envoie cette facture exprès, mais des générateurs avec un bug d'arrondi se tiennent exactement sur de telles frontières, et une suite de tests qui ne couvre qu'une syntaxe ne le remarquera pas

Comparaison de tolérance BR-CO-17 dans la validation de factures PDFlibPas pour Delphi : la même facture avec un écart d'arrondi d'une unité passe en CII et échoue en UBL
une unité d'arrondi se tient exactement sur la frontière de BR-CO-17, donc la même facture passe en CII et échoue en UBL

Les différences vont plus loin que BR-CO-17. BR-xx-08 exige une égalité exacte après arrondi à deux décimales en CII, tandis qu'UBL tolère ±1 et ne se déclenche que si la catégorie a réellement des lignes, des remises ou des frais. BR-O-08 est exact dans les deux. L'artefact CII définit BR-AF-09 et BR-AG-09 comme true(), donc ils n'y échouent jamais, alors qu'UBL les contrôle avec ±1. Et quand une ventilation de TVA CII n'a aucun taux, les règles de catégories non O passent vide, car un every $rate XPath sur une séquence vide est vrai. Le code natif doit les sauter de la même façon, sinon il rapporte des erreurs sur les fichiers d'exemple officiels

Un second piège concerne les IDs de règles. Le préfixe de catégorie d'une règle n'est pas le code de catégorie de TVA : K correspond à BR-IC, L à BR-AF et M à BR-AG. Dériver l'ID du code de catégorie produit BR-K-09 et BR-K-10, des IDs qui n'existent pas dans l'EN 16931. Si votre application mappe les IDs de règles vers des textes d'aide, prenez l'ID du rapport et ne le construisez jamais à partir du code de catégorie de taxe

Comment les règles XRechnung et Peppol sont-elles activées ?

Le validateur intégré sélectionne les règles de profil à partir de l'identifiant de spécification (BT-24). Un identifiant contenant xrechnung exécute les règles CIUS XRechnung 3.0.2 sur CII ou UBL ; l'identifiant d'extension urn:xeinkauf.de:kosit:extension:xrechnung_3.0 active en plus les règles BR-DEX. Les contrôles de montants de sous-lignes et de TVA (BR-DEX-02, -03 et -09 à -14) ne tournent que sur UBL ; sur CII, BR-DEX-15 signale que les sous-lignes ne sont pas disponibles. Un identifiant contenant peppol exécute les règles Peppol BIS Billing 3.0.20, que Peppol ne définit que pour UBL. Un document CII revendiquant une customisation Peppol reçoit une note informative BuiltIn.Coverage.Profile disant que ces règles n'ont pas été contrôlées, au lieu d'un passe silencieux. Pour les autres profils CIUS, utilisez les artefacts officiels

Un détail UBL mérite d'être connu : BR-06 et BR-07 n'acceptent que PartyLegalEntity/RegistrationName comme nom du vendeur ou de l'acheteur. Retomber sur PartyName/Name a l'air indulgent mais masque de vrais échecs BR-06, voilà pourquoi le contrôle est strict

Quand avez-vous encore besoin du Schematron officiel ?

Utilisez les artefacts officiels, le Schematron EN 16931 via Saxon ou le validateur KoSIT pour XRechnung, dès qu'un résultat porte un poids légal ou contractuel : rejeter une facture fournisseur, certifier votre propre générateur, ou trancher un litige. Le rapport intégré ne met jamais fullConformance à true. Ce drapeau exige un paquet de règles vérifié, les deux étapes passées, les deux étapes exécutées par un moteur qui exécute les règles propres au paquet, et rien qui traîne dans notCovered

ValidateInvoiceBusinessRulesWithPackage emprunte ce chemin. Un paquet de règles est un répertoire avec un manifeste rulepackage.xml, écrit par CreateInvoiceRulePackage, qui nomme le standard, la version, le schema et la feuille de styles Schematron, et enregistre le SHA-256 de chaque fichier. PDFlibPas vérifie chaque condensat au chargement du paquet et à nouveau juste avant qu'un moteur lise le fichier, ce qui réduit la fenêtre pour un échange après vérification. Les chemins ne peuvent pas sortir du répertoire du paquet, et le chargeur XSD natif refuse les imports que le manifeste ne liste pas. Avec xmllint configuré, les schémas importés sont lus par xmllint lui-même, donc seul le contrôle au chargement les protège

const
  // SHA-256 du rulepackage.xml que vous avez relu et approuvé
  ApprovedManifestSHA256 = '<64 hex digits from your release review>';
var
  Report, Pkg: TJSONObject;
begin
  // PDF : un TPDFlib avec la facture déjà chargée, comme dans le premier exemple
  Report := TJSONObject.ParseJSONValue(
    PDF.ValidateInvoiceBusinessRulesWithPackage('',
      'C:\Rules\EN16931-CII-1.3.16', ApprovedManifestSHA256,
      '',                          // vide : étape XSD native
      'C:\Java\bin\java.exe', 'C:\Saxon\saxon-he.jar',
      0)) as TJSONObject;          // 0 : 30000 ms par outil
  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;

Le condensat épinglé n'est pas une décoration optionnelle. Les condensats de fichiers prouvent que les fichiers correspondent au manifeste, mais rien dans un paquet ne prouve que le manifeste lui-même est authentique : un attaquant capable de réécrire les règles peut réécrire le manifeste avec elles. Seul un condensat fixé par l'appelant, dans le code ou dans une configuration signée, ferme ce trou, et le rapport garde pinned et verified séparés pour que vous sachiez quelle garantie vous avez réellement

Flux d'intégrité d'un paquet de règles de factures PDFlibPas en Delphi : condensats SHA-256 du manifeste revérifiés au chargement et avant chaque lecture, épinglés par l'appelant
les condensats de fichiers protègent le contenu du paquet, et seul un condensat épinglé par l'appelant protège le manifeste lui-même

Si vous passez un JavaExecutable vide, l'étape schema valide toujours nativement contre le XSD du paquet, facets et contraintes d'identité incluses depuis la v3.672.0, mais les règles métier retombent sur le sous-ensemble intégré et fullConformance reste false. Les constructions de schéma que le chargeur natif ne supporte pas, comme les substitution groups, font échouer le chargement avec XSD.Unsupported au lieu d'être ignorées

Où se situent veraPDF et Mustang

Aucune de ces fonctions ne regarde le conteneur PDF. La conformité PDF/A-3 sous ISO 19005-3, le schéma d'extension XMP Factur-X et la relation associated-file sont des questions à part, et veraPDF et Mustang restent les outils de référence pour elles, comme décrit dans la validation d'e-factures avec veraPDF et Mustang. Le côté production, de l'attachement du XML à la déclaration du profil, est couvert dans la construction de factures hybrides Factur-X et ZUGFeRD en Delphi et les schémas d'extension PDF/A-3 pour les métadonnées Factur-X

Un découpage sensé fait tourner le contrôle intégré dans le processus sur chaque facture entrante, où il coûte des millisecondes : une facture UBL d'environ 1 MB avec pièces jointes base64 se valide en 15 ms, ou 79 ms avec le XSD UBL 2.1. Le run Saxon packagé va en CI et dans le chemin de rejet, et veraPDF couvre le conteneur. Quel que soit le résultat intégré, il décrit le sous-ensemble couvert, pas le standard, et c'est exactement ce que le champ coverage est là pour vous rappeler

Les fonctions de validation de factures, le catalogue de règles et les outils de paquets de règles se livrent avec la Delphi PDF Library pour Delphi et C++Builder, avec les API Factur-X, PDF/A-3 et fichiers embarqués utilisées plus haut