Odoo 模块依赖与钩子机制完整分析报告
执行总结
核心问题:当编写的模块依赖联系人(contacts)或公司(res.company)模块但没有写钩子时,启动 Odoo 会导致系统崩溃且无报错信息。
根本原因:不是缺少钩子本身导致的崩溃,而是模块在安装/升级时没有正确初始化其对基础模型(如 res.partner 或 res.company)的继承和扩展,导致模型关系不完整。
版本号修改的作用:修改版本号会触发 Odoo 的模块升级流程,重新执行模型初始化和数据库结构同步,从而"修复"了之前未完成的初始化过程。
一、Odoo 模块加载机制深度解析
1.1 模块加载流程
根据 loading.py 的核心代码分析,Odoo 的模块加载分为以下关键步骤:
# odoo/modules/loading.py: load_modules 函数 STEP 1: 加载 base 模块 STEP 2: 标记其他模块为待加载/更新 STEP 3: 加载标记的模块(跳过 base) STEP 4: 结束迁移 STEP 5: 记录菜单、视图、报表到 ir_ui_menu STEP 6: 验证自定义视图 STEP 7: 卸载标记为 'to remove' 的模块 STEP 8: 验证自定义字段 STEP 9: 调用所有模型的 _register_hook
1.2 关键代码位置
if update_operation == 'install':
py_module = sys.modules['odoo.addons.%s' % (module_name,)]
pre_init = package.manifest.get('pre_init_hook')
if pre_init:
registry._setup_models__(env.cr, [])
# incremental setupgetattr(py_module, pre_init)(env)
model_names = registry.load(package)
if update_operation:
model_names = registry.descendants(model_names, '_inherit', '_inherits')
models_updated |= model_names
models_to_check -= model_names
registry._setup_models__(env.cr, [])
# incremental setup
registry.init_models(env.cr, model_names, {'module': package.name}, update_operation == 'install')
二、依赖处理机制
2.1 模块状态转换
ir_module.py 定义了以下状态:
STATES = [ ('uninstallable', 'Uninstallable'), ('uninstalled', 'Not Installed'), ('installed', 'Installed'), ('to upgrade', 'To be upgraded'), ('to remove', 'To be removed'), ('to install', 'To be installed'), ]
2.2 依赖关系验证
install_states = frozenset(('installed', 'to install', 'to upgrade')) defmust_install(module): states = {dep.state for dep in module.dependencies_id if dep.auto_install_required} return states <= install_states and'to install'in states and ( not module.country_ids or module.country_ids & company_countries )
关键发现:Odoo 会自动递归处理依赖,但仅检查模块状态,不验证模型初始化是否完成。
三、钩子函数的作用与时机
3.1 三种钩子类型
1 pre_init_hook:在模型加载前执行
2 post_init_hook:在模块数据加载后执行
3 uninstall_hook:在模块卸载时执行
3.2 钩子的调用时机
if update_operation: if update_operation == 'install': post_init = package.manifest.get('post_init_hook') if post_init: getattr(py_module, post_init)(env) elif update_operation == 'upgrade': # validate the views that have not been checked yet env['ir.ui.view']._validate_module_views(module_name)
3.3 钩子的典型用途
分析多个官方模块后发现常见用途:
# 示例:payment_stripe/__init__.pydefpost_init_hook(env): payment.setup_provider(env, 'stripe') # 示例:timesheet_grid/__init__.pydefpre_init_hook(env): # 在模型加载前修改数据库结构 env.cr.execute("ALTER TABLE...")
四、为什么缺少钩子会导致崩溃
4.1 问题根源分析
核心问题不在于缺少钩子本身,而在于:
1 模型继承未完成初始化:
§ 当你的模块继承 res.partner 或 res.company
§ 但没有触发 update_operation='install' 流程
§ 模型的 init_models() 方法未被调用
definit_models(self, cr: Cursor, model_names: Iterable[str], context: dict[str, typing.Any], install: bool = True): for model in models: model._auto_init() # 创建/更新数据库表 model.init() # 执行模型特定初始化 env['ir.model']._reflect_models(model_names) # ← 关键:反射模型元数据 env['ir.model.fields']._reflect_fields(model_names) # ← 关键:反射字段元数据 env['ir.model.fields.selection']._reflect_selections(model_names) env['ir.model.constraint']._reflect_constraints(model_names) env['ir.model.inherit']._reflect_inherits(model_names)
1 _register_hook 未被调用:
# STEP 9: call _register_hook on every model# This is done *exactly once* when the registry is being loaded.for model in env.values(): model._register_hook()
_register_hook 的重要性:许多模块(如 base_automation)依赖此钩子来注册动态行为:
# base_automation/models/base_automation.pydef_register_hook(self): """Patch models that should trigger action rules"""for automation_rule inself.search([]): Model = self.env.get(automation_rule.model_name) # 动态修改模型方法 patch(Model, "create", make_create())
4.2 崩溃场景复现
场景:你创建了一个模块 my_module,继承 res.partner:
# my_module/models/res_partner.pyfrom odoo import models, fields classResPartner(models.Model): _inherit = 'res.partner' custom_field = fields.Char("Custom Field")
问题:
1 如果模块通过文件复制安装(而非 button_install)
2 或数据库中模块状态为 installed 但实际未执行安装流程
3 custom_field 不会在 ir.model.fields 中注册
4 访问该字段时导致 AttributeError 或数据库约束错误
4.3 为什么"没有报错"
loading.py#L157-L170 的异常处理:
try: load_openerp_module(package.name) # ... 模型加载逻辑except Exception: _logger.error('Failed to load registry') del cls.registries[db_name] raise
问题:
§ 如果错误发生在 registry.ready = True 之后
§ 异常可能被 Odoo 的事务回滚机制吞掉
§ 或者错误发生在首次访问模型时(延迟加载)
五、版本号与模块更新机制
5.1 版本号比较逻辑
if parse_version(terp.get('version', default_version)) > parse_version(mod.latest_version or default_version): res[0] += 1# 标记为需要更新
5.2 版本号字段的命名混淆
注意:Odoo 的字段命名非常混乱!
# 注意:字段名称不正确!# installed_version 指的是最新版本(磁盘上的)# latest_version 指的是已安装版本(数据库中的)# published_version 指的是仓库中可用的版本 installed_version = fields.Char('Latest Version', compute='_get_latest_version') latest_version = fields.Char('Installed Version', readonly=True) published_version = fields.Char('Published Version', readonly=True)
5.3 版本号修改触发的流程
当你在 __manifest__.py 中修改版本号:
'version': '1.0.0.1', # 从 1.0.0.0 修改为 1.0.0.1
执行 odoo-bin -u my_module 时:
1 update_list() 检测到版本变化
2 模块状态从 installed → to upgrade
3 触发完整的升级流程:
§ 调用 pre 迁移脚本(如果存在)
§ 重新加载模块(load_openerp_module)
§ 调用 registry._setup_models__()
§ 调用 registry.init_models()
§ 调用 post 迁移脚本(如果存在)
4 重新注册所有钩子:
for model in env.values(): model._register_hook()
六、根本原因与解决方案
6.1 根本原因总结
问题不是"没有钩子",而是:
1 模块未通过正常安装流程
§ 直接复制模块文件到 addons 目录
§ 手动修改数据库的 ir_module_module 表
§ 使用了不完整的安装脚本
2 模型初始化未完成
§ init_models() 未被调用
§ 字段未在 ir.model.fields 中注册
§ 约束未在 ir.model.constraint 中注册
3 _register_hook 未执行
§ 动态行为未注册
§ 依赖模块的钩子未被触发
6.2 为什么修改版本号"能解决"
修改版本号并执行 -u module_name 会:
1 强制执行完整升级流程
2 重新初始化所有模型
3 重新注册所有钩子
4 同步数据库结构
这是一种"暴力修复",实际上是重新走了一遍正确的安装流程。
七、正确的开发实践
7.1 模块安装的正确方式
永远不要:
§ ❌ 直接复制模块到 addons 并重启
§ ❌ 手动修改 ir_module_module 表
§ ❌ 使用 --load 参数加载未安装的模块
应该使用:
# 方法 1:通过 UI 安装# 应用 → 更新应用列表 → 找到模块 → 点击安装# 方法 2:通过命令行安装 odoo-bin -i my_module -d mydb # 方法 3:通过命令行升级 odoo-bin -u my_module -d mydb
7.2 何时需要写钩子
必须写 post_init_hook 的情况:
1 需要创建初始数据:
defpost_init_hook(env): # 创建默认配置记录 env['my.config'].create({ 'name': 'Default', 'value': 100, })
1 需要修改已有数据:
defpost_init_hook(env): # 更新所有合作伙伴的新字段 partners = env['res.partner'].search([]) partners.write({'new_field': 'default_value'})
1 需要设置系统参数:
defpost_init_hook(env): env['ir.config_parameter'].set_param('my_module.enabled', 'True')
必须写 pre_init_hook 的情况:
1 需要在模型加载前修改数据库结构:
defpre_init_hook(env): # 重命名列(避免字段迁移问题)if env.cr.column_exists('res_partner', 'old_field'): env.cr.execute("ALTER TABLE res_partner RENAME COLUMN old_field TO new_field")
1 需要在模型加载前清理数据:
defpre_init_hook(env): # 删除可能导致约束冲突的数据 env.cr.execute("DELETE FROM my_table WHERE invalid_condition")
7.3 依赖 res.partner 或 res.company 的正确做法
示例:正确的模块结构
# __manifest__.py { 'name': 'My Module', 'version': '1.0.0', 'depends': ['base', 'contacts'], # ← 明确声明依赖'data': [ 'security/ir.model.access.csv', 'views/res_partner_views.xml', ], 'post_init_hook': 'post_init_hook', # ← 可选,仅在需要时添加 } # __init__.pyfrom . import models defpost_init_hook(env): """仅在确实需要初始化数据时才写"""# 例如:为所有现有合作伙伴设置默认值 partners = env['res.partner'].search([('my_custom_field', '=', False)]) partners.write({'my_custom_field': 'default'}) # models/res_partner.pyfrom odoo import models, fields classResPartner(models.Model): _inherit = 'res.partner' my_custom_field = fields.Char( string="My Field", help="This field extends res.partner", default='default', # ← 为新记录设置默认值 ) def_register_hook(self): """仅在需要动态修改行为时才覆盖"""super()._register_hook() # 你的自定义钩子逻辑
八、常见错误与诊断
8.1 错误场景 1:字段不存在
症状:
AttributeError: 'res.partner' object has no attribute 'my_custom_field'
原因:
§ 模块文件存在,但未通过 button_install 或 -i 安装
§ ir.model.fields 中未注册该字段
诊断:
-- 检查模块状态SELECT name, state, latest_version FROM ir_module_module WHERE name ='my_module'; -- 检查字段是否注册SELECT name, model, state FROM ir_model_fields WHERE model ='res.partner'AND name ='my_custom_field';
修复:
odoo-bin -u my_module -d mydb
8.2 错误场景 2:数据库约束错误
症状:
psycopg2.errors.NotNullViolation: null value in column "my_custom_field" violates not-null constraint
原因:
§ 字段定义了 required=True
§ 但未在 post_init_hook 中为已有记录设置值
修复:
defpost_init_hook(env): # 方法1:批量设置默认值 env.cr.execute(""" UPDATE res_partner SET my_custom_field = 'default' WHERE my_custom_field IS NULL """) # 方法2:使用 ORM(更安全但更慢) partners = env['res.partner'].search([('my_custom_field', '=', False)]) partners.write({'my_custom_field': 'default'})
8.3 错误场景 3:_register_hook 未执行
症状:
§ 依赖动态行为的功能不工作
§ 例如:base_automation 的规则不触发
原因:
§ 模块未通过完整安装流程
§ registry.ready 为 False 时尝试注册钩子
诊断:
# 在模型中添加日志def_register_hook(self): _logger.info("=== _register_hook called for %s ===", self._name) super()._register_hook()
修复:
§ 确保使用 -i 或 -u 安装/升级模块
§ 检查 odoo.conf 中是否禁用了某些模块加载
九、最佳实践总结
9.1 开发阶段
1 始终使用 -u 更新模块:
odoo-bin -u my_module -d dev_db --dev=all
1 每次修改模型后增加版本号:
# 从'version': '1.0.0.1', # 改为'version': '1.0.0.2',
1 使用 --dev=all 开启自动重载:
odoo-bin --dev=all
9.2 生产部署
1 使用迁移脚本而非钩子:
# migrations/1.0.0.2/pre-migrate.pydefmigrate(cr, version): # 在模型加载前执行pass# migrations/1.0.0.2/post-migrate.pydefmigrate(cr, version): # 在模型加载后执行pass
1 在 CI/CD 中强制使用 -u:
# deploy.sh odoo-bin -u my_module --stop-after-init -d prod_db
1 记录版本变更日志:
# __manifest__.py'version': '1.0.0.2', # CHANGELOG.md## 1.0.0.2 (2025-01-01) - Added custom_field to res.partner - Fixed initialization issue
十、结论
10.1 关键要点
1 钩子不是必需的:大多数情况下不需要钩子,只要正确安装模块即可。
2 版本号修改不是"避开错误":它是触发完整升级流程的手段,本质是重新执行了正确的初始化。
3 根本问题是安装流程不完整:
§ 必须通过 -i、-u 或 UI 安装
§ 不能跳过 init_models() 和 _register_hook()
4 "无报错崩溃"通常是延迟错误:
§ 错误可能在首次访问模型时才发生
§ 使用 --log-level=debug 查看详细日志
10.2 推荐做法
对于简单的模块(只添加字段):
# __manifest__.py { 'name': 'Simple Module', 'version': '1.0.0', 'depends': ['base', 'contacts'], 'data': ['views/views.xml'], # 不需要任何钩子! }
对于复杂的模块(需要初始化数据):
# __manifest__.py { 'name': 'Complex Module', 'version': '1.0.0', 'depends': ['base', 'contacts'], 'data': [ 'security/ir.model.access.csv', 'data/initial_data.xml', # ← 优先使用 XML 数据文件'views/views.xml', ], 'post_init_hook': 'post_init_hook', # ← 仅在 XML 无法完成时使用 } defpost_init_hook(env): """仅用于必须用代码完成的初始化""" _logger.info("Initializing %s", __name__) # 你的初始化代码
10.3 故障排查流程
遇到"无报错崩溃"时的检查清单:
# 1. 检查模块状态 SELECT name, state, latest_version FROM ir_module_module WHERE name = 'my_module'; # 2. 检查模型是否注册 SELECT model FROM ir_model WHERE model = 'res.partner'; # 3. 检查字段是否注册 SELECT name, model FROM ir_model_fields WHERE model = 'res.partner' AND module = 'my_module'; # 4. 检查约束是否注册 SELECT name, model FROM ir_model_constraint WHERE model = 'res.partner' AND module = 'my_module'; # 5. 使用调试模式重启 odoo-bin -d mydb --log-level=debug --dev=all # 6. 强制重新安装 odoo-bin -d mydb -u my_module --stop-after-init # 7. 查看 PostgreSQL 日志tail -f /var/log/postgresql/postgresql-*.log
附录:参考代码位置
A. 模块加载
§ odoo/modules/loading.py#L342-L625 - load_modules() 函数
§ odoo/modules/loading.py#L157-L250 - load_module_graph() 函数
§ odoo/modules/module.py#L492-L522 - load_openerp_module() 函数
B. 注册表管理
§ odoo/orm/registry.py#L140-L235 - Registry.new() 方法
§ odoo/orm/registry.py#L367-L398 - Registry.load() 方法
§ odoo/orm/registry.py#L399-L540 - Registry._setup_models__() 方法
§ odoo/orm/registry.py#L730-L778 - Registry.init_models() 方法
C. 模块管理
§ odoo/addons/base/models/ir_module.py#L400-L468 - button_install() 方法
§ odoo/addons/base/models/ir_module.py#L692-L740 - button_upgrade() 方法
§ odoo/addons/base/models/ir_module.py#L760-L830 - update_list() 方法
D. 迁移管理
§ odoo/modules/migration.py#L59-L220 - MigrationManager 类
§ odoo/modules/migration.py#L229-L268 - exec_script() 函数
报告日期:2025 年 12 月 30 日
Odoo 版本:19.0 Enterprise Edition
分析对象:odoo19ee
最终答案
问:修改版本号是否能避开错误?
答:不是"避开",而是"修复"。
修改版本号触发的是完整的模块升级流程,它会:
1 重新加载模块代码
2 重新初始化所有模型(init_models)
3 重新注册所有钩子(_register_hook)
4 同步数据库结构和元数据
本质上,这是在补偿之前未完成的安装步骤。
正确的做法是:从一开始就使用 -i 或 -u 参数正确安装模块,而不是依赖版本号修改来"修复"问题。
如果你必须依赖版本号修改:说明你的部署流程有问题,应该检查:
§ 是否直接复制文件而非使用 -i 安装?
§ 是否手动修改了数据库的模块状态?
§ CI/CD 脚本是否正确执行了 -u 命令?
