第 9 章:计算字段与 onchange¶
模型之间的关系 是任何 Odoo 模块的关键组件,是对任何业务场景建模的必要条件。不过,我们可能还希望同一个模型内的字段之间建立联系:有时一个字段的值由其他字段的值决定,有时我们想在数据录入时帮助用户。
这些场景由计算字段和 onchange 概念来支持。虽然这一章在技术上并不复杂,但这两个概念的语义非常重要。这也是我们第一次编写 Python 逻辑。在此之前,我们只写过类定义和字段声明。
计算字段¶
参考 :与该主题相关的文档可参阅 计算字段 。
Note
目标:在本节结束时:
在房产模型中,总面积和最佳报价应当是计算得出的:
在房产报价模型中,有效日期应当是计算得出的,且可以更新:
在房产模块中,我们已经定义了居住面积和花园面积。那么很自然地,可以把总面积定义为这两个字段之和。我们将为此使用计算字段的概念,即某个字段的值由其他字段的值计算得出。
到目前为止,字段都是直接存入数据库并从中直接读取的。字段也可以是 计算型 的。在这种情况下,字段的值不从数据库中读取,而是通过调用模型的一个方法即时计算得出。
要创建计算字段,请创建一个字段,并将其属性 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
Note
self 是一个集合。
对象 self 是一个 记录集 (recordset),即记录的有序集合。它支持集合上的标准 Python 操作,例如 len(self) 和 iter(self),还支持额外的集合运算,如 recs1 | recs2。
对 self 进行迭代会逐个给出记录,其中每条记录本身又是一个大小为 1 的集合。您可以使用点号表示法访问/赋值单条记录上的字段,例如 record.name。
Odoo 中有许多计算字段的示例。这里 就是一个简单的例子。
Exercise
计算总面积。
向
estate.property添加total_area字段。它定义为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 同样有效。可以在 此处 找到示例。
让我们通过下面的练习在模块中试一试吧!
Exercise
计算最佳报价。
向
estate.property添加best_price字段。它定义为各报价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
可以在 此处 找到示例。
compute 方法设置字段本身,而 inverse 方法设置字段的依赖项。
注意,inverse 方法在保存记录时调用,而 compute 方法在其依赖项每次发生变化时调用。
Exercise
为报价计算有效日期。
向
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():
Note
目标:在本节结束时,启用花园(garden)将设置默认面积为 10,朝向为北。
在不动产模块中,我们还想帮助用户录入数据。当设置 ‘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)
在这个例子中,更改合作伙伴也会同时更改 name 和 description 的值。之后是否修改 name 和 description 的值由用户决定。另请注意,我们没有遍历 self,这是因为该方法只在表单视图中触发,而表单视图中 self 始终是单条记录。
Exercise
设置花园面积和朝向的值。
在 estate.property 模型中创建一个 onchange,以便在 garden 设置为 True 时为花园面积(10)和朝向(North)设置值。取消设置时,清空这些字段。
附加信息¶
onchange 方法还可以返回一条非阻断性的警告消息(示例)。
如何使用它们?¶
对于计算字段和 onchange 的使用,并没有严格的规则。
在很多情况下,计算字段和 onchange 都可以用来实现相同的结果。请始终优先使用计算字段,因为它们在表单视图之外的上下文中也会被触发。切勿使用 onchange 向模型添加业务逻辑。这是一个 非常糟糕 的想法,因为以编程方式创建记录时 onchange 不会被自动触发;它们只在表单视图中触发。
计算字段和 onchange 的常见陷阱是试图通过添加过多逻辑来“过于聪明”。这可能产生与预期相反的结果:最终用户会被各种自动化搞糊涂。
计算字段通常更容易调试:这样的字段由指定方法设置,因此很容易追踪值是在何时设置的。另一方面,onchange 可能令人困惑:很难知道一个 onchange 的影响范围。由于多个 onchange 方法可能设置相同的字段,这很容易让人难以追踪值的来源。
使用存储型计算字段时,请密切关注依赖关系。当计算字段依赖其他计算字段时,更改一个值可能触发大量的重新计算。这会导致性能不佳。
在 下一章 中,我们将了解如何在点击按钮时触发业务逻辑。