11. 计算字段
模型之间的关系是 Odoo 中各模块的关键组成部分。它们对于任何业务场景的建模都必不可少。然而,我们可能希望在某个模型内的字段之间建立关联。有时,一个字段的值是由其他字段的值决定的;有时,希望在数据录入过程中为用户提供帮助。
“计算字段”和“onchanges”这两个概念支持上述场景。虽然本章在技术上并不复杂,但这两个概念的语义非常重要。这也是我们首次编写 Python 逻辑代码。此前,我们编写的内容仅限于类定义和字段声明。
1、计算字段
在房产模型中,应计算总面积和最佳(最高)报价:

在房产报价模型中,有效期应进行计算,并且可以更新:
在estate模块中,已经定义了居住面积和花园面积。因此,将总面积定义为这两个字段的和是顺理成章的。我们将为此使用“计算字段”的概念,即某个字段的值将根据其他字段的值进行计算。
到目前为止,字段都是直接存储在数据库中并直接从数据库中检索的。字段也可以通过计算生成。在这种情况下,字段的值并非从数据库中检索,而是通过调用模型的方法在运行时动态计算得出。
要创建计算字段,需先创建一个字段,并将它的 compute 属性设置为某个方法的名称。该计算方法应为 self 中的每条记录设置计算字段的值。
按惯例,compute 方法是私有的,这意味着它们不能从展示层调用,只能从业务层调用。私有方法的名称以下划线 _ 开头。
1.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.amountself 是一个集合。
对象 self 是一个记录集,即有序的记录集合。它支持 Python 集合的标准操作,例如 len(self) 和 iter(self),以及额外的集合运算,如 recs1 | recs2。
遍历 self 会依次返回记录,其中每个记录本身也是一个大小为 1 的集合。可以使用点表示法访问或赋值单个记录的字段,例如 record.name。
在 Odoo 中可以找到许多计算字段的示例。以下是一个简单的示例。
实践开发:计算总面积
在 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虽然示例中使用的是“多对一”关系,但这同样适用于“多对多”或“一对多”关系。
计算最佳报价。
在 estate.property 中添加 best_price 字段。该字段定义为所有报价中的最高(即最优的售价)价格。
@api.depends('offer_ids.price')
def _compute_best_price(self):
for record in self:
# 提取所有报价的价格列表
prices = record.offer_ids.mapped('price')
# 如果存在报价,取最大值;否则设为 0.0
if prices:
record.best_price = max(prices)
else:
record.best_price = 0.0依赖装饰器:在实际 Odoo 开发中,这段逻辑通常放在一个由 @api.depends('offer_ids.price') 装饰的计算字段方法中,以确保当报价发生变化时,该字段会自动重新计算。
1.2、反向函数
可能已经注意到,计算字段默认是只读的。这是意料之中的,因为用户本就不应设置它们的值。在某些情况下,能够直接设置值可能会很有用。在房地产(Estate)示例中,可以为报价定义有效期,并设置有效日期。希望能够设置有效期或日期,且两者相互影响。
为支持这一需求,Odoo 提供了使用反向函数的功能:
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 | Integer | 7 | 7天有效期 |
| date_deadline | Date |
其中 date_deadline 是一个计算字段,其定义为报价中两个字段(create_date 和 validity)的和。请定义一个合适的反向函数,以便用户可以设置日期或有效期。
提示:create_date 字段仅在记录创建时才会填充,因此需要设置一个备用方案,以防止在创建时发生崩溃,在表单视图和列表视图中添加这些字段。
1.3、补充信息
计算字段默认不会存储在数据库中。因此,除非定义了搜索方法,否则无法对计算字段进行搜索。此主题超出了本教程的范围,因此这里先不讨论。
另一种解决方案是使用 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每当合作伙伴名称发生变更时,系统会自动重新计算所有引用该名称的记录的描述!当需要重新计算数百万条记录时,这种重新计算操作很快就会变得难以承受。
另外值得注意的是,计算字段可能依赖于另一个计算字段。ORM 足够智能,能够按正确顺序重新计算所有依赖关系……但有时这会以性能下降为代价。
总的来说,在定义计算字段时必须时刻关注性能。字段的计算越复杂(例如存在大量依赖关系,或计算字段依赖于其他计算字段),计算所需的时间就越长。请务必预先花些时间评估计算字段的性能开销。大多数情况下,只有当代码部署到生产服务器后,才会意识到它拖慢了整个流程。所以有条件,最好提前规划。
2、onchange
开发实践: 启用花园功能将设置默认面积为 10,并将其朝向设为北方。

在estate模块中,为了方便用户录入数据。当“花园”字段被设置时,系统会自动将花园面积和朝向提供默认值。此外,当“花园”字段未被设置时,我们希望花园面积重置为:0,并移除朝向字段。在这种情况下,某个字段的值会影响其他字段的值。
“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 事件,以便在花园属性设置为 True 时,将花园面积(10)和朝向(北)的值分别赋给相应字段。当该属性未设置时,清空这些字段。
附加信息
Onchanges 方法还可以返回一条非阻塞的警告消息。
3、如何使用
计算字段和 onchange 事件的使用并没有严格的规则。
在许多情况下,计算字段和 onchange 事件都可以用来实现相同的效果。但应始终优先使用计算字段,因为它们在表单视图之外也会被触发。切勿使用 onchange 事件向模型添加业务逻辑。这是一个非常糟糕的主意,因为在通过编程方式创建记录时,onchange 事件不会自动触发;它们仅在表单视图中触发。
计算字段和 onchange 事件常见的陷阱在于试图通过添加过多逻辑来“过于聪明”。这可能会产生与预期相反的结果:最终用户会被所有自动化操作弄得一头雾水。
计算字段通常更容易调试:此类字段由特定方法设置,因此很容易追踪值的设置时间。另一方面,onchange 事件可能令人困惑:很难确定 onchange 的作用范围。由于多个 onchange 方法可能设置相同的字段,因此很难追踪值的来源。