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/authenticateAccessDenied sur SaaS. Toujours utiliser common.login sur /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   draftsentsaledonecancel

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 Email
barcode char Code barre BilletWeb
state selection draftopendonecancel
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 draftpaiddone
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_account installe les taux métropole (20%, 10%, 5.5%). Désactiver avec write([[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