Skip to content

API Decorators

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': '15.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_unit

Constraint 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")

@api.model_create_multi

Optimizes batch record creation.

python
@api.model_create_multi
def create(self, vals_list):
    for vals in vals_list:
        if 'code' not in vals:
            vals['code'] = self.env['ir.sequence'].next_by_code('model.code')
    return super().create(vals_list)

Return Value Decorators

@api.returns

Specifies the return type and post-processing.

python
@api.returns('self', lambda value: value.id)
def get_parent_record(self):
    return self.parent_id

@api.returns('res.partner')
def get_related_partners(self):
    return self.partner_ids.filtered(lambda p: p.is_company)

@api.multi (Deprecated in v13+)

Legacy decorator for methods working on recordsets.

python
# Deprecated - use recordset methods directly
@api.multi
def action_confirm(self):
    for record in self:
        record.state = 'confirmed'

# Modern approach
def action_confirm(self):
    for record in self:
        record.state = 'confirmed'

Advanced Decorators

@api.autovacuum

Automatically called by the vacuum cleaner cron.

python
@api.autovacuum
def _gc_temp_files(self):
    """Clean up temporary files"""
    temp_files = self.search([
        ('is_temp', '=', True),
        ('create_date', '<', fields.Datetime.now() - timedelta(days=7))
    ])
    temp_files.unlink()

@api.model_cr

Low-level method with direct cursor access.

python
@api.model_cr
def init_database(self):
    """Initialize database with custom SQL"""
    self.env.cr.execute("""
        UPDATE res_company 
        SET currency_id = %s 
        WHERE currency_id IS NULL
    """, (self.env.ref('base.USD').id,))

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.0

Onchange 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 records

Write Method

python
def write(self, vals):
    result = super().write(vals)
    if 'state' in vals:
        self._trigger_state_change()
    return result
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_field

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

Common 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'
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
  • @api.cr: Use @api.model_cr instead
  • @api.v7: Legacy compatibility, avoid in new code

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