Une facture émise, un numéro attribué, un PDF envoyé au client, un message de succès à l'écran. Et aucune écriture comptable. Le défaut tenait en trois lignes : une capture d'exception large, un message journalisé en avertissement, et un retour vide que personne ne testait.
Nous avons corrigé cela dans notre ERP, Cleo, et le correctif s'est révélé beaucoup plus large que prévu. Introduire un simple statut « brouillon » sur une facture a mis au jour six indicateurs faux, une créance fantôme, une action qui renvoyait systématiquement une erreur serveur, et une question d'ordonnancement que PostgreSQL tranche à notre place. Cet article raconte ce que cette séquence apprend sur la conception d'une application de gestion.
Portée
- S'adresse à qui conçoit, intègre ou audite une application de facturation — éditeur, intégrateur, direction financière, commissaire aux comptes.
- Le contexte est celui d'une facture qui déclenche une écriture comptable et consomme un numéro dans une séquence continue.
- Les contraintes de numérotation évoquées sont celles de la plupart des référentiels ; les règles exactes dépendent de votre juridiction.
- Il ne s'agit pas d'un article sur la facturation électronique, qui pose d'autres questions.
Deux statuts qui n'en formaient qu'un
À l'origine, une facture n'avait qu'un statut, et il décrivait son paiement : impayée, partiellement payée, payée. Cela paraît suffisant tant qu'on considère qu'une facture existe dès qu'elle est saisie.
Or une facture a deux vies distinctes. La première est documentaire : elle est en préparation, puis elle est émise, puis éventuellement annulée. La seconde est financière : elle est due, puis encaissée. Les confondre revient à dire qu'un devis en cours de rédaction est une créance impayée.
Le correctif consiste à introduire un second champ, indépendant du premier : brouillon, validée, annulée. Cette séparation n'est pas cosmétique. Elle change la réponse à une question que tout le reste du système pose en permanence : cette facture existe-t-elle ?
Le numéro est une ressource qu'on ne récupère pas
Une facture validée porte un numéro issu d'une séquence continue. Un brouillon n'en porte aucun, et c'est le point le plus important de la conception.
Si le numéro était attribué à la saisie, chaque brouillon abandonné laisserait un trou dans la séquence. Un trou dans une numérotation de factures n'est pas une imperfection esthétique : c'est une anomalie que tout contrôle relève, et qu'il faut ensuite justifier document par document.
Attribuer le numéro à la validation résout cela. Mais la manière de le faire compte autant que la décision elle-même, et voici pourquoi.
Les séquences PostgreSQL ne sont pas transactionnelles. Une valeur consommée l'est définitivement, y compris si la transaction qui l'a demandée échoue ensuite. C'est un choix de conception délibéré du moteur, qui permet à plusieurs transactions concurrentes de numéroter sans se bloquer, et il ne se contourne pas.
La conséquence est un ordre d'opérations non négociable : tout ce qui peut échouer doit être vérifié avant de demander le numéro. Dans notre cas, cela signifie contrôler l'existence du journal des ventes, l'ouverture de la période comptable, la présence des comptes nécessaires et l'identité de l'auteur de l'écriture — puis seulement numéroter, puis écrire, le tout dans une même transaction.
Inverser ces deux blocs produit un défaut qui ne se manifeste qu'en cas d'erreur, donc rarement, donc tard : une période fermée un lundi matin, et la séquence a sauté un numéro que personne ne saura expliquer six mois plus tard.
L'échec silencieux, ou trois lignes très coûteuses
Le mécanisme d'origine appelait une fonction utilitaire chargée de créer l'écriture. Cette fonction capturait toute exception, journalisait un avertissement et renvoyait une valeur vide. L'appelant ne testait pas ce retour.
Le résultat mérite d'être énoncé précisément, parce qu'il est pire qu'une panne : la facture était créée, numérotée, envoyable et payable, sans aucune contrepartie comptable, et l'utilisateur voyait un message de succès. La divergence entre le module de vente et la comptabilité s'installait sans qu'aucun signal ne la révèle, jusqu'au rapprochement suivant.
Une capture d'exception large qui journalise en avertissement est presque toujours le symptôme d'une décision non prise. Elle exprime « je ne sais pas quoi faire de cette erreur », et elle transforme une opération atomique en opération partielle.
La règle que nous en tirons : dans une application de gestion, une opération qui produit à la fois un document et son écriture réussit entièrement ou échoue entièrement. Il n'existe pas de succès partiel acceptable, et une transaction est faite pour cela.
Un brouillon doit être inerte, et cela se mesure en filtres
Voici l'enseignement que nous n'avions pas anticipé, et qui vaut pour toute introduction tardive d'un statut.
Avant ce changement, toute facture en base était une facture réelle. Chaque consommateur de données — tableau de bord, listes, états — avait donc été écrit sur cette hypothèse, sans jamais l'exprimer. L'ajout d'un brouillon rend cette hypothèse fausse, partout à la fois.
Il a fallu six filtres pour rétablir la cohérence. Le chiffre d'affaires de la période, celui de la période précédente, la courbe des douze mois, les créances clients, les factures échues, le classement des produits et celui des clients : tous comptaient des brouillons. Les totaux de page des listes également.
Un brouillon visible dans le chiffre d'affaires est plus dangereux qu'un brouillon absent d'un état, parce qu'il gonfle un indicateur sur lequel se prennent des décisions, et qu'il le fait de manière plausible.
La leçon est méthodologique : introduire un statut, c'est s'engager à recenser tous les endroits qui lisaient la table sans le connaître. Cette liste ne se devine pas, elle s'établit en parcourant les requêtes une à une. C'est fastidieux, et c'est la partie du travail qui décide de la justesse du résultat.
Revenir en arrière : annuler ne suffit pas, il faut détacher
Le retour au brouillon est une fonctionnalité que les utilisateurs demandent immédiatement — une facture validée par erreur, une ligne oubliée, un client à corriger.
Elle n'est acceptable que sous conditions, et sa mise en œuvre réserve un piège.
Les conditions d'abord : le retour au brouillon n'est possible qu'en l'absence de tout paiement enregistré. Une facture partiellement encaissée qui redeviendrait modifiable ouvrirait la porte à des incohérences qu'aucune reprise ne rattrape.
Le piège ensuite : annuler l'écriture ne suffit pas. Il faut aussi la détacher de son document source. Une écriture annulée mais toujours rattachée à sa facture crée une ambiguïté durable — la facture semble avoir une comptabilité, et les traitements qui suivent ce lien récupèrent une écriture sans effet.
Un dernier point qui surprend souvent : le numéro n'est pas rendu. Une facture revenue au brouillon conserve le numéro qu'elle avait reçu, et le reprend à sa prochaine validation. C'est la seule façon d'éviter à la fois le trou dans la séquence et le doublon.
L'annulation et la créance fantôme
Annuler une facture demande de traiter un champ que l'on oublie facilement : le montant restant dû.
Une facture annulée dont le solde dû reste renseigné produit une créance qui n'existe plus mais que le système continue de compter. Elle apparaît dans les créances clients, dans les relances éventuelles, et dans tout indicateur d'encours. Elle est particulièrement difficile à repérer parce que le document, lui, est visiblement annulé — c'est la donnée dérivée qui ment.
Toute mise à jour de statut qui invalide un montant dérivé doit remettre ce montant à zéro dans la même opération. Sinon la donnée dérivée survit à la donnée dont elle dérive.
Ce qu'on interdit sur un brouillon est une décision de conception
Une fois le statut introduit, il faut répondre à une question par action : est-elle légitime sur un brouillon ?
Nous en avons recensé dix qui ne le sont pas : générer le PDF, l'envoyer par courriel, enregistrer un paiement, annuler la facture, la certifier auprès de l'administration fiscale, produire un avoir, déclencher un encaissement mobile, et quelques autres. Toutes présupposent un document qui existe.
Symétriquement, deux actions ne sont légitimes que sur un brouillon : la modification et la suppression. Une facture validée ne se modifie pas et ne se supprime pas — elle s'annule, ou elle donne lieu à un avoir.
Établir cette liste explicitement, plutôt que de traiter les cas au fil des remontées, transforme une série de correctifs en une règle unique. Et elle révèle des défauts préexistants : nous avons découvert à cette occasion qu'une action d'annulation écrivait sur un champ qui n'existait pas, et renvoyait donc une erreur serveur systématique. Personne ne l'avait signalée, sans doute parce que personne n'annulait jamais.
Ce que le rattrapage a coûté
Le passage en base est la partie la plus simple : toutes les factures existantes basculent en « validée », par une migration réversible. Elles ont un numéro et une écriture, elles sont donc validées par définition.
Un point technique mérite d'être signalé à qui utilise un cadre à modèles hérités. Rendre le numéro facultatif suppose de redéfinir un champ déclaré dans une classe abstraite parente, ce que Django n'autorise pas. La migration porte donc sur les trois documents qui héritent de cette classe — devis, commande, facture — alors qu'un seul diffère réellement sa numérotation. C'est sans effet, mais cela surprend à la relecture du fichier de migration, et cela mérite un commentaire.
Un contrôle est en revanche indispensable avant la bascule : vérifier que les séquences sont en avance sur le maximum présent en base. Si une reprise de données a inséré des numéros sans faire progresser la séquence, la première validation après migration produira un numéro déjà attribué. Ce contrôle prend deux minutes et évite un incident qui se manifeste devant un client.
Ce que cet article ne traite pas
Les avoirs et leur articulation avec la facture d'origine, qui posent leurs propres questions de numérotation et de rapprochement. La facturation électronique et la certification auprès d'une administration fiscale, qui ajoutent un état irréversible de plus. Le lettrage automatique et le rapprochement bancaire, que nous avons touchés au passage sans les traiter. Et les règles de numérotation propres à chaque juridiction, qui sont une question réglementaire avant d'être une question logicielle.
Ce qui reste vrai au-delà de la facture : un statut ajouté après coup ne se contente pas d'ajouter une valeur possible. Il invalide une hypothèse que tout le code avait faite sans jamais l'écrire, et le travail réel consiste à retrouver tous les endroits qui la faisaient.
Nous éditons Cleo ERP et nous accompagnons sa mise en place, reprise de données comprise — c'est là que les séquences et les hypothèses implicites se révèlent. Les revues de conception sur une application de gestion existante relèvent de notre conseil en architecture. Si vous n'en retenez qu'une chose : cherchez dans votre code les captures d'exception qui journalisent en avertissement et renvoient une valeur vide.