Version 2.0 du 02/09/2026 : testée de bout en bout (XML-RPC, JSON-RPC, et tir réel des 2 webhooks). Contact technique : direction NEOSEN.
sale.order), de leurs lignes (sale.order.line), du catalogue produits (product.template, product.product) et des devises (res.currency).Limites importantes (par conception, périmètre « ventes seules ») :
account.move) ne sont pas lisibles en API. Le webhook paiement vous notifie, mais vous ne pouvez pas re-lire la facture ; vous rebasculez sur la commande via le champ invoice_origin (voir §6).| Paramètre | Valeur |
| URL | `https://www.digimoov.fr` |
| Base de données | `marouane177-odoo-17-production-19137003` |
| Login | `goralab-api@digimoov.fr` |
| Mot de passe | la clé API (transmise séparément par canal sécurisé, jamais par email) |
Protocoles : XML-RPC (/xmlrpc/2/) ou JSON-RPC (/jsonrpc). Les deux sont testés et fonctionnels. La clé API s'utilise à la place du mot de passe.
import xmlrpc.client
URL = "https://www.digimoov.fr"
DB = "marouane177-odoo-17-production-19137003"
LOGIN = "goralab-api@digimoov.fr"
API_KEY = "..." # transmise séparément
common = xmlrpc.client.ServerProxy(f"{URL}/xmlrpc/2/common")
uid = common.authenticate(DB, LOGIN, API_KEY, {}) # entier stable, cacheable
models = xmlrpc.client.ServerProxy(f"{URL}/xmlrpc/2/object")
# Ventes confirmées depuis une date (curseur incrémental sur write_date)
orders = models.execute_kw(DB, uid, API_KEY, "sale.order", "search_read",
[[["state", "=", "sale"], ["write_date", ">=", "2026-09-01 00:00:00"]]],
{"fields": ["name", "date_order", "state", "amount_untaxed", "amount_total",
"currency_id", "partner_id", "company_id", "order_line", "write_date"],
"limit": 200, "order": "write_date asc"})
# Lignes d'une commande
lines = models.execute_kw(DB, uid, API_KEY, "sale.order.line", "read",
[orders[0]["order_line"]],
{"fields": ["product_id", "name", "product_uom_qty", "price_unit", "price_total"]})
En search_read/read, les champs relationnels reviennent en paire [id, libellé] :
partner_id = [88298, "MIMMIH Nabil"], currency_id = [1, "EUR"], company_id = [2, "DIGIMOOV"], order_line = [id1, id2, ...].
→ C'est ainsi que vous obtenez le nom du client (même si l'accès direct au partenaire est bloqué).
curl -s https://www.digimoov.fr/jsonrpc -H 'Content-Type: application/json' -d '{
"jsonrpc": "2.0", "method": "call",
"params": {"service": "object", "method": "execute_kw",
"args": ["marouane177-odoo-17-production-19137003", UID, "API_KEY",
"sale.order", "search_read",
[[["state", "=", "sale"]]],
{"fields": ["name", "date_order", "amount_total"], "limit": 10}]}}'
UID = l'entier renvoyé par common.authenticate (stable, cacheable).
sale.order : name, date_order, state (draft/sent/sale/cancel), amount_untaxed, amount_total, currency_id, partner_id, company_id, order_line, write_date.
sale.order.line : order_id, product_id, name, product_uom_qty, price_unit, price_subtotal, price_total.
AAAA-MM-JJ HH:MM:SS en UTC (pas d'heure locale : convertissez côté Goralab).currency_id). Ne supposez pas EUR partout : lisez toujours currency_id.company_id : 1 = MCM ACADEMY, 2 = DIGIMOOV.limit/offset et un curseur incrémental sur write_date (jamais un export complet répété).Deux notifications sortantes (POST JSON, Content-Type: application/json). Le type d'événement se lit sur le champ _model :
| Événement | `_model` | Déclencheur | Fréquence |
| Vente confirmée | `sale.order` | la commande passe à l'état `sale` | à chaque confirmation |
| Paiement reçu | `account.move` | la facture entre en `in_payment` ou `paid` | **une seule fois par facture** (pas de doublon) |
Payload réel (tel que reçu lors des tests) :
Vente :
{"_model": "sale.order", "_id": 54510, "_action": "GORALAB_vente_confirmee(#4727)",
"id": 54510, "name": "S54510", "state": "sale",
"date_order": "2026-09-02 16:21:08", "amount_untaxed": 100.0, "amount_total": 100.0,
"partner_id": 88347, "company_id": 2}
Paiement :
{"_model": "account.move", "_id": 23881, "_action": "GORALAB_paiement_recu(#4728)",
"id": 23881, "name": "FAD210425-02175", "payment_state": "paid",
"invoice_date": "2026-09-02", "invoice_origin": "S54510",
"amount_total": 100.0, "partner_id": 88347, "company_id": 2}
Points à respecter impérativement (sinon « ça marche pas ») :
1. Discriminez par _model (sale.order vs account.move). N'analysez pas _action (c'est un libellé humain, pas un type stable).
2. Dans le webhook, les champs relationnels sont des ID entiers NUS : partner_id: 88347, company_id: 2 : pas [id, nom] comme dans search_read. Vous n'avez pas le nom dans le payload.
3. Le webhook est une notification, pas la donnée complète. Modèle recommandé :
- Vente : sur réception, faites read/search_read sur sale.order id=_id → vous obtenez le nom du client et le détail des lignes.
- Paiement : account.move n'est pas lisible en API. Utilisez invoice_origin (ici "S54510") pour faire search_read sur sale.order où name = invoice_origin → vous récupérez client + montants.
4. Paiement, valeur de payment_state : peut valoir in_payment (paiement enregistré, en attente de rapprochement bancaire) ou paid (soldé). L'événement ne part qu'une fois ; traitez-le en upsert idempotent sur name (numéro de facture).
5. Sécurité : nous mettrons un token secret en query string (?token=...). Vérifiez-le et rejetez toute requête sans le bon token. (Odoo n'envoie pas de signature HMAC.)
6. Pas de retry : le webhook Odoo est « send and forget », timeout 1 s, aucune reprise si votre endpoint est indisponible. Prévoyez impérativement un poll de réconciliation (ex. toutes les 15 min : search_read sur write_date >= dernier_curseur) pour rattraper les événements manqués. Le temps réel est un confort, le poll est le filet de sécurité.
1. L'URL HTTPS de votre endpoint webhook (nous y ajoutons le token et activons les 2 automatisations, désactivées aujourd'hui).
2. Un contact technique pour un test d'activation en réel ensemble.
Ce document est directement exploitable par votre Claude Code : payloads réels, chemins de re-fetch et pièges connus y sont explicites.