diff --git a/addons/mail/models/mail_mail.py b/addons/mail/models/mail_mail.py
index ff25c60f04a..0c374d18ad8 100644
--- a/addons/mail/models/mail_mail.py
+++ b/addons/mail/models/mail_mail.py
@@ -63,7 +63,18 @@ class MailMail(models.Model):
values['notification'] = True
if not values.get('mail_message_id'):
self = self.with_context(message_create_from_mail_mail=True)
- return super(MailMail, self).create(values)
+ new_mail = super(MailMail, self).create(values)
+ if values.get('attachment_ids'):
+ new_mail.attachment_ids.check(mode='read')
+ return new_mail
+
+ @api.multi
+ def write(self, vals):
+ res = super(MailMail, self).write(vals)
+ if vals.get('attachment_ids'):
+ for mail in self:
+ mail.attachment_ids.check(mode='read')
+ return res
@api.multi
def unlink(self):
diff --git a/addons/mail/models/mail_message.py b/addons/mail/models/mail_message.py
index 3716cd4e6f3..c3b351264d3 100644
--- a/addons/mail/models/mail_message.py
+++ b/addons/mail/models/mail_message.py
@@ -740,6 +740,10 @@ class Message(models.Model):
# delegate creation of tracking after the create as sudo to avoid access rights issues
tracking_values_cmd = values.pop('tracking_value_ids', False)
message = super(Message, self).create(values)
+
+ if values.get('attachment_ids'):
+ message.attachment_ids.check(mode='read')
+
if tracking_values_cmd:
message.sudo().write({'tracking_value_ids': tracking_values_cmd})
@@ -762,6 +766,9 @@ class Message(models.Model):
if 'model' in vals or 'res_id' in vals:
self._invalidate_documents()
res = super(Message, self).write(vals)
+ if vals.get('attachment_ids'):
+ for mail in self:
+ mail.attachment_ids.check(mode='read')
self._invalidate_documents()
return res
diff --git a/addons/mail/wizard/invite.py b/addons/mail/wizard/invite.py
index acb89641f75..3fee1d78eac 100644
--- a/addons/mail/wizard/invite.py
+++ b/addons/mail/wizard/invite.py
@@ -1,6 +1,9 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
+from lxml import etree
+from lxml.html import builder as html
+
from odoo import _, api, fields, models
@@ -12,18 +15,27 @@ class Invite(models.TransientModel):
@api.model
def default_get(self, fields):
result = super(Invite, self).default_get(fields)
+ if self._context.get('mail_invite_follower_channel_only'):
+ result['send_mail'] = False
+ if 'message' not in fields:
+ return result
+
user_name = self.env.user.name_get()[0][1]
model = result.get('res_model')
res_id = result.get('res_id')
- if self._context.get('mail_invite_follower_channel_only'):
- result['send_mail'] = False
- if 'message' in fields and model and res_id:
- model_name = self.env['ir.model']._get(model).display_name
- document_name = self.env[model].browse(res_id).name_get()[0][1]
- message = _('
Hello,
%s invited you to follow %s document: %s.
') % (user_name, model_name, document_name)
- result['message'] = message
- elif 'message' in fields:
- result['message'] = _('Hello,
%s invited you to follow a new document.
') % user_name
+ if model and res_id:
+ document = self.env['ir.model']._get(model).display_name
+ title = self.env[model].browse(res_id).display_name
+ msg_fmt = _('%(user_name)s invited you to follow %(document)s document: %(title)s')
+ else:
+ msg_fmt = _('%(user_name)s invited you to follow a new document')
+
+ text = msg_fmt % locals()
+ message = html.DIV(
+ html.P(_('Hello,')),
+ html.P(text)
+ )
+ result['message'] = etree.tostring(message)
return result
res_model = fields.Char('Related Document Model', required=True, index=True, help='Model of the followed resource')
diff --git a/odoo/addons/base/ir/ir_translation.py b/odoo/addons/base/ir/ir_translation.py
index 2d013121a5f..072fb002fa6 100644
--- a/odoo/addons/base/ir/ir_translation.py
+++ b/odoo/addons/base/ir/ir_translation.py
@@ -464,15 +464,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'),
('name', '=', "%s,%s" % (field.model_name, field.name)),
('res_id', '=', record.id),
@@ -480,25 +480,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 328906c89c2..0c4e9c95cda 100644
--- a/odoo/addons/base/tests/test_translate.py
+++ b/odoo/addons/base/tests/test_translate.py
@@ -275,30 +275,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 zip(terms, trans_terms):
+ self.env['ir.translation'].create({
+ 'type': 'model',
+ '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 = ''
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 zip(terms_en, terms_fr):
- self.env['ir.translation'].create({
- 'type': 'model',
- '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)
@@ -325,17 +331,29 @@ class TestXMLTranslation(TransactionCase):
archf = ''
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 zip(terms_en, terms_fr):
- self.env['ir.translation'].create({
- 'type': 'model',
- '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 = ''
+ 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)