API Odoo Online (SaaS) — Référence complète
Instance : Parc de la Luge — Odoo Online Standard Dernière mise à jour : 29/05/2026
1. Authentification
Endpoint
POST https://<subdomain>.odoo.com/jsonrpc
Login (JSON-RPC)
payload = {
"jsonrpc": "2.0", "method": "call",
"params": {
"service": "common", "method": "login",
"args": [DB, "login@email.com", "api_key_or_password"]
}, "id": 1
}
uid = requests.post(ODOO_URL, json=payload).json()["result"]
⚠️ ERREUR CLASSIQUE :
/web/session/authenticate→AccessDeniedsur SaaS. Toujours utilisercommon.loginsur/jsonrpc.
API Key
Générée dans Odoo : Profil → Preferences → Account Security → API Keys.
2. Helper générique
def odoo_call(model, method, *args):
payload = {
"jsonrpc": "2.0", "method": "call",
"params": {
"service": "object",
"method": "execute_kw",
"args": [DB, UID, PWD, model, method] + list(args)
}, "id": 1
}
r = requests.post(ODOO_URL, json=payload, timeout=30)
data = r.json()
if "error" in data:
msg = data['error']['data']['message'][:200]
raise Exception(f"Odoo error: {msg}")
return data.get("result")
3. Opérations CRUD
search_read — Rechercher + lire
records = odoo_call("res.partner", "search_read",
[[["email", "=", "x@y.com"]]], # domain (list of tuples)
{"fields": ["id", "name", "email"]}) # fields dict
search_count — Compter
count = odoo_call("sale.order", "search_count",
[[["state", "=", "sale"]]])
create — Créer (⚠️ un seul dict)
new_id = odoo_call("sale.order", "create",
[{"name": "ORDER-REF", "partner_id": 42, ...}]) # ONE dict, PAS une liste
write — Modifier
odoo_call("sale.order", "write",
[[42], {"state": "sale"}]) # [[id], {values}]
unlink — Supprimer
odoo_call("sale.order", "unlink", [[42]])
fields_get — Découvrir les champs
fields = odoo_call("sale.order", "fields_get", [],
{"attributes": ["string", "type", "required"]})
required = [(k, v) for k, v in fields.items() if v.get("required")]
4. Modèles clés
4.1 sale.order — Commande
| Champ | Type | Obligatoire | Notes |
|---|---|---|---|
name |
char | ✅ | Référence commande |
partner_id |
m2o → res.partner | ✅ | Client |
partner_invoice_id |
m2o → res.partner | ✅ | Adresse facturation |
partner_shipping_id |
m2o → res.partner | ✅ | Adresse livraison |
company_id |
m2o → res.company | ✅ | Généralement 1
|
date_order |
datetime | ✅ | Format "2026-01-01 00:00:00"
|
picking_policy |
selection | ✅ | "direct" |
client_order_ref |
char | Référence externe (ex: BilletWeb) | |
state |
selection |
draft → sent → sale → done → cancel
|
Création minimale :
odoo_call("sale.order", "create", [{
"name": "ORDER-REF",
"partner_id": partner_id,
"partner_invoice_id": partner_id,
"partner_shipping_id": partner_id,
"company_id": 1,
"date_order": "2026-01-01 00:00:00",
"picking_policy": "direct",
}])
4.2 sale.order.line — Ligne de commande
| Champ | Type | Notes |
|---|---|---|
order_id |
m2o → sale.order | ID commande |
product_id |
m2o → product.product | ID produit |
product_uom_qty |
float | Quantité (défaut: 1) |
price_unit |
float | Prix unitaire |
registration_ids |
o2m → event.registration | Lie les inscriptions |
4.3 res.partner — Client
| Champ | Type | Notes |
|---|---|---|
name |
char | Nom complet |
email |
char | Email (unique par client) |
phone |
char | Téléphone |
barcode |
char | Code barre |
4.4 product.product — Produit
| Champ | Type | Notes |
|---|---|---|
name |
char | Nom |
barcode |
char | Code barre (utilisé pour mapper BilletWeb) |
list_price |
float | Prix de vente |
active |
bool |
False pour désactiver sans supprimer |
4.5 event.registration — Inscription
| Champ | Type | Notes |
|---|---|---|
event_id |
m2o → event.event | Événement |
name |
char | Nom participant |
email |
char | |
barcode |
char | Code barre BilletWeb |
state |
selection |
draft → open → done → cancel
|
event_ticket_id |
m2o → event.event.ticket | Type de billet |
Création :
reg_id = odoo_call("event.registration", "create", [{
"event_id": event_id,
"name": "Jean Dupont",
"email": "jean@email.com",
"barcode": "1234567890",
"state": "open",
"event_ticket_id": ticket_id,
}])
Lier à une ligne de commande :
odoo_call("sale.order.line", "write",
[[line_id], {"registration_ids": [[6, 0, [reg_id]]]}])
4.6 event.event.ticket — Type de billet
odoo_call("event.event.ticket", "create", [{
"name": "Luge - Forfait 4 tours",
"event_id": event_id,
"product_id": product_id,
"price": 11.0,
"seats_limited": False,
}])
4.7 pos.order — Commande PDV
| Champ | Type | Notes |
|---|---|---|
name |
char | Référence session |
date_order |
datetime | Date commande |
amount_total |
float | Total TTC |
amount_tax |
float | Total TVA |
state |
selection |
draft → paid → done
|
lines |
o2m → pos.order.line | Lignes |
5. Flux BilletWeb → Odoo
Ordre correct pour importer un participant :
1. res.partner — find by email, or create
2. product.product — find by barcode (BW-*)
3. event.event.ticket — find by product_id
4. sale.order — create with partner_id
5. sale.order.line — create with order_id + product_id
6. event.registration — create with event_id + barcode + event_ticket_id
7. sale.order.line — write registration_ids to link
6. TVA — Réunion (DOM)
| Taux | Usage |
|---|---|
| 8,5% | Standard DOM |
| 2,1% | Réduit |
| 0% | Exonéré |
odoo_call("account.tax", "create", [{
"name": "TVA 8.5% (Réunion)",
"amount": 8.5,
"amount_type": "percent",
"type_tax_use": "sale",
"tax_group_id": group_id,
"price_include": False,
}])
⚠️
l10n_fr_accountinstalle les taux métropole (20%, 10%, 5.5%). Désactiver avecwrite([[id], {"active": False}]).
7. Modules de localisation française
| Module | Rôle |
|---|---|
l10n_fr |
Localisation France |
l10n_fr_account |
Plan comptable + taxes |
l10n_fr_pos_cert |
Certification NF 525 (obligatoire en France) |
l10n_fr_reports |
Rapports comptables |
Installation via API :
mid = odoo_call("ir.module.module", "search_read",
[[["name", "=", "l10n_fr"]]], {"fields": ["id"]})[0]["id"]
odoo_call("ir.module.module", "button_immediate_install", [[mid]])
8. Pièges et bonnes pratiques
Emails (limite 5/jour sur SaaS)
# Désactiver TOUS les emails après import
odoo_call("ir.mail_server", "write", [all_ids], {"active": False})
odoo_call("fetchmail.server", "write", [all_ids], {"active": False})
odoo_call("mail.template", "write", [all_ids], {"auto_delete": True})
odoo_call("ir.cron", "write", [ids], {"active": False}) # jobs mail/email/send
odoo_call("event.event", "write", [[1,2]], {"auto_confirm": False})
Produits de démo
Ne pas unlink (référencés par les moyens de paiement PDV). Utiliser :
odoo_call("product.product", "write", [[pid], {"active": False}])
Groupes de taxes — country_id
Ne PAS mettre country_id sauf s'il correspond au pays de la société :
# ✅ OK
odoo_call("account.tax.group", "create", [{"name": "TVA 8.5%"}])
# ❌ Erreur si société = Réunion (187)
odoo_call("account.tax.group", "create", [{"name": "TVA", "country_id": 75}])
create() — un seul dict
L'API JSON-RPC Odoo Online attend un dict pour create, pas une liste de dicts :
# ✅ OK
odoo_call("sale.order", "create", [{"name": "X", ...}])
# ❌ Erreur silencieuse
odoo_call("sale.order", "create", [[{"name": "X", ...}]])
Anti-doublon
Trois niveaux de protection :
# 1. Charger tous les barcodes existants au démarrage
existing = odoo_call("event.registration", "search_read", [[]],
{"fields": ["barcode"]})
done = {r["barcode"] for r in existing if r.get("barcode")}
# 2. Vérifier le barcode avant création
if barcode in done:
continue
# 3. Réutiliser les commandes existantes (ne pas bloquer)
existing_so = odoo_call("sale.order", "search_read",
[[["client_order_ref", "=", ref]]], {"fields": ["id"]})
if existing_so:
so = existing_so[0]["id"] # RÉUTILISER
else:
so = odoo_call("sale.order", "create", [{...}])
9. Limites Odoo Online (SaaS)
| Possible | Impossible |
|---|---|
API JSON-RPC (/jsonrpc) |
Accès SSH au serveur |
| CRUD sur tous les modèles standards | Modules customs (pas de __manifest__.py) |
| Modules Studio (custom fields, vues) | Accès direct PostgreSQL |
| Imports CSV/XML | Modifications du core Odoo |
| Webhooks sortants (Automatisations) | Installation de packages Python |
10. Événements Odoo du Parc de la Luge
| ID | Nom |
|---|---|
| 1 | Parc de la Luge — Billetterie |
| 2 | BON CADEAU |