Table of Contents
- 1. State Machine
- 2. Installation
- 3. Upgrade
- 4. Uninstallation
- 5. Data Loss
- 6. Discovery and Manifest
- 7. Dependency Graph
- 8. Loading Sequence
- 9. Safe Uninstallation
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().
State Diagramuninstallable
update_list()
installable=Trueuninstalleduninstalledto install
button_install / cancelinstalled
Registry.new()installedto upgrade
button_upgrade / cancelinstalled
after upgradeinstalled
to upgradeto remove
button_uninstalluninstalled
module_uninstall()Pending states: 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
Steps of button_install()
- Marking — recursively sets state='to install' on the module AND all its dependencies (from depends in manifest)
- check_external_dependencies() — checks Python libraries and bin dependencies for EVERY module
- Auto-install — looks for modules with auto_install=True whose 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.upgrade wizard with a "Confirm" button
button_immediate_install() — difference
- 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
Manifest declaration
'external_dependencies': {
'python': ['cryptography>=3.0', 'lxml'], # PEP 508
'bin': ['wkhtmltopdf'], # binaries
}
How they are checked
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'Warning: If you upgrade base → ALL installed modules get upgraded! If there are NEW dependencies (uninstalled in manifest) → installs them via button_install().
Migration scripts
Directory structure
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: <module>/migrations/<version>/ or <module>/upgrades/<version>/
Signature: def migrate(cr, version):
Version 0.0.0 = on EVERY version change.
Migration stages
| 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 |
Migration scripts run ONLY during upgrade, NOT during new installation. Signature: def migrate(cr, version):
4. Uninstallation — button_uninstall()
What the "Uninstall" button does
button_uninstall() steps
Checks:
- Not a server-wide module (web, base) → UserError
- Module is installed or to upgrade
- downstream_dependencies() — SQL recursion for all modules that depend on us
- Marking — the module + ALL dependents as 'to remove'
- Shows the base.module.upgrade wizard
button_uninstall_wizard() — safer
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 |
Savepoint: Every deletion is within a savepoint. On error — binary split (divides in half and retries).uninstall_hook: Called BEFORE _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
Practical tips:
- Before uninstallation — always back up the database
- downstream_dependencies() shows ALL cascading affected modules
- base.module.uninstall wizard shows record counts — if you see large numbers → STOP
- Server-wide modules (web, base) cannot be uninstalled (protected)
- auto_install modules can add surprising dependencies
6. Discovery and Manifest
How Odoo discovers modules
Step 1
initialize_sys_path()
Populates odoo.addons.__path__ from:
- addons_data_dir
- --addons-path
- Built-in odoo/addons/
Step 2
get_modules()
Scans all addons paths, checks for __manifest__.py
Step 3
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)
Modified Kahn's Algorithm
- 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
Flag cascading
When init, update or demo is set on a Node → it is recursively propagated down to children.
initupdatedemo
Flags are recursively propagated down to dependent modules
8. Loading Sequence (load_modules)
Loading sequence
| 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:
Pre-uninstallation checklist1. Run downstream_dependencies() — show ALL cascading affected modules2. Check "Documents to Delete" — record count by model3. Warn about data loss in specific tables4. Recommend a database backup5. Recommend testing on a staging/duplicate database6. Check whether the module is server-wide7. Check for uninstall_hook — it may perform custom cleanup8. After uninstallation — check for orphan recordsNever uninstall a module in production without a backup! Even Odoo's wizard deliberately makes the "Discard" button green and "Uninstall" gray — to encourage cancellation.
Key Files in Odoo 18
Models and logic
| 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 |
Infrastructure
| 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