Beyond CRUD: Advanced Coding Patterns Every Odoo Developer Should Master

February 20, 2026 by
Beyond CRUD: Advanced Coding Patterns Every Odoo Developer Should Master
Alfin Isnain Hariawan
| No comments yet

Most developers can build a model, create a view, and override a method in Odoo.

But real-world Odoo systems don’t fail because someone forgot a field.

They fail because of:

  • N+1 queries
  • Incorrect override patterns
  • Misused sudo()
  • Heavy logic inside onchange
  • Poor aggregation strategies
  • Breaking accounting integrity

If you're building production-grade modules, these patterns are not optional — they’re mandatory.


1️⃣ The N+1 Query Trap (And How to Eliminate It)

❌ Anti-Pattern

for partner in partners:
    moves = self.env['account.move.line'].search([
        ('partner_id', '=', partner.id)
    ])

If you have 1,000 partners, you execute 1,000 queries.

That’s how you silently destroy performance.

✅ Proper Pattern

move_lines = self.env['account.move.line'].search([
    ('partner_id', 'in', partners.ids)
])

grouped = {}
for line in move_lines:
    grouped.setdefault(line.partner_id.id, []).append(line)

Rule: Query once. Process in memory.


2️⃣ Use read_group() for Aggregations (Let PostgreSQL Work)

Instead of:

total = sum(
    self.env['account.move.line']
    .search(domain)
    .mapped('debit')
)

Use:

result = self.env['account.move.line'].read_group(
    domain=domain,
    fields=['debit:sum'],
    groupby=[]
)

Why?

Because read_group() pushes aggregation to PostgreSQL using GROUP BY.
Python loops should not replace database engines.


3️⃣ @api.onchange Is Not Business Logic

Many developers mistakenly treat onchange as persistent logic.

It is not.

It runs in memory, client-side.

❌ Dangerous Pattern

@api.onchange('picking_ids')
def _onchange_picking(self):
    for picking in self.picking_ids:
        self.line_ids += self.env['my.line'].new({
            'name': picking.name
        })

Risks:

  • Duplicate lines
  • Different behavior via RPC/API
  • Inconsistent data

✅ Safe Approach

Move logic to create() / write():

@api.model_create_multi
def create(self, vals_list):
    records = super().create(vals_list)
    for record in records:
        record._generate_lines()
    return records

Rule: Onchange = UI helper.
Create/Write = Business logic.


4️⃣ Override create() and write() the Right Way

Incorrect overrides break multi-record operations.

❌ Wrong

def create(self, vals):
    record = super().create(vals)

✅ Correct

@api.model_create_multi
def create(self, vals_list):
    records = super().create(vals_list)
    for rec in records:
        rec._post_create_logic()
    return records

Odoo often creates multiple records at once.
Ignoring this breaks imports and batch operations.


5️⃣ Computed Fields: Store or Not Store?

Non-stored:

  • Always recomputed
  • Heavy for large datasets

Stored:

  • Faster reads
  • Requires correct dependency declaration
amount_total = fields.Float(
    compute='_compute_amount',
    store=True
)

@api.depends('line_ids.price_subtotal')
def _compute_amount(self):
    for rec in self:
        rec.amount_total = sum(
            rec.line_ids.mapped('price_subtotal')
        )

If dependencies are wrong, your data silently becomes inaccurate.


6️⃣ When to Use Raw SQL

ORM is powerful — but not magical.

For large financial reports:

self.env.cr.execute("""
    SELECT partner_id, SUM(debit - credit)
    FROM account_move_line
    WHERE date BETWEEN %s AND %s
    GROUP BY partner_id
""", (date_from, date_to))

data = self.env.cr.fetchall()

Use SQL when:

  • Handling millions of rows
  • Complex joins
  • Heavy aggregation

But remember:

  • Security rules are bypassed
  • Multi-company logic must be handled manually


7️⃣ sudo() Is Not a Shortcut

❌ Bad Practice

self.env['res.partner'].sudo().search([])

This bypasses access rules entirely.

Use sudo() only for:

  • Scheduled jobs
  • System-level automation
  • Controlled internal processes

Never use it to “fix access errors”.


8️⃣ Protect Accounting Integrity

Accounting in Odoo is interconnected through:

  • account_move
  • account_move_line
  • account_partial_reconcile

Deleting or directly modifying records via SQL can break financial consistency permanently.

Best practices:

  • Use reversal instead of delete
  • Never update posted entries manually
  • Respect reconciliation structures


🚀 Final Thought

Writing Odoo code is easy.

Writing scalable, safe, and accounting-compliant Odoo code is a different skill entirely.

The difference between a junior developer and a system architect in Odoo is not about syntax.

It’s about:

  • Understanding ORM internals
  • Respecting accounting flows
  • Avoiding hidden performance killers
  • Designing for scale

If you master these patterns, you’re not just customizing Odoo.

You’re engineering financial infrastructure.
Beyond CRUD: Advanced Coding Patterns Every Odoo Developer Should Master
Alfin Isnain Hariawan February 20, 2026
Share this post
Archive
Sign in to leave a comment