En Odoo, un dominio es una expresión que filtra registros: el equivalente a la cláusula WHERE de SQL pero declarada en Python. Los usas para filtrar listas, restringir relaciones Many2one/Many2many, definir reglas de seguridad y mucho más. En esta guía vemos desde lo básico hasta los patrones que más se usan en producción con Odoo 19.
1. Sintaxis básica
Un dominio es una lista de tuplas, cada tupla con tres elementos: (campo, operador, valor).
# Facturas de cliente confirmadas
[('move_type', '=', 'out_invoice'), ('state', '=', 'posted')]
Cuando hay varias tuplas y no especificas nada, Odoo las une con AND implícito. Operadores más usados:
=,!=,>,<,>=,<=like,ilike(sensible/insensible a mayúsculas)in,not incon una lista de valoreschild_of,parent_ofpara jerarquías (cuentas, categorías, locations)
2. Operadores lógicos: prefix notation
Para combinar condiciones con OR o NOT, Odoo usa notación prefija. Cada operador (&, |, !) afecta a las siguientes condiciones.
# Facturas borrador OR confirmadas (de cliente)
['&',
('move_type', '=', 'out_invoice'),
'|',
('state', '=', 'draft'),
('state', '=', 'posted')]
El truco: & y | son operadores binarios (toman las dos siguientes), ! es unario (toma la siguiente). Si lo escribes a mano, cuenta los argumentos para que cuadren.
3. Dominios con contexto
El context es un diccionario que viaja con cada acción y vista. Te sirve para que un mismo botón abra una vista filtrada distinto según desde dónde se haya invocado.
3.1 Pasar default desde la acción
<record id="action_facturas_pendientes" model="ir.actions.act_window">
<field name="name">Facturas pendientes</field>
<field name="res_model">account.move</field>
<field name="view_mode">list,form</field>
<field name="domain">[('payment_state', 'in', ['not_paid', 'partial'])]</field>
<field name="context">{'default_move_type': 'out_invoice'}</field>
</record>
Cuando el usuario crea un registro desde esa acción, el campo move_type ya viene rellenado.
3.2 Filtrar por usuario actual
# Solo mis órdenes de venta
[('user_id', '=', uid)]
La variable uid está disponible automáticamente en los dominios evaluados en vistas y reglas. Si necesitas la compañía actual, usa company_id con allowed_company_ids:
[('company_id', 'in', allowed_company_ids)]
4. Dominios dinámicos en vistas
Los más útiles son los que dependen de otro campo del mismo formulario. Se escriben directo en el atributo domain del XML:
<field name="product_id"
domain="[('type', '=', 'product'),
('categ_id', '=', categ_filter_id)]"/>
Aquí categ_filter_id es otro campo del mismo formulario. Cada vez que cambia su valor, Odoo re-evalúa el dominio y limita las opciones.
4.1 Cambiar el dominio con @api.onchange
Cuando el dominio depende de cálculos más complejos, usa un onchange que devuelve {'domain': {...}}:
from odoo import api, fields, models
class SaleOrder(models.Model):
_inherit = 'sale.order'
warehouse_id = fields.Many2one('stock.warehouse')
@api.onchange('warehouse_id')
def _onchange_warehouse_filter_products(self):
if not self.warehouse_id:
return {'domain': {'order_line.product_id': []}}
# Solo productos con stock en ese almacén
product_ids = self.env['stock.quant'].search([
('location_id.warehouse_id', '=', self.warehouse_id.id),
('quantity', '>', 0),
]).product_id.ids
return {'domain': {
'order_line.product_id': [('id', 'in', product_ids)]
}}
5. Reglas de registro (seguridad por dominio)
Las record rules son dominios que Odoo aplica automáticamente a cada query, según el grupo del usuario. Es la forma estándar de implementar visibilidad por sucursal, vendedor o cualquier criterio de negocio.
<record id="rule_factura_solo_propia_sucursal" model="ir.rule">
<field name="name">Factura: solo de mi sucursal</field>
<field name="model_id" ref="account.model_account_move"/>
<field name="domain_force">
[('branch_id', 'in', user.branch_ids.ids)]
</field>
<field name="groups" eval="[(4, ref('account.group_account_invoice'))]"/>
<field name="perm_read" eval="True"/>
<field name="perm_write" eval="True"/>
<field name="perm_create" eval="True"/>
<field name="perm_unlink" eval="False"/>
</record>
Tres cosas a recordar con record rules:
- Se evalúa con permisos elevados. Dentro del
domain_forcetienes acceso auser(recordset del usuario actual) sin restricciones. - Las reglas del mismo grupo se unen con OR; las de grupos distintos con AND. Esto importa cuando hay varias reglas que tocan el mismo modelo.
- Si un usuario es superuser o el contexto trae
bypass_rules, la regla no aplica.
6. Dominios multi-compañía
En multi-compañía, Odoo trae allowed_company_ids en el contexto: la lista de IDs de las compañías que el usuario tiene activas en el selector superior derecho. Úsala en lugar de hardcodear.
# Mal: solo ve la compañía "principal"
[('company_id', '=', user.company_id.id)]
# Bien: respeta el selector multi-compañía
[('company_id', 'in', allowed_company_ids)]
En las record rules es exactamente igual:
<field name="domain_force">
['|', ('company_id', '=', False),
('company_id', 'in', allowed_company_ids)]
</field>
El '|' con company_id = False es para los registros compartidos (sin compañía asignada).
7. Tips de producción
- Debug fácil: en el modo desarrollador, abre Filtros > Añadir filtro personalizado y arma el dominio visualmente. Después copia el resultado del developer tooltip.
- No mezcles tipos:
('field', '=', False)es muy distinto a('field', '=', 0). Para campos relacionales vacíos, siempre= False. - Performance: dominios con muchos
likesobre tablas grandes son lentos. Si vas a filtrar millones de registros, asegúrate de tener el campo indexado (index=Trueen la definición). - Evita
search().idsinnecesario: en lugar de[('id', 'in', self.env['x'].search([...]).ids)], casi siempre puedes hacer un dominio relacional con.(ej:('partner_id.country_id', '=', mx_id)).
Cierre
Los dominios son la base de cómo Odoo expresa filtros en todos los rincones: vistas, acciones, record rules, métodos de búsqueda. Dominarlos te ahorra muchísimo código Python repetitivo y te permite implementar reglas de negocio (visibilidad por sucursal, restricciones por rol, vistas contextuales) usando configuración en lugar de overrides.
Si en tu implementación tienes casos de visibilidad complejos (varias sucursales, múltiples compañías, vendedores con territorios), antes de meterte a sobrescribir _search, revisa si una record rule bien diseñada no resuelve el problema con menos riesgo.