Odoo16 中 X2Many 字段指定动态视图

大家应该都知道在 Odoo 的 context 中有若干用以指定视图的属性,如 form_view_reftree_view_ref 等,可以给字段指定其展示所使用的对应类型视图的。这个功能在 Odoo 16 之前的版本中,是可以指定一个动态的视图的,如根据单据类型的不同,在打开关系型字段的弹窗表单也可以随着单据类型变化。
<notebook>
  <page name="items" string="订单明细">
    <field name="item_ids" context="{'form_view_ref': 'foo.' + order_type + '_order_item_view_form'}"/>
  </page>
</notebook>
这个功能在 Odoo 16 中变得不太好使了,具体表现为只会使用当前记录首次解析出来的 form_view_ref 值,以上面的代码为例,若打开页面时,order_type 的值为 bar,则无论如何改变 order_type 的值,打开或创建 item_ids 对应的记录时打开的页面会一直是 foo.bar_order_item_view_form 对应的视图。

原因

通过 X2Many 字段组件的源码(/web/static/src/views/fields/x2many/x2many_field.js)可以发现创建和编辑的操作调用了 _openRecord(params) 这个方法,但是并没有发现任何跟视图相关的逻辑,继续往上一层找,可以看到调用了 useOpenX2ManyRecord() 这个方法,其中可以看到和视图有关的一段逻辑:
/**
 * /web/static/src/views/fields/relational_utils.js
 */
const form = await getFormViewInfo({ list, activeField, viewService, userService, env });
跳转到这个方法里面继续查看,在调用 loadViews() 加载视图的方法里将 context 作为参数传递了进去,到这里直觉告诉我离想要的答案不远了。
const { fields, relatedModels, views } = await viewService.loadViews({
    context: list.context,
    resModel: comodel,
    views: [[false, "form"]],
});
接着打开 loadViews() 所在的源文件(/web/static/src/views/view_service.js)看看里面的实现,会发现下面这一段,把各种参数序列化成字符串后作为缓存的 Key 使用了,如果参数值没有变化,就不会通过 RPC 去请求获取视图的方法 get_views 了。
const { context, resModel, views } = params;
const filteredContext = Object.fromEntries(
    Object.entries(context || {}).filter((k, v) => !String(k).startsWith("default_"))
);
const key = JSON.stringify([resModel, views, filteredContext, loadViewsOptions]);
if (!cache[key]) {
	...
}
在最开始我们提到过,context 中的 form_view_ref 值只会使用首次解析(实际上这里是 context 本身没有被重新解析)出来的值,因为 context 没有变化,其他参数也没有变化,通过上面的这段逻辑就可以知道,其实打开的视图都是缓存下来的。
很明显,问题就出在 context 没有重新解析这一点上,或者说,使用的 context 值是首次打开页面时解析的值,之后就不更新了。因此也很容易就可以定位到导致该问题的具体代码,就是 loadViews() 时传入的 list.context 有问题。

解决方案

再往回翻看一下 useOpenX2ManyRecord() 里的逻辑,可以看到 async function openRecord({ record, mode, context, title, onClose }) 这个方法的定义,是有传入 context 作为参数的,而在调用 getFormViewInfo() 时,却没有传递进去,而是直接通过 list 来获取 context 的值。
而在 Odoo 10 中,加载视图前会 var context = dataset.get_context(); 获取最新的 context 值,在 Odoo 14 中,则会使用通过参数传递进去的 context 值,这些 context 都是 load_views 前最新解析出来的值。所以在 Odoo 16 中,要修复这个问题,只需要让 getFormViewInfo() 这个方法可以用上最新的 context 值即可。
先来看看最终的实现:
/** @odoo-module */

import { patch } from "@web/core/utils/patch";
import { X2ManyField } from "@web/views/fields/x2many/x2many_field";

patch(X2ManyField.prototype, 'x2many_field_with_dynamic_form_view_ref', {
    /**
     * x2m 字段打开前更新 list.context 中的视图引用值
     * 使动态视图上下文可以生效
     * 
     * @override
     */
    setup() {
        this._super(...arguments);
				// 保留原本的逻辑
        const __openRecord = this._openRecord;
        this._openRecord = (params) => {
						// 仅当 context 中出现 form_view_ref 才更新
            if (params.context && params.context.form_view_ref) {
                this.list.context.form_view_ref = params.context.form_view_ref;
            }
						// 执行原本的逻辑
            __openRecord(params);
        };
    }
});
非常简单的一个 patch 就可以解决指定动态视图的问题,只需要在 X2ManyField 组件的 setup() 初始化方法里将 _openRecord 的逻辑做一点变更,在它执行前把 list.context 更新成最新的值。这里只处理了 form_view_ref 这个属性,是因为想要保证在 patch 时只做最小化的修改,尽量不影响原本的行为。

©️
 本文采用 CC BY-NC-ND 4.0 许可协议。转载或引用时请遵守协议内容!

© Ruter Lü 2016 - 2023