Odoo二开必修:模块继承之模型与视图继承完整实战
发布时间:2026/10/16 1:03:57 锦皓数字建站

做Odoo二开的都知道最怕的不是业务逻辑复杂而是你接手的时候系统已经跑了大半年客户天天在用。这时候你说这个字段我得改核心源码才能加没人敢让你动。我早期带团队时有个同事就是直接改了某标准模块的源文件来加一个客户分类字段结果客户升级系统后那个文件被覆盖字段全没了数据倒是还在表里但界面一片混乱。那次之后我定了个规矩凡是二开需求一律通过模块继承来做不碰任何标准模块的源代码。Odoo模块继承是官方为二次开发准备的核心机制它允许你在不改动原模块代码的前提下给现有模型加字段、改方法、调整视图、扩展菜单和报表。这篇是模块继承第一讲我默认你已经能写一个最简单的Odoo模块有manifest、有模型、有视图、能安装下面我重点拆解三种继承方式里最常用、最值得先吃透的两种模型继承_inherit和视图继承inherit_id。这两种搞定市面上八九成的二次开发需求你都能接得住。1. 从改源码到写模块Odoo继承机制到底解决了什么问题先说一个反常识的事情Odoo的源码几乎完全可以当作文档来看但它绝不适合直接修改。原因很简单模块化架构决定了标准模块之间互相依赖你今天改了某个模型里的字段逻辑可能没影响当前功能但下次官方升级或安装新模块时这个改动就变成了冲突源。1.1 继承机制的本质在不破坏原模型的前提下叠加能力Odoo的模型层基于ORM实现它的继承不是传统编程里子类扩展父类那么简单更像是给同一个模型动态叠加补丁。当你写下class ResPartner(models.Model): _inherit res.partner这段代码的意思是请在原有合作伙伴模型上再加点东西而不是请创建一个新模型。原模型的所有字段、方法、约束都还在你的代码只是往里面追加内容。这种设计的好处极其明显逻辑解耦。你的模块负责你的功能标准模块负责标准功能两者在代码层面互不侵入但在运行时无缝融合。1.2 改源码与写模块的真实代价对比我见过太多刚入门的开发者纠结这个问题。直接改源码确实爽加字段到标准视图里保存刷新完事。但代价你迟早要还对比维度直接改源码写模块继承开发速度快改完即用稍慢需要搭模块骨架升级兼容性升级必冲突手动合并几乎无感继承自动合并多环境同步容易漏改文件模块即部署单元团队协作代码冲突频繁各自模块互不干扰卸载回滚几乎不可能卸载模块即恢复原状我后来带项目有个习惯接任何需求先问一句这个功能能不能用继承实现如果答案是肯定的就走模块化路线。有一次客户要求把采购订单的编号前缀改成自定义格式我们做了一个只有几十行代码的继承模块后来客户升级系统前后不到十分钟就完成了那边的IT负责人自己都惊讶。1.3 第一讲你该掌握到什么程度继承机制本身有三个层次模型字段扩展、业务方法重写、视图结构修改。第一讲不贪多主攻前两个加上第三个的基础用法。也就是说学完这篇你应该能做到给现有模型加字段、加默认值、加选择项把现有方法的逻辑篡改一部分且不影响原始流程以及在表单视图里插入、调整字段位置。如果你能顺手把这三个能力组合起来用比如加一个字段这个字段影响某个按钮的行为同时把这个字段显示在指定位置那你对Odoo的理解就超过了大多数只写业务模块的开发新手。2. 三种继承方式模型继承、视图继承、模块依赖继承第一讲先吃透哪几种很多人一查资料看到继承有三四种写法直接被唬住了。实际上日常开发里用得最多的是两类剩下的是特定场景才需要。我在这一节帮你把概念彻底理清。2.1 模型继承两种写法要分清模型继承有两种典型写法效果完全不同这是最容易被混淆的点。第一种是扩展式继承也是最常用的class SaleOrder(models.Model): _inherit sale.order这种写法不创建新模型只对sale.order现有模型打补丁。你在里面定义的字段会直接加到sale.order模型上定义的方法会直接替换或扩展同名方法。这是二开最主力的方式。第二种是原型继承也叫复制继承class CustomOrder(models.Model): _inherit sale.order _name custom.order注意这里同时定义了_inherit和_name。含义是以sale.order为模板复制出一个新模型custom.order。新模型拥有原模型的所有字段但本质上和原模型没有ORM层面的耦合。这种写法主要用于你不想污染原模型、但要复用一套完整字段设计的情况比如做特殊单据类型时。第一讲你只需要吃透第一种第二种等做到复杂业务再回头补也不晚。2.2 视图继承界面改动的正确打开方式视图继承的核心思路和模型继承如出一辙通过inherit_id指定要继承的原始视图然后在arch结构里用XPath定位到某个具体位置插入或修改内容。我先给你一个感性的认识record idview_partner_form_inherit modelir.ui.view field namenameres.partner.form.inherit.customer_source/field field namemodelres.partner/field field nameinherit_id refbase.view_partner_form/ field namearch typexml xpath expr//field[namecity] positionafter field namecustomer_source/ /xpath /field /record这段代码的意思是把合作伙伴表单视图找出来在city字段后面插入一个叫customer_source的字段。inherit_id指向你要改的原始视图xpath负责告诉你改在哪里。2.3 三种继承方式一张表看懂分工继承方式核心关键字作用对象第一讲掌握等级模型扩展继承_inherit模型字段与方法必须掌握模型原型继承_inherit_name复制模型生成新模型了解即可视图继承inherit_id视图界面结构与行为必须掌握顺带解释一个常见困惑为什么讲继承一定要扯到depends因为你要继承res.partner你的模块必须在manifest的depends里声明依赖基础模块base你要继承销售订单就要声明依赖sale。这不是走形式Odoo在升级模块时会根据依赖关系决定加载顺序。依赖声明错误继承的模型或视图根本没被加载报错的时候你会莫名其妙。3. 模型继承实操_inherit、字段扩展与方法重写的完整代码拆解这一节是硬核部分我以一个我从实际项目里提炼出来的需求为例带着你完整过一遍。需求背景是这样客户做销售业务想知道每个客户是通过什么渠道来的比如官网、老客户转介绍、广告。他们希望这个字段直接挂在合作伙伴表单上并且当销售订单确认时如果是转介绍来的客户自动在订单备注里写一句话提醒跟单员回访。这个需求典型地覆盖了模型继承的三个动作加字段、上加字段相关的界面、重写方法。我们一部分一部分来。3.1 搭建模块骨架别把manifest写错先建一个模块目录我习惯命名为partner_customer_sourcepartner_customer_source/ ├── __init__.py ├── __manifest__.py ├── models/ │ ├── __init__.py │ └── res_partner.py └── views/ └── res_partner_views.xml__manifest__.py是模块的门面第一讲你重点关注depends和data这两项{ name: 客户来源管理扩展, version: 16.0.1.0.0, category: Sales, depends: [base, sale], data: [ views/res_partner_views.xml, ], installable: True, application: False, }这里有两个关键点。第一depends为什么既要base又要sale因为我们要给res.partner加字段它属于基础模型声明base是底线同时我们要重写sale.order的方法必须声明依赖sale。第二data里只放视图文件没有模型文件因为模型文件是通过Python的import机制加载的不需要在data中声明。3.2 用_inherit给既有模型加字段models/res_partner.py里面这样写from odoo import models, fields class ResPartner(models.Model): _inherit res.partner customer_source fields.Selection( [(website, 官网询盘), (referral, 老客户转介绍), (ad, 广告投放), (other, 其他)], string客户来源, defaultother, help记录该客户第一次接触公司的渠道 )这段代码要这么理解_inherit res.partner告诉Odoo我要扩展这个现有模型customer_source字段此时就真的成为了合作伙伴模型的一个字段。它跟你在标准模块里定义字段没有本质区别可以正常用在搜索、分组、列表中。我来解释一下这个字段设计里的实用细节。用Selection是因为渠道这种东西枚举值通常可控defaultother是为了防止历史数据为空导致列表视图排序或过滤异常help写清楚业务含义鼠标悬停会显示对后续维护的人非常友好。然后因为我的业务场景要求销售订单确认时读取这个字段所以还需要在销售订单模型里同样通过继承追加一条关联逻辑。新建models/sale_order.pyfrom odoo import models, fields class SaleOrder(models.Model): _inherit sale.order note fields.Text(string备注)等等sale.order本身已经有note字段了。如果你在扩展继承里重新定义一个同名普通字段会造成字段定义冲突。正确的做法是如果只是想要一个备注字段直接用原生的note即可。我在这里特意提这个是想提醒你一个第一讲必须建立的意识扩展字段之前先确认原模型里有没有同名字段别重复定义。你可以从界面开发者菜单里查看模型字段列表或者直接查ir.model.fields表。既然原模型已有note那models/sale_order.py就不需要写任何字段了我们只需要在后续重写方法时直接使用order.note。3.3 方法重写super()的写法和返回值处理在前面的基础上现在我要重写action_confirm()这个方法。先看代码from odoo import models, fields, api class SaleOrder(models.Model): _inherit sale.order def action_confirm(self): res super().action_confirm() for order in self: if order.partner_id.customer_source referral: order.note (order.note or ) 本单来自老客户转介绍请在交付后安排回访。 return res关键点全在super()这一行。它的作用是先把控制权交还给被我们覆盖的原始方法让标准模块的确认逻辑完整执行一遍然后我们拿到返回结果再做扩展处理。这里有一个新手特别容易犯的错在super()调用之后立刻return导致后面的扩展代码永远不会执行。比如# 错误写法 def action_confirm(self): res super().action_confirm() return res for order in self: # 这行永远执行不到 ...还有一种错误是忘记保存返回值。action_confirm()在不同的Odoo版本里返回值不一样有时是bool有时是action字典。如果你在方法里做了很多操作却漏了return res前端点击确认按钮时行为会变得异常有时代理单能确认但页面不跳转有时代理单根本没建出来。之所以在super()之后做扩展是因为订单确认动作本身可能修改了state、生成了仓库单据、创建了日志。如果我先遍历再调super()我基于的状态就是旧状态扩展逻辑可能写错上下文。先执行原始逻辑再在结果基础上调整是方法扩展最安全的姿势。另外要注意super()在Odoo里的使用风格。新版Python推荐直接写super().action_confirm()不需要传参简洁且安全。老代码里你会看到super(SaleOrder, self).action_confirm()那是Python 2时代遗留写法现在没必要模仿。3.4 字段顺序、默认值与相关字段的补充说明围绕模型继承还有几个冷门但实用的细节值得第一讲就掌握。字段在表单里的显示顺序通常由视图里的位置决定跟模型里定义顺序无关。但如果你用fields_view_get动态加字段或者在列表视图里没显式写出字段顺序默认顺序就按ORM模型字段的创建顺序来。继承字段的创建顺序晚于原生字段所以在列表视图如果不指定继承字段会排在很后面。再说related字段。如果你要继承的源字段在关联模型上可以用related直接拉过来source fields.Selection(relatedpartner_id.customer_source, string客户来源, readonlyTrue)这样在订单行甚至订单层面都能直接看到客户来源不需要手动同步值。related字段是Odoo的高级特性但在第一讲里我建议你只读使用不要想着通过它自动回写等完全理解了字段存储机制再玩双向联动。关于默认值的补充Selection字段设置defaultother只是界面层的预选值不是数据库层面的约束。如果某条记录没有设置这个字段查询结果可能是False。所以我在扩展方法里判断时用的是 referral而不是if order.partner_id.customer_source:因为other和False都有可能。这种细节在大批量数据处理时非常关键经常有人的判断逻辑莫名失效查到最后是默认值没有真正落到数据上。4. 视图继承实操XPath定位、arch结构修改与属性调整模型层的字段加好了方法也重写了现在必须让用户界面把这些新东西显示出来。视图继承是这中间最难、也最体现经验的部分。4.1 在XML中声明视图继承的完整姿势还是用前面那个views/res_partner_views.xml?xml version1.0 encodingUTF-8? odoo record idview_partner_form_inherit modelir.ui.view field namenameres.partner.form.inherit.customer_source/field field namemodelres.partner/field field nameinherit_id refbase.view_partner_form/ field namearch typexml xpath expr//field[namecity] positionafter field namecustomer_source requiredFalse/ /xpath /field /record /odoo几点说明name字段理论上全局可以不唯一但在项目里养成用模型类型业务含义的命名习惯比如res.partner.form.inherit.customer_source后续排查视图问题时光看名字就知道它继承自谁。model声明这个视图作用于哪个模型。最关键的是inherit_id里的ref它指向你要修改的目标视图的外部ID。base.view_partner_form是Odoo基础模块里合作伙伴表单视图的外部ID。问题来了我怎么知道每个标准视图的外部ID最简单的办法是利用开发者模式。在开发模式下打开视图界面下拉框选择对应视图就能看到它的External ID。如果你没有开发模式权限也可以去源码里翻base/views/res_partner_views.xml不过那确实费劲。4.2 XPath定位的几种写法按场景选择xpath的expr属性是整个视图继承的核心它决定了你要操作的位置。最常见的写法是基于字段名定位xpath expr//field[namecity] positionafter这种写法的完整含义是找到文档树中field标签且name属性等于city的第一个节点在它后面插入内容。除了字段还可以用其他属性定位。比如找按钮xpath expr//button[stringConfirm Sale] positionattributes或者用更宽泛的属性组合。expr本质是XPath语法理论上任何标准的XPath表达式都可用。但我在实践中总结出一条铁律能通过field[namexxx]定位就不要绕弯子。理由有两个第一是标准模块里字段名相对稳定第二是name属性在视图arch中语义清晰不容易误伤。如果你要操作的元素在重复区域里比如订单行明细里面某个字段xpath expr//field[nameorder_line]/tree/field[nameproduct_id] positionafter这里的关键是层级关系必须写对。order_line是一个One2many字段它内部有子tree你要定位的是这个树视图内部的字段。很多新手写成全局的//field[nameproduct_id]如果表单里恰好还有其他地方有同名product_id就会定位出多个节点直接报错。4.3 常用操作加字段、改显示、加按钮、替换文本视图继承在position属性上有五种操作第一讲你必须掌握两种其他的了解即可。**after和before**是最常用的插入类操作用法前面已经示范过。在某个字段前或后插入新字段这是加字段的标准姿势。**attributes**用于修改已有节点的属性。比如你想把某个字段的必填属性动态改掉xpath expr//field[namecustomer_source] positionattributes attribute namerequiredTrue/attribute /xpath再比如你想给按钮添加确认弹窗xpath expr//button[nameaction_confirm] positionattributes attribute nameconfirm确定要确认这个订单吗/attribute /xpath**replace**用于整体替换一个节点。比如你要替换某个提示文本xpath expr//div[namesome_info] positionreplace div新的提示内容/div /xpathreplace能不留痕迹地改界面但风险也高——如果目标节点里包含其他模块继承添加的内容替换会把这些内容一起干掉。我建议能用attributes解决的绝不replace这是惨痛教训换来的。关于视图继承还有一个新版版本细节。Odoo 13之后继承视图默认就是modeextension你不需要在record里显式写mode系统会自动把它当作扩展视图处理。旧资料里教人写modeprimary或modeextension那是Odoo 11时代的老黄历现在别照搬。4.4 视图继承后的优先级与字段归属问题当你写了多个视图继承同一个原始视图时会出现叠加效应。Odoo会根据sequence属性和模块加载顺序决定这些扩展视图的应用顺序。默认sequence是16如果你希望自己的修改在最后生效就把sequence设大一些要覆盖别人的修改就设小一些。再看字段归属。在表单视图中显示一个字段不代表这个字段属于你当前开发的模型。比如你在合作伙伴表单上插入customer_source这个字段本身定义在合作伙伴模型上任何模块都能在视图里引用它。视图继承只是界面层的事不产生字段所有权。有一种需求是字段要显示但只能由特定用户组修改。我经常在继承视图里配合groups属性使用field namecustomer_source groupssales_team.group_sale_salesman/这样普通用户看得到字段但不可见可编辑销售团队成员才能看到完整字段。这种受限可见性完全是视图层控制比在模型层做权限判断要直观得多。5. 模块继承开发中必须避开的几个坑第一讲版教程讲思路是线性的但实际开发会遇到各种意外。这一节我专门整理第一讲阶段最常踩的坑每个都是我或我身边的同事真实碰到过的。5.1 视图继承报错Element cannot be located的完整排查链路这是视图继承里最高频的报错几乎每个写继承的人都会遇到。完整的报错信息长这样odoo.tools.convert.ParseError: while parsing res.partner.form.inherit.customer_source Element cannot be located in parent view: field[attributename] with value city别慌这个信息很明确你的XPath表达式在target视图里没找到对应的元素。排查按以下顺序来第一步确认inherit_id里ref指向的视图编号是否正确。打开开发者模式的视图菜单搜索目标模型看它的表单视图外部ID是什么。如果你继承的是base.view_partner_form但实际视图的external ID根本不是这个必然报错。第二步确认目标字段真的存在于你继承的那个视图文件里。注意同一个字段可能在模型里存在但不在这个特定视图的架构里。比如city这个字段可能只存在于base.view_partner_form里而res.partner的树视图里根本没有。这时候你如果用一个树视图作为inherit_id再去定位city字段自然定位不到。第三步检查你的XPath表达式是否写得太泛或太具体。//field[namecity]这种写法在表单视图里通常没问题但如果字段在可编辑列表视图的多行区域内可能需要更精确的定位路径。最后检查你的模块在升级时有没有报其他错误。视图继承是相互依赖的——如果多个模块同时修改同一个视图其中一个定位失败后面的都会失败。排查时把所有自定义模块的日志打开找到最早报错的那个。5.2 视图外部ID冲突与继承顺序问题你能继承标准视图别人也能。这是Odoo多模块协作的正常形态但也带来一个隐患多个模块往同一个位置插入内容顺序不可控。举个例子A模块在city字段后加一个字段B模块也在city字段后加一个字段。最终哪个在前取决于两个模块的安装顺序和加载顺序。如果你是B模块的维护者你没法保证用户在装B模块之前一定装了A模块。解决思路有两个。第一不要刻意依赖其他自定义模块的加载顺序第二如果你的字段必须显示在某个确定的相对位置优先选择一个标准视图里稳定存在的字段作为锚点而不是依赖另一个自定义模块插入的字段。id冲突也是新手碰得多的。视图的id在模块内部要唯一但更重要的是跨模块引用时的可预期性。我给你一个建议所有视图继承的id都加上模块名前缀比如我们模块里的view_partner_form_inherit就够清晰但如果你公司有多个项目组同时开发最好写成partner_customer_source.view_partner_form_inherit这种带模块名的形式。避免未来合并代码时出现external ID撞车。5.3 升级模块但看不到效果时的检查顺序这个问题几乎每周都有人在社区里问为什么我加了字段更新模块后界面没变化我的检查顺序是第一步确认是否真的在Odoo里执行了升级。在应用菜单里找到你的模块点击升级而不是只点击应用。修改Python代码仅重新启动服务是不够的修改XML视图必须升级模块才会重新解析。如果你只重启了服务旧的视图定义还留在数据库里。第二步看是否有报错。升级模块后立即查看日志。视图继承相关的错误通常会在升级时被捕获。如果你忽略了报错界面自然不会有变化。第三步确认你在正确的视图下查看。我见过有人改了销售订单表单视图却去合作伙伴列表界面找新字段找了半天没找到。字段在哪个模型上就要去哪个模型的视图里看。第四步考虑浏览器缓存。Odoo前端的assets会做缓存特别是视图相关的静态资源。多按几下CtrlShiftR强刷再进界面看。如果这步之前一切正常很可能是缓存导致的——这种情况我碰到的次数可比想象中多得多。5.4 方法重写时super()用错导致的连锁问题前面讲了super()的顺序问题但还有一个更隐蔽的坑别把super()的结果随意修改后返回。某些扩展方法比如创建或写入记录create()、write()返回值的类型和语义有严格约定。你在扩展时往往需要对记录集做额外操作但绝对要遵循原方法的返回约定。我曾经看到一个同事这样写def write(self, vals): if customer_source in vals: vals[customer_source] other return super().write(vals)表面上逻辑没问题但他在某些条件下直接return True跳过了super().write()导致字段值根本没写入数据库。这个Bug因为每个条件分支都走了不同的返回路径排查了整整一下午。更安全的做法是在任何情况下都让super()完整执行一次再在返回值基础上做微调。如果确实需要提前结束一定要清醒地知道你在放弃什么。另外不要在create()或write()里用self.field value这样的ORM自动更新语法这个在重写方法中很容易触发二次写入甚至递归。需要改值就去改vals字典。这个细节值得贴在你的IDE注释里。5.5 多模块协作时的继承冲突预防当一个系统里跑着好几个二开模块时视图继承的冲突概率直线上升。我处理过的最典型一个场景是两个模块同时继承销售订单的表单视图各自在不同位置加了字段结果安装顺序一变有个字段跑到完全不合理的区域里。预防有四个原则建议你在团队里推广一个模块只继承一个视图的一个位置不要贪多。聚合需求到同一个模块里处理比分散在N个模块里更容易维护。记录锚点字段的选择尽量选那些标准视图里长期稳定存在的字段而不是其他模块的私有字段。最后定期做全局视图审查开发模式下打开每个视图检查arch结构是否符合预期。这几个原则执行到位所谓继承冲突基本就能扼杀在摇篮里。最后再分享一点我个人体会。第一讲的内容学完你能做的已经不少了但别急着把所有继承技术都塞进脑子里。我在带新人时发现把模型扩展继承和视图继承吃透的人后续学习报表继承、菜单继承、数据继承时都非常快。Odoo官方文档很长很多人翻两页就把自己劝退了说实话没必要——上手就用遇到问题再查是这门技术最有效的打开方式。下一篇我会讲计算字段和约束算法在继承场景里的应用那种字段值自动从关联模型算出来的需求很多电商和分销项目都会碰到到时候我们继续用第一讲搭好的模块往里加内容。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。