Skip to Content

Odoo Development

Odoo Wizards and Transient Models

A wizard is a dialog that collects input before an action runs, and underneath it is an ordinary model with one difference: its records are temporary and get cleaned up automatically. That single difference drives everything about how you write one.

Part of our guide to Odoo Development

A wizard is a model with a shorter life

from odoo import fields, models

class StockAdjustWizard(models.TransientModel):
    _name = "stock.adjust.wizard"
    _description = "Adjust Stock"

    reason = fields.Char(required=True)
    adjustment_date = fields.Date(default=fields.Date.context_today)
    line_ids = fields.One2many("stock.adjust.wizard.line", "wizard_id")

Everything you know about models applies. Fields, defaults, onchange, computed fields, constraints, all identical. The difference is TransientModel instead of Model.

Two consequences follow from that base class.

Access rights are simplified. Any user can create records, and can only access the ones they created. The superuser sees everything. So a wizard does not need the elaborate access setup a normal model does, though it still needs a line in ir.model.access.csv.

Records are vacuumed. Odoo runs an automatic cleanup that removes old transient records based on two configured limits, a maximum age in hours and a maximum row count. The practical meaning is that a wizard record is guaranteed to exist for the length of the interaction and guaranteed not to exist forever.

Getting the records the wizard acts on

A wizard opened from a list or form does not automatically know its subject. Odoo passes it in the context:

  • active_model, the model name it was opened from
  • active_id, a single record
  • active_ids, the full selection

Read them in a default method:

from odoo import api, fields, models

class StockAdjustWizard(models.TransientModel):
    _name = "stock.adjust.wizard"
    _description = "Adjust Stock"

    picking_ids = fields.Many2many("stock.picking")

    @api.model
    def default_get(self, fields_list):
        res = super().default_get(fields_list)
        if self.env.context.get("active_model") == "stock.picking":
            res["picking_ids"] = [(6, 0, self.env.context.get("active_ids", []))]
        return res

The frequent bug is reading active_id when the wizard can be opened from a multi-selection. It works perfectly in testing on one record and silently processes one of five in production. Use active_ids and let it contain a single element when that is the case.

Opening it

A wizard is opened by an action with target set to new, which is what makes it a dialog rather than a page:


    Adjust Stock
    stock.adjust.wizard
    form
    new

To put it on a list view's action menu, set binding_model_id on the action. To put it on a form, add a button calling the action by name.

The form, and the button that does the work


    stock.adjust.wizard.form
    stock.adjust.wizard
    
        

The footer element is what places the buttons in the dialog's button bar. special="cancel" closes without calling anything.

What the method returns

The return value controls what happens next, and the three useful cases are:

Close the dialog. Return {"type": "ir.actions.act_window_close"}, or simply return nothing, which does the same.

Go somewhere. Return an action dictionary, which is how a wizard that creates a record can open it:

def action_apply(self):
    self.ensure_one()
    move = self._create_adjustment()
    return {
        "type": "ir.actions.act_window",
        "res_model": "stock.move",
        "res_id": move.id,
        "view_mode": "form",
    }

Open another wizard. Return an action pointing at a second transient model, passing anything it needs through the context. This is how multi-step wizards are built, and it is worth noting that each step is a separate record rather than one record moving through states.

ensure_one() at the top of the method is worth the habit. A wizard method should be acting on exactly one wizard record, and the assertion makes that explicit.

When not to use a wizard

Wizards are easy to build and easy to overuse.

If the action needs no input, it should be a button that just does it. A dialog asking only for confirmation is friction, and Odoo has a lighter confirmation mechanism for that.

If the input belongs to the record permanently, it belongs on the record as a field, not collected transiently each time.

If the process is long-running, a wizard is the wrong container: the user is waiting on a dialog while it runs. Consider a scheduled action doing the work in the background, with the wizard only queueing it.

FAQ

Questions, answered.

What is a TransientModel in Odoo?

A model whose records are temporary and periodically deleted by an automatic vacuum. It has simplified access rights: any user can create records and can only see their own. It is the base for wizards, where the record exists purely to hold the values a dialog collects.

How does a wizard know which records it was opened from?

Through the context. Odoo passes active_id for a single record, active_ids for a selection and active_model for the model name. Reading those in a default method is how a wizard picks up its subject, and forgetting active_ids is why a wizard often works on one record and misses the rest.

Can I store data in a wizard record permanently?

No, and you should not try. Transient records are deleted by the vacuum based on age and count limits, so anything you need to keep must be written to a normal model before the wizard closes.

With CODEerts

Need something built properly?

We are certified Odoo partners. We build custom Odoo modules that extend the framework rather than fight it, and we publish our own apps on the Odoo Store.

See how we can helpBook a call