Create Custom Many2One Widget in Odoo 18
By Braincuber Team
Published on December 26, 2025
Standard Many2One fields in Odoo show just a dropdown selector. Users need to open the full record form to see details like email addresses, phone numbers, or other critical information—wasting clicks and time. A custom Many2One widget that displays key details on hover transforms the user experience by putting essential information at your fingertips without navigation overhead.
This tutorial demonstrates how to build an interactive Many2One widget in Odoo 18 using the OWL framework. You'll create an info button that reveals partner details in a popup—perfect for purchase orders, sales quotations, or any form where quick access to related record data improves productivity.
What You'll Build:
- Custom JavaScript widget extending Many2OneField
- Python backend method to fetch partner data
- QWeb template with interactive popup
- Info button displaying name, email, and phone
- Integration with Purchase Order form
Understanding Many2One Fields
Many2One fields represent relationships between models (e.g., a Purchase Order linked to a Vendor). Odoo's default implementation provides:
- Dropdown selection with search
- Quick create option
- External link to full record form
By extending this base widget, we can inject custom UI elements—like info buttons, badges, or inline previews—that make forms more informative and efficient.
Architecture Overview
Component Structure:
- JavaScript Widget: Extends
Many2OneField, adds info button and popup logic - Python Method: Backend RPC endpoint that fetches partner data
- QWeb Template: Inherits base Many2One template, injects info button and popup HTML
- View Integration: XML view inheritance to apply widget to specific fields
Step 1: Create JavaScript Component
First, create the custom widget by extending Odoo's standard Many2OneField component.
File: custom_widget/static/src/js/many2one_widget.js
/** @odoo-module **/
import { registry } from '@web/core/registry';
import { Many2OneField, many2OneField } from '@web/views/fields/many2one/many2one_field';
import { usePopover } from "@web/core/popover/popover_hook";
import { useState, useRef } from "@odoo/owl";
export class Many2OneFieldWidget extends Many2OneField {
static template = "custom_widget.Many2OneFieldWidget";
static props = {
...Many2OneField.props,
}
setup() {
super.setup();
this.popover = usePopover(this.constructor.components.Popover, {
position: "top"
});
this.DetailPop = useRef("detail_pop");
this.state = useState({
...this.state,
data: []
})
}
async showPopup(ev) {
if (this.DetailPop.el.classList.contains("d-none")) {
// Fetch partner data from backend
this.state.data = await this.orm.call(
'res.partner',
'get_partner_info',
[this.props.record.data.partner_id[0]]
)
this.DetailPop.el.classList.remove("d-none");
} else {
this.DetailPop.el.classList.add("d-none");
}
}
}
export const many2OneFieldWidget = {
...many2OneField,
component: Many2OneFieldWidget,
fieldDependencies: [
...(many2OneField.fieldDependencies || []),
],
extractProps({ options }) {
const props = many2OneField.extractProps(...arguments);
return props;
},
};
registry.category("fields").add("m2o_info", many2OneFieldWidget);
Code Explanation
Class Extension
Extends Many2OneField to inherit all standard functionality
setup() Method
Initializes component state, creates DOM ref for popup, and sets up popover hook
showPopup() Method
Toggles popup visibility, makes RPC call to fetch partner data when opened
Registry Registration
Registers widget as m2o_info for use in XML views
Step 2: Create Python Backend Method
Add a method to res.partner model that returns partner information.
File: custom_widget/models/res_partner.py
# -*- coding: utf-8 -*-
from odoo import api, models
class ResPartner(models.Model):
_inherit = 'res.partner'
@api.model
def get_partner_info(self, vals):
"""
Fetch partner information for widget display
Args:
vals: Partner ID (as integer or list)
Returns:
List of dict containing partner name, email, phone
"""
partner_id = self.browse(int(vals))
data = [{
'name': partner_id.name or 'N/A',
'email': partner_id.email or 'No email',
'phone': partner_id.phone or 'No phone'
}]
return data
Security Note: This method uses @api.model which respects record rules. Only partners the user has access to will be returned.
Step 3: Create QWeb Template
Define the template that injects the info button and popup into the Many2One field.
File: custom_widget/static/src/xml/many2one_widget.xml
<?xml version="1.0" encoding="UTF-8" ?>
<templates>
<t t-name="custom_widget.Many2OneFieldWidget"
t-inherit="web.Many2OneField"
t-inherit-mode="primary">
<!-- Add info button before external button -->
<xpath expr="//t[@t-if='hasExternalButton']" position="before">
<button class="m2o-info-icon btn btn-sm btn-outline-primary ml4"
t-on-click="showPopup">
<i class="fa fa-info-circle"/>
</button>
</xpath>
<!-- Add popup container -->
<xpath expr="//div[hasclass('o_field_many2one_extra')]" position="before">
<div class="popover d-none"
style="max-width: none; position: absolute; z-index: 1000;"
t-ref="detail_pop">
<div class="popover-body">
<t t-foreach="state.data" t-as="item" t-key="item_index">
<div class="mb-2">
<strong>Name:</strong> <t t-esc="item.name"/>
</div>
<div class="mb-2">
<strong>Email:</strong> <t t-esc="item.email"/>
</div>
<div>
<strong>Phone:</strong> <t t-esc="item.phone"/>
</div>
</t>
</div>
</div>
</xpath>
</t>
</templates>
Template Explanation
- Inheritance: Uses
t-inheritto extend base Many2One template - Info Button: Injected before external link button using XPath
- Popup Div: Hidden by default (
d-none), referenced byt-ref - Dynamic Content: Loops through
state.datato display fetched partner info
Step 4: Apply Widget to Form View
Inherit the target view and apply the custom widget to specific Many2One fields.
File: custom_widget/views/purchase_order_view.xml
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<record id="purchase_order_form_custom" model="ir.ui.view">
<field name="name">purchase.order.form.custom</field>
<field name="model">purchase.order</field>
<field name="inherit_id" ref="purchase.purchase_order_form"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='partner_id']" position="attributes">
<attribute name="widget">m2o_info</attribute>
</xpath>
</field>
</record>
</odoo>
Step 5: Register Assets
Add JavaScript and XML files to your module's asset bundle.
File: custom_widget/__manifest__.py
{
'name': 'Custom Many2One Widget',
'version': '18.0.1.0.0',
'category': 'Tools',
'depends': ['web', 'purchase'],
'data': [
'views/purchase_order_view.xml',
],
'assets': {
'web.assets_backend': [
'custom_widget/static/src/js/many2one_widget.js',
'custom_widget/static/src/xml/many2one_widget.xml',
],
},
'installable': True,
'application': False,
}
Testing Your Widget
- Install Module: Activate developer mode, go to Apps, install your custom module
- Open Purchase Order: Navigate to Purchase → Orders → Create
- Select Vendor: Choose a partner in the Vendor field
- Click Info Button: Click the info icon (ℹ️) next to the vendor field
- View Popup: Partner details should appear in popup
- Toggle: Click info button again to hide popup
Customization Ideas
Additional Fields
Add address, tax ID, payment terms, or custom fields to popup
Styling
Customize popup background, add icons, use Bootstrap classes
Other Models
Apply to product Many2One to show stock levels, pricing
Hover Behavior
Replace click with hover event for instant info display
Frequently Asked Questions
orm.call to target different models and update the Python method accordingly. For example, use it on product Many2One fields to show stock levels or on project Many2One fields to show task counts.
'street': partner_id.street to the Python method and add corresponding HTML in the template.
t-on-click="showPopup" with t-on-mouseenter="showPopup" and add t-on-mouseleave to hide it when the mouse leaves.
Conclusion
Building custom Many2One widgets in Odoo 18 demonstrates the power and flexibility of the OWL framework. By combining JavaScript components, Python backend methods, and QWeb templates, you can transform standard field interactions into rich, informative experiences that save users time and improve data visibility.
This info-button pattern works brilliantly for any Many2One field where quick access to related record details improves workflow efficiency—vendor selections in purchase orders, customer fields in sales, products in inventory moves, or projects in timesheets.
Need Help Building Custom Odoo Widgets?
Our Odoo development team can help you create custom widgets, develop advanced OWL components, integrate complex business logic, and optimize your Odoo user interface for maximum productivity.
