diff --git a/addons/transifex/models/ir_translation.py b/addons/transifex/models/ir_translation.py index 51edae39206..6c9e84755d7 100644 --- a/addons/transifex/models/ir_translation.py +++ b/addons/transifex/models/ir_translation.py @@ -63,7 +63,7 @@ class IrTranslation(models.Model): project_modules[module] = tx_project for translation in self: - if not translation.module or not translation.source or translation.lang == 'en_US': + if not translation.module or not translation.src or translation.lang == 'en_US': # custom or source term translation.transifex_url = False continue @@ -85,6 +85,6 @@ class IrTranslation(models.Model): 'lang': lang_code, 'module': translation.module, 'src': "text:'" + werkzeug.url_quote_plus( - translation.source[:50].replace("\n", "").replace("'", "") + translation.src[:50].replace("\n", "").replace("'", "") ) + "'", } diff --git a/addons/website_gengo/controllers/main.py b/addons/website_gengo/controllers/main.py index 56eeec5ff5c..ceb380671a5 100644 --- a/addons/website_gengo/controllers/main.py +++ b/addons/website_gengo/controllers/main.py @@ -14,7 +14,7 @@ class WebsiteGengo(http.Controller): result = {"done": 0} gengo_translation_ids = request.env['ir.translation'].search([('id', 'in', translated_ids), ('gengo_translation', '!=', False)]) for trans in gengo_translation_ids: - result['done'] += len(trans.source.split()) + result['done'] += len(trans.src.split()) return result @http.route('/website/check_gengo_set', type='json', auth='user', website=True) diff --git a/odoo/addons/base/models/ir_translation.py b/odoo/addons/base/models/ir_translation.py index b05cffcc1d9..8c5906a19a4 100644 --- a/odoo/addons/base/models/ir_translation.py +++ b/odoo/addons/base/models/ir_translation.py @@ -171,8 +171,6 @@ class IrTranslation(models.Model): lang = fields.Selection(selection='_get_languages', string='Language', validate=False) type = fields.Selection(TRANSLATION_TYPE, string='Type', index=True) src = fields.Text(string='Internal Source') # stored in database, kept for backward compatibility - source = fields.Text(string='Source term', compute='_compute_source', - inverse='_inverse_source', search='_search_source') value = fields.Text(string='Translation Value') module = fields.Char(index=True, help="Module this term belongs to") @@ -196,55 +194,6 @@ class IrTranslation(models.Model): langs = self.env['res.lang'].search([]) return [(lang.code, lang.name) for lang in langs] - @api.depends('type', 'name', 'res_id') - def _compute_source(self): - ''' Get source name for the translation. If object type is model, return - the value stored in db. Otherwise, return value store in src field. - ''' - for record in self: - record.source = record.src - if record.type != 'model': - continue - model_name, field_name = record.name.split(',') - if model_name not in self.env: - continue - model = self.env[model_name] - field = model._fields.get(field_name) - if field is None: - continue - if not callable(field.translate): - # Pass context without lang, need to read real stored field, not translation - try: - result = model.browse(record.res_id).with_context(lang=None).read([field_name]) - except AccessError: - # because we can read self but not the record, - # that means we would get an access error when accessing the translations - # so instead we defer the access right to the "check" method - result = [{field_name: _("Cannot be translated; record not accessible.")}] - record.source = result[0][field_name] if result else False - - def _inverse_source(self): - ''' When changing source term of a translation, change its value in db - for the associated object, and the src field. - ''' - self.ensure_one() - if self.type == 'model': - model_name, field_name = self.name.split(',') - model = self.env[model_name] - field = model._fields[field_name] - if not callable(field.translate): - # Make a context without language information, because we want - # to write on the value stored in db and not on the one - # associated with the current language. Also not removing lang - # from context trigger an error when lang is different. - model.browse(self.res_id).with_context(lang=None).write({field_name: self.source}) - if self.src != self.source: - self.write({'src': self.source}) - - def _search_source(self, operator, value): - ''' the source term is stored on 'src' field ''' - return [('src', operator, value)] - def _auto_init(self): res = super(IrTranslation, self)._auto_init() # Add separate md5 index on src (no size limit on values, and good performance). @@ -329,6 +278,22 @@ class IrTranslation(models.Model): ]) return len(ids) + def _set_source(self, name, ids, src): + """ Update the translation source of records. + + :param name: a string defined as "," + :param ids: the ids of the given records + :param src: the source of the translation + """ + self._cr.execute("""UPDATE ir_translation + SET src=%s + WHERE type=%s AND name=%s AND res_id IN %s + RETURNING id""", + (src, 'model', name, tuple(ids))) + existing_ids = [row[0] for row in self._cr.fetchall()] + # invalidate src for updated translations + self.invalidate_cache(fnames=['src'], ids=existing_ids) + @api.model def _get_source_query(self, name, types, lang, source, res_id): if source: @@ -645,6 +610,8 @@ class IrTranslation(models.Model): This method is used for creations of translations where the given ``vals_list`` is trusted to be the right values and potential conflicts should be updated to the new given value. + Mandatory values: name, lang, res_id, src, type + The other keys are ignored during update if not present """ rows_by_type = defaultdict(list) for vals in vals_list: @@ -661,7 +628,10 @@ class IrTranslation(models.Model): ON CONFLICT (type, lang, name, res_id) WHERE type='model' DO UPDATE SET (name, lang, res_id, src, type, value, module, state, comments) = (EXCLUDED.name, EXCLUDED.lang, EXCLUDED.res_id, EXCLUDED.src, EXCLUDED.type, - EXCLUDED.value, EXCLUDED.module, EXCLUDED.state, EXCLUDED.comments) + EXCLUDED.value, + COALESCE(EXCLUDED.module, ir_translation.module), + COALESCE(EXCLUDED.state, ir_translation.state), + COALESCE(EXCLUDED.comments, ir_translation.comments)) WHERE EXCLUDED.value IS NOT NULL AND EXCLUDED.value != ''; """.format(", ".join(["%s"] * len(rows_by_type['model']))) self.env.cr.execute(query, rows_by_type['model']) diff --git a/odoo/addons/base/tests/test_translate.py b/odoo/addons/base/tests/test_translate.py index 1477d41e788..f4a3cb29c94 100644 --- a/odoo/addons/base/tests/test_translate.py +++ b/odoo/addons/base/tests/test_translate.py @@ -275,6 +275,13 @@ class TestTranslation(TransactionCase): category_fr = category.with_context({'lang': 'fr_FR'}) self.assertEqual(category_fr.name, 'Clients', "Did not found translation for initial value") + translation_fr = self.env['ir.translation'].search([ + ('name', '=', 'res.partner.category,name'), + ('res_id', '=', category.id), + ('lang', '=', 'fr_FR'), + ]) + self.assertEqual(translation_fr.src, 'Customers', "Did not set English version as source") + def test_103_duplicate_record_fr(self): category = self.customers.with_context({'lang': 'fr_FR'}).copy({'name': 'Clients (copie)'}) @@ -284,6 +291,13 @@ class TestTranslation(TransactionCase): category_fr = category.with_context({'lang': 'fr_FR'}) self.assertEqual(category_fr.name, 'Clients (copie)', "Did not used default value for translated value") + translation_fr = self.env['ir.translation'].search([ + ('name', '=', 'res.partner.category,name'), + ('res_id', '=', category.id), + ('lang', '=', 'fr_FR'), + ]) + self.assertEqual(translation_fr.src, 'Clients (copie)', "Did not set new name as source") + def test_104_orderby_translated_field(self): """ Test search ordered by a translated field. """ # create a category with a French translation @@ -328,6 +342,57 @@ class TestTranslation(TransactionCase): self.assertEqual(len(translations), 1, "Translations were not duplicated after `translate_fields` call") self.assertEqual(translations.value, "Apprenti", "The first translation must stay") + def test_106_en_us_translation(self): + """ Test synchronizing translations with duplicated source """ + # create a category with a French translation + cheese = self.env['res.partner.category'].create({'name': 'Cheese'}) + + self.env['ir.translation'].translate_fields('res.partner.category', cheese.id, 'name') + + translations = self.env['ir.translation'].search([('name', '=', 'res.partner.category,name'), ('res_id', '=', cheese.id)], order='lang') + self.assertEqual(len(translations), 2) + self.assertRecordValues(translations, + [{'lang': 'en_US', 'src': 'Cheese', 'value': 'Cheese'}, + {'lang': 'fr_FR', 'src': 'Cheese', 'value': 'Cheese'}]) + + # Translate in both language + translations[0].value = 'The Cheese' + translations[1].value = 'Fromage' + + # lang=None bypass translation system + self.assertEqual(cheese.with_context(lang=None).name, 'Cheese') + self.assertEqual(cheese.with_context(lang='fr_FR').name, 'Fromage') + self.assertEqual(cheese.with_context(lang='en_US').name, 'The Cheese') + + # set a new master value + cheese.with_context(lang='en_US').write({'name': 'Delicious Cheese'}) + + # every src must be updated + self.assertEqual(cheese.with_context(lang=None).name, 'Delicious Cheese') + self.assertRecordValues(translations, + [{'lang': 'en_US', 'src': 'Delicious Cheese', 'value': 'Delicious Cheese'}, + {'lang': 'fr_FR', 'src': 'Delicious Cheese', 'value': 'Fromage'}]) + + self.assertEqual(cheese.with_context(lang=None).name, 'Delicious Cheese') + self.assertEqual(cheese.with_context(lang='fr_FR').name, 'Fromage') + self.assertEqual(cheese.with_context(lang='en_US').name, 'Delicious Cheese') + + def test_107_duplicate_record_en(self): + category = self.customers.with_context({'lang': 'en_US'}).copy() + + category_no = category.with_context({}) + self.assertEqual(category_no.name, 'Customers', "Duplication did not set untranslated value") + + category_fr = category.with_context({'lang': 'fr_FR'}) + self.assertEqual(category_fr.name, 'Clients', "Did not found translation for initial value") + + translation_fr = self.env['ir.translation'].search([ + ('name', '=', 'res.partner.category,name'), + ('res_id', '=', category.id), + ('lang', '=', 'fr_FR'), + ]) + self.assertEqual(translation_fr.src, 'Customers', "Did not set English version as source") + class TestXMLTranslation(TransactionCase): def setUp(self): diff --git a/odoo/addons/base/views/ir_translation_views.xml b/odoo/addons/base/views/ir_translation_views.xml index f1c373fcc96..772fa1d297b 100644 --- a/odoo/addons/base/views/ir_translation_views.xml +++ b/odoo/addons/base/views/ir_translation_views.xml @@ -11,7 +11,7 @@ string="Web-only translations" domain="[('comments', 'like', 'openerp-web')]"/> - + @@ -38,7 +38,7 @@ - + @@ -55,7 +55,7 @@ ir.translation - + @@ -71,7 +71,7 @@ - + diff --git a/odoo/models.py b/odoo/models.py index 7b4a2ac283d..01c728a65d2 100644 --- a/odoo/models.py +++ b/odoo/models.py @@ -3410,6 +3410,9 @@ Fields: # val is not a translation: update the table val = field.convert_to_column(val, self, vals) columns.append((name, field.column_format, val)) + tname = "%s,%s" % (self._name, name) + if field.translate is True and not single_lang: + self.env['ir.translation']._set_source(tname, self.ids, val) updated.append(name) else: other_fields.append(field) @@ -3440,6 +3443,7 @@ Fields: + '\n\n({} {}, {} {})'.format(_('Records:'), sub_ids[:6], _('User:'), self._uid) ) + translation_values = [] for name in updated: field = self._fields[name] if callable(field.translate): @@ -3449,15 +3453,23 @@ Fields: elif has_translation and field.translate: # The translated value of a field has been modified. - src_trans = self.read([name])[0][name] + src_trans = self.with_context(lang=None).read([name])[0][name] if not src_trans: # Insert value to DB src_trans = vals[name] self.with_context(lang=None).write({name: src_trans}) tname = "%s,%s" % (self._name, name) val = field.convert_to_column(vals[name], self, vals) - self.env['ir.translation']._set_ids( - tname, 'model', self.env.lang, self.ids, val, src_trans) + translation_values += [dict( + src=src_trans, + value=val, + name=tname, + lang=self.env.lang, + type='model', + state='translated', + res_id=res_id) for res_id in self.ids] + + self.env['ir.translation']._upsert_translations(translation_values) # mark fields to recompute; do this before setting other fields, because # the latter can require the value of computed fields, e.g., a one2many @@ -4283,11 +4295,12 @@ Fields: vals_list = [] for vals in Translation.search_read(domain): del vals['id'] - del vals['source'] # remove source to avoid triggering _set_src del vals['module'] # duplicated vals is not linked to any module vals['res_id'] = target_id + if not callable(field.translate): + vals['src'] = new_wo_lang[name] if vals['lang'] == old.env.lang and field.translate is True: - # force a source if the new_val was not changed by copy override + # update master record if the new_val was not changed by copy override if new_val == old[name]: new_wo_lang[name] = old_wo_lang[name] vals['src'] = old_wo_lang[name]