# Record rules (Odoo glossary)

Canonical: https://erpfly.com/glossary/record-rules/
Last updated: September 14, 2026

**Definition:** Record rules are ir.rule records that limit which individual records of an Odoo model a user can read, write, create or delete. Each rule holds a domain that is checked record by record after access rights, so record rules can narrow what ir.model.access.csv grants but never go beyond it.

Also called: ir.rule, record rule, Odoo record rules

An access line answers "can this user work with sale orders at all?" A record rule answers the next question: which sale orders. The standard sale module is a good model to copy. A salesperson in the "own documents" group sees orders where they're the salesperson or none is set, a salesperson with "all documents" sees everything, and a company rule sits over both.

### The fields that matter

- **model_id** is the model the rule applies to.
- **domain_force** is a domain, written as a Python expression. If a record matches, the operation is allowed. Inside it you can use `user` (the current user as a record), `company_id` (the active company's id), `company_ids` (a list of ids for every company the user can access) and `time`.
- **groups** decides the rule type. With no groups it's a global rule. With one or more groups it's a group rule.
- **perm_read, perm_write, perm_create, perm_unlink** mean something different from the access CSV. Here they pick which operations the rule is checked for, and they're all on by default. A rule with `perm_read` off doesn't stop anyone reading.

### How global and group rules combine

This is the part to get right, because the two types behave in opposite ways.

Global rules intersect. If two global rules apply, a record has to satisfy both. Every global rule you add can only take access away.

Group rules unify. Of the rules attached to the user's groups, a record only needs to satisfy one. Adding a group rule can widen what a user sees, but only within the limits the global rules set.

Then the two sets intersect with each other. So a manager whose group has an "all records" rule still can't see another company's data if a global company rule exists.

And if no rule applies to a model and operation, access is granted. Rules are default-allow, which is why a loose access line with no rules behind it is a real leak.

### An example

Bookings belong to a company and a user. Regular users see their own, managers see all of them, and no one sees another company's bookings:

```xml
<odoo>
    <record id="equipment_rental_company_rule" model="ir.rule">
        <field name="name">Equipment rental: multi-company</field>
        <field name="model_id" ref="model_equipment_rental"/>
        <field name="domain_force">[('company_id', 'in', company_ids)]</field>
    </record>

    <record id="equipment_rental_own_rule" model="ir.rule">
        <field name="name">Equipment rental: own bookings</field>
        <field name="model_id" ref="model_equipment_rental"/>
        <field name="groups" eval="[Command.link(ref('group_rental_user'))]"/>
        <field name="domain_force">['|', ('user_id', '=', user.id), ('user_id', '=', False)]</field>
    </record>

    <record id="equipment_rental_all_rule" model="ir.rule">
        <field name="name">Equipment rental: all bookings</field>
        <field name="model_id" ref="model_equipment_rental"/>
        <field name="groups" eval="[Command.link(ref('group_rental_manager'))]"/>
        <field name="domain_force">[(1, '=', 1)]</field>
    </record>
</odoo>
```

The company rule has no groups, so it's global. If the manager group implies the user group, a manager matches both group rules, and because group rules unify the `(1, '=', 1)` rule wins. This XML works unchanged on Odoo 17, 18 and 19. The sale module loads its rules inside `<odoo noupdate="1">`, which keeps an admin's edits safe from module updates but also means later changes to your XML won't reach an existing database on `-u`.

### Where rules get bypassed or misused

**`sudo()` ignores them.** So does raw SQL, since rules are enforced by the ORM. A controller that runs a search with `sudo()` hands back every record, whatever the rules say.

**Making the company rule a group rule.** Odoo's own docs warn about this. A multi-company rule attached to a group can be widened by any other group rule the user has, so keep it global.

**Stacking global rules.** Two global rules with no overlap in what they match remove all access to the model, for every user who isn't in superuser mode.

**Restricting with a group rule alone.** Putting a narrow rule on the user group does nothing for someone who also holds a group with a wider rule.

On a multi-company database, rules like the ones above belong in the addon next to the access lines, as module code that gets reviewed, rather than as records someone edited by hand in the settings. [Odoo module development](https://erpfly.com/odoo-module-development/) describes the full output, and [Odoo customization](https://erpfly.com/odoo-customization/) covers changes to standard apps.

See also: https://erpfly.com/glossary/ir-model-access-csv/, https://erpfly.com/glossary/odoo-addon/, https://erpfly.com/glossary/odoo-studio/

### Sources

- [Security in Odoo: Record Rules, Odoo 19 documentation](https://www.odoo.com/documentation/19.0/developer/reference/backend/security.html#record-rules)
- [Restrict access to data, Odoo 19 developer tutorial](https://www.odoo.com/documentation/19.0/developer/tutorials/restrict_data_access.html)
- [addons/sale/security/ir_rules.xml (19.0), odoo/odoo on GitHub](https://github.com/odoo/odoo/blob/19.0/addons/sale/security/ir_rules.xml)