Blog de développement

47 - Factur-X Minimum : construire le XML ex nihilo en ASP Classic (voire VBS/VBA)

Dernière Modification le :
2026-05-23

Dans un billet précédent Lire →, j'ai transformé une facture PDF produite par mon serveur en document hybride Factur-X: PDF/A-3, metadata XMP, XML embarqué, puis validation. Cette première étape était volontairement pragmatique: partir d'un XML modèle, remplacer quelques valeurs, puis insérer le fichier factur-x.xml dans le PDF.

Cela permet de démarrer vite et de comprendre le rôle de chaque couche: le PDF visible, le XML caché, les metadata XMP et les validateurs. Elle a une limite: dès que le nombre de cas réels (export, chorus, ...) augmente, le remplacement de texte dans un template XML devient fragile. Les normes évoluent... Enfin, la méthode de substitution est inadaptée à EN16931, graal en passe de devenir obligatoire.

Ic, je traite uniquement du profil Factur-X Minimum,et pas du profil EN16931, ni de PEPPOL, ni des lignes détaillées, ni du dictionnaire complet des règles métier. C'est volontaire. Le but ici est de construire proprement un XML Minimum, avec ASP Classic (voire VBS/VBA) et MSXML, pour disposer d'une base robuste avant de passer à aux profils plus riches, avec énumération.

Pourquoi abandonner le remplacement dans un template XML ?

La première version de mon script utilisait un fichier XML modèle extrait d'une facture Factur-X valide. Le serveur lisait le fichier, puis remplaçait des fragments par les valeurs de la facture: numéro, date, SIRET, TVA, montants et références Chorus.

C'est une bonne méthode d'apprentissage. Elle a un avantage majeur: on part d'un fichier connu comme valide. Mais en production, les inconvénients apparaissent vite:

  • le script dépend de chaînes exactes dans le template;
  • un espace, un retour ligne ou une variation du template peut casser le remplacement;
  • on risque de laisser dans le XML des valeurs de démonstration;
  • on embarque parfois des balises non pertinentes pour une facture donnée;
  • les balises facultatives vides provoquent des erreurs de validation;
  • le code devient difficile à relire et à auditer.

Exemple typique de remplacement fragile:

sContent = Replace(sContent, "<ram:BuyerReference>SRVIT</ram:BuyerReference>", _
                             "<ram:BuyerReference>" & sChorus_service & "</ram:BuyerReference>")

sContent = Replace(sContent, "<ram:TaxTotalAmount currencyID=""EUR"">69.40</ram:TaxTotalAmount>", _
                             "<ram:TaxTotalAmount currencyID=""EUR"">" & TaxTotalAmount & "</ram:TaxTotalAmount>")

Ce code marche tant que le modèle reste exactement identique. Mais il ne sait pas raisonner sur la structure XML. Il ne sait pas dire: « si la référence acheteur est vide, je n'ajoute pas la balise ». Il remplace, point final.

La logique ex nihilo

Construire le XML ex nihilo signifie que le serveur ne part plus d'un fichier XML modèle. Il crée lui-même l'arbre XML, noeud par noeud. En ASP Classic (voire VBS/VBA), cela se fait très bien avec MSXML2.DOMDocument.6.0.

Le changement est important. On ne manipule plus une chaîne de caractères. On manipule un document XML. Le code devient plus long au départ, mais beaucoup plus sûr:

Les namespaces sont déclarés une seule fois, les montants sont normalisés avant insertion et surtout les balises facultatives ne sont créées que lorsqu'elles ont réellement une valeur.

C'est aussi plus conforme à la logique serveur: une facture est déjà un objet de gestion, présent dans la base SQL et dans le panier ASP. Le XML Factur-X n'est qu'une autre représentation de cette facture.

Initialiser MSXML2.DOMDocument.6.0

Première brique: créer le document XML et fixer les namespaces utilisés par Factur-X. Le profil Minimum reste simple, mais il utilise quand même les préfixes rsm, ram et udt.

Set oXml = Server.CreateObject("MSXML2.DOMDocument.6.0")
oXml.async = False
oXml.validateOnParse = False
oXml.resolveExternals = False
oXml.preserveWhiteSpace = True

' Dans MSXML, les namespaces doivent etre declares au moment de creer les noeuds.
Const NS_RSM = "urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100"
Const NS_RAM = "urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100"
Const NS_UDT = "urn:un:unece:uncefact:data:standard:UnqualifiedDataType:100"

Le point important est que createElement seul ne suffit pas toujours lorsqu'on veut contrôler correctement les namespaces. Avec MSXML, la méthode createNode est plus explicite: on indique le type de noeud, le nom qualifié et l'URI du namespace.

Deux petites fonctions: AddNode et AddTextNode

Pour ne pas transformer le script en forêt de appendChild, j'utilise deux fonctions utilitaires. Elles ne font rien de magique: elles créent un noeud, l'ajoutent au parent, et renvoient le noeud créé.

Function AddNode(ByRef oDoc, ByRef oParent, ByVal nsUri, ByVal qName)
    Dim oNode
    Set oNode = oDoc.createNode(1, qName, nsUri)
    oParent.appendChild oNode
    Set AddNode = oNode
End Function

Function AddTextNode(ByRef oDoc, ByRef oParent, ByVal nsUri, ByVal qName, ByVal sText)
    Dim oNode
    Set oNode = AddNode(oDoc, oParent, nsUri, qName)
    oNode.Text = CStr(sText)
    Set AddTextNode = oNode
End Function

Sub AddTextNodeIfValue(ByRef oDoc, ByRef oParent, ByVal nsUri, ByVal qName, ByVal sText)
    If Len(Trim(CStr(Nz(sText)))) > 0 Then
        Call AddTextNode(oDoc, oParent, nsUri, qName, sText)
    End If
End Sub

Function AddAmountNode(ByRef oDoc, ByRef oParent, ByVal qName, ByVal amount, ByVal currencyId)
    Dim oNode
    Set oNode = AddTextNode(oDoc, oParent, NS_RAM, qName, XmlAmount(amount))
    If Len(currencyId) > 0 Then oNode.setAttribute "currencyID", currencyId
    Set AddAmountNode = oNode
End Function

Le vrai gain est dans AddTextNodeIfValue. Cette fonction évite de créer des balises vides. Cela paraît un détail, mais c'est une des erreurs les plus pénibles en validation Schematron: le XML est bien formé, mais il n'est pas acceptable pour le profil.

Normaliser dates et montants

Avant de construire le XML, il faut normaliser les données venant du serveur. En ASP Classic français, les montants circulent souvent avec une virgule décimale, parfois avec des espaces. Le XML attend des nombres propres avec un point décimal.

'------------------------------------------------------------
' Helpers XML Factur-X Minimum - ASP Classic / VBScript
'------------------------------------------------------------

Function Nz(ByVal v)
    If IsNull(v) Or IsEmpty(v) Then
        Nz = ""
    Else
        Nz = Trim(CStr(v))
    End If
End Function

Function XmlAmount(ByVal v)
    ' Factur-X attend un separateur decimal point.
    ' On normalise les montants ASP/FR: "1 234,56" -> "1234.56"
    Dim s
    s = Trim(CStr(v))
    s = Replace(s, Chr(160), "")
    s = Replace(s, " ", "")
    s = Replace(s, ",", ".")
    If InStr(1, s, ".", vbTextCompare) = 0 Then s = s & ".00"
    XmlAmount = s
End Function

Function Date102(ByVal d)
    ' udt:DateTimeString format="102" = YYYYMMDD
    Date102 = CStr(Year(d)) & Right("0" & Month(d), 2) & Right("0" & Day(d), 2)
End Function

Function HasValue(ByVal v)
    HasValue = (Len(Trim(CStr(Nz(v)))) > 0)
End Function

La date de facture est également un cas typique. Dans Factur-X, pour le champ udt:DateTimeString avec format="102", le format est YYYYMMDD. Une facture du 23 mai 2026 devient donc 20260523.

Créer la racine du XML Minimum

La fonction suivante construit l'ossature du XML Minimum. Elle ne prétend pas couvrir tous les cas. C'est volontairement un noyau minimal, lisible, testable, qui correspond à une facture simple: un vendeur, un acheteur, une devise, des montants globaux et des références Chorus si elles existent.

Function BuildFacturXMinimumXml( _
    ByVal numeroFacture, ByVal dateFacture, _
    ByVal sellerName, ByVal sellerSiret, ByVal sellerVat, _
    ByVal buyerName, ByVal buyerSiret, _
    ByVal buyerReference, ByVal orderReference, _
    ByVal totalHT, ByVal totalTVA, ByVal totalTTC)

    Dim oXml, root, docContext, guideline
    Dim exchangedDocument, issueDateTime
    Dim transaction, agreement, seller, buyer, settlement, monetary

    Set oXml = Server.CreateObject("MSXML2.DOMDocument.6.0")
    oXml.async = False
    oXml.validateOnParse = False
    oXml.resolveExternals = False
    oXml.preserveWhiteSpace = True

    Set root = oXml.createNode(1, "rsm:CrossIndustryInvoice", NS_RSM)
    root.setAttribute "xmlns:rsm", NS_RSM
    root.setAttribute "xmlns:ram", NS_RAM
    root.setAttribute "xmlns:udt", NS_UDT
    oXml.appendChild root

    Set docContext = AddNode(oXml, root, NS_RSM, "rsm:ExchangedDocumentContext")
    Set guideline = AddNode(oXml, docContext, NS_RAM, "ram:GuidelineSpecifiedDocumentContextParameter")
    Call AddTextNode(oXml, guideline, NS_RAM, "ram:ID", "urn:factur-x.eu:1p0:minimum")

    Set exchangedDocument = AddNode(oXml, root, NS_RSM, "rsm:ExchangedDocument")
    Call AddTextNode(oXml, exchangedDocument, NS_RAM, "ram:ID", numeroFacture)
    Call AddTextNode(oXml, exchangedDocument, NS_RAM, "ram:TypeCode", "380")

    Set issueDateTime = AddNode(oXml, exchangedDocument, NS_RAM, "ram:IssueDateTime")
    Dim dateNode
    Set dateNode = AddTextNode(oXml, issueDateTime, NS_UDT, "udt:DateTimeString", Date102(dateFacture))
    dateNode.setAttribute "format", "102"

    Set transaction = AddNode(oXml, root, NS_RSM, "rsm:SupplyChainTradeTransaction")
    Set agreement = AddNode(oXml, transaction, NS_RAM, "ram:ApplicableHeaderTradeAgreement")

    ' Attention: BuyerReference est facultatif dans mon flux.
    ' S'il est vide, on ne cree PAS la balise.
    Call AddTextNodeIfValue(oXml, agreement, NS_RAM, "ram:BuyerReference", buyerReference)

    Set seller = AddNode(oXml, agreement, NS_RAM, "ram:SellerTradeParty")
    Call AddTextNode(oXml, seller, NS_RAM, "ram:Name", sellerName)
    Call AddLegalOrg(oXml, seller, sellerSiret)
    Call AddVatId(oXml, seller, sellerVat)

    Set buyer = AddNode(oXml, agreement, NS_RAM, "ram:BuyerTradeParty")
    Call AddTextNode(oXml, buyer, NS_RAM, "ram:Name", buyerName)
    Call AddLegalOrgIfValue(oXml, buyer, buyerSiret)

    If HasValue(orderReference) Then
        Dim orderDoc
        Set orderDoc = AddNode(oXml, agreement, NS_RAM, "ram:BuyerOrderReferencedDocument")
        Call AddTextNode(oXml, orderDoc, NS_RAM, "ram:IssuerAssignedID", orderReference)
    End If

    Set settlement = AddNode(oXml, transaction, NS_RAM, "ram:ApplicableHeaderTradeSettlement")
    Call AddTextNode(oXml, settlement, NS_RAM, "ram:InvoiceCurrencyCode", "EUR")

    Set monetary = AddNode(oXml, settlement, NS_RAM, "ram:SpecifiedTradeSettlementHeaderMonetarySummation")
    Call AddAmountNode(oXml, monetary, "ram:TaxBasisTotalAmount", totalHT, "")
    Call AddAmountNode(oXml, monetary, "ram:TaxTotalAmount", totalTVA, "EUR")
    Call AddAmountNode(oXml, monetary, "ram:GrandTotalAmount", totalTTC, "")
    Call AddAmountNode(oXml, monetary, "ram:DuePayableAmount", totalTTC, "")

    BuildFacturXMinimumXml = "<?xml version=""1.0"" encoding=""UTF-8""?>" & vbCrLf & oXml.xml
End Function

Dans cette version, la structure est volontairement compacte. On ajoute le contexte documentaire, le numéro de facture, la date, le vendeur, l'acheteur, les références et la sommation monétaire. C'est le coeur du profil Minimum.

Ajouter vendeur, acheteur, SIRET et TVA

Le vendeur doit être identifié proprement. Pour une société française, le SIRET est placé avec schemeID="0002". Le numéro de TVA intracommunautaire est placé avec schemeID="VA".

Sub AddLegalOrg(ByRef oDoc, ByRef partyNode, ByVal siret)
    Dim org, idNode
    Set org = AddNode(oDoc, partyNode, NS_RAM, "ram:SpecifiedLegalOrganization")
    Set idNode = AddTextNode(oDoc, org, NS_RAM, "ram:ID", siret)
    idNode.setAttribute "schemeID", "0002"  ' 0002 = SIRET
End Sub

Sub AddLegalOrgIfValue(ByRef oDoc, ByRef partyNode, ByVal siret)
    If HasValue(siret) Then Call AddLegalOrg(oDoc, partyNode, siret)
End Sub

Sub AddVatId(ByRef oDoc, ByRef partyNode, ByVal vatNumber)
    If HasValue(vatNumber) Then
        Dim taxReg, idNode
        Set taxReg = AddNode(oDoc, partyNode, NS_RAM, "ram:SpecifiedTaxRegistration")
        Set idNode = AddTextNode(oDoc, taxReg, NS_RAM, "ram:ID", vatNumber)
        idNode.setAttribute "schemeID", "VA"
    End If
End Sub

La même logique s'applique à l'acheteur, mais avec une nuance pratique: dans mes flux Chorus Pro, l'acheteur public a souvent un SIRET et parfois un service destinataire ou une référence d'engagement. Ces données ne doivent pas être inventées. Si elles ne sont pas disponibles, le script doit soit omettre la balise facultative, soit refuser de produire une facture Chorus si la donnée est indispensable pour le destinataire.

Construire dynamiquement depuis le serveur ASP (voire VBS/VBA)

Le serveur dispose déjà des informations: dossier client, numéro de facture, panier, TVA, mode Chorus, référence d'engagement. L'appel à la fonction de génération doit rester simple et lisible.

Dim sXml, xmlOutput
xmlOutput = Server.MapPath("/Factures/XML/factur-x.xml")

sXml = BuildFacturXMinimumXml( _
    numero_facture, Date(), _
    "GRANULOSHOP SAS", "91510597700010", "FR81915105977", _
    RsDossier("Client_Nom").Value, RsDossier("Client_Siret").Value, _
    RsDossier("Chorus_Service").Value, RsDossier("Engagement").Value, _
    Basket_HT, Basket_TVA, Basket_TTC)

Set objFile = objFSO.OpenTextFile(xmlOutput, 2, True) ' 2 = ForWriting
objFile.Write sXml
objFile.Close
Set objFile = Nothing

Il faut rester attentif à l'encodage. Le XML annonce UTF-8. En ASP Classic ancien, les conversions implicites peuvent provoquer des surprises, surtout avec les accents. Dans mon cas, je conserve un pipeline simple: valeurs propres en entrée, XML construit par MSXML, puis fichier écrit en sortie. Si le site manipule beaucoup d'accents, il faut tester explicitement le fichier produit avec des noms clients réels.

Le piège des balises facultatives vides

C'est probablement le point qui justifie à lui seul l'abandon du template. Une balise facultative n'est pas une balise obligatoire avec une valeur vide. Si une donnée n'existe pas, la balise doit souvent disparaître.

Cas réel: BuyerReference. Dans certaines factures Chorus, on a une référence de service. Dans d'autres, on ne l'a pas. La mauvaise réponse consiste à produire ceci:

' Mauvaise idee: produire une balise vide.
Call AddTextNode(oXml, agreement, NS_RAM, "ram:BuyerReference", "")

' XML obtenu:
' <ram:BuyerReference></ram:BuyerReference>

Cela peut paraître inoffensif. Pourtant, selon le contexte, le validateur Schematron peut refuser une balise vide ou mal positionnée. La bonne approche est de ne pas créer le noeud si la donnée est absente:

' Bonne pratique: ne creer la balise que si elle a une vraie valeur.
Call AddTextNodeIfValue(oXml, agreement, NS_RAM, "ram:BuyerReference", buyerReference)

Cette logique vaut aussi pour les références d'engagement, certains identifiants acheteur, certains compléments d'adresse et, plus généralement, toutes les informations qui ne sont pas garanties dans la base.

Valider d'abord le XML seul

Avant d'insérer le XML dans un PDF/A-3, il faut le valider seul. Sinon, on mélange deux problèmes: la conformité du XML et la conformité du PDF hybride. Le debug devient beaucoup plus lent.

Je commence donc par ouvrir le fichier XML avec MSXML. Cette vérification ne remplace pas le validateur FNFE, mais elle permet au moins de détecter immédiatement les erreurs de XML mal formé: guillemets, esperluettes, balises non fermées, caractères interdits.

If Not oXml.load(xmlOutput) Then
    Response.Write "<h3>Erreur XML</h3>"
    Response.Write "<pre>"
    Response.Write Server.HTMLEncode(oXml.parseError.reason)
    Response.Write "Ligne: " & oXml.parseError.line & vbCrLf
    Response.Write "Position: " & oXml.parseError.linepos & vbCrLf
    Response.Write "</pre>"
    Response.End
End If

Ensuite seulement, je passe le fichier dans le validateur FNFE-MPE. Pour le profil Minimum, les messages sont en général assez directs: mauvais noeud, noeud non autorisé, attribut absent, format de date incorrect, montant mal formé, identifiant vide.

Validation FNFE du XML Factur-X Minimum
Validation FNFE du fichier XML seul: c'est la première barrière à franchir avant de toucher au PDF.

Vérifier le PDF/A-3 et les metadata XMP

Un PDF Factur-X valide n'est pas seulement un PDF avec un XML attaché. Il faut aussi que les metadata XMP annoncent correctement la nature du document, le profil Factur-X, le nom du fichier XML embarqué et la conformité PDF/A-3.

Pour cette couche, j'utilise trois outils:

  • veraPDF pour la conformité PDF/A;
  • ExifTool pour inspecter les metadata et les pièces jointes;
  • FNFE-MPE pour la validation globale Factur-X.

Sous Windows, ExifTool est particulièrement pratique parce qu'il donne un rapport texte exploitable, archivable et comparable.

@echo off
set EXIF=C:\Tools\exiftool-13.58_64\exiftool.exe
set PDF=C:\Temp\Facture_FacturX.pdf

"%EXIF%" -a -G1 -s "%PDF%" > C:\Temp\Facture_XMP.txt

echo.
echo Controle XMP termine.
echo Voir C:\Temp\Facture_XMP.txt
pause
Validation veraPDF PDF/A-3
Validation PDF/A-3 avec veraPDF: cette étape est indépendante de la justesse métier du XML.
Inspection XMP avec ExifTool
Inspection XMP avec ExifTool: utile pour vérifier que le PDF final annonce bien Factur-X Minimum.

Extraire le XML réellement embarqué

Une erreur très courante consiste à valider le bon fichier XML, puis à embarquer un autre fichier dans le PDF final. Cela arrive facilement lorsqu'on a plusieurs dossiers: tests, archives, XML Minimum, XML EN16931, factures anciennes, fichiers temporaires.

Il faut donc extraire le XML du PDF final et le comparer au XML source. Le test peut être rudimentaire au début: même taille, même numéro de facture, mêmes montants, même profil. Puis on peut automatiser une comparaison stricte.

' Exemple de controle simple apres extraction du XML depuis le PDF final
sourceXml = ReadAllText("C:\Temp\factur-x-source.xml")
embeddedXml = ReadAllText("C:\Temp\factur-x-extrait-du-pdf.xml")

If sourceXml <> embeddedXml Then
    Call Log("ATTENTION: le XML embarque n'est pas identique au XML source.")
Else
    Call Log("OK: XML source = XML embarque.")
End If

C'est une étape de debug très rentable. Elle évite de chercher une erreur dans MSXML alors que le problème est simplement un mauvais chemin de fichier dans la phase d'insertion PDF.

Extraire le XML embarqué avec PDFdetach

Un petit utilitaire très pratique pour inspecter un PDF Factur-X est PDFdetach, fourni avec la suite Poppler, aussi disponible en ligne de commande (CLI).

Il permet simplement de lister les pièces jointes embarquées puis d’extraire le fichier factur-x.xml réellement présent dans le PDF final transmis à Chorus Pro.

C'est extrêmement utile lors des phases de debug, notamment lorsqu'on manipule plusieurs versions de XML (Minimum, EN16931, fichiers temporaires, archives, etc.).

pdfdetach.exe -list Facture.pdf

pdfdetach.exe -saveall Facture.pdf

On peut télécharger les utilitaires Poppler pour Windows ici : Télécharger Poppler / PDFdetach → ou télécharger la verson CLI Télécharger CLI →

Ecosio: utile, mais à utiliser avec prudence ici

Ecosio et d'autres validateurs XML en ligne sont utiles pour comprendre certains messages, mais il faut garder en tête le périmètre du billet. Ici, on travaille le profil Factur-X Minimum. On ne cherche pas encore à produire un XML PEPPOL BIS Billing 3.0 ni un EN16931 complet.

Donc, si un validateur orienté PEPPOL réclame des lignes, des codes TVA plus détaillés ou des blocs absents du profil Minimum, ce n'est pas forcément une erreur de notre billet. C'est souvent le signe que l'outil valide un autre contexte que celui que l'on vise.

Chorus Pro: le test final de terrain

Le dernier test reste Chorus Pro. L'objectif opérationnel est très simple: que Chorus reconnaisse le PDF comme une facture hybride Factur-X et ne renvoie pas l'utilisateur vers une page de correction manuelle interminable.

Dans mon flux, le bénéfice est immédiat. Le serveur produit la facture, ajoute le XML Minimum, ajoute les metadata, puis le PDF est déposé. Si Chorus identifie correctement la facture, les champs essentiels sont repris sans repasser par la pseudo-OCR.

Dépôt Chorus Pro reconnu comme Factur-X
Contrôle terrain: Chorus Pro reconnaît la facture comme Factur-X et évite la ressaisie manuelle.

Debug: comparer un XML valide et un XML invalide

Quand un validateur refuse un XML, il ne faut pas corriger au hasard. Ma méthode consiste à conserver trois fichiers:

  • le XML source produit par le serveur;
  • le XML extrait du PDF final;
  • un XML de référence déjà validé.

Ensuite, je compare les trois. Les différences les plus importantes sont rarement les valeurs visibles. Ce sont plutôt:

  • une balise vide qui ne devrait pas exister;
  • un namespace absent ou mal déclaré;
  • un attribut format="102" oublié;
  • un montant avec virgule;
  • un ancien numéro de facture resté dans le template;
  • une référence Chorus placée dans le mauvais bloc.

Dans cette logique, le XML ex nihilo est plus facile à corriger que le template. On remonte directement à la fonction qui crée la balise fautive.

Validation XML et validation métier: deux choses très différentes

Une difficulté importante lorsqu'on commence avec Factur-X est de comprendre qu'il existe plusieurs niveaux de validation totalement distincts.

La première validation est une validation purement informatique:

  • le PDF est-il bien un PDF/A-3 valide ?
  • le XML respecte-t-il le schéma XSD ?
  • le profil Factur-X MINIMUM est-il conforme ?
  • les règles Schematron du profil sont-elles satisfaites ?

Dans ce cas, un validateur comme FNFE-MPE, veraPDF ou Ecosio peut parfaitement déclarer le document conforme.

Mais cela ne signifie absolument pas que Chorus Pro acceptera la facture au niveau métier.

En effet, Chorus ajoute ses propres contrôles fonctionnels. Typiquement:

  • le SIRET du client doit exister et être valide ;
  • le destinataire doit être connu dans l'annuaire Chorus ;
  • certains clients imposent un service destinataire ;
  • le service doit exister exactement dans Chorus ;
  • si aucun service n'est demandé, il ne faut surtout pas créer une balise vide.

C'est un point très important: la notion de "service Chorus" n'est pas intrinsèquement liée au profil Factur-X MINIMUM lui-même. On peut donc produire un XML parfaitement valide au sens de la norme, mais rejeté ensuite par Chorus lors de la validation métier.

En pratique, Chorus effectue souvent deux étapes successives:

  1. une validation technique immédiate ;
  2. une validation métier différée.

Le cas est assez déroutant au début: Chorus peut d'abord délivrer un accusé technique ou un code de confirmation, puis envoyer plusieurs heures plus tard un mail indiquant finalement que la facture est rejetée. Comme ici:

Suivi des flux :
n°26015 du ... reçue le ... par le flux FSO1117A_CPP001_CPP... a été rejetée pour le(s) motif(s) suivants, identifié(s) dans le flux cycle de vie : 
L'element FichierXml.SupplyChainTradeTransaction.ApplicableHeaderTradeAgreement.BuyerTradeParty.SpecifiedLegalOrganization.ID.value est obligatoire.
L'identifiant debiteur de la demande de paiement (balise : FichierXml.SupplyChainTradeTransaction.ApplicableHeaderTradeAgreement.BuyerTradeParty.
SpecifiedLegalOrganization.ID.value) n'est pas reference dans notre systeme Ce type d'identifiant n'est pas autorise en entree.

Dans la pratique, les causes réelles de rejet restent souvent limitées:

  • SIRET incorrect ;
  • service Chorus inexistant ;
  • code postal incohérent ;
  • identifiant acheteur erroné ;
  • balise facultative vide.

Le plus efficace consiste souvent à récupérer:

  • le mail de rejet Chorus ;
  • le XML réellement envoyé ;
  • le message d'erreur exact.

On peut alors demander à ChatGPT d'analyser précisément le rejet et de proposer une correction ciblée du XML.

Cette distinction entre validation informatique et validation métier est fondamentale. Un XML "valide" n'est pas forcément une facture "acceptable" pour Chorus Pro.

Ce que le profil Minimum permet déjà

Le profil Minimum ne remplace pas un XML EN16931 complet. Il ne porte pas toute la richesse sémantique d'une facture structurée moderne. Mais il suffit pour une première automatisation propre: numéro, date, vendeur, acheteur, montants, devise, références utiles.

Pour un serveur ASP Classic existant, c'est une transition raisonnable. On ne réécrit pas toute la facturation. On ajoute une couche XML, on l'insère dans le PDF, on valide, puis on fiabilise progressivement.

Bilan provisoire

Le remplacement de template m'a permis de faire ma première Factur-X rapidement. La construction ex nihilo m'a permis de comprendre le format et de rendre le serveur plus robuste. Le point clé n'est pas la quantité de code, mais le contrôle: savoir exactement quelle balise est produite, pourquoi elle est là, et quand elle ne doit pas être produite.

La prochaine étape sera un autre billet, séparé, consacré au profil EN16931: lignes détaillées, dictionnaire de champs, règles métier, PEPPOL, TVA plus complexe et génération avancée. Ce billet-ci s'arrête volontairement au Minimum, parce que c'est le bon niveau pour construire une base fiable.