Este artículo actualmente está disponible solo en inglés. La traducción estará disponible próximamente.
A user clicks a button on an Odoo form. Nothing happens. No error toast, no page change, no console log. The button visually depresses but the side effect never occurs. This is one of the most user-baffling bugs in Odoo 17.0/18.0/19.0 because every layer (view, controller, server, ACL, JS) is a candidate for the cause.
Quick Fix
Open the JS console (F12) before clicking. Then click. The first error is your answer 80 percent of the time. If the console is silent, check the network tab — does the click trigger an HTTP request? If not, the button is not bound. If it does and returns success, the action handler is broken.
# Common server-side bug: method does not exist on the model
def action_confirm_priority(self):
self.ensure_one()
self.priority = '3'
return True
The button's XML must reference an existing method:
<button name="action_confirm_priority" string="Confirm Priority" type="object"/>
Mismatch between name= and the actual method = silent no-op.
Why This Happens
Odoo buttons travel a long path from XML to Python execution:
- View arch declares the button with
name=,type=,class=. - Web client binds a click handler.
- On click, sends RPC to
/web/dataset/call_button(or similar) with model + method + ids. - Server resolves the method on the model, runs it.
- Server returns the result (often an action dict for the client to interpret).
Failures at each step:
- Bad XML. Wrong
type=, missingname=, orinvisible=evaluating True. - JS client error. A custom widget broke binding. Console shows it.
- RPC fails. ACL denies, but the failure is swallowed in older Odoo versions.
- Method does not exist on the model. Server logs
AttributeErrorbut client gets a 200 with empty action. - Method returns nothing. No visual feedback, user thinks nothing happened.
Step-by-Step Diagnosis
1. Read the button XML.
<button name="action_confirm_priority"
string="Confirm Priority"
type="object"
class="btn-primary"
invisible="state == 'done'"/>
Verify:
name=matches a method on the model.type="object"(Python method) versustype="action"(server action XML id) versustype="workflow"(deprecated).invisible=does not silently hide the button.
2. Click with DevTools open. Network tab. Filter for "call_button" or "call_kw". Click. Observe.
If no request fires: client-side binding broken. Look at console for JS errors. Check invisible= is False for the current state.
If request fires and returns 200 with false body: server-side method ran but returned no action.
If request returns 500: server raised. Check server log.
3. Test the method in a shell.
order = env['sale.order'].browse(14)
print(hasattr(order, 'action_confirm_priority')) # True or False
order.action_confirm_priority() # does it do what you expect?
If hasattr returns False, the method is not where you think. Could be a typo, a missing inherit, a load-order issue.
4. Check ACL.
env['sale.order'].with_user(user_id).check_access_rights('write', raise_exception=False)
If False for non-admin, the button click cannot write — but the failure may be silent.
5. Check method's @api decorator.
@api.model # WRONG for a button on a record — runs without self
def action_confirm_priority(self):
pass
# RIGHT — recordset action
def action_confirm_priority(self):
self.ensure_one()
...
@api.model makes the method a class-level method that does not get a record. Buttons on form views need recordset methods.
Permanent Fix
Standardize the button shape:
<button name="action_confirm_priority"
string="Confirm Priority"
type="object"
class="btn-primary"
invisible="state in ('done', 'cancel')"/>
Standardize the method:
class SaleOrder(models.Model):
_inherit = 'sale.order'
def action_confirm_priority(self):
for order in self:
if order.state in ('done', 'cancel'):
continue
order.priority = '3'
return {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'title': _("Priority confirmed"),
'message': _("%s orders updated.") % len(self),
'type': 'success',
},
}
Returning a notification action gives the user visible confirmation. Returning True is technically correct but leaves no feedback.
For methods that must return another record's action:
def action_open_invoice(self):
self.ensure_one()
return {
'type': 'ir.actions.act_window',
'res_model': 'account.move',
'res_id': self.invoice_ids[:1].id,
'view_mode': 'form',
'target': 'current',
}
For ACL-blocked buttons, decide:
- If the user should never see the button, add
groups=to hide it. - If the user should see it but get a clear error, add an explicit check:
def action_confirm_priority(self):
if not self.env.user.has_group('sales_team.group_sale_manager'):
raise UserError(_("Only Sales Managers can confirm priority."))
...
Silent failure is the worst possible UX. Either show or hide explicitly.
How to Prevent It
- Pre-commit lint that matches button name= to method. A simple Python script that walks XML, extracts every button name, and asserts the method exists on the named model. Catches typos before merge.
- Always return a notification or action from button methods. Make
return Truea code smell. Either an action dict or a notification keeps the UX honest. - Test buttons with Playwright. A test that clicks every visible button on the form view and asserts a side effect catches every silent regression.
- Consistent button naming. Use
action_<verb>_<object>consistently. Predictable names mean lint rules can verify shapes. - No
@api.modelon form-view buttons. Reserve@api.modelfor class-level methods called from server actions. Form buttons always operate on a record. - Show error toasts on failure. Wrap risky actions in try/except and surface the user-friendly error via
UserError. Hidden 500s on click are always confusing.
Related Errors
- Wizard popup not opening — same family of action-return issues.
- attrs/states deprecated in Odoo 19 —
states=on buttons must be migrated. - Form view renders blank — if the form is broken, buttons cannot be clicked at all.
- AccessError on res.users — silent ACL failures often look like broken buttons.
Frequently Asked Questions
Why does my button work in dev but not in production?
Three common reasons. First, a JS asset bundle is stale on production — clear /web/assets/%. Second, the production database is missing a custom field referenced by the button's invisible= expression, hiding it. Third, ACL is stricter on production than dev.
Can I make a button visible to admins only?
Yes:
<button name="action_admin_only"
groups="base.group_system"
string="Reset"/>
This is enforced server-side too — even if a user constructs an RPC call to the method, the access check fires.
My button used to fire on Odoo 17.0 but does not on 19.0. What changed?
Most likely states="..." on the button. Convert to invisible="state not in (...)":
<button name="action_confirm" states="draft,sent"/>
<!-- becomes -->
<button name="action_confirm" invisible="state not in ('draft', 'sent')"/>
See attrs/states deprecated in Odoo 19 for the full migration.
How do I add a confirmation dialog before the action runs?
Use confirm= on the button:
<button name="action_dangerous"
string="Delete All"
confirm="Are you sure? This cannot be undone."/>
The user sees a native modal with OK/Cancel before the RPC fires.
Can I trigger a Python method from the kanban or list view, not just form?
Yes. Kanban supports <button name="..." type="object"> inside the kanban template. List views support button columns:
<list>
<field name="name"/>
<button name="action_confirm" type="object" icon="fa-check"/>
</list>
These follow the same rules as form buttons — type="object", valid name=, return action dict for any visual feedback.
How do I show a loading spinner while the action runs?
Long-running button actions should return early and signal "in progress". Patterns: (1) launch the work in a queue_job and return immediately with a notification "Job queued"; (2) use an ir.actions.client to display a custom spinner widget. Avoid synchronous methods longer than 5 seconds — users repeat-click and you get duplicate executions.
Need help with a tricky Odoo error? ECOSIRE's Odoo experts have shipped 215+ modules — get expert help.
Escrito por
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
Transforme su negocio con Odoo ERP
Implementación, personalización y soporte experto de Odoo para optimizar sus operaciones.
Artículos relacionados
Cómo agregar un botón personalizado a una vista de formulario de Odoo (2026)
Agregue botones de acción personalizados a las vistas de formulario de Odoo 19: método de acción de Python, herencia de vistas, visibilidad condicional, cuadros de diálogo de confirmación. Probado en producción.
Cómo agregar un campo personalizado en Odoo sin Studio (2026)
Agregue campos personalizados a través de un módulo personalizado en Odoo 19: herencia de modelo, extensión de vista, campos calculados, decisiones de tienda/no tienda. Código primero, controlado por versiones.
Cómo agregar un informe personalizado en Odoo usando un diseño externo
Cree un informe PDF con su marca en Odoo 19 usando web.external_layout: plantilla QWeb, formato de papel, enlace de acción. Con logotipo impreso + anulaciones de pie de página.