模型之間的關係是任何Odoo模組的關鍵組成部分。它們對於任何業務案例的建模都是必要的。然而,我們可能需要給定模型中欄位之間的連結。有時,一個欄位的值是根據其他欄位的值確定的,有時我們希望幫助使用者輸入資料。
「Computed Fields And Onchanges」的概念支援這些情況。雖然本章在技術上並不複雜,但這兩個概念的語意都非常重要。這也是我們第一次編寫Python邏輯。到目前為止,除了類定義和欄位宣告之外,我們還沒有編寫任何其他東西。
參考: 主題關聯檔案可查閱 Computed Fields.
本章目標
- 在房地產模型中,自動計算總的面積和最佳報價
預期效果:
- 在地產報價模型中,自動計算合法的日期且可被更新
預期效果:
在我們的房地產模組中,我們定義了生活區和花園區。自然地我們將總面積定義這兩者的總和,我們將為此使用計算的欄位的概念,即給定欄位的值將從其他欄位的值中計算出來。
到目前為止,欄位已直接儲存在資料庫中並直接從資料庫中檢索。欄位也可以被計算。在這種情況下,不會從資料庫中檢索欄位的值,而是通過呼叫模型的方法來動態計算的欄位的值。
要建立計算的欄位,請建立欄位並將其屬性compute
設定為方法的名稱。計算方法應為self
中的每個記錄設定計算的欄位的值。
按約定,compute
方法是私有的,這意味著它們不能從表示層呼叫,只能從業務層呼叫。私有方法的名稱以下劃線_
開頭。
計算的欄位的值通常取決於計算記錄中其他欄位的值。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_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
範例以 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.xml
,estate_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>
重啟服務,驗證效果(參考本章目標中第一個動畫連線)
你可能已經注意到,計算的欄位預設總是唯讀的。這正是我們期望的,因為不支援使用者設定值。
某些情況下,可以直接設定值可能會很有用。在我們的房產範例中,我們可以定義報價的有效期間並設定有效日期。我們希望能夠設定有效期間或日期,並且兩者之間相互影響。
為了支援這個需求,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_date
和 validity
兩個欄位的和。定義一個適當的inverse
函數這樣,以便使用者可以編輯 create_date
或 validity
。
提示: 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足夠聰明,可以按照正確的順序正確地重新計算所有依賴項……但有時會以降低效能為代價。
通常,在定義計算的欄位時,必須始終牢記效能。要計算的欄位越複雜(例如,具有大量依賴項或當計算的欄位依賴於其他計算的欄位時),計算所需的時間就越長。請務必事先花一些時間評估計算的欄位的成本。大多數時候,只有當您的程式碼到達生產伺服器時,你才意識到它會減慢整個過程。
參考: 主題關聯檔案可檢視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時,請密切注意依賴項。當計算欄位依賴於其他計算欄位時,更改值可能會觸發大量重新計算。這會導致效能不佳。
作者:授客
微信/QQ:1033553122
全國軟體測試QQ交流群:7156436
Git地址:https://gitee.com/ishouke
友情提示:限於時間倉促,文中可能存在錯誤,歡迎指正、評論!
作者五行缺錢,如果覺得文章對您有幫助,請掃描下邊的二維條碼打賞作者,金額隨意,您的支援將是我繼續創作的源動力,打賞後如有任何疑問,請聯絡我!!!
微信打賞
支付寶打賞 全國軟體測試交流QQ群