Complete Analysis Report on Odoo Module Dependencies and Hook Mechanisms
Executive Summary
Core issue: When a custom module depends on the contacts or company (res.company) module but no hook is written, starting Odoo causes the system to crash without any error message.
Root cause: The crash is not caused by the lack of the hook itself; rather, during installation/upgrade, the module did not correctly initialize its inheritance and extension of base models (such as res.partner or res.company), resulting in incomplete model relationships.
The effect of modifying the version number: Modifying the version number triggers Odoo's module upgrade process, re-executing model initialization and database structure synchronization, thereby "fixing" the previously incomplete initialization process.
1. In-Depth Analysis of the Odoo Module Loading Mechanism
1.1 Module Loading Process
According to loading.py core code analysis, Odoo's module loading is divided into the following key steps:
# odoo/modules/loading.py: load_modules function STEP 1: Load the base module STEP 2: Mark other modules as pending load/update STEP 3: Load marked modules (skip base) STEP 4: End migration STEP 5: Record menus, views, reports to ir_ui_menu STEP 6: Validate custom views STEP 7: Uninstall modules marked as 'to remove' STEP 8: Validate custom fields STEP 9: Call _register_hook of all models
1.2 Key Code Locations
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')
II. Dependency Handling Mechanism
2.1 Module State Transitions
ir_module.py The following states are defined:
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 Dependency Verification
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 )
Key finding: Odoo automatically handles dependencies recursively, but only checks module status, not whether model initialization has completed.
III. The Role and Timing of Hook Functions
3.1 Three Types of Hooks
1 pre_init_hook: executed before the model is loaded
2 post_init_hook: Executed after the module data is loaded
3 uninstall_hook: Executed when the module is uninstalled
3.2 Timing of Hook Invocation
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 Typical Uses of Hooks
After analyzing multiple official modules, common uses were found:
# Example:payment_stripe/__init__.pydefpost_init_hook(env): payment.setup_provider(env, 'stripe') # Example:timesheet_grid/__init__.pydefpre_init_hook(env): # Modify database structure before model loading env.cr.execute("ALTER TABLE...")
IV. Why Missing Hooks Cause Crashes
4.1 Root Cause Analysis
The core issue is not the lack of hooks themselves, but rather:
1 Model inheritance did not complete initialization:
When your module inherits res.partner or res.company
but did not trigger the update_operation='install' process
The model's init_models() method was not called
definit_models(self, cr: Cursor, model_names: Iterable[str], context: dict[str, typing.Any], install: bool = True): for model in models: model._auto_init() # Create/update database tables model.init() # Execute model-specific initialization env['ir.model']._reflect_models(model_names) # ← Key: reflect model metadata env['ir.model.fields']._reflect_fields(model_names) # ← Key: reflect field metadata 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 was not called:
# 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()
The importance of _register_hook: Many modules (such as base_automation) rely on this hook to register dynamic behavior:
# 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) # Dynamically modify model methods patch(Model, "create", make_create())
4.2 Reproducing the Crash Scenario
Scenario: You created a module my_module that inherits res.partner:
# my_module/models/res_partner.pyfrom odoo import models, fields classResPartner(models.Model): _inherit = 'res.partner' custom_field = fields.Char("Custom Field")
Question:
1 If the module is installed via file copy (rather than button_install)
2 or the module status in the database is installed but the installation process was not actually executed
3 custom_field will not be registered in ir.model.fields
4 Accessing this field causes an AttributeError or database constraint error
4.3 Why "No Error Was Reported"
loading.py#L157-L170 Exception handling:
try: load_openerp_module(package.name) # ... model loading logicexcept Exception: _logger.error('Failed to load registry') del cls.registries[db_name] raise
Question:
§ If the error occurs after registry.ready = True
§ Exceptions may be swallowed by Odoo's transaction rollback mechanism
§ or the error occurs when the model is first accessed (lazy loading)
V. Version Numbers and Module Update Mechanism
5.1 Version Number Comparison Logic
if parse_version(terp.get('version', default_version)) > parse_version(mod.latest_version or default_version): res[0] += 1# Mark as needing update
5.2 Naming Confusion of Version Number Fields
Note: Odoo's field naming is very confusing!
# Note: The field names are incorrect!# installed_version refers to the latest version (on disk)# latest_version refers to the installed version (in the database)# published_version refers to the version available in the repository 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 Process Triggered by Version Number Modification
When you modify the version number in __manifest__.py:
'version': '1.0.0.1', # Changed from 1.0.0.0 to 1.0.0.1
When running odoo-bin -u my_module:
1 update_list() detected a version change
2 Module status from installed → to upgrade
3 Trigger the complete upgrade process:
§ Call the pre-migration script (if it exists)
§ Reload module (load_openerp_module)
§ Call registry._setup_models__()
§ Call registry.init_models()
§ Call the post migration script (if it exists)
4 Re-register all hooks:
for model in env.values(): model._register_hook()
VI. Root Causes and Solutions
6.1 Root Cause Summary
The problem is not "there are no hooks," but rather:
1 The module did not go through the normal installation process
Directly copy the module files to the addons directory
Manually modify the ir_module_module table in the database
Used an incomplete installation script
2 Model initialization not completed
init_models() was not called
Field is not registered in ir.model.fields
Constraint not registered in ir.model.constraint
3 _register_hook was not executed
Dynamic behavior not registered
The dependent module's hook was not triggered
6.2 Why Modifying the Version Number "Can Solve It"
Modifying the version number and running -u module_name will:
1 Enforce the complete upgrade process
2 Reinitialize all models
3 Re-register all hooks
4 Synchronize database structure
This is a "brute-force fix", which in reality amounts to going through the correct installation process all over again.
VII. Correct Development Practices
7.1 The correct way to install the module
Never:
❌ Directly copy the module to addons and restart
❌ Manually modifying the ir_module_module table
❌ Using the --load parameter to load an uninstalled module
Should use:
# Method 1: Install via UI# Apps → Update Apps List → Find the module → Click Install# Method 2: Install via command line odoo-bin -i my_module -d mydb # Method 3: Upgrade via command line odoo-bin -u my_module -d mydb
7.2 When do you need to write a hook
Situations where post_init_hook must be written:
1 Initial data needs to be created:
defpost_init_hook(env): # Create default configuration record env['my.config'].create({ 'name': 'Default', 'value': 100, })
1 Need to modify existing data:
defpost_init_hook(env): # Update the new field for all partners partners = env['res.partner'].search([]) partners.write({'new_field': 'default_value'})
1 System parameters need to be set:
defpost_init_hook(env): env['ir.config_parameter'].set_param('my_module.enabled', 'True')
Situations where pre_init_hook must be written:
1 The database structure needs to be modified before the model is loaded:
defpre_init_hook(env): # Rename column (to avoid field migration issues)if env.cr.column_exists('res_partner', 'old_field'): env.cr.execute("ALTER TABLE res_partner RENAME COLUMN old_field TO new_field")
1 Data needs to be cleaned before model loading:
defpre_init_hook(env): # Delete data that may cause constraint conflicts env.cr.execute("DELETE FROM my_table WHERE invalid_condition")
7.3 Correct Approach for Depending on res.partner or res.company
Example: Correct module structure
# __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() # 你的自定义钩子逻辑
VIII. Common Errors and Diagnosis
8.1 Error Scenario 1: Field Does Not Exist
Symptoms:
AttributeError: 'res.partner' object has no attribute 'my_custom_field'
Reason:
The module file exists, but was not installed via button_install or -i
This field is not registered in ir.model.fields
Diagnosis:
-- Check module statusSELECT name, state, latest_version FROM ir_module_module WHERE name ='my_module'; -- Check whether the field is registeredSELECT name, model, state FROM ir_model_fields WHERE model ='res.partner'AND name ='my_custom_field';
Fix:
odoo-bin -u my_module -d mydb
8.2 Error Scenario 2: Database Constraint Error
Symptoms:
psycopg2.errors.NotNullViolation: null value in column "my_custom_field" violates not-null constraint
Reason:
field defines required=True
but did not set values for existing records in post_init_hook
Fix:
defpost_init_hook(env): # Method 1: bulk set default value env.cr.execute(""" UPDATE res_partner SET my_custom_field = 'default' WHERE my_custom_field IS NULL """) # Method 2: use ORM (safer but slower) partners = env['res.partner'].search([('my_custom_field', '=', False)]) partners.write({'my_custom_field': 'default'})
8.3 Error Scenario 3: _register_hook not executed
Symptoms:
Features that rely on dynamic behavior do not work
For example: rules in base_automation do not trigger
Reason:
Module did not pass the complete installation process
Attempt to register hook when registry.ready is False
Diagnosis:
# Add logging in the modeldef_register_hook(self): _logger.info("=== _register_hook called for %s ===", self._name) super()._register_hook()
Fix:
Ensure the use of -i or -u to install/upgrade modules
Check whether loading of certain modules is disabled in odoo.conf
IX. Summary of Best Practices
9.1 Development Phase
1 Always use -u to update modules:
odoo-bin -u my_module -d dev_db --dev=all
1 Increment the version number after each model modification:
# From 'version': '1.0.0.1', # change to 'version': '1.0.0.2',
1 Use --dev=all to enable automatic reload:
odoo-bin --dev=all
9.2 Production Deployment
1 Use migration scripts instead of hooks:
# migrations/1.0.0.2/pre-migrate.pydefmigrate(cr, version): # Execute before the model is loadedpass# migrations/1.0.0.2/post-migrate.pydefmigrate(cr, version): # Execute after the model is loadedpass
1 Enforce the use of -u in CI/CD:
# deploy.sh odoo-bin -u my_module --stop-after-init -d prod_db
1 Record version change log:
# __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. Conclusion
10.1 Key Takeaways
1 Hooks are not required: In most cases, hooks are not needed; simply install the module correctly.
2 Modifying the version number is not "avoiding errors": it is a means of triggering the complete upgrade process; in essence, it re-executes the correct initialization.
3 The root problem is that the installation process is incomplete:
Must be installed via -i, -u, or UI
Cannot skip init_models() and _register_hook()
4 "Crash with no error message" is usually a delayed error:
Errors may only occur when the model is first accessed
Use --log-level=debug to view detailed logs
10.2 Recommended Practices
For simple modules (only adding fields):
# __manifest__.py { 'name': 'Simple Module', 'version': '1.0.0', 'depends': ['base', 'contacts'], 'data': ['views/views.xml'], # No hooks needed! }
For complex modules (that require data initialization):
# __manifest__.py { 'name': 'Complex Module', 'version': '1.0.0', 'depends': ['base', 'contacts'], 'data': [ 'security/ir.model.access.csv', 'data/initial_data.xml', # ← Prefer using XML data files'views/views.xml', ], 'post_init_hook': 'post_init_hook', # ← Use only when XML cannot accomplish it } defpost_init_hook(env): """Only for initialization that must be completed in code""" _logger.info("Initializing %s", __name__) # Your initialization code
10.3 Troubleshooting Process
Checklist for when encountering a "crash without errors":
# 1. Check module status SELECT name, state, latest_version FROM ir_module_module WHERE name = 'my_module'; # 2. Check whether model is registered SELECT model FROM ir_model WHERE model = 'res.partner'; # 3. Check whether fields are registered SELECT name, model FROM ir_model_fields WHERE model = 'res.partner' AND module = 'my_module'; # 4. Check whether constraints are registered SELECT name, model FROM ir_model_constraint WHERE model = 'res.partner' AND module = 'my_module'; # 5. Restart in debug mode odoo-bin -d mydb --log-level=debug --dev=all # 6. Force reinstall odoo-bin -d mydb -u my_module --stop-after-init # 7. View PostgreSQL logs tail -f /var/log/postgresql/postgresql-*.log
Appendix: Reference Code Location
A. Module Loading
§ odoo/modules/loading.py#L342-L625 - load_modules() function
§ odoo/modules/loading.py#L157-L250 - load_module_graph() function
§ odoo/modules/module.py#L492-L522 - load_openerp_module() function
B. Registry Management
§ odoo/orm/registry.py#L140-L235 - Registry.new() method
§ odoo/orm/registry.py#L367-L398 - Registry.load() method
§ odoo/orm/registry.py#L399-L540 - Registry._setup_models__() method
§ odoo/orm/registry.py#L730-L778 - Registry.init_models() method
C. Module Management
§ odoo/addons/base/models/ir_module.py#L400-L468 - button_install() method
§ odoo/addons/base/models/ir_module.py#L692-L740 - button_upgrade() method
§ odoo/addons/base/models/ir_module.py#L760-L830 - update_list() method
D. Migration Management
§ odoo/modules/migration.py#L59-L220 - MigrationManager class
§ odoo/modules/migration.py#L229-L268 - exec_script() function
Report Date: December 30, 2025
Odoo Version: 19.0 Enterprise Edition
Analysis object: odoo19ee
Final Answer
Q: Can modifying the version number avoid the error?
Answer: It is not "avoiding", but "fixing".
Modifying the version number triggers the complete module upgrade process, which will:
1 Reload module code
2 Reinitialize all models (init_models)
3 Re-register all hooks (_register_hook)
4 Synchronize database structure and metadata
Essentially, this is compensating for installation steps that were not completed previously.
The correct approach is: use the -i or -u flags from the start to properly install the module, rather than relying on version number changes to "fix" the problem.
If you must rely on version number changes: it means your deployment process has problems, and you should check:
Should the file be copied directly rather than installed using -i?
Has the module status in the database been manually modified?
Did the CI/CD script correctly execute the -u command?
