第九章:计算字段和Onchanges

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

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

计算字段

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

注解

目标:在本节结束时:

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

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

使用反函数计算字段

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

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

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

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

依赖项

计算字段的值通常取决于计算记录中其他字段的值。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 方法可能设置相同的字段,因此很容易跟踪值来自何处。

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

下一章 中,我们将看到当按钮被点击时,我们如何触发一些业务逻辑。