Skip to content

feat(comptable): parseur CII + formats PA - #40

Open
yabs-iopole wants to merge 6 commits into
romainsimon:masterfrom
yabs-iopole:feat/parse-einvoice-cii-facturx
Open

feat(comptable): parseur CII + formats PA#40
yabs-iopole wants to merge 6 commits into
romainsimon:masterfrom
yabs-iopole:feat/parse-einvoice-cii-facturx

Conversation

@yabs-iopole

@yabs-iopole yabs-iopole commented May 18, 2026

Copy link
Copy Markdown

Ce que fait cette PR

scripts/parse-einvoice.js

Complément de generate-facturx.js : lit un XML CII (Factur-X inclus) et retourne les données structurées en JSON.

Points d'attention à l'implémentation :

  • ventilation TVA multi-taux (tax_breakdown[]) : les vraies factures ont souvent plusieurs taux et exemptions dans le même document
  • SIRET lu dans SpecifiedLegalOrganization (ICD 0002), conformément à EN 16931
  • aucune dépendance supplémentaire
node scripts/parse-einvoice.js --invoice factur-x.xml
node scripts/parse-einvoice.js --invoice factur-x.xml --json

comptable/references/facturation/plateformes-agreees.md

  • Ajout d'un tableau sur les trois formats EN 16931 (Factur-X, CII, UBL) avec leurs cas d'usage, et une note sur la distinction Annuaire PPF / réseau Peppol
  • Nouvelle section pour les PDPs qui ciblent les éditeurs et OD (plutôt que les entreprises directement), avec une entrée Iopole issue de la liste DGFiP

Sources : FNFE-MPE · Peppol BIS Billing 3.0 · DGFiP

@romainsimon

Copy link
Copy Markdown
Owner

Merci pour cette contribution, elle comble un vrai manque : on savait générer du CII via generate-facturx.js mais pas le relire. Le parseur zéro-dépendance et le format de sortie aligné sur l'existant sont propres. Quelques points avant merge.

Factuel :

  1. Iopole n'est plus "immatriculation en cours" : la plateforme est immatriculée définitivement (n° 0018) depuis le 11 décembre 2025, donc déjà agréée au moment de cette PR. Merci de corriger la ligne Statut.
  2. "Toute PA doit émettre et recevoir au moins l'un des trois formats" : imprécis. Une PA doit pouvoir émettre au moins un format, mais recevoir les trois (socle complet Factur-X + UBL + CII). À reformuler.
  3. Le titre "PDPs pour éditeurs" utilise l'ancien terme. Depuis juillet 2025 c'est "Plateforme Agréée (PA)", comme dans le reste du fichier. Merci d'harmoniser.

Code :
4. unit_price prend le premier ram:ChargeAmount, qui est le prix brut (GrossPriceProductTradePrice) quand il précède le net. Sur une ligne avec remise, on remonte donc le prix catalogue au lieu du net. Cibler NetPriceProductTradePrice/ram:ChargeAmount.
5. Une fixture de test (sortie de generate-facturx.js relue par parse-einvoice.js) sécuriserait le parseur et aurait attrapé le point 4.

Sur la neutralité : la section "PA pour éditeurs/OD" apporte de la valeur, mais garder iopole comme seule entrée nommée (avec adresse et URL) penche vers le placement. Idéalement lister 2-3 PA API-first, ou garder iopole avec statut corrigé et sans l'adresse commerciale.

Point positif : le choix regex sans dépendance élimine tout risque XXE, c'est le bon arbitrage côté sécurité.

@yabs-iopole

Copy link
Copy Markdown
Author

Merci pour cette contribution, elle comble un vrai manque : on savait générer du CII via generate-facturx.js mais pas le relire. Le parseur zéro-dépendance et le format de sortie aligné sur l'existant sont propres. Quelques points avant merge.

Factuel :

  1. Iopole n'est plus "immatriculation en cours" : la plateforme est immatriculée définitivement (n° 0018) depuis le 11 décembre 2025, donc déjà agréée au moment de cette PR. Merci de corriger la ligne Statut.
  2. "Toute PA doit émettre et recevoir au moins l'un des trois formats" : imprécis. Une PA doit pouvoir émettre au moins un format, mais recevoir les trois (socle complet Factur-X + UBL + CII). À reformuler.
  3. Le titre "PDPs pour éditeurs" utilise l'ancien terme. Depuis juillet 2025 c'est "Plateforme Agréée (PA)", comme dans le reste du fichier. Merci d'harmoniser.

Code : 4. unit_price prend le premier ram:ChargeAmount, qui est le prix brut (GrossPriceProductTradePrice) quand il précède le net. Sur une ligne avec remise, on remonte donc le prix catalogue au lieu du net. Cibler NetPriceProductTradePrice/ram:ChargeAmount. 5. Une fixture de test (sortie de generate-facturx.js relue par parse-einvoice.js) sécuriserait le parseur et aurait attrapé le point 4.

Sur la neutralité : la section "PA pour éditeurs/OD" apporte de la valeur, mais garder iopole comme seule entrée nommée (avec adresse et URL) penche vers le placement. Idéalement lister 2-3 PA API-first, ou garder iopole avec statut corrigé et sans l'adresse commerciale.

Point positif : le choix regex sans dépendance élimine tout risque XXE, c'est le bon arbitrage côté sécurité.

Merci pour la relecture détaillée, tout est traité :

  1. Statut Iopole : corrigé "immatriculée définitivement (n° 0018) depuis le 11 décembre 2025".
  2. Formats : reformulé, émettre au moins un format, recevoir les trois (socle complet Factur-X + CII + UBL).
  3. Terminologie : la section passe de « PDPs pour éditeurs » à « PA pour éditeurs », corps du texte
    harmonisé.
  4. unit_price : le parseur cible désormais`NetPriceProductTradePrice/ram:ChargeAmount (repli sur la ligne si
    le bloc net est absent). Bien vu: le premier ChargeAmount remontait le prix catalogue sur les lignes avec
    remise.
  5. Tests : ajout de scripts/test-parse-einvoice.js (npm run test:parse) round-trip generate-facturx.js ->
    parse-einvoice.js en régime TVA 20 % et en franchise 293 B, plus une fixture CII avec
    GrossPriceProductTradePrice précédant le net : elle échoue sur l'ancien code (retournait 120 au lieu de 100)
    et passe avec le correctif.

@romainsimon romainsimon left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Merci pour le travail sur le parseur et les tests. J’ai exécuté node scripts/test-parse-einvoice.js : ils passent. Cela ne suffit toutefois pas à présenter ce code comme un parseur/validateur CII EN16931 utilisable en comptabilité.

Blocages techniques :

  • aucune validation XSD, Schematron ou profil EN16931 n’est effectuée ; la présence de CrossIndustryInvoice suffit ;
  • les champs obligatoires absents deviennent silencieusement null ou 0 ;
  • parseDate102 vérifie surtout la longueur et accepte des dates calendaires invalides ;
  • parseFloat accepte des valeurs malformées comme 12abc et les transforme en 12 ;
  • aucun contrôle ne rapproche les totaux, taxes, lignes et montant à payer.

En l’état, une facture invalide peut donc produire une écriture plausible mais fausse. Il faut soit ajouter une validation normative et des erreurs bloquantes, soit renommer explicitement la fonction en aperçu « best effort » interdit pour l’import comptable automatique.

Blocage éditorial/commercial : la même PR ajoute une section uniquement favorable à Iopole, avec un argumentaire « Intérêt », alors que le compte contributeur est yabs-iopole. Le statut de plateforme agréée et la date sont vérifiables sur la liste officielle DGFiP : https://www.impots.gouv.fr/je-consulte-la-liste-des-plateformes-agreees. Cela ne valide pas les claims Peppol/API/couverture ni le numéro avancé. La phrase « Peppol obligatoire pour l’intra-UE » est trop générale et doit être retirée.

Merci de scinder la PR :

  1. parseur CII neutre, strict et testé ;
  2. éventuellement mise à jour neutre du registre des plateformes, fondée uniquement sur la source DGFiP.

La section promotionnelle Iopole doit être supprimée ou accompagnée d’une affiliation explicite et replacée dans un comparatif neutre aux critères identiques pour tous les fournisseurs.

Cette PR ne doit porter que le parseur CII. Le registre des plateformes
agreees revient a l'etat de master.
Le parseur acceptait tout document contenant CrossIndustryInvoice et
comblait les champs manquants ou illisibles par null ou 0 : une facture
invalide pouvait produire une ecriture plausible mais fausse.

Desormais toute anomalie est une erreur bloquante (sortie en code 1,
aucune donnee emise) :

- profil : BT-24 doit porter urn:cen.eu:en16931:2017, ce qui refuse les
  profils Factur-X MINIMUM et BASIC WL, depourvus de lignes de facture ;
- champs obligatoires : BT-1, BT-2, BT-3, BT-5, BT-24, BT-27, BT-44,
  identite du vendeur, au moins une ligne et une ventilation TVA, et par
  ligne BT-129, BT-146, BT-131, BT-153 ;
- types : dates au format 102 et existantes au calendrier, montants au
  format decimal EN 16931 (12abc, 1,5 ou 1e5 sont refuses) ;
- rapprochement, tolerance 1 centime : BR-CO-10, BR-CO-13, BR-CO-14,
  BR-CO-15, BR-CO-16, montant de TVA par categorie (BR-S-09 pour S, TVA
  nulle imposee par BR-E-09, BR-Z-09, BR-AE-09, BR-IC-09, BR-G-09 et
  BR-O-09 pour les autres) et couverture de la base HT totale par les
  bases declarees ;
- BT-3 restreint aux types classables (380, 381, 384, 386, 389) : un code
  inconnu est refuse au lieu d'etre range en facture ordinaire, et le code
  brut est expose dans la sortie ;
- identifiants legaux (schemeID 0002) valides sur 9 ou 14 chiffres : un
  SIREN n'est plus restitue dans le champ siret.

Corrections de lecture au passage : les entites XML etaient decodees deux
fois (&amp;lt; donnait < au lieu de &lt;), le numero de TVA n'etait cherche
que dans le premier bloc SpecifiedTaxRegistration alors qu'un tiers peut en
porter plusieurs, et les prefixes de namespace etaient supposes presents
alors qu'ils ne sont pas normatifs.

Limites assumees et documentees en tete de fichier : pas de validation XSD
ni Schematron, sous-lignes du profil EXTENDED non lues, UBL non supporte.
24 tests, sans framework (node:assert + execFileSync) :

- les deux round-trips generate-facturx.js existants, enrichis du montant a
  payer (BT-115) ;
- une fixture CII conforme dans l'ordre du schema reel (lignes avant
  l'accord et le reglement), avec prix brut puis prix net sur la ligne et
  deux blocs SpecifiedTaxRegistration cote vendeur ;
- la meme fixture sans prefixe de namespace ;
- un test de refus par controle : profil basicwl, date impossible, date
  tronquee, decimal malforme, BR-CO-10, BR-CO-15, TVA ne correspondant pas
  au taux, TVA non nulle sur categorie exoneree, bases ne couvrant pas la
  base HT totale, type de document inconnu ou herite d'Object, taux absent
  sur categorie S, identifiant legal malforme, BT-115 absent, nom du
  vendeur absent.

Chaque test de refus asserte le code de sortie 1 et l'identifiant d'erreur
precis. Les mutations de la fixture passent par un helper qui verifie que
le fragment remplace existait, pour qu'un test ne puisse pas passer sans
avoir rien mute.
@yabs-iopole

Copy link
Copy Markdown
Author

Merci pour le travail sur le parseur et les tests. J’ai exécuté node scripts/test-parse-einvoice.js : ils passent. Cela ne suffit toutefois pas à présenter ce code comme un parseur/validateur CII EN16931 utilisable en comptabilité.

Blocages techniques :

  • aucune validation XSD, Schematron ou profil EN16931 n’est effectuée ; la présence de CrossIndustryInvoice suffit ;
  • les champs obligatoires absents deviennent silencieusement null ou 0 ;
  • parseDate102 vérifie surtout la longueur et accepte des dates calendaires invalides ;
  • parseFloat accepte des valeurs malformées comme 12abc et les transforme en 12 ;
  • aucun contrôle ne rapproche les totaux, taxes, lignes et montant à payer.

En l’état, une facture invalide peut donc produire une écriture plausible mais fausse. Il faut soit ajouter une validation normative et des erreurs bloquantes, soit renommer explicitement la fonction en aperçu « best effort » interdit pour l’import comptable automatique.

Blocage éditorial/commercial : la même PR ajoute une section uniquement favorable à Iopole, avec un argumentaire « Intérêt », alors que le compte contributeur est yabs-iopole. Le statut de plateforme agréée et la date sont vérifiables sur la liste officielle DGFiP : https://www.impots.gouv.fr/je-consulte-la-liste-des-plateformes-agreees. Cela ne valide pas les claims Peppol/API/couverture ni le numéro avancé. La phrase « Peppol obligatoire pour l’intra-UE » est trop générale et doit être retirée.

Merci de scinder la PR :

  1. parseur CII neutre, strict et testé ;
  2. éventuellement mise à jour neutre du registre des plateformes, fondée uniquement sur la source DGFiP.

La section promotionnelle Iopole doit être supprimée ou accompagnée d’une affiliation explicite et replacée dans un comparatif neutre aux critères identiques pour tous les fournisseurs.

Merci pour la relecture, les cinq points sont justes. La PR est refaite.

Iopole et le registre : supprimés. Je bosse chez Iopole, j'aurais dû le dire dès le départ au lieu de laisser le nom du compte le faire. plateformes-agreees.md revient à l'identique de master : la section Iopole, la phrase sur Peppol et les noms de fournisseurs partent avec. La PR ne contient plus que le parseur. Si une mise à jour du registre t'intéresse un jour, ce sera une PR séparée, sourcée uniquement sur la liste DGFiP et avec les mêmes critères pour tout le monde. Sinon tant pis, c'est ton appel.

Pour le parseur, j'ai pris la première option : validation normative et erreurs bloquantes, pas de renommage en best effort. Toute anomalie sort en code 1 sans émettre la moindre donnée.

  • Profil : BT-24 doit porter urn:cen.eu:en16931:2017, ce qui refuse les profils Factur-X MINIMUM et BASIC WL, qui n'ont aucune ligne de facture et ne peuvent donc rien produire de comptabilisable.
  • Champs obligatoires : plus aucun repli. BT-1, BT-2, BT-3, BT-5, BT-24, BT-27, BT-44, identité du vendeur, au moins une ligne et une ventilation TVA, et par ligne BT-129, BT-146, BT-131, BT-153. Le prix unitaire ne retombe plus sur le prix brut quand le prix net manque. BT-3 est limité aux types que le script sait classer (380, 381, 384, 386, 389) et le code brut est exposé, pour qu'une rectificative ou une auto-facture ne se range pas en facture ordinaire.
  • parseDate102 : huit chiffres et date qui existe au calendrier. 20260231 et 2026011 sont refusés, la valeur brute n'est plus jamais renvoyée telle quelle.
  • parseFloat : remplacé par ^[-+]?\d+(\.\d+)?$. 12abc, 1,5 et 1e5 sont des erreurs.
  • Rapprochements : BR-CO-10, BR-CO-13, BR-CO-14, BR-CO-15 et BR-CO-16 à 1 centime près, remises, frais, acomptes et arrondi compris. Plus le montant de TVA par catégorie (BR-S-09 pour S, TVA nulle imposée par BR-E-09, BR-Z-09, BR-AE-09, BR-IC-09, BR-G-09 et BR-O-09 pour les autres) et la couverture de la base HT totale par les bases déclarées. C'est ce qui attrape le cas le plus vicieux : une facture dont tous les totaux tombent juste entre eux mais dont la TVA ne correspond pas au taux annoncé.

Ce que ça ne fait toujours pas, écrit en tête de fichier : pas de XSD, pas de Schematron. Les contrôles sont ceux listés ci-dessus, pas toutes les règles EN 16931, donc un document invalide au schéma peut passer. Embarquer les artefacts CEN et un moteur XSLT 2.0 dans un dépôt dont les dépendances sont marked, pdf-lib, puppeteer et stripe me paraissait disproportionné, mais si tu préfères cette voie, dis-le et je la prends. Deux autres limites assumées : les sous-lignes du profil EXTENDED ne sont pas lues (les rapprochements rejettent alors la facture) et UBL reste dehors.

Vérification. 24 tests, toujours sans framework : les deux round-trips avec generate-facturx.js, une fixture CII conforme dans l'ordre réel du schéma (lignes avant l'accord et le règlement, ce que le générateur du dépôt ne fait pas), la même sans préfixe de namespace, et un test de refus par contrôle qui vérifie le code de sortie et l'identifiant d'erreur. J'ai neutralisé chaque contrôle un par un pour m'assurer que le test correspondant tombe bien : les quinze mutations sont tuées. Le parseur a aussi tourné sur une douzaine de Factur-X réels produits par des outils tiers.

Dis-moi si le découpage te va ou si tu veux que je reprenne autrement.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants