This article is currently available in English only. Translation coming soon.
A user clicks a button that should open a wizard popup. Nothing visible happens. The browser console shows a clean response. The server log shows the action method ran successfully. But the modal never appears.
A user clicks a button that should open a wizard popup. Nothing visible happens. The browser console shows a clean response. The server log shows the action method ran successfully. But the modal never appears. This is a quiet but maddening bug on Odoo 17.0/18.0/19.0 — the action ran, the action returned, but the client-side router did not interpret the return as "open a modal".
Quick Fix
Verify the action method returns a properly-shaped action dict with target='new':
def action_open_priority_wizard(self):
self.ensure_one()
return {
'name': 'Set Priority',
'type': 'ir.actions.act_window',
'res_model': 'sale.order.priority.wizard',
'view_mode': 'form',
'target': 'new', # critical — opens as modal
'context': {'default_order_id': self.id},
}
Without 'target': 'new', Odoo navigates to a full-page form instead of opening a modal. With a typo or missing key, the client silently does nothing.
Why This Happens
When a Python button method returns a dictionary, Odoo's web client interprets it as an action to execute. The interpretation depends on every key being correct. The five common failure modes:
- Missing
'target': 'new'— the action opens as a full page navigation instead of a modal, sometimes silently if the current breadcrumb stack is mid-flow. - Wrong
type—'ir.actions.act_window'versus'ir.actions.client'versus'ir.actions.server'each have different semantics. Wrong type = wrong handler. - Missing
view_idfor a model that has no default form. The client errors silently in some versions. - Permissions — the user does not have access to the wizard model. The action returns successfully but the next step (opening the wizard form) hits an
AccessErrorthat is swallowed. - JavaScript error in the frame surrounding the action processor, which aborts before the action is interpreted.
Step-by-Step Diagnosis
1. Open the JS console. F12, Console tab. Click the button. Look for any error.
2. Inspect the network response. F12, Network tab. The /web/dataset/call_kw request should return a 200 with a body containing your action dict. Verify every key:
{
"type": "ir.actions.act_window",
"res_model": "sale.order.priority.wizard",
"view_mode": "form",
"target": "new",
"context": {"default_order_id": 14}
}
If target is missing or anything else is wrong, the bug is in your Python.
3. Check ACL on the wizard model.
env['sale.order.priority.wizard'].with_user(user_id).check_access_rights('create', raise_exception=False)
False means the user cannot create the wizard, which silently fails the modal open.
4. Verify the wizard's form view exists.
SELECT id, name, type FROM ir_ui_view
WHERE model = 'sale.order.priority.wizard' AND type = 'form';
If empty, you have a model but no form view — the modal has nothing to render.
5. Test the action manually.
action = order.action_open_priority_wizard()
print(action)
# Verify shape matches expectation
Permanent Fix
Always return the full action dict with all required keys:
class SaleOrder(models.Model):
_inherit = 'sale.order'
def action_open_priority_wizard(self):
self.ensure_one()
return {
'name': _('Set Priority'),
'type': 'ir.actions.act_window',
'res_model': 'sale.order.priority.wizard',
'view_mode': 'form',
'view_id': self.env.ref('my_module.view_priority_wizard_form').id,
'target': 'new',
'context': {
'default_order_id': self.id,
'default_priority': self.priority,
},
}
Including the explicit view_id is belt-and-braces — it removes ambiguity if there are multiple form views for the wizard model.
For wizards triggered from a list view multi-select, also include active_ids:
def action_set_priority_bulk(self):
return {
'type': 'ir.actions.act_window',
'res_model': 'sale.order.priority.wizard',
'view_mode': 'form',
'target': 'new',
'context': {
'active_model': 'sale.order',
'active_ids': self.ids,
'active_id': self.ids[0] if self.ids else False,
},
}
The wizard's compute methods can read self.env.context.get('active_ids') to operate on the selection.
For wizards that should stay open after the action, return another action:
class SaleOrderPriorityWizard(models.TransientModel):
_name = 'sale.order.priority.wizard'
def action_apply(self):
order = self.env['sale.order'].browse(self.env.context['active_ids'])
order.priority = self.priority
return {
'type': 'ir.actions.act_window_close', # or another wizard
}
act_window_close cleanly closes the modal. Returning nothing leaves the modal open with no feedback.
For ACL fixes, grant the wizard model's create/read rights to the relevant group:
<record id="access_priority_wizard_user" model="ir.model.access">
<field name="name">priority.wizard user</field>
<field name="model_id" ref="my_module.model_sale_order_priority_wizard"/>
<field name="group_id" ref="sales_team.group_sale_salesman"/>
<field name="perm_read" eval="True"/>
<field name="perm_write" eval="True"/>
<field name="perm_create" eval="True"/>
<field name="perm_unlink" eval="True"/>
</record>
Wizard models are TransientModel — they are written to constantly during use, so users need full CRUD on them.
How to Prevent It
- Standardize wizard return dicts. Put a helper on a base mixin so every wizard call returns the same shape:
class ActionMixin(models.AbstractModel):
_name = 'my_module.action.mixin'
def _wizard_action(self, model, view_xmlid, ctx=None):
return {
'type': 'ir.actions.act_window',
'res_model': model,
'view_mode': 'form',
'view_id': self.env.ref(view_xmlid).id,
'target': 'new',
'context': dict(self.env.context, **(ctx or {})),
}
- Test wizard opens with Playwright. A 5-line test that clicks the button and asserts the modal element is visible catches every regression.
- Always grant ACL to wizards in the same group as the trigger button. Make this part of the module's test data — install the module, attempt to trigger the wizard as the smallest-privilege user who should have access, fail loudly if blocked.
- No silent server actions. Server-side methods that return
Nonefrom a button on a form leave the user with no visual feedback. Even areturn {'effect': 'rainbow_man'}is better than nothing. - Lint return dicts. A simple grep for
return {inside button methods, plus a check thattarget: 'new'is present for any method namedaction_open_*, catches most of these.
Related Errors
- Button onclick fires no action — closely related, sometimes the same root cause.
- Form view renders blank — what happens when the modal opens but the wizard form is broken.
- AccessError on res.users — sibling ACL error.
- attrs/states deprecated in Odoo 19 — buttons may stop firing actions after 19.0 upgrade if attrs were used.
Frequently Asked Questions
Why does my wizard open as a full page instead of a modal?
Almost always missing 'target': 'new'. The client defaults to full-page navigation when target is unset.
Can I open a wizard from JavaScript directly?
Yes, via this.actionService.doAction({...}):
import { useService } from "@web/core/utils/hooks";
setup() {
this.action = useService("action");
}
async openWizard() {
await this.action.doAction({
type: "ir.actions.act_window",
res_model: "sale.order.priority.wizard",
view_mode: "form",
target: "new",
});
}
But you still need ACL and a form view, just like the Python path.
Why does my wizard show stale data the second time I open it?
TransientModel rows are not deleted between opens. If you set a default in default_get and write to it, subsequent opens see the previous write. Use @api.model_create_multi to ensure fresh defaults, or explicitly clear stale rows in default_get.
How do I pass a complex object (like multiple ids) to a wizard?
Through context:
'context': {
'active_ids': self.ids,
'default_partner_ids': [(6, 0, partner_ids)],
}
For one2many and many2many initial values, the default_<field> context key with the right command tuple format works. For complex setup, override default_get on the wizard model to read from active_ids and compute the initial state explicitly.
How do I chain two wizards?
Return another action from the first wizard's submit:
def action_step1(self):
# do step 1 work
return {
'type': 'ir.actions.act_window',
'res_model': 'my.wizard.step2',
'view_mode': 'form',
'target': 'new',
'context': {'default_step1_result_id': self.id},
}
The first modal closes, the second opens immediately. Users see a continuous flow.
Should TransientModel records be cleaned up?
Odoo auto-cleans TransientModel rows older than transient_max_hours (default 1 hour) via a built-in vacuum cron. You normally do not need to do anything. If you store sensitive data in a wizard, set a shorter retention with _transient_max_hours = 0.1 on your wizard class.
Need help with a tricky Odoo error? ECOSIRE's Odoo experts have shipped 215+ modules — get expert help.
تحریر
ECOSIRE TeamTechnical Writing
The ECOSIRE technical writing team covers Odoo ERP, Shopify eCommerce, AI agents, Power BI analytics, GoHighLevel automation, and enterprise software best practices. Our guides help businesses make informed technology decisions.
ECOSIRE
Odoo ERP کے ساتھ اپنے کاروبار کو تبدیل کریں
آپ کے کاموں کو ہموار کرنے کے لیے ماہر Odoo کا نفاذ، حسب ضرورت، اور معاونت۔
متعلقہ مضامین
German E-Invoicing (E-Rechnung) in Odoo: XRechnung, ZUGFeRD and the 2025–2028 Deadlines
Receiving e-invoices is mandatory in Germany since 2025 and issuing follows in 2027/2028: accepted formats, how Odoo produces XRechnung and ZUGFeRD, and what to test.
France's E-Invoicing Reform (2026–2027): What Changes for a Company on Odoo
Receiving e-invoices is mandatory in France since 1 September 2026 and SMEs issue them from 1 September 2027: calendar, approved platforms, Factur-X/UBL/CII and an Odoo checklist.
Odoo 19 سے 20 اپ گریڈ گائیڈ: سرکاری طریقہ کار اور کسٹم ماڈیولز میں کیا چیک کریں
Odoo 19 (یا 17 اور 18) سے Odoo 20 پر اپ گریڈ کیسے کریں: Odoo Online، Odoo.sh اور آن پریمائس کے لیے پہلے ٹیسٹ پھر پروڈکشن کا سرکاری طریقہ، اور 20.0 کی وہ تبدیلیاں جو کسٹم ماڈیولز کو متاثر کرتی ہیں۔