目录
概述:
模型之间的关系是任何 Odoo 模块的关键组成部分。任何业务案例的建模都离不开它们。然而,我们可能希望在给定模型内的字段之间建立联系。有时,一个字段的值由其他字段的值决定,有时,我们希望帮助用户输入数据。
计算字段和 onchanges 概念支持这些情况。虽然本章在技术上并不复杂,但这两个概念的语义非常重要。这也是我们第一次编写 Python 逻辑。到目前为止,除了类定义和字段声明之外,我们还没有写过其他东西。
计算字段Computed Fields
参考:有关此主题的文档可在计算字段的文档中找到。
在房地产模块中,我们定义了居住面积和花园面积。因此,将总面积定义为这两个字段的总和是很自然的。为此,我们将使用计算字段的概念,即某个字段的值将根据其他字段的值计算得出。
到目前为止,字段都是直接存储在数据库中或直接从数据库中检索的。字段也可以计算。在这种情况下,字段的值不是从数据库中检索出来的,而是通过调用模型的方法即时计算出来的。
要创建计算字段,请创建一个字段并将其属性 compute 设置为方法名称。计算方法应为 self 中的每条记录设置计算字段的值。
按照惯例,计算方法是私有的,这意味着它们不能从表现层调用,只能从业务层调用(参见第 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。
实践:
计算总面积
在 estate.property 中添加 total_area 字段。它的定义是居住面积和花园面积之和。
计算最佳报价。
在 estate.property 中添加 best_price 字段。它被定义为报价中的最高价(即最大值)。
修改estate_perperty模型为如下代码:
class EstateProperty(models.Model):
_name = "estate_property"
_description = "Estate Property"
def _get_default_date_available(self):
# 使用 fields.Date.today() 获取当前日期,并添加三个月
return fields.Date.today() + relativedelta(months=+3)
name = fields.Char(required=True, default="Unknown")
description = fields.Char(compute="_compute_description", store=True)
postcode = fields.Char()
date_available = fields.Date(copy=False, default=_get_default_date_available)
expected_price = fields.Float(required=True)
selling_price = fields.Float(readonly=True, copy=False)
bedrooms = fields.Integer()
living_area = fields.Integer()
facades = fields.Integer() # 外墙
garage = fields.Boolean()
garden = fields.Boolean()
garden_area = fields.Integer()
garden_orientation = fields.Selection([
("north", "North"),
("south", "South"),
("east", "East"),
("west", "West"),
])
active = fields.Boolean(default=True)
state = fields.Selection([
("new", "New"),
("offer_received", "Offer Received"),
("offer_accepted", "Offer Accepted"),
("sold", "Sold"),
("canceled", "Canceled"),
], default="new", copy=False)
property_type_id = fields.Many2one("estate_property_type", string="Property Type")
user_id = fields.Many2one("res.users", string="Salesperson", default=lambda self: self.env.user)
partner_id = fields.Many2one("res.partner", string="Partner", copy=False)
tag_ids = fields.Many2many("estate_property_tag", string="Tags")
offer_ids = fields.One2many("estate_property_offer", "property_id", string="Offers")
total_area = fields.Integer(compute="_compute_total_area")
best_price = fields.Float(compute="_compute_best_price", string="Best Price")
@api.depends("offer_ids")
def _compute_best_price(self):
for record in self:
record.best_price = max(record.offer_ids.mapped("price"))
@api.depends("living_area", "garden_area")
def _compute_total_area(self):
for record in self:
record.total_area = record.living_area + record.garden_area
@api.depends("partner_id.name")
def _compute_description(self):
for record in self:
record.description = "Test for partner %s" % record.partner_id.name
我们成功添加了Total Area和Best Price自动计算字段!
Inverse Function反函数
您可能已经注意到,计算字段默认为只读。这是意料之中的,因为用户不应该设置值。
在某些情况下,直接设置值可能还是有用的。在我们的房地产示例中,我们可以定义要约的有效期并设置有效日期。我们希望能够设置有效期或有效日期,其中一个会影响另一个。
为此,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
计算方法设置字段,而反函数则设置字段的依赖。
⚠️请注意,逆方法在保存记录时被调用,而计算方法则在每次改变其依赖关系时被调用。
实践:计算报价的有效日期。
在 estate.property.offer 模型中添加以下字段:
Field | Type | Default |
---|---|---|
validity | Integer | 7 |
date_deadline | Date |
其中 date_deadline 是一个计算字段,定义为报价中两个字段的总和:创建日期和有效期。定义一个适当的反函数,以便用户可以设置日期或有效期。
其他信息
计算字段默认不存储在数据库中。因此,除非定义了搜索方法,否则无法对计算字段进行搜索。这一主题超出了本培训的范围,因此我们将不作介绍。在这里可以找到一个示例。
另一种解决方案是使用 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 足够聪明,可以按照正确的顺序正确地重新计算所有依赖关系......但有时会以降低性能为代价。
一般来说,在定义计算字段时必须始终牢记性能。要计算的字段越复杂(例如,有很多依赖关系或一个计算字段依赖于其他计算字段),计算所需的时间就越长。一定要事先花些时间评估计算字段的成本。大多数情况下,只有当你的代码到达生产服务器时,你才会意识到它拖慢了整个流程。这可不好 :-(
Onchanges
在我们的房地产模块中,我们还希望帮助用户输入数据。当设置 "花园 "字段时,我们希望给出花园面积和方向的默认值。此外,当 "花园 "字段未设置时,我们希望将花园面积重置为零,并删除朝向。在这种情况下,给定字段的值会修改其他字段的值。
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)
实践:设置花园面积和方向值。
在 estate.property 模型中创建一个 onchange,以便在花园设置为 True 时设置花园面积 (10) 和朝向 (North) 的值。当未设置时,清除字段。
@api.onchange("garden")
def _onchange_garden(self):
if self.garden:
self.garden_area = 10
self.garden_orientation = "north"
else:
self.garden_area = 0
self.garden_orientation = ""
如何使用它们?
对于计算字段和 onchanges 的使用没有严格的规定。
在很多情况下,使用计算字段和 onchanges 可以达到相同的效果。我们总是倾向于使用计算字段,因为它们也是在表单视图的上下文之外触发的。切勿使用 onchange 向模型添加业务逻辑。这是一个非常糟糕的想法,因为在以编程方式创建记录时,不会自动触发 onchange;它们只会在表单视图中触发。
使用存储的计算字段时,请密切注意其依赖关系。当计算字段依赖于其他计算字段时,更改一个值可能会触发大量的重新计算。这会导致性能低下。