# -*- 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.addons.bus.models.bus_presence import AWAY_TIMER
from odoo.addons.bus.models.bus_presence import DISCONNECTION_TIMER
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.blacklist.mixin']
_mail_flat_thread = False
message_bounce = fields.Integer('Bounce', help="Counter of the number of bounced emails for this contact", default=0)
channel_ids = fields.Many2many('mail.channel', 'mail_channel_partner', 'partner_id', 'channel_id', string='Channels', copy=False)
# override the field to track the visibility of user
user_id = fields.Many2one(tracking=True)
@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 = "
--
%s
" % message.author_id.name
company = record.company_id.sudo() 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
# Retrieve the language in which the template was rendered, in order to render the custom
# layout in the same language.
lang = self.env.context.get('lang')
if {'default_template_id', 'default_model', 'default_res_id'} <= self.env.context.keys():
template = self.env['mail.template'].browse(self.env.context['default_template_id'])
if template and template.lang:
lang = template._render_template(template.lang, self.env.context['default_model'], self.env.context['default_res_id'])
if not model_description and message.model:
model_description = self.env['ir.model'].with_context(lang=lang)._get(message.model).display_name
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.user_has_groups(groups):
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,
'lang': lang,
}
@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
base_template_ctx = self._notify_prepare_template_context(message, record, model_description=model_description)
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).with_context(lang=base_template_ctx['lang'])
except ValueError:
_logger.warning('QWeb template %s not found when sending notification emails. Sending without layouting.' % (template_xmlid))
base_template = False
# 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]
@api.model
def im_search(self, name, limit=20):
""" Search partner with a name and return its id, name and im_status.
Note : the user must be logged
:param name : the partner name to search
:param limit : the limit of result to return
"""
# This method is supposed to be used only in the context of channel creation or
# extension via an invite. As both of these actions require the 'create' access
# right, we check this specific ACL.
if self.env['mail.channel'].check_access_rights('create', raise_exception=False):
name = '%' + name + '%'
excluded_partner_ids = [self.env.user.partner_id.id]
self.env.cr.execute("""
SELECT
U.id as user_id,
P.id as id,
P.name as name,
CASE WHEN B.last_poll IS NULL THEN 'offline'
WHEN age(now() AT TIME ZONE 'UTC', B.last_poll) > interval %s THEN 'offline'
WHEN age(now() AT TIME ZONE 'UTC', B.last_presence) > interval %s THEN 'away'
ELSE 'online'
END as im_status
FROM res_users U
JOIN res_partner P ON P.id = U.partner_id
LEFT JOIN bus_presence B ON B.user_id = U.id
WHERE P.name ILIKE %s
AND P.id NOT IN %s
AND U.active = 't'
LIMIT %s
""", ("%s seconds" % DISCONNECTION_TIMER, "%s seconds" % AWAY_TIMER, name, tuple(excluded_partner_ids), limit))
return self.env.cr.dictfetchall()
else:
return {}