One2many Fields in Odoo
One2many fields are the inverse side of Many2one relationships. A sale order has many order lines; each line belongs to one order. One2many fields are always rendered inline within a parent form, making them central to master-detail interfaces in Odoo.
Basic Inline List
The simplest One2many rendering is an inline list (tree view):
<field name="line_ids">
<list>
<field name="sequence" widget="handle"/>
<field name="product_id"/>
<field name="description"/>
<field name="quantity"/>
<field name="price_unit"/>
<field name="subtotal"/>
</list>
</field>
Without editable, clicking a line opens a popup form. With editable, users edit directly in the list.
Editable Lists
Enable inline editing with the editable attribute:
<field name="line_ids">
<list editable="bottom">
<field name="product_id"/>
<field name="quantity"/>
<field name="price_unit"/>
</list>
</field>
Options: editable="bottom" adds new rows at the bottom, editable="top" adds them at the top. The choice depends on UX: bottom is standard for order lines, top is useful for logs or comments.
Drag-and-Drop Ordering
Add the handle widget to an Integer sequence field for drag-and-drop reordering:
class OrderLine(models.Model):
_name = 'sale.order.line'
_order = 'sequence, id'
sequence = fields.Integer(default=10)
<field name="line_ids">
<list editable="bottom">
<field name="sequence" widget="handle"/>
<field name="product_id"/>
<field name="quantity"/>
</list>
</field>
The handle widget renders a drag icon. Odoo automatically updates the sequence field when rows are reordered.
Section and Note Lines
Odoo's sale order uses a pattern where some lines are section headers or notes rather than product lines:
class OrderLine(models.Model):
_name = 'order.line'
display_type = fields.Selection([
('line_section', 'Section'),
('line_note', 'Note'),
], default=False)
name = fields.Text(string='Description')
product_id = fields.Many2one('product.product')
<list editable="bottom">
<field name="display_type" column_invisible="1"/>
<field name="product_id"
invisible="display_type == 'line_section'
or display_type == 'line_note'"/>
<field name="name"/>
<field name="quantity"
invisible="display_type == 'line_section'
or display_type == 'line_note'"/>
</list>
Embedded Form View
Define a custom form for the popup editor:
<field name="line_ids">
<list>
<field name="product_id"/>
<field name="quantity"/>
</list>
<form>
<group>
<field name="product_id"/>
<field name="description"/>
</group>
<group>
<field name="quantity"/>
<field name="price_unit"/>
<field name="discount"/>
<field name="subtotal"/>
</group>
</form>
</field>
The list shows summary columns; the form shows all details when clicking a line.
Embedded Kanban View
One2many fields can also render as kanban cards:
<field name="task_ids" mode="kanban">
<kanban>
<field name="name"/>
<field name="state"/>
<templates>
<t t-name="card">
<field name="name" class="fw-bold"/>
<field name="state" widget="label_selection"
options="{'classes': {
'draft': 'info',
'done': 'success'}}"/>
</t>
</templates>
</kanban>
</field>
Default Values from Parent
Pass parent context to set defaults on child records:
<field name="line_ids"
context="{'default_currency_id': currency_id,
'default_company_id': company_id}"/>
Or define defaults in the model:
class OrderLine(models.Model):
_name = 'order.line'
currency_id = fields.Many2one(
related='order_id.currency_id', store=True)
Computed Totals
Common pattern: compute totals from One2many lines:
class SaleOrder(models.Model):
_name = 'sale.order'
line_ids = fields.One2many(
'sale.order.line', 'order_id')
amount_total = fields.Monetary(
compute='_compute_amount', store=True)
@api.depends('line_ids.subtotal')
def _compute_amount(self):
for order in self:
order.amount_total = sum(
order.line_ids.mapped('subtotal'))
Controlling Add/Delete
Restrict line operations with list attributes:
<field name="line_ids">
<list editable="bottom" create="false" delete="false">
<field name="product_id" readonly="1"/>
<field name="delivered_qty"/>
</list>
</field>
create="false" hides Add a Line. delete="false" hides the delete button. Useful for views where users should only edit existing lines, not add or remove them.
Column Totals
Show totals at the bottom of list columns:
<field name="quantity" sum="Total Qty"/>
<field name="subtotal" sum="Total Amount"/>
The sum attribute label appears in the footer row with the column total.
Performance Tips
- Avoid heavy computed fields on One2many lines since they are recalculated when any line changes
- Use
store=Trueon line-level computed fields to avoid recomputation on every form load - Limit visible columns in the inline list since each column triggers field access
- For very large line sets (100+ lines), consider pagination or lazy loading patterns
Common Pitfalls
- Forgetting _order: without
_order = 'sequence, id', lines appear in creation order which may confuse users after reordering - Missing inverse field: the One2many field must reference the correct Many2one field name on the child model
- Onchange loops: be careful with onchange methods that modify parent fields from child changes, as this can cause infinite recomputation