Odoo 模块依赖与钩子机制完整分析报告

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 关键代码位置

loading.py#L175-L189:

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 依赖关系验证

ir_module.py#L407-L416:

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 钩子的调用时机

loading.py#L218-L229:

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() 方法未被调用

registry.py#L744-L772:

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 未被调用:

loading.py#L573-L587:

# 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 版本号比较逻辑

ir_module.py#L790:

if parse_version(terp.get('version', default_version)) > parse_version(mod.latest_version or default_version):     res[0] += 1# 标记为需要更新

5.2 版本号字段的命名混淆

注意:Odoo 的字段命名非常混乱!

ir_module.py#L276-L281:

# 注意:字段名称不正确!# 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 重新注册所有钩子:

loading.py#L573-L587:

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 命令?

关于我们

​我们致力于帮助中小企业实现数字化转型,我们的团队由一群充满激情和创新思维的专业人士组成,他们具备丰富的行业经验和技术专长。

扫一扫获取顾问以及手册

归档
登录 留下评论
Odoo 19 销售模块业务逻辑深度分析:流程、架构与系统协同解析