Blog de développement

Le serveur fait sa première Factur-X

Dernière Modification le :
2026-05-23

Dès quelques factures par mois, il devient nécessaire d'automatiser Chorus Pro. Il est inexact de croire que Chorus Pro entreprend bien docilement d'Océriser votre facture pour améliorer votre productivité. Quels que soient vos efforts pour optimiser polices ou mots clés, vous serez systématiquement conduit à vérifier et corriger une 'lecture' qui est souvent erronnée. C'est une méchante arnaque! Un trou noir de la productivité des PME... Lire →

La seule parade c'est Factur-X. Il n'y a pas d'alternative, mais au moins, ça marche! Donc, il faut passer à Factur-X. Lire →

En fait, même, si on n'a pas la possibilité de passer à PDF/A et Factur-x, il reste possible d'optimiser un PDF 'classique' pour une lecture optimale par Chorus voire même d'automatiser le dépôt. Lire →

Les Outils PDF et Factur-X

J. van der Knijff, maintient une liste d'outils de manipulation PDF Lire →. L. Abbati en a une aussi. Lire →

Le site officiel de Factur-X fournit une liste d'outils. Lire →. En fait il y en a d'autres, plus récents, certains très pertinents! Explorer →

En Desktop, les utilisateurs de LibreOffice installeront le module proposé par Alexis de Lattre (Akretion) Lire et Télécharger →. Les utilisateurs d'Excel considéreront l'outil Excel d'Admarel Télécharger →

En mode Serveur, on pourra procéder par étapes: création du PDF, conversion en PDF/A, puis ajout du XML de Factur-X, avec des outils gratuits comme: FPDF, Mustang, en ligne de commande

Pour la validation on notera: avepdf.com, verapdf.org et son outil en ligne (difficile à dénicher, au demeurant...), l'outil de validation FNFE-MPE et enfin... Chorus Pro, tortionnaire devenu cobaye.

Mise à niveau PDF/A avec ASPPDF

J'utilise ASPPDF, donc je le décris en premier... Il a été mis à jour pour satisfaire aux nouvelles normes: il faut donc avoir la toute dernière version. Vérifier →. Sans ASPPDF, c'est ici: Lire →

Certaines règles à respecter... En particulier, toutes les polices doivent être incorporées, donc que les polices standard, non incorporées, devront l'être aussi...

					    Set oPDF = Server.CreateObject("Persits.Pdf")	' // Server. c'est pour ASP... En script VBS : CreateObject("Persits.Pdf")	
Set oDoc = oPDF.CreateDocument
Set oPage = oDoc.Pages.Add ("595,3", "841,9")	' //pour être en A4 vertical
	' // la meilleure police en vue d'OCR c'est Tahoma, mais, ici avec Factur-X on ne passe plus par l'OCR, ni l'extracteur de texte!
Set oTimesFont = oDoc.Fonts.LoadFromFile("c:\windows\fonts\times.ttf")
Set oTimesBoldFont = oDoc.Fonts.LoadFromFile("c:\windows\fonts\timesbd.ttf")
Set oTimesItalicFont = oDoc.Fonts.LoadFromFile("c:\windows\fonts\timesi.ttf")
				
De même il nous faut insérer un profile de couleurs. On en trouve un chez ADOBE. Le lien est en bas de la page... Lire et Télécharger →
					    sProfilePath = "C:\Inetpub\MonSite\AdobeRGB1998.icc"
oDoc.AddOutputIntent "AdobeRGB", "Custom", strProfilePath, 3
				

Le métadata proposé en exemple par ASPPDF ne passe pas la validation. On extrait celui d'Akretion avec un outil en ligne : metadata2go. Il est aussi disponible dans le ZIP de ressources.

					    		' on utilise un modèle (template) Metadata extrait d'une factur-X valide plutôt que metadata de persists (ASPPDF)
sMetaDataFilePath = "C:\Inetpub\MonSite\metadata.txt"
Set objFile = objFSO.OpenTextFile(sMetaDataFilePath, 1, true)	'ForReading = 1, true
sMetadata = objFile.ReadAll
objFile.Close
Set objFile = nothing
sDateFacture_ISO = ToIsoDate (date(now()))
sDateFacture_pour_XML = Replace (sDateFacture_ISO, "-", "") 	' // mise au format ISO de la date d'aujoud'hui
NumeroFacture = "23001"

sMetadata = Replace(sMetadata, "<rdf:li xml:lang=""x-default"">LE FOURNISSEUR: Invoice F20220023</rdf:li>", " <rdf:li xml:lang=""x-default"">MaSociété: Facture " &  numero_facture & "</rdf:li>")
sMetadata = Replace(sMetadata, "<rdf:li>LE FOURNISSEUR</rdf:li>" ,  "<rdf:li>MaSociété</rdf:li>")
sMetadata = Replace(sMetadata, "<rdf:li xml:lang=""x-default"">Invoice F20220023 dated 2022-01-31 issued by LE FOURNISSEUR</rdf:li>" , "<rdf:li xml:lang=""x-default"">Invoice " &  numero_facture & " dated " & sDateFacture_ISO & " issued by MaSociété</rdf:li>")
sMetadata = Replace(sMetadata, "<pdf:Producer>PyPDF4</pdf:Producer>" , "<pdf:Producer>Persits Software AspPDF - www.persits.com</pdf:Producer>")
sMetadata = Replace(sMetadata, "<xmp:CreatorTool>factur-x python lib v2.3 by Alexis de Lattre</xmp:CreatorTool>" , "<xmp:CreatorTool>factur-x for MaSociété by MySelf</xmp:CreatorTool>")
sMetadata = Replace(sMetadata, "<xmp:CreateDate>2022-04-12T20:05:03+00:00</xmp:CreateDate>" , "<xmp:CreateDate>" & sDateFacture_ISO & "T20:05:03+00:00</xmp:CreateDate>")
sMetadata = Replace(sMetadata, "<xmp:ModifyDate>2022-04-12T20:05:03+00:00</xmp:ModifyDate>" , "<xmp:ModifyDate>" & sDateFacture_ISO & "T20:05:03+00:00</xmp:ModifyDate>")

oDoc.MetaData = sMetadata	' // on insère le metadata (XMP)
	
Public Function ToIsoDate(datetime)
	ToIsoDate = CStr(Year(datetime)) & "-" & StrN2(Month(datetime)) & "-" & StrN2(Day(datetime))
End Function  
Private Function StrN2(n)
	If Len(CStr(n)) < 2 Then StrN2 = "0" & n Else StrN2 = n
End Function
		
				

Extraction et validation des metadata XMP

Les metadata XMP sont un point sensible du PDF hybride Factur-X. Le XML embarqué peut être correct, le PDF peut sembler lisible, mais le document être refusé parce que les informations XMP ne déclarent pas correctement le profil Factur-X, le nom du fichier attaché, le type de document ou la conformité PDF/A-3.

Dans mon cas, la solution pratique consiste à partir d'un modèle XMP extrait d'une facture Factur-X valide, puis à remplacer les valeurs variables: numéro de facture, date, producteur PDF, outil de création et niveau de conformité.

Pour inspecter rapidement les metadata effectivement présentes dans un PDF, ExifTool est très utile. Sous Windows, une commande typique est:

C:\Tools\exiftool-13.58_64\exiftool.exe -a -G1 -s Facture.pdf

L'installation est un peu dérourante: on télécharge et change le nom de exiftool_k.exe à exiftool.exe. Télécharger →. On peut préparer une commande, par exemple ce .bat


C:\Tools\exiftool-13.58_64\exiftool.exe -a -G1 -s "C:\Temp\Facture.pdf" > "C:\Temp\Facture_XMP.txt"

Cette commande permet de vérifier notamment le producteur PDF, les champs XMP, le profil Factur-X, le numéro de facture, la date, les montants et certains champs du XML exposés dans les metadata.

On peut aussi utiliser des validateurs XMP en ligne, par exemple le validateur XMP de PDFlib: Tester →.

Validation, extraction et debug de Factur-X

La vraie difficulté de Factur-X n'est pas uniquement de produire un PDF contenant un fichier XML. Il faut vérifier simultanément plusieurs couches: XML, XMP, PDF/A-3, pièce jointe embarquée et cohérence du profil annoncé. Un PDF qui semble correct visuellement peut être refusé par un validateur ou par Chorus Pro.

Le workflow de debug que j'ai retenu est le suivant:

  • valider le XML Factur-X seul;
  • valider les metadata XMP;
  • vérifier la conformité PDF/A-3;
  • extraire le XML réellement embarqué dans le PDF final;
  • comparer le XML source et le XML extrait;
  • tester le PDF hybride complet avec le validateur FNFE-MPE;
  • tester enfin le dépôt Chorus Pro, qui ajoute une couche de validation métier.

Plusieurs outils gratuits ou accessibles en ligne sont utiles à chaque étape: FNFE-MPE, veraPDF, metadata2go, ExifTool et certains validateurs XML/PEPPOL comme ecosio.

Valider le XML seul avant de valider le PDF

Une bonne pratique consiste à valider d'abord le fichier factur-x.xml seul, avant de l'insérer dans le PDF. Cela permet d'isoler les erreurs XML et Schematron sans être perturbé par les problèmes propres au PDF/A ou aux metadata.

Le validateur FNFE accepte directement les fichiers XML. Il indique clairement si le XML est valide contre le XSD et contre les règles Schematron du profil choisi. Pour le profil Minimum, les erreurs sont généralement plus simples à corriger que pour EN16931. Ce billet reste volontairement limité au schéma Minimum; le profil EN16931 fera l'objet d'un billet séparé.

Une erreur typique est l'ajout d'un élément non autorisé dans le contexte du profil Minimum, par exemple une adresse acheteur trop détaillée dans un profil qui ne l'attend pas:

Element 'ram:PostalTradeAddress' is marked as not used in the given context.

Dans ce cas, il ne faut pas chercher à enrichir le XML Minimum avec toutes les informations disponibles sur la facture papier. Il faut respecter strictement les éléments attendus par le profil.

Extraire le XML embarqué

Il ne suffit pas de valider le XML source que le script a produit. Il faut aussi vérifier le XML réellement embarqué dans le PDF final. En effet, un script peut modifier le PDF après l'insertion, réouvrir le document, remplacer les metadata ou attacher par erreur un ancien fichier XML.

Firefox permet parfois d'ouvrir ou d'extraire facilement le fichier XML attaché dans le PDF. D'autres outils PDF affichent aussi les pièces jointes embarquées. L'objectif est toujours le même: comparer le factur-x.xml que le serveur a généré avec celui qui se trouve effectivement dans le PDF envoyé à Chorus.

Cette étape a permis d'identifier plusieurs confusions classiques entre dossiers de sortie: XML Minimum, XML de test, XML EN16931, archive XML et fichier temporaire factur-x.xml.

Construire le XML Minimum ex nihilo

La première version fonctionnait par remplacement de valeurs dans un XML modèle. C'est simple et efficace pour démarrer, mais cela devient vite fragile: on remplace des chaînes exactes, on dépend de la structure du modèle et on risque d'embarquer des éléments inutiles ou interdits.

La seconde étape consiste donc à construire le XML Minimum directement, à partir des valeurs de la facture. Cette construction ex nihilo permet de mieux contrôler:

  • le numéro de facture;
  • la date au format 102, c'est-à-dire YYYYMMDD;
  • le vendeur et son SIRET;
  • le client et, si nécessaire, son identifiant Chorus;
  • la référence de commande;
  • les montants HT, TVA, TTC et net à payer;
  • le profil Factur-X Minimum.

Cette partie peut être décrite plus en détail dans un billet séparé, car elle introduit une vraie petite bibliothèque de génération XML en ASP Classic.

Function BuildFacturXMinimum(numeroFacture, dateFacture102, sellerName, sellerSiret, sellerVat, sellerCountry, buyerName, buyerLegalId, buyerCountry, buyerReference, orderReference, totalHT, totalTVA, totalTTC)
    ' Construction du XML Factur-X Minimum
    ' Le détail de cette fonction sera présenté dans un billet technique séparé.
End Function

On insère le XML de Factur-X

On doit ajouter cette annotation. ASPPDF suggère AFRelationship=0. En fait, if faut AFRelationship=1 pour passer la validation...

					    	' // AFRelationship entry must be 1
Set oAnnot = oPage.Annots.Add("", "x=1,y=700;width=10;height=10; Type=FileAttachment; names=true; AF=1;", "Paperclip", xmloutput)

				

Chorus propose une série d'exemples. Télécharger →. J'ai extrait mon modèle XML minimal de la facture modèle d'Akretion.

					    
' // Attention: le séparateur décimal est ici le point. Se référer à la norme pour voir ce qu'il en est exactement.
TaxBasisTotalAmount = Replace (Replace (Basket_HT, " ", ""), ",", ".")
TaxTotalAmount = Replace (Replace (Basket_TVA, " ", ""), ",", ".")'Basket_TVA
GrandTotalAmount = Replace (Replace (Basket_TTC, " ", ""), ",", ".")
DuePayableAmount = GrandTotalAmount
	' // à personnaliser
sMaSociete = "Ma Boutique"
sMonSIRET = "79237773100023"
sMonNumeroTVA = "FR86792377731"
sNomClient = "MonClient"
sChorus_service = "Voirie"
sSIRET_Client = "21690266800013"
sDateFacture_pour_XML = Replace (ToIsoDate (date(now())), "-", "")	' // le format pour Factur-X est un format ISO simplifié
Set objFile = objFSO.OpenTextFile(xml_Template, 1, true) 	' //  1 = ForReading
sContent = objFile.ReadAll
objFile.Close

sContent = replace(sContent, "<rsm:ExchangedDocument><ram:ID>F19042</ram:ID>" , "<rsm:ExchangedDocument><ram:ID>" & numero_facture &"</ram:ID>")
sContent = replace(sContent, "<udt:DateTimeString format=""102"">20191221</udt:DateTimeString>", "<udt:DateTimeString format=""102"">" & sDateFacture_pour_XML & "</udt:DateTimeString>")
sContent = replace(sContent, "<ram:BuyerReference>SRVIT</ram:BuyerReference>", "<ram:BuyerReference>" & sChorus_service & "</ram:BuyerReference>")
sContent = replace(sContent, "<ram:SellerTradeParty><ram:Name>Akretion France</ram:Name>", "<ram:SellerTradeParty><ram:Name>" & sMaSociete & "</ram:Name>")
'le siret du Granuloshop = 91510597700010
sContent = replace(sContent, "<ram:SpecifiedLegalOrganization><ram:ID schemeID=""0002"">79237773100023</ram:ID>"  , "<ram:SpecifiedLegalOrganization><ram:ID schemeID=""0002"">" & sMaSociete & "</ram:ID>")
'le Numero TVA du Granuloshop : FR81915105977
sContent = replace(sContent, "<ram:ID schemeID=""VA"">FR86792377731</ram:ID>" , "<ram:ID schemeID=""VA"">" & sMonNumeroTVA & "</ram:ID>")
sContent = replace(sContent, "<ram:Name>Mairie de Villeurbanne</ram:Name>", "<ram:Name>" & sNomClient & "</ram:Name>")
' le SIRET client
sContent = replace(sContent, "<ram:ID schemeID=""0002"">21690266800013</ram:ID>" , "<ram:ID schemeID=""0002"">" & sSIRET_Client & "</ram:ID>")
sContent = replace(sContent, "<ram:IssuerAssignedID>PO19012</ram:IssuerAssignedID></ram:BuyerOrderReferencedDocument>"  , "<ram:IssuerAssignedID>" & sEngagement & "</ram:IssuerAssignedID></ram:BuyerOrderReferencedDocument>")
sContent = replace(sContent, "<ram:TaxBasisTotalAmount>347.00</ram:TaxBasisTotalAmount>" ,  "<ram:TaxBasisTotalAmount>" & TaxBasisTotalAmount & "</ram:TaxBasisTotalAmount>")
sContent = replace(sContent, "<ram:TaxTotalAmount currencyID=""EUR"">69.40</ram:TaxTotalAmount>", "<ram:TaxTotalAmount currencyID=""EUR"">" & TaxTotalAmount & "</ram:TaxTotalAmount>")
sContent = replace(sContent, "<ram:GrandTotalAmount>416.40</ram:GrandTotalAmount>"  , "<ram:GrandTotalAmount>" & GrandTotalAmount & "</ram:GrandTotalAmount>")
sContent = replace(sContent, "<ram:DuePayableAmount>416.40</ram:DuePayableAmount>" , "<ram:DuePayableAmount>" & DuePayableAmount & "</ram:DuePayableAmount>")
sContent = "<?xml version='1.0' encoding='UTF-8'?>" & sContent
Set objFile = objFSO.OpenTextFile(xmloutput, ForWriting, True)   'True va creer un nouveau fichier
objFile.Write  sContent
objFile.Close
					    
				    

Dans le cas particulier où le TVA n'est pas facturée, par exemple l'échange intracommunautaire, il faut penser à mettre la TVA à 0.00, car sinon cela ne passe pas le validateur.

La vraie vie: déposer sur Chorus en un clin d'oeil

Ce qui est remarquable, c'est que maintenant Chorus PRO va identifier la facture comme étant au format hybride Factur-x et vous dispenser de la page de correction à laquelle vous aviez systématiquement droit: le gain de temps et de productivité est énorme!

Nous ajoutons cette ligne au PDF: "Cette facture est au format Factur-x: norme professionnelle agrée par l'Etat et Chorus Pro."