十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Odoo开发实战:模型与视图继承机制详解与避坑指南

Odoo开发实战:模型与视图继承机制详解与避坑指南 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标准模块的源码作为参考这是最快的学习路径。
返回列表