Odoo 12開發(fā)者指南第五章 應(yīng)用模型

全書完整目錄請(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í)別的屬性:

  1. 加入如下代碼來添加一個(gè)用戶友好的模型標(biāo)題:
_description = 'Library Book'
  1. 首先對(duì)記錄進(jìn)行排序(按時(shí)間最近排序,然后按標(biāo)題排序),添加如下代碼:
_order = 'date_release desc, name'
  1. 添加如下代碼來使用short_name字段作為記錄的表示:
_rec_name = 'short_name'
    short_name = fields.Char('Short Title', required=True)
  1. 在表單視圖中添加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文件,我們將編輯該文件來新增字段:

  1. 使用最小化語法來向圖書模型添加字段:
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),
            )
  1. 我們已向模型新增了字段。仍需在表單視圖中添加這些字段來在用戶界面中反映出這些修改。參見如下在表單視圖中增加字段的代碼:
<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,然后在模型字段中使用它。

  1. 確保安裝了數(shù)字精度模塊,在頂級(jí)菜單中選擇Apps,刪除默認(rèn)過濾器,搜索Decimal Precision Configuration模塊,若未安裝則進(jìn)行安裝:


    image.png
  2. 通過Settings菜單下的鏈接激活開發(fā)者模式(參見第一章 安裝Odoo開發(fā)環(huán)境中的激活Odoo開發(fā)者工具一節(jié))。這會(huì)啟用Settings > Technical菜單。

  3. 訪問數(shù)字精度設(shè)置。這需要打開Settings頂級(jí)菜單并選擇Technical > Database Structure > Decimal Accuracy。我們應(yīng)該會(huì)看到一個(gè)當(dāng)前定義的設(shè)置列表。

  4. 添加一個(gè)新配置,設(shè)置Usage為Book Price并選擇數(shù)字精度:


    image.png
  5. manifest.py聲明文件中添加這個(gè)新依賴。如下所示:

{ 
        'name': 'Chapter 05 code',
        'depends': ['base', 'decimal_precision'],
        'data': ['views/library_book.xml'] 
    }
  1. 要使用數(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ǔ)模型。我們將編輯該文件來添加所需的字段:

  1. 添加所要使用的字段來存儲(chǔ)幣種:
class LibraryBook(models.Model):
        # ...
        currency_id = fields.Many2one(
            'res.currency', string='Currency')
  1. 添加貨幣字段來存儲(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ì)是如下這樣:

image.png

運(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文件來添加這些字段:

  1. 向圖書模型添加圖書出版商的many-to-one字段:
class LibraryBook(models.Model):
        # ...
        publisher_id = fields.Many2one(
            'res.partner', string='Publisher',
            # optional:
            ondelete='set null',
            context={},
            domain=[],
        )
  1. 為出版社的書籍添加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)行講解。
  1. 我們已在圖書和作者之間創(chuàng)建了一個(gè)many-to-many關(guān)聯(lián),讓我們?cè)俅尾榭匆幌拢?/li>
class LibraryBook(models.Model):
         # ...
         author_ids = fields.Many2many(
            'res.partner', string='Authors')
  1. 相同的關(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,如下:

  1. 在models/init.py中載入如下行來加載新的Python代碼文件:
from . import library_book_categ
  1. 創(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')
  1. 同時(shí)添加如下代碼來啟動(dòng)特別的等級(jí)支持:
_parent_store = True
    _parent_name = "parent_id" # optional if field is 'parent_id'
    parent_path = fields.Char(index=True)
  1. 在模型中添加如下行來新增一個(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.')
  1. 這時(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)庫來獲取代碼。

image.png

運(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類:

  1. 添加模型屬性來創(chuàng)建數(shù)據(jù)庫約束:
class LibraryBook(models.Model):
         # ...
         _sql_constraints = [
         ('name_uniq',
         'UNIQUE (name)',
         'Book title must be unique.')
         ]
  1. 添加一個(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è)字段及支持它的邏輯的方法:

  1. 首先向圖書模型添加一個(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
         )
  1. 然后,添加一個(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
  1. 要添加方法及實(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
  1. 使用如下代碼實(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)字段:

  1. 確保我們有一個(gè)圖書出版社的字段:
class LibraryBook(models.Model):
         # ...
         publisher_id = fields.Many2one(
             'res.partner', string='Publisher')
  1. 接著,為出版社城市添加一個(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)字段:

  1. 首先,我們需要添加一個(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]
  1. 然后,我們需要添加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代碼文件:

  1. 首先,我們將確保在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' )
  1. 然后,添加需要用于計(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文件中:

  1. 為存檔功能添加抽象模型。應(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
  1. 接著,我們將編輯圖書模型來繼承存檔模型:
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文件:

  1. 添加新模型繼承res.partner:
class LibraryMember(models.Model):
         _name = 'library.member'
         _inherits = {'res.partner': 'partner_id'}
         partner_id = fields.Many2one(
             'res.partner',
             ondelete='cascade')
  1. 接下來,我們將添加針對(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)行了討論。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容