What Is a Wizard?
A wizard is a temporary form that collects user input and performs an action. Unlike regular models (stored permanently), wizards use TransientModel — records are automatically deleted after a period. Common uses: confirmation dialogs, bulk updates, data imports, and multi-step processes.
Basic Wizard
# models/equipment_assign_wizard.py
from odoo import api, fields, models
class EquipmentAssignWizard(models.TransientModel):
_name = 'equipment.assign.wizard'
_description = 'Assign Equipment Wizard'
equipment_id = fields.Many2one(
'equipment.item', string='Equipment', required=True,
)
employee_id = fields.Many2one(
'hr.employee', string='Assign To', required=True,
)
assignment_date = fields.Date(
string='Assignment Date', default=fields.Date.today,
)
notes = fields.Text(string='Notes')
def action_assign(self):
"""Assign equipment to employee."""
self.ensure_one()
self.equipment_id.write({
'assigned_to': self.employee_id.id,
'status': 'in_use',
})
# Post message on equipment
self.equipment_id.message_post(
body=f"Assigned to {self.employee_id.name} on {self.assignment_date}",
)
return {'type': 'ir.actions.act_window_close'}Wizard View
<record id="equipment_assign_wizard_form" model="ir.ui.view">
<field name="name">equipment.assign.wizard.form</field>
<field name="model">equipment.assign.wizard</field>
<field name="arch" type="xml">
<form string="Assign Equipment">
<group>
<field name="equipment_id"/>
<field name="employee_id"/>
<field name="assignment_date"/>
<field name="notes"/>
</group>
<footer>
<button string="Assign" type="object" name="action_assign"
class="btn-primary"/>
<button string="Cancel" class="btn-secondary" special="cancel"/>
</footer>
</form>
</field>
</record>
<record id="equipment_assign_wizard_action" model="ir.actions.act_window">
<field name="name">Assign Equipment</field>
<field name="res_model">equipment.assign.wizard</field>
<field name="view_mode">form</field>
<field name="target">new</field> <!-- Opens as dialog -->
</record>Passing Context to Wizard
# Button on equipment form that opens wizard with pre-filled equipment:
<button name="%(equipment_assign_wizard_action)d" type="action"
string="Assign" class="btn-primary"
context="{'default_equipment_id': active_id}"/>
# In the wizard, equipment_id is auto-filled from contextBulk Operation Wizard
class EquipmentBulkStatusWizard(models.TransientModel):
_name = 'equipment.bulk.status.wizard'
_description = 'Bulk Status Update'
new_status = fields.Selection([
('available', 'Available'),
('maintenance', 'In Maintenance'),
('retired', 'Retired'),
], string='New Status', required=True)
equipment_ids = fields.Many2many(
'equipment.item', string='Equipment',
)
@api.model
def default_get(self, fields_list):
"""Pre-fill with selected records from list view."""
res = super().default_get(fields_list)
if self.env.context.get('active_ids'):
res['equipment_ids'] = [(6, 0, self.env.context['active_ids'])]
return res
def action_update(self):
"""Update status for all selected equipment."""
self.equipment_ids.write({'status': self.new_status})
return {'type': 'ir.actions.act_window_close'}Multi-Step Wizard
class ImportWizard(models.TransientModel):
_name = 'equipment.import.wizard'
_description = 'Import Equipment'
state = fields.Selection([
('upload', 'Upload File'),
('preview', 'Preview'),
('done', 'Done'),
], default='upload')
file = fields.Binary(string='CSV File')
filename = fields.Char()
preview_html = fields.Html(string='Preview', readonly=True)
imported_count = fields.Integer(readonly=True)
def action_preview(self):
"""Parse file and show preview."""
# Parse CSV, generate preview HTML
self.state = 'preview'
self.preview_html = '<table>...preview data...</table>'
return self._reopen()
def action_import(self):
"""Import the data."""
# Create records from parsed data
self.state = 'done'
self.imported_count = 42
return self._reopen()
def _reopen(self):
"""Return action to refresh the wizard form."""
return {
'type': 'ir.actions.act_window',
'res_model': self._name,
'res_id': self.id,
'view_mode': 'form',
'target': 'new',
}Key Patterns
- target='new' — Opens as dialog/popup (standard for wizards)
- default_get — Pre-fill wizard from context (active_id, active_ids)
- context={'default_field': value} — Pass defaults from button
- act_window_close — Close dialog after action
- _reopen() — Refresh wizard for multi-step
Security
# Wizards need ir.model.access too!
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_assign_wizard,equipment.assign.wizard,model_equipment_assign_wizard,base.group_user,1,1,1,1Common Mistakes
- Using Model instead of TransientModel — Records persist forever, bloating the database
- Forgetting security — Wizards need ir.model.access entries
- Not handling empty active_ids — Check context before using
- Returning None — Always return an action dict or act_window_close