Skip to Content

Odoo 19 中 Many2one 字段的定义与实战应用

      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 方法即可

Odoo 19 中 Many2one 字段的定义与实战应用
中国 Odoo, 苏州远鼎 April 27, 2026
Tags
Archive