API Decorators
Examples target Odoo 18 and 19 unless marked. Platform covers Odoo 11 to 20.
Basic Decorators
@api.model
Used for methods that don't work on a specific record set.
python
@api.model
def create_default_config(self):
"""Create default configuration"""
return self.create({
'name': 'Default Config',
'active': True
})
@api.model
def get_system_info(self):
"""Get system information"""
return {
'version': '18.0',
'database': self.env.cr.dbname
}@api.depends
Defines field dependencies for computed fields.
python
total_amount = fields.Float(compute='_compute_total_amount')
@api.depends('line_ids.price_total')
def _compute_total_amount(self):
for record in self:
record.total_amount = sum(record.line_ids.mapped('price_total'))
@api.depends('first_name', 'last_name')
def _compute_full_name(self):
for record in self:
record.full_name = f"{record.first_name} {record.last_name}"@api.onchange
Triggers when field values change in the UI.
python
@api.onchange('partner_id')
def _onchange_partner_id(self):
if self.partner_id:
self.invoice_address = self.partner_id.street
self.payment_term_id = self.partner_id.property_payment_term_id
@api.onchange('product_id', 'quantity')
def _onchange_product_quantity(self):
if self.product_id and self.quantity:
self.price_unit = self.product_id.list_price
self.subtotal = self.quantity * self.price_unitConstraint Decorators
@api.constrains
Validates field values and raises validation errors.
python
@api.constrains('email')
def _check_email_format(self):
for record in self:
if record.email and '@' not in record.email:
raise ValidationError("Invalid email format")
@api.constrains('start_date', 'end_date')
def _check_dates(self):
for record in self:
if record.start_date and record.end_date:
if record.start_date > record.end_date:
raise ValidationError("Start date must be before end date")Combined Decorators
Multiple Dependencies
python
@api.depends('partner_id', 'partner_id.country_id', 'amount_total')
def _compute_tax_amount(self):
for record in self:
if record.partner_id and record.partner_id.country_id:
tax_rate = record.partner_id.country_id.tax_rate or 0.0
record.tax_amount = record.amount_total * tax_rate / 100
else:
record.tax_amount = 0.0Onchange with Constraints
python
@api.onchange('quantity', 'price_unit')
def _onchange_quantity_price(self):
if self.quantity and self.price_unit:
self.total_price = self.quantity * self.price_unit
@api.constrains('quantity')
def _check_quantity_positive(self):
for record in self:
if record.quantity <= 0:
raise ValidationError("Quantity must be positive")CRUD Method Decorators
Create Method
python
@api.model_create_multi
def create(self, vals_list):
records = super().create(vals_list)
for record in records:
record._send_notification()
return recordsWrite Method
python
def write(self, vals):
result = super().write(vals)
if 'state' in vals:
self._trigger_state_change()
return resultUnlink Method
python
def unlink(self):
if any(record.state == 'confirmed' for record in self):
raise UserError("Cannot delete confirmed records")
return super().unlink()Context and Environment
Working with Context
python
@api.model
def with_company_context(self, company_id):
return self.with_context(
allowed_company_ids=[company_id],
company_id=company_id
)
@api.depends('amount', 'currency_id')
def _compute_amount_company_currency(self):
for record in self:
if record.currency_id:
record.amount_company = record.currency_id._convert(
record.amount,
record.company_id.currency_id,
record.company_id,
record.date or fields.Date.context_today(record)
)Sudo Operations
python
@api.model
def create_system_record(self, vals):
"""Create record with system privileges"""
return self.sudo().create(vals)
def get_private_data(self):
"""Access private data with sudo"""
return self.sudo().private_fieldError Handling Decorators
Custom Validation
python
@api.constrains('code')
def _check_code_unique(self):
for record in self:
if self.search_count([('code', '=', record.code), ('id', '!=', record.id)]) > 0:
raise ValidationError(f"Code '{record.code}' already exists")
@api.constrains('amount')
def _check_amount_limit(self):
for record in self:
if record.amount > 10000:
raise UserError("Amount cannot exceed 10,000")Performance Decorators
Batch Processing
python
@api.model
def process_batch(self, record_ids, batch_size=100):
"""Process records in batches"""
records = self.browse(record_ids)
for batch in records.batch(batch_size):
batch._process_records()
def _process_records(self):
"""Process current recordset"""
for record in self:
record.processed = TrueCommon Patterns
State Management
python
@api.onchange('state')
def _onchange_state(self):
if self.state == 'draft':
self.can_edit = True
else:
self.can_edit = False
@api.depends('state')
def _compute_can_edit(self):
for record in self:
record.can_edit = record.state == 'draft'Related Field Computation
python
@api.depends('partner_id.commercial_partner_id')
def _compute_commercial_partner(self):
for record in self:
record.commercial_partner_id = record.partner_id.commercial_partner_id
@api.depends('order_line.price_total')
def _compute_amount_total(self):
for order in self:
order.amount_total = sum(order.order_line.mapped('price_total'))Migration Notes
Deprecated Decorators
@api.one: Removed in v13, use recordset iteration@api.multi: Deprecated in v13, methods work on recordsets by default
Modern Patterns
python
# Old style (deprecated)
@api.one
def old_method(self):
return self.name
# New style
def new_method(self):
result = []
for record in self:
result.append(record.name)
return result