尧图精选

Odoo 19模块结构全解析:从目录到加载顺序的实战指南

🕒 发布时间:2026/9/18 4:26:58 📁 来源:尧图网络
老实说Odoo 这个框架我一直觉得是 ERP 领域里最适合“边用边改”的方案尤其是从 17、18 一路用到 19版本越升模块化这套东西反而越显得重要。很多人上来就盯着界面和功能点忽略了它背后的模块结构设计结果真到要二次开发或者排查问题的时候才发现连自己改过的东西放在哪个目录都说不清楚。这篇文章我就拿 Odoo 19 为例把模块结构的方方面面掰开来讲一个标准模块应该有哪些目录、每个文件是干嘛的、加载顺序是怎么回事、从零搭一个模块要走哪些流程最后再聊聊我实际踩过的坑。不管你是刚接触 Odoo 的初学者还是已经做过几个定制模块的老手这篇内容都值得从头到尾看一遍。理解模块结构这件事短期看是“写代码更顺手”长期看是“少熬夜排查问题”性价比非常高。1. Odoo 19 模块结构到底干的是什么活1.1 模块化架构为什么 Odoo 要搞成积木Odoo 的模块化设计和乐高积木的思路几乎一模一样。一个完整的业务系统如果只做成一坨代码开发时图省事后期维护就是灾难改一个支付逻辑可能影响库存动一下销售订单又牵扯到财务对账。Odoo 从设计之初就决定把所有能力拆成独立模块每个模块负责一块业务边界比如销售、采购、库存、会计、人力资源各有各的模块。模块之间再通过显式的依赖关系连接起来形成一张清晰的依赖网。这种方案能流行这么多年核心在于三个好处。第一是边界清晰每个模块只关心自己的业务对象和数据表代码量可控逻辑相对独立。第二是可插拔需要什么功能就安装什么模块用不到的模块可以保持未安装状态不影响系统运行。第三是易于扩展官方模块没覆盖到的需求开发者可以新建自己的模块通过继承机制修改现有模型、视图、流程而不是去改动系统原始代码。这一点在 Odoo 19 里体现得尤为明显官方对继承和扩展的支持越来越成熟几乎任何原生页面都能通过“模块”这种形式做二次开发。那模块在文件系统层面长什么样简单说就是一个包含__manifest__.py的目录。这个目录里放着一整套配套文件Python 模型、XML 视图、CSV 权限数据、静态资源等等最后打包成一个独立的“包裹”。Odoo 19 启动时会扫描配置好的 addons 路径下所有目录凡是包含有效__manifest__.py的目录都会被识别为可安装模块。这个机制从早期版本一直延续到现在稳定且高效。1.2 Odoo 19 与旧版本在结构上的差异很多从 Odoo 16、17 升级上来的团队最关心的就是“我以前的模块能不能直接拿来用”。答案没那么简单因为 Odoo 19 在结构层面确实有不少变化。最明显的一点是 Python 版本要求。Odoo 19 运行在 Python 3.10 及以上环境实测 Python 3.12 也没问题这意味着模块代码里可以用更多新语法比如match语句、更友好的类型标注。但反过来说如果你的服务器还在用 Python 3.8 或者 3.9那基本告别 Odoo 19 了。另一个变化在视图层面。Odoo 19 延续了 17 版本开始的可扩展视图架构官方把视图解析变得更灵活xpath表达式支持的场景更多了模块之间互相改视图时的冲突概率比旧版本低不少。同时19 里对列表视图、表单视图的 JS 组件体系做了进一步收敛web前端框架里的 OWL 组件成为绝对主流老式的qweb模板仍然能用但新写的模块最好直接采用 OWL 开发方式。还有一个容易被忽略的点是__manifest__.py里的键值变化。Odoo 19 对depends、data、assets这些键的解析逻辑做了优化比如assets键现在支持更细粒度地指定后端和前端资源data键对 CSV、XML 文件的加载顺序要求更严格。这些变化不会让你写不出模块但会影响你组织代码的方式。2. 一个标准模块的目录解剖2.1__manifest__.py模块的身份证任何 Odoo 模块的根目录下都必须有一个__manifest__.py它是模块的元数据文件也是 Odoo 识别一个目录是否具备模块资格的依据。我用一个实际例子来说明它的典型结构{ name: Asset Maintenance, version: 19.0.1.0.0, category: Operations, summary: Manage equipment maintenance plans, description: Extend asset management with maintenance plans., author: Your Company, website: https://example.com, license: LGPL-3, depends: [base, mail, account_asset], data: [ security/ir.model.access.csv, security/asset_maintenance_security.xml, data/maintenance_plan_data.xml, views/asset_maintenance_views.xml, views/menu_views.xml, ], demo: [ demo/maintenance_demo.xml, ], assets: { web.assets_backend: [ asset_maintenance/static/src/js/maintenance_dashboard.js, asset_maintenance/static/src/scss/maintenance_dashboard.scss, ], }, installable: True, application: False, auto_install: False, }这里每个键都有自己的作用。depends是重中之重它声明了这个模块依赖哪些其他模块。Odoo 19 在加载模块时会先递归加载所有依赖保证当前模块的模型和视图可以安全引用依赖模块里定义的东西。如果漏写依赖轻则运行时找不到对象重则整个模块直接加载失败。data键按顺序列出模块需要加载的数据文件和视图文件。这个顺序非常敏感如果security/ir.model.access.csv放在视图文件后面可能导致页面能打开但操作时提示权限不足如果data里的 XML 记录引用了还没加载的视图同样会报错。所以我在实际项目中养成一个习惯先加载安全规则再加载数据文件然后加载视图最后加载菜单。assets键在新版本里越来越重要它负责声明模块的前端静态资源。Odoo 19 会把同一 bundle 下的所有资源合并压缩模块只需要把自己对应的 JS、SCSS 文件按 bundle 名挂载进去就行。需要注意资源路径的格式是模块名/static/src/...少了模块名作为前缀Odoo 19 会直接忽略这个资源文件。2.2 models 与 viewsMVC 里的核心两件套models目录存放所有 Python 模型定义文件这是整个模块业务逻辑的载体。一个模块可以有一个或多个模型文件文件内部用from odoo import fields, models引入基类然后通过继承或者新建的方式定义模型。比如from odoo import fields, models class MaintenancePlan(models.Model): _name asset.maintenance.plan _description Maintenance Plan asset_id fields.Many2one(account.asset, stringAsset, requiredTrue) maintenance_date fields.Date(stringNext Maintenance Date) interval_days fields.Integer(stringInterval (Days), default90) active fields.Boolean(defaultTrue)在 Odoo 19 的模型定义里我特别提醒几个细节。第一_name采用点分命名法比如asset.maintenance.plan这决定了对应数据库表的默认名称最终表名会将点替换为下划线并加上模块前缀。第二字段类型要用 ORM 提供的类型而不是原生 Python 类型这样才能被框架的 ORM 机制正确映射到数据库。第三如果你继承的是现有模块的模型只需用_inherit account.asset加上新字段即可不需要重复定义_name。views目录存放 XML 格式的视图文件包括表单视图、列表视图、搜索视图、看板视图、菜单定义等等。一个最小化的表单视图长这样?xml version1.0 encodingutf-8? odoo record idview_maintenance_plan_form modelir.ui.view field namenameasset.maintenance.plan.form/field field namemodelasset.maintenance.plan/field field namearch typexml form stringMaintenance Plan sheet group field nameasset_id/ field namemaintenance_date/ field nameinterval_days/ /group /sheet /form /field /record /odoo视图 id 在整个模块中必须唯一这个 id 会用于后续的继承修改或菜单关联。model字段声明这个视图渲染哪个模型。arch字段里的 XML 是视图结构本体form 标签内的所有元素在 Odoo 19 里都会被解析成对应的前端组件。2.3 security 与 data权限和初始数据的门道权限这块Odoo 19 沿用了老一套但非常有效的机制security/ir.model.access.csv控制模型级别的增删改查权限security/*.xml里可以定义记录规则Record Rule实现行级别的数据过滤。先看最基础的 CSV 权限文件id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink access_asset_maintenance_plan_user,asset.maintenance.plan.user,model_asset_maintenance_plan,base.group_user,1,1,1,0 access_asset_maintenance_plan_manager,asset.maintenance.plan.manager,model_asset_maintenance_plan,base.group_system,1,1,1,1这里每一行代表一个访问权限条目。model_id:id指向模型的 external id格式是model_加上模型名下划线分隔group_id:id指向安全组base.group_user是所有内部用户base.group_system是管理员。perm 系列字段分别对应读、写、创建、删除四个权限1 表示允许0 表示不允许。data目录通常放模块初始化时需要加载的静态数据比如配置参数、默认分类、初始业务记录。这里的数据文件是 XML 格式本质是通过record标签向 Odoo 模型写入记录。注意data与demo目录有本质区别data里的数据在模块安装时无论如何都会加载而demo里的数据只在开启演示数据模式时才加载。生产环境千万别把测试数据放在data里这点我吃过不少亏。2.4 controllers、static、report 等辅助目录除了核心的 models 和 views一个完整模块还常有这些辅助目录controllers/存放 HTTP 控制器文件用来处理网站路由、HTTP 请求。这类文件通常在使用 Odoo 网站功能或需要暴露自定义接口时用到。模块如果没声明依赖website那这里的控制器基本只对内部调用有效。static/静态资源目录包含src/jsJavaScript 文件、src/scss样式文件、src/xml前端模板和src/img图片。这个目录下的文件需要通过assets键挂载到对应 bundle否则不会生效。report/报表模板目录存放 QWeb 报表的 XML 定义。Odoo 19 的报表仍然基于 QWeb 模板定义在report目录下的 XML 会被专门收集。wizards/向导模型目录一般放临时交互模型比如批量处理表单、状态转换弹窗等。向导模型表名通常以.wizard结尾。i18n/国际化翻译文件目录后缀是.po每个语言一个文件。模块开发阶段可以先不管但上线前最好补齐。tests/自动化测试文件目录Odoo 19 使用 Python 标准库unittest风格编写测试用例。CI/CD 流程里通常会自动跑这里的用例。这些目录不是每个模块都必须齐全但理解它们各自的作用后你在组织代码时就不会把所有东西都堆在 models 里面了。3. 手把手从一个最小模块开始结构落地实操3.1 创建模块目录与基础文件纸上谈兵没用真正动手才能理解结构。我建议你从零建一个模块试试不追求功能多复杂重点是跑通整个加载链路。以一个“设备检修计划”模块为例目标是在资产管理的基础上增加检修计划表。首先在 addons 路径下创建目录asset_maintenance然后在里面依次创建以下文件asset_maintenance/ ├── __init__.py ├── __manifest__.py ├── models/ │ ├── __init__.py │ └── maintenance_plan.py ├── views/ │ ├── asset_maintenance_views.xml │ └── menu_views.xml ├── security/ │ └── ir.model.access.csv └── data/ └── maintenance_plan_data.xml__init__.py文件在两个层级都要存在因为 Python 会把目录当作包来导入。模块根目录的__init__.py内容很简单from . import modelsmodels/__init__.py则导入具体的模型文件from . import maintenance_plan不要小看这一层导入关系很多新人在models目录下新建了文件却忘了写from . import xxx结果运行时报“模型不存在”却找不到原因。3.2 写一个真实模型并配上视图接着是models/maintenance_plan.py我们可以写得稍微完善一点加上一些业务方法from datetime import timedelta from odoo import api, fields, models class MaintenancePlan(models.Model): _name asset.maintenance.plan _description Asset Maintenance Plan _order maintenance_date ASC asset_id fields.Many2one(account.asset, stringAsset, requiredTrue) plan_name fields.Char(stringPlan Name, requiredTrue) maintenance_date fields.Date(stringNext Maintenance Date, requiredTrue) interval_days fields.Integer(stringInterval (Days), default90) last_execution_date fields.Date(stringLast Execution Date) note fields.Text(stringNotes) api.model def _cron_update_maintenance_dates(self): plans self.search([(maintenance_date, , fields.Date.today())]) for plan in plans: plan.write({ last_execution_date: plan.maintenance_date, maintenance_date: plan.maintenance_date timedelta(daysplan.interval_days), })这里体现了一个基本但完整的模型结构几个常见字段类型、一个搜索排序约束、一个可以交由定时任务调用的方法。视图文件views/asset_maintenance_views.xml里我同时定义一个列表视图和一个表单视图?xml version1.0 encodingutf-8? odoo record idview_maintenance_plan_list modelir.ui.view field namenameasset.maintenance.plan.list/field field namemodelasset.maintenance.plan/field field namearch typexml list stringMaintenance Plans multi_edit1 field nameplan_name/ field nameasset_id/ field namemaintenance_date/ field nameinterval_days/ field namelast_execution_date/ /list /field /record record idview_maintenance_plan_form modelir.ui.view field namenameasset.maintenance.plan.form/field field namemodelasset.maintenance.plan/field field namearch typexml form stringMaintenance Plan sheet group group field nameplan_name/ field nameasset_id/ /group group field namemaintenance_date/ field nameinterval_days/ field namelast_execution_date/ /group /group group field namenote/ /group /sheet /form /field /record /odoo注意我在 Odoo 19 里用了list标签而不是老版本的tree标签。官方在 17 之后逐渐推list19 中两种写法都兼容但新代码建议直接用list这也是保持未来兼容性的做法。菜单视图单独放一个文件views/menu_views.xml?xml version1.0 encodingutf-8? odoo menuitem idmenu_asset_maintenance_root nameMaintenance sequence20/ menuitem idmenu_asset_maintenance_plan nameMaintenance Plans parentmenu_asset_maintenance_root actionaction_maintenance_plan/ record idaction_maintenance_plan modelir.actions.act_window field namenameMaintenance Plans/field field nameres_modelasset.maintenance.plan/field field nameview_modelist,form/field /record /odoo3.3 加载模块并验证结构所有文件就位后在 Odoo 19 中通过命令行更新模块列表然后安装模块python3 odoo-bin -d mydb -i asset_maintenance如果模块已经安装过后续代码变更使用-u asset_maintenance更新模块。安装完成后打开“主菜单 - Maintenance - Maintenance Plans”应该能看到列表页面。这个最小模块验证的核心链路是__manifest__.py声明依赖和数据文件顺序 -models里的 Python 模型注册到 ORM -security里的 CSV 生成访问权限 -views里的 XML 生成界面视图 - 前端成功渲染。只要这条链路通畅模块结构就算是落地了。4. Odoo 19 模块结构里的隐藏细节与版本坑4.1 manifest 的版本与升级机制version字段的写法看着简单实际影响很大。Odoo 官方模块的版本号通常采用19.0.x.y.z格式模块内部结构如果要触发数据库变更或者重新加载 XML需要依赖“版本号提升”。当你在开发期修改了模型字段定义或视图 XML只保存文件不升版本号然后直接重启服务有时候会发现界面没有变化原因就是 Odoo 的模块更新机制把版本号作为是否重新加载的关键判断依据。实际操作中我的做法是还在开发期的新模块每次改动后直接执行-u 模块名强制更新不管版本号;但模块发布后每次正式改动必须至少将版本号末位加 1同时在 changelog 里记一笔这样后续升级才能平滑。另外注意installable和application两个字段。application为True时模块会显示在应用商店的应用列表里适合业务型模块为False的技术类模块建议显式声明避免用户误认为这是一个独立应用。4.2 文件加载顺序的讲究文件加载顺序是 Odoo 模块开发里最容易被低估的一个环节。__manifest__.py的data列表里如果存在相互引用的记录顺序错乱会导致External ID not found或KeyError之类的报错。以我常用的顺序为例security/ir.model.access.csv先建模型权限避免后续数据加载时操作无权限。security/*.xml再加载记录规则、安全组。安全组定义最好放在权限 CSV 之前因为 CSV 里的group_id:id引用了安全组的 external id。data/*.xml加载基础数据、配置项、序列等。views/*.xml加载视图、动作、菜单。菜单的action字段会引用动作的 external id所以动作必须先定义。有报表或模板的话放在视图之后。这个顺序不是绝对的但遵循它能规避 90% 以上的加载报错。真遇到顺序问题报错信息里往往会指名道姓地告诉你哪个 external id 找不到直接去data列表里把对应文件提前即可。4.3 视图模型字段的变更注意Odoo 19 在视图解析上的改变值得单独提一下。旧版本里视图字段如果引用了不存在的字段通常会直接抛错。但在 19 中部分视图尤其是看板、表单的容忍度有变化有时前端只报一个 warning界面照常渲染缺失字段的占位符。这其实是很危险的行为因为生产环境中你可能会忽略这个 warning一直到用户点某个按钮才发现数据根本没显示。遇到这种情况我的排查习惯是每次升级模块后打开开发者模式的“视图”菜单检查是否有已安装视图的“架构”里出现了未知字段或者在浏览器控制台里过滤warning级别日志。视图继承时也建议多用xpath定位少用按位置替换的老写法这样即使原视图结构有微调继承视图也不容易失联。5. 常见问题与排查技巧实录5.1 模块加载报错排查模块无法安装是最高频的问题报错形式五花八门。我按大家最容易遇到的几种情况整理成速查表现象可能原因排查思路Module not found__manifest__.py缺失或 addons 路径未包含该目录检查模块目录是否有 manifest 文件检查启动参数-p/ addons_path 配置External ID not founddata文件顺序错误引用了尚未加载的记录打开报错中提到的 external id全局搜索确认定义位置调换data顺序KeyError: xxx或字段不存在视图引用了模型里未定义的字段打开开发者模式 - 视图检查架构对照模型字段名注意大小写和下划线权限不足、看不到菜单ir.model.access.csv未正确配置用管理员账号更新模块检查 CSV 中的模型 external id 是否准确JS 资源不生效assets键路径写错或 bundle 名称不对打开浏览器开发者工具 Network搜索模块名确认资源是否加载还有一个常见场景修改了 Python 代码后执行模块更新却发现改动没生效。这种情况大概率是 Odoo 进程没完全重启或者本地缓存了旧的.pyc文件。直接用pkill -f odoo-bin后重新启动能解决一大半“改了没反应”的问题。5.2 字段不生效、视图报错等高频问题模块结构本身没问题但运行期间报错的情况更多。我遇到过的典型案例包括场景一新增字段保存后数据库无该列。在models里正常加了fields.Char然后更新模块但数据库里就是看不到这个字段。检查后发现模型的 Python 文件虽然创建了但models/__init__.py里没有from . import xxx导致模型根本没被注册。解决方法是补上导入语句再执行一次模块更新。场景二视图能打开但点保存时提示某字段违反唯一性约束。这通常不是视图结构问题而是模型层面缺少sql_constraints或没有合理设置_rec_name。在 XML 视图里直接加required1只能保证界面校验数据库层的约束需要在模型里定义。场景三继承视图不生效。用_inherit ir.ui.view方式继承视图时record的id必须和原视图的 external id 一致而且arch里要写xpath表达式。如果继承的是官方模块视图别忘记在depends里声明对应模块依赖。5.3 性能与调试建议速查表模块装多了之后启动变慢、页面卡顿都是正常的。建议按下面几条做优化把不用的模块及时卸载不要保留一堆半残模块它们即使没被打开也会参与视图注册和权限计算。在data里尽量用轻量记录避免加载海量演示数据。生产数据库别安装带demo数据的模块。视图字段不要贪多列表视图里三五个核心字段就够了字段越多前端渲染越慢。启动时加--log-leveldebug可以查看模块加载的每个文件耗时定位启动慢的瓶颈。开发期为减少改动后的等待时间建议只启动http端口不加载gevent或者longpolling相关服务。调试方面的小技巧在 Python 代码里用_logger.info输出关键变量比在 XML 里频繁print要靠谱得多。前端报错则优先看浏览器控制台Odoo 19 的错误信息比旧版本详细很多通常会直接告诉你哪个组件、哪个字段、哪条记录出了问题。6. Odoo 19 模块结构还可以怎么扩展模块结构不是一成不变的标准模板它会随着业务复杂度演进。比如模块越做越大数据文件和视图文件全都堆在views目录下会非常混乱这时候可以按业务子域拆分子目录比如views/sale/、views/inventory/、views/accounting/只要__manifest__.py的路径写对Odoo 19 完全支持这种组织方式。模型文件也一样。早期项目可能把所有模型都塞在models/下的一个xxx.py里后期模型数量超过二十个我基本会按继承对象或业务域拆文件比如models/asset.py、models/maintenance.py、models/partner.py。这样模块结构更清晰多人协作时也能减少代码冲突。除了目录层面的扩展模块结构还允许你挂载很多高级能力。比如通过static/src/js/里的 OWL 组件扩展前端界面通过controllers/暴露 REST API通过report/生成自定义 PDF 报表。这些能力并不需要改变模块的基础结构但充分利用它们能让模块从“记录管理工具”升级成“业务处理平台”。最后说一个扩展时的原则保持模块独立性和依赖最小化。一个模块只做一件事完成一个完整的业务闭环。如果两个模块必须互相依赖那通常是设计有问题应该把公共逻辑抽取到更底层的模块里。这样每个模块都能独立安装、卸载、升级测试和维护的成本都会大幅降低。我自己在项目里见过太多“全家桶”式模块一个模块塞了销售、采购、库存、财务、报表、审批流最后谁都不敢动它。与其这样不如踏踏实实按业务边界拆模块。Odoo 19 的模块结构设计已经给了你足够好的框架剩下的是看你怎么规划和保持自律。
上一篇/下一篇内容由系统自动关联 返回资讯列表 →