diff --git a/odoo/addons/base/models/ir_translation.py b/odoo/addons/base/models/ir_translation.py index 334c845dc98..30242ee0601 100644 --- a/odoo/addons/base/models/ir_translation.py +++ b/odoo/addons/base/models/ir_translation.py @@ -521,9 +521,13 @@ class IrTranslation(models.Model): # check for read/write access on translated field records fmode = 'read' if mode == 'read' else 'write' for mname, ids in model_ids.items(): - records = self.env[mname].browse(ids) + records = self.env[mname].browse(ids).exists() records.check_access_rights(fmode) records.check_field_access_rights(fmode, model_fields[mname]) + if mode == 'create' and set(records._ids) != set(ids): + raise ValidationError(_("Creating translation on non existing records")) + if not records: + continue records.check_access_rule(fmode) @api.constrains('type', 'name', 'value') diff --git a/odoo/addons/base/tests/test_translate.py b/odoo/addons/base/tests/test_translate.py index c0afb400151..a8586eeaaa1 100644 --- a/odoo/addons/base/tests/test_translate.py +++ b/odoo/addons/base/tests/test_translate.py @@ -1,8 +1,9 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. +from odoo.exceptions import AccessError, ValidationError from odoo.tools import mute_logger from odoo.tools.translate import quote, unquote, xml_translate, html_translate -from odoo.tests.common import TransactionCase, BaseCase +from odoo.tests.common import TransactionCase, BaseCase, new_test_user from psycopg2 import IntegrityError @@ -570,6 +571,56 @@ class TestTranslationWrite(TransactionCase): "Did not fallback to source when reset" ) + def test_orphan(self): + """ What happens with orphan translations. """ + self.env['res.lang']._activate_lang('fr_FR') + + # create a user with access rights on partner categories + user = new_test_user(self.env, 'deleter') + group = self.env.ref('base.group_partner_manager') + user.groups_id = [(4, group.id)] + + # this access rule triggers a MissingError + self.env['ir.rule'].create({ + 'model_id': self.env['ir.model']._get_id('res.partner.category'), + 'groups': [(4, group.id)], + 'domain_force': "[('name', 'ilike', 'e')]", + }) + + # create a translation, and delete the record from the database + translation = self.env['ir.translation'].create({ + 'type': 'model', + 'name': 'res.partner.category,name', + 'lang': 'fr_FR', + 'res_id': self.category.id, + 'src': 'Reblochon', + 'value': 'Parfum Exquis', + 'state': 'translated', + }) + translation.flush() + translation.invalidate_cache() + self.cr.execute("DELETE FROM res_partner_category WHERE id=%s", [self.category.id]) + + # deleting the translation should be possible, provided the user has + # access rights on the translation's model + user0 = new_test_user(self.env, 'cannot modify category') + with self.assertRaises(AccessError): + translation.with_user(user0).unlink() + + translation.with_user(user).unlink() + + # however, creating orphan translations should not be possible + with self.assertRaises(ValidationError): + translation.with_user(user).create({ + 'type': 'model', + 'name': 'res.partner.category,name', + 'lang': 'fr_FR', + 'res_id': self.category.id, + 'src': 'Reblochon', + 'value': 'Parfum Exquis', + 'state': 'translated', + }) + def test_field_selection(self): """ Test translations of field selections. """ field = self.env['ir.model']._fields['state']