[MERGE] forward port of branch 8.0 up to 491372e
[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 = obj._fields.get(groupby_field.split(':')[0])
305         pattern = tools.DEFAULT_SERVER_DATE_FORMAT if field.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         'keep_archives': fields.boolean('Keep Archives'),
386         'mass_mailing_campaign_id': fields.many2one(
387             'mail.mass_mailing.campaign', 'Mass Mailing Campaign',
388             ondelete='set null',
389         ),
390         'state': fields.selection(
391             [('draft', 'Draft'), ('test', 'Tested'), ('done', 'Sent')],
392             string='Status', required=True, copy=False,
393         ),
394         'color': fields.related(
395             'mass_mailing_campaign_id', 'color',
396             type='integer', string='Color Index',
397         ),
398         # mailing options
399         'reply_to_mode': fields.selection(
400             [('thread', 'In Document'), ('email', 'Specified Email Address')],
401             string='Reply-To Mode', required=True,
402         ),
403         'reply_to': fields.char('Reply To', help='Preferred Reply-To Address'),
404         # recipients
405         'mailing_model': fields.selection(_mailing_model, string='Recipients Model', required=True),
406         'mailing_domain': fields.char('Domain', oldname='domain'),
407         'contact_list_ids': fields.many2many(
408             'mail.mass_mailing.list', 'mail_mass_mailing_list_rel',
409             string='Mailing Lists',
410         ),
411         'contact_ab_pc': fields.integer(
412             'AB Testing percentage',
413             help='Percentage of the contacts that will be mailed. Recipients will be taken randomly.'
414         ),
415         # statistics data
416         'statistics_ids': fields.one2many(
417             'mail.mail.statistics', 'mass_mailing_id',
418             'Emails Statistics',
419         ),
420         'total': fields.function(
421             _get_statistics, string='Total',
422             type='integer', multi='_get_statistics',
423         ),
424         'scheduled': fields.function(
425             _get_statistics, string='Scheduled',
426             type='integer', multi='_get_statistics',
427         ),
428         'failed': fields.function(
429             _get_statistics, string='Failed',
430             type='integer', multi='_get_statistics',
431         ),
432         'sent': fields.function(
433             _get_statistics, string='Sent',
434             type='integer', multi='_get_statistics',
435         ),
436         'delivered': fields.function(
437             _get_statistics, string='Delivered',
438             type='integer', multi='_get_statistics',
439         ),
440         'opened': fields.function(
441             _get_statistics, string='Opened',
442             type='integer', multi='_get_statistics',
443         ),
444         'replied': fields.function(
445             _get_statistics, string='Replied',
446             type='integer', multi='_get_statistics',
447         ),
448         'bounced': fields.function(
449             _get_statistics, string='Bounced',
450             type='integer', multi='_get_statistics',
451         ),
452         'received_ratio': fields.function(
453             _get_statistics, string='Received Ratio',
454             type='integer', multi='_get_statistics',
455         ),
456         'opened_ratio': fields.function(
457             _get_statistics, string='Opened Ratio',
458             type='integer', multi='_get_statistics',
459         ),
460         'replied_ratio': fields.function(
461             _get_statistics, string='Replied Ratio',
462             type='integer', multi='_get_statistics',
463         ),
464         # daily ratio
465         'opened_daily': fields.function(
466             _get_daily_statistics, string='Opened',
467             type='char', multi='_get_daily_statistics',
468         ),
469         'replied_daily': fields.function(
470             _get_daily_statistics, string='Replied',
471             type='char', multi='_get_daily_statistics',
472         )
473     }
474
475     def default_get(self, cr, uid, fields, context=None):
476         res = super(MassMailing, self).default_get(cr, uid, fields, context=context)
477         if 'reply_to_mode' in fields and not 'reply_to_mode' in res and res.get('mailing_model'):
478             if res['mailing_model'] in ['res.partner', 'mail.mass_mailing.contact']:
479                 res['reply_to_mode'] = 'email'
480             else:
481                 res['reply_to_mode'] = 'thread'
482         return res
483
484     _defaults = {
485         'state': 'draft',
486         'email_from': lambda self, cr, uid, ctx=None: self.pool['mail.message']._get_default_from(cr, uid, context=ctx),
487         'reply_to': lambda self, cr, uid, ctx=None: self.pool['mail.message']._get_default_from(cr, uid, context=ctx),
488         'mailing_model': 'mail.mass_mailing.contact',
489         'contact_ab_pc': 100,
490     }
491
492     #------------------------------------------------------
493     # Technical stuff
494     #------------------------------------------------------
495
496     def copy_data(self, cr, uid, id, default=None, context=None):
497         mailing = self.browse(cr, uid, id, context=context)
498         default = dict(default or {},
499                        name=_('%s (copy)') % mailing.name)
500         return super(MassMailing, self).copy_data(cr, uid, id, default, context=context)
501
502     def read_group(self, cr, uid, domain, fields, groupby, offset=0, limit=None, context=None, orderby=False, lazy=True):
503         """ Override read_group to always display all states. """
504         if groupby and groupby[0] == "state":
505             # Default result structure
506             # states = self._get_state_list(cr, uid, context=context)
507             states = [('draft', 'Draft'), ('test', 'Tested'), ('done', 'Sent')]
508             read_group_all_states = [{
509                 '__context': {'group_by': groupby[1:]},
510                 '__domain': domain + [('state', '=', state_value)],
511                 'state': state_value,
512                 'state_count': 0,
513             } for state_value, state_name in states]
514             # Get standard results
515             read_group_res = super(MassMailing, self).read_group(cr, uid, domain, fields, groupby, offset=offset, limit=limit, context=context, orderby=orderby)
516             # Update standard results with default results
517             result = []
518             for state_value, state_name in states:
519                 res = filter(lambda x: x['state'] == state_value, read_group_res)
520                 if not res:
521                     res = filter(lambda x: x['state'] == state_value, read_group_all_states)
522                 res[0]['state'] = [state_value, state_name]
523                 result.append(res[0])
524             return result
525         else:
526             return super(MassMailing, self).read_group(cr, uid, domain, fields, groupby, offset=offset, limit=limit, context=context, orderby=orderby)
527
528     #------------------------------------------------------
529     # Views & Actions
530     #------------------------------------------------------
531
532     def on_change_model_and_list(self, cr, uid, ids, mailing_model, list_ids, context=None):
533         value = {}
534         if mailing_model == 'mail.mass_mailing.contact':
535             mailing_list_ids = set()
536             for item in list_ids:
537                 if isinstance(item, (int, long)):
538                     mailing_list_ids.add(item)
539                 elif len(item) == 3:
540                     mailing_list_ids |= set(item[2])
541             if mailing_list_ids:
542                 value['mailing_domain'] = "[('list_id', 'in', %s)]" % list(mailing_list_ids)
543             else:
544                 value['mailing_domain'] = "[('list_id', '=', False)]"
545         else:
546             value['mailing_domain'] = False
547         return {'value': value}
548
549     def action_duplicate(self, cr, uid, ids, context=None):
550         copy_id = None
551         for mid in ids:
552             copy_id = self.copy(cr, uid, mid, context=context)
553         if copy_id:
554             return {
555                 'type': 'ir.actions.act_window',
556                 'view_type': 'form',
557                 'view_mode': 'form',
558                 'res_model': 'mail.mass_mailing',
559                 'res_id': copy_id,
560                 'context': context,
561             }
562         return False
563
564     def action_test_mailing(self, cr, uid, ids, context=None):
565         ctx = dict(context, default_mass_mailing_id=ids[0])
566         return {
567             'name': _('Test Mailing'),
568             'type': 'ir.actions.act_window',
569             'view_mode': 'form',
570             'res_model': 'mail.mass_mailing.test',
571             'target': 'new',
572             'context': ctx,
573         }
574
575     def action_edit_html(self, cr, uid, ids, context=None):
576         if not len(ids) == 1:
577             raise ValueError('One and only one ID allowed for this action')
578         mail = self.browse(cr, uid, ids[0], context=context)
579         url = '/website_mail/email_designer?model=mail.mass_mailing&res_id=%d&template_model=%s&enable_editor=1' % (ids[0], mail.mailing_model)
580         return {
581             'name': _('Open with Visual Editor'),
582             'type': 'ir.actions.act_url',
583             'url': url,
584             'target': 'self',
585         }
586
587     #------------------------------------------------------
588     # Email Sending
589     #------------------------------------------------------
590
591     def get_recipients(self, cr, uid, mailing, context=None):
592         if mailing.mailing_domain:
593             domain = eval(mailing.mailing_domain)
594             res_ids = self.pool[mailing.mailing_model].search(cr, uid, domain, context=context)
595         else:
596             res_ids = []
597             domain = [('id', 'in', res_ids)]
598
599         # randomly choose a fragment
600         if mailing.contact_ab_pc < 100:
601             contact_nbr = self.pool[mailing.mailing_model].search(cr, uid, domain, count=True, context=context)
602             topick = int(contact_nbr / 100.0 * mailing.contact_ab_pc)
603             if mailing.mass_mailing_campaign_id and mailing.mass_mailing_campaign_id.unique_ab_testing:
604                 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]
605             else:
606                 already_mailed = set([])
607             remaining = set(res_ids).difference(already_mailed)
608             if topick > len(remaining):
609                 topick = len(remaining)
610             res_ids = random.sample(remaining, topick)
611         return res_ids
612
613     def send_mail(self, cr, uid, ids, context=None):
614         author_id = self.pool['res.users'].browse(cr, uid, uid, context=context).partner_id.id
615         for mailing in self.browse(cr, uid, ids, context=context):
616             # instantiate an email composer + send emails
617             res_ids = self.get_recipients(cr, uid, mailing, context=context)
618             if not res_ids:
619                 raise Warning('Please select recipients.')
620             comp_ctx = dict(context, active_ids=res_ids)
621             composer_values = {
622                 'author_id': author_id,
623                 'body': mailing.body_html,
624                 'subject': mailing.name,
625                 'model': mailing.mailing_model,
626                 'email_from': mailing.email_from,
627                 'record_name': False,
628                 'composition_mode': 'mass_mail',
629                 'mass_mailing_id': mailing.id,
630                 'mailing_list_ids': [(4, l.id) for l in mailing.contact_list_ids],
631                 'no_auto_thread': mailing.reply_to_mode != 'thread',
632             }
633             if mailing.reply_to_mode == 'email':
634                 composer_values['reply_to'] = mailing.reply_to
635             composer_id = self.pool['mail.compose.message'].create(cr, uid, composer_values, context=comp_ctx)
636             self.pool['mail.compose.message'].send_mail(cr, uid, [composer_id], context=comp_ctx)
637             self.write(cr, uid, [mailing.id], {'sent_date': fields.datetime.now(), 'state': 'done'}, context=context)
638         return True