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) andtime. - 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_readoff 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:
<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 describes the full output, and Odoo customization covers changes to standard apps.