In Odoo, Wizards are transient models (models.TransientModel) used to collect user inputs and perform short-term actions — like generating reports, confirming operations, or configuring records.
A Dynamic Wizard adapts its behavior based on user input or context, such as showing extra fields when a checkbox is selected or changing steps depending on what the user chooses.
Now, we’ll create a dynamic multi-step wizard that:
- Use @api.onchange to show/hide or enable/disable fields based on user input.
- Create a wizard with conditional steps (step?1 > step?2 or skip step?2 depending on a choice).
- Pre-fill default values using context (for example when wizard is launched from a record).
Example:
Module implementing a compact TransientModel wizard (partner.tag.wizard) to bulk-assign tags (partner categories) to contacts in Odoo 19. The wizard demonstrates a two-step flow (Select > Confirm), conditional UI elements (showing country/state fields only when location filtering is enabled), previews, and dynamic behavior for "add vs replace" tag modes.

Simple_wizard.py:
This file defines the transient model for the Bulk Partner Tag Assignment wizard.
# -*- coding: utf-8 -*-
from odoo import api, fields, models
from odoo.exceptions import UserError
class PartnerTagWizard(models.TransientModel):
_name = "partner.tag.wizard"
_description = "Bulk Partner Tag Assignment Wizard"
step = fields.Selection([
('select', 'Filter Partners'),
('confirm', 'Review & Assign Tags')
], default='select', string="Step")
# Step 1: Filter Selection
partner_type = fields.Selection([
('customer', 'Customers'),
('supplier', 'Vendors'),
('both', 'Both'),
('all', 'All')
], string="Partner Type", required=True, default='customer')
include_inactive = fields.Boolean(
string="Include Archived Partners",
help="Include archived/inactive partners in the selection"
)
filter_by_location = fields.Boolean(
string="Filter by Location",
help="Enable location-based filtering"
)
country_ids = fields.Many2many(
'res.country',
string="Countries",
help="Filter partners by specific countries"
)
state_ids = fields.Many2many(
'res.country.state',
string="States",
help="Filter partners by specific states"
)
# Step 2: Tag Assignment
tag_ids = fields.Many2many(
'res.partner.category',
string="Tags to Assign",
help="Select tags to assign to filtered partners"
)
replace_tags = fields.Boolean(
string="Replace Existing Tags",
help="If checked, removes all existing tags before assigning new ones"
)
partner_count = fields.Integer(
string="Partners Found",
compute="_compute_partner_count",
store=False
)
preview_partner_ids = fields.Many2many(
'res.partner',
string="Partners to be Tagged",
compute="_compute_preview_partners",
store=False
)
# Pre-fill data using context
@api.model
def default_get(self, fields_list):
res = super().default_get(fields_list)
context = self.env.context
# Pre-fill from active partners if called from partner list view
active_ids = context.get('active_ids', [])
if active_ids:
partners = self.env['res.partner'].browse(active_ids)
# Determine partner type from selected records
has_customers = any(p.customer_rank > 0 for p in partners)
has_suppliers = any(p.supplier_rank > 0 for p in partners)
if has_customers and has_suppliers:
res['partner_type'] = 'both'
elif has_customers:
res['partner_type'] = 'customer'
elif has_suppliers:
res['partner_type'] = 'supplier'
return res
# Dynamic field visibility
@api.onchange('filter_by_location')
def _onchange_filter_by_location(self):
"""Clear location fields when filter is disabled"""
if not self.filter_by_location:
self.country_ids = False
self.state_ids = False
# Compute partner count based on filters
@api.depends('partner_type', 'include_inactive', 'filter_by_location',
'country_ids', 'state_ids')
def _compute_partner_count(self):
for wizard in self:
domain = wizard._get_partner_domain()
wizard.partner_count = self.env['res.partner'].search_count(domain)
@api.depends('partner_type', 'include_inactive', 'filter_by_location',
'country_ids', 'state_ids')
def _compute_preview_partners(self):
"""Fetch partners based on domain for preview"""
for wizard in self:
domain = wizard._get_partner_domain()
wizard.preview_partner_ids = self.env['res.partner'].search(domain)
def _get_partner_domain(self):
"""Build domain based on wizard filters"""
domain = []
# If active partners in context
if self.env.context.get('active_ids'):
domain.append(('id', 'in', self.env.context.get('active_ids')))
# Partner type filter
if self.partner_type == 'customer':
domain.append(('customer_rank', '>', 0))
elif self.partner_type == 'supplier':
domain.append(('supplier_rank', '>', 0))
elif self.partner_type == 'both':
domain.append('|')
domain.append(('customer_rank', '>', 0))
domain.append(('supplier_rank', '>', 0))
# Active/Inactive filter
if self.include_inactive:
domain.append(('active', 'in', [True, False]))
# Location filters
if self.filter_by_location:
if self.country_ids:
domain.append(('country_id', 'in', self.country_ids.ids))
if self.state_ids:
domain.append(('state_id', 'in', self.state_ids.ids))
return domain
# Step navigation
def action_next(self):
"""Move to confirmation step"""
self.ensure_one()
if self.partner_count == 0:
raise UserError("No partners found matching your filters. Please adjust your criteria.")
self.step = 'confirm'
return {
'type': 'ir.actions.act_window',
'res_model': 'partner.tag.wizard',
'view_mode': 'form',
'target': 'new',
'res_id': self.id,
'context': {
'active_ids': self.preview_partner_ids.ids,
}
}
def action_back(self):
"""Return to selection step"""
self.ensure_one()
self.step = 'select'
return {
'type': 'ir.actions.act_window',
'res_model': 'partner.tag.wizard',
'view_mode': 'form',
'target': 'new',
'res_id': self.id,
'context': {
'active_ids': self.preview_partner_ids.ids,
}
}
def action_assign_tags(self):
"""Assign tags to filtered partners"""
self.ensure_one()
if not self.tag_ids:
raise UserError("Please select at least one tag to assign.")
# Get all matching partners
domain = self._get_partner_domain()
partners = self.env['res.partner'].search(domain)
if not partners:
raise UserError("No partners found to assign tags.")
# Assign tags
for partner in partners:
if self.replace_tags:
# Replace all existing tags
partner.category_id = [(6, 0, self.tag_ids.ids)]
else:
# Add tags to existing ones
partner.category_id = [(4, tag.id) for tag in self.tag_ids]
# Prepare success message with counts
partner_count = len(partners)
tag_count = len(self.tag_ids)
tag_names = ', '.join(self.tag_ids.mapped('name'))
action = self.replace_tags and 'replaced with' or 'assigned to'
message = f"""Tags Successfully {action.title()}!
{partner_count} partner(s) updated,
{tag_count} tag(s): {tag_names}
"""
return {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'title': 'Success',
'message': message,
'type': 'success',
'sticky': False,
},
}
- action_next() — validates choices and moves wizard to the confirm step.
- action_back() — moves back to the select step.
- action_assign_tags() — performs the actual write on matched partners and returns a display_notification.
- _get_partner_domain() — Returns domain based on user choice.
- _onchange_country_ids() — Changes available state based on country.
Simple_wizard_action.xml:
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<!-- Wizard Action -->
<record id="action_partner_tag_wizard" model="ir.actions.act_window">
<field name="name">Bulk Tag Assignment</field>
<field name="res_model">partner.tag.wizard</field>
<field name="view_mode">form</field>
<field name="target">new</field>
<field name="binding_model_id" ref="base.model_res_partner"/>
</record>
<!-- Menu Item under Contacts -->
<menuitem id="menu_partner_tag_wizard"
name="Bulk Tag Assignment"
parent="contacts.menu_contacts"
action="action_partner_tag_wizard"
sequence="50"/>
<!-- Server action to perform on specific partners-->
<record id="action_open_partner_tag_wizard" model="ir.actions.server">
<field name="name">Bulk Assign Tags</field>
<field name="model_id" ref="base.model_res_partner"/>
<field name="state">code</field>
<field name="code">
action = {
'type': 'ir.actions.act_window',
'res_model': 'partner.tag.wizard',
'context': {
'active_model': 'res.partner',
'active_ids': records.ids,
}
}
</field>
</record>
</odoo>
- Menuitem — Adds a clickable menu in the contacts module.

- Window Action — Defines how and which view to open.
- Server Action — Executes the window action with context as selected partners from the action button.

Simple_wizard_view.xml:
Wizard form view with multiple steps.
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<!-- Wizard Form View -->
<record id="view_partner_tag_wizard_form" model="ir.ui.view">
<field name="name">partner.tag.wizard.form</field>
<field name="model">partner.tag.wizard</field>
<field name="arch" type="xml">
<form string="Bulk Tag Assignment">
<field name="step" invisible="1"/>
<!-- Step 1: Filter Selection -->
<group invisible="step != 'select'">
<group class="w-100">
<div class="alert alert-info" role="alert">
<strong>Step 1:</strong>
Select filters to identify partners for tag assignment.
</div>
</group>
<group string="Partner Type">
<field name="partner_type" widget="radio"/>
<field name="include_inactive"/>
</group>
<group string="Location Filters">
<field name="filter_by_location"/>
<field name="country_ids" widget="many2many_tags"
invisible="not filter_by_location"
options="{'no_create': True}"/>
<field name="state_ids" widget="many2many_tags"
domain="[('country_id', 'in', country_ids)]"
invisible="not filter_by_location"
options="{'no_create': True}"/>
</group>
<group class="w-100">
<div class="alert alert-success" role="alert">
<strong>
<field name="partner_count"/>
partner(s)
</strong>
match your criteria.
</div>
</group>
</group>
<footer>
<button name="action_next" string="Next: Select Tags"
type="object" class="btn-primary" invisible="step != 'select'"/>
</footer>
<!-- Step 2: Confirmation & Tag Selection -->
<group invisible="step != 'confirm'">
<group class="w-100">
<div class="alert alert-info" role="alert">
<strong>Step 2:</strong>
Review partners and select tags to assign.
</div>
</group>
<group string="Tag Assignment Options" class="w-100">
<field name="tag_ids" widget="many2many_tags"
options="{'color_field': 'color', 'no_create_edit': True}"
placeholder="Select tags to assign..."/>
<field name="replace_tags"/>
<div class="text-muted" invisible="not replace_tags">
Warning: This will remove all existing tags from selected partners.
</div>
</group>
<group string="Preview" class="w-100">
<group class="w-100">
<div class="alert alert-warning" role="alert">
<strong>
<field name="partner_count"/>
partner(s)
</strong>
will be updated.
</div>
</group>
<field name="preview_partner_ids" nolabel="1"
widget="many2many"
readonly="1">
<list limit="10" decoration-muted="not active">
<field name="display_name"/>
<field name="category_id" widget="many2many_tags"/>
<field name="active" invisible="1"/>
</list>
</field>
</group>
</group>
<footer>
<button name="action_assign_tags" string="Assign Tags"
type="object" class="btn-primary" invisible="step != 'confirm'"
confirm="Are you sure you want to proceed with tag assignment?"/>
<button name="action_back" string="Back"
type="object" class="btn-secondary" invisible="step != 'confirm'"/>
<button string="Cancel" class="btn-secondary" special="cancel"/>
</footer>
</form>
</field>
</record>
</odoo>
Ir.model.access.csv:
id,name,model_id/id,group_id/id,perm_read,perm_write,perm_create,perm_unlink
access_partner_tag_wizard,access.partner.tag.wizard,model_partner_tag_wizard,base.group_user,1,1,1,1
- Here, the Partner Type gets selected by default based on the selected partners in the action.

- Here, state domain changes based on countries.

- The Next: Select Tags button shows the next page.

Conclusion
By combining the patterns above, you can build very dynamic, user-friendly wizards in Odoo 19. Whether you need conditional input, extra steps only when certain choices are made, or pre-filled defaults from the context, the approach remains the same: transient model + @api.onchange + step control + context. This leads to cleaner UIs, fewer errors, and a better user experience.
To read more about How to Create and Manage Wizard in Odoo 18, refer to our blog How to Create and Manage Wizard in Odoo 18.