[IMP] mail: correctly propagate language when posting from composer
RATIONALE
Naming conventions
* content = core message / subject (+ other fields);
* email layout = layouting applied to messages when sent to recipients who
receive notifications by email e.g. access button, ...;
* comment mode / mailing mode: composer main composition mode: posting on
record(s) / sending a mailing on records;
Content situation when using a template to post on a composer
* comment, monorecord mode: choose a template, content is rendered directly
from template side according to 'lang' field definition -> ok;
* comment, multirecord mode / mailing mode: choose a template, content is
the raw content. At post / send, content is rendered and translated if
matching template content -> ok;
Email layout situation when posting / mailing
* comment, monorecord model, using a template from UX: layout is partly
translated based on template 'lang' field to match the content translation.
Part of the layout still uses the current user lang;
* other comment use cases: layout is translated based on current user lang;
* mailing mode: layout not supported;
PURPOSE
Correctly propagate lang from template in all situations whenever a template
is used, and translate all layout parts.
SPECIFICATIONS
When using the composer to post messages (either as comment or in batch) the
language coming from the template is not propagated until the notification
process.
A hack has been done to try to guess the language inside the notification
process, based on context keys at odoo/odoo@9e71d228ed. This was done to partly fix
the bug in stable.
Since odoo/odoo@3eb9680602 it is possible to propagate a lang from 'message_post' or
'message_notify' calls until the notification process. We can therefore call
the posting methods using the rendered lang (in both mono and multi record
modes) and remove that hack.
When a lang is propagated using the 'force_email_lang' it is used to choose
the language of the layout (content, buttons). Currently all recipients
receives the same language for layouting. We plan to soon use the recipient
language when possible, then fallback on that forced lang. This will offer
more granularity and a better user experience.
When using scheduled messages (creating message but delaying the sending of
notifications) it is also working as notification parameters are saved when
creating the scheduling record, see odoo/odoo@9b11a9d82b.
EXAMPLE
A user in english uses a template on a lead whose customer is in spanish
with a follower being in german. Template lang is customer's lang:
* content is translated into spanish (customer's lang);
* layout is translated into spanish (customer's lang);
Both recipients (customer and follower) receive the content in spanish.
LINKS
Task-3046371 (Mail: Better Language Support in Composer)
Task-2555155 (Mail: Translate notification action/access buttons)
Part-of: odoo/odoo#106177
This commit is contained in:
@@ -3012,17 +3012,24 @@ class MailThread(models.AbstractModel):
|
||||
if not partners_data:
|
||||
return True
|
||||
|
||||
model = msg_vals.get('model') if msg_vals else message.model
|
||||
model_name = model_description or (self.env['ir.model']._get(model).display_name if model else False) # one query for display name
|
||||
recipients_groups_list = self._notify_get_recipients_classify(
|
||||
message, partners_data, model_name, msg_vals=msg_vals
|
||||
lang = force_email_lang if force_email_lang else self.env.context.get('lang')
|
||||
|
||||
if not model_description:
|
||||
model_description = self.with_context(lang=lang)._get_model_description(
|
||||
msg_vals['model'] if msg_vals and msg_vals.get('model') else message.model
|
||||
)
|
||||
recipients_groups_list = self.with_context(lang=lang)._notify_get_recipients_classify(
|
||||
message,
|
||||
partners_data,
|
||||
model_description,
|
||||
msg_vals=msg_vals,
|
||||
)
|
||||
|
||||
if not recipients_groups_list:
|
||||
return True
|
||||
force_send = self.env.context.get('mail_notify_force_send', force_send)
|
||||
|
||||
template_values = self._notify_by_email_prepare_rendering_context(
|
||||
template_values = self.with_context(lang=lang)._notify_by_email_prepare_rendering_context(
|
||||
message, msg_vals=msg_vals, model_description=model_description,
|
||||
force_email_company=force_email_company,
|
||||
force_email_lang=force_email_lang,
|
||||
@@ -3032,7 +3039,10 @@ class MailThread(models.AbstractModel):
|
||||
|
||||
email_layout_xmlid = msg_vals.get('email_layout_xmlid') if msg_vals else message.email_layout_xmlid
|
||||
template_xmlid = email_layout_xmlid if email_layout_xmlid else 'mail.mail_notification_layout'
|
||||
base_mail_values = self._notify_by_email_get_base_mail_values(message, additional_values={'auto_delete': mail_auto_delete})
|
||||
base_mail_values = self.with_context(lang=lang)._notify_by_email_get_base_mail_values(
|
||||
message,
|
||||
additional_values={'auto_delete': mail_auto_delete},
|
||||
)
|
||||
|
||||
# Clean the context to get rid of residual default_* keys that could cause issues during
|
||||
# the mail.mail creation.
|
||||
@@ -3158,6 +3168,8 @@ class MailThread(models.AbstractModel):
|
||||
"""
|
||||
if msg_vals is False:
|
||||
msg_vals = {}
|
||||
lang = force_email_lang if force_email_lang else self.env.context.get('lang')
|
||||
record_wlang = self.with_context(lang=lang)
|
||||
|
||||
# compute send user and its related signature; try to use self.env.user instead of browsing
|
||||
# user_ids if they are the author will give a sudo user, improving access performances and cache usage.
|
||||
@@ -3174,33 +3186,19 @@ class MailThread(models.AbstractModel):
|
||||
if force_email_company:
|
||||
company = force_email_company
|
||||
else:
|
||||
company = self.company_id.sudo() if self and 'company_id' in self and self.company_id else self.env.company
|
||||
company = record_wlang.company_id.sudo() if (
|
||||
record_wlang and 'company_id' in record_wlang and record_wlang.company_id
|
||||
) else record_wlang.env.company
|
||||
if company.website:
|
||||
website_url = 'http://%s' % company.website if not company.website.lower().startswith(('http:', 'https:')) else company.website
|
||||
else:
|
||||
website_url = False
|
||||
|
||||
# compute lang in which content was rendered or typed
|
||||
lang = False
|
||||
if force_email_lang:
|
||||
lang = force_email_lang
|
||||
elif {'default_template_id', 'default_model', 'default_res_ids'} <= self.env.context.keys():
|
||||
# TDE FIXME: this whole brol should be cleaned !
|
||||
if msg_vals.get('res_id'):
|
||||
res_ids = [msg_vals['res_id']]
|
||||
else:
|
||||
res_ids = self.env['mail.compose.message']._parse_res_ids(self.env.context['default_res_ids'])
|
||||
template = self.env['mail.template'].browse(self.env.context['default_template_id'])
|
||||
if res_ids and template and template.lang:
|
||||
lang = template._render_lang(res_ids)[res_ids[0]]
|
||||
if not lang:
|
||||
lang = self.env.context.get('lang')
|
||||
|
||||
# record, model
|
||||
if not model_description:
|
||||
model = msg_vals.get('model') if 'model' in msg_vals else message.model
|
||||
if model:
|
||||
model_description = self.env['ir.model'].with_context(lang=lang)._get(model).display_name
|
||||
model_description = record_wlang._get_model_description(
|
||||
msg_vals.get('model') if 'model' in msg_vals else message.model
|
||||
)
|
||||
record_name = msg_vals.get('record_name') if 'record_name' in msg_vals else message.record_name
|
||||
|
||||
# tracking
|
||||
@@ -3224,7 +3222,7 @@ class MailThread(models.AbstractModel):
|
||||
'tracking_values': tracking,
|
||||
# record
|
||||
'model_description': model_description,
|
||||
'record': self,
|
||||
'record': record_wlang,
|
||||
'record_name': record_name,
|
||||
'subtitles': [record_name],
|
||||
# user / environment
|
||||
@@ -3605,6 +3603,14 @@ class MailThread(models.AbstractModel):
|
||||
hm = hmac.new(secret.encode('utf-8'), token.encode('utf-8'), hashlib.sha1).hexdigest()
|
||||
return hm
|
||||
|
||||
@api.model
|
||||
def _get_model_description(self, model_name):
|
||||
if not model_name:
|
||||
return False
|
||||
if not 'lang' in self.env.context:
|
||||
raise ValueError(_('At this point lang should be correctly set'))
|
||||
return self.env['ir.model']._get(model_name).display_name # one query for display name
|
||||
|
||||
def _is_thread_message(self, msg_vals=None):
|
||||
""" Tool method to compute thread validity in notification methods.
|
||||
msg_vals is used as a replacement for self, allowing to force model
|
||||
|
||||
@@ -549,6 +549,19 @@ class MailComposer(models.TransientModel):
|
||||
if not composer.template_id:
|
||||
composer.scheduled_date = False
|
||||
|
||||
# Overrides of mail.compose.mixin
|
||||
@api.depends('template_id')
|
||||
def _compute_lang(self):
|
||||
""" Computation is coming either from template, either reset. When
|
||||
having a template with a value set, copy it (in batch mode) or render
|
||||
it (in monorecord comment mode) on the composer. When removing the
|
||||
template, reset it. """
|
||||
for composer in self:
|
||||
if composer.template_id:
|
||||
composer._set_value_from_template('lang')
|
||||
if not composer.template_id:
|
||||
composer.lang = False
|
||||
|
||||
# Overrides of mail.render.mixin
|
||||
@api.depends('model')
|
||||
def _compute_render_model(self):
|
||||
@@ -743,6 +756,7 @@ class MailComposer(models.TransientModel):
|
||||
DYN - 'body',
|
||||
STA - 'email_add_signature',
|
||||
STA - 'email_layout_xmlid',
|
||||
DYN - 'force_email_lang', # notify parameter
|
||||
|
||||
BOTH
|
||||
DYN - 'attachment_ids',
|
||||
@@ -861,8 +875,10 @@ class MailComposer(models.TransientModel):
|
||||
RecordsModel = self.env[self.model].with_prefetch(res_ids)
|
||||
email_mode = self.composition_mode == 'mass_mail'
|
||||
|
||||
# langs, used currently only to propagate in comment mode for notification
|
||||
# layout translation
|
||||
langs = self._render_field('lang', res_ids) if not email_mode else {}
|
||||
subjects = self._render_field('subject', res_ids, compute_lang=True)
|
||||
# We want to preserve comments in emails so as to keep mso conditionals
|
||||
bodies = self._render_field(
|
||||
'body', res_ids, compute_lang=True,
|
||||
# We want to preserve comments in emails so as to keep mso conditionals
|
||||
@@ -881,7 +897,14 @@ class MailComposer(models.TransientModel):
|
||||
{
|
||||
'body_html': bodies[res_id],
|
||||
'res_id': res_id,
|
||||
} if email_mode else {}),
|
||||
} if email_mode else {}
|
||||
),
|
||||
**(
|
||||
{
|
||||
# notify parameter to force layout lang
|
||||
'force_email_lang': langs[res_id],
|
||||
} if not email_mode else {}
|
||||
),
|
||||
}
|
||||
for res_id in res_ids
|
||||
}
|
||||
@@ -979,6 +1002,7 @@ class MailComposer(models.TransientModel):
|
||||
populate in '_prepare_mail_values';
|
||||
"""
|
||||
self.ensure_one()
|
||||
email_mode = self.composition_mode == 'mass_mail'
|
||||
|
||||
# Duplicate attachments linked to the email.template. Indeed, composer
|
||||
# duplicates attachments in mass mode. But in 'rendered' mode attachments
|
||||
@@ -1005,6 +1029,12 @@ class MailComposer(models.TransientModel):
|
||||
'partner_ids': self.partner_ids.ids,
|
||||
'scheduled_date': self.scheduled_date,
|
||||
'subject': self.subject or '',
|
||||
**(
|
||||
{
|
||||
# notify parameter to force layout lang
|
||||
'force_email_lang': self.lang,
|
||||
} if not email_mode else {}
|
||||
),
|
||||
}
|
||||
for res_id in res_ids
|
||||
}
|
||||
|
||||
@@ -1581,25 +1581,20 @@ class TestComposerResultsComment(TestMailComposer, CronMixinCase):
|
||||
email = self._find_sent_email(test_record.user_id.email_formatted, [test_record.customer_id.email_formatted])
|
||||
self.assertTrue(bool(email), 'Email not found, check recipients')
|
||||
|
||||
# TDE FIXME: as it currently depends on a context-based hack
|
||||
# translation is not supported when scheduling notifications
|
||||
# or when a domain is given. Moreover access buttons are not
|
||||
# translated
|
||||
exp_layout_content_en = 'English Layout for Ticket-like model'
|
||||
exp_layout_content_es = 'Spanish Layout para Spanish Model Description'
|
||||
exp_button_en = 'View Ticket-like model'
|
||||
# exp_button_es = 'SpanishView Spanish Model Description'
|
||||
exp_button_es = 'SpanishView Spanish Model Description'
|
||||
if email_layout_xmlid:
|
||||
if exp_lang == 'es_ES' and batch_mode != 'domain' and not scheduled_date:
|
||||
if exp_lang == 'es_ES':
|
||||
self.assertIn(exp_layout_content_es, email['body'])
|
||||
self.assertIn(exp_button_en, email['body'],
|
||||
'TODO: buttons should be translated')
|
||||
self.assertIn(exp_button_es, email['body'])
|
||||
else:
|
||||
self.assertIn(exp_layout_content_en, email['body'])
|
||||
self.assertIn(exp_button_en, email['body'])
|
||||
else:
|
||||
# check default layouting applies
|
||||
if exp_lang == 'es_ES' and batch_mode != 'domain' and not scheduled_date:
|
||||
if exp_lang == 'es_ES':
|
||||
self.assertIn('html lang="es_ES"', email['body'])
|
||||
else:
|
||||
self.assertIn('html lang="en_US"', email['body'])
|
||||
|
||||
@@ -1583,19 +1583,14 @@ class TestMessagePostLang(TestMailCommon, TestRecipients):
|
||||
self.assertIn('NotificationButtonTitle', body,
|
||||
'Groups-based action names translation failed')
|
||||
else:
|
||||
self.assertIn('Spanish Layout para', body, 'Layout content should be translated')
|
||||
self.assertNotIn('English Layout for', body)
|
||||
self.assertIn('Spanish Layout para Spanish Model Description', body, 'Model name should be translated')
|
||||
# check notification layout strings
|
||||
self.assertIn('View Lang Chatter Model', body,
|
||||
'Fixme: "View document" should be translated')
|
||||
# self.assertIn('SpanishView Spanish Model Description', body,
|
||||
# '"View document" should be translated')
|
||||
# self.assertNotIn(f'View {test_record._description}', body,
|
||||
# '"View document" should be translated')
|
||||
# self.assertIn('SpanishButtonTitle', body,
|
||||
# 'Groups-based action names should be translated')
|
||||
self.assertIn('NotificationButtonTitle', body, 'Fixme: Groups-based action names should be translated')
|
||||
self.assertIn('SpanishView Spanish Model Description', body,
|
||||
'"View document" should be translated')
|
||||
self.assertNotIn(f'View {test_record._description}', body,
|
||||
'"View document" should be translated')
|
||||
self.assertIn('SpanishButtonTitle', body,
|
||||
'Groups-based action names should be translated')
|
||||
self.assertNotIn('NotificationButtonTitle', body,
|
||||
'Groups-based action names should be translated')
|
||||
|
||||
@users('employee')
|
||||
@mute_logger('odoo.addons.mail.models.mail_mail')
|
||||
@@ -1654,7 +1649,8 @@ class TestMessagePostLang(TestMailCommon, TestRecipients):
|
||||
# check notification layout strings
|
||||
self.assertIn('SpanishView Spanish Model Description', body,
|
||||
'"View document" should be translated')
|
||||
self.assertNotIn(f'View {test_records[1]._description}', body)
|
||||
self.assertNotIn(f'View {test_records[1]._description}', body,
|
||||
'"View document" should be translated')
|
||||
self.assertIn('SpanishButtonTitle', body, 'Groups-based action names should be translated')
|
||||
self.assertNotIn('NotificationButtonTitle', body)
|
||||
|
||||
@@ -1697,13 +1693,13 @@ class TestMessagePostLang(TestMailCommon, TestRecipients):
|
||||
self.assertNotIn('English Layout for', body)
|
||||
self.assertIn('Spanish Layout para Spanish Model Description', body,
|
||||
'Model name should be translated')
|
||||
# self.assertIn('SpanishView Spanish Model Description', body,
|
||||
# '"View document" should be translated')
|
||||
self.assertIn(f'View {test_records[1]._description}', body,
|
||||
'Fixme: "View document" should be translated')
|
||||
# self.assertIn('NotificationButtonTitle', body,
|
||||
# 'Groups-based action names should be translated')
|
||||
self.assertIn('NotificationButtonTitle', body,
|
||||
self.assertIn('SpanishView Spanish Model Description', body,
|
||||
'"View document" should be translated')
|
||||
self.assertNotIn(f'View {test_records[1]._description}', body,
|
||||
'"View document" should be translated')
|
||||
self.assertIn('SpanishButtonTitle', body,
|
||||
'Groups-based action names should be translated')
|
||||
self.assertNotIn('NotificationButtonTitle', body,
|
||||
'Fixme: groups-based action names should be translated')
|
||||
|
||||
@users('employee')
|
||||
|
||||
@@ -369,7 +369,7 @@ class TestMailAPIPerformance(BaseMailPerformance):
|
||||
'partner_ids': [(4, customer.id)],
|
||||
})
|
||||
|
||||
with self.assertQueryCount(admin=37, employee=37):
|
||||
with self.assertQueryCount(admin=35, employee=35):
|
||||
composer._action_send_mail()
|
||||
|
||||
@users('admin', 'employee')
|
||||
@@ -393,7 +393,7 @@ class TestMailAPIPerformance(BaseMailPerformance):
|
||||
composer_form.attachment_ids.add(attachment)
|
||||
composer = composer_form.save()
|
||||
|
||||
with self.assertQueryCount(admin=48, employee=48): # tm+com 47/47
|
||||
with self.assertQueryCount(admin=46, employee=46): # tm+com 45/45
|
||||
composer._action_send_mail()
|
||||
|
||||
# notifications
|
||||
@@ -479,7 +479,7 @@ class TestMailAPIPerformance(BaseMailPerformance):
|
||||
'default_template_id': test_template.id,
|
||||
}).create({})
|
||||
|
||||
with self.assertQueryCount(admin=44, employee=44):
|
||||
with self.assertQueryCount(admin=42, employee=42):
|
||||
composer._action_send_mail()
|
||||
|
||||
# notifications
|
||||
@@ -511,7 +511,7 @@ class TestMailAPIPerformance(BaseMailPerformance):
|
||||
)
|
||||
composer = composer_form.save()
|
||||
|
||||
with self.assertQueryCount(admin=44, employee=44):
|
||||
with self.assertQueryCount(admin=43, employee=43):
|
||||
composer._action_send_mail()
|
||||
|
||||
# notifications
|
||||
@@ -541,7 +541,7 @@ class TestMailAPIPerformance(BaseMailPerformance):
|
||||
)
|
||||
composer = composer_form.save()
|
||||
|
||||
with self.assertQueryCount(admin=64, employee=64):
|
||||
with self.assertQueryCount(admin=61, employee=61):
|
||||
composer._action_send_mail()
|
||||
|
||||
# notifications
|
||||
@@ -1309,7 +1309,7 @@ class TestMailHeavyPerformancePost(BaseMailPerformance):
|
||||
attachments = self.env['ir.attachment'].with_user(self.env.user).create(self.test_attachments_vals)
|
||||
# enable_logging = self.cr._enable_logging() if self.warm else nullcontext()
|
||||
# with self.assertQueryCount(employee=63), enable_logging:
|
||||
with self.assertQueryCount(employee=63):
|
||||
with self.assertQueryCount(employee=60):
|
||||
record_container.with_context({}).message_post(
|
||||
body='<p>Test body <img src="cid:cid1"> <img src="cid:cid2"></p>',
|
||||
subject='Test Subject',
|
||||
|
||||
Reference in New Issue
Block a user