全書(shū)完整目錄請(qǐng)見(jiàn):Odoo 12開(kāi)發(fā)者指南(Cookbook)第三版
本章中资昧,我們將講解如下內(nèi)容:
- 定義模型方法及使用API裝飾器
- 向用戶報(bào)出錯(cuò)誤
- 為不同模型獲取一個(gè)空記錄集
- 新建記錄
- 更新記錄集中記錄值
- 搜索記錄
- 合并記錄集
- 過(guò)濾記錄集
- 遍歷記錄集關(guān)聯(lián)
- 記錄集排序
- 繼承模型中定義的業(yè)務(wù)邏輯
- 繼承write()和create()
- 自定義記錄的搜索方式
引言
在第五章 應(yīng)用模型中苔悦,我們學(xué)習(xí)了如何在自定義模塊中聲明或繼承業(yè)務(wù)模型狂魔。該章中的各小節(jié)涵蓋了為計(jì)算字段編寫(xiě)方法,以及編寫(xiě)約束字段值的方法凌彬。本章中將集中講解基本服務(wù)的服務(wù)端開(kāi)發(fā)沸柔,有Odoo方法定義、數(shù)據(jù)集操縱及擴(kuò)展已繼承方法铲敛。
技術(shù)準(zhǔn)備
本章的技術(shù)準(zhǔn)備包括Odoo在線平臺(tái)褐澎。
本章中所使用的代碼可以在如下GitHub倉(cāng)庫(kù)中下載:https://github.com/alanhou/odoo12-cookbook。
觀看視頻來(lái)查看實(shí)時(shí)代碼操作:https://dwz.cn/dBKge1V5
定義模型方法及使用API裝飾器
定義自定義模型的模型類(lèi)聲明由模型處理的數(shù)據(jù)的字段伐蒋。它們也可以通過(guò)在模型類(lèi)上定義方法來(lái)定義自定義行為工三。
本節(jié)中迁酸,我們將來(lái)看如何編寫(xiě)可由用戶界面中按鈕或應(yīng)用其它代碼塊調(diào)用的方法。這一方法會(huì)作用于圖書(shū)并執(zhí)行所需的動(dòng)作來(lái)修改所選圖書(shū)的狀態(tài)俭正。
準(zhǔn)備工作
本節(jié)假定你已有準(zhǔn)備好了實(shí)例奸鬓,包含第四章 創(chuàng)建Odoo插件模塊中所描述的my_library插件模塊。你會(huì)需要在LibraryBook模型中有一個(gè)state字段掸读,定義如下:
from odoo import models, fields, api
class LibraryBook(models.Model):
# [...]
state = fields.Selection([
('draft', 'Unavailable'),
('available', 'Available'),
('borrowed', 'Borrowed'),
('lost', 'Lost')],
'State', default="draft")
參見(jiàn)第四章 創(chuàng)建Odoo插件模塊中添加模型一節(jié)獲取更詳細(xì)信息串远。
如何操作...
要定義方法來(lái)修改所選圖書(shū)的狀態(tài),你需要在模型定義中添加如下代碼:
- 添加一個(gè)幫助方法來(lái)查看是否允許狀態(tài)轉(zhuǎn)換:
@api.model
def is_allowed_transition(self, old_state, new_state):
allowed = [('draft', 'available'),
('available', 'borrowed'),
('borrowed', 'available'),
('available', 'lost'),
('borrowed', 'lost'),
('lost', 'available')]
return (old_state, new_state) in allowed
- 添加方法來(lái)修改一些書(shū)籍的狀態(tài)為所傳參數(shù)的新?tīng)顟B(tài):
@api.multi
def change_state(self, new_state):
for book in self:
if book.is_allowed_transition(book.state, new_state):
book.state = new_state
else:
continue
- 添加方法來(lái)通過(guò)調(diào)用change_state方法修改圖書(shū)狀態(tài):
def make_available(self):
self.change_state('available')
def make_borrowed(self):
self.change_state('borrowed')
def make_lost(self):
self.change_state('lost')
- 在<form>視圖中添加按鈕儿惫。這會(huì)幫助我們從用戶界面中觸發(fā)這些方法:
<form>
...
<button name="make_available" string="Make Available"
type="object"/>
<button name="make_borrowed" string="Make Borrowed"
type="object"/>
<button name="make_lost" string="Make Lost"
type="object"/>
...
</form>
更新或安裝該模塊來(lái)讓這些修改生效澡罚。
運(yùn)行原理...
本節(jié)中的代碼定義了一些方法。它們是常規(guī)的Python方法肾请,帶有self來(lái)作為其第一個(gè)參數(shù)留搔,并且還帶有其它參數(shù)。這些方法通過(guò)odoo.api 模塊裝飾器進(jìn)行裝飾铛铁。
??這些中很多裝飾器是在Odoo 9.0中引入的隔显,用于確保將使用老的或傳統(tǒng)的API的調(diào)用轉(zhuǎn)化為新的 API 調(diào)用。在Odoo 10.0中饵逐,不再支持老的 API 了荣月,但這些裝飾器是新 API 中的核心部分。
在編寫(xiě)一個(gè)新方法時(shí)梳毙,,你通常會(huì)使用@api.multi捐下。這一裝飾器表示該方法用于在記錄集上執(zhí)行账锹。在此類(lèi)方法中,self是可以引用指定數(shù)量數(shù)據(jù)庫(kù)記錄的記錄集(包括空記錄集)坷襟,代碼經(jīng)常會(huì)遍歷self中的記錄來(lái)對(duì)每個(gè)記錄進(jìn)行操作奸柬。
@api.model裝飾器也類(lèi)似,但它用于僅僅是模型很重要而非方法所不操作的記錄集中的內(nèi)容的方法婴程。它的概念類(lèi)似于Python的@classmethod裝飾器廓奕。
在第1步中,我們創(chuàng)建了is_allowed_transition() 方法档叔。這個(gè)方法的目的是驗(yàn)證從一個(gè)狀態(tài)到另一個(gè)狀態(tài)的轉(zhuǎn)換是否有效桌粉。在可允許列表中的元組即為可使用轉(zhuǎn)換。例如衙四,我們不想要允許lost到borrow的轉(zhuǎn)換铃肯,因此沒(méi)有加入('lost, 'borrowed')。
第2步中传蹈,我們創(chuàng)建了 change_state()方法押逼。這個(gè)方法的用途是修改圖書(shū)的狀態(tài)步藕。調(diào)用該方法時(shí),它將圖書(shū)的狀態(tài)修改為給定new_state參數(shù)的狀態(tài)挑格。僅在允許進(jìn)行轉(zhuǎn)換時(shí)它都會(huì)修改圖書(shū)狀態(tài)咙冗。這里使用了循環(huán)是因?yàn)槲覀兪褂昧丝梢蕴幚矶鄠€(gè)記錄集的@api.multi裝飾器。
在第3步中漂彤,我們創(chuàng)建了一些通過(guò)調(diào)用change_state()方法來(lái)修改圖書(shū)狀態(tài)的方法雾消。本例中,方法會(huì)由添加到用戶界面中的按鈕所觸發(fā)显歧。
第4步中仪或,我們添加了<form>視圖。點(diǎn)擊這些按鈕時(shí)士骤,Odoo客戶端會(huì)觸發(fā)name屬性中所包含的Python函數(shù)范删。參見(jiàn)第十章 后端視圖中向表單添加按鈕一節(jié)來(lái)學(xué)習(xí)如何從用戶界面中調(diào)用此類(lèi)方法。
在用戶從用戶界面中點(diǎn)擊按鈕時(shí)拷肌,會(huì)調(diào)用第3步中所定義的方法到旦。self是一個(gè)包含library.book模型記錄的記錄集(可能為空)。然后巨缘,我們調(diào)用了 change_state()方法并根據(jù)所點(diǎn)擊按鈕傳遞相應(yīng)的參數(shù)添忘。
在調(diào)用change_state() 時(shí),self是library.book模型中的相同數(shù)據(jù)集若锁。change_state()方法中的內(nèi)容體對(duì)self進(jìn)行循環(huán)來(lái)處理記錄集中的每一本書(shū)搁骑。循環(huán)self一開(kāi)始看上去很奇怪,這但快你就會(huì)習(xí)慣這種模式又固。
在循環(huán)中仲器,change_state() 調(diào)用is_allowed_transition()。調(diào)用通過(guò)本地變量book進(jìn)行仰冠,但也可對(duì)library.book模型中的任意記錄集進(jìn)行調(diào)用乏冀,例如self,因?yàn)閕s_allowed_transition()由@api.model所裝飾洋只。如果允許轉(zhuǎn)換辆沦,change_state()通過(guò)對(duì)記錄的屬性分配值來(lái)為書(shū)籍分配一個(gè)新的狀態(tài)。這僅在記錄集長(zhǎng)度為1時(shí)有效识虚,用于確保和遍歷self的情況一致肢扯。
擴(kuò)展知識(shí)...
在閱讀源代碼時(shí)你可能會(huì)碰到@api.one裝飾器。該裝飾器因其初看起會(huì)引起混淆而被淘汰担锤。同時(shí)鹃彻,如果你知道@api.multi的話,可能會(huì)想這個(gè)裝飾器僅在記錄集大小為1時(shí)允許調(diào)用該方法妻献,但并非這樣蛛株。在記錄集大小方面团赁,@api.one和@api.multi是相似的,但它是在方法外對(duì)記錄集做for循環(huán)谨履,并在列表中對(duì)循環(huán)的每個(gè)遍歷返回值進(jìn)行累加欢摄,然后返回給調(diào)用者。
向用戶報(bào)出錯(cuò)誤
在方法執(zhí)行期間笋粟,因?yàn)橛脩粽?qǐng)求的該動(dòng)作無(wú)效或碰到錯(cuò)誤條件有時(shí)需要停止進(jìn)程怀挠。本節(jié)向你展示了如何通過(guò)顯示有用的錯(cuò)誤信息來(lái)管理這些用例。
準(zhǔn)備工作
本節(jié)假定你已經(jīng)準(zhǔn)備好了一個(gè)實(shí)例害捕,并包含有前面小節(jié)中所描述的my_library插件模塊绿淋。
如何操作...
我們會(huì)對(duì)前面小節(jié)中的change_state方法作出修改,并對(duì)用戶嘗試修改is_allowed_transition所不允許的狀態(tài)時(shí)顯示幫助信息尝盼。按照如下步驟來(lái)進(jìn)行操作:
- 在該P(yáng)ython文件的開(kāi)頭添加如下導(dǎo)入語(yǔ)句:
from odoo.exceptions import UserError
from odoo.tools.translate import _
- 修改change_state方法并拋出else部分中的UserError異常:
@api.multi
def change_state(self, new_state):
for book in self:
if book.is_allowed_transition(book.state, new_state):
book.state = new_state
else:
msg = _('Moving from %s to %s is not allowed') % (book.state, new_state)
raise UserError(msg)
運(yùn)行原理...
在 Python 中拋出異常時(shí)吞滞,它會(huì)延著調(diào)用棧進(jìn)行傳遞直接被處理。在Odoo中盾沫,響應(yīng)網(wǎng)頁(yè)客戶端發(fā)出調(diào)用的RPC層會(huì)捕獲所有異常裁赠,根據(jù)異常類(lèi)的不同來(lái)觸發(fā)網(wǎng)頁(yè)客戶端上的不同行為。
odoo.exceptions中所未定義的異常由通過(guò)棧軌跡內(nèi)部服務(wù)器錯(cuò)誤(HTTP狀態(tài)碼500)來(lái)處理赴精。UserError 會(huì)在用戶界面中顯示錯(cuò)誤信息佩捞。本節(jié)中拋出UserError錯(cuò)誤的代碼用于確保錯(cuò)誤消息以用戶友好的方式顯示。在所有的情況中蕾哟,當(dāng)前數(shù)據(jù)庫(kù)事務(wù)會(huì)被回滾一忱。
我使用了一個(gè)名稱(chēng)很奇怪的函數(shù),_()谭确,它由odoo.tools.translate定義帘营。這個(gè)函數(shù)用于標(biāo)記字符串為可翻譯,并在運(yùn)行時(shí)根據(jù)在執(zhí)行上下文中查找到的終端用戶語(yǔ)言獲取翻譯字符串琼富。更多詳情請(qǐng)參見(jiàn)第十二章 國(guó)際化。
小貼士:在使用 ()時(shí)庄新,確保你僅傳遞帶有插值占位符的字符串鞠眉,而非整個(gè)插值字符串冯袍。比如齐唆,('Warning: could not find %s') % value是正確的梗醇,_('Warning: could not find %s' % value) 就是錯(cuò)誤的查乒,因?yàn)榈谝粋€(gè)字符串不會(huì)在翻譯數(shù)據(jù)庫(kù)中找到替換值芍耘。
擴(kuò)展知識(shí)...
有時(shí)熔萧,你會(huì)處理有錯(cuò)誤傾向的代碼婆排,表示你所執(zhí)行的該操作會(huì)生成錯(cuò)誤玻佩。Odoo會(huì)捕獲這一錯(cuò)誤并對(duì)用戶顯示追蹤軌跡唯咬。如果你不想要向用戶顯示完整錯(cuò)誤日志,可以緩存錯(cuò)誤并拋出一個(gè)更具含義的自定義異常胆胰。在給出的示例中狞贱,我們?cè)趖ry...cache代碼塊中生成了UserError錯(cuò)誤來(lái)代替顯示完整錯(cuò)誤日志。現(xiàn)在Odoo會(huì)顯示有明確含義的警告:
def post_to_webservice(self, data):
try:
req = requests.post('http://my-test-service.com', data=data, timeout=10)
content = req.json()
except IOError:
error_msg = _("Something went wrong during data submission")
raise UserError(error_msg)
return content
在odoo.exceptions中定義了更多的異常類(lèi)蜀涨,都從基本的原有except_orm異常類(lèi)進(jìn)行派生瞎嬉。它們大多數(shù)僅在內(nèi)部使用,除以下幾種:
- Warning:在Odoo 8.0中厚柳,odoo.exceptions.Warning起著和9.0及之后版本中UserError相同的作用⊙踉妫現(xiàn)在它被淘汰的原因是名稱(chēng)帶有欺騙性(它是一個(gè)錯(cuò)誤,而非警告)别垮,并且它與Python內(nèi)置的Warning類(lèi)有沖突便监。它僅保持著向后兼容,你應(yīng)當(dāng)在代碼中使用UserError宰闰。
- ValidationError:這個(gè)異常在沒(méi)有滿足Python對(duì)字段的約束時(shí)拋出茬贵。參見(jiàn)第五章 應(yīng)用模型中向模型添加約束驗(yàn)證一節(jié)獲取更多信息。
為不同模型獲取一個(gè)空記錄集
在編寫(xiě)Odoo代碼時(shí)移袍,當(dāng)前模型的方法可通過(guò)self訪問(wèn)解藻。如果你需要操作不同模型,不能直接實(shí)例化該模型的類(lèi)葡盗,你需要獲取該模型的一個(gè)數(shù)據(jù)集在進(jìn)行操作螟左。
本節(jié)向你展示如何在Odoo的模型方法中注冊(cè)任意模型在獲取空記錄集胶背。
準(zhǔn)備工作
本節(jié)將復(fù)用my_library插件模塊中所設(shè)置的圖書(shū)示例窘拯。
我們將在library.book模型中編寫(xiě)一個(gè)小方法并搜索所有的圖書(shū)會(huì)員暇番。這時(shí)需要獲取library.members的空記錄集。
如何操作...
需要按照如下步驟來(lái)獲取library.book方法中獲取library.members的記錄集:
- 在LibraryBook類(lèi)中岳服,編寫(xiě)一個(gè)名為get_all_library_members的方法:
class LibraryBook(models.Model):
# ...
@api.model
def get_all_library_members(self):
# ...
- 在該方法體中,使用如下代碼:
library_member_model = self.env['library.member']
return library_member_model.search([])
運(yùn)行原理...
在啟動(dòng)時(shí)贫母,Odoo加載了所有的模塊并合并從Model中所獲取的不同類(lèi)因块,同時(shí)也定義或繼承了給定的模型趾断。這些類(lèi)存儲(chǔ)在Odoo倉(cāng)庫(kù)中,以名稱(chēng)作為索引脐帝。任意記錄集中的env屬性星澳,可通過(guò)self.env訪問(wèn)腿堤,是定義在odoo.api模型中的Environment類(lèi)的實(shí)例装处。
這個(gè)類(lèi)在Odoo開(kāi)發(fā)中扮演著中心角色:
- 它通過(guò)模擬Python字典來(lái)提供對(duì)倉(cāng)庫(kù)的快速訪問(wèn)寝蹈。如果你知道所要查找的模型名,self.env[model_name]即要獲取該模型的空記錄集。再者牲蜀,該記錄集會(huì)共享self的環(huán)境。
- 它有一個(gè)cr屬性度苔,是你可以用于傳遞原生SQL查詢(xún)的數(shù)據(jù)庫(kù)游標(biāo)。參見(jiàn)第九章 高級(jí)服務(wù)端開(kāi)發(fā)技巧中的執(zhí)行原生SQL查詢(xún)一節(jié)獲取更多信息。
- 它有一個(gè)user屬性横漏,是對(duì)執(zhí)行調(diào)用當(dāng)前用戶的調(diào)用。參見(jiàn)第九章 高級(jí)服務(wù)端開(kāi)發(fā)技巧中的更改執(zhí)行動(dòng)作的用戶一節(jié)獲取更多信息。
- 它有一個(gè)context屬性,是一個(gè)包含調(diào)用的上下文的字典踩验。這包含用戶語(yǔ)言袭异、時(shí)區(qū)碴里、當(dāng)前記錄選項(xiàng)等等的信息谷羞。參見(jiàn)第九章 高級(jí)服務(wù)端開(kāi)發(fā)技巧中的使用變更的上下文調(diào)用方法一節(jié)獲取更多信息犀填。
search()的調(diào)用在稍后的搜索記錄一節(jié)中進(jìn)行講解。
其它內(nèi)容
第九章 高級(jí)服務(wù)端開(kāi)發(fā)技巧中的更改執(zhí)行動(dòng)作的用戶和使用變更的上下文調(diào)用方法小節(jié)中講解有關(guān)運(yùn)行時(shí)修改self.env的知識(shí)。
新建記錄
在編寫(xiě)業(yè)務(wù)邏輯方法時(shí)一個(gè)普遍的要求是新建記錄。本節(jié)講解如何創(chuàng)建library.book.category中的記錄。在我們的示例中溯饵,將會(huì)為library.book.category模型添加一個(gè)創(chuàng)建dummy分類(lèi)的方法增拥。要觸發(fā)這一方法渣玲,需要添加一個(gè)表單視圖中的按鈕逗概。
準(zhǔn)備工作
你需要知道想要新建目錄的模型結(jié)構(gòu),尤其是它們的名稱(chēng)和類(lèi)型忘衍,以及各字段中所存在的約束(如有些字段為必填)逾苫。本節(jié)中,我們將復(fù)用第五章 應(yīng)用模型中的my_library模塊枚钓。我們看一下以下示例來(lái)快速回顧library.book.category模型:
class BookCategory(models.Model):
_name = 'library.book.category'
name = fields.Char('Category')
description = fields.Text('Description')
parent_id = fields.Many2one(
'library.book.category',
string='Parent Category',
ondelete='restrict',
index=True
)
child_ids = fields.One2many(
'library.book.category', 'parent_id',
string='Child Categories')
如何操作...
你需要執(zhí)行如下步驟來(lái)創(chuàng)建帶有一些子分類(lèi)的分類(lèi):
- 在library.book.category中創(chuàng)建一個(gè)名為create_categories的方法:
def create_categories(self):
......
- 在這一方法體內(nèi),準(zhǔn)備一個(gè)包含第一個(gè)子類(lèi)各字段值的字典:
categ1 = {
'name': 'Child category 1',
'description': 'Description for child 1'
}
- 準(zhǔn)備包含第二個(gè)子類(lèi)各字段值的字典:
categ2 = {
'name': 'Child category 2',
'description': 'Description for child 2'
}
- 準(zhǔn)備包含父類(lèi)各字段值的字典:
parent_category_val = {
'name': 'Parent category',
'email': 'Description for parent category',
'child_ids': [
(0, 0, categ1),
(0, 0, categ2),
]
}
- 調(diào)用create()方法來(lái)新建記錄:
record = self.env['library.book.category'].create(parent_category_val)
- 在<form>視圖中添加用戶界面中觸發(fā)create_categories方法的按鈕:
<button name="create_categories" string="Create Categories" type="object"/>
運(yùn)行原理...
為模型新建記錄饭于,我們可以對(duì)任意與模型關(guān)聯(lián)的記錄集調(diào)用create(values)方法。該方法返回一個(gè)長(zhǎng)度為1的新記錄集昭卓,其中包含帶有值字典中所指定字段值的這條新記錄镜硕。
在字典中,各個(gè)鍵給定字段的名稱(chēng)匣摘,相對(duì)的值與字段值對(duì)應(yīng)。根據(jù)字段類(lèi)型的不同彤灶,你需要為值傳遞不同的Python類(lèi)型:
- Text字段值給定的類(lèi)型為Python字符串。
- Float和Integer字段值使用Python的浮點(diǎn)型和整型。
- Boolean字段值最好使用Python布爾類(lèi)型或整型。
- Date字段值使用Python的datetime.date 對(duì)象。
- Datetime字段值使用Python的datetime.datetime對(duì)象麦轰。
- Binary字段值以Base64編碼字符串進(jìn)行傳遞眶熬。Python標(biāo)準(zhǔn)庫(kù)中的base64模塊提供諸如encodebytes(bytestring)的方法來(lái)對(duì)字符串以Base64進(jìn)行編碼。
- Many2one字段值給定的為整型讯沈,應(yīng)為關(guān)聯(lián)記錄的數(shù)據(jù)庫(kù)ID。
- One2many和Many2many字段使用一個(gè)特殊語(yǔ)法崖飘。值為包含三個(gè)元素元組的一個(gè)列表榴捡,如下:
[table id=13 /]
在本節(jié)中,我們?cè)谒獎(jiǎng)?chuàng)建的公司中新建了兩個(gè)聯(lián)系人的字典朱浴,然后我們使用通過(guò)前述(0, 0, dict_val)語(yǔ)法所創(chuàng)建公司字典child_ids鍵中的這些字典吊圾。
在第5步中調(diào)用create()時(shí),創(chuàng)建了三條記錄:
- 一條是父級(jí)圖書(shū)分類(lèi)翰蠢,由create返回
- 兩條為子級(jí)圖書(shū)分類(lèi)可通過(guò)record.child_ids進(jìn)行獲取
擴(kuò)展知識(shí)...
如果該模型為某些字段定義了一些默認(rèn)值项乒,不需要做什么特別的事情,create()會(huì)處理所提供字典中不存在字段默認(rèn)值的計(jì)算梁沧。
從Odoo 12開(kāi)始檀何,create()方法還支持批量創(chuàng)建記錄集。要批量創(chuàng)建多條記錄趁尼,你需要向create() 方法傳遞一個(gè)多值列表埃碱,如下例所示:
categ1 = {
'name': 'Category 1',
'description': 'Description for Category 1'
}
categ2 = {
'name': 'Category 2',
'description': 'Description for Category 2'
}
multiple_records = self.env['library.book.category'].create([categ1, categ2])
更新記錄集中記錄值
業(yè)務(wù)邏輯經(jīng)常要求我們通過(guò)修改其中的一些值來(lái)更新記錄猖辫。本節(jié)將展示如何partner中的date字段酥泞。
準(zhǔn)備工作
本節(jié)將使用新建記錄一節(jié)中相同的簡(jiǎn)化library.book定義。你可以參照這一簡(jiǎn)化定義來(lái)找到這些字段啃憎。
我們?cè)趌ibrary.book模型中有date_updated字段芝囤。為進(jìn)行演示,我們通過(guò)點(diǎn)擊按鈕在這一字段上進(jìn)行寫(xiě)入辛萍。
如何操作...
要更新圖書(shū)的date_updated字段悯姊,可以編寫(xiě)一個(gè)名為change_update_date()的新方法,定義如下:
@api.multi
def change_update_date(self):
self.ensure_one()
self.date_updated = fields.Datetime.now()
然后我們以xml在圖書(shū)<form>視圖中添加一個(gè)按鈕贩毕,如下:
<form>
<button name="change_update_date" string="Update Date" type="object"/>
重啟服務(wù)并更新my_library模塊來(lái)查看變化悯许。在點(diǎn)擊Update Date按鈕時(shí),update_date會(huì)被修改辉阶。
</form>
運(yùn)行原理...
該方法通過(guò)調(diào)用ensure_one()檢查以self傳遞的圖書(shū)記錄是否是一條記錄先壕。如并非如此該方法會(huì)拋出一個(gè)異常,并停止處理谆甜。需要這么做是因?yàn)槲覀儾幌M薷亩鄺l記錄的日期垃僚。如果你想要更新多條值,可以刪除ensure_one()并使用對(duì)記錄集的循環(huán)來(lái)更新該屬性规辱。
最后谆棺,該方法修改圖書(shū)記錄的各屬性值。它以當(dāng)前時(shí)間更新date_updated字段罕袋。通過(guò)修改記錄集中的字段屬性改淑,你可以執(zhí)行寫(xiě)操作。
擴(kuò)展知識(shí)...
如果你想要向記錄字段寫(xiě)入新值有三個(gè)可選項(xiàng):
- 選項(xiàng)一在本節(jié)中進(jìn)行了講解侍郭。它通過(guò)直接向代表記錄字段的屬性分配值在所有上下文中均可使用询吴。也可以一次同時(shí)對(duì)所有記錄集分配值,這時(shí)你需要遍歷記錄集亮元,除非你非常確定僅處理單條記錄猛计。
- 選項(xiàng)二通過(guò)使用update()方法將字典映射字段傳遞給你想要設(shè)置的值爆捞。僅也同樣僅可作用于長(zhǎng)度為1的記錄集。在需要同時(shí)對(duì)同一記錄更新多個(gè)字段的值時(shí)可以節(jié)約一些代碼煮甥。本節(jié)中第二步使用此選項(xiàng)重寫(xiě)如下:
@api.multi
def change_update_date(self):
self.ensure_one()
self.update({
'date_updated': fields.Datetime.now(),
'another_field': 'value'
...
})
- 選項(xiàng)三為調(diào)用write()方法卖局,向你所希望設(shè)置的值傳遞一個(gè)映射字段名的字典。該方法在前面兩個(gè)選項(xiàng)對(duì)執(zhí)行單記錄單字段的單數(shù)據(jù)庫(kù)調(diào)用的情況下作用于指定大小的記錄集并會(huì)在單條數(shù)據(jù)庫(kù)運(yùn)行中以指定值更新所有記錄双霍。但是它也有一些限制:
- 如果記錄尚不存在于數(shù)據(jù)庫(kù)中則無(wú)法使用它砚偶。(更多相關(guān)知識(shí)參見(jiàn)第九章 高級(jí)服務(wù)端開(kāi)發(fā)技巧中的定義onchange方法一節(jié))
- 在寫(xiě)入關(guān)聯(lián)字段時(shí)它要求有特殊的格式,類(lèi)似于create() 方法所使用的格式洒闸。查看下表中用于為關(guān)聯(lián)字段生成不同值的格式:
[table id=14 /]
??在寫(xiě)本書(shū)時(shí)染坯,官方文檔更新并不及時(shí),其中提到了運(yùn)算3, 4, 5和6在One2many字段中不可用丘逸,但早非如此了单鹿。但根據(jù)模型的約束不同,其中某些可能會(huì)無(wú)法用于One2many字段深纲。例如仲锄,如果要求有反向Many2one關(guān)聯(lián),那么運(yùn)算3將會(huì)失敗囤萤,因?yàn)樗鼤?huì)產(chǎn)生一個(gè)未設(shè)置的Many2one關(guān)聯(lián)昼窗。
搜索記錄
對(duì)記錄的搜索也是業(yè)務(wù)邏輯方法中常見(jiàn)的操作。本節(jié)將展示如何通過(guò)書(shū)名和分類(lèi)查找圖書(shū)涛舍。
準(zhǔn)備工作
本節(jié)將使用與新建記錄一節(jié)中相同的library.book定義澄惊。我們?cè)诿麨閒ind_book(self)的方法中編寫(xiě)代碼。
如何操作...
你需要執(zhí)行如下步驟來(lái)查看書(shū)籍:
- 在library.book模型中添加find_book方法:
def find_book(self):
...
- 為你的條件編寫(xiě)搜索域:
domain = [
'|',
'&', ('name', 'ilike', 'Book Name'),
('category_id.name', 'ilike', 'Category Name'),
'&', ('name', 'ilike', 'Book Name 2'),
('category_id.name', 'ilike', 'Category Name 2')
]
- 通過(guò)域調(diào)用search()方法,它會(huì)返回記錄集:
books = self.search(domain)
運(yùn)行原理...
第1步中定義了該方法掸驱。
第2步中在本地變量中創(chuàng)建了一個(gè)搜索域肛搬。通常你會(huì)在對(duì)搜索調(diào)用的行內(nèi)看到這一創(chuàng)建,但對(duì)于復(fù)雜的作用域毕贼,最好是分開(kāi)進(jìn)行定義温赔。
關(guān)于搜索域語(yǔ)法的詳細(xì)講解,參見(jiàn)第十章 后端視圖中的在記錄列表上定義過(guò)濾器 - 域一節(jié)鬼癣。
第3步使用該域調(diào)用了search()方法陶贼。該方法返回包含所有匹配這個(gè)作用域的記錄的一個(gè)記錄集,還可以對(duì)它進(jìn)行進(jìn)一步處理待秃。本節(jié)中拜秧,我們僅通過(guò)該域調(diào)用此方法,但同時(shí)也支持如下關(guān)鍵詞參數(shù):
- offset=N:它用于跳過(guò)前 N 條記錄來(lái)匹配查詢(xún)章郁⊥鞯可與limit一起使用來(lái)實(shí)現(xiàn)分頁(yè)或減少在處理大量記錄時(shí)的內(nèi)存消耗。其默認(rèn)值為0.
- limit=N:這表示最多返回 N 條記錄暖庄。默認(rèn)不設(shè)置上限聊替。
- order=sort_specification:它用于強(qiáng)制所返回記錄集的排序。默認(rèn)培廓,排序通過(guò)模型類(lèi)的_order屬性給定惹悄。
- count=boolean:若為T(mén)rue,它返回記錄數(shù)而非實(shí)際記錄集医舆。默認(rèn)值為False俘侠。
小貼士:我們推薦使用search_count(domain)方法象缀,而非search(domain, count=True)蔬将,因?yàn)樗姆椒愿逦胤绞絺鬟f了其用途,兩者返回的值相同央星。
有時(shí)霞怀,你需要搜索另一個(gè)模型來(lái)讓對(duì)self的搜索返回當(dāng)前模型的記錄集。從另一個(gè)模型中進(jìn)行搜索莉给,我們需要獲取該模型的空記錄集毙石。例如,假設(shè)我們想要搜索res.partner模型颓遏,因?yàn)槲覀兿M阉鱮es.partner記錄中的聯(lián)系人徐矩。那么,我們需要對(duì)res.partner模型的空記錄集進(jìn)行搜索叁幢。參見(jiàn)如下代碼:
@api.multi
def find_partner(self):
PartnerObj = self.env['res.partner']
domain = [
'&', ('name', 'ilike', 'Parth Gajjar'),
('company_id.name', '=', 'Odoo')
]
partner = PartnerObj.search(domain)
擴(kuò)展知識(shí)...
前面我們說(shuō)到search()方法返回匹配搜索域的所有記錄滤灯。但事實(shí)并非完全如此。該方法會(huì)確保僅返回執(zhí)行搜索的用戶擁有訪問(wèn)權(quán)限的記錄。此外鳞骤,如果模型中有名為active的布爾字段窒百,而搜索域中并未指定該字段的搜索條件,那么會(huì)隱式地添加一個(gè)active=True條件來(lái)僅返回這部分值豫尽。因此篙梢,如果你想要搜索返回內(nèi)容而僅返回空記錄集時(shí),確保檢查active的值(如果存在)來(lái)檢查記錄規(guī)則美旧。
參見(jiàn)第九章 高級(jí)服務(wù)端開(kāi)發(fā)技巧中的使用變更的上下文調(diào)用方法一節(jié)渤滞,來(lái)了解如何不隱式的添加active = True條件。參見(jiàn)第十一章 權(quán)限安全中的使用記錄規(guī)則限制記錄訪問(wèn)一節(jié)有獲取有關(guān)記錄級(jí)別權(quán)限規(guī)則的知識(shí)榴嗅。
如果出于一些原因蔼水,你要使用原生SQL查詢(xún)來(lái)查找記錄ID,確保獲取ID 后使用self.env['record.model'].search([('id', 'in', tuple(ids))]).ids來(lái)應(yīng)用權(quán)限規(guī)則录肯。這對(duì)于需對(duì)公司采取區(qū)別對(duì)待的記錄規(guī)則的多公司(租戶)Odoo實(shí)例尤為重要趴腋。
合并記錄集
有時(shí),你會(huì)發(fā)現(xiàn)所獲取的記錄集并非你真正所需的论咏。本節(jié)展示合并它們的不同方式优炬。
準(zhǔn)備工作
本節(jié)要求你在同一個(gè)模型中有兩個(gè)或多個(gè)記錄集。
如何操作...
按照如下步驟來(lái)對(duì)記錄集執(zhí)行通過(guò)操作:
- 將兩個(gè)記錄集合并為一個(gè)并保留排序厅贪,使用如下操作:
result = recordset1 + recordset2
- 使用如下操作合并兩個(gè)記錄集并確保結(jié)果中沒(méi)有重復(fù)內(nèi)容:
result = recordset1 | recordset2
- 使用如下操作來(lái)查找兩個(gè)記錄集中共同的記錄:
result = recordset1 & recordset2
運(yùn)行原理...
針對(duì)記錄集的類(lèi)實(shí)現(xiàn)了不同的Python運(yùn)算符的重定義蠢护,在此處進(jìn)行了使用。以下為可用于記錄集的最有用的Python運(yùn)算符的總結(jié)表:
[table id=16 /]
還有些賦值運(yùn)算符+=, -=, &=和 |=养涮,它們會(huì)修改左側(cè)的運(yùn)算項(xiàng)而不會(huì)新建記錄集葵硕。在更新記錄的One2many或Many2many字段時(shí)這些會(huì)非常的有用。參見(jiàn)更新記錄集中記錄值一節(jié)有查看此類(lèi)示例贯吓。
過(guò)濾記錄集
在某些情況下懈凹,你已有一個(gè)記錄集,僅需對(duì)其中的某些記錄進(jìn)行操作悄谐。當(dāng)然你可以遍歷記錄集并對(duì)每條遍歷進(jìn)行條件判斷并根據(jù)所查看的結(jié)果執(zhí)行操作介评,構(gòu)造一個(gè)僅包含需操作的記錄的新記錄集并對(duì)該記錄集調(diào)用同一操作會(huì)更容易,在某些情況下還會(huì)更高效爬舰。
本節(jié)向你展示如何使用 filter()方法來(lái)從另一個(gè)記錄集中提取記錄集们陆。
準(zhǔn)備工作
我們將復(fù)用新建記錄一節(jié)中所展示的簡(jiǎn)化的library.book 模型。本節(jié)定義一個(gè)從給定記錄集中提取含有多名作者的圖書(shū)的方法情屹。
如何操作...
執(zhí)行如下步驟來(lái)從一個(gè)記錄集中提取包含多名作者的記錄:
- 定義接收原始記錄集的方法:
@api.model
def books_with_multiple_authors(self, all_books):
- 定義內(nèi)部的predicate函數(shù):
def predicate(book):
if len(book.author_ids) > 1:
return True
return False
- 調(diào)用filter(),如下:
return all_books.filter(predicate)
運(yùn)行原理...
filter()方法的實(shí)現(xiàn)創(chuàng)建了一個(gè)空記錄集坪仇,其中添加predicate函數(shù)運(yùn)行結(jié)果為T(mén)rue的所有記錄。最終返回一個(gè)新記錄集垃你。保留原記錄集中記錄的排序椅文。
前面部分使用了一個(gè)內(nèi)部命名函數(shù)颈墅。對(duì)這種簡(jiǎn)單場(chǎng)景你會(huì)經(jīng)常發(fā)現(xiàn)匿名函數(shù) Lambda 的使用:
@api.model
def books_with_multiple_authors(self, all_books):
return all_books.filter(lambda b: len(b.author_ids) > 1)
事實(shí)上,你需要基于 Python 層面為真的字段值(非空字符串雾袱,非零數(shù)字恤筛、非空容器等)進(jìn)行記錄集的過(guò)濾。因此如果你希望過(guò)濾出帶有某分類(lèi)集合的記錄芹橡,可以傳遞字段名來(lái)進(jìn)行類(lèi)似如下過(guò)濾:all_books.filter('category_id')毒坛。
擴(kuò)展知識(shí)...
記住filter()是在內(nèi)存中進(jìn)行運(yùn)算。如果你嘗試對(duì)關(guān)鍵路徑上的方法進(jìn)行性能優(yōu)化林说,可能會(huì)要使用搜索域或者甚至是轉(zhuǎn)向SQL煎殷,代價(jià)是損失代碼易讀性。
遍歷記錄集關(guān)聯(lián)
在操作長(zhǎng)度為1的記錄集時(shí)腿箩,有很多字段可用作記錄屬性豪直。帶有記錄值作為值的關(guān)聯(lián)屬性(One2many, Many2one和Many2many)也同樣可以使用。作為一個(gè)示例珠移,我們假定需要訪問(wèn)library.book模型記錄集中的分類(lèi)名稱(chēng)弓乙。你可以通過(guò)遍歷category_id many2one字段來(lái)訪問(wèn)分類(lèi)名,如下:book.category_id.name钧惧。但是暇韧,在操作帶有一條以上記錄的記錄集時(shí),則不能使用該屬性浓瞪。
本節(jié)向你展示如何使用mapped() 方法來(lái)遍歷記錄集關(guān)聯(lián)懈玻,我們會(huì)編寫(xiě)一個(gè)方法并傳遞圖書(shū)參數(shù)來(lái)獲取圖書(shū)記錄集中作者的名稱(chēng)。
準(zhǔn)備工作
我們將復(fù)用本章中新建記錄一節(jié)中使用的 library.book模型乾颁。
如何操作...
你需要執(zhí)行如下步驟來(lái)從圖書(shū)記錄集中獲取作者名稱(chēng):
- 定義名為get_author_names()的方法:
@api.model
def get_author_names(self, books):
- 調(diào)用mapped()來(lái)獲取成員聯(lián)系人的email地址:
return books.mapped('author_ids.name')
運(yùn)行原理...
第1步中僅是定義了該方法涂乌。第2步中,我們調(diào)用了mapped(path) 方法來(lái)遍歷該記錄集中的字段:path是包含以點(diǎn)號(hào)分隔字段名的字符串英岭。對(duì)于path中的每一個(gè)字段湾盒,mapped()生成了一個(gè)包含該字段所關(guān)聯(lián)當(dāng)前記錄集所有元素的所有記錄。然后將其應(yīng)用于新記錄集中path的下一個(gè)元素巴席。如果path中的最后一個(gè)字段是關(guān)聯(lián)字段历涝,mapped()會(huì)返回一個(gè)記錄集诅需,否則漾唉,返回一個(gè)Python列表。
mapped()方法有兩個(gè)有用的屬性:
- 如果path是一個(gè)標(biāo)量字段名堰塌,那么返回的列表與所處理記錄集使用相同的排序赵刑。
- 如果path包含一個(gè)關(guān)聯(lián)字段,那么不保留排序场刑,復(fù)制內(nèi)容會(huì)從結(jié)果中進(jìn)行刪除般此。
??第二個(gè)屬性在以@api.multi,修飾的方法中非常有用蚪战,你可以對(duì)self中所有記錄的所有由Many2many字段指向的記錄執(zhí)行操作,但需要確保操作僅會(huì)執(zhí)行一次(哪怕是self中的兩條記錄共享同一個(gè)目錄記錄)铐懊。
擴(kuò)展知識(shí)...
在使用mapped()時(shí)邀桑,要記住它在Odoo服務(wù)的內(nèi)存中進(jìn)行操作,該服務(wù)返利地遍歷關(guān)聯(lián)并因此產(chǎn)生很多SQL查詢(xún)科乎,這可能會(huì)影響效率壁畸。但是這種代碼很直白且具備表達(dá)性。如果你在嘗試優(yōu)化實(shí)例關(guān)鍵路徑上的方法提高性能的話茅茂,可能會(huì)要重寫(xiě)調(diào)用為mapped()并以相應(yīng)的域來(lái)以search()進(jìn)行表達(dá)捏萍,甚至是轉(zhuǎn)向SQL(代價(jià)是損失代碼易讀性)。
mapped()方法也可以通過(guò)函數(shù)作為參數(shù)來(lái)進(jìn)行調(diào)用空闲。這種情況下令杈,它返回包含應(yīng)用于self每條記錄的函數(shù)的結(jié)果列表,或者返回在函數(shù)返回的是記錄集的情況下由該函數(shù)返回的記錄集的并集碴倾。
其它內(nèi)容
- 本章中的搜索記錄一節(jié)
- 第九章 高級(jí)服務(wù)端開(kāi)發(fā)技巧中的執(zhí)行原生SQL查詢(xún)一節(jié)
記錄集排序
在通過(guò)search()方法獲取一個(gè)記錄集時(shí)逗噩,你可以傳遞一個(gè)可選參數(shù)order來(lái)以指定排序獲取記錄集。這對(duì)于在此前代碼中已獲取記錄集并想對(duì)其排序會(huì)非常有用跌榔。例如對(duì)使用集合運(yùn)算來(lái)合并兩個(gè)記錄集時(shí)(會(huì)丟失排序)可能也會(huì)很有用给赞。
本節(jié)向你展示如何使用sorted()方法來(lái)對(duì)已有記錄集進(jìn)行排序。我們會(huì)對(duì)圖書(shū)進(jìn)行發(fā)生日期的排序矫户。
準(zhǔn)備工作
我們將復(fù)用本章中新建記錄一節(jié)中所展示的library.book模型片迅。
如何操作...
你需要執(zhí)行如下步驟來(lái)獲取基于release_date排序的圖書(shū)記錄集:
- 定義一個(gè)名為 sort_books_by_date()的方法:
@api.model
def sort_books_by_date(self, books):
- 調(diào)用sort()來(lái)獲取成員聯(lián)系人的郵箱地址:
return books.sorted(key='release_date')
運(yùn)行原理...
第1步只是對(duì)方法的定義。在第2步中皆辽,我們調(diào)用了圖書(shū)記錄集中的sorted() 方法柑蛇。sorted() 方法在內(nèi)部會(huì)獲取以參數(shù)key進(jìn)行傳遞的字段數(shù)據(jù)。然后驱闷,通過(guò)使用Python的原生sorted方法返回一個(gè)排序后記錄集耻台。
它還有一個(gè)可選參數(shù)reverse=True,以逆向排序返回記錄集空另。reverse的用法如下:
books.sorted(key='release_date', reverse=True)
擴(kuò)展知識(shí)...
sorted()方法會(huì)在記錄集中進(jìn)行排序盆耽。調(diào)用時(shí)若不傳入?yún)?shù),則會(huì)使用模型中的_order屬性扼菠。另外摄杂,可傳入一個(gè)函數(shù)來(lái)以Python內(nèi)置sorted (sequence, key)函數(shù)相同的方式計(jì)算一個(gè)比較鍵。
??在使用模型的默認(rèn)_order參數(shù)時(shí)循榆,排序由數(shù)據(jù)庫(kù)來(lái)代理析恢,執(zhí)行了一個(gè)新的SELECT函數(shù)來(lái)獲取排序。否則秧饮,排序由Odoo來(lái)執(zhí)行映挂。根據(jù)所操縱的對(duì)旬以及記錄集的大小的不同泽篮,可能會(huì)有很大的性能上的不同。
繼承模型中定義的業(yè)務(wù)邏輯
將應(yīng)用功能分成不同模塊是極其常見(jiàn)的實(shí)踐柑船。通過(guò)這樣帽撑,你可以通過(guò)安裝/制裁應(yīng)用來(lái)啟用/禁用功能。然后自定義一些由原模型定義的方法中的行為也很有必要鞍时。有時(shí)油狂,你需要向已有模型添加一個(gè)新字段。在Odoo中這是一個(gè)非常輕松的任務(wù)寸癌,也是框架下一個(gè)非常強(qiáng)大的功能专筷。
我們將通過(guò)繼承一他創(chuàng)建記錄的方法以添加所創(chuàng)建記錄中的新字段來(lái)進(jìn)行演示。
準(zhǔn)備工作
本節(jié)中蒸苇,我們將繼續(xù)使用上一節(jié)中的my_library模塊磷蛹。要確保my_library中存在library.book.category 模型。
這一節(jié)中我們將創(chuàng)建一個(gè)新的名為my_library_return的模塊溪烤,它依賴(lài)于my_library模塊味咳。在這一模塊中,我們將管理借閱圖書(shū)的返回日期檬嘀。我們還會(huì)自動(dòng)地基于分類(lèi)來(lái)計(jì)算返回日期槽驶。
在第五章 應(yīng)用模型中的使用繼承向模型添加功能一節(jié),我們學(xué)習(xí)了如何在已有模型中添加字段鸳兽。在這個(gè)模型中掂铐,繼承l(wèi)ibrary.book模型如下:
class LibraryBook(models.Model):
_inherit = 'library.book'
date_return = fields.Date('Date to return')
然后繼承l(wèi)ibrary.book.category模型如下:
class LibraryBookCategory(models.Model):
_inherit = 'library.book.category'
max_borrow_days = fields.Integer(
'Maximum borrow days',
help="For how many days book can be borrowed",
default=10)
需要按照第十章 后端視圖中的修改已有視圖 - 視圖繼承一節(jié)來(lái)在視圖中添加該字段。你可以在https://github.com/alanhou/odoo12-cookbook中查看代碼的完整示例揍异。
如何操作...
你需要執(zhí)行如下步驟來(lái)在library.book模型中繼承這一業(yè)務(wù)邏輯:
- 在my_module_return中全陨,我們希望在修改圖書(shū)狀態(tài)為Borrowed時(shí)在圖書(shū)記錄中設(shè)置date_return。為此衷掷,我們將重載my_module_return模塊中的make_borrowed方法:
def make_borrowed(self):
day_to_borrow = self.category_id.max_borrow_days or 10
self.date_return = fields.Date.today() + timedelta(days=day_to_borrow)
return super(LibraryBook, self).make_borrowed()
- 我們還希望在圖書(shū)歸還并可供借閱時(shí)重置date_return辱姨,因此我們將重載make_available方法來(lái)重置該日期:
def make_available(self):
self.date_return = False
return super(LibraryBook, self).make_available()
運(yùn)行原理...
第1步和第2步執(zhí)行對(duì)業(yè)務(wù)邏輯的繼承。我們定義了一個(gè)繼承l(wèi)ibrary.books的模型并且重定義了make_borrowed()和make_available()方法戚嗅。在這兩個(gè)方法的最后一行雨涛,返回由父類(lèi)實(shí)現(xiàn)的結(jié)果:
return super(LibraryBook, self).make_borrowed()
在Odoo模型的用例中,父類(lèi)和你在Python類(lèi)定義中所看到的并不太一樣懦胞√婢茫框架動(dòng)態(tài)為我們的記錄集生成一個(gè)類(lèi)等級(jí),父類(lèi)由我們所依賴(lài)的模型的模型定義医瘫。因此侣肄,調(diào)用super()回到了my_module模塊中的 library.book的實(shí)現(xiàn)。在這一實(shí)現(xiàn)中醇份,make_borrowed() 修改圖書(shū)的狀態(tài)為Borrowed稼锅。因此調(diào)用 super 會(huì)觸發(fā)父類(lèi)方法并且它會(huì)設(shè)置圖書(shū)的狀態(tài)為Borrowed。
擴(kuò)展知識(shí)...
在本節(jié)中僚纷,我們選擇了繼承方法的默認(rèn)實(shí)現(xiàn)矩距。在make_boorrow()和make_available()方法中,我們?cè)趕uper()的調(diào)用前修改了返回的結(jié)果怖竭。注意在調(diào)用super() 時(shí)锥债,它會(huì)執(zhí)行其默認(rèn)實(shí)現(xiàn)。也可能在super() 調(diào)用之后執(zhí)行一些動(dòng)作痊臭。當(dāng)然哮肚,也可以同時(shí)執(zhí)行兩者。
但是广匙,在方法的中間修改行為會(huì)更為困難允趟。這時(shí),我們需要重構(gòu)代碼來(lái)提取一個(gè)繼承點(diǎn)以分隔方法并重載繼承模塊中的這一新方法鸦致。
??你可能會(huì)萌生重寫(xiě)一個(gè)方法的念頭潮剪。這么做時(shí)一定要小心,如果你不調(diào)用你的方法的super() 實(shí)現(xiàn)分唾,你在破壞繼承機(jī)制并可能破壞繼承該方法的插件抗碰,也即永遠(yuǎn)不調(diào)用該繼承方法。除非你所使用的環(huán)境完全受控绽乔,你了解具體安裝了哪些插件并查看過(guò)不會(huì)破壞這些插件弧蝇,否則不要這么做。同時(shí)你應(yīng)該確保心一種可見(jiàn)的方法以來(lái)以文檔記錄所做的操作折砸。
在調(diào)用方法的原有實(shí)現(xiàn)之前和之后你可以做哪些事呢捍壤?有很多,包括但不限于如下這些:
- 修改傳遞給原有實(shí)現(xiàn)的參數(shù)(之前)
- 修改傳遞給原有實(shí)現(xiàn)的上下文(之前)
- 修改原有實(shí)現(xiàn)返回的結(jié)果(之后)
- 調(diào)用另一個(gè)方法(之前和之后)
- 創(chuàng)建方法(之前和之后)
- 拋出一個(gè)UserError來(lái)在禁止用例中取消執(zhí)行(之前和之后)
- 分拆self為更小的記錄集鞍爱,并以不同方式調(diào)用每個(gè)子集的原有實(shí)現(xiàn)(之前)
繼承write()和create()
本章中的繼承模型中定義的業(yè)務(wù)邏輯一節(jié)向我們展示了如何繼承模型類(lèi)中定義的方法鹃觉。如果你考慮一下,模型的父類(lèi)中定義的方法也是模型的一部分睹逃。這表示models.Model(實(shí)際為models.Model的父類(lèi)models.BaseModel)上定義的所有基礎(chǔ)方法都可以使用或被繼承盗扇。
本節(jié)將展示如何繼承create()和write() 來(lái)控制對(duì)記錄中某些字段的訪問(wèn)。
準(zhǔn)備工作
我們將通過(guò)第四章 創(chuàng)建Odoo插件模塊中的my_library插件模塊擴(kuò)展圖書(shū)示例沉填。
在library.book模型中添加一個(gè)manager_remarks字段疗隶。我們僅希望圖書(shū)管理員分組中的成員可以寫(xiě)入該字段:
from odoo import models, api, exceptions
class LibraryBook(models.Model):
_name = 'library.book'
manager_remarks = fields.Text('Manager Remarks')
在view/library_book.xml文件的<form>中添加manager_remarks字段來(lái)通過(guò)用戶界面訪問(wèn)該字段:
<field name="manager_remarks"/>
修改security/ir.model.access.csv文件來(lái)給圖書(shū)用戶寫(xiě)入的權(quán)限:
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
acl_book_user,library.book_default,model_library_book,base.group_user,1,1,0,0
acl_book_librarian,library.book_librarian,model_library_book,group_librarian,1,1,1,1
</form>
如何操作...
要防止非Librarian組的成員修改manager_remarks的值,需要執(zhí)行如下步驟:
- 繼承create()方法如下:
@api.model
def create(self, values):
if not self.user_has_groups('my_library.acl_book_librarian'):
if 'manager_remarks' in values:
raise UserError(
'You are not allowed to modify '
'manager_remarks'
)
return super(LibraryBook, self).create(values)
- 繼承write()方法如下:
@api.multi
def write(self, values):
if not self.user_has_groups('my_library.acl_book_librarian'):
if 'manager_remarks' in values:
raise UserError(
'You are not allowed to modify '
'manager_remarks'
)
return super(LibraryBook, self).write(values)
運(yùn)行原理...
第1步中重新定義了create()方法翼闹。在調(diào)用create() 的基礎(chǔ)實(shí)現(xiàn)之前斑鼻,我們的方法使用了user_has_groups() 方法來(lái)查看啟用是否屬于my_library.group_librarian組(這是該組的XML ID)。如果并非如此且向manager_remarks傳遞了值猎荠,則拋出一個(gè)UserError異常坚弱,以防止記錄的創(chuàng)建蜀备。這一檢查在基礎(chǔ)實(shí)現(xiàn)的調(diào)用之前執(zhí)行。
第2步為 write() 執(zhí)行相同的操作荒叶,在寫(xiě)入之前碾阁,我們檢查組以及寫(xiě)入的值中有哪此字段,在有問(wèn)題時(shí)拋出UserError異常些楣。
??在網(wǎng)頁(yè)客戶端中將該字段設(shè)為只讀并不會(huì)防止RPC調(diào)用對(duì)其寫(xiě)入脂凶。這也是為什么我們繼承了create()和write()。
測(cè)試這一實(shí)現(xiàn)愁茁,你可以demo用戶登錄或收回當(dāng)前用戶的librarian權(quán)限蚕钦。
擴(kuò)展知識(shí)...
在繼承write()時(shí),注意調(diào)用write()的super()實(shí)現(xiàn)之前鹅很,self仍未被修改嘶居。你可以使用它對(duì)比該字段的當(dāng)前值和值字典中的字段。
本節(jié)中道宅,我們選擇拋出異常食听,但你也可以選擇從值字典中刪除不符合規(guī)則的字段,靜默地跳過(guò)記錄中該字段的更新:
@api.multi
def write(self, values):
if not self.user_has_groups( 'my_library.group_librarian'):
if 'manager_remarks' in values:
del values['manager_remarks']
return super(LibraryBook, self).write(values)
在調(diào)用super().write()之后污茵,如果你希望執(zhí)行其它動(dòng)作樱报,則需要對(duì)任何其它引發(fā)write()的調(diào)用保持警惕,否則會(huì)創(chuàng)建一個(gè)無(wú)限遞歸循環(huán)泞当。規(guī)避的方法是在上下文中加入標(biāo)記來(lái)進(jìn)行檢查以解除這種遞歸:
class MyModel(models.Model):
@api.multi
def write(self, values):
sup = super(MyModel, self).write(values)
if self.env.context.get('MyModelLoopBreaker'):
return
self = self.with_context(MyModelLoopBreaker=True)
self.compute_things() # can cause calls to writes
return sup
自定義記錄的搜索方式
在第五章 應(yīng)用模型中定義模型表示及排序一節(jié)中引入了name_get()方法迹蛤,用于完成不同地方記錄的展現(xiàn),包含用于在網(wǎng)頁(yè)客戶端中用于展示Many2one關(guān)聯(lián)的組件襟士。
本節(jié)將向你展示如何通過(guò)重新定義name_search在Many2one組件中通過(guò)標(biāo)題盗飒、作者或ISBN來(lái)搜索圖書(shū)。
準(zhǔn)備工作
本節(jié)中陋桂,我們將使用如下模型定義:
class LibraryBook(models.Model):
_name = 'library.book'
name = fields.Char('Title')
isbn = fields.Char('ISBN')
author_ids = fields.Many2many('res.partner', 'Authors')
@api.multi
def name_get(self):
result = []
for book in self:
authors = book.author_ids.mapped('name')
name = '%s (%s)' % (book.name, ', '.join(authors))
result.append((book.id, name))
return result
在使用這一模型時(shí)逆趣,Many2one組中的圖書(shū)以圖書(shū)名(作者1,作者2...)進(jìn)行顯示。用戶預(yù)設(shè)可通過(guò)輸入作者名查找根據(jù)姓名過(guò)濾出的列表嗜历,但并不會(huì)這樣宣渗,因?yàn)閚ame_search的默認(rèn)實(shí)現(xiàn)僅使用了模型類(lèi)中_rec_name屬性所引用的屬性,本例中為name梨州。我們也希望可通過(guò)ISBN號(hào)來(lái)進(jìn)行過(guò)濾痕囱。
如何操作...
要對(duì)library.book能夠使用書(shū)名、作者或ISBN號(hào)進(jìn)行搜索暴匠,你需要在LibraryBook類(lèi)中定義一個(gè)_name_search() 方法鞍恢,如下:
@api.model
def _name_search(self, name='', args=None, operator='ilike',
limit=100, name_get_uid=None):
args = [] if args is None else args.copy()
if not(name == '' and operator == 'ilike'):
args += ['|', '|',
('name', operator, name),
('isbn', operator, name),
('author_ids.name', operator, name)
]
return super(LibraryBook, self)._name_search(
name=name, args=args, operator=operator,
limit=limit, name_get_uid=name_get_uid)
在library.book模型中添加old_editions Many2one字段來(lái)測(cè)試 _name_search的實(shí)現(xiàn):
old_edition = fields.Many2one('library.book', string='Old Edition')
向用戶界面中添加如下字段:
<field name="old_edition" />
重啟服務(wù)并更新模塊來(lái)讓修改生效。你可以通過(guò)在old_edition Many2one字段中進(jìn)行搜索來(lái)調(diào)用_name_search方法。
運(yùn)行原理...
name_search()的默認(rèn)實(shí)現(xiàn)實(shí)際上僅僅是調(diào)用了_name_search()方法帮掉,它執(zhí)行了真正的任務(wù)弦悉。_name_search()方法有一個(gè)額外的參數(shù)name_get_uid,用于一些極端用例中旭寿,如你希望使用sudo() 或通過(guò)不同的用戶來(lái)計(jì)算結(jié)果警绩。
我們將接收到的大部分參數(shù)不做修改的傳遞給該方法的super()實(shí)現(xiàn):
- name是包含到此所輸入值的字符串崇败。
- args為None或者一個(gè)用于預(yù)過(guò)濾可能記錄的搜索域盅称。(比如,它可以來(lái)自Many2one關(guān)聯(lián)的domain參數(shù)后室。)
- operator是一個(gè)包含匹配運(yùn)算符的字符串缩膝。通常會(huì)有'ilike' 或 '='。
- limit是要獲取的最大行數(shù)岸霹。
- name_get_uid可在調(diào)用 name_get()計(jì)算組件中顯示字符串時(shí)用于指定不同的用戶疾层。
我們實(shí)現(xiàn)的方法做了如下操作:
- 如果args為None,它生成一個(gè)新的空列表贡避,否則對(duì)args進(jìn)行拷貝痛黎。我們通過(guò)做拷貝來(lái)避免對(duì)列表的修改對(duì)調(diào)用者產(chǎn)生負(fù)面效果。
- 然后刮吧,我們查看name是否為非空字符串或者運(yùn)算符是否不是'ilike'湖饱。這用于避免生成無(wú)效的域, [('name', ilike, '')]杀捻,它并不能過(guò)濾任務(wù)東西井厌。在這種情況下,我們直接進(jìn)行super()的調(diào)用實(shí)現(xiàn)劳跃。
- 如果name存在春弥,或者運(yùn)算符并非'ilike'蛔糯,那么我們對(duì)args添加一些過(guò)濾條件。在本例中墓拜,我們添加了對(duì)所提供名稱(chēng)在圖書(shū)標(biāo)題、ISNB 或作者姓名中搜索的語(yǔ)句请契。
- 最后咳榜,我們以args中修改的域調(diào)用了super()實(shí)現(xiàn)并強(qiáng)制name為''以及運(yùn)算符為ilike。我們這么做來(lái)強(qiáng)制_name_search() 的默認(rèn)實(shí)現(xiàn)不對(duì)它所接收到的域做任何修改姚糊,因而使用我們所指定的域贿衍。
擴(kuò)展知識(shí)...
我們?cè)谝灾刑岬竭@一方法用于Many2one組件。為保持完整性救恨,它還可用于Odoo中如下部分:
- 在域中的One2many和Many2many字段上的運(yùn)算符中使用
- 搜索many2many_tags組件中記錄
- 搜索CSV導(dǎo)入文件中的記錄
其它內(nèi)容
在第五章 應(yīng)用模型中定義模型表示及排序一節(jié)中演示了如何定義name_get()方法贸辈,該方法用于創(chuàng)建記錄的文本表現(xiàn)。
在第十章 后端視圖中的在記錄列表上定義過(guò)濾器 - 域一節(jié),提供了有關(guān)搜索域語(yǔ)法的更多信息擎淤。
通過(guò)read_group()獲取組中的數(shù)據(jù)
在前面的各節(jié)中奢啥,我們學(xué)習(xí)了如何從數(shù)據(jù)庫(kù)中搜索和獲取數(shù)據(jù)。但有時(shí)嘴拢,你希望通過(guò)聚合記錄來(lái)獲取結(jié)果桩盲,如上個(gè)月銷(xiāo)售訂單的平均成本。在這種情況下席吴,你可以使用read_group() 方法來(lái)獲取聚合結(jié)果赌结。
準(zhǔn)備工作
本小節(jié)中,我們將使用第四章 創(chuàng)建Odoo插件模塊中的my_library插件模塊圖書(shū)示例孝冒。
修改 library.book模型柬姚,如下面的模型定義所示:
class LibraryBook(models.Model):
_name = 'library.book'
name = fields.Char('Title', required=True)
date_release = fields.Date('Release Date')
pages = fields.Integer('Number of Pages')
cost_price = fields.Float('Book Cost')
category_id = fields.Many2one('library.book.category')
author_ids = fields.Many2many('res.partner', string='Authors')
添加library.book.category模型。為保持簡(jiǎn)化庄涡,我們僅將其添加到同一library_book.py文件中:
class BookCategory(models.Model):
_name = 'library.book.category'
name = fields.Char('Category')
description = fields.Text('Description')
我們將使用 library.book模型并獲取每個(gè)分類(lèi)的平均成本價(jià)量承。
如何操作...
要提取分組結(jié)果,我們將添加_get_average_cost方法穴店,它會(huì)使用read_group() 方法來(lái)獲取分組中的數(shù)據(jù):
@api.model
def _get_average_cost(self):
grouped_result = self.read_group(
[('cost_price', "!=", False)], # Domain
['category_id', 'cost_price:avg'], # Fields to access
['category_id'] # group_by
)
return grouped_result
要測(cè)試這一實(shí)現(xiàn)撕捍,需要在用戶界面中添加一個(gè)按鈕來(lái)觸發(fā)該方法。
運(yùn)行原理...
read_group()方法的內(nèi)部使用SQL的group by及累加函數(shù)來(lái)獲取數(shù)據(jù)泣洞。傳遞給read_group() 方法的最常用參數(shù)如下:
- domain:用于為分組過(guò)濾記錄忧风。更多有關(guān)過(guò)濾域的知識(shí),請(qǐng)參見(jiàn)第十章 后端視圖中的定義搜索視圖一節(jié)斜棚。
- fields:它傳遞你希望獲取分組數(shù)據(jù)的字段名稱(chēng)阀蒂。該參數(shù)的值可能如下:
- 字段名:你可以向fields參數(shù)傳遞字段名,但如果使用這一選項(xiàng)弟蚀,還應(yīng)將該字段名同時(shí)傳遞給groupby參數(shù)蚤霞,否則會(huì)產(chǎn)生錯(cuò)誤
- field_name:agg:你可以傳遞帶有聚合函數(shù)的字段名。例如义钉,在cost_price:avg中昧绣,avg是一個(gè)SQL聚合函數(shù)。PostgreSQL中的聚合函數(shù)請(qǐng)參見(jiàn)https://www.postgresql.org/docs/current/functions-aggregate.html捶闸。
- name:agg(field_name):它與前面一個(gè)相同夜畴,但使用這種語(yǔ)句,你可以給數(shù)據(jù)列一個(gè)別名删壮,例如average_price:avg(cost_price)贪绘。
- groupby:這個(gè)參數(shù)接收一個(gè)字段描述列表。記錄將根據(jù)這些字段分組央碟。對(duì)于date和datetime列税灌,你可以傳遞groupby_function來(lái)根據(jù)不同的時(shí)長(zhǎng)應(yīng)用日期分組,如 date_release:month。這會(huì)根據(jù)月來(lái)應(yīng)用分組菱涤。
read_group()還支持一些可選參數(shù)苞也,如下:
- offset:表示可以跳過(guò)可選記錄數(shù)量
- limit:表示可選的返回記錄最大數(shù)量
- orderby:如果傳遞了該選項(xiàng),結(jié)果會(huì)根據(jù)給定字段進(jìn)行排序
- lazy:它接收布爾值粘秆,并且默認(rèn)值為T(mén)rue如迟。如果傳遞了True,結(jié)果僅通過(guò)第一個(gè)groupby進(jìn)行分組攻走,剩余的groupby會(huì)被放到__context鍵中殷勘。若為False,所有的groupby在一次調(diào)用中完成陋气。
小貼士:性能貼士:read_group()要比從記錄集中讀取和處理值快速的多劳吠。因此對(duì)KPI或圖表應(yīng)保持使用read_group()引润。