Purpose ======= Currently fields with a `groups` attribute can't be tracked. Otherwise changes would be visible by all in the chatter, including users which normally don't have the access rights because they are not members of `groups`. Specification ============= Filter tracking field values according to the `groups` attributes. Users without the access rights should not see them. If a message is only composed of tracking values and the user doesn't have the rights to see them, an empty message should not be displayed.
294 lines
14 KiB
Python
294 lines
14 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.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 = "<p>-- <br/>%s</p>" % 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.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 {}
|