Odoo · Automatización · Seguridad
Las acciones de servidor (ir.actions.server) y las reglas automatizadas permiten automatizar casi cualquier cosa sin desplegar un módulo. Precisamente por eso son un riesgo: código Python que vive en la base de datos, sin control de versiones, sin tests y editable desde el navegador por cualquiera con permisos de administración.
El problema
Código en la base de datos es código que nadie revisa
Una acción de servidor de tipo Ejecutar código se evalúa con safe_eval: no permite import ni acceder a atributos que empiezan por guion bajo, pero sí todo lo que el ORM expone al usuario que la ejecuta. Si alguien escribe ahí un records.sudo().write(...) o un env['res.partner'].search([]).unlink(), se ejecutará. Y como no está en Git, la próxima persona que la toque no sabrá qué cambió ni por qué.
# ❌ Lógica de negocio completa dentro de la acción de servidor
for rec in records:
if rec.amount_total > 1000 and not rec.partner_id.vat:
rec.sudo().write({'state': 'blocked'})
env['mail.mail'].sudo().create({
'email_to': 'admin@empresa.com',
'subject': 'Pedido bloqueado %s' % rec.name,
}).send()
Este bloque tiene tres problemas: el sudo() se salta las record rules sin justificación, el email se envía de forma síncrona dentro de la transacción y ninguna de estas reglas de negocio tiene un test que la proteja.
El patrón
La acción llama a un método; el método vive en el módulo
La forma segura es reducir la acción de servidor a una sola línea que invoca un método público del modelo. La lógica queda en Python versionado, con sus tests, su revisión de código y sus comprobaciones de permisos explícitas.
# models/sale_order.py
from odoo import _, models
from odoo.exceptions import UserError
class SaleOrder(models.Model):
_inherit = "sale.order"
def action_block_risky_orders(self):
if not self.env.user.has_group("sales_team.group_sale_manager"):
raise UserError(_("Only sales managers can block orders."))
risky = self.filtered(
lambda o: o.amount_total > 1000 and not o.partner_id.vat
)
risky.write({"state": "cancel"})
for order in risky:
order.message_post(body=_("Blocked: customer without VAT."))
return True
# Contenido de la acción de servidor (Ejecutar código)
records.action_block_risky_orders()
<record id="action_server_block_risky_orders" model="ir.actions.server">
<field name="name">Block risky orders</field>
<field name="model_id" ref="sale.model_sale_order"/>
<field name="binding_model_id" ref="sale.model_sale_order"/>
<field name="state">code</field>
<field name="code">records.action_block_risky_orders()</field>
</record>
Permisos
¿Con qué usuario se ejecuta?
Esta es la pregunta que hay que hacerse siempre. Una acción lanzada desde el menú Acción se ejecuta como el usuario que pulsa el botón, con sus ACL y record rules. Un cron se ejecuta como el usuario configurado en el propio ir.cron, que suele ser OdooBot o el administrador: todo lo que haga tendrá permisos amplios. Por eso las comprobaciones de grupo dentro del método no son redundantes: son lo único que impide que una acción pensada para gerentes la dispare cualquier usuario interno.
- Restringe la visibilidad de la acción a los grupos que deben verla.
- Comprueba el grupo también dentro del método: ocultar un botón no es control de acceso.
- Si necesitas
sudo(), aplícalo a la operación concreta que lo requiere, nunca a todo el recordset de entrada.
Decisión
Cron, regla automatizada o acción manual
No todas las automatizaciones deberían dispararse igual. La elección depende de qué la desencadena y de cuánto trabajo hace por ejecución.
| Mecanismo | Disparador | Úsalo para | Evítalo si |
|---|---|---|---|
| Acción de servidor manual | Botón del menú Acción | Operaciones puntuales sobre registros seleccionados | Debe ocurrir sin intervención humana |
| Regla automatizada | Crear, modificar, campo cambiado, fecha | Reacciones inmediatas y baratas a un cambio | Llama a APIs externas o procesa miles de registros |
ir.cron | Intervalo de tiempo | Lotes, sincronizaciones, limpiezas | El usuario espera ver el resultado al guardar |
Una regla automatizada al modificar se ejecuta dentro de la misma transacción que el write del usuario: si llama a un servicio externo lento, el formulario se queda colgado; si falla, el guardado entero se revierte. Para eso existe el cron (ver cron jobs y colas).
Resumen
Una acción de servidor segura tiene una línea de código, vive en un XML del módulo, llama a un método con tests y comprobaciones de grupo, y nunca hace sudo() sobre todo lo que recibe. Si la automatización es lenta o externa, va a un cron; si es inmediata y barata, a una regla automatizada.