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
245 lines
11 KiB
Python
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]
|