第九章 计算字段和onchanges
模型之间的关系是Odoo模块的关键组件。他们对任何商业情景的模型化都是必要的。然而,我们想连接同一个模型中的不同字段。有时候一个字段的值由其他字段来决定,有时候我们想帮助用户进行数据输入。
这些场景可以通过计算字段和onchanges的概念来支持。虽然这一章在技术上不是很复杂,这两个概念的含义很重要。这也是我们第一次写python逻辑,直到现在除了class定义和字段声明我们还没写过其他东西。
计算字段(Computed Fields)
参考:关于这个主题的文档在这里Computed Fields.
目标: 在这一小节结尾
- 在房产模型中,总面积和最好的报价应该计算出来。
- I在房产报价模型中,有效时间可以被计算并且可以被更新。
在我们的房地产模块,我们定义了使用面积和花园面积,那很自然的我们可以定义一个总面积,通过这两个面积求和来实现。 这种情况可以使用计算字段的概念,也就是说,这个字段的值是通过其他字段计算得到的。
到目前为止,字段都是通过数据库来直接存储和查询的。除此之外,字段值还可以被计算出来,在这种情况下,字段的值不是从数据库检索出来的,而是通过一个模型的方法实时计算出来的。
创建一个计算字段,可以新建一个字段并设置compute属性为一个方法的名字, 计算方法会为self中每条记录的的计算字段设置值。
一般来说,计算方法是私有的,也就是说不能通过表现层调用,只能通过逻辑层调用,私有的方法名前缀是下划线_
依赖(Dependencies)
计算字段的值通常依赖于其他字段的值,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的集合运算。
可以对for循环对self进行便利操作。 用. 来获取字段的值。
Odoo中有很多关于计算字段的例子,这里有一个简单的例子Here
练习:
计算总面积
- 给estate.property增加字段
total_area
,通过living_area 和`garden_area 二者求和来计算。 - 在form视图中增加这个字段
For relational fields it’s possible to use paths through a field as a dependency:
对于关系字段来说,可以使用关联字段生成的路径作为依赖项
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
这个例子是一个多对一的例子,但是对于多对多和1对多来说也有效。这里有个例子 here.
让我们在我们的模块中尝试以下练习。
练习:
计算最好的报价
- 给estate.property模型增加
best_price
字段, 它被定义为最高的报价 - 将这个字段添加到form view中
Tip: 你可能想使用mapped()方法,这里here 有个简单的例子
@api.depends("offer_ids.price")
def _get_max_price(self):
for rec in self:
list_price = rec.offer_ids.mapped("price")
if list_price:
rec.best_price = max(list_price)
else:
rec.best_price =0
Inverse Function
你可能注意到了,计算字段默认是只读的,这是合理的因为用户并不想设置一个值。
但是有时候,用户希望直接设置一个值,在我们的房地产的例子里,我们可以设置一个有效天数然后计算一个日期,也可以先设置一个有效日期反推出有效天数。
为了支持这个功能,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
这里有个例子 here.
一个计算方法设置计算字段同时一个相反的方法设置依赖字段。
注意: inverse方法只有在保存数据的时候被调用,而compute方法在每次依赖项发生变化的时候都会计算。
练习:
计算报价的有效期
- 给
estate.property.offer
model 增加下列字段
Field | Type | Default |
---|---|---|
validity | Integer | 7 |
date_deadline | Date |
date_deadline 是一个计算字段依赖于create_date 和validity, 定义一个合适的反推方法,用户可以设置时间或者有效期。
validity = fields.Integer(string="过期天数",default = 7)
date_deadline = fields.Date(string="过期时间",compute="_compute_date",inverse="_inverse_validity")
@api.depends("create_date","validity")
def _compute_date(self):
for rec in self:
if rec.create_date:
rec.date_deadline = rec.create_date + timedelta(days=rec.validity)
else:
rec.date_deadline = fields.date.today() + timedelta(days=rec.validity)
def _inverse_validity(self):
for rec in self:
rec.validity = (rec.date_deadline-rec.create_date.date()).days
Tip: create_date 只有当创建记录的时候产生,所以你需要一个fallback来阻止创建 的时候崩溃。 fallback是啥玩意?
- 在form和list视图中添加字段,就像上面第二张图片显示的那样。
额外的信息(Additional Information)
计算字段默认不在数据库中保存。 因此查询一个计算字段是不可能的,除非定义一个search方法。 这个主题已经超出了本章的范围,这里有个简单的例子here.
另外一个解决方案是存储数据,通过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
每当partner name变化的时候,引用它的所有的记录的description字段会自动的重新计算, 当记录数达到百万级别的时候,这个代价是非常高昂的。
另外一点也值得注意: 一个计算字段依赖另外一个计算字段,ORM足够聪明按照正确的顺序重新计算,但是有时候也很消耗性能。
一般来说,在定义计算字段时必须始终牢记性能。你要计算的字段越复杂(例如,有很多依赖项,或者当一个计算字段依赖于其他计算字段时),计算所需的时间就越多。总是花一些时间事先评估计算字段的成本。大多数情况下,只有当您的代码到达生产服务器时,您才会意识到它减慢了整个过程。这不好玩。。
Onchanges
参考:关于这个主题的文档在这里onchange()
:
目标: 这一小节结尾,花园面积给一个默认值10 朝向 北。North.
在我们的房地产模块中,我们希望帮助用户输入数据。 当graden字段被设置,我们希望给花园面积和朝向一个默认值。 另外,当graden没有设置的时候,我们希望花园面积重置成0,朝向也重置为空。这种情况意味着给定字段的值会修改其他字段的值。
’ onchange ‘机制为客户端界面提供了一种方法,可以在用户填写字段值时更新表单,而无需将任何内容保存到数据库中。为了实现这一点,我们定义了一个方法,其中’ self '代表表单视图中的记录,并使用’ onchange() '来修饰它,以指定触发它的字段。您可能想尝试使用mapped()方法。请看这里的一个简单示例。你对“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)
在这个例子中,改变partner将会同时改变name和description的值。 注意这里不用使用循环。 因为这个方法只有在form视图中被触发,self是一条记录。
练习: 设置花园面积和朝向
在estate.property模型中 新建一个onchange方法,当garden设置为true的时候设置花园面积为10,朝向为北,garden为false的时候,清空这两个字段。
@api.onchange("garden")
def set_area(self):
if self.garden:
self.garden_area=10
self.garden_orientation="北"
else:
self.garden_area=0
self.garden_orientation=None
其他信息
onchanges方法可以返回一个非阻塞的警告信息
return {'warning': {
'title': _("Warning"),
'message': ('This option is not supported for Authorize.net')}}
怎么使用他们?
对计算字段和onchanges并没有严格的规则。
很多时候,计算字段和onchanges可以实现相同的结果。 总是首选计算字段,因为它们也会在表单视图的上下文之外触发。永远不要使用onchange向模型添加业务逻辑。这是一个非常糟糕的想法,因为当以编程方式创建记录时,onchanges不会自动触发;它们只在表单视图中被触发。
The usual pitfall of computed fields and onchanges is trying to be ‘too smart’ by adding too much logic. This can have the opposite result of what was expected: the end user is confused from all the automation.
计算字段和onchanges的常见陷阱是试图通过添加太多逻辑来“过于聪明”。这可能会产生与预期相反的结果:最终用户对所有的自动化感到困惑。
Computed fields tend to be easier to debug: such a field is set by a given method, so it’s easy to track when the value is set. Onchanges, on the other hand, may be confusing: it is very difficult to know the extent of an onchange. Since several onchange methods may set the same fields, it easily becomes difficult to track where a value is coming from.
计算字段往往更容易调试:这样的字段是由给定的方法设置的,因此很容易跟踪设置值的时间。另一方面,Onchanges可能会令人困惑:很难知道onchange的范围。由于几个onchange方法可能会设置相同的字段,因此很容易难以跟踪值的来源。
When using stored computed fields, pay close attention to the dependencies. When computed fields depend on other computed fields, changing a value can trigger a large number of recomputations. This leads to poor performance.
当使用存储的计算字段,注意依赖,当计算字段依赖其他的计算字段,改变一个值可能会触发大量的重新计算,这往往导致性能低下。
总结一波:
1 onchanges 只在form视图中起作用,self是单条记录,它只是在客户端起作用,不要有跟后台交互的逻辑
2 计算字段可以用,但是不要过于复杂,self是一个记录集
3 如果要保存计算字段,更要注意值的改变引发的重新计算引发的性能问题。
下一章,让我们看看通过点击按钮来触发商业逻辑。