Selection Fields Overview
Selection fields store a single value from a predefined list of options. They map to a VARCHAR column in PostgreSQL and are displayed as dropdowns, radio buttons, or statusbars depending on the widget. Selection fields are fundamental to Odoo's workflow system.
Basic Selection Definition
class SupportTicket(models.Model):
_name = 'support.ticket'
priority = fields.Selection([
('0', 'Low'),
('1', 'Normal'),
('2', 'High'),
('3', 'Urgent'),
], string='Priority', default='1', required=True)
state = fields.Selection([
('new', 'New'),
('in_progress', 'In Progress'),
('done', 'Done'),
('cancelled', 'Cancelled'),
], string='Status', default='new',
required=True, tracking=True, copy=False)
Each option is a tuple of (value, label). The value is stored in the database; the label is displayed to users and is translatable.
Extending Selection Fields (selection_add)
One of the most powerful patterns in Odoo is extending existing Selection fields from inherited models without modifying the original:
class SupportTicketExtended(models.Model):
_inherit = 'support.ticket'
state = fields.Selection(
selection_add=[
('waiting', 'Waiting for Customer'),
('escalated', 'Escalated'),
],
ondelete={
'waiting': 'set default',
'escalated': 'cascade',
})
The selection_add parameter appends new options. The ondelete parameter specifies what happens to records with these values if the extending module is uninstalled:
'set default': reset to the field's default value'cascade': delete the records'set null': set the field to False (only if not required)
Controlling Insertion Order
You can control where new options appear in the list:
state = fields.Selection(
selection_add=[
('review', 'Under Review'), # inserted after existing options
])
# To insert between existing options, reference the option it should follow:
state = fields.Selection(
selection_add=[
('review', 'Under Review'), # added after last
])
In practice, Odoo appends selection_add values at the end of the list. To insert between specific values, you need to redefine the entire selection.
Dynamic Selection from Method
Use a method to generate selection options dynamically:
class DynamicModel(models.Model):
_name = 'dynamic.model'
report_type = fields.Selection(
selection='_get_report_types',
string='Report Type')
@api.model
def _get_report_types(self):
types = [('sales', 'Sales Report'),
('inventory', 'Inventory Report')]
if self.env.user.has_group('account.group_account_manager'):
types.append(('financial', 'Financial Report'))
return types
The method is called each time the field is rendered, allowing options to change based on user, config, or installed modules.
Related Selection Fields
Access a selection from a related record:
class OrderLine(models.Model):
_name = 'order.line'
order_state = fields.Selection(
related='order_id.state', string='Order Status',
store=True, readonly=True)
Widget Options
Default Dropdown
<field name="priority"/>
Radio Buttons
<field name="priority" widget="radio"/>
<field name="priority" widget="radio"
options="{'horizontal': true}"/>
Statusbar
<field name="state" widget="statusbar"
statusbar_visible="new,in_progress,done"/>
Badge
<field name="state" widget="badge"
decoration-success="state == 'done'"
decoration-danger="state == 'cancelled'"
decoration-info="state == 'new'"/>
Selection in Search Views
<search>
<field name="state"/>
<filter name="filter_new" string="New"
domain="[('state', '=', 'new')]"/>
<filter name="filter_active" string="Active"
domain="[('state', 'not in', ['done', 'cancelled'])]"/>
</search>
Conditional Logic Based on Selection
# Python: onchange
@api.onchange('priority')
def _onchange_priority(self):
if self.priority == '3': # Urgent
return {'warning': {
'title': 'Urgent Priority',
'message': 'This will notify all managers.',
}}
# Python: computed field
@api.depends('state')
def _compute_is_editable(self):
for record in self:
record.is_editable = record.state not in ('done', 'cancelled')
# XML: conditional visibility
<field name="resolution" invisible="state != 'done'"/>
Getting the Display Label
To get the human-readable label of a selection value:
# Get label for current value
label = dict(self._fields['state'].selection).get(self.state)
# Or using the fields_get method
selection_labels = dict(
self.fields_get(['state'])['state']['selection'])
label = selection_labels.get(self.state)
Migration Considerations
When adding or removing selection values:
- Adding values is safe; existing records are unaffected
- Removing values requires migrating existing records first
- Renaming values (changing the stored string) requires a migration script
- The
ondeleteparameter onselection_addhandles cleanup when modules are uninstalled
# Migration script example
def migrate(cr, version):
cr.execute("""
UPDATE support_ticket
SET state = 'done'
WHERE state = 'completed'
""")
Best Practices
- Use short, lowercase values (e.g.,
'draft', not'Draft'or'DRAFT') - Always set
required=Truefor state/status fields - Use
tracking=Truefor status fields to log changes in chatter - Set
copy=Falseon state fields so duplicated records start fresh - Prefer Selection over Many2one for fixed, small option sets since there is no join overhead
- Use
selection_addfor extensibility instead of redefining the entire selection