base.automation qui bascule
automatiquement la priorité en Haute quand un ticket concerne un client VIP,
plus un cron qui escalade les tickets dont l'échéance est imminente.Ce que tu vas apprendre
ir.cron
Les tâches planifiées, leur intervalle, leur code Python sécurisé.
ir.actions.server
Déclencher du code depuis un bouton ou un menu contextuel.
base.automation
Règles déclenchées sur create/write
/change de champ / alarme temps.
safe_eval
Ce qui est autorisé dans le code XML et ce qui ne l'est pas.
1. Trois outils, trois cas d'usage
En Odoo 19, ir.cron hérite directement d'ir.actions.server.
Techniquement, une tâche planifiée est une action serveur avec un intervalle.
Cette unification simplifie le mental model.
| Outil | Quand | Cas typique |
|---|---|---|
ir.actions.server |
L'utilisateur clique. | Bouton "Escalader", "Envoyer email groupé", "Générer PDF batch". |
ir.cron |
Régulièrement, sans intervention humaine. | Nettoyage nocturne, rapports quotidiens, rappels J-1, escalade SLA. |
base.automation |
Un événement métier survient. | Sur création d'un ticket VIP, sur changement d'état, sur dépassement de deadline. |
2. ir.actions.server — le bouton métier
Le pattern type : une méthode Python métier bien isolée, et une action serveur minimaliste qui l'appelle.
# models/helpdesk_ticket.py
def action_escalate_sla(self):
"""Escalade : bascule en priorité Urgente et poste un message.
Appelable depuis une ir.actions.server (manuel) ou un ir.cron (batch).
Idempotent : ignore les tickets déjà urgents ou résolus.
"""
escalated = self.filtered(lambda t: t.state != 'done' and t.priority != '3')
if not escalated:
return False
escalated.write({'priority': '3'})
for ticket in escalated:
ticket.message_post(
body="⚠️ <strong>Escalade SLA automatique</strong> — priorité Urgente"
)
return True
Puis l'action serveur XML :
<record id="server_action_escalate_sla" model="ir.actions.server">
<field name="name">Escalader en priorité Urgente</field>
<field name="model_id" ref="model_helpdesk_ticket"/>
<field name="binding_model_id" ref="model_helpdesk_ticket"/>
<field name="binding_view_types">form,list</field>
<field name="state">code</field>
<field name="code">
if records:
records.action_escalate_sla()
</field>
</record>
ir.actions.server côté backend. binding_model_id
fait apparaître l'action dans le menu Actions de la form et de la liste — pratique
pour opérer en masse depuis une vue liste cochée.Champs clés :
| Champ | Rôle |
|---|---|
model_id |
Modèle sur lequel opère l'action. |
state |
code (Python), object_write (updater déclaratif),
object_create, webhook, mail_post, etc. |
binding_model_id |
Expose l'action dans le menu Actions — comme vu en T19 pour les wizards. |
code |
Python exécuté via safe_eval avec un contexte restreint :
records, record, env, Warning,
UserError. |
3. safe_eval — le piège STORE_ATTR
Le code d'une ir.actions.server passe par safe_eval : un exécuteur
Python restreint qui bloque les opérations dangereuses (imports arbitraires,
écriture fichier, socket, etc.).
Effet de bord : certaines syntaxes Python "normales" sont interdites, en particulier l'assignation directe d'attribut sur un record :
# ❌ CRASH — "forbidden opcode(s) in ... STORE_ATTR"
for record in records:
record.priority = '2'
# ✅ OK — utiliser write() ou l'ORM
for record in records:
record.write({'priority': '2'})
# ✅ OK — batch
records.write({'priority': '2'})
.write() ou des appels de méthodes, jamais par record.field = valeur.
Le STORE_ATTR n'est autorisé que dans le code Python compilé classique (fichiers
.py du module).
Variables disponibles dans code :
| Variable | Valeur |
|---|---|
env |
L'environnement ORM — équivalent self.env. |
records |
Recordset sur lequel l'action a été lancée (1+ records). |
record |
Premier record si le contexte en définit un — à éviter pour les actions batch. |
model |
Le modèle sans filtrage — env['helpdesk.ticket']. Utile dans les
crons pour model.search([...]). |
Warning, UserError |
Exceptions standard à lever pour remonter un message à l'utilisateur. |
4. ir.cron — la tâche planifiée
On ajoute au modèle une méthode _cron_escalate_sla qui bouclera sur les
tickets à risque :
@api.model
def _cron_escalate_sla(self):
"""Cron : escalade les tickets dont l'échéance est dans les 24h."""
from datetime import timedelta
threshold = fields.Date.today() + timedelta(days=1)
tickets = self.search([
('state', '!=', 'done'),
('priority', '!=', '3'),
('deadline', '!=', False),
('deadline', '<=', threshold),
])
if tickets:
tickets.action_escalate_sla()
return len(tickets)
Puis le cron XML :
<record id="ir_cron_escalate_sla" model="ir.cron">
<field name="cron_name">Helpdesk — Escalade SLA automatique</field>
<field name="name">Helpdesk — Escalade SLA automatique</field>
<field name="model_id" ref="model_helpdesk_ticket"/>
<field name="state">code</field>
<field name="code">model._cron_escalate_sla()</field>
<field name="interval_number">30</field>
<field name="interval_type">minutes</field>
<field name="active" eval="True"/>
</record>
ir.cron en v19 : bouton Exécuter manuellement
— pratique en dev pour valider sans attendre l'intervalle. La Prochaine date
d'exécution est tracée automatiquement.numbercall et doall
n'existent plus. Le cron runner gère nativement les reprises et le catch-up. Moins de
paramètres à comprendre, comportement plus prévisible.
Intervalles disponibles
interval_type ∈ {minutes, hours, days,
weeks, months}. Jamais seconds — Odoo n'est pas un moteur
temps réel.
Exécuter le cron maintenant pour tester
# Depuis un shell Odoo
./odoo-bin shell -c config/odoo.conf -d ma_db
# Python interactif
>>> env['helpdesk.ticket']._cron_escalate_sla()
2
>>> env.cr.commit()
Ou via l'UI, bouton Exécuter manuellement en haut de la form cron. Très utile en dev pour itérer sans attendre 30 min.
5. base.automation — la règle événementielle
En Odoo 19, base.automation ne porte plus le code directement
: elle déclare un trigger (quand ?) et un filter_domain (sur quoi ?),
puis pointe vers une ou plusieurs ir.actions.server via
action_server_ids.
Étape 1 — l'action serveur qui fait le boulot :
<record id="server_action_set_vip_priority" model="ir.actions.server">
<field name="name">Ticket : priorité haute pour client VIP</field>
<field name="model_id" ref="model_helpdesk_ticket"/>
<field name="state">code</field>
<field name="code">
for record in records:
if record.priority in ('0', '1'):
record.write({'priority': '2'})
record.message_post(body="Priorité auto-ajustée (client VIP).")
</field>
</record>
Étape 2 — la règle qui branche l'action sur un événement :
<record id="automation_vip_priority" model="base.automation">
<field name="name">Ticket VIP → priorité haute auto</field>
<field name="model_id" ref="model_helpdesk_ticket"/>
<field name="trigger">on_create_or_write</field>
<field name="filter_domain">
[('partner_id.is_vip', '=', True), ('priority', 'in', ['0', '1'])]
</field>
<field name="action_server_ids"
eval="[(6, 0, [ref('server_action_set_vip_priority')])]"/>
<field name="active" eval="True"/>
</record>
Les trigger disponibles :
| Trigger | Effet |
|---|---|
on_create |
À la création d'un record. |
on_write |
À chaque écriture. |
on_create_or_write |
Les deux (le plus courant). |
on_unlink |
À la suppression. |
on_change |
À la modification d'un champ précis (spécifier trigger_field_ids). |
on_time |
Alarme temporelle basée sur un champ Date/Datetime (ex : 2j après
deadline). |
on_webhook |
Appel externe sur une URL unique générée par Odoo. |
6. Bonnes pratiques
Idempotence
Un cron ou une automation peut tourner 10 fois avant que tu t'en aperçoives. Écris
toujours ton code comme s'il allait s'exécuter N fois sans effet de bord.
Exemple : records.filtered(lambda t: t.priority != '3') avant d'écrire
priority='3'.
Logique métier dans le modèle, pas dans le XML
Garde le code XML court (1 à 3 lignes) et pointe vers une méthode
.py. Raisons :
- Testable avec
TransactionCase. - Lintable par ton IDE.
- Réutilisable depuis plusieurs déclencheurs.
- Git diff propre.
Manifest — dépendance à base_automation
# __manifest__.py
'depends': ['base', 'mail', 'base_automation'],
'data': [
# ...
'data/automation.xml',
# ...
],
Le module base_automation n'est pas dans base — il faut
l'inclure explicitement si tu utilises base.automation. Omettre cette dépendance
= crash à l'install : External ID not found: model_base_automation.
Runner cron en développement
Odoo démarre un cron runner interne dès qu'un worker est disponible. En mode "serveur simple"
(sans --workers), les crons tournent dans le même process que HTTP — latence
jusqu'à une minute. En prod, préférer au moins --workers=2 pour un worker cron
dédié.
Les pièges à connaître
record.field = valeurdans le code XML → forbidden opcode STORE_ATTR. Utiliser.write().- Oublier
base_automationdansdepends→ External ID not found: model_base_automation. numbercall/doall→ champs v18 supprimés en v19. Si tu copies un vieux cron, nettoie.- Cron non idempotent → doubler ou tripler les effets (deux emails
envoyés, priorité remontée puis re-remontée). Toujours
filterpréalable. - Action automation sans
action_server_ids→ règle déclenchée mais rien ne s'exécute. Silencieux mais vide. filter_domaintrop large → automation qui tourne sur TOUS les tickets à chaque write. Performance catastrophique. Commencer par un domain strict et élargir si besoin.- Code XML qui accède à
self→selfn'existe pas. Utiliserrecordsourecord.
Voir aussi dans cette série
create, write, actions
TransientModel, modals
mail.template, envoi auto
Prochain article — T23 · fin du Bloc 5
Place aux controllers HTTP et API REST :
http.Controller, route @http.route(type='json'),
authentification et payload, pour exposer ton module à l'extérieur.