Skip to content

Workflows ​

Examples target Odoo 18 and 19 unless marked. Platform covers Odoo 11 to 20.

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"
                invisible="state != 'draft'"/>
        <button name="action_done" 
                string="Mark as Done" 
                type="object" 
                class="oe_highlight"
                invisible="state != 'confirmed'"/>
        <button name="action_cancel" 
                string="Cancel" 
                type="object" 
                invisible="state in ('done', 'cancel')"/>
        <button name="action_draft" 
                string="Reset to Draft" 
                type="object" 
                invisible="state != 'cancel'"/>
    </header>
    <sheet>
        <field name="state" widget="statusbar"/>
        <!-- Other fields -->
    </sheet>
</form>

From Odoo 17, attrs and states are removed. Put the condition directly in invisible.

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)

Built by Cybrosys Technologies.