ODOO13之六:Odoo 13開發之模型 – 結構化應用數據


 

在本系列文章第三篇Odoo 13 開發之創建第一個 Odoo 應用中,我們概覽了創建 Odoo 應用所需的所有組件。本文及接下來的一篇我們將深入到組成應用的每一層:模型層、視圖層和業務邏輯層。

本文中我們將深入學習模型層,以及學習如何使用模型來設計應用所需的數據結構。我們會探索模型和字段的各項作用,包括定義模型關系、添加計算字段、創建數據約束。

本文的主要內容有:

  • 學習項目 – 優化圖書館應用
  • 創建模型
  • 創建字段
  • 模型間的關系
  • 計算字段
  • 模型約束
  • 了解 Odoo的 base 模型

開發准備

本文代碼基於第三章 Odoo 13 開發之創建第一個 Odoo 應用中所創建的代碼。相關代碼參見 GitHub 倉庫,本文學習完成項目請參見GitHub 倉庫。相關代碼需放在一個 addons 路徑中,然后在 Odoo中安裝了 library_app 模型,本文中例子將會對該模塊修改和新增代碼。

學習項目 – 優化圖書應用

在第三章 Odoo 13 開發之創建第一個 Odoo 應用中,我們創建了一個library_app插件模塊,實現了一個簡單的library.book模型用於展示圖書目錄。本文中,我們將回到該模塊來豐富圖書數據。我們將添加一個分類層級,添加如下用作圖書分類:

  • Name:分類標題
  • Parent:所屬父級分類
  • Subcategories:將此作為父級分類的子分類
  • Featured book或author: 此分類中所選圖書或作者

圖書模型中已有一些基本信息字段,我們會添加一些字段來展示 Odoo中的數據類型。我們還會為圖書模型添加一些約束:

  • 標題和出版日期應唯一
  • 輸入的ISBN應為有效

創建模型

模型是 Odoo 框架的核心,它們描述應用的數據結構,是應用服務和數據庫存儲之間的橋梁。可圍繞模型實現業務邏輯來為應用添加功能,用戶界面也建立在模型之上。下面我們將學習模型的通用屬性,用於影響行為,以及幾種模型類型:普通(regular)、臨時(transient)和抽象(abstract)類型。

模型屬性

模型類可以使用控制其部分行為的額外屬性,以下是最常用的屬性:

  • _name 是我們創建的 Odoo 模型的內部標識符,在創建新模型時為必填。
  • _description是對用戶友好的模塊記錄標題,在用戶界面中查看模型時顯示。可選但推薦添加。
  • _order設置瀏覽模型記錄時或列表視圖的默認排序。其值為 SQL 語句中 order by 使用的字符串,所以可以傳入符合 SQL 語法的任意值,它有智能模式並支持可翻譯及many-to-one字段名。 我們的圖書模型中已使用了_name 和_description屬性,可以添加一個_order屬性來默認以圖書名排序,然后按出版日期倒序排(新出版在前)。  class Book(models.Model): _name = 'library.book' _description = 'Book' _order = 'name, date_published desc' 

在高級用例中還會用到如下屬性:

  • _rec_name在從關聯字段(如many-to-one關聯)中引用時作為記錄描述。默認使用模型中常用的 name字段,但可以指定任意其它字段。
  • _table是模型對應的數據表名。默認表名由 ORM 通過替換模塊名中的點為下划線來自動定義,但是可通過該屬性指定表名。
  • _log_access=False用於設置不自動創建審計追蹤字段:create_uid, create_date, write_uid和write_date。
  • _auto=False 用於設置不自動創建模型對應的數據表。如有需要,可通過重載init()方法來創建數據庫對象:數據表或視圖。 還有用於繼承模塊的_inherit和_inherits屬性,在本文后續會深入學習。

模型和 Python 類

Odoo 模型以 Python 類的形式展現,在前面的代碼中,有一個繼承了 models.Model類的 Python 類:Book,創建了新 Odoo 模型:library.book。Odoo的模型保存在中央注冊表(central registry)中,可通過 env 環境對象(老 API 中稱為 pool)獲取。 它是一個數據庫保存所有可用模型類引用的字典,其中的詞條可通過模型名引用 。具體來說,模型方法中的代碼可使用self.env[‘library.book’]來獲取表示 library.book模型的模型類。

可以看出模型名非常重要,因為它是訪問該注冊表的關鍵。模型名的規則是以點號連接的小寫單詞,如library.book或library.book.category。內核模塊中的其它示例有project.project, project.task和project.task.type。模型名應使用單數,如library.book而非library.books。

ℹ️由於歷史原因,有些內核模型沒有遵循這一規則,如res.users。

模型名必須全局唯一,因此第一個單詞應使用模塊關聯的主應用對應,以圖書應用而言,模型名前綴使用 library。其它示例如內核模塊的project, crm和sale。另一方面 Python 類僅為所聲明文件本地內容,名稱僅需在代碼文件中唯一即可。因為類名不會與其它模塊中的類產生沖突,也就不需為其添加主應用相關的前綴。

類的命名規范是使用駝峰命名法(CamelCase),這與 Python 標准的 PEP8編碼規范一致。

臨時(Transient)模型和抽象模型

在前述代碼中以及在大多數據 Odoo 模型中的類會繼承models.Model類。這類模型在數據庫中持久化存儲:會為模型創建數據表並存儲記錄直至刪除。但 Odoo 中還有另外兩種模型類型:臨時模型和抽象模型。

臨時模型繼承models.TransientModel類,用於向導式的用戶交互。這類數據會存儲在數據庫中,但僅是臨時性的。會定時運行清空 job 來清除這些表中的老數據。比如Settings > Translations菜單下的Load a Language對話窗口,就使用了臨時模型來存儲用戶選擇並實現向導邏輯。在第八章 Odoo 13開發之業務邏輯 – 業務流程的支持中會有討論臨時模型的示例。

抽象模型繼承models.AbstractModel類,它不帶有數據存儲。抽象模型用作可復用的功能集,與使用 Odoo 繼承功能的其它模型配合使用。例如mail.thread是 Discuss 應用中的一個抽象模型,用於為其它模型添加消息和follower 功能。

檢查已有模型

通過 Python 類創建的模型和字段在用戶界面中有自己的元標簽。啟動開發者模式,訪問菜單Settings > Technical > Database Structure > Models,這里有數據庫中的所有模型。點擊列表中的模型會打開詳情表單:

這是一個檢查模型結構很好的工具,因為在這里可以看到不同模塊所有自定義結果。上圖中在右上角 In Apps字段中可以看到library.book模型的定義來自library_app和library_member兩個模塊。下方區域中還有幾個包含附加信息的標簽:

  • Fields可快速查看模型字段
  • Access Rights是授予不同權限組的訪問控制規則
  • Views顯示模型所帶的視圖列表 我們可以通過開發者菜單下的View Metadata選項查看模型的外部標識符。模型的外部標識符或XML ID由 ORM 自動生成,但根據規則可預知,如library.book模型的外部標識符為model_library_book。在定義安全訪問控制列表經常在 CSV 文件中使用到這些XML ID。

小貼士:如第一章 使用開發者模式快速入門 Odoo 13中所見,模型表單是可編輯的。通過這里是可以創建並修改模型、字段和視圖的。可在此處創建原型然后在插件模塊中實現。

創建字段

創建新模型后的第一步是添加字段。Odoo 支持我們能想到的所有基本數據類型,如文本字符串、整型、浮點型、布爾型、日期、日期時間以及圖片或二進制數據。下面就來看看 Odoo 中一些可用的字段類型吧。

基本字段類型

我們將為圖書模型添加幾種可用的字段類型,編輯library_app/models/library_book.py文件后 Book 類會長這樣:

class Book(models.Model): ... # String fields name = fields.Char('Title', required=True) isbn = fields.Char('ISBN') book_type = fields.Selection( [('paper', 'Paperback'), ('hard', 'Hardcover'), ('electronic', 'Electronic'), ('other', 'Other')], 'Type') notes = fields.Text('Internal Notes') descr = fields.Html('Description') # Numeric fields: copies = fields.Integer(default=1) avg_rating = fields.Float('Average Rating', (3,2)) price = fields.Monetary('Price', 'currency_id') currency_id = fields.Many2one('res.currency') # price helper # Date and time fields date_published = fields.Date() last_borrow_date = fields.Datetime( 'Last Borrowed On', default=lambda self: fields.Datetime.now()) # Other fields active = fields.Boolean('Active?', default=True) image = fields.Binary('Cover') # Relational Fields ... 

此處是 Odoo 中所帶的非關聯字段示例,每個字段都帶有所需的位置參數。

ℹ️Python 中有兩類參數:位置參數和關鍵字參數。位置參數需按指定順序使用。例如,f(x, y)應以f(1, 2)方式調用。關鍵字參數通過參數名傳遞。如同一個例子,可使用f(x=1, y=2)甚至是f(1, y=2)兩種傳參方式混用。更多有關關鍵字參數知識參見 Python 官方文檔。

對於大多數非關聯字段,第一個參數是字段標題,與字符串字段參數相對應。它用作用戶界面標簽的默認文本。這個參數是可選的,如未傳入,會根據字段名將下划線替換為空格並將單詞首字母大寫來自動生成。以下為可用的非關聯字段類型以及其對應的位置參數:

  • Char(string)是一個單行文本,唯一位置參數是string字段標簽。
  • Text(string)是一個多行文本,唯一位置參數是string字段標簽。
  • Selection(selection, string)是一個下拉選擇列表。選項位置參數是一個[(‘value’, ‘Title’),]元組列表。元組第一個元素是存儲在數據庫中的值,第二個元素是展示在用戶界面中的描述。該列表可由其它模塊使用selection_add關鍵字參數擴展。
  • Html(string)存儲為文本字段,但有針對用戶界面 HTML 內容展示的特殊處理。出於安全考慮,該字段會被清洗,但清洗行為可被重載。
  • Integer(string)僅需字段標題字符串參數。
  • Float(string, digits)帶有第二個可選參數digits,該字段是一個指定字段精度的(x,y)元組,x 是數字總長,y 是小數位。
  • Monetary(string, currency_field)與浮點字段類似,但帶有貨幣的特殊處理。第二個參數currency_field用於存儲所使用貨幣,默認應傳入currency_id字段。
  • Date(string)和Datetime(string)字段只需一個字符串文本位置參數。
  • Boolean(string)的值為True 或False,可傳入一個字符串文本位置參數。
  • Binary(string)存儲文件類二進制文件,只需一個字符串文本位置參數。它可由Python使用 base64編碼字符串進行處理。

ℹ️Odoo 13中的修改

Date和Datetime 字段現在 ORM 中作為日期對象處理。此前的版本中作為文本字符串處理,進行操作時需與 Python 日期對象間進行轉換。

文本字符串:Char, Text和Html有一些特有屬性:

  • size (Char)設置最大允許尺寸。無特殊原因建議不要使用,例如可用於帶有最大允許長度的社保賬號。
  • translate使用得字段內容可翻譯,帶有針對不同語言的不同值。
  • trim默認值為 True,啟動在網絡客戶端中自動去除周圍的空格。可通過設置trim=false來取消。

ℹ️Odoo 12中的修改 trim字段屬性在 Odoo 12中引入,此前版本中文本字段保存前后的空格。

除這些以外,還有在后面會介紹到的關聯字段。不過, 我們還要先了解下有關字段屬性的其它知識。

常用字段屬性

字段還有一些其它屬性供我們定義其行為。以下是常用的屬性,通常都作為關鍵字參數:

  • string是字段的默認標簽,在用戶界面中使用。除Selection和關聯字段外,它都是第一個位置參數,所以大多數情況下它用作關鍵字參數。如未傳入,將由字段名自動生成。
  • default設置字段默認值。可以是具體值(如 active字段中的default=True),或是可調用引用,有名函數或匿名函數均可。
  • help提供 UI 中鼠標懸停字段向用戶顯示的提示文本。
  • readonly=True會使用戶界面中的字段默認不可編輯。在 API 層面並沒有強制,模型方法中的代碼仍然可以向其寫入。僅針對用戶界面設置。
  • required=True使得用戶界面中字段默認必填。這通過在數據庫層面為列添加NOT NULL 約束來實現。
  • index=True為字段添加數據庫索引,讓搜索更快速,但同時也會部分降低寫操作速度。
  • copy=False讓字段在使用 ORM copy()方法復制字段時忽略該字段。除 to-many 關聯字段外,其它字段值默認會被復制。
  • groups可限制字段僅對一些組可訪問並可見。值為逗號分隔的安全組XML ID列表,如groups=’base.group_user,base.group_system’。
  • states傳入依賴 state字段值的 UI 屬性的字典映射值。可用屬性有readonly, required和invisible,例如states={‘done’:[(‘readonly’,True)]}。

ℹ️注意states 字段等價於視圖中的 attrs 屬性。同時注意視圖也支持 states 屬性,但用途不同,傳入逗號分隔的狀態列表來控制元素什么時候可見。

以下為字段屬性關鍵字參數的使用示例:

    name = fields.Char( 'Title', default=None, index=True, help='Book cover title', readonly=False, required=True, translate=False, ) 

如前所述,default 屬性可帶有固定值,或引用函數來自動計算默認值。對於簡單運算,可使用 lambda 函數來避免過重的有名函數或方法的創建。以下是一個計算當前日期和時間默認值的常用示例:

    last_borrow_date = fields.Datetime(
        'Last Borrowed On', default=lambda self: fields.Datetime.now(), ) 

默認值也可以是一個函數引用,或待定義函數名字符串:

    def _default_last_borrow_date(self): return fields.Datetime.now() last_borrow_date = fields.Datetime( 'Last Borrowed On', default=_default_last_borrow_date, ) 

當模塊數據結構在不同版本中變更時以下兩個屬性非常有用:

  • deprecated=True在字段被使用時記錄一條 warning 日志
  • oldname=’field’是在新版本中重命名字段時使用,可在升級模塊時將老字段中的數據自動拷貝到新字段中

特殊字段名

一些字段名很特別,可能是因為它們出於特殊目的作為 ORM 保留字,或者是由於內置功能使用了一些默認字段名。id 字段保留以用作標識每條記錄的自增數字以及數據庫主鍵,每個模型都會自動添加。

以下字段只要模型中沒設置_log_access=False都會在新模型中自動創建:

  • create_uid為創建記錄的用戶
  • create_date是記錄創建的日期和時間
  • write_uid是最后寫入記錄的用戶
  • write_date是最后修改記錄的日期和時間

每條記錄的這些字段信息都可通過開發者菜單下的View Metadata進行查看。一些內置 API 功能默認需要一些指定字段名。避免在不必要的場合使用這些字段名會讓開發更輕松。其中有些字段名被保留並且不能在其它地方使用:

  • name (通常為 Char)默認作為記錄的顯示名稱。通過是一個 Char,但也可以是 Text 或Many2one字段類型。用作顯示名的字段可修改為_rec_name模型屬性。
  • active (Boolean型)允許我們關閉記錄。帶有active=False的記錄會自動從查詢中排除掉。可在當前上下文中添加{‘active_test’: False} 來關閉這一自動過濾。可用作記錄存檔或假刪除(soft delete)。
  • state (Selection類型) 表示記錄生命周期的基本狀態。它允許使用states字段屬性來根據記錄狀態以具備不同的 UI 行為。動態修改視圖:字段可在特定記錄狀態下變為readonly, required或invisible。
  • parent_id和parent_path Integer和Char型)對於父子層級關系具有特殊意義。本文后續會進行討論。

ℹ️Odoo 12中的修改

層級關聯現在使用parent_path字段,它替代了老版本中已淘汰的parent_left和 parent_right字段(整型)。

到目前為止我們討論的都是非關聯字段。但應用數據結構中很大一部分是描述實體間關聯的。下面就一起來學習。

模型間的關系

中、大型業務應用有一個結構數據模型,需要關聯所涉及到的不同實體間的數據。要實現這點,需要使用關聯字段。再來看看我們的圖書應用,圖書模型中有如下關系:

每本書有一個出版商。這是一個many-to-one 關聯,在數據庫引擎中通過外鍵實現。反過來則是one-to-many關聯,表示一個出版商可出版多本書。

每本書可以有多名作者。這是一個many-to-many關聯,反過來還是many-to-many關聯,因為一個作者也可以有多本書。

下面我們就會分別討論這些關聯。具體的用例就是層級關聯,即一個模型中的記錄與同模型中的其它記錄關聯。我們將引入一個圖書分類模型解釋這一情況。最后,Odoo 框架還支持彈性關系,即一個字段可指向其它表中的字段,這稱為引用字段。

Many-to-one關聯

many-to-one關聯是對其它模型中記錄的引用,例如在圖書模型中,publisher_id表示圖書出版商,是對partner記錄的一個引用:

    publisher_id = fields.Many2one( 'res.partner', string='Publisher') 

與所有關聯字段一樣,Many2one字段的第一個位置參數是關聯模型(comodel關鍵字參數)。第二位置參數是字段標簽(string關鍵字參數),但這和其它關聯字段不同,所以推薦使用像以上代碼一樣一直使用string關鍵字參數。

many-to-one模型字段在數據表中創建一個字段,並帶有指向關聯表的外鍵,其中為關聯記錄的數據庫 ID。以下是many-to-one字段可用的關鍵字參數:

  • ondelete定義關聯記錄刪除時執行的操作:
  • set null (默認值): 關聯字段刪除時會置為空值
  • restricted:拋出錯誤阻止刪除
  • cascade:在關聯記錄刪除時同時刪除當前記錄
  • context是一個數據字典,可在瀏覽關聯時為網頁客戶端傳遞信息,比如設置默認值。第八章 Odoo 13開發之業務邏輯 – 業務流程的支持中會做深入說明。
  • domain是一個域表達式:使用一個元組列表過濾記錄來作為關聯記錄選項,第八章 Odoo 13開發之業務邏輯 – 業務流程的支持中會詳細說明。
  • auto_join=True允許ORM在使用關聯進行搜索時使用SQL連接。使用時會跳過訪問安全規則,用戶可以訪問安全規則不允許其訪問的關聯記錄,但這樣 SQL 的查詢會更有效率且更快。

One-to-many反向關聯

one-to-many關聯是many-to-one的反向關聯。它列出引用該記錄的關聯模型記錄。比如在圖書模型中,publisher_id與 parnter 模型是一個many-to-one關聯。這說明partner與圖書模型可以有一個one-to-many的反向關聯,列出每個出版商出版的圖書。

要讓關聯可用,我們可在 partner 模型中添加它,在library_app/models/res_partner.py文件中添加如下代碼:

from odoo import fields, models class Partner(models.Model): _inherit = 'res.partner' published_book_ids = fields.One2many( 'library.book', # related model 'publisher_id', # fields for "this" on related model string='Published Books') 

我們向模塊添加了新文件,所以不要忘記在library_app/models/init.py中導入該文件:

from . import library_book from . import res_partner 

One2many字段接收三個位置參數:

  • 關聯模型 (comodel_name關鍵字參數)
  • 引用該記錄的模型字段 (inverse_name關鍵字參數)
  • 字段標簽 (string關鍵字參數) 其它可用的關鍵字參數與many-to-one字段相同:context, domain和ondelete(此處作用於關聯中的 many 這一方)。

Many-to-many關聯

在兩端都存在to-many關聯時使用many-to-many關聯。還是以我們的圖書應用為例,書和作者之間是many-to-many關聯:一本書可以有多個作者,一個作者可以有多本書。圖書端有一個library.book模型:

class Book(models.Model): _name = 'library.book' ... author_ids = fields.Many2many( 'res.partner', string='Authors') 

在作者端,我們也可以為res.partner添加一個反向關聯:

class Partner(models.Model): _inherit = 'res.partner' book_ids = fields.Many2many( 'library.book', string='Authored Books') 

Many2many最少要包含一個關聯模型位置參數(comodel_name關鍵字參數),推薦為字段標簽提供一個string參數。

在數據庫層面上,many-to-many關聯不會在已有表中添加任何列。而是自動創建一個關聯表來存儲記錄間的關聯,該表僅有兩個 ID 字段,為兩張關聯表的外鍵。默認關聯表名由兩個表名中間加下划線並在最后加上_rel 來組成。我們圖書和作者關聯,表名應為library_book_res_partner_rel。

有時我們可能需要重寫這種自動生成的默認值。一種情況是關聯模型名稱過長,導致關聯表名的長度超出PostgreSQL數據庫63個字符的上限。這時就需要手動選擇一個關聯表名來符合字符數據要求。另一種情況是我們需要在相同模型間建立第二張many-to-many關聯表。這時也需要手動提供一個關聯表名來避免與已存在的第一張表名沖突。

有兩種方案來重寫關聯表名:位置參數或關鍵字參數。通過字段位置參數定義示例如下:

# Book <-> Authors關聯(使用位置參數)
author_ids = fields.Many2many(
    'res.partner', # 關聯模型(尾款) 'library_book_res_partner_rel', # 要使用的關聯表名 'a_id', # 本記錄關聯表字段 'p_id', # 關聯記錄關聯表字段 'Authors') # string標簽文本 

要使可讀性更強,也可使用關鍵字參數:

# Book <-> Authors關聯(使用關鍵字參數) author_ids = fields.Many2many( comodel_name='res.partner', # 關聯模型(必填) relation='library_book_res_partner_rel', # 關聯表名 column1='a_id', # 本記錄關聯表字段 column2='p_id', # 關聯記錄關聯表字段 string='Authors') # string標簽文本 

與one-to-many relational字段相似,many-to-many 字段還可以使用context, domain和auto_join這些關鍵字參數。

ℹ️在創建抽象模型時,many-to-many中不要使用column1和column2屬性。在 ORM 設計中對抽象模型有一個限制,如果指定關聯表列名,就無法再被正常繼承。

層級關聯

父子樹狀關聯使用同一模型中many-to-one關聯表示,來將每條記錄引用其父級。反向的one-to-many關聯對應記錄的子級。Odoo 通過域表達式附加的child_of和parent_of操作符改良了對這些層級數據結構的支持。只要這些模型有parent_id字段(或_parent_name有效模型定義)就可以使用這些操作符。

通過設置_parent_store=True和添加parent_path幫助字段可加快層級樹的查詢速度。該字段存儲用於加速查詢速度的層級樹結構信息。

ℹ️Odoo 12中的修改 parent_path幫助字段在 Odoo 12中引入。此前版本中使用parent_left和parent_right整型字段來實現相同功能,但在 Odoo 12中淘汰了這些字段。

注意這些附加操作會帶來存儲和執行速度的開銷,所以最好是用到讀的頻率大於寫的情況下,比如本例中的分類樹。僅在優化多節點深度層級時才需要使用,對於小層級或淺層級的可能會被誤用。

為演示層級結構,我們將為圖書應用添加一個分類樹,用於為圖書分類。在library_app/models/library_book_category.py文件中添加如下代碼:

from odoo import api, fields, models class BookCategory(models.Model): _name = 'library.book.category' _description = 'Book Category' _parent_store = True name = fields.Char(translate=True, required=True) # Hierarchy fields parent_id = fields.Many2one( 'library.book.category', 'Parent Category', ondelete='restrict') parent_path = fields.Char(index=True) # Optional but good to have: child_ids = fields.One2many( 'library.book.category', 'parent_id', 'Subcategories') 

這里定義了一個基本模型,包含引用父級記錄的parent_id字段。為啟用層級索引來加快樹級搜索,添加了一個_parent_store=True 模型屬性。使用該屬性必須要添加且必須要索引parent_path字段。引用父級的字段名應為parent_id,但如果聲明了可選的_parent_name模型屬性,則可以使用任意其它字段名。

添加字段列出直接的子級非常方便,即為上述代碼中的one-to-many反向關聯。還有不要忘記在library_app/models/init.py文件中添加對以上代碼的引用:

from . import library_book from . import res_partner from . import library_book_category 

使用引用字段的彈性關聯

普通關聯字段指定固定的引用co-model模型,但Reference字段類型不受這一限制,它支持彈性關聯,因此相同字段不用限制只指向相同的目標模型。作為示例,我們使用圖書分類模型來添加引用重點圖書或作者。因此該字段可引用圖書或 partner:

class BookCategory(models.Model): ... highlighted_id = fields.Reference( [('library.book', 'Book'), ('res.partner', 'Author')], 'Category Highlight' ) 

該字段定義與 selection 字段相似,但這里選擇項為該字段中可以使用的模型。在用戶界面中,用戶會先選擇列表中的模型,然后選擇模型中的指定記錄。

ℹ️Odoo 12中的修改 刪除了可引用模型配置表。在此前版本中,可用於配置在 Reference 字段中可用的模型。通過菜單Settings > Technical > Database Structure可進行查看。這些配置可在 Reference 字段中使用odoo.addons.res.res_request.referenceable_models函數來替代模型選擇列表。

以下為有關引用字段的一些其它有用技術細節:

  • 引用字段在數據庫中以model,id字符串形式存儲
  • read()方法供外部應用使用,以格式化的(‘model_name’, id)元組返回,而不是常用的many-to-one字段的(id, ‘display_name’)形式

計算字段

字段值除普通的讀取數據庫中存儲值外,還可自動由函數計算。計算字段的聲明和普通字段相似,但有一個額外的compute參數來定義用於計算的函數。大多數情況下,計算字段包含書寫業務邏輯。因此要完全使用這一功能,還要學習第八章 Odoo 13開發之業務邏輯 – 業務流程的支持。此處我們將解釋計算字段用法,但會使用簡單的業務邏輯。

圖書有出版商,我們的例子是在圖書表單中添加出版商的國別。實現該功能,我們會使用基於publisher_id的計算字段,將會從出版商的country_id字段中獲取值。

編輯library_app/models/library_book.py文件中的圖書模型,代碼如下:

class Book(models.Model): ... publisher_country_id = fields.Many2one( 'res.country', string='Publisher Country', compute='_compute_publisher_country' ) @api.depends('publisher_id.country_id') def _compute_publisher_country(self): for book in self: book.publisher_country_id = book.publisher_id.country_id 

以上代碼添加了一個publisher_country_id字段,和一個計算其值的_compute_publisher_country方法。方法名作為字符串參數傳入字段中,但也可以傳遞一個可調用引用(方法標識符,不帶引號)。但這時需確定Python 文件中方法在字段之前定義。

計算如果依賴其它字段的話就需要使用@api.depends裝飾器,通常都會依賴其它字段。它告訴服務器何時重新計算或緩存值。參數可接受一個或多個字段名,點號標記可用於了解字段關聯。本例中,只要圖書publisher_id的country_id變更了就會重新進行計算。

和平常一樣,self 參數是要操作的字符集對象。我們需要對其遍歷來作用於每條記錄。計算值通過常用(寫)操作來設置,本例中計算相當簡單,我們為其分配當前圖書的publisher_id.country_id值。

同樣的計算方法可用於一個以上字段。這時同一方法在多個compute 字段參數中使用,計算方法將為所有計算字段分配值。

小貼士:計算函數必須為一個或多個字段分配值用於計算。如果計算方法有 if 條件分支,確保每個分支中為計算字段分配了值。否則在未分配置值的分支中將會報錯。

現在我們還不會修改該模塊的視圖,但可通過在圖書表單視圖中點擊開發者菜單中的Edit View選項,直接在表單 XML 中添加該字段查看效果。不必擔心出問題,在下次模塊升級時會進行覆蓋。

搜索和寫入計算字段

我們剛剛創建的計算字段可讀取但不可搜索或寫入。默認情況下計算字段是實時計算,而不存儲在數據庫中。這也是無法像普通字段那樣進行搜索的原因。

我們可通過實現特殊方法來開啟搜索和寫入操作。計算字段可與 compute 方法一起設置實現搜索邏輯的 search 方法,以及實現寫入邏輯的 inverse 方法。使用這些方法,計算字段可修改如下:

class Book(models.Model): ... publisher_country_id = fields.Many2one( 'res.country', string='Publisher Country', compute='_compute_publisher_country', # store = False, # 默認不在數據庫中存儲 inverse='_inverse_publisher_country', search='_search_publisher_country', ) 

計算字段中的寫入是計算的反向(inverse)邏輯。因此處理寫入操作的方法稱為 inverse,本例中 inverse 方法很簡單。計算將book.publisher_id.country_id 的值復制給book.publisher_country_id,反向操作是將寫入book.publisher_country_id的值拷貝給book.publisher_id.country_id field字段:

    def _inverse_publisher_country(self): for book in self: book.publisher_id.country_id = book.publisher_country_id 

注意這會修改出版商partner記錄數據,因此也會修改相同出版商圖書的相關字段。常規權限控制對這類寫操作有效,因此僅有對 partner 模型有寫權限的當前用戶才能成功執行操作。

要為計算字段開啟搜索操作,需要實現search 方法。為此我們需要能夠將計算字段的搜索轉換為使用常規存儲字段的搜索域。本例中,實際的搜索可通過關聯的publisher_id Partner 記錄的country_id來實現:

    def _search_publisher_country(self, operator, value): return [('publisher_id.country_id', operator, value)] 

在模型上執行搜索時,域表達式用作實施過濾的參數。域表達式在第八章 Odoo 13開發之業務邏輯 – 業務流程的支持會做詳細講解,現在我們應了解它是一系列(field, operator, value)條件。

當域表達式的條件中出現該計算字段時就會調用這個搜索方法。它接收搜索的操作符和值,並將原搜索元素轉換為一個域搜索表達式。country_id字段存儲在關聯的partner模型中,因此我們的搜索實現僅需修改原搜索表達式來使用publisher_id.country_id字段。

存儲計算字段

通過在定義時設置store = True還可以將計算字段值保存到數據庫中。在任意依賴變更時值就會重新計算。因為值已被存儲,所以可以像普通字段一樣被搜索,也就不需要使用 search 方法了。

關聯字段

前面我們實現的計算字段僅僅是從關聯記錄中將值拷貝到模型自己的字段中。這種常用情況可以由 Odoo 使用關聯字段功能自動處理。關聯字段通過關聯模型的字段可在模型中直接可用,並且可通過點號標記法直接訪問。這樣在點號標記法不可用時(如 UI 表單視圖)也可以使用該字段。

要創建關聯字段,我們像普通計算字段那樣聲明一個所需類型的字段,但使用的不是 compute 屬性,而是 related屬性,設置用點號標記鏈來使用所需字段。我們可以使用引用字段來獲取與上例publisher_country_id計算字段相同的效果:

    publisher_country_id = fields.Many2one( 'res.country', string='Publisher Country', related='publisher_id.country_id', ) 

本質上關聯字段僅僅是快捷實現 search 和 inverse 方法的計算字段。也就是說可以直接對其進行搜索和寫入,而無需書寫額外的代碼。默認關聯字段是只讀的,因inverse寫操作不可用,可通過readonly=False字段屬性來開啟寫操作。

ℹ️Odoo 13中的修改

現在關聯字段默認為只讀:readonly=True。此前版本中它默認可寫,便事實證明這是一個默認值,因為它可能會允許修改配置或主數據這些不應被修改的數據。

還應指出這些關聯字段和計算字段一樣可使用store=True來在數據庫中存儲。

模型約束

通常應用需保證數據完整性,並執行一些驗證來保證數據是完整和正確的。PostgreSQL數據庫管理器支持很多可用驗證:如避免重復,或檢查值以符合某些簡單條件。模型為此可聲明並使用 PostgreSQL約束。一些檢查要求更復雜的邏輯,最好是使用 Python 代碼來實現。對這些情況,我們可使用特定的模型方法來實現 Python 約束邏輯。

SQL模型約束

SQL約束加在數據表定義中,並由PostgreSQL直接執行。它由_sql_constraints類屬性來定義。這是一個元組組成的列表,並且每個元組的格式為(name, code, error):

  • name是約束標識名
  • code是約束的PostgreSQL語法
  • error是在約束驗證未通過時向用戶顯示的錯誤消息 我們將向圖書模型添加兩個SQL約束。一條是唯一性約束,用於通過標題和出版日期是否相同來確保沒有重復的圖書;另一條是檢查出版日期是否為未出版:
class Book(models.Model): ... _sql_constraints = [ ('library_book_name_date_uq', # 約束唯一標識符 'UNIQUE (name, date_published)', # 約束 SQL 語法 'Book title and publication date must be unique'), # 消息 ('library_book_check_date', 'CHECK (date_published <= current_date)', 'Publication date must not be in the future.'), ] 

更多有關PostgreSQL約束語法,請參見官方文檔。

Python模型約束

Python 約束可使用自定義代碼來檢查條件。檢查方法應添加@api.constrains裝飾器,並且包含要檢查的字段列表,其中任意字段被修改就會觸發驗證,並且在未滿足條件時拋出異常。就圖書應用來說,一個明顯的示例就是防止插入不正確的 ISBN 號。我們已經在_check_isbn()方法中書寫了 ISBN 的校驗邏輯。可以在模型約束中使用它來防止保存錯誤數據:

from odoo.exceptions import ValidationError class Book(models.Model): ...  @api.constrains('isbn') def _constrain_isbn_valid(self): for book in self: if book.isbn and not book._check_isbn(): raise ValidationError('%s is an invalid ISBN' % book.isbn) 

了解 Odoo的 base 模型

在前面文章中,我們一起創建了新模型,如圖書模型,但也使用了已有的模型,如 Odoo 自帶的Partner 模型。下面就來介紹下這些內置模型。Odoo 內核中有一個base插件模塊。它提供了 Odoo 應用所需的基本功能。然后有一組內置插件模塊來提供標准產品中的官方應用和功能。base模塊中包含兩類模型:

  • 信息倉庫(Information Repository), ir.*模型
  • 資源(Resources), res.*模型 信息倉庫用於存儲 Odoo 所需數據,以知道如何作為應用來運作,如菜單、視圖、模型、Action 等等。Technical菜單下的數據通常都存儲在信息倉庫中。相關的例子有:

  • ir.actions.act_window用於窗口操作

  • ir.ui.menu用於菜單項

  • ir.ui.view用於視圖

  • ir.model用於模型

  • ir.model.fields用於模型字段

  • ir.model.data用於XML ID

資源包含基本數據,基本上用於應用。以下是一些重要的資源模型:

  • res.partner用於業務伙伴,如客戶、供應商和地址等等
  • res.company用於公司數據
  • res.currency用於貨幣
  • res.country用於國家
  • res.users用於應用用戶
  • res.groups用於應用安全組

這些應該有助於你在未來遇到這些模型時理解它們來自何處。

總結

學習完本文,我們熟悉了模型帶給我們構造數據模型的可能性。我們看到模型通常繼承models.Model類,但還可使用models.Abstract來創建可復用的 mixin 模型、使用models.Transient來創建向導或高級用戶對話。我們還學習了常見的模型屬性,如_order 用於排序,_rec_name用於記錄展示的默認值。

模型中的字段定義了所有它存儲的數據。我們了解了可用的非關聯字段類型以及它們支持的屬性。我們還學習了關聯字段的幾種類型:many-to-one, one-to-many和many-to-many,以及它們如何定義模型間的關系,包括層級父子關系。

大多數字段在數據庫中存儲用戶的輸入,但字段也可以通過 Python 代碼自動計算值。我們看到了如何實現計算字段,以及一些高級用法,如使計算字段可寫及可搜索。

還有模型定義的一部分是約束,保持數據一致性和執行驗證,可以通過PostgreSQL或Python代碼實現。

一旦我們創建了數據模型,就應該為它提供一些默認和演示數據。在下一篇文章中我們將學習如何使用數據文件在系統中導入、導出和加載數據。

☞☞☞第七章 Odoo 13開發之記錄集 – 使用模型數據


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM