diff --git a/addons/website/tests/test_menu.py b/addons/website/tests/test_menu.py index ef30bdaa9f3..f44394e298a 100644 --- a/addons/website/tests/test_menu.py +++ b/addons/website/tests/test_menu.py @@ -2,6 +2,8 @@ import json +from hashlib import sha256 +from lxml import html from unittest.mock import Mock, patch from werkzeug.urls import url_parse @@ -301,3 +303,35 @@ class TestMenuHttp(common.HttpCase): self.assertFalse(self.menu.page_id, "M2o should have been unset as this is an anchor URL.") self.assertEqual(self.menu.url, self.page_url + '#anchor', "Page URL should have been properly prefixed with the referer url") self.assertEqual(self.page.url, self.page_url, "Page URL should not have changed") + + def test_03_mega_menu_translate(self): + # Setup + fr = self.env['res.lang']._activate_lang('fr_FR') + Menu = self.env['website.menu'] + website = self.env['website'].browse(1) + website.language_ids += fr + menu = Menu.create({ + 'name': 'Test Mega Menu Content Translation Edit Mode', + 'mega_menu_content': '
something
', + 'parent_id': website.menu_id.id, + 'website_id': website.id, + }) + self.env['ir.module.module']._load_module_terms(['website'], [fr.code]) + + # Load cache + self.url_open('/%s' % fr.url_code) + self.url_open('/%s?edit_translations=1' % fr.url_code) + + # Translate + root = html.fromstring(menu.mega_menu_content) + to_translate = root.text_content() + sha = sha256(to_translate.encode()).hexdigest() + menu.update_field_translations_sha('mega_menu_content', {fr.code: {sha: 'french_mega_menu_content'}}) + self.assertIn("french_mega_menu_content", + menu.with_context(lang=fr.code, website_id=website.id).mega_menu_content) + + # Checks + page = self.url_open('/%s' % fr.url_code) + self.assertIn(b"french_mega_menu_content", page.content) + page = self.url_open('/%s?edit_translations=1' % fr.url_code) + self.assertIn(b"french_mega_menu_content", page.content) diff --git a/odoo/addons/base/models/ir_model.py b/odoo/addons/base/models/ir_model.py index 79af2e695fa..9a28e687ab7 100644 --- a/odoo/addons/base/models/ir_model.py +++ b/odoo/addons/base/models/ir_model.py @@ -986,8 +986,9 @@ class IrModelFields(models.Model): # names of the models to patch patched_models = set() if vals and self: + translate_only = all(self._fields[field_name].translate for field_name in vals) for item in self: - if item.state != 'manual': + if item.state != 'manual' and not translate_only: raise UserError(_('Properties of base fields cannot be altered in this manner! ' 'Please modify them through Python code, ' 'preferably through a custom addon!')) @@ -1056,12 +1057,6 @@ class IrModelFields(models.Model): return res - def update_field_translations(self, field_name, translations): - res = super().update_field_translations(field_name, translations) - if res: - self.env.registry.clear_cache() - return res - @api.depends('field_description', 'model') def _compute_display_name(self): for field in self: diff --git a/odoo/addons/base/models/ir_ui_view.py b/odoo/addons/base/models/ir_ui_view.py index 3840eeb2570..440181f493b 100644 --- a/odoo/addons/base/models/ir_ui_view.py +++ b/odoo/addons/base/models/ir_ui_view.py @@ -540,10 +540,7 @@ actual arch. return super(View, self).unlink() def _update_field_translations(self, fname, translations, digest=None): - res = super()._update_field_translations(fname, translations, digest) - if fname == 'arch_db' and 'install_filename' not in self._context: - self.write({'arch_updated': True}) - return res + return super(View, self.with_context(no_save_prev=True))._update_field_translations(fname, translations, digest) @api.returns('self', lambda value: value.id) def copy(self, default=None): diff --git a/odoo/addons/test_new_api/models/test_new_api.py b/odoo/addons/test_new_api/models/test_new_api.py index bd3aa6d176a..953deed757f 100644 --- a/odoo/addons/test_new_api/models/test_new_api.py +++ b/odoo/addons/test_new_api/models/test_new_api.py @@ -1749,6 +1749,20 @@ class RelatedTranslation2(models.Model): parent_id = fields.Many2one('test_new_api.related_translation_1', string='Parent Model') name = fields.Char('Name Related', related='parent_id.name', readonly=False) html = fields.Html('HTML Related', related='parent_id.html', readonly=False) + computed_name = fields.Char('Name Computed', compute='_compute_name') + computed_html = fields.Char('HTML Computed', compute='_compute_html') + + @api.depends_context('lang') + @api.depends('parent_id.name') + def _compute_name(self): + for record in self: + record.computed_name = record.parent_id.name + + @api.depends_context('lang') + @api.depends('parent_id.html') + def _compute_html(self): + for record in self: + record.computed_html = record.parent_id.html class RelatedTranslation3(models.Model): diff --git a/odoo/addons/test_new_api/tests/test_related_translation.py b/odoo/addons/test_new_api/tests/test_related_translation.py index 2dd2c91432a..f47843324bc 100644 --- a/odoo/addons/test_new_api/tests/test_related_translation.py +++ b/odoo/addons/test_new_api/tests/test_related_translation.py @@ -89,6 +89,8 @@ class TestRelatedTranslation(odoo.tests.TransactionCase): self.assertEqual(self.test1.with_context(lang='fr_FR').name, 'Couteau') self.assertEqual(self.test2.with_context(lang='en_US').name, 'New knife') self.assertEqual(self.test2.with_context(lang='fr_FR').name, 'Couteau') + self.assertEqual(self.test2.with_context(lang='en_US').computed_name, 'New knife') + self.assertEqual(self.test2.with_context(lang='fr_FR').computed_name, 'Couteau') self.assertEqual(self.test3.with_context(lang='en_US').name, 'New knife') self.assertEqual(self.test3.with_context(lang='fr_FR').name, 'Couteau') self.test1.update_field_translations('name', {'fr_FR': 'Nouveau couteau'}) @@ -96,6 +98,8 @@ class TestRelatedTranslation(odoo.tests.TransactionCase): self.assertEqual(self.test1.with_context(lang='fr_FR').name, 'Nouveau couteau') self.assertEqual(self.test2.with_context(lang='en_US').name, 'New knife') self.assertEqual(self.test2.with_context(lang='fr_FR').name, 'Nouveau couteau') + self.assertEqual(self.test2.with_context(lang='en_US').computed_name, 'New knife') + self.assertEqual(self.test2.with_context(lang='fr_FR').computed_name, 'Nouveau couteau') self.assertEqual(self.test3.with_context(lang='en_US').name, 'New knife') self.assertEqual(self.test3.with_context(lang='fr_FR').name, 'Nouveau couteau') @@ -105,6 +109,8 @@ class TestRelatedTranslation(odoo.tests.TransactionCase): self.assertEqual(self.test1.with_context(lang='fr_FR').name, 'Couteau') self.assertEqual(self.test2.with_context(lang='en_US').name, 'New knife') self.assertEqual(self.test2.with_context(lang='fr_FR').name, 'Couteau') + self.assertEqual(self.test2.with_context(lang='en_US').computed_name, 'New knife') + self.assertEqual(self.test2.with_context(lang='fr_FR').computed_name, 'Couteau') self.assertEqual(self.test3.with_context(lang='en_US').name, 'New knife') self.assertEqual(self.test3.with_context(lang='fr_FR').name, 'Couteau') self.test3.update_field_translations('name', {'fr_FR': 'Nouveau couteau'}) @@ -112,6 +118,8 @@ class TestRelatedTranslation(odoo.tests.TransactionCase): self.assertEqual(self.test1.with_context(lang='fr_FR').name, 'Nouveau couteau') self.assertEqual(self.test2.with_context(lang='en_US').name, 'New knife') self.assertEqual(self.test2.with_context(lang='fr_FR').name, 'Nouveau couteau') + self.assertEqual(self.test2.with_context(lang='en_US').computed_name, 'New knife') + self.assertEqual(self.test2.with_context(lang='fr_FR').computed_name, 'Nouveau couteau') self.assertEqual(self.test3.with_context(lang='en_US').name, 'New knife') self.assertEqual(self.test3.with_context(lang='fr_FR').name, 'Nouveau couteau') @@ -136,6 +144,8 @@ class TestRelatedTranslation(odoo.tests.TransactionCase): self.assertEqual(self.test1.with_context(lang='fr_FR').html, 'Couteau
Fourchette
Cuiller
') self.assertEqual(self.test2.with_context(lang='en_US').html, 'Knife
Fork
Spoon
') self.assertEqual(self.test2.with_context(lang='fr_FR').html, 'Couteau
Fourchette
Cuiller
') + self.assertEqual(self.test2.with_context(lang='en_US').computed_html, 'Knife
Fork
Spoon
') + self.assertEqual(self.test2.with_context(lang='fr_FR').computed_html, 'Couteau
Fourchette
Cuiller
') self.assertEqual(self.test3.with_context(lang='en_US').html, 'Knife
Fork
Spoon
') self.assertEqual(self.test3.with_context(lang='fr_FR').html, 'Couteau
Fourchette
Cuiller
') self.test1.update_field_translations('html', {'fr_FR': {'Couteau': 'Nouveau couteau'}}) @@ -143,6 +153,8 @@ class TestRelatedTranslation(odoo.tests.TransactionCase): self.assertEqual(self.test1.with_context(lang='fr_FR').html, 'Nouveau couteau
Fourchette
Cuiller
') self.assertEqual(self.test2.with_context(lang='en_US').html, 'Knife
Fork
Spoon
') self.assertEqual(self.test2.with_context(lang='fr_FR').html, 'Nouveau couteau
Fourchette
Cuiller
') + self.assertEqual(self.test2.with_context(lang='en_US').computed_html, 'Knife
Fork
Spoon
') + self.assertEqual(self.test2.with_context(lang='fr_FR').computed_html, 'Nouveau couteau
Fourchette
Cuiller
') self.assertEqual(self.test3.with_context(lang='en_US').html, 'Knife
Fork
Spoon
') self.assertEqual(self.test3.with_context(lang='fr_FR').html, 'Nouveau couteau
Fourchette
Cuiller
') @@ -151,6 +163,8 @@ class TestRelatedTranslation(odoo.tests.TransactionCase): self.assertEqual(self.test1.with_context(lang='fr_FR').html, 'Couteau
Fourchette
Cuiller
') self.assertEqual(self.test2.with_context(lang='en_US').html, 'Knife
Fork
Spoon
') self.assertEqual(self.test2.with_context(lang='fr_FR').html, 'Couteau
Fourchette
Cuiller
') + self.assertEqual(self.test2.with_context(lang='en_US').computed_html, 'Knife
Fork
Spoon
') + self.assertEqual(self.test2.with_context(lang='fr_FR').computed_html, 'Couteau
Fourchette
Cuiller
') self.assertEqual(self.test3.with_context(lang='en_US').html, 'Knife
Fork
Spoon
') self.assertEqual(self.test3.with_context(lang='fr_FR').html, 'Couteau
Fourchette
Cuiller
') self.test3.update_field_translations('html', {'fr_FR': {'Couteau': 'Nouveau couteau'}}) @@ -158,6 +172,8 @@ class TestRelatedTranslation(odoo.tests.TransactionCase): self.assertEqual(self.test1.with_context(lang='fr_FR').html, 'Nouveau couteau
Fourchette
Cuiller
') self.assertEqual(self.test2.with_context(lang='en_US').html, 'Knife
Fork
Spoon
') self.assertEqual(self.test2.with_context(lang='fr_FR').html, 'Nouveau couteau
Fourchette
Cuiller
') + self.assertEqual(self.test2.with_context(lang='en_US').computed_html, 'Knife
Fork
Spoon
') + self.assertEqual(self.test2.with_context(lang='fr_FR').computed_html, 'Nouveau couteau
Fourchette
Cuiller
') self.assertEqual(self.test3.with_context(lang='en_US').html, 'Knife
Fork
Spoon
') self.assertEqual(self.test3.with_context(lang='fr_FR').html, 'Nouveau couteau
Fourchette
Cuiller
') diff --git a/odoo/models.py b/odoo/models.py index 47a08221071..633b81307c7 100644 --- a/odoo/models.py +++ b/odoo/models.py @@ -3490,6 +3490,10 @@ class BaseModel(metaclass=MetaModel): """ self.ensure_one() + self.check_access_rights('write') + self.check_field_access_rights('write', [field_name]) + self.check_access_rule('write') + valid_langs = set(code for code, _ in self.env['res.lang'].get_installed()) | {'en_US'} missing_langs = set(translations) - valid_langs if missing_langs: @@ -3516,9 +3520,6 @@ class BaseModel(metaclass=MetaModel): if field.related and not field.store: related_path, field_name = field.related.rsplit(".", 1) return self.mapped(related_path)._update_field_translations(field_name, translations, digest) - self.check_access_rights('write') - self.check_field_access_rights('write', [field_name]) - self.check_access_rule('write') if field.translate is True: # falsy values (except emtpy str) are used to void the corresponding translation @@ -3545,7 +3546,6 @@ class BaseModel(metaclass=MetaModel): '{{}}'::jsonb) WHERE id = %s ''', (Json({'en_US': translation_fallback}), Json(translations), self.id)) - self.modified([field_name]) else: # Note: # update terms in 'en_US' will not change its value other translated values @@ -3573,7 +3573,13 @@ class BaseModel(metaclass=MetaModel): } new_translations[lang] = field.translate(translation.get, old_value) self.env.cache.update_raw(self, field, [new_translations], dirty=True) - self.modified([field_name]) + + # the following write is incharge of + # 1. mark field as modified + # 2. execute logics in the override `write` method + # 3. update write_date of the record if exists to support 't-cache' + # even if the value in cache is the same as the value written + self[field_name] = self[field_name] return True def get_field_translations(self, field_name, langs=None): diff --git a/odoo/tools/translate.py b/odoo/tools/translate.py index ed0437d9787..12a6cef456e 100644 --- a/odoo/tools/translate.py +++ b/odoo/tools/translate.py @@ -1397,6 +1397,7 @@ class TranslationImporter: self.model_translations.clear() env.invalidate_all() + env.registry.clear_cache() if self.verbose: _logger.info("translations are loaded successfully")