Toute entreprise suisse reçoit un fichier ISO 20022 de sa banque. Depuis que la QR-facture a remplacé le bulletin de versement orange en octobre 2022, l’avis d’encaissement qui arrivait en fichier V11 arrive en camt.054, et le relevé de fin de journée en camt.053.
Les deux sont du XML, les deux sont spécifiés sur plusieurs centaines de pages, et les deux portent un piège de structure qui casse silencieusement le rapprochement.
Le voici en une phrase :
Un avis camt.054 ne contient pas quarante paiements. Il contient une écriture qui contient quarante paiements.
Votre banque crédite votre compte une fois, du total de la journée. À l’intérieur de cette unique balise <Ntry> se trouvent quarante <TxDtls>, chacune avec son montant, son payeur et sa référence QR. Un parseur qui lit le montant de l’écriture produit un mouvement de CHF 12 450.00 qui ne correspond à aucune facture de votre comptabilité — et quarante factures qui restent ouvertes.
Voici l’analyseur ISO 20022 d’AltiusOne, la plateforme fiduciaire et comptable que nous exploitons pour des mandats suisses. Quatre cents lignes, dont la plupart n’existent qu’à cause de cette phrase.
Deux formats, un parseur, et pourquoi ils vont ensemble
Les deux documents répondent à des questions différentes — quel est mon solde et qui m’a payé — et partagent la structure Acct / Ntry sous la racine. Un seul parseur, une détection automatique, et les différences traitées là où elles existent vraiment :
Un commentaire consigne la seule asymétrie qui compte :
Un avis ne porte pas de soldes. Un camt.054 dit ce qui est arrivé, pas ce que vous avez. Du code qui lit Bal sans condition obtient un solde d’ouverture et de clôture à zéro et, si quelque chose les compare, signale un écart pour chaque avis jamais importé.
La version est dans l’espace de noms
ISO 20022 place la version du message dans l’URI de l’espace de noms. Un fichier conforme à camt.053.001.04 et un fichier conforme au .08 sont, pour un analyseur XML, des documents écrits dans deux vocabulaires sans rapport. Toute expression XPath liée à l’un ne renvoie rien contre l’autre.
C’est la panne qui tombe sans prévenir, parce que ce n’est pas vous qui montez de version — c’est votre banque. Un matin le fichier arrive en version 08, tous les find() renvoient None, et l’import annonce « élément Stmt introuvable », ce qui se lit exactement comme un fichier corrompu. On dit au client que sa banque a envoyé quelque chose d’invalide. Ce n’est pas le cas.
Sept versions de chaque format, engendrées plutôt que saisies : ajouter la version 09 est un chiffre. Et la détection est faite proprement :
Deux stratégies, parce que les banques ne s’accordent pas sur l’endroit où déclarer l’espace de noms. La plupart le posent sur la racine <Document> ; certaines le déclarent sur l’enfant. Le repli cherche l’élément enfant connu dans chaque espace de noms candidat — plus lent, et atteint seulement quand la voie rapide échoue.
Et un espace de noms non reconnu rend une erreur nommée, pas une exception :
L’erreur voyage dans l’objet de résultat, donc l’écran d’import peut dire ce fichier n’est pas un camt que nous connaissons — ce sur quoi on peut agir — plutôt que d’afficher une trace d’exécution pour un fichier que le comptable a téléchargé de son e-banking cinq minutes plus tôt.
Éclater le lot
C’est le cœur.
_parse_entries renvoie une liste. C’est la décision de conception, prise dans la signature : une <Ntry> correspond à zéro, un ou plusieurs mouvements, et tout appelant doit le savoir. Une fonction qui renverrait une seule écriture devrait être redécouverte comme fausse plus tard, quand tous les appelants supposent déjà un pour un.
Trois cas, et le troisième est ce qui rend l’ensemble sûr.
Zéro ou une TxDtls — un paiement ordinaire, une transaction par carte, un ordre permanent. Le montant de niveau écriture est le montant de la transaction. Les détails sont fusionnés s’ils existent.
Plusieurs TxDtls — un lot. Chaque transaction devient son propre mouvement, avec son montant, sa devise, son sens et sa référence. Quarante paiements QR donnent quarante lignes, chacune rapprochable d’une facture. C’est tout l’objet du fichier.
Un lot dont les transactions n’ont pas de montant exploitable — repli sur l’agrégat, et le journal le dit.
Cette troisième branche mérite qu’on s’y arrête :
La réparation tentante consiste à diviser le total de l’écriture par le nombre de transactions, ou à attribuer le reliquat à la dernière. Les deux produisent un fichier qui s’importe proprement et une comptabilité fausse — et faux en comptabilité n’est pas un défaut d’affichage, c’est un chiffre que quelqu’un déposera auprès d’une administration fiscale.
Le parseur dégrade donc vers quelque chose de vrai mais moins utile : un mouvement agrégé que le comptable devra éclater à la main. Il le remarquera, parce qu’un mouvement de CHF 12 450.00 ne correspond à aucune facture, et le remarquer est le bon résultat. La ligne de journal porte la référence bancaire pour retrouver le fichier.
Quand un parseur ne peut pas déterminer un montant, il doit produire une vérité plus grossière, jamais une fabrication plausible. Tout ce qui rapproche silencieusement et faussement est pire qu’un import que le comptable doit terminer à la main.
Notez replace(base, ...) de dataclasses : chaque transaction part d’une copie des champs de niveau écriture — date de comptabilisation, date de valeur, référence bancaire, que le lot partage — et ne surcharge que ce qui est propre à la transaction. Muter base dans la boucle donnerait à toutes les écritures les valeurs de la dernière transaction, ce qui est la version classique de ce défaut et passe n’importe quel test contenant une seule transaction.
sens_tx n’est appliqué que s’il est présent. Un lot peut porter des crédits et des débits : le sens est donc pris par transaction là où le fichier le dit, et hérité de l’écriture là où il ne le dit pas.
L’argent est un Decimal, et la devise est dans le fichier
Decimal(amt_elem.text) — construit depuis la chaîne, jamais en passant par float. float("1234.55") ne vaut pas 1234.55, et une erreur d’arrondi dans un rapprochement bancaire est un écart sur lequel quelqu’un passe un après-midi.
InvalidOperation renvoie None pour l’écriture, ce que _parse_entries transforme en liste vide. Un montant illisible fait tomber le mouvement plutôt que d’importer un zéro — et un mouvement à zéro franc dans un import bancaire est exactement le genre de chose qu’on rapproche d’une différence d’arrondi, ce qui masque le vrai problème.
Et la devise est lue dans le fichier, depuis l’attribut Ccy de Amt. Pas supposée CHF. Une société suisse détient couramment des comptes EUR et USD, reçoit un camt pour chacun, et les importe dans le même système. Le warning sur un Ccy absent est au bon niveau : le fichier est inhabituel, le mouvement reste utilisable, et quelqu’un devrait regarder.
Les soldes reçoivent le traitement du signe :
ISO 20022 n’emploie jamais de nombre négatif. Tout montant est positif, et un élément frère CdtDbtInd dit dans quel sens il va. Un parseur qui lit les montants sans lire l’indicateur obtient un relevé où chaque débit ressemble à un crédit — et les totaux s’équilibrent quand même, ce qui explique qu’il survive à la relecture.
OPBD et CLBD — soldes comptabilisés d’ouverture et de clôture — sont deux des plusieurs types de solde présents dans le même fichier. PRCD, ITBD, FWAV et d’autres peuvent aussi s’y trouver. Sélectionner par code plutôt que par position fait la différence entre un solde de clôture correct et le <Bal> qui se trouvait en dernier.
Deux lignes de défense sur un fichier téléversé
Le fichier arrive par téléversement, d’une personne, à travers un navigateur. L’analyser avec l’ElementTree de la bibliothèque standard offre à qui saura convaincre un comptable d’importer un fichier à la fois XXE — lire n’importe quel fichier accessible au processus, émettre des requêtes depuis l’intérieur de votre réseau — et billion laughs — quelques centaines d’octets qui se déploient en gigaoctets et tuent le processus.
La justification du refus pur et simple est écrite, et c’est la bonne : aucun camt légitime n’en contient. Ce n’est pas une heuristique. Les schémas ISO 20022 n’emploient pas de déclarations d’entités, donc les refuser ne rejette rien de réel.
Et une seconde borne, de nature différente :
defusedxml arrête l’expansion d’entités ; il n’arrête pas un fichier véritablement énorme mais parfaitement valide. Dix mille transactions, c’est bien au-dessus d’un mois de relevés pour une PME et bien en dessous de ce qui épuiserait la mémoire. Et le contrôle est à l’intérieur de la boucle : l’analyse s’arrête quand la borne est franchie, au lieu de construire toute la liste avant de la mesurer.
La référence au Power of Ten n’est pas décorative : la règle 2 est toute boucle doit avoir une borne supérieure fixe, et une boucle d’import sur un document téléversé est exactement le cas pour lequel elle a été écrite.
Pourquoi cela compte plus en Suisse qu’ailleurs
La QR-facture a rendu ce problème de structure universel ici.
Sous l’ancien système BVR/ESR, une entreprise recevait un fichier V11 — une liste plate, à largeur fixe, une ligne par paiement, chacune avec son numéro de référence. L’analyser était fastidieux et la forme était évidente : des lignes entrent, des paiements sortent.
Le camt.054 qui l’a remplacé est plus riche et hiérarchique, et c’est dans la hiérarchie que loge la faute. La banque groupe toujours les encaissements du jour en une écriture, parce que c’est ce qui apparaît sur votre compte. Le détail est un niveau plus bas. Un logiciel écrit contre un seul fichier d’essai — un paiement, une écriture, une transaction — fonctionne parfaitement et échoue le premier jour où deux clients paient à la même date.
D’où le mode de défaillance à retenir pour quiconque intègre un format financier hiérarchique :
Testez avec un lot. Le cas à un élément et le cas à plusieurs sont deux chemins distincts dans le format, et le cas à un élément est précisément ce que votre banque vous envoie quand vous demandez un échantillon.
Pour un fiduciaire, l’écart se mesure en heures par client et par mois : soit les références QR arrivent attachées à leurs montants et les factures se soldent seules, soit quelqu’un ouvre le détail de l’e-banking et saisit.
Ce qu’il faut en retenir
- Une
<Ntry>n’est pas un paiement. Éclatez parTxDtls, et concevez la fonction d’analyse pour rendre une liste dès le départ. - La version ISO 20022 vit dans l’espace de noms. Supportez une plage, engendrez-la, et détectez à la racine comme sur l’enfant — votre banque montera de version sans vous prévenir.
- N’inventez jamais un montant. Repliez-vous sur une vérité plus grossière et journalisez ; un import qui rapproche faussement est pire qu’un import que le comptable doit terminer.
- Copiez les champs partagés, surchargez ceux de la transaction. Muter la base dans la boucle passe tous les tests à une seule transaction.
Decimaldepuis la chaîne, devise depuis l’attributCcy, signe depuisCdtDbtInd. Les montants ISO 20022 sont toujours positifs.- Sélectionnez les soldes par code (
OPBD,CLBD), jamais par position. defusedxmlplus une borne dure de transactions. L’un arrête les attaques par entités, l’autre un gros fichier valide, et il faut les deux.- Testez avec un fichier de lot, parce que l’échantillon que votre banque vous donne contient une seule transaction.