Refaire un import de commandes Business Central dans Odoo 19 : ce qui se transpose
Par Fred Morin ·
Aussi en anglais : Rebuilding a Business Central order import in Odoo 19: what carries over
J'ai bâti pour un distributeur québécois un import de commandes dans Business Central : un traducteur par format, une extension AL qui crée les commandes, et beaucoup de temps passé sur les échecs. Pour voir ce qui se transpose, je l'ai refait dans Odoo 19 Community, sur une entreprise fictive (« Meubles Laurentides »), avec trois formats fabriqués exprès :
- un CSV français d'un portail de détaillant (
;, virgule décimale, dates jj/mm/aaaa, UTF-8 ou Windows-1252) ; - un Excel de distributeur, avec des lignes de titre au-dessus du tableau et des codes UPC qu'Excel transforme en nombres ;
- un CSV de place de marché, une ligne par commande, les articles tassés dans une cellule (
SKU:qté|SKU:qté), sans prix.
Je travaille dans Business Central tous les jours ; Odoo, je l'ai appris pour ce projet. Voici la correspondance, et les pièges.
La correspondance
| Business Central | Odoo 19 |
|---|---|
| Extension AL | Module Python (fm_sale_order_import) |
| Traducteurs dans une Azure Function (C#) | Traducteurs en Python pur dans le module, sans import d'Odoo |
| Tables d'attente (staging) | Modèle fm.import.order, la commande lue gardée dans un champ Json |
| Table de configuration par source | Modèle fm.import.source : format, client, règles |
External Document No. |
client_order_ref |
| Zone de taxe selon la province | Position fiscale appliquée seule (localisation l10n_ca) |
| Supprimer la commande à moitié créée | cr.savepoint() autour de chaque commande |
| Page API sur mesure | Méthode publique appelée par l'API JSON-2 |
| Application de test dans un sandbox | Tests TransactionCase lancés avec --test-tags |
fr-CA.xlf |
i18n/fr.po |
Les traducteurs restent à part, comme dans la version Azure : lib/parsers.py ne connaît pas Odoo, il prend des octets et rend une liste de commandes au même format. Une erreur sur tout le fichier lève une exception ; une erreur sur une ligne reste sur cette ligne, et ce sont les règles de la source qui décident.

Les échecs
Un savepoint par commande. C'est l'équivalent le plus propre de « rien de créé à moitié ». Chaque commande est créée dans with self.env.cr.savepoint():. Si une règle la rejette après coup (une ligne refusée, la confirmation qui échoue), tout ce qu'elle a écrit est défait, y compris l'adresse de livraison qu'elle venait de créer. Les autres commandes du fichier passent. En AL, j'avais supprimé l'en-tête à la main ; ici, la base de données le fait.
Les doublons. Odoo ne fait qu'avertir quand deux commandes ont la même référence client. L'import cherche donc client_order_ref chez le partenaire commercial (et ses adresses), en ignorant les commandes annulées, et rejette la commande si elle existe. Un fichier déposé deux fois ne crée rien en double, et une commande annulée peut être réimportée.
Réessayer. Une commande rejetée garde la commande lue (payload). Après avoir ajouté l'article manquant, le service à la clientèle clique sur Réessayer : la même logique repasse, et le nombre d'essais monte.
Les taxes : rien à coder
Avec l10n_ca, Odoo crée une position fiscale par province, appliquée selon l'adresse de livraison. L'import n'a qu'à trouver ou créer l'adresse de livraison sous le client, avec la bonne province : une commande livrée au Québec reçoit TPS et TVQ (14,975 %), une commande livrée en Ontario la TVH de 13 %. Une province inconnue dans le fichier fait rejeter la commande plutôt que de la taxer au hasard.

L'API : JSON-2, pas XML-RPC
C'est le piège le plus important pour qui connaît les vieux exemples. Dans Odoo 19, XML-RPC et JSON-RPC sont dépréciés, et prévus pour disparaître dans la version 22. La nouvelle API, JSON-2, prend une clé d'API dans un en-tête Authorization: Bearer :
POST /json/2/fm.import.source/import_file
{"ids": [3], "file_name": "...", "content_base64": "...",
"context": {"lang": "fr_CA"}}
Deux surprises :
- JSON-2 remplace tout le contexte. Sans
lang, les messages de rejet reviennent en anglais, même pour un utilisateur configuré en français. - Les droits. L'utilisateur de l'import est un simple vendeur, mais il crée des adresses de livraison : il lui faut aussi le droit « Création de contacts ».
La traduction
Les messages sont en français du Québec dans fr.po (Odoo s'en sert pour fr_CA). Un piège : les traducteurs en Python pur marquent leurs messages avec un _() local, et Odoo les traduit à l'affichage. Mais self.env._(line["error"]) a été extrait tel quel comme chaîne « error ». Passer par une variable règle l'extraction.
Les tests
24 tests : les traducteurs seuls (fichiers fabriqués dans les tests, Excel compris), les règles d'import (doublons, rejet ou ligne écartée, prix manquant, province inconnue, échec de confirmation suivi d'un nouvel essai), les messages en français, et un vrai appel JSON-2 avec une clé d'API. Ils roulent dans un conteneur Docker :
odoo -d demo -u fm_sale_order_import --test-tags /fm_sale_order_import --stop-after-init
Ce qu'il faut retenir
Le travail difficile, c'est de décider la clé de doublon, la règle pour un article inconnu, la source du prix et l'endroit où les rejets apparaissent. Ces décisions passent d'un ERP à l'autre sans changer. Ce qui change, c'est le savoir propre à chaque plateforme : ici le savepoint, les positions fiscales et l'API JSON-2.
Un import de commandes à bâtir, dans Business Central ou dans Odoo? Parlons-en.