Enable Dark Mode!
overview-of-dynamic-wizards-in-odoo-19.jpg
By: Sayed Mahir Abdulla KK

Overview of Dynamic Wizards in Odoo 19

Technical Odoo 19 Odoo Enterprises Odoo Community

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.

Overview of Dynamic Wizards in Odoo 19-cybrosys

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.

Overview of Dynamic Wizards in Odoo 19-cybrosys

  • Window Action — Defines how and which view to open.
  • Server Action — Executes the window action with context as selected partners from the action button.
Overview of Dynamic Wizards in Odoo 19-cybrosys

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.
Overview of Dynamic Wizards in Odoo 19-cybrosys
  • Here, state domain changes based on countries.
Overview of Dynamic Wizards in Odoo 19-cybrosys
  • The Next: Select Tags button shows the next page.
Overview of Dynamic Wizards in Odoo 19-cybrosys

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.


If you need any assistance in odoo, we are online, please chat with us.



0
Comments



Leave a comment



Recent Posts

whatsapp_icon
location

Calicut

Cybrosys Technologies Pvt. Ltd.
Neospace, Kinfra Techno Park
Kakkancherry, Calicut
Kerala, India - 673635

location

Kochi

Cybrosys Technologies Pvt. Ltd.
1st Floor, Thapasya Building,
Infopark, Kakkanad,
Kochi, India - 682030.

location

Bangalore

Cybrosys Techno Solutions
The Estate, 8th Floor,
Dickenson Road,
Bangalore, India - 560042

Send Us A Message