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