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, 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]),
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
68 'Campaign Name', required=True,
70 'user_id': fields.many2one(
71 'res.users', 'Responsible',
74 'mass_mailing_ids': fields.one2many(
75 'mail.mass_mailing', 'mass_mailing_campaign_id',
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 '
85 'statistics_ids': fields.one2many(
86 'mail.mail.statistics', 'mass_mailing_campaign_id',
89 'color': fields.integer('Color Index'),
91 'sent': fields.function(
94 type='integer', multi='_get_statistics'
96 'delivered': fields.function(
99 type='integer', multi='_get_statistics',
101 'opened': fields.function(
104 type='integer', multi='_get_statistics',
106 'replied': fields.function(
109 type='integer', multi='_get_statistics'
111 'bounced': fields.function(
114 type='integer', multi='_get_statistics'
119 'user_id': lambda self, cr, uid, ctx=None: uid,
122 def launch_mass_mailing_create_wizard(self, cr, uid, ids, context=None):
125 'default_mass_mailing_campaign_id': ids[0],
128 'name': _('Create a Mass Mailing for the Campaign'),
129 'type': 'ir.actions.act_window',
132 'res_model': 'mail.mass_mailing.create',
133 'views': [(False, 'form')],
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. """
144 _name = 'mail.mass_mailing'
145 _description = 'Wave of sending emails'
146 # number of periods for tracking mail_mail statistics
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).
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
160 :return list section_result: a list of dicts: [
161 { 'value': (int) bar_column_value,
162 'tootip': (str) bar_column_tooltip,
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
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']
183 context['datetime_format'] = {
186 'groupby_format': 'yyyy-mm-dd',
187 'display_format': 'dd MMMM YYYY'
191 'groupby_format': 'yyyy-mm-dd',
192 'display_format': 'dd MMMM YYYY'
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)
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]),
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,
226 'template_id': fields.many2one(
227 'email.template', 'Email Template',
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',
237 'statistics_ids': fields.one2many(
238 'mail.mail.statistics', 'mass_mailing_id',
241 'sent': fields.function(
243 string='Sent Emails',
244 type='integer', multi='_get_statistics'
246 'delivered': fields.function(
249 type='integer', multi='_get_statistics',
251 'opened': fields.function(
254 type='integer', multi='_get_statistics',
256 'replied': fields.function(
259 type='integer', multi='_get_statistics'
261 'bounced': fields.function(
264 type='integer', multi='_get_statistics'
267 'opened_monthly': fields.function(
268 _get_daily_statistics,
270 type='char', multi='_get_daily_statistics',
272 'replied_monthly': fields.function(
273 _get_daily_statistics,
275 type='char', multi='_get_daily_statistics',
280 'date': fields.datetime.now,
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. """
290 _name = 'mail.mail.statistics'
291 _description = 'Email Statistics'
292 _rec_name = 'message_id'
293 _order = 'message_id'
296 'mail_mail_id': fields.integer(
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.'
301 'message_id': fields.char(
304 'model': fields.char(
307 'res_id': fields.integer(
310 # campaign / wave data
311 'mass_mailing_id': fields.many2one(
312 'mail.mass_mailing', 'Mass Mailing',
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,
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,
329 # Bounce and tracking
330 'opened': fields.datetime(
332 help='Date when this email has been opened for the first time.'),
333 'replied': fields.datetime(
335 help='Date when this email has been replied for the first time.'),
336 'bounced': fields.datetime(
338 help='Date when this email has bounced.'
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)
350 for stat in self.browse(cr, uid, ids, context=context):
352 self.write(cr, uid, [stat.id], {'opened': fields.datetime.now()}, context=context)
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)
363 for stat in self.browse(cr, uid, ids, context=context):
365 self.write(cr, uid, [stat.id], {'replied': fields.datetime.now()}, context=context)
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)
376 for stat in self.browse(cr, uid, ids, context=context):
378 self.write(cr, uid, [stat.id], {'bounced': fields.datetime.now()}, context=context)