Skip to content

Top 20 Odoo Development Anti-Patterns to Avoid

DeployMonkey Team · March 22, 2026 16 min read

Why Anti-Patterns Matter

Bad Odoo code doesn't just fail — it fails slowly, at scale, and in production. These 20 anti-patterns cause: performance degradation, security holes, data corruption, and upgrade failures. Each is a real mistake seen in production code.

ORM Anti-Patterns

1. N+1 Queries in Loops

# BAD — each iteration triggers a separate query
for order in orders:
    partner_name = order.partner_id.name  # lazy load each
    country = order.partner_id.country_id.name  # another query

# GOOD — prefetch all at once
orders.mapped('partner_id')  # prefetch
orders.mapped('partner_id.country_id')  # prefetch relation
for order in orders:
    partner_name = order.partner_id.name  # from cache
    country = order.partner_id.country_id.name  # from cache

2. Using sudo() to Fix Access Errors

# BAD — masks the real problem, creates security holes
records = self.env['sale.order'].sudo().search([])

# GOOD — fix the actual permissions
# Add proper ir.model.access.csv entries
# Adjust record rules
# Grant the correct security group

3. Using onchange for Calculated Values

# BAD — only fires from UI, not API/imports/cron
@api.onchange('price', 'qty')
def _onchange_total(self):
    self.total = self.price * self.qty

# GOOD — fires on any write
@api.depends('price', 'qty')
def _compute_total(self):
    for rec in self:
        rec.total = rec.price * rec.qty

4. Hardcoding Database IDs

# BAD — IDs differ between databases
partner = self.env['res.partner'].browse(42)

# GOOD — use XML IDs
partner = self.env.ref('base.main_partner')

5. search() Inside Computed Fields Without store=True

# BAD — search runs on EVERY read of every record
@api.depends('partner_id')
def _compute_order_count(self):
    for rec in self:
        rec.order_count = self.env['sale.order'].search_count(
            [('partner_id', '=', rec.partner_id.id)]  # N queries!
        )

# GOOD — use read_group for aggregation
@api.depends('partner_id')
def _compute_order_count(self):
    data = self.env['sale.order'].read_group(
        [('partner_id', 'in', self.mapped('partner_id').ids)],
        ['partner_id'], ['partner_id']
    )
    mapped_data = {d['partner_id'][0]: d['partner_id_count'] for d in data}
    for rec in self:
        rec.order_count = mapped_data.get(rec.partner_id.id, 0)

6. Creating Records One by One

# BAD — N insert queries
for vals in vals_list:
    self.env['my.model'].create(vals)

# GOOD — batch create (one query)
self.env['my.model'].create(vals_list)

7. Ignoring @api.depends Dependencies

# BAD — missing dependency, field doesn't update
@api.depends('price')  # Missing 'discount'!
def _compute_final_price(self):
    for rec in self:
        rec.final_price = rec.price * (1 - rec.discount / 100)

View Anti-Patterns

8. Using attrs (Odoo 17+ Deprecated)

# BAD (Odoo 17+)
<field name="x" attrs="{'invisible': [('state', '!=', 'draft')]}"/>

# GOOD (Odoo 17+)
<field name="x" invisible="state != 'draft'"/>

9. Using <tree> Instead of <list> (Odoo 18+)

# BAD (deprecated)
<tree>...</tree>

# GOOD
<list>...</list>

10. XPath Matching Too Broadly

# BAD — might match wrong element
<xpath expr="//field" position="after">

# GOOD — specific selector
<xpath expr="//field[@name='phone']" position="after">

Security Anti-Patterns

11. No ir.model.access for Custom Models

Every custom model needs access rules. Without them, all users get AccessError.

12. SQL Injection via String Formatting

# BAD — SQL injection vulnerability!
self.env.cr.execute(f"SELECT * FROM res_partner WHERE name = '{user_input}'")

# GOOD — parameterized query
self.env.cr.execute("SELECT * FROM res_partner WHERE name = %s", [user_input])

13. Exposing Tracebacks in API

# BAD — shows internal details to API consumers
@http.route('/api/data', type='json', auth='public')
def get_data(self):
    return self.env['my.model'].search_read([])  # Crashes expose traceback

# GOOD — catch and format errors
try:
    return self.env['my.model'].sudo().search_read([], fields=['name'])
except Exception as e:
    return {'error': 'An error occurred'}

Performance Anti-Patterns

14. Not Using Indexes on Filtered Fields

# BAD — full table scan on every search
code = fields.Char()

# GOOD — indexed for fast search
code = fields.Char(index=True)

15. Storing Large Data in Database

# BAD — huge file stored as base64 in DB
file_data = fields.Binary(attachment=False)

# GOOD — stored in filestore
file_data = fields.Binary(attachment=True)

Architecture Anti-Patterns

16. Modifying Core Odoo Files

Never edit files in the odoo/ directory. Always use inheritance in a custom module.

17. Circular Module Dependencies

Module A depends on B, B depends on A. Refactor into a third module or merge.

18. God Module (Everything in One)

One module with 50 models and 100 views. Split into logical modules with clear boundaries.

19. Ignoring noupdate Flag

Views should be noupdate='0' (update on upgrade). Email templates should be noupdate='1' (preserve customizations).

20. Not Writing Tests

If it's not tested, it's broken — you just don't know it yet.

DeployMonkey

DeployMonkey's AI agent detects anti-patterns in custom modules. It reviews code for N+1 queries, sudo abuse, missing indexes, and security vulnerabilities — catching problems before they reach production.