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', '無法解除安裝'), ('uninstalled', '未安裝'), ('installed', '已安裝'), ('to upgrade', '待升級'), ('to remove', '待移除'), ('to install', '待安裝'), ]
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 未被調用:
# 步驟 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 版本號比較邏輯
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 遷移腳本(如果存在)
重新註冊所有鉤子:
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 命令?
