Chapter 8: Computed Fields And Onchanges

The relations between models are a key component of any Odoo module. They are necessary for the modelization of any business case. However, we may want links between the fields within a given model. Sometimes the value of one field is determined from the values of other fields and other times we want to help the user with data entry.

这些情况是由计算字段和onchange的概念支持的。虽然本章不是技术上复杂的,但是这两个概念的语义非常重要。这也是我们第一次编写Python逻辑。到目前为止,我们除了类定义和字段声明之外还没有编写任何东西。

计算字段

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

注解

目标:在本节结束时:

  • 在物业模型中,应该计算总面积和最佳报价:

计算字段
  • 在物业报价模型中,应该计算有效日期并进行更新:

使用反函数计算字段

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

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

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

By convention, compute methods are private, meaning that they cannot be called from the presentation tier, only from the business tier (see 第一章:架构概述). Private methods have a name starting with an underscore _.

依赖项

计算字段的值通常取决于计算记录中其他字段的值。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 is a collection.

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

迭代 self 逐个给出记录,其中每个记录本身是一个大小为1的集合。您可以使用点表示法访问/分配单个记录上的字段,例如 record.name

在Odoo中可以找到许多计算字段的示例。 这里 <https://github.com/odoo/odoo/blob/713dd3777ca0ce9d121d5162a3d63de3237509f4/addons/account/models/account_move.py#L3420-L3423> __ 是一个简单的示例。

Exercise

计算总面积。

  • total_area 字段添加到 estate.property。它被定义为 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

计算最佳报价。

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

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

提示:你可能想尝试使用 mapped() 方法。查看 这里 以获取一个简单的示例。

反函数

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

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

为了支持这一点,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 方法。这个主题超出了本培训的范围,所以我们不会涉及它。一个示例可以在 这里 <https://github.com/odoo/odoo/blob/f011c9aacf3a3010c436d4e4f408cd9ae265de1b/addons/event/models/event_event.py#L188> __找到。

另一种解决方案是使用 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 足够智能,可以正确地按正确顺序重新计算所有依赖项… 但有时会以性能降低为代价。

在定义计算字段时,通常需要考虑性能问题。计算字段越复杂(例如具有许多依赖关系或计算字段依赖于其他计算字段),计算所需的时间就越长。在定义计算字段之前,始终要花些时间评估其成本。大多数情况下,只有在您的代码到达生产服务器时,您才会意识到它会减慢整个过程的速度。这很不好 :-(

触发器

参考: 有关此主题的文档可以在 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)和方向(North)设置值。当取消设置时,清除这些字段。

附加信息

Onchanges 方法也可以返回一个非阻塞的警告消息(示例)。

如何使用它们?

在使用计算字段和onchange时没有严格的规则。

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

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

计算字段往往更容易调试:这样的字段由给定的方法设置,因此很容易跟踪何时设置值。另一方面,onchange 可能会令人困惑:很难知道 onchange 的范围。由于多个 onchange 方法可能设置相同的字段,因此很容易跟踪值来自何处。

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

In the next chapter, we’ll see how we can trigger some business logic when buttons are clicked.