-
+
+
@@ -104,8 +104,8 @@
"{{ object.company_id.name }}" <{{ (object.company_id.email or user.email) }}>
{{ object.email_formatted }}
-
-
+
+
@@ -199,8 +199,8 @@
{{ object.company_id.partner_id.email_formatted }}
{{ object.email_formatted }}
-
-
+
+
@@ -250,8 +250,8 @@
"{{ object.company_id.name }}" <{{ (object.company_id.email or user.email) }}>
{{ object.email_formatted }}
-
-
+
+
diff --git a/addons/crm/models/crm_lead.py b/addons/crm/models/crm_lead.py
index 9bcd56bd1cd..d7b909347fb 100644
--- a/addons/crm/models/crm_lead.py
+++ b/addons/crm/models/crm_lead.py
@@ -1751,10 +1751,10 @@ class Lead(models.Model):
return self.env.ref('crm.mt_lead_lost')
return super(Lead, self)._track_subtype(init_values)
- def _notify_get_groups(self, msg_vals=None):
+ def _notify_get_recipients_groups(self, msg_vals=None):
""" Handle salesman recipients that can convert leads into opportunities
and set opportunities as won / lost. """
- groups = super(Lead, self)._notify_get_groups(msg_vals=msg_vals)
+ groups = super(Lead, self)._notify_get_recipients_groups(msg_vals=msg_vals)
local_msg_vals = dict(msg_vals or {})
self.ensure_one()
@@ -1777,19 +1777,20 @@ class Lead(models.Model):
salesman_group_id = self.env.ref('sales_team.group_sale_salesman').id
new_group = (
- 'group_sale_salesman', lambda pdata: pdata['type'] == 'user' and salesman_group_id in pdata['groups'], {
- 'actions': salesman_actions,
- })
+ 'group_sale_salesman',
+ lambda pdata: pdata['type'] == 'user' and salesman_group_id in pdata['groups'],
+ {'actions': salesman_actions}
+ )
return [new_group] + groups
- def _notify_get_reply_to(self, default=None, records=None, company=None, doc_names=None):
+ def _notify_get_reply_to(self, default=None):
""" Override to set alias of lead and opportunities to their sales team if any. """
- aliases = self.mapped('team_id').sudo()._notify_get_reply_to(default=default, records=None, company=company, doc_names=None)
+ aliases = self.mapped('team_id').sudo()._notify_get_reply_to(default=default)
res = {lead.id: aliases.get(lead.team_id.id) for lead in self}
leftover = self.filtered(lambda rec: not rec.team_id)
if leftover:
- res.update(super(Lead, leftover)._notify_get_reply_to(default=default, records=None, company=company, doc_names=doc_names))
+ res.update(super(Lead, leftover)._notify_get_reply_to(default=default))
return res
def _message_get_default_recipients(self):
diff --git a/addons/google_calendar/tests/test_sync_odoo2google.py b/addons/google_calendar/tests/test_sync_odoo2google.py
index c69dfdeac86..75374bf6412 100644
--- a/addons/google_calendar/tests/test_sync_odoo2google.py
+++ b/addons/google_calendar/tests/test_sync_odoo2google.py
@@ -70,7 +70,7 @@ class TestSyncOdoo2Google(TestSyncGoogle):
})
partner_model = self.env.ref('base.model_res_partner')
partner = self.env['res.partner'].search([], limit=1)
- with self.assertQueryCount(__system__=1211):
+ with self.assertQueryCount(__system__=1112):
events = self.env['calendar.event'].create([{
'name': "Event %s" % (i),
'start': datetime(2020, 1, 15, 8, 0),
@@ -102,7 +102,7 @@ class TestSyncOdoo2Google(TestSyncGoogle):
})
partner_model = self.env.ref('base.model_res_partner')
partner = self.env['res.partner'].search([], limit=1)
- with self.assertQueryCount(__system__=3635):
+ with self.assertQueryCount(__system__=2916):
event = self.env['calendar.event'].create({
'name': "Event",
'start': datetime(2020, 1, 15, 8, 0),
diff --git a/addons/hr_holidays/models/hr_leave.py b/addons/hr_holidays/models/hr_leave.py
index 1d38a5057bf..7d8a4b8dcfc 100644
--- a/addons/hr_holidays/models/hr_leave.py
+++ b/addons/hr_holidays/models/hr_leave.py
@@ -1430,10 +1430,10 @@ class HolidaysRequest(models.Model):
return leave_notif_subtype or self.env.ref('hr_holidays.mt_leave')
return super(HolidaysRequest, self)._track_subtype(init_values)
- def _notify_get_groups(self, msg_vals=None):
+ def _notify_get_recipients_groups(self, msg_vals=None):
""" Handle HR users and officers recipients that can validate or refuse holidays
directly from email. """
- groups = super(HolidaysRequest, self)._notify_get_groups(msg_vals=msg_vals)
+ groups = super(HolidaysRequest, self)._notify_get_recipients_groups(msg_vals=msg_vals)
local_msg_vals = dict(msg_vals or {})
self.ensure_one()
@@ -1447,9 +1447,10 @@ class HolidaysRequest(models.Model):
holiday_user_group_id = self.env.ref('hr_holidays.group_hr_holidays_user').id
new_group = (
- 'group_hr_holidays_user', lambda pdata: pdata['type'] == 'user' and holiday_user_group_id in pdata['groups'], {
- 'actions': hr_actions,
- })
+ 'group_hr_holidays_user',
+ lambda pdata: pdata['type'] == 'user' and holiday_user_group_id in pdata['groups'],
+ {'actions': hr_actions}
+ )
return [new_group] + groups
diff --git a/addons/hr_holidays/models/hr_leave_allocation.py b/addons/hr_holidays/models/hr_leave_allocation.py
index c204e89d82e..fa80f799a33 100644
--- a/addons/hr_holidays/models/hr_leave_allocation.py
+++ b/addons/hr_holidays/models/hr_leave_allocation.py
@@ -743,10 +743,10 @@ class HolidaysAllocation(models.Model):
return allocation_notif_subtype_id or self.env.ref('hr_holidays.mt_leave_allocation')
return super(HolidaysAllocation, self)._track_subtype(init_values)
- def _notify_get_groups(self, msg_vals=None):
+ def _notify_get_recipients_groups(self, msg_vals=None):
""" Handle HR users and officers recipients that can validate or refuse holidays
directly from email. """
- groups = super(HolidaysAllocation, self)._notify_get_groups(msg_vals=msg_vals)
+ groups = super(HolidaysAllocation, self)._notify_get_recipients_groups(msg_vals=msg_vals)
local_msg_vals = dict(msg_vals or {})
self.ensure_one()
@@ -760,9 +760,10 @@ class HolidaysAllocation(models.Model):
holiday_user_group_id = self.env.ref('hr_holidays.group_hr_holidays_user').id
new_group = (
- 'group_hr_holidays_user', lambda pdata: pdata['type'] == 'user' and holiday_user_group_id in pdata['groups'], {
- 'actions': hr_actions,
- })
+ 'group_hr_holidays_user',
+ lambda pdata: pdata['type'] == 'user' and holiday_user_group_id in pdata['groups'],
+ {'actions': hr_actions}
+ )
return [new_group] + groups
diff --git a/addons/hr_holidays/tests/test_company_leave.py b/addons/hr_holidays/tests/test_company_leave.py
index 2a7b78b4b67..73acca240e1 100644
--- a/addons/hr_holidays/tests/test_company_leave.py
+++ b/addons/hr_holidays/tests/test_company_leave.py
@@ -323,7 +323,7 @@ class TestCompanyLeave(TransactionCase):
})
company_leave._compute_date_from_to()
- with self.assertQueryCount(__system__=773, admin=865):
+ with self.assertQueryCount(__system__=659, admin=865):
# Original query count: 1987
# Without tracking/activity context keys: 5154
company_leave.action_validate()
diff --git a/addons/hr_recruitment/models/hr_recruitment.py b/addons/hr_recruitment/models/hr_recruitment.py
index ae3a479aeff..8dd4e0ed81a 100644
--- a/addons/hr_recruitment/models/hr_recruitment.py
+++ b/addons/hr_recruitment/models/hr_recruitment.py
@@ -500,13 +500,13 @@ class Applicant(models.Model):
return self.env.ref('hr_recruitment.mt_applicant_stage_changed')
return super(Applicant, self)._track_subtype(init_values)
- def _notify_get_reply_to(self, default=None, records=None, company=None, doc_names=None):
+ def _notify_get_reply_to(self, default=None):
""" Override to set alias of applicants to their job definition if any. """
- aliases = self.mapped('job_id')._notify_get_reply_to(default=default, records=None, company=company, doc_names=None)
+ aliases = self.mapped('job_id')._notify_get_reply_to(default=default)
res = {app.id: aliases.get(app.job_id.id) for app in self}
leftover = self.filtered(lambda rec: not rec.job_id)
if leftover:
- res.update(super(Applicant, leftover)._notify_get_reply_to(default=default, records=None, company=company, doc_names=doc_names))
+ res.update(super(Applicant, leftover)._notify_get_reply_to(default=default))
return res
def _message_get_suggested_recipients(self):
diff --git a/addons/mail/__manifest__.py b/addons/mail/__manifest__.py
index bf0987a60bc..c4b7a755e7b 100644
--- a/addons/mail/__manifest__.py
+++ b/addons/mail/__manifest__.py
@@ -2,7 +2,7 @@
{
'name': 'Discuss',
- 'version': '1.6',
+ 'version': '1.7',
'category': 'Productivity/Discuss',
'sequence': 145,
'summary': 'Chat, mail gateway and private channels',
diff --git a/addons/mail/controllers/mail.py b/addons/mail/controllers/mail.py
index 209afa1d9a6..57d842c0150 100644
--- a/addons/mail/controllers/mail.py
+++ b/addons/mail/controllers/mail.py
@@ -93,9 +93,9 @@ class MailController(http.Controller):
except AccessError:
return cls._redirect_to_messaging()
else:
- record_action = record_sudo.get_access_action(access_uid=uid)
+ record_action = record_sudo._get_access_action(access_uid=uid)
else:
- record_action = record_sudo.get_access_action()
+ record_action = record_sudo._get_access_action()
if record_action['type'] == 'ir.actions.act_url' and record_action.get('target_type') != 'public':
return cls._redirect_to_messaging()
diff --git a/addons/mail/data/mail_templates_email_layouts.xml b/addons/mail/data/mail_templates_email_layouts.xml
index 890ea6c5f25..4dba8f28efe 100644
--- a/addons/mail/data/mail_templates_email_layouts.xml
+++ b/addons/mail/data/mail_templates_email_layouts.xml
@@ -49,7 +49,7 @@
: ->
-
+
Sent
@@ -131,33 +131,39 @@
-
+
-
-
+
+
-
-
-
-
-
-
- View
-
-
+
- Your
-
-
-
+
+
+
+
+
+
+ Your
+
+
+
+
+
+
+
+ |
+
+
+
+
+
+
|
|
@@ -171,18 +177,12 @@
|
-
-
+
Best regards,
-
+
diff --git a/addons/mail/models/mail_channel.py b/addons/mail/models/mail_channel.py
index 5bb4f27cd89..3986b4d8da6 100644
--- a/addons/mail/models/mail_channel.py
+++ b/addons/mail/models/mail_channel.py
@@ -349,14 +349,14 @@ class Channel(models.Model):
new_partner_id=channel_partner.partner_id.id,
new_partner_name=channel_partner.partner_id.name,
)
- channel_partner.channel_id.message_post(body=notification, message_type="notification", subtype_xmlid="mail.mt_comment", notify_by_email=False)
+ channel_partner.channel_id.message_post(body=notification, message_type="notification", subtype_xmlid="mail.mt_comment")
members_data.append({
'id': channel_partner.partner_id.id,
'im_status': channel_partner.partner_id.im_status,
'name': channel_partner.partner_id.name,
})
for channel_partner in new_members.filtered(lambda channel_partner: channel_partner.guest_id):
- channel_partner.channel_id.message_post(body=_(' joined the channel '), message_type="notification", subtype_xmlid="mail.mt_comment", notify_by_email=False)
+ channel_partner.channel_id.message_post(body=_(' joined the channel '), message_type="notification", subtype_xmlid="mail.mt_comment")
guest_members_data.append({
'id': channel_partner.guest_id.id,
'name': channel_partner.guest_id.name,
@@ -479,13 +479,13 @@ class Channel(models.Model):
return False
return super(Channel, self)._alias_get_error_message(message, message_dict, alias)
- def _notify_compute_recipients(self, message, msg_vals):
+ def _notify_get_recipients(self, message, msg_vals):
""" Override recipients computation as channel is not a standard
mail.thread document. Indeed there are no followers on a channel.
Instead of followers it has members that should be notified.
- :param message: see ``MailThread._notify_compute_recipients()``;
- :param msg_vals: see ``MailThread._notify_compute_recipients()``;
+ :param message: see ``MailThread._notify_get_recipients()``;
+ :param msg_vals: see ``MailThread._notify_get_recipients()``;
:return recipients: structured data holding recipients data. See
``MailThread._notify_thread()`` for more details about its content
@@ -536,13 +536,13 @@ class Channel(models.Model):
return recipients_data
- def _notify_get_groups(self, msg_vals=None):
+ def _notify_get_recipients_groups(self, msg_vals=None):
""" All recipients of a message on a channel are considered as partners.
This means they will receive a minimal email, without a link to access
in the backend. Mailing lists should indeed send minimal emails to avoid
the noise. """
- groups = super(Channel, self)._notify_get_groups(msg_vals=msg_vals)
- for (index, (group_name, group_func, group_data)) in enumerate(groups):
+ groups = super(Channel, self)._notify_get_recipients_groups(msg_vals=msg_vals)
+ for (index, (group_name, _group_func, group_data)) in enumerate(groups):
if group_name != 'customer':
groups[index] = (group_name, lambda partner: False, group_data)
return groups
diff --git a/addons/mail/models/mail_followers.py b/addons/mail/models/mail_followers.py
index 15777f5977b..1f9f1473bea 100644
--- a/addons/mail/models/mail_followers.py
+++ b/addons/mail/models/mail_followers.py
@@ -83,78 +83,225 @@ class Followers(models.Model):
subtype if records and subtype are set;
* partners if pids is given;
- :param records: fetch data from followers of records that follow subtype_id;
- :param message_type: mail.message.message_type in order to allow custom behavior depending on it (SMS for example);
+ :param records: fetch data from followers of ``records`` that follow
+ ``subtype_id``;
+ :param message_type: mail.message.message_type in order to allow custom
+ behavior depending on it (SMS for example);
:param subtype_id: mail.message.subtype to check against followers;
- :param pids: additional set of partner IDs from which to fetch recipient data;
+ :param pids: additional set of partner IDs from which to fetch recipient
+ data independently from following status;
- :return: list of recipient data which is a tuple containing
- partner ID ,
- active value (always True for channels),
- share status of partner,
- notification status of partner or channel (email or inbox),
- user groups of partner,
+ :return dict: recipients data based on record.ids if given, else a generic
+ '0' key to keep a dict-like return format. Each item is a dict based on
+ recipients partner ids formatted like
+ {'active': whether partner is active;
+ 'id': res.partner ID;
+ 'is_follower': True if linked to a record and if partner is a follower;
+ 'lang': lang of the partner;
+ 'groups': groups of the partner's user. If several users exist preference
+ is given to internal user, then share users. In case of multiples
+ users of same kind groups are unioned;
+ 'notif': notification type ('inbox' or 'email'). Overrides may change
+ this value (e.g. 'sms' in sms module);
+ 'share': if partner is a customer (no user or share user);
+ 'ushare': if partner has users, whether all are shared (public or portal);
+ 'type': summary of partner 'usage' (portal, customer, internal user);
+ }
"""
self.env['mail.followers'].flush(['partner_id', 'subtype_ids'])
self.env['mail.message.subtype'].flush(['internal'])
self.env['res.users'].flush(['notification_type', 'active', 'partner_id', 'groups_id'])
self.env['res.partner'].flush(['active', 'partner_share'])
self.env['res.groups'].flush(['users'])
+ # if we have records and a subtype: we have to fetch followers
if records and subtype_id:
query = """
-SELECT DISTINCT ON (pid) * FROM (
WITH sub_followers AS (
- SELECT fol.partner_id,
- coalesce(subtype.internal, false) as internal
+ SELECT fol.partner_id AS pid,
+ fol.id AS fid,
+ fol.res_id AS res_id,
+ TRUE as is_follower,
+ COALESCE(subrel.follow, FALSE) AS subtype_follower,
+ COALESCE(subrel.internal, FALSE) AS internal
FROM mail_followers fol
- JOIN mail_followers_mail_message_subtype_rel subrel ON subrel.mail_followers_id = fol.id
- JOIN mail_message_subtype subtype ON subtype.id = subrel.mail_message_subtype_id
- WHERE subrel.mail_message_subtype_id = %s
- AND fol.res_model = %s
- AND fol.res_id IN %s
+ LEFT JOIN LATERAL (
+ SELECT TRUE AS follow,
+ subtype.internal AS internal
+ FROM mail_followers_mail_message_subtype_rel m
+ LEFT JOIN mail_message_subtype subtype ON subtype.id = m.mail_message_subtype_id
+ WHERE m.mail_followers_id = fol.id AND m.mail_message_subtype_id = %s
+ ) subrel ON TRUE
+ WHERE fol.res_model = %s
+ AND fol.res_id IN %s
UNION ALL
- SELECT id,
- FALSE
+ SELECT res_partner.id AS pid,
+ 0 AS fid,
+ 0 AS res_id,
+ FALSE as is_follower,
+ FALSE as subtype_follower,
+ FALSE as internal
FROM res_partner
- WHERE id=ANY(%s)
+ WHERE res_partner.id = ANY(%s)
)
SELECT partner.id as pid,
partner.active as active,
+ partner.lang as lang,
partner.partner_share as pshare,
- users.notification_type AS notif,
- array_agg(groups_rel.gid) AS groups
+ sub_user.uid as uid,
+ COALESCE(sub_user.share, FALSE) as ushare,
+ COALESCE(sub_user.notification_type, 'email') as notif,
+ sub_user.groups as groups,
+ sub_followers.res_id as res_id,
+ sub_followers.is_follower as _insert_followerslower
FROM res_partner partner
- LEFT JOIN res_users users ON users.partner_id = partner.id
- AND users.active
- LEFT JOIN res_groups_users_rel groups_rel ON groups_rel.uid = users.id
- JOIN sub_followers ON sub_followers.partner_id = partner.id
- AND NOT (sub_followers.internal AND partner.partner_share)
- GROUP BY partner.id,
- users.notification_type
-) AS x
-ORDER BY pid, notif
+ JOIN sub_followers ON sub_followers.pid = partner.id
+ AND (NOT sub_followers.internal OR NOT partner.partner_share)
+ LEFT JOIN LATERAL (
+ SELECT users.id AS uid,
+ users.share AS share,
+ users.notification_type AS notification_type,
+ ARRAY_AGG(groups_rel.gid) FILTER (WHERE groups_rel.gid IS NOT NULL) AS groups
+ FROM res_users users
+ LEFT JOIN res_groups_users_rel groups_rel ON groups_rel.uid = users.id
+ WHERE users.partner_id = partner.id AND users.active
+ GROUP BY users.id,
+ users.share,
+ users.notification_type
+ ORDER BY users.share ASC NULLS FIRST, users.id ASC
+ FETCH FIRST ROW ONLY
+ ) sub_user ON TRUE
+
+ WHERE sub_followers.subtype_follower OR partner.id = ANY(%s)
"""
- params = [subtype_id, records._name, tuple(records.ids), list(pids) or []]
+ params = [subtype_id, records._name, tuple(records.ids), list(pids or []), list(pids or [])]
self.env.cr.execute(query, tuple(params))
res = self.env.cr.fetchall()
- elif pids:
+ # partner_ids and records: no sub query for followers but check for follower status
+ elif pids and records:
params = []
- query_pid = """
-SELECT partner.id as pid,
-partner.active as active, partner.partner_share as pshare,
-users.notification_type AS notif, NULL AS groups
-FROM res_partner partner
-LEFT JOIN res_users users ON users.partner_id = partner.id AND users.active
-WHERE partner.id IN %s"""
- params.append(tuple(pids))
- query = 'SELECT DISTINCT ON (pid) * FROM (%s) AS x ORDER BY pid, notif' % query_pid
+ query = """
+ SELECT partner.id as pid,
+ partner.active as active,
+ partner.lang as lang,
+ partner.partner_share as pshare,
+ sub_user.uid as uid,
+ COALESCE(sub_user.share, FALSE) as ushare,
+ COALESCE(sub_user.notification_type, 'email') as notif,
+ sub_user.groups as groups,
+ ARRAY_AGG(fol.res_id) FILTER (WHERE fol.res_id IS NOT NULL) AS res_ids
+ FROM res_partner partner
+ LEFT JOIN mail_followers fol ON fol.partner_id = partner.id
+ AND fol.res_model = %s
+ AND fol.res_id IN %s
+ LEFT JOIN LATERAL (
+ SELECT users.id AS uid,
+ users.share AS share,
+ users.notification_type AS notification_type,
+ ARRAY_AGG(groups_rel.gid) FILTER (WHERE groups_rel.gid IS NOT NULL) AS groups
+ FROM res_users users
+ LEFT JOIN res_groups_users_rel groups_rel ON groups_rel.uid = users.id
+ WHERE users.partner_id = partner.id AND users.active
+ GROUP BY users.id,
+ users.share,
+ users.notification_type
+ ORDER BY users.share ASC NULLS FIRST, users.id ASC
+ FETCH FIRST ROW ONLY
+ ) sub_user ON TRUE
+
+ WHERE partner.id IN %s
+ GROUP BY partner.id,
+ sub_user.uid,
+ sub_user.share,
+ sub_user.notification_type,
+ sub_user.groups
+"""
+ params = [records._name, tuple(records.ids), tuple(pids)]
+ self.env.cr.execute(query, tuple(params))
+ simplified_res = self.env.cr.fetchall()
+ # simplified query contains res_ids -> flatten it by making it a list
+ # with res_id and add follower status
+ res = []
+ for item in simplified_res:
+ res_ids = item[-1]
+ if not res_ids: # keep res_ids Falsy (global), set as not follower
+ flattened = [list(item) + [False]]
+ else: # generate an entry for each res_id with partner being follower
+ flattened = [list(item[:-1]) + [res_id, True]
+ for res_id in res_ids]
+ res += flattened
+ # only partner ids: no follower status involved, fetch only direct recipients information
+ elif pids:
+ query = """
+ SELECT partner.id as pid,
+ partner.active as active,
+ partner.lang as lang,
+ partner.partner_share as pshare,
+ sub_user.uid as uid,
+ COALESCE(sub_user.share, FALSE) as ushare,
+ COALESCE(sub_user.notification_type, 'email') as notif,
+ sub_user.groups as groups,
+ 0 as res_id,
+ FALSE as is_follower
+ FROM res_partner partner
+ LEFT JOIN LATERAL (
+ SELECT users.id AS uid,
+ users.share AS share,
+ users.notification_type AS notification_type,
+ ARRAY_AGG(groups_rel.gid) FILTER (WHERE groups_rel.gid IS NOT NULL) AS groups
+ FROM res_users users
+ LEFT JOIN res_groups_users_rel groups_rel ON groups_rel.uid = users.id
+ WHERE users.partner_id = partner.id AND users.active
+ GROUP BY users.id,
+ users.share,
+ users.notification_type
+ ORDER BY users.share ASC NULLS FIRST, users.id ASC
+ FETCH FIRST ROW ONLY
+ ) sub_user ON TRUE
+
+ WHERE partner.id IN %s
+ GROUP BY partner.id,
+ sub_user.uid,
+ sub_user.share,
+ sub_user.notification_type,
+ sub_user.groups
+"""
+ params = [tuple(pids)]
self.env.cr.execute(query, tuple(params))
res = self.env.cr.fetchall()
else:
res = []
- return res
+
+ res_ids = records.ids if records else [0]
+ doc_infos = dict((res_id, {}) for res_id in res_ids)
+ for (partner_id, is_active, lang, pshare, uid, ushare, notif, groups, res_id, is_follower) in res:
+ to_update = [res_id] if res_id else res_ids
+ for res_id_to_update in to_update:
+ # avoid updating already existing information, unnecessary dict update
+ if not res_id and partner_id in doc_infos[res_id_to_update]:
+ continue
+ follower_data = {
+ 'active': is_active,
+ 'id': partner_id,
+ 'is_follower': is_follower,
+ 'lang': lang,
+ 'groups': set(groups or []),
+ 'notif': notif,
+ 'share': pshare,
+ 'uid': uid,
+ 'ushare': ushare,
+ }
+ # additional information
+ if follower_data['ushare']: # any type of share user
+ follower_data['type'] = 'portal'
+ elif follower_data['share']: # no user, is share -> customer (partner only)
+ follower_data['type'] = 'customer'
+ else: # has a user not share -> internal user
+ follower_data['type'] = 'user'
+ doc_infos[res_id_to_update][partner_id] = follower_data
+
+ return doc_infos
def _get_subscription_data(self, doc_data, pids, include_pshare=False, include_active=False):
""" Private method allowing to fetch follower data from several documents of a given model.
diff --git a/addons/mail/models/mail_message.py b/addons/mail/models/mail_message.py
index bc748045b83..fb30d12fd9e 100644
--- a/addons/mail/models/mail_message.py
+++ b/addons/mail/models/mail_message.py
@@ -166,7 +166,7 @@ class Message(models.Model):
mail_server_id = fields.Many2one('ir.mail_server', 'Outgoing mail server')
# keep notification layout informations to be able to generate mail again
email_layout_xmlid = fields.Char('Layout', copy=False) # xml id of layout
- add_sign = fields.Boolean(default=True)
+ email_add_signature = fields.Boolean(default=True)
# `test_adv_activity`, `test_adv_activity_full`, `test_message_assignation_inbox`,...
# By setting an inverse for mail.mail_message_id, the number of SQL queries done by `modified` is reduced.
# 'mail.mail' inherits from `mail.message`: `_inherits = {'mail.message': 'mail_message_id'}`
diff --git a/addons/mail/models/mail_notification.py b/addons/mail/models/mail_notification.py
index 1ee834b21a9..fcd5834c56d 100644
--- a/addons/mail/models/mail_notification.py
+++ b/addons/mail/models/mail_notification.py
@@ -44,7 +44,7 @@ class MailNotification(models.Model):
failure_reason = fields.Text('Failure reason', copy=False)
_sql_constraints = [
- # email notification;: partner is required
+ # email notification: partner is required
('notification_partner_required',
"CHECK(notification_type NOT IN ('email', 'inbox') OR res_partner_id IS NOT NULL)",
'Customer is required for inbox / email notification'),
@@ -59,6 +59,11 @@ class MailNotification(models.Model):
CREATE INDEX IF NOT EXISTS mail_notification_res_partner_id_is_read_notification_status_mail_message_id
ON mail_notification (res_partner_id, is_read, notification_status, mail_message_id)
""")
+ self.env.cr.execute(
+ """CREATE UNIQUE INDEX IF NOT EXISTS unique_mail_message_id_res_partner_id_if_set
+ ON %s (mail_message_id, res_partner_id)
+ WHERE res_partner_id IS NOT NULL""" % self._table
+ )
@api.model_create_multi
def create(self, vals_list):
diff --git a/addons/mail/models/mail_template.py b/addons/mail/models/mail_template.py
index e831054da18..02887737cae 100644
--- a/addons/mail/models/mail_template.py
+++ b/addons/mail/models/mail_template.py
@@ -7,6 +7,7 @@ import logging
from odoo import _, api, fields, models, tools, Command
from odoo.exceptions import UserError
+from odoo.tools import is_html_empty
_logger = logging.getLogger(__name__)
@@ -301,10 +302,21 @@ class MailTemplate(models.Model):
model = model.with_context(lang=lang)
template_ctx = {
+ # message
'message': self.env['mail.message'].sudo().new(dict(body=values['body_html'], record_name=record.display_name)),
+ 'subtype': self.env['mail.message.subtype'].sudo(),
+ # record
'model_description': model.display_name,
- 'company': 'company_id' in record and record['company_id'] or self.env.company,
'record': record,
+ 'record_name': False,
+ 'subtitle': False,
+ # user / environment
+ 'company': 'company_id' in record and record['company_id'] or self.env.company,
+ 'email_add_signature': False,
+ 'signature': '',
+ 'website_url': '',
+ # tools
+ 'is_html_empty': is_html_empty,
}
body = template._render(template_ctx, engine='ir.qweb', minimal_qcontext=True)
values['body_html'] = self.env['mail.render.mixin']._replace_local_links(body)
diff --git a/addons/mail/models/mail_thread.py b/addons/mail/models/mail_thread.py
index bd2ac656927..59c72e55bf5 100644
--- a/addons/mail/models/mail_thread.py
+++ b/addons/mail/models/mail_thread.py
@@ -26,7 +26,7 @@ from xmlrpc import client as xmlrpclib
from odoo import _, api, exceptions, fields, models, tools, registry, SUPERUSER_ID, Command
from odoo.exceptions import MissingError
from odoo.osv import expression
-
+from odoo.tools import is_html_empty
from odoo.tools.misc import clean_context, split_every
_logger = logging.getLogger(__name__)
@@ -1638,18 +1638,28 @@ class MailThread(models.AbstractModel):
]).write({'author_id': partner.id})
return result
- # ------------------------------------------------------
+ # ------------------------------------------------------------
# MESSAGE POST MAIN
- # ------------------------------------------------------
+ # ------------------------------------------------------------
def _message_post_process_attachments(self, attachments, attachment_ids, message_values):
""" Preprocess attachments for mail_thread.message_post() or mail_mail.create().
+ Purpose is to
- :param list attachments: list of attachment tuples in the form ``(name,content)``, #todo xdo update that
- where content is NOT base64 encoded
- :param list attachment_ids: a list of attachment ids, not in tomany command form
- :param dict message_data: model: the model of the attachments parent record,
- res_id: the id of the attachments parent record
+ * transfer attachments given by ``attachment_ids`` from the composer to
+ the record (if any);
+ * limit attachments manipulation when being a shared user;
+ * create attachments from ``attachments``. If those are linked to the
+ content (body) through CIDs body is updated accordingly;
+
+ :param list(tuple(str,str), tuple(str,str, dict)) attachments : list of attachment
+ tuples in the form ``(name,content)`` or ``(name,content, info)`` where content
+ is NOT base64 encoded;
+ :param list attachment_ids: list of existing attachments to link to this message;
+ :param message_values: dictionary of values that will be used to create the
+ message. It is used to find back record- or content- context;
+
+ :return dict: new values for message: 'attachment_ids' and optionally 'body'
"""
return_values = {}
body = message_values.get('body')
@@ -1703,7 +1713,7 @@ class MailThread(models.AbstractModel):
content = content.as_bytes()
elif content is None:
continue
- attachement_values= {
+ attachement_values = {
'name': name,
'datas': base64.b64encode(content),
'type': 'binary',
@@ -1753,29 +1763,37 @@ class MailThread(models.AbstractModel):
email_from=None, author_id=None, parent_id=False,
subtype_xmlid=None, subtype_id=False, partner_ids=None,
attachments=None, attachment_ids=None,
- add_sign=True, record_name=False,
**kwargs):
- """ Post a new message in an existing thread, returning the new
- mail.message ID.
- :param str body: body of the message, usually raw HTML that will
- be sanitized
- :param str subject: subject of the message
- :param str message_type: see mail_message.message_type field. Can be anything but
- user_notification, reserved for message_notify
- :param int parent_id: handle thread formation
- :param int subtype_id: subtype_id of the message, used mainly use for
- followers notification mechanism;
- :param list(int) partner_ids: partner_ids to notify in addition to partners
- computed based on subtype / followers matching;
- :param list(tuple(str,str), tuple(str,str, dict) or int) attachments : list of attachment tuples in the form
- ``(name,content)`` or ``(name,content, info)``, where content is NOT base64 encoded
- :param list id attachment_ids: list of existing attachement to link to this message
- -Should only be setted by chatter
- -Attachement object attached to mail.compose.message(0) will be attached
- to the related document.
- Extra keyword arguments will be used as default column values for the
- new mail.message record.
- :return int: ID of newly created mail.message
+ """ Post a new message in an existing thread, returning the new mail.message.
+
+ :param str body: body of the message, usually raw HTML that will
+ be sanitized
+ :param str subject: subject of the message
+ :param str message_type: see mail_message.message_type field. Can be anything but
+ user_notification, reserved for message_notify
+ :param str email_from: from address of the author. See ``_message_compute_author``
+ that uses it to make email_from / author_id coherent;
+ :param int author_id: optional ID of partner record being the author. See
+ ``_message_compute_author`` that uses it to make email_from / author_id coherent;
+ :param int parent_id: handle thread formation
+ :param int subtype_id: subtype_id of the message, used mainly for followers
+ notification mechanism;
+ :param list(int) partner_ids: partner_ids to notify in addition to partners
+ computed based on subtype / followers matching;
+ :param list(tuple(str,str), tuple(str,str, dict)) attachments : list of attachment
+ tuples in the form ``(name,content)`` or ``(name,content, info)`` where content
+ is NOT base64 encoded;
+ :param list attachment_ids: list of existing attachments to link to this message
+ -Should only be set by chatter
+ -Attachment object attached to mail.compose.message(0) will be attached
+ to the related document.
+
+ Extra keyword arguments will be used either
+ * as default column values for the new mail.message record if they match
+ mail.message fields;
+ * propagated to notification methods;
+
+ :return record: newly create mail.message
"""
self.ensure_one() # should always be posted on a record, use message_notify if no record
# split message additional values from notify additional values
@@ -1795,12 +1813,11 @@ class MailThread(models.AbstractModel):
if any(not isinstance(pc_id, int) for pc_id in partner_ids):
raise ValueError(_('message_post partner_ids and must be integer list, not commands.'))
- self = self._fallback_lang() # add lang to context imediatly since it will be usefull in various flows latter.
+ self = self._fallback_lang() # add lang to context immediately since it will be useful in various flows latter.
# Explicit access rights check, because display_name is computed as sudo.
self.check_access_rights('read')
self.check_access_rule('read')
- record_name = record_name or self.display_name
# Find the message's author
if self.env.user._is_public() and 'guest' in self.env.context:
@@ -1821,38 +1838,43 @@ class MailThread(models.AbstractModel):
parent_id = self._message_compute_parent_id(parent_id)
- values = dict(msg_kwargs)
- values.update({
+ msg_values = dict(msg_kwargs)
+ if 'email_add_signature' not in msg_values:
+ msg_values['email_add_signature'] = True
+ if not msg_values.get('record_name'):
+ msg_values['record_name'] = self.display_name
+ msg_values.update({
'author_id': author_id,
'author_guest_id': author_guest_id,
'email_from': email_from,
'model': self._name,
'res_id': self.id,
+ # content
'body': body,
'subject': subject or False,
'message_type': message_type,
'parent_id': parent_id,
'subtype_id': subtype_id,
+ # recipients
'partner_ids': partner_ids,
- 'add_sign': add_sign,
- 'record_name': record_name,
})
+
attachments = attachments or []
attachment_ids = attachment_ids or []
- attachement_values = self._message_post_process_attachments(attachments, attachment_ids, values)
- values.update(attachement_values) # attachement_ids, [body]
+ attachement_values = self._message_post_process_attachments(attachments, attachment_ids, msg_values)
+ msg_values.update(attachement_values) # attachement_ids, [body]
- new_message = self._message_create(values)
+ new_message = self._message_create(msg_values)
# Set main attachment field if necessary
- self._message_set_main_attachment_id(values['attachment_ids'])
+ self._message_set_main_attachment_id(msg_values['attachment_ids'])
- if values['author_id'] and values['message_type'] != 'notification' and not self._context.get('mail_create_nosubscribe'):
- if self.env['res.partner'].browse(values['author_id']).active: # we dont want to add odoobot/inactive as a follower
- self._message_subscribe(partner_ids=[values['author_id']])
+ if msg_values['author_id'] and msg_values['message_type'] != 'notification' and not self._context.get('mail_create_nosubscribe'):
+ if self.env['res.partner'].browse(msg_values['author_id']).active: # we dont want to add odoobot/inactive as a follower
+ self._message_subscribe(partner_ids=[msg_values['author_id']])
- self._message_post_after_hook(new_message, values)
- self._notify_thread(new_message, values, **notif_kwargs)
+ self._message_post_after_hook(new_message, msg_values)
+ self._notify_thread(new_message, msg_values, **notif_kwargs)
return new_message
def _message_set_main_attachment_id(self, attachment_ids): # todo move this out of mail.thread
@@ -1868,9 +1890,9 @@ class MailThread(models.AbstractModel):
message and computed value are given, to try to lessen query count by
using already-computed values instead of having to rebrowse things. """
- # ------------------------------------------------------
+ # ------------------------------------------------------------
# MESSAGE POST API / WRAPPERS
- # ------------------------------------------------------
+ # ------------------------------------------------------------
def _message_compose_with_view(self, views_or_xmlid, message_log=False, **kwargs):
""" Helper method to send a mail / post a message / log a note using
@@ -1957,7 +1979,7 @@ class MailThread(models.AbstractModel):
res_id = False
MailThread = self.env['mail.thread']
- values = {
+ msg_values = {
'parent_id': parent_id,
'model': self._name if self else model,
'res_id': self.id if self else res_id,
@@ -1970,12 +1992,15 @@ class MailThread(models.AbstractModel):
'subtype_id': self.env['ir.model.data']._xmlid_to_res_id('mail.mt_note'),
'is_internal': True,
'record_name': False,
- 'reply_to': MailThread._notify_get_reply_to(default=email_from, records=None)[False],
+ 'reply_to': MailThread._notify_get_reply_to(default=email_from)[False],
'message_id': tools.generate_tracking_message_id('message-notify'),
}
- values.update(msg_kwargs)
- new_message = MailThread._message_create(values)
- MailThread._notify_thread(new_message, values, **notif_kwargs)
+ msg_values.update(msg_kwargs)
+ if 'email_add_signature' not in msg_values:
+ msg_values['email_add_signature'] = True
+
+ new_message = MailThread._message_create(msg_values)
+ MailThread._notify_thread(new_message, msg_values, **notif_kwargs)
return new_message
def _message_log_with_view(self, views_or_xmlid, **kwargs):
@@ -1992,7 +2017,7 @@ class MailThread(models.AbstractModel):
self.ensure_one()
author_id, email_from = self._message_compute_author(author_id, email_from, raise_exception=False)
- message_values = {
+ msg_values = {
'subject': subject,
'body': body,
'author_id': author_id,
@@ -2003,11 +2028,12 @@ class MailThread(models.AbstractModel):
'subtype_id': self.env['ir.model.data']._xmlid_to_res_id('mail.mt_note'),
'is_internal': True,
'record_name': False,
- 'reply_to': self.env['mail.thread']._notify_get_reply_to(default=email_from, records=None)[False],
+ 'reply_to': self.env['mail.thread']._notify_get_reply_to(default=email_from)[False],
'message_id': tools.generate_tracking_message_id('message-notify'), # why? this is all but a notify
+ 'email_add_signature': False, # False as no notification -> no need to compute signature
}
- message_values.update(kwargs)
- return self.sudo()._message_create(message_values)
+ msg_values.update(kwargs)
+ return self.sudo()._message_create(msg_values)
def _message_log_batch(self, bodies, author_id=None, email_from=None, subject=False, message_type='notification'):
""" Shortcut allowing to post notes on a batch of documents. It achieve the
@@ -2026,8 +2052,9 @@ class MailThread(models.AbstractModel):
'subtype_id': self.env['ir.model.data']._xmlid_to_res_id('mail.mt_note'),
'is_internal': True,
'record_name': False,
- 'reply_to': self.env['mail.thread']._notify_get_reply_to(default=email_from, records=None)[False],
+ 'reply_to': self.env['mail.thread']._notify_get_reply_to(default=email_from)[False],
'message_id': tools.generate_tracking_message_id('message-notify'), # why? this is all but a notify
+ 'email_add_signature': False,
}
values_list = [dict(base_message_values,
res_id=record.id,
@@ -2035,6 +2062,10 @@ class MailThread(models.AbstractModel):
for record in self]
return self.sudo()._message_create(values_list)
+ # ------------------------------------------------------------
+ # MAIL.MESSAGE HELPERS
+ # ------------------------------------------------------------
+
def _message_compute_author(self, author_id=None, email_from=None, raise_exception=True):
""" Tool method computing author information for messages. Purpose is
to ensure maximum coherence between author / current user / email_from
@@ -2097,43 +2128,60 @@ class MailThread(models.AbstractModel):
# NOTIFICATION API
# ------------------------------------------------------
- def _notify_thread(self, message, msg_vals=False, notify_by_email=True, **kwargs):
+ def _notify_thread(self, message, msg_vals=False, **kwargs):
""" Main notification method. This method basically does two things
- * call ``_notify_compute_recipients`` that computes recipients to
+ * call ``_notify_get_recipients`` that computes recipients to
notify based on message record or message creation values if given
(to optimize performance if we already have data computed);
* performs the notification process by calling the various notification
methods implemented;
- :param message: mail.message record to notify;
- :param msg_vals: dictionary of values used to create the message. If given
- it is used instead of accessing ``self`` to lessen query count in some
- simple cases where no notification is actually required;
+ :param message: ``mail.message`` record to notify;
+ :param msg_vals: dictionary of values used to create the message. If given it
+ may be used to access values related to ``message`` without accessing it
+ directly. It lessens query count in some optimized use cases by avoiding
+ access message content in db;
Kwargs allow to pass various parameters that are given to sub notification
methods. See those methods for more details about the additional parameters.
- Parameters used for email-style notifications
+
+ :return: recipients data (see ``MailThread._notify_get_recipients()``)
"""
+ # add lang to context immediately since it will be useful in various rendering later
+ self = self._fallback_lang()
+
msg_vals = msg_vals if msg_vals else {}
- rdata = self._notify_compute_recipients(message, msg_vals)
- if not rdata:
- return rdata
+ recipients_data = self._notify_get_recipients(message, msg_vals)
+ if not recipients_data:
+ return recipients_data
- self._notify_record_by_inbox(message, rdata, msg_vals=msg_vals, **kwargs)
- if notify_by_email:
- self._notify_record_by_email(message, rdata, msg_vals=msg_vals, **kwargs)
+ self._notify_thread_by_inbox(message, recipients_data, msg_vals=msg_vals, **kwargs)
+ self._notify_thread_by_email(message, recipients_data, msg_vals=msg_vals, **kwargs)
- return rdata
+ return recipients_data
- def _notify_record_by_inbox(self, message, recipients_data, msg_vals=False, **kwargs):
- """ Notification method: inbox. Do two main things
+ def _notify_thread_by_inbox(self, message, recipients_data, msg_vals=False, **kwargs):
+ """ Notification method: inbox. Does two main things :
- * create an inbox notification for users;
+ * create inbox notifications for users;
* send bus notifications;
- TDE/XDO TODO: flag rdata directly, with for example r['notif'] = 'ocn_client' and r['needaction']=False
- and correctly override notify_recipients
+ :param message: ``mail.message`` record to notify;
+ :param recipients_data: list of recipients information (based on res.partner
+ records), formatted like
+ [{'active': partner.active;
+ 'id': id of the res.partner being recipient to notify;
+ 'groups': res.group IDs if linked to a user;
+ 'notif': 'inbox', 'email', 'sms' (SMS App);
+ 'share': partner.partner_share;
+ 'type': 'customer', 'portal', 'user;'
+ }, {...}].
+ See ``MailThread._notify_get_recipients``;
+ :param msg_vals: dictionary of values used to create the message. If given it
+ may be used to access values related to ``message`` without accessing it
+ directly. It lessens query count in some optimized use cases by avoiding
+ access message content in db;
"""
bus_notifications = []
inbox_pids = [r['id'] for r in recipients_data if r['notif'] == 'inbox']
@@ -2151,22 +2199,38 @@ class MailThread(models.AbstractModel):
bus_notifications.append((self.env['res.partner'].browse(partner_id), 'mail.message/inbox', dict(message_format_values)))
self.env['bus.bus'].sudo()._sendmany(bus_notifications)
- def _notify_record_by_email(self, message, recipients_data, msg_vals=False,
- model_description=False, mail_auto_delete=True, check_existing=False,
- force_send=True, send_after_commit=True,
+ def _notify_thread_by_email(self, message, recipients_data, msg_vals=False,
+ mail_auto_delete=True, # mail.mail
+ model_description=False, force_email_company=False, force_email_lang=False, # rendering
+ check_existing=False, force_send=True, send_after_commit=True, # email send
**kwargs):
""" Method to send email linked to notified messages.
- :param message: mail.message record to notify;
- :param recipients_data: see ``_notify_thread``;
- :param msg_vals: see ``_notify_thread``;
+ :param message: ``mail.message`` record to notify;
+ :param recipients_data: list of recipients information (based on res.partner
+ records), formatted like
+ [{'active': partner.active;
+ 'id': id of the res.partner being recipient to notify;
+ 'groups': res.group IDs if linked to a user;
+ 'notif': 'inbox', 'email', 'sms' (SMS App);
+ 'share': partner.partner_share;
+ 'type': 'customer', 'portal', 'user;'
+ }, {...}].
+ See ``MailThread._notify_get_recipients``;
+ :param msg_vals: dictionary of values used to create the message. If given it
+ may be used to access values related to ``message`` without accessing it
+ directly. It lessens query count in some optimized use cases by avoiding
+ access message content in db;
+
+ :param mail_auto_delete: delete notification emails once sent;
:param model_description: model description used in email notification process
(computed if not given);
- :param mail_auto_delete: delete notification emails once sent;
+ :param force_email_company: see ``_notify_by_email_prepare_rendering_context``;
+ :param force_email_lang: see ``_notify_by_email_prepare_rendering_context``;
+
:param check_existing: check for existing notifications to update based on
mailed recipient, otherwise create new notifications;
-
:param force_send: send emails directly instead of using queue;
:param send_after_commit: if force_send, tells whether to send emails after
the transaction has been committed using a post-commit hook;
@@ -2176,14 +2240,18 @@ class MailThread(models.AbstractModel):
return True
model = msg_vals.get('model') if msg_vals else message.model
- model_name = model_description or (self._fallback_lang().env['ir.model']._get(model).display_name if model else False) # one query for display name
- recipients_groups_data = self._notify_classify_recipients(partners_data, model_name, msg_vals=msg_vals)
+ model_name = model_description or (self.env['ir.model']._get(model).display_name if model else False) # one query for display name
+ recipients_groups_data = self._notify_get_recipients_classify(partners_data, model_name, msg_vals=msg_vals)
if not recipients_groups_data:
return True
force_send = self.env.context.get('mail_notify_force_send', force_send)
- template_values = self._notify_prepare_template_context(message, msg_vals, model_description=model_description) # 10 queries
+ template_values = self._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,
+ ) # 10 queries
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.message_notification_email'
@@ -2193,19 +2261,7 @@ class MailThread(models.AbstractModel):
_logger.warning('QWeb template %s not found when sending notification emails. Sending without layouting.' % (template_xmlid))
base_template = False
- mail_subject = message.subject or (message.record_name and 'Re: %s' % message.record_name) # in cache, no queries
- # Replace new lines by spaces to conform to email headers requirements
- mail_subject = ' '.join((mail_subject or '').splitlines())
- # prepare notification mail values
- base_mail_values = {
- 'mail_message_id': message.id,
- 'mail_server_id': message.mail_server_id.id, # 2 query, check acces + read, may be useless, Falsy, when will it be used?
- 'auto_delete': mail_auto_delete,
- # due to ir.rule, user have no right to access parent message if message is not published
- 'references': message.parent_id.sudo().message_id if message.parent_id else False,
- 'subject': mail_subject,
- }
- base_mail_values = self._notify_by_email_add_values(base_mail_values)
+ base_mail_values = self._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.
@@ -2235,22 +2291,15 @@ class MailThread(models.AbstractModel):
# create email
for recipients_ids_chunk in split_every(recipients_max, recipients_ids):
- recipient_values = self._notify_email_recipient_values(recipients_ids_chunk)
- email_to = recipient_values['email_to']
- recipient_ids = recipient_values['recipient_ids']
+ mail_values = self._notify_by_email_get_final_mail_values(
+ recipients_ids_chunk,
+ base_mail_values,
+ additional_values={'body_html': mail_body}
+ )
+ new_email = SafeMail.create(mail_values)
- create_values = {
- 'body_html': mail_body,
- 'subject': mail_subject,
- 'recipient_ids': [Command.link(pid) for pid in recipient_ids],
- }
- if email_to:
- create_values['email_to'] = email_to
- create_values.update(base_mail_values) # mail_message_id, mail_server_id, auto_delete, references, headers
- email = SafeMail.create(create_values)
-
- if email and recipient_ids:
- tocreate_recipient_ids = list(recipient_ids)
+ if new_email and recipients_ids_chunk:
+ tocreate_recipient_ids = list(recipients_ids_chunk)
if check_existing:
existing_notifications = self.env['mail.notification'].sudo().search([
('mail_message_id', '=', message.id),
@@ -2258,20 +2307,20 @@ class MailThread(models.AbstractModel):
('res_partner_id', 'in', tocreate_recipient_ids)
])
if existing_notifications:
- tocreate_recipient_ids = [rid for rid in recipient_ids if rid not in existing_notifications.mapped('res_partner_id.id')]
+ tocreate_recipient_ids = [rid for rid in recipients_ids_chunk if rid not in existing_notifications.mapped('res_partner_id.id')]
existing_notifications.write({
'notification_status': 'ready',
- 'mail_mail_id': email.id,
+ 'mail_mail_id': new_email.id,
})
notif_create_values += [{
'mail_message_id': message.id,
'res_partner_id': recipient_id,
'notification_type': 'email',
- 'mail_mail_id': email.id,
+ 'mail_mail_id': new_email.id,
'is_read': True, # discard Inbox notification
'notification_status': 'ready',
} for recipient_id in tocreate_recipient_ids]
- emails |= email
+ emails |= new_email
if notif_create_values:
SafeNotification.create(notif_create_values)
@@ -2301,75 +2350,114 @@ class MailThread(models.AbstractModel):
return True
- @api.model
- def _notify_prepare_template_context(self, message, msg_vals, model_description=False, mail_auto_delete=True):
- # compute send user and its related signature
+ def _notify_by_email_prepare_rendering_context(self, message, msg_vals=False, model_description=False,
+ force_email_company=False, force_email_lang=False):
+ """ Prepare rendering context for notification email.
+
+ Signature: if asked a default signature is computed based on author. Either
+ it has an user and we use the user's signature. Either we do not find any
+ user and we compute a default one based on the author's name.
+
+ Company: either there is one defined on the record (company_id field set
+ with a value), either we use env.company. A new parameter allows to force
+ its value.
+
+ Lang: when calling this method, ``_fallback_lang`` should already been
+ called, or a lang set in context with another way. A wild guess is done
+ based on templates to try to retrieve the recipient's language when a flow
+ like "send by email" is performed. Lang is used to try to have the
+ notification layout in the same language as the email content. A new
+ parameter allows to force its value.
+
+ :param msg_vals: dictionary of values used to create the message. If given it
+ may be used to access values related to ``message`` without accessing it
+ directly. It lessens query count in some optimized use cases by avoiding
+ access message content in db;
+ :param model_description: model description used in email notification process
+ (computed if not given);
+ :param force_email_company: res.company record used when rendering notification
+ layout. Otherwise computed based on current record;
+ :param force_email_lang: lang used when rendering content, used notably to
+ compute model name;
+ """
+ if msg_vals is False:
+ msg_vals = {}
+
+ # compute send user and its related signature; try to use self.env.user instead of browsing
+ # user_ids if he is the author will give a sudo user, improving access performances and cache usage.
signature = ''
- user = self.env.user
- author = message.env['res.partner'].browse(msg_vals.get('author_id')) if msg_vals else message.author_id
- model = msg_vals.get('model') if msg_vals else message.model
- add_sign = msg_vals.get('add_sign') if msg_vals else message.add_sign
- subtype_id = msg_vals.get('subtype_id') if msg_vals else message.subtype_id.id
- message_id = message.id
- record_name = msg_vals.get('record_name') if msg_vals else message.record_name
- author_user = user if user.partner_id == author else author.user_ids[0] if author and author.user_ids else False
- # trying to use user (self.env.user) instead of browing user_ids if he is the author will give a sudo user,
- # improving access performances and cache usage.
- if author_user:
- user = author_user
- if add_sign:
- signature = user.signature
- else:
- if add_sign:
+ email_add_signature = msg_vals.get('email_add_signature') if msg_vals and 'email_add_signature' in msg_vals else message.email_add_signature
+ if email_add_signature:
+ author = message.env['res.partner'].browse(msg_vals.get('author_id')) if 'author_id' in msg_vals else message.author_id
+ author_user = self.env.user if self.env.user.partner_id == author else author.user_ids[0] if author and author.user_ids else False
+ if author_user:
+ signature = author_user.signature
+ else:
signature = " -- %s " % author.name
- # company value should fall back on env.company if:
- # - no company_id field on record
- # - company_id field available but not set
- company = self.company_id.sudo() if self and 'company_id' in self and self.company_id else self.env.company
+ 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
if company.website:
website_url = 'http://%s' % company.website if not company.website.lower().startswith(('http:', 'https:')) else company.website
else:
website_url = False
- # Retrieve the language in which the template was rendered, in order to render the custom
- # layout in the same language.
- # TDE FIXME: this whole brol should be cleaned !
- lang = self.env.context.get('lang')
- if {'default_template_id', 'default_model', 'default_res_id'} <= self.env.context.keys():
+ # 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_id'} <= self.env.context.keys():
+ # TDE FIXME: this whole brol should be cleaned !
template = self.env['mail.template'].browse(self.env.context['default_template_id'])
if template and template.lang:
lang = template._render_lang([self.env.context['default_res_id']])[self.env.context['default_res_id']]
+ if not lang:
+ lang = self.env.context.get('lang')
- if not model_description and model:
- model_description = self.env['ir.model'].with_context(lang=lang)._get(model).display_name
+ # 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
+ record_name = msg_vals.get('record_name') if 'record_name' in msg_vals else message.record_name
+ # tracking
tracking = []
if msg_vals.get('tracking_value_ids', True) if msg_vals else bool(self): # could be tracking
for tracking_value in self.env['mail.tracking.value'].sudo().search([('mail_message_id', '=', message.id)]):
groups = tracking_value.field_groups
if not groups or self.env.is_superuser() or self.user_has_groups(groups):
tracking.append((tracking_value.field_desc,
- tracking_value.get_old_display_value()[0],
- tracking_value.get_new_display_value()[0]))
+ tracking_value.get_old_display_value()[0],
+ tracking_value.get_new_display_value()[0]))
+ subtype_id = msg_vals.get('subtype_id') if msg_vals and 'subtype_id' in msg_vals else message.subtype_id.id
is_discussion = subtype_id == self.env['ir.model.data']._xmlid_to_res_id('mail.mt_comment')
return {
+ # message
+ 'is_discussion': is_discussion,
'message': message,
- 'signature': signature,
- 'website_url': website_url,
- 'company': company,
+ 'subtype': message.subtype_id,
+ 'tracking_values': tracking,
+ # record
'model_description': model_description,
'record': self,
'record_name': record_name,
- 'tracking_values': tracking,
- 'is_discussion': is_discussion,
- 'subtype': message.subtype_id,
+ 'subtitle': False,
+ # user / environment
+ 'company': company,
+ 'email_add_signature': email_add_signature,
'lang': lang,
+ 'signature': signature,
+ 'website_url': website_url,
+ # tools
+ 'is_html_empty': is_html_empty,
}
- def _notify_by_email_add_values(self, base_mail_values):
+ def _notify_by_email_get_base_mail_values(self, message, additional_values=None):
""" Add model-specific values to the dictionary used to create the
notification email. Its base behavior is to compute model-specific
headers.
@@ -2377,14 +2465,55 @@ class MailThread(models.AbstractModel):
:param dict base_mail_values: base mail.mail values, holding message
to notify (mail_message_id and its fields), server, references, subject.
"""
- headers = self._notify_email_headers()
+ mail_subject = message.subject or (message.record_name and 'Re: %s' % message.record_name) # in cache, no queries
+ # Replace new lines by spaces to conform to email headers requirements
+ mail_subject = ' '.join((mail_subject or '').splitlines())
+ # prepare notification mail values
+ base_mail_values = {
+ 'mail_message_id': message.id,
+ 'mail_server_id': message.mail_server_id.id, # 2 query, check acces + read, may be useless, Falsy, when will it be used?
+ # due to ir.rule, user have no right to access parent message if message is not published
+ 'references': message.parent_id.sudo().message_id if message.parent_id else False,
+ 'subject': mail_subject,
+ }
+ if additional_values:
+ base_mail_values.update(additional_values)
+
+ headers = self._notify_by_email_get_headers()
if headers:
- base_mail_values['headers'] = headers
+ base_mail_values['headers'] = repr(headers)
return base_mail_values
- def _notify_compute_recipients(self, message, msg_vals):
+ def _notify_by_email_get_final_mail_values(self, recipient_ids, base_mail_values, additional_values=None):
+ """ Format email notification recipient values to store on the notification
+ mail.mail. Basic method just set the recipient partners as mail_mail
+ recipients. Override to generate other mail values like email_to or
+ email_cc.
+ :param recipient_ids: res.partner recordset to notify
+ """
+ final_mail_values = dict(base_mail_values)
+ final_mail_values['recipient_ids'] = [Command.link(pid) for pid in recipient_ids]
+ if additional_values:
+ final_mail_values.update(additional_values)
+ return final_mail_values
+
+ def _notify_get_recipients(self, message, msg_vals):
""" Compute recipients to notify based on subtype and followers. This
- method returns data structured as expected for ``_notify_recipients``. """
+ method returns data structured as expected for ``_notify_recipients``.
+
+ TDE/XDO TODO: flag rdata directly, with for example r['notif'] = 'ocn_client' and r['needaction']=False
+ and correctly override _notify_get_recipients
+
+ :return list recipients_data: this is a list of recipients information (see
+ ``MailFollowers._get_recipient_data()`` for more details) formatted like
+ [{'active': partner.active;
+ 'id': id of the res.partner;
+ 'groups': res.group IDs if linked to a user;
+ 'notif': 'inbox', 'email', 'sms' (SMS App);
+ 'share': partner.partner_share;
+ 'type': 'customer', 'portal', 'user;'
+ }, {...}]
+ """
msg_sudo = message.sudo()
# get values from msg_vals or from message if msg_vals doen't exists
pids = msg_vals.get('partner_ids', []) if msg_vals else msg_sudo.partner_ids.ids
@@ -2393,70 +2522,20 @@ class MailThread(models.AbstractModel):
# is it possible to have record but no subtype_id ?
recipients_data = []
- res = self.env['mail.followers']._get_recipient_data(self, message_type, subtype_id, pids)
+ res = self.env['mail.followers']._get_recipient_data(self, message_type, subtype_id, pids)[self.id if self else 0]
if not res:
return recipients_data
author_id = msg_vals.get('author_id') or message.author_id.id
- for pid, active, pshare, notif, groups in res:
+ for pid, pdata in res.items():
if pid and pid == author_id and not self.env.context.get('mail_notify_author'): # do not notify the author of its own messages
continue
- if pid:
- if active is False:
- continue
- pdata = {'id': pid, 'active': active, 'share': pshare, 'groups': groups or []}
- if notif == 'inbox':
- recipients_data.append(dict(pdata, notif=notif, type='user'))
- elif not pshare and notif: # has an user and is not shared, is therefore user
- recipients_data.append(dict(pdata, notif=notif, type='user'))
- elif pshare and notif: # has an user but is shared, is therefore portal
- recipients_data.append(dict(pdata, notif=notif, type='portal'))
- else: # has no user, is therefore customer
- recipients_data.append(dict(pdata, notif=notif if notif else 'email', type='customer'))
-
+ if pdata['active'] is False:
+ continue
+ recipients_data.append(pdata)
return recipients_data
- @api.model
- def _notify_encode_link(self, base_link, params):
- secret = self.env['ir.config_parameter'].sudo().get_param('database.secret')
- token = '%s?%s' % (base_link, ' '.join('%s=%s' % (key, params[key]) for key in sorted(params)))
- hm = hmac.new(secret.encode('utf-8'), token.encode('utf-8'), hashlib.sha1).hexdigest()
- return hm
-
- def _notify_get_action_link(self, link_type, **kwargs):
- """ Prepare link to an action: view document, follow document, ... """
- params = {
- 'model': kwargs.get('model', self._name),
- 'res_id': kwargs.get('res_id', self.ids and self.ids[0] or False),
- }
- # whitelist accepted parameters: action (deprecated), token (assign), access_token
- # (view), auth_signup_token and auth_login (for auth_signup support)
- params.update(dict(
- (key, value)
- for key, value in kwargs.items()
- if key in ('action', 'token', 'access_token', 'auth_signup_token', 'auth_login')
- ))
-
- if link_type in ['view', 'assign', 'follow', 'unfollow']:
- base_link = '/mail/%s' % link_type
- elif link_type == 'controller':
- controller = kwargs.get('controller')
- params.pop('model')
- base_link = '%s' % controller
- else:
- return ''
-
- if link_type not in ['view']:
- token = self._notify_encode_link(base_link, params)
- params['token'] = token
-
- link = '%s?%s' % (base_link, urls.url_encode(params))
- if self:
- link = self[0].get_base_url() + link
-
- return link
-
- def _notify_get_groups(self, msg_vals=None):
+ def _notify_get_recipients_groups(self, msg_vals=None):
""" Return groups used to classify recipients of a notification email.
Groups is a list of tuple containing of form (group_name, group_func,
group_data) where
@@ -2477,58 +2556,65 @@ class MailThread(models.AbstractModel):
* actions: list of action buttons to display in the notification email.
Each action is a dict containing url and title of the button.
Groups has a default value that you can find in mail_thread
- ``_notify_classify_recipients`` method.
+ ``_notify_get_recipients_classify`` method.
"""
return [
- (
+ [
'user',
lambda pdata: pdata['type'] == 'user',
{'has_button_access': True}
- ), (
+ ], [
'portal',
lambda pdata: pdata['type'] == 'portal',
- {'has_button_access': False}
- ), (
+ {'active': False, # activate only on demand if rights are enabled
+ 'has_button_access': False,
+ }
+ ], [
+ 'follower',
+ lambda pdata: pdata['is_follower'],
+ {'active': False, # activate only on demand if rights are enabled
+ 'has_button_access': False,
+ }
+ ], [
'customer',
lambda pdata: True,
{'has_button_access': False}
- )
+ ]
]
- def _notify_classify_recipients(self, recipient_data, model_name, msg_vals=None):
+ def _notify_get_recipients_classify(self, recipient_data, model_name, msg_vals=None):
""" Classify recipients to be notified of a message in groups to have
specific rendering depending on their group. For example users could
have access to buttons customers should not have in their emails.
- Module-specific grouping should be done by overriding ``_notify_get_groups``
+ Module-specific grouping should be done by overriding ``_notify_get_recipients_groups``
method defined here-under.
- :param recipient_data:todo xdo UPDATE ME
- return example:
- [{
- 'actions': [],
- 'button_access': {'title': 'View Simple Chatter Model',
+
+ :param recipient_data: list of recipients information (based on res.partner
+ records). See ``MailThread._notify_get_recipients()``;
+
+ :return list: list of groups formatted for notification processing like
+ [{'active': True,
+ 'actions': [],
+ 'button_access': {},
+ 'has_button_access': False,
+ 'recipients': [11],},
+ {'active': True,
+ 'actions': [],
+ 'button_access': {'title': 'View Simple Chatter Model',
'url': '/mail/view?model=mail.test.simple&res_id=1497'},
- 'has_button_access': False,
- 'recipients': [11]
- },
- {
- 'actions': [],
- 'button_access': {'title': 'View Simple Chatter Model',
- 'url': '/mail/view?model=mail.test.simple&res_id=1497'},
- 'has_button_access': False,
- 'recipients': [4, 5, 6]
- },
- {
- 'actions': [],
- 'button_access': {'title': 'View Simple Chatter Model',
+ 'has_button_access': True,
+ 'recipients': [4, 5, 6],},
+ {'active': True,
+ 'actions': [],
+ 'button_access': {'title': 'View Simple Chatter Model',
'url': '/mail/view?model=mail.test.simple&res_id=1497'},
- 'has_button_access': True,
- 'recipients': [10, 11, 12]
- }]
- only return groups with recipients
+ 'has_button_access': True,
+ 'recipients': [10, 11, 12],}
+ ]
"""
# keep a local copy of msg_vals as it may be modified to include more information about groups or links
local_msg_vals = dict(msg_vals) if msg_vals else {}
- groups = self._notify_get_groups(msg_vals=local_msg_vals)
+ groups = self._notify_get_recipients_groups(msg_vals=local_msg_vals)
access_link = self._notify_get_action_link('view', **local_msg_vals)
if model_name:
@@ -2538,40 +2624,69 @@ class MailThread(models.AbstractModel):
# fill group_data with default_values if they are not complete
for group_name, group_func, group_data in groups:
- group_data.setdefault('notification_group_name', group_name)
- group_data.setdefault('notification_is_customer', False)
+ group_data.setdefault('active', True)
+ group_data.setdefault('actions', list())
group_data.setdefault('has_button_access', True)
+ group_data.setdefault('notification_is_customer', False)
+ group_data.setdefault('notification_group_name', group_name)
+ group_data.setdefault('recipients', list())
group_button_access = group_data.setdefault('button_access', {})
group_button_access.setdefault('url', access_link)
group_button_access.setdefault('title', view_title)
- group_data.setdefault('actions', list())
- group_data.setdefault('recipients', list())
# classify recipients in each group
for recipient in recipient_data:
for group_name, group_func, group_data in groups:
- if group_func(recipient):
+ if group_data['active'] and group_func(recipient):
group_data['recipients'].append(recipient['id'])
break
- result = []
- for group_name, group_method, group_data in groups:
- if group_data['recipients']:
- result.append(group_data)
+ # filter out groups without recipients
+ return [group_data for _group_name, _group_func, group_data in groups
+ if group_data['recipients']]
- return result
+ @api.model
+ def _notify_encode_link(self, base_link, params):
+ secret = self.env['ir.config_parameter'].sudo().get_param('database.secret')
+ token = '%s?%s' % (base_link, ' '.join('%s=%s' % (key, params[key]) for key in sorted(params)))
+ hm = hmac.new(secret.encode('utf-8'), token.encode('utf-8'), hashlib.sha1).hexdigest()
+ return hm
- def _notify_email_recipient_values(self, recipient_ids):
- """ Format email notification recipient values to store on the notification
- mail.mail. Basic method just set the recipient partners as mail_mail
- recipients. Override to generate other mail values like email_to or
- email_cc.
- :param recipient_ids: res.partner recordset to notify
- """
- return {
- 'email_to': False,
- 'recipient_ids': recipient_ids,
+ def _notify_get_action_link(self, link_type, **kwargs):
+ """ Prepare link to an action: view document, follow document, ... """
+ params = {
+ 'model': kwargs.get('model', self._name),
+ 'res_id': kwargs.get('res_id', self.ids and self.ids[0] or False),
}
+ # keep only accepted parameters:
+ # - action (deprecated), token (assign), access_token (view)
+ # - auth_signup: auth_signup_token and auth_login
+ # - portal: pid, hash
+ params.update(dict(
+ (key, value)
+ for key, value in kwargs.items()
+ if key in ('action', 'token', 'access_token', 'auth_signup_token',
+ 'auth_login', 'pid', 'hash')
+ ))
+
+ if link_type in ['view', 'assign', 'follow', 'unfollow']:
+ base_link = '/mail/%s' % link_type
+ elif link_type == 'controller':
+ controller = kwargs.get('controller')
+ params.pop('model')
+ base_link = '%s' % controller
+ else:
+ return ''
+
+ if link_type not in ['view']:
+ token = self._notify_encode_link(base_link, params)
+ params['token'] = token
+
+ link = '%s?%s' % (base_link, urls.url_encode(params))
+ if self:
+ link = self[0].get_base_url() + link
+
+ return link
# ------------------------------------------------------
# FOLLOWERS API
diff --git a/addons/mail/models/models.py b/addons/mail/models/models.py
index 2e31ed89bd7..335f144bd2e 100644
--- a/addons/mail/models/models.py
+++ b/addons/mail/models/models.py
@@ -84,7 +84,7 @@ class BaseModel(models.AbstractModel):
res[record.id] = {'partner_ids': recipient_ids, 'email_to': email_to, 'email_cc': email_cc}
return res
- def _notify_get_reply_to(self, default=None, records=None, company=None, doc_names=None):
+ def _notify_get_reply_to(self, default=None):
""" Returns the preferred reply-to email address when replying to a thread
on documents. This method is a generic implementation available for
all models as we could send an email through mail templates on models
@@ -104,19 +104,9 @@ class BaseModel(models.AbstractModel):
An example would be tasks taking their reply-to alias from their project.
:param default: default email if no alias or catchall is found;
- :param records: DEPRECATED, self should be a valid record set or an
- empty recordset if a generic reply-to is required;
- :param company: used to compute company name part of the from name; provide
- it if already known, otherwise fall back on user company;
- :param doc_names: dict(res_id, doc_name) used to compute doc name part of
- the from name; provide it if already known to avoid queries, otherwise
- name_get on document will be performed;
:return result: dictionary. Keys are record IDs and value is formatted
like an email "Company_name Document_name "/
"""
- if records:
- raise ValueError('Use of records is deprecated as this method is available on BaseModel.')
-
_records = self
model = _records._name if _records and _records._name != 'mail.thread' else False
res_ids = _records.ids if _records and model else []
@@ -125,7 +115,7 @@ class BaseModel(models.AbstractModel):
alias_domain = self.env['ir.config_parameter'].sudo().get_param("mail.catchall.domain")
result = dict.fromkeys(_res_ids, False)
result_email = dict()
- doc_names = doc_names if doc_names else dict()
+ doc_names = dict()
if alias_domain:
if model and res_ids:
@@ -148,7 +138,7 @@ class BaseModel(models.AbstractModel):
result_email.update(dict((rid, '%s@%s' % (catchall, alias_domain)) for rid in left_ids))
# compute name of reply-to - TDE tocheck: quotes and stuff like that
- company_name = company.name if company else self.env.company.name
+ company_name = self.env.company.name
for res_id in result_email:
name = '%s%s%s' % (company_name, ' ' if doc_names.get(res_id) else '', doc_names.get(res_id, ''))
result[res_id] = tools.formataddr((name, result_email[res_id]))
@@ -203,16 +193,11 @@ class BaseModel(models.AbstractModel):
'&', ('hidden', '=', False),
'|', ('res_model', '=', self._name), ('res_model', '=', False)])
- def _notify_email_headers(self):
- """
- Generate the email headers based on record
- """
+ def _notify_by_email_get_headers(self):
+ """ Generate the email headers based on record """
if not self:
return {}
self.ensure_one()
- return repr(self._notify_email_header_dict())
-
- def _notify_email_header_dict(self):
return {
'X-Odoo-Objects': "%s-%s" % (self._name, self.id),
}
diff --git a/addons/mail/tests/common.py b/addons/mail/tests/common.py
index 78311e2b6b8..d8331d8de05 100644
--- a/addons/mail/tests/common.py
+++ b/addons/mail/tests/common.py
@@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
+import base64
import email
import email.policy
import time
@@ -601,21 +602,23 @@ class MailCase(MockEmail):
return cls.email_template
- def _generate_notify_recipients(self, partners):
+ def _generate_notify_recipients(self, partners, record=None):
""" Tool method to generate recipients data according to structure used
in notification methods. Purpose is to allow testing of internals of
some notification methods, notably testing links or group-based notification
details.
- See notably ``MailThread._notify_compute_recipients()``.
+ See notably ``MailThread._notify_get_recipients()``.
"""
return [
{'id': partner.id,
- 'active': True,
- 'share': partner.partner_share,
+ 'active': partner.active,
+ 'is_follower': partner in record.message_partner_ids if record else False,
'groups': partner.user_ids.groups_id.ids,
'notif': partner.user_ids.notification_type or 'email',
+ 'share': partner.partner_share,
'type': 'user' if partner.user_ids and not partner.partner_share else partner.user_ids and 'portal' or 'customer',
+ 'ushare': all(user.share for user in partner.user_ids) if partner.user_ids else False,
} for partner in partners
]
@@ -940,3 +943,168 @@ class MailCommon(common.TransactionCase, MailCase):
signature='--\nEnguerrand'
)
cls.partner_employee_c2 = cls.user_employee_c2.partner_id
+
+ @classmethod
+ def _activate_multi_lang(cls, lang_code='es_ES', layout_arch_db=None, test_record=False, test_template=False):
+ """ Summary of es_ES matching done here (a bit hardcoded to ease tests)
+
+ * layout
+ * 'English Layout for' -> Spanish Layout para
+ * model
+ * description: English: Lang Chatter Model (depends on test_record._name)
+ translated: Spanish description
+ * module
+ * _('TestStuff') -> TestSpanishStuff (used as link button name in layout)
+ * _('View %s') -> SpanishView %s
+ * template
+ * body: English: EnglishBody for (depends on test_template.body)
+ translated: SpanishBody for
+ * subject: English: EnglishSubject for {{ object.name }} (depends on test_template.subject)
+ translated: SpanishSubject for {{ object.name }}
+ """
+ # activate translations
+ cls.env['res.lang']._activate_lang(lang_code)
+ cls.env.ref('base.module_base')._update_translations([lang_code])
+
+ # Make sure Spanish translations have not been altered
+ if test_record:
+ description_translations = cls.env['ir.translation'].search([
+ ('module', '=', 'test_mail'),
+ ('src', '=', test_record._description),
+ ('lang', '=', lang_code)
+ ])
+ if description_translations:
+ description_translations.update({'value': 'Spanish description'})
+ else:
+ description_translations.create({
+ 'lang': lang_code,
+ 'module': 'test_mail',
+ 'name': 'ir.model,name',
+ 'res_id': cls.env['ir.model']._get_id(test_record._name),
+ 'src': test_record._description,
+ 'state': 'translated',
+ 'type': 'model',
+ 'value': 'Spanish description',
+ })
+
+ translations_tocreate = []
+ # Have a TestStuff always available
+ test_stuff_translations = cls.env['ir.translation'].search([
+ ('module', '=', 'test_mail'),
+ ('src', '=', 'TestStuff'),
+ ('lang', '=', lang_code)
+ ])
+ if test_stuff_translations:
+ test_stuff_translations.update({'value': 'TestSpanishStuff'})
+ else:
+ translations_tocreate.append({
+ 'lang': lang_code,
+ 'name': 'idontknow',
+ 'module': 'test_mail',
+ 'res_id': False,
+ 'src': 'TestStuff',
+ 'state': 'translated',
+ 'type': 'code',
+ 'value': 'TestSpanishStuff',
+ })
+
+ view_translations = cls.env['ir.translation'].search([
+ ('module', '=', 'mail'),
+ ('src', '=', 'View %s'),
+ ('lang', '=', lang_code)
+ ])
+ if view_translations:
+ view_translations.update({'value': 'SpanishView'})
+ else:
+ translations_tocreate.append({
+ 'lang': lang_code,
+ 'name': 'idontknow',
+ 'module': 'mail',
+ 'res_id': False,
+ 'src': 'View %s',
+ 'state': 'translated',
+ 'type': 'code',
+ 'value': 'SpanishView %s',
+ })
+
+ # Prepare some translated value for template if given
+ if test_template:
+ translations_tocreate += [{
+ 'lang': lang_code,
+ 'module': 'mail',
+ 'name': 'mail.template,subject',
+ 'res_id': test_template.id,
+ 'state': 'translated',
+ 'type': 'model',
+ 'value': 'SpanishSubject for {{ object.name }}',
+ }, {
+ 'lang': lang_code,
+ 'module': 'mail',
+ 'name': 'mail.template,body_html',
+ 'res_id': test_template.id,
+ 'state': 'translated',
+ 'type': 'model',
+ 'value': 'SpanishBody for ',
+ }]
+
+ # create a custom layout for email notification
+ if not layout_arch_db:
+ layout_arch_db = """
+
+ English Layout for
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Sent by
+"""
+ view = cls.env['ir.ui.view'].create({
+ 'arch_db': layout_arch_db,
+ 'key': 'test_layout',
+ 'name': 'test_layout',
+ 'type': 'qweb',
+ })
+ cls.env['ir.model.data'].create({
+ 'model': 'ir.ui.view',
+ 'module': 'mail',
+ 'name': 'test_layout',
+ 'res_id': view.id
+ })
+ translations_tocreate.append({
+ 'lang': lang_code,
+ 'module': 'mail',
+ 'name': 'ir.ui.view,arch_db',
+ 'res_id': view.id,
+ 'src': 'English Layout for',
+ 'state': 'translated',
+ 'type': 'model_terms',
+ 'value': 'Spanish Layout para',
+ })
+ cls.env['ir.translation'].create(translations_tocreate)
+
+ def _generate_attachments_data(self, count, res_model=None, res_id=None):
+ # attachment visibility depends on what they are attached to
+ if res_model is None:
+ res_model = self.template._name
+ if res_id is None:
+ res_id = self.template.id
+ return [{
+ 'name': '%02d.txt' % x,
+ 'datas': base64.b64encode(b'Att%02d' % x),
+ 'res_model': res_model,
+ 'res_id': res_id,
+ } for x in range(count)]
diff --git a/addons/mail/wizard/mail_compose_message.py b/addons/mail/wizard/mail_compose_message.py
index b0acedd9705..2c7499994bc 100644
--- a/addons/mail/wizard/mail_compose_message.py
+++ b/addons/mail/wizard/mail_compose_message.py
@@ -100,8 +100,7 @@ class MailComposer(models.TransientModel):
'ir.attachment', 'mail_compose_message_ir_attachments_rel',
'wizard_id', 'attachment_id', 'Attachments')
email_layout_xmlid = fields.Char('Email Notification Layout', copy=False)
- layout = fields.Char('Layout', copy=False) # xml id of layout
- add_sign = fields.Boolean(default=True)
+ email_add_signature = fields.Boolean(default=True)
# origin
email_from = fields.Char('From', help="Email address of the sender. This field is set when no matching partner is found and replaces the author_id field in the chatter.")
author_id = fields.Many2one(
@@ -252,7 +251,7 @@ class MailComposer(models.TransientModel):
# 'purchase.order' which is used for a RFQ and and PO. To avoid confusion, we must use a
# different wording depending on the state of the object.
# Therefore, we can set the description in the context from the beginning to avoid falling
- # back on the regular display_name retrieved in '_notify_prepare_template_context'.
+ # back on the regular display_name retrieved in ``_notify_by_email_prepare_rendering_context()``.
model_description = self._context.get('model_description')
for wizard in self:
@@ -311,7 +310,7 @@ class MailComposer(models.TransientModel):
message_type=wizard.message_type,
subtype_id=subtype_id,
email_layout_xmlid=wizard.email_layout_xmlid,
- add_sign=not bool(wizard.template_id),
+ email_add_signature=not bool(wizard.template_id) and wizard.email_add_signature,
mail_auto_delete=wizard.template_id.auto_delete if wizard.template_id else self._context.get('mail_auto_delete', True),
model_description=model_description)
post_params.update(mail_values)
@@ -395,7 +394,7 @@ class MailComposer(models.TransientModel):
# mass mailing: rendering override wizard static values
if mass_mail_mode and self.model:
record = self.env[self.model].browse(res_id)
- mail_values['headers'] = record._notify_email_headers()
+ mail_values['headers'] = repr(record._notify_by_email_get_headers())
# keep a copy unless specifically requested, reset record name (avoid browsing records)
mail_values.update(is_notification=not self.auto_delete_message, model=self.model, res_id=res_id, record_name=False)
# auto deletion of mail_mail
diff --git a/addons/mail/wizard/mail_resend_message.py b/addons/mail/wizard/mail_resend_message.py
index 411126f1362..d2b197048f5 100644
--- a/addons/mail/wizard/mail_resend_message.py
+++ b/addons/mail/wizard/mail_resend_message.py
@@ -68,17 +68,16 @@ class MailResendMessage(models.TransientModel):
record = self.env[message.model].browse(message.res_id) if message.is_thread_message() else self.env['mail.thread']
email_partners_data = []
- for pid, active, pshare, notif, groups in self.env['mail.followers']._get_recipient_data(None, 'comment', False, pids=to_send.ids):
- if pid and notif == 'email' or not notif:
- pdata = {'id': pid, 'share': pshare, 'active': active, 'notif': 'email', 'groups': groups or []}
- if not pshare and notif: # has an user and is not shared, is therefore user
- email_partners_data.append(dict(pdata, type='user'))
- elif pshare and notif: # has an user and is shared, is therefore portal
- email_partners_data.append(dict(pdata, type='portal'))
- else: # has no user, is therefore customer
- email_partners_data.append(dict(pdata, type='customer'))
+ recipients_data = self.env['mail.followers']._get_recipient_data(None, 'comment', False, pids=to_send.ids)[0]
+ for pid, pdata in recipients_data.items():
+ if pid and pdata.get('notif', 'email') == 'email':
+ email_partners_data.append(pdata)
- record._notify_record_by_email(message, email_partners_data, check_existing=True, send_after_commit=False)
+ record._notify_thread_by_email(
+ message, email_partners_data,
+ check_existing=True,
+ send_after_commit=False
+ )
self.mail_message_id._notify_message_notification_update()
return {'type': 'ir.actions.act_window_close'}
diff --git a/addons/mail/wizard/mail_wizard_invite.py b/addons/mail/wizard/mail_wizard_invite.py
index d1200264c59..a39da2284bc 100644
--- a/addons/mail/wizard/mail_wizard_invite.py
+++ b/addons/mail/wizard/mail_wizard_invite.py
@@ -68,20 +68,18 @@ class Invite(models.TransientModel):
'model': wizard.res_model,
'res_id': wizard.res_id,
'reply_to_force_new': True,
- 'add_sign': True,
+ 'email_add_signature': True,
})
- partners_data = []
- recipient_data = self.env['mail.followers']._get_recipient_data(document, 'comment', False, pids=new_partners.ids)
- for pid, active, pshare, notif, groups in recipient_data:
- pdata = {'id': pid, 'share': pshare, 'active': active, 'notif': 'email', 'groups': groups or []}
- if not pshare and notif: # has an user and is not shared, is therefore user
- partners_data.append(dict(pdata, type='user'))
- elif pshare and notif: # has an user and is shared, is therefore portal
- partners_data.append(dict(pdata, type='portal'))
- else: # has no user, is therefore customer
- partners_data.append(dict(pdata, type='customer'))
+ email_partners_data = []
+ recipients_data = self.env['mail.followers']._get_recipient_data(document, 'comment', False, pids=new_partners.ids)[document.id]
+ for _pid, pdata in recipients_data.items():
+ pdata['notif'] = 'email'
+ email_partners_data.append(pdata)
- document._notify_record_by_email(message, partners_data, send_after_commit=False)
+ document._notify_thread_by_email(
+ message, email_partners_data,
+ send_after_commit=False
+ )
# in case of failure, the web client must know the message was
# deleted to discard the related failure notification
self.env['bus.bus']._sendone(self.env.user.partner_id, 'mail.message/delete', {'message_ids': message.ids})
diff --git a/addons/mail_group/models/mail_group.py b/addons/mail_group/models/mail_group.py
index e1de925ba2e..7cbfa2cc1e1 100644
--- a/addons/mail_group/models/mail_group.py
+++ b/addons/mail_group/models/mail_group.py
@@ -424,7 +424,7 @@ class MailGroup(models.Model):
# SMTP headers related to the subscription
email_url_encoded = urls.url_quote(email_member)
headers = {
- ** self._notify_email_header_dict(),
+ ** self._notify_by_email_get_headers(),
'List-Archive': f'<{base_url}/groups/{slug(self)}>',
'List-Subscribe': f'<{base_url}/groups?email={email_url_encoded}>',
'List-Unsubscribe': f'<{base_url}/groups?unsubscribe&email={email_url_encoded}>',
diff --git a/addons/portal/controllers/mail.py b/addons/portal/controllers/mail.py
index af3ee0a28ce..e2bb31c961b 100644
--- a/addons/portal/controllers/mail.py
+++ b/addons/portal/controllers/mail.py
@@ -230,7 +230,7 @@ class MailController(mail.MailController):
record_sudo.with_user(uid).check_access_rule('read')
except AccessError:
if record_sudo.access_token and access_token and consteq(record_sudo.access_token, access_token):
- record_action = record_sudo.with_context(force_website=True).get_access_action()
+ record_action = record_sudo._get_access_action(force_website=True)
if record_action['type'] == 'ir.actions.act_url':
pid = kwargs.get('pid')
hash = kwargs.get('hash')
diff --git a/addons/portal/models/portal_mixin.py b/addons/portal/models/portal_mixin.py
index be4f9e762cb..cc963833ee0 100644
--- a/addons/portal/models/portal_mixin.py
+++ b/addons/portal/models/portal_mixin.py
@@ -46,10 +46,14 @@ class PortalMixin(models.AbstractModel):
:return: the url of the record with access parameters, if any.
"""
self.ensure_one()
- params = {
- 'model': self._name,
- 'res_id': self.id,
- }
+ if redirect:
+ # model / res_id used by mail/view to check access on record
+ params = {
+ 'model': self._name,
+ 'res_id': self.id,
+ }
+ else:
+ params = {}
if share_token and hasattr(self, 'access_token'):
params['access_token'] = self._portal_ensure_token()
if pid:
@@ -60,14 +64,16 @@ class PortalMixin(models.AbstractModel):
return '%s?%s' % ('/mail/view' if redirect else self.access_url, url_encode(params))
- def _notify_get_groups(self, msg_vals=None):
+ def _notify_get_recipients_groups(self, msg_vals=None):
access_token = self._portal_ensure_token()
- groups = super(PortalMixin, self)._notify_get_groups(msg_vals=msg_vals)
+ groups = super(PortalMixin, self)._notify_get_recipients_groups(msg_vals=msg_vals)
local_msg_vals = dict(msg_vals or {})
if access_token and 'partner_id' in self._fields and self['partner_id']:
customer = self['partner_id']
local_msg_vals['access_token'] = self.access_token
+ local_msg_vals['pid'] = customer.id
+ local_msg_vals['hash'] = self._sign_token(customer.id)
local_msg_vals.update(customer.signup_get_auth_param()[customer.id])
access_link = self._notify_get_action_link('view', **local_msg_vals)
@@ -82,11 +88,18 @@ class PortalMixin(models.AbstractModel):
]
else:
new_group = []
+
+ # enable portal users that should have access through portal (if not access rights
+ # will do their duty)
+ portal_group = next(group for group in groups if group[0] == 'portal')
+ portal_group[2]['active'] = True
+ portal_group[2]['has_button_access'] = True
+
return new_group + groups
- def get_access_action(self, access_uid=None):
+ def _get_access_action(self, access_uid=None, force_website=False):
""" Instead of the classic form view, redirect to the online document for
- portal users or if force_website=True in the context. """
+ portal users or if force_website=True. """
self.ensure_one()
user, record = self.env.user, self
@@ -95,15 +108,17 @@ class PortalMixin(models.AbstractModel):
record.check_access_rights('read')
record.check_access_rule("read")
except exceptions.AccessError:
- return super(PortalMixin, self).get_access_action(access_uid)
+ return super(PortalMixin, self)._get_access_action(
+ access_uid=access_uid, force_website=force_website
+ )
user = self.env['res.users'].sudo().browse(access_uid)
+ if user.share or force_website:
record = self.with_user(user)
- if user.share or self.env.context.get('force_website'):
try:
record.check_access_rights('read')
record.check_access_rule('read')
except exceptions.AccessError:
- if self.env.context.get('force_website'):
+ if force_website:
return {
'type': 'ir.actions.act_url',
'url': record.access_url,
@@ -119,7 +134,9 @@ class PortalMixin(models.AbstractModel):
'target': 'self',
'res_id': record.id,
}
- return super(PortalMixin, self).get_access_action(access_uid)
+ return super(PortalMixin, self)._get_access_action(
+ access_uid=access_uid, force_website=force_website
+ )
@api.model
def action_share(self):
diff --git a/addons/project/models/project.py b/addons/project/models/project.py
index 99e9211d9d7..8bd5d765a9c 100644
--- a/addons/project/models/project.py
+++ b/addons/project/models/project.py
@@ -1951,12 +1951,12 @@ class Task(models.Model):
res -= dependency_subtype
return res
- def _notify_get_groups(self, msg_vals=None):
+ def _notify_get_recipients_groups(self, msg_vals=None):
""" Handle project users and managers recipients that can assign
tasks and create new one directly from notification emails. Also give
access button to portal users and portal customers. If they are notified
they should probably have access to the document. """
- groups = super(Task, self)._notify_get_groups(msg_vals=msg_vals)
+ groups = super(Task, self)._notify_get_recipients_groups(msg_vals=msg_vals)
local_msg_vals = dict(msg_vals or {})
self.ensure_one()
@@ -1983,13 +1983,13 @@ class Task(models.Model):
return groups
- def _notify_get_reply_to(self, default=None, records=None, company=None, doc_names=None):
+ def _notify_get_reply_to(self, default=None):
""" Override to set alias of tasks to their project if any. """
- aliases = self.sudo().mapped('project_id')._notify_get_reply_to(default=default, records=None, company=company, doc_names=None)
+ aliases = self.sudo().mapped('project_id')._notify_get_reply_to(default=default)
res = {task.id: aliases.get(task.project_id.id) for task in self}
leftover = self.filtered(lambda rec: not rec.project_id)
if leftover:
- res.update(super(Task, leftover)._notify_get_reply_to(default=default, records=None, company=company, doc_names=doc_names))
+ res.update(super(Task, leftover)._notify_get_reply_to(default=default))
return res
def email_split(self, msg):
@@ -2044,8 +2044,8 @@ class Task(models.Model):
task._message_add_suggested_recipient(recipients, email=task.email_from, reason=_('Customer Email'))
return recipients
- def _notify_email_header_dict(self):
- headers = super(Task, self)._notify_email_header_dict()
+ def _notify_by_email_get_headers(self):
+ headers = super(Task, self)._notify_by_email_get_headers()
if self.project_id:
current_objects = [h for h in headers.get('X-Odoo-Objects', '').split(',') if h]
current_objects.insert(0, 'project.project-%s, ' % self.project_id.id)
diff --git a/addons/purchase/data/mail_template_data.xml b/addons/purchase/data/mail_template_data.xml
index bdd1a4fc9cf..31573688f43 100644
--- a/addons/purchase/data/mail_template_data.xml
+++ b/addons/purchase/data/mail_template_data.xml
@@ -23,6 +23,10 @@
If you have any questions, please do not hesitate to contact us.
Best regards,
+
+
+ -- Mitchell Admin
+
@@ -56,6 +60,11 @@
Could you please acknowledge the receipt of this order?
+
+
+ -- Mitchell Admin
+
+
@@ -90,6 +99,11 @@
undefined.
Could you please confirm it will be delivered on time?
+
+
+ -- Mitchell Admin
+
+
diff --git a/addons/purchase/data/mail_templates.xml b/addons/purchase/data/mail_templates.xml
index a26cffe37ad..33f6b254199 100644
--- a/addons/purchase/data/mail_templates.xml
+++ b/addons/purchase/data/mail_templates.xml
@@ -1,41 +1,6 @@
-
-
-
-
- Confirm
-
-
-
-
-
-
-
- Yes
-
-
- No, Update Dates
-
-
-
- If you have any questions, please do not hesitate to contact us.
-
- Best regards,
-
-
-
-
-
-
-
-
-
-
The ordered quantity has been updated.
diff --git a/addons/purchase/models/purchase.py b/addons/purchase/models/purchase.py
index 3d2dc6d67ca..73bf338b228 100644
--- a/addons/purchase/models/purchase.py
+++ b/addons/purchase/models/purchase.py
@@ -10,7 +10,7 @@ from werkzeug.urls import url_encode
from odoo import api, fields, models, _
from odoo.osv import expression
-from odoo.tools import DEFAULT_SERVER_DATETIME_FORMAT, format_amount, formatLang, get_lang, groupby
+from odoo.tools import DEFAULT_SERVER_DATETIME_FORMAT, format_amount, format_date, formatLang, get_lang, groupby
from odoo.tools.float_utils import float_compare, float_is_zero, float_round
from odoo.exceptions import AccessError, UserError, ValidationError
@@ -289,18 +289,6 @@ class PurchaseOrder(models.Model):
del line[2]['date_planned']
return result
- def _track_subtype(self, init_values):
- self.ensure_one()
- if 'state' in init_values and self.state == 'purchase':
- if init_values['state'] == 'to approve':
- return self.env.ref('purchase.mt_rfq_approved')
- return self.env.ref('purchase.mt_rfq_confirmed')
- elif 'state' in init_values and self.state == 'to approve':
- return self.env.ref('purchase.mt_rfq_confirmed')
- elif 'state' in init_values and self.state == 'done':
- return self.env.ref('purchase.mt_rfq_done')
- return super(PurchaseOrder, self)._track_subtype(init_values)
-
def _get_report_base_filename(self):
self.ensure_one()
return 'Purchase Order-%s' % (self.name)
@@ -353,6 +341,69 @@ class PurchaseOrder(models.Model):
return {'warning': warning}
return {}
+ # ------------------------------------------------------------
+ # MAIL.THREAD
+ # ------------------------------------------------------------
+
+ @api.returns('mail.message', lambda value: value.id)
+ def message_post(self, **kwargs):
+ if self.env.context.get('mark_rfq_as_sent'):
+ self.filtered(lambda o: o.state == 'draft').write({'state': 'sent'})
+ return super(PurchaseOrder, self.with_context(mail_post_autofollow=self.env.context.get('mail_post_autofollow', True))).message_post(**kwargs)
+
+ def _notify_get_recipients_groups(self, msg_vals=None):
+ """ Tweak 'view document' button for portal customers, calling directly
+ routes for confirm specific to PO model. """
+ groups = super(PurchaseOrder, self)._notify_get_recipients_groups(msg_vals=msg_vals)
+
+ customer_portal_group = next(group for group in groups if group[0] == 'portal_customer')
+ if customer_portal_group:
+ access_opt = customer_portal_group[2].setdefault('button_access', {})
+ if self.env.context.get('is_reminder'):
+ access_opt['title'] = _('View')
+ actions = customer_portal_group[2].setdefault('actions', list())
+ actions.extend([
+ {'url': self.get_confirm_url(confirm_type='reminder'), 'title': _('Accept')},
+ {'url': self.get_update_url(), 'title': _('Update Dates')},
+ ])
+ else:
+ access_opt['title'] = _('Confirm')
+ access_opt['url'] = self.get_confirm_url(confirm_type='reception')
+
+ return groups
+
+ def _notify_by_email_prepare_rendering_context(self, message, msg_vals, model_description=False,
+ force_email_company=False, force_email_lang=False):
+ render_context = super()._notify_by_email_prepare_rendering_context(
+ message, msg_vals, model_description=model_description,
+ force_email_company=force_email_company, force_email_lang=force_email_lang
+ )
+ if self.date_order:
+ amount_txt = _('%(amount)s due %(date)s',
+ amount=format_amount(self.env, self.amount_total, self.currency_id, lang_code=render_context.get('lang')),
+ date=format_date(self.env, self.date_order, date_format='short', lang_code=render_context.get('lang'))
+ )
+ else:
+ amount_txt = format_amount(self.env, self.amount_total, self.currency_id, lang_code=render_context.get('lang'))
+ render_context['subtitle'] = Markup("%s %s") % (self.name, amount_txt)
+ return render_context
+
+ def _track_subtype(self, init_values):
+ self.ensure_one()
+ if 'state' in init_values and self.state == 'purchase':
+ if init_values['state'] == 'to approve':
+ return self.env.ref('purchase.mt_rfq_approved')
+ return self.env.ref('purchase.mt_rfq_confirmed')
+ elif 'state' in init_values and self.state == 'to approve':
+ return self.env.ref('purchase.mt_rfq_confirmed')
+ elif 'state' in init_values and self.state == 'done':
+ return self.env.ref('purchase.mt_rfq_done')
+ return super(PurchaseOrder, self)._track_subtype(init_values)
+
+ # ------------------------------------------------------------
+ # ACTIONS
+ # ------------------------------------------------------------
+
def action_rfq_send(self):
'''
This function opens a window to compose an email, with the edi purchase template message loaded by default
@@ -410,12 +461,6 @@ class PurchaseOrder(models.Model):
'context': ctx,
}
- @api.returns('mail.message', lambda value: value.id)
- def message_post(self, **kwargs):
- if self.env.context.get('mark_rfq_as_sent'):
- self.filtered(lambda o: o.state == 'draft').write({'state': 'sent'})
- return super(PurchaseOrder, self.with_context(mail_post_autofollow=self.env.context.get('mail_post_autofollow', True))).message_post(**kwargs)
-
def print_quotation(self):
self.write({'state': "sent"})
return self.env.ref('purchase.report_purchase_quotation').report_action(self)
diff --git a/addons/sale/__manifest__.py b/addons/sale/__manifest__.py
index 189dc41c6c6..de75dfc1812 100644
--- a/addons/sale/__manifest__.py
+++ b/addons/sale/__manifest__.py
@@ -24,7 +24,6 @@ This module contains all the common features of Sales Management and eCommerce.
'data/ir_sequence_data.xml',
'data/mail_data_various.xml',
'data/mail_template_data.xml',
- 'data/mail_templates.xml',
'data/sale_data.xml',
'wizard/account_accrued_orders_wizard_views.xml',
diff --git a/addons/sale/data/mail_template_data.xml b/addons/sale/data/mail_template_data.xml
index 481187503a8..d84e9f8239c 100644
--- a/addons/sale/data/mail_template_data.xml
+++ b/addons/sale/data/mail_template_data.xml
@@ -30,7 +30,11 @@
Do not hesitate to contact us if you have any questions.
-
+
+
+ -- Mitchell Admin
+
+
@@ -65,6 +69,10 @@
Do not hesitate to contact us if you have any questions.
+
+
+ -- Mitchell Admin
+
diff --git a/addons/sale/data/mail_templates.xml b/addons/sale/data/mail_templates.xml
deleted file mode 100644
index a51640fce01..00000000000
--- a/addons/sale/data/mail_templates.xml
+++ /dev/null
@@ -1,27 +0,0 @@
-
-
-
-
-
-
-
-
-
- View Quotation
- Review, Sign & Pay Quotation
-
- Review, Accept & Sign Quotation
-
-
- Review, Accept & Pay Quotation
-
-
- View Quotation
-
-
-
-
-
-
diff --git a/addons/sale/models/sale_order.py b/addons/sale/models/sale_order.py
index 8b1c0855849..f039e4b491e 100644
--- a/addons/sale/models/sale_order.py
+++ b/addons/sale/models/sale_order.py
@@ -3,12 +3,14 @@
from datetime import timedelta
from itertools import groupby
+from markupsafe import Markup
+
import json
from odoo import api, fields, models, SUPERUSER_ID, _
from odoo.exceptions import AccessError, UserError, ValidationError
from odoo.osv import expression
-from odoo.tools import float_is_zero, html_keep_url, is_html_empty
+from odoo.tools import float_is_zero, format_amount, format_date, html_keep_url, is_html_empty
READONLY_FIELD_STATES = {
state: [('readonly', True)]
@@ -1069,6 +1071,64 @@ class SaleOrder(models.Model):
transaction = self.get_portal_last_transaction()
return (self.state == 'sent' or (self.state == 'draft' and include_draft)) and not self.is_expired and self.require_payment and transaction.state != 'done' and self.amount_total
+ def _notify_get_recipients_groups(self, msg_vals=None):
+ """ Give access button to users and portal customer as portal is integrated
+ in sale. Customer and portal group have probably no right to see
+ the document so they don't have the access button. """
+ groups = super(SaleOrder, self)._notify_get_recipients_groups(msg_vals=msg_vals)
+ if self._context.get('proforma'):
+ for group in [g for g in groups if g[0] in ('portal_customer', 'portal', 'follower', 'customer')]:
+ group[2]['has_button_access'] = False
+ return groups
+ local_msg_vals = dict(msg_vals or {})
+
+ # portal customers have full access (existence not granted, depending on partner_id)
+ try:
+ customer_portal_group = next(group for group in groups if group[0] == 'portal_customer')
+ except StopIteration:
+ pass
+ else:
+ access_opt = customer_portal_group[2].setdefault('button_access', {})
+ is_tx_pending = self.get_portal_last_transaction().state == 'pending'
+ if self.has_to_be_signed(include_draft=True):
+ if self.has_to_be_paid():
+ access_opt['title'] = _('View Quotation') if is_tx_pending else _('Sign & Pay Quotation')
+ else:
+ access_opt['title'] = _('Accept & Sign Quotation')
+ elif self.has_to_be_paid(include_draft=True) and not is_tx_pending:
+ access_opt['title'] = _('Accept & Pay Quotation')
+ elif self.state in ('draft', 'sent'):
+ access_opt['title'] = _('View Quotation')
+
+ # enable followers that have access through portal
+ follower_group = next(group for group in groups if group[0] == 'follower')
+ follower_group[2]['active'] = True
+ follower_group[2]['has_button_access'] = True
+ access_opt = follower_group[2].setdefault('button_access', {})
+ if self.state in ('draft', 'sent'):
+ access_opt['title'] = _('View Quotation')
+ else:
+ access_opt['title'] = _('View Order')
+ access_opt['url'] = self._notify_get_action_link('view', **local_msg_vals)
+
+ return groups
+
+ def _notify_by_email_prepare_rendering_context(self, message, msg_vals, model_description=False,
+ force_email_company=False, force_email_lang=False):
+ render_context = super()._notify_by_email_prepare_rendering_context(
+ message, msg_vals, model_description=model_description,
+ force_email_company=force_email_company, force_email_lang=force_email_lang
+ )
+ if self.validity_date:
+ amount_txt = _('%(amount)s due %(date)s',
+ amount=format_amount(self.env, self.amount_total, self.currency_id, lang_code=render_context.get('lang')),
+ date=format_date(self.env, self.validity_date, date_format='short', lang_code=render_context.get('lang'))
+ )
+ else:
+ amount_txt = format_amount(self.env, self.amount_total, self.currency_id, lang_code=render_context.get('lang'))
+ render_context['subtitle'] = Markup("%s %s") % (self.name, amount_txt)
+ return render_context
+
def preview_sale_order(self):
self.ensure_one()
return {
diff --git a/addons/sale_stock/tests/test_create_perf.py b/addons/sale_stock/tests/test_create_perf.py
index c53810c0393..50e2a83d0de 100644
--- a/addons/sale_stock/tests/test_create_perf.py
+++ b/addons/sale_stock/tests/test_create_perf.py
@@ -160,6 +160,6 @@ class TestPERF(common.TransactionCase):
],
} for i in range(self.ENTITIES)]
- # 1592 locally, 1593 in nightly runbot
- with self.assertQueryCount(admin=1593):
+ # 1592 locally, 1593 in nightly runbot, 1954 sometimes
+ with self.assertQueryCount(admin=1594):
self.env["sale.order"].create(vals_list)
diff --git a/addons/sms/models/mail_followers.py b/addons/sms/models/mail_followers.py
index d45a9556f74..248da8afd1d 100644
--- a/addons/sms/models/mail_followers.py
+++ b/addons/sms/models/mail_followers.py
@@ -1,24 +1,29 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
-from odoo import api, fields, models
+from odoo import models
class Followers(models.Model):
_inherit = ['mail.followers']
def _get_recipient_data(self, records, message_type, subtype_id, pids=None):
- if message_type == 'sms':
- if pids is None:
- sms_pids = records._sms_get_default_partners().ids
- else:
- sms_pids = pids
- res = super(Followers, self)._get_recipient_data(records, message_type, subtype_id, pids=pids)
- new_res = []
- for pid, active, pshare, notif, groups in res:
- if pid and pid in sms_pids:
- notif = 'sms'
- new_res.append((pid, active, pshare, notif, groups))
- return new_res
- else:
+ if message_type != 'sms' or not (pids or records):
return super(Followers, self)._get_recipient_data(records, message_type, subtype_id, pids=pids)
+
+ if pids is None and records:
+ records_pids = dict(
+ (record.id, record._sms_get_default_partners().ids)
+ for record in records
+ )
+ elif pids and records:
+ records_pids = dict((record.id, pids) for record in records)
+ else:
+ records_pids = {0: pids if pids else []}
+ recipients_data = super(Followers, self)._get_recipient_data(records, message_type, subtype_id, pids=pids)
+ for rid, rdata in recipients_data.items():
+ sms_pids = records_pids.get(rid) or []
+ for pid, pdata in rdata.items():
+ if pid in sms_pids:
+ pdata['notif'] = 'sms'
+ return recipients_data
diff --git a/addons/sms/models/mail_thread.py b/addons/sms/models/mail_thread.py
index c8a80d38288..7954b523dba 100644
--- a/addons/sms/models/mail_thread.py
+++ b/addons/sms/models/mail_thread.py
@@ -204,8 +204,8 @@ class MailThread(models.AbstractModel):
:param partner_ids: if set is a record set of partners to notify;
:param number_field: if set is a name of field to use on current record
to compute a number to notify;
- :param sms_numbers: see ``_notify_record_by_sms``;
- :param sms_pid_to_number: see ``_notify_record_by_sms``;
+ :param sms_numbers: see ``_notify_thread_by_sms``;
+ :param sms_pid_to_number: see ``_notify_thread_by_sms``;
"""
self.ensure_one()
sms_pid_to_number = sms_pid_to_number if sms_pid_to_number is not None else {}
@@ -237,17 +237,29 @@ class MailThread(models.AbstractModel):
def _notify_thread(self, message, msg_vals=False, **kwargs):
recipients_data = super(MailThread, self)._notify_thread(message, msg_vals=msg_vals, **kwargs)
- self._notify_record_by_sms(message, recipients_data, msg_vals=msg_vals, **kwargs)
+ self._notify_thread_by_sms(message, recipients_data, msg_vals=msg_vals, **kwargs)
return recipients_data
- def _notify_record_by_sms(self, message, recipients_data, msg_vals=False,
+ def _notify_thread_by_sms(self, message, recipients_data, msg_vals=False,
sms_numbers=None, sms_pid_to_number=None,
check_existing=False, put_in_queue=False, **kwargs):
""" Notification method: by SMS.
- :param message: mail.message record to notify;
- :param recipients_data: see ``_notify_thread``;
- :param msg_vals: see ``_notify_thread``;
+ :param message: ``mail.message`` record to notify;
+ :param recipients_data: list of recipients information (based on res.partner
+ records), formatted like
+ [{'active': partner.active;
+ 'id': id of the res.partner being recipient to notify;
+ 'groups': res.group IDs if linked to a user;
+ 'notif': 'inbox', 'email', 'sms' (SMS App);
+ 'share': partner.partner_share;
+ 'type': 'customer', 'portal', 'user;'
+ }, {...}].
+ See ``MailThread._notify_get_recipients``;
+ :param msg_vals: dictionary of values used to create the message. If given it
+ may be used to access values related to ``message`` without accessing it
+ directly. It lessens query count in some optimized use cases by avoiding
+ access message content in db;
:param sms_numbers: additional numbers to notify in addition to partners
and classic recipients;
@@ -264,7 +276,7 @@ class MailThread(models.AbstractModel):
sms_all = self.env['sms.sms'].sudo()
# pre-compute SMS data
- body = msg_vals['body'] if msg_vals and msg_vals.get('body') else message.body
+ body = msg_vals['body'] if msg_vals and 'body' in msg_vals else message.body
sms_base_vals = {
'body': html2plaintext(body),
'mail_message_id': message.id,
diff --git a/addons/sms/wizard/sms_resend.py b/addons/sms/wizard/sms_resend.py
index 25882a91871..4a262eb933d 100644
--- a/addons/sms/wizard/sms_resend.py
+++ b/addons/sms/wizard/sms_resend.py
@@ -90,15 +90,16 @@ class SMSResend(models.TransientModel):
pids = list(sms_pid_to_number.keys())
numbers = [r.sms_number for r in self.recipient_ids if r.resend and not r.partner_id]
- rdata = []
- for pid, active, pshare, notif, groups in self.env['mail.followers']._get_recipient_data(record, 'sms', False, pids=pids):
- if pid and notif == 'sms':
- rdata.append({'id': pid, 'share': pshare, 'active': active, 'notif': notif, 'groups': groups or [], 'type': 'customer' if pshare else 'user'})
- if rdata or numbers:
- record._notify_record_by_sms(
- self.mail_message_id, rdata, check_existing=True,
+ recipients_data = []
+ all_recipients_data = self.env['mail.followers']._get_recipient_data(record, 'sms', False, pids=pids)[record.id]
+ for pid, pdata in all_recipients_data.items():
+ if pid and pdata['notif'] == 'sms':
+ recipients_data.append(pdata)
+ if recipients_data or numbers:
+ record._notify_thread_by_sms(
+ self.mail_message_id, recipients_data,
sms_numbers=numbers, sms_pid_to_number=sms_pid_to_number,
- put_in_queue=False
+ check_existing=True, put_in_queue=False
)
self.mail_message_id._notify_message_notification_update()
diff --git a/addons/test_mail/models/test_mail_corner_case_models.py b/addons/test_mail/models/test_mail_corner_case_models.py
index 8272a559d17..95e0ad9ae7f 100644
--- a/addons/test_mail/models/test_mail_corner_case_models.py
+++ b/addons/test_mail/models/test_mail_corner_case_models.py
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
-from odoo import api, fields, models
+from odoo import api, fields, models, _
class MailPerformanceThread(models.Model):
@@ -66,6 +66,19 @@ class MailTestLang(models.Model):
customer_id = fields.Many2one('res.partner')
lang = fields.Char('Lang')
+ def _notify_get_recipients_groups(self, msg_vals=None):
+ groups = super(MailTestLang, self)._notify_get_recipients_groups(msg_vals=msg_vals)
+ local_msg_vals = dict(msg_vals or {})
+
+ for group in [g for g in groups if g[0] in('follower', 'customer')]:
+ group_options = group[2]
+ group_options['has_button_access'] = True
+ group_options['actions'] = [
+ {'url': self._notify_get_action_link('controller', controller='/test_mail/do_stuff', **local_msg_vals),
+ 'title': _('TestStuff')}
+ ]
+ return groups
+
class MailTestTrackCompute(models.Model):
_name = 'mail.test.track.compute'
diff --git a/addons/test_mail/models/test_mail_models.py b/addons/test_mail/models/test_mail_models.py
index a514b00f0cd..61aab7393b2 100644
--- a/addons/test_mail/models/test_mail_models.py
+++ b/addons/test_mail/models/test_mail_models.py
@@ -41,11 +41,6 @@ class MailTestStandard(models.Model):
container_id = fields.Many2one('mail.test.container', tracking=True)
company_id = fields.Many2one('res.company')
- def _get_share_url(self, redirect, signup_partner, share_token):
- """This function is required for a test on 'mail.mail_notification_paynow' template (test_message_post/test_mail_add_signature),
- another model should be created in master"""
- return '/mail/view'
-
class MailTestActivity(models.Model):
""" This model can be used to test activities in addition to simple chatter
diff --git a/addons/test_mail/tests/test_invite.py b/addons/test_mail/tests/test_invite.py
index dc543d1963b..718481a3ffd 100644
--- a/addons/test_mail/tests/test_invite.py
+++ b/addons/test_mail/tests/test_invite.py
@@ -2,9 +2,11 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.addons.test_mail.tests.common import TestMailCommon
+from odoo.tests import tagged
from odoo.tools import mute_logger
+@tagged('mail_followers')
class TestInvite(TestMailCommon):
@mute_logger('odoo.addons.mail.models.mail_mail')
diff --git a/addons/test_mail/tests/test_mail_composer.py b/addons/test_mail/tests/test_mail_composer.py
index 6703ae4450f..ffcc01eeeca 100644
--- a/addons/test_mail/tests/test_mail_composer.py
+++ b/addons/test_mail/tests/test_mail_composer.py
@@ -1,8 +1,6 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
-import base64
-
from unittest.mock import patch
from odoo.addons.mail.tests.common import mail_new_test_user
@@ -69,19 +67,6 @@ class TestMailComposer(TestMailCommon, TestRecipients):
'auto_delete': True,
})
- def _generate_attachments_data(self, count, res_model=None, res_id=None):
- # attachment visibility depends on what they are attached to
- if res_model is None:
- res_model = self.template._name
- if res_id is None:
- res_id = self.template.id
- return [{
- 'name': '%02d.txt' % x,
- 'datas': base64.b64encode(b'Att%02d' % x),
- 'res_model': res_model,
- 'res_id': res_id,
- } for x in range(count)]
-
def _get_web_context(self, records, add_web=True, **values):
""" Helper to generate composer context. Will make tests a bit less
verbose.
diff --git a/addons/test_mail/tests/test_mail_followers.py b/addons/test_mail/tests/test_mail_followers.py
index f06f1c006e5..5b358c6bc29 100644
--- a/addons/test_mail/tests/test_mail_followers.py
+++ b/addons/test_mail/tests/test_mail_followers.py
@@ -1,12 +1,9 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
-from psycopg2 import IntegrityError
-
from odoo.addons.test_mail.tests.common import TestMailCommon
from odoo.tests import tagged
from odoo.tests import users
-from odoo.tools.misc import mute_logger
@tagged('mail_followers')
@@ -417,62 +414,264 @@ class AdvancedResponsibleNotifiedTest(TestMailCommon):
self.assertEqual(mail_notification.mail_mail_id.state, 'outgoing')
-@tagged('post_install', '-at_install')
-class DuplicateNotificationTest(TestMailCommon):
- def test_no_duplicate_notification(self):
- """
- Check that we only create one mail.notification per partner
+@tagged('mail_followers', 'post_install', '-at_install')
+class RecipientsNotificationTest(TestMailCommon):
+ """ Test advanced and complex recipients computation / notification, such
+ as multiple users, batch computation, ... Post install because we need the
+ registry to be ready to send notifications."""
- Post install because we need the registery to be ready to send notification
- """
- #Simulate case of 2 users that got their partner merged
- common_partner = self.env['res.partner'].create({"name": "demo1", "email": "demo1@test.com"})
- user_1 = self.env['res.users'].create({'login': 'demo1', 'partner_id': common_partner.id, 'notification_type': 'email'})
- user_2 = self.env['res.users'].create({'login': 'demo2', 'partner_id': common_partner.id, 'notification_type': 'inbox'})
+ @classmethod
+ def setUpClass(cls):
+ super(RecipientsNotificationTest, cls).setUpClass()
- #Trigger auto subscribe notification
- test = self.env['mail.test.track'].create({"name": "Test Track", "user_id": user_2.id})
+ # portal user for testing share status / internal subtypes
+ cls.user_portal = cls._create_portal_user()
+ cls.partner_portal = cls.user_portal.partner_id
+
+ # simple customer
+ cls.customer = cls.env['res.partner'].create({
+ 'email': 'customer@test.customer.com',
+ 'name': 'Customer',
+ 'phone': '+32455778899',
+ })
+
+ # Simulate case of 2 users that got their partner merged
+ cls.common_partner = cls.env['res.partner'].create({
+ 'email': 'common.partner@test.customer.com',
+ 'name': 'Common Partner',
+ 'phone': '+32455998877',
+ })
+ cls.user_1, cls.user_2 = cls.env['res.users'].with_context(no_reset_password=True).create([
+ {'groups_id': [(4, cls.env.ref('base.group_portal').id)],
+ 'login': '_login_portal',
+ 'notification_type': 'email',
+ 'partner_id': cls.common_partner.id,
+ },
+ {'groups_id': [(4, cls.env.ref('base.group_user').id)],
+ 'login': '_login_internal',
+ 'notification_type': 'inbox',
+ 'partner_id': cls.common_partner.id,
+ }
+ ])
+ (cls.user_1 + cls.user_2).flush()
+
+ def assertRecipientsData(self, recipients_data, records, partners, partner_to_users=None):
+ """ Custom assert as recipients structure is custom and may change due
+ to some implementation choice. """
+ self.assertEqual(set(recipients_data.keys()), set(records.ids))
+ for record in records:
+ record_data = recipients_data[record.id]
+ self.assertEqual(set(record_data.keys()), set(partners.ids))
+ for partner in partners:
+ partner_data = record_data[partner.id]
+ if partner_to_users and partner_to_users.get(partner.id): #helps making test explicit
+ user = partner_to_users[partner.id]
+ else:
+ user = next((user for user in partner.user_ids if not user.share), self.env['res.users'])
+ if not user:
+ user = next((user for user in partner.user_ids), self.env['res.users'])
+ self.assertEqual(partner_data['active'], partner.active)
+ if user:
+ self.assertEqual(partner_data['groups'], set(user.groups_id.ids))
+ self.assertEqual(partner_data['notif'], user.notification_type)
+ self.assertEqual(partner_data['uid'], user.id)
+ else:
+ self.assertEqual(partner_data['groups'], set())
+ self.assertEqual(partner_data['notif'], 'email')
+ self.assertFalse(partner_data['uid'])
+ self.assertEqual(partner_data['is_follower'], partner in record.message_partner_ids)
+ self.assertEqual(partner_data['share'], partner.partner_share)
+ self.assertEqual(partner_data['ushare'], user.share)
+
+ @users('employee')
+ def test_notification_nodupe(self):
+ """ Check that we only create one mail.notification per partner. """
+ # Trigger auto subscribe notification
+ test = self.env['mail.test.track'].create({"name": "Test Track", "user_id": self.user_2.id})
mail_message = self.env['mail.message'].search([
- ('res_id', '=', test.id),
- ('model', '=', 'mail.test.track'),
- ('message_type', '=', 'user_notification')
+ ('res_id', '=', test.id),
+ ('model', '=', 'mail.test.track'),
+ ('message_type', '=', 'user_notification')
])
notif = self.env['mail.notification'].search([
('mail_message_id', '=', mail_message.id),
- ('res_partner_id', '=', common_partner.id)
+ ('res_partner_id', '=', self.common_partner.id)
])
self.assertEqual(len(notif), 1)
- self.assertEqual(notif.notification_type, 'email')
+ self.assertEqual(notif.notification_type, 'inbox', 'Multi users should take internal users if possible')
- subtype = self.env.ref('mail.mt_comment')
- res = self.env['mail.followers']._get_recipient_data(test, 'comment', subtype.id, pids=common_partner.ids)
- partner_notif = [r for r in res if r[0] == common_partner.id]
- self.assertEqual(len(partner_notif), 1)
- self.assertEqual(partner_notif[0][3], 'email')
+ recipients_data = self.env['mail.followers']._get_recipient_data(
+ test, 'comment', self.env.ref('mail.mt_comment').id,
+ pids=self.common_partner.ids)
+ self.assertRecipientsData(recipients_data, test, self.common_partner + self.partner_employee,
+ partner_to_users={self.common_partner.id: self.user_2})
-@tagged('post_install', '-at_install')
-class UnlinkedNotificationTest(TestMailCommon):
- def test_unlinked_notification(self):
- """
- Check that we unlink the created user_notification after unlinked the related document
-
- Post install because we need the registery to be ready to send notification
- """
- common_partner = self.env['res.partner'].create({"name": "demo1", "email": "demo1@test.com"})
- user_1 = self.env['res.users'].create({'login': 'demo1', 'partner_id': common_partner.id, 'notification_type': 'inbox'})
-
- test = self.env['mail.test.track'].create({"name": "Test Track", "user_id": user_1.id})
- test_id = test.id
+ @users('employee')
+ def test_notification_unlink(self):
+ """ Check that we unlink the created user_notification after unlinked the
+ related document. """
+ test = self.env['mail.test.track'].create({"name": "Test Track", "user_id": self.user_1.id})
mail_message = self.env['mail.message'].search([
- ('res_id', '=', test_id),
- ('model', '=', 'mail.test.track'),
- ('message_type', '=', 'user_notification')
+ ('res_id', '=', test.id),
+ ('model', '=', 'mail.test.track'),
+ ('message_type', '=', 'user_notification')
])
self.assertEqual(len(mail_message), 1)
test.unlink()
- mail_message = self.env['mail.message'].search([
- ('res_id', '=', test_id),
- ('model', '=', 'mail.test.track'),
- ('message_type', '=', 'user_notification')
+ self.assertEqual(
+ self.env['mail.message'].search_count([
+ ('res_id', '=', test.id),
+ ('model', '=', 'mail.test.track'),
+ ('message_type', '=', 'user_notification')
+ ]), 0
+ )
+
+ @users('employee')
+ def test_notification_user_choice(self):
+ """ Check fetching user information when notifying someone with multiple
+ users (more complex use case). """
+ company_other = self.env['res.company'].sudo().create({
+ 'currency_id': self.env.ref('base.CAD').id,
+ 'email': 'company_other@test.example.com',
+ 'name': 'Company Other',
+ })
+ shared_partner = self.env['res.partner'].sudo().create({
+ 'email': 'common.partner@test.customer.com',
+ 'name': 'Common Partner',
+ 'phone': '+32455998877',
+ })
+ cids = (company_other + self.company_admin).ids
+ user_2_1, user_2_2, user_2_3 = self.env['res.users'].sudo().with_context(no_reset_password=True).create([
+ {'company_ids': [(6, 0, cids)],
+ 'company_id': self.company_admin.id,
+ 'groups_id': [(4, self.env.ref('base.group_portal').id)],
+ 'login': '_login2_portal',
+ 'notification_type': 'email',
+ 'partner_id': shared_partner.id,
+ },
+ {'company_ids': [(6, 0, cids)],
+ 'company_id': self.company_admin.id,
+ 'groups_id': [(4, self.env.ref('base.group_user').id)],
+ 'login': '_login2_internal',
+ 'notification_type': 'inbox',
+ 'partner_id': shared_partner.id,
+ },
+ {'company_ids': [(6, 0, cids)],
+ 'company_id': company_other.id,
+ 'groups_id': [(4, self.env.ref('base.group_user').id), (4, self.env.ref('base.group_partner_manager').id)],
+ 'login': '_login2_manager',
+ 'notification_type': 'inbox',
+ 'partner_id': shared_partner.id,
+ }
])
- self.assertEqual(len(mail_message), 0)
+ (user_2_1 + user_2_2 + user_2_3).flush()
+
+ # just ensure current share status
+ self.assertFalse(shared_partner.partner_share)
+ self.assertTrue(user_2_1.share)
+ self.assertFalse(user_2_2.share or user_2_3.share)
+
+ test = self.env['mail.test.track'].create({"name": "Test Track", "user_id": False})
+ self.assertEqual(test.message_partner_ids, self.partner_employee)
+
+ with self.assertSinglePostNotifications(
+ [{'group': 'customer', 'partner': shared_partner,
+ 'status': 'sent', 'type': 'inbox'}],
+ message_info={'content': 'User Choice Notification'}):
+ test.message_post(
+ body='User Choice Notification ',
+ message_type='comment',
+ partner_ids=shared_partner.ids,
+ subtype_xmlid='mail.mt_comment',
+ )
+
+ recipients_data = self.env['mail.followers']._get_recipient_data(
+ test, 'comment', self.env.ref('mail.mt_comment').id,
+ pids=shared_partner.ids)
+ self.assertRecipientsData(recipients_data, test, self.partner_employee + shared_partner,
+ partner_to_users={shared_partner.id: user_2_2})
+
+ @users('employee')
+ def test_recipients_fetch(self):
+ test_records = self.env['mail.test.simple'].create([
+ {'email_from': 'ignasse@example.com',
+ 'name': 'Test %s' % idx,
+ } for idx in range(5)
+ ])
+ # make followers listen to notes to use it and check portal will never be notified of it (internal)
+ test_records.message_follower_ids.sudo().write({'subtype_ids': [(4, self.env.ref('mail.mt_note').id)]})
+ for test_record in test_records:
+ self.assertEqual(test_record.message_partner_ids, self.env.user.partner_id)
+
+ test_records[0].message_subscribe(self.partner_portal.ids)
+ self.assertNotIn(
+ self.env.ref('mail.mt_note'),
+ test_records[0].message_follower_ids.filtered(lambda fol: fol.partner_id == self.partner_portal).subtype_ids,
+ 'Portal user should not follow notes by default')
+
+ # just fetch followers
+ recipients_data = self.env['mail.followers']._get_recipient_data(
+ test_records[0], 'comment', self.env.ref('mail.mt_comment').id,
+ pids=None
+ )
+ self.assertRecipientsData(recipients_data, test_records[0], self.env.user.partner_id + self.partner_portal)
+
+ # followers + additional recipients
+ recipients_data = self.env['mail.followers']._get_recipient_data(
+ test_records[0], 'comment', self.env.ref('mail.mt_comment').id,
+ pids=(self.customer + self.common_partner + self.partner_admin).ids
+ )
+ self.assertRecipientsData(recipients_data, test_records[0],
+ self.env.user.partner_id + self.partner_portal + self.customer + self.common_partner + self.partner_admin)
+
+ # ensure filtering on internal: should exclude Portal even if misconfiguration
+ follower_portal = test_records[0].message_follower_ids.filtered(lambda fol: fol.partner_id == self.partner_portal).sudo()
+ follower_portal.write({'subtype_ids': [(4, self.env.ref('mail.mt_note').id)]})
+ follower_portal.flush()
+ recipients_data = self.env['mail.followers']._get_recipient_data(
+ test_records[0], 'comment', self.env.ref('mail.mt_note').id,
+ pids=(self.common_partner + self.partner_admin).ids
+ )
+ self.assertRecipientsData(recipients_data, test_records[0], self.env.user.partner_id + self.common_partner + self.partner_admin)
+
+ # ensure filtering on subtype: should exclude Portal as it does not follow comment anymore
+ follower_portal.write({'subtype_ids': [(3, self.env.ref('mail.mt_comment').id)]})
+ recipients_data = self.env['mail.followers']._get_recipient_data(
+ test_records[0], 'comment', self.env.ref('mail.mt_comment').id,
+ pids=(self.common_partner + self.partner_admin).ids
+ )
+ self.assertRecipientsData(recipients_data, test_records[0], self.env.user.partner_id + self.common_partner + self.partner_admin)
+
+ # check without subtype
+ recipients_data = self.env['mail.followers']._get_recipient_data(
+ test_records[0], 'comment', False,
+ pids=(self.common_partner + self.partner_admin).ids
+ )
+ self.assertRecipientsData(recipients_data, test_records[0], self.common_partner + self.partner_admin)
+
+ # multi mode
+ test_records[1].message_subscribe(self.partner_portal.ids)
+ test_records[0:4].message_subscribe(self.common_partner.ids)
+ recipients_data = self.env['mail.followers']._get_recipient_data(
+ test_records, 'comment', self.env.ref('mail.mt_comment').id,
+ pids=self.partner_admin.ids
+ )
+ # 0: portal is follower but does not follow comment + common partner (+ admin as pid)
+ recipients_data_1 = dict((r, recipients_data[r]) for r in recipients_data if r in test_records[0:1].ids)
+ self.assertRecipientsData(recipients_data_1, test_records[0:1], self.env.user.partner_id + self.common_partner + self.partner_admin)
+ # 1: portal is follower with comment + common partner (+ admin as pid)
+ recipients_data_1 = dict((r, recipients_data[r]) for r in recipients_data if r in test_records[1:2].ids)
+ self.assertRecipientsData(recipients_data_1, test_records[1:2], self.env.user.partner_id + self.common_partner + self.partner_portal + self.partner_admin)
+ # 2-3: common partner (+ admin as pid)
+ recipients_data_2 = dict((r, recipients_data[r]) for r in recipients_data if r in test_records[2:4].ids)
+ self.assertRecipientsData(recipients_data_2, test_records[2:4], self.env.user.partner_id + self.common_partner + self.partner_admin)
+ # 4+: env user partner (+ admin as pid)
+ recipients_data_3 = dict((r, recipients_data[r]) for r in recipients_data if r in test_records[4:].ids)
+ self.assertRecipientsData(recipients_data_3, test_records[4:], self.env.user.partner_id + self.partner_admin)
+
+ # multi mode, pids only
+ recipients_data = self.env['mail.followers']._get_recipient_data(
+ test_records, 'comment', False,
+ pids=(self.env.user.partner_id + self.partner_admin).ids
+ )
+ self.assertRecipientsData(recipients_data, test_records, self.env.user.partner_id + self.partner_admin)
diff --git a/addons/test_mail/tests/test_mail_template.py b/addons/test_mail/tests/test_mail_template.py
index 006e9a6706c..5b5016ca39e 100644
--- a/addons/test_mail/tests/test_mail_template.py
+++ b/addons/test_mail/tests/test_mail_template.py
@@ -8,7 +8,7 @@ from odoo.tests import tagged
from odoo.tools import mute_logger
-@tagged('mail_template')
+@tagged('mail_template', 'multi_lang')
class TestMailTemplate(TestMailCommon, TestRecipients):
@classmethod
@@ -39,81 +39,22 @@ class TestMailTemplate(TestMailCommon, TestRecipients):
cls.email_2 = 'test2@example.com'
cls.email_3 = cls.partner_1.email
- # activate translations
- cls.env['res.lang']._activate_lang('es_ES')
- cls.env.ref('base.module_base')._update_translations(['es_ES'])
-
# create a complete test template
cls.test_template = cls._create_template('mail.test.lang', {
'attachment_ids': [(0, 0, cls._attachments[0]), (0, 0, cls._attachments[1])],
- 'body_html': 'English Body for ',
+ 'body_html': 'EnglishBody for ',
'lang': '{{ object.customer_id.lang or object.lang }}',
'email_to': '%s, %s' % (cls.email_1, cls.email_2),
'email_cc': '%s' % cls.email_3,
'partner_to': '%s,%s' % (cls.partner_2.id, cls.user_admin.partner_id.id),
- 'subject': 'English for {{ object.name }}',
+ 'subject': 'EnglishSubject for {{ object.name }}',
})
- # Make sure Spanish translations have not been altered
- description_translations = cls.env['ir.translation'].search([
- ('module', '=', 'test_mail'),
- ('src', '=', cls.test_record._description),
- ('lang', '=', 'es_ES')
- ])
- if description_translations:
- description_translations.update({'value': 'Spanish description'})
- else:
- description_translations.create({
- 'type': 'model',
- 'name': 'ir.model,name',
- 'module': 'test_mail',
- 'lang': 'es_ES',
- 'res_id': cls.env['ir.model']._get_id('mail.test.lang'),
- 'src': cls.test_record._description,
- 'value': 'Spanish description',
- 'state': 'translated',
- })
-
- cls.env['ir.translation'].create({
- 'type': 'model',
- 'name': 'mail.template,subject',
- 'module': 'mail',
- 'lang': 'es_ES',
- 'res_id': cls.test_template.id,
- 'value': 'Spanish for {{ object.name }}',
- 'state': 'translated',
- })
- cls.env['ir.translation'].create({
- 'type': 'model',
- 'name': 'mail.template,body_html',
- 'module': 'mail',
- 'lang': 'es_ES',
- 'res_id': cls.test_template.id,
- 'value': 'Spanish Body for ',
- 'state': 'translated',
- })
- view = cls.env['ir.ui.view'].create({
- 'name': 'test_layout',
- 'key': 'test_layout',
- 'type': 'qweb',
- 'arch_db': ' English Layout '
- })
- cls.env['ir.model.data'].create({
- 'name': 'test_layout',
- 'module': 'test_mail',
- 'model': 'ir.ui.view',
- 'res_id': view.id
- })
- cls.env['ir.translation'].create({
- 'type': 'model_terms',
- 'name': 'ir.ui.view,arch_db',
- 'module': 'test_mail',
- 'lang': 'es_ES',
- 'res_id': view.id,
- 'src': 'English Layout',
- 'value': 'Spanish Layout',
- 'state': 'translated',
- })
+ # activate translations
+ cls._activate_multi_lang(
+ layout_arch_db=' English Layout for ',
+ test_record=cls.test_record, test_template=cls.test_template
+ )
# admin should receive emails
cls.user_admin.write({'notification_type': 'email'})
@@ -127,7 +68,7 @@ class TestMailTemplate(TestMailCommon, TestRecipients):
self.assertEqual(mail.email_cc, self.test_template.email_cc)
self.assertEqual(mail.email_to, self.test_template.email_to)
self.assertEqual(mail.recipient_ids, self.partner_2 | self.user_admin.partner_id)
- self.assertEqual(mail.subject, 'English for %s' % self.test_record.name)
+ self.assertEqual(mail.subject, 'EnglishSubject for %s' % self.test_record.name)
@mute_logger('odoo.addons.mail.models.mail_mail')
def test_template_translation_lang(self):
@@ -137,11 +78,11 @@ class TestMailTemplate(TestMailCommon, TestRecipients):
})
test_template = self.env['mail.template'].browse(self.test_template.ids)
- mail_id = test_template.send_mail(test_record.id, email_layout_xmlid='test_mail.test_layout')
+ mail_id = test_template.send_mail(test_record.id, email_layout_xmlid='mail.test_layout')
mail = self.env['mail.mail'].sudo().browse(mail_id)
self.assertEqual(mail.body_html,
- 'Spanish Body for %s Spanish Layout Spanish description' % self.test_record.name)
- self.assertEqual(mail.subject, 'Spanish for %s' % self.test_record.name)
+ 'SpanishBody for %s Spanish Layout para Spanish description' % self.test_record.name)
+ self.assertEqual(mail.subject, 'SpanishSubject for %s' % self.test_record.name)
@mute_logger('odoo.addons.mail.models.mail_mail')
def test_template_translation_partner_lang(self):
@@ -156,11 +97,11 @@ class TestMailTemplate(TestMailCommon, TestRecipients):
})
test_template = self.env['mail.template'].browse(self.test_template.ids)
- mail_id = test_template.send_mail(test_record.id, email_layout_xmlid='test_mail.test_layout')
+ mail_id = test_template.send_mail(test_record.id, email_layout_xmlid='mail.test_layout')
mail = self.env['mail.mail'].sudo().browse(mail_id)
self.assertEqual(mail.body_html,
- 'Spanish Body for %s Spanish Layout Spanish description' % self.test_record.name)
- self.assertEqual(mail.subject, 'Spanish for %s' % self.test_record.name)
+ 'SpanishBody for %s Spanish Layout para Spanish description' % self.test_record.name)
+ self.assertEqual(mail.subject, 'SpanishSubject for %s' % self.test_record.name)
def test_template_add_context_action(self):
self.test_template.create_action()
diff --git a/addons/test_mail/tests/test_mail_template_preview.py b/addons/test_mail/tests/test_mail_template_preview.py
index de8a2eadfa1..87becbaaeb1 100644
--- a/addons/test_mail/tests/test_mail_template_preview.py
+++ b/addons/test_mail/tests/test_mail_template_preview.py
@@ -20,7 +20,7 @@ class TestMailTemplateTools(TestMailTemplate):
'resource_ref': test_record,
'lang': 'es_ES',
})
- self.assertEqual(preview.body_html, 'Spanish Body for %s ' % test_record.name)
+ self.assertEqual(preview.body_html, 'SpanishBody for %s ' % test_record.name)
preview.write({'lang': 'en_US'})
- self.assertEqual(preview.body_html, 'English Body for %s ' % test_record.name)
+ self.assertEqual(preview.body_html, 'EnglishBody for %s ' % test_record.name)
diff --git a/addons/test_mail/tests/test_message_management.py b/addons/test_mail/tests/test_message_management.py
index c0e463f6dce..57d24e47f92 100644
--- a/addons/test_mail/tests/test_message_management.py
+++ b/addons/test_mail/tests/test_message_management.py
@@ -16,7 +16,7 @@ class TestMailResend(TestMailCommon):
cls.test_record = cls.env['mail.test.simple'].with_context(cls._test_context).create({'name': 'Test', 'email_from': 'ignasse@example.com'})
#Two users
- cls.user1 = mail_new_test_user(cls.env, login='e1', groups='base.group_public', name='Employee 1', notification_type='email', email='e1') # invalid email
+ cls.user1 = mail_new_test_user(cls.env, login='e1', groups='base.group_user', name='Employee 1', notification_type='email', email='e1') # invalid email
cls.user2 = mail_new_test_user(cls.env, login='e2', groups='base.group_portal', name='Employee 2', notification_type='email', email='e2@example.com')
#Two partner
cls.partner1 = cls.env['res.partner'].with_context(cls._test_context).create({
diff --git a/addons/test_mail/tests/test_message_post.py b/addons/test_mail/tests/test_message_post.py
index 1f0b66c51aa..c95502b5ef5 100644
--- a/addons/test_mail/tests/test_message_post.py
+++ b/addons/test_mail/tests/test_message_post.py
@@ -53,9 +53,10 @@ class TestMessagePost(TestMailCommon, TestRecipients):
for email_xmlid in ['mail.message_notification_email',
'mail.mail_notification_light',
'mail.mail_notification_paynow']:
+ test_message.sudo().notification_ids.unlink() # otherwise partner/message constraint fails
test_message.write({'email_layout_xmlid': email_xmlid})
with self.mock_mail_gateway():
- test_record._notify_record_by_email(
+ test_record._notify_thread_by_email(
test_message,
recipients_data,
force_send=False
@@ -71,7 +72,7 @@ class TestMessagePost(TestMailCommon, TestRecipients):
self.assertTrue(user_email)
@users('employee')
- def test_notify_mail_add_signature(self):
+ def test_notify_by_mail_add_signature(self):
self.test_track = self.env['mail.test.track'].with_context(self._test_context).with_user(self.user_employee).create({
'name': 'Test',
'email_from': 'ignasse@example.com'
@@ -84,19 +85,31 @@ class TestMessagePost(TestMailCommon, TestRecipients):
self.assertIn("record.user_id.sudo().signature", template.arch)
with self.mock_mail_gateway():
- self.test_track.message_post(body="Test body", mail_auto_delete=False, add_sign=True, partner_ids=[self.partner_1.id, self.partner_2.id], email_layout_xmlid="mail.mail_notification_paynow")
+ self.test_track.message_post(
+ body="Test body",
+ email_add_signature=True,
+ email_layout_xmlid="mail.mail_notification_paynow",
+ mail_auto_delete=False,
+ partner_ids=[self.partner_1.id, self.partner_2.id],
+ )
found_mail = self._new_mails
self.assertIn(signature, found_mail.body_html)
self.assertEqual(found_mail.body_html.count(signature), 1)
with self.mock_mail_gateway():
- self.test_track.message_post(body="Test body", mail_auto_delete=False, add_sign=False, partner_ids=[self.partner_1.id, self.partner_2.id], email_layout_xmlid="mail.mail_notification_paynow")
+ self.test_track.message_post(
+ body="Test body",
+ email_add_signature=False,
+ email_layout_xmlid="mail.mail_notification_paynow",
+ mail_auto_delete=False,
+ partner_ids=[self.partner_1.id, self.partner_2.id]
+ )
found_mail = self._new_mails
self.assertNotIn(signature, found_mail.body_html)
self.assertEqual(found_mail.body_html.count(signature), 0)
@users('employee')
- def test_notify_prepare_template_context_company_value(self):
+ def test_notify_by_email_prepare_rendering_contextt(self):
""" Verify that the template context company value is right
after switching the env company or if a company_id is set
on mail record.
@@ -113,7 +126,7 @@ class TestMessagePost(TestMailCommon, TestRecipients):
# self.env.company.id = Main Company AND test_record.company_id = False
self.assertEqual(self.env.company.id, main_company.id)
self.assertEqual(test_record.company_id.id, False)
- template_values = test_record._notify_prepare_template_context(test_record.message_ids, {})
+ template_values = test_record._notify_by_email_prepare_rendering_context(test_record.message_ids, {})
self.assertEqual(template_values.get('company').id, self.env.company.id)
# self.env.company.id = Other Company AND test_record.company_id = False
@@ -121,7 +134,7 @@ class TestMessagePost(TestMailCommon, TestRecipients):
test_record = self.env['mail.test.multi.company'].browse(test_record.id)
self.assertEqual(self.env.company.id, other_company.id)
self.assertEqual(test_record.company_id.id, False)
- template_values = test_record._notify_prepare_template_context(test_record.message_ids, {})
+ template_values = test_record._notify_by_email_prepare_rendering_context(test_record.message_ids, {})
self.assertEqual(template_values.get('company').id, self.env.company.id)
# self.env.company.id = Other Company AND test_record.company_id = Main Company
@@ -129,7 +142,7 @@ class TestMessagePost(TestMailCommon, TestRecipients):
test_record = self.env['mail.test.multi.company'].browse(test_record.id)
self.assertEqual(self.env.company.id, other_company.id)
self.assertEqual(test_record.company_id.id, main_company.id)
- template_values = test_record._notify_prepare_template_context(test_record.message_ids, {})
+ template_values = test_record._notify_by_email_prepare_rendering_context(test_record.message_ids, {})
self.assertEqual(template_values.get('company').id, main_company.id)
def test_notify_recipients_internals(self):
@@ -147,7 +160,7 @@ class TestMessagePost(TestMailCommon, TestRecipients):
'auth_login': 'auth_login_val',
}
notify_msg_vals = dict(msg_vals, **link_vals)
- classify_res = self.env[self.test_record._name]._notify_classify_recipients(pdata, 'My Custom Model Name', msg_vals=notify_msg_vals)
+ classify_res = self.env[self.test_record._name]._notify_get_recipients_classify(pdata, 'My Custom Model Name', msg_vals=notify_msg_vals)
# find back information for each recipients
partner_info = next(item for item in classify_res if item['recipients'] == self.partner_1.ids)
emp_info = next(item for item in classify_res if item['recipients'] == self.partner_employee.ids)
@@ -528,3 +541,133 @@ class TestMessagePostGlobal(TestMailCommon, TestRecipients):
{'body': 'test'}
)
self.assertTrue(isinstance(message_id, int))
+
+
+@tagged('mail_post', 'multi_lang')
+class TestMessagePostLang(TestMailCommon, TestRecipients):
+
+ @classmethod
+ def setUpClass(cls):
+ super(TestMessagePostLang, cls).setUpClass()
+
+ cls.test_records = cls.env['mail.test.lang'].create([
+ {'customer_id': False,
+ 'email_from': 'test.record.1@test.customer.com',
+ 'lang': 'es_ES',
+ 'name': 'TestRecord1',
+ },
+ {'customer_id': cls.partner_2.id,
+ 'email_from': 'valid.other@gmail.com',
+ 'name': 'TestRecord2',
+ },
+ ])
+
+ cls.test_template = cls.env['mail.template'].create({
+ 'auto_delete': True,
+ 'body_html': 'EnglishBody for ',
+ 'email_from': '{{ user.email_formatted }}',
+ 'email_to': '{{ (object.email_from if not object.customer_id else "") }}',
+ 'lang': '{{ object.customer_id.lang or object.lang }}',
+ 'model_id': cls.env['ir.model']._get('mail.test.lang').id,
+ 'name': 'TestTemplate',
+ 'partner_to': '{{ object.customer_id.id if object.customer_id else "" }}',
+ 'subject': 'EnglishSubject for {{ object.name }}',
+ })
+ cls.user_employee.write({ # add group to create contacts, necessary for templates
+ 'groups_id': [(4, cls.env.ref('base.group_partner_manager').id)],
+ })
+
+ cls._activate_multi_company()
+ cls._activate_multi_lang(test_record=cls.test_records[0], test_template=cls.test_template)
+
+ cls.partner_2.write({'lang': 'es_ES'})
+
+ @users('employee')
+ def test_composer_lang_template(self):
+ test_records = self.test_records.with_user(self.env.user)
+ test_template = self.test_template.with_user(self.env.user)
+
+ with self.mock_mail_gateway():
+ test_records.message_post_with_template(
+ test_template.id,
+ composition_mode='mass_mail',
+ # email_layout_xmlid='mail.test_layout', Not supported
+ message_type='comment',
+ subtype_id=self.env.ref('mail.mt_comment').id,
+ )
+
+ record0_customer = self.env['res.partner'].search([('email_normalized', '=', 'test.record.1@test.customer.com')], limit=1)
+ self.assertTrue(record0_customer, 'Template usage should have created a contact based on record email')
+
+ for record, customer in zip(test_records, record0_customer + self.partner_2):
+ customer_email = self._find_sent_mail_wemail(customer.email_formatted)
+ self.assertTrue(customer_email)
+ body = customer_email['body']
+ # check content
+ # self.assertIn('SpanishBody for %s' % record.name, body, 'Body based on template should be translated')
+ self.assertIn('EnglishBody for %s' % record.name, body, 'Fixme: this should be translated')
+ # check subject
+ # self.assertEqual('SpanishSubject for %s' % record.name, customer_email['subject'], 'Subject based on template should be translated')
+ self.assertEqual('EnglishSubject for %s' % record.name, customer_email['subject'], 'Fixme: this should be translated')
+
+ @users('employee')
+ def test_layout_email_lang_context(self):
+ test_records = self.test_records.with_user(self.env.user).with_context(lang='es_ES')
+ test_records[1].message_subscribe(self.partner_2.ids)
+
+ with self.mock_mail_gateway():
+ test_records[1].message_post(
+ body='Hello ',
+ email_layout_xmlid='mail.test_layout',
+ message_type='comment',
+ subject='Subject',
+ subtype_xmlid='mail.mt_comment',
+ )
+
+ customer_email = self._find_sent_mail_wemail(self.partner_2.email_formatted)
+ self.assertTrue(customer_email)
+ body = customer_email['body']
+ # check notification layout translation
+ self.assertIn('Spanish Layout para', body, 'Layout content should be translated')
+ self.assertNotIn('English Layout for', body)
+ self.assertIn('Spanish Layout para Spanish description', body, 'Model name should be translated')
+ self.assertIn('SpanishView Spanish description', body, '"View document" should be translated')
+ self.assertNotIn('View %s' % test_records[1]._description, body)
+ self.assertIn('TestSpanishStuff', body, 'Groups-based action names should be translated')
+ self.assertNotIn('TestStuff', body)
+ # check content
+ self.assertIn('Hello', body, 'Body of posted message should be present')
+
+ @users('employee')
+ def test_layout_email_lang_template(self):
+ test_records = self.test_records.with_user(self.env.user)
+ test_template = self.test_template.with_user(self.env.user)
+
+ with self.mock_mail_gateway():
+ for test_record in test_records:
+ test_record.message_post_with_template(
+ test_template.id,
+ email_layout_xmlid='mail.test_layout',
+ message_type='comment',
+ subtype_id=self.env.ref('mail.mt_comment').id,
+ )
+
+ record0_customer = self.env['res.partner'].search([('email_normalized', '=', 'test.record.1@test.customer.com')], limit=1)
+ self.assertTrue(record0_customer, 'Template usage should have created a contact based on record email')
+
+ for record, customer in zip(test_records, record0_customer + self.partner_2):
+ customer_email = self._find_sent_mail_wemail(customer.email_formatted)
+ self.assertTrue(customer_email)
+ body = customer_email['body']
+ # check notification layout translation
+ self.assertIn('Spanish Layout para', body, 'Layout content should be translated')
+ self.assertNotIn('English Layout for', body)
+ self.assertIn('Spanish Layout para Spanish description', body, 'Model name should be translated')
+ # self.assertIn('SpanishView Spanish description', body, '"View document" should be translated')
+ self.assertIn('View %s' % test_records[1]._description, body, 'Fixme: this should be translated')
+ # self.assertIn('TestSpanishStuff', body, 'Groups-based action names should be translated')
+ self.assertIn('TestStuff', body, 'Fixme: groups-based action names should be translated')
+ # check content
+ self.assertIn('SpanishBody for %s' % record.name, body, 'Body based on template should be translated')
+ # check subject
+ self.assertEqual('SpanishSubject for %s' % record.name, customer_email['subject'], 'Subject based on template should be translated')
diff --git a/addons/test_mail/tests/test_message_track.py b/addons/test_mail/tests/test_message_track.py
index 4be1f2cacae..bc3226a03e8 100644
--- a/addons/test_mail/tests/test_message_track.py
+++ b/addons/test_mail/tests/test_message_track.py
@@ -349,8 +349,8 @@ class TestTrackingInternals(TestMailCommon):
self.assertFalse(msg_emp[0].get('tracking_value_ids'), "should not have protected tracking values")
self.assertTrue(msg_sudo[0].get('tracking_value_ids'), "should have protected tracking values")
- msg_emp = self.record._notify_prepare_template_context(self.record.message_ids, {})
- msg_sudo = self.record.sudo()._notify_prepare_template_context(self.record.message_ids, {})
+ msg_emp = self.record._notify_by_email_prepare_rendering_context(self.record.message_ids, {})
+ msg_sudo = self.record.sudo()._notify_by_email_prepare_rendering_context(self.record.message_ids, {})
self.assertFalse(msg_emp.get('tracking_values'), "should not have protected tracking values")
self.assertTrue(msg_sudo.get('tracking_values'), "should have protected tracking values")
diff --git a/addons/test_mail/tests/test_performance.py b/addons/test_mail/tests/test_performance.py
index eec84943fa2..1ccbf127981 100644
--- a/addons/test_mail/tests/test_performance.py
+++ b/addons/test_mail/tests/test_performance.py
@@ -587,7 +587,7 @@ class TestMailComplexPerformance(BaseMailPerformance):
record = self.container.with_user(self.env.user)
template_id = self.env.ref('test_mail.mail_test_container_tpl').id
- with self.assertQueryCount(__system__=72, employee=73):
+ with self.assertQueryCount(__system__=72, employee=73): # about 20 queries per additional customer group
record.message_post_with_template(template_id, message_type='comment', composition_mode='comment')
self.assertEqual(record.message_ids[0].body, 'Adding stuff on %s ' % record.name)
@@ -1018,7 +1018,7 @@ class TestMailHeavyPerformancePost(BaseMailPerformance):
parent_id=False,
attachments=attachements,
attachment_ids=attachement_ids,
- add_sign=True,
+ email_add_signature=True,
model_description=False,
mail_auto_delete=True
)
diff --git a/addons/test_mail_full/tests/test_sms_post.py b/addons/test_mail_full/tests/test_sms_post.py
index a52f33b7734..c8619d84281 100644
--- a/addons/test_mail_full/tests/test_sms_post.py
+++ b/addons/test_mail_full/tests/test_sms_post.py
@@ -41,7 +41,7 @@ class TestSMSPost(TestMailFullCommon, TestMailFullRecipients):
with self.with_user('employee'), self.mockSMSGateway():
test_record = self.env['mail.test.sms'].browse(self.test_record.id)
- test_record._notify_record_by_sms(messages, [{'id': self.partner_1.id, 'notif': 'sms'}], check_existing=True)
+ test_record._notify_thread_by_sms(messages, [{'id': self.partner_1.id, 'notif': 'sms'}], check_existing=True)
self.assertSMSNotification([{'partner': self.partner_1}], self._test_body, messages)
def test_message_sms_internals_sms_numbers(self):
diff --git a/addons/website_blog/models/website_blog.py b/addons/website_blog/models/website_blog.py
index b1c5b02dd89..3ed2c5fa551 100644
--- a/addons/website_blog/models/website_blog.py
+++ b/addons/website_blog/models/website_blog.py
@@ -258,13 +258,13 @@ class BlogPost(models.Model):
default = dict(default or {}, name=name)
return super(BlogPost, self).copy_data(default)
- def get_access_action(self, access_uid=None):
+ def _get_access_action(self, access_uid=None, force_website=False):
""" Instead of the classic form view, redirect to the post on website
directly if user is an employee or if the post is published. """
self.ensure_one()
- user = access_uid and self.env['res.users'].sudo().browse(access_uid) or self.env.user
- if user.share and not self.sudo().website_published:
- return super(BlogPost, self).get_access_action(access_uid)
+ user = self.env['res.users'].sudo().browse(access_uid) if access_uid else self.env.user
+ if not force_website and user.share and not self.sudo().website_published:
+ return super(BlogPost, self)._get_access_action(access_uid=access_uid, force_website=force_website)
return {
'type': 'ir.actions.act_url',
'url': self.website_url,
@@ -273,23 +273,25 @@ class BlogPost(models.Model):
'res_id': self.id,
}
- def _notify_get_groups(self, msg_vals=None):
+ def _notify_get_recipients_groups(self, msg_vals=None):
""" Add access button to everyone if the document is published. """
- groups = super(BlogPost, self)._notify_get_groups(msg_vals=msg_vals)
+ groups = super(BlogPost, self)._notify_get_recipients_groups(msg_vals=msg_vals)
if self.website_published:
- for group_name, group_method, group_data in groups:
+ for _group_name, _group_method, group_data in groups:
group_data['has_button_access'] = True
return groups
- def _notify_record_by_inbox(self, message, recipients_data, msg_vals=False, **kwargs):
+ def _notify_thread_by_inbox(self, message, recipients_data, msg_vals=False, **kwargs):
""" Override to avoid keeping all notified recipients of a comment.
We avoid tracking needaction on post comments. Only emails should be
sufficient. """
+ if msg_vals is None:
+ msg_vals = {}
if msg_vals.get('message_type', message.message_type) == 'comment':
return
- return super(BlogPost, self)._notify_record_by_inbox(message, recipients_data, msg_vals=msg_vals, **kwargs)
+ return super(BlogPost, self)._notify_thread_by_inbox(message, recipients_data, msg_vals=msg_vals, **kwargs)
def _default_website_meta(self):
res = super(BlogPost, self)._default_website_meta()
diff --git a/addons/website_crm_partner_assign/models/crm_lead.py b/addons/website_crm_partner_assign/models/crm_lead.py
index f5038e49a26..31922c121f4 100644
--- a/addons/website_crm_partner_assign/models/crm_lead.py
+++ b/addons/website_crm_partner_assign/models/crm_lead.py
@@ -283,9 +283,9 @@ class CrmLead(models.Model):
# DO NOT FORWARD PORT IN MASTER
# instead, crm.lead should implement portal.mixin
#
- def get_access_action(self, access_uid=None):
+ def _get_access_action(self, access_uid=None, force_website=False):
""" Instead of the classic form view, redirect to the online document for
- portal users or if force_website=True in the context. """
+ portal users or if force_website=True. """
self.ensure_one()
user, record = self.env.user, self
@@ -294,10 +294,10 @@ class CrmLead(models.Model):
record.check_access_rights('read')
record.check_access_rule("read")
except AccessError:
- return super(CrmLead, self).get_access_action(access_uid)
+ return super(CrmLead, self)._get_access_action(access_uid=access_uid, force_website=force_website)
user = self.env['res.users'].sudo().browse(access_uid)
+ if user.share or force_website:
record = self.with_user(user)
- if user.share or self.env.context.get('force_website'):
try:
record.check_access_rights('read')
record.check_access_rule('read')
@@ -308,4 +308,4 @@ class CrmLead(models.Model):
'type': 'ir.actions.act_url',
'url': '/my/opportunity/%s' % record.id,
}
- return super(CrmLead, self).get_access_action(access_uid)
+ return super(CrmLead, self)._get_access_action(access_uid=access_uid, force_website=force_website)
diff --git a/addons/website_crm_partner_assign/tests/test_partner_assign.py b/addons/website_crm_partner_assign/tests/test_partner_assign.py
index 43250c16641..60fba253dbe 100644
--- a/addons/website_crm_partner_assign/tests/test_partner_assign.py
+++ b/addons/website_crm_partner_assign/tests/test_partner_assign.py
@@ -155,6 +155,6 @@ class TestPartnerLeadPortal(TestCrmCommon):
self.assertEqual(opportunity.partner_assigned_id, self.user_portal.partner_id, 'Assigned Partner of created opportunity is the (portal) creator.')
def test_portal_mixin_url(self):
- record_action = self.lead_portal.get_access_action(self.user_portal.id)
+ record_action = self.lead_portal._get_access_action(access_uid=self.user_portal.id)
self.assertEqual(record_action['url'], '/my/opportunity/%s' % self.lead_portal.id)
self.assertEqual(record_action['type'], 'ir.actions.act_url')
diff --git a/addons/website_forum/models/forum.py b/addons/website_forum/models/forum.py
index 1c9d83a5a79..d057a7dcbbd 100644
--- a/addons/website_forum/models/forum.py
+++ b/addons/website_forum/models/forum.py
@@ -911,9 +911,11 @@ class Post(models.Model):
self.ensure_one()
return sql.increment_field_skiplock(self, 'views')
- def get_access_action(self, access_uid=None):
+ def _get_access_action(self, access_uid=None, force_website=False):
""" Instead of the classic form view, redirect to the post on the website directly """
self.ensure_one()
+ if not force_website and not self.state == 'active':
+ return super(Post, self)._get_access_action(access_uid=access_uid, force_website=force_website)
return {
'type': 'ir.actions.act_url',
'url': '/forum/%s/%s' % (self.forum_id.id, self.id),
@@ -922,12 +924,12 @@ class Post(models.Model):
'res_id': self.id,
}
- def _notify_get_groups(self, msg_vals=None):
+ def _notify_recipients_get_groups(self, msg_vals=None):
""" Add access button to everyone if the document is active. """
- groups = super(Post, self)._notify_get_groups(msg_vals=msg_vals)
+ groups = super(Post, self)._notify_recipients_get_groups(msg_vals=msg_vals)
if self.state == 'active':
- for group_name, group_method, group_data in groups:
+ for _group_name, _group_method, group_data in groups:
group_data['has_button_access'] = True
return groups
@@ -954,13 +956,15 @@ class Post(models.Model):
kwargs['record_name'] = self.parent_id.name
return super(Post, self).message_post(message_type=message_type, **kwargs)
- def _notify_record_by_inbox(self, message, recipients_data, msg_vals=False, **kwargs):
+ def _notify_thread_by_inbox(self, message, recipients_data, msg_vals=False, **kwargs):
""" Override to avoid keeping all notified recipients of a comment.
We avoid tracking needaction on post comments. Only emails should be
sufficient. """
+ if msg_vals is None:
+ msg_vals = {}
if msg_vals.get('message_type', message.message_type) == 'comment':
return
- return super(Post, self)._notify_record_by_inbox(message, recipients_data, msg_vals=msg_vals, **kwargs)
+ return super(Post, self)._notify_thread_by_inbox(message, recipients_data, msg_vals=msg_vals, **kwargs)
def _compute_website_url(self):
return '/forum/{forum}/{post}{anchor}'.format(
diff --git a/addons/website_sale/data/mail_template_data.xml b/addons/website_sale/data/mail_template_data.xml
index 60fd6e92f67..656668378b1 100644
--- a/addons/website_sale/data/mail_template_data.xml
+++ b/addons/website_sale/data/mail_template_data.xml
@@ -8,30 +8,12 @@
{{ (object.user_id.email_formatted or user.email_formatted or '') }}
{{ object.partner_id.id }}
-
-
+
-
-
-
-
-
-
- Your Cart
- S00060
- |
-
- |
-
-
- |
-
- |
-
-
+
THERE'S SOMETHING IN YOUR CART.
Would you like to complete your purchase?
@@ -61,42 +43,13 @@
Resume order
+
Thank you for shopping with My Company (San Francisco)!
|
-
-
- |
-
-
-
-
-
- |
-
|
-
- |
-
-|
-
- |
{{ object.partner_id.lang }}
diff --git a/addons/website_sale/models/sale_order.py b/addons/website_sale/models/sale_order.py
index 53b3c1115a9..b3614f3cf87 100644
--- a/addons/website_sale/models/sale_order.py
+++ b/addons/website_sale/models/sale_order.py
@@ -300,6 +300,7 @@ class SaleOrder(models.Model):
'target': 'new',
'context': {
'default_composition_mode': 'mass_mail' if len(self.ids) > 1 else 'comment',
+ 'default_email_layout_xmlid': 'mail.mail_notification_paynow',
'default_res_id': self.ids[0],
'default_model': 'sale.order',
'default_use_template': bool(template_id),
@@ -335,6 +336,19 @@ class SaleOrder(models.Model):
sent_orders |= order
sent_orders.write({'cart_recovery_email_sent': True})
+ def _notify_get_groups(self, msg_vals=None):
+ """ In case of cart recovery email, update link to redirect directly
+ to the cart (like ``mail_template_sale_cart_recovery`` template). """
+ groups = super(SaleOrder, self)._notify_get_groups(msg_vals=msg_vals)
+
+ customer_portal_group = next(group for group in groups if group[0] == 'portal_customer')
+ if customer_portal_group:
+ access_opt = customer_portal_group[2].setdefault('button_access', {})
+ if self._context.get('website_sale_send_recovery_email'):
+ access_opt['title'] = _('Resume Order')
+ access_opt['url'] = '%s/shop/cart?access_token=%s' % (self.get_base_url(), self.access_token)
+ return groups
+
def action_confirm(self):
res = super(SaleOrder, self).action_confirm()
for order in self:
diff --git a/addons/website_sale/tests/test_website_sale_mail.py b/addons/website_sale/tests/test_website_sale_mail.py
index 4962dd0de5e..5b3e3a89167 100644
--- a/addons/website_sale/tests/test_website_sale_mail.py
+++ b/addons/website_sale/tests/test_website_sale_mail.py
@@ -26,6 +26,8 @@ class TestWebsiteSaleMail(HttpCase):
# we override unlink because we don't want the email to be auto deleted
MailMail = odoo.addons.mail.models.mail_mail.MailMail
+ # as we check some link content, avoid mobile doing its link management
+ self.env['ir.config_parameter'].sudo().set_param('mail_mobile.disable_redirect_firebase_dynamic_link', True)
with patch.object(MailMail, 'unlink', lambda self: None):
self.start_tour("/", 'shop_mail', login="admin")
diff --git a/addons/website_slides/models/slide_slide.py b/addons/website_slides/models/slide_slide.py
index 631945e371d..17207b0b62a 100644
--- a/addons/website_slides/models/slide_slide.py
+++ b/addons/website_slides/models/slide_slide.py
@@ -649,10 +649,10 @@ class Slide(models.Model):
raise AccessError(_('Not enough karma to comment'))
return super(Slide, self).message_post(message_type=message_type, **kwargs)
- def get_access_action(self, access_uid=None):
+ def _get_access_action(self, access_uid=None, force_website=False):
""" Instead of the classic form view, redirect to website if it is published. """
self.ensure_one()
- if self.website_published:
+ if force_website or self.website_published:
return {
'type': 'ir.actions.act_url',
'url': '%s' % self.website_url,
@@ -660,14 +660,14 @@ class Slide(models.Model):
'target_type': 'public',
'res_id': self.id,
}
- return super(Slide, self).get_access_action(access_uid)
+ return super(Slide, self)._get_access_action(access_uid=access_uid, force_website=force_website)
- def _notify_get_groups(self, msg_vals=None):
+ def _notify_get_recipients_groups(self, msg_vals=None):
""" Add access button to everyone if the document is active. """
- groups = super(Slide, self)._notify_get_groups(msg_vals=msg_vals)
+ groups = super(Slide, self)._notify_get_recipients_groups(msg_vals=msg_vals)
if self.website_published:
- for group_name, group_method, group_data in groups:
+ for _group_name, _group_method, group_data in groups:
group_data['has_button_access'] = True
return groups
diff --git a/addons/website_slides/wizard/slide_channel_invite.py b/addons/website_slides/wizard/slide_channel_invite.py
index bae1be76d4b..2aee778a3ce 100644
--- a/addons/website_slides/wizard/slide_channel_invite.py
+++ b/addons/website_slides/wizard/slide_channel_invite.py
@@ -89,7 +89,7 @@ class SlideChannelInvite(models.TransientModel):
except ValueError:
_logger.warning('QWeb template %s not found when sending slide channel mails. Sending without layout.', email_layout_xmlid)
else:
- # could be great to use _notify_prepare_template_context someday
+ # could be great to use ``_notify_by_email_prepare_rendering_context`` someday
template_ctx = {
'message': self.env['mail.message'].sudo().new(dict(body=mail_values['body_html'], record_name=self.channel_id.name)),
'model_description': self.env['ir.model']._get('slide.channel').display_name,
diff --git a/odoo/addons/base/models/res_users.py b/odoo/addons/base/models/res_users.py
index 2b3490aa8c8..25c92b3caf2 100644
--- a/odoo/addons/base/models/res_users.py
+++ b/odoo/addons/base/models/res_users.py
@@ -14,6 +14,7 @@ import time
from collections import defaultdict
from hashlib import sha256
from itertools import chain, repeat
+from markupsafe import Markup
import decorator
import pytz
@@ -28,7 +29,7 @@ from odoo.exceptions import AccessDenied, AccessError, UserError, ValidationErro
from odoo.http import request
from odoo.osv import expression
from odoo.service.db import check_super
-from odoo.tools import partition, collections, frozendict, lazy_property
+from odoo.tools import is_html_empty, partition, collections, frozendict, lazy_property
_logger = logging.getLogger(__name__)
@@ -285,7 +286,7 @@ class Users(models.Model):
help="Specify a value only when creating a user or if you're "\
"changing the user's password, otherwise leave empty. After "\
"a change of password, the user has to login again.")
- signature = fields.Html(string="Email Signature", default="")
+ signature = fields.Html(string="Email Signature", compute='_compute_signature', readonly=False, store=True)
active = fields.Boolean(default=True)
active_partner = fields.Boolean(related='partner_id.active', readonly=True, string="Partner is Active")
action_id = fields.Many2one('ir.actions.actions', string='Home Action',
@@ -401,6 +402,11 @@ class Users(models.Model):
else:
user.password = user.new_password
+ @api.depends('name')
+ def _compute_signature(self):
+ for user in self.filtered(lambda user: user.name and is_html_empty(user.signature)):
+ user.signature = Markup('-- %s ') % user['name']
+
@api.depends('groups_id')
def _compute_share(self):
user_group_id = self.env['ir.model.data']._xmlid_to_res_id('base.group_user')
diff --git a/odoo/addons/base/tests/test_ir_actions.py b/odoo/addons/base/tests/test_ir_actions.py
index 53a1384bc56..379af5f6b78 100644
--- a/odoo/addons/base/tests/test_ir_actions.py
+++ b/odoo/addons/base/tests/test_ir_actions.py
@@ -503,7 +503,7 @@ class TestCustomFields(common.TransactionCase):
# create a non-computed field, and assert how many queries it takes
model_id = self.env['ir.model']._get_id('res.partner')
- query_count = 44
+ query_count = 42
with self.assertQueryCount(query_count):
self.env.registry.clear_caches()
self.env['ir.model.fields'].create({
diff --git a/odoo/models.py b/odoo/models.py
index 6bd7ea83510..799e8e27abf 100644
--- a/odoo/models.py
+++ b/odoo/models.py
@@ -1767,15 +1767,19 @@ class BaseModel(metaclass=MetaModel):
'context': dict(self._context),
}
- def get_access_action(self, access_uid=None):
+ def _get_access_action(self, access_uid=None, force_website=False):
""" Return an action to open the document. This method is meant to be
overridden in addons that want to give specific access to the document.
By default, it opens the formview of the document.
- An optional access_uid holds the user that will access the document
- that could be different from the current user.
+ :param integer access_uid: optional access_uid being the user that
+ accesses the document. May be different from the current user as we
+ may compute an access for someone else.
+ :param integer force_website: force frontend redirection if available
+ on self. Used in overrides, notably with portal / website addons.
"""
- return self[0].get_formview_action(access_uid=access_uid)
+ self.ensure_one()
+ return self.get_formview_action(access_uid=access_uid)
@api.model
def search_count(self, args):
|
| | |
|
|
|
|
|
|
|
|
|