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', '無法解除安裝'), ('uninstalled', '未安裝'), ('installed', '已安裝'), ('to upgrade', '待升級'), ('to remove', '待移除'), ('to install', '待安裝'), ]

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:

# 步驟 9:對每個 model 呼叫 _register_hook# 這會在註冊表載入時*恰好執行一次*。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 遷移腳本(如果存在)

重新註冊所有鉤子:

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' 物件沒有 '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: 欄位「my_custom_field」中的 null 值違反非空約束

原因:

欄位定義了 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) - 將 custom_field 新增至 res.partner - 修正初始化問題

十、結論

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 銷售模組業務邏輯深度分析:流程、架構與系統協同解析