Odoo 18 Module Lifecycle
Installation, Upgrade, Uninstallation — a complete analysis of the module lifecycle
Install Upgrade Uninstall Registry
1. Module States (State Machine)
Every module in ir.module.module has one of the following states. Transitions are managed by the install/upgrade/uninstall buttons and by Registry.new().
update_list()
installable=True
button_install / cancel
Registry.new()
button_upgrade / cancel
after upgrade
to upgrade
button_uninstall
module_uninstall()
to install, to upgrade and to remove are temporary — they tell Registry.new() what to do. On crash, reset_modules_state() returns everything to a stable state.2. Installation — button_install()
What the "Install" button does
- Marking — recursively sets
state='to install'on the module AND all its dependencies (fromdependsin manifest) - check_external_dependencies() — checks Python libraries and bin dependencies for EVERY module
- Auto-install — looks for modules with
auto_install=Truewhose required deps are already installed/to install. Loops until exhausted. - Exclusion check — if two incompatible modules are in install states → UserError
- Category exclusion — if a category is
exclusive=True, only one module from it can be installed - Returns ACTION_DICT → opens the
base.module.upgradewizard with a "Confirm" button
- Marks + immediately reloads registry (
Registry.new(update_module=True)) - Locks
ir_cron(FOR UPDATE NOWAIT) — if cron is running → UserError - After installation: client reload or next config wizard
Resolving depends
_state_update() recursively (max depth=100): for each dependency if state == 'unknown' → UserError "module not available". The demo flag is inherited from dependencies.
Checking external_dependencies
'external_dependencies': {
'python': ['cryptography>=3.0', 'lxml'], # PEP 508
'bin': ['wkhtmltopdf'], # binaries
}
Python dependencies:
- Parses with
packaging.Requirement(PEP 508) - Checks environment markers (e.g.
; sys_platform == 'win32') importlib.metadata.version()— whether the package is installed- Fallback:
importlib.import_module()for non-standard names - Version specifier check (>=, ==, etc.)
Bin dependencies:
tools.find_in_path(binary) — searches in the system PATH
3. Upgrade — button_upgrade()
Steps of button_upgrade()
1. update_list()
Refreshes manifest from disk
2. Reverse deps
Adds dependent modules
3. External deps
Checks dependencies
4. Marking
state='to upgrade'
base → ALL installed modules get upgraded! If there are NEW dependencies (uninstalled in manifest) → installs them via button_install().Migration scripts
migrations/
├── 1.0/
│ ├── pre-update_table.py
│ ├── post-create_records.py
│ └── end-cleanup.py
├── 18.0.2.0/
│ └── pre-rename_column.py
└── 0.0.0/
└── end-invariants.py
Directory: or
Signature: def migrate(cr, version):
Version 0.0.0 = on EVERY version change.
| Stage | When it runs | Context |
|---|---|---|
| pre | Before loading new Python code | Old DB schema |
| post | After init_models + load_data | New DB schema, new code |
| end | After loading ALL modules | Entire system ready |
def migrate(cr, version):4. Uninstallation — button_uninstall()
What the "Uninstall" button does
- Checks:
- Not a server-wide module (web, base) → UserError
- Module is
installedorto upgrade
- downstream_dependencies() — SQL recursion for all modules that depend on us
- Marking — the module + ALL dependents as
'to remove' - Shows the
base.module.upgradewizard
Opens the base.module.uninstall wizard, which shows:
- A. Affected modules — kanban with icons and names of all modules to be uninstalled
- B. "Documents to Delete" — list of models and record counts that will be LOST
- C. UI warnings:
- Warning box: "Uninstalling modules can be risky..."
- "Discard" button is PRIMARY, "Uninstall" is SECONDARY
The actual uninstallation — module_uninstall()
def module_uninstall(self):
modules_to_remove = self.mapped('name')
self.env['ir.model.data']._module_data_uninstall(modules_to_remove)
self.write({'state': 'uninstalled', 'latest_version': False})
_module_data_uninstall() — deletion order
| Order | What is deleted | Details |
|---|---|---|
| 1 | Regular records | views, actions, menus, server actions, data records, cron jobs |
| 2 | _remove_copied_views() | Deletes view copies by key pattern |
| 3 | ir.model.constraint | SQL and Python constraints |
| 4 | ir.model.fields.selection | Before fields (due to ondelete='cascade') |
| 5 | ir.model.fields | Deletes COLUMNS from tables |
| 6 | ir.model.relation | DROP TABLE CASCADE for M2M relation tables |
| 7 | ir.model | Deletes ENTIRE TABLES |
_module_data_uninstall(), in reverse graph order (dependents first). After uninstallation — Registry is recreated recursively.5. Data Loss Warnings
When there is a risk of data loss
| Situation | What is lost | How to warn |
|---|---|---|
| Module uninstallation | ALL records in tables defined ONLY by this module | Show "Documents to Delete" from the wizard |
| Uninstallation + downstream deps | Cascading uninstallation of dependent modules | Show downstream_dependencies() |
| Upgrade with removed XML ID | Record is removed by _process_end() |
Check diff of data files |
| Upgrade with removed field | Column is DROPped | Check model changes |
| Upgrade of base | ALL modules get upgraded | Inform the user |
What is NOT lost during uninstallation
Shared records
Records owned by ANOTHER installed module (shared ir.model.data)
User data
Records without an external ID (user data in shared models)
Models from another module
Models defined by another module — fields from the extension are removed, but the model remains
- Before uninstallation — always back up the database
downstream_dependencies()shows ALL cascading affected modulesbase.module.uninstallwizard shows record counts — if you see large numbers → STOP- Server-wide modules (web, base) cannot be uninstalled (protected)
auto_installmodules can add surprising dependencies
6. Discovery and Manifest
How Odoo discovers modules
initialize_sys_path()
Populates odoo.addons.__path__ from:
addons_data_dir--addons-path- Built-in
odoo/addons/
get_modules()
Scans all addons paths, checks for __manifest__.py
Priority
The first found path wins
Manifest keys for dependencies
{
'depends': ['base', 'account'], # required module dependencies
'external_dependencies': {
'python': ['cryptography>=3.0'], # PEP 508 Python packages
'bin': ['wkhtmltopdf'], # system binaries
},
'auto_install': True, # or ['sale', 'purchase'] — list of trigger deps
'installable': True, # False = cannot be installed
'application': True, # shown as an application
}
auto_install logic
| Value | Behavior |
|---|---|
auto_install=True |
Installs when ALL depends are installed |
auto_install=['sale'] |
Installs when sale is installed (other deps must be available, but don't trigger) |
auto_install_required field in ir.module.module.dependency marks trigger deps. Also checks countries — if the module is country-specific, at least one company must be in that country. |
|
7. Dependency Graph
Algorithm (graph.py)
- add_modules() — modified Kahn's algorithm with queue
- For each module: if all deps are in the graph → add; otherwise → defer
- add_node() — the "parent" is the dependency with max depth →
depth = father.depth + 1 - Iteration: BFS by levels (depth 0, 1, 2...), within a level — alphabetical order
- Circular dependencies: don't throw an error — they are simply skipped with a log warning
When init, update or demo is set on a Node → it is recursively propagated down to children.
Flags are recursively propagated down to dependent modules
8. Loading Sequence (load_modules)
| 1 | Initialization → initialize_sys_path() |
| 2 | Loading base (always first) → pre-migrate → load python → load models → init_models → load_data → post-migrate |
| 3 | Marking (-i → button_install, -u → button_upgrade) |
| 4 | Loading installed / to upgrade / to remove |
| 5 | Loading to install |
| 6 | End-migration scripts (all modules) |
| 7 | Finalize constraints |
| 8 | Cleanup orphan data (_process_end) |
| 9 | Uninstallation (to remove) → uninstall_hook → module_uninstall → recursive Registry.new() |
| 10 | Validation of custom views |
| 11 | _register_hook() on every model |
init vs update vs install
| Characteristic | init (-i) |
update (-u) |
install (button) |
|---|---|---|---|
| mode | 'init' |
'update' |
'init' |
| pre-migrate | NO | YES | NO |
| post-migrate | NO | YES | NO |
| pre_init_hook | NO | NO | YES |
| post_init_hook | NO | NO | YES |
| load_data | YES | YES | YES |
| view validation | NO | YES | NO |
9. Safe Uninstallation — Checklist
Before suggesting a module uninstallation, go through all steps:
downstream_dependencies() — show ALL cascading affected modules
uninstall_hook — it may perform custom cleanup
Key Files in Odoo 18
odoo/addons/base/models/ir_module.py | ir.module.module — buttons, states, deps |
odoo/addons/base/models/ir_model.py | _module_data_uninstall() — deletion |
odoo/addons/base/wizard/base_module_uninstall.py | Warning wizard |
odoo/modules/module.py | Discovery, manifest, external deps |
odoo/modules/loading.py | load_modules() — main orchestration |
odoo/modules/graph.py | Dependency graph, topological sort |
odoo/modules/migration.py | pre/post/end migration scripts |
odoo/modules/db.py | ir_module_module table, initialize |
Generated from analysis of odoo/addons/base/models/ir_module.py, odoo/modules/loading.py, odoo/modules/graph.py, odoo/modules/migration.py — Odoo 18 Module Lifecycle