第 8 章:计算字段与变更(Computed Fields and Onchanges)

模型之间的关系 是任何 Odoo 模块的关键组成部分。它们对于任何业务场景的建模都是必要的。然而,我们可能希望在给定模型内的字段之间建立联系。有时一个字段的值由其他字段的值决定,有时我们希望帮助用户进行数据输入。

这些情况由计算字段和变更的概念支持。虽然本章在技术上并不复杂,但这两个概念的语义非常重要。这也是我们第一次编写 Python 逻辑。在此之前,我们只编写了类定义和字段声明。

计算字段

参考:与此主题相关的文档可以在 计算字段 中找到。

注解

目标:在本节结束时:

  • 在房产模型中,总面积和最佳报价应被计算:

计算字段(Compute Fields)
  • 在房产报价模型中,有效期应被计算并可以更新:

带逆向函数的计算字段(Compute Field with Inverse)

在我们的房地产模块中,我们定义了居住面积和花园面积。因此,将总面积定义为两者的总和是很自然的。我们将使用计算字段的概念来实现这一点,即给定字段的值将根据其他字段的值计算得出。

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

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

按照惯例,compute 方法是私有的,这意味着它们不能从表示层调用,只能从业务层调用(参见 第 1 章:架构概述 )。私有方法的名称以下划线 _ 开头。

依赖项

计算字段的值通常依赖于计算记录中其他字段的值。ORM 要求开发者使用装饰器 depends() 在计算方法中指定这些依赖项。当某些依赖项被修改时,ORM 会使用这些依赖项触发字段的重新计算::

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 是一个 记录集(recordset) ,即有序的记录集合。它支持标准的 Python 集合操作,例如 len(self)iter(self) ,以及额外的集合操作,如 recs1 | recs2

迭代 self 会逐一返回记录,其中每条记录本身是一个大小为 1 的集合。您可以使用点符号访问或赋值单条记录的字段,例如 record.name

Odoo 中有许多计算字段的示例。 这里 是一个简单的例子。

Exercise

计算总面积。

  • estate.property 添加 total_area 字段。它被定义为 living_areagarden_area 的总和。

  • 将字段添加到表单视图中,如本节 目标 中的第一张图片所示。

对于关系字段,可以使用字段路径作为依赖项::

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

示例中使用了 Many2one ,但它同样适用于 Many2manyOne2many 。一个示例可以在这里找到: 链接

让我们在模块中尝试以下练习!

Exercise

计算最佳报价。

  • estate.property 添加 best_price 字段。它被定义为所有报价的 price 中的最高值(即最大值)。

  • 将字段添加到表单视图中,如本节 目标 中的第一张图片所示。

提示:您可以尝试使用 mapped() 方法。请参阅 此处 的简单示例。

逆向函数(Inverse Function)

您可能已经注意到,计算字段默认是只读的。这是预期行为,因为用户不应该设置值。

在某些情况下,仍然能够直接设置值可能会很有用。在我们的房地产示例中,我们可以为报价定义一个有效期,并设置有效日期。我们希望能够设置有效期或日期,其中一个会影响另一个。

为了支持这一点,Odoo 提供了使用 inverse 函数的能力::

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 方法在其依赖项每次发生变化时调用。

Exercise

计算报价的有效期。

  • estate.property.offer 模型添加以下字段:

字段

类型

默认

有效期

整数

7

截止日期

日期

其中,date_deadline 是一个计算字段,定义为报价中的两个字段之和:create_datevalidity。定义一个适当的逆函数,以便用户可以设置日期或有效期。

提示:create_date 仅在记录创建时填充,因此您需要提供一个回退机制以防止在创建时崩溃。

  • 将字段添加到表单视图和列表视图中,如本节 目标 中的第二张图片所示。

附加信息

计算字段默认情况下 不会存储 在数据库中。因此,除非定义了 search 方法,否则无法对计算字段进行搜索。这一主题超出了本培训的范围,因此我们不会深入讨论。一个示例可以在这里找到: 链接

另一种解决方案是使用 store=True 属性存储字段。虽然这通常很方便,但要注意可能增加的模型计算负载。让我们重用以下示例::

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

每当合作伙伴的 name 发生变化时,所有引用它的记录的 description 都会自动重新计算!当需要重新计算数百万条记录时,这很快会变得难以承受。

还值得注意的是,一个计算字段可以依赖于另一个计算字段。ORM 足够智能,可以以正确的顺序重新计算所有依赖项……但有时会导致性能下降。

总的来说,在定义计算字段时必须始终牢记性能问题。计算字段越复杂(例如,具有许多依赖项,或者当一个计算字段依赖于其他计算字段时),计算所需的时间就越多。务必提前花时间评估计算字段的成本。大多数时候,只有当代码进入生产服务器时,你才会意识到它拖慢了整个流程。这就不好了 :-(

变更(Onchanges)

参考:与此主题相关的文档可以在 onchange() 中找到:

注解

目标:在本节结束时,启用花园将设置默认面积为 10,并将方向设置为北。

字段变化触发机制

在我们的房地产模块中,我们还想帮助用户输入数据。当设置了“花园”字段时,我们希望为花园面积和方向提供默认值。此外,当取消设置“花园”字段时,我们希望将花园面积重置为零,并移除方向。在这种情况下,给定字段的值会修改其他字段的值。

‘onchange’ 机制为客户端界面提供了一种方式,可以在用户填写字段值时更新表单,而无需将任何内容保存到数据库中。为此,我们定义一个方法,其中 self 表示表单视图中的记录,并使用 onchange() 装饰器指定它由哪个字段触发。您对 self 的任何更改都会反映在表单中::

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 始终是一个单一记录。

Exercise

设置花园面积和朝向的值。

estate.property 模型中创建一个 onchange,以便在花园设置为 True 时,为花园面积(10)和朝向(北)设置值。当取消设置时,清除这些字段。

附加信息

变更方法还可以返回非阻塞警告消息( 示例 )。

如何使用它们?

对于计算字段和变更的使用,没有严格的规则。

在许多情况下,计算字段和变更都可以用来实现相同的结果。始终优先选择计算字段,因为它们也可以在表单视图上下文之外触发。永远不要使用变更来为模型添加业务逻辑。这是一个 非常糟糕 的想法,因为在通过编程创建记录时,变更不会自动触发;它们仅在表单视图中触发。

计算字段和变更的常见陷阱是试图通过添加过多逻辑来变得“过于聪明”。这可能会产生与预期相反的结果:最终用户因所有的自动化操作而感到困惑。

计算字段往往更容易调试:某个字段由特定方法设置,因此很容易跟踪值何时被设置。另一方面,变更可能会令人困惑:很难了解变更的范围。由于多个变更方法可能设置相同的字段,因此很容易难以追踪值的来源。

在使用存储的计算字段时,请密切关注依赖项。当计算字段依赖于其他计算字段时,更改值可能会触发大量重新计算。这会导致性能下降。

下一章 中,我们将了解如何在按钮点击时触发一些业务逻辑。