Odoo 的一個強大功能是無需直接修改底層對象就可以添加功能。這是通過其繼承機制來實現的,采取在已有對象之上修改層來完成。這種修改可以在不同層上進行-模型層、視圖層和業務邏輯層。我們創建新的模塊來做出所需修改而無需在原有模塊中直接修改。
上一篇文章中我們從零開始創建了一個新應用,本文中我們學習如何通過繼承已有的核心應用或第三方模塊來創建新的模塊。實現以上本文將主要涵蓋:
- 原模型擴展,為已有模型添加功能
- 修改數據記錄來繼承視圖,添加功能或修改數據來修改其它模塊創建的數據記錄
- 其它模型繼承機制,如代理繼承和 mixin 類
- 繼承 Python 方法來為應用業務邏輯添加功能
- 繼承 Web 控制器和模板來為網頁添加功能
開發准備
本文要求可通過命令行來啟動 Odoo 服務。代碼將在第三章 Odoo 12 開發之創建第一個 Odoo 應用的基礎上進行修改。通過該文的學習現在我們已經有了library_app模塊。本系列文章代碼請參見 GitHub 倉庫。
學習項目-繼承圖書館應用
在第三章 Odoo 12 開發之創建第一個 Odoo 應用中我們創建了一個圖書應用的初始模塊,可供查看圖書目錄。現在我們要創建一個library_member模塊,來對圖書應用進行擴展以讓圖書會員可以借書。它繼承 Book 模型,並添加一個圖書是否可借的標記。該信息將在圖書表單和圖書目錄頁顯示。
應添加圖書會員主數據模型Member,類似 Partner 來存儲個人數據,如姓名、地址和 email,還有一些特殊字段,如圖書會員卡號。最有效的方案是代理繼承,自動創建圖書會員記錄並包含關聯 Partner 記錄。該方案使得所有的Partner 字段在 Member 中可用,沒有任何數據結構上的重復。
我們還要在借書表單中為會員提供消息和社交功能,包括計划活動組件來實現更好地協作。我們還要添加會員從圖書館中借書的功能,但暫不涉及。以下是當前所要修改內容的總結:
- 圖書
- 添加一個Is Available? 字段。現在通過手動管理,以后會自動化
- 擴展 ISBN 驗證邏輯來同時支持10位數的ISBN
- 擴展圖書目錄頁來分辨不可借閱圖書並允許用戶過濾出可借圖書
- 會員
- 添加一個新模型來存儲姓名、卡號和 Email、地址一類的聯系信息
- 添加社交討論和計划活動功能
首先在library_app同級目錄創建一個library_member目錄來作為擴展模塊,並在其中添加兩個文件,一個__init__.py空文件和一個包含如下內容的__manifest__.py文件:
原模型繼承
第一步我們來為Book模型添加is_available布爾型字段。這里使用經典的 in-place 模型繼承。該字段值可通過圖書借出和歸還記錄自動計算,但現在我們先使用普通字段。要繼承已有模型,需要在 Python 類中添加一個_inherit 屬性來標明所繼承的模型。新類繼承父 Odoo 模型的所有功能,僅需在其中聲明要做的修改。在任何地方使用該模型修改都可用,可以認為這類繼承是對已有模型的引用並在原處做了一些修改。
為模型添加字段
通過 Python 類來新建模型,繼承模型同樣是通過 Python 以及 Odoo 自有的繼承機制,即_inherit 類屬性。該屬性標明所繼承的模型。新的類繼承父 Odoo 模型的所有功能,僅需聲明要做修改的部分。編碼指南推薦為每個模型創建一個 Python 文件,因此我們添加library_member/models/library_book.py文件來繼承原模型,首先創建__init__.py文件來導入該文件:
1、添加library_member/__init__.py文件來導入 models 子文件夾
2、添加library_member/models/__init__.py文件子文件夾中的代碼文件:
3、創建library_member/models/library_book.py文件來繼承library.book模型:
使用_inherit類屬性來聲明所繼承模型。注意我們並沒有使用到其它類屬性,甚至是_name 也沒使用。除非想要做出修改,否則不需要使用這些屬性。
ℹ️_name是模型標識符,如果修改會發生什么呢?其實你可以修改,這時它會創建所繼承模型的拷貝,成為一個新模型。這叫作原型繼承,本文后面會討論。
可以把這個想成是對模型定義的一個引用,在原處做了一個修改。可以添加字段、修改已有字段、修改模型類屬性甚至是包含業務邏輯的方法。要在數據表中添加新的模型字段需要安裝該模塊。如果一切順利,通過Settings > Technical > Database Structure > Models菜單查看library.book模型即可看到該字段。
修改已有字段
通過上面部分可以看到向已有模型添加新字段非常簡單。有時還要對已有字段進行修改,也非常簡單。在繼承模型時,可對已有字段疊加修改,也就是說僅需定義要增加或修改的字段屬性。
我們將對原來創建的library_app模塊的 Book模型做兩處簡單修改:
- 為isbn字段添加一條提示,說明同時支持10位數的 ISBN(稍后會實現該功能)
- 為publisher_id字段添加數據庫索引,以提升搜索效率
編輯library_member/models/library_book.py文件,並在library.book 模型中添加如下代碼:
這會對字段進行指定屬性修改,未涉及的屬性不會被修改。升級模塊,進入圖書表單,將鼠標懸停在 ISBN 字段上,就可以看到所添加的提示信息了。index=True這一修改不太容易發現,通過Settings > Technical > Database Structure > Models菜單下的字段定義中可進行查看。
修改視圖和數據
模塊中視圖和其它數據構件也可通過繼承來修改。就視圖而言,通常需要添加功能。視圖的展示結構在 arch 字段中使用 XML定義。這一 XML 數據可通過定位到所需修改的地方來進行繼承,然后聲明需執行的操作,如在該處添加 XML 元素。對於剩余的數據元素,它們代表寫入數據庫中的記錄,繼承模型可通過寫操作來修改它們的值。
繼承視圖
表單、列表和搜索視圖通過arch XML結構定義。要繼承視圖,就要一種修改 XML 的方式,也即定位 XML 元素然后對該處進行修改。視圖繼承的 XML 記錄和普通視圖中相似,多一個 inherit_id屬性來引用所要繼承的視圖。下面我們來繼承圖書視圖並添加is_available字段。
首先要查找待繼承的視圖的XML ID,通過Settings > Technical > User Interface > Views菜單來查看。圖書表單的XML ID是library_app.view_form_book。然后還要找到要插入修改的XML元素,我們在 ISBN 字段之后添加Is Available?通常通過name 屬性定位元素,此處為<field name=”isbn” />。
我們添加views/book_view.xml文件來繼承 Partner 視圖,加入如下內容:
以上代碼中,我們高亮顯示了繼承相關的元素。inherit_id記錄字段通過 ref 屬性指向繼承視圖的外部標識符,我們將在第五章 Odoo 12開發之導入、導出以及模塊數據討論外部標識符詳情。視圖使用 XML 定義並存儲在結構字段 arch 中。要繼承一個視圖,先定位要擴展的節點,然后執行要做的操作,如添加 XML 元素。
定位節點的最簡單方法是使用唯一標識屬性,通常是 name。然后添加定位屬性,聲明要做的修改。本例中繼承節點是name=”isbn”元素,修改是在選定元素后加一段 XML:
除string 屬性外的任意 XML 元素和屬性可作為繼承節點,字符串屬性會被翻譯成用戶所使用的語言,因此不能作為節點選擇器。
ℹ️在9.0以前,string 屬性(顯示標簽文本)也可作為繼承定位符。在9.0之后則不再允許。這一限制主要源自這些字符串的語言翻譯機制。
一旦 XML 節點被選為繼承點,需要指明要執行的繼承操作。這通過 position 屬性實現:
- inside(默認值):在所選節點內添加內容,這一節點應是<group>或<page>一類的容器
- after:在選定節點之后向父節點添加內容
- before:在選定節點之前向父節點添加內容
- replace:替換所選節點。若使用空元素則會刪除該元素。Odoo 之后還允許使用其它標記來包裹元素,通過在內容中使用$0來表示被替換的元素。
- attributes:修改匹配元素屬性值。內容中應包含帶有一個或多個<attribute name=”attr-name”>value</attribute>元素。如<attribute name=”invisible”>True</attribute>,若不帶內容,如<attribute name=”invisible” />則 attribute 會從所選元素中刪除。
小貼士:通過position=”replace”可刪除 XML 元素,但應避免這么做。這么做會破壞其它依賴所刪除節點、將其作為占位符添加元素的模塊。一個替代方案是,讓該元素不可見。
除了attributes定位,上述定位符可與帶position=”move”的子元素合並。效果是將子定位符目標節點移到父定位符目錄位置。
ℹ️Odoo 12中的修改
position=”move”子定位符是 Odoo 12中新增的,之前的版本中沒有
例如:
其它視圖類型,如列表和搜索視圖,也有 arch 字段,可以表單視圖同樣的方式被繼承。
在聲明文件data 中加入該視圖文件並更新模塊即可:
使用 XPath 選取繼承點
有時可能沒有帶唯一值的屬性來用作 XML 節點選擇器。在所選元素沒有 name 屬性時可能出現這一情況,如<group>、<notebook>或<page>視圖元素。另外就是有多個帶有相同 name 屬性的元素,比如在看板 QWeb 視圖中相同字段可能在同一 XML 模板中被多次包含。
在這些情況下我們就需要更高級的方式來定位待擴展 XML 元素。定位 XML 中元素的一種自然方式是 XPath 表達式。以上一篇文章中定義的 Book 表單視圖為例,定位<field name=”isbn”>元素的 XPath 表達式是//field[@name]=’isbn’。該表達式查找 name 屬性等於 isbn 的<field>元素。
前一部分對圖書表單視圖繼承的 XPath 寫法是:
XPath 語法的更多知識請見 Python 官方文檔。
如果 XPath 表達式匹配到了多個元素,僅會選取第一個作為擴展目錄。所以表達式應越精確越好,使用唯一屬性。name 屬性最易於確保找到精確元素作為擴展點,因此在創建視圖 XML 元素時添加唯一標識符就非常重要。
修改數據
普通數據記錄不同於視圖,它沒有 XML arch 結構,也不能使用 XPath 來進行擴展。但還是可以通過替換字段值來進行修改。
<record id=”x” model=”y”>數據加載元素實際是對 y 模型進行插入或更新操作。若不存在記錄 x,則被創建,否則被更新/覆蓋。其它模塊中的記錄可通過<module>.<identifier>全局標識符訪問,因此可以在我們的模塊中重寫其它模塊中已寫入的數據。
ℹ️點號是保留符號,用於分隔模塊名和對象標識符,所以在標識符名中不要使用點號,而應使用下划線字符。
舉個例子,我們將 User 安全組的名稱修改為 Librarian,對應修改library_app.library_group_user記錄。添加library_member/security/library_security.xml並加入如下代碼:
這里我們使用了一個<record>元素,僅寫了 name 字段。可以認為這是對所選字段的一次寫操作。
小貼士:使用<record>元素時,可以選擇要執行寫操作的字段,但對 shortcut 元素則並非如此,如<menuitem>和<act_window>。它們需要提供所有的屬性,漏寫任何一個都會將對應字段置為空值。但可使用<record>為原本通過 shortcut 元素創建的字段設置值。
在聲明文件data 中加入security/library_security.xml並更新模塊即可看到效果。
其它模型繼承機制
前面我們介紹了模型的基本繼承,在官方文檔中稱為經典繼承。這是最常用的繼承方式,最容易想到的就是in-place繼承。獲取模型並對其繼承。添加的新功能會自動添加到已有模型中,而不會創建新模型。
可以為_inherit 屬性傳入多個值來繼承多個父模型。大多數情況下這通過 mixin 類完成,mixin類是實現可復用的通用功能。也可以像普通模型那樣獨立使用,像是一個功能容器,可隨時加到其它模型中。
如在使用_inherit 屬性的同時還使用了與父模型不同的_name屬性,此時會復用所繼承並創建一個新的模型,並帶有自己的數據表和數據。官方文檔稱其為原型(prototype)繼承。下面我們會拿一個模型,並為其創建一個拷貝。在添加新功能時,只會被加到新模型中,而不會變更原模型。
此外還有代理(delegation)繼承,通過_inherits 屬性來使用(注意最后有一個 s)。這允許我們創建一個包含和繼承已有模型的新模型。新模型創建新記錄時,在原模型中也會被創建並使用many-to-one 字段關聯。查看新模型的人可以看到所有原模型和新模型中的字段,但在后台兩個模型分別處理各自的數據。
下面我們一起來了解詳情。
使用原型繼承拷貝功能
前文我們繼承模型時使用了_inherit 屬性,創建一個類繼承library.book 並添加了一些功能。類中沒有使用_name屬性,不指明即使用library.book。如果設置了不個不同值的_name 屬性,會通過從所繼承的模型拷貝功能創建新模型。
在實際開發中,這類繼承一般通過抽象 mixin 類,很少這樣直接繼承普通模型,因為這樣會創建冗余的數據結構。Odoo 還有一種代理繼承機制可避免這類數據結構冗余,所以普通模型通常會使用這種方法來做繼承。
使用代理繼承內嵌模型
使用代理繼承無需復制數據即可在數據庫中復用數據結構,這通過將一個模型嵌入另一個來實現。UML 中這種稱作組合(composition)關系:父類無需子類即可存在,而子類必須要有父類才能存在。
比如,對於內核 User模型,每條記錄包含一條 Partner 記錄,因此包含 Partner 中的所有字段以及User自身的一些字段。
在圖書項目中,我們要添加一個圖書會員模型。會員有會員卡並通過會員卡借閱讀書。我們要記錄卡號,還要存儲email 和地址這類個人信息。Partner 模型已包含聯系和地址信息,所以最好是進行復用,而不去創建重復的數據結構。
為會員模型創建library_member/models/library_member.py文件並加入如下代碼:
使用代理繼承,library.member 中嵌入了繼承模型res.partner,因此在創建會員記錄時,一個關聯的 Partner 會自動被創建並通過partner_id字段引用。
ℹ️Odoo 8中的修改
在新的 API 中引入了delegate=True字段屬性。在那之前,代理繼承通過模型屬性來定義,類似_inherits = {‘res.partner’: ‘partner_id’}。現在仍支持這一寫法,官網中還有相應介紹,但delegate=True 字段屬性可起到相同效果且使用更簡單。
透過代理機制,嵌套模型的所有字段就像父模型字段一樣自動可用。本例中,會員卡模型可使用 Partner 中的所有字段,如 name, address和 email,以及會員自身的獨有字段,如card_number。在后台中,Partner 字段存儲在關聯的 Partner 記錄,沒有重復的數據結構。
ℹ️對於模型方法則並非如此,Partner 模型中的方法在 Member 模型中不可使用。
與原型繼承相比,代理繼承的好處在於無需跨表重復像地址這樣的數據。任何需包含地址的新模型通過代理嵌入了 Partner 模型。如果在 Partner 中修改 address字段,在所有嵌入的模型中可以馬上使用。
小貼士:代理繼承可通過如下組合來進行替代:
- 父模型中的一個 many-to-one 字段
- 重載 create()方法自動創建並設置父級記錄
- 父字段中希望暴露的特定字段的關聯字段
有時這比完整的代理繼承更為合適。例如res.company並沒有繼承res.partner,但使用到了其中好幾個字段。
不要忘記在library_member/model/__init__.py文件中加入:
要使用我們創建的 Member 模型,還要完成以下步驟:
- 添加安全權限控制列表(ACL)
- 添加菜單項
- 添加表單和列表視圖
- 更新manifest文件來聲明這些新增數據文件
讀者可以先嘗試自己添加,再來看下面的詳細步驟:
要創建安全ACL,創建library_member/security/ir.model.access.csv文件並加入如下代碼:
要添加菜單項,創建library_member/views/library_menu.xml文件並加入如下代碼:
要添加視圖,創建library_member/views/member_view.xml文件並加入如下代碼:
最后,編輯manifest文件來聲明這三個新文件:
如果編寫正確,在進行模型更新后即可使用新的圖書會員模型了。
使用 mixin類繼承模型
原型繼承主要用於支持 mixin 類。mixin 是基於 models.Abstract 的抽象的模型(而不是models.Model),它在數據庫中沒有實際的體現,而是提供功能供其它模型復用(混合 mixed in)。Odoo 插件提供多種 mixin,最常的兩種由 Discuss 應用(mail 模塊)提供:
- mail.thread提供在許多文檔表單下方或右側的消息面板功能,以及消息和通知相關邏輯。這在我們自己的模型中將經常會添加,下面就來一起學習下。
- mail.activity.mixin模型提供待辦任務計划。
ℹ️Odoo 11中的修改
mail 模塊現在通過mail.activity.mixin抽象模型提供Activities任務管理功能。該功能在 Odoo 11中才添加,此前的版本中沒有。
我們一起來為 Member 模型添加上述兩種 mixin。社交消息功能由 mail 模塊的mail.thread模型提供,要將其加入自定義模型,應進行如下操作:
- 通過 mixin 模型 mail 為插件模塊添加依賴
- 讓類繼承mail.thread和mail.activity.mixin兩個 mixin 類
- 將message_follower_ids, message_ids和activity_id這些 mixin 的數據字段添加到表單視圖
對於第一步擴展模型需要在__manifest__.py文件中添加對 mail 的依賴。
第二步中對 mixin 類的繼承通過_inherit屬性完成,應編輯library_member/models/library_member.py並添加如下代碼:
通過添加額外的這行代碼,我們的模型就會包含這些 mixin 的所有字段和方法。
第三步中向表單視圖添加相關字段,編輯library_member/views/member_view.xml文件並在表單最后添加如下代碼:
mail 模塊還為這些字段提供了一些特定的網頁組件,以上代碼中已使用到。在升級模塊后會員表單將變成這樣:
有時普通用戶僅能訪問正在 follow 的記錄。在這些情況下我們應添加訪問記錄規則來讓用戶可以看到 follow 的記錄。本例中用不到這一功能,但可通過[(‘message_partner_ids’, ‘in’, [user.partner_id.id])]或來進行添加。
繼承 Python 方法
Python 方法中編寫的業務邏輯也可以被繼承。Odoo 借用了 Python 已有的父類行為的對象繼承機制。
作為一個實際的例子,我們將繼承圖書 ISBN 驗證邏輯。在圖書應用中僅能驗證13位的 ISBN,但老一些的圖書可能只有10位數的 ISBN。我們將繼承_check_isbn()方法來完成這種情況的驗證。在library_member/models/library_book.py文件中添加如下方法:
要繼承方法,我們要重新定義該方法,可以使用 super()來調用已實現的部分。在這個方法中我們驗證是否為10位數 ISBN,然后插入遺失的驗證邏輯。若不是10位,則進入原有的13位驗證邏輯。
如果想要進行測試甚至是書寫測試用例,可使用0-571-05686-5作為例子,該書是威廉·戈爾丁的《蠅王》。
ℹ️Odoo 11中的修改
從 Odoo 11開始,支持的主Python版本為 Python 3(Odoo 12中為 Python 3.5)。而此前的 Odoo 版本使用 Python 2,其中 super()需傳入類名和 self 兩個參數,那么,上例中的代碼應修改為super(Book, self)._check_isbn()。
繼承 Web 控制器和模板
Odoo 中的所有功能都帶有擴展性,web 功能也不例外,所以已有控制器和模塊都能被繼承。
作為示例,我們將繼承圖書目錄網頁,加入前面添加的圖書可用性信息:
- 在控制器端添加對查詢參數的支持,訪問/library/books?available=1過濾出可借閱圖書
- 在模板端,添加一個圖書不可用的表示
繼承網頁控制器
網頁控制器不應包含實際業務邏輯,僅集中於展示邏輯。我們可能會需要添加對額外 URL 參數甚至是路由的支持,來改變網頁的展示。我們將擴展/library/books來支持available=1參數,以過濾出可借閱圖書。
要繼承已有控制器,需導入對應對象,然后用方法新增邏輯來進行實現。下面新增ibrary_member/controllers/main.py文件並加入如下代碼:
我們要繼承的Books控制器在library_app/controllers/main.py中定義。因此需要通過odoo.addons.library_app.controllers.main導入。這和模型不同,模型可以通過 env 對象中的central registry 來引用任意模型類,而無需了解實現它的文件。控制器沒有這個,我們需要知道實現需繼承控制器的模塊和文件。
然后基於Books聲明了一個BooksExtended類,類名不具關聯性,僅用於繼承和擴展原類中定義的方法。
再后我們(重)定義了一個控制器方法 list()。它至少需要一個簡單的@http.route()裝飾器來保持路徑活躍。如果不帶參數,將會保留父類中定義的路由。但也可以為@http.route() 裝飾器添加參數,來重新定義或替換類路由。
在繼承的 list()方法中,一開始使用了 super()來運行已有代碼。處理結果返回一個 Response 對象,Response 帶有模塊要渲染的屬性 template,以及渲染使用的上下文qcontext。但還需要生成 HTML,僅會在控制器結束運行時生成。這也讓我們可以在最終渲染完成之前可以修改 Response 屬性。
list()方法帶有**kwargs參數,捕獲所有kwargs字典中的參數。這些是 URL 中的參數,如?available=1。方法檢測kwargs中available鍵的值,檢測到后改變qcontext來獲取僅為可借閱圖書的圖書記錄集。
還要記得讓模塊知道這個新 Python 文件,需通過將 controllers 子文件夾中添加到library_member/__init__.py中:
在library_member/controllers/__init__.py文件中添加一行代碼:
然后更新模板並訪問http://<your-server>:8069/library/books?available=1 將僅顯示勾選了Is Available? 的圖書
繼承 QWeb 模板
要修改網頁的實際展示,就需要繼承所使用的 QWeb 模板。我們將繼承library_app.book_list_template來展示更多有關不可借閱圖書的信息。添加library_member/views/book_list_template.xml文件並加入如下代碼:
網頁模板像其它 Odoo 視圖類型一樣是 XML 文件,同樣也可以使用 XPath 來定位元素並對它們進行操作。所繼承模型通過在元素中的inherit_id來指明。
小貼士:在前例中使用了靈活性很強的 XPath 標記,但這里也可以使用等價的簡化標記:<span t-field=”book.publisher_id” position=”after”>
然后在 library_member/__manifest__.py文件中加入該文件的聲明:
然后訪問http://<your-server>:8069/library/books即可對不可借閱圖書展示額外的(Not Available)信息。
總結
擴展性是 Odoo 框架的一個重要功能。我們可以創建插件來為需要實現功能的多個層的已有插件修改或添加功能。
模型層中,我們使用_inherit模型屬性來引用已有模型,然后在原處執行修改。模型內的字段對象還支持疊加定義,這樣可對已有字段重新聲明,僅修改屬性。
其它的模型繼承機制允許我們利用數據結構和業務邏輯。代理繼承通過多對一關聯字段上的delegate=True屬性(或老式的 inherits 模型屬性),來讓所有關聯模塊的所有字段可用,並復用它們的數據結構。原型繼承使用_inherit屬性,來復制其它模型的功能(數據結構定義和方法),並啟用抽象 mixin 類,提供一系列像文檔討論消息和 follower 的可復用功能。
視圖層中,視圖結構通過 XML 定義,(使用 XPath 或 Odoo 簡化語法)定位 XML 元素來進行繼承及添加 XML 片斷。其它由模塊創建的記錄已可由繼承模塊修改,僅需引用 對應的完整 XML ID 並在設想的字段上執行寫操作。
業務邏輯層中,可使用模型繼承相同的機制來進行繼承,以及重新聲明要繼承的方法。在方法內,Python 的super()函數可用於調用所繼承方法的代碼,添加代碼可在其之前或之后運行。
對於前端網頁,控制器中的展示邏輯繼承方式和模型方法相似,網頁模板也是包含 XML 結構的視圖,因此可以像其它視圖類型一樣的被繼承。
下一篇文章中,我們將更深入學習模型,探索模型提供給我們的所有功能。
☞☞☞第五章 Odoo 12開發之導入、導出以及模塊數據
學霸專區
- 如何繼承已有模型來添加 minin,如mail.thread?
- 要在會員表單視圖中添加Phone字段需要做哪些修改?
- 如果創建一個與繼承屬性的屬性名不同的模型類會發生什么(例如_name=’y’ and _inherit=’x’)?
- XPath是否可用於修改其它模塊的數據記錄?
- 繼承一個模型時,是否可擴展其方法但不使用 super()調用所繼承的原始代碼?
- 如何在不引用任何特定字段名的情況下繼承圖書目錄頁並在末行添加 ISBN 字段?
擴展閱讀
以下是對官方文檔的其它引用,可對模塊的擴展和繼承機制的知識進行補充: