全書完整目錄請(qǐng)見:Odoo 12開發(fā)者指南(Cookbook)第三版
本章中我們將講解如下小節(jié):
- 定義模型表現(xiàn)及順序
- 向模型添加數(shù)據(jù)字段
- 使用可配置精度的浮點(diǎn)字段
- 向模型添加貨幣字段
- 向模型添加關(guān)聯(lián)字段
- 向模型添加等級(jí)
- 向模型添加約束驗(yàn)證
- 向模型添加計(jì)算字段
- 暴露存儲(chǔ)在其它模型中的關(guān)聯(lián)字段
- 使用引用字段添加動(dòng)態(tài)關(guān)聯(lián)
- 使用繼承向模型添加功能
- 為可復(fù)用模型功能使用抽象模型
- 使用代理繼承將功能拷貝至另一個(gè)模型
引言
本章中的各小節(jié)會(huì)對(duì)已有的那個(gè)插件模型做一些小的新增。我們將使用在第四章 創(chuàng)建Odoo插件模塊中所創(chuàng)建的模塊。
技術(shù)準(zhǔn)備
要按照本章中的示例進(jìn)行操作,你應(yīng)該要有一個(gè)第四章 創(chuàng)建Odoo插件模塊中所創(chuàng)建的模塊并且該模型應(yīng)可用。
本章中使用的代碼可以在GitHub倉(cāng)庫中進(jìn)行下載,地址為https://github.com/alanhou/odoo12-cookbook/tree/master/Chapter05。
觀看如下視頻來查看實(shí)時(shí)代碼操作:http://t.cn/E9ZHCPR
定義模型表示及順序
模型中有結(jié)構(gòu)性屬性來定義它們的行為。這是以下劃線作為前綴。最重要的屬性是_name,因?yàn)檫@定義了內(nèi)部全局標(biāo)識(shí)符。Odoo通過這一_name屬性來創(chuàng)建數(shù)據(jù)表。例如,如果你使用_name="library.book",那么Odoo ORM會(huì)在數(shù)據(jù)庫中創(chuàng)建一張library_book數(shù)據(jù)表。這也是為什么_name必須在Odoo系統(tǒng)中要保持唯一。
在模型中可以使用另外兩個(gè)屬性:一個(gè)設(shè)置用于記錄展示或標(biāo)題的字段,另一個(gè)設(shè)置記錄的展現(xiàn)的順序。
準(zhǔn)備工作
這一節(jié)中我們假定你已經(jīng)有一個(gè)包含my_library模塊的實(shí)例,如第四章 創(chuàng)建Odoo插件模塊中所描述。
如何操作...
my_library實(shí)例應(yīng)包含一個(gè)名為models/library_book.py的Python文件,它定義一個(gè)基礎(chǔ)模型。我們將編輯該文件來在_name之后添加一個(gè)新的類級(jí)別的屬性:
- 加入如下代碼來添加一個(gè)用戶友好的模型標(biāo)題:
_description = 'Library Book'
- 首先對(duì)記錄進(jìn)行排序(按時(shí)間最近排序,然后按標(biāo)題排序),添加如下代碼:
_order = 'date_release desc, name'
- 添加如下代碼來使用short_name字段作為記錄的表示:
_rec_name = 'short_name'
short_name = fields.Char('Short Title', required=True)
- 在表單視圖中添加short_name字段,這樣會(huì)在該視圖中顯示這一新字段:
<field name="short_name"/>
完成如上操作之后,我們library_book.py文件應(yīng)該是下面這樣:
from odoo import models, fields
class LibraryBook(models.Model):
_name = 'library.book'
_description = 'Library Book'
_order = 'date_release desc, name'
_rec_name = 'short_name'
name = fields.Char('Title', required=True)
short_name = fields.Char('Short Title', required=True)
date_release = fields.Date('Release Date')
author_ids = fields.Many2many('res.partner', string='Authors')
你的library_book.xml 文件中的<form>視圖應(yīng)該是下面這樣的:
<form>
<group>
<group>
<field name="name"/>
<field name="author_ids" widget="many2many_tags"/>
</group>
<group>
<field name="short_name"/>
<field name="date_release"/>
</group>
</group>
</form>
然后我們應(yīng)升級(jí)模塊讓這些更改在Odoo中生效。
運(yùn)行原理...
第一步為定義的模型添加了一個(gè)用戶友好的標(biāo)題。這并非強(qiáng)制的,但可以為一些插件所用。例如,它可在創(chuàng)建記錄時(shí)用于mail插件模塊中追蹤功能的通知文本。更多詳情,請(qǐng)參見第二十三章 在Odoo中管理email。
默認(rèn)Odoo使用內(nèi)部id值進(jìn)行排序。但是,可對(duì)其進(jìn)行修改來使用我們自己選擇的字段排序,做法是提供一個(gè)包含字段名逗號(hào)分隔列表字符串的_order屬性。字段名后可接desc關(guān)鍵字來進(jìn)行降序排序。
僅能使用存儲(chǔ)在數(shù)據(jù)庫中的字段。未存儲(chǔ)的計(jì)算字段無法用于記錄排序。
??_order字符串的語法類似于SQL中的ORDER BY語句,但進(jìn)行了簡(jiǎn)化。例如,不允許NULLS FIRST這樣的特殊語句。
模型記錄在其它記錄中引用時(shí)使用了一種表現(xiàn)形式。例如,帶有值1的user_id表示管理員用戶。在表單視圖中顯示時(shí),Odoo會(huì)顯示用戶名,而非數(shù)據(jù)庫ID。默認(rèn)使用的是name字段。事實(shí)上這是_rec_name屬性的默認(rèn)值,這也是為什么在我們的模型中添加name字段會(huì)很方便。
如果模型中不存在name字段,會(huì)通過模型和記錄標(biāo)識(shí)符生成一個(gè)描述,類似(library.book, 1)。
因?yàn)槲覀冊(cè)谀P椭行略隽藄hort_name字段,Odoo ORM會(huì)在數(shù)據(jù)表中新加一列,但該字段不會(huì)在視圖中顯示。要進(jìn)行顯示,我們需要在表單視圖中添加該字段。在第4步中,我們?cè)诒韱我晥D中添加了short_name。
擴(kuò)展知識(shí)...
記錄表示可采用一個(gè)魔法計(jì)算字段display_name,從版本8.0起會(huì)自動(dòng)添加它到所有模型中。它的值由name_get() 模型方法生成,在Odoo此前的版本中已存有這一方法。
name_get()的默認(rèn)實(shí)現(xiàn)使用_rec_name屬性來查找放置數(shù)據(jù)的字段,使用它生成顯示名稱。如果你想要自己實(shí)現(xiàn)顯示名稱,可以重載name_get()中的邏輯來生成一個(gè)自定義顯示名稱。該方法應(yīng)返回一個(gè)包含兩個(gè)元素的元組列表:記錄ID和記錄的Unicode字符串表示。
例如,在表示中包含標(biāo)題和發(fā)行日期,類似Moby Dick (1851-10-18),我們可以進(jìn)行如下定義:
def name_get(self):
result = []
for record in self:
rec_name = "%s (%s)" % (record.name, record.date_release)
result.append((record.id, rec_name))
return result
向模型添加數(shù)據(jù)字段
模型用于存儲(chǔ)數(shù)據(jù),這些數(shù)據(jù)以字段進(jìn)行結(jié)構(gòu)化組織。這里,你將學(xué)習(xí)到可存儲(chǔ)在字段中的不同數(shù)據(jù)類型,以及如何在模型中進(jìn)行添加。
準(zhǔn)備工作
這一節(jié)假定你已經(jīng)有一個(gè)如第四章 創(chuàng)建Odoo插件模塊中所述的帶有my_library插件模型的實(shí)例準(zhǔn)備就緒。
如何操作...
my_library插件模型應(yīng)該已經(jīng)有定義了基本模型的models/library_book.py文件,我們將編輯該文件來新增字段:
- 使用最小化語法來向圖書模型添加字段:
from odoo import models, fields
class LibraryBook(models.Model):
# ...
short_name = fields.Char('Short Title')
notes = fields.Text('Internal Notes')
state = fields.Selection(
[('draft', 'Not Available'),
('available', 'Available'),
('lost', 'Lost')],
'State')
description = fields.Html('Description')
cover = fields.Binary('Book Cover')
out_of_print = fields.Boolean('Out of Print?')
date_release = fields.Date('Release Date')
date_updated = fields.Datetime('Last Updated')
pages = fields.Integer('Number of Pages')
reader_rating = fields.Float(
'Reader Average Rating',
digits=(14, 4), # Optional precision (total, decimals),
)
- 我們已向模型新增了字段。仍需在表單視圖中添加這些字段來在用戶界面中反映出這些修改。參見如下在表單視圖中增加字段的代碼:
<form>
<group>
<group>
<field name="name"/>
<field name="author_ids" widget="many2many_tags"/>
<field name="state"/>
<field name="pages"/>
<field name="notes"/>
</group>
<group>
<field name="short_name"/>
<field name="date_release"/>
<field name="date_updated"/>
<field name="cover" widget="image" class="oe_avatar"/>
<field name="reader_rating"/>
</group>
</group>
<group>
<field name="description"/>
</group>
</form>
升級(jí)模型會(huì)使用Odoo模型中這些修改生效。
查看如下這些不同字段的示例。這里我們對(duì)字段使用了不同類型的屬性。這會(huì)讓你對(duì)字段聲明有一些更好的概念:
short_name = fields.Char('Short Title',translate=True, index=True)
state = fields.Selection(
[('draft', 'Not Available'),
('available', 'Available'),
('lost', 'Lost')],
'State', default="draft")
description = fields.Html('Description', sanitize=True, strip_style=False)
pages = fields.Integer('Number of Pages',
groups='base.group_user',
states={'lost': [('readonly', True)]},
help='Total book page count', company_dependent=False)
運(yùn)行原理...
通過在Python類中定義屬性向模型添加字段。可以使用的非關(guān)系型字段如下:
Char用于字符串值。
Text用于多選字符串值。
-
Selection用于選擇列表。這是一個(gè)值和描述對(duì)的列表。所選擇的值存儲(chǔ)于數(shù)據(jù)庫中,可以是字符串或整數(shù)。描述自動(dòng)可翻譯。
小貼士:在Selection類型的字段中,你可以使用整型的鍵,但應(yīng)注意Odoo內(nèi)部將0解釋為未設(shè)置,不會(huì)顯示存儲(chǔ)值為0的描述。這可能會(huì)發(fā)生,所以應(yīng)當(dāng)記住。
Html類似于text字段,但通常用于以HTML格式存儲(chǔ)的富文本。
Binary字段存儲(chǔ)二進(jìn)制文件,如圖像或文檔。
Boolean存儲(chǔ)True/False 值。
Date存儲(chǔ)日期值。它在數(shù)據(jù)庫中以日期進(jìn)行存儲(chǔ)。ORM中以Python date對(duì)象的形式對(duì)其進(jìn)行處理。Odoo 12之前的版本中ORM以字符串的形式處理日期。所使用的格式在odoo.fields.DATE_FORMAT中定義。
Datetime用于日期時(shí)間值。在數(shù)據(jù)庫中以原生UTC時(shí)間datetime進(jìn)行存儲(chǔ)。ORM中以Python datetime對(duì)象的形式對(duì)其進(jìn)行處理。Odoo 12之前的版本中ORM以字符串的形式處理datetime。所使用的格式在odoo.fields.DATETIME_FORMAT中定義。
Integer字段無需過多解釋了。
Float(浮點(diǎn))字段存儲(chǔ)數(shù)值。精度可由位數(shù)和小數(shù)位數(shù)對(duì)來定義。
Monetary可存儲(chǔ)某個(gè)幣種的數(shù)量值。這會(huì)在本章中的向模型添加貨幣字段一節(jié)進(jìn)行講解。
本節(jié)的第一步中顯示了添加各個(gè)字段類型的最小化語法。字段定義可像第2步中那樣進(jìn)行擴(kuò)展來添加其它可選屬性。
以下是有關(guān)所使用的屬性的講解:
string是字段的標(biāo)題,在UI視圖標(biāo)簽中使用。它是可選項(xiàng),如未設(shè)置,會(huì)通過首字母大寫及將空格替換為下劃線來從字段名獲取標(biāo)簽。
translate,在設(shè)置為True時(shí),讓字段可翻譯,它可根據(jù)用戶界面的語言保存不同值。
default是默認(rèn)值。也可以是一個(gè)用于計(jì)算默認(rèn)值的函數(shù),例如default=_compute_default,_compute_default是在定義字段前模型中所定義的一個(gè)方法。
help是在UI提示工具中顯示的解釋性文本。
groups讓字段僅對(duì)安全組可用。它是包含安全組的XML ID逗號(hào)分隔列表的一個(gè)字符串。這個(gè)話題將會(huì)在第十一章 權(quán)限安全中進(jìn)行討論。
states允許用戶界面依據(jù)state字段的值來動(dòng)態(tài)設(shè)置readonly, required和invisible的值。因此,它要求存在一個(gè)state字段并在表單視圖中使用(即便是隱藏的)。state屬性的名稱是在Odoo硬編碼且無法修改的。
copy標(biāo)記在復(fù)制記錄時(shí)是否拷貝字段值。對(duì)于非關(guān)系型字段和Many2one它的默認(rèn)值是True、對(duì)One2many和計(jì)算字段它的值是False。
index,在設(shè)置為True時(shí),為該字段創(chuàng)建一個(gè)數(shù)據(jù)庫索引,有時(shí)可供更快速搜索使用。它取代了已淘汰的select=1屬性。
readonly標(biāo)記讓該字段在用戶界面中默認(rèn)僅為只讀。
required標(biāo)記強(qiáng)制字段在用戶界面中默認(rèn)為必填。
-
sanitize標(biāo)記用于HTML字段并去除包含不安全標(biāo)簽的內(nèi)容。使用它會(huì)對(duì)輸入進(jìn)行全局清理。如果需要更精細(xì)的控制,可以使用一些關(guān)鍵字,僅在啟用sanitize時(shí)生效:
- sanitize_tags=True刪除白名單列表以外的標(biāo)簽(默認(rèn)項(xiàng))
- sanitize_attributes=True刪除白名單列表以外的標(biāo)簽屬性
- sanitize_style=True刪除白名單列表以外的樣式屬性
- strip_style=True刪除所有樣式元素
- strip_class=True刪除所有class屬性
??這里所提及的各個(gè)白名單列表在odoo/tools/mail.py中定義。
company_dependent標(biāo)記讓該字段根據(jù)公司存儲(chǔ)不同值。它取代了已淘汰的Property字段類型。
最后,我們根據(jù)模型中新增的字段更新了表單視圖,我們?cè)谶@里以自己的方式放置<field>標(biāo)簽,但你可以根據(jù)自己的需要放置在任意位置。表單視圖在第十章 后端視圖中會(huì)進(jìn)行細(xì)致的講解。
擴(kuò)展知識(shí)...
Selection字段還接收一個(gè)函數(shù)引用來替代列表作為selection屬性。這允許動(dòng)態(tài)生成選項(xiàng)列表,你會(huì)在本章的使用引用字段添加動(dòng)態(tài)關(guān)聯(lián)一節(jié)中看到一個(gè)示例,其中使用了selection屬性。
Date和Datetime字段對(duì)象暴露了一些非常方便的工具方法。
Date有如下方法:
- fields.Date.to_date(string_value將字符串解析為一個(gè)date對(duì)象。
- fields.Date.to_string(date_value)將Date對(duì)象表示為字符串。
- fields.Date.today()以字符串格式返回當(dāng)前日期。這適合用于默認(rèn)值。
- fields.Date.context_today(record, timestamp)根據(jù)記錄(或記錄集)上下文的時(shí)區(qū)以字符串格式返回時(shí)間戳的日期(或者在省略時(shí)間戳?xí)r返回當(dāng)天)。
Datetime有如下方法:
- fields.Datetime.to_datetime(string_value)將字符串解析為datetime對(duì)象。
- fields.Datetime.to_string(datetime_value)將datetime對(duì)象表示為字符串。
- fields.Datetime.now()以字符串格式返回當(dāng)天及當(dāng)前時(shí)間。它適合用作默認(rèn)值。
- fields.Datetime.context_timestamp(record, timestamp)將時(shí)間戳原生datetime按照記錄上下文的時(shí)區(qū)轉(zhuǎn)化為對(duì)應(yīng)時(shí)區(qū)。它不適合用作默認(rèn)值,但是在向外部系統(tǒng)發(fā)送數(shù)據(jù)等操作時(shí)可以使用。
除基本字段外,我們還有關(guān)聯(lián)字段:Many2one, One2many和Many2many。這些會(huì)在本章向模型添加關(guān)聯(lián)字段一節(jié)中進(jìn)行講解。
字段也可以有動(dòng)態(tài)計(jì)算的值,使用compute字段屬性定義計(jì)算函數(shù)。這在向模型添加計(jì)算字段一節(jié)中進(jìn)行講解。
有些字段在Odoo模型中默認(rèn)添加,因此我們不應(yīng)在字段中使用這些名稱。這些是記錄自動(dòng)生成的標(biāo)識(shí)符的id字段以及一些審計(jì)日志字段,如下所示:
- create_date是記錄創(chuàng)建的時(shí)間戳
- create_uid是創(chuàng)建該記錄的用戶
- write_date是最近記錄的編輯時(shí)間戳
- write_uid是最后編輯記錄的用戶
這些日志字段的自動(dòng)創(chuàng)建可通過設(shè)置模型屬性_log_access=False來進(jìn)行禁用。
另一個(gè)可以向模型添加的特殊列是active。它應(yīng)是布爾型字段,允許用將記錄標(biāo)記為非活躍(inactive)。它的定義如下:
active = fields.Boolean('Active', default=True)
默認(rèn)只有將active設(shè)置為True的記錄才可見。要獲取隱藏字段,我們需要使用域過濾器[('active', '=', False)]。而如果向環(huán)境上下文添加了'active_test': False 值,ORM則不會(huì)過濾掉非活躍記錄。
小貼士:在有些情況下,你可能不能修改上下文來獲取活躍及非活躍記錄。這時(shí),可以使用['|', ('active', '=', True), ('active', '=', False)] 域。
注意:[('active', 'in' (True, False))]可能不會(huì)如你所預(yù)期那樣。Odoo在域中顯式地查找('active', '=', False)語句。它默認(rèn)會(huì)限制僅搜索活躍記錄。
使用可配置精度的浮點(diǎn)字段
在使用浮點(diǎn)字段時(shí),我們可能會(huì)想讓終端用戶配置所使用的精度。Decimal Precision Configuration模塊插件提供了這一功能。
我們將向圖書模型字段添加一個(gè)帶有用戶可配置位數(shù)的成本價(jià)字段。
準(zhǔn)備工作
我們將復(fù)用在第四章 創(chuàng)建Odoo插件模塊中創(chuàng)建的my_library插件模塊。
如何操作...
我們需要安裝decimal_precision模塊,在配置中添加Usage,然后在模型字段中使用它。
-
確保安裝了數(shù)字精度模塊,在頂級(jí)菜單中選擇Apps,刪除默認(rèn)過濾器,搜索Decimal Precision Configuration模塊,若未安裝則進(jìn)行安裝:
image.png 通過Settings菜單下的鏈接激活開發(fā)者模式(參見第一章 安裝Odoo開發(fā)環(huán)境中的激活Odoo開發(fā)者工具一節(jié))。這會(huì)啟用Settings > Technical菜單。
訪問數(shù)字精度設(shè)置。這需要打開Settings頂級(jí)菜單并選擇Technical > Database Structure > Decimal Accuracy。我們應(yīng)該會(huì)看到一個(gè)當(dāng)前定義的設(shè)置列表。
-
添加一個(gè)新配置,設(shè)置Usage為Book Price并選擇數(shù)字精度:
image.png 在manifest.py聲明文件中添加這個(gè)新依賴。如下所示:
{
'name': 'Chapter 05 code',
'depends': ['base', 'decimal_precision'],
'data': ['views/library_book.xml']
}
- 要使用數(shù)字精度設(shè)置添加模型字段,編輯models/library_book.py文件并添加如下代碼:
from odoo.addons import decimal_precision as dp
class LibraryBook(models.Model):
cost_price = fields.Float(
'Book Cost', dp.get_precision('Book Price'))
小貼士:不論何時(shí)向模型添加新字段,你將需要將它們添加到視圖中以在用戶界面中訪問它們。在前例中,我們添加了cost_price。要在表單視圖中看到它,需要添加<field name="cost_price"/>。
運(yùn)行原理...
get_precision()函數(shù)查找數(shù)字精度中的Usage字段并返回一個(gè)16位精度的元組以及在配置中所定義的小數(shù)位數(shù)。
在字段定義中使用這一函數(shù)來取代硬編碼,可以讓終端用戶根據(jù)自己的需求來進(jìn)行配置。
向模型添加貨幣字段
Odoo對(duì)于與幣種相關(guān)的貨幣值有特別的支持。我們來看如何在模型中使用它。
準(zhǔn)備工作
我們將復(fù)用第四章 創(chuàng)建Odoo插件模塊中的my_library插件模塊。
如何操作...
貨幣字段需要一個(gè)補(bǔ)充的幣種字段來存儲(chǔ)相應(yīng)數(shù)量的貨幣值。
my_library中已經(jīng)有了models/library_book.py并定義了一個(gè)基礎(chǔ)模型。我們將編輯該文件來添加所需的字段:
- 添加所要使用的字段來存儲(chǔ)幣種:
class LibraryBook(models.Model):
# ...
currency_id = fields.Many2one(
'res.currency', string='Currency')
- 添加貨幣字段來存儲(chǔ)數(shù)額:
class LibraryBook(models.Model):
# ...
retail_price = fields.Monetary(
'Retail Price',
# optional: currency_field='currency_id',
)
現(xiàn)在升級(jí)這個(gè)插件模塊,模型中即可使用新增字段了。未在視圖添加時(shí)它們還不會(huì)在視圖中顯示,但通過Settings > Technical > Database Structure > Model查看模型字段可確定添加是否成功。在將它們添加到表單視圖中之后,會(huì)是如下這樣:

運(yùn)行原理...
貨幣字段與浮點(diǎn)字段類似,但因通過第二個(gè)字段知道了幣種,Odoo可以在用戶界面中進(jìn)行正確的展示。
這個(gè)幣種字段一般稱為currency_id,但我們可以使用任意字段,只要在currency_field可選參數(shù)中使用它即可。
小貼士:在你需要在相同記錄中維護(hù)不同幣種的數(shù)額時(shí)這會(huì)非常有用,例如,在我們想要包含銷售訂單的幣種和公司的幣種時(shí)。你可以配置兩個(gè) fields.Many2one(res.currency) 字段并使用為第一個(gè)數(shù)額使用第一個(gè)字段、第二個(gè)數(shù)額使用第二個(gè)字段。
你還應(yīng)知道數(shù)額的小數(shù)精度來自幣種的定義(res.currency模型中的decimal_precision字段)。
向模型添加關(guān)聯(lián)字段
Odoo之間的關(guān)聯(lián)通過關(guān)聯(lián)字段來體現(xiàn)。有三種不同的關(guān)聯(lián)類型:
- many-to-one, 常縮寫為m2o
- one-to-many, ??s寫為o2m
- many-to-many, 常縮寫為m2m
以圖書應(yīng)用為例,我們看到每本書只有一個(gè)出版社,因此在圖書和出版社之間可以使用 many-to-one 關(guān)聯(lián)。
而每個(gè)出版社都可以出版多本書。因此對(duì)前面的many-to-one 關(guān)聯(lián)進(jìn)行反向則成為one-to-many 關(guān)聯(lián)。
最后,某些情況下我們會(huì)有many-to-many關(guān)聯(lián)。在本例中,每本書可以有多個(gè)作者。而反過來每位作者也可以寫多本書。從任意一方看,這是一個(gè)many-to-many關(guān)聯(lián)。
準(zhǔn)備工作
我們將復(fù)用第四章 創(chuàng)建Odoo插件模塊中的my_library插件模塊。
如何操作...
Odoo使用伙伴模型res.partner來表示人、組織和地址。對(duì)于作者和出版社我們應(yīng)使用它。我們將編輯models/library_book.py文件來添加這些字段:
- 向圖書模型添加圖書出版商的many-to-one字段:
class LibraryBook(models.Model):
# ...
publisher_id = fields.Many2one(
'res.partner', string='Publisher',
# optional:
ondelete='set null',
context={},
domain=[],
)
- 為出版社的書籍添加one-to-many字段,我們需要繼承partner模型。為進(jìn)行簡(jiǎn)化,我們將其添加到相同的Python文件中:
class ResPartner(models.Model):
_inherit = 'res.partner'
published_book_ids = fields.One2many(
'library.book', 'publisher_id',
string='Published Books')
> ??我們這里使用的_inherit屬性用于繼承已有模型。這一點(diǎn)會(huì)在本章后面的使用繼承向模型添加功能一節(jié)中進(jìn)行講解。
- 我們已在圖書和作者之間創(chuàng)建了一個(gè)many-to-many關(guān)聯(lián),讓我們?cè)俅尾榭匆幌拢?/li>
class LibraryBook(models.Model):
# ...
author_ids = fields.Many2many(
'res.partner', string='Authors')
- 相同的關(guān)聯(lián),但是作者對(duì)圖書的關(guān)聯(lián),應(yīng)加入到partner模型中:
class ResPartner(models.Model):
# ...
authored_book_ids = fields.Many2many(
'library.book',
string='Authored Books',
# relation='library_book_res_partner_rel' # optional
)
此時(shí)升級(jí)該插件模型,模型中的新字段就可以使用了。需要先將它們添加到視圖中才會(huì)顯示,但我們可以通Settings > Technical > Database Structure > Models來查看模型字段是否添加成功。
運(yùn)行原理...
Many-to-one字段向模型的數(shù)據(jù)表中添加了一列,存儲(chǔ)關(guān)聯(lián)記錄的數(shù)據(jù)庫ID。在數(shù)據(jù)庫級(jí)別上,還會(huì)創(chuàng)建外鍵約束,確保保存的ID是對(duì)關(guān)聯(lián)表中記錄的有效引用 。對(duì)這些關(guān)聯(lián)字段不會(huì)創(chuàng)建數(shù)據(jù)庫索引,但這可通過添加 index=True 屬性來進(jìn)行完成。
我們可以看到對(duì)many-to-one字段還可以使用另外的4個(gè)屬性。ondelete屬性決定在關(guān)聯(lián)記錄刪除時(shí)執(zhí)行什么操作。例如,在出版社記錄刪除后圖書會(huì)怎么樣?默認(rèn)值為'set null',,會(huì)將該字段置為空值。也可以為'restrict',會(huì)阻止關(guān)聯(lián)字段的刪除,或者是 'cascade',這會(huì)導(dǎo)致關(guān)聯(lián)的字段同樣被刪除。
最后的兩個(gè)屬性(context和domain)對(duì)其它的關(guān)聯(lián)字段同樣有效。這些大多在客戶端更具意義,在模型層次上,它們作為會(huì)在客戶端視圖中使用的默認(rèn)值。
- 在點(diǎn)擊字段進(jìn)入關(guān)聯(lián)記錄視圖時(shí)context會(huì)向客戶端上下文添加變量。例如,我們可以使用它來為新記錄設(shè)置通過該視圖創(chuàng)建的默認(rèn)值。
- domain是用于限制可用的關(guān)聯(lián)記錄列表的搜索過濾器。
context和domain都將在第十章 后端視圖中進(jìn)行更詳細(xì)的講解。
One-to-many字段是many-to-one的反向關(guān)聯(lián),雖然它們像其它字段一樣添加在模型中,在數(shù)據(jù)庫中并沒有實(shí)際的體現(xiàn)。他們僅是編程捷徑,啟用視圖來展現(xiàn)這些關(guān)聯(lián)記錄列表。
Many-to-many關(guān)聯(lián)也不會(huì)向模型數(shù)據(jù)表添加列。這類關(guān)聯(lián)在數(shù)據(jù)庫中使用中間關(guān)聯(lián)表進(jìn)行體現(xiàn),其中有兩列分別存儲(chǔ)這兩個(gè)關(guān)聯(lián)的ID。在圖書和作者之間添加新關(guān)聯(lián)在這個(gè)關(guān)聯(lián)表中使用圖書ID和作者ID創(chuàng)建一條新記錄。
Odoo自動(dòng)處理這一關(guān)聯(lián)表的創(chuàng)建。關(guān)聯(lián)表的名稱默認(rèn)使用兩個(gè)關(guān)聯(lián)模型名按字母排序加上一個(gè)_rel后綴來創(chuàng)建。但我們可以使用relation屬性來進(jìn)行覆蓋。
??需要考慮的一種情況是兩個(gè)表名過長(zhǎng)導(dǎo)致自動(dòng)生成的數(shù)據(jù)庫標(biāo)識(shí)符超過PostgreSQL的上限63個(gè)字符。按照經(jīng)驗(yàn),如果兩個(gè)關(guān)聯(lián)的表名超過23個(gè)字符時(shí),應(yīng)使用relation屬性來設(shè)置一個(gè)更短的名稱。下一節(jié)中,我們將進(jìn)行更深入的討論。
擴(kuò)展知識(shí)...
Many2one字段支持一個(gè)額外的auto_join屬性。這個(gè)標(biāo)記允許ORM對(duì)這個(gè)字段使用SQL連接(join)。因此它不受普通的ORM限制,如用戶訪問控制和記錄權(quán)限規(guī)則。在具體的用例中,它可以解決性能問題,但建議盡量避免使用。
我們講解了定義關(guān)聯(lián)字段的最簡(jiǎn)短的方式。下面來看針對(duì)這一字段類型的具體屬性。
One2many的字段屬性如下:
- comodel_name:這是目標(biāo)模型標(biāo)識(shí)符,對(duì)所有關(guān)聯(lián)字段是強(qiáng)制的,但可以占位定義而無需使用關(guān)鍵字
- inverse_name:它僅應(yīng)用于One2many,是反向Many2one關(guān)聯(lián)的目標(biāo)模型中的字段名
- limit:它在One2many和Many2many中使用,對(duì)在用戶界面級(jí)別上用于記錄讀取的數(shù)量設(shè)置可選限制
Many2many的字段屬性如下:
- comodel_name:它的功能與One2many字段中相同
- relation:這是用于支持關(guān)聯(lián)的數(shù)據(jù)表的名稱,覆蓋自動(dòng)定義的名稱
- column1:這是連接這個(gè)模型的關(guān)聯(lián)表中的Many2one字段的名稱
- column2:這是在關(guān)聯(lián)數(shù)據(jù)表中連接comodel的Many2one字段的名稱
對(duì)于Many2many在大多數(shù)情況下,ORM會(huì)處理這些屬性的默認(rèn)值。它甚至可以監(jiān)測(cè)反向的Many2many關(guān)聯(lián),監(jiān)測(cè)已有關(guān)聯(lián)表及適當(dāng)?shù)姆聪騝olumn1和column2值。
但是,有兩種情況我們需要介入并為這些屬性提供自己的值。一種情況是我們需在相同的兩個(gè)模型中添加一個(gè)以上的Many2many關(guān)聯(lián)。這時(shí),我們必須為第二個(gè)關(guān)聯(lián)提供關(guān)聯(lián)表名,且應(yīng)與第一個(gè)關(guān)聯(lián)不同。另一種情況是在自動(dòng)生成的關(guān)聯(lián)數(shù)據(jù)表名長(zhǎng)度超過PostgreSQL對(duì)于數(shù)據(jù)對(duì)象名的上限63個(gè)字符時(shí)。
自動(dòng)生成的關(guān)聯(lián)表名為<model1>_<model2>_rel。但關(guān)聯(lián)表還會(huì)為這一關(guān)聯(lián)名創(chuàng)建一個(gè)主鍵索引,標(biāo)識(shí)符如下:
<model1>_<model2>_rel_<model1>_id_<model2>_id_key
這個(gè)主鍵也需要滿足63個(gè)字符的上限。因此,如果兩個(gè)表名組合起來超過63個(gè)字符,你會(huì)無法滿足這一上限并需要手動(dòng)設(shè)置relation屬性。
向模型添加等級(jí)
等級(jí)(Hierarchy) 的表現(xiàn)就像是模型與自身存在著關(guān)聯(lián),每條記錄在相同模型中有一個(gè)父級(jí)記錄以及多個(gè)子記錄。這只需通過在模型和自身之間建立many-to-one關(guān)聯(lián)來進(jìn)行實(shí)現(xiàn)。
但是,Odoo通過使用嵌套集合模型來對(duì)這類字段提供更好的支持。在啟用后,在它們的域過濾器中使用child_of運(yùn)算符進(jìn)行查詢會(huì)顯著的提升速度。
繼續(xù)使用圖書示例,我們將創(chuàng)建一個(gè)等級(jí)分類樹來用于圖書分類。
準(zhǔn)備工作
我們將復(fù)用第四章 創(chuàng)建Odoo插件模塊中的my_library插件模塊。
如何操作...
我們會(huì)為分類樹新建一個(gè)Python文件models/library_book_categ.py,如下:
- 在models/init.py中載入如下行來加載新的Python代碼文件:
from . import library_book_categ
- 創(chuàng)建models/library_book_categ.py 文件并加入如下代碼來為圖書分類模型創(chuàng)建父子關(guān)聯(lián):
from odoo import models, fields, api
class BookCategory(models.Model):
_name = 'library.book.category'
name = fields.Char('Category')
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')
- 同時(shí)添加如下代碼來啟動(dòng)特別的等級(jí)支持:
_parent_store = True
_parent_name = "parent_id" # optional if field is 'parent_id'
parent_path = fields.Char(index=True)
- 在模型中添加如下行來新增一個(gè)防止循環(huán)關(guān)聯(lián)的檢查:
from odoo.exceptions import ValidationError
...
@api.constrains('parent_id')
def _check_hierarchy(self):
if not self._check_recursion():
raise models.ValidationError('Error! You cannot create recursive categories.')
- 這時(shí),我們需要向圖書分配一個(gè)分類。我們將在library.book模型中新增一個(gè)many2one字段進(jìn)行實(shí)現(xiàn):
category_id = fields.Many2one('library.book.category')
最后,升級(jí)模型來讓這些修改生效。
??要在用戶界面中顯示library.book.category模型,你需要添加菜單、視圖和權(quán)限規(guī)則。更多相關(guān)內(nèi)容請(qǐng)參見第四章 創(chuàng)建Odoo插件模塊。你也可通過訪問GitHub 倉(cāng)庫來獲取代碼。

運(yùn)行原理...
第1和第2步中新建了一個(gè)帶有等級(jí)關(guān)聯(lián)的模型。Many2one關(guān)聯(lián)添加了一個(gè)引用父級(jí)記錄的字段。為進(jìn)行更快速的子記錄發(fā)現(xiàn),這個(gè)字段使用index=True參數(shù)在數(shù)據(jù)庫中進(jìn)行了索引。parent_id應(yīng)將ondelete設(shè)置為'cascade' 或'restrict'。到這里,我們擁有了實(shí)現(xiàn)等級(jí)結(jié)構(gòu)所需的所有內(nèi)容,但還需要做一些增添來對(duì)其進(jìn)行改善。One2many關(guān)聯(lián)不會(huì)在數(shù)據(jù)庫中添加額外的字段,但提供了一個(gè)通過將這些記錄作為父級(jí)來訪問所有記錄的快捷方式。
在第3步,我們啟動(dòng)了對(duì)于等級(jí)的特別支持。這是一個(gè)非常有用的高讀取低寫入指令,因?yàn)樗ㄟ^更大的寫入運(yùn)算開銷帶來了更快速的數(shù)據(jù)瀏覽。這通過添加一個(gè)幫助字段parent_path及設(shè)置模型屬性為 _parent_store=True來實(shí)現(xiàn)。在啟用了這個(gè)屬性之后,該幫助字段會(huì)用于在等級(jí)樹的搜索中存儲(chǔ)數(shù)據(jù)。默認(rèn),它假定記錄的父級(jí)字段名為parent_id,但也可以使用不同的名稱。這種情況下,正確的字段名應(yīng)使用額外的模型屬性_parent_name來進(jìn)行表明。默認(rèn)值如下:
_parent_name = 'parent_id'
推薦使用第4步來防止等級(jí)中的循環(huán)依賴,即在上級(jí)樹和下級(jí)樹中都包含同一條記錄。這對(duì)于通過樹導(dǎo)航的程序非常的危險(xiǎn),因?yàn)闀?huì)進(jìn)入到無限死循環(huán)。models.Model為此提供了一個(gè)工具方法(_check_recursion),我們?cè)谶@里進(jìn)行了復(fù)用。
第5步為向library.book添加一個(gè)類型為many2one的category_id字段,這樣我們可以對(duì)圖書記錄設(shè)置分類。這個(gè)只是為完成我們的示例。
擴(kuò)展知識(shí)...
這里所展示的技術(shù)應(yīng)該用于靜態(tài)等級(jí),即經(jīng)常進(jìn)行讀取和查詢但更新卻不頻繁。圖書分類是一個(gè)很好的示例,因?yàn)閳D書館不會(huì)持續(xù)地新建分類,但讀者會(huì)經(jīng)常將搜索限定到分類或子分類中。這么說的原因是實(shí)現(xiàn)是在數(shù)據(jù)庫中的嵌套集合模型中,要求在插入、刪除或修改分類時(shí)更新parent_path列(以及相關(guān)聯(lián)的數(shù)據(jù)庫索引)。那會(huì)非常耗資源,尤其是在并行事務(wù)中執(zhí)行多個(gè)編輯的情況下。
小貼士:如果你在處理動(dòng)態(tài)等級(jí)結(jié)構(gòu),標(biāo)準(zhǔn)的parent_id和child_ids關(guān)聯(lián)會(huì)通過避免表級(jí)鎖來形成更好的性能。
向模型添加約束驗(yàn)證
模型可擁有阻止它們輸入不想要的條件的驗(yàn)證。
可以使用兩種不同類型的約束:
- 數(shù)據(jù)庫級(jí)別的約束檢查
- 服務(wù)級(jí)別的約束檢查
數(shù)據(jù)庫級(jí)別的約束由PostgreSQL所支持的約束進(jìn)行限制。最常用的是UNIQUE約束,但也可使用CHECK和EXCLUDE約束。如果這還無法滿足需要,可以編寫Python代碼來使用Odoo服務(wù)級(jí)別的約束。
我們將使用第四章 創(chuàng)建Odoo插件模塊中所創(chuàng)建的圖書模型,并向其添加一些約束。我們會(huì)添加一個(gè)數(shù)據(jù)庫約束來防止重復(fù)的書名,以及一個(gè) Python 模型約束來防止使用未來的日期作為發(fā)行日期。
準(zhǔn)備工作
本節(jié)中,我們將在library.book模型中添加約束。為此我們使用第四章 創(chuàng)建Odoo插件模塊中的my_library模型。
我們預(yù)期它至少應(yīng)包含如下內(nèi)容:
from odoo import models, fields
class LibraryBook(models.Model):
_name = 'library.book'
name = fields.Char('Title', required=True)
date_release = fields.Date('Release Date')
如何操作...
我們將在models/library_book.py Python文件中編輯LibraryBook類:
- 添加模型屬性來創(chuàng)建數(shù)據(jù)庫約束:
class LibraryBook(models.Model):
# ...
_sql_constraints = [
('name_uniq',
'UNIQUE (name)',
'Book title must be unique.')
]
- 添加一個(gè)模型方法來創(chuàng)建Python代碼約束:
from odoo import api, models
from odoo.exceptions import ValidationError
class LibraryBook(models.Model):
# ...
@api.constrains('date_release')
def _check_release_date(self):
for record in self:
if record.date_release and
record.date_release > fields.Date.today():
raise models.ValidationError('Release date must be in the past')
在對(duì)這些代碼文件進(jìn)行修改后,需要升級(jí)模塊并重啟服務(wù)。
運(yùn)行原理...
第1步在模型表中創(chuàng)建了一個(gè)數(shù)據(jù)庫約束。這是在數(shù)據(jù)庫級(jí)別進(jìn)行的強(qiáng)制。_sql_constraints模型屬性接收一個(gè)待創(chuàng)建的約束列表。每個(gè)約束由一個(gè)三個(gè)元素的元組定義,如下所示:
- 約束標(biāo)識(shí)符所使用的后綴。本例中,我們使用了name_uniq,產(chǎn)生的約束名稱為library_book_name_uniq。
- PostgreSQL中用于修改或創(chuàng)建數(shù)據(jù)表的SQL指令。
- 在違反約束時(shí)向用戶報(bào)出的消息。
我們?cè)谇懊嬉呀?jīng)提到,也可以使用其它數(shù)據(jù)表約束。注意列級(jí)約束如NOT NULL不能以這種方式進(jìn)行使用。有關(guān)PostgreSQL的通用約束以及具體的數(shù)據(jù)表約束更詳細(xì)的信息,請(qǐng)參見https://www.postgresql.org/docs/current/ddl-constraints.html。
在第2步中,我們添加了一個(gè)方法來執(zhí)行Python代碼驗(yàn)證。它使用了@api.constrains裝飾器,表示在參數(shù)列表中字段發(fā)生變化時(shí)應(yīng)執(zhí)行它來運(yùn)行檢查 。如果檢查失敗,會(huì)拋出一個(gè)ValidationError異常。
擴(kuò)展知識(shí)...
通常如果有復(fù)雜的驗(yàn)證約束,可以使用@api.constrains,但對(duì)于簡(jiǎn)單用例,也可以使用帶有CHECK選項(xiàng)的_sql_constraints??聪孪旅娴氖纠乐褂脩籼砑記]有頁面數(shù)或頁面數(shù)或負(fù)值的圖書:
_sql_constraints = [
('positive_page', 'CHECK(pages>0)', 'No. of pages must be positive')
]
向模型添加計(jì)算字段
有時(shí),我們的字段需要通過計(jì)算獲取值或從相同記錄或關(guān)聯(lián)記錄中的值獲取值。一個(gè)典型的示例是總額,由單價(jià)乘以數(shù)量計(jì)算所得。在Odoo模型中,可使用計(jì)算字段來實(shí)現(xiàn)。
為顯示計(jì)算字段如何運(yùn)作,我們將向圖書模型中添加一個(gè)字段來計(jì)算圖書發(fā)行日之后的天數(shù)。
也可以讓計(jì)算字段可編輯和可搜索。我們也會(huì)在示例中進(jìn)行實(shí)現(xiàn)。
準(zhǔn)備工作
我們將復(fù)用第四章 創(chuàng)建Odoo插件模塊中的my_library插件模塊。
如何操作...
我們將編輯models/library_book.py代碼文件來新增一個(gè)字段及支持它的邏輯的方法:
- 首先向圖書模型添加一個(gè)新字段:
class LibraryBook(models.Model):
# ...
age_days = fields.Float(
string='Days Since Release',
compute='_compute_age',
inverse='_inverse_age',
search='_search_age',
store=False, # optional
compute_sudo=False # optional
)
- 然后,添加一個(gè)值計(jì)算邏輯的方法:
# ...
from odoo import api # if not already imported
# ...
class LibraryBook(models.Model):
# ...
@api.depends('date_release')
def _compute_age(self):
today = fields.Date.today()
for book in self.filtered('date_release'):
delta = today - book.date_release
book.age_days = delta.days
- 要添加方法及實(shí)現(xiàn)客入計(jì)算字段的邏輯,使用如下代碼:
from datetime import timedelta
# ...
class LibraryBook(models.Model):
# ...
def _inverse_age(self):
today = fields.Date.today()
for book in self.filtered('date_release'):
d = today - timedelta(days=book.age_days)
book.date_release = d
- 使用如下代碼實(shí)現(xiàn)允許你在計(jì)算字段中進(jìn)行搜索的邏輯:
from datetime import timedelta
class LibraryBook(models.Model):
# ...
def _search_age(self, operator, value):
today = fields.Date.today()
value_days = timedelta(days=value)
value_date = today - value_days
# convert the operator:
# book with age > value have a date < value_date
operator_map = {
'>': '<', '>=': '<=',
'<': '>', '<=': '>=',
}
new_op = operator_map.get(operator, operator)
return [('date_release', new_op, value_date)]
需升級(jí)模塊并重啟Odoo來正確地啟用這些新條件。
運(yùn)行原理...
計(jì)算字段的定義和普通字段一致,不同的是添加了一個(gè)compute屬性來指定用作計(jì)算的方法名。
它們的相似性帶有欺騙性,因?yàn)橛?jì)算字段的內(nèi)部與普通字段有非常大的不同。計(jì)算字段是在運(yùn)行時(shí)動(dòng)態(tài)計(jì)算的,并且除非你自己特別的添加支持,否則它們是不可寫、不可搜索的。
計(jì)算字段在運(yùn)行時(shí)動(dòng)態(tài)計(jì)算,但ORM使用緩存來避免在每次訪問值時(shí)的低效重計(jì)算。因此,它需要知道所依賴的其它字段。它使用@depends裝飾器來監(jiān)測(cè)緩存值何時(shí)應(yīng)置為無效并重新計(jì)算。
??確保compute函數(shù)總是為計(jì)算字段設(shè)置一個(gè)值。否則會(huì)拋出錯(cuò)誤。這在代碼中包含if條件而對(duì)計(jì)算字段設(shè)置值失敗時(shí)會(huì)發(fā)生。那樣會(huì)很難進(jìn)行調(diào)試。
寫操作可通過實(shí)現(xiàn)inverse函數(shù)來添加。這使用分配給計(jì)算字段的值來更新原字段。當(dāng)然,這只在較簡(jiǎn)單的計(jì)算中有意義,但還是有些用例可以用到它的。在我們的示例中, 我們讓通過編輯Days Since Release計(jì)算字段來設(shè)置圖書發(fā)布日期成為可能。search是可選屬性,如果不想讓該計(jì)算字段可編輯,可以忽略它。
也可以通過將search屬性設(shè)置為方法名(類似compute和inverse)來讓非存儲(chǔ)的計(jì)算字段可搜索。類似inverse,search也是可選屬性,如果不想讓該計(jì)算字段可搜索,可以忽略它。
但是,這個(gè)方法預(yù)期不實(shí)現(xiàn)實(shí)際的搜索。而是接收用于搜索該字段的運(yùn)算符和值來作為參數(shù),并預(yù)期返回一個(gè)帶有用于替換搜索條件的域。在我們的示例中,我們將一個(gè)Days Since Release 字段的搜索轉(zhuǎn)換為Release Date字段上等價(jià)的搜索條件。
可選的store=True標(biāo)記存儲(chǔ)數(shù)據(jù)庫中的字段。在這種情況下,執(zhí)行計(jì)算后字段值會(huì)存儲(chǔ)在數(shù)據(jù)庫中,此后它們會(huì)像普通字段一樣進(jìn)行獲取,而不是運(yùn)行時(shí)重新計(jì)算。借助@api.depends裝飾器,ORM會(huì)知道何時(shí)需要重新計(jì)算并更新這些存儲(chǔ)值。你可以把它看作一個(gè)持久緩存。它還具有可將該字段作為搜索條件的好處,包含通過運(yùn)算排序和分組,而無需實(shí)現(xiàn)search方法。
compute_sudo=True標(biāo)記用于需要提權(quán)來執(zhí)行計(jì)算的情況。這種情況可能是計(jì)算時(shí)需要使用終端用戶無法訪問的數(shù)據(jù)。
??使用它時(shí)需要小心,因?yàn)樗鼤?huì)跳過權(quán)限規(guī)則 ,包含多公司設(shè)置的按公司分隔的規(guī)則。確保反復(fù)確認(rèn)在計(jì)算中所使用的域來避免相關(guān)的問題。
暴露存儲(chǔ)在其它模型中的關(guān)聯(lián)字段
在從服務(wù)端讀取數(shù)據(jù)時(shí),Odoo客戶端僅能獲取模型中存在的字段及查詢的值??蛻舳舜a不同于服務(wù)端,無法使用點(diǎn)號(hào)標(biāo)記來獲取關(guān)聯(lián)表中的數(shù)據(jù)。
但是,這些字段可通過將它們添加為關(guān)聯(lián)字段來進(jìn)行訪問。我們將會(huì)讓圖書模型中的出版社城市可以被訪問。
準(zhǔn)備工作
我們將復(fù)用第四章 創(chuàng)建Odoo插件模塊中的my_library插件模塊。
如何操作...
編輯models/library_book.py文件添加一個(gè)新關(guān)聯(lián)字段:
- 確保我們有一個(gè)圖書出版社的字段:
class LibraryBook(models.Model):
# ...
publisher_id = fields.Many2one(
'res.partner', string='Publisher')
- 接著,為出版社城市添加一個(gè)關(guān)聯(lián)字段:
# class LibraryBook(models.Model):
# ...
publisher_city = fields.Char(
'Publisher City',
related='publisher_id.city',
readonly=True)
最后,我們需要升級(jí)該插件模塊來讓新字段在模型中可用。
運(yùn)行原理...
關(guān)聯(lián)字段和普通字段相似,但是有一個(gè)額外的屬性related,帶有一個(gè)字符串供分隔的字段鏈遍歷。
在本例中,我們通過publisher_id訪問出版社的關(guān)聯(lián)記錄,然后讀取它的city字段。我們還可使用更長(zhǎng)的鏈?zhǔn)?,例?publisher_id.country_id.country_code。
注意在本節(jié)中,我們?cè)O(shè)置關(guān)聯(lián)字段為只讀。如果不這么做,字段將可寫,用戶可能會(huì)修改其值。這會(huì)產(chǎn)生修改關(guān)聯(lián)出版社城市字段值的影響。這可能會(huì)既有用又有副作用,在操作時(shí)應(yīng)小心;所有由相同出版社出版的圖書的publisher_city都會(huì)被更新,這可能會(huì)在用戶的預(yù)料之外。
擴(kuò)展知識(shí)...
關(guān)聯(lián)字段實(shí)際上是計(jì)算字段。它們僅提供一種方便的快捷語法來從關(guān)聯(lián)模型讀取字段值。作為一個(gè)計(jì)算字段,這意味著也可以使用store屬性。作為快捷方式,它們也擁有引用字段的所有屬性,如name, translatable和required。
此外,它支持一個(gè)類似compute_sudo的related_sudo標(biāo)記,在設(shè)置為True時(shí),字段鏈會(huì)在不進(jìn)行用戶權(quán)限檢查的情況下進(jìn)行遍歷。
小貼士:在create()方法中使用關(guān)聯(lián)字段會(huì)影響到性能,因此這些字段的計(jì)算會(huì)延遲到它們創(chuàng)建結(jié)束的時(shí)候。因此 ,如果有一個(gè)One2many關(guān)聯(lián),如sale.order和sale.order.line模型,有一個(gè)line模型的關(guān)聯(lián)字段引用訂單模型的一個(gè)字段,你需要在記錄創(chuàng)建時(shí)在訂單模型中顯式地讀取該字段,而不是使用關(guān)聯(lián)字段快捷方式,尤其是在有很多訂單條目(line)時(shí)。
使用引用字段添加動(dòng)態(tài)關(guān)聯(lián)
對(duì)于引用字段,首先我們需要決定關(guān)聯(lián)的目標(biāo)模型(或comodel)。但有時(shí)我們會(huì)讓用戶來做決定,首先選定我們所要的模型然后記錄想要關(guān)聯(lián)的記錄。
在Odoo中這通過使用引用字段來實(shí)現(xiàn)。
準(zhǔn)備工作
我們將復(fù)用第四章 創(chuàng)建Odoo插件模塊中的my_library插件模塊。
如何操作...
編輯models/library_book.py文件來添加新的關(guān)聯(lián)字段:
- 首先,我們需要添加一個(gè)幫助方法來運(yùn)行構(gòu)建一個(gè)可選目標(biāo)模型列表:
from odoo import models, fields, api
class LibraryBook(models.Model):
# ...
@api.model
def _referencable_models(self):
models = self.env['ir.model'].search([
('field_id.name', '=', 'message_ids')])
return [(x.model, x.name) for x in models]
- 然后,我們需要添加Reference字段來使用上述的函數(shù)提供一個(gè)可選模型列表:
ref_doc_id = fields.Reference(
selection='_referencable_models',
string='Reference Document')
因?yàn)槲覀冃薷牧四P偷慕Y(jié)構(gòu),需要升級(jí)模塊來啟動(dòng)這些修改。
運(yùn)行原理...
引用字段類似于many-to-one字段,不同的是它們?cè)试S用戶選擇要關(guān)聯(lián)的模型。
目標(biāo)模型可通過由selection屬性提供的列表進(jìn)行選擇。selection屬性應(yīng)是一個(gè)包含兩個(gè)元素的元組,第一個(gè)元素是模型的內(nèi)部標(biāo)識(shí)符,第二個(gè)是它的文件描述。
以下是一個(gè)示例:
[('res.users', 'User'), ('res.partner', 'Partner')]
但是,不需要提供一個(gè)固定的列表,我們可以使用最通用的模型。為進(jìn)行簡(jiǎn)化,我們使用帶有消息功能的所有模型。使用_referencable_models方法,我們動(dòng)態(tài)地提供了一個(gè)模型列表。
本節(jié)一開始提供了一個(gè)函數(shù)來瀏覽所有模型記錄,可供動(dòng)態(tài)引用來創(chuàng)建用于提供給selection屬性的列表。雖然兩種形式都允許,我們?cè)谝?hào)內(nèi)聲明了函數(shù)名,而不是不加引號(hào)直接引用函數(shù)。這更為靈活,比如它允許所引用的函數(shù)可以在代碼的后面進(jìn)行定義,在使用直接引用時(shí)則不能這么做。
該函數(shù)需要一個(gè)@api.model裝飾器,因?yàn)樗谀P图?jí)別而非記錄集級(jí)別上進(jìn)行操作。
??雖然這個(gè)功能看起來很棒,它運(yùn)行的開銷會(huì)很大。使用引用字段顯示大量記錄(如在列表視圖中)會(huì)帶來很重的數(shù)據(jù)庫負(fù)載,因?yàn)槊總€(gè)值都需在一個(gè)單獨(dú)的查詢中進(jìn)行查找。它也不能像常規(guī)關(guān)聯(lián)字段那樣利用數(shù)據(jù)庫的引用一致性。
使用繼承向模型添加功能
Odoo一個(gè)最重要的功能是模塊插件可以繼承其它模塊插件中定義的功能,而無需編輯原功能中的代碼。這可以是添加字段或方法,修改已有字段或繼承已有方法來執(zhí)行額外的邏輯。
這是繼承中最常使用的方法,在官方文檔中稱之為傳統(tǒng)繼承或經(jīng)典繼承。
我們將繼承內(nèi)置的Partner模型來添加所著書數(shù)量的計(jì)算字段。這包含對(duì)已有模型添加一個(gè)字段或一個(gè)方法。
準(zhǔn)備工作
我們將復(fù)用第四章 創(chuàng)建Odoo插件模塊中的my_library插件模塊。
如何操作...
我們將繼承內(nèi)置的Partner模型。應(yīng)在其自身的Python代碼文件中實(shí)現(xiàn),但為了簡(jiǎn)化講解,我們將復(fù)用 models/library_book.py代碼文件:
- 首先,我們將確保在Partner模型中有authored_book_ids反向關(guān)聯(lián)并添加該計(jì)算字段:
class ResPartner(models.Model):
_inherit = 'res.partner'
_order = 'name'
authored_book_ids = fields.Many2many(
'library.book', string='Authored Books')
count_books = fields.Integer( 'Number of Authored Books',
compute='_compute_count_books' )
- 然后,添加需要用于計(jì)算圖書數(shù)量的方法:
# ...
from odoo import api # if not already imported
# class ResPartner(models.Model):
# ...
@api.depends('authored_book_ids')
def _compute_count_books(self):
for r in self:
r.count_books = len(r.authored_book_ids)
最后,我們需要升級(jí)這個(gè)插件模塊來讓修改生效。
運(yùn)行原理...
在模型類通過_inherit屬性進(jìn)行定義時(shí),它向所繼承模型添加了修改,而沒有進(jìn)行替換。
這意味著在繼承類中中定義的字段會(huì)在父級(jí)模型中新增或修改。在數(shù)據(jù)庫層,ORM對(duì)同一張數(shù)據(jù)表添加字段。
字段也被增量修改。這表示如果該字段在父類中已存在,僅修改在繼承類中聲明的屬性,其它的保持原有父類中的內(nèi)容不變。
在繼承類中定義的方法替換父類中的方法。如果你不通過super調(diào)用觸發(fā)父級(jí)方法,那么父級(jí)版本的方法則不會(huì)被調(diào)用,我們也就不擁有該項(xiàng)功能。因此,當(dāng)你通過繼承在已有方法中添加新邏輯時(shí),應(yīng)包含一個(gè)帶有super的語句來調(diào)用其父類中的方法。這在第六章 基本服務(wù)端部署中做進(jìn)一步的講解。
??本節(jié)會(huì)向已有模型新增字段。如果你想在已有視圖(用戶界面)添加這些新字段的話,參見第十章 后端視圖中的修改已有視圖 - 視圖繼承一節(jié)。
擴(kuò)展知識(shí)...
通過_inherit經(jīng)典繼承,也可以將父級(jí)模型的功能拷貝到一個(gè)全新的模型中。這通過添加一個(gè)在帶有不同標(biāo)識(shí)符的_name類屬性來實(shí)現(xiàn)。以下是一個(gè)示例:
class LibraryMember(models.Model):
_inherit = 'res.partner'
_name = 'library.member'
新模型有其自己的數(shù)據(jù)表,包含完全獨(dú)立于res.partner父模型的自身數(shù)據(jù)。因其仍繼承Partner模型,此后的任意修改也會(huì)影響到新模型。
在官方文檔中,這被稱為原型繼承,但在實(shí)踐中鮮有使用。原因在于代理繼承通??梢愿咝У姆绞綕M足了這一需求,也無需復(fù)制數(shù)據(jù)結(jié)構(gòu)。參見本章中的使用代理繼承將功能拷貝至另一個(gè)模型一節(jié)了解更多內(nèi)容。
為可復(fù)用模型功能使用抽象模型
有時(shí),會(huì)有一個(gè)具體的功能,我們想要添加到幾個(gè)不同的模型中。在不同的文件中重復(fù)相同代碼基本上是一種不良編程實(shí)踐,最好可以一次實(shí)現(xiàn)多次復(fù)用。
抽象模型讓我們可以創(chuàng)建一個(gè)通用模型來實(shí)現(xiàn)一些功能,然后由普通模型進(jìn)行繼承以使用該功能。
作為示例,我們將實(shí)現(xiàn)一個(gè)簡(jiǎn)單的存檔功能。它將active字段加入到模型中(如果尚未存在)并添加一個(gè)存儲(chǔ)方法來切換active標(biāo)記。這可以生效是因?yàn)閍ctive是一個(gè)魔法字段,如果默認(rèn)在模型中出現(xiàn),active=False 的記錄會(huì)在查詢中被過濾掉。
下面我們將在圖書模型中添加它。
準(zhǔn)備工作
我們將復(fù)用第四章 創(chuàng)建Odoo插件模塊中的my_library插件模塊。
如何操作...
存儲(chǔ)功能顯然可獨(dú)立為一個(gè)插件模塊或者至少應(yīng)有自己的Python代碼文件。但為保持講解盡可能簡(jiǎn)單,我們將會(huì)把它塞到models/library_book.py文件中:
- 為存檔功能添加抽象模型。應(yīng)在使用它的圖書模型中定義:
class BaseArchive(models.AbstractModel):
_name = 'base.archive'
active = fields.Boolean(default=True)
def do_archive(self):
for record in self:
record.active = not record.active
- 接著,我們將編輯圖書模型來繼承存檔模型:
class LibraryBook(models.Model):
_name = 'library.book'
_inherit = ['base.archive']
# ...
需要對(duì)插件模塊進(jìn)行升級(jí) 來讓修改生效。
運(yùn)行原理...
抽象模型由基于models.AbstractModel的類進(jìn)行創(chuàng)建,而非常用的models.Model。它擁有常規(guī)模型的所有屬性和功能,區(qū)別在于ORM不會(huì)在數(shù)據(jù)庫中創(chuàng)建實(shí)際的體現(xiàn)。這表示它不能存儲(chǔ)任何數(shù)據(jù)。僅用作添加到常規(guī)模型中的可復(fù)用功能的一個(gè)模板。
我們的存檔抽象模型非常簡(jiǎn)單,僅添加active字段和一個(gè)方法來切換active標(biāo)記的值,我們將在稍后在用戶界面中通過按鈕進(jìn)行使用。
模型類中定義了_inherit屬性時(shí),它繼承這些類的屬性方法,定義在當(dāng)前類中的屬性方法對(duì)這些繼承功能進(jìn)行修改。
這里所采用的機(jī)制與常規(guī)模型繼承相同(如使用繼承向模型添加功能一節(jié))。你可能注意到了_inherit使用一個(gè)模型標(biāo)識(shí)符列表而不是帶有一個(gè)模型標(biāo)識(shí)符的字符串。其實(shí)_inherit可以使用這兩種形式。使用列表形式允許我們繼承多個(gè)(尤其是抽象)類。在本例中,我們僅繼承了一個(gè)類,因此使用文本字符串也沒有問題。為進(jìn)行演示我們使用了列表。
擴(kuò)展知識(shí)...
值得注意的內(nèi)置抽象模型是mail.thread,這由mail(Discuss)插件模塊提供。在模型中它啟用討論功能來驅(qū)動(dòng)在不同表單底部看到的消息墻。
AbstractModel外,還有第三種模型類型:models.TransientModel。這像models.Model有一個(gè)數(shù)據(jù)庫體現(xiàn),但所創(chuàng)建的記錄供臨時(shí)使用,會(huì)定期由服務(wù)端調(diào)試任務(wù)清除。除此之后,臨時(shí)模型和常規(guī)模型的功能一致。
models.TransientModel對(duì)于稱之為向?qū)У母鼮閺?fù)雜的用戶交互會(huì)更為有用。該向?qū)в糜趶挠脩粽?qǐng)求輸入。在第九章 高級(jí)服務(wù)端開發(fā)技巧中,我們探討如何使用它們來實(shí)現(xiàn)高級(jí)用戶交互。
使用代理繼承將功能拷貝至另一個(gè)模型
傳統(tǒng)繼承使用_inherit執(zhí)行原位修改來繼承模型的功能。
但是有一些情況下,我們不想修改已有模型,而是基于已有模型新建一個(gè)模型來使用其已有的功能。這借由Odoo的代理繼承實(shí)現(xiàn),使用_inherits模型屬性(注意這里多一個(gè) s)。
傳統(tǒng)繼承與面向?qū)ο缶幊痰母拍钣泻艽蟛煌4砝^承則與其相似,其中可創(chuàng)建一個(gè)新的模型來包含父級(jí)模型中的功能。它還支持多態(tài)繼承,這時(shí)從兩個(gè)或多個(gè)其它的模型中進(jìn)行繼承。
我們的圖書館中有多本書籍。是時(shí)候修改讓圖書館擁有會(huì)員了。對(duì)于圖書會(huì)員,我們需要Partner模型中的所有身份和地址數(shù)據(jù),也會(huì)想要保留一些有關(guān)會(huì)員的信息:起始日期、結(jié)束日期和會(huì)員卡號(hào)。
向Partner模型添加這些字段不是最好的方案,因?yàn)閷?duì)于非會(huì)員的成員們無需使用到這些。使用一個(gè)帶有額外字段的新模型繼承Partner模型則會(huì)非常好。
準(zhǔn)備工作
我們將復(fù)用第四章 創(chuàng)建Odoo插件模塊中的my_library插件模塊。
如何操作...
新圖書會(huì)員模型應(yīng)有自己的獨(dú)立Python代碼文件,但為保持講解盡可能簡(jiǎn)單,我們將復(fù)用models/library_book.py文件:
- 添加新模型繼承res.partner:
class LibraryMember(models.Model):
_name = 'library.member'
_inherits = {'res.partner': 'partner_id'}
partner_id = fields.Many2one(
'res.partner',
ondelete='cascade')
- 接下來,我們將添加針對(duì)圖書會(huì)員的字段:
# class LibraryMember(models.Model):
# ...
date_start = fields.Date('Member Since')
date_end = fields.Date('Termination Date')
member_number = fields.Char()
date_of_birth = fields.Date('Date of birth')
此時(shí),我們應(yīng)升級(jí)該插件模型來讓修改生效。
運(yùn)行原理...
_inherits模型屬性設(shè)置我們想要繼承的父級(jí)模型。本例中只有一個(gè)res.partner模型。它的值是一個(gè)鍵值對(duì)字典,鍵是被繼承的模型,而值是用于關(guān)聯(lián)它們的字段名。這些是我們必須同時(shí)在模型中定義的Many2one字段。在本例中,partner_id是用于關(guān)聯(lián)父級(jí)模型Partner的字段。
為更好理解它如何運(yùn)行,我們來看在新建會(huì)員時(shí)數(shù)據(jù)庫級(jí)別上會(huì)發(fā)生什么:
- res_partner在表中新建記錄
- 在library_member表中新建記錄
- library_member表中的partner_id字段設(shè)置為所創(chuàng)建的res_partner記錄的id
會(huì)員記錄自動(dòng)關(guān)聯(lián)到一個(gè)新的Partner記錄。它僅是一個(gè) many-to-one關(guān)聯(lián),但代理機(jī)制注入了一些魔力來讓Partner的字段看起來就好像屬于Member記錄一樣,新的Partner記錄會(huì)和新的會(huì)員記錄一同創(chuàng)建。
你可能會(huì)想要知道這個(gè)自動(dòng)創(chuàng)建的Partner記錄并沒有什么特別的。這是一個(gè)常規(guī)的Partner,如果你查看Partner模型,就會(huì)看到這條記錄(當(dāng)然其中不包含那些額外的會(huì)員數(shù)據(jù))。所有的會(huì)員都是成員(Partner),但只有部分成員是會(huì)員。
那么在刪除同時(shí)還是會(huì)員的成員時(shí)會(huì)發(fā)生什么呢?你可通過關(guān)聯(lián)字段的ondelete值來進(jìn)行決定。對(duì)partner_id我們使用了cascade。這表示刪除成員會(huì)同時(shí)刪除對(duì)應(yīng)的會(huì)員。我們可以使用更為保守的設(shè)置restrict來禁止在有關(guān)聯(lián)會(huì)員時(shí)刪除成員。這樣的話只有刪除會(huì)員時(shí)才會(huì)生效。
需要注意代理繼承僅用于字段,而不能用于方法。因此,如果Partner模型有一個(gè)do_something()方法,成員模型不會(huì)自動(dòng)繼承它。
擴(kuò)展知識(shí)...
對(duì)于這個(gè)繼承代理有一個(gè)快捷方式。代替創(chuàng)建一個(gè)_inherits字典,你可以使用在Many2one字段定義中使用delegate=True屬性。這和_inherits選項(xiàng)的功能完全一樣。其主要優(yōu)點(diǎn)是更為簡(jiǎn)潔。在給出的示例中,我們執(zhí)行了與前述相同的繼承代理 ,但在這種情況下,我們對(duì)partner_id字段使用了delegate=True選項(xiàng)來代替_inherits字典的創(chuàng)建:
class LibraryMember(models.Model):
_name = 'library.member'
partner_id = fields.Many2one('res.partner', ondelete='cascade', delegate=True)
date_start = fields.Date('Member Since')
date_end = fields.Date('Termination Date')
member_number = fields.Char()
date_of_birth = fields.Date('Date of birth')
關(guān)于代理繼承一個(gè)值得注意的用例是用戶模型 res.users。它繼承自成員(res.partner)。這表示其中在User中可見的一些字段實(shí)際存儲(chǔ)Partner模型中(尤其是name字段)。在新用戶創(chuàng)建時(shí),我們還獲取了一個(gè)新的自動(dòng)創(chuàng)建的Partner。
還應(yīng)說明帶有_inherit的傳統(tǒng)繼承會(huì)將功能拷貝到新模型中,雖然效率并不高。這在使用繼承向模型添加功能一節(jié)中進(jìn)行了討論。

