Files
odoo_source/addons/mail/models/res_partner.py
T
David Beguin 59b4836e8d [IMP] mail, mass_mailing : apply blacklist, opt_out per mailing list
Purpose
=======
- Apply the blacklist implementation to Improve mailing subscription to be more
  compliant with the European GDPR law. Keep a list of people who does not want
  to receive promotional emails (or mass mailing in general) anymore.
- Allows the recipient to update himself his mailing preferences

Specifications
===========
This commit is regrouping some main changes on mass mailing.
  Apply Blacklist for following models through blacklist.mixin :
    - crm.lead
    - res.partner
    - mail.mass_mailing.contact
    - mail.channel.partner
- Opt_out per mailing list instead of per mailing contact.
- Replace opt_out by blacklist in crm.lead + res.partner models
- Added 'ignored' state for mass_mailing. Ignored = blacklisted, opted-out
  Ignored email are not included into final statistics to avoid confusion.
- Unsubscribe(d) pages migrated from website_mass_mailing to mass_mailing module
  as thoses pages should work without having the website module installed

Detailed implementation
===================
Mass-mailing :
	- A blacklisted email is notified by the ban icon next to the email field.
	  (at the left of the email field for display purpose)
	- Renaming the '_get_blacklist' method that was actually
	  searching opt_out list into 'get_opt_out_list'
	- Opt out per mailing list : Add opt_out + related fields (for display and
	  ergonomy reasons) on the relational model
	- Display relational model tree view instead of mass_mailing.contact tree
	  view when clicking on mass_mailing_list in kanban view
	  -> In order to be align between contact_nbr displayed in kanban tile and
	     the content of the tree view
	  -> mass_mailing contact is accesible via the user icon in this tree view
	- Remove custom filters 'filter_contact_subscription' and
	  'filter_contact_unsubscription' as opt_out is not on mass_mailing.contact
	  model anymore
	- Add 'is_public' to mailing list
	    The name of this mailing list can be seen (or not) by recipient in
	    the unsubscription page
	    If the mailing lists used in the mass mailing are not public,
	    the user in only informed that he has been unsubscribed.
	    If the mailing lists are public, the user is informed that he has been
	    unsubscribed from the mailing lists
	    and he has the choice to modify his subscription to all the public
	    mailing list he is or was subscribed to.
	- Unsubscription Page :
		- mass_mailing_contact :
		  Opt_out per mailing list, done by email and not by id, as multiple
		  contact can have the same email
		  The recipient can add/remove himself to/from the blacklist
		  The recipient can send a feedback about why he unsubscribed
		- crm.lead + res.partner :
		  Once the recipient unsubscribe, he is automatically blacklisted
		  The recipient can 'Come back' and remove himself from the blacklist
		  if he changes his mind
		- Show blacklist button parameter added in config :
		  The idea is to enable/disable the fact that the recipient can add
		  himself to the blacklist by showing or not the 'blacklist me' button
		  Only applies for mass_mailing.contact. The recipient, once
		  blacklisted, can always, no matter the value of this parameter,
		  'come back' and unblacklist himself

crm.lead + res.partner :
	- Replace opt_out by blacklist in crm.lead + res.partner models.
	  As when a res.partner or crm.lead unsubcribe, we assume that the recipient
	  does not want to receive mass mailing anymore, at all, even if we adds him
	  to a mailing list afterwards. He is then blacklisted to avoid this.
	  With this behaviour, if a new lead is created with the same email address,
	  he won't be able to receive mail in mass_mode.
	  But he will still be able to receive '1 to 1' direct email.

Task ID 33224
Closes #25966
2018-08-10 17:46:08 +02:00

245 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.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)
@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]