首页 / 资讯中心 / 文章详情

Odoo模块继承核心套路:模型、视图、方法重写全解析

Odoo模块继承核心套路:模型、视图、方法重写全解析 ★ FEATURED ARTICLE
做Odoo二次开发模块继承是躲不开的一道门槛。我做了这些年Odoo实施经手的项目没有一个能绕开它。客户要改表单布局、加字段、改审核流程、调价格计算逻辑这些需求如果每次都要去改源码文件那升级官方模块的时候就是灾难。Odoo给你提供了一套“不碰源码也能改一切”的机制这就是模块继承。这一讲先把最核心的套路讲透模型继承、视图继承、方法重写讲清楚它们各自解决的场景以及背后的运行逻辑。内容面向刚入门Odoo开发、或者已经在做实施想转开发的读者你不需要有深入Python功底只要写过一点面向对象代码就能跟上。1. 为什么Odoo开发绕不开“模块继承”1.1 不修改源码的硬约束很多新手拿到Odoo第一反应是找到模型定义文件把字段加上改完界面功能正常觉得挺爽。等官方模块一升级才发现自己改的东西全被覆盖了或者更糟代码直接报错冲突。Odoo本身把核心逻辑、官方应用、第三方模块都放在了独立的目录里。这些代码就像一台机器的原厂零件升级就等于换掉整个零件。你在这个零件上钻孔、焊接换新的零件时你的加工成果就没了。模块继承的思路是不在原厂零件上动刀而是在旁边做一个“改装层”运行时由Odoo框架把原定义和你的扩展合并成一个完整对象。这个设计带来的好处非常实际。公司生产环境里官方模块升级是常态安全漏洞要补、新版本功能要上如果你之前继承方式写得规范升级就是把原厂零件换掉你的改装层照常工作。反过来如果全部改源码升级就意味着要重新做一遍二次开发工作量完全是翻倍的。1.2 继承的三种姿势Odoo的模块继承本质上可以分为三个层面这三个层面我建议在一开始就分清后面学习才不会乱。第一是模型继承面向后端数据结构。你想要给一个业务对象加字段或者改已有字段的属性就在Python代码里继承模型。这个继承决定了数据库里最终存储哪些字段、字段怎么计算、校验规则是什么。第二是视图继承面向用户界面。你要在表单页、列表页、看板页里调整布局插入或隐藏字段就在XML里继承视图结构。第三是方法重写面向业务逻辑。你要改变某个功能的行为比如下单时自动算优惠、保存联系人时自动发通知就继承Python方法并在其中加入自己的逻辑。这三个层面在使用时通常是配合的。给客户加“客户等级”字段模型继承负责把字段加进数据库视图继承把这个字段显示在表单上方法重写则负责在等级变化时执行一些额外动作。三者不是可选项而是Odoo二次开发的基本功单独掌握任何一个都不够。继承类型作用对象实现位置典型用途模型继承模型与字段Python模型定义新增字段、修改字段属性、创建新模型视图继承界面结构XML视图描述改表单布局、加按钮、隐藏字段方法继承业务逻辑Python方法在保存、创建、删除时插入自定义逻辑2. 模型继承的核心_inherit 与 _name 的语义差异2.1 只写 _inherit原地扩展现有模型模型继承里最基础也最常用的是这样一段代码from odoo import models, fields class ResPartnerInherit(models.Model): _inherit res.partner customer_level fields.Selection( [(normal, 普通客户), (vip, VIP客户), (key, 关键客户)], string客户等级, defaultnormal, )这里最关键的写法是_inherit res.partner它告诉Odoo我要在已有的res.partner模型上做扩展。模型名不变还是res.partnerPython类名ResPartnerInherit只是一个代码内部名字跟数据库表、跟业务模型名称没有一一对应的关系。这种“原地扩展”的效果是数据库里原来那张res_partner表不会被重建只是往里增加新字段。原模型原来的所有字段、方法、约束全部保留你的新字段和它们共存。我经常用一个生活化的类比帮新人理解原模型是一栋已经建好的房子你不需要拆墙只在客厅里加了一个新的家具。家具和房子一体呈现只要房子不倒家具就在。同理只要原模块还在你的扩展就一直生效。这种写法的优点在于数据层面的兼容性。因为表没有变之前存在的数据都还在原来的地方不需要迁移旧数据里的记录会拿到你新字段的默认值。绝大多数加字段的需求都应该优先用这种方式。2.2 _inherit 与 _name 同时出现复制出新的业务对象原地扩展很好用但有时你需要的不是给原模型加东西而是基于原模型创建一套全新的业务对象。举个例子销售订单sale.order已有的字段和逻辑都很完善现在你想做一套“销售订单草稿”保存时不需要走正常订单的校验和状态流。如果直接往sale.order加字段改逻辑会影响所有正常的销售订单这时候就需要复制继承。from odoo import models, fields class SaleOrderDraft(models.Model): _name sale.order.draft _inherit sale.order这段代码的含义是_name指定了一个新模型sale.order.draft_inherit负责把sale.order的所有字段和方法复制过来当作底子。Odoo框架看到既有_name又有_inherit时会新建一张独立的数据库表新表里包含原模型的字段结构但两张表的数据互不相通。复制继承适合的场景很典型你既要复用原模型的复杂度又要给它一个独立的身份。比如订单归档、报价单转合同、历史数据快照这类需求本质上是“长得像原对象但不是原对象”的东西。用复制继承原模型的校验规则、字段定义、方法逻辑全部免费继承你只需要在子模型里覆盖要改变的部分。需要注意的是复制继承与原地扩展有一个非常重要的区别数据不互通。假设某客户在sale.order里有一条记录关联着你的新模型你想在sale.order.draft里看到这条数据是不可能的因为数据表已经分开了。所以在设计模型时要想清楚这个对象和原对象是“同一种事物的不同状态”还是“两种完全不同的业务概念”。前者用原地扩展后者才考虑复制继承。2.3 字段继承与字段属性重写模型继承不只是“加新字段”它还能对原模型已有的字段做属性重写。这个能力经常被忽视但在真实需求里使用频率非常高。from odoo import models, fields class SaleOrderInherit(models.Model): _inherit sale.order partner_id fields.Many2one( res.partner, string客户, readonlyTrue, help订单保存后不允许修改客户, )你可能会觉得奇怪partner_id本来就在sale.order里我再声明一遍不等于重复吗实际上Odoo的字段继承机制不是“替换”而是“合并”。你重新声明同名同类的字段时原定义里的属性依然存在你写的属性值会覆盖原属性值。比如上面的代码里我改的就是readonly和help模型的类型还是 Many2one关联的还是res.partner。这个机制非常实用。原模块为了通用场景字段可能是可编辑的、没有帮助提示的。但到了你的项目里业务要求这个字段在某些时候不能被改动你不需要去改源码只需要在继承层重写属性。类似的需求还有给字段加默认值、给字段加组别控制、给字段加新的domain过滤条件。唯一要注意的是字段重写只影响属性层面的覆盖。如果原字段是compute计算字段你重写时如果提供了新的compute方法必须同时注意原字段的其他依赖装饰器。而related字段如果你重写成普通字段数据库行为会完全不同这种操作要格外谨慎我建议第一讲阶段先只改string、readonly、help、default这类简单属性。3. 视图继承实操从列表页到表单页的扩展3.1 视图继承的最小骨架模型里加好了字段下一步要让用户在界面上看到它。Odoo界面是由视图View控制的视图像一个图纸决定页面上哪些控件出现、排列顺序、是否只读。想改界面就要用视图继承。视图继承的XML结构有个固定骨架我给你一个最小可用的例子这是一个给联系人表单加字段的经典实现?xml version1.0 encodingutf-8? odoo record idview_partner_customer_level_form modelir.ui.view field namenameres.partner.form.inherit.customer.level/field field namemodelres.partner/field field nameinherit_id refbase.view_partner_form/ field namearch typexml xpath expr//field[namevat] positionafter field namecustomer_level/ field namelast_visit_date/ /xpath /field /record /odoo逐行解释一下关键点。model指这个视图属于哪个模型。inherit_id是视图继承的核心ref指向父视图的 external IDbase.view_partner_form是Odoo内置的联系人表单视图。arch标签里写的就是你的修改内容在页面上找到某个节点在它后面插入你要的内容。xpath用的是标准XML路径语法第一讲你可以先记住一个最常用的模板用//field[namexxx]表达“在页面里找名叫xxx的字段控件”typedefine了位置然后在position属性里写你打算怎么改。视图继承文件写好之后要在模块清单文件的data列表里注册{ name: Partner Customer Level, version: 1.0, depends: [base], data: [ views/partner_views.xml, ], }然后升级模块刷新页面字段就会出现。每次修改XML视图后都必须“升级模块”而不是“重启服务”这个操作顺序很多人会漏。3.2 xpath 定位与 position 的六种用法视图继承里真正决定你修改效果的就是xpath的表达式和position属性。我把六种position的用法给你整理齐第一讲把它们过一遍后面写视图基本不用查文档。position值作用示例场景after在目标节点之后插入在手机号字段后面加一个客户等级字段before在目标节点之前插入在备注字段前面插入一个说明文字inside进入目标节点内部追加到最后在按钮盒里增加一个自定义按钮attribute修改目标节点的某个属性把字段的readonly改为1replace用新内容替换目标节点替换掉原来界面上某个控件move把某个节点移动到新位置把字段从页签A挪到页签B举几个典型写法。修改字段属性时这样写xpath expr//field[namecustomer_level] positionattribute namereadonly True /xpath把某个字段从原位置挪到另一个位置时用 move 配合目标位置xpath expr//field[namelast_visit_date] positionmove / xpath expr//field[namenotes] positionafter field namelast_visit_date/ /xpath写 xpath 表达式我最想强调一个经验不要把目标节点写得太依赖层级也不要使用绝对索引。比如//field[1]这种写法非常脆弱原模型的视图只要加载顺序变一下表达式就会断掉。优先使用//field[namexxx]这种通过字段名定位的方式可读性强且只要页面上没有重名控件就不会出问题。3.3 一个完整的联系人扩展实战现在把前面所有内容串起来做一个可以完整跑通的例子。需求很简单给联系人增加两个字段“客户等级”和“最近回访日期”在联系人的表单里显示在列表里也能看得到。第一步写模型定义。from odoo import models, fields class ResPartnerInherit(models.Model): _inherit res.partner customer_level fields.Selection( [(normal, 普通客户), (vip, VIP客户), (key, 关键客户)], string客户等级, defaultnormal, ) last_visit_date fields.Date(string最近回访日期)第二步写视图继承表单加入口。?xml version1.0 encodingutf-8? odoo record idview_partner_customer_level_form modelir.ui.view field namenameres.partner.form.inherit.customer.level/field field namemodelres.partner/field field nameinherit_id refbase.view_partner_form/ field namearch typexml xpath expr//field[namevat] positionafter field namecustomer_level/ field namelast_visit_date/ /xpath /field /record /odoo第三步写列表视图继承把两个字段加到列表页最右边。?xml version1.0 encodingutf-8? odoo record idview_partner_customer_level_tree modelir.ui.view field namenameres.partner.tree.inherit.customer.level/field field namemodelres.partner/field field nameinherit_id refbase.view_partner_tree/ field namearch typexml xpath expr//field[namephone] positionafter field namecustomer_level/ field namelast_visit_date/ /xpath /field /record /odoo第四步在清单文件中注册视图升级模块。操作完成后打开联系人列表页右边会多出两列进入联系人表单页在税号字段后面能看到这两个字段。整个过程没有改一行Odoo源码完全靠继承实现。这个流程我建议你完整敲一遍哪怕是照抄。因为模块继承的第一讲目标不是写得多复杂而是理解“改一个东西不需要动它本体的文件”这个核心思想。把这个流程跑通后面所有的进阶操作都是在这个基础上加细节。4. 方法重写与逻辑扩展super() 的正确打法4.1 重写 create / write 等核心方法模型继承加字段解决了“存什么”视图继承解决“显示什么”但很多需求还要求“做什么”。比如客户等级从普通改成VIP时自动记录一条操作日志联系人新建时自动填上默认的跟进人员。这些动作需要你重写模型里的方法。Odoo所有模型都有几个核心方法最常用的是create创建记录和write修改记录。重写它们的固定套路如下from odoo import models, fields class ResPartnerInherit(models.Model): _inherit res.partner def write(self, vals): # 在调用原方法之前执行逻辑 if customer_level in vals: self._log_customer_level_change(vals[customer_level]) # 调用原方法 result super().write(vals) # 在原方法执行之后执行逻辑 return result这里的super()是Python面向对象里的标准语法意思是“调用父类的这个方法”。在Odoo的框架机制里所有继承方法紧紧围绕着这条链我的扩展方法先执行然后调用原方法原方法执行完再回到我的代码里继续。整个过程像一个流水线每个环节都有机会包装。4.2 在超类方法前后插入业务逻辑我把上面的例子展开做一个有完整业务含义的逻辑客户等级发生变化时往一个日志模型里写一条记录。虽然日志模型的定义需要额外建模但这里我重点展示方法重写本身。from odoo import models, fields, api class ResPartnerInherit(models.Model): _inherit res.partner def write(self, vals): if customer_level in vals: for record in self: old_level record.customer_level new_level vals.get(customer_level, old_level) if old_level ! new_level: record._create_level_log(old_level, new_level) return super().write(vals) def _create_level_log(self, old_level, new_level): level_map dict(self._fields[customer_level].selection) self.env[customer.level.log].create({ partner_id: self.id, old_level: level_map.get(old_level), new_level: level_map.get(new_level), operator_id: self.env.user.id, })这里有一个特别容易踩的坑在write里遍历self然后在循环内部调用self.env[...].create时如果self包含多条记录每次create都要明确指定是哪一条伙伴记录。上面代码里_create_level_log是单条记录级别的方法所以我在遍历时逐条调用不会导致日志关联错对象。你注意到代码里我用了dict(self._fields[customer_level].selection)这是在获取Selection字段的选项映射。Selection字段的底层含义是“存储值”和“界面显示文案”的配对比如存储值vip显示为VIP客户。日志里存的是文案而不是机器可读的键值这样后期做统计报表更方便阅读。super()调用的位置决定了你的逻辑发生在原逻辑之前还是之后。写在super()之前你的代码先执行此时数据还没真正修改写在super()之后数据已经变了你可以读取最新状态。有些场景需要两边配合核心原则是有依赖原逻辑状态的校验放在super()之前有依赖修改后状态的记录工作放在super()之后。4.3 方法继承与装饰器配合除了create和writeOdoo里还有一批通过装饰器Decorator定义的方法最典型的是api.onchange界面字段联动、api.constrains数据校验、api.depends计算字段依赖。第一讲不需要把这些全部深入但要理解一个关键区别装饰器方法的继承和普通方法继承不完全一样。举个例子。你有一个计算字段根据customer_level生成一个说明文字from odoo import models, fields, api class ResPartnerInherit(models.Model): _inherit res.partner level_description fields.Char( string等级说明, compute_compute_level_description, ) api.depends(customer_level) def _compute_level_description(self): level_map { normal: 普通客户需要保持关注, vip: VIP客户优先处理需求, key: 关键客户由专人跟进, } for record in self: record.level_description level_map.get(record.customer_level, )这个逻辑看起来很简单但你想在原模型已有的计算字段基础上做扩展时就要特别注意。Odoo的装饰器方法继承和普通方法一样可以用super()叠加但如果你重写一个计算字段的compute方法要把原方法的调用保留在合适的位置否则原逻辑丢失。还有一个我踩过的坑某些装饰器方法重写时装饰器不会自动合并。比如两个模块都写了一个api.constrains(vat)方法它们之间是平级的都会在满足条件时执行。但你重写同一个模块里已有的方法时如果不加super()调用原校验就跑不掉了。这个细节在多人协作开发时很容易出现排查思路就是看对方模块是否也重写了同一个方法。方法继承的经验一句话总结能不全量重写就尽量不全量重写能在原方法前后追加逻辑就别把原方法删了重来。框架里那么多方法环环相扣删掉一个环节牵一发动全身。5. 常见报错和排查经验5.1 xpath 找不到节点视图继承里最常见的报错长这样Element cannot be located in parent view。这句话翻译过来就是“你在xpath里写的那个节点父视图里根本找不到”。我遇到这类问题会按下面顺序排查。先确认inherit_id引用的视图是不是真的包含你写的字段。方法是在开发者模式里打开那个视图查看它的结构搜一下字段名。然后检查xpath表达式里的字段名是否和视图结构里完全一致一个字母都不能差注意大小写和下划线。最后看是不是多个视图继承互相影响有时候别的模块已经把这个节点移走或者改名了你的表达式就会扑空。xpath找不到节点还有一个隐蔽原因找了个在视图里没有渲染的字段。比如某个字段只在特定条件多公司、特定用户组下才会显示普通视图里不渲染那你引用它自然会失败。这种情况下更好的定位策略是找一个始终存在的节点比如页面的标题字段围绕它做插入。5.2 新模型在界面看不到建立了一个新模型菜单点什么都没有或者直接报权限错误。这个排查要分两层。第一层是权限。Odoo的模型默认没有给任何人访问权限必须在模块的security目录下配置访问规则。最常见的写法是在ir.model.access.csv文件里加一行id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink access_customer_level_log,customer.level.log,model_customer_level_log,base.group_user,1,1,1,1model_id字段的值是model_加上你模型名字里的点号换成下划线比如customer.level.log就是model_customer_level_log。这一行表示所有内部用户都可以增删改查这个模型。忘记配置权限代码里create一执行就会报权限错误很多人以为是自己业务代码写错了其实是权限文件没配。第二层是界面入口。模型有权限了但用户要能打开它还需要动作Action和菜单。动作负责决定跳转去哪个视图菜单负责把动作挂到导航栏。没有菜单模型在界面上就是存在的但你看不见它。5.3 字段属性不生效有时候重写字段属性比如设置了readonlyTrue但界面上字段仍然可以编辑。这个问题需要区分是视图层面还是模型层面。如果readonly是在模型字段上写的那它影响的是所有视图里的显示状态。但要注意Odoo视图可以单独给字段设置readonly0覆盖模型属性所以排查时先看视图定义里有没有显式写。如果你的模块加载顺序靠后继承了同一个视图的另一个模块可能已经把属性改回去了。还有一种是字段根本没出现在视图里。你在模型里加了新字段也确实写了视图继承但页面上就是没有。这种时候先不要怀疑代码逻辑去数据库里看看字段是否真的创建成功。打开开发者模式的“字段视图”或直接查数据库表如果字段存在那就把焦点放在视图上如果字段不存在说明模块升级失败可能是XML语法错误把整个模块卡住了。我曾经被这个问题折腾过一整天最后发现是清单文件里data列表写错路径模块升级时根本没加载到视图文件页面用的还是没字段的旧视图。排查基础问题千万别靠猜一步步按模型、视图、权限、清单文件的顺序来最快。写在最后的实操体会第一讲的内容量不小但真正要记住的核心只有一点Odoo模块继承的本质是在不破坏原有系统的基础上做一个可以被框架自动合并的“扩展层”。把_inherit吃透把super()调用的位置想清楚把xpath当成一个精准的定位器而不是靠感觉写这三个能力能覆盖八成以上的二次开发需求。我个人在这个领域踩过最多的坑反而是“想当然”。觉得原模型某个字段肯定在某个位置不打开父视图直接写表达式结果升级时报错或者觉得重写方法就是把代码抄一份然后改两行结果漏了super()导致原逻辑整个失效。所以每次动工之前我会打开开发者模式的结构视图把要改的节点位置确认一遍再动手写代码。这个习惯很笨但省下的排查时间比写代码的时间多得多。这一讲的内容消化之后下一讲可以开始聊更进阶的玩法继承多个模型、多态模型、以及复杂表单联动。到时候这些基础打牢了进阶内容会轻松很多。
阅读完成 · 觉得有帮助?
咨询建站