How to Create a Custom Field Component in Odoo 18 Using OWL
By Braincuber Team
Published on February 26, 2026
Your client needs a month picker on a form view. Not a full date picker — just month and year. You Google "Odoo month picker widget" and find nothing that works out of the box. The built-in date widget insists on showing day-level precision. So you're stuck choosing between a hacky Selection field with 12 hardcoded options (that breaks every January) or building a proper OWL component from scratch. This guide walks you through option two — the one that actually scales.
What You'll Learn:
- Building an OWL 2 field component with useInputField hook and standardFieldProps
- Creating the XML template with conditional rendering for edit vs. read-only modes
- Registering assets in __manifest__.py under web.assets_backend
- Applying the custom widget in form views and defining the backing Python field
- Odoo 18-specific notes on translations, OWL compatibility, and asset bundles
Prerequisites
Before you touch any code, make sure these are in place. We've seen developers burn 3 hours debugging what turned out to be a missing Python version or a module that wasn't scaffolded correctly.
| Requirement | Details |
|---|---|
| Odoo 18 Instance | Running development environment with debug mode enabled |
| Python Version | Python 3.10 or higher (Odoo 18 requirement) |
| JavaScript Knowledge | Familiarity with ES6+ modules, OWL 2, and Odoo's registry system |
| Custom Module | A scaffolded module with static/src/js/ and static/src/xml/ directories |
File Structure Overview
Here's what your module directory should look like when you're done. Four files, three languages, one working component.
your_module_name/
__manifest__.py # Asset registration
models/your_model.py # Python field definition
views/your_view.xml # Form view with widget
static/src/js/
input_month.js # OWL component
static/src/xml/
input_month.xml # OWL template
Step 1: Build the JavaScript OWL Component
This is the core of the whole operation. The JavaScript file defines an OWL 2 component that registers itself as a field widget called month. It uses Odoo 18's useInputField hook to sync the HTML input with the ORM, and standardFieldProps to inherit all the standard field behavior (readonly, required, etc.).
/** @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 = "your_module_name.InputMonth";
static props = {
...standardFieldProps,
placeholder: { type: String, optional: true },
};
setup() {
useInputField({
getValue: () =>
this.props.record.data[this.props.name] || ""
});
}
}
export const month = {
component: InputMonth,
displayName: _t("Month"),
supportedTypes: ["char"],
extractProps: ({ attrs }) => ({
placeholder: attrs.placeholder,
}),
};
registry.category("fields").add("month", month);
What Each Import Does
| Import | Purpose |
|---|---|
registry | Odoo's central registry system — used to register the component under the fields category so form views can find it |
_t | Translation function — wraps the display name so it can be translated into other languages |
useInputField | OWL hook that syncs HTML input value with Odoo's data model. Handles onChange, validation, and dirty state tracking |
standardFieldProps | Predefined prop types every field widget needs: record, name, readonly, required, etc. |
Component | OWL 2 base class — every custom component extends this |
Template Name Must Match
The static template string in your JS file must exactly match the t-name attribute in your XML template. If you name your module custom_fields, the template should be custom_fields.InputMonth in both files. Mismatch = blank field with zero error messages in the console. We've seen developers lose 2 hours to this.
Step 2: Design the XML Template
The template handles rendering in two modes: edit mode (shows the native HTML month picker) and read-only mode (shows the stored value as plain text). The conditional rendering uses OWL's t-if / t-else directives.
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="your_module_name.InputMonth">
<div class="o_phone_content d-inline-flex w-100">
<t t-if="props.readonly">
<a t-if="props.record.data[props.name]"
class="o_form_uri" target="_blank"
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 Breakdown
Read-Only Mode
When props.readonly is true, the template renders a clickable <a> tag showing the stored value (e.g., "2025-06"). No input element — just text display.
Edit Mode
The t-else branch renders a native HTML <input type="month"> picker. The t-ref="input" attribute is critical — it connects to the useInputField hook for data syncing.
Step 3: Register Assets in __manifest__.py
Without this step, Odoo has no idea your JS and XML files exist. The web.assets_backend bundle loads assets for all backend views — form views, list views, kanban views. Miss this and your widget silently fails.
'assets': {
'web.assets_backend': [
'your_module_name/static/src/js/input_month.js',
'your_module_name/static/src/xml/input_month.xml',
],
},
Step 4: Apply the Widget in a Form View
One line in your view XML. The widget="month" attribute tells Odoo to use your registered component instead of the default Char widget.
<field name="month_field" widget="month"/>
Step 5: Define the Python Model Field
The backing field is a simple Char field. The HTML month picker stores values in YYYY-MM format (e.g., "2025-06"), which fits perfectly into a Char field. Don't use a Date field here — the format mismatch will cause silent data corruption.
from odoo import fields, models
class YourModel(models.Model):
_name = 'your.model'
_description = 'Your Model'
month_field = fields.Char(string='Month')
Don't Use fields.Date
The HTML month picker returns values like "2025-06". If you use fields.Date, Odoo expects "2025-06-01" format. The mismatch won't throw an error during development — it'll silently store False in the database. Use fields.Char and validate on the Python side if you need month-year integrity checks.
Step 6: Test the Component
Install or Upgrade Your Module
Run -u your_module_name or upgrade via Apps menu. Odoo needs to reload assets and register the new field widget.
Navigate to the Form View
Open any record that uses the month_field. The field should render as a native month picker dropdown in edit mode.
Verify Both Modes
Select a month in edit mode, save the record, then confirm the value displays correctly in read-only mode as a text link (e.g., "2025-06").
Check for Console Errors
Open browser DevTools > Console. Look for OWL warnings, missing template errors, or asset loading failures. Common issue: "Template not found" means your t-name doesn't match the static template string.
Odoo 18-Specific Notes
Translation API
Odoo 18 uses _t from @web/core/l10n/translation — same import path as Odoo 17. Backward-compatible for simple component translations.
OWL 2 Compatibility
OWL 2 is backward-compatible for simple components like this. For advanced reactivity, explore useState and t-on event handlers.
Asset Bundles
web.assets_backend is still the standard bundle for backend views. Check Odoo 18 release notes for any new bundle names if your component targets POS or website.
Python 3.10+ Required
Odoo 18 requires Python 3.10 or higher. If your dev environment runs 3.8 or 3.9, upgrade before you start — otherwise module installation fails with cryptic syntax errors.
Need Custom Odoo 18 Components Built?
Our team has built 130+ custom OWL components across Odoo 16, 17, and 18 — field widgets, dashboard panels, POS extensions, and website builders. We handle the JS/XML/Python wiring so your internal team doesn't spend 3 weeks on what should take 3 days.
