Custom Field Component Odoo 18
By Braincuber Team
Published on December 29, 2025
Developer building student enrollment system needs month-year picker instead standard date picker for capturing graduation month, default Odoo date field showing full calendar day-month-year forcing users click through unnecessary day selection wasting time frustrating UX, custom business requirement demands displaying selected month formatted cleanly "June 2025" not "2025-06-15" creating display inconsistencies across reports forms, third-party widget libraries expensive $299 licensing fees adding unnecessary dependencies bloating module size, existing Char field lacking interactive picker requiring manual typing "2025-06" format causing data validation errors user input mistakes, and reusing same month picker across multiple modules Employee models Project models Purchase models requiring code duplication maintenance nightmares—creating development delays poor user experience validation errors licensing costs code redundancy and inability building reusable custom UI components matching exact business requirements without relying expensive third-party solutions requiring clean maintainable extensible field component architecture supporting native HTML5 month picker seamless Odoo OWL framework integration flexible reusability across models.
Odoo 18 custom field components enable specialized UI development through OWL framework integration creating reusable field widgets, JavaScript component creation extending Component class from @odoo/owl framework, XML template design defining visual rendering logic conditional read-only editable modes, useInputField hook integration synchronizing field value with Odoo record data, field registry registration making custom widget available form views, manifest asset configuration loading JavaScript XML files into web.assets_backend bundle, widget attribute application applying custom component specific model fields, standardFieldProps implementation supporting readonly placeholder common field properties, translation support using _t function from @web/core/l10n/translation module, and Python model field definition providing backend storage Char Integer field types—reducing development time 60 percent through reusable component architecture eliminating third-party widget dependencies via native HTML5 input types enabling consistent UX across modules through standardized field components supporting business-specific requirements through custom widget development and achieving maintainable scalable UI customization through OWL framework best practices component registry patterns clean separation concerns.
Custom Field Component Features:OWL integration, JavaScript component class, XML templates, useInputField hook, Field registry, Asset bundling, Widget attributes, Translation support, Reusable architecture, Native HTML5 inputs
Understanding Custom Field Components
Core concepts and architecture:
What Are Field Components:
Field components are reusable UI widgets extending Odoo base field functionality providing custom input controls specialized data display tailored business logic. Built using OWL framework registered in field registry applied via widget attribute.
Key Components:
- JavaScript class extending Component from @odoo/owl
- XML template defining visual structure
- Field registry entry making widget available
- Python model field storing data
- Manifest asset configuration loading files
Example Use Case Month Picker:
We will create InputMonth component allowing users select month-year using native HTML month picker. Perfect for graduation dates contract periods fiscal months project timelines.
Prerequisites
Before building custom field component:
- Odoo 18 instance with development environment configured
- Custom module created (e.g.,
student_management) - JavaScript ES6 knowledge OWL framework basics
- XML templating understanding
- Python 3.10 or higher
- Text editor or IDE (VS Code recommended)
Step 1 Creating JavaScript Component
Building the component logic:
File Structure:
Create file: student_management/static/src/js/input_month.js
JavaScript Code:
/** @odoo-module **/
import { registry } from "@web/core/registry";
import { _t } from "@web/core/l10n/translation";
import { useInputField } from "@web/views/fields/input_field_hook";
import { standardFieldProps } from "@web/views/fields/standard_field_props";
import { Component } from "@odoo/owl";
export class InputMonth extends Component {
static template = "student_management.InputMonth";
static props = {
...standardFieldProps,
placeholder: { type: String, optional: true },
};
setup() {
useInputField({
getValue: () => this.props.record.data[this.props.name] || ""
});
}
}
export const monthField = {
component: InputMonth,
displayName: _t("Month Picker"),
supportedTypes: ["char"],
extractProps: ({ attrs }) => ({
placeholder: attrs.placeholder,
}),
};
registry.category("fields").add("month_picker", monthField);Code Explanation:
Imports:
registry: Odoo registry system for registering components_t: Translation function supporting multilingual interfacesuseInputField: Hook connecting input to Odoo data modelstandardFieldProps: Standard field properties readonly name recordComponent: Base OWL component class
InputMonth Class:
static template: References XML template namestatic props: Defines component properties accepting placeholdersetup(): Lifecycle method calling useInputField hookgetValue: Function returning field value from record data
Field Registration:
monthField: Object containing component metadatasupportedTypes: Field types widget supports (char)extractProps: Extracts attributes from XML viewregistry.category("fields").add(): Registers widget as "month_picker"
Step 2 Designing XML Template
Creating visual representation:
File Structure:
Create file: student_management/static/src/xml/input_month.xml
XML Template Code:
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="student_management.InputMonth">
<div class="o_field_month d-inline-flex w-100">
<t t-if="props.readonly">
<span
t-if="props.record.data[props.name]"
class="o_field_char"
t-esc="props.record.data[props.name]"
/>
</t>
<t t-else="">
<input
class="o_input"
t-att-id="props.id"
type="month"
autocomplete="off"
t-att-placeholder="props.placeholder"
t-ref="input"
/>
</t>
</div>
</t>
</templates>Template Explanation:
Template Name:
t-name="student_management.InputMonth" matches JavaScript static template property.
Conditional Rendering:
t-if="props.readonly": Read-only mode displays value as textt-else="": Edit mode shows interactive month picker
Input Element:
type="month": HTML5 month picker inputt-ref="input": Reference connecting useInputField hookt-att-placeholder: Dynamic placeholder from propst-att-id: Unique field ID from props
Step 3 Manifest Configuration
Loading component assets:
Manifest File Update:
Edit file: student_management/__manifest__.py
{
'name': 'Student Management',
'version': '18.0.1.0.0',
'category': 'Education',
'summary': 'Student enrollment and management',
'depends': ['base', 'web'],
'data': [
'security/ir.model.access.csv',
'views/student_views.xml',
],
'assets': {
'web.assets_backend': [
'student_management/static/src/js/input_month.js',
'student_management/static/src/xml/input_month.xml',
],
},
'installable': True,
'application': True,
}Asset Bundle Explanation:
web.assets_backend: Backend asset bundle loading JavaScript XML- Files load automatically when module installed
- Order matters: JavaScript before XML ensures proper registration
- Paths relative to module root directory
Step 4 Python Model Field
Defining backend storage:
Model File:
Create/edit file: student_management/models/student.py
from odoo import fields, models, api
class Student(models.Model):
_name = 'student.enrollment'
_description = 'Student Enrollment'
name = fields.Char(string='Student Name', required=True)
enrollment_number = fields.Char(string='Enrollment Number')
graduation_month = fields.Char(
string='Expected Graduation',
help='Select month and year of expected graduation'
)
email = fields.Char(string='Email')
phone = fields.Char(string='Phone')
@api.depends('graduation_month')
def _compute_display_graduation(self):
"""Convert 2025-06 format to June 2025 for reports"""
for record in self:
if record.graduation_month:
# Format: 2025-06 -> June 2025
year, month = record.graduation_month.split('-')
months = ['','Jan','Feb','Mar','Apr','May','Jun',
'Jul','Aug','Sep','Oct','Nov','Dec']
record.display_graduation = f"{months[int(month)]} {year}"
else:
record.display_graduation = False
display_graduation = fields.Char(
string='Graduation',
compute='_compute_display_graduation',
store=True
)Field Explanation:
graduation_month = fields.Char(): Stores month in "YYYY-MM" format (e.g., "2025-06")- Char type suitable for month picker output
- Computed field
display_graduationconverts to readable format "June 2025" - Help text provides user guidance
Step 5 Applying Widget in View
Using custom component:
Form View XML:
Create file: student_management/views/student_views.xml
<odoo>
<record id="view_student_form" model="ir.ui.view">
<field name="name">student.enrollment.form</field>
<field name="model">student.enrollment</field>
<field name="arch" type="xml">
<form string="Student Enrollment">
<sheet>
<group>
<group>
<field name="name"/>
<field name="enrollment_number"/>
<field
name="graduation_month"
widget="month_picker"
placeholder="Select graduation month"
/>
</group>
<group>
<field name="email"/>
<field name="phone"/>
<field name="display_graduation" readonly="1"/>
</group>
</group>
</sheet>
</form>
</field>
</record>
<record id="view_student_tree" model="ir.ui.view">
<field name="name">student.enrollment.tree</field>
<field name="model">student.enrollment</field>
<field name="arch" type="xml">
<tree string="Students">
<field name="name"/>
<field name="enrollment_number"/>
<field name="display_graduation"/>
<field name="email"/>
</tree>
</field>
</record>
</odoo>Widget Application:
widget="month_picker": Applies custom InputMonth component to graduation_month field.
placeholder="Select graduation month": Placeholder text extracted by extractProps function.
Field displays as interactive month picker in edit mode, plain text in tree view read-only mode.
Step 6 Testing Component
Verification steps:
- Install Module:
- Navigate to Apps menu
- Update Apps List
- Search "Student Management"
- Click Install
- Verify Assets Loaded:
- Open browser Developer Tools (F12)
- Console tab check no JavaScript errors
- Network tab verify input_month.js input_month.xml loaded
- Test Form View:
- Navigate to Students menu
- Create new student record
- Click graduation_month field
- Verify month picker appears
- Select month (e.g., June 2025)
- Save record
- Test Read Mode:
- Open saved student record
- Switch to read-only mode
- Verify graduation_month displays "2025-06"
- Verify display_graduation shows "Jun 2025"
- Test Tree View:
- Navigate to Students list view
- Verify display_graduation column shows readable format
Advanced Enhancements
Enhancement 1 Custom Validation:
Add validation preventing past month selection:
// Add to input_month.js setup() method
setup() {
useInputField({
getValue: () => this.props.record.data[this.props.name] || "",
refName: "input",
});
this.props.record.model.bus.addEventListener(
"FIELD_IS_DIRTY",
this.onFieldChange.bind(this)
);
}
onFieldChange(ev) {
if (ev.detail.name === this.props.name) {
const value = this.props.record.data[this.props.name];
const today = new Date();
const currentMonth = today.getFullYear() + '-' + String(today.getMonth() + 1).padStart(2, '0');
if (value < currentMonth) {
this.props.record.setInvalidField(
this.props.name,
"Graduation month cannot be in the past"
);
}
}
}Enhancement 2 Date Range Picker:
Extend component supporting month ranges (start-end):
// input_month_range.xml
<t t-name="student_management.InputMonthRange">
<div class="d-flex gap-2">
<input
class="o_input"
type="month"
placeholder="Start Month"
t-ref="start_input"
/>
<span class="align-self-center">to</span>
<input
class="o_input"
type="month"
placeholder="End Month"
t-ref="end_input"
/>
</div>
</t>Enhancement 3 Localization:
Support international date formats:
from odoo import fields, models, api
from babel.dates import format_date
class Student(models.Model):
_name = 'student.enrollment'
@api.depends('graduation_month')
def _compute_display_graduation(self):
for record in self:
if record.graduation_month:
year, month = record.graduation_month.split('-')
# Use user's language for month name
date_obj = fields.Date.from_string(f"{year}-{month}-01")
formatted = format_date(
date_obj,
format='MMMM y',
locale=self.env.user.lang or 'en_US'
)
record.display_graduation = formatted
else:
record.display_graduation = FalseCommon Issues and Solutions
Issue 1: Component Not Appearing
Symptom: Field shows as regular text input.
Solutions:
- Check browser console for JavaScript errors
- Verify assets loaded in Network tab
- Confirm widget name matches registry:
widget="month_picker" - Restart Odoo server after manifest changes
- Clear browser cache (Ctrl+Shift+R)
Issue 2: Value Not Saving
Symptom: Selected month not persisting.
Solutions:
- Verify
t-ref="input"in XML template - Check useInputField hook correctly configured
- Confirm field name matches model field:
props.name - Check model field type supports char data
Issue 3: Translation Not Working
Symptom: Display name not translating.
Solutions:
- Update translations: Settings → Translations → Load Translation
- Export/Import translation terms for "Month Picker"
- Verify
_t()function used correctly
Best Practices
Use Descriptive Widget Names Matching Functionality: Generic widget name "custom_field" equals confusion which widget what purpose across modules. Descriptive name "month_picker" "color_selector" "rating_widget" immediately communicates widget purpose enabling quick identification appropriate widget selection reducing integration errors saving development time.
Implement Proper Error Handling and User Feedback: Widget crashing on invalid input null values equals poor UX user frustration. Add try-catch blocks validation logic user notifications. Example: Validate month format before saving, display user-friendly error "Please select valid graduation month" instead technical error maintaining professional UX building user confidence.
Follow OWL Framework Best Practices Component Lifecycle: Improper lifecycle management equals memory leaks performance issues. Use setup() method initialization, cleanup in willUnmount(). Leverage useState for reactivity, useRef for DOM access. Following OWL patterns ensures maintainable efficient components compatible future Odoo versions preventing technical debt.
Document Component Usage and Props in Code Comments: Undocumented custom widget equals maintenance nightmare new developers unable understanding usage. Add JSDoc comments describing component purpose, props, examples. Include README.md explaining installation configuration usage. Documentation accelerates team onboarding reduces support questions enables component reusability across projects.
Conclusion
Odoo 18 custom field components enable specialized UI development through OWL framework integration JavaScript component creation XML template design useInputField hook integration field registry registration manifest asset configuration widget attribute application standardFieldProps implementation translation support and Python model field definition. Reduce development time through reusable component architecture eliminating third-party widget dependencies via native HTML5 input types enabling consistent UX across modules through standardized field components supporting business-specific requirements through custom widget development achieving maintainable scalable UI customization through OWL framework best practices component registry patterns clean separation concerns improving developer productivity through modular architecture enhancing user experience through tailored interactive controls and future-proofing applications through standards-based implementation achieving operational efficiency user satisfaction and competitive advantage.
