odoo 開發入門教學系列-計算的欄位和變更(Computed Fields And Onchanges)

2023-04-01 12:00:29

計算的欄位和變更(Computed Fields And Onchanges)

模型之間的關係是任何Odoo模組的關鍵組成部分。它們對於任何業務案例的建模都是必要的。然而,我們可能需要給定模型中欄位之間的連結。有時,一個欄位的值是根據其他欄位的值確定的,有時我們希望幫助使用者輸入資料。

「Computed Fields And Onchanges」的概念支援這些情況。雖然本章在技術上並不複雜,但這兩個概念的語意都非常重要。這也是我們第一次編寫Python邏輯。到目前為止,除了類定義和欄位宣告之外,我們還沒有編寫任何其他東西。

計算的欄位(Computed Fields)

參考: 主題關聯檔案可查閱 Computed Fields.

本章目標

  • 在房地產模型中,自動計算總的面積和最佳報價

預期效果:

  • 在地產報價模型中,自動計算合法的日期且可被更新

預期效果:

在我們的房地產模組中,我們定義了生活區和花園區。自然地我們將總面積定義這兩者的總和,我們將為此使用計算的欄位的概念,即給定欄位的值將從其他欄位的值中計算出來。

到目前為止,欄位已直接儲存在資料庫中並直接從資料庫中檢索。欄位也可以被計算。在這種情況下,不會從資料庫中檢索欄位的值,而是通過呼叫模型的方法來動態計算的欄位的值。

要建立計算的欄位,請建立欄位並將其屬性compute設定為方法的名稱。計算方法應為self中的每個記錄設定計算的欄位的值。

按約定,compute方法是私有的,這意味著它們不能從表示層呼叫,只能從業務層呼叫。私有方法的名稱以下劃線_開頭。

依賴(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物件是一個結果集(recordset),即一個有序記錄集合。支援標準Python集合運算,比如len(self)iter(self), 外加其它集合操作,比如 recs1 | recs2

self 上迭代,會一個接一個的生成記錄,其中每個記錄本身是長度為1的集合。可以使用.(比如 record.name)存取單條記錄的欄位或者給欄位賦值。

一個簡單的範例

    @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的總和。
  • 新增欄位到表單檢視,正如本章目標中展示的那樣

對於關係型欄位,可以使用通過欄位的路徑作為依賴項:

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一樣的。

一個簡單的範例

    @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

修改

from odoo import models, fields

from odoo import models, fields, api

最末尾新增以下內容

    total_area = fields.Integer(compute='_compute_total_area')

    @api.depends("garden_area, living_area")
    def _compute_total_area(self):
        for record in self:
            record.total_area = record.living_area + record.garden_area

修改odoo14\custom\estate\views\estate_property_views.xmlestate_property_view_form檢視,Description描述頁,新增total_area欄位

                        <page string="Description">
                            <group>
                                <field name="description"></field>
                                <field name="bedrooms"></field>
                                <field name="living_area"></field>
                                <field name="facades"></field>
                                <field name="garage"></field>
                                <field name="garden"></field>
                                <field name="garden_area"></field>
                                <field name="garden_orientation"></field>
                                <field name="total_area" string="Total Area"></field><!--本次新增的內容-->
                            </group>
                        </page>

重啟服務,重新整理瀏覽器驗證效果


)

練習--計算最佳報價
  • 新增best_price欄位到estate.property。該欄位被定義為最高報價
  • 新增該欄位到表單檢視,正如本章目標中的第一個動畫

提示:你可能會想用 mapped() 方法,檢視範例

                writeoff_amount = sum(writeoff_lines.mapped('amount_currency'))

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

    best_price = fields.Float(compute='_compute_best_offer')

最末尾新增以下函數

    @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檢視

                        <group>
                            <field name="expected_price" string="Expected Price"></field>
                            <field name="selling_price" string="Selling Price"></field>
                        </group>

修改為

                        <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函數的能力:

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

一個簡單的範例

    @api.depends('partner_id.email')
    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

compute方法設定欄位,而inverse方法設定欄位的相關性。

注意,儲存記錄時呼叫inverse方法,而每次更改依賴項時呼叫compute方法。

練習--為報價計算一個有效期
  • 新增以下欄位到 estate.property.offer 模型:
Field Type Default
validity Integer 7
date_deadline Date

其中,date_deadline 為一個計算的欄位,定義為 create_datevalidity兩個欄位的和。定義一個適當的inverse函數這樣,以便使用者可以編輯 create_datevalidity

提示: create_date 僅在記錄建立時被填充,因此需要一個回退,防止建立時的奔潰

  • 在表單和列表檢視中新增欄位,正如本章目標中顯示的第二個動畫中的一樣。

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

from odoo import models, fields

修改為

from odoo import models, fields, api
from datetime import timedelta

末尾新增以下程式碼

    validity = fields.Integer(default=7)
    date_deadline = fields.Date(compute='_compute_date_deadline', inverse='_inverse_date_deadline')

    @api.depends('validity', 'create_date')
    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('validity', 'create_date')
    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

修改odoo14\custom\estate\views\estate_property_offer_views.xml

<?xml version="1.0"?>
<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 方法。該主題不在訓練範圍內,所以,這裡不做介紹。一個簡單範例

    is_ongoing = fields.Boolean('Is Ongoing', compute='_compute_is_ongoing', search='_search_is_ongoing')

另一個解決方法是使用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

每次partnername被改變, 自動為所有參照了它的記錄更新 description 當數以百萬計的記錄需要重新計算時,這可能會很快會變得無法承受

還值得注意的是,計算的欄位可以依賴於另一個計算的欄位。ORM足夠聰明,可以按照正確的順序正確地重新計算所有依賴項……但有時會以降低效能為代價。

通常,在定義計算的欄位時,必須始終牢記效能。要計算的欄位越複雜(例如,具有大量依賴項或當計算的欄位依賴於其他計算的欄位時),計算所需的時間就越長。請務必事先花一些時間評估計算的欄位的成本。大多數時候,只有當您的程式碼到達生產伺服器時,你才意識到它會減慢整個過程。

Onchanges

參考: 主題關聯檔案可檢視onchange():

在我們的房地產模組中,我們還想幫助使用者輸入資料。設定「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)

這個例子中,修改partner的同時也將改變名稱和描述值。最終取決於使用者是否修改名稱和描述值。 同時,需要注意的是,不要回圈遍歷 self,因為該方法在表單檢視中觸發,self總是代表單條記錄。

練習--為花園面積和朝向賦值

estate.property模型中建立 onchange 方法以便當勾選花園時,設定花園面積(10)和朝向(North),未勾選時,移除花園面積和朝向值。

修改odoo14\custom\estate\models\estate_property.py,末尾新增一下程式碼

    @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 = ''

重啟服務,驗證效果(預期效果參考動畫:https://www.odoo.com/documentation/14.0/zh_CN/_images/onchange.gif)

其它資訊

Onchanges方法也可以返回非阻塞告警訊息(範例)

    @api.onchange('provider', 'check_validity')
    def onchange_check_validity(self):
        if self.provider == 'authorize' and self.check_validity:
            self.check_validity = False
            return {'warning': {
                'title': _("Warning"),
                'message': ('This option is not supported for Authorize.net')}}

如何使用它們?

對於computed field 和Onchanges的使用沒有嚴格的規則。

在許多情況下,可以使用computed field和onchanges來實現相同的結果。始終首選computed field,因為它們也是在表單檢視上下文之外觸發的。永遠不要使用onchange將業務邏輯新增到模型中。這是一個非常糟糕的想法,因為在以程式設計方式建立記錄時不會自動觸發onchanges;它們僅在表單檢視中觸發。

computed field和onchanges的常見陷阱是試圖通過新增過多邏輯來變得「過於智慧」。這可能會產生與預期相反的結果:終端使用者被所有自動化所迷惑。

computed field往往更容易偵錯:這樣的欄位是由給定的方法設定的,因此很容易跟蹤設定值的時間。另一方面,onchanges可能會令人困惑:很難知道onchange的程度。由於幾個onchange方法可能會設定相同的欄位,因此跟蹤值的來源很容易變得困難。

儲存computed fields時,請密切注意依賴項。當計算欄位依賴於其他計算欄位時,更改值可能會觸發大量重新計算。這會導致效能不佳。