/helpdesk/status/<ref> sans authentification, rendue avec le
website.layout natif. Le client colle sa référence, voit le statut
de son ticket, la date d'ouverture, la catégorie, la résolution.Ce que tu vas apprendre
http.Controller
La classe qui expose du code Python à HTTP.
@http.route
Le décorateur qui mappe URL → méthode.
type='http' vs 'json'
Page HTML rendue ou endpoint JSON-RPC.
Auth & sécurité
public, user, bearer,
csrf=False pour API externes.
1. Le squelette d'un controller
Un controller vit dans controllers/ à la racine du module et s'importe
depuis le __init__.py :
# odooskills_helpdesk/__init__.py
from . import models
from . import controllers
# odooskills_helpdesk/controllers/__init__.py
from . import main
# odooskills_helpdesk/controllers/main.py
from odoo import http
from odoo.http import request
class HelpdeskController(http.Controller):
@http.route('/ma/route', type='http', auth='public')
def ma_methode(self, **kwargs):
return "Hello, world !"
Trois éléments-clés :
class HelpdeskController(http.Controller)— une classe pour regrouper toutes tes routes. Pas de_nameni d'ORM.@http.route('/ma/route', ...)— le décorateur qui déclare l'URL, le type, l'authentification.request— l'objet global qui donne accès àrequest.env,request.params,request.httprequest,request.session.
2. type='http' vs type='json'
type='http' — page rendue
@http.route('/helpdesk/status/<path:ref>', type='http', auth='public', website=True)
def helpdesk_status_page(self, ref, **kwargs):
"""Page publique de tracking d'un ticket."""
ticket = request.env['helpdesk.ticket'].sudo().search(
[('reference', '=', ref)], limit=1,
)
values = {'ref': ref, 'ticket': ticket}
return request.render('odooskills_helpdesk.ticket_status_page', values)
Points clés :
<path:ref>— converter qui accepte plusieurs segments séparés par/. Obligatoire ici : les références sont de la formeHLP/2026/00006.website=True— rend la route compatible avec le framework website (menu public, thème, multi-website). Sans ça, pas d'accès àwebsite.layout..sudo()sur la recherche — auth=public = utilisateur anonyme sans ACL surhelpdesk.ticket. On bypasse pour afficher le ticket, mais on limite les champs exposés côté template.request.render(template_xmlid, values)— rend un template QWeb avec le dict values comme contexte.
type='json' — API REST
@http.route('/api/v1/tickets', type='json', auth='user', methods=['POST'])
def api_tickets(self, **kwargs):
"""Lister ou créer un ticket."""
action = kwargs.get('action', 'list')
Ticket = request.env['helpdesk.ticket']
if action == 'list':
domain = kwargs.get('domain') or []
limit = min(int(kwargs.get('limit', 20)), 100)
tickets = Ticket.search(domain, limit=limit, order='create_date desc')
return {
'count': len(tickets),
'tickets': [{
'id': t.id,
'reference': t.reference,
'name': t.name,
'state': t.state,
'partner': t.partner_id.name,
} for t in tickets],
}
if action == 'create':
ticket = Ticket.create({
'name': kwargs['name'],
'partner_id': int(kwargs['partner_id']),
'channel': kwargs.get('channel', 'email'),
})
return {'id': ticket.id, 'reference': ticket.reference}
return {'error': f"unknown action '{action}'"}
Trois différences structurelles :
- Pas d'
ensure_oneou de template — on retourne undict, Odoo le sérialise en JSON. methods=['POST']— conventions REST : list et create en POST dans l'enveloppe JSON-RPC Odoo, pas de GET pour modifier.- Les paramètres arrivent dans
kwargs— Odoo parse automatiquement le body JSON et le dispatche en arguments nommés.
3. Les modes d'authentification
auth= | Qui peut appeler ? | Cas d'usage |
|---|---|---|
public |
Tout le monde, y compris les anonymes. request.env pointe
vers l'utilisateur public. |
Pages publiques website, page de tracking, webhook entrant. |
user |
Utilisateur connecté uniquement. 403 sinon. | Endpoints backend réservés aux collaborateurs (ex : portail client connecté, API interne). |
none |
Aucune auth, pas de session Odoo initialisée, pas d'env ORM. | Health check, redirections, endpoints purement techniques. |
bearer |
Token API passé dans Authorization: Bearer <token>. |
APIs tierces sans session cookie. Tokens gérés via Paramètres > Utilisateurs > API Keys. |
bearer simplifie radicalement
l'authentification d'APIs externes. L'admin crée un token côté
Paramètres > Utilisateurs > API Keys, le client envoie ce token en header,
Odoo connecte la session avec les droits de l'utilisateur propriétaire du token. Plus
besoin de hacker la session_id ou de stocker login/password.
4. CSRF — le piège des formulaires publics POST
Odoo active par défaut une protection CSRF sur les routes
type='http' en POST. Concrètement, tout formulaire qui appelle ta route
doit contenir un champ caché csrf_token.
<form action="/ma/route/post" method="POST">
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
<input type="text" name="nom"/>
<button type="submit">Envoyer</button>
</form>
Sans ça, Odoo renvoie 403 Forbidden — Session expired.
Désactiver CSRF pour une API externe
Un webhook extérieur (Slack, Stripe, GitHub) ne peut PAS envoyer le CSRF token. On le désactive explicitement :
@http.route('/webhook/stripe', type='http', auth='none', methods=['POST'], csrf=False)
def stripe_webhook(self, **post):
# Vérifier la signature Stripe manuellement ici
signature = request.httprequest.headers.get('Stripe-Signature')
# ...
return 'ok'
csrf=False retire une couche de défense.
Compense-la : vérifier une signature (webhook tiers), limiter par IP
(request.httprequest.remote_addr), ou imposer un header secret custom.
Les routes type='json' ont csrf=False par défaut — le format
JSON-RPC inclut l'ID de requête comme protection alternative.
5. Tester l'API en ligne de commande
Deux étapes : authentification (obtenir la session cookie) puis appel de l'endpoint.
# 1. Authentification — récupère un cookie de session
curl -c cookies.txt -H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"params": {
"db": "vs19_odooskills_test",
"login": "iastagenie@gmail.com",
"password": "St@odoodev"
}
}' \
http://localhost:9019/web/session/authenticate
# 2. Appel de l'API — envoie le cookie
curl -b cookies.txt -H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"params": {"action": "list", "limit": 3}
}' \
http://localhost:9019/api/v1/tickets
Réponse typique :
{
"jsonrpc": "2.0",
"id": null,
"result": {
"count": 3,
"tickets": [
{
"id": 9,
"reference": "HLP/2026/00009",
"name": "Test deadline tomorrow",
"state": "new",
"priority": "1",
"partner": "Cabinet Medina",
"create_date": "2026-04-13T05:44:35"
},
{
"id": 8,
"reference": "HLP/2026/00008",
"name": "VIP auto priority",
"state": "new",
"priority": "2",
"partner": "InfoSphere SARL",
"create_date": "2026-04-13T05:44:35"
}
]
}
}
"result" qui enveloppe ta
donnée. Odoo suit le protocole JSON-RPC 2.0 : les erreurs sérialisées reviennent sous
"error" au même niveau, avec code, message,
data.debug (traceback en dev).
Créer un ticket via l'API
curl -b cookies.txt -H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"params": {
"action": "create",
"name": "Bug signé via API",
"partner_id": 8,
"channel": "email",
"description": "Posté depuis un script externe"
}
}' \
http://localhost:9019/api/v1/tickets
# -> {"result": {"id": 10, "reference": "HLP/2026/00010", "state": "new"}}
6. Page publique — le template QWeb côté website
Côté vue, on crée un template qui enveloppe le contenu dans website.layout :
<template id="ticket_status_page" name="Helpdesk — Suivi de ticket">
<t t-call="website.layout">
<div id="wrap" class="oe_structure">
<div class="container pt-5 pb-5">
<div class="row">
<div class="col-lg-8 mx-auto">
<t t-if="not ticket">
<div class="alert alert-warning">
<h3>Ticket introuvable</h3>
<p>Aucun ticket ne correspond à <strong><t t-out="ref"/></strong>.</p>
</div>
</t>
<t t-else="">
<h1>Suivi du ticket <t t-out="ticket.reference"/></h1>
<div class="card shadow-sm">
<div class="card-body">
<h4><t t-out="ticket.name"/></h4>
<!-- ...contenu... -->
</div>
</div>
</t>
</div>
</div>
</div>
</div>
</t>
</template>
t-if/t-else gère les trois scénarios (ticket résolu, en cours, introuvable)
sans dupliquer le layout.Trois détails qui comptent :
t-call="website.layout"— enveloppe automatique avec header/footer/menu/logo du site. Ton contenu hérite du thème actif.<div id="wrap" class="oe_structure">— zone éditable depuis le builder website (admin peut ajouter des snippets autour de ta page).- Bootstrap 5 + FontAwesome dispos — pas besoin de charger d'assets
supplémentaires, ils sont déjà dans
web.assets_frontend.
Les pièges à connaître
<string:ref>au lieu de<path:ref>→ les slashes deHLP/2026/00006cassent le routing, 404 garanti.- Oublier
website=Truesur une route type='http' qui utilisewebsite.layout→ le layout ne charge pas le thème, rendu cassé. - auth='public' + accès direct au modèle sans
.sudo()→ AccessError : l'utilisateur public n'a pas le droit de lirehelpdesk.ticket. Utiliser.sudo()ET filtrer les champs exposés côté template. csrf=Falsesur une route POST publique type='http' sans compensation → porte d'entrée pour scripts malveillants. Ajouter signature, IP whitelist ou rate limiting.- Retourner un objet ORM sans
.idsdepuis un endpoint JSON → erreur de sérialisation Object of type RecordSet is not JSON serializable. Toujours transformer enlist[dict]avant de renvoyer. - Oublier
auth='bearer'pour les APIs externes — réinventer un système de tokens custom est douloureux et fragile. Utiliser le natif v17+. - Route
type='json'appelée via GET → 405 Method Not Allowed. Les endpoints JSON-RPC Odoo attendent POST.
🎯 Tu as complété le Parcours Fondamentaux
23 articles, 5 blocs, un module helpdesk construit bloc par bloc de T06 à T23. Tu maîtrises maintenant les fondamentaux pour développer et maintenir un module Odoo 19 production-ready.
Bloc 1 — Installation
Ubuntu, Windows, Docker — 3 articles
Bloc 2 — Environnement dev
PyCharm, base de données, découverte — 4 articles
Bloc 3 — Framework ORM
Modèles, champs, relations, héritage, méthodes — 8 articles
Bloc 4 — Interface utilisateur
Form/List/Search, Kanban/Graph/Pivot, héritage, wizards — 4 articles
Bloc 5 — Rapports & automatisation
QWeb PDF, emails, cron/actions, controllers & API — 4 articles
La suite — d'autres parcours arrivent
Le Parcours Fondamentaux est un socle. À partir de cette base, d'autres parcours et articles spécialisés se publieront progressivement sur le blog.
📗 Parcours Avancé à venir
Pour aller plus loin dans le framework.
- Composants OWL customs
- Tests unitaires & intégration
- Migration v18 → v19
- Debug & profiling performance
📙 Parcours DevOps Odoo à venir
Industrialiser la prod.
- CI/CD (GitHub Actions, GitLab)
- Monitoring (Grafana, Loki)
- Scaling workers & reverse proxy
- Sauvegardes automatisées
🏷 Articles transverses par tag
En complément des parcours linéaires, des articles isolés regroupés par couche technique — chacun autonome, pas besoin de lire les précédents :
#framework-orm #web-ui #integrations #infrastructure
Voir aussi dans cette série
ir.actions.report
mail.template
ir.cron, base.automation
Guide technique Odoo 19 — version PDF
Le Parcours Fondamentaux synthétisé en un guide de référence téléchargeable, avec le module helpdesk complet prêt à installer. Mis à jour à chaque nouvel article publié. Gratuit.
Télécharger le guide (PDF gratuit)