The Permission Problem
Your D2C sales manager:
Can see customer list ✓
Can see their own orders ✓
Tries to see invoice details ✗ "Access Denied"
Can't figure out why
Your developer checks permissions:
User has "Sales" group ✓
Invoice model allows read access ✓
But still "Access Denied" ✗
The Problem: You're missing Layer 2: Record Rules.
Odoo Has Two Permission Layers
Layer 1: Access Rights
Can you access the model?
Layer 2: Record Rules
Which records can you see?
Both must pass. If either denies access, user can't see the data.
Common Breakdown
| Issue | Percentage |
|---|---|
| Missing ACL | 40% |
| Wrong Record Rules | 50% |
| Field Visibility | 10% |
We've implemented 150+ Odoo systems. The ones where admins understand both layers? Perfect permission control, zero "access denied" complaints. The ones where they don't? Constant frustration, users complain constantly, admins give everyone blanket permissions (security disaster). That's $30,000-$80,000 in wasted troubleshooting time and compromised security.
The Two Permission Layers (The Foundation)
Layer 1: Access Control Lists (ACL) - Model Level
Question: Does this user have READ permission on sale.order model?
Answer: Check ir.model.access.csv
User Jane in "Sales" group
access_sale_order_salesman,sale.order.salesman,sale.model_sale_order,sales_team.group_sale_salesman,1,1,1,0
↑ = READ permission = YES
Result: Jane CAN access sale.order model
Layer 2: Record Rules - Record Level
Question: Which sale.order records can Jane see?
Answer: Check ir.rule
<record id="sale_order_rule" model="ir.rule">
<field name="domain_force">[('user_id', '=', user.id)]</field>
</record>
Result: Jane can only see orders where user_id = Jane
Other salespeople's orders: HIDDEN
Both Must Allow Access
Jane accesses Invoice #123:
Layer 1 Check: Does Jane have read permission on account.move?
→ Check ACL → YES ✓
Layer 2 Check: Is Invoice #123 in Jane's allowed records?
→ Check Record Rules → domain_force applied
→ If invoice matches domain → YES ✓
→ If invoice doesn't match → NO ✗
Result: If either says NO → "Access Denied"
Common Issue 1: Missing ACL (Access Denied)
Problem: User tries to access model, gets "Access Denied."
Cause: No entry in ir.model.access.csv for that group/model.
Example: Missing Entry
# ir.model.access.csv
# Missing invoice access!
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_order_salesman,sale.order salesman,sale.model_sale_order,sales_team.group_sale_salesman,1,1,1,0
# ← No entry for account.move!
# When salesman tries to view invoice → AccessError
Fix: Add Missing ACL Entry
# Add missing ACL entry
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_order_salesman,sale.order salesman,sale.model_sale_order,sales_team.group_sale_salesman,1,1,1,0
access_invoice_salesman,account.move salesman,account.model_account_move,sales_team.group_sale_salesman,1,0,0,0
# ↑ Added invoice read access (no write/create/delete)
Debugging
# In Python console
user = self.env['res.users'].browse(user_id)
print(user.groups_id) # What groups is user in?
# Check if user can read model
try:
self.env['account.move'].check_access_rights('read')
print("✓ User can read account.move")
except:
print("✗ User cannot read account.move (missing ACL)")
Common Issue 2: Wrong Record Rules (Silent Hiding)
Problem: User can access model, but can't see specific records. No error message, just empty list.
Cause: Record rule domain doesn't match the records user is trying to view.
Example: Silent Filtering
# Salesman Jane tries to view all orders
orders = self.env['sale.order'].search([])
# Result: Shows only Jane's orders (record rule filters silently)
# She expects: All orders for her territory
# She gets: Only her personal orders
# No error, just different data
# This is SILENT filtering (hardest to debug!)
Real Scenario: Wrong Domain
<!-- WRONG: Salesman only sees own orders -->
<record id="sale_order_rule_salesman" model="ir.rule">
<field name="name">Order - Salesman Own</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>
<!-- This filters by user_id, but what if orders don't have user_id? -->
</record>
# Salesman Jane with user_id = 10
# Order #1: user_id = 10 → VISIBLE ✓
# Order #2: user_id = 11 (another rep) → HIDDEN ✗
# Order #3: user_id = False (no owner) → HIDDEN ✗
# Jane thinks orders are missing, but actually they don't match domain
Fix: Right Record Rule Domain
<!-- RIGHT: Salesman sees own + team orders -->
<record id="sale_order_rule_salesman_own_team" model="ir.rule">
<field name="name">Order - Salesman Own & Team</field>
<field name="model_id" ref="sale.model_sale_order"/>
<field name="groups" eval="[(4, ref('sales_team.group_sale_salesman'))]"/>
<!-- Allow both own orders AND team orders -->
<field name="domain_force">
['|',
('user_id', '=', user.id),
('team_id', '=', user.sale_team_id.id)
]
</field>
</record>
Debugging Record Rules
# In Python console, simulate user's view
user = self.env['res.users'].browse(user_id)
# What records does this user see?
with self.env.with_user(user):
visible_orders = self.env['sale.order'].search([])
print(f"User can see {len(visible_orders)} orders")
# Check if specific order is visible
order = self.env['sale.order'].browse(order_id)
try:
order.check_access_rule('read')
print(f"✓ Order {order.id} is visible")
except:
print(f"✗ Order {order.id} is hidden by record rule")
Common Issue 3: Field Visibility (Hiding Specific Fields)
Problem: User can see record, but specific fields are hidden.
Cause: Field has groups= attribute restricting visibility.
Example: Field Restriction
class SaleOrder(models.Model):
_inherit = 'sale.order'
# Everyone can see
customer = fields.Many2one('res.partner')
# Only managers can see
cost_price = fields.Float(
groups='sales_team.group_sale_manager' # Hidden from salesman!
)
# Only accountants can see
profit_margin = fields.Float(
groups='account.group_account_accountant'
)
In Form View
<field name="cost_price" groups="sales_team.group_sale_manager"/>
<!-- Only visible to managers -->
Debugging Field Visibility
# Check if field visible to user
field = self.env['sale.order']._fields['cost_price']
print(field.groups) # ('sales_team.group_sale_manager',)
user = self.env['res.users'].browse(user_id)
print(user.groups_id.mapped('id')) # User's groups
# Is user in required group?
has_group = user.has_group('sales_team.group_sale_manager')
print(f"User can see cost_price: {has_group}")
Real D2C Example: Complete Permission Troubleshooting
Scenario: Sales rep reports "I can't see customer invoices!"
Step 1: Check ACL (Layer 1)
# Does user have read permission on account.move?
user = self.env['res.users'].search([('login', '=', 'john@example.com')])
print(f"User groups: {user.groups_id.mapped('name')}")
# Check access rights
try:
self.env['account.move'].check_access_rights('read')
print("✓ Layer 1 OK: User has read access on account.move")
except AccessError as e:
print(f"✗ Layer 1 FAIL: {e}")
# Add to ir.model.access.csv
Step 2: Check Record Rules (Layer 2)
# With user context, what invoices can they see?
with self.env.with_user(user):
invoices = self.env['account.move'].search([])
print(f"User can see {len(invoices)} invoices")
# Find which invoices they CAN'T see
all_invoices = self.env['account.move'].search([], count=True)
invisible = all_invoices - len(invoices)
print(f"User cannot see {invisible} invoices (filtered by record rules)")
# Check a specific invoice
invoice = self.env['account.move'].browse(invoice_id)
try:
invoice.check_access_rule('read')
print(f"✓ Invoice {invoice.id} matches record rule")
except AccessError:
print(f"✗ Invoice {invoice.id} filtered out by record rule")
print(f" Rule domain: {invoice._rule_domain}")
Step 3: Check Field Visibility
# Are there hidden fields?
move = self.env['account.move']
for field_name, field in move._fields.items():
if field.groups:
print(f"Field '{field_name}' restricted to: {field.groups}")
# Can user see this field?
if user.has_group(field.groups[0]):
print(f" ✓ User can see {field_name}")
else:
print(f" ✗ User cannot see {field_name}")
Complete Fix
# Solution: Add proper ACL + Record Rule
# 1. ir.model.access.csv
access_invoice_salesman,account.move salesman,account.model_account_move,sales_team.group_sale_salesman,1,0,0,0
# 2. ir_rule.xml
<record id="invoice_rule_salesman" model="ir.rule">
<field name="name">Invoice - Related to customer</field>
<field name="model_id" ref="account.model_account_move"/>
<field name="groups" eval="[(4, ref('sales_team.group_sale_salesman'))]"/>
<!-- See invoices for customers they sold to -->
<field name="domain_force">[('partner_id', 'in', user.partner_ids.ids)]</field>
</record>
# 3. Test
user = self.env['res.users'].search([('login', '=', 'john@example.com')])
with self.env.with_user(user):
invoices = self.env['account.move'].search([])
print(f"✓ User can now see {len(invoices)} invoices")
Your Action Items
When User Reports "Access Denied"
❏ Step 1: Check ACL (does group have model read permission?)
❏ Step 2: Check Record Rules (do records match domain?)
❏ Step 3: Check field visibility (are specific fields hidden?)
❏ Step 4: Clear cache and test again
For Debugging
❏ Use Python console to test permissions
❏ Check both layers (ACL + Record Rules)
❏ Test with specific user in user context
❏ Log which records are filtered out
Best Practices
❏ Document your permission structure
❏ Test as different user roles
❏ Use meaningful rule names
❏ Log permission changes
Free Permission Troubleshooting Workshop
Stop wasting time debugging permissions. Most D2C brands have permission issues causing frustration. Fixing them improves usability $20,000-$50,000/year. We'll review your current permission structure, identify missing ACLs, fix wrong record rules, test as different user roles, and document everything.
