| errors |
| Failed to load YAML frontmatter: Tried to load unspecified class: Date |
|
Référence API Odoo — Documentation Complète
Base URL : https://sarl-le-relais-de-louest.odoo.com/jsonrpc
Protocole : JSON-RPC 2.0
Auth : common.login + API Key
1. Authentification
1.1 Obtenir un UID
POST /jsonrpc
Content-Type: application/json
{
"jsonrpc": "2.0",
"method": "call",
"params": {
"service": "common",
"method": "login",
"args": ["<db_name>", "<email>", "<api_key>"]
},
"id": 1
}
Réponse :
{"jsonrpc": "2.0", "id": 1, "result": 2}
result = UID (integer, généralement 2 pour l'admin).
1.2 API Key
Générée dans Odoo : Profil → Preferences → Account Security → API Keys → New API Key.
⚠️ Ne pas confondre avec un token de session navigateur (SHA visible dans l'URL Odoo).
1.3 Helper Python
import requests
DB = "sarl-le-relais-de-louest"
UID = None
PWD = None # API key
URL = f"https://{DB}.odoo.com/jsonrpc"
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
}
resp = requests.post(URL, json=payload, timeout=30).json()
if "error" in resp:
raise Exception(resp["error"]["data"]["message"])
return resp.get("result")
2. Méthodes génériques
Tous les modèles supportent ces 7 méthodes :
search_read — Rechercher et lire
records = odoo_call("model.name", "search_read",
[domain], # liste de conditions
{kwargs} # fields, offset, limit, order
)
Paramètres :
| Param | Type | Description |
|——-|——|————-|
| domain | list | Conditions de filtrage (voir §2.1) |
| fields | list | Champs à retourner |
| offset | int | Pagination : sauter N enregistrements |
| limit | int | Max résultats (défaut: 80) |
| order | str | Tri : "date_order desc" |
search — Rechercher (IDs seulement)
ids = odoo_call("model.name", "search", [domain])
# Retourne [1, 5, 42, ...]
search_count — Compter
count = odoo_call("model.name", "search_count", [domain])
# Retourne int
read — Lire par ID
data = odoo_call("model.name", "read", [[id1, id2]], {"fields": ["name", "email"]})
create — Créer
new_id = odoo_call("model.name", "create", [{champs}])
# ⚠️ UN seul dict, pas une liste de dicts
write — Modifier
odoo_call("model.name", "write", [[id1, id2], {champs}])
unlink — Supprimer
odoo_call("model.name", "unlink", [[id1, id2]])
2.1 Syntaxe des domaines
# Égalité
["field", "=", value]
# Inégalité
["field", "!=", value]
["field", ">", value]
["field", "<", value]
["field", ">=", value]
["field", "<=", value]
# LIKE / NOT LIKE
["field", "=like", "pattern%"] # % = wildcard
["field", "not like", "xyz"]
# IN / NOT IN
["field", "in", [val1, val2]]
["field", "not in", [val1, val2]]
# AND = liste de conditions
[["field1", "=", v1], ["field2", "=", v2]]
# field1 = v1 ET field2 = v2
# OR = conditions dans une sous-liste
[["field1", "=", v1], "|", ["field1", "=", v2]]
# field1 = v1 OU field1 = v2
2.2 Many2one / One2many / Many2many
# Many2one → integer
"partner_id": 42
# Many2many → commandes spéciales
"taxes_id": [(6, 0, [id1, id2])] # Remplacer par cette liste
"taxes_id": [(4, id3)] # Ajouter un élément
"taxes_id": [(3, id3)] # Supprimer un élément
"taxes_id": [(5, 0, 0)] # Vider tout
# One2many → commandes identiques
"order_line": [(0, 0, {vals})] # Créer et lier
"order_line": [(1, id, {vals})] # Modifier existant
"order_line": [(2, id)] # Supprimer existant
3. Modèles — Référence exhaustive
3.1 res.partner — Contacts / Clients
| Champ |
Type |
Obligatoire |
Description |
name |
char |
✅ |
Nom complet |
email |
char |
|
Email |
phone |
char |
|
Téléphone fixe |
mobile |
char |
|
Téléphone mobile |
street |
char |
|
Rue |
city |
char |
|
Ville |
zip |
char |
|
Code postal |
country_id |
m2o |
|
Pays |
customer_rank |
int |
|
1 = client, 0 = prospect |
supplier_rank |
int |
|
1 = fournisseur |
is_company |
bool |
|
True = société, False = particulier |
company_type |
selection |
|
person ou company
|
vat |
char |
|
N° TVA intracommunautaire |
barcode |
char |
|
Code-barres client |
active |
bool |
|
False = archivé |
comment |
text |
|
Notes internes |
category_id |
m2m |
|
Étiquettes (tags) |
Recherche par email :
partner = odoo_call("res.partner", "search_read",
[[["email", "=", "jean@email.com"]]],
{"fields": ["id", "name", "email"]})
3.2 sale.order — Commandes / Devis
| Champ |
Type |
Obligatoire |
Description |
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 |
✅ |
Société (généralement 1) |
date_order |
datetime |
✅ |
Date commande |
picking_policy |
selection |
✅ |
direct (livraison immédiate) ou one
|
state |
selection |
|
draft, sent, sale, done, cancel
|
client_order_ref |
char |
|
Référence externe (ex: BilletWeb) |
team_id |
m2o → crm.team |
|
Équipe de vente |
user_id |
m2o → res.users |
|
Vendeur |
partner_shipping_id |
m2o |
|
Adresse livraison |
warehouse_id |
m2o |
|
Entrepôt |
payment_term_id |
m2o |
|
Conditions de paiement |
pricelist_id |
m2o |
|
Liste de prix |
currency_id |
m2o |
|
Devise (automatique) |
note |
text |
|
Notes (visibles sur le PDF) |
amount_untaxed |
float |
|
Total HT (lecture seule) |
amount_tax |
float |
|
Total TVA (lecture seule) |
amount_total |
float |
|
Total TTC (lecture seule) |
invoice_status |
selection |
|
to invoice, invoiced, no
|
require_signature |
bool |
|
Signature requise |
require_payment |
bool |
|
Paiement requis avant confirmation |
order_line |
o2m |
|
Lignes de commande |
Création minimale :
so_id = odoo_call("sale.order", "create", [{
"name": "SO-REF-123",
"partner_id": partner_id,
"partner_invoice_id": partner_id,
"partner_shipping_id": partner_id,
"company_id": 1,
"date_order": "2026-05-29 10:00:00",
"picking_policy": "direct",
"state": "draft",
}])
3.3 sale.order.line — Lignes de commande
| Champ |
Type |
Description |
order_id |
m2o → sale.order |
Commande parente |
product_id |
m2o → product.product |
Produit |
product_uom_qty |
float |
Quantité |
product_uom |
m2o |
Unité de mesure |
price_unit |
float |
Prix unitaire |
discount |
float |
Remise (%) |
name |
char |
Description |
event_ticket_id |
m2o → event.event.ticket |
Billet lié |
registration_ids |
o2m → event.registration |
Inscriptions liées |
tax_id |
m2m → account.tax |
Taxes |
customer_lead |
float |
Délai de livraison (jours) |
state |
selection |
draft, sale, done, cancel
|
3.4 product.product — Produits
| Champ |
Type |
Obligatoire |
Description |
name |
char |
✅ |
Nom du produit |
type |
selection |
|
consu, service, product
|
list_price |
float |
|
Prix de vente TTC |
standard_price |
float |
|
Prix de revient |
barcode |
char |
|
Code-barres |
categ_id |
m2o → product.category |
|
Catégorie |
sale_ok |
bool |
|
Disponible à la vente |
purchase_ok |
bool |
|
Disponible à l'achat |
available_in_pos |
bool |
|
Visible dans le PDV |
active |
bool |
|
Actif |
taxes_id |
m2m → account.tax |
|
Taxes à la vente |
supplier_taxes_id |
m2m |
|
Taxes à l'achat |
description_sale |
text |
|
Description sur les devis |
default_code |
char |
|
Référence interne |
weight |
float |
|
Poids (kg) |
⚠️ detailed_type n'existe pas sur Odoo Online SaaS → utiliser type.
3.5 product.category — Catégories
| Champ |
Type |
Description |
name |
char ✅ |
Nom |
parent_id |
m2o |
Catégorie parente |
complete_name |
char |
Chemin complet (lecture seule) |
3.6 product.template — Modèles de produits
Mêmes champs que product.product pour l'essentiel. La différence :
-
product.template = produit générique
-
product.product = variante spécifique
3.7 event.event — Événements
| Champ |
Type |
Obligatoire |
Description |
name |
char |
✅ |
Nom de l'événement |
date_begin |
datetime |
|
Date début |
date_end |
datetime |
|
Date fin |
seats_max |
int |
|
Places maximum |
seats_limited |
bool |
|
Limiter les places |
seats_available |
int |
|
Places restantes (lecture seule) |
auto_confirm |
bool |
|
⚠️ Confirmation auto par email |
event_ticket_ids |
o2m |
|
Types de billets |
registration_ids |
o2m |
|
Inscriptions |
state |
selection |
|
draft, confirm, done, cancel
|
user_id |
m2o |
|
Responsable |
address_id |
m2o |
|
Lieu |
country_id |
m2o |
|
Pays |
3.8 event.event.ticket — Types de billets
| Champ |
Type |
Obligatoire |
Description |
name |
char |
✅ |
Nom du billet |
event_id |
m2o → event.event |
✅ |
Événement parent |
product_id |
m2o → product.product |
✅ |
Produit associé |
price |
float |
|
Prix |
seats_limited |
bool |
|
Nombre limité |
seats_max |
int |
|
Places max |
description |
text |
|
Description |
3.9 event.registration — Participants
| Champ |
Type |
Description |
event_id |
m2o → event.event |
Événement |
name |
char |
Nom du participant |
email |
char |
Email |
phone |
char |
Téléphone |
barcode |
char |
QR/Barcode (utilisé pour BilletWeb) |
state |
selection |
draft, open, done, cancel
|
event_ticket_id |
m2o → event.event.ticket |
Type de billet |
sale_order_id |
m2o → sale.order |
Commande liée |
sale_order_line_id |
m2o → sale.order.line |
Ligne de commande liée |
attendee_partner_id |
m2o → res.partner |
Contact participant |
date_closed |
datetime |
Date de clôture |
⚠️ description n'existe pas → utiliser barcode pour les références externes.
3.10 pos.order — Commandes PDV
| Champ |
Type |
Description |
name |
char |
Référence (ex: POS/2026/05/29/001) |
date_order |
datetime |
Date |
session_id |
m2o → pos.session |
Session PDV |
partner_id |
m2o → res.partner |
Client (si renseigné) |
amount_total |
float |
Total TTC |
amount_tax |
float |
Total TVA |
amount_paid |
float |
Montant payé |
amount_return |
float |
Rendu monnaie |
state |
selection |
draft, paid, done, invoiced, cancel
|
lines |
o2m → pos.order.line |
Lignes |
payment_ids |
o2m → pos.payment |
Paiements |
3.11 pos.order.line — Lignes PDV
| Champ |
Type |
Description |
order_id |
m2o → pos.order |
Commande parente |
product_id |
m2o → product.product |
Produit |
qty |
float |
Quantité |
price_unit |
float |
Prix unitaire |
discount |
float |
Remise |
price_subtotal |
float |
Sous-total |
tax_ids |
m2m → account.tax |
Taxes |
3.12 pos.session — Sessions PDV
| Champ |
Type |
Description |
name |
char |
Nom session |
config_id |
m2o → pos.config |
Configuration PDV |
start_at |
datetime |
Ouverture |
stop_at |
datetime |
Fermeture |
state |
selection |
opening_control, opened, closing_control, closed
|
cash_register_balance_start |
float |
Solde ouverture |
cash_register_balance_end |
float |
Solde fermeture |
order_ids |
o2m → pos.order |
Commandes |
3.13 pos.config — Configuration PDV
| Champ |
Type |
Description |
name |
char ✅ |
Nom ("Parc de la Luge - Caisse") |
iface_cashdrawer |
bool |
Tiroir caisse |
iface_print_via_proxy |
bool |
Impression ticket |
iface_scan_via_proxy |
bool |
Scanner |
module_pos_restaurant |
bool |
Mode restaurant |
company_id |
m2o |
Société |
3.14 account.move — Factures / Écritures comptables
| Champ |
Type |
Description |
name |
char |
Numéro de facture |
move_type |
selection |
out_invoice, out_refund, in_invoice, in_refund
|
partner_id |
m2o → res.partner |
Client/Fournisseur |
invoice_date |
date |
Date facture |
invoice_date_due |
date |
Date d'échéance |
state |
selection |
draft, posted, cancel
|
amount_untaxed |
float |
HT |
amount_tax |
float |
TVA |
amount_total |
float |
TTC |
amount_residual |
float |
Restant dû |
payment_state |
selection |
not_paid, in_payment, paid, partial, reversed
|
invoice_line_ids |
o2m |
Lignes de facture |
ref |
char |
Référence externe |
3.15 account.payment — Paiements
| Champ |
Type |
Description |
payment_type |
selection |
inbound (reçu) / outbound (envoyé) |
partner_type |
selection |
customer / supplier
|
partner_id |
m2o |
Client |
amount |
float |
Montant |
date |
date |
Date |
journal_id |
m2o → account.journal |
Journal (banque, caisse) |
payment_method_id |
m2o |
Mode de paiement |
state |
selection |
draft, posted, sent, reconciled, cancelled
|
3.16 account.tax — Taxes
| Champ |
Type |
Description |
name |
char ✅ |
Nom ("TVA 8.5% Réunion") |
amount |
float |
Taux (8.5, 2.1, 0) |
amount_type |
selection |
percent, fixed, division, group
|
type_tax_use |
selection |
sale, purchase, none
|
tax_group_id |
m2o |
Groupe |
active |
bool |
Actif |
price_include |
bool |
Prix TTC |
description |
char |
Texte sur la facture |
3.17 account.tax.group — Groupes de taxes
| Champ |
Type |
Description |
name |
char ✅ |
Nom |
country_id |
m2o |
Pays (⚠️ ne pas mettre si DOM !) |
3.18 stock.quant — Inventaire
| Champ |
Type |
Description |
product_id |
m2o |
Produit |
location_id |
m2o |
Emplacement |
quantity |
float |
Quantité |
inventory_quantity |
float |
Qté comptée |
inventory_diff_quantity |
float |
Écart |
3.19 ir.module.module — Modules
| Champ |
Type |
Description |
name |
char |
Nom technique |
display_name |
char |
Nom affiché |
state |
selection |
uninstalled, installed, to install, to upgrade
|
Installer un module :
mid = odoo_call("ir.module.module", "search_read",
[[["name", "=", "point_of_sale"]]], {"fields": ["id"]})[0]["id"]
odoo_call("ir.module.module", "button_immediate_install", [[mid]])
3.20 mail.mail — Emails en file d'attente
| Champ |
Type |
Description |
state |
selection |
outgoing, sent, exception, cancel
|
subject |
char |
Sujet |
email_to |
char |
Destinataires |
3.21 ir.mail_server — Serveur d'envoi
| Champ |
Type |
Description |
name |
char |
Nom |
smtp_host |
char |
Serveur SMTP |
active |
bool |
False pour désactiver |
3.22 ir.cron — Tâches planifiées
| Champ |
Type |
Description |
name |
char |
Nom |
model_id |
m2o |
Modèle |
active |
bool |
Actif |
interval_number |
int |
Fréquence |
interval_type |
selection |
minutes, hours, days, weeks, months
|
3.23 res.company — Société
| Champ |
Type |
Description |
name |
char |
Nom |
country_id |
m2o |
Pays (187 = Réunion) |
currency_id |
m2o |
Devise (EUR) |
phone |
char |
Téléphone |
email |
char |
Email |
street |
char |
Adresse |
4. Flux BilletWeb → Odoo
4.1 Ordre des opérations
① res.partner — find or create (par email)
② product.product — find (par barcode BW-*)
③ event.event.ticket — find or create (produit + event)
④ sale.order — create
⑤ sale.order.line — create (lié au ticket)
⑥ event.registration — create (avec barcode BilletWeb)
⑦ sale.order.line — write registration_ids ← lier
4.2 Anti-doublon
# 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")}
# Vérifier avant création
if barcode in done:
continue # déjà importé
# Réutiliser les commandes existantes
existing_so = odoo_call("sale.order", "search_read",
[[["client_order_ref", "=", ref]]], {"fields": ["id"]})
so_id = existing_so[0]["id"] if existing_so else None
4.3 Désactiver les emails (avant import massif)
# 1. Vider la file d'attente
stuck = odoo_call("mail.mail", "search",
[[["state", "in", ["outgoing", "exception"]]]])
for mid in stuck:
odoo_call("mail.mail", "unlink", [[mid]])
# 2. Désactiver serveur SMTP
servers = odoo_call("ir.mail_server", "search_read", [],
{"fields": ["id"]})
for s in servers:
odoo_call("ir.mail_server", "write", [[s["id"]], {"active": False}])
# 3. Désactiver fetchmail
for s in odoo_call("fetchmail.server", "search_read", [],
{"fields": ["id"]}):
odoo_call("fetchmail.server", "write", [[s["id"]], {"active": False}])
# 4. Désactiver templates d'email
for t in odoo_call("mail.template", "search_read", [],
{"fields": ["id"]}):
odoo_call("mail.template", "write", [[t["id"]], {"auto_delete": True}])
# 5. Désactiver cron jobs email
for c in odoo_call("ir.cron", "search_read", [],
{"fields": ["id", "name"]}):
if any(kw in c["name"].lower() for kw in
["mail", "email", "send", "notification"]):
odoo_call("ir.cron", "write", [[c["id"]], {"active": False}])
# 6. Désactiver confirmation auto événements
for ev_id in event_ids:
odoo_call("event.event", "write", [[ev_id],
{"auto_confirm": False}])
5. TVA Réunion (DOM)
5.1 Taux applicables
| Taux |
Vente (ID) |
Achat (ID) |
Usage |
| 8,5% |
79 |
32 |
Taux normal DOM |
| 2,1% |
80 |
35 |
Taux réduit |
| 0% |
61-70 |
50 |
Exonéré |
5.2 Créer une taxe DOM
# Groupe (sans country_id !)
grp = odoo_call("account.tax.group", "create",
[{"name": "TVA 8.5%"}])
# Taxe vente
odoo_call("account.tax", "create", [{
"name": "8.5% S (Réunion)",
"amount": 8.5,
"amount_type": "percent",
"type_tax_use": "sale",
"tax_group_id": grp,
"price_include": False,
}])
5.3 Désactiver les taux métropole
# Désactiver 20%
for t in odoo_call("account.tax", "search_read",
[[["amount", "=", 20.0]]], {"fields": ["id"]}):
odoo_call("account.tax", "write", [[t["id"]], {"active": False}])
# Idem pour 10%, 5.5%
5.4 Appliquer une taxe à un produit
odoo_call("product.product", "write",
[[product_id], {"taxes_id": [(6, 0, [tax_id])]}])
6. Modules de localisation française
| Module |
Rôle |
l10n_fr |
Base localisation France |
l10n_fr_account |
Plan comptable français + templates taxes |
l10n_fr_pos_cert |
Certification NF 525 (obligatoire) |
l10n_fr_reports |
Rapports comptables français |
# Vérifier les modules installés
modules = odoo_call("ir.module.module", "search_read",
[[["name", "in",
["l10n_fr", "l10n_fr_account",
"l10n_fr_pos_cert", "l10n_fr_reports"]]]],
{"fields": ["name", "state"]})
# Installer
for m in modules:
if m["state"] != "installed":
odoo_call("ir.module.module",
"button_immediate_install", [[m["id"]]])
⚠️ Le plan comptable (l10n_fr_account) doit être installé manuellement via l'interface web (Paramètres → Comptabilité → Localisation fiscale). L'API ne peut pas déclencher l'installation du plan comptable sur SaaS.
7. Pièges et erreurs courantes
| N° |
Erreur |
Solution |
| 1 |
AccessDenied |
Utiliser common.login (pas /web/session/authenticate) |
| 2 |
create retourne None
|
Il manque un champ obligatoire → fields_get
|
| 3 |
create prend une liste |
Non → UN dict. Passer [{...}] pas [[{...}]]
|
| 4 |
detailed_type introuvable |
Ne pas utiliser → type uniquement |
| 5 |
Taxe "country_id mismatch" |
Créer les groupes de taxe sans country_id
|
| 6 |
Taxes écrasées après l10n_fr_account
|
Ré-appliquer après installation |
| 7 |
auto_confirm envoie des emails |
Désactiver avant import massif |
| 8 |
unlink échoue |
Utiliser write → {"active": False}
|
| 9 |
search_read limit=80 par défaut |
Utiliser limit=0 pour tout récupérer |
| 10 |
barcode champ inexistant |
Vérifier le modèle — event.registration l'a, sale.order non |
| 11 |
BilletWeb 403 Forbidden |
Ajouter User-Agent: Mozilla/5.0
|
| 12 |
API key scope événement |
La clé doit être créée avec "Tous les événements" |
8. Limites Odoo Online (SaaS)
| ✅ Possible |
❌ Impossible |
| API JSON-RPC complète |
SSH / accès serveur |
| CRUD tous modèles standards |
Modules Python customs |
| Modules Studio (UI) |
Accès direct PostgreSQL |
| Imports CSV/XML |
Modification du core Odoo |
| Webhooks sortants |
Installation packages Python |
| Extensions Chrome (DOM) |
Scripts serveur personnalisés |
9. Exemples pratiques
9.1 Créer un client
partner_id = odoo_call("res.partner", "create", [{
"name": "Jean Dupont",
"email": "jean@email.com",
"phone": "+262692123456",
"customer_rank": 1,
}])
9.2 Créer une commande complète
# Étape 1 : créer la commande
so_id = odoo_call("sale.order", "create", [{
"name": "CMD-2026-001",
"partner_id": partner_id,
"partner_invoice_id": partner_id,
"partner_shipping_id": partner_id,
"company_id": 1,
"date_order": "2026-05-29 10:00:00",
"picking_policy": "direct",
"client_order_ref": "BW-ORDER-456",
"team_id": 4, # BilletWeb
}])
# Étape 2 : ajouter une ligne
line_id = odoo_call("sale.order.line", "create", [{
"order_id": so_id,
"product_id": 13,
"product_uom_qty": 2,
"price_unit": 11.00,
"name": "Luge - Forfait 4 tours",
}])
# Étape 3 : confirmer en commande
odoo_call("sale.order", "write", [[so_id], {"state": "sale"}])
9.3 Créer une inscription événement
reg_id = odoo_call("event.registration", "create", [{
"event_id": 1,
"name": "Jean Dupont",
"email": "jean@email.com",
"barcode": "1234567890",
"state": "open",
"event_ticket_id": 8,
"sale_order_id": so_id,
}])
9.4 Lier inscription → ligne de commande
odoo_call("sale.order.line", "write",
[[line_id], {"registration_ids": [(6, 0, [reg_id])]}])
9.5 Rechercher avec pagination
all_records = []
offset = 0
while True:
batch = odoo_call("event.registration", "search_read",
[[["event_id", "=", 1]]],
{"fields": ["id", "name", "barcode", "state"],
"offset": offset, "limit": 100})
if not batch:
break
all_records.extend(batch)
offset += 100