PAYATHON 2026

Odoo 18:如何按预算过滤 analytic_distribution 中的分析账户?

数据小许

结论

在 Odoo 18.0 的上游实现中,可以扩展 AnalyticDistribution.recordProps(line),将预算允许的账户 ID 加入内部 account.analytic.account 字段的 domain。该组件没有提供通过 XML options 直接设置内部账户 domain 的通用配置项,business_domain 也不用于此目的。参见 Odoo 18.0 组件源码。

以下示例适用于部署环境仍沿用该组件实现的情况。Enterprise 扩展或其他定制模块可能改变调用链,需要对照实际加载的代码确认。

为什么外层 context 没有达到预期效果

弹窗会创建独立的临时 Record,其内部账户字段由 recordProps(line) 动态生成。模板将这些字段各自的 domain 传给内部 Field,外层 analytic_distribution 的 context 因此不会自动成为内部账户选择器的自定义 context。参见弹窗模板。

账户候选搜索使用标准 Many2one 自动补全,调用 account.analytic.account.name_search;“搜索更多”也沿用该选择器的 domain。没有看到日志,不足以判断该 widget 绕过了 Python 搜索方法。相关调用见 Many2XAutocomplete 源码。

1. 将允许账户加载到采购订单行

下面假设采购订单上的 allowed_analytic_account_ids 已经是正确的 Many2many 字段,并且会随 overall_budget_id 更新。原问题没有提供预算模型上的账户字段名,示例因此复用已有字段。

在自定义模块中为订单行增加 related 字段。如果已有同名字段,直接复用并检查其计算逻辑:

from odoo import fields, models


class PurchaseOrderLine(models.Model):
    _inherit = "purchase.order.line"

    allowed_analytic_account_ids = fields.Many2many(
        related="order_id.allowed_analytic_account_ids",
        readonly=True,
    )

在现有采购订单表单继承视图中加载这个行字段:

<xpath expr="//field[@name='order_line']/list" position="inside">
    <field name="allowed_analytic_account_ids" column_invisible="True"/>
</xpath>

如果也允许通过订单行的独立表单编辑分配,需要在该表单中加载此字段。

2. 给内部账户字段追加 domain

将以下代码放入自定义模块的 static/src/js/analytic_distribution_budget.js:

/** @odoo-module **/

import { AnalyticDistribution } from "@analytic/components/analytic_distribution/analytic_distribution";
import { Domain } from "@web/core/domain";
import { patch } from "@web/core/utils/patch";

patch(AnalyticDistribution.prototype, {
    recordProps(line) {
        const result = super.recordProps(line);
        const record = this.props.record;

        if (record.resModel !== "purchase.order.line") {
            return result;
        }

        const allowedIds =
            record.data.allowed_analytic_account_ids?.currentIds || [];

        for (const field of Object.values(result.fields)) {
            if (field.relation === "account.analytic.account") {
                field.domain = Domain.and([
                    field.domain || [],
                    [["id", "in", allowedIds]],
                ]).toList();
            }
        }

        return result;
    },
});

这段代码保留原有过滤条件,并与允许账户范围取交集。Domain.and() 的行为见 domain 工具源码;Many2many 前端集合的 currentIds 表示当前关联 ID,见 StaticList 源码。

示例采用“允许列表为空时不显示任何账户”的规则。如果业务要求未选预算时不限制,应增加明确条件,区分“未选择预算”和“预算中没有允许账户”。

3. 加载资源并检查请求

在现有 __manifest__.py 的 assets 中加入资源,将 your_module 替换成实际模块名:

"assets": {
    "web.assets_backend": [
        "your_module/static/src/js/analytic_distribution_budget.js",
    ],
},

同时确认模型文件已通过 __init__.py 导入,模块依赖包含所扩展的采购、分析功能及预算字段所属模块。更新模块后刷新前端资源。

在浏览器 Network 面板中输入账户名称,检查 account.analytic.account 的 name_search 请求。其 kwargs.args 应包含预算账户的 id in [...] 条件,同时保留原有计划和公司条件。

如果依然没有进入自定义 name_search,依次检查:

  • Python 文件是否被导入,运行中的数据库是否已更新模块。
  • 日志是否来自实际处理请求的进程;排查时使用服务端 logger 比 print 更可靠。
  • 实际加载的组件是否被其他模块替换。
  • context 键名是否一致,尤其是 filter_budget_id 与 allowed_analytic_account_ids。

原示例展示的是 search(),不是 _search(),排查时应区分二者。

注意已有分配与保存校验

上述改动只过滤候选账户,不会自动清理已有的 analytic_distribution。切换预算后,应检查允许账户字段是否及时刷新,并明确如何处理已经选中、但不再属于新预算的账户。

如果“只能使用预算账户”是必须执行的业务规则,还需要在服务端保存或业务确认环节增加校验,覆盖订单行分配变化和订单预算变化两种情况。解析分配数据时要注意:一个 JSON 键可能包含多个以逗号分隔的账户 ID。

以上代码基于上游源码,尚未在你的 Enterprise 数据库及自定义预算模型中运行验证。

备注:内容仅供参考。