[MERGE] forward port of branch 8.0 up to 591e329
[odoo/odoo.git] / addons / mass_mailing / models / mass_mailing.py
1 # -*- coding: utf-8 -*-
2
3 from datetime import datetime
4 from dateutil import relativedelta
5 import json
6 import random
7
8 from openerp import tools
9 from openerp.exceptions import Warning
10 from openerp.tools.safe_eval import safe_eval as eval
11 from openerp.tools.translate import _
12 from openerp.tools import ustr
13 from openerp.osv import osv, fields
14
15
16 class MassMailingCategory(osv.Model):
17     """Model of categories of mass mailing, i.e. marketing, newsletter, ... """
18     _name = 'mail.mass_mailing.category'
19     _description = 'Mass Mailing Category'
20     _order = 'name'
21
22     _columns = {
23         'name': fields.char('Name', required=True),
24     }
25
26
27 class MassMailingContact(osv.Model):
28     """Model of a contact. This model is different from the partner model
29     because it holds only some basic information: name, email. The purpose is to
30     be able to deal with large contact list to email without bloating the partner
31     base."""
32     _name = 'mail.mass_mailing.contact'
33     _inherit = 'mail.thread'
34     _description = 'Mass Mailing Contact'
35     _order = 'email'
36     _rec_name = 'email'
37
38     _columns = {
39         'name': fields.char('Name'),
40         'email': fields.char('Email', required=True),
41         'create_date': fields.datetime('Create Date'),
42         'list_id': fields.many2one(
43             'mail.mass_mailing.list', string='Mailing List',
44             ondelete='cascade', required=True,
45         ),
46         'opt_out': fields.boolean('Opt Out', help='The contact has chosen not to receive mails anymore from this list'),
47         'unsubscription_date': fields.datetime('Unsubscription Date'),
48         'message_bounce': fields.integer('Bounce', help='Counter of the number of bounced emails for this contact.'),
49     }
50
51     def _get_latest_list(self, cr, uid, context={}):
52         lid = self.pool.get('mail.mass_mailing.list').search(cr, uid, [], limit=1, order='id desc', context=context)
53         return lid and lid[0] or False
54
55     _defaults = {
56         'list_id': _get_latest_list
57     }
58
59     def on_change_opt_out(self, cr, uid, id, opt_out, context=None):
60         return {'value': {
61             'unsubscription_date': opt_out and fields.datetime.now() or False,
62         }}
63
64     def create(self, cr, uid, vals, context=None):
65         if 'opt_out' in vals:
66             vals['unsubscription_date'] = vals['opt_out'] and fields.datetime.now() or False
67         return super(MassMailingContact, self).create(cr, uid, vals, context=context)
68
69     def write(self, cr, uid, ids, vals, context=None):
70         if 'opt_out' in vals:
71             vals['unsubscription_date'] = vals['opt_out'] and fields.datetime.now() or False
72         return super(MassMailingContact, self).write(cr, uid, ids, vals, context=context)
73
74     def get_name_email(self, name, context):
75         name, email = self.pool['res.partner']._parse_partner_name(name, context=context)
76         if name and not email:
77             email = name
78         if email and not name:
79             name = email
80         return name, email
81
82     def name_create(self, cr, uid, name, context=None):
83         name, email = self.get_name_email(name, context=context)
84         rec_id = self.create(cr, uid, {'name': name, 'email': email}, context=context)
85         return self.name_get(cr, uid, [rec_id], context)[0]
86
87     def add_to_list(self, cr, uid, name, list_id, context=None):
88         name, email = self.get_name_email(name, context=context)
89         rec_id = self.create(cr, uid, {'name': name, 'email': email, 'list_id': list_id}, context=context)
90         return self.name_get(cr, uid, [rec_id], context)[0]
91
92     def message_get_default_recipients(self, cr, uid, ids, context=None):
93         res = {}
94         for record in self.browse(cr, uid, ids, context=context):
95             res[record.id] = {'partner_ids': [], 'email_to': record.email, 'email_cc': False}
96         return res
97
98     def message_receive_bounce(self, cr, uid, ids, mail_id=None, context=None):
99         """Called by ``message_process`` when a bounce email (such as Undelivered
100         Mail Returned to Sender) is received for an existing thread. As contacts
101         do not inherit form mail.thread, we have to define this method to be able
102         to track bounces (see mail.thread for more details). """
103         for obj in self.browse(cr, uid, ids, context=context):
104             self.write(cr, uid, [obj.id], {'message_bounce': obj.message_bounce + 1}, context=context)
105
106
107 class MassMailingList(osv.Model):
108     """Model of a contact list. """
109     _name = 'mail.mass_mailing.list'
110     _order = 'name'
111     _description = 'Mailing List'
112
113     def _get_contact_nbr(self, cr, uid, ids, name, arg, context=None):
114         result = dict.fromkeys(ids, 0)
115         Contacts = self.pool.get('mail.mass_mailing.contact')
116         for group in Contacts.read_group(cr, uid, [('list_id', 'in', ids), ('opt_out', '!=', True)], ['list_id'], ['list_id'], context=context):
117             result[group['list_id'][0]] = group['list_id_count']
118         return result
119
120     _columns = {
121         'name': fields.char('Mailing List', required=True),
122         'create_date': fields.datetime('Creation Date'),
123         'contact_nbr': fields.function(
124             _get_contact_nbr, type='integer',
125             string='Number of Contacts',
126         ),
127     }
128
129
130 class MassMailingStage(osv.Model):
131     """Stage for mass mailing campaigns. """
132     _name = 'mail.mass_mailing.stage'
133     _description = 'Mass Mailing Campaign Stage'
134     _order = 'sequence'
135
136     _columns = {
137         'name': fields.char('Name', required=True, translate=True),
138         'sequence': fields.integer('Sequence'),
139     }
140
141     _defaults = {
142         'sequence': 0,
143     }
144
145
146 class MassMailingCampaign(osv.Model):
147     """Model of mass mailing campaigns. """
148     _name = "mail.mass_mailing.campaign"
149     _description = 'Mass Mailing Campaign'
150
151     def _get_statistics(self, cr, uid, ids, name, arg, context=None):
152         """ Compute statistics of the mass mailing campaign """
153         results = {}
154         cr.execute("""
155             SELECT
156                 c.id as campaign_id,
157                 COUNT(s.id) AS total,
158                 COUNT(CASE WHEN s.sent is not null THEN 1 ELSE null END) AS sent,
159                 COUNT(CASE WHEN s.scheduled is not null AND s.sent is null AND s.exception is null THEN 1 ELSE null END) AS scheduled,
160                 COUNT(CASE WHEN s.scheduled is not null AND s.sent is null AND s.exception is not null THEN 1 ELSE null END) AS failed,
161                 COUNT(CASE WHEN s.id is not null AND s.bounced is null THEN 1 ELSE null END) AS delivered,
162                 COUNT(CASE WHEN s.opened is not null THEN 1 ELSE null END) AS opened,
163                 COUNT(CASE WHEN s.replied is not null THEN 1 ELSE null END) AS replied ,
164                 COUNT(CASE WHEN s.bounced is not null THEN 1 ELSE null END) AS bounced
165             FROM
166                 mail_mail_statistics s
167             RIGHT JOIN
168                 mail_mass_mailing_campaign c
169                 ON (c.id = s.mass_mailing_campaign_id)
170             WHERE
171                 c.id IN %s
172             GROUP BY
173                 c.id
174         """, (tuple(ids), ))
175         for row in cr.dictfetchall():
176             results[row.pop('campaign_id')] = row
177             total = row['total'] or 1
178             row['delivered'] = row['sent'] - row['bounced']
179             row['received_ratio'] = 100.0 * row['delivered'] / total
180             row['opened_ratio'] = 100.0 * row['opened'] / total
181             row['replied_ratio'] = 100.0 * row['replied'] / total
182         return results
183
184     _columns = {
185         'name': fields.char('Name', required=True),
186         'stage_id': fields.many2one('mail.mass_mailing.stage', 'Stage', required=True),
187         'user_id': fields.many2one(
188             'res.users', 'Responsible',
189             required=True,
190         ),
191         'category_ids': fields.many2many(
192             'mail.mass_mailing.category', 'mail_mass_mailing_category_rel',
193             'category_id', 'campaign_id', string='Categories'),
194         'mass_mailing_ids': fields.one2many(
195             'mail.mass_mailing', 'mass_mailing_campaign_id',
196             'Mass Mailings',
197         ),
198         'unique_ab_testing': fields.boolean(
199             'AB Testing',
200             help='If checked, recipients will be mailed only once, allowing to send'
201                  'various mailings in a single campaign to test the effectiveness'
202                  'of the mailings.'),
203         'color': fields.integer('Color Index'),
204         # stat fields
205         'total': fields.function(
206             _get_statistics, string='Total',
207             type='integer', multi='_get_statistics'
208         ),
209         'scheduled': fields.function(
210             _get_statistics, string='Scheduled',
211             type='integer', multi='_get_statistics'
212         ),
213         'failed': fields.function(
214             _get_statistics, string='Failed',
215             type='integer', multi='_get_statistics'
216         ),
217         'sent': fields.function(
218             _get_statistics, string='Sent Emails',
219             type='integer', multi='_get_statistics'
220         ),
221         'delivered': fields.function(
222             _get_statistics, string='Delivered',
223             type='integer', multi='_get_statistics',
224         ),
225         'opened': fields.function(
226             _get_statistics, string='Opened',
227             type='integer', multi='_get_statistics',
228         ),
229         'replied': fields.function(
230             _get_statistics, string='Replied',
231             type='integer', multi='_get_statistics'
232         ),
233         'bounced': fields.function(
234             _get_statistics, string='Bounced',
235             type='integer', multi='_get_statistics'
236         ),
237         'received_ratio': fields.function(
238             _get_statistics, string='Received Ratio',
239             type='integer', multi='_get_statistics',
240         ),
241         'opened_ratio': fields.function(
242             _get_statistics, string='Opened Ratio',
243             type='integer', multi='_get_statistics',
244         ),
245         'replied_ratio': fields.function(
246             _get_statistics, string='Replied Ratio',
247             type='integer', multi='_get_statistics',
248         ),
249     }
250
251     def _get_default_stage_id(self, cr, uid, context=None):
252         stage_ids = self.pool['mail.mass_mailing.stage'].search(cr, uid, [], limit=1, context=context)
253         return stage_ids and stage_ids[0] or False
254
255     _defaults = {
256         'user_id': lambda self, cr, uid, ctx=None: uid,
257         'stage_id': lambda self, *args: self._get_default_stage_id(*args),
258     }
259
260     def get_recipients(self, cr, uid, ids, model=None, context=None):
261         """Return the recipients of a mailing campaign. This is based on the statistics
262         build for each mailing. """
263         Statistics = self.pool['mail.mail.statistics']
264         res = dict.fromkeys(ids, False)
265         for cid in ids:
266             domain = [('mass_mailing_campaign_id', '=', cid)]
267             if model:
268                 domain += [('model', '=', model)]
269             stat_ids = Statistics.search(cr, uid, domain, context=context)
270             res[cid] = set(stat.res_id for stat in Statistics.browse(cr, uid, stat_ids, context=context))
271         return res
272
273
274 class MassMailing(osv.Model):
275     """ MassMailing models a wave of emails for a mass mailign campaign.
276     A mass mailing is an occurence of sending emails. """
277
278     _name = 'mail.mass_mailing'
279     _description = 'Mass Mailing'
280     # number of periods for tracking mail_mail statistics
281     _period_number = 6
282     _order = 'sent_date DESC'
283
284     def __get_bar_values(self, cr, uid, obj, domain, read_fields, value_field, groupby_field, date_begin, context=None):
285         """ Generic method to generate data for bar chart values using SparklineBarWidget.
286             This method performs obj.read_group(cr, uid, domain, read_fields, groupby_field).
287
288             :param obj: the target model (i.e. crm_lead)
289             :param domain: the domain applied to the read_group
290             :param list read_fields: the list of fields to read in the read_group
291             :param str value_field: the field used to compute the value of the bar slice
292             :param str groupby_field: the fields used to group
293
294             :return list section_result: a list of dicts: [
295                                                 {   'value': (int) bar_column_value,
296                                                     'tootip': (str) bar_column_tooltip,
297                                                 }
298                                             ]
299         """
300         date_begin = date_begin.date()
301         section_result = [{'value': 0,
302                            'tooltip': ustr((date_begin + relativedelta.relativedelta(days=i)).strftime('%d %B %Y')),
303                            } for i in range(0, self._period_number)]
304         group_obj = obj.read_group(cr, uid, domain, read_fields, groupby_field, context=context)
305         field = obj._fields.get(groupby_field.split(':')[0])
306         pattern = tools.DEFAULT_SERVER_DATE_FORMAT if field.type == 'date' else tools.DEFAULT_SERVER_DATETIME_FORMAT
307         for group in group_obj:
308             group_begin_date = datetime.strptime(group['__domain'][0][2], pattern).date()
309             timedelta = relativedelta.relativedelta(group_begin_date, date_begin)
310             section_result[timedelta.days] = {'value': group.get(value_field, 0), 'tooltip': group.get(groupby_field)}
311         return section_result
312
313     def _get_daily_statistics(self, cr, uid, ids, field_name, arg, context=None):
314         """ Get the daily statistics of the mass mailing. This is done by a grouping
315         on opened and replied fields. Using custom format in context, we obtain
316         results for the next 6 days following the mass mailing date. """
317         obj = self.pool['mail.mail.statistics']
318         res = {}
319         for mailing in self.browse(cr, uid, ids, context=context):
320             res[mailing.id] = {}
321             date = mailing.sent_date if mailing.sent_date else mailing.create_date
322             date_begin = datetime.strptime(date, tools.DEFAULT_SERVER_DATETIME_FORMAT)
323             date_end = date_begin + relativedelta.relativedelta(days=self._period_number - 1)
324             date_begin_str = date_begin.strftime(tools.DEFAULT_SERVER_DATETIME_FORMAT)
325             date_end_str = date_end.strftime(tools.DEFAULT_SERVER_DATETIME_FORMAT)
326             domain = [('mass_mailing_id', '=', mailing.id), ('opened', '>=', date_begin_str), ('opened', '<=', date_end_str)]
327             res[mailing.id]['opened_daily'] = json.dumps(self.__get_bar_values(cr, uid, obj, domain, ['opened'], 'opened_count', 'opened:day', date_begin, context=context))
328             domain = [('mass_mailing_id', '=', mailing.id), ('replied', '>=', date_begin_str), ('replied', '<=', date_end_str)]
329             res[mailing.id]['replied_daily'] = json.dumps(self.__get_bar_values(cr, uid, obj, domain, ['replied'], 'replied_count', 'replied:day', date_begin, context=context))
330         return res
331
332     def _get_statistics(self, cr, uid, ids, name, arg, context=None):
333         """ Compute statistics of the mass mailing """
334         results = {}
335         cr.execute("""
336             SELECT
337                 m.id as mailing_id,
338                 COUNT(s.id) AS total,
339                 COUNT(CASE WHEN s.sent is not null THEN 1 ELSE null END) AS sent,
340                 COUNT(CASE WHEN s.scheduled is not null AND s.sent is null AND s.exception is null THEN 1 ELSE null END) AS scheduled,
341                 COUNT(CASE WHEN s.scheduled is not null AND s.sent is null AND s.exception is not null THEN 1 ELSE null END) AS failed,
342                 COUNT(CASE WHEN s.sent is not null AND s.bounced is null THEN 1 ELSE null END) AS delivered,
343                 COUNT(CASE WHEN s.opened is not null THEN 1 ELSE null END) AS opened,
344                 COUNT(CASE WHEN s.replied is not null THEN 1 ELSE null END) AS replied,
345                 COUNT(CASE WHEN s.bounced is not null THEN 1 ELSE null END) AS bounced
346             FROM
347                 mail_mail_statistics s
348             RIGHT JOIN
349                 mail_mass_mailing m
350                 ON (m.id = s.mass_mailing_id)
351             WHERE
352                 m.id IN %s
353             GROUP BY
354                 m.id
355         """, (tuple(ids), ))
356         for row in cr.dictfetchall():
357             results[row.pop('mailing_id')] = row
358             total = row['total'] or 1
359             row['received_ratio'] = 100.0 * row['delivered'] / total
360             row['opened_ratio'] = 100.0 * row['opened'] / total
361             row['replied_ratio'] = 100.0 * row['replied'] / total
362         return results
363
364     def _get_mailing_model(self, cr, uid, context=None):
365         res = []
366         for model_name in self.pool:
367             model = self.pool[model_name]
368             if hasattr(model, '_mail_mass_mailing') and getattr(model, '_mail_mass_mailing'):
369                 res.append((model._name, getattr(model, '_mail_mass_mailing')))
370         res.append(('mail.mass_mailing.contact', _('Mailing List')))
371         return res
372
373     # indirections for inheritance
374     _mailing_model = lambda self, *args, **kwargs: self._get_mailing_model(*args, **kwargs)
375
376     _columns = {
377         'name': fields.char('Subject', required=True),
378         'email_from': fields.char('From', required=True),
379         'create_date': fields.datetime('Creation Date'),
380         'sent_date': fields.datetime('Sent Date', oldname='date', copy=False),
381         'body_html': fields.html('Body'),
382         'attachment_ids': fields.many2many(
383             'ir.attachment', 'mass_mailing_ir_attachments_rel',
384             'mass_mailing_id', 'attachment_id', 'Attachments'
385         ),
386         'keep_archives': fields.boolean('Keep Archives'),
387         'mass_mailing_campaign_id': fields.many2one(
388             'mail.mass_mailing.campaign', 'Mass Mailing Campaign',
389             ondelete='set null',
390         ),
391         'state': fields.selection(
392             [('draft', 'Draft'), ('test', 'Tested'), ('done', 'Sent')],
393             string='Status', required=True, copy=False,
394         ),
395         'color': fields.related(
396             'mass_mailing_campaign_id', 'color',
397             type='integer', string='Color Index',
398         ),
399         # mailing options
400         'reply_to_mode': fields.selection(
401             [('thread', 'In Document'), ('email', 'Specified Email Address')],
402             string='Reply-To Mode', required=True,
403         ),
404         'reply_to': fields.char('Reply To', help='Preferred Reply-To Address'),
405         # recipients
406         'mailing_model': fields.selection(_mailing_model, string='Recipients Model', required=True),
407         'mailing_domain': fields.char('Domain', oldname='domain'),
408         'contact_list_ids': fields.many2many(
409             'mail.mass_mailing.list', 'mail_mass_mailing_list_rel',
410             string='Mailing Lists',
411         ),
412         'contact_ab_pc': fields.integer(
413             'AB Testing percentage',
414             help='Percentage of the contacts that will be mailed. Recipients will be taken randomly.'
415         ),
416         # statistics data
417         'statistics_ids': fields.one2many(
418             'mail.mail.statistics', 'mass_mailing_id',
419             'Emails Statistics',
420         ),
421         'total': fields.function(
422             _get_statistics, string='Total',
423             type='integer', multi='_get_statistics',
424         ),
425         'scheduled': fields.function(
426             _get_statistics, string='Scheduled',
427             type='integer', multi='_get_statistics',
428         ),
429         'failed': fields.function(
430             _get_statistics, string='Failed',
431             type='integer', multi='_get_statistics',
432         ),
433         'sent': fields.function(
434             _get_statistics, string='Sent',
435             type='integer', multi='_get_statistics',
436         ),
437         'delivered': fields.function(
438             _get_statistics, string='Delivered',
439             type='integer', multi='_get_statistics',
440         ),
441         'opened': fields.function(
442             _get_statistics, string='Opened',
443             type='integer', multi='_get_statistics',
444         ),
445         'replied': fields.function(
446             _get_statistics, string='Replied',
447             type='integer', multi='_get_statistics',
448         ),
449         'bounced': fields.function(
450             _get_statistics, string='Bounced',
451             type='integer', multi='_get_statistics',
452         ),
453         'received_ratio': fields.function(
454             _get_statistics, string='Received Ratio',
455             type='integer', multi='_get_statistics',
456         ),
457         'opened_ratio': fields.function(
458             _get_statistics, string='Opened Ratio',
459             type='integer', multi='_get_statistics',
460         ),
461         'replied_ratio': fields.function(
462             _get_statistics, string='Replied Ratio',
463             type='integer', multi='_get_statistics',
464         ),
465         # daily ratio
466         'opened_daily': fields.function(
467             _get_daily_statistics, string='Opened',
468             type='char', multi='_get_daily_statistics',
469         ),
470         'replied_daily': fields.function(
471             _get_daily_statistics, string='Replied',
472             type='char', multi='_get_daily_statistics',
473         )
474     }
475
476     def default_get(self, cr, uid, fields, context=None):
477         res = super(MassMailing, self).default_get(cr, uid, fields, context=context)
478         if 'reply_to_mode' in fields and not 'reply_to_mode' in res and res.get('mailing_model'):
479             if res['mailing_model'] in ['res.partner', 'mail.mass_mailing.contact']:
480                 res['reply_to_mode'] = 'email'
481             else:
482                 res['reply_to_mode'] = 'thread'
483         return res
484
485     _defaults = {
486         'state': 'draft',
487         'email_from': lambda self, cr, uid, ctx=None: self.pool['mail.message']._get_default_from(cr, uid, context=ctx),
488         'reply_to': lambda self, cr, uid, ctx=None: self.pool['mail.message']._get_default_from(cr, uid, context=ctx),
489         'mailing_model': 'mail.mass_mailing.contact',
490         'contact_ab_pc': 100,
491     }
492
493     #------------------------------------------------------
494     # Technical stuff
495     #------------------------------------------------------
496
497     def copy_data(self, cr, uid, id, default=None, context=None):
498         mailing = self.browse(cr, uid, id, context=context)
499         default = dict(default or {},
500                        name=_('%s (copy)') % mailing.name)
501         return super(MassMailing, self).copy_data(cr, uid, id, default, context=context)
502
503     def read_group(self, cr, uid, domain, fields, groupby, offset=0, limit=None, context=None, orderby=False, lazy=True):
504         """ Override read_group to always display all states. """
505         if groupby and groupby[0] == "state":
506             # Default result structure
507             # states = self._get_state_list(cr, uid, context=context)
508             states = [('draft', 'Draft'), ('test', 'Tested'), ('done', 'Sent')]
509             read_group_all_states = [{
510                 '__context': {'group_by': groupby[1:]},
511                 '__domain': domain + [('state', '=', state_value)],
512                 'state': state_value,
513                 'state_count': 0,
514             } for state_value, state_name in states]
515             # Get standard results
516             read_group_res = super(MassMailing, self).read_group(cr, uid, domain, fields, groupby, offset=offset, limit=limit, context=context, orderby=orderby)
517             # Update standard results with default results
518             result = []
519             for state_value, state_name in states:
520                 res = filter(lambda x: x['state'] == state_value, read_group_res)
521                 if not res:
522                     res = filter(lambda x: x['state'] == state_value, read_group_all_states)
523                 res[0]['state'] = [state_value, state_name]
524                 result.append(res[0])
525             return result
526         else:
527             return super(MassMailing, self).read_group(cr, uid, domain, fields, groupby, offset=offset, limit=limit, context=context, orderby=orderby)
528
529     #------------------------------------------------------
530     # Views & Actions
531     #------------------------------------------------------
532
533     def on_change_model_and_list(self, cr, uid, ids, mailing_model, list_ids, context=None):
534         value = {}
535         if mailing_model == 'mail.mass_mailing.contact':
536             mailing_list_ids = set()
537             for item in list_ids:
538                 if isinstance(item, (int, long)):
539                     mailing_list_ids.add(item)
540                 elif len(item) == 3:
541                     mailing_list_ids |= set(item[2])
542             if mailing_list_ids:
543                 value['mailing_domain'] = "[('list_id', 'in', %s)]" % list(mailing_list_ids)
544             else:
545                 value['mailing_domain'] = "[('list_id', '=', False)]"
546         else:
547             value['mailing_domain'] = False
548         return {'value': value}
549
550     def action_duplicate(self, cr, uid, ids, context=None):
551         copy_id = None
552         for mid in ids:
553             copy_id = self.copy(cr, uid, mid, context=context)
554         if copy_id:
555             return {
556                 'type': 'ir.actions.act_window',
557                 'view_type': 'form',
558                 'view_mode': 'form',
559                 'res_model': 'mail.mass_mailing',
560                 'res_id': copy_id,
561                 'context': context,
562             }
563         return False
564
565     def action_test_mailing(self, cr, uid, ids, context=None):
566         ctx = dict(context, default_mass_mailing_id=ids[0])
567         return {
568             'name': _('Test Mailing'),
569             'type': 'ir.actions.act_window',
570             'view_mode': 'form',
571             'res_model': 'mail.mass_mailing.test',
572             'target': 'new',
573             'context': ctx,
574         }
575
576     def action_edit_html(self, cr, uid, ids, context=None):
577         if not len(ids) == 1:
578             raise ValueError('One and only one ID allowed for this action')
579         mail = self.browse(cr, uid, ids[0], context=context)
580         url = '/website_mail/email_designer?model=mail.mass_mailing&res_id=%d&template_model=%s&enable_editor=1' % (ids[0], mail.mailing_model)
581         return {
582             'name': _('Open with Visual Editor'),
583             'type': 'ir.actions.act_url',
584             'url': url,
585             'target': 'self',
586         }
587
588     #------------------------------------------------------
589     # Email Sending
590     #------------------------------------------------------
591
592     def get_recipients(self, cr, uid, mailing, context=None):
593         if mailing.mailing_domain:
594             domain = eval(mailing.mailing_domain)
595             res_ids = self.pool[mailing.mailing_model].search(cr, uid, domain, context=context)
596         else:
597             res_ids = []
598             domain = [('id', 'in', res_ids)]
599
600         # randomly choose a fragment
601         if mailing.contact_ab_pc < 100:
602             contact_nbr = self.pool[mailing.mailing_model].search(cr, uid, domain, count=True, context=context)
603             topick = int(contact_nbr / 100.0 * mailing.contact_ab_pc)
604             if mailing.mass_mailing_campaign_id and mailing.mass_mailing_campaign_id.unique_ab_testing:
605                 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]
606             else:
607                 already_mailed = set([])
608             remaining = set(res_ids).difference(already_mailed)
609             if topick > len(remaining):
610                 topick = len(remaining)
611             res_ids = random.sample(remaining, topick)
612         return res_ids
613
614     def send_mail(self, cr, uid, ids, context=None):
615         author_id = self.pool['res.users'].browse(cr, uid, uid, context=context).partner_id.id
616         for mailing in self.browse(cr, uid, ids, context=context):
617             # instantiate an email composer + send emails
618             res_ids = self.get_recipients(cr, uid, mailing, context=context)
619             if not res_ids:
620                 raise Warning('Please select recipients.')
621             comp_ctx = dict(context, active_ids=res_ids)
622             composer_values = {
623                 'author_id': author_id,
624                 'attachment_ids': [(4, attachment.id) for attachment in mailing.attachment_ids],
625                 'body': mailing.body_html,
626                 'subject': mailing.name,
627                 'model': mailing.mailing_model,
628                 'email_from': mailing.email_from,
629                 'record_name': False,
630                 'composition_mode': 'mass_mail',
631                 'mass_mailing_id': mailing.id,
632                 'mailing_list_ids': [(4, l.id) for l in mailing.contact_list_ids],
633                 'no_auto_thread': mailing.reply_to_mode != 'thread',
634             }
635             if mailing.reply_to_mode == 'email':
636                 composer_values['reply_to'] = mailing.reply_to
637             composer_id = self.pool['mail.compose.message'].create(cr, uid, composer_values, context=comp_ctx)
638             self.pool['mail.compose.message'].send_mail(cr, uid, [composer_id], context=comp_ctx)
639             self.write(cr, uid, [mailing.id], {'sent_date': fields.datetime.now(), 'state': 'done'}, context=context)
640         return True