Ad – 728×90
🔧 Backend Development

Odoo Wizards – Transient Models and Dialog Windows

Wizards are a pattern in Odoo for collecting user input in a popup dialog before executing a batch or complex operation. They are built on models.TransientModel — temporary records that Odoo cleans up automatically after the session ends. Understanding wizards lets you build user-friendly confirmation dialogs, bulk-update forms, and multi-step workflow assistants without polluting your permanent data models.

⏱️ 22 min 🎯 Intermediate 📅 Updated 2026

📋 What You Will Learn

  • The difference between TransientModel and Model
  • How to define a wizard Python class
  • Creating a wizard form view with a dialog target
  • Triggering the wizard from a button on another model
  • Returning actions from wizard execute methods

What is a Wizard?

A wizard is a form that pops up as a dialog window, collects some input from the user, then executes an action when the user clicks a button (typically labelled "Confirm" or "Apply"). Common uses include:

  • Confirming a destructive action with a reason or note field
  • Bulk processing selected records (e.g. marking 50 loans as returned at once)
  • Collecting parameters for a report or export
  • Multi-step onboarding or configuration flows

Wizards are implemented as TransientModel — a model subclass whose database records are automatically garbage-collected by Odoo's scheduled vacuum job after a configurable timeout (default: 1 hour).

TransientModel vs Model

Featuremodels.Modelmodels.TransientModel
Data persistencePermanent — stays until deletedTemporary — auto-cleaned after session
Appears in menusUsually yesNo — accessed only via action
Access rightsFull — defined in ir.model.access.csvAll internal users can create/read by default
Typical useBusiness data (invoices, products…)Popup forms, parameter collection, batch ops
Vacuum cleanupNever automaticYes — records older than 1 hr are deleted

Wizard Python Class

A wizard is just a Python class that inherits from models.TransientModel. It has fields for user input and a method (usually action_execute) that performs the work and returns an action dict:

Python
from odoo import models, fields, api
from odoo.exceptions import UserError
import logging

_logger = logging.getLogger(__name__)

class LibraryBulkReturnWizard(models.TransientModel):
    _name = 'library.bulk.return.wizard'
    _description = 'Bulk Book Return Wizard'

    # Wizard fields — what we collect from the user
    return_date = fields.Date(
        string='Return Date',
        required=True,
        default=fields.Date.today,
    )
    condition = fields.Selection([
        ('good', 'Good Condition'),
        ('damaged', 'Damaged'),
        ('lost', 'Lost'),
    ], string='Condition', required=True, default='good')
    note = fields.Text(string='Internal Note')

    # Reference back to the loans being returned
    # (populated automatically when wizard is launched from a list)
    loan_ids = fields.Many2many(
        'library.loan',
        string='Loans to Return',
    )

    @api.model
    def default_get(self, fields_list):
        """Pre-populate loan_ids from the records selected in the list view."""
        res = super().default_get(fields_list)
        active_ids = self.env.context.get('active_ids', [])
        if 'loan_ids' in fields_list and active_ids:
            res['loan_ids'] = [(6, 0, active_ids)]
        return res

    def action_execute(self):
        """Mark all selected loans as returned and close the dialog."""
        self.ensure_one()
        if not self.loan_ids:
            raise UserError("No loans selected to return.")

        for loan in self.loan_ids:
            if loan.state not in ('active', 'overdue'):
                continue
            loan.write({
                'state': 'returned',
                'return_date': self.return_date,
                'return_condition': self.condition,
                'return_note': self.note,
            })
            _logger.info("Loan %d returned on %s", loan.id, self.return_date)

        # Return an action to reload the originating list view
        return {
            'type': 'ir.actions.act_window',
            'res_model': 'library.loan',
            'view_mode': 'list,form',
            'name': 'Loans',
        }

    def action_cancel(self):
        """Just close the dialog without doing anything."""
        return {'type': 'ir.actions.act_window_close'}
Ad – 336×280

Wizard View (Form Dialog)

The wizard view is a standard <form> view. The target: 'new' in the action (not in the view itself) is what makes it open as a dialog. Keep wizard forms concise — they should show only what the user needs to fill in:

XML
<odoo>
    <record id="view_library_bulk_return_wizard_form" model="ir.ui.view">
        <field name="name">library.bulk.return.wizard.form</field>
        <field name="model">library.bulk.return.wizard</field>
        <field name="arch" type="xml">
            <form string="Return Books">
                <group>
                    <field name="return_date"/>
                    <field name="condition"/>
                </group>
                <group string="Loans Being Returned">
                    <field name="loan_ids" widget="many2many_tags" readonly="1"/>
                </group>
                <group>
                    <field name="note" placeholder="Optional internal note…"/>
                </group>
                <footer>
                    <!-- Primary action button -->
                    <button name="action_execute"
                            string="Confirm Return"
                            type="object"
                            class="btn-primary"/>
                    <!-- Cancel discards the wizard record -->
                    <button string="Cancel"
                            class="btn-secondary"
                            special="cancel"/>
                </footer>
            </form>
        </field>
    </record>
</odoo>

The <footer> element renders buttons in the dialog's footer bar. special="cancel" closes the dialog without calling any Python method — it discards the wizard record automatically.

Triggering a Wizard from a Button

There are two common ways to open a wizard: from a list view's Action menu (good for bulk operations on selected records) or from a button on a form view.

From the Action Menu (List View)

Define an ir.actions.act_window and bind it to the model:

XML
<odoo>
    <record id="action_bulk_return_wizard" model="ir.actions.act_window">
        <field name="name">Return Selected Books</field>
        <field name="res_model">library.bulk.return.wizard</field>
        <field name="view_mode">form</field>
        <field name="target">new</field>  <!-- 'new' opens as popup dialog -->
        <!-- Bind to the loan model so it appears in Actions dropdown -->
        <field name="binding_model_id" ref="model_library_loan"/>
        <field name="binding_view_types">list</field>  <!-- show only in list view -->
    </record>
</odoo>

From a Form View Button

A button on a form calls a Python method on the current model that creates a wizard record and returns an action:

XML
<!-- In the library.loan form view -->
<button name="action_open_return_wizard"
        string="Return This Book"
        type="object"
        class="btn-primary"
        attrs="{'invisible': [('state', '!=', 'active')]}"/>
Python
class LibraryLoan(models.Model):
    _inherit = 'library.loan'

    def action_open_return_wizard(self):
        """Open the return wizard pre-populated with this single loan."""
        self.ensure_one()
        wizard = self.env['library.bulk.return.wizard'].create({
            'loan_ids': [(6, 0, self.ids)],
        })
        return {
            'type': 'ir.actions.act_window',
            'res_model': 'library.bulk.return.wizard',
            'view_mode': 'form',
            'res_id': wizard.id,
            'target': 'new',
        }

Wizard Return Actions

The method called by the wizard's confirm button should return an action dict. Here are the most common return values:

Python
def action_execute(self):
    # ... do the work ...

    # Option 1: Close the dialog and do nothing else
    return {'type': 'ir.actions.act_window_close'}

    # Option 2: Navigate to a list of the affected records
    return {
        'type': 'ir.actions.act_window',
        'name': 'Returned Loans',
        'res_model': 'library.loan',
        'view_mode': 'list,form',
        'domain': [('id', 'in', self.loan_ids.ids)],
    }

    # Option 3: Navigate to a single record's form view
    return {
        'type': 'ir.actions.act_window',
        'res_model': 'library.loan',
        'view_mode': 'form',
        'res_id': new_loan.id,
        'target': 'current',  # opens in the main window, not a popup
    }

    # Option 4: Reload the parent form (useful when wizard modified the parent record)
    return {
        'type': 'ir.actions.client',
        'tag': 'reload',
    }

Summary

📋 Key Points

  • models.TransientModel creates temporary records automatically cleaned up by Odoo's vacuum
  • Wizard forms open as dialog popups when the action has target: 'new'
  • Use default_get to pre-populate wizard fields from context['active_ids']
  • The execute method does the work and returns an action dict to control what happens next
  • special="cancel" on a footer button closes the dialog and discards the wizard record cleanly
  • Bind the wizard action to a model with binding_model_id to add it to the Actions menu
  • Return {'type': 'ir.actions.act_window_close'} to simply dismiss the dialog

FAQ

How long do TransientModel records persist in the database? +

By default, Odoo's automated vacuum job deletes transient model records older than 1 hour. This is controlled by the base_setup.default_transient_age_override system parameter. You can also call self.env['library.bulk.return.wizard']._transient_clean_rows_older(3600) manually. For very large databases, keeping transient tables small improves performance.

Can a wizard have multiple pages or steps? +

Yes. Use Odoo's notebook/tab widget to create a multi-tab form, or use the page field (an Integer or Selection on the wizard model) combined with attrs visibility rules to show/hide different groups of fields. Each "Next" button writes to the page field and the view dynamically shows the next set of inputs. Alternatively, the first wizard's confirm button can return an action that opens a second wizard.

Do I need to add security rules for my wizard model? +

TransientModel records are accessible to all internal users by default — you don't need to add an entry in ir.model.access.csv for basic read/create/write access. However, if you want to restrict which user groups can even open the wizard, you should add a groups attribute to the action that opens it, or check permissions inside the action_execute method.

What is the difference between target: 'new' and target: 'current'? +

target: 'new' opens the view in a popup dialog window, overlaid on top of the current view. target: 'current' (the default) replaces the current view in the main content area. Use 'new' for wizards and confirmation dialogs, and 'current' for normal navigation between records and views.