實戰(zhàn):模型與視圖繼承機制詳解與避坑指南)
1. 從一個真實的業(yè)務(wù)需求說起為什么我們總在“改”O(jiān)doo最近在給一個客戶做Odoo的二次開發(fā)他們提了一個很典型的需求現(xiàn)有的銷售訂單sale.order表單上客戶希望增加一個“項目緊急程度”的字段并且根據(jù)這個字段的值自動高亮顯示訂單行。聽起來很簡單對吧但如果你直接去修改Odoo標(biāo)準(zhǔn)模塊sale里的views/sale_order_views.xml文件那就踩進(jìn)了第一個大坑。下次Odoo版本升級你的修改會被無情地覆蓋所有定制化工作付諸東流。這就是Odoo開發(fā)中永恒的核心命題如何在不動原模塊“一磚一瓦”的前提下實現(xiàn)功能的擴展、修改甚至重寫答案就是“繼承”Inheritance。Odoo的繼承機制是其模塊化架構(gòu)的基石它允許你像搭積木一樣在現(xiàn)有功能之上構(gòu)建新的功能而無需修改底層代碼。這不僅關(guān)乎代碼的整潔更關(guān)乎項目未來的可維護(hù)性和升級的平滑性。今天我們就拋開那些抽象的概念直接深入到代碼和視圖層面手把手拆解Odoo的繼承與擴展。我會結(jié)合我這些年趟過的坑告訴你什么時候該用哪種繼承方式視圖繼承的xpath到底怎么寫才不報錯以及如何讓你的新模塊既干凈又強大。2. 理解Odoo繼承的“道”與“術(shù)”模型、字段與方法的擴展在動手寫代碼之前我們必須先理解Odoo繼承的幾種類型。這就像木匠的工具箱你知道什么時候該用鋸子什么時候該用刨子。2.1 類繼承Classical Inheritance最直接的“是什么”類繼承也叫_inherit用于擴展或修改一個現(xiàn)有的模型。你創(chuàng)建的新模塊模型直接聲明繼承自某個已存在的模型。這是最常用的一種。核心場景為現(xiàn)有模型添加新字段、覆蓋現(xiàn)有方法、添加新的約束或計算字段。讓我們用代碼說話。假設(shè)我們要給標(biāo)準(zhǔn)的res.partner客戶/供應(yīng)商模型加一個“客戶等級”字段。錯誤的做法直接修改原模塊找到odoo/addons/base/models/res_partner.py就開改。這是自殺式行為。正確的做法創(chuàng)建新模塊新建一個模塊目錄例如my_partner_extension。創(chuàng)建模型文件models/partner.py# models/partner.py from odoo import models, fields, api class ResPartner(models.Model): # 關(guān)鍵在這里_inherit 指定了要繼承的原始模型 _inherit res.partner # 添加新字段 customer_rank fields.Selection( selection[(basic, 普通), (vip, VIP), (vvip, 尊享VIP)], string客戶等級, defaultbasic ) # 覆蓋重寫父類的方法 api.model def create(self, vals): # 在創(chuàng)建前做一些事情例如自動根據(jù)公司名生成客戶等級邏輯示例 if vals.get(name) and 科技 in vals.get(name): vals[customer_rank] vip # 必須調(diào)用super()來執(zhí)行原始的邏輯 return super(ResPartner, self).create(vals) # 添加一個新的方法 def send_vip_greeting(self): self.ensure_one() # 發(fā)送VIP問候郵件的邏輯 # ... return True關(guān)鍵點解析_inherit ‘res.partner’這行代碼告訴Odoo我這個ResPartner類不是全新的它是在原有res.partner模型基礎(chǔ)上的擴展。Odoo會在運行時將兩個類合并。super()的調(diào)用在重寫方法時幾乎總是需要調(diào)用super()。除非你的意圖是完全取代原方法的行為。不調(diào)用super()會導(dǎo)致原始邏輯丟失引發(fā)各種詭異問題。字段添加直接像在普通模型中一樣定義字段即可Odoo會自動將它們合并到原模型中。2.2 原型繼承Prototypal Inheritance創(chuàng)建一個“變種”原型繼承使用_inherit和_name的組合。它基于一個現(xiàn)有模型創(chuàng)建一個全新的模型。新模型擁有父模型的所有字段和方法但它們在數(shù)據(jù)庫中是兩個獨立的表。核心場景你需要一個和現(xiàn)有模型高度相似但又是獨立實體的模型。例如從product.template產(chǎn)品模板繼承出service.template服務(wù)模板。# models/service.py from odoo import models, fields class ServiceTemplate(models.Model): _name service.template # 新模型的唯一標(biāo)識 _inherit product.template # 繼承自產(chǎn)品模板 _description 服務(wù)模板 # 可以添加服務(wù)特有的字段 service_duration fields.Float(string服務(wù)時長小時) is_online_service fields.Boolean(string在線服務(wù)) # 可以覆蓋繼承來的字段屬性 # 例如所有服務(wù)類型的“產(chǎn)品類型”固定為‘service’ type fields.Selection(selection_add[(service, 服務(wù))], ondelete{service: set default})關(guān)鍵點解析_name和_inherit同時存在這告訴Odoo創(chuàng)建一個名為service.template的新模型并以product.template為藍(lán)本。獨立表數(shù)據(jù)庫中會有一張名為service_template的表它包含了product.template的所有字段通過Odoo的機制映射以及自己新增的字段。使用場景更特定當(dāng)你需要邏輯上的嚴(yán)格區(qū)分時使用。比如你不希望服務(wù)和實物產(chǎn)品在列表視圖、菜單或業(yè)務(wù)規(guī)則上混在一起。2.3 委托繼承Delegation Inheritance “我有一個…”委托繼承使用_inherits屬性。它實現(xiàn)的是對象組合“has-a”關(guān)系而非類繼承“is-a”。子模型實例“擁有”一個父模型實例并通過委托來訪問父模型的字段。核心場景擴展現(xiàn)有模型但希望保持?jǐn)?shù)據(jù)的獨立性。最經(jīng)典的例子是res.users對res.partner的繼承。每個用戶User都是一個伙伴Partner但用戶有自己額外的信息。# 這是一個概念示例Odoo標(biāo)準(zhǔn)模塊已實現(xiàn) # models/extended_user.py from odoo import models, fields class ExtendedUser(models.Model): _name extended.user _inherits {res.partner: partner_id} # 委托繼承 partner_id fields.Many2one(res.partner, string關(guān)聯(lián)伙伴, requiredTrue, ondeletecascade) # 添加用戶特有的字段 internal_phone fields.Char(string內(nèi)部分機號) department fields.Char(string部門)關(guān)鍵點解析_inherits是一個字典{‘父模型名’: ‘子模型中用于鏈接的Many2one字段名’}。數(shù)據(jù)存儲當(dāng)創(chuàng)建一個extended.user記錄時Odoo會同時創(chuàng)建一條res.partner記錄。extended.user記錄只存儲自己的字段和指向res.partner記錄的partner_id。字段訪問你可以直接通過extended_user_record.name訪問伙伴的姓名Odoo會自動通過委托機制從關(guān)聯(lián)的res.partner記錄中獲取。何時使用當(dāng)你需要復(fù)用另一個模型的完整功能包括其所有視圖、權(quán)限、業(yè)務(wù)邏輯但又需要保持?jǐn)?shù)據(jù)實體分離時。不如類繼承常用但理解它有助于讀懂Odoo標(biāo)準(zhǔn)代碼。實操心得選擇繼承類型的“直覺”90%的情況下你用的是類繼承_inherit。當(dāng)你只是想給現(xiàn)有模型加點東西或改點東西時就用它。 當(dāng)你覺得“我需要一個和XX很像但完全是另一個東西”的時候考慮原型繼承_name_inherit。 委托繼承_inherits在標(biāo)準(zhǔn)模塊中很常見但在自定義開發(fā)中較少除非你在設(shè)計一個非常復(fù)雜的模型關(guān)系。拿不準(zhǔn)時先用類繼承。3. 視圖繼承的實戰(zhàn)精準(zhǔn)定位與優(yōu)雅修改模型繼承搞定了數(shù)據(jù)和邏輯但用戶是通過界面視圖來交互的。視圖繼承讓你可以修改任何現(xiàn)有視圖而無需復(fù)制整個視圖文件。Odoo的視圖繼承核心是inherit標(biāo)簽和xpath表達(dá)式。xpath是一種用于在XML中定位節(jié)點的查詢語言雖然聽起來有點技術(shù)性但用起來就像“地圖坐標(biāo)”。3.1 視圖繼承的基本結(jié)構(gòu)首先在你的新模塊中創(chuàng)建視圖文件例如views/partner_view.xml。?xml version1.0 encodingutf-8? odoo data !-- 繼承 res.partner 的表單視圖 -- record idview_partner_form_inherit modelir.ui.view field namenameres.partner.form.inherit.my.module/field field namemodelres.partner/field field nameinherit_id refbase.view_partner_form/ !-- 關(guān)鍵指定繼承哪個視圖 -- field namearch typexml !-- 在這里使用 xpath 進(jìn)行修改 -- xpath expr//field[namename] positionafter field namecustomer_rank widgetradio/ /xpath !-- 更常見的簡寫語法 -- field nameemail positionafter field nameinternal_phone/ /field !-- 在表單最底部添加一個新分組頁簽 -- xpath expr//sheet positioninside div classoe_button_box namebutton_box !-- 可以在這里添加按鈕 -- /div footer button namesend_vip_greeting string發(fā)送VIP問候 typeobject classbtn-primary/ /footer /xpath /field /record /data /odoo關(guān)鍵點解析inherit_id通過ref屬性指向你要繼承的原始視圖的XML ID。這是視圖繼承的“錨點”。arch字段這里包含了所有你對原始視圖結(jié)構(gòu)的修改指令。xpathvs 簡寫xpath expr”…”功能最強大可以定位到任何節(jié)點。//表示在整個文檔中查找[name‘xxx’]是屬性選擇器。field name”email” position”after”這是最常見的簡寫。Odoo會將其解釋為xpath expr”//field[name’email’]”。僅當(dāng)目標(biāo)節(jié)點有唯一的name屬性時才適用。3.2position屬性的五種武器position屬性告訴Odoo找到節(jié)點后你想怎么“處置”它。這是視圖繼承的靈魂。inside默認(rèn)將內(nèi)容插入到目標(biāo)節(jié)點的內(nèi)部末尾。xpath expr//div[classoe_button_box] positioninside button namemy_action string自定義動作/ /xpath用途向一個容器如group、div、sheet內(nèi)添加新元素。after將內(nèi)容插入到目標(biāo)節(jié)點之后作為兄弟節(jié)點。field namephone positionafter field namemobile/ /field用途在某個字段后面添加新字段。最常用。before將內(nèi)容插入到目標(biāo)節(jié)點之前。field namestreet positionbefore label forcountry_id string國家/ field namecountry_id/ /xpath用途在某個字段前面添加內(nèi)容。replace替換整個目標(biāo)節(jié)點。小心使用field namewebsite positionreplace field namewebsite readonly1/ !-- 將網(wǎng)站字段改為只讀 -- /field用途修改一個現(xiàn)有元素的屬性或者完全替換一個復(fù)雜的結(jié)構(gòu)。注意替換時新節(jié)點通常需要保持相同的核心屬性如name。move將目標(biāo)節(jié)點移動到另一個xpath表達(dá)式定位的位置。xpath expr//field[namechild_ids] positionmove xpath expr//field[namecategory_id] positionafter/ /xpath用途調(diào)整界面元素的順序。比較進(jìn)階但非常強大。踩坑實錄xpath定位失敗的那些事兒視圖繼承90%的錯誤來自于xpath寫錯了找不到節(jié)點。坑1name屬性不唯一。原視圖中有兩個field name”date”一個在抬頭一個在行內(nèi)。你的簡寫field name”date” position”after”會作用于第一個可能不是你想要的。務(wù)必使用更精確的xpath例如//field[name‘date’ and ancestor::div[class‘oe_title’]]。坑2視圖結(jié)構(gòu)因模塊加載順序改變。模塊A修改了視圖模塊B又基于A修改后的視圖做繼承。如果B在A之前加載B的繼承就會失敗。解決方案在模塊的__manifest__.py中用‘depends’聲明依賴關(guān)系確保加載順序。坑3替換replace時改變了關(guān)鍵結(jié)構(gòu)。比如你把一個tree視圖的editable屬性去掉了但模型層沒有相應(yīng)調(diào)整可能導(dǎo)致界面錯誤。替換前最好先看看原節(jié)點的完整結(jié)構(gòu)。3.3 繼承列表視圖Tree和搜索視圖Search原理和表單視圖一模一樣只是定位的目標(biāo)不同。繼承列表視圖添加一列record idview_partner_tree_inherit modelir.ui.view field nameinherit_id refbase.view_partner_tree/ field namearch typexml xpath expr//field[namephone] positionafter field namecustomer_rank/ /xpath /field /record繼承搜索視圖添加篩選條件record idview_partner_filter_inherit modelir.ui.view field nameinherit_id refbase.view_res_partner_filter/ field namearch typexml !-- 在搜索框的篩選條件區(qū)域添加 -- xpath expr//filter[namecompany] positionafter filter namefilter_by_rank stringVIP客戶 domain[(customer_rank, , vip)]/ /xpath !-- 在搜索框的搜索字段區(qū)域添加 -- field nameemail positionafter field namecustomer_rank/ /field /field /record4. 構(gòu)建一個完整的新模塊從理論到實踐理解了繼承的“零件”后我們來組裝一輛“車”。我們將創(chuàng)建一個完整的模塊my_partner_extension實現(xiàn)前面提到的所有功能。4.1 模塊結(jié)構(gòu)my_partner_extension/ ├── __init__.py ├── __manifest__.py ├── models/ │ ├── __init__.py │ └── partner.py # 包含我們擴展的 ResPartner 類 └── views/ └── partner_view.xml # 包含所有視圖繼承的定義4.2 關(guān)鍵文件詳解__manifest__.py模塊的“身份證”和“說明書”。{ name: 客戶擴展模塊, version: 16.0.1.0.0, category: Sales, summary: 為合作伙伴模型添加客戶等級和自定義功能, description: 本模塊擴展了Odoo標(biāo)準(zhǔn)的合作伙伴(res.partner)模型。 功能包括 - 添加客戶等級字段普通/VIP/尊享VIP - 在銷售訂單等相關(guān)表單中顯示該字段 - 提供發(fā)送VIP問候郵件的功能 , author: 你的名字/公司, website: https://www.yourwebsite.com, depends: [base, sale], # 關(guān)鍵聲明依賴確保在base和sale模塊之后加載 data: [ views/partner_view.xml, # 聲明視圖文件 ], demo: [], installable: True, application: False, auto_install: False, license: LGPL-3, }depends至關(guān)重要。這里聲明了本模塊正常運行所依賴的其他模塊。Odoo會根據(jù)這個順序加載模塊。因為我們繼承了sale模塊的視圖所以必須依賴它。models/__init__.pyfrom . import partnermodels/partner.py內(nèi)容同2.1節(jié)略views/partner_view.xml綜合示例?xml version1.0 encodingutf-8? odoo data !-- 繼承合作伙伴表單視圖 -- record idview_partner_form_inherit modelir.ui.view field nameinherit_id refbase.view_partner_form/ field namearch typexml !-- 在“名稱”字段后添加“客戶等級”單選框 -- field namename positionafter field namecustomer_rank widgetradio/ /field !-- 在“電話”字段后添加“內(nèi)部分機號” -- field namephone positionafter field nameinternal_phone/ /field !-- 在表單底部添加一個自定義按鈕 -- xpath expr//sheet positionbefore div classoe_button_box namebutton_box button namesend_vip_greeting string發(fā)送問候 typeobject classoe_stat_button iconfa-envelope field namecustomer_rank widgetstatinfo string等級/ /button /div /xpath /field /record !-- 繼承合作伙伴列表視圖 -- record idview_partner_tree_inherit modelir.ui.view field nameinherit_id refbase.view_partner_tree/ field namearch typexml field namephone positionafter field namecustomer_rank/ /field /field /record !-- 繼承合作伙伴搜索視圖 -- record idview_partner_filter_inherit modelir.ui.view field nameinherit_id refbase.view_res_partner_filter/ field namearch typexml xpath expr//filter[nameactive] positionafter filter namefilter_vip stringVIP客戶 domain[(customer_rank,,vip)]/ filter namefilter_vvip string尊享VIP domain[(customer_rank,,vvip)]/ /xpath field namephone positionafter field namecustomer_rank filter_domain[(customer_rank,ilike,self)]/ /field /field /record !-- 繼承銷售訂單表單視圖將客戶等級字段顯示在客戶信息附近 -- record idview_sale_order_form_inherit modelir.ui.view field nameinherit_id refsale.view_order_form/ field namearch typexml !-- 定位到銷售訂單的客戶信息區(qū)域 -- xpath expr//div[namepartner_shipping_id]/.. positionbefore label forpartner_id_customer_rank string客戶等級/ field namepartner_id.customer_rank readonly1 classoe_inline/ /xpath /field /record /data /odoo4.3 模塊的安裝與調(diào)試放置模塊將my_partner_extension文件夾放到Odoo的插件路徑下通常是addons/目錄。更新應(yīng)用列表在Odoo開發(fā)者模式下進(jìn)入“應(yīng)用” - “更新應(yīng)用列表”。搜索并安裝搜索“客戶擴展模塊”并安裝。調(diào)試視圖如果視圖沒有按預(yù)期顯示進(jìn)入開發(fā)者模式?debug1然后在表單視圖上點擊“調(diào)試圖標(biāo)小蟲子” - “編輯視圖表單”。這會打開視圖結(jié)構(gòu)編輯器你可以看到最終渲染的視圖XML檢查你的xpath是否生效定位是否準(zhǔn)確。查看日志。Odoo服務(wù)端日志通常終端或日志文件會詳細(xì)記錄視圖加載時的錯誤如xpath找不到節(jié)點。5. 進(jìn)階技巧與避坑指南掌握了基礎(chǔ)我們來看看那些能讓你的開發(fā)更高效、更穩(wěn)健的進(jìn)階知識。5.1 使用attrs屬性實現(xiàn)條件顯示/必填/只讀這是Odoo視圖中最強大的動態(tài)特性之一。你可以讓一個字段的可見性、是否必填、是否只讀取決于另一個字段的值。field nameinternal_phone attrs{invisible: [(customer_rank, !, vip)], required: [(customer_rank, , vvip)]}/invisible當(dāng)customer_rank不是vip時該字段隱藏。required當(dāng)customer_rank是vvip時該字段必填。還可以用readonly。避坑點attrs中的域domain表達(dá)式其左值必須是當(dāng)前視圖所在模型的字段。如果你需要根據(jù)關(guān)聯(lián)模型的字段來控制通常需要在當(dāng)前模型中創(chuàng)建一個相關(guān)的計算字段related字段。5.2 繼承并修改ir.actions.act_window上下文或域有時你不僅想改視圖還想改打開這個視圖的“動作”行為比如默認(rèn)的篩選條件。!-- 修改“客戶”菜單動作默認(rèn)只顯示VIP客戶 -- record idaction_partner_form_inherit modelir.actions.act_window field namename客戶/field field nameres_modelres.partner/field field nameinherit_id refbase.action_partner_form/ field namecontext{search_default_filter_vip: 1}/field !-- 默認(rèn)啟用名為filter_vip的篩選器 -- !-- 或者使用 domain -- !-- field namedomain[(customer_rank, in, [vip, vvip])]/field -- /record5.3 處理多模塊繼承沖突當(dāng)多個模塊試圖繼承并修改同一個視圖的同一位置時會發(fā)生沖突。Odoo通過視圖的priority字段和模塊加載順序來決定誰“勝出”。priority值越高優(yōu)先級越高。最佳實踐盡量避免直接競爭。如果必須修改同一節(jié)點考慮通過更精確的xpath定位到不同子節(jié)點或者在你的模塊中創(chuàng)建一個更高優(yōu)先級的視圖。record idview_partner_form_inherit_high_priority modelir.ui.view field namepriority20/field !-- 默認(rèn)是16更高的值后加載會覆蓋先加載的 -- ... 其余繼承定義 ... /record5.4 模型繼承中的api.model與api.model_create_multi在重寫create方法時Odoo 13之后推薦使用api.model_create_multi裝飾器以支持批量創(chuàng)建但內(nèi)部邏輯要處理好。api.model_create_multi def create(self, vals_list): for vals in vals_list: # 你的預(yù)處理邏輯 if vals.get(name): vals.setdefault(customer_rank, basic) # 務(wù)必調(diào)用super return super(ResPartner, self).create(vals_list)5.5 視圖繼承的“核武器”直接替換整個視圖在極少數(shù)情況下原有視圖結(jié)構(gòu)過于復(fù)雜或不適合你的需求你可以選擇不繼承而是直接定義一個新的視圖并讓菜單動作指向它。這相當(dāng)于放棄了繼承的優(yōu)雅換來了完全的控制權(quán)。不到萬不得已不要用這招。!-- 1. 定義一個全新的視圖 -- record idview_partner_form_custom modelir.ui.view field namenameres.partner.form.custom/field field namemodelres.partner/field field namearch typexml form !-- 完全自定義的布局 -- /form /field /record !-- 2. 修改或創(chuàng)建一個動作使用這個新視圖 -- record idaction_partner_custom modelir.actions.act_window field namename客戶自定義視圖/field field nameres_modelres.partner/field field nameview_modetree,form/field field nameview_id refview_partner_form_custom/ !-- 指定默認(rèn)表單視圖 -- ... /recordOdoo的繼承機制是其作為強大ERP框架的靈活性所在。它迫使開發(fā)者以一種可維護(hù)、可升級的方式進(jìn)行定制。核心思想永遠(yuǎn)是通過創(chuàng)建新的、獨立的模塊來擴展而非修改原有模塊。從模型到視圖這條原則一以貫之。剛開始接觸xpath和繼承語法可能會覺得繁瑣但一旦掌握你會發(fā)現(xiàn)它是應(yīng)對千變?nèi)f化業(yè)務(wù)需求的瑞士軍刀。記住多利用開發(fā)者工具查看視圖結(jié)構(gòu)多查看Odoo標(biāo)準(zhǔn)模塊的源碼作為參考這是最快的學(xué)習(xí)路徑。