1 # -*- coding: utf-8 -*-
2 ##############################################################################
4 # OpenERP, Open Source Management Solution
5 # Copyright (C) 2013-today OpenERP SA (<http://www.openerp.com>)
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
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
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/>
20 ##############################################################################
22 from datetime import datetime
23 from dateutil import relativedelta
25 from openerp import tools
26 from openerp.tools.translate import _
27 from openerp.osv import osv, fields
30 class MassMailingCampaign(osv.Model):
31 """Model of mass mailing campaigns.
33 _name = "mail.mass_mailing.campaign"
34 _description = 'Mass Mailing Campaign'
35 # number of embedded mailings in kanban view
36 _kanban_mailing_nbr = 4
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, {
51 COUNT(CASE WHEN bounced is null THEN 1 ELSE null END) AS delivered,
52 COUNT(CASE WHEN opened is not null THEN 1 ELSE null END) AS opened,
53 COUNT(CASE WHEN replied is not null THEN 1 ELSE null END) AS replied ,
54 COUNT(CASE WHEN bounced is not null THEN 1 ELSE null END) AS bounced
62 for (campaign_id, sent, delivered, opened, replied, bounced) in cr.fetchall():
63 results[campaign_id] = {
65 # delivered: shouldn't be: all mails - (failed + bounced) ?
66 'delivered': delivered,
73 def _get_mass_mailing_kanban_ids(self, cr, uid, ids, name, arg, context=None):
74 """ Gather data about mass mailings to display them in kanban view as
75 nested kanban views is not possible currently. """
76 results = dict.fromkeys(ids, '')
77 for campaign_id in ids:
78 mass_mailing_results = []
79 mass_mailing_results = self.pool['mail.mass_mailing'].search_read(cr, uid,
80 domain=[('mass_mailing_campaign_id', '=', campaign_id)],
81 fields=['name', 'sent', 'delivered', 'opened', 'replied', 'bounced'],
82 limit=self._kanban_mailing_nbr,
84 results[campaign_id] = mass_mailing_results
89 'Campaign Name', required=True,
91 'user_id': fields.many2one(
92 'res.users', 'Responsible',
95 'mass_mailing_ids': fields.one2many(
96 'mail.mass_mailing', 'mass_mailing_campaign_id',
99 'mass_mailing_kanban_ids': fields.function(
100 _get_mass_mailing_kanban_ids,
101 type='text', string='Mass Mailings (kanban data)',
102 help='This field has for purpose to gather data about mass mailings '
103 'to display them in kanban view as nested kanban views is not '
104 'possible currently',
106 'statistics_ids': fields.one2many(
107 'mail.mail.statistics', 'mass_mailing_campaign_id',
110 'color': fields.integer('Color Index'),
112 'sent': fields.function(
114 string='Sent Emails',
115 type='integer', multi='_get_statistics'
117 'delivered': fields.function(
120 type='integer', multi='_get_statistics',
122 'opened': fields.function(
125 type='integer', multi='_get_statistics',
127 'replied': fields.function(
130 type='integer', multi='_get_statistics'
132 'bounced': fields.function(
135 type='integer', multi='_get_statistics'
140 'user_id': lambda self, cr, uid, ctx=None: uid,
143 def launch_mass_mailing_create_wizard(self, cr, uid, ids, context=None):
146 'default_mass_mailing_campaign_id': ids[0],
149 'name': _('Create a Mass Mailing for the Campaign'),
150 'type': 'ir.actions.act_window',
153 'res_model': 'mail.mass_mailing.create',
154 'views': [(False, 'form')],
161 class MassMailing(osv.Model):
162 """ MassMailing models a wave of emails for a mass mailign campaign.
163 A mass mailing is an occurence of sending emails. """
165 _name = 'mail.mass_mailing'
166 _description = 'Wave of sending emails'
167 # number of periods for tracking mail_mail statistics
171 def __get_bar_values(self, cr, uid, id, obj, domain, read_fields, value_field, groupby_field, context=None):
172 """ Generic method to generate data for bar chart values using SparklineBarWidget.
173 This method performs obj.read_group(cr, uid, domain, read_fields, groupby_field).
175 :param obj: the target model (i.e. crm_lead)
176 :param domain: the domain applied to the read_group
177 :param list read_fields: the list of fields to read in the read_group
178 :param str value_field: the field used to compute the value of the bar slice
179 :param str groupby_field: the fields used to group
181 :return list section_result: a list of dicts: [
182 { 'value': (int) bar_column_value,
183 'tootip': (str) bar_column_tooltip,
187 date_begin = datetime.strptime(self.browse(cr, uid, id, context=context).date, tools.DEFAULT_SERVER_DATETIME_FORMAT).date()
188 section_result = [{'value': 0,
189 'tooltip': (date_begin + relativedelta.relativedelta(days=i)).strftime('%d %B %Y'),
190 } for i in range(0, self._period_number)]
191 group_obj = obj.read_group(cr, uid, domain, read_fields, groupby_field, context=context)
192 field_col_info = obj._all_columns.get(groupby_field.split(':')[0])
193 pattern = tools.DEFAULT_SERVER_DATE_FORMAT if field_col_info.column._type == 'date' else tools.DEFAULT_SERVER_DATETIME_FORMAT
194 for group in group_obj:
195 group_begin_date = datetime.strptime(group['__domain'][0][2], pattern).date()
196 timedelta = relativedelta.relativedelta(group_begin_date, date_begin)
197 section_result[timedelta.days] = {'value': group.get(value_field, 0), 'tooltip': group.get(groupby_field)}
198 return section_result
200 def _get_daily_statistics(self, cr, uid, ids, field_name, arg, context=None):
201 """ Get the daily statistics of the mass mailing. This is done by a grouping
202 on opened and replied fields. Using custom format in context, we obtain
203 results for the next 6 days following the mass mailing date. """
204 obj = self.pool['mail.mail.statistics']
208 date_begin = datetime.strptime(self.browse(cr, uid, id, context=context).date, tools.DEFAULT_SERVER_DATETIME_FORMAT)
209 date_end = date_begin + relativedelta.relativedelta(days=self._period_number - 1)
210 date_begin_str = date_begin.strftime(tools.DEFAULT_SERVER_DATETIME_FORMAT)
211 date_end_str = date_end.strftime(tools.DEFAULT_SERVER_DATETIME_FORMAT)
212 domain = [('mass_mailing_id', '=', id), ('opened', '>=', date_begin_str), ('opened', '<=', date_end_str)]
213 res[id]['opened_monthly'] = self.__get_bar_values(cr, uid, id, obj, domain, ['opened'], 'opened_count', 'opened:day', context=context)
214 domain = [('mass_mailing_id', '=', id), ('replied', '>=', date_begin_str), ('replied', '<=', date_end_str)]
215 res[id]['replied_monthly'] = self.__get_bar_values(cr, uid, id, obj, domain, ['replied'], 'replied_count', 'replied:day', context=context)
218 def _get_statistics(self, cr, uid, ids, name, arg, context=None):
219 """ Compute statistics of the mass mailing """
220 results = dict.fromkeys(ids, {
231 COUNT(CASE WHEN bounced is null THEN 1 ELSE null END) AS delivered,
232 COUNT(CASE WHEN opened is not null THEN 1 ELSE null END) AS opened,
233 COUNT(CASE WHEN replied is not null THEN 1 ELSE null END) AS replied ,
234 COUNT(CASE WHEN bounced is not null THEN 1 ELSE null END) AS bounced
238 mass_mailing_id IN %s
242 for (campaign_id, sent, delivered, opened, replied, bounced) in cr.fetchall():
243 results[campaign_id] = {
245 # delivered: shouldn't be: all mails - (failed + bounced) ?
246 'delivered': delivered,
254 'name': fields.char('Name', required=True),
255 'mass_mailing_campaign_id': fields.many2one(
256 'mail.mass_mailing.campaign', 'Mass Mailing Campaign',
257 ondelete='cascade', required=True,
259 'template_id': fields.many2one(
260 'email.template', 'Email Template',
263 'domain': fields.char('Domain'),
264 'date': fields.datetime('Date'),
265 'color': fields.related(
266 'mass_mailing_campaign_id', 'color',
267 type='integer', string='Color Index',
270 'statistics_ids': fields.one2many(
271 'mail.mail.statistics', 'mass_mailing_id',
274 'sent': fields.function(
276 string='Sent Emails',
277 type='integer', multi='_get_statistics'
279 'delivered': fields.function(
282 type='integer', multi='_get_statistics',
284 'opened': fields.function(
287 type='integer', multi='_get_statistics',
289 'replied': fields.function(
292 type='integer', multi='_get_statistics'
294 'bounced': fields.function(
297 type='integer', multi='_get_statistics'
300 'opened_monthly': fields.function(
301 _get_daily_statistics,
303 type='char', multi='_get_daily_statistics',
305 'replied_monthly': fields.function(
306 _get_daily_statistics,
308 type='char', multi='_get_daily_statistics',
313 'date': fields.datetime.now,
317 class MailMailStats(osv.Model):
318 """ MailMailStats models the statistics collected about emails. Those statistics
319 are stored in a separated model and table to avoid bloating the mail_mail table
320 with statistics values. This also allows to delete emails send with mass mailing
321 without loosing the statistics about them. """
323 _name = 'mail.mail.statistics'
324 _description = 'Email Statistics'
325 _rec_name = 'message_id'
326 _order = 'message_id'
329 'mail_mail_id': fields.integer(
331 help='ID of the related mail_mail. This field is an integer field because'
332 'the related mail_mail can be deleted separately from its statistics.'
334 'message_id': fields.char(
337 'model': fields.char(
340 'res_id': fields.integer(
343 # campaign / wave data
344 'mass_mailing_id': fields.many2one(
345 'mail.mass_mailing', 'Mass Mailing',
348 'mass_mailing_campaign_id': fields.related(
349 'mass_mailing_id', 'mass_mailing_campaign_id',
350 type='many2one', ondelete='set null',
351 relation='mail.mass_mailing.campaign',
352 string='Mass Mailing Campaign',
353 store=True, readonly=True,
355 'template_id': fields.related(
356 'mass_mailing_id', 'template_id',
357 type='many2one', ondelete='set null',
358 relation='email.template',
359 string='Email Template',
360 store=True, readonly=True,
362 # Bounce and tracking
363 'opened': fields.datetime(
365 help='Date when this email has been opened for the first time.'),
366 'replied': fields.datetime(
368 help='Date when this email has been replied for the first time.'),
369 'bounced': fields.datetime(
371 help='Date when this email has bounced.'
375 def set_opened(self, cr, uid, ids=None, mail_mail_ids=None, mail_message_ids=None, context=None):
376 """ Set as opened """
377 if not ids and mail_mail_ids:
378 ids = self.search(cr, uid, [('mail_mail_id', 'in', mail_mail_ids)], context=context)
379 elif not ids and mail_message_ids:
380 ids = self.search(cr, uid, [('message_id', 'in', mail_message_ids)], context=context)
383 for stat in self.browse(cr, uid, ids, context=context):
385 self.write(cr, uid, [stat.id], {'opened': fields.datetime.now()}, context=context)
388 def set_replied(self, cr, uid, ids=None, mail_mail_ids=None, mail_message_ids=None, context=None):
389 """ Set as replied """
390 if not ids and mail_mail_ids:
391 ids = self.search(cr, uid, [('mail_mail_id', 'in', mail_mail_ids)], context=context)
392 elif not ids and mail_message_ids:
393 ids = self.search(cr, uid, [('message_id', 'in', mail_message_ids)], context=context)
396 for stat in self.browse(cr, uid, ids, context=context):
398 self.write(cr, uid, [stat.id], {'replied': fields.datetime.now()}, context=context)
401 def set_bounced(self, cr, uid, ids=None, mail_mail_ids=None, mail_message_ids=None, context=None):
402 """ Set as bounced """
403 if not ids and mail_mail_ids:
404 ids = self.search(cr, uid, [('mail_mail_id', 'in', mail_mail_ids)], context=context)
405 elif not ids and mail_message_ids:
406 ids = self.search(cr, uid, [('message_id', 'in', mail_message_ids)], context=context)
409 for stat in self.browse(cr, uid, ids, context=context):
411 self.write(cr, uid, [stat.id], {'bounced': fields.datetime.now()}, context=context)