From 4265026ee23ab71e8b199a9d8cbbd439409d7a03 Mon Sep 17 00:00:00 2001 From: Adrian Torres Date: Mon, 26 Mar 2018 14:58:08 +0200 Subject: [PATCH] [FIX] ir_model: cascade delete custom o2m and related fields Before this revision: * Install a module, for this example we'll use `project` * Create a o2m field or a related field whose inverse is a field from the `project` module. * Uninstall the `project` module. Result -> BOOM! The registry can't be reloaded because it can't find the inverse field of our custom field, ayayay!! After this revision, this shouldn't break the registry anymore because we create two m2o fields which will trigger cascade deletes for both o2m and related fields upon the deletion of the inverse field, these fields are both computed stored fields and they're not shown to the end user, this is all done "under the rug". Task-ID: 58808 --- odoo/addons/base/models/ir_model.py | 17 +++++++++ odoo/addons/base/tests/test_ir_actions.py | 43 +++++++++++++++++++++++ 2 files changed, 60 insertions(+) diff --git a/odoo/addons/base/models/ir_model.py b/odoo/addons/base/models/ir_model.py index 96187c76a32..0176b2a8ed2 100644 --- a/odoo/addons/base/models/ir_model.py +++ b/odoo/addons/base/models/ir_model.py @@ -308,6 +308,8 @@ class IrModelFields(models.Model): relation = fields.Char(string='Object Relation', help="For relationship fields, the technical name of the target model") relation_field = fields.Char(help="For one2many fields, the field on the target model that implement the opposite many2one relationship") + relation_field_id = fields.Many2one('ir.model.fields', compute='_compute_relation_field_id', + store=True, ondelete='cascade', string='Relation field') model_id = fields.Many2one('ir.model', string='Model', required=True, index=True, ondelete='cascade', help="The model this field belongs to") field_description = fields.Char(string='Field Label', default='', required=True, translate=True) @@ -319,6 +321,8 @@ class IrModelFields(models.Model): "For example: [('blue','Blue'),('yellow','Yellow')]") copy = fields.Boolean(string='Copied', help="Whether the value is copied when duplicating a record.") related = fields.Char(string='Related Field', help="The corresponding related field, if any. This must be a dot-separated list of field names.") + related_field_id = fields.Many2one('ir.model.fields', compute='_compute_related_field_id', + store=True, string="Related field", ondelete='cascade') required = fields.Boolean() readonly = fields.Boolean() index = fields.Boolean(string='Indexed') @@ -346,6 +350,19 @@ class IrModelFields(models.Model): " name, partner_id.name") store = fields.Boolean(string='Stored', default=True, help="Whether the value is stored in the database.") + @api.depends('relation', 'relation_field') + def _compute_relation_field_id(self): + for rec in self: + if rec.state == 'manual' and rec.relation_field: + rec.relation_field_id = self._get(rec.relation, rec.relation_field) + + @api.depends('related') + def _compute_related_field_id(self): + for rec in self: + if rec.state == 'manual' and rec.related: + field = rec._related_field() + rec.related_field_id = self._get(field.model_name, field.name) + @api.depends() def _in_modules(self): installed_modules = self.env['ir.module.module'].search([('state', '=', 'installed')]) diff --git a/odoo/addons/base/tests/test_ir_actions.py b/odoo/addons/base/tests/test_ir_actions.py index 355ddaa143d..75982358d6c 100644 --- a/odoo/addons/base/tests/test_ir_actions.py +++ b/odoo/addons/base/tests/test_ir_actions.py @@ -230,6 +230,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 @@ -273,6 +274,48 @@ class TestCustomFields(common.TransactionCase): with self.assertRaises(ValidationError): self.create_field('foo') + def test_create_custom_o2m(self): + """ try creating a custom o2m and then deleting its m2o inverse """ + model = self.env['ir.model'].search([('model', '=', self.MODEL)]) + comodel = self.env['ir.model'].search([('model', '=', 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, + }) + + m2o_field.unlink() + self.assertFalse(o2m_field.exists()) + + def test_create_custom_related(self): + """ try creating a custom related then deleting its inverse """ + comodel = self.env['ir.model'].search([('model', '=', self.COMODEL)]) + + field = self.create_field('x_my_char') + + related_field = 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', + }) + + field.unlink() + self.assertFalse(related_field.exists()) + def test_rename_custom(self): """ custom field names must be start with 'x_' """ field = self.create_field('x_foo')