import logging from odoo import api, fields, models, SUPERUSER_ID, tools, _ from odoo.exceptions import AccessError, UserError, ValidationError _logger = logging.getLogger(__name__) MODULE_UNINSTALL_FLAG = '_force_unlink' class IrModelData(models.Model): _inherit = 'ir.model.data' @api.model def _module_data_uninstall(self, modules_to_remove): """Deletes all the records referenced by the ir.model.data entries ``ids`` along with their corresponding database backed (including dropping tables, columns, FKs, etc, as long as there is no other ir.model.data entry holding a reference to them (which indicates that they are still owned by another module). Attempts to perform the deletion in an appropriate order to maximize the chance of gracefully deleting all records. This step is performed as part of the full uninstallation of a module. """ if not (self._uid == SUPERUSER_ID or self.env.user.has_group('base.group_system')): raise AccessError(_('Administrator access is required to uninstall a module')) # enable model/field deletion self = self.with_context(**{MODULE_UNINSTALL_FLAG: True}) datas = self.search([('module', 'in', modules_to_remove)]) to_unlink = tools.OrderedSet() undeletable = self.browse([]) for data in datas.sorted(key='id', reverse=True): model = data.model res_id = data.res_id if data.model == 'ir.model' and 'mailchimp' in modules_to_remove: model_id = self.env[model].browse(res_id).with_context(prefetch_fields=False) if model_id.model == 'mail.mass_mailing.list_contact_rel': continue if data.model == 'ir.model.fields' and 'mailchimp' in modules_to_remove: field_id = self.env[model].browse(res_id).with_context(prefetch_fields=False) if field_id.name in ['contact_id', 'list_id']: continue to_unlink.add((model, res_id)) def unlink_if_refcount(to_unlink): undeletable = self.browse() for model, res_id in to_unlink: external_ids = self.search([('model', '=', model), ('res_id', '=', res_id)]) if external_ids - datas: # if other modules have defined this record, we must not delete it continue if model == 'ir.model.fields': # Don't remove the LOG_ACCESS_COLUMNS unless _log_access # has been turned off on the model. field = self.env[model].browse(res_id).with_context( prefetch_fields=False, ) if not field.exists(): _logger.info('Deleting orphan external_ids %s', external_ids) external_ids.unlink() continue if field.name in models.LOG_ACCESS_COLUMNS and field.model in self.env and self.env[field.model]._log_access: continue if field.name == 'id': continue _logger.info('Deleting %s@%s', res_id, model) try: self._cr.execute('SAVEPOINT record_unlink_save') self.env[model].browse(res_id).unlink() except Exception: _logger.info('Unable to delete %s@%s', res_id, model, exc_info=True) undeletable += external_ids self._cr.execute('ROLLBACK TO SAVEPOINT record_unlink_save') else: self._cr.execute('RELEASE SAVEPOINT record_unlink_save') return undeletable # Remove non-model records first, then model fields, and finish with models undeletable += unlink_if_refcount(item for item in to_unlink if item[0] not in ('ir.model', 'ir.model.fields', 'ir.model.constraint')) undeletable += unlink_if_refcount(item for item in to_unlink if item[0] == 'ir.model.constraint') modules = self.env['ir.module.module'].search([('name', 'in', modules_to_remove)]) constraints = self.env['ir.model.constraint'].search([('module', 'in', modules.ids)]) constraints._module_data_uninstall() undeletable += unlink_if_refcount(item for item in to_unlink if item[0] == 'ir.model.fields') relations = self.env['ir.model.relation'].search([('module', 'in', modules.ids)]) relations._module_data_uninstall() undeletable += unlink_if_refcount(item for item in to_unlink if item[0] == 'ir.model') (datas - undeletable).unlink()