Skip to content

11. 计算字段

模型之间的关系是 Odoo 中各模块的关键组成部分。它们对于任何业务场景的建模都必不可少。然而,我们可能希望在某个模型内的字段之间建立关联。有时,一个字段的值是由其他字段的值决定的;有时,希望在数据录入过程中为用户提供帮助。

“计算字段”和“onchanges”这两个概念支持上述场景。虽然本章在技术上并不复杂,但这两个概念的语义非常重要。这也是我们首次编写 Python 逻辑代码。此前,我们编写的内容仅限于类定义和字段声明。

1、计算字段

在房产模型中,应计算总面积和最佳(最高)报价:

在房产报价模型中,有效期应进行计算,并且可以更新:

estate模块中,已经定义了居住面积和花园面积。因此,将总面积定义为这两个字段的和是顺理成章的。我们将为此使用“计算字段”的概念,即某个字段的值将根据其他字段的值进行计算。

到目前为止,字段都是直接存储在数据库中并直接从数据库中检索的。字段也可以通过计算生成。在这种情况下,字段的值并非从数据库中检索,而是通过调用模型的方法在运行时动态计算得出。

要创建计算字段,需先创建一个字段,并将它的 compute 属性设置为某个方法的名称。该计算方法应为 self 中的每条记录设置计算字段的值。

按惯例,compute 方法是私有的,这意味着它们不能从展示层调用,只能从业务层调用。私有方法的名称以下划线 _ 开头。

1.1、依赖关系

计算字段的值通常取决于计算记录中其他字段的值。ORM 期望开发者通过在计算方法上使用 depends() 装饰器来指定这些依赖关系。当某些依赖关系发生更改时,ORM 会利用这些指定的依赖关系触发该字段的重新计算:

python
from odoo import api, fields, models

class TestComputed(models.Model):
    _name = "test.computed"

    total = fields.Float(compute="_compute_total")
    amount = fields.Float()

    @api.depends("amount")
    def _compute_total(self):
        for record in self:
            record.total = 2.0 * record.amount

self 是一个集合。

对象 self 是一个记录集,即有序的记录集合。它支持 Python 集合的标准操作,例如 len(self)iter(self),以及额外的集合运算,如 recs1 | recs2

遍历 self 会依次返回记录,其中每个记录本身也是一个大小为 1 的集合。可以使用点表示法访问或赋值单个记录的字段,例如 record.name

在 Odoo 中可以找到许多计算字段的示例。以下是一个简单的示例。

实践开发:计算总面积

estate.property 表中添加 total_area 字段。该字段定义为 living_areagarden_area 的和。在表单视图中添加该字段。

对于关系字段,可以将字段中的路径用作依赖关系:

python
description = fields.Char(compute="_compute_description")
partner_id = fields.Many2one("res.partner")

@api.depends("partner_id.name")
def _compute_description(self):
    for record in self:
        record.description = "Test for partner %s" % record.partner_id.name

虽然示例中使用的是“多对一”关系,但这同样适用于“多对多”或“一对多”关系。

计算最佳报价。

estate.property 中添加 best_price 字段。该字段定义为所有报价中的最高(即最优的售价)价格。

python
@api.depends('offer_ids.price')
def _compute_best_price(self):
    for record in self:
        # 提取所有报价的价格列表
        prices = record.offer_ids.mapped('price')

        # 如果存在报价,取最大值;否则设为 0.0
        if prices:
            record.best_price = max(prices)
        else:
            record.best_price = 0.0

依赖装饰器‌:在实际 Odoo 开发中,这段逻辑通常放在一个由 @api.depends('offer_ids.price') 装饰的计算字段方法中,以确保当报价发生变化时,该字段会自动重新计算。

1.2、反向函数

可能已经注意到,计算字段默认是只读的。这是意料之中的,因为用户本就不应设置它们的值。在某些情况下,能够直接设置值可能会很有用。在房地产(Estate)示例中,可以为报价定义有效期,并设置有效日期。希望能够设置有效期或日期,且两者相互影响。

为支持这一需求,Odoo 提供了使用反向函数的功能:

python
from odoo import api, fields, models

class TestComputed(models.Model):
    _name = "test.computed"

    total = fields.Float(compute="_compute_total", inverse="_inverse_total")
    amount = fields.Float()

    @api.depends("amount")
    def _compute_total(self):
        for record in self:
            record.total = 2.0 * record.amount

    def _inverse_total(self):
        for record in self:
            record.amount = record.total / 2.0

计算方法用于设置字段,而反向方法用于设置字段的依赖关系。

注意,inverse方法在保存记录时被调用,而compute方法则在其依赖关系发生每次变化时被调用。

开发实践:计算报价的有效期

将以下字段添加到 estate.property.offer 模型中:

字段类型默认备注
validityInteger77天有效期
date_deadlineDate

其中 date_deadline 是一个计算字段,其定义为报价中两个字段(create_datevalidity)的和。请定义一个合适的反向函数,以便用户可以设置日期或有效期。

提示:create_date 字段仅在记录创建时才会填充,因此需要设置一个备用方案,以防止在创建时发生崩溃,在表单视图和列表视图中添加这些字段。

1.3、补充信息

计算字段默认不会存储在数据库中。因此,除非定义了搜索方法,否则无法对计算字段进行搜索。此主题超出了本教程的范围,因此这里先不讨论。

另一种解决方案是使用 store=True 属性来存储该字段。虽然这通常很方便,但请注意这可能会给模型增加计算负载,下面看看之前的示例:

python
description = fields.Char(compute="_compute_description", store=True)
partner_id = fields.Many2one("res.partner")

@api.depends("partner_id.name")
def _compute_description(self):
    for record in self:
        record.description = "Test for partner %s" % record.partner_id.name

每当合作伙伴名称发生变更时,系统会自动重新计算所有引用该名称的记录的描述!当需要重新计算数百万条记录时,这种重新计算操作很快就会变得难以承受。

另外值得注意的是,计算字段可能依赖于另一个计算字段。ORM 足够智能,能够按正确顺序重新计算所有依赖关系……但有时这会以性能下降为代价。

总的来说,在定义计算字段时必须时刻关注性能。字段的计算越复杂(例如存在大量依赖关系,或计算字段依赖于其他计算字段),计算所需的时间就越长。请务必预先花些时间评估计算字段的性能开销。大多数情况下,只有当代码部署到生产服务器后,才会意识到它拖慢了整个流程。所以有条件,最好提前规划。

2、onchange

开发实践: 启用花园功能将设置默认面积为 10,并将其朝向设为北方

estate模块中,为了方便用户录入数据。当“花园”字段被设置时,系统会自动将花园面积和朝向提供默认值。此外,当“花园”字段未被设置时,我们希望花园面积重置为:0,并移除朝向字段。在这种情况下,某个字段的值会影响其他字段的值。

“onchange”机制为客户端界面提供了一种方式,当用户填写字段值时,无需将任何内容保存到数据库即可更新表单。为实现此功能,定义了一个方法,其中 self 代表表单视图中的记录,并使用 onchange() 装饰器来指定触发该方法的字段。对 self 进行的任何更改都将反映在表单上:

python
from odoo import api, fields, models

class TestOnchange(models.Model):
    _name = "test.onchange"

    name = fields.Char(string="Name")
    description = fields.Char(string="Description")
    partner_id = fields.Many2one("res.partner", string="Partner")

    @api.onchange("partner_id")
    def _onchange_partner_id(self):
        self.name = "Document for %s" % (self.partner_id.name)
        self.description = "Default description for %s" % (self.partner_id.name)

在此示例中,更改关联对象也会随之更改名称和描述的值。用户可以自行决定是否在之后修改名称和描述的值。另外请注意,这里没有对 self 进行循环,这是因为该方法仅在表单视图中触发,而在表单视图中,self 始终代表单条记录。

设置花园面积和朝向的值

estate.property 模型中创建一个 onchange 事件,以便在花园属性设置为 True 时,将花园面积(10)和朝向(北)的值分别赋给相应字段。当该属性未设置时,清空这些字段。

附加信息

Onchanges 方法还可以返回一条非阻塞的警告消息。

3、如何使用

计算字段和 onchange 事件的使用并没有严格的规则。

在许多情况下,计算字段和 onchange 事件都可以用来实现相同的效果。但应始终优先使用计算字段,因为它们在表单视图之外也会被触发。切勿使用 onchange 事件向模型添加业务逻辑。这是一个非常糟糕的主意,因为在通过编程方式创建记录时,onchange 事件不会自动触发;它们仅在表单视图中触发。

计算字段和 onchange 事件常见的陷阱在于试图通过添加过多逻辑来“过于聪明”。这可能会产生与预期相反的结果:最终用户会被所有自动化操作弄得一头雾水。

计算字段通常更容易调试:此类字段由特定方法设置,因此很容易追踪值的设置时间。另一方面,onchange 事件可能令人困惑:很难确定 onchange 的作用范围。由于多个 onchange 方法可能设置相同的字段,因此很难追踪值的来源。

最近更新