summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAdrian Torres <adt@odoo.com>2018-07-23 07:42:57 +0200
committerGitHub <noreply@github.com>2018-07-23 07:42:57 +0200
commitcb4266787bca9722d62ee9b1b658db0d1b6d5f2a (patch)
tree2ebe51ad8d6eec70e8a96c98823f148c6e91a9a6
parente6b570c8989f211591afc5808fdb22e9f6d0d521 (diff)
[FIX] ir_model_fields: properly unlink manual fields
Before this commit: * Install module A that implements model M and field X. * Create a new custom field Y whose reference model is not M, implemented in any module other than A. * Make this field either a one2many, related or compute field depending on field X. * Uninstall module A. => The registry crashes fatally, the only way to recover from this is to log directly into the postgres database and manually eliminate the infringing ir.model.fields record. The reasons are multiple: * When creating custom fields that are relational, char fields are used to hold data like model name, field name, instead of properly storing the related field, which would let postgres handle the unlinking properly. * Despite this, the ir.model.fields method _prepare_update tries to make up for this by verifying that the ir.model.fields being unlinked during an uninstallation are not needed elsewhere and drops them, however it does so naively: It assumes that the "problematic" field will be defined within the module's scope, i.e. its model reference will be a model defined by the module being uninstalled, this isn't always the case. This is solved by getting the dependants of each field by accessing the model's field_triggers, if there are any triggers (i.e. dependants) then we add them to a `failed_dependencies` list for later processing. The same is done for field inverses (only in the case of a many2one). After finding the failed_dependencies, a check is performed to see if the unlink is being performed in the context of an uninstallation, in which case the failed dependencies will be unlinked, otherwise an error will be shown to the user. This commit is a "backport" + generalization of commit 4265026ee23ab71e8b199a9d8cbbd439409d7a03 Task-ID: 1856263
-rw-r--r--odoo/addons/base/ir/ir_model.py32
-rw-r--r--odoo/addons/base/tests/test_ir_actions.py57
2 files changed, 73 insertions, 16 deletions
diff --git a/odoo/addons/base/ir/ir_model.py b/odoo/addons/base/ir/ir_model.py
index c8b0a17d9c6..bbd6c22e2ff 100644
--- a/odoo/addons/base/ir/ir_model.py
+++ b/odoo/addons/base/ir/ir_model.py
@@ -546,23 +546,29 @@ class IrModelFields(models.Model):
This method prevents the modification/deletion of many2one fields
that have an inverse one2many, for instance.
"""
+ failed_dependencies = []
+ for rec in self:
+ model = self.env[rec.model]
+ field = model._fields[rec.name]
+ for dependant, path in model._field_triggers.get(field, ()):
+ if dependant.manual:
+ failed_dependencies.append((field, dependant))
+ for inverse in model._field_inverses.get(field, ()):
+ if inverse.manual and inverse.type == 'one2many':
+ failed_dependencies.append((field, inverse))
+
+ if not self._context.get(MODULE_UNINSTALL_FLAG) and failed_dependencies:
+ msg = _("The field '%s' cannot be removed because the field '%s' depends on it.")
+ raise UserError(msg % failed_dependencies[0])
+ elif failed_dependencies:
+ dependants = {rel[1] for rel in failed_dependencies}
+ to_unlink = [self._get(field.model_name, field.name) for field in dependants]
+ self.browse().union(*to_unlink).unlink()
+
self = self.filtered(lambda record: record.state == 'manual')
if not self:
return
- for record in self:
- model = self.env[record.model]
- field = model._fields[record.name]
- if field.type == 'many2one' and model._field_inverses.get(field):
- if self._context.get(MODULE_UNINSTALL_FLAG):
- # automatically unlink the corresponding one2many field(s)
- inverses = self.search([('relation', '=', field.model_name),
- ('relation_field', '=', field.name)])
- inverses.unlink()
- continue
- msg = _("The field '%s' cannot be removed because the field '%s' depends on it.")
- raise UserError(msg % (field, model._field_inverses[field][0]))
-
# remove fields from registry, and check that views are not broken
fields = [self.env[record.model]._pop_field(record.name) for record in self]
domain = expression.OR([('arch_db', 'like', record.name)] for record in self)
diff --git a/odoo/addons/base/tests/test_ir_actions.py b/odoo/addons/base/tests/test_ir_actions.py
index 110bd2fe83c..01d41bfb1a2 100644
--- a/odoo/addons/base/tests/test_ir_actions.py
+++ b/odoo/addons/base/tests/test_ir_actions.py
@@ -229,6 +229,7 @@ class TestActionBindings(common.TransactionCase):
class TestCustomFields(common.TransactionCase):
MODEL = 'res.partner'
+ COMODEL = 'res.users'
def setUp(self):
# check that the registry is properly reset
@@ -244,9 +245,6 @@ class TestCustomFields(common.TransactionCase):
self.registry.enter_test_mode()
self.addCleanup(self.registry.leave_test_mode)
- # do not reload the registry after removing a field
- self.env = self.env(context={'_force_unlink': True})
-
def create_field(self, name):
""" create a custom field and return it """
model = self.env['ir.model'].search([('model', '=', self.MODEL)])
@@ -331,3 +329,56 @@ class TestCustomFields(common.TransactionCase):
with self.assertRaises(UserError):
field.name = 'x_bar'
self.assertIn('x_foo', self.env[self.MODEL]._fields)
+
+ def test_unlink_with_inverse(self):
+ """ create a custom o2m and then delete its m2o inverse """
+ model = self.env['ir.model']._get(self.MODEL)
+ comodel = self.env['ir.model']._get(self.COMODEL)
+
+ m2o_field = self.env['ir.model.fields'].create({
+ 'model_id': comodel.id,
+ 'name': 'x_my_m2o',
+ 'field_description': 'my_m2o',
+ 'ttype': 'many2one',
+ 'relation': self.MODEL,
+ })
+
+ o2m_field = self.env['ir.model.fields'].create({
+ 'model_id': model.id,
+ 'name': 'x_my_o2m',
+ 'field_description': 'my_o2m',
+ 'ttype': 'one2many',
+ 'relation': self.COMODEL,
+ 'relation_field': m2o_field.name,
+ })
+
+ # normal mode: you cannot break dependencies
+ with self.assertRaises(UserError):
+ m2o_field.unlink()
+
+ # uninstall mode: unlink dependant fields
+ m2o_field.with_context(_force_unlink=True).unlink()
+ self.assertFalse(o2m_field.exists())
+
+ def test_unlink_with_dependant(self):
+ """ create a computed field, then delete its dependency """
+ # Also applies to compute fields
+ comodel = self.env['ir.model'].search([('model', '=', self.COMODEL)])
+
+ field = self.create_field('x_my_char')
+
+ dependant = self.env['ir.model.fields'].create({
+ 'model_id': comodel.id,
+ 'name': 'x_oh_boy',
+ 'field_description': 'x_oh_boy',
+ 'ttype': 'char',
+ 'related': 'partner_id.x_my_char',
+ })
+
+ # normal mode: you cannot break dependencies
+ with self.assertRaises(UserError):
+ field.unlink()
+
+ # uninstall mode: unlink dependant fields
+ field.with_context(_force_unlink=True).unlink()
+ self.assertFalse(dependant.exists())