Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[13.0] Removing of model with selection field failed #44767

Closed
Tatider opened this issue Feb 6, 2020 · 2 comments
Closed

[13.0] Removing of model with selection field failed #44767

Tatider opened this issue Feb 6, 2020 · 2 comments
Labels
13.0 ORM ORM, python Framework related

Comments

@Tatider
Copy link

Tatider commented Feb 6, 2020

Impacted versions:
13.0

Steps to reproduce:

  1. Create a model with selection field
from odoo import fields, models


class TestModel(models.Model):
    _name = "test.model"
    _description = "Test Model"

    name = fields.Char()
    state = fields.Selection(
        [
            ("draft", "New"),
            ("sent", "Sent"),
            ("failed", "Failed"),
        ],
        "State",
    )
  1. Import new model to a module.
  2. Update/install the module .
  3. Remove the model
  4. Update the module

Current behavior:
Raise an exception for selection option removing.
selection_field_traceback.txt

Expected behavior:
Remove model with all fields and selection options with out exceptions.

Video/Screenshot link (optional):

@pedrobaeza pedrobaeza added 13.0 ORM ORM, python Framework related labels Feb 6, 2020
@Elkasitu
Copy link
Contributor

I don't think it's a bug, more of a limitation of the ORM and frankly I don't know if we want to support this use-case.

Deleting a model is considered an "unstable change" i.e. should not be done in production because the mechanism that cleans up models is done during module uninstallation, the problem in your case is that there's a leftover reflection model (ir.models.fields.selection) that points to a model that is not loaded and does not exist anymore, it's not something specific to a Selection field, if you replace your Selection field by a Many2many field you will face the same error as it creates an ir.model.relation record that points to your deleted, unloaded model.

The uninstall process takes care of all of these steps

@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.env.is_system():
raise AccessError(_('Administrator access is required to uninstall a module'))
# enable model/field deletion
# we deactivate prefetching to not try to read a column that has been deleted
self = self.with_context(**{MODULE_UNINSTALL_FLAG: True, 'prefetch_fields': False})
# determine records to unlink
records_items = [] # [(model, id)]
model_ids = []
field_ids = []
selection_ids = []
constraint_ids = []
module_data = self.search([('module', 'in', modules_to_remove)], order='id DESC')
for data in module_data:
if data.model == 'ir.model':
model_ids.append(data.res_id)
elif data.model == 'ir.model.fields':
field_ids.append(data.res_id)
elif data.model == 'ir.model.fields.selection':
selection_ids.append(data.res_id)
elif data.model == 'ir.model.constraint':
constraint_ids.append(data.res_id)
else:
records_items.append((data.model, data.res_id))
# to collect external ids of records that cannot be deleted
undeletable_ids = []
def delete(records):
# do not delete records that have other external ids (and thus do
# not belong to the modules being installed)
ref_data = self.search([
('model', '=', records._name),
('res_id', 'in', records.ids),
])
records -= records.browse((ref_data - module_data).mapped('res_id'))
if not records:
return
# special case for ir.model.fields
if records._name == 'ir.model.fields':
# do not remove LOG_ACCESS_COLUMNS unless _log_access is False
# on the model
records -= records.filtered(lambda f: f.name == 'id' or (
f.name in models.LOG_ACCESS_COLUMNS and
f.model in self.env and self.env[f.model]._log_access
))
# delete orphan external ids right now
missing_ids = set(records.ids) - set(records.exists().ids)
orphans = ref_data.filtered(lambda r: r.res_id in missing_ids)
if orphans:
_logger.info('Deleting orphan ir_model_data %s', orphans)
orphans.unlink()
# now delete the records
_logger.info('Deleting %s', records)
try:
with self._cr.savepoint():
records.unlink()
except Exception:
if len(records) <= 1:
_logger.info('Unable to delete %s', records, exc_info=True)
undeletable_ids.extend(ref_data._ids)
else:
# divide the batch in two, and recursively delete them
half_size = len(records) // 2
delete(records[:half_size])
delete(records[half_size:])
# remove non-model records first, grouped by batches of the same model
for model, items in itertools.groupby(records_items, itemgetter(0)):
delete(self.env[model].browse(item[1] for item in items))
# Remove copied views. This must happen after removing all records from
# the modules to remove, otherwise ondelete='restrict' may prevent the
# deletion of some view. This must also happen before cleaning up the
# database schema, otherwise some dependent fields may no longer exist
# in database.
modules = self.env['ir.module.module'].search([('name', 'in', modules_to_remove)])
modules._remove_copied_views()
# remove constraints
delete(self.env['ir.model.constraint'].browse(constraint_ids))
constraints = self.env['ir.model.constraint'].search([('module', 'in', modules.ids)])
constraints._module_data_uninstall()
# Remove fields, selections and relations. Note that the selections of
# removed fields do not require any "data fix", as their corresponding
# column no longer exists. We can therefore completely ignore them. That
# is why selections are removed after fields: most selections are
# deleted on cascade by their corresponding field.
delete(self.env['ir.model.fields'].browse(field_ids))
delete(self.env['ir.model.fields.selection'].browse(selection_ids).exists())
relations = self.env['ir.model.relation'].search([('module', 'in', modules.ids)])
relations._module_data_uninstall()
# remove models
delete(self.env['ir.model'].browse(model_ids))
# remove remaining module data records
(module_data - self.browse(undeletable_ids)).unlink()

If you want to do it for development purposes, it may be best to first uninstall the module and then reinstall it or to start with a fresh database

@Jerther
Copy link
Contributor

Jerther commented May 19, 2022

Sorry don't want to necro-thread ;) But I got here from the 4th result in Google trying to find a solution to this in 14.0. The problem here occurs on a production database because I'm removing an obsolete model so uninstalling the module or restarting a database is out of question ;)

So to fix this you have to do proceed in two phases:

Phase 1

  • Delete everything that's related to this model (views, records, security stuff, etc.)
  • Keep the class definition but remove all fields from it:
from odoo import models


class TestModel(models.Model):
    _name = "test.model"
    _description = "Test Model"
  • Upgrade the module. This will delete all the fields including the problematic ones.

Phase 2:

  • Delete the class
  • Upgrade the module again. This will delete the model, and won't fail because of the selection fields (or any other field) because they're already gone.

Hope this helps someone :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
13.0 ORM ORM, python Framework related
Projects
None yet
Development

No branches or pull requests

4 participants