Ce que tu vas apprendre
Architecture
Comment ir.ui.view, ir.actions.act_window et
ir.ui.menu s'articulent.
Vue List
<list> (ex-tree), decoration-*, widgets colonnes.
Vue Form
Header, sheet, notebook, statusbar, boutons d'action, chatter.
Vue Search
Filtres, group by, search_default_*, recherche multi-champs.
Prérequis
- Module
odooskills_helpdeskv19.0.1.7.0 (T15). - Sécurité du modèle déjà posée (
security/ir.model.access.csv) — sinon le menu s'affiche mais les records sont invisibles. - Connaître les champs et relations du modèle (T10/T11).
1. L'architecture Menu → Action → Vue
Le backend Odoo suit une chaîne simple : l'utilisateur clique sur un menu, le menu déclenche une action, l'action ouvre un ou plusieurs vues sur un modèle donné.
<tree> a été renommée en
<list>, et view_mode utilise list,form (pas
tree,form). Tout comme attrs et states sont
supprimés — on utilise invisible="state == 'done'" directement.
2. La vue <list>
Tableau scrollable, tri, multi-sélection, exports. C'est la porte d'entrée vers les
enregistrements. Pour notre modèle helpdesk.ticket :
<record id="view_helpdesk_ticket_list" model="ir.ui.view">
<field name="name">helpdesk.ticket.list</field>
<field name="model">helpdesk.ticket</field>
<field name="arch" type="xml">
<list string="Tickets"
decoration-danger="is_overdue"
decoration-success="state == 'done'"
decoration-muted="state == 'done'">
<field name="reference"/>
<field name="name"/>
<field name="partner_id"/>
<field name="category_id"/>
<field name="user_id" widget="many2one_avatar_user"/>
<field name="tag_ids" widget="many2many_tags"
options="{'color_field': 'color'}"/>
<field name="deadline"/>
<field name="is_overdue" invisible="1"/>
<field name="state" widget="badge"
decoration-info="state == 'new'"
decoration-warning="state == 'in_progress'"
decoration-success="state == 'done'"/>
</list>
</field>
</record>
HLP/2026/NNNNN, lignes rouges pour les tickets en retard, lignes vertes pour les résolus, badges colorés sur state.Points notables :
decoration-*colorise les lignes selon une expression Python —danger(rouge),warning(orange),success(vert),info,muted,primary.invisible="1"— on chargeis_overduedans la vue pour pouvoir l'évaluer dansdecoration-danger, sans l'afficher comme colonne.widget="badge"affiche la selection comme pastille colorée — très lisible sur un statut.widget="many2one_avatar_user"montre l'avatar utilisateur — UI Odoo moderne, disponible sur toutMany2oneversres.users.
3. La vue <form>
Écran de détail d'un enregistrement — la vue la plus riche, structurée en trois zones : header (actions + statusbar), sheet (contenu édité), chatter (historique + messages).
<form string="Ticket">
<header>
<button name="action_start" type="object" string="Démarrer"
class="oe_highlight" invisible="state != 'new'"/>
<button name="action_resolve" type="object" string="Résoudre"
class="oe_highlight" invisible="state != 'in_progress'"/>
<field name="state" widget="statusbar"
statusbar_visible="new,in_progress,done"/>
</header>
<sheet>
<div class="oe_title">
<label for="name" string="Sujet"/>
<h1><field name="name" placeholder="Ex : Imprimante bloquée"/></h1>
</div>
<group>
<group>
<field name="partner_id"/>
<field name="category_id" options="{'no_create': True}"/>
<field name="user_id" widget="many2one_avatar_user"/>
<field name="channel"/>
</group>
<group>
<field name="deadline"/>
<field name="resolved_at" readonly="1" invisible="state != 'done'"/>
<field name="sla_hours"/>
<field name="sla_status" widget="badge"
decoration-success="sla_status == 'ok'"
decoration-danger="sla_status == 'breach'"/>
</group>
</group>
<notebook>
<page string="Description" name="description">
<field name="description"/>
</page>
<page string="Tags" name="tags">
<field name="tag_ids" widget="many2many_tags"/>
</page>
<page string="Résolution" name="resolution"
invisible="state != 'done'">
<field name="resolution_note"/>
<field name="hours_spent"/>
</page>
</notebook>
</sheet>
<chatter/>
</form>
state != 'new'), statusbar en haut, deux colonnes de champs, notebook Description/Tags/Commentaires, chatter à droite avec les messages auto postés par create() (T15).Les cinq mécaniques à retenir :
- Boutons header —
type="object"appelle une méthode Python (nosaction_start/action_resolvede T15).class="oe_highlight"les rend bleus. widget="statusbar"transforme leSelectionstateen flux visuel.statusbar_visiblelimite les étapes affichées.- Expressions v19 directes :
invisible="state != 'new'"remplace l'ancienattrs="{'invisible': [('state','!=','new')]}". - Deux
<group>imbriqués produisent la mise en colonnes gauche/droite — c'est la grille Odoo native. <chatter/>(v19) remplace l'ancien blocoe_chatter. Ajoute automatiquement followers, activités, messages — à condition que le modèle hérite demail.thread.
4. La vue <search>
Souvent négligée, c'est pourtant la différence entre un module jouable et un module pro. Trois briques : champs de recherche, filtres prédéfinis, groupements.
<search string="Tickets">
<!-- Champs : ce que tape l'utilisateur dans la barre -->
<field name="name" string="Sujet/Référence"
filter_domain="['|', ('name', 'ilike', self),
('reference', 'ilike', self)]"/>
<field name="partner_id"/>
<field name="category_id"/>
<field name="tag_ids"/>
<!-- Filtres : cases à cocher rapides -->
<filter name="filter_mine" string="Mes tickets"
domain="[('user_id', '=', uid)]"/>
<filter name="filter_unassigned" string="Non assignés"
domain="[('user_id', '=', False)]"/>
<separator/>
<filter name="filter_new" string="Nouveaux"
domain="[('state', '=', 'new')]"/>
<filter name="filter_overdue" string="En retard"
domain="[('is_overdue', '=', True)]"/>
<!-- Groupements -->
<separator/>
<filter name="group_state" string="Statut"
context="{'group_by': 'state'}"/>
<filter name="group_category" string="Catégorie"
context="{'group_by': 'category_id'}"/>
<filter name="group_user" string="Assigné à"
context="{'group_by': 'user_id'}"/>
</search>
context="{'group_by': 'state'}") activé — les tickets sont regroupés et chaque groupe est pliable.Trois pépites à connaître :
filter_domainremplace le domaine auto — indispensable pour les recherches sur plusieurs champs à la fois (ici sujet OU référence).selfdésigne la chaîne saisie par l'utilisateur.uid(pasuser.id!) est disponible dans les domaines des vues search — utilisateur courant, sans eval Python.context="{'group_by': 'champ'}"transforme un filtre en groupement — même syntaxe, effet différent. Plusieursgroup_byactivés s'empilent.
5. L'action ir.actions.act_window
L'action fait le lien entre un menu et un modèle, et décide des vues à ouvrir.
<record id="action_helpdesk_ticket" model="ir.actions.act_window">
<field name="name">Tickets</field>
<field name="res_model">helpdesk.ticket</field>
<field name="view_mode">list,form</field>
<field name="search_view_id" ref="view_helpdesk_ticket_search"/>
<field name="context">{'search_default_filter_new': 1}</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">Créer un premier ticket</p>
<p>Enregistre les demandes de support, assigne-les, suis les SLA.</p>
</field>
</record>
| Champ | Rôle |
|---|---|
view_mode | Ordre des vues. La première (list) s'ouvre par défaut. |
search_view_id | Vue search à utiliser (sinon Odoo prend la plus prioritaire sur le modèle). |
context | search_default_<name>: 1 active automatiquement le filtre <name> à l'ouverture. |
help | Écran vide — message quand aucun enregistrement. HTML autorisé. |
domain (optionnel) | Filtre statique toujours appliqué. Ex: [('active','=',True)]. |
6. Les menus ir.ui.menu
Trois niveaux suffisent pour un module bien structuré : racine (menu principal), catégorie (fonctionnel ou configuration), entrée cliquable (liée à une action).
<menuitem id="menu_helpdesk_root"
name="Helpdesk"
sequence="90"
web_icon="odooskills_helpdesk,static/description/icon.png"/>
<menuitem id="menu_helpdesk_tickets"
name="Tickets"
parent="menu_helpdesk_root"
action="action_helpdesk_ticket"
sequence="10"/>
<menuitem id="menu_helpdesk_config"
name="Configuration"
parent="menu_helpdesk_root"
sequence="90"/>
<menuitem id="menu_helpdesk_categories"
name="Catégories"
parent="menu_helpdesk_config"
action="action_helpdesk_ticket_category"
sequence="10"/>
complete_name du T14 affiche le chemin entier Support / Matériel / Imprimante.Règles d'or :
- Un menu racine sans action est juste un conteneur — l'utilisateur ne peut pas cliquer dessus.
- L'attribut
web_iconacceptemodule_name,chemin/icon.png— obligatoire pour apparaître dans l'App Switcher. sequencepilote l'ordre d'affichage — 10, 20, 30… laissent de la marge pour insérer.- Ranger la configuration sous un sous-menu "Configuration" est la convention Odoo.
7. Manifest mis à jour
'data': [
'security/ir.model.access.csv',
'data/ir_sequence.xml',
'views/helpdesk_ticket_category_views.xml',
'views/helpdesk_ticket_views.xml',
'views/helpdesk_menus.xml', # ← toujours après les actions !
],
External ID not found:
action_helpdesk_ticket à l'install.
Upgrade et test visuel
./odoo-bin -c config/odoo.conf -u odooskills_helpdesk -d ta_base --stop-after-init
# Puis démarre le serveur normal et va sur http://localhost:8069
# Dans l'App Switcher : le menu "Helpdesk" doit apparaître avec son icône.
# Clique "Tickets" → tu tombes sur la liste filtrée "Nouveaux" (search_default).
Les pièges Odoo 19 à éviter
- Utiliser
<tree>— renommé<list>en v19. Etview_mode="tree,form"lève l'erreur « View types not defined tree ». - Écrire
attrs="{'invisible': [(...)]}"— supprimé. Utiliser directementinvisible="condition". - Oublier
search_view_idsur l'action — Odoo prend une vue search auto-générée, tes filtres n'apparaissent pas. - Charger les menus avant les actions dans
data— External ID introuvable à l'install. - Référencer
user.iddans un domain — utiliseruid(clé spéciale du contexte search).
Voir aussi dans cette série
_inherit, mixins, mail.thread
parent_id, _parent_store
create, write, actions métier
Prochain article — T17
On passe aux vues Kanban : templates QWeb, colonnes draggables, widgets visuels, Graph et Pivot pour les dashboards.
Télécharger le guide technique Odoo 19 (PDF gratuit)