VERSICH

Odoo ORM Explained: A Practical Guide for Developers

odoo orm explained: a practical guide for developers

Odoo ORM Explained for Developers 

When developing in Odoo, you rarely need to write SQL to create or update business records. Instead, you work with Python models, fields and methods through the Odoo ORM. 

Understanding the Odoo ORM is essential when building custom modules, automating business processes or troubleshooting unexpected behaviour. It also helps you avoid inefficient database calls and security mistakes. 

This guide explains the core concepts with practical examples you can adapt to a custom module. 

What Is the Odoo ORM? 

ORM stands for Object-Relational Mapping. It connects Python objects to database records, allowing developers to work with business data through Odoo's model layer rather than manually writing SQL for every operation. 

For example, customer records belong to the res.partner model. You can search for a customer, read their details or update their information through the model's methods. 

partners = self.env['res.partner'].search([ 

    ('is_company', '=', True) 

]) 

 

for partner in partners: 

    print(partner.name) 

This example retrieves company contacts and prints their names. The domain filters the records, while the ORM handles the database interaction. 

The Odoo ORM also works with access rights, record rules, field definitions, relational data and the current execution environment. Those responsibilities make it more than a convenient database wrapper. 

How Models and Fields Work 

An Odoo model defines the structure and behaviour of a type of business record. Models can represent customers, products, sales orders, invoices or records introduced by a custom module. Not every business requirement needs a custom module. Before changing models or adding new logic, assess whether standard Odoo functionality can meet the requirement. When existing features need to be extended with new fields, workflows, or business logic, our Odoo Customization Services help businesses adapt Odoo to their operational needs. Our guide to choosing between standard Odoo features and custom code explains how to decide when customization is necessary. 

A model typically defines fields and methods. 

from odoo import models, fields 

 

class ServiceRequest(models.Model): 

    _name = 'service.request' 

    _description = 'Service Request' 

 

    name = fields.Char(required=True) 

    customer_id = fields.Many2one('res.partner') 

    request_date = fields.Date() 

    status = fields.Selection([ 

        ('new', 'New'), 

        ('in_progress', 'In Progress'), 

        ('done', 'Done'), 

    ], default='new') 

Here, service.request represents a service request. The fields define its name, customer, date and status. If you want to see how models and fields fit into a working application, read our guide to building a custom module in Odoo, which walks through creating a model, defining fields, configuring access permissions, and adding views. 

Common field types include: 

  • Char and Text for text. 

  • Integer and Float for numeric values. 

  • Boolean for true-or-false values. 

  • Date and Datetime for dates and times. 

  • Many2one for a link to one record. 

  • One2many for related records accessed through an inverse relationship. 

  • Many2many for a relationship between multiple records on both sides. 

Choose field types according to the business relationship you need to represent. A relational field is usually more appropriate than storing another record's ID as an ordinary text value.  

Understanding Recordsets 

One of the most important concepts in the Odoo ORM is the recordset. 

A recordset is an ordered collection of records belonging to the same model. It can contain multiple records, one record or no records at all. Even when a method appears to operate on a single record, self is still a recordset. 

For example: 

partners = self.env['res.partner'].search([ 

    ('is_company', '=', True) 

]) 

 

print(len(partners)) 

The result is the number of matching records. If the search finds no companies, the recordset is empty. 

When you iterate over it, each item is a single-record recordset: 

for partner in partners: 

    print(partner.name) 

This distinction matters when writing model methods. Avoid assuming that self always contains one record. 

If a method genuinely requires exactly one record, use ensure_one(): 

def action_process_request(self): 

    self.ensure_one() 

    # Process this request. 

This raises an error when the method receives zero or multiple records, making the assumption explicit. 

How self.env Works 

The execution environment, available through self.env, gives your code access to the database cursor, current user, context and model registry. 

A common use is accessing another model: 

customers = self.env['res.partner'].search([ 

    ('customer_rank', '>', 0) 

]) 

The environment also carries context that can influence how operations behave. For example, language, company and active-record settings may affect the results or behaviour of a method. 

Be deliberate when using context modifiers such as with_context() and company modifiers such as with_company(). They can change the conditions under which code runs. 

1. What about sudo()? 

sudo() changes the access-rights environment in which an operation runs. It can bypass normal access checks, so it should not be used as a blanket fix for permission errors. 

Before using it, establish why the current user lacks access and whether the operation is supposed to cross that security boundary. Unnecessary use of sudo() can expose or modify records users should not be able to access. 

Searching for Records with Domains 

A domain specifies which records an ORM search should return. Each condition usually consists of a field name, an operator and a value. 

orders = self.env['sale.order'].search([ 

    ('state', '=', 'sale'), 

    ('amount_total', '>', 1000), 

]) 

This retrieves confirmed sales orders with a total above 1,000 in the database's configured currency context. The numeric comparison alone does not convert currencies. 

Common domain operators include: 

  • = and != for equality and inequality. 

  • > and < for numeric or comparable values. 

  • in and not in for membership checks. 

  • like and ilike for text matching, with ilike performing case-insensitive matching. 

For example: 

customers = self.env['res.partner'].search([ 

    ('name', 'ilike', 'north') 

], limit=20) 

The limit prevents the search from returning an unlimited number of records. When working with large datasets, filter in the domain instead of fetching many records and filtering them in Python. 

The Four Core Data Operations 

Developers commonly use four ORM methods to interact with records. 

1. create(): Create records 

request = self.env['service.request'].create({ 

    'name': 'Website access issue', 

    'status': 'new', 

}) 

create() accepts field values and returns the created record or recordset. In modern Odoo development, model creation methods should also be designed with batch creation in mind. 

2. search(): Find records 

requests = self.env['service.request'].search([ 

    ('status', '=', 'new') 

]) 

Use search() when you need records matching a domain. Use browse() when you already have record IDs and need a recordset for those IDs; browse() does not perform the same search for matching business conditions. 

3. write(): Update records 

requests.write({ 

    'status': 'in_progress' 

}) 

This updates all records in the recordset. If the recordset contains several requests, the change applies to all of them. 

That is useful for batch updates, but it also means you should confirm which records are in the recordset before writing. When ORM methods are used as part of a larger integration, developers also need to consider authentication, data validation, error handling, and how records are exchanged between systems. Our Odoo API integration guide explores these integration considerations in more detail. 

4. unlink(): Delete records 

request.unlink() 

unlink() deletes the selected records when permitted. Deletion may be blocked by access rights, business rules or relational constraints. 

Do not use it simply to hide a record from a workflow. Archiving or changing a status may be more appropriate, depending on the model and business requirements. 

Computed Fields and Dependencies 

Computed fields derive their values from other fields or records. They are useful when a value should follow from business data rather than being entered manually. 

from odoo import models, fields, api 

 

class ServiceRequest(models.Model): 

    _name = 'service.request' 

 

    name = fields.Char(required=True) 

    hours = fields.Float() 

    hourly_rate = fields.Float() 

    estimated_cost = fields.Float( 

        compute='_compute_estimated_cost', 

        store=True, 

    ) 

 

    @api.depends('hours', 'hourly_rate') 

    def _compute_estimated_cost(self): 

        for record in self: 

            record.estimated_cost = ( 

                record.hours * record.hourly_rate 

            ) 

The @api.depends decorator declares the fields that affect the result. Odoo uses these dependencies to determine when the computed value needs updating. 

Use store=True when storing the computed value is useful, for example, when it must be searched or grouped efficiently. Stored computed fields also introduce recomputation work when dependencies change, so storage should be a deliberate choice. 

A missing or incomplete dependency declaration can lead to stale computed values. If a computed field looks wrong, check the computation, its dependencies and whether the relevant source fields actually changed. 

Common Odoo ORM Mistakes 

1. Running database queries inside loops 

This pattern can generate many database queries: 

for partner in partners: 

    count = self.env['sale.order'].search_count([ 

        ('partner_id', '=', partner.id) 

    ]) 

For a large recordset, repeated searches can become expensive. Consider batching the operation with _read_group()  or another appropriate grouped query. 

2. Ignoring prefetching 

The ORM prefetches fields across recordsets to reduce repeated database reads. Browsing one record at a time inside a loop can undermine that benefit. 

Prefer working with a recordset of all required IDs and reading the needed fields from it. 

3. Assuming self is a singleton 

A method may work in a form view and fail when called on multiple records. Use batch-safe logic or explicitly call ensure_one () when the method must handle exactly one record. 

4. Using sudo() to silence errors 

Permission errors may reveal an actual access-control problem. Investigate the user's access rights, record rules and company context before bypassing them. 

5. Adding too many indexes 

Indexes can improve searches, but they use storage and add work to inserts, updates and deletes. Index fields based on observed query patterns and evidence, not guesswork. 

How to Debug ORM Problems 

When an ORM operation behaves unexpectedly, use a consistent process: 

  1. Reproduce the issue: Identify the model, method, user and records involved. 

  1. Inspect the recordset: Confirm the IDs, record count and domain. 

  1. Check access rules: Test using the relevant user's permissions and company context. 

  1. Inspect dependencies: For computed fields, verify @api.depends and the source values. 

  1. Profile the operation: Use Odoo's profiler to identify expensive functions and SQL queries. ORM performance is only one part of keeping an Odoo environment reliable. Regularly reviewing custom modules, monitoring recurring errors, and testing changes can help prevent small technical issues from disrupting business workflows. See our Odoo maintenance checklist for a broader review process. 

  1. Test the fix: Verify normal use, empty recordsets, multiple records and access restrictions. 

Avoid changing business logic or adding indexes before you understand where the failure or delay originates. When ORM issues involve custom model methods, conflicting overrides, complex computed fields, or access-control logic, resolving them may require a deeper review of the underlying implementation. Our Odoo Development Services can help investigate custom modules, improve business logic, and address technical issues while keeping the code maintainable. 

Conclusion 

Odoo’s ORM simplifies data management and custom development, but using it effectively requires a solid understanding of models, recordsets, environments, domains, and CRUD methods. Applying these concepts correctly helps developers write cleaner code, avoid common errors, and improve application performance and maintainability. 

When troubleshooting complex ORM issues or building custom functionality, having the right development expertise can make a significant difference. Contact Us to get support with custom modules, business logic, and performance improvements that help keep your Odoo applications reliable and efficient