12 Errors Monthly Because Checkbox Too Small? Create Custom Field Widgets in Odoo 18
By Braincuber Team
Published on December 20, 2025
Your operations team needs a simple yes/no field: "Order Approved?" Standard Odoo checkbox is tiny. Users miss it. Last month, 12 unapproved orders shipped because warehouse staff didn't notice the unchecked box. Cost: $4,200 in returns and re-shipping.
Your manager asks: "Can we make it more obvious? Like a big green checkmark when approved, red X when not?" You tell your developer. They say: "That's a custom widget. 8 hours of work. $680."
But really, custom field widgets in Odoo 18 take 45 minutes to build once you know the pattern. Here's how to create one without hiring consultants or reading 200-page OWL documentation.
You Need Custom Widgets If:
What We're Building
A custom boolean widget called bool_badge. Instead of a checkbox, it displays:
When True (Approved)
Click to toggle to False
When False (Not Approved)
Click to toggle to True
Prerequisites
- Odoo 18 instance with developer mode enabled
- Custom module (or create new one)
- Basic JavaScript knowledge (OWL/React patterns)
- Access to static folder in your module
Module Structure
Create this folder structure in your module:
my_custom_module/
├── __init__.py
├── __manifest__.py
├── models/
│ └── sale_order.py (example model)
├── views/
│ └── sale_order_views.xml
└── static/
└── src/
├── js/
│ └── bool_badge.js
└── xml/
└── bool_badge.xml
Step 1: Create JavaScript Component
Create static/src/js/bool_badge.js:
/** @odoo-module */
import { registry } from "@web/core/registry";
import { standardFieldProps } from "@web/views/fields/standard_field_props";
import { Component } from "@odoo/owl";
export class BoolBadge extends Component {
/**
* Update field value when badge is clicked
*/
updateValue(newValue) {
this.props.record.update({
[this.props.name]: newValue
});
}
/**
* Get current field value
*/
get value() {
return this.props.record.data[this.props.name];
}
}
// Link to XML template
BoolBadge.template = "BoolBadge";
// Define accepted props
BoolBadge.props = {
...standardFieldProps,
options: { type: Object, optional: true }
};
// Register widget in Odoo's field registry
export const boolBadge = {
component: BoolBadge,
supportedTypes: ["boolean"],
extractProps: ({ attrs }) => {
return {
options: attrs.options
};
}
};
registry.category("fields").add("bool_badge", boolBadge);
Code Breakdown
| Part | What It Does |
|---|---|
updateValue() |
Updates the field when user clicks badge |
get value() |
Returns current field value (true/false) |
BoolBadge.template |
Links to XML template name |
supportedTypes |
Tells Odoo this widget works on boolean fields |
registry.add() |
Registers widget so Odoo can find it |
Step 2: Create XML Template
Create static/src/xml/bool_badge.xml:
<?xml version="1.0" encoding="UTF-8"?>
<templates>
<t t-name="BoolBadge" owl="1">
<span
class="badge rounded-circle p-2 border"
t-att-class="value ? 'bg-success text-white' : 'bg-danger text-white'"
t-on-click="() => this.updateValue(!value)"
style="cursor: pointer; width: 40px; height: 40px; display: inline-flex; align-items: center; justify-content: center;"
>
<i t-att-class="value ? 'fa fa-check' : 'fa fa-times'"/>
</span>
</t>
</templates>
Template Breakdown
| Attribute | Purpose |
|---|---|
t-name="BoolBadge" |
Template name (must match JS template property) |
t-att-class |
Dynamic class: green if true, red if false |
t-on-click |
Toggle value when clicked |
fa fa-check / fa-times |
FontAwesome icons (✓ or ✗) |
Step 3: Register Assets in Manifest
Update __manifest__.py:
{
'name': 'Custom Field Widgets',
'version': '18.0.1.0.0',
'category': 'Custom',
'depends': ['web', 'sale'],
'data': [
'views/sale_order_views.xml',
],
'assets': {
'web.assets_backend': [
'my_custom_module/static/src/js/bool_badge.js',
'my_custom_module/static/src/xml/bool_badge.xml',
],
},
'installable': True,
'application': False,
}
Critical: Path must match your module name exactly. If module is custom_widgets, path is custom_widgets/static/src/...
Step 4: Add Field to Model
Example: Add "Order Approved" field to Sales Order.
Create models/sale_order.py:
from odoo import models, fields
class SaleOrder(models.Model):
_inherit = 'sale.order'
is_approved = fields.Boolean(
string='Order Approved',
default=False,
help='Manager approval before processing'
)
Step 5: Use Widget in View
Create views/sale_order_views.xml:
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<record id="view_order_form_inherit" model="ir.ui.view">
<field name="name">sale.order.form.inherit</field>
<field name="model">sale.order</field>
<field name="inherit_id" ref="sale.view_order_form"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='partner_id']" position="after">
<field name="is_approved" widget="bool_badge"/>
</xpath>
</field>
</record>
</odoo>
The widget="bool_badge" attribute tells Odoo to use your custom widget instead of standard checkbox.
Step 6: Install & Test
- Restart Odoo server (to detect new module files)
- Update Apps List (Apps → Update Apps List)
- Install/Upgrade module
- Clear browser cache (Ctrl+Shift+R)
- Open Sales Order
- Look for "Order Approved" field
- Click badge → Should toggle green ↔ red
Advanced: Add Custom Options
Make widget configurable with options (e.g., custom colors).
Updated JavaScript
export class BoolBadge extends Component {
get trueColor() {
return this.props.options?.trueColor || 'bg-success';
}
get falseColor() {
return this.props.options?.falseColor || 'bg-danger';
}
updateValue(newValue) {
this.props.record.update({
[this.props.name]: newValue
});
}
get value() {
return this.props.record.data[this.props.name];
}
}
Updated Template
<span
class="badge rounded-circle p-2 border"
t-att-class="value ? trueColor + ' text-white' : falseColor + ' text-white'"
t-on-click="() => this.updateValue(!value)"
>
<i t-att-class="value ? 'fa fa-check' : 'fa fa-times'"/>
</span>
Use in View with Options
<field name="is_approved"
widget="bool_badge"
options="{'trueColor': 'bg-primary', 'falseColor': 'bg-warning'}"/>
Common Mistakes That Break Widgets
1. Template Name Mismatch
JS says BoolBadge.template = "BoolBadge" but XML has t-name="bool_badge" (lowercase). Widget doesn't render.
Fix: Names must match exactly (case-sensitive).
2. Wrong Asset Bundle
Added files to web.assets_frontend instead of web.assets_backend. Widget works on website but not in Odoo backend.
Fix: Use web.assets_backend for Odoo forms.
3. Forgot to Clear Browser Cache
Code updated but old widget still shows. Browser cached old JS/XML.
Fix: Hard refresh (Ctrl+Shift+R) or open incognito window.
4. Registry Name Typo
Registered as registry.add("bool_badge") but view uses widget="boolbadge". Odoo can't find widget.
Fix: Match registry name exactly in view XML.
Real-World Use Cases
1. Priority Badges (Low/Medium/High)
Replace selection field with color-coded badges: 🟢 Low, 🟡 Medium, 🔴 High
Benefit: Visual priority at a glance without reading text.
2. Star Rating Widget
Click stars to rate: ⭐⭐⭐⭐⭐ for customer satisfaction scores
Benefit: More intuitive than dropdown or number field.
3. Progress Bar Widget
Show order completion as progress bar: [████████░░] 80%
Benefit: Visual status without reading percentage.
Debugging Tips
Widget Not Showing?
- Check browser console for JS errors (F12)
- Verify asset paths in manifest match actual file locations
- Confirm
registry.add()name matcheswidget=""in XML - Check if field type is supported (e.g., widget for boolean on char field won't work)
- Try different field to isolate if issue is widget or field-specific
Widget Shows But Doesn't Update?
- Check if
updateValue()is callingthis.props.record.update() - Verify field name in update matches actual field name
- Ensure field is not readonly in view
ROI: Custom Widgets vs User Errors
Scenario: Approval Checkbox Missed
Before Custom Widget:
- Approval checkbox: tiny, easy to miss
- Errors: 12 unapproved orders shipped/month
- Returns cost: $4,200/month
- Support time: 8 hours/month handling complaints
- Total cost: $4,424/month
After Badge Widget:
- Big red/green badge: impossible to miss
- Errors: 1 mistake in 3 months
- Returns cost: $350 (one-time)
- Development: 45 min @ $85/hour = $64 (one-time)
- Savings: $4,360/month = $52,320/year
ROI: 81,650% in first year
$64 investment → $52,320 annual savings
Quick Implementation Checklist
- Create JavaScript component (45 lines of code)
- Create XML template (10 lines of code)
- Register in manifest (2 lines in assets)
- Add field to model (if needed)
- Use widget in view (
widget="your_widget") - Restart Odoo and upgrade module
- Clear browser cache and test
Pro Tip: Start simple. Get basic widget working first. Then add options, animations, custom colors. Don't try to build everything at once or you'll spend 3 hours debugging.
Team Missing Critical Fields? Losing Money on User Errors?
We build custom Odoo widgets that match your workflow. Visual indicators, intuitive UX, error-proof interfaces. Stop losing $4K/month because standard widgets are too small to notice.
