{"id":2051,"date":"2026-05-04T00:24:54","date_gmt":"2026-05-04T00:24:54","guid":{"rendered":"https:\/\/exdoo.mx\/blogs\/dominios-en-odoo-19-filtros-basicos-reglas-avanzadas\/"},"modified":"2026-05-04T00:24:54","modified_gmt":"2026-05-04T00:24:54","slug":"dominios-en-odoo-19-filtros-basicos-reglas-avanzadas","status":"publish","type":"post","link":"https:\/\/exdoo.mx\/blogs\/dominios-en-odoo-19-filtros-basicos-reglas-avanzadas\/","title":{"rendered":"Dominios en Odoo 19: de filtros b\u00e1sicos a reglas avanzadas"},"content":{"rendered":"<p>En Odoo, un <strong>dominio<\/strong> es una expresi\u00f3n que filtra registros: el equivalente a la cl\u00e1usula <code>WHERE<\/code> de SQL pero declarada en Python. Los usas para filtrar listas, restringir relaciones <em>Many2one\/Many2many<\/em>, definir reglas de seguridad y mucho m\u00e1s. En esta gu\u00eda vemos desde lo b\u00e1sico hasta los patrones que m\u00e1s se usan en producci\u00f3n con Odoo 19.<\/p>\n<h2>1. Sintaxis b\u00e1sica<\/h2>\n<p>Un dominio es una <strong>lista de tuplas<\/strong>, cada tupla con tres elementos: <code>(campo, operador, valor)<\/code>.<\/p>\n<pre><code class=\"language-python\"># Facturas de cliente confirmadas\n[('move_type', '=', 'out_invoice'), ('state', '=', 'posted')]\n<\/code><\/pre>\n<p>Cuando hay varias tuplas y no especificas nada, Odoo las une con <code>AND<\/code> impl\u00edcito. Operadores m\u00e1s usados:<\/p>\n<ul>\n<li><code>=<\/code>, <code>!=<\/code>, <code>&gt;<\/code>, <code>&lt;<\/code>, <code>&gt;=<\/code>, <code>&lt;=<\/code><\/li>\n<li><code>like<\/code>, <code>ilike<\/code> (sensible\/insensible a may\u00fasculas)<\/li>\n<li><code>in<\/code>, <code>not in<\/code> con una lista de valores<\/li>\n<li><code>child_of<\/code>, <code>parent_of<\/code> para jerarqu\u00edas (cuentas, categor\u00edas, locations)<\/li>\n<\/ul>\n<h2>2. Operadores l\u00f3gicos: prefix notation<\/h2>\n<p>Para combinar condiciones con OR o NOT, Odoo usa <strong>notaci\u00f3n prefija<\/strong>. Cada operador (<code>&amp;<\/code>, <code>|<\/code>, <code>!<\/code>) afecta a las <em>siguientes<\/em> condiciones.<\/p>\n<pre><code class=\"language-python\"># Facturas borrador OR confirmadas (de cliente)\n['&amp;',\n    ('move_type', '=', 'out_invoice'),\n    '|',\n        ('state', '=', 'draft'),\n        ('state', '=', 'posted')]\n<\/code><\/pre>\n<p>El truco: <code>&amp;<\/code> y <code>|<\/code> son operadores binarios (toman las dos siguientes), <code>!<\/code> es unario (toma la siguiente). Si lo escribes a mano, cuenta los argumentos para que cuadren.<\/p>\n<h2>3. Dominios con contexto<\/h2>\n<p>El <code>context<\/code> es un diccionario que viaja con cada acci\u00f3n y vista. Te sirve para que un mismo bot\u00f3n abra una vista filtrada distinto seg\u00fan desde d\u00f3nde se haya invocado.<\/p>\n<h3>3.1 Pasar default desde la acci\u00f3n<\/h3>\n<pre><code class=\"language-xml\">&lt;record id=\"action_facturas_pendientes\" model=\"ir.actions.act_window\"&gt;\n    &lt;field name=\"name\"&gt;Facturas pendientes&lt;\/field&gt;\n    &lt;field name=\"res_model\"&gt;account.move&lt;\/field&gt;\n    &lt;field name=\"view_mode\"&gt;list,form&lt;\/field&gt;\n    &lt;field name=\"domain\"&gt;[('payment_state', 'in', ['not_paid', 'partial'])]&lt;\/field&gt;\n    &lt;field name=\"context\"&gt;{'default_move_type': 'out_invoice'}&lt;\/field&gt;\n&lt;\/record&gt;\n<\/code><\/pre>\n<p>Cuando el usuario crea un registro desde esa acci\u00f3n, el campo <code>move_type<\/code> ya viene rellenado.<\/p>\n<h3>3.2 Filtrar por usuario actual<\/h3>\n<pre><code class=\"language-python\"># Solo mis \u00f3rdenes de venta\n[('user_id', '=', uid)]\n<\/code><\/pre>\n<p>La variable <code>uid<\/code> est\u00e1 disponible autom\u00e1ticamente en los dominios evaluados en vistas y reglas. Si necesitas la compa\u00f1\u00eda actual, usa <code>company_id<\/code> con <code>allowed_company_ids<\/code>:<\/p>\n<pre><code class=\"language-python\">[('company_id', 'in', allowed_company_ids)]\n<\/code><\/pre>\n<h2>4. Dominios din\u00e1micos en vistas<\/h2>\n<p>Los m\u00e1s \u00fatiles son los que dependen de otro campo del mismo formulario. Se escriben directo en el atributo <code>domain<\/code> del XML:<\/p>\n<pre><code class=\"language-xml\">&lt;field name=\"product_id\"\n    domain=\"[('type', '=', 'product'),\n            ('categ_id', '=', categ_filter_id)]\"\/&gt;\n<\/code><\/pre>\n<p>Aqu\u00ed <code>categ_filter_id<\/code> es otro campo del mismo formulario. Cada vez que cambia su valor, Odoo re-eval\u00faa el dominio y limita las opciones.<\/p>\n<h3>4.1 Cambiar el dominio con @api.onchange<\/h3>\n<p>Cuando el dominio depende de c\u00e1lculos m\u00e1s complejos, usa un <code>onchange<\/code> que devuelve <code>{'domain': {...}}<\/code>:<\/p>\n<pre><code class=\"language-python\">from odoo import api, fields, models\n\nclass SaleOrder(models.Model):\n    _inherit = 'sale.order'\n\n    warehouse_id = fields.Many2one('stock.warehouse')\n\n    @api.onchange('warehouse_id')\n    def _onchange_warehouse_filter_products(self):\n        if not self.warehouse_id:\n            return {'domain': {'order_line.product_id': []}}\n        # Solo productos con stock en ese almac\u00e9n\n        product_ids = self.env['stock.quant'].search([\n            ('location_id.warehouse_id', '=', self.warehouse_id.id),\n            ('quantity', '&gt;', 0),\n        ]).product_id.ids\n        return {'domain': {\n            'order_line.product_id': [('id', 'in', product_ids)]\n        }}\n<\/code><\/pre>\n<h2>5. Reglas de registro (seguridad por dominio)<\/h2>\n<p>Las <strong>record rules<\/strong> son dominios que Odoo aplica autom\u00e1ticamente a cada query, seg\u00fan el grupo del usuario. Es la forma est\u00e1ndar de implementar visibilidad por sucursal, vendedor o cualquier criterio de negocio.<\/p>\n<pre><code class=\"language-xml\">&lt;record id=\"rule_factura_solo_propia_sucursal\" model=\"ir.rule\"&gt;\n    &lt;field name=\"name\"&gt;Factura: solo de mi sucursal&lt;\/field&gt;\n    &lt;field name=\"model_id\" ref=\"account.model_account_move\"\/&gt;\n    &lt;field name=\"domain_force\"&gt;\n        [('branch_id', 'in', user.branch_ids.ids)]\n    &lt;\/field&gt;\n    &lt;field name=\"groups\" eval=\"[(4, ref('account.group_account_invoice'))]\"\/&gt;\n    &lt;field name=\"perm_read\" eval=\"True\"\/&gt;\n    &lt;field name=\"perm_write\" eval=\"True\"\/&gt;\n    &lt;field name=\"perm_create\" eval=\"True\"\/&gt;\n    &lt;field name=\"perm_unlink\" eval=\"False\"\/&gt;\n&lt;\/record&gt;\n<\/code><\/pre>\n<p>Tres cosas a recordar con record rules:<\/p>\n<ol>\n<li><strong>Se eval\u00faa con permisos elevados.<\/strong> Dentro del <code>domain_force<\/code> tienes acceso a <code>user<\/code> (recordset del usuario actual) sin restricciones.<\/li>\n<li><strong>Las reglas del mismo grupo se unen con OR<\/strong>; las de grupos distintos con AND. Esto importa cuando hay varias reglas que tocan el mismo modelo.<\/li>\n<li>Si un usuario es <strong>superuser<\/strong> o el contexto trae <code>bypass_rules<\/code>, la regla no aplica.<\/li>\n<\/ol>\n<h2>6. Dominios multi-compa\u00f1\u00eda<\/h2>\n<p>En multi-compa\u00f1\u00eda, Odoo trae <code>allowed_company_ids<\/code> en el contexto: la lista de IDs de las compa\u00f1\u00edas que el usuario tiene activas en el selector superior derecho. \u00dasala en lugar de hardcodear.<\/p>\n<pre><code class=\"language-python\"># Mal: solo ve la compa\u00f1\u00eda \"principal\"\n[('company_id', '=', user.company_id.id)]\n\n# Bien: respeta el selector multi-compa\u00f1\u00eda\n[('company_id', 'in', allowed_company_ids)]\n<\/code><\/pre>\n<p>En las record rules es exactamente igual:<\/p>\n<pre><code class=\"language-xml\">&lt;field name=\"domain_force\"&gt;\n    ['|', ('company_id', '=', False),\n          ('company_id', 'in', allowed_company_ids)]\n&lt;\/field&gt;\n<\/code><\/pre>\n<p>El <code>'|'<\/code> con <code>company_id = False<\/code> es para los registros compartidos (sin compa\u00f1\u00eda asignada).<\/p>\n<h2>7. Tips de producci\u00f3n<\/h2>\n<ul>\n<li><strong>Debug f\u00e1cil:<\/strong> en el modo desarrollador, abre <em>Filtros &gt; A\u00f1adir filtro personalizado<\/em> y arma el dominio visualmente. Despu\u00e9s copia el resultado del developer tooltip.<\/li>\n<li><strong>No mezcles tipos:<\/strong> <code>('field', '=', False)<\/code> es muy distinto a <code>('field', '=', 0)<\/code>. Para campos relacionales vac\u00edos, siempre <code>= False<\/code>.<\/li>\n<li><strong>Performance:<\/strong> dominios con muchos <code>like<\/code> sobre tablas grandes son lentos. Si vas a filtrar millones de registros, aseg\u00farate de tener el campo indexado (<code>index=True<\/code> en la definici\u00f3n).<\/li>\n<li><strong>Evita <code>search().ids<\/code> innecesario:<\/strong> en lugar de <code>[('id', 'in', self.env['x'].search([...]).ids)]<\/code>, casi siempre puedes hacer un dominio relacional con <code>.<\/code> (ej: <code>('partner_id.country_id', '=', mx_id)<\/code>).<\/li>\n<\/ul>\n<h2>Cierre<\/h2>\n<p>Los dominios son la base de c\u00f3mo Odoo expresa filtros en todos los rincones: vistas, acciones, record rules, m\u00e9todos de b\u00fasqueda. Dominarlos te ahorra much\u00edsimo c\u00f3digo Python repetitivo y te permite implementar reglas de negocio (visibilidad por sucursal, restricciones por rol, vistas contextuales) usando configuraci\u00f3n en lugar de overrides.<\/p>\n<p>Si en tu implementaci\u00f3n tienes casos de visibilidad complejos (varias sucursales, m\u00faltiples compa\u00f1\u00edas, vendedores con territorios), antes de meterte a sobrescribir <code>_search<\/code>, revisa si una record rule bien dise\u00f1ada no resuelve el problema con menos riesgo.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Gu\u00eda t\u00e9cnica de dominios en Odoo 19: sintaxis, operadores l\u00f3gicos, contexto, dominios din\u00e1micos en vistas, record rules y multi-compa\u00f1\u00eda.<\/p>\n","protected":false},"author":1,"featured_media":2052,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[3],"tags":[],"class_list":["post-2051","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-base"],"_links":{"self":[{"href":"https:\/\/exdoo.mx\/blogs\/wp-json\/wp\/v2\/posts\/2051","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/exdoo.mx\/blogs\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/exdoo.mx\/blogs\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/exdoo.mx\/blogs\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/exdoo.mx\/blogs\/wp-json\/wp\/v2\/comments?post=2051"}],"version-history":[{"count":0,"href":"https:\/\/exdoo.mx\/blogs\/wp-json\/wp\/v2\/posts\/2051\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/exdoo.mx\/blogs\/wp-json\/wp\/v2\/media\/2052"}],"wp:attachment":[{"href":"https:\/\/exdoo.mx\/blogs\/wp-json\/wp\/v2\/media?parent=2051"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/exdoo.mx\/blogs\/wp-json\/wp\/v2\/categories?post=2051"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/exdoo.mx\/blogs\/wp-json\/wp\/v2\/tags?post=2051"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}