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