The Odoo models normally do not operate separately. The business records are linked to other records through relationships. There is a relationship between a sales order and a customer, a sales order and its lines, and products and their tags. All these relationships are managed through relational fields.
There are three major types of relational fields in Odoo, namely, Many2one, One2many, and Many2many. Besides, there are related fields in Odoo that enable a field to use a value from an existing relationship. It is vital to understand the nature of these fields for the creation of custom modules.
In this blog, we will look at the relational fields available in Odoo 20 and see how they are defined and used.
Many2One Field
The Many2one field is used when a record can be linked to one record of another model. For example, a custom library.book model can have an author:
from odoo import fields, models
class LibraryBook(models.Model):
_name = "library.book"
_description = "Library Book"
name = fields.Char(string="Title")
author_id = fields.Many2one(
"library.author",
string="Author"
)
Here, author_id is a Many2one field that points to the library.author model.
The basic syntax is:
field_name = fields.Many2one(
comodel_name,
string="Field Label"
)
This is the first argument specifying the related model:
"library.author"
The string parameter determines the label displayed on the user interface. The Many2one field saves the ID of the related record, which in the database is reflected through the foreign key field referencing the record in the related model. Thus, for instance, if the ID of the library.author record is 5, a book may have the following attribute:
author_id = 5
However, when working with the Many2one field in Python, the Odoo framework supplies the recordset instead of the ID of the database.
book = self.env["library.book"].browse(10)
print(book.author_id.name)
Wherein, book.author_id references the library.author record, while book.author_id.name refers to the name of this record.
Many2One Field in the View
The field could be added to the form view with the following code:
<field name="author_id"/>
Odoo gives a many2one field where the user can either look for an existing author or create a new one according to his permission.
Many2One Field Options
The Many2one field has several common options.
author_id = fields.Many2one(
"library.author",
string="Author",
required=True,
ondelete="restrict"
)
required=True means that the field is mandatory. On the other hand, the ondelete parameter determines what action Odoo performs on the related records when deleting the record referenced by the field. The following options exist:
set null
restrict
cascade
It depends on the particular relationship and business requirement. For instance, specifying ondelete="restrict" will avoid deletion of an author while there are books associated with the author.
Default ondelete Behaviour
Without mentioning ondelete, Odoo sets a default value depending on the required status of the field. If the field is required, then the default value will be restricted, and if it is not required, the default value will be set to null.
Thus, the required Many2one field will protect deletion of the referenced record by default, but an optional field will only reset the link to null.
One condition should be noted. A required field cannot use ondelete="set null" since it is meaningless to set null to a required field. Otherwise, Odoo will throw an error at the moment of defining the model. Only restrict and cascade are valid for required fields.
Delegation with delegate=True
A Many2one field can also be used for delegation inheritance by setting delegate=True.
partner_id = fields.Many2one(
"res.partner",
string="Partner",
delegate=True
)
When delegate is True, all fields of the target model become directly accessible on the current model. Instead of writing record.partner_id.name, you can write record.name directly. This is how _inherits works in Odoo. The current model stores a foreign key to the parent model, and the parent's fields are exposed without being copied.
Filtering and Context on Relational Fields
All three relational field types accept domain and context parameters. The domain parameter filters the candidate records shown to the user.
partner_id = fields.Many2one(
"res.partner",
string="Customer",
domain="[('is_company', '=', True)]"
)
This limits the selectable partners to companies only. The domain can also be a Python expression that gets evaluated at runtime.
The context parameter passes a context dictionary to the comodel when the user opens or creates a related record. This is useful for setting default values.
partner_id = fields.Many2one(
"res.partner",
string="Customer",
context="{'default_is_company': True}"
)
When the user creates a new partner from this field, is_company will be set to True by default.
check_company for Multi-Company Validation
For modules that work across multiple companies, relational fields support the check_company parameter.
partner_id = fields.Many2one(
"res.partner",
string="Customer",
check_company=True
)
When check_company is True, Odoo verifies that the selected record belongs to a company compatible with the current record. This prevents linking records across incompatible companies and is used heavily in accounting and inventory modules.
One2Many Field
The One2many field represents the reverse side of a Many2one relationship. It allows one record to access multiple records from another model.
For example, if library.book contains:
author_id = fields.Many2one(
"library.author",
string="Author"
)
the library.author model can define:
book_ids = fields.One2many(
"library.book",
"author_id",
string="Books"
)
The complete example is:
from odoo import fields, models
class LibraryBook(models.Model):
_name = "library.book"
_description = "Library Book"
name = fields.Char(string="Title")
author_id = fields.Many2one(
"library.author",
string="Author"
)
class LibraryAuthor(models.Model):
_name = "library.author"
_description = "Library Author"
name = fields.Char(string="Name")
book_ids = fields.One2many(
"library.book",
"author_id",
string="Books"
)
There are two important arguments in the One2many definition:
fields.One2many(
"library.book",
"author_id",
string="Books"
)
The first argument is the related model. The second argument is the inverse Many2one field in that model.
In this example:
library.author
book_ids
|
v
library.book
author_id
|
v
Library.author
The One2many field itself does not store a separate list of IDs in the database. Odoo obtains the related records through the inverse Many2one field.
For example, when:
book.author_id = author
The book becomes part of:
author.book_ids
One thing to note is that One2many fields are not copied by default. When you duplicate a parent record, the One2many lines are not included unless you explicitly handle the copy logic. This is the expected behaviour for document lines, since duplicating an order should not automatically copy its lines.
Displaying One2Many Records
A One2Many field is commonly displayed as a list inside the parent form. In Odoo 20, this can be written as:
<field name="book_ids">
<list editable="bottom">
<field name="name"/>
<field name="author_id"/>
</list>
</field>
The <list> element is used for list views in Odoo 20. In earlier versions, this element was called <tree>, but it was renamed to <list> starting from Odoo 17.5. The old <tree> tag is no longer part of the view schema.
Accessing One2Many Records in Python
A One2Many field returns an Odoo recordset.
for author in self:
for book in author.book_ids:
print(book.name)
Here, author.book_ids contains the library.book records related to the current author. One2Many fields are frequently used for document lines and other child records.
Lines Using One2Many
One of the most common uses of One2Many in Odoo is handling document lines. A parent document can have multiple child lines. For example:
Sale Order
|
+-- Sale Order Line
+-- Sale Order Line
+-- Sale Order Line
A simplified custom implementation can be defined as:
from odoo import fields, models
class CustomSaleOrder(models.Model):
_name = "custom.sale.order"
_description = "Custom Sale Order"
name = fields.Char(string="Order Reference")
line_ids = fields.One2many(
"custom.sale.order.line",
"order_id",
string="Order Lines"
)
class CustomSaleOrderLine(models.Model):
_name = "custom.sale.order.line"
_description = "Custom Sale Order Line"
order_id = fields.Many2one(
"custom.sale.order",
string="Order"
)
product_id = fields.Many2one(
"product.product",
string="Product"
)
quantity = fields.Float(
string="Quantity"
)
The relationship is established through:
line_ids = fields.One2many(
"custom.sale.order.line",
"order_id",
string="Order Lines"
)
and:
order_id = fields.Many2one(
"custom.sale.order",
string="Order"
)
The order_id field is the inverse field of line_ids.
Adding Lines to the Form
The lines can be displayed inside the sale order form:
<field name="line_ids">
<list editable="bottom">
<field name="product_id"/>
<field name="quantity"/>
</list>
</field>
This allows users to add and edit child records directly from the parent form. This pattern is used throughout Odoo for records such as:
- Sale Order > Sale Order Lines
- Purchase Order > Purchase Order Lines
- Invoice > Invoice Lines
- Manufacturing Order > Components
Creating One2Many Records Programmatically
Odoo provides the fields.Command class for working with relational fields.
For example:
order.write({
"line_ids": [
fields.Command.create({
"product_id": product.id,
"quantity": 2,
}),
]
})fields.Command.create() creates a new related line and associates it with the parent record. Other commands can be used to update, link, unlink, or delete relational records. This is particularly useful when manipulating document lines from Python code.
Many2Many Field
The Many2many field is used when multiple records from one model can be associated with multiple records from another model. Unlike Many2one, a Many2many relationship cannot be represented by a single foreign key column on one of the models.
Odoo therefore uses a relation table to maintain the relationship between the two models.
For example:
from odoo import fields, models
class LibraryBook(models.Model):
_name = "library.book"
_description = "Library Book"
name = fields.Char(string="Title")
category_ids = fields.Many2many(
"library.category",
string="Categories"
)
A book can now be associated with multiple categories, while each category can be associated with multiple books.
Many2Many Relation Table
When a Many2Many field is defined, Odoo maintains the relationship using a separate relation table. Conceptually, the table contains pairs of IDs:
| book_id | category_id |
| 1 | 2 |
| 1 | 4 |
| 2 | 2 |
| 3 | 5 |
This means:
Book 1 > Category 2
Book 1 > Category 4
Book 2 > Category 2
Book 3 > Category 5
The relationship is therefore many-to-many in both directions.
Explicit Many2Many Relation Parameters
Odoo can automatically determine the relation table in many cases. However, developers can also explicitly define the relation table and its columns.
For example:
category_ids = fields.Many2many(
"library.category",
"book_category_rel",
"book_id",
"category_id",
string="Categories"
)
Here: library.category is the related model. book_category_rel is the relation table. book_id is the column representing the current model. category_id is the column representing the related model.
This is useful when a specific relation table structure is required. When the parameters are not given, Odoo generates them automatically. The relation table name is built from the alphabetically sorted table names of both models, followed by _rel. The column names are based on the table names followed by _id.
Self-Referential Many2Many
When a model has a Many2many field pointing to itself, Odoo cannot generate the relation table name automatically. The two table names would be the same, and the automatic naming logic requires them to be different.
In this case, the relation, column 1, and column 2 parameters must be specified explicitly.
related_book_ids = fields.Many2many(
"library.book",
"book_book_rel",
"book_id",
"related_book_id",
string="Related Books"
)
Without these explicit parameters, Odoo will raise an error during model setup.
Many2Many ondelete
The Many2many field also accepts an ondelete parameter, but it works differently from Many2one. Only two values are accepted: cascade and restrict. The default is cascade. set null is not a valid option for Many2many because the relationship is stored in a separate relation table, not as a column on one of the models.
category_ids = fields.Many2many(
"library.category",
string="Categories",
ondelete="restrict"
)
Using ondelete="restrict" will prevent a category from being deleted if any book is still linked to it.
Many2Many in a View
The field can be added to a form view using:
<field name="category_ids"/>
The user can select multiple related records. Many2Many fields can also be displayed using different widgets depending on the requirement.
Tags Using Many2Many
Tags are a common use case for Many2many fields. For example, a custom task model can have multiple tags. The tag model can be defined as:
from odoo import fields, models
class ProjectTag(models.Model):
_name = "project.tag"
_description = "Project Tag"
name = fields.Char(
string="Name",
required=True
)
The task model can then define:
class ProjectTask(models.Model):
_name = "project.task.custom"
_description = "Project Task"
name = fields.Char(string="Task")
tag_ids = fields.Many2many(
"project.tag",
string="Tags"
)
The field can be displayed as tags using the many2many_tags widget:
<field name="tag_ids" widget="many2many_tags"/>
This changes the way the Many2many records are presented in the interface. For example, the field may display:
[Development] [Odoo] [Backend]
The important point is that this is still a Many2many field. The many2many_tags widget only changes its presentation in the view.
Working with Commands on Relational Fields
Both One2many and Many2many fields can be manipulated using fields.Command. This class provides several methods for creating, updating, linking, unlinking, and deleting related records.
Creating a new related record:
task.write({
"tag_ids": [
fields.Command.create({
"name": "Odoo"
}),
]
})This creates a new tag and links it to the task.
Linking an existing record:
task.write({
"tag_ids": [
fields.Command.link(tag.id),
]
})This adds an existing tag to the task without creating a new one.
Unlinking a record:
task.write({
"tag_ids": [
fields.Command.unlink(tag.id),
]
})This removes the relationship between the task and the tag. For a Many2many, the tag record stays in the database but is no longer linked. For a One2many, the behaviour depends on the inverse field's ondelete setting. If the inverse field uses ondelete="cascade", the child record is deleted. Otherwise, the inverse field is set to False, and the record is kept.
Updating an existing related record:
order.write({
"line_ids": [
fields.Command.update(line.id, {
"quantity": 5
}),
]
})This writes new values on an already linked record.
Deleting a related record entirely:
task.write({
"tag_ids": [
fields.Command.delete(tag.id),
]
})This removes the record from the database completely. This is different from unlink, which only removes the relationship.
Clearing all related records:
task.write({
"tag_ids": [
fields.Command.clear(),
]
})This removes all relationships at once. It behaves like running unlink on every linked record.
Replacing all related records:
task.write({
"tag_ids": [
fields.Command.set([tag1.id, tag2.id, tag3.id]),
]
})This replaces the entire set of related records with the given list of IDs. It unlinks records that are no longer in the list and links the new ones.
These commands are useful when working with relational records programmatically, especially in business logic where the set of related records needs to change based on certain conditions.
Related Field
A related field is used to access a field through an existing relationship. Consider a model with a Many2one field:
department_id = fields.Many2one(
"hr.department",
string="Department"
)
The department itself may have a manager:
manager_id = fields.Many2one(
"res.users",
string="Manager"
)
Instead of defining a separate field and manually copying the manager value, the employee model can access it using a related field:
department_manager_id = fields.Many2one(
"res.users",
related="department_id.manager_id",
string="Department Manager"
)
The related path is:
department_id.manager_id
Odoo first follows department_id and then accesses manager_id from that record.
Related Field Syntax
The basic syntax is:
field_name = fields.FieldType(
related="field1.field2"
)
The field type should correspond to the value being accessed.
For example, if:
department_id = fields.Many2one("hr.department")and hr.department contains:
name = fields.Char()
we can define:
department_name = fields.Char(
related="department_id.name",
string="Department"
)
The value of department_name comes from:
employee.department_id.name
There is no need to manually synchronize the two fields.
Chained Related Fields
Related fields can follow multiple relationships.
company_country_id = fields.Many2one(
"res.country",
related="department_id.company_id.country_id",
string="Company Country"
)
Odoo follows:
department_id
|
v
company_id
|
v
country_id
and exposes the final value through company_country_id.This can be useful when a value is frequently required in another model.
Stored Related Fields
By default, a related field is computed from the related record rather than being stored as an independent database value.
If the value needs to be stored, it store=True can be used:
department_name = fields.Char(
related="department_id.name",
string="Department",
store=True
)
With store=True, Odoo stores the value in the database and updates it when the relevant dependencies change. Stored related fields can be useful when the field needs to be searched, grouped, or used efficiently in certain database operations.
However, it store=True should not be added automatically to every related field. It should be used when there is a requirement for the value to be stored.
Read-only Behaviour
Related fields are read-only by default. This is because they are meant to reflect a value coming from another record, not to be edited independently. If the value needs to be editable through the related field, you have to set readonly= False explicitly:
department_name = fields.Char(
related="department_id.name",
string="Department",
readonly=False
)
Even with the related field, it is only writable if the source field is also not readonly. If the source field is readonly, the related field will remain read-only regardless of the setting.
Whether a related field should be editable depends on the relationship and the intended behaviour of the field. In most cases, leaving the default readonly behaviour is the right choice.
Python: Relational Fields
Since relational fields return Odoo recordsets, developers may apply usual ORM functions to them. For example, for a Many2one field:
book.author_id
Returns the related author record.
For a One2many field:
author.book_ids
Returns the books related to the author.
For a Many2many field:
book.category_ids
Returns the categories related to the book.
Odoo ORM functions such as search, filter, map may be applied to the returned objects.
books = author.book_ids.filtered(
lambda book: book.active
)
It works because book_ids is an Odoo recordset.
Choosing the Correct Relational Field
The correct relational field depends on how records are linked to each other. Apply Many2one if one record has to refer to one record from another model.
sale.order > res.partner
partner_id = fields.Many2one(
"res.partner",
string="Customer"
)
Apply One2many if one record has to retrieve multiple records from another model, where the other model has a Many2one field.
sale.order > sale.order.line
order_line_ids = fields.One2many(
"sale.order.line",
"order_id",
string="Order Lines"
)
Use Many2many when multiple records on both sides can be related to each other.
product.product <> product.tag
tag_ids = fields.Many2many(
"product.tag",
string="Tags"
)
Use Related when the required value already exists through another relationship and needs to be exposed from the current model.
department_name = fields.Char(
related="department_id.name"
)
This avoids maintaining a second independent copy of the same value.
Relational fields form an important part of Odoo's ORM. They define how records from different models are connected and how those relationships can be accessed from Python and the user interface.
Many2one is used to reference a single record, while One2many provides the reverse access to multiple records through the inverse Many2one field. Many2many is used when records on both sides can have multiple relationships and is implemented using a relation table. One of its common applications is displaying tags with the many2many_tags widget.
A related field is useful when a value needs to be accessed through an existing relationship. It can follow multiple relationships and can be stored when required. Related fields are readonly by default, and making them editable requires setting readonly=False along with a writable source field.
All relational fields accept domain and context parameters for filtering candidate records and passing context to the comodel. The check_company parameter adds multi-company validation. The fields.Command class provides create, update, link, unlink, delete, clear, and set methods for manipulating related records programmatically.
Having a clear understanding of these field types makes it easier to design Odoo models, create form and list views, work with relational records through the ORM, and build custom modules in Odoo 20.
To read more about An Overview of Relational Fields in Odoo 19, refer to our blog, An Overview of Relational Fields in Odoo 19.