From 83d3b281a4a7b0d0b16eb373031de64551a39ad9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Tue, 12 Sep 2017 16:03:37 +0200 Subject: [PATCH 1/9] [IMP] mass_mailing: allow to send a mailing on a given set of records Currently mass mailing automatically computes the remaining recipients to mail based on existing statistics and A/B testing. However in some cases we would like to send the mailing on a given set of records. An example of use is to use the mass mailing mechanism on records coming from marketing activities. This commit adds this feature by adding an optional parameter to send. If not given it falls back on the classic mass mailing behavior. --- addons/mass_mailing/models/mass_mailing.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/addons/mass_mailing/models/mass_mailing.py b/addons/mass_mailing/models/mass_mailing.py index a10e6143c89..a2560657ce8 100644 --- a/addons/mass_mailing/models/mass_mailing.py +++ b/addons/mass_mailing/models/mass_mailing.py @@ -659,11 +659,12 @@ class MassMailing(models.Model): already_mailed_res_ids = [record['res_id'] for record in already_mailed] return list(set(res_ids) - set(already_mailed_res_ids)) - def send_mail(self): + def send_mail(self, res_ids=None): author_id = self.env.user.partner_id.id + for mailing in self: - # instantiate an email composer + send emails - res_ids = mailing.get_remaining_recipients() + if not res_ids: + res_ids = mailing.get_remaining_recipients() if not res_ids: raise UserError(_('Please select recipients.')) From 7accd77999ebcd4ce16388f2a23ee296093fecbc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Tue, 12 Sep 2017 16:18:41 +0200 Subject: [PATCH 2/9] [REF][FIX] link_tracker, mass_mailing: factorize method creating link tracker click values This commit factorizes the link.tracker.click values computation out of the add_click method. This method is called when customers click on links in email that are tracked in Odoo. Purpose of this commit is * to effectively put mass mailing related code into mass mailing. Currently link_tracker holds code related to mass mailing which is not in its dependency tree. Let us therefore try to be a bit modular and add a proper inheritance in mass mailing; * allow addons to inherit from the click values prepare method to add specific values depending on their behavior like mass mailing adding links to a mass mailing and a mass mailing campaign; --- addons/link_tracker/models/link_tracker.py | 34 ++++++++++------------ addons/mass_mailing/models/link_tracker.py | 11 +++++++ 2 files changed, 27 insertions(+), 18 deletions(-) diff --git a/addons/link_tracker/models/link_tracker.py b/addons/link_tracker/models/link_tracker.py index d256429a0c4..36f43d485a8 100644 --- a/addons/link_tracker/models/link_tracker.py +++ b/addons/link_tracker/models/link_tracker.py @@ -244,23 +244,21 @@ class link_tracker_click(models.Model): again = self.search_count([('link_id', '=', code_rec.link_id.id), ('ip', '=', ip)]) if not again: - country_record = self.env['res.country'].search([('code', '=', country_code)], limit=1) + self.create( + self._get_click_values(dict( + code=code, + ip=ip, + country_code=country_code, + stat_id=stat_id, + ))) - vals = { - 'link_id': code_rec.link_id.id, - 'create_date': datetime.date.today(), - 'ip': ip, - 'country_id': country_record.id, - 'mail_stat_id': stat_id - } + def _get_click_values_from_route(self, route_values): + code = self.env['link.tracker.code'].search([('code', '=', route_values['code'])], limit=1) + country = self.env['res.country'].search([('code', '=', route_values['country_code'])], limit=1) - if stat_id: - mail_stat = self.env['mail.mail.statistics'].search([('id', '=', stat_id)]) - - if mail_stat.mass_mailing_campaign_id: - vals['mass_mailing_campaign_id'] = mail_stat.mass_mailing_campaign_id.id - - if mail_stat.mass_mailing_id: - vals['mass_mailing_id'] = mail_stat.mass_mailing_id.id - - self.create(vals) + return { + 'link_id': code.link_id.id, + 'create_date': datetime.date.today(), + 'ip': route_values['ip'], + 'country_id': country.id, + } diff --git a/addons/mass_mailing/models/link_tracker.py b/addons/mass_mailing/models/link_tracker.py index 2ced9d90992..51e1db97318 100644 --- a/addons/mass_mailing/models/link_tracker.py +++ b/addons/mass_mailing/models/link_tracker.py @@ -17,3 +17,14 @@ class LinkTrackerClick(models.Model): mail_stat_id = fields.Many2one('mail.mail.statistics', string='Mail Statistics') mass_mailing_id = fields.Many2one('mail.mass_mailing', string='Mass Mailing') mass_mailing_campaign_id = fields.Many2one('mail.mass_mailing.campaign', string='Mass Mailing Campaign') + + def _get_click_values_from_route(self, route_values): + click_values = super(LinkTrackerClick, self)._get_click_values_from_route(route_values) + if route_values['stat_id']: + mail_stat = self.env['mail.mail.statistics'].browse(route_values['stat_id']) + click_values['mail_stat_id'] = mail_stat.id + if mail_stat.mass_mailing_campaign_id: + click_values['mass_mailing_campaign_id'] = mail_stat.mass_mailing_campaign_id.id + if mail_stat.mass_mailing_id: + click_values['mass_mailing_id'] = mail_stat.mass_mailing_id.id + return click_values From ae455fb1f5bf6ec2238f6fd65f697de4f7c9a4f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Tue, 12 Sep 2017 16:39:26 +0200 Subject: [PATCH 3/9] [FIX] mass_mailing: fix issue in _get_mailing_model getattr crashes and in this case we probably meant to use hasattr as we have a fallback behavior. --- addons/mass_mailing/models/mass_mailing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/mass_mailing/models/mass_mailing.py b/addons/mass_mailing/models/mass_mailing.py index a2560657ce8..147f5447545 100644 --- a/addons/mass_mailing/models/mass_mailing.py +++ b/addons/mass_mailing/models/mass_mailing.py @@ -295,7 +295,7 @@ class MassMailing(models.Model): for model_name in self.env: model = self.env[model_name] if hasattr(model, '_mail_mass_mailing') and getattr(model, '_mail_mass_mailing'): - if getattr(model, 'message_mass_mailing_enabled'): + if hasattr(model, 'message_mass_mailing_enabled'): res.append((model._name, model.message_mass_mailing_enabled())) else: res.append((model._name, model._mail_mass_mailing)) From e9fca14452273be1807d3b47f6220f563c343481 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Tue, 12 Sep 2017 17:08:24 +0200 Subject: [PATCH 4/9] [IMP] mass_mailing: add support of clicked event on statistics Currently there is no way to know on mail statistics if the customer clicked on a link or not. This commit adds support of click like what is already implemented for opened or replied. It stores the date when the first tracked link has been clicked. A clicked mail is considered as being opened related to its state if no other statistics has been checked. --- addons/mass_mailing/models/link_tracker.py | 11 ++++++++++- addons/mass_mailing/models/mass_mailing_stats.py | 12 ++++++++++-- .../mass_mailing/views/mass_mailing_stats_views.xml | 3 +++ 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/addons/mass_mailing/models/link_tracker.py b/addons/mass_mailing/models/link_tracker.py index 51e1db97318..c7077191dab 100644 --- a/addons/mass_mailing/models/link_tracker.py +++ b/addons/mass_mailing/models/link_tracker.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from odoo import fields, models +from odoo import api, fields, models class LinkTracker(models.Model): @@ -18,6 +18,15 @@ class LinkTrackerClick(models.Model): mass_mailing_id = fields.Many2one('mail.mass_mailing', string='Mass Mailing') mass_mailing_campaign_id = fields.Many2one('mail.mass_mailing.campaign', string='Mass Mailing Campaign') + @api.model + def add_click(self, code, ip, country_code, stat_id=False): + res = super(LinkTrackerClick, self).add_click(code, ip, country_code, stat_id=stat_id) + if stat_id: + stat_sudo = self.env['mail.mail.statistics'].sudo().browse(stat_id) + stat_sudo.set_opened() + stat_sudo.set_clicked() + return res + def _get_click_values_from_route(self, route_values): click_values = super(LinkTrackerClick, self)._get_click_values_from_route(route_values) if route_values['stat_id']: diff --git a/addons/mass_mailing/models/mass_mailing_stats.py b/addons/mass_mailing/models/mass_mailing_stats.py index e330975fe5f..2b22ae1d508 100644 --- a/addons/mass_mailing/models/mass_mailing_stats.py +++ b/addons/mass_mailing/models/mass_mailing_stats.py @@ -38,7 +38,10 @@ class MailMailStats(models.Model): opened = fields.Datetime(help='Date when the email has been opened the first time') replied = fields.Datetime(help='Date when this email has been replied for the first time.') bounced = fields.Datetime(help='Date when this email has bounced.') + # Link tracking links_click_ids = fields.One2many('link.tracker.click', 'mail_stat_id', string='Links click') + clicked = fields.Datetime(help='Date when customer clicked on at least one tracked link') + # Status state = fields.Selection(compute="_compute_state", selection=[('outgoing', 'Outgoing'), ('exception', 'Exception'), @@ -51,7 +54,7 @@ class MailMailStats(models.Model): store=True) recipient = fields.Char(compute="_compute_recipient") - @api.depends('sent', 'opened', 'replied', 'bounced', 'exception') + @api.depends('sent', 'opened', 'clicked', 'replied', 'bounced', 'exception') def _compute_state(self): self.update({'state_update': fields.Datetime.now()}) for stat in self: @@ -59,7 +62,7 @@ class MailMailStats(models.Model): stat.state = 'exception' elif stat.sent: stat.state = 'sent' - elif stat.opened: + elif stat.opened or stat.clicked: stat.state = 'opened' elif stat.replied: stat.state = 'replied' @@ -105,6 +108,11 @@ class MailMailStats(models.Model): statistics.write({'opened': fields.Datetime.now()}) return statistics + def set_clicked(self, mail_mail_ids=None, mail_message_ids=None): + statistics = self._get_records(mail_mail_ids, mail_message_ids, [('clicked', '=', False)]) + statistics.write({'clicked': fields.Datetime.now()}) + return statistics + def set_replied(self, mail_mail_ids=None, mail_message_ids=None): statistics = self._get_records(mail_mail_ids, mail_message_ids, [('replied', '=', False)]) statistics.write({'replied': fields.Datetime.now()}) diff --git a/addons/mass_mailing/views/mass_mailing_stats_views.xml b/addons/mass_mailing/views/mass_mailing_stats_views.xml index 142b83c1233..497e629e532 100644 --- a/addons/mass_mailing/views/mass_mailing_stats_views.xml +++ b/addons/mass_mailing/views/mass_mailing_stats_views.xml @@ -11,6 +11,7 @@ + @@ -34,6 +35,7 @@ + @@ -54,6 +56,7 @@ + From 51b5ee7c3c1bbc08c5aa284300fd95f63cbf7ad1 Mon Sep 17 00:00:00 2001 From: Yannick Tivisse Date: Wed, 13 Sep 2017 15:18:12 +0200 Subject: [PATCH 5/9] [IMP] mass_mailing: choose model among all chatter models This commit removes the strange selection box based on a magic flag and some strange methods returning a string. Instead just allow to send mass mailing on all models inheriting from mail.thread using the is mail thread flag. Various addons are updated to remove the _mail_mass_mailing class attribute used to determine mass mailing capability. We consider people could send a mass mailing on every model inheriting from mail.thread. It makes no sense to limit it to a given set of addons. --- addons/crm/models/crm_lead.py | 1 - addons/event/models/event.py | 2 +- .../hr_recruitment/models/hr_recruitment.py | 1 - addons/mail/models/mail_thread.py | 11 ------- addons/mail/models/res_partner.py | 1 - addons/mass_mailing/controllers/main.py | 2 +- .../mass_mailing/data/mass_mailing_demo.xml | 4 +-- addons/mass_mailing/models/mass_mailing.py | 30 +++++-------------- .../mass_mailing/views/mass_mailing_views.xml | 11 +++---- .../wizard/mail_compose_message.py | 4 +-- addons/mass_mailing_event/models/event.py | 2 +- .../mass_mailing_event_track/models/event.py | 2 +- .../website_event_track/models/event_track.py | 1 - .../website_mass_mailing/controllers/main.py | 2 +- odoo/addons/base/security/base_security.xml | 2 +- 15 files changed, 24 insertions(+), 52 deletions(-) diff --git a/addons/crm/models/crm_lead.py b/addons/crm/models/crm_lead.py index 90d24275051..3f1b79cff76 100644 --- a/addons/crm/models/crm_lead.py +++ b/addons/crm/models/crm_lead.py @@ -51,7 +51,6 @@ class Lead(models.Model): _description = "Lead/Opportunity" _order = "priority desc,activity_date_deadline,id desc" _inherit = ['mail.thread', 'mail.activity.mixin', 'utm.mixin', 'format.address.mixin'] - _mail_mass_mailing = _('Leads / Opportunities') def _default_probability(self): stage_id = self._default_stage_id() diff --git a/addons/event/models/event.py b/addons/event/models/event.py index c5f7f43915a..85406ba3ba9 100644 --- a/addons/event/models/event.py +++ b/addons/event/models/event.py @@ -319,12 +319,12 @@ class EventEvent(models.Model): def _is_event_registrable(self): return True + class EventRegistration(models.Model): _name = 'event.registration' _description = 'Attendee' _inherit = ['mail.thread'] _order = 'name, create_date desc' - _mail_mass_mailing = _('Event Attendees') origin = fields.Char( string='Source Document', readonly=True, diff --git a/addons/hr_recruitment/models/hr_recruitment.py b/addons/hr_recruitment/models/hr_recruitment.py index 3a237e9f283..5f432488587 100644 --- a/addons/hr_recruitment/models/hr_recruitment.py +++ b/addons/hr_recruitment/models/hr_recruitment.py @@ -89,7 +89,6 @@ class Applicant(models.Model): _description = "Applicant" _order = "priority desc, id desc" _inherit = ['mail.thread', 'mail.activity.mixin', 'utm.mixin'] - _mail_mass_mailing = _('Applicants') def _default_stage_id(self): if self._context.get('default_job_id'): diff --git a/addons/mail/models/mail_thread.py b/addons/mail/models/mail_thread.py index 6e097196a1b..2587601e2b1 100644 --- a/addons/mail/models/mail_thread.py +++ b/addons/mail/models/mail_thread.py @@ -79,7 +79,6 @@ class MailThread(models.AbstractModel): _description = 'Email Thread' _mail_flat_thread = True # flatten the discussino history _mail_post_access = 'write' # access required on the document to post on it - _mail_mass_mailing = False # enable mass mailing on this model _Attachment = namedtuple('Attachment', ('fname', 'content', 'info')) message_is_follower = fields.Boolean( @@ -2201,13 +2200,3 @@ class MailThread(models.AbstractModel): msg_comment.write({"res_id": new_thread.id, "model": new_thread._name}) msg_not_comment.write({"res_id": new_thread.id, "model": new_thread._name, "subtype_id": None}) return True - - # ------------------------------------------------------ - # Mass mailing - # ------------------------------------------------------ - - def message_mass_mailing_enabled(self): - if self._mail_mass_mailing: - # TODO master properly translate - # the _mail_mass_mailing is evaluted at code start so not translated - return _(self._mail_mass_mailing) diff --git a/addons/mail/models/res_partner.py b/addons/mail/models/res_partner.py index 9140959f89e..0b7533c853d 100644 --- a/addons/mail/models/res_partner.py +++ b/addons/mail/models/res_partner.py @@ -18,7 +18,6 @@ class Partner(models.Model): _name = "res.partner" _inherit = ['res.partner', 'mail.thread', 'mail.activity.mixin'] _mail_flat_thread = False - _mail_mass_mailing = _('Customers') message_bounce = fields.Integer('Bounce', help="Counter of the number of bounced emails for this contact", default=0) opt_out = fields.Boolean( diff --git a/addons/mass_mailing/controllers/main.py b/addons/mass_mailing/controllers/main.py index b2e77c2875e..bcee631aad6 100644 --- a/addons/mass_mailing/controllers/main.py +++ b/addons/mass_mailing/controllers/main.py @@ -17,7 +17,7 @@ class MassMailController(http.Controller): if mailing.exists(): res_id = res_id and int(res_id) res_ids = [] - if mailing.mailing_model == 'mail.mass_mailing.contact': + if mailing.mailing_model_name == 'mail.mass_mailing.contact': contacts = request.env['mail.mass_mailing.contact'].sudo().search([ ('email', '=', email), ('list_id', 'in', [mailing_list.id for mailing_list in mailing.contact_list_ids]) diff --git a/addons/mass_mailing/data/mass_mailing_demo.xml b/addons/mass_mailing/data/mass_mailing_demo.xml index 0afa9973dd0..347a04deaf1 100644 --- a/addons/mass_mailing/data/mass_mailing_demo.xml +++ b/addons/mass_mailing/data/mass_mailing_demo.xml @@ -64,7 +64,7 @@ done - res.partner + [('customer', '=', True)] email ]]> @@ -77,7 +77,7 @@ Second Newsletter draft - res.partner + [('customer', '=', True)] email ]]> diff --git a/addons/mass_mailing/models/mass_mailing.py b/addons/mass_mailing/models/mass_mailing.py index 147f5447545..7050926c56a 100644 --- a/addons/mass_mailing/models/mass_mailing.py +++ b/addons/mass_mailing/models/mass_mailing.py @@ -290,22 +290,6 @@ class MassMailing(models.Model): res['reply_to_mode'] = 'thread' return res - def _get_mailing_model(self): - res = [] - for model_name in self.env: - model = self.env[model_name] - if hasattr(model, '_mail_mass_mailing') and getattr(model, '_mail_mass_mailing'): - if hasattr(model, 'message_mass_mailing_enabled'): - res.append((model._name, model.message_mass_mailing_enabled())) - else: - res.append((model._name, model._mail_mass_mailing)) - res.append(('mail.mass_mailing.contact', _('Mail Contacts'))) - res.append(('mail.mass_mailing.list', _('Mailing List'))) - return res - - # indirections for inheritance - _mailing_model = lambda self: self._get_mailing_model() - active = fields.Boolean(default=True) email_from = fields.Char(string='From', required=True, default=lambda self: self.env['mail.message']._get_default_from()) @@ -335,7 +319,9 @@ class MassMailing(models.Model): default=lambda self: self.env['mail.message']._get_default_from()) # recipients mailing_model_real = fields.Char(compute='_compute_model', string='Recipients Real Model', default='mail.mass_mailing.contact', required=True) - mailing_model = fields.Selection(selection=_mailing_model, string='Recipients Model', default='mail.mass_mailing.list') + mailing_model_id = fields.Many2one('ir.model', string='Recipients Model', domain="[('is_mail_thread', '=', True)]", + default=lambda self: self.env.ref('mass_mailing.model_mail_mass_mailing_list').id) + mailing_model_name = fields.Char(related='mailing_model_id.model', string='Recipients Model Name') mailing_domain = fields.Char(string='Domain', oldname='domain', default=[]) contact_list_ids = fields.Many2many('mail.mass_mailing.list', 'mail_mass_mailing_list_rel', string='Mailing Lists') @@ -376,10 +362,10 @@ class MassMailing(models.Model): for mass_mailing in self: mass_mailing.clicks_ratio = mapped_data.get(mass_mailing.id, 0) - @api.depends('mailing_model') + @api.depends('mailing_model_id') def _compute_model(self): for record in self: - record.mailing_model_real = (record.mailing_model != 'mail.mass_mailing.list') and record.mailing_model or 'mail.mass_mailing.contact' + record.mailing_model_real = (record.mailing_model_name != 'mail.mass_mailing.list') and record.mailing_model_name or 'mail.mass_mailing.contact' def _compute_statistics(self): """ Compute statistics of the mass mailing """ @@ -451,14 +437,14 @@ class MassMailing(models.Model): 'medium_id': self.mass_mailing_campaign_id.medium_id} self.update(dic) - @api.onchange('mailing_model', 'contact_list_ids') + @api.onchange('mailing_model_id', 'contact_list_ids') def _onchange_model_and_list(self): - if self.mailing_model == 'mail.mass_mailing.list': + if self.mailing_model_name == 'mail.mass_mailing.list': if self.contact_list_ids: self.mailing_domain = "[('list_ids', 'in', [%s]), ('opt_out', '=', False)]" % (','.join(str(id) for id in self.contact_list_ids.ids),) else: self.mailing_domain = "[(0, '=', 1)]" - elif 'opt_out' in self.env[self.mailing_model]._fields and not self.mailing_domain: + elif 'opt_out' in self.env[self.mailing_model_name]._fields and not self.mailing_domain: self.mailing_domain = "[('opt_out', '=', False)]" self.body_html = "on_change_model_and_list" diff --git a/addons/mass_mailing/views/mass_mailing_views.xml b/addons/mass_mailing/views/mass_mailing_views.xml index 6eba6d8573b..7708a620335 100644 --- a/addons/mass_mailing/views/mass_mailing_views.xml +++ b/addons/mass_mailing/views/mass_mailing_views.xml @@ -413,15 +413,16 @@
-