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