Wasting $15K on Extra Clicks? Add Custom Buttons to Odoo 18 Views
By Braincuber Team
Published on December 22, 2025
Sales manager uses Odoo product list view. Needs to create sales order from product—clicks product, clicks "Sales" tab, clicks "Create", selects customer, adds product line manually. Takes 8 clicks, 47 seconds average. Does this 23 times daily = 18 minutes wasted. Wishes: "Single 'Quick Order' button from product list." Developer quotes: "$2,400 for custom button." Manager declines (too expensive for "simple button"). Keeps clicking 8 times. Annual waste: 18 min × 5 days × 52 weeks × $67/hour = $5,226.
Your UI customization disaster: Users need quick actions from list/form views (create related record, run report, trigger automation). Standard Odoo: Must navigate multiple menus. Users click 6-12 times for common actions. Productivity killers everywhere. Request custom buttons from developers. Developer backlog 6 weeks. Estimate: $2,400 per button (seems expensive). Business rejects (ROI unclear). Users create workarounds (bookmarks, saved searches, manual processes). Frustration compounds.
Cost: Wasted clicks = 18 min daily × 3 users × 252 work days × $67/hour = $15,097/year. Developer cost for custom buttons = $2,400 × 8 requested buttons = $19,200. Business rejects all (budget constraints). Lost productivity continues. User frustration = decreased morale, increased turnover. Can't compete with companies that have streamlined UX. Workarounds create errors (manual data entry mistakes = 8% error rate = customer complaints).
Odoo 18 Custom Button Implementation fixes this: Add buttons next to "Create" in tree/form views. Inherit web.ListView/web.FormView templates in XML. Create JavaScript controller extending ListController/FormController. Add click handler calling doAction(). Register custom view with js_class attribute. Takes 30 minutes to implement vs $2,400 quoted. Works for any model, any action. Here's how to add custom buttons so you stop losing $15K annually to extra clicks.
You're Losing Money If:
What Custom Buttons Do
Add action buttons next to "Create" button in list/form view headers. Click button → Execute action (open wizard, create record, run report, trigger automation). Reduces 8 clicks to 1 click.
| Standard Odoo (No Custom Button) | With Custom Button |
|---|---|
| 8 clicks to create sales order from product (47 sec) | 1 click "Quick Order" button (6 sec) |
| Developer: $2,400 per button | DIY: 30 min implementation, $0 cost |
| 6-week developer backlog | Implement today (follow tutorial) |
| 18 min daily wasted clicks | Save 16 min daily (streamlined workflow) |
| Manual workarounds (8% errors) | Standardized process (0.2% errors) |
💡 Example Use Cases:
- Product List → "Quick Order": Open sales order wizard pre-filled with product
- Customer Form → "Generate Report": Export customer statement PDF
- Lead List → "Bulk Email": Send mass email to filtered leads
- Invoice Form → "Send Reminder": Email payment reminder instantly
- Project List → "Create Task": Quick task creation from project view
Implementation Overview
Three files needed for each button:
- XML Template (static/src/xml/): Button HTML markup
- JavaScript Controller (static/src/js/): Button click handler logic
- View Inheritance (views/): Register js_class to target view
Add Custom Button to Tree View
Step 1: Create XML Template
Inherit web.ListView template, add button via XPath.
Template Breakdown:
- t-name: Unique template identifier
- t-inherit: Inherit from web.ListView (standard list view)
- xpath expr: Target create button container
- position="inside": Add button inside container (next to Create)
- t-on-click: JavaScript method called on click
Step 2: Create JavaScript Controller
Extend ListController, add click handler method.
/** @odoo-module **/
import { ListController } from "@web/views/list/list_controller";
import { registry } from '@web/core/registry';
import { listView } from '@web/views/list/list_view';
export class ProductListController extends ListController {
setup() {
super.setup();
// Inherits all default ListController functionality
}
/**
* Handle Quick Order button click
* Opens sale order wizard
*/
onClickQuickOrder() {
// actionService already available in ListController
this.actionService.doAction({
type: 'ir.actions.act_window',
res_model: 'sale.order',
name: 'Quick Sale Order',
view_mode: 'form',
view_type: 'form',
views: [[false, 'form']],
target: 'new', // Open as popup
res_id: false, // New record
context: {
// Pre-fill context if needed
'default_product_id': this.model.root.selection[0]?.resId,
}
});
}
}
// Link template to controller
ProductListController.template = "custom_buttons.ListView.Buttons";
// Create custom view configuration
export const CustomProductListView = {
...listView,
Controller: ProductListController,
};
// Register custom view in Odoo registry
registry.category("views").add("product_list_quick_order", CustomProductListView);
JavaScript Breakdown:
- Extend ListController: Inherit all list view functionality
- onClickQuickOrder(): Method called when button clicked
- doAction(): Opens window/wizard/view
- target: 'new': Opens as popup (vs 'current' = same window)
- res_id: false: New record (vs specific ID = edit existing)
- registry.add(): Register view with unique name
Step 3: Inherit Product Tree View
Add js_class attribute to target tree view.
product.template.tree.custom.button
product.template
product_list_quick_order
View Inheritance Breakdown:
- inherit_id: Reference to original view (product tree)
- xpath expr="//tree": Target tree element
- position="attributes": Modify attributes (not content)
- js_class: Must match registry name from JS file
Step 4: Register Assets
Add JavaScript and XML files to __manifest__.py.
{
'name': 'Custom Buttons',
'version': '18.0.1.0.0',
'depends': ['web', 'product', 'sale'],
'data': [
'views/product_views.xml',
],
'assets': {
'web.assets_backend': [
'custom_buttons/static/src/js/tree_button.js',
'custom_buttons/static/src/xml/tree_button.xml',
],
},
}
Add Custom Button to Form View
Step 1: Create Form XML Template
Step 2: Create Form JavaScript Controller
/** @odoo-module **/
import { FormController } from "@web/views/form/form_controller";
import { useService } from "@web/core/utils/hooks";
import { registry } from '@web/core/registry';
import { formView } from '@web/views/form/form_view';
export class ProductFormController extends FormController {
setup() {
super.setup();
// Import action service for opening windows
this.actionService = useService("action");
}
/**
* Handle Quick Order button click from form view
* Access current record data via this.model.root.data
*/
onFormClickQuickOrder() {
const currentProduct = this.model.root.data;
this.actionService.doAction({
type: 'ir.actions.act_window',
res_model: 'sale.order',
name: 'Quick Sale Order',
view_mode: 'form',
view_type: 'form',
views: [[false, 'form']],
target: 'new',
res_id: false,
context: {
// Pre-fill with current product
'default_order_line': [(0, 0, {
'product_id': currentProduct.id,
'product_uom_qty': 1,
})],
}
});
}
}
ProductFormController.template = "custom_buttons.FormView.Buttons";
export const CustomProductFormView = {
...formView,
Controller: ProductFormController,
};
registry.category("views").add("product_form_quick_order", CustomProductFormView);
Step 3: Inherit Product Form View
product.template.form.custom.button
product.template
product_form_quick_order
Step 4: Update Manifest Assets
'assets': {
'web.assets_backend': [
'custom_buttons/static/src/js/tree_button.js',
'custom_buttons/static/src/xml/tree_button.xml',
'custom_buttons/static/src/js/form_button.js',
'custom_buttons/static/src/xml/form_button.xml',
],
},
Advanced Button Examples
Example 1: Export Report Button
onClickExportReport() {
const selectedIds = this.model.root.selection.map(rec => rec.resId);
if (selectedIds.length === 0) {
this.notification.add(
'Please select records to export',
{ type: 'warning' }
);
return;
}
this.actionService.doAction({
type: 'ir.actions.report',
report_type: 'qweb-pdf',
report_name: 'module.report_template_name',
data: {
'ids': selectedIds,
'model': 'product.template'
}
});
}
Example 2: Bulk Action Button
async onClickBulkUpdate() {
const selectedIds = this.model.root.selection.map(rec => rec.resId);
const result = await this.orm.call(
'product.template',
'bulk_update_price',
[selectedIds, { 'discount': 10 }]
);
if (result.success) {
this.notification.add(
`Updated ${selectedIds.length} products`,
{ type: 'success' }
);
this.model.load(); // Reload list view
}
}
Example 3: Conditional Button Display
Testing & Debugging
Common Issues:
Button Not Appearing:
- Check js_class name matches registry name exactly
- Verify assets added to __manifest__.py
- Restart Odoo server + update module
- Clear browser cache (Ctrl+Shift+R)
Click Handler Not Firing:
- Check t-on-click method name matches JS method
- Check browser console for JavaScript errors (F12)
- Verify controller extends correct parent (ListController/FormController)
Action Not Opening:
- Verify res_model exists and accessible
- Check view_mode matches available views
- Test doAction parameters in console first
Real-World Impact
Distribution Company Example:
Before Custom Buttons:
- Warehouse staff creates delivery orders from product list
- Process: Click product → Sales tab → Create → Select customer → Add lines
- 8 clicks, 47 seconds average
- Done 23 times daily × 3 staff = 69 times
- Daily waste: 69 × 47 sec = 54 minutes
- Annual waste: 54 min × 252 days = 227 hours = $15,209 @ $67/hr
After Implementing "Quick Delivery" Button:
- Button opens delivery wizard pre-filled with product
- Staff selects customer, confirms
- 1 click, 6 seconds
- Time saved: 41 seconds per delivery × 69 daily = 47 minutes daily
- Annual savings: 198 hours = $13,266
- Implementation cost: 30 minutes dev time = $34
- ROI: 39,018%
Additional Benefits:
- Error rate: 8% → 0.3% (pre-filled data reduces mistakes)
- User satisfaction increased (streamlined workflow)
- Implemented 5 more custom buttons (total 8 hours dev time)
- Total annual productivity gain: $47,820
Best Practices
- Use Descriptive Button Text
- Good: "Quick Order", "Export Report", "Send Reminder"
- Bad: "Action", "Click Here", "Button1"
- Add Loading State for Async Actions
- Disable button while processing
- Show loading spinner or change text to "Processing..."
- Re-enable after action completes
- Validate Before Action
- Check if records selected (for bulk actions)
- Validate user permissions
- Show warning notifications for invalid states
- Use Consistent Styling
- Primary action: btn-primary (blue)
- Secondary: btn-secondary (gray)
- Danger: btn-danger (red) for destructive actions
- Provide User Feedback
- Show success notification after action completes
- Display error message if action fails
- Use this.notification.add() for messages
Pro Tip: Before building custom button, ask: "Do users do this action 10+ times daily?" If yes, button ROI is massive. If no, standard menu navigation is fine. Map all repetitive user actions (watch over their shoulder for 1 hour). Found 8 actions repeated 10+ times daily? That's 8 button opportunities = $47K annual savings. 30 minutes implementation each = 4 hours total dev time. ROI insane.
Wasting $15K Annually on Extra Clicks?
We implement custom buttons in Odoo 18: Tree/form view customization, JavaScript controllers, action wizards, UX optimization. Reduce 8 clicks to 1, save $15K/year per workflow.
