Sur un PDF, la TVA tient dans deux colonnes : un taux et un montant. Quand le taux est à zéro, on écrit la raison en bas de page, en français, et la personne qui reçoit la facture comprend. En facture électronique structurée, cette ligne de texte n’existe pas. Il faut déclarer un code de régime, et si ce régime n’appelle pas de TVA, un motif lui aussi codé. Sans ça, le flux est rejeté à l’entrée.

Je maintiens einvoicing, une gem Ruby open source qui génère des factures Factur-X, UBL et CII conformes à la norme EN 16931. Récemment, je lui ai fait passer un lot de quinze factures volontairement hostiles : multi-taux, autoliquidation, cinq cents lignes, taux d’outre-mer, devise étrangère. Le lot est passé au schéma XSD, aux contrôles du conteneur PDF/A-3 et à un jeu de règles EN 16931 recalculé à la main.

Le lot a sorti quatre défauts, tous dans ma gem.

À gauche, une facture PDF où le taux à zéro est expliqué par une phrase libre en bas de page. À droite, le même cas dans un flux structuré : une catégorie de TVA, un code de motif normalisé et son texte, sans lesquels le flux est rejeté.
La même exonération, écrite pour un lecteur à gauche et pour un validateur à droite.

Sept régimes, pas un taux

La norme ne connaît pas « une facture à 0 % ». Elle connaît sept catégories de TVA, et le zéro peut venir de cinq d’entre elles pour des raisons juridiques différentes :

Code Régime Cas courant
S Taux normal ou réduit La facture ordinaire
Z Taux zéro Certaines livraisons taxées à 0 %
E Exonéré Franchise en base, acte médical, formation
AE Autoliquidation Sous-traitance dans le bâtiment
K Livraison intracommunautaire Client européen avec numéro de TVA valide
G Export hors UE Vente à un client suisse ou américain
O Hors champ de la TVA Certaines indemnités, subventions

Ma gem s’arrêtait à trois : standard, taux zéro, autoliquidation. Tout le reste ressortait en « standard », sans bruit et sans erreur visible.

Le champ que le papier ne demandait pas

Quand une catégorie n’appelle pas de TVA, la norme exige un motif : un texte lisible et, selon les cas, un code européen. Cinq règles de conformité le vérifient, une par catégorie.

Ma gem n’en écrivait aucun. Conséquence : toutes les factures en autoliquidation qu’elle émettait étaient non conformes. Elles produisaient un PDF correct et un XML valide au schéma, tout en étant rejetables par la règle métier.

Pour l’autoliquidation, l’intracommunautaire, l’export et le hors champ, le motif est mécanique : il existe un code européen dédié. Pour l’exonération simple, non. Le motif dépend de l’article invoqué, et une TPE en franchise en base n’est pas exonérée pour la même raison qu’un cabinet médical.

Voir le code Ruby
Einvoicing::LineItem.new(
  description: "Prestation de conseil",
  quantity:    1,
  unit_price:  BigDecimal("2500.00"),
  vat_rate:    0,
  category:    :exempt,
  exemption_reason:      "TVA non applicable, art. 293 B du CGI",
  exemption_reason_code: Einvoicing::Tax::VATEX_FR_FRANCHISE
)

C’est trois lignes de configuration. Encore faut-il que le logiciel qui émet la facture ait un endroit où les stocker, ce qui suppose qu’il modélise le régime et pas seulement le taux.

Trois centimes d’écart, et la facture est refusée

Deuxième défaut, plus sournois. Le total de TVA d’une catégorie peut se calculer de deux façons : en additionnant la TVA arrondie de chaque ligne, ou en appliquant le taux à la base imposable de la catégorie.

Sur une facture de cinq lignes, les deux méthodes donnent le même chiffre. Sur cinq cents lignes, elles divergent : trois centimes d’écart sur une catégorie, quinze sur une autre. La règle de contrôle se vérifie au centime près, et une facture « presque bonne » est refusée comme les autres.

Une soustraction posée sur cinq cents lignes à 20 pour cent : la somme des TVA de ligne arrondies donne 4 231,47 euros, le taux appliqué à la base agrégée donne 4 231,44 euros, et l'écart de trois centimes suffit à faire échouer le contrôle.
Les deux méthodes sont défendables. Le contrôle, lui, se vérifie au centime près.

La norme tranche pour la seconde méthode : le montant de TVA d’une catégorie se déduit de sa base agrégée, et non de la somme des lignes. Une multiplication, un arrondi, à la fin.

Voir le code Ruby
# EN 16931, BR-CO-17 : TVA de la catégorie = base de la catégorie x taux,
# arrondi au centime. Les bases s'additionnent donc en BigDecimal, sans
# arrondi intermédiaire, et l'arrondi ne tombe qu'après la multiplication.
base = lines.sum(&:net_amount)      # BigDecimal, aucun arrondi ici
vat  = (base * rate).round(2)       # rate vaut 0.20 pour 20 %, comme dans la gem

Tout arrondi placé plus haut dans la chaîne rouvre l’écart, ce qui explique que le défaut résiste au débogage ligne à ligne : prises une par une, les lignes sont justes.

Et il ne se voit jamais en recette, parce que personne ne teste une facture de cinq cents lignes.

Les taux qui existent quand même

20, 10, 5,5 et 0. Ma liste de taux français s’arrêtait là, comme dans à peu près tous les tutoriels. Elle refusait donc 2,1 %, le taux de la presse et des médicaments remboursables. Elle refusait 8,5 %, le taux normal en Guadeloupe, en Martinique et à La Réunion. Elle refusait les taux corses. Tous parfaitement légaux.

Même famille d’erreur sur les numéros de TVA. Mon validateur imposait le format français à tout le monde, acheteur compris. Une facture intracommunautaire vers l’Allemagne échouait donc sur le numéro de TVA du client, qui était pourtant valide.

Le point commun de ces deux bugs est un référentiel écrit depuis le cas courant. Ça tient jusqu’au jour où un utilisateur vend en Corse ou achète depuis Munich.

Pourquoi ça devient un problème d’entreprise

Aucun de ces cas n’est exotique. La franchise en base concerne des centaines de milliers de très petites entreprises. L’autoliquidation est la règle en sous-traitance du bâtiment. Les taux d’outre-mer, ce sont plusieurs régions. La devise étrangère arrive au premier client hors zone euro.

Chacun de ces cas est le quotidien de quelqu’un.

Aujourd’hui, ces factures partent sans que personne ne se plaigne, parce qu’un humain les lit à l’arrivée et corrige mentalement ce qui manque. À partir du moment où c’est une machine qui les reçoit, il n’y a plus de correction mentale. Il y a un rejet, un retard de paiement, et un client qui vous demande de renvoyer.

La réception devient obligatoire en septembre 2026, l’émission en septembre 2027 pour les TPE et PME. Le premier rejet, lui, arrivera avant l’échéance qui figure au calendrier.

La question à poser à son éditeur

Pas « serez-vous prêts pour 2027 ? ». Tout le monde répond oui.

Plutôt : « qu’est-ce que votre logiciel écrit dans le champ motif d’exonération quand j’émets une facture en autoliquidation ? ». S’il n’y a pas de réponse, c’est qu’il n’y a pas de champ.

Et pour lever le doute sur une facture précise, il y a plus rapide que la théorie : la passer dans un validateur, analyse locale, sans upload. Si vous voulez faire le tour de votre chaîne complète, la page facturation électronique 2026 décrit le diagnostic que je propose.

Ce que j’en retiens

Ma conclusion est un peu inconfortable. J’écris cette gem depuis des mois, je connais la norme, et j’ai quand même émis des factures non conformes pendant tout ce temps sans qu’aucun test ne bronche.

La norme n’est pas piégeuse en elle-même : un XML valide au schéma peut être invalide au métier, et rien ne vous le dira tant que personne n’aura écrit les factures qui font mal. Le lot de quinze factures hostiles a coûté une demi-journée. Il aurait coûté beaucoup plus cher en septembre 2027.

Tout est corrigé dans la version 0.9.0 de la gem, publiée dans la foulée.