diff options
| author | Christophe Simonis <chs@odoo.com> | 2018-11-29 18:38:55 +0100 |
|---|---|---|
| committer | Christophe Simonis <chs@odoo.com> | 2018-11-29 18:38:55 +0100 |
| commit | f5c3dafb045f4eb5e832c45596dd59396667ea09 (patch) | |
| tree | 8265091d53c54dfa47e5a67f00e98cfda404ade2 /odoo | |
| parent | 5644ab53ab6f370a2fb48242e22702655afd6655 (diff) | |
| parent | c0eef4271110ebcab07811328a7b3f4309fb4d06 (diff) | |
[MERGE] forward port branch saas-11.3 up to c0eef427111
Diffstat (limited to 'odoo')
| -rw-r--r-- | odoo/addons/base/models/ir_translation.py | 49 | ||||
| -rw-r--r-- | odoo/addons/base/tests/test_translate.py | 82 | ||||
| -rw-r--r-- | odoo/addons/test_new_api/tests/test_onchange.py | 33 | ||||
| -rw-r--r-- | odoo/api.py | 6 | ||||
| -rw-r--r-- | odoo/fields.py | 39 |
5 files changed, 144 insertions, 65 deletions
diff --git a/odoo/addons/base/models/ir_translation.py b/odoo/addons/base/models/ir_translation.py index 60616552967..4bc659ef6bd 100644 --- a/odoo/addons/base/models/ir_translation.py +++ b/odoo/addons/base/models/ir_translation.py @@ -457,15 +457,15 @@ class IrTranslation(models.Model): if not callable(field.translate): return - trans = self.env['ir.translation'] - outdated = trans - discarded = trans + Translation = self.env['ir.translation'] + outdated = Translation + discarded = Translation for record in records: # get field value and terms to translate value = record[field.name] terms = set(field.get_trans_terms(value)) - record_trans = trans.search([ + translations = Translation.search([ ('type', '=', 'model_terms'), ('name', '=', "%s,%s" % (field.model_name, field.name)), ('res_id', '=', record.id), @@ -473,25 +473,32 @@ class IrTranslation(models.Model): if not terms: # discard all translations for that field - discarded += record_trans + discarded += translations continue - # remap existing translations on terms when possible - trans_src = record_trans.mapped('src') - for trans in record_trans: - if trans.src == trans.value: - discarded += trans - elif trans.src not in terms: - matches = get_close_matches(trans.src, terms, 1, 0.9) - if matches: - if matches[0] in trans_src: - # there is already a translation for this term; discard this one - discarded += trans - else: - trans.write({'src': matches[0], 'state': trans.state}) - trans_src.append(matches[0]) # avoid reuse of term - else: - outdated += trans + # remap existing translations on terms when possible; each term + # should be translated at most once per language + done = set() # {(src, lang), ...} + translations_to_match = [] + + for translation in translations: + if translation.src == translation.value: + discarded += translation + elif translation.src in terms: + done.add((translation.src, translation.lang)) + else: + translations_to_match.append(translation) + + for translation in translations_to_match: + matches = get_close_matches(translation.src, terms, 1, 0.9) + src = matches[0] if matches else None + if not src: + outdated += translation + elif (src, translation.lang) in done: + discarded += translation + else: + translation.write({'src': src, 'state': translation.state}) + done.add((src, translation.lang)) # process outdated and discarded translations outdated.write({'state': 'to_translate'}) diff --git a/odoo/addons/base/tests/test_translate.py b/odoo/addons/base/tests/test_translate.py index 57081e6b1c7..c9d7f35ec4c 100644 --- a/odoo/addons/base/tests/test_translate.py +++ b/odoo/addons/base/tests/test_translate.py @@ -310,30 +310,36 @@ class TestTranslation(TransactionCase): class TestXMLTranslation(TransactionCase): def setUp(self): super(TestXMLTranslation, self).setUp() - self.env['ir.translation'].load_module_terms(['base'], ['fr_FR']) + self.env['ir.translation'].load_module_terms(['base'], ['fr_FR', 'nl_NL']) + + def create_view(self, archf, terms, **kwargs): + view = self.env['ir.ui.view'].create({ + 'name': 'test', + 'model': 'res.partner', + 'arch': archf % terms, + }) + for lang, trans_terms in kwargs.items(): + for src, val in pycompat.izip(terms, trans_terms): + self.env['ir.translation'].create({ + 'type': 'model_terms', + 'name': 'ir.ui.view,arch_db', + 'lang': lang, + 'res_id': view.id, + 'src': src, + 'value': val, + 'state': 'translated', + }) + return view def test_copy(self): """ Create a simple view, fill in translations, and copy it. """ - env_en = self.env(context={}) - env_fr = self.env(context={'lang': 'fr_FR'}) - archf = '<form string="%s"><div>%s</div><div>%s</div></form>' terms_en = ('Knife', 'Fork', 'Spoon') terms_fr = ('Couteau', 'Fourchette', 'Cuiller') - view0 = self.env['ir.ui.view'].create({ - 'name': 'test', - 'model': 'res.partner', - 'arch': archf % terms_en, - }) - for src, value in list(pycompat.izip(terms_en, terms_fr)): - self.env['ir.translation'].create({ - 'type': 'model_terms', - 'name': 'ir.ui.view,arch_db', - 'lang': 'fr_FR', - 'res_id': view0.id, - 'src': src, - 'value': value, - }) + view0 = self.create_view(archf, terms_en, fr_FR=terms_fr) + + env_en = self.env(context={}) + env_fr = self.env(context={'lang': 'fr_FR'}) # check translated field self.assertEqual(view0.with_env(env_en).arch_db, archf % terms_en) @@ -360,17 +366,29 @@ class TestXMLTranslation(TransactionCase): archf = '<form string="%s"><div>%s</div><div>%s</div></form>' terms_en = ('Knife', 'Fork', 'Spoon') terms_fr = (' Couteau', 'Fourchette ', ' Cuiller ') - view0 = self.env['ir.ui.view'].create({ - 'name': 'test', - 'model': 'res.partner', - 'arch': archf % terms_en, - }) - for src, value in list(pycompat.izip(terms_en, terms_fr)): - self.env['ir.translation'].create({ - 'type': 'model_terms', - 'name': 'ir.ui.view,arch_db', - 'lang': 'fr_FR', - 'res_id': view0.id, - 'src': src, - 'value': value, - }) + self.create_view(archf, terms_en, fr_FR=terms_fr) + + def test_sync(self): + """ Check translations after minor change in source terms. """ + archf = '<form string="X">%s</form>' + terms_en = ('Bread and cheeze',) + terms_fr = ('Pain et fromage',) + terms_nl = ('Brood and kaas',) + view = self.create_view(archf, terms_en, fr_FR=terms_fr, nl_NL=terms_nl) + + env_en = self.env(context={}) + env_fr = self.env(context={'lang': 'fr_FR'}) + env_nl = self.env(context={'lang': 'nl_NL'}) + + self.assertEqual(view.with_env(env_en).arch_db, archf % terms_en) + self.assertEqual(view.with_env(env_fr).arch_db, archf % terms_fr) + self.assertEqual(view.with_env(env_nl).arch_db, archf % terms_nl) + + # modify source term in view (fixed type in 'cheeze') + terms_en = ('Bread and cheese',) + view.write({'arch_db': archf % terms_en}) + + # check whether translations have been synchronized + self.assertEqual(view.with_env(env_en).arch_db, archf % terms_en) + self.assertEqual(view.with_env(env_fr).arch_db, archf % terms_fr) + self.assertEqual(view.with_env(env_nl).arch_db, archf % terms_nl) diff --git a/odoo/addons/test_new_api/tests/test_onchange.py b/odoo/addons/test_new_api/tests/test_onchange.py index 7f91209b8b0..0c58844bb09 100644 --- a/odoo/addons/test_new_api/tests/test_onchange.py +++ b/odoo/addons/test_new_api/tests/test_onchange.py @@ -1,4 +1,8 @@ # -*- coding: utf-8 -*- +try: + from unittest.mock import patch +except ImportError: + from mock import patch from odoo.tests import common @@ -466,3 +470,32 @@ class TestOnChange(common.TransactionCase): result = Message.onchange(value, ['message', 'message_name', 'message_currency'], field_onchange) self.assertEqual(result['value'], onchange_result) + + def test_onchange_many2one_one2many(self): + """ Setting a many2one field should not read the inverse one2many. """ + discussion = self.env.ref('test_new_api.discussion_0') + field_onchange = self.Message._onchange_spec() + self.assertEqual(field_onchange.get('discussion'), '1') + + values = { + 'discussion': discussion.id, + 'name': "[%s] %s" % ('', self.env.user.name), + 'body': False, + 'author': self.env.uid, + 'size': 0, + } + + called = [False] + orig_read = type(discussion).read + + def mock_read(self, fields=None, load='_classic_read'): + if discussion in self and 'messages' in (fields or ()): + called[0] = True + return orig_read(self, fields, load) + + # changing 'discussion' on message should not read 'messages' on discussion + with patch.object(type(discussion), 'read', mock_read, create=True): + self.env.cache.invalidate() + self.Message.onchange(values, 'discussion', field_onchange) + + self.assertFalse(called[0], "discussion.messages has been read") diff --git a/odoo/api.py b/odoo/api.py index 22b82d17147..75d04ad21c3 100644 --- a/odoo/api.py +++ b/odoo/api.py @@ -1057,6 +1057,12 @@ class Cache(object): value = self._data[field][record.id].get(key, SpecialValue(None)) return default if isinstance(value, SpecialValue) else value + def get_special(self, record, field, default=None): + """ Return the special value of ``field`` for ``record``. """ + key = field.cache_key(record) + value = self._data[field][record.id].get(key) + return value.get if isinstance(value, SpecialValue) else default + def set_special(self, record, field, getter): """ Set the value of ``field`` for ``record`` to return ``getter()``. """ key = field.cache_key(record) diff --git a/odoo/fields.py b/odoo/fields.py index 00923886d0a..ce49ddc5e93 100644 --- a/odoo/fields.py +++ b/odoo/fields.py @@ -2166,6 +2166,26 @@ class Many2one(_Relational): return super(Many2one, self).convert_to_onchange(value, record, names) +class _RelationalMultiUpdate(object): + """ A getter to update the value of an x2many field, without reading its + value until necessary. + """ + __slots__ = ['record', 'field', 'value'] + + def __init__(self, record, field, value): + self.record = record + self.field = field + self.value = value + + def __call__(self): + # determine the current field's value, and update it in cache only + record, field, value = self.record, self.field, self.value + cache = record.env.cache + cache.remove(record, field) + val = field.convert_to_cache(record[field.name] | value, record, validate=False) + cache.set(record, field, val) + return val + class _RelationalMulti(_Relational): """ Abstract class for relational fields *2many. """ @@ -2177,7 +2197,12 @@ class _RelationalMulti(_Relational): """ Update the cached value of ``self`` for ``records`` with ``value``. """ cache = records.env.cache for record in records: - if cache.contains(record, self): + special = cache.get_special(record, self) + if isinstance(special, _RelationalMultiUpdate): + # include 'value' in the existing _RelationalMultiUpdate; this + # avoids reading the field's value (which may be large) + special.value |= value + elif cache.contains(record, self): try: val = self.convert_to_cache(record[self.name] | value, record, validate=False) cache.set(record, self, val) @@ -2185,17 +2210,7 @@ class _RelationalMulti(_Relational): # delay the failure until the field is necessary cache.set_failed(record, [self], exc) else: - cache.set_special(record, self, self._update_getter(record, value)) - - def _update_getter(self, record, value): - def getter(): - # determine the current field's value, and update it in cache only - cache = record.env.cache - cache.remove(record, self) - val = self.convert_to_cache(record[self.name] | value, record, validate=False) - cache.set(record, self, val) - return val - return getter + cache.set_special(record, self, _RelationalMultiUpdate(record, self, value)) def convert_to_cache(self, value, record, validate=True): # cache format: tuple(ids) |
