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