[FIX] Event tour was broken by change
[odoo/odoo.git] / addons / mass_mailing / mass_mailing.py
1 # -*- coding: utf-8 -*-
2 ##############################################################################
3 #
4 #    OpenERP, Open Source Management Solution
5 #    Copyright (C) 2013-today OpenERP SA (<http://www.openerp.com>)
6 #
7 #    This program is free software: you can redistribute it and/or modify
8 #    it under the terms of the GNU Affero General Public License as
9 #    published by the Free Software Foundation, either version 3 of the
10 #    License, or (at your option) any later version
11 #
12 #    This program is distributed in the hope that it will be useful,
13 #    but WITHOUT ANY WARRANTY; without even the implied warranty of
14 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 #    GNU Affero General Public License for more details
16 #
17 #    You should have received a copy of the GNU Affero General Public License
18 #    along with this program.  If not, see <http://www.gnu.org/licenses/>
19 #
20 ##############################################################################
21
22 from datetime import datetime
23 from dateutil import relativedelta
24
25 from openerp import tools
26 from openerp.tools.translate import _
27 from openerp.osv import osv, fields
28
29
30 class MassMailingCampaign(osv.Model):
31     """Model of mass mailing campaigns.
32     """
33     _name = "mail.mass_mailing.campaign"
34     _description = 'Mass Mailing Campaign'
35     # number of embedded mailings in kanban view
36     _kanban_mailing_nbr = 4
37
38     def _get_statistics(self, cr, uid, ids, name, arg, context=None):
39         """ Compute statistics of the mass mailing campaign """
40         results = dict.fromkeys(ids, False)
41         for campaign in self.browse(cr, uid, ids, context=context):
42             results[campaign.id] = {
43                 'sent': len(campaign.statistics_ids),
44                 # delivered: shouldn't be: all mails - (failed + bounced) ?
45                 'delivered': len([stat for stat in campaign.statistics_ids if not stat.bounced]),  # stat.state == 'sent' and
46                 'opened': len([stat for stat in campaign.statistics_ids if stat.opened]),
47                 'replied': len([stat for stat in campaign.statistics_ids if stat.replied]),
48                 'bounced': len([stat for stat in campaign.statistics_ids if stat.bounced]),
49             }
50         return results
51
52     def _get_mass_mailing_kanban_ids(self, cr, uid, ids, name, arg, context=None):
53         """ Gather data about mass mailings to display them in kanban view as
54         nested kanban views is not possible currently. """
55         results = dict.fromkeys(ids, '')
56         for campaign in self.browse(cr, uid, ids, context=context):
57             mass_mailing_results = []
58             for mass_mailing in campaign.mass_mailing_ids[:self._kanban_mailing_nbr]:
59                 mass_mailing_object = {}
60                 for attr in ['name', 'sent', 'delivered', 'opened', 'replied', 'bounced']:
61                     mass_mailing_object[attr] = getattr(mass_mailing, attr)
62                 mass_mailing_results.append(mass_mailing_object)
63             results[campaign.id] = mass_mailing_results
64         return results
65
66     _columns = {
67         'name': fields.char(
68             'Campaign Name', required=True,
69         ),
70         'user_id': fields.many2one(
71             'res.users', 'Responsible',
72             required=True,
73         ),
74         'mass_mailing_ids': fields.one2many(
75             'mail.mass_mailing', 'mass_mailing_campaign_id',
76             'Mass Mailings',
77         ),
78         'mass_mailing_kanban_ids': fields.function(
79             _get_mass_mailing_kanban_ids,
80             type='text', string='Mass Mailings (kanban data)',
81             help='This field has for purpose to gather data about mass mailings '
82                  'to display them in kanban view as nested kanban views is not '
83                  'possible currently',
84         ),
85         'statistics_ids': fields.one2many(
86             'mail.mail.statistics', 'mass_mailing_campaign_id',
87             'Sent Emails',
88         ),
89         'color': fields.integer('Color Index'),
90         # stat fields
91         'sent': fields.function(
92             _get_statistics,
93             string='Sent Emails',
94             type='integer', multi='_get_statistics'
95         ),
96         'delivered': fields.function(
97             _get_statistics,
98             string='Delivered',
99             type='integer', multi='_get_statistics',
100         ),
101         'opened': fields.function(
102             _get_statistics,
103             string='Opened',
104             type='integer', multi='_get_statistics',
105         ),
106         'replied': fields.function(
107             _get_statistics,
108             string='Replied',
109             type='integer', multi='_get_statistics'
110         ),
111         'bounced': fields.function(
112             _get_statistics,
113             string='Bounced',
114             type='integer', multi='_get_statistics'
115         ),
116     }
117
118     _defaults = {
119         'user_id': lambda self, cr, uid, ctx=None: uid,
120     }
121
122     def launch_mass_mailing_create_wizard(self, cr, uid, ids, context=None):
123         ctx = dict(context)
124         ctx.update({
125             'default_mass_mailing_campaign_id': ids[0],
126         })
127         return {
128             'name': _('Create a Mass Mailing for the Campaign'),
129             'type': 'ir.actions.act_window',
130             'view_type': 'form',
131             'view_mode': 'form',
132             'res_model': 'mail.mass_mailing.create',
133             'views': [(False, 'form')],
134             'view_id': False,
135             'target': 'new',
136             'context': ctx,
137         }
138
139
140 class MassMailing(osv.Model):
141     """ MassMailing models a wave of emails for a mass mailign campaign.
142     A mass mailing is an occurence of sending emails. """
143
144     _name = 'mail.mass_mailing'
145     _description = 'Wave of sending emails'
146     # number of periods for tracking mail_mail statistics
147     _period_number = 6
148     _order = 'date DESC'
149
150     def __get_bar_values(self, cr, uid, id, obj, domain, read_fields, value_field, groupby_field, context=None):
151         """ Generic method to generate data for bar chart values using SparklineBarWidget.
152             This method performs obj.read_group(cr, uid, domain, read_fields, groupby_field).
153
154             :param obj: the target model (i.e. crm_lead)
155             :param domain: the domain applied to the read_group
156             :param list read_fields: the list of fields to read in the read_group
157             :param str value_field: the field used to compute the value of the bar slice
158             :param str groupby_field: the fields used to group
159
160             :return list section_result: a list of dicts: [
161                                                 {   'value': (int) bar_column_value,
162                                                     'tootip': (str) bar_column_tooltip,
163                                                 }
164                                             ]
165         """
166         date_begin = datetime.strptime(self.browse(cr, uid, id, context=context).date, tools.DEFAULT_SERVER_DATETIME_FORMAT).date()
167         section_result = [{'value': 0,
168                            'tooltip': (date_begin + relativedelta.relativedelta(days=i)).strftime('%d %B %Y'),
169                            } for i in range(0, self._period_number)]
170         group_obj = obj.read_group(cr, uid, domain, read_fields, groupby_field, context=context)
171         for group in group_obj:
172             group_begin_date = datetime.strptime(group['__domain'][0][2], tools.DEFAULT_SERVER_DATE_FORMAT).date()
173             timedelta = relativedelta.relativedelta(group_begin_date, date_begin)
174             section_result[timedelta.days] = {'value': group.get(value_field, 0), 'tooltip': group.get(groupby_field)}
175         return section_result
176
177     def _get_daily_statistics(self, cr, uid, ids, field_name, arg, context=None):
178         """ Get the daily statistics of the mass mailing. This is done by a grouping
179         on opened and replied fields. Using custom format in context, we obtain
180         results for the next 6 days following the mass mailing date. """
181         obj = self.pool['mail.mail.statistics']
182         res = {}
183         context['datetime_format'] = {
184             'opened': {
185                 'interval': 'day',
186                 'groupby_format': 'yyyy-mm-dd',
187                 'display_format': 'dd MMMM YYYY'
188             },
189             'replied': {
190                 'interval': 'day',
191                 'groupby_format': 'yyyy-mm-dd',
192                 'display_format': 'dd MMMM YYYY'
193             },
194         }
195         for id in ids:
196             res[id] = {}
197             date_begin = datetime.strptime(self.browse(cr, uid, id, context=context).date, tools.DEFAULT_SERVER_DATETIME_FORMAT)
198             date_end = date_begin + relativedelta.relativedelta(days=self._period_number - 1)
199             date_begin_str = date_begin.strftime(tools.DEFAULT_SERVER_DATETIME_FORMAT)
200             date_end_str = date_end.strftime(tools.DEFAULT_SERVER_DATETIME_FORMAT)
201             domain = [('mass_mailing_id', '=', id), ('opened', '>=', date_begin_str), ('opened', '<=', date_end_str)]
202             res[id]['opened_monthly'] = self.__get_bar_values(cr, uid, id, obj, domain, ['opened'], 'opened_count', 'opened', context=context)
203             domain = [('mass_mailing_id', '=', id), ('replied', '>=', date_begin_str), ('replied', '<=', date_end_str)]
204             res[id]['replied_monthly'] = self.__get_bar_values(cr, uid, id, obj, domain, ['replied'], 'replied_count', 'replied', context=context)
205         return res
206
207     def _get_statistics(self, cr, uid, ids, name, arg, context=None):
208         """ Compute statistics of the mass mailing campaign """
209         results = dict.fromkeys(ids, False)
210         for mass_mailing in self.browse(cr, uid, ids, context=context):
211             results[mass_mailing.id] = {
212                 'sent': len(mass_mailing.statistics_ids),
213                 'delivered': len([stat for stat in mass_mailing.statistics_ids if not stat.bounced]),  # mail.state == 'sent' and
214                 'opened': len([stat for stat in mass_mailing.statistics_ids if stat.opened]),
215                 'replied': len([stat for stat in mass_mailing.statistics_ids if stat.replied]),
216                 'bounced': len([stat for stat in mass_mailing.statistics_ids if stat.bounced]),
217             }
218         return results
219
220     _columns = {
221         'name': fields.char('Name', required=True),
222         'mass_mailing_campaign_id': fields.many2one(
223             'mail.mass_mailing.campaign', 'Mass Mailing Campaign',
224             ondelete='cascade', required=True,
225         ),
226         'template_id': fields.many2one(
227             'email.template', 'Email Template',
228             ondelete='set null',
229         ),
230         'domain': fields.char('Domain'),
231         'date': fields.datetime('Date'),
232         'color': fields.related(
233             'mass_mailing_campaign_id', 'color',
234             type='integer', string='Color Index',
235         ),
236         # statistics data
237         'statistics_ids': fields.one2many(
238             'mail.mail.statistics', 'mass_mailing_id',
239             'Emails Statistics',
240         ),
241         'sent': fields.function(
242             _get_statistics,
243             string='Sent Emails',
244             type='integer', multi='_get_statistics'
245         ),
246         'delivered': fields.function(
247             _get_statistics,
248             string='Delivered',
249             type='integer', multi='_get_statistics',
250         ),
251         'opened': fields.function(
252             _get_statistics,
253             string='Opened',
254             type='integer', multi='_get_statistics',
255         ),
256         'replied': fields.function(
257             _get_statistics,
258             string='Replied',
259             type='integer', multi='_get_statistics'
260         ),
261         'bounced': fields.function(
262             _get_statistics,
263             string='Bounce',
264             type='integer', multi='_get_statistics'
265         ),
266         # monthly ratio
267         'opened_monthly': fields.function(
268             _get_daily_statistics,
269             string='Opened',
270             type='char', multi='_get_daily_statistics',
271         ),
272         'replied_monthly': fields.function(
273             _get_daily_statistics,
274             string='Replied',
275             type='char', multi='_get_daily_statistics',
276         ),
277     }
278
279     _defaults = {
280         'date': fields.datetime.now,
281     }
282
283
284 class MailMailStats(osv.Model):
285     """ MailMailStats models the statistics collected about emails. Those statistics
286     are stored in a separated model and table to avoid bloating the mail_mail table
287     with statistics values. This also allows to delete emails send with mass mailing
288     without loosing the statistics about them. """
289
290     _name = 'mail.mail.statistics'
291     _description = 'Email Statistics'
292     _rec_name = 'message_id'
293     _order = 'message_id'
294
295     _columns = {
296         'mail_mail_id': fields.integer(
297             'Mail ID',
298             help='ID of the related mail_mail. This field is an integer field because'
299                  'the related mail_mail can be deleted separately from its statistics.'
300         ),
301         'message_id': fields.char(
302             'Message-ID',
303         ),
304         'model': fields.char(
305             'Document model',
306         ),
307         'res_id': fields.integer(
308             'Document ID',
309         ),
310         # campaign / wave data
311         'mass_mailing_id': fields.many2one(
312             'mail.mass_mailing', 'Mass Mailing',
313             ondelete='set null',
314         ),
315         'mass_mailing_campaign_id': fields.related(
316             'mass_mailing_id', 'mass_mailing_campaign_id',
317             type='many2one', ondelete='set null',
318             relation='mail.mass_mailing.campaign',
319             string='Mass Mailing Campaign',
320             store=True, readonly=True,
321         ),
322         'template_id': fields.related(
323             'mass_mailing_id', 'template_id',
324             type='many2one', ondelete='set null',
325             relation='email.template',
326             string='Email Template',
327             store=True, readonly=True,
328         ),
329         # Bounce and tracking
330         'opened': fields.datetime(
331             'Opened',
332             help='Date when this email has been opened for the first time.'),
333         'replied': fields.datetime(
334             'Replied',
335             help='Date when this email has been replied for the first time.'),
336         'bounced': fields.datetime(
337             'Bounced',
338             help='Date when this email has bounced.'
339         ),
340     }
341
342     def set_opened(self, cr, uid, ids=None, mail_mail_ids=None, mail_message_ids=None, context=None):
343         """ Set as opened """
344         if not ids and mail_mail_ids:
345             ids = self.search(cr, uid, [('mail_mail_id', 'in', mail_mail_ids)], context=context)
346         elif not ids and mail_message_ids:
347             ids = self.search(cr, uid, [('message_id', 'in', mail_message_ids)], context=context)
348         else:
349             ids = []
350         for stat in self.browse(cr, uid, ids, context=context):
351             if not stat.opened:
352                 self.write(cr, uid, [stat.id], {'opened': fields.datetime.now()}, context=context)
353         return ids
354
355     def set_replied(self, cr, uid, ids=None, mail_mail_ids=None, mail_message_ids=None, context=None):
356         """ Set as replied """
357         if not ids and mail_mail_ids:
358             ids = self.search(cr, uid, [('mail_mail_id', 'in', mail_mail_ids)], context=context)
359         elif not ids and mail_message_ids:
360             ids = self.search(cr, uid, [('message_id', 'in', mail_message_ids)], context=context)
361         else:
362             ids = []
363         for stat in self.browse(cr, uid, ids, context=context):
364             if not stat.replied:
365                 self.write(cr, uid, [stat.id], {'replied': fields.datetime.now()}, context=context)
366         return ids
367
368     def set_bounced(self, cr, uid, ids=None, mail_mail_ids=None, mail_message_ids=None, context=None):
369         """ Set as bounced """
370         if not ids and mail_mail_ids:
371             ids = self.search(cr, uid, [('mail_mail_id', 'in', mail_mail_ids)], context=context)
372         elif not ids and mail_message_ids:
373             ids = self.search(cr, uid, [('message_id', 'in', mail_message_ids)], context=context)
374         else:
375             ids = []
376         for stat in self.browse(cr, uid, ids, context=context):
377             if not stat.bounced:
378                 self.write(cr, uid, [stat.id], {'bounced': fields.datetime.now()}, context=context)
379         return ids