Skip to Content

Odoo 18 Module Lifecycle

Installation, Upgrade, Uninstallation — a complete analysis of the module lifecycle Install > Upgrade > Uninstall > Registry
April 18, 2026 by
Odoo 18 Module Lifecycle
BL Consulting Ltd, Boyan Yordanov


Table of Contents

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

  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 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

StageWhen it runsContext
preBefore loading new Python codeOld DB schema
postAfter init_models + load_dataNew DB schema, new code
endAfter loading ALL modulesEntire 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
1Regular recordsviews, actions, menus, server actions, data records, cron jobs
2_remove_copied_views()Deletes view copies by key pattern
3ir.model.constraintSQL and Python constraints
4ir.model.fields.selectionBefore fields (due to ondelete='cascade')
5ir.model.fieldsDeletes COLUMNS from tables
6ir.model.relationDROP TABLE CASCADE for M2M relation tables
7ir.modelDeletes 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 uninstallationALL records in tables defined ONLY by this moduleShow "Documents to Delete" from the wizard
Uninstallation + downstream depsCascading uninstallation of dependent modulesShow downstream_dependencies()
Upgrade with removed XML IDRecord is removed by _process_end()Check diff of data files
Upgrade with removed fieldColumn is DROPpedCheck model changes
Upgrade of baseALL modules get upgradedInform 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=TrueInstalls 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.

initupdatedemo

Flags are recursively propagated down to dependent modules

8. Loading Sequence (load_modules)

Loading sequence

1Initialization → initialize_sys_path()
2Loading base (always first)
→ pre-migrate → load python → load models → init_models → load_data → post-migrate
3Marking (-i → button_install, -u → button_upgrade)
4Loading installed / to upgrade / to remove
5Loading to install
6End-migration scripts (all modules)
7Finalize constraints
8Cleanup orphan data (_process_end)
9Uninstallation (to remove) → uninstall_hook → module_uninstall → recursive Registry.new()
10Validation of custom views
11_register_hook() on every model

init vs update vs install

Characteristicinit (-i)update (-u)install (button)
mode'init''update''init'
pre-migrateNOYESNO
post-migrateNOYESNO
pre_init_hookNONOYES
post_init_hookNONOYES
load_dataYESYESYES
view validationNOYESNO

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

Share this post
Tags
Bulgarian Localization for Odoo 18: Configuration
Module l10n_bg_tax_adminfor Odoo 18 — fiscal positions, taxes, protocols, customs