Purpose of this commit is to avoid browsing and prefetching data about recipients when notifying a message to partners and channels. Mail message _notify computes all necessary data in a single query. This commit allow to re-use this data by propagating it through the call chain. Addons inheriting from classification methods used when sending notification emails are updated accordingly to the API and data update. This commit is linked to task ID 47934 and PR #24033. No functional change should occur with this commit.
248 lines
11 KiB
Python
248 lines
11 KiB
Python
# -*- coding: utf-8 -*-
|
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
|
|
|
import logging
|
|
import threading
|
|
|
|
from odoo.tools.misc import split_every
|
|
|
|
from odoo import _, api, fields, models, registry, SUPERUSER_ID
|
|
from odoo.osv import expression
|
|
|
|
_logger = logging.getLogger(__name__)
|
|
|
|
|
|
class Partner(models.Model):
|
|
""" Update partner to add a field about notification preferences. Add a generic opt-out field that can be used
|
|
to restrict usage of automatic email templates. """
|
|
_name = "res.partner"
|
|
_inherit = ['res.partner', 'mail.thread', 'mail.activity.mixin']
|
|
_mail_flat_thread = False
|
|
|
|
message_bounce = fields.Integer('Bounce', help="Counter of the number of bounced emails for this contact", default=0)
|
|
opt_out = fields.Boolean(
|
|
'Opt-Out', help="If opt-out is checked, this contact has refused to receive emails for mass mailing and marketing campaign. "
|
|
"Filter 'Available for Mass Mailing' allows users to filter the partners when performing mass mailing.")
|
|
channel_ids = fields.Many2many('mail.channel', 'mail_channel_partner', 'partner_id', 'channel_id', string='Channels', copy=False)
|
|
|
|
@api.multi
|
|
def message_get_suggested_recipients(self):
|
|
recipients = super(Partner, self).message_get_suggested_recipients()
|
|
for partner in self:
|
|
partner._message_add_suggested_recipient(recipients, partner=partner, reason=_('Partner Profile'))
|
|
return recipients
|
|
|
|
@api.multi
|
|
def message_get_default_recipients(self):
|
|
return dict((res_id, {'partner_ids': [res_id], 'email_to': False, 'email_cc': False}) for res_id in self.ids)
|
|
|
|
@api.model
|
|
def _notify_prepare_template_context(self, message, record, model_description=False, mail_auto_delete=True):
|
|
# compute send user and its related signature
|
|
signature = ''
|
|
if message.author_id and message.author_id.user_ids:
|
|
user = message.author_id.user_ids[0]
|
|
if message.add_sign:
|
|
signature = user.signature
|
|
else:
|
|
user = self.env.user
|
|
if message.add_sign:
|
|
signature = "<p>-- <br/>%s</p>" % message.author_id.name
|
|
|
|
company = record.company_id if record and 'company_id' in record else user.company_id
|
|
if company.website:
|
|
website_url = 'http://%s' % company.website if not company.website.lower().startswith(('http:', 'https:')) else company.website
|
|
else:
|
|
website_url = False
|
|
|
|
if not model_description and message.model:
|
|
model_description = self.env['ir.model']._get(message.model).display_name
|
|
|
|
tracking = []
|
|
for tracking_value in self.env['mail.tracking.value'].sudo().search([('mail_message_id', '=', message.id)]):
|
|
tracking.append((tracking_value.field_desc,
|
|
tracking_value.get_old_display_value()[0],
|
|
tracking_value.get_new_display_value()[0]))
|
|
|
|
is_discussion = message.subtype_id.id == self.env['ir.model.data'].xmlid_to_res_id('mail.mt_comment')
|
|
|
|
return {
|
|
'message': message,
|
|
'signature': signature,
|
|
'website_url': website_url,
|
|
'company': company,
|
|
'model_description': model_description,
|
|
'record': record,
|
|
'record_name': message.record_name,
|
|
'tracking_values': tracking,
|
|
'is_discussion': is_discussion,
|
|
'subtype': message.subtype_id,
|
|
}
|
|
|
|
@api.model
|
|
def _notify(self, message, rdata, record, force_send=False, send_after_commit=True, model_description=False, mail_auto_delete=True):
|
|
""" Method to send email linked to notified messages. The recipients are
|
|
the recordset on which this method is called.
|
|
|
|
:param message: mail.message record to notify;
|
|
:param rdata: recipient data (see mail.message _notify);
|
|
:param record: optional record on which the message was posted;
|
|
:param force_send: tells whether to send notification emails within the
|
|
current transaction or to use the email queue;
|
|
:param send_after_commit: if force_send, tells whether to send emails after
|
|
the transaction has been committed using a post-commit hook;
|
|
:param model_description: optional data used in notification process (see
|
|
notification templates);
|
|
:param mail_auto_delete: delete notification emails once sent;
|
|
"""
|
|
if not rdata:
|
|
return True
|
|
|
|
template_xmlid = message.layout if message.layout else 'mail.message_notification_email'
|
|
try:
|
|
base_template = self.env.ref(template_xmlid, raise_if_not_found=True)
|
|
except ValueError:
|
|
_logger.warning('QWeb template %s not found when sending notification emails. Sending without layouting.' % (template_xmlid))
|
|
base_template = False
|
|
|
|
base_template_ctx = self._notify_prepare_template_context(message, record, model_description=model_description)
|
|
|
|
# prepare notification mail values
|
|
base_mail_values = {
|
|
'mail_message_id': message.id,
|
|
'mail_server_id': message.mail_server_id.id,
|
|
'auto_delete': mail_auto_delete,
|
|
'references': message.parent_id.message_id if message.parent_id else False
|
|
}
|
|
if record:
|
|
base_mail_values.update(self.env['mail.thread']._notify_specific_email_values_on_records(message, records=record))
|
|
|
|
# classify recipients: actions / no action
|
|
recipients = self.env['mail.thread']._notify_classify_recipients_on_records(message, rdata, records=record)
|
|
|
|
Mail = self.env['mail.mail'].sudo()
|
|
emails = self.env['mail.mail'].sudo()
|
|
email_pids = set()
|
|
recipients_nbr, recipients_max = 0, 50
|
|
for group_tpl_values in [group for group in recipients.values() if group['recipients']]:
|
|
# generate notification email content
|
|
template_ctx = {**base_template_ctx, **group_tpl_values}
|
|
mail_body = base_template.render(template_ctx, engine='ir.qweb', minimal_qcontext=True)
|
|
mail_body = self.env['mail.thread']._replace_local_links(mail_body)
|
|
mail_subject = message.subject or (message.record_name and 'Re: %s' % message.record_name)
|
|
|
|
# send email
|
|
for email_chunk in split_every(50, group_tpl_values['recipients']):
|
|
recipient_values = self.env['mail.thread']._notify_email_recipients_on_records(message, email_chunk, records=record)
|
|
create_values = {
|
|
'body_html': mail_body,
|
|
'subject': mail_subject,
|
|
}
|
|
create_values.update(base_mail_values)
|
|
create_values.update(recipient_values)
|
|
recipient_ids = [r[1] for r in create_values.get('recipient_ids', [])]
|
|
email = Mail.create(create_values)
|
|
|
|
if email and recipient_ids:
|
|
notifications = self.env['mail.notification'].sudo().search([
|
|
('mail_message_id', '=', email.mail_message_id.id),
|
|
('res_partner_id', 'in', list(recipient_ids))
|
|
])
|
|
notifications.write({
|
|
'is_email': True,
|
|
'mail_id': email.id,
|
|
'is_read': True, # handle by email discards Inbox notification
|
|
'email_status': 'ready',
|
|
})
|
|
|
|
emails |= email
|
|
email_pids.update(recipient_ids)
|
|
|
|
# NOTE:
|
|
# 1. for more than 50 followers, use the queue system
|
|
# 2. do not send emails immediately if the registry is not loaded,
|
|
# to prevent sending email during a simple update of the database
|
|
# using the command-line.
|
|
test_mode = getattr(threading.currentThread(), 'testing', False)
|
|
if force_send and len(emails) < recipients_max and \
|
|
(not self.pool._init or test_mode):
|
|
email_ids = emails.ids
|
|
dbname = self.env.cr.dbname
|
|
_context = self._context
|
|
|
|
def send_notifications():
|
|
db_registry = registry(dbname)
|
|
with api.Environment.manage(), db_registry.cursor() as cr:
|
|
env = api.Environment(cr, SUPERUSER_ID, _context)
|
|
env['mail.mail'].browse(email_ids).send()
|
|
|
|
# unless asked specifically, send emails after the transaction to
|
|
# avoid side effects due to emails being sent while the transaction fails
|
|
if not test_mode and send_after_commit:
|
|
self._cr.after('commit', send_notifications)
|
|
else:
|
|
emails.send()
|
|
|
|
return True
|
|
|
|
@api.multi
|
|
def _notify_by_chat(self, message):
|
|
""" Broadcast the message to all the partner since """
|
|
if not self:
|
|
return
|
|
message_values = message.message_format()[0]
|
|
notifications = []
|
|
for partner in self:
|
|
notifications.append([(self._cr.dbname, 'ir.needaction', partner.id), dict(message_values)])
|
|
self.env['bus.bus'].sendmany(notifications)
|
|
|
|
@api.model
|
|
def get_needaction_count(self):
|
|
""" compute the number of needaction of the current user """
|
|
if self.env.user.partner_id:
|
|
self.env.cr.execute("""
|
|
SELECT count(*) as needaction_count
|
|
FROM mail_message_res_partner_needaction_rel R
|
|
WHERE R.res_partner_id = %s AND (R.is_read = false OR R.is_read IS NULL)""", (self.env.user.partner_id.id,))
|
|
return self.env.cr.dictfetchall()[0].get('needaction_count')
|
|
_logger.error('Call to needaction_count without partner_id')
|
|
return 0
|
|
|
|
@api.model
|
|
def get_starred_count(self):
|
|
""" compute the number of starred of the current user """
|
|
if self.env.user.partner_id:
|
|
self.env.cr.execute("""
|
|
SELECT count(*) as starred_count
|
|
FROM mail_message_res_partner_starred_rel R
|
|
WHERE R.res_partner_id = %s """, (self.env.user.partner_id.id,))
|
|
return self.env.cr.dictfetchall()[0].get('starred_count')
|
|
_logger.error('Call to starred_count without partner_id')
|
|
return 0
|
|
|
|
@api.model
|
|
def get_static_mention_suggestions(self):
|
|
""" To be overwritten to return the id, name and email of partners used as static mention
|
|
suggestions loaded once at webclient initialization and stored client side. """
|
|
return []
|
|
|
|
@api.model
|
|
def get_mention_suggestions(self, search, limit=8):
|
|
""" Return 'limit'-first partners' id, name and email such that the name or email matches a
|
|
'search' string. Prioritize users, and then extend the research to all partners. """
|
|
search_dom = expression.OR([[('name', 'ilike', search)], [('email', 'ilike', search)]])
|
|
fields = ['id', 'name', 'email']
|
|
|
|
# Search users
|
|
domain = expression.AND([[('user_ids.id', '!=', False)], search_dom])
|
|
users = self.search_read(domain, fields, limit=limit)
|
|
|
|
# Search partners if less than 'limit' users found
|
|
partners = []
|
|
if len(users) < limit:
|
|
partners = self.search_read(search_dom, fields, limit=limit)
|
|
# Remove duplicates
|
|
partners = [p for p in partners if not len([u for u in users if u['id'] == p['id']])]
|
|
|
|
return [users, partners]
|