Technical Article

Factures hybrides Factur-X et ZUGFeRD dans Delphi

Une facture électronique conforme n'est pas un PDF avec un fichier XML agrafé sur le côté. Il s'agit d'un document PDF/A-3 unique qui transporte la facture deux fois : une fois en tant que page lisible par un humain, et une fois en tant que XML Cross Industry Invoice lisible par une machine stocké à l'intérieur du fichier en tant que fichier associé. Les deux représentations décrivent la même facture. Cette double nature est tout l'intérêt des familles de formats que les mandats européens exigent désormais, Factur-X en France et en Allemagne, ZUGFeRD sur les marchés germanophones, et XRechnung pour la facturation du secteur public allemand. Cet article explique comment PDFlibPas assemble une telle facture hybride dans Delphi, où les normes laissent de la place pour se tromper, et pourquoi un profil dans le catalogue a besoin d'un constructeur XML complètement séparé

Ce qu'est réellement une facture hybride

La page visible et le XML intégré servent à des lecteurs différents. Un commis qui approuve un paiement regarde la page rendue. Un système de comptabilité fournisseurs ingère le XML, lit les totaux et la répartition des taxes sous forme de champs structurés, et enregistre l'écriture sans qu'aucun humain ne saisisse quoi que ce soit. Le contenu sémantique de ce XML est régi par EN 16931, la norme européenne qui définit le modèle de données de la facture : quels champs existent, ce qu'ils signifient et lesquels sont obligatoires. EN 16931 est un modèle sémantique, pas un format de fichier. Factur-X, ZUGFeRD 2.x et XRechnung réalisent tous ce modèle en tant que document UN/CEFACT Cross Industry Invoice, la syntaxe qui transporte les champs EN 16931 sur le réseau

Pour que le document soit à la fois archivable et auto-descriptif, le conteneur est PDF/A-3, défini par la norme ISO 19005-3. Le PDF/A-3 est le niveau de conformité qui permet des fichiers intégrés arbitraires, ce qui est exactement ce qu'un XML de facture doit être. Le PDF/A-2 interdit l'intégration de fichiers qui ne sont pas eux-mêmes PDF/A, de sorte qu'une facture Factur-X ne peut pas être PDF/A-2. Le choix du PDF/A-3 n'est donc pas une préférence, c'est une exigence qui découle directement de la volonté d'intégrer des données non PDF dans un document d'archive

Pourquoi la relation est Alternative

L'intégration des octets est la partie facile. La norme ISO 32000 §7.11.4 définit le flux de fichiers intégrés, l'objet qui contient le XML brut et ses paramètres. La partie qui fait du fichier un fichier associé valide est le §14.13, qui ajoute le concept de fichier associé et la clé /AFRelationship. Cette clé indique comment les données intégrées sont liées au contenu auquel elles sont attachées, et la valeur imposée par Factur-X est Alternative

Le choix est important car les autres valeurs affirmeraient quelque chose de faux à propos du document. Source signifierait que le XML est le matériel à partir duquel le contenu visible a été généré, un maître dont dérive la page. Supplement signifierait que le XML ajoute des informations au-delà de ce que montre la page, un supplément non contenu dans le rendu. Ce n'est ni l'un ni l'autre de ce qu'est une facture Factur-X. Le XML et la page sont deux expressions équivalentes d'une même facture, véhiculant le même contenu légal sous deux formes. Alternative est la valeur qui dit exactement cela : une représentation alternative équivalente du contenu visible. Un validateur qui lit toute autre relation sur un fichier Factur-X le rejettera, et à juste titre, car la relation est une affirmation lisible par une machine sur ce à quoi sert la pièce jointe

Le catalogue de profils

L'exemple E-Invoice fourni avec PDFlibPas pilote le même chemin de génération à travers six profils, définis comme un tableau d'enregistrements (records) dans InvoiceModel.pas. Chaque profil contient les valeurs dont l'auteur a besoin : un nom d'affichage, le nom du fichier intégré, un niveau de conformité, la /AFRelationship, une version, un code pays facultatif et l'URN GuidelineID que le XML annonce dans son contexte de document

Les six sont Factur-X EN16931, Factur-X BASIC, Factur-X EXTENDED pour la France, XRechnung 3.0, ZUGFeRD 1.0 COMFORT et ZUGFeRD 2.0 BASIC. Le GuidelineID est le champ qui indique précisément à un récepteur quel profil attendre, et les valeurs sont spécifiques. Factur-X EN16931 annonce urn:cen.eu:en16931:2017. XRechnung 3.0 annonce urn:cen.eu:en16931:2017#compliant#urn:xeinkauf.de:kosit:xrechnung_3.0. ZUGFeRD 2.0 BASIC annonce urn:cen.eu:en16931:2017#compliant#urn:zugferd.de:2p0:basic. Le nom du fichier intégré fait également partie du contrat. Les profils Factur-X intègrent factur-x.xml, XRechnung intègre xrechnung.xml, et les profils ZUGFeRD intègrent ZUGFeRD-invoice.xml ou zugferd-invoice.xml. Un récepteur scanne les noms de pièces jointes pour trouver la facture, de sorte que le nom du fichier n'est pas cosmétique

Un détail dans le catalogue mérite d'être lu attentivement. La plupart des profils utilisent la relation Alternative, mais l'entrée XRechnung 3.0 dans l'exemple utilise Source. Les deux formats répondent à des validateurs et des conventions différents, et l'exemple définit la relation de chaque profil à partir du catalogue plutôt que de coder en dur une seule valeur, c'est pourquoi le champ par profil existe plutôt qu'une constante

Le piège ZUGFeRD 1.0

Il est tentant de supposer que chaque profil est la facture Cross Industry Invoice EN 16931 avec des variations mineures dans le nombre de champs facultatifs que vous remplissez. Cela vaut pour cinq des six. Cela ne vaut pas pour ZUGFeRD 1.0 COMFORT, et la raison est structurelle plutôt que cosmétique

Les profils modernes émettent une facture UN/CEFACT Cross Industry Invoice avec la version de l'espace de noms :100, dont l'élément racine est rsm:CrossIndustryInvoice. ZUGFeRD 1.0 est antérieur à ce schéma. Il s'agit du CrossIndustryDocument de 2014 avec la version de l'espace de noms :1p0, et son élément racine est rsm:CrossIndustryDocument. Les URN de l'espace de noms diffèrent, l'élément racine diffère et l'arbre d'éléments diffère tout au long : le schéma :1p0 regroupe les données sous ApplicableSupplyChainTradeAgreement, ApplicableSupplyChainTradeDelivery et ApplicableSupplyChainTradeSettlement, où :100 utilise ApplicableHeaderTradeAgreement, ApplicableHeaderTradeDelivery et ApplicableHeaderTradeSettlement. Le nommage est suffisamment similaire pour induire en erreur et suffisamment différent pour casser

Le mot COMFORT dans le nom du profil décrit la richesse des données, un profil de niveau automatisation avec des éléments de ligne complets, la répartition des taxes et les conditions de paiement, et non le schéma qui le transporte. Vous ne pouvez donc pas prendre un document :100 et le réétiqueter pour ZUGFeRD 1.0. L'exemple gère cela avec un drapeau (flag) sur chaque enregistrement de profil et deux fonctions de construction distinctes, en sélectionnant la bonne avant qu'aucun XML ne soit généré

function BuildInvoiceXMLText(const AProfile: TeInvoiceProfile;
  const Data: TInvoiceData): string;
begin
  // XMLFamily = 1 means the legacy ZUGFeRD 1.0 :1p0 schema; every
  // other profile is the modern UN/CEFACT :100 Cross Industry Invoice.
  if AProfile.XMLFamily = 1 then
    Result := BuildZUGFeRD1Text(AProfile, Data)
  else
    Result := BuildCII100Text(AProfile, Data);
end;

La scission n'est pas une subtilité d'implémentation. Le fait de fournir un arbre :100 à un récepteur ZUGFeRD 1.0 produit un document qui échoue à la validation du schéma au niveau de l'élément racine, de sorte que les deux familles doivent être construites par un code qui sait laquelle il écrit

Sélection du niveau PDF/A-3

Le PDF/A-3 a trois niveaux de conformité, et PDFlibPas les sélectionne via SetPDFAMode. Le mode 5 est le PDF/A-3b, le niveau qui garantit une reproduction visuelle fiable. Le mode 6 est le PDF/A-3a, qui ajoute la structure balisée et les exigences d'accessibilité du niveau a. Le mode 7 est le PDF/A-3u, qui exige que tout le texte soit mappé sur Unicode. L'activation du mode intègre également l'intention de sortie (output intent) sRGB intégrée de la bibliothèque, la caractérisation des couleurs que le PDF/A exige pour que la couleur rendue soit définie plutôt que dépendante de l'appareil

La plupart des flux de factures s'exécutent en 3b, ce qui est suffisant pour une page visible fidèle plus le XML intégré. Si vous avez besoin d'un profil ICC explicite plutôt que celui intégré, LoadOutputIntentProfile l'échange une fois le mode défini. L'exemple charge le profil sRGB du référentiel de cette façon et se rabat sur l'intention intégrée lorsque le fichier n'est pas accessible, de sorte que l'intention de sortie est toujours présente

PDF := TPDFlib.Create;
try
  // Mode 5 = PDF/A-3b, 6 = PDF/A-3a, 7 = PDF/A-3u.
  if PDF.SetPDFAMode(5) <> 1 then
    raise Exception.Create('PDF/A-3 mode could not be enabled');

  // Optional: swap the built-in sRGB intent for an explicit ICC profile.
  if PDF.LoadOutputIntentProfile(ICCFile, 'DeviceRGB') <> 1 then
    { fall back to the built-in sRGB intent that SetPDFAMode embedded };
finally
  // ... continue building the document
end;

Construire la facture hybride

Une fois le conteneur configuré, le reste se fait en trois étapes dans l'ordre : définir le mode PDF/A-3, dessiner la page lisible par un humain, puis joindre le XML en tant que fichier associé. La page visible est un contenu ordinaire. La seule contrainte à retenir est que le PDF/A interdit les polices Standard 14 non intégrées, de sorte que la page doit intégrer une police réelle plutôt que de faire référence à une police intégrée

L'attachement est un seul appel. AddFacturXAssociatedFileFromString prend les octets XML UTF-8 bruts ainsi que les métadonnées de profil, écrit le flux de fichier intégré, l'enregistre dans le tableau /AF du catalogue requis par le PDF/A-3, applique le /AFRelationship et génère les métadonnées de facture électronique XMP qui identifient le document comme Factur-X, ZUGFeRD ou XRechnung. Il vérifie également que l'ID de directive du XML correspond au niveau de conformité que vous avez demandé, de sorte qu'une non-concordance entre le XML que vous avez construit et le profil que vous avez nommé est interceptée plutôt que d'être expédiée silencieusement

// 1. PDF/A-3 mode and output intent are already set.
// 2. Draw the visible page (embeds a real TrueType font).
DrawInvoicePage(PDF, AProfile, Data);

// 3. Build the profile-correct XML and attach it as an
//    associated file with /AFRelationship = Alternative.
InvoiceXML := BuildInvoiceXML(AProfile, Data);   // AnsiString of UTF-8 bytes
FileID := PDF.AddFacturXAssociatedFileFromString(
  InvoiceXML,
  AProfile.ConformanceLevel,   // e.g. 'EN16931'
  AProfile.FileName,           // 'factur-x.xml'
  AProfile.Description,
  AProfile.Relationship,       // 'Alternative'
  AProfile.Version,            // '1.0'
  AProfile.CountryCode);       // '' or 'DE' or 'FR'
if FileID <= 0 then
  raise Exception.Create('Invoice XML could not be attached');

PDF.SaveToFile(TargetFile);

Une subtilité dans le chemin des données est l'encodage. Le XML intégré déclare encoding="UTF-8", et la méthode prend ses octets sous forme de AnsiString, de sorte qu'un nom de vendeur ou d'acheteur non ASCII doit atteindre l'appel sous forme d'octets UTF-8 bruts. Une simple conversion via la page de codes ANSI du système corromprait ces caractères et produirait silencieusement une facture dont le XML ne correspondrait plus à sa propre déclaration. L'exemple encode en UTF-8 explicitement avant de remettre les octets, ce qui est la façon sûre d'alimenter toute API PDF orientée octet à partir d'une string Unicode

Pour l'ajout d'un XML qui n'est pas un profil de facture électronique reconnu, AddPDFA3AssociatedFileFromString est la contrepartie générique. Il prend un nom de fichier, un type MIME, une description, une relation et des octets, et écrit un simple fichier associé PDF/A-3 sans aucune métadonnée spécifique à la facture ni vérification de directive. Utilisez-le pour des données supplémentaires ; utilisez la méthode Factur-X pour les factures, de sorte que les métadonnées de profil et la correspondance de directive soient écrites pour vous

Une fois le document produit, les questions suivantes sont de savoir s'il réussit la validation PDF/A et d'accessibilité, et s'il peut être signé sans rompre la conformité. Ces points sont couverts dans la description détaillée du preflight PDF/A et PDF/UA dans Delphi et dans l'établi de conformité et de signature. Tout cela est fourni dans le cadre de PDFlibPas Delphi PDF Library, aux côtés des API PDF/A, de balisage et de propriétés de document sur lesquelles s'appuie le chemin de la facture électronique