Odoo 19模块结构全解析:从目录到加载顺序的实战指南
2026/9/18 4:26:47 网站建设 项目流程

老实说,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 对dependsdataassets这些键的解析逻辑做了优化,比如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 与 views:MVC 里的核心两件套

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', string='Asset', required=True) maintenance_date = fields.Date(string='Next Maintenance Date') interval_days = fields.Integer(string='Interval (Days)', default=90) active = fields.Boolean(default=True)

在 Odoo 19 的模型定义里,我特别提醒几个细节。第一,_name采用点分命名法,比如asset.maintenance.plan,这决定了对应数据库表的默认名称,最终表名会将点替换为下划线并加上模块前缀。第二,字段类型要用 ORM 提供的类型,而不是原生 Python 类型,这样才能被框架的 ORM 机制正确映射到数据库。第三,如果你继承的是现有模块的模型,只需用_inherit = 'account.asset'加上新字段即可,不需要重复定义_name

views目录存放 XML 格式的视图文件,包括表单视图、列表视图、搜索视图、看板视图、菜单定义等等。一个最小化的表单视图长这样:

<?xml version="1.0" encoding="utf-8"?> <odoo> <record id="view_maintenance_plan_form" model="ir.ui.view"> <field name="name">asset.maintenance.plan.form</field> <field name="model">asset.maintenance.plan</field> <field name="arch" type="xml"> <form string="Maintenance Plan"> <sheet> <group> <field name="asset_id"/> <field name="maintenance_date"/> <field name="interval_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 模型写入记录。注意,datademo目录有本质区别:data里的数据在模块安装时无论如何都会加载,而demo里的数据只在开启演示数据模式时才加载。生产环境千万别把测试数据放在data里,这点我吃过不少亏。

2.4 controllers、static、report 等辅助目录

除了核心的 models 和 views,一个完整模块还常有这些辅助目录:

  • controllers/:存放 HTTP 控制器文件,用来处理网站路由、HTTP 请求。这类文件通常在使用 Odoo 网站功能或需要暴露自定义接口时用到。模块如果没声明依赖website,那这里的控制器基本只对内部调用有效。
  • static/:静态资源目录,包含src/js(JavaScript 文件)、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 models

models/__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', string='Asset', required=True) plan_name = fields.Char(string='Plan Name', required=True) maintenance_date = fields.Date(string='Next Maintenance Date', required=True) interval_days = fields.Integer(string='Interval (Days)', default=90) last_execution_date = fields.Date(string='Last Execution Date') note = fields.Text(string='Notes') @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(days=plan.interval_days), })

这里体现了一个基本但完整的模型结构:几个常见字段类型、一个搜索排序约束、一个可以交由定时任务调用的方法。

视图文件views/asset_maintenance_views.xml里,我同时定义一个列表视图和一个表单视图:

<?xml version="1.0" encoding="utf-8"?> <odoo> <record id="view_maintenance_plan_list" model="ir.ui.view"> <field name="name">asset.maintenance.plan.list</field> <field name="model">asset.maintenance.plan</field> <field name="arch" type="xml"> <list string="Maintenance Plans" multi_edit="1"> <field name="plan_name"/> <field name="asset_id"/> <field name="maintenance_date"/> <field name="interval_days"/> <field name="last_execution_date"/> </list> </field> </record> <record id="view_maintenance_plan_form" model="ir.ui.view"> <field name="name">asset.maintenance.plan.form</field> <field name="model">asset.maintenance.plan</field> <field name="arch" type="xml"> <form string="Maintenance Plan"> <sheet> <group> <group> <field name="plan_name"/> <field name="asset_id"/> </group> <group> <field name="maintenance_date"/> <field name="interval_days"/> <field name="last_execution_date"/> </group> </group> <group> <field name="note"/> </group> </sheet> </form> </field> </record> </odoo>

注意我在 Odoo 19 里用了<list>标签而不是老版本的<tree>标签。官方在 17 之后逐渐推list,19 中两种写法都兼容,但新代码建议直接用list,这也是保持未来兼容性的做法。

菜单视图单独放一个文件views/menu_views.xml

<?xml version="1.0" encoding="utf-8"?> <odoo> <menuitem id="menu_asset_maintenance_root" name="Maintenance" sequence="20"/> <menuitem id="menu_asset_maintenance_plan" name="Maintenance Plans" parent="menu_asset_maintenance_root" action="action_maintenance_plan"/> <record id="action_maintenance_plan" model="ir.actions.act_window"> <field name="name">Maintenance Plans</field> <field name="res_model">asset.maintenance.plan</field> <field name="view_mode">list,form</field> </record> </odoo>

3.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 里记一笔,这样后续升级才能平滑。

另外注意installableapplication两个字段。applicationTrue时,模块会显示在应用商店的应用列表里,适合业务型模块;为False的技术类模块,建议显式声明,避免用户误认为这是一个独立应用。

4.2 文件加载顺序的讲究

文件加载顺序是 Odoo 模块开发里最容易被低估的一个环节。__manifest__.pydata列表里,如果存在相互引用的记录,顺序错乱会导致External ID not foundKeyError之类的报错。

以我常用的顺序为例:

  1. security/ir.model.access.csv:先建模型权限,避免后续数据加载时操作无权限。
  2. security/*.xml:再加载记录规则、安全组。安全组定义最好放在权限 CSV 之前,因为 CSV 里的group_id:id引用了安全组的 external id。
  3. data/*.xml:加载基础数据、配置项、序列等。
  4. views/*.xml:加载视图、动作、菜单。菜单的action字段会引用动作的 external id,所以动作必须先定义。
  5. 有报表或模板的话,放在视图之后。

这个顺序不是绝对的,但遵循它能规避 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 视图里直接加required="1"只能保证界面校验,数据库层的约束需要在模型里定义。

场景三:继承视图不生效。_inherit = 'ir.ui.view'方式继承视图时,recordid必须和原视图的 external id 一致,而且arch里要写xpath表达式。如果继承的是官方模块视图,别忘记在depends里声明对应模块依赖。

5.3 性能与调试建议速查表

模块装多了之后,启动变慢、页面卡顿都是正常的。建议按下面几条做优化:

  • 把不用的模块及时卸载,不要保留一堆半残模块,它们即使没被打开也会参与视图注册和权限计算。
  • data里尽量用轻量记录,避免加载海量演示数据。生产数据库别安装带demo数据的模块。
  • 视图字段不要贪多,列表视图里三五个核心字段就够了,字段越多前端渲染越慢。
  • 启动时加--log-level=debug可以查看模块加载的每个文件耗时,定位启动慢的瓶颈。
  • 开发期为减少改动后的等待时间,建议只启动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.pymodels/maintenance.pymodels/partner.py。这样模块结构更清晰,多人协作时也能减少代码冲突。

除了目录层面的扩展,模块结构还允许你挂载很多高级能力。比如通过static/src/js/里的 OWL 组件扩展前端界面,通过controllers/暴露 REST API,通过report/生成自定义 PDF 报表。这些能力并不需要改变模块的基础结构,但充分利用它们能让模块从“记录管理工具”升级成“业务处理平台”。

最后说一个扩展时的原则:保持模块独立性和依赖最小化。一个模块只做一件事,完成一个完整的业务闭环。如果两个模块必须互相依赖,那通常是设计有问题,应该把公共逻辑抽取到更底层的模块里。这样每个模块都能独立安装、卸载、升级,测试和维护的成本都会大幅降低。

我自己在项目里见过太多“全家桶”式模块,一个模块塞了销售、采购、库存、财务、报表、审批流,最后谁都不敢动它。与其这样,不如踏踏实实按业务边界拆模块。Odoo 19 的模块结构设计已经给了你足够好的框架,剩下的是看你怎么规划和保持自律。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询