Odoo is an all-inclusive Enterprise Resource Planning (ERP) platform that offers a wide range of business apps that are ready to use and fully integrated. It spans areas such as accounting, sales, inventory, e-commerce, manufacturing, human resources, and more. Because of Odoo's modular design, companies of various sizes and sectors may only use the features they require, guaranteeing a scalable, flexible, and affordable solution. Central to Odoo’s architecture is a robust database model that seamlessly interacts with all aspects of the system’s functionalities.
What is ORM?
Odoo's data handling capabilities are based on its Object-Relational Mapping (ORM) layer. ORM is a technique for moving data across incompatible systems, especially relational databases and object-oriented programming languages, without having to write raw SQL queries. Odoo's ORM architecture lets developers work directly with Python objects that represent database rows, abstracting away low-level database concerns. This improves the code's readability, maintainability, and security while streamlining database operations.
Definition and Role of ORM in Odoo
At the core of Odoo’s architecture is a powerful Object-Relational Mapping (ORM) layer that bridges the gap between Python code and the underlying relational database. For routine tasks like adding, reading, editing, or removing records, developers use Pythonic methods and object operations to communicate with Odoo's database rather than manually creating SQL queries. This abstraction significantly lowers the risk of SQL injection and other database-related problems, enhances code maintainability, and streamlines development.
Developers can handle database records as Python objects instead of table rows by utilizing the ORM. This means that Odoo automatically translates Python method calls (like create(), search(), write(), and unlink()) into the appropriate SQL statements behind the scenes. Consequently, developers can focus on business logic and user experience instead of wrestling with SQL intricacies.
Odoo Model Structure
In Odoo, models represent database tables and their corresponding business logic. Each model is defined as a Python class inherited from models.Model, and its attributes (fields) map directly to database columns. For example, if you define a model named res.partner with fields like name, email, and phone, Odoo creates or updates the corresponding database table (often named res_partner) with columns that align to these fields.
The simplicity of this structure is that everything you need is encapsulated within the model class:
Fields: Fields like Char, Integer, Float, Many2one, and others define the type and behavior of the data.
Methods: Methods within the model implement the business logic—creating new records, validating data, modifying existing records, or computing computed fields.
By working with the model’s fields and methods, developers can build complex, feature-rich applications without ever having to manually write SQL. This ensures that future changes, upgrades, and database migrations remain as smooth and hassle-free as possible.
Overall, the ORM and model structure in Odoo form a foundational layer that makes the entire system more flexible, maintainable, and developer-friendly.
Core ORM Methods in Odoo
At the heart of Odoo’s ORM lie a handful of core methods that correspond to the fundamental operations performed on database records: creating new records, reading existing ones, updating fields, and deleting entries. Comprehending these processes enables developers to carry out repetitive activities effectively and expand on them to incorporate increasingly intricate features.
Create
The create() method is used to insert new records into the database. Instead of writing an SQL INSERT statement, you simply call create() on the model’s recordset, passing a dictionary of field values. Odoo then takes care of the low-level database details — like schema checking, generating the necessary SQL, and handling defaults and constraints.
Syntax Example:
new_partner = self.env['res.partner'].create({
'name': 'John Doe',
'email': 'john.doe@example.com',
})
Explanation:
In the above example, res.partner is the model for partners (customers, vendors, etc.). By calling create() with a dictionary of values, we instruct Odoo to create a new record in the res_partner table. The returned value, new_partner, is a recordset representing the newly created record.
Read (Search and Browse)
To retrieve data from the database, Odoo provides search(), search_count(), and browse() methods. Together, these methods replace the need for raw SQL SELECT statements.
search(): Locates records that meet a certain set of requirements. It returns a recordset containing all matching records.
search_count(): Returns an integer representing the count of records matching the given criteria, useful for pagination or conditional logic.
browse(): Given a list of record IDs, browse() returns a recordset. This is useful for retrieving records when you already know their IDs.
Syntax Examples:
# Search Example: Finding all partners whose name starts with 'John'
john_partners = self.env['res.partner'].search([('name', 'ilike', 'John%')])
# Search Count Example: Counting how many such partners exist
john_count = self.env['res.partner'].search_count([('name', 'ilike', 'John%')])
# Browse Example: Browsing by known record IDs
specific_partners = self.env['res.partner'].browse([1, 2, 3])
Explanation:
The search() method uses domain filters (lists of tuples defining field-operator-value conditions) to query the database. Here, ('name', 'ilike', 'John%') finds records where the name field contains “John” at the start. After the search, john_partners behaves like a list of partner objects, making it easy to iterate over them or access their fields. search_count() is similar but returns an integer count instead of a recordset. browse() fetches records directly from their IDs—handy when you already have the IDs from another process.
Write
The write() method updates existing records. Rather than crafting SQL UPDATE statements, you call write() on a recordset and pass in a dictionary of field changes.
Syntax Example:
order = self.env['sale.order'].search([('name', '=', 'SO001')], limit=1)
if order:
order.write({'state': 'done'})
Explanation:
In this example, we first search for a sale order named “SO001.” If found, we use write() to change its state to done. Behind the scenes, Odoo constructs the appropriate SQL UPDATE query and commits the change to the database.
Unlink
To delete records from the database, use the unlink() method. Instead of writing DELETE FROM statements, call unlink() on the recordset you wish to remove.
Syntax Example:
partner_to_remove = self.env['res.partner'].search([('email', '=', 'spam@example.com')], limit=1)
if partner_to_remove:
partner_to_remove.unlink()
Explanation:
The code searches for a partner with a specific email address and, if found, deletes it from the database using unlink().
In conclusion, the basis that makes database operations simpler is formed by the fundamental ORM methods of Odoo: `create(), `search()/search_count(), browse(), write(), and `unlink()`. Building, maintaining, and expanding Odoo applications is made easier for developers by working at the object level instead of directly interacting with SQL syntax.
Advanced ORM Methods
Beyond the basic create, read, write, and delete operations, Odoo’s ORM provides additional methods and concepts that allow developers to perform more refined queries, set default values, dynamically react to form changes, and even customize how fields are presented in views. These cutting-edge methods improve the user experience and assist better customize the system to meet certain corporate needs.
Search with Domain Filters
In Odoo, domains are used to specify search criteria and filter records based on conditions. A domain is a group of tuples, each of which provides a condition. Developers can create intricate queries utilizing domains without ever writing a single SQL line. Usually, each tuple has the following structure: field_name, operator, value.
Common Operators:
- = : Equal to
- != : Not equal to
- > : Greater than
- < : Less than
- >= : Greater than or equal to
- <= : Less than or equal to
- ilike : Case-insensitive substring match
- in : Value in a set of values
Example: Searching for Orders Placed in the Last Month:
Suppose you want to find all sale orders created within the last 30 days. You can use the search() method with a domain that filters on the create_date field:
from datetime import datetime, timedelta
today = datetime.today()
last_month = today - timedelta(days=30)
domain = [('create_date', '>=', last_month.strftime('%Y-%m-%d'))]
recent_orders = self.env['sale.order'].search(domain)
Here, recent_orders is a recordset containing all sale orders created in the last month. The domain ensures that the search operates at the database level, returning only the records that match the criteria.
Onchange
Developers can dynamically update field values in a form view if certain fields change by using the onchange technique. This works closely with Odoo's ORM and models, even if it isn't exactly a direct database operation. The onchange method can calculate or recommend new values for other fields when a user makes changes to one of the form's fields without instantly saving the changes to the database.
Example:
Imagine you have a sale order form where changing the customer_id should automatically update the customer’s address details on the form:
@api.onchange('customer_id')
def _onchange_customer_id(self):
if self.customer_id:
self.street = self.customer_id.street
self.city = self.customer_id.city
self.country_id = self.customer_id.country_id
This method ensures that as soon as the customer_id changes, the related address fields are updated in the form view, providing a better user experience.
Default_get
When creating a new record, Odoo can automatically populate certain fields with default values. The default_get() method allows developers to define these default values programmatically. This can be especially useful when different contexts or user roles need different defaults.
Example: Setting a Default Salesperson:
@api.model
def default_get(self, fields_list):
defaults = super(SaleOrder, self).default_get(fields_list)
if self.env.user and 'user_id' in fields_list:
defaults['user_id'] = self.env.user.id
return defaults
In this example, each new sale order defaults to the current logged-in user as the salesperson if user_id is a field on the model. This reduces repetitive data entry and ensures consistency.
Fields_view_get
A view's structure can be dynamically changed using the fields_view_get() method. Before the view is rendered, you can programmatically modify fields, labels, and other characteristics by retrieving the definition of a specific view (such as a tree, form, or search view). This uses the ORM's knowledge of models and fields to guarantee consistency between the model and how it is presented, even if it is more about personalizing the user interface than it is about working directly with the database.
Example: Modifying a Form View Dynamically:
@api.model
def fields_view_get(self, view_id=None, view_type='form', toolbar=False, submenu=False):
res = super(SaleOrder, self).fields_view_get(view_id=view_id, view_type=view_type, toolbar=toolbar, submenu=submenu)
if view_type == 'form':
doc = etree.XML(res['arch'])
# Suppose we want to hide a certain field based on user group
if not self.env.user.has_group('sales_team.group_sale_manager'):
field_node = doc.xpath("//field[@name='margin']")[0]
field_node.set('invisible', '1')
res['arch'] = etree.tostring(doc, encoding='unicode')
return res
Here, if the current user is not a sales manager, the margin field is hidden in the form view. This customization happens dynamically at runtime based on the user’s permissions.
Additional ORM Methods
To further extend the power of Odoo’s ORM, several other methods and utilities exist to handle more specialized scenarios:
copy()
In order to reproduce an existing record, copy() may choose to override specific fields.
Example:
existing_partner = self.env['res.partner'].search([('name', '=', 'John Doe')], limit=1)
if existing_partner:
new_partner = existing_partner.copy({'name': 'John Doe Copy'})
name_get()
name_get() specifies the method used to construct the record's display name.
Example:
@api.multi
def name_get(self):
res = []
for record in self:
display_name = f"{record.name} ({record.email})"
res.append((record.id, display_name))
return res
name_search()
When a user enters data into a Many2one field, name_search() modifies the way records are located.
Example:
@api.model
def name_search(self, name='', args=None, operator='ilike', limit=100):
args = args or []
if name:
args = [('name', operator, name)] + args
return self.search(args, limit=limit).name_get()
search_read()
search_read() returns a list of dictionaries for simple serialization by combining search() and read() into a single operation.
Example:
partners_info = self.env['res.partner'].search_read(
domain=[('customer', '=', True)],
fields=['name', 'email'],
limit=10
)
read_group()
Useful for summaries and reports, read_group() aggregates data (such as sum, count, min, and max) and groups results by given variables.
Example:
result = self.env['sale.order'].read_group(
domain=[],
fields=['partner_id', 'amount_total:sum'],
groupby=['partner_id']
)
exists()
exists() makes ensuring that only records that are still in the database are included in the current recordset.
Example:
recordset = self.env['res.partner'].browse([1, 2, 999])
valid_records = recordset.exists()
filtered() and mapped()
These utility methods operate on in-memory recordsets:
- filtered() returns a subset of records matching a condition.
- mapped() extracts the values of a specified field from each record.
Example:
partners = self.env['res.partner'].search([])
partners_with_email = partners.filtered(lambda p: p.email)
partner_names = partners.mapped('name')
sudo()
sudo() bypasses access control and record rules, allowing actions as a superuser.
Example:
admin_partner = self.env['res.partner'].sudo().create({'name': 'Admin Partner'})
Customizing ORM Behavior
Although the majority of common use cases are covered by Odoo's default ORM behavior, there are several situations in which you might need to modify, expand, or improve the ORM's functionality. You can apply more validation, add more logic during record creation, or enforce data integrity requirements that go beyond accepted limits by altering ORM behavior.
Overriding ORM Methods
The fundamental ORM functions, such create(), write(), and unlink()`, can be overridden by developers in their own modules. To accomplish this, a model class that inherits from an existing Odoo model is defined, and new implementations of these functions are then provided. Calling `super() is crucial when overriding ORM methods in order to preserve the original logic and guarantee compatibility with other modules that could depend on it.
Example: Modifying the create() Method
Suppose you want to add a custom validation step or automatically compute some fields whenever a new record is created:
from odoo import models, api, exceptions, _
class SaleOrder(models.Model):
_inherit = 'sale.order'
@api.model
def create(self, vals):
# Custom logic before creation
if 'partner_id' not in vals:
raise exceptions.UserError(_("A partner is required for creating a sale order."))
# Call the original create() method
order = super(SaleOrder, self).create(vals)
# Custom logic after creation
order.message_post(body=_("Sale order created successfully with ID %s!") % order.id)
return order
The custom create() method in this example verifies whether partner_id is supplied. Otherwise, a user-friendly error is raised. The superclass call creates the record and then posts an informational message.
Using Constraints
Developers can use restrictions in addition to overriding methods to ensure data consistency and integrity. There are two types of constraints: Python-based and SQL-based. They guarantee that specific requirements are maintained by the database, such as avoiding duplicate records or guaranteeing that numeric fields stay within a given range.
SQL Constraints
SQL constraints are defined in _sql_constraints at the model level. They are enforced by the database itself and are suitable for straightforward conditions.
class ResPartner(models.Model):
_inherit = 'res.partner'
_sql_constraints = [
('unique_email', 'unique(email)', 'Email addresses must be unique!'),
]
This constraint prevents multiple partners from having the same email. If you attempt to create or update a partner to an existing email address, the database will raise an error.
Python Constraints
By utilizing the @api.constrains decorator, Python constraints enable more intricate validations. Every time a record is created or changed, these restrictions are examined. Python logic can be used to validate data against any criteria.
class SaleOrder(models.Model):
_inherit = 'sale.order'
@api.constrains('amount_total')
def _check_amount_total(self):
for order in self:
if order.amount_total < 0:
raise exceptions.ValidationError(_("The total amount cannot be negative."))
Here, a ValidationError is triggered to stop the record from being saved if the total amount of an order is ever estimated as negative (which could imply some data issue).
In summary, customizing the ORM’s default behavior in Odoo empowers developers to tailor the system to their exact business requirements. You can implement custom validations, automate processes at record creation or update, and enforce data integrity at the database level by overriding basic methods and using constraints. These modifications guarantee that Odoo will always be adaptable, reliable, and precisely matched with the particular procedures of any company.