Skip to content

Workflows

Basic State Management

State Field Definition

python
from odoo import models, fields, api

class ModelName(models.Model):
    _name = 'model.name'
    
    state = fields.Selection([
        ('draft', 'Draft'),
        ('confirmed', 'Confirmed'),
        ('done', 'Done'),
        ('cancel', 'Cancelled')
    ], string='Status', default='draft', tracking=True)

State Change Method

python
def action_confirm(self):
    self.write({'state': 'confirmed'})

def action_done(self):
    self.write({'state': 'done'})

def action_cancel(self):
    self.write({'state': 'cancel'})

def action_draft(self):
    self.write({'state': 'draft'})

Workflow with Conditions

State Change with Validation

python
def action_confirm(self):
    if not self.line_ids:
        raise UserError(_('Please add at least one line.'))
    if not self.partner_id:
        raise UserError(_('Please select a partner.'))
    self.write({'state': 'confirmed'})

State Change with Computed Fields

python
def _compute_can_confirm(self):
    for record in self:
        record.can_confirm = (
            record.state == 'draft' and 
            record.line_ids and 
            record.partner_id
        )

can_confirm = fields.Boolean(
    string='Can Confirm',
    compute='_compute_can_confirm'
)

Workflow with Buttons

Form View Buttons

xml
<form>
    <header>
        <button name="action_confirm" 
                string="Confirm" 
                type="object" 
                class="oe_highlight"
                attrs="{'invisible': [('state', '!=', 'draft')]}"/>
        <button name="action_done" 
                string="Mark as Done" 
                type="object" 
                class="oe_highlight"
                attrs="{'invisible': [('state', '!=', 'confirmed')]}"/>
        <button name="action_cancel" 
                string="Cancel" 
                type="object" 
                attrs="{'invisible': [('state', 'in', ['done', 'cancel'])]}"/>
        <button name="action_draft" 
                string="Reset to Draft" 
                type="object" 
                attrs="{'invisible': [('state', '!=', 'cancel')]}"/>
    </header>
    <sheet>
        <field name="state" widget="statusbar"/>
        <!-- Other fields -->
    </sheet>
</form>

Workflow with Automatic Actions

Scheduled Actions

python
def _cron_check_expired(self):
    expired_records = self.search([
        ('state', '=', 'confirmed'),
        ('date_deadline', '<', fields.Date.today())
    ])
    expired_records.write({'state': 'cancel'})

Onchange Methods

python
@api.onchange('state')
def _onchange_state(self):
    if self.state == 'confirmed':
        self.date_confirmed = fields.Datetime.now()
    elif self.state == 'done':
        self.date_done = fields.Datetime.now()

Workflow with Notifications

Email Notifications

python
def action_confirm(self):
    self.write({'state': 'confirmed'})
    template = self.env.ref('module.email_template_confirm')
    template.send_mail(self.id, force_send=True)

Chatter Messages

python
def action_confirm(self):
    self.write({'state': 'confirmed'})
    self.message_post(
        body=_('Order confirmed by %s') % self.env.user.name,
        message_type='comment'
    )

Workflow with Approvals

Approval Process

python
class ModelName(models.Model):
    _name = 'model.name'
    
    state = fields.Selection([
        ('draft', 'Draft'),
        ('to_approve', 'To Approve'),
        ('approved', 'Approved'),
        ('done', 'Done'),
        ('cancel', 'Cancelled')
    ], string='Status', default='draft')
    
    approver_id = fields.Many2one('res.users', string='Approver')
    
    def action_to_approve(self):
        self.write({'state': 'to_approve'})
    
    def action_approve(self):
        if self.env.user != self.approver_id:
            raise UserError(_('Only the approver can approve this record.'))
        self.write({'state': 'approved'})

Workflow with Stages

Kanban Stages

python
class ModelName(models.Model):
    _name = 'model.name'
    
    stage_id = fields.Many2one(
        'model.stage',
        string='Stage',
        default=lambda self: self.env['model.stage'].search([], limit=1)
    )
    
    def action_next_stage(self):
        next_stage = self.env['model.stage'].search([
            ('sequence', '>', self.stage_id.sequence)
        ], limit=1)
        if next_stage:
            self.write({'stage_id': next_stage.id})

Workflow with Validation Rules

State-based Validation

python
@api.constrains('state', 'amount')
def _check_amount(self):
    for record in self:
        if record.state == 'confirmed' and record.amount <= 0:
            raise ValidationError(_('Amount must be greater than 0 in confirmed state.'))

State-based Computed Fields

python
@api.depends('state', 'line_ids.price_subtotal')
def _compute_amount_total(self):
    for record in self:
        if record.state == 'draft':
            record.amount_total = sum(record.line_ids.mapped('price_subtotal'))
        else:
            record.amount_total = record.amount_total

Best Practices

State Names

python
# Good
state = fields.Selection([
    ('draft', 'Draft'),
    ('confirmed', 'Confirmed'),
    ('done', 'Done'),
    ('cancel', 'Cancelled')
])

# Bad
state = fields.Selection([
    ('new', 'New'),
    ('ok', 'OK'),
    ('finish', 'Finish'),
    ('stop', 'Stop')
])

State Transitions

python
# Good
def action_confirm(self):
    if self.state != 'draft':
        raise UserError(_('Only draft records can be confirmed.'))
    self.write({'state': 'confirmed'})

# Bad
def action_confirm(self):
    self.write({'state': 'confirmed'})  # No validation

State Documentation

python
class ModelName(models.Model):
    _name = 'model.name'
    
    state = fields.Selection([
        ('draft', 'Draft'),      # Initial state, can be edited
        ('confirmed', 'Confirmed'),  # Validated, ready for processing
        ('done', 'Done'),        # Processed, read-only
        ('cancel', 'Cancelled')  # Cancelled, can be reset to draft
    ], string='Status', default='draft', tracking=True)