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