Skip to content

One2many Inline Editing Patterns in Odoo: Lists, Forms, and Kanban

DeployMonkey Team · March 23, 2026 12 min read

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=True on 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