Une facture Factur-X ou ZUGFeRD correspond à deux documents sous un seul nom de fichier. Le document extérieur est un conteneur PDF/A-3 qu'un lecteur d'archives doit accepter pour les dix prochaines années. Le document intérieur est une facture XML que le système comptable d'un acheteur doit analyser par rapport à la norme EN 16931. L'erreur qui expédie des factures cassées en production est de croire qu'obtenir le premier correctement donne le second gratuitement. Ce n'est pas le cas. Un fichier peut être un PDF/A-3 sans faille et transporter du XML qu'aucune autorité fiscale n'acceptera, et il peut transporter un XML EN 16931 parfait à l'intérieur d'un conteneur qui échoue à la validation archivistique. Les deux couches sont validées par deux outils différents qui ne savent rien l'un de l'autre, et un véritable pipeline doit satisfaire les deux
Deux validateurs, deux questions différentes
veraPDF est l'implémentation de référence pour PDF/A. Pointez-le sur une facture et il répond à une question : s'agit-il d'un fichier PDF/A-3 conforme. Il vérifie les choses dont se soucie ISO 19005-3. Chaque police est-elle intégrée. Y a-t-il un OutputIntent. Les métadonnées XMP déclarent-elles la bonne partie et le bon niveau de conformité. Pour une facture électronique, il vérifie également la plomberie du fichier associé que requiert PDF/A-3, car le XML l'accompagne en tant que fichier intégré avec une /AFRelationship et une entrée dans le tableau /AF du catalogue de documents. veraPDF ne dit rien sur le fait que le total de la facture soit correct, car cela ne relève pas de sa compétence
Mustang est le validateur open-source de Mustangproject. Il pose la question orthogonale : le XML intégré est-il une facture valide. Il exécute le XML contre le schéma du profil déclaré, puis applique les règles métier EN 16931 et les ensembles de règles spécifiques au pays superposés, dont le CIUS de XRechnung. Il vérifie qu'un identifiant de TVA du vendeur est présent lorsque les totaux l'exigent, que les montants des remises et des frais se réconcilient avec le total du document, que l'URN du profil dans le XML correspond à ce que le fichier prétend être. Mustang ne se soucie pas de savoir si le PDF environnant intègre ses polices, car c'est le travail de veraPDF
Aucun des deux outils n'est un sur-ensemble de l'autre. veraPDF valide un conteneur structurellement parfait autour d'un XML dénué de sens. Mustang valide un XML parfait enveloppé dans un conteneur avec un OutputIntent manquant. Chacun détecte exactement la classe de défaut à laquelle l'autre est aveugle, ce qui est la raison entière pour laquelle un harnais de validation sérieux exécute les deux et traite un fichier comme expédiable uniquement lorsque les deux sont d'accord
La matrice de validation
Pour prouver que la bibliothèque produit des fichiers qui survivent aux deux portes, le harnais construit une matrice. Six profils de factures couvrent la gamme qu'un pipeline européen rencontre dans la pratique : Factur-X EN 16931, Factur-X BASIC, la variante Factur-X EXTENDED France B2B, XRechnung 3.0, ZUGFeRD 1.0 COMFORT et ZUGFeRD 2.0 BASIC. Chaque profil est généré contre deux sous-niveaux de conformité PDF/A, 3b et 3u, car les exigences du niveau B et du niveau U divergent sur le mappage Unicode et un fichier qui passe l'un peut échouer à l'autre. Six profils fois deux niveaux donnent douze fichiers, chacun d'eux étant construit sans interface (headless) par le même chemin de code que l'exemple GUI fournit, de sorte que les artefacts testés ne sont pas ajustés à la main pour le test
Le générateur écrit les douze et un script soumet chacun d'eux aux deux validateurs. Lors de la première exécution complète, veraPDF a validé les douze. La plomberie du conteneur était correcte à tous les niveaux : fichiers associés enregistrés, conformité XMP déclarée, intentions de sortie (output intents) en place. Mustang en a validé huit. Quatre factures étaient des fichiers PDF/A-3 structurellement valides transportant du XML que le validateur de règles métier a rejeté, ce qui est précisément la division que l'approche à deux outils existe pour mettre en évidence. Si le harnais n'avait fait confiance qu'à veraPDF, ces quatre-là auraient semblé terminés
Les deux correctifs qui ont comblé l'écart
Les quatre échecs de Mustang provenaient de deux causes distinctes, et le correctif de chacune est un détail qui vaut la peine d'être connu avant que vous ne génériez ces profils vous-même
Le premier était le profil Factur-X EXTENDED France B2B. Le générateur d'origine passait une étiquette interne comme niveau de conformité et un URN interne comme directive, et Mustang a rejeté le fichier avec une erreur invalid-conformance-value suivie d'une erreur unsupported-profile-type. La raison est que le champ XMP fx:ConformanceLevel n'est pas un emplacement de texte libre pour votre propre nommage de profil. Factur-X définit exactement cinq valeurs standard pour lui : MINIMUM, BASIC WL, BASIC, EN 16931 et EXTENDED. Une facture B2B spécifique à la France est toujours un document de profil EXTENDED en ce qui concerne les métadonnées XMP. Le caractère français de la facture ne s'exprime pas en inventant une sixième valeur de conformité. Il s'exprime par le code pays, FR, et par l'identifiant de la directive dans le XML, qui doit porter le préfixe urn:cen.eu:en16931:2017#conformant# qui marque un CIUS conforme à EN 16931. Le fait de passer la valeur standard EXTENDED avec FR comme code pays et l'URN de directive correct a rendu le fichier conforme
Dans l'API de la bibliothèque, c'est un appel à AddFacturXAssociatedFileFromString avec la conformité, le pays et la directive alignés. L'argument du niveau de conformité porte le jeton standard, l'argument du code pays porte FR, et l'URN de la directive vit dans les octets XML que vous transmettez
var
FileID: Integer;
begin
PDF.SetPDFAMode(5); // PDF/A-3b
PDF.NewDocument;
// ... draw the human-readable invoice page ...
// ExtendedXML carries an EN 16931 guideline URN of the form
// urn:cen.eu:en16931:2017#conformant#urn:factur-x.eu:1p0:extended
FileID := PDF.AddFacturXAssociatedFileFromString(
ExtendedXML,
'EXTENDED', // standard fx:ConformanceLevel, not an internal label
'factur-x.xml',
'Factur-X EXTENDED invoice',
'Alternative', // /AFRelationship
'1.0',
'FR'); // France B2B marked by country code, not by conformance
if FileID = 0 then
raise Exception.Create('Factur-X attachment rejected');
PDF.SaveToFile('02_Factur-X-EXTENDED-FR_PDFA-3b.pdf');
end;
La deuxième cause était le profil ZUGFeRD 1.0 COMFORT, et n'avait rien à voir avec les métadonnées. ZUGFeRD 1.0 est validé par rapport au XSD :1p0, qui est plus strict en matière de cardinalité que ce que suggèrent les résumés en prose. Le XSD exige que la sommation de règlement d'en-tête, ram:SpecifiedTradeSettlementMonetarySummation, contienne ram:ChargeTotalAmount et ram:AllowanceTotalAmount chacun exactement une fois. Le XML généré omettait les deux, Mustang a donc signalé que les éléments devaient apparaître exactement une fois. Ceux-ci ne sont pas facultatifs lorsque le schéma indique que minOccurs est égal à un. L'émission des deux dans l'ordre de séquence XSD, immédiatement après ram:LineTotalAmount, avec une valeur de 0.00 lorsqu'il n'y a pas de frais ou de remises, a satisfait le schéma. Un zéro est un élément présent ; un élément absent est une violation du schéma. Avec ces deux correctifs en place, la matrice est passée à douze sur douze sur Mustang tout en restant à douze sur douze sur veraPDF
Les champs XRechnung qui font basculer d'invalide à valide
XRechnung mérite sa propre note car son CIUS allemand ajoute des règles métier qui sont absentes de l'ensemble de base EN 16931, et elles échouent d'une manière qui donne l'impression que rien ne cloche avec le document au premier coup d'œil. Deux d'entre elles concernent les adresses électroniques. BT-34 est l'adresse électronique du vendeur et BT-49 est l'adresse électronique de l'acheteur, les points finaux de routage qu'un portail du secteur public allemand utilise pour livrer et accuser réception de la facture. Le modèle EN 16931 de base les considère comme facultatifs. Ce n'est pas le cas de XRechnung. Omettez l'un ou l'autre et la facture est bien formée, conforme au schéma et rejetée
Le troisième est la règle BR-DE-6, qui exige que le numéro de téléphone de contact du vendeur soit présent. C'est le genre de champ qu'un développeur abandonne parce qu'il ressemble plus à une présentation qu'à des données, et son absence produit un échec de validation qui pointe vers le groupe de contact du vendeur plutôt que vers quoi que ce soit d'évident. Fournir BT-34, BT-49 et le numéro de téléphone du vendeur est ce qui fait passer un fichier XRechnung d'invalide à valide sous Mustang, et rien de tout cela ne change ce que veraPDF voit, car tous les trois vivent dans le XML
Câbler la sortie de la bibliothèque à un validateur
Le point architectural derrière le harnais se généralise à tout système d'entreprise. La bibliothèque PDF écrit un conteneur conforme et intègre le XML. Elle ne le fait pas, et ne devrait pas, tenter d'être l'autorité des règles métier EN 16931. ValidateFacturXInvoice dans la bibliothèque vérifie la cohérence du conteneur, que le tableau /AF du catalogue, l'arbre de noms des fichiers intégrés, le DocumentFileName XMP, le profil, la directive et le /AFRelationship s'accordent tous, mais il ne valide pas les codes fiscaux ni ne réconcilie les montants. La bonne division du travail consiste pour le système d'entreprise à extraire le XML et à le transmettre à un validateur de factures dédié, exactement comme le harnais le transmet à Mustang
Relire le fichier vous indique ce qui a été réellement écrit. DetectFacturXInvoice indique si une facture a été reconnue, et GetFacturXInvoiceInfo lit les champs de métadonnées par balise : la balise 1 est le nom du fichier intégré, la balise 2 le DocumentFileName XMP, la balise 5 le niveau de conformité, la balise 6 l'identifiant de la directive et la balise 7 le /AFRelationship. Confirmer que le niveau de conformité que vous relisez est le jeton standard et non une étiquette interne est le moyen le moins cher de détecter l'erreur EXTENDED avant qu'un fichier ne quitte votre compilation
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);
// Hand the raw XML to a dedicated EN 16931 / Mustang validator.
Result := PDF.ExtractFacturXXMLToString;
end;
end;
ExtractFacturXXMLToString renvoie les octets XML bruts sous la forme d'un AnsiString, prêt à être écrit dans un fichier ou diffusé dans un processus de validation. Dans le harnais de test, cette cible est Mustang, invoqué via son jar en ligne de commande, avec veraPDF exécuté dans la même passe sur le même fichier. Le câblage est réduit : un générateur de console, EInvoiceValidation.dpr, écrit les douze fichiers en utilisant le modèle de facture partagé de l'exemple, et un script, run-validation.ps1, pilote les deux validateurs sur le répertoire de sortie et imprime un tableau de réussite et d'échec. La même forme en deux étapes, générer avec la bibliothèque et vérifier avec des validateurs externes, est ce qu'une tâche d'intégration continue devrait exécuter à chaque modification de la génération de factures, car le seul moyen de savoir qu'un fichier satisfait aux deux couches est de demander aux deux outils
Si votre pipeline doit également certifier le conteneur avant la signature, l'aspect preflight de ce travail est couvert dans notre description détaillée du preflight PDF/A et PDF/UA dans Delphi, et le flux plus large certifier-puis-signer est décrit dans l'établi de conformité et de signature. Tous deux s'appuient sur le même chemin de génération qui est fourni avec Delphi PDF Library pour Delphi et C++Builder, aux côtés des API PDF/A, de fichiers associés et de métadonnées utilisées ici