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)