Skip to content

Many2many Field UI Patterns in Odoo: Tags, Checkboxes, and Custom Widgets

DeployMonkey Team · March 23, 2026 11 min read

Many2many Fields in Odoo

Many2many fields represent bidirectional relationships between two models. A product can have many tags, and a tag can be on many products. Odoo provides several widgets to render these relationships in forms, each suited to different use cases.

How Many2many Works Internally

Odoo creates a relation table (junction table) for each Many2many field. By default, the table name is model1_model2_rel:

tag_ids = fields.Many2many(
    'project.tags',                    # comodel
    'project_task_tags_rel',           # relation table
    'task_id',                          # column1 (this model)
    'tag_id',                           # column2 (comodel)
    string='Tags')

You can omit the relation table parameters and Odoo generates them automatically. Specify them explicitly when you need predictable table names for migrations or SQL queries.

Widget: many2many_tags (Default)

The most common widget renders related records as colored pill-shaped tags:

<field name="tag_ids" widget="many2many_tags"
       options="{'color_field': 'color'}"/>

The color_field option maps to an Integer field (0-11) on the comodel that determines the tag color. Users can type to search and select tags, or create new ones inline.

Tag Options

<field name="tag_ids" widget="many2many_tags"
       options="{
         'color_field': 'color',
         'no_create': true,
         'no_create_edit': true,
         'no_quick_edit': true
       }"/>
  • no_create: hides the Create option in the dropdown
  • no_create_edit: hides Create and Edit
  • no_quick_edit: disables inline editing of tag names

Widget: many2many_checkboxes

Renders each possible value as a checkbox. Best for small, fixed sets of options:

<field name="category_ids" widget="many2many_checkboxes"/>

This fetches all records from the comodel and displays them as a vertical list of checkboxes. Not suitable for large datasets since it loads everything at once.

Widget: many2many_binary

Specialized widget for file attachments stored via Many2many to ir.attachment:

attachment_ids = fields.Many2many(
    'ir.attachment', string='Attachments')
<field name="attachment_ids" widget="many2many_binary"
       string="Attach Files"/>

This renders a drag-and-drop file upload zone. Users can attach multiple files, preview images, and download existing attachments.

Widget: many2many (Inline List)

The default widget (without specifying widget) renders a Many2many as an inline list with Add and Remove buttons:

<field name="user_ids">
  <list>
    <field name="name"/>
    <field name="email"/>
    <field name="phone"/>
  </list>
</field>

Users click Add a Line to open a selection dialog, or Remove to detach records (not delete them).

Widget: many2many_tags_avatar

Shows avatar images alongside tag names, commonly used for user assignments:

<field name="reviewer_ids" widget="many2many_tags_avatar"
       options="{'no_create': true}"/>

Programmatic Many2many Operations

Odoo uses special command tuples for Many2many write operations:

# Add a record (link)
record.write({'tag_ids': [(4, tag_id)]})

# Remove a record (unlink, does not delete)
record.write({'tag_ids': [(3, tag_id)]})

# Replace all with a new set
record.write({'tag_ids': [(6, 0, [id1, id2, id3])]})

# Create and link in one step
record.write({'tag_ids': [(0, 0, {'name': 'New Tag'})]})

# Unlink all
record.write({'tag_ids': [(5, 0, 0)]})

The command format is (command_code, id_or_0, values_or_ids):

CodeActionUsage
0Create and link(0, 0, {vals})
1Update linked record(1, id, {vals})
2Delete and unlink(2, id, 0)
3Unlink (keep record)(3, id, 0)
4Link existing(4, id, 0)
5Unlink all(5, 0, 0)
6Replace all(6, 0, [ids])

Domain Filtering

Filter which records are available for selection:

<field name="allowed_user_ids" invisible="1"/>
<field name="reviewer_ids" widget="many2many_tags"
       domain="[('id', 'in', allowed_user_ids)]"/>

Or use a static domain:

<field name="tag_ids" widget="many2many_tags"
       domain="[('active', '=', True)]"/>

Performance Considerations

  • many2many_checkboxes: loads all comodel records. Avoid for large models.
  • many2many_tags: uses autocomplete search. Good for any size.
  • Prefetching: accessing record.tag_ids triggers a query to the relation table plus the comodel. Use mapped() for batch access.
  • Count without loading: use search_count with a domain on the relation rather than len(record.tag_ids).

Common Patterns

Allowed Values from Parent

class ProjectTask(models.Model):
    _inherit = 'project.task'

    allowed_tag_ids = fields.Many2many(
        related='project_id.tag_ids')
    tag_ids = fields.Many2many(
        'project.tags',
        domain="[('id', 'in', allowed_tag_ids)]")

Computed Many2many

related_partner_ids = fields.Many2many(
    'res.partner', compute='_compute_related_partners',
    store=True)

@api.depends('order_line.partner_id')
def _compute_related_partners(self):
    for record in self:
        record.related_partner_ids = \
            record.order_line.mapped('partner_id')