From d5bc161438954729227e5c530aba3759588b1948 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Tue, 1 Apr 2014 15:24:54 +0200 Subject: [PATCH] [IMP] mass_mailing: now allowing to mail only a part of the mailing lists, to perform AB testing. Also allows to avoid mailing contacts more than once in a given campaign. bzr revid: tde@openerp.com-20140401132454-g65utzd1zd8crpq9 --- addons/mass_mailing/models/mass_mailing.py | 85 ++++++++++++++++++---- addons/mass_mailing/views/mass_mailing.xml | 9 +++ 2 files changed, 80 insertions(+), 14 deletions(-) diff --git a/addons/mass_mailing/models/mass_mailing.py b/addons/mass_mailing/models/mass_mailing.py index 432fc4515bd..88053127dd9 100644 --- a/addons/mass_mailing/models/mass_mailing.py +++ b/addons/mass_mailing/models/mass_mailing.py @@ -21,6 +21,7 @@ from datetime import datetime from dateutil import relativedelta +import random import urllib import urlparse @@ -69,9 +70,6 @@ class MassMailingContact(osv.Model): rec_id = self.create(cr, uid, {'name': name, 'email': email}, context=context) return self.name_get(cr, uid, [rec_id], context)[0] - def name_get(self, cr, uid, ids, context=None): - return [(contact.id, '%s <%s>' % (contact.name, contact.email)) for contact in self.browse(cr, uid, ids, context=context)] - class MassMailingList(osv.Model): """Model of a contact list. """ @@ -257,10 +255,11 @@ class MassMailingCampaign(osv.Model): 'mail.mass_mailing', 'mass_mailing_campaign_id', 'Mass Mailings', ), - 'statistics_ids': fields.one2many( - 'mail.mail.statistics', 'mass_mailing_campaign_id', - 'Sent Emails', - ), + 'ab_testing': fields.boolean( + 'AB Testing', + help='If checked, recipients will be mailed only once, allowing to send' + 'various mailings in a single campaign to test the effectiveness' + 'of the mailings.'), 'color': fields.integer('Color Index'), # stat fields 'total': fields.function( @@ -338,6 +337,23 @@ class MassMailingCampaign(osv.Model): 'context': dict(context, default_mass_mailing_campaign_id=ids[0]), } + #------------------------------------------------------ + # API + #------------------------------------------------------ + + def get_recipients(self, cr, uid, ids, model=None, context=None): + """Return the recipints of a mailing campaign. This is based on the statistics + build for each mailing. """ + Statistics = self.pool['mail.mail.statistics'] + res = dict.fromkeys(ids, False) + for cid in ids: + domain = [('mass_mailing_campaign_id', '=', cid)] + if model: + domain += [('model', '=', model)] + stat_ids = Statistics.search(cr, uid, domain, context=context) + res[cid] = set(stat.res_id for stat in Statistics.browse(cr, uid, stat_ids, context=context)) + return res + class MassMailing(osv.Model): """ MassMailing models a wave of emails for a mass mailign campaign. @@ -416,13 +432,18 @@ class MassMailing(osv.Model): return results def _get_contact_nbr(self, cr, uid, ids, name, arg, context=None): - res = dict.fromkeys(ids, 0) + res = dict.fromkeys(ids, False) for mailing in self.browse(cr, uid, ids, context=context): - res[mailing.id] = self.pool[mailing.mailing_model].search( + val = {'contact_nbr': 0, 'contact_ab_nbr': 0, 'contact_ab_done': 0} + val['contact_nbr'] = self.pool[mailing.mailing_model].search( cr, uid, self.pool['mail.mass_mailing.list'].get_global_domain(cr, uid, [c.id for c in mailing.contact_list_ids], context=context)[mailing.mailing_model], count=True, context=context ) + val['contact_ab_nbr'] = int(val['contact_nbr'] * mailing.contact_ab_pc / 100.0) + if mailing.mass_mailing_campaign_id and mailing.ab_testing: + val['contact_ab_done'] = len(self.pool['mail.mass_mailing.campaign'].get_recipients(cr, uid, [mailing.mass_mailing_campaign_id.id], context=context)[mailing.mass_mailing_campaign_id.id]) + res[mailing.id] = val return res def _get_mailing_model(self, cr, uid, context=None): @@ -457,6 +478,10 @@ class MassMailing(osv.Model): 'mail.mass_mailing.campaign', 'Mass Mailing Campaign', ondelete='set null', ), + 'ab_testing': fields.related( + 'mass_mailing_campaign_id', 'ab_testing', + type='boolean', string='AB Testing' + ), 'color': fields.related( 'mass_mailing_campaign_id', 'color', type='integer', string='Color Index', @@ -471,7 +496,22 @@ class MassMailing(osv.Model): string='Mailing Lists', domain="[('model', '=', mailing_model)]", ), - 'contact_nbr': fields.function(_get_contact_nbr, type='integer', string='Contact Number'), + 'contact_nbr': fields.function( + _get_contact_nbr, type='integer', multi='_get_contact_nbr', + string='Contact Number' + ), + 'contact_ab_pc': fields.integer( + 'AB Testing percentage', + help='Percentage of the contacts that will be mailed. Recipients will be taken randomly.' + ), + 'contact_ab_nbr': fields.function( + _get_contact_nbr, type='integer', multi='_get_contact_nbr', + string='Contact Number in AB Testing' + ), + 'contact_ab_done': fields.function( + _get_contact_nbr, type='integer', multi='_get_contact_nbr', + string='Number of already mailed contacts' + ), # statistics data 'statistics_ids': fields.one2many( 'mail.mail.statistics', 'mass_mailing_id', @@ -535,6 +575,7 @@ class MassMailing(osv.Model): 'date': fields.datetime.now, 'email_from': lambda self, cr, uid, ctx=None: self.pool['mail.message']._get_default_from(cr, uid, context=ctx), 'mailing_model': 'res.partner', + 'contact_ab_pc': 100, } #------------------------------------------------------ @@ -654,6 +695,25 @@ class MassMailing(osv.Model): partners = self.pool['res.partner'].browse(cr, uid, res_ids, context=context) return dict((partner.id, {'partner_id': partner.id, 'name': partner.name, 'email': partner.email}) for partner in partners) + def get_recipients(self, cr, uid, mailing, context=None): + domain = self.pool['mail.mass_mailing.list'].get_global_domain( + cr, uid, [l.id for l in mailing.contact_list_ids], context=context + )[mailing.mailing_model] + res_ids = self.pool[mailing.mailing_model].search(cr, uid, domain, context=context) + + # randomly choose a fragment + if mailing.contact_ab_pc != 100: + topick = mailing.contact_ab_nbr + if mailing.mass_mailing_campaign_id and mailing.ab_testing: + already_mailed = self.pool['mail.mass_mailing.campaign'].get_recipients(cr, uid, [mailing.mass_mailing_campaign_id.id], context=context)[mailing.mass_mailing_campaign_id.id] + else: + already_mailed = set([]) + remaining = set(res_ids).difference(already_mailed) + if topick > len(remaining): + topick = len(remaining) + res_ids = random.sample(remaining, topick) + return res_ids + def get_unsubscribe_url(self, cr, uid, mailing_id, res_id, email, msg=None, context=None): base_url = self.pool.get('ir.config_parameter').get_param(cr, uid, 'web.base.url') url = urlparse.urljoin( @@ -674,10 +734,7 @@ class MassMailing(osv.Model): raise Warning('Please select recipients.') # get mail and recipints data - domain = self.pool['mail.mass_mailing.list'].get_global_domain( - cr, uid, [l.id for l in mailing.contact_list_ids], context=context - )[mailing.mailing_model] - res_ids = self.pool[mailing.mailing_model].search(cr, uid, domain, context=context) + res_ids = self.get_recipients(cr, uid, mailing, context=context) template_values = self.pool['mail.compose.message'].generate_email_for_composer_batch( cr, uid, mailing.template_id.id, res_ids, context=context, fields=['body_html', 'attachment_ids', 'mail_server_id']) diff --git a/addons/mass_mailing/views/mass_mailing.xml b/addons/mass_mailing/views/mass_mailing.xml index 0a22c81a3eb..aef24670b1f 100644 --- a/addons/mass_mailing/views/mass_mailing.xml +++ b/addons/mass_mailing/views/mass_mailing.xml @@ -275,6 +275,14 @@ contacts selected