La list view et le form view restent le choix naturel pour travailler dans le back-office Odoo : ils apportent déjà recherche, droits, création et édition. Un template QWeb répond à un autre besoin : une URL qui renvoie une page HTML préparée par le serveur. Les deux peuvent lire les mêmes notes.
Nous construisons ici un endpoint de page HTTP dans un contrôleur : le navigateur demande ou envoie une page HTML. Ce n'est pas une API JSON générale.
Dans ce cours, les fields s’appellent title et body. Utilisez ces mêmes noms dans Python, XML, les données et les templates. Un libellé français comme string="Titre" ne change pas le nom technique du field.
1. Lecture seule : le contrôleur choisit les records
from odoo import http
from odoo.http import request
class TrainingNotesController(http.Controller):
@http.route("/training/notes", type="http", auth="user", methods=["GET"])
def notes_page(self):
notes = request.env["training.note"].search([], order="id desc")
return request.render("training_hello.training_notes_page", {
"notes": notes,
})<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<template id="training_notes_page">
<h1>Notes de formation</h1>
<ul>
<li t-foreach="notes" t-as="note">
<strong><t t-out="note.title"/></strong>
<p><t t-out="note.body"/></p>
</li>
</ul>
</template>
</odoo>Dans un contrôleur, request représente la requête HTTP en cours et les outils qu'Odoo lui associe. request.env est son environnement ORM : il relie la base ouverte, l'utilisateur connecté et le contexte courant. L'expression request.env["training.note"] donne donc accès au model training.note pour cet utilisateur. La recherche respecte ses droits de lecture et les éventuelles record rules. Enfin, request.render() prépare la réponse HTML à partir du template et des notes.
À observer : quand vous ouvrez /training/notes, le navigateur effectue une requête GET. Odoo choisit notes_page(), car son décorateur accepte uniquement methods=["GET"]. Le template reçoit notes et construit la liste sans rien écrire.
Pour rendre ce contrôleur chargeable, importez-le dans controllers/__init__.py, gardez from . import controllers dans __init__.py à la racine du module et ajoutez "views/training_note_templates.xml", dans data du manifeste. Redémarrez Odoo pour le Python, puis mettez le module à niveau pour le XML.
2. Séparez la lecture GET de la création POST
Le template ne doit jamais écrire directement dans PostgreSQL. Il affiche un formulaire HTML ; le navigateur envoie ce formulaire en POST ; seule la fonction POST valide les valeurs puis appelle create() sur le model. Remplacez le fichier Python précédent par cette version complète :
from odoo import http
from odoo.http import request
class TrainingNotesController(http.Controller):
@http.route("/training/notes", type="http", auth="user", methods=["GET"])
def notes_page(self):
notes = request.env["training.note"].search([], order="id desc")
return request.render("training_hello.training_notes_page", {
"notes": notes,
})
@http.route("/training/notes", type="http", auth="user", methods=["POST"])
def create_note(self, **kwargs):
title = (kwargs.get("title") or "").strip()
body = kwargs.get("body") or ""
if title:
request.env["training.note"].create({
"title": title,
"body": body,
})
return request.redirect("/training/notes", code=303)Les deux fonctions partagent la même URL, mais pas la même méthode HTTP. Odoo compare aussi methods avant de choisir la fonction : une visite ordinaire déclenche notes_page(), tandis que le formulaire avec method="post" déclenche create_note(). La fonction de lecture ne peut donc pas créer de record, et la fonction d'écriture ne construit pas la page.
**kwargs est une syntaxe Python : elle rassemble dans un dictionnaire les paramètres nommés qu'Odoo transmet à la fonction. Pour cette requête POST, Odoo extrait notamment les champs du formulaire nommés title et body. Le code les lit avec kwargs.get(...), retire les espaces autour du titre, puis ne crée la note que si ce titre reste non vide. Le model reste fixé à training.note et les autres paramètres éventuels ne sont pas utilisés.
Après la création, request.redirect() demande au navigateur de revenir sur la même URL avec une nouvelle requête GET. Le statut HTTP 303 rend ce passage explicite : POST crée, la redirection relance GET, puis GET affiche la liste mise à jour. Ce cycle Post/Redirect/Get évite qu'un simple rechargement du navigateur renvoie le formulaire.
<form action="/training/notes" method="post">
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
<label>Titre <input name="title" required="required"/></label>
<label>Contenu <textarea name="body"></textarea></label>
<button type="submit">Enregistrer</button>
</form>L'attribut required aide dans le navigateur, mais la fonction POST contrôle aussi le titre : une requête peut être envoyée sans passer par ce formulaire. Pour une route type="http", Odoo active la protection CSRF par défaut sur le POST ; le champ caché fournit le jeton attendu. auth="user" impose une session connectée et create() vérifie encore l'ACL perm_create. Les notes restent donc accessibles selon les droits Odoo déjà définis dans le cours. Une page publique demanderait des règles bien plus précises : ne remplacez pas ce choix par sudo().
Résultat attendu : la page s'ouvre en GET et affiche les notes. Quand vous envoyez un titre valide, le POST crée une note, renvoie la redirection 303, puis le GET suivant affiche cette nouvelle note. Un titre composé uniquement d'espaces ne crée aucun record.