Odoo 的 Web 框架提供了强大且灵活的方式来自定义字段组件,允许开发者在默认行为基础上增强用户体验。Many2One 是 Odoo 中最常用的字段类型之一,用于表示模型之间的关联关系,例如采购订单上的供应商字段。
默认情况下,Many2One 字段提供带搜索功能的下拉选择框。但如果您希望在不离开当前表单的前提下显示所选记录的附加信息,该如何实现?本文将一步步搭建一个适用于 Odoo 19 的自定义 Many2One 组件,点击信息按钮时弹出合作伙伴详情(姓名、邮箱、电话)。
Odoo 19 中 Many2One 组件的新变化
Odoo 19 对 Many2One 组件引入了两项重要的破坏性更新:
- 模板拆分:渲染逻辑被拆分为两个组件 —— web.Many2OneField(外层容器)和 web.Many2One(实际 DOM 结构)。模板定制请始终继承 web.Many2One。
- props.value 变为对象:Odoo 18 中 props.value 是数组 [id, display_name],通过 this.props.value?.[0] 获取 ID;Odoo 19 中改为普通对象 { id, display_name },应使用 this.props.value?.id。
模块结构
many2one_info_widget/
├── __manifest__.py
├── __init__.py
├── models/
│ ├── __init__.py
│ └── res_partner.py
├── views/
│ └── purchase_order_view.xml
└── static/
└── src/
├── js/
│ └── many2one_info_widget.js
└── xml/
└── many2one_info_widget.xml
创建模块文件
manifest.py
# -*- coding: utf-8 -*-
{
'name': 'Many2One Info Widget',
'version': '19.0.1.0.0',
'category': 'Technical',
'summary': 'Custom Many2One widget that displays partner info popover on click',
'depends': ['base', 'purchase', 'web'],
'data': [
'views/purchase_order_view.xml',
],
'assets': {
'web.assets_backend': [
'many2one_info_widget/static/src/xml/many2one_info_widget.xml',
'many2one_info_widget/static/src/js/many2one_info_widget.js',
],
},
'license': 'LGPL-3',
'installable': True,
}
init.py
from . import models
models/init.py
from . import res_partner
添加 Python 方法
扩展 res.partner 提供一个 RPC 接口,供组件获取合作伙伴信息。
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, partner_id):
partner = self.browse(int(partner_id))
if not partner.exists():
return []
return [{
'name': partner.name or '',
'email': partner.email or '',
'phone': partner.phone or partner.mobile or '',
'street': partner.street or '',
'city': partner.city or '',
'country': partner.country_id.name if partner.country_id else '',
}]
JavaScript 组件实现
核心逻辑部分,需重点理解 Odoo 19 的两点设计:
为什么继承 Many2One 而不是 Many2OneField?
- Many2OneField:外层字段包装,仅负责渲染
- Many2One:包含输入框、按钮、附加行的真实 DOM 结构
因此实际 DOM 与逻辑都应继承 Many2One,再替换到 Many2OneField 中。
static/src/js/many2one_info_widget.js
/** @odoo-module **/
import { registry } from '@web/core/registry';
import { Many2OneField } from '@web/views/fields/many2one/many2one_field';
import { Many2One } from '@web/views/fields/many2one/many2one';
import { useService } from '@web/core/utils/hooks';
import { useState, useRef } from "@odoo/owl";
export class Many2OneInfoWidget extends Many2One {
static template = "many2one_info_widget.Many2OneInfoWidget";
setup() {
super.setup();
this.orm = useService("orm");
this.detailPop = useRef("detail_pop");
this.infoState = useState({ data: [] });
}
async showPopup(ev) {
ev.stopPropagation();
const popEl = this.detailPop.el;
if (!popEl) return;
if (popEl.classList.contains("d-none")) {
const partnerId = this.props.value?.id;
if (!partnerId) return;
try {
const result = await this.orm.call(
'res.partner',
'get_partner_info',
[partnerId]
);
this.infoState.data = result || [];
} catch (err) {
console.error("m2o_info: failed to fetch partner info", err);
this.infoState.data = [];
}
popEl.classList.remove("d-none");
} else {
popEl.classList.add("d-none");
this.infoState.data = [];
}
}
}
export class Many2OneInfoField extends Many2OneField {
static components = {
...Many2OneField.components,
Many2One: Many2OneInfoWidget,
};
}
const baseDescriptor = registry.category("fields").get("many2one", {});
registry.category("fields").add("m2o_info", {
...baseDescriptor,
component: Many2OneInfoField,
fieldDependencies: baseDescriptor.fieldDependencies || [],
});
QWeb 模板
两个关键设计点:
- 按钮需同时放在编辑模式和只读模式中,否则切换状态后按钮消失
- 弹出框必须挂载到 .o_many2one,因为 .o_field_many2one_extra 不一定存在
static/src/xml/many2one_info_widget.xml
<?xml version="1.0" encoding="UTF-8" ?>
<templates xml:space="preserve">
<t t-name="many2one_info_widget.Many2OneInfoWidget"
t-inherit="web.Many2One"
t-inherit-mode="primary">
<!-- 编辑模式按钮 -->
<xpath expr="//Many2XAutocomplete" position="after">
<button class="m2o-info-icon btn btn-sm btn-outline-primary ms-1"
t-on-click.stop="showPopup"
type="button">
i
</button>
</xpath>
<!-- 只读模式按钮 -->
<xpath expr="//t[@t-if='props.readonly']" position="inside">
<button t-if="props.value"
class="m2o-info-icon btn btn-sm btn-outline-primary ms-1"
t-on-click.stop="showPopup"
type="button">
i
</button>
</xpath>
<!-- 弹出层 -->
<xpath expr="//div[hasclass('o_many2one')]" position="inside">
<div class="popover d-none"
style="max-width: none"
t-ref="detail_pop">
<t t-if="infoState.data.length">
<t t-foreach="infoState.data" t-as="item" t-key="item_index">
<b>Name</b>: <t t-esc="item.name"/><br/>
<b>Email</b>: <t t-esc="item.email"/><br/>
<b>Phone</b>: <t t-esc="item.phone"/><br/>
</t>
</t>
</div>
</xpath>
</t>
</templates>
视图中应用组件
将组件应用到采购订单的 partner_id 字段。
views/purchase_order_view.xml
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<record id="purchase_order_form_m2o_info" model="ir.ui.view">
<field name="name">purchase.order.form.m2o.info</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>
效果说明
安装模块后,打开任意采购订单,供应商字段旁会显示一个 i 按钮,编辑与只读模式均可用。点击后弹出层显示合作伙伴详细信息,无需跳转页面。
总结
- Odoo 19 Many2One 采用双组件结构:Many2OneField + Many2One
- 模板与逻辑继承 Many2One,再替换进 Many2OneField
- props.value 从数组变为对象,使用 props.value?.id 获取 ID
- 该模式可用于任意模型的任意 Many2One 字段,只需修改对应视图与 RPC 方法即可