odoo 开发入门教程系列-计算的字段和变更(Computed Fields And Onchanges)

计算的字段和变更(Computed Fields And Onchanges)

模型之间的关系是任何Odoo模块的关键组成部分。它们对于任何业务案例的建模都是必要的。然而,我们可能需要给定模型中字段之间的链接。有时,一个字段的值是根据其他字段的值确定的,有时我们希望帮助用户输入数据。

“Computed Fields And Onchanges”的概念支持这些情况。虽然本章在技术上并不复杂,但这两个概念的语义都非常重要。这也是我们第一次编写Python逻辑。到目前为止,除了类定义和字段声明之外,我们还没有编写任何其他东西。

计算的字段(Computed Fields)

参考: 主题关联文档可查阅 Computed Fields.

本章目标

  • 在房地产模型中,自动计算总的面积和最佳报价

预期效果:

  • 在地产报价模型中,自动计算合法的日期且可被更新

在我们的房地产模块中,我们定义了生活区和花园区。自然地我们将总面积定义这两者的总和,我们将为此使用计算的字段的概念,即给定字段的值将从其他字段的值中计算出来。

到目前为止,字段已直接存储在数据库中并直接从数据库中检索。字段也可以被计算。在这种情况下,不会从数据库中检索字段的值,而是通过调用模型的方法来动态计算的字段的值。

要创建计算的字段,请创建字段并将其属性compute设置为方法的名称。计算方法应为self中的每个记录设置计算的字段的值。

按约定,compute方法是私有的,这意味着它们不能从表示层调用,只能从业务层调用。私有方法的名称以下划线_开头。

依赖(Dependencies)

计算的字段的值通常取决于计算记录中其他字段的值。ORM期望开发人员使用修饰符depends()指定计算方法上的依赖项。每当修改字段的某些依赖项时,ORM使用给定的依赖项来触发字段的重新计算

代码语言:javascript
复制
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</code></pre></div></div><blockquote><p> 注解

self 是一个集合
self对象是一个结果集(recordset),即一个有序记录集合。支持标准Python集合运算,比如len(self)iter(self), 外加其它集合操作,比如 recs1 | recs2
self 上迭代,会一个接一个的生成记录,其中每个记录本身是长度为1的集合。可以使用.(比如 record.name)访问单条记录的字段或者给字段赋值。

一个简单的示例

代码语言:javascript
复制
    @api.depends('debit', 'credit')
    def _compute_balance(self):
        for line in self:
            line.balance = line.debit - line.credit
练习--计算总面积
  • 添加total_area 字段到 estate.property。该字段被定义为living_areagarden_area的总和。
  • 添加字段到表单视图,正如本章目标中展示的那样

对于关系型字段,可以使用通过字段的路径作为依赖项:

代码语言:javascript
复制
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一样的。

一个简单的示例

代码语言:javascript
复制
    @api.depends('line_ids.amount_type')
def _compute_show_decimal_separator(self):
for record in self:
record.show_decimal_separator = any(l.amount_type == 'regex' for l in record.line_ids)

修改odoo14\custom\estate\models\estate_property.py

修改

代码语言:javascript
复制
from odoo import models, fields

代码语言:javascript
复制
from odoo import models, fields, api

最末尾添加以下内容

代码语言:javascript
复制
    total_area = fields.Integer(compute='_compute_total_area')

@api.depends(&#34;garden_area, living_area&#34;)
def _compute_total_area(self):
    for record in self:
        record.total_area = record.living_area + record.garden_area</code></pre></div></div><p>修改<code>odoo14\custom\estate\views\estate_property_views.xml</code>,<code>estate_property_view_form</code>视图,<code>Description</code>描述页,添加<code>total_area</code>字段</p><div class="rno-markdown-code"><div class="rno-markdown-code-toolbar"><div class="rno-markdown-code-toolbar-info"><div class="rno-markdown-code-toolbar-item is-type"><span class="is-m-hidden">代码语言:</span>javascript</div></div><div class="rno-markdown-code-toolbar-opt"><div class="rno-markdown-code-toolbar-copy"><i class="icon-copy"></i><span class="is-m-hidden">复制</span></div></div></div><div class="developer-code-block"><pre class="prism-token token line-numbers language-javascript"><code class="language-javascript" style="margin-left:0">                        &lt;page string=&#34;Description&#34;&gt;
                        &lt;group&gt;
                            &lt;field name=&#34;description&#34;&gt;&lt;/field&gt;
                            &lt;field name=&#34;bedrooms&#34;&gt;&lt;/field&gt;
                            &lt;field name=&#34;living_area&#34;&gt;&lt;/field&gt;
                            &lt;field name=&#34;facades&#34;&gt;&lt;/field&gt;
                            &lt;field name=&#34;garage&#34;&gt;&lt;/field&gt;
                            &lt;field name=&#34;garden&#34;&gt;&lt;/field&gt;
                            &lt;field name=&#34;garden_area&#34;&gt;&lt;/field&gt;
                            &lt;field name=&#34;garden_orientation&#34;&gt;&lt;/field&gt;
                            &lt;field name=&#34;total_area&#34; string=&#34;Total Area&#34;&gt;&lt;/field&gt;&lt;!--本次添加的内容--&gt;
                        &lt;/group&gt;
                    &lt;/page&gt;</code></pre></div></div><p>重启服务,刷新浏览器验证效果</p><figure class=""><div class="rno-markdown-img-url" style="text-align:center"><div class="rno-markdown-img-url-inner" style="width:58.04%"><div style="width:100%"><img src="https://cdn.static.attains.cn/app/developer-bbs/upload/1723325339201062141.png" /></div></div></div></figure><p>

)

练习--计算最佳报价
  • 添加best_price字段到estate.property。该字段被定义为最高报价
  • 添加该字段到表单视图,正如本章目标中的第一个动画

提示:你可能会想用 mapped() 方法,查看示例

代码语言:javascript
复制
                writeoff_amount = sum(writeoff_lines.mapped('amount_currency'))

修改odoo14\custom\estate\models\estate_property.py,在total_area下方添加best_price

代码语言:javascript
复制
    best_price = fields.Float(compute='_compute_best_offer')

最末尾添加以下函数

代码语言:javascript
复制
    @api.depends('offer_ids.price')
def _compute_best_offer(self):
for record in self:
prices = record.mapped('offer_ids.price')
if prices:
record.best_price = max(prices)
else:
record.best_price = 0.00

修改odoo14\custom\estate\views\estate_property_views.xml文件estate_property_view_form视图

代码语言:javascript
复制
                        <group>
<field name="expected_price" string="Expected Price"></field>
<field name="selling_price" string="Selling Price"></field>
</group>

修改为

代码语言:javascript
复制
                        <group>
<field name="expected_price" string="Expected Price"></field>
<field name="best_price" string="Best Price" />
<field name="selling_price" string="Selling Price"></field>
</group>

重启服务,验证效果(参考本章目标中第一个动画连接)

Inverse函数

你可能已经注意到,计算的字段默认总是只读的。这正是我们期望的,因为不支持用户设置值。

某些情况下,可以直接设置值可能会很有用。在我们的房产示例中,我们可以定义报价的有效期间并设置有效日期。我们希望能够设置有效期间或日期,并且两者之间相互影响。

为了支持这个需求,odoo提供了使用inverse函数的能力:

代码语言:javascript
复制
from odoo import api, fields, models

class TestComputed(models.Model):
_name = "test.computed"

total = fields.Float(compute=&#34;_compute_total&#34;, inverse=&#34;_inverse_total&#34;)
amount = fields.Float()

@api.depends(&#34;amount&#34;)
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</code></pre></div></div><p>一个简单的示例</p><div class="rno-markdown-code"><div class="rno-markdown-code-toolbar"><div class="rno-markdown-code-toolbar-info"><div class="rno-markdown-code-toolbar-item is-type"><span class="is-m-hidden">代码语言:</span>javascript</div></div><div class="rno-markdown-code-toolbar-opt"><div class="rno-markdown-code-toolbar-copy"><i class="icon-copy"></i><span class="is-m-hidden">复制</span></div></div></div><div class="developer-code-block"><pre class="prism-token token line-numbers language-javascript"><code class="language-javascript" style="margin-left:0">    @api.depends(&#39;partner_id.email&#39;)
def _compute_email_from(self):
    for lead in self:
        if lead.partner_id.email and lead.partner_id.email != lead.email_from:
            lead.email_from = lead.partner_id.email

def _inverse_email_from(self):
    for lead in self:
        if lead.partner_id and lead.email_from != lead.partner_id.email:
            lead.partner_id.email = lead.email_from</code></pre></div></div><p>compute方法设置字段,而inverse方法设置字段的相关性。</p><p><strong>注意,保存记录时调用<code>inverse</code>方法,而每次更改依赖项时调用<code>compute</code>方法。</strong></p><h6 id="3n407" name="%E7%BB%83%E4%B9%A0--%E4%B8%BA%E6%8A%A5%E4%BB%B7%E8%AE%A1%E7%AE%97%E4%B8%80%E4%B8%AA%E6%9C%89%E6%95%88%E6%9C%9F">练习--为报价计算一个有效期</h6><ul class="ul-level-0"><li>添加以下字段到 <code>estate.property.offer</code> 模型:</li></ul><div class="table-wrapper"><table><thead><tr><th style="text-align:left"><div><div class="table-header"><p>Field</p></div></div></th><th style="text-align:left"><div><div class="table-header"><p>Type</p></div></div></th><th style="text-align:left"><div><div class="table-header"><p>Default</p></div></div></th></tr></thead><tbody><tr><td style="text-align:left"><div><div class="table-cell"><p>validity</p></div></div></td><td style="text-align:left"><div><div class="table-cell"><p>Integer</p></div></div></td><td style="text-align:left"><div><div class="table-cell"><p>7</p></div></div></td></tr><tr><td style="text-align:left"><div><div class="table-cell"><p>date_deadline</p></div></div></td><td style="text-align:left"><div><div class="table-cell"><p>Date</p></div></div></td><td style="text-align:left"><div><div class="table-cell"><p></p></div></div></td></tr></tbody></table></div><p>其中,<code>date_deadline</code> 为一个计算的字段,定义为 <code>create_date</code>和 <code>validity</code>两个字段的和。定义一个适当的<code>inverse</code>函数这样,以便用户可以编辑 <code>create_date</code>或 <code>validity</code>。</p><p>提示: <code>create_date</code> 仅在记录创建时被填充,因此需要一个回退,防止创建时的奔溃</p><ul class="ul-level-0"><li>在表单和列表视图中添加字段,正如本章目标中显示的第二个动画中的一样。</li></ul><p>修改<code>odoo14\custom\estate\models\estate_property_offer.py</code></p><div class="rno-markdown-code"><div class="rno-markdown-code-toolbar"><div class="rno-markdown-code-toolbar-info"><div class="rno-markdown-code-toolbar-item is-type"><span class="is-m-hidden">代码语言:</span>javascript</div></div><div class="rno-markdown-code-toolbar-opt"><div class="rno-markdown-code-toolbar-copy"><i class="icon-copy"></i><span class="is-m-hidden">复制</span></div></div></div><div class="developer-code-block"><pre class="prism-token token line-numbers language-javascript"><code class="language-javascript" style="margin-left:0">from odoo import models, fields</code></pre></div></div><p>修改为</p><div class="rno-markdown-code"><div class="rno-markdown-code-toolbar"><div class="rno-markdown-code-toolbar-info"><div class="rno-markdown-code-toolbar-item is-type"><span class="is-m-hidden">代码语言:</span>javascript</div></div><div class="rno-markdown-code-toolbar-opt"><div class="rno-markdown-code-toolbar-copy"><i class="icon-copy"></i><span class="is-m-hidden">复制</span></div></div></div><div class="developer-code-block"><pre class="prism-token token line-numbers language-javascript"><code class="language-javascript" style="margin-left:0">from odoo import models, fields, api

from datetime import timedelta

末尾添加以下代码

代码语言:javascript
复制
    validity = fields.Integer(default=7)
date_deadline = fields.Date(compute='_compute_date_deadline', inverse='_inverse_date_deadline')

@api.depends(&#39;validity&#39;, &#39;create_date&#39;)
def _compute_date_deadline(self):
    for record in self:
        if record.create_date:
            record.date_deadline = record.create_date.date() + timedelta(days=record.validity)
        else:
            record.date_deadline = datetime.now().date() + timedelta(days=record.validity)

@api.depends(&#39;validity&#39;, &#39;create_date&#39;)
def _inverse_date_deadline(self):
    for record in self:
        if record.create_date:
            record.validity = (record.date_deadline - record.create_date.date()).days
        else:
            record.validity = 7</code></pre></div></div><p>修改<code>odoo14\custom\estate\views\estate_property_offer_views.xml</code></p><div class="rno-markdown-code"><div class="rno-markdown-code-toolbar"><div class="rno-markdown-code-toolbar-info"><div class="rno-markdown-code-toolbar-item is-type"><span class="is-m-hidden">代码语言:</span>javascript</div></div><div class="rno-markdown-code-toolbar-opt"><div class="rno-markdown-code-toolbar-copy"><i class="icon-copy"></i><span class="is-m-hidden">复制</span></div></div></div><div class="developer-code-block"><pre class="prism-token token line-numbers language-javascript"><code class="language-javascript" style="margin-left:0">&lt;?xml version=&#34;1.0&#34;?&gt;

<odoo>
<record id="estate_property_offer_view_tree" model="ir.ui.view">
<field name="name">estate.property.offer.tree</field>
<field name="model">estate.property.offer</field>
<field name="arch" type="xml">
<tree string="PropertyOffers">
<field name="price" string="Price"/>
<field name="partner_id" string="partner ID"/>
<field name="validity" string="Validity(days)"/>
<field name="date_deadline" string="Deadline"/>
<field name="status" string="Status"/>
</tree>
</field>
</record>
<record id="estate_property_offer_view_form" model="ir.ui.view">
<field name="name">estate.property.offer.form</field>
<field name="model">estate.property.offer</field>
<field name="arch" type="xml">
<form string="estate property offer form">
<sheet>
<group>
<field name="price" string="Price"/>
<field name="validity" string="Validity(days)"/>
<field name="date_deadline" string="Deadline"/>
<field name="partner_id" string="partner ID"/>
<field name="status" string="Status"/>
</group>
</sheet>
</form>
</field>
</record>
</odoo>

重启服务,浏览器中验证(参考本章目标中的第二个动画视图)

其它信息

默认的,计算的字段不会存到数据库中,因此,不可能基于计算的字段进行搜索,除非定义一个search 方法。该主题不在训练范围内,所以,这里不做介绍。一个简单示例

代码语言:javascript
复制
    is_ongoing = fields.Boolean('Is Ongoing', compute='_compute_is_ongoing', search='_search_is_ongoing')

另一个解决方法是使用store=True属性存储该字段。虽然这通常很方便,但请注意给模型增加的潜在计算压力。让我们重新使用我们的示例。复用我们的示例:

代码语言:javascript
复制
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

每次partnername被改变, 自动为所有引用了它的记录更新 description 当数以百万计的记录需要重新计算时,这可能会很快会变得无法承受

还值得注意的是,计算的字段可以依赖于另一个计算的字段。ORM足够聪明,可以按照正确的顺序正确地重新计算所有依赖项……但有时会以降低性能为代价。

通常,在定义计算的字段时,必须始终牢记性能。要计算的字段越复杂(例如,具有大量依赖项或当计算的字段依赖于其他计算的字段时),计算所需的时间就越长。请务必事先花一些时间评估计算的字段的成本。大多数时候,只有当您的代码到达生产服务器时,你才意识到它会减慢整个过程。

Onchanges

参考: 主题关联文档可查看onchange():

在我们的房地产模块中,我们还想帮助用户输入数据。设置“garden”字段后,我们希望为花园面积和朝向提供默认值。此外,当“花园”字段未设置时,我们希望花园面积和重置为零,并删除朝向。在这种情况下,给定字段的值会影响其他字段的值。

“onchange”机制为客户端界面提供了一种,无论用户合适填写字段值更新表单,都无需存储任何东西到数据库的一种方法。为了实现这一点,我们定义了一个方法,其中self表示表单视图中的记录,并用 onchange()修饰该方法,以指明它由哪个字段触发。你对self所做的任何更改都将反映在表单上:

代码语言:javascript
复制
from odoo import api, fields, models

class TestOnchange(models.Model):
_name = "test.onchange"

name = fields.Char(string=&#34;Name&#34;)
description = fields.Char(string=&#34;Description&#34;)
partner_id = fields.Many2one(&#34;res.partner&#34;, string=&#34;Partner&#34;)

@api.onchange(&#34;partner_id&#34;)
def _onchange_partner_id(self):
    self.name = &#34;Document for %s&#34; % (self.partner_id.name)
    self.description = &#34;Default description for %s&#34; % (self.partner_id.name)</code></pre></div></div><p>这个例子中,修改partner的同时也将改变名称和描述值。最终取决于用户是否修改名称和描述值。 同时,需要注意的是,不要循环遍历 <code>self</code>,因为该方法在表单视图中触发,<code>self</code>总是代表单条记录。</p><h5 id="9dppa" name="%E7%BB%83%E4%B9%A0--%E4%B8%BA%E8%8A%B1%E5%9B%AD%E9%9D%A2%E7%A7%AF%E5%92%8C%E6%9C%9D%E5%90%91%E8%B5%8B%E5%80%BC">练习--为花园面积和朝向赋值</h5><p>在<code>estate.property</code>模型中创建 <code>onchange</code> 方法以便当勾选花园时,设置花园面积(10)和朝向(North),未勾选时,移除花园面积和朝向值。</p><p>修改<code>odoo14\custom\estate\models\estate_property.py</code>,末尾添加一下代码</p><div class="rno-markdown-code"><div class="rno-markdown-code-toolbar"><div class="rno-markdown-code-toolbar-info"><div class="rno-markdown-code-toolbar-item is-type"><span class="is-m-hidden">代码语言:</span>javascript</div></div><div class="rno-markdown-code-toolbar-opt"><div class="rno-markdown-code-toolbar-copy"><i class="icon-copy"></i><span class="is-m-hidden">复制</span></div></div></div><div class="developer-code-block"><pre class="prism-token token line-numbers language-javascript"><code class="language-javascript" style="margin-left:0">    @api.onchange(&#34;garden&#34;)
def _onchange_garden(self):
    if self.garden:
        self.garden_area = 10
        self.garden_orientation = &#39;North&#39;
    else:
        self.garden_area = 0
        self.garden_orientation = &#39;&#39;</code></pre></div></div><p>重启服务,验证效果(预期效果参考动画:https://www.odoo.com/documentation/14.0/zh_CN/_images/onchange.gif)</p><figure class=""><div class="rno-markdown-img-url" style="text-align:center"><div class="rno-markdown-img-url-inner" style="width:100%"><div style="width:100%"><img src="https://cdn.static.attains.cn/app/developer-bbs/upload/1723325340078666484.gif" /></div></div></div></figure><h5 id="986v0" name="%E5%85%B6%E5%AE%83%E4%BF%A1%E6%81%AF">其它信息</h5><p>Onchanges方法也可以返回非阻塞告警消息(示例)</p><div class="rno-markdown-code"><div class="rno-markdown-code-toolbar"><div class="rno-markdown-code-toolbar-info"><div class="rno-markdown-code-toolbar-item is-type"><span class="is-m-hidden">代码语言:</span>javascript</div></div><div class="rno-markdown-code-toolbar-opt"><div class="rno-markdown-code-toolbar-copy"><i class="icon-copy"></i><span class="is-m-hidden">复制</span></div></div></div><div class="developer-code-block"><pre class="prism-token token line-numbers language-javascript"><code class="language-javascript" style="margin-left:0">    @api.onchange(&#39;provider&#39;, &#39;check_validity&#39;)
def onchange_check_validity(self):
    if self.provider == &#39;authorize&#39; and self.check_validity:
        self.check_validity = False
        return {&#39;warning&#39;: {
            &#39;title&#39;: _(&#34;Warning&#34;),
            &#39;message&#39;: (&#39;This option is not supported for Authorize.net&#39;)}}</code></pre></div></div><h4 id="effdn" name="%E5%A6%82%E4%BD%95%E4%BD%BF%E7%94%A8%E5%AE%83%E4%BB%AC?">如何使用它们?</h4><p>对于computed field 和Onchanges的使用没有严格的规则。</p><p>在许多情况下,可以使用computed field和onchanges来实现相同的结果。始终首选computed field,因为它们也是在表单视图上下文之外触发的。永远不要使用onchange将业务逻辑添加到模型中。这是一个<strong>非常糟糕的</strong>想法,因为在以编程方式创建记录时不会自动触发onchanges;它们仅在表单视图中触发。</p><p>computed field和onchanges的常见陷阱是试图通过添加过多逻辑来变得“过于智能”。这可能会产生与预期相反的结果:终端用户被所有自动化所迷惑。</p><p>computed field往往更容易调试:这样的字段是由给定的方法设置的,因此很容易跟踪设置值的时间。另一方面,onchanges可能会令人困惑:很难知道onchange的程度。由于几个onchange方法可能会设置相同的字段,因此跟踪值的来源很容易变得困难。</p><p>存储computed fields时,请密切注意依赖项。当计算字段依赖于其他计算字段时,更改值可能会触发大量重新计算。这会导致性能不佳。</p>