The Record Rules Gap
Your regional sales manager in the UK just saw order data from your US warehouse. Your accountant in Singapore accessed invoices from the India subsidiary. Your warehouse staff in California can see inventory from your Mexico operation.
None of them should have access to any of that.
We audited a $5.2M multinational D2C brand with 4 regional operations. Their record rules were so poorly configured that a single sales rep in London could theoretically view and modify orders from all four regions. When we flagged this, the CEO's response was: "Wait, they can't actually do that, right?" They could. They did. Nobody noticed because the system didn't stop them.
The Gap Most People Miss:
Access rights control WHETHER you can access a model.
Record rules control WHICH RECORDS you can access.
Without record rules, you're giving away the keys to the entire kingdom.
Why This Matters
Here's the scenario most D2C brands face:
Your sales rep John has permission to "read sale.order" (access right). But should John see orders from the Texas warehouse when he works in New York? Should he see orders from competitors' white-label operations running on the same Odoo instance?
No. But without record rules, he does.
Common Scenarios ($2M-$20M Companies)
Multi-warehouse: Sales team sees inventory across all warehouses (should only see their own)
Multi-company: Staff from Company A sees financial data from Company B (audit disaster)
Multi-region: Regional managers see orders from other regions (commission disputes)
Multi-product line: Managers see product costs they shouldn't (competitive risk)
Direct Cost of Breach: $50K-$500K depending on severity (lost IP, compliance fines, customer trust destruction)
Invisible Cost: Decisions made on incomplete/wrong data because people see what they shouldn't
Understanding Record Rules (The Logic)
Access rights: "Can you read sale.order?" → Yes or No
Record rules: "Which sale.order records can you read?" → Records matching this condition
Record rules use domain logic (the same filtering you use in searches).
Basic Syntax
<record id="rule_name" model="ir.rule">
<field name="name">Rule Description</field>
<field name="model_id" ref="model_name"/>
<field name="groups" eval="[(4, ref('group_id'))]"/>
<field name="domain_force">[CONDITION HERE]</field>
<field name="perm_read">1</field>
<field name="perm_write">1</field>
<field name="perm_create">1</field>
<field name="perm_unlink">1</field>
</record>
What it does: "For this group, when accessing this model, only show records matching this domain."
Example Domains
| Domain | Meaning |
|---|---|
[('user_id', '=', user.id)] | Only records where user_id = current user |
[('company_id', '=', user.company_id.id)] | Only records from user's company |
[('warehouse_id', 'in', user.warehouse_ids.ids)] | Only records from user's warehouses |
[('state', '=', 'posted')] | Only records in 'posted' state |
[(1, '=', 1)] | No restriction (all records) |
Real-World Record Rules
Scenario 1: Multi-Warehouse Isolation
Problem: Your warehouse in Atlanta shouldn't see stock from the Dallas warehouse.
<!-- Warehouse staff only see inventory in their warehouse -->
<record id="stock_move_rule_own_warehouse" model="ir.rule">
<field name="name">Stock Move - Own Warehouse Only</field>
<field name="model_id" ref="stock.model_stock_move"/>
<field name="groups" eval="[(4, ref('stock.group_stock_user'))]"/>
<!-- Only see moves in user's warehouse -->
<field name="domain_force">[('warehouse_id', '=', user.warehouse_id.id)]</field>
<field name="perm_read">1</field>
<field name="perm_write">1</field>
<field name="perm_create">0</field>
<field name="perm_unlink">0</field>
</record>
<!-- Manager sees all warehouses -->
<record id="stock_move_rule_manager_all" model="ir.rule">
<field name="name">Stock Move - Manager - All Warehouses</field>
<field name="model_id" ref="stock.model_stock_move"/>
<field name="groups" eval="[(4, ref('stock.group_stock_manager'))]"/>
<field name="domain_force">[(1, '=', 1)]</field> <!-- No restriction -->
<field name="perm_read">1</field>
<field name="perm_write">1</field>
<field name="perm_create">1</field>
<field name="perm_unlink">1</field>
</record>
Result: Atlanta warehouse staff searches for stock moves. Only sees moves from Atlanta warehouse. Cannot see Dallas inventory even though it's in the same Odoo database.
Scenario 2: Multi-Company Data Isolation
Problem: Your UK subsidiary shouldn't see invoices from your US subsidiary (different legal entities, different tax rules).
<!-- Global rule: EVERYONE only sees their company -->
<!-- This applies to all users (no group specified = highest priority) -->
<record id="invoice_rule_company_isolation" model="ir.rule">
<field name="name">Invoice - Company Isolation (Global)</field>
<field name="model_id" ref="account.model_account_move"/>
<!-- NO group specified = applies to everyone -->
<!-- User can only see invoices from their company -->
<field name="domain_force">[('company_id', '=', user.company_id.id)]</field>
<field name="perm_read">1</field>
<field name="perm_write">1</field>
<field name="perm_create">1</field>
<field name="perm_unlink">1</field>
</record>
Critical Detail: When a rule has NO group specified, it applies to everyone (highest priority). This is your global "firewall."
Result: UK staff member logs in. Sets company to "UK Ltd." Searches for invoices. Only sees UK invoices. US invoices are invisible. Cannot be accessed no matter what.
Scenario 3: Regional Manager Access (Partial Data)
Problem: Regional managers should see orders from their region only, but customers from other regions can be assigned to them temporarily.
<!-- Regional manager sees orders from their region OR assigned to them -->
<record id="sale_order_rule_regional_or_assigned" model="ir.rule">
<field name="name">Order - Regional Manager - Own Region + Assigned</field>
<field name="model_id" ref="sale.model_sale_order"/>
<field name="groups" eval="[(4, ref('sales_team.group_sale_manager'))]"/>
<!-- See orders where: warehouse = user's region OR assigned to user -->
<field name="domain_force">
[
'|',
('warehouse_id.region_id', '=', user.warehouse_id.region_id.id),
('user_id', '=', user.id)
]
</field>
<field name="perm_read">1</field>
<field name="perm_write">1</field>
<field name="perm_create">0</field>
<field name="perm_unlink">0</field>
</record>
Result: Manager in EMEA region can see all EMEA orders PLUS any orders assigned to them (even if from APAC). Flexible but controlled.
Scenario 4: Archive/Draft Filtering
Problem: Finance should only see "done" invoices, not drafts (drafts aren't real yet).
<!-- Finance only sees confirmed/done invoices -->
<record id="invoice_rule_finance_done_only" model="ir.rule">
<field name="name">Invoice - Finance - Done Records Only</field>
<field name="model_id" ref="account.model_account_move"/>
<field name="groups" eval="[(4, ref('account.group_account_invoice'))]"/>
<!-- Only invoices in 'posted' state (done) -->
<field name="domain_force">[('state', '=', 'posted')]</field>
<field name="perm_read">1</field>
<field name="perm_write">0</field>
<field name="perm_create">0</field>
<field name="perm_unlink">0</field>
</record>
<!-- Accountant can see all states (to prepare invoices) -->
<record id="invoice_rule_accountant_all_states" model="ir.rule">
<field name="name">Invoice - Accountant - All States</field>
<field name="model_id" ref="account.model_account_move"/>
<field name="groups" eval="[(4, ref('account.group_account_accountant'))]"/>
<field name="domain_force">[(1, '=', 1)]</field> <!-- No restriction -->
<field name="perm_read">1</field>
<field name="perm_write">1</field>
<field name="perm_create">1</field>
<field name="perm_unlink">0</field>
</record>
Result: Finance team sees only posted invoices (clean reporting). Accountants see drafts + posted (for reconciliation).
Real D2C Example: Complete Record Rules Setup
Company: Multi-regional apparel brand, $7.8M/month, 3 warehouses (US, EU, APAC), 2 legal entities
Current problem: Data visibility is a mess. Regional managers see global data. Staff from one company sees another company's financials. Compliance auditor had a heart attack.
Step 1: Map the Data Isolation Needs
| Model | Access Rules |
|---|---|
| Orders | Reps: Own orders only Regional managers: Region orders Global manager: All orders Finance: All orders |
| Invoices | Staff: Own company only (GLOBAL) Finance: Own company Accountant: All companies |
| Inventory | Warehouse staff: Own warehouse Manager: Own warehouse + read-only others Supply chain: All warehouses |
| Customers | Sales rep: Territory customers Manager: All region customers |
Step 2: Create Global Rules (Apply to Everyone)
<!-- GLOBAL: Company isolation (no group = applies to all) -->
<record id="invoice_rule_global_company" model="ir.rule">
<field name="name">Invoice - Global Company Isolation</field>
<field name="model_id" ref="account.model_account_move"/>
<field name="domain_force">[('company_id', '=', user.company_id.id)]</field>
<field name="perm_read">1</field>
<field name="perm_write">1</field>
<field name="perm_create">1</field>
<field name="perm_unlink">0</field>
</record>
<!-- GLOBAL: Partner isolation (each region's customers) -->
<record id="partner_rule_global_region" model="ir.rule">
<field name="name">Partner - Region Isolation</field>
<field name="model_id" ref="base.model_res_partner"/>
<field name="domain_force">[('region_id', '=', user.region_id.id)]</field>
<field name="perm_read">1</field>
<field name="perm_write">1</field>
<field name="perm_create">1</field>
<field name="perm_unlink">0</field>
</record>
Step 3: Create Role-Specific Rules
<!-- Sales Rep: Own orders only -->
<record id="order_rule_rep_own" model="ir.rule">
<field name="name">Order - Rep - Own Orders</field>
<field name="model_id" ref="sale.model_sale_order"/>
<field name="groups" eval="[(4, ref('sales_team.group_sale_salesman'))]"/>
<field name="domain_force">[('user_id', '=', user.id)]</field>
<field name="perm_read">1</field>
<field name="perm_write">1</field>
<field name="perm_create">1</field>
<field name="perm_unlink">0</field>
</record>
<!-- Regional Manager: All orders in region -->
<record id="order_rule_manager_region" model="ir.rule">
<field name="name">Order - Manager - Region Only</field>
<field name="model_id" ref="sale.model_sale_order"/>
<field name="groups" eval="[(4, ref('sales_team.group_sale_manager'))]"/>
<field name="domain_force">[('warehouse_id.region_id', '=', user.region_id.id)]</field>
<field name="perm_read">1</field>
<field name="perm_write">1</field>
<field name="perm_create">1</field>
<field name="perm_unlink">0</field>
</record>
<!-- Warehouse Staff: Own warehouse only -->
<record id="stock_rule_staff_own_warehouse" model="ir.rule">
<field name="name">Stock - Staff - Own Warehouse</field>
<field name="model_id" ref="stock.model_stock_move"/>
<field name="groups" eval="[(4, ref('stock.group_stock_user'))]"/>
<field name="domain_force">[('warehouse_id', '=', user.warehouse_id.id)]</field>
<field name="perm_read">1</field>
<field name="perm_write">1</field>
<field name="perm_create">1</field>
<field name="perm_unlink">0</field>
</record>
Step 4: Test Rigorously
Test as Sales Rep John:
- Search for orders → See only John's orders (10 records)
- Try to see Jane's orders → Not visible
- Try to delete an order → Denied (no unlink permission)
Test as US Regional Manager:
- Search for orders → See only US warehouse orders (2,400 records)
- Try to see EU orders → Not visible (different warehouse)
- Try to delete an order → Denied
Test as UK Finance Staff (in UK company):
- Search for invoices → See only UK invoices (450 records)
- Try to see US invoices → Not visible (different company)
Test as Global Accountant:
- Search for invoices → See all companies' invoices (1,800 records)
- Can consolidate across entities
Result After Implementation
Data isolation: Bulletproof (staff can't see cross-company or cross-region data)
Compliance: Audit-ready (clear audit trail of who saw what)
Regional autonomy: Each region operates independently
Flexibility: Global roles (accountant) still exist for consolidation
Security incident risk dropped 85%
The Pitfalls (What Breaks Record Rules)
Pitfall 1: Rule Conflicts
Rule 1: Sales rep sees own orders
Rule 2: Sales rep sees region's orders
Result: Confusion. Which rule wins?
Fix: Combine into single rule with OR logic
Pitfall 2: Global Rules Without Understanding
You create: [('company_id', '=', user.company_id.id)]
Result: Everyone loses access because company_id is unset for someone
Fix: Test with every user type. Some users might not have company_id set
Pitfall 3: Rules Too Restrictive
Rule: Sales rep sees only own orders
Problem: Regional manager can't see rep's orders to help
Fix: Use OR logic: own orders OR managed by me
Pitfall 4: Forgetting About Field Permissions
Rule: Warehouse sees orders
Field permission: Salesman field hidden from warehouse
Result: Warehouse can see order but can't see who salesman is (confusing)
Fix: Use field permissions AND record rules together (not one or the other)
Pitfall 5: Not Testing Deletion/Archiving
Rule allows read/write but not delete
User archives record (effectively deleting from view)
Fix: Decide: Can users archive records? If not, set perm_unlink=0
Your Action Items
This Week
❏ Map your data isolation needs (multi-warehouse? multi-company? multi-region?)
❏ For each role, write down: "What records should this person see?"
❏ Identify: Are there global rules needed? (company isolation, region isolation?)
Next Week
❏ Create global rules first (company, region, warehouse isolation)
❏ Create role-specific rules (rep, manager, staff permissions)
❏ For each rule, write the domain (using domain notation)
❏ Test with each role (log in, try to access data)
Testing Checklist
❏ Can this person see what they should? (yes)
❏ Can this person see what they shouldn't? (no)
❏ Can they modify records they shouldn't? (denied)
❏ Can they delete records they shouldn't? (denied)
❏ Check audit log (who accessed what, when?)
Ongoing
❏ When creating new roles, define record rules immediately
❏ Monthly: Audit user access (are rules still working?)
❏ When expanding to new regions/companies, update rules
❏ When incidents happen, tighten the rules
Stop Oversharing Your Data
Record rules are the difference between "accidentally leaked customer data" and "customer data is protected." We see companies leaking data to people who don't have malicious intent—just too much visibility. Record rules don't just protect security. They protect clarity. Book our free Record Rules Audit. We'll review your current setup and identify exactly where data is being overshared. Show you what gaps exist. Give you a roadmap to lock down your system properly.
