Skip to content

Odoo Module Uninstall and Cleanup: What Happens and How to Handle It

DeployMonkey Team · March 23, 2026 10 min read

What Uninstalling a Module Does

When you uninstall an Odoo module, the framework performs a cascade of cleanup operations. Understanding this process is essential for module developers who need to handle uninstallation gracefully.

Automatically Removed

  • Views: All ir.ui.view records owned by the module
  • Menu items: All ir.ui.menu records from the module
  • Actions: ir.actions.act_window, ir.actions.server, ir.actions.client
  • Security: ir.model.access and ir.rule records
  • Groups: res.groups defined by the module
  • Cron jobs: ir.cron records from the module
  • Data records: Records with noupdate="0" in data files
  • Email templates: mail.template records
  • Report definitions: ir.actions.report records

NOT Automatically Removed

  • Database tables: Tables created by module models are NOT dropped
  • Columns: Fields added to existing tables are NOT removed
  • Data with noupdate="1": Records flagged as no-update survive uninstallation
  • User data: Records created by users (not data files) stay in the database
  • Custom indexes: Indexes created in init() are not dropped
  • ir.config_parameter: System parameters persist

The uninstall_hook

Use uninstall_hook to clean up resources the framework does not handle:

def _uninstall_hook(env):
    """Clean up before module removal."""
    # Remove config parameters
    env['ir.config_parameter'].sudo().search([
        ('key', 'like', 'my_module.%')
    ]).unlink()

    # Drop custom indexes
    env.cr.execute("DROP INDEX IF EXISTS my_custom_index")

    # Clean up data from other models
    env['mail.template'].search([
        ('name', 'like', 'My Module:%')
    ]).unlink()

    # Remove custom fields from existing models
    fields_to_remove = env['ir.model.fields'].search([
        ('model', '=', 'res.partner'),
        ('name', 'like', 'x_mymodule_%'),
    ])
    fields_to_remove.unlink()

Why Tables Are Not Dropped

Odoo intentionally preserves database tables after uninstallation for several reasons:

  • Data safety: Users may have created thousands of records that should not be silently destroyed
  • Reinstallation: If the module is reinstalled, existing data is preserved
  • Foreign keys: Other tables may reference the module's tables
  • Audit trail: Regulatory requirements may mandate data retention

If you need to drop tables, do it explicitly in the uninstall_hook:

def _uninstall_hook(env):
    # WARNING: This permanently deletes all data in this table
    env.cr.execute("DROP TABLE IF EXISTS my_module_data CASCADE")

Handling Orphaned Columns

When your module adds fields to existing models (via inheritance), those columns remain after uninstallation:

class ResPartner(models.Model):
    _inherit = 'res.partner'
    x_loyalty_points = fields.Integer()  # column stays after uninstall

Clean up in uninstall_hook:

def _uninstall_hook(env):
    env.cr.execute("""
        ALTER TABLE res_partner
        DROP COLUMN IF EXISTS x_loyalty_points
    """)

Cleaning Up Cron Jobs

Cron jobs defined in data files are usually removed automatically. But if they were created with noupdate="1", they persist:

def _uninstall_hook(env):
    cron = env.ref('my_module.cron_daily_sync', raise_if_not_found=False)
    if cron:
        cron.write({'active': False})
        cron.unlink()

Safe Uninstall Pattern

A comprehensive uninstall_hook:

import logging
_logger = logging.getLogger(__name__)

def _uninstall_hook(env):
    _logger.info('Running my_module uninstall hook')

    # 1. Deactivate cron jobs first
    crons = env['ir.cron'].search([
        ('ir_actions_server_id.model_id.model', 'in', [
            'my.model1', 'my.model2'
        ])
    ])
    crons.write({'active': False})

    # 2. Clean up config parameters
    env['ir.config_parameter'].sudo().search([
        ('key', 'like', 'my_module.%')
    ]).unlink()

    # 3. Remove custom indexes
    for idx in ['my_index_1', 'my_index_2']:
        env.cr.execute(f"DROP INDEX IF EXISTS {idx}")

    # 4. Clean up orphaned columns on inherited models
    for col in ['x_custom_field1', 'x_custom_field2']:
        env.cr.execute(f"""
            ALTER TABLE res_partner
            DROP COLUMN IF EXISTS {col}
        """)

    # 5. Optionally drop module tables (destructive!)
    # env.cr.execute("DROP TABLE IF EXISTS my_module_data CASCADE")

    _logger.info('my_module uninstall hook completed')

Testing Uninstallation

class TestUninstall(TransactionCase):

    def test_uninstall_hook_cleans_params(self):
        # Setup
        self.env['ir.config_parameter'].sudo().set_param('my_module.test', 'value')

        # Run hook
        _uninstall_hook(self.env)

        # Verify cleanup
        param = self.env['ir.config_parameter'].sudo().get_param('my_module.test')
        self.assertFalse(param)

Common Mistakes

  • Not handling errors: If uninstall_hook raises, the uninstallation aborts. Wrap operations in try/except
  • Forgetting CASCADE: When dropping tables with foreign keys, use DROP TABLE ... CASCADE
  • Not logging: Silent hooks are impossible to debug in production
  • Dropping user data without warning: Consider archiving instead of deleting
  • Circular dependencies: If Module A depends on Module B, uninstalling B first triggers A's removal too

Module Dependencies and Uninstall Order

When you uninstall a module, all modules that depend on it are also uninstalled. The order is:

  1. Dependent modules are uninstalled first (reverse dependency order)
  2. Each module's uninstall_hook runs during its uninstallation
  3. Framework cleanup (views, menus, etc.) happens per module
  4. The base module being uninstalled is processed last

Best Practices

  • Always define an uninstall_hook if your module creates custom indexes, config parameters, or modifies other models
  • Log all cleanup operations for traceability
  • Use raise_if_not_found=False when referencing XML IDs — they may already be deleted
  • Wrap operations in try/except to prevent uninstallation from failing
  • Consider the impact on user data — archive instead of delete when possible
  • Test uninstallation as part of your module's test suite