Skip to Content

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().

State Diagram
uninstallable

update_list()
installable=True
uninstalled

uninstalled
to install
button_install / cancel
installed
Registry.new()

installed
to upgrade
button_upgrade / cancel
installed
after upgrade

installed
to upgrade
to remove
button_uninstall
uninstalled
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()
  1. Marking — recursively sets state='to install' on the module AND all its dependencies (from depends in manifest)
  2. check_external_dependencies() — checks Python libraries and bin dependencies for EVERY module
  3. Auto-install — looks for modules with auto_install=True whose required deps are already installed/to install. Loops until exhausted.
  4. Exclusion check — if two incompatible modules are in install states → UserError
  5. Category exclusion — if a category is exclusive=True, only one module from it can be installed
  6. 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:
  1. Parses with packaging.Requirement (PEP 508)
  2. Checks environment markers (e.g. ; sys_platform == 'win32')
  3. importlib.metadata.version() — whether the package is installed
  4. Fallback: importlib.import_module() for non-standard names
  5. 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 baseALL 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: /migrations// or /upgrades//

Signature: def migrate(cr, version):

Version 0.0.0 = on EVERY version change.

Migration stages
StageWhen it runsContext
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
  1. Checks:
    • Not a server-wide module (web, base) → UserError
    • Module is installed or to upgrade
  2. downstream_dependencies() — SQL recursion for all modules that depend on us
  3. Marking — the module + ALL dependents as 'to remove'
  4. 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

OrderWhat is deletedDetails
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

SituationWhat is lostHow 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:
  1. Before uninstallation — always back up the database
  2. downstream_dependencies() shows ALL cascading affected modules
  3. base.module.uninstall wizard shows record counts — if you see large numbers → STOP
  4. Server-wide modules (web, base) cannot be uninstalled (protected)
  5. 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

ValueBehavior
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
  1. add_modules() — modified Kahn's algorithm with queue
  2. For each module: if all deps are in the graph → add; otherwise → defer
  3. add_node() — the "parent" is the dependency with max depth → depth = father.depth + 1
  4. Iteration: BFS by levels (depth 0, 1, 2...), within a level — alphabetical order
  5. 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.

init
update
demo

Flags are recursively propagated down to dependent modules

8. Loading Sequence (load_modules)

Loading sequence
1 Initializationinitialize_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 checklist
1. Run downstream_dependencies() — show ALL cascading affected modules
2. Check "Documents to Delete" — record count by model
3. Warn about data loss in specific tables
4. Recommend a database backup
5. Recommend testing on a staging/duplicate database
6. Check whether the module is server-wide
7. Check for uninstall_hook — it may perform custom cleanup
8. After uninstallation — check for orphan records
Never 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.pyir.module.module — buttons, states, deps
odoo/addons/base/models/ir_model.py_module_data_uninstall() — deletion
odoo/addons/base/wizard/base_module_uninstall.pyWarning wizard
Infrastructure
odoo/modules/module.pyDiscovery, manifest, external deps
odoo/modules/loading.pyload_modules() — main orchestration
odoo/modules/graph.pyDependency graph, topological sort
odoo/modules/migration.pypre/post/end migration scripts
odoo/modules/db.pyir_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