Ir al contenido

Record rules en profundidad: OR, AND y la regla que abre más de lo que crees

Cómo combina Odoo las reglas globales y las de grupo, y qué puede usar un domain_force
14 de octubre de 2026 por
Record rules en profundidad: OR, AND y la regla que abre más de lo que crees
ATEMI, Aitor Atencia

Odoo · Seguridad · ir.rule

En ACL y record rules paso a paso vimos cómo crear reglas. Esta entrada va de lo que pasa cuando hay varias: cómo se combinan, qué permisos tiene una regla que no los declara y por qué un comentario en el XML que dice «esto se combina en AND» puede estar equivocado sin que ningún test lo detecte.

Logo Odoo
Globales en AND, de grupo en OR, y el resultado de ambas en AND.

La fórmula

Cómo combina Odoo las reglas

Para un modelo y una operación (leer, escribir, crear, borrar), Odoo reúne las reglas activas que aplican a esa operación y las combina así:

  • Reglas globales (sin grupos): se combinan entre sí con AND. Son restricciones que nadie puede saltarse, como el filtro multicompañía.
  • Reglas de grupo: todas las que apliquen a cualquiera de los grupos del usuario se combinan con OR. Cada una concede acceso a un conjunto de registros.
  • El resultado final es (globales en AND) AND (de grupo en OR).
# Dos reglas del mismo grupo "Usuario de viajes":
# R1: [('user_id', '=', user.id)]        → mis viajes
# R2: [('state', '=', 'published')]      → viajes publicados
#
# Dominio efectivo:  R1 OR R2
# El usuario accede a sus viajes Y a todos los publicados, de cualquiera.

Hasta aquí, nada raro si R2 solo debería dar lectura. El problema está en el siguiente punto.

La trampa

Una regla sin flags lo concede todo

Los campos perm_read, perm_write, perm_create y perm_unlink valen True por defecto. Una regla pensada «para que se vean los publicados» que no declara sus flags también aplica a la escritura, y como se combina en OR con las demás, cualquier usuario del grupo puede editar y borrar los viajes publicados de otros.

<!-- ❌ Pretende ser de lectura, pero concede escritura y borrado -->
<record id="fleet_trip_rule_published" model="ir.rule">
    <field name="name">Trips: published are visible</field>
    <field name="model_id" ref="model_fleet_trip"/>
    <field name="domain_force">[('state', '=', 'published')]</field>
    <field name="groups" eval="[(4, ref('group_fleet_trip_user'))]"/>
</record>

<!-- ✅ Flags explícitos: solo lectura -->
<record id="fleet_trip_rule_published" model="ir.rule">
    <field name="name">Trips: published are visible</field>
    <field name="model_id" ref="model_fleet_trip"/>
    <field name="domain_force">[('state', '=', 'published')]</field>
    <field name="groups" eval="[(4, ref('group_fleet_trip_user'))]"/>
    <field name="perm_read" eval="True"/>
    <field name="perm_write" eval="False"/>
    <field name="perm_create" eval="False"/>
    <field name="perm_unlink" eval="False"/>
</record>

Límites

Qué puede usar un domain_force

El dominio de una regla se evalúa con un contexto mínimo: user, company_id y company_ids. No hay fecha actual, ni context_today, ni acceso a otros modelos. Una regla como «los eventos cuya fecha de publicación ya pasó» falla al instalar con un NameError poco expresivo.

# ❌ No existe en el contexto de evaluación de ir.rule
[('publish_date', '<=', context_today())]

# ✅ Un booleano almacenado que un cron mantiene al día
[('is_published', '=', True)]

Si la condición es una ventana que se abre y se cierra con el reloj, el cron debe recalcular en ambos sentidos. Y recuerda que las reglas no aplican al superusuario: cualquier código en sudo() las ignora por completo.

Verificar

El único test que detecta estos fallos

Como el superusuario no pasa por las reglas, la única forma de probarlas es actuar como un usuario real del grupo e intentar justo lo que no debería poder hacer:

from odoo.exceptions import AccessError


def test_user_cannot_edit_others_published_trip(self):
    other_trip = self.env["fleet.trip"].create({
        "name": "Ajeno", "user_id": self.other_user.id, "state": "published",
    })
    trip_as_user = other_trip.with_user(self.trip_user)
    self.assertEqual(trip_as_user.name, "Ajeno")  # puede leerlo
    with self.assertRaises(AccessError):
        trip_as_user.write({"name": "Editado"})    # pero no editarlo
Tipo de reglaCombinaciónPara qué
Global (sin grupos)AND con las demás globalesRestricciones absolutas: compañía, archivado lógico
De grupoOR con las de los grupos del usuarioConceder acceso a subconjuntos
Sin flags declaradosAplica a las 4 operacionesCasi nunca lo que quieres en una regla de visibilidad

Resumen

Las reglas globales restringen en AND; las de grupo conceden en OR. Una regla de grupo sin flags concede también escritura y borrado, así que declara siempre los cuatro. El domain_force no conoce la fecha actual: usa campos almacenados. Y prueba cada regla con with_user() intentando lo prohibido, porque como administrador todo pasa.

en Odoo
El chatter desde código: message_post, subtipos y a quién le llega
Notas internas, mensajes a seguidores, autor del mensaje y HTML seguro