第 8 章:计算字段与 onchange

模型之间的关联 是任何 Odoo 模块的关键组成部分。它们对于任何业务场景的建模都是必需的。不过,我们可能希望同一个模型内部的字段之间也建立关联。有时一个字段的值由其他字段的值决定,有时我们想帮助用户录入数据。

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

计算字段

参考:与本主题相关的文档请参阅 二进制字段。

注解

目标:本节结束时:

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

计算字段
  • 在房产报价模型中,应计算有效日期,并且可以更新:

带反向函数的计算字段

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

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

要创建计算字段,请先创建字段,并将其 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 是一个 记录集,即记录的有序集合。它支持对集合的标准 Python 操作,例如 len(self) 和 iter(self),以及 recs1 | recs2 之类的额外集合运算。

遍历 self 会逐条得到记录,其中每条记录本身都是大小为 1 的集合。可以使用点号表示法访问或赋值单条记录的字段,例如 record.name。

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

练习

计算总面积。

  • 将 total_area 字段添加到 estate.property。它定义为 living_area 与 garden_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 给出,但它同样适用于 Many2many 或 One2many。一个示例可在 这里 找到。

让我们通过下面的练习在我们的模块中试一试!

练习

计算最佳报价。

  • 将 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 方法。

练习

为报价计算有效期日期。

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

字段

类型

默认

validity

整数

7

date_deadline

日期

其中 date_deadline 是一个计算字段,定义为报价中两个字段之和:create_date 和 validity。请定义合适的反向函数,使用户既能设置日期,也能设置有效期。

提示: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 足够智能,能按正确的顺序重新计算所有依赖项……但有时会以性能下降为代价。

一般而言,定义计算字段时必须始终考虑性能。字段的计算越复杂(例如依赖项很多,或计算字段依赖其他计算字段),计算所需的时间就越长。务必事先花些时间评估计算字段的开销。大多数情况下,只有当代码运行到生产服务器上时,你才会发现它拖慢了整个流程。真不酷 :-(

Onchange 机制

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

注解

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

Onchange 机制

在我们的房地产模块中,我们也希望帮助用户录入数据。当设置 ‘garden’ 字段时,我们希望为花园面积和朝向提供默认值。此外,当取消设置 ‘garden’ 字段时,我们希望花园面积重置为零并移除朝向。在这种情况下,某个字段的值会修改其他字段的值。

‘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 始终是单条记录。

练习

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

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

附加信息

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

如何使用它们?

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

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

计算字段和 onchange 常见的陷阱是试图通过添加过多逻辑来’过于聪明’。这可能产生与预期相反的结果:最终用户会被过多的自动化弄糊涂。

计算字段通常更容易调试:这类字段由特定的方法设置,因此很容易追踪值是在何时设置的。另一方面,onchange 可能会令人困惑:很难知道一个 onchange 的影响范围。由于多个 onchange 方法可能设置相同的字段,很容易变得难以追踪某个值来自何处。

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

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