From 227b9a917d7a79089ad04515aa86920ebf0f5c7e Mon Sep 17 00:00:00 2001 From: Fabien Pinckaers Date: Sun, 13 Apr 2014 01:25:48 +0200 Subject: [PATCH] [IMP] Adding stats file bzr revid: fp@tinyerp.com-20140412232548-csyb7j13fyu4cwnw --- .../mass_mailing/models/mass_mailing_stats.py | 350 ++++++++++++++++++ addons/mass_mailing/views/mass_mailing.xml | 12 +- 2 files changed, 356 insertions(+), 6 deletions(-) create mode 100644 addons/mass_mailing/models/mass_mailing_stats.py diff --git a/addons/mass_mailing/models/mass_mailing_stats.py b/addons/mass_mailing/models/mass_mailing_stats.py new file mode 100644 index 00000000000..a9134c5e574 --- /dev/null +++ b/addons/mass_mailing/models/mass_mailing_stats.py @@ -0,0 +1,350 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Management Solution +# Copyright (C) 2013-today OpenERP SA () +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see +# +############################################################################## + +from datetime import datetime +from dateutil import relativedelta +import random +try: + import simplejson as json +except ImportError: + import json +import urllib +import urlparse + +from openerp import tools +from openerp.exceptions import Warning +from openerp.tools.safe_eval import safe_eval as eval +from openerp.tools.translate import _ +from openerp.osv import osv, fields + +class MassMailingList(osv.Model): + _inherit = 'mail.mass_mailing.list' + def _get_contact_nbr(self, cr, uid, ids, name, arg, context=None): + result = dict.fromkeys(ids, 0) + mlc = self.pool.get('mail.mass_mailing.contact') + for m in mlc.read_group(cr, uid, [('list_id','in',ids)], ['list_id'], ['list_id'], context=context): + result[m['list_id'][0]] = m['list_id_count'] + return result + + _columns = { + 'contact_nbr': fields.function( + _get_contact_nbr, type='integer', + string='Number of Contacts', + ), + } + + +class MassMailingCampaign(osv.Model): + _inherit = "mail.mass_mailing.campaign" + def _get_statistics(self, cr, uid, ids, name, arg, context=None): + """ Compute statistics of the mass mailing campaign """ + Statistics = self.pool['mail.mail.statistics'] + results = dict.fromkeys(ids, False) + for cid in ids: + stat_ids = Statistics.search(cr, uid, [('mass_mailing_campaign_id', '=', cid)], context=context) + stats = Statistics.browse(cr, uid, stat_ids, context=context) + results[cid] = { + 'total': len(stats), + 'failed': len([s for s in stats if not s.scheduled is False and s.sent is False and not s.exception is False]), + 'scheduled': len([s for s in stats if not s.scheduled is False and s.sent is False and s.exception is False]), + 'sent': len([s for s in stats if not s.sent is False]), + 'opened': len([s for s in stats if not s.opened is False]), + 'replied': len([s for s in stats if not s.replied is False]), + 'bounced': len([s for s in stats if not s.bounced is False]), + } + results[cid]['delivered'] = results[cid]['sent'] - results[cid]['bounced'] + results[cid]['received_ratio'] = 100.0 * results[cid]['delivered'] / (results[cid]['sent'] or 1) + results[cid]['opened_ratio'] = 100.0 * results[cid]['opened'] / (results[cid]['sent'] or 1) + results[cid]['replied_ratio'] = 100.0 * results[cid]['replied'] / (results[cid]['sent'] or 1) + return results + + _columns = { + 'total': fields.function( + _get_statistics, string='Total', + type='integer', multi='_get_statistics' + ), + 'scheduled': fields.function( + _get_statistics, string='Scheduled', + type='integer', multi='_get_statistics' + ), + 'failed': fields.function( + _get_statistics, string='Failed', + type='integer', multi='_get_statistics' + ), + 'sent': fields.function( + _get_statistics, string='Sent Emails', + type='integer', multi='_get_statistics' + ), + 'delivered': fields.function( + _get_statistics, string='Delivered', + type='integer', multi='_get_statistics', + ), + 'opened': fields.function( + _get_statistics, string='Opened', + type='integer', multi='_get_statistics', + ), + 'replied': fields.function( + _get_statistics, string='Replied', + type='integer', multi='_get_statistics' + ), + 'bounced': fields.function( + _get_statistics, string='Bounced', + type='integer', multi='_get_statistics' + ), + 'received_ratio': fields.function( + _get_statistics, string='Received Ratio', + type='integer', multi='_get_statistics', + ), + 'opened_ratio': fields.function( + _get_statistics, string='Opened Ratio', + type='integer', multi='_get_statistics', + ), + 'replied_ratio': fields.function( + _get_statistics, string='Replied Ratio', + type='integer', multi='_get_statistics', + ), + } + + +class MassMailing(osv.Model): + _inherit = 'mail.mass_mailing' + _period_number = 6 + def __get_bar_values(self, cr, uid, id, obj, domain, read_fields, value_field, groupby_field, context=None): + """ Generic method to generate data for bar chart values using SparklineBarWidget. + This method performs obj.read_group(cr, uid, domain, read_fields, groupby_field). + + :param obj: the target model (i.e. crm_lead) + :param domain: the domain applied to the read_group + :param list read_fields: the list of fields to read in the read_group + :param str value_field: the field used to compute the value of the bar slice + :param str groupby_field: the fields used to group + + :return list section_result: a list of dicts: [ + { 'value': (int) bar_column_value, + 'tootip': (str) bar_column_tooltip, + } + ] + """ + date_begin = datetime.strptime(self.browse(cr, uid, id, context=context).date, tools.DEFAULT_SERVER_DATETIME_FORMAT).date() + section_result = [{'value': 0, + 'tooltip': (date_begin + relativedelta.relativedelta(days=i)).strftime('%d %B %Y'), + } for i in range(0, self._period_number)] + group_obj = obj.read_group(cr, uid, domain, read_fields, groupby_field, context=context) + field_col_info = obj._all_columns.get(groupby_field.split(':')[0]) + pattern = tools.DEFAULT_SERVER_DATE_FORMAT if field_col_info.column._type == 'date' else tools.DEFAULT_SERVER_DATETIME_FORMAT + for group in group_obj: + group_begin_date = datetime.strptime(group['__domain'][0][2], pattern).date() + timedelta = relativedelta.relativedelta(group_begin_date, date_begin) + section_result[timedelta.days] = {'value': group.get(value_field, 0), 'tooltip': group.get(groupby_field)} + return section_result + + def _get_daily_statistics(self, cr, uid, ids, field_name, arg, context=None): + """ Get the daily statistics of the mass mailing. This is done by a grouping + on opened and replied fields. Using custom format in context, we obtain + results for the next 6 days following the mass mailing date. """ + obj = self.pool['mail.mail.statistics'] + res = {} + for id in ids: + res[id] = {} + date_begin = datetime.strptime(self.browse(cr, uid, id, context=context).date, tools.DEFAULT_SERVER_DATETIME_FORMAT) + date_end = date_begin + relativedelta.relativedelta(days=self._period_number - 1) + date_begin_str = date_begin.strftime(tools.DEFAULT_SERVER_DATETIME_FORMAT) + date_end_str = date_end.strftime(tools.DEFAULT_SERVER_DATETIME_FORMAT) + domain = [('mass_mailing_id', '=', id), ('opened', '>=', date_begin_str), ('opened', '<=', date_end_str)] + res[id]['opened_dayly'] = json.dumps(self.__get_bar_values(cr, uid, id, obj, domain, ['opened'], 'opened_count', 'opened:day', context=context)) + domain = [('mass_mailing_id', '=', id), ('replied', '>=', date_begin_str), ('replied', '<=', date_end_str)] + res[id]['replied_dayly'] = json.dumps(self.__get_bar_values(cr, uid, id, obj, domain, ['replied'], 'replied_count', 'replied:day', context=context)) + return res + + def _get_statistics(self, cr, uid, ids, name, arg, context=None): + """ Compute statistics of the mass mailing campaign """ + Statistics = self.pool['mail.mail.statistics'] + results = dict.fromkeys(ids, False) + for mid in ids: + stat_ids = Statistics.search(cr, uid, [('mass_mailing_id', '=', mid)], context=context) + stats = Statistics.browse(cr, uid, stat_ids, context=context) + results[mid] = { + 'total': len(stats), + 'failed': len([s for s in stats if not s.scheduled is False and s.sent is False and not s.exception is False]), + 'scheduled': len([s for s in stats if not s.scheduled is False and s.sent is False and s.exception is False]), + 'sent': len([s for s in stats if not s.sent is False]), + 'opened': len([s for s in stats if not s.opened is False]), + 'replied': len([s for s in stats if not s.replied is False]), + 'bounced': len([s for s in stats if not s.bounced is False]), + } + results[mid]['delivered'] = results[mid]['sent'] - results[mid]['bounced'] + results[mid]['received_ratio'] = 100.0 * results[mid]['delivered'] / (results[mid]['sent'] or 1) + results[mid]['opened_ratio'] = 100.0 * results[mid]['opened'] / (results[mid]['sent'] or 1) + results[mid]['replied_ratio'] = 100.0 * results[mid]['replied'] / (results[mid]['sent'] or 1) + return results + + # To improve + def _get_contact_nbr(self, cr, uid, ids, name, arg, context=None): + res = dict.fromkeys(ids, False) + for mailing in self.browse(cr, uid, ids, context=context): + if not mailing.mailing_domain: + res[mailing.id] = 0 + continue + print mailing.mailing_model, mailing.mailing_domain + res[mailing.id] = self.pool[mailing.mailing_model].search( + cr, uid, eval(mailing.mailing_domain), count=True, context=context + ) + return res + + _columns = { + 'contact_nbr': fields.function(_get_contact_nbr, type='integer', string='Contact Number'), + # statistics data + 'statistics_ids': fields.one2many( + 'mail.mail.statistics', 'mass_mailing_id', + 'Emails Statistics', + ), + 'total': fields.function( + _get_statistics, string='Total', + type='integer', multi='_get_statistics', + ), + 'scheduled': fields.function( + _get_statistics, string='Scheduled', + type='integer', multi='_get_statistics', + ), + 'failed': fields.function( + _get_statistics, string='Failed', + type='integer', multi='_get_statistics', + ), + 'sent': fields.function( + _get_statistics, string='Sent', + type='integer', multi='_get_statistics', + ), + 'delivered': fields.function( + _get_statistics, string='Delivered', + type='integer', multi='_get_statistics', + ), + 'opened': fields.function( + _get_statistics, string='Opened', + type='integer', multi='_get_statistics', + ), + 'replied': fields.function( + _get_statistics, string='Replied', + type='integer', multi='_get_statistics', + ), + 'bounced': fields.function( + _get_statistics, string='Bounced', + type='integer', multi='_get_statistics', + ), + 'received_ratio': fields.function( + _get_statistics, string='Received Ratio', + type='integer', multi='_get_statistics', + ), + 'opened_ratio': fields.function( + _get_statistics, string='Opened Ratio', + type='integer', multi='_get_statistics', + ), + 'replied_ratio': fields.function( + _get_statistics, string='Replied Ratio', + type='integer', multi='_get_statistics', + ), + # dayly ratio + 'opened_dayly': fields.function( + _get_daily_statistics, string='Opened', + type='char', multi='_get_daily_statistics', + oldname='opened_monthly', + ), + 'replied_dayly': fields.function( + _get_daily_statistics, string='Replied', + type='char', multi='_get_daily_statistics', + oldname='replied_monthly', + ), + } + + + + +# Merge this on emails +class MailMailStats(osv.Model): + """ MailMailStats models the statistics collected about emails. Those statistics + are stored in a separated model and table to avoid bloating the mail_mail table + with statistics values. This also allows to delete emails send with mass mailing + without loosing the statistics about them. """ + + _name = 'mail.mail.statistics' + _description = 'Email Statistics' + _rec_name = 'message_id' + _order = 'message_id' + + _columns = { + 'mail_mail_id': fields.integer( + 'Mail ID', + help='ID of the related mail_mail. This field is an integer field because' + 'the related mail_mail can be deleted separately from its statistics.' + ), + 'message_id': fields.char('Message-ID'), + 'model': fields.char('Document model'), + 'res_id': fields.integer('Document ID'), + # campaign / wave data + 'mass_mailing_id': fields.many2one( + 'mail.mass_mailing', 'Mass Mailing', + ondelete='set null', + ), + 'mass_mailing_campaign_id': fields.related( + 'mass_mailing_id', 'mass_mailing_campaign_id', + type='many2one', ondelete='set null', + relation='mail.mass_mailing.campaign', + string='Mass Mailing Campaign', + store=True, readonly=True, + ), + # Bounce and tracking + 'scheduled': fields.datetime('Scheduled', help='Date when the email has been created'), + 'sent': fields.datetime('Sent', help='Date when the email has been sent'), + 'exception': fields.datetime('Exception', help='Date of technical error leading to the email not being sent'), + 'opened': fields.datetime('Opened', help='Date when the email has been opened the first time'), + 'replied': fields.datetime('Replied', help='Date when this email has been replied for the first time.'), + 'bounced': fields.datetime('Bounced', help='Date when this email has bounced.'), + } + + _defaults = { + 'scheduled': fields.datetime.now, + } + + def _get_ids(self, cr, uid, ids=None, mail_mail_ids=None, mail_message_ids=None, domain=None, context=None): + if not ids and mail_mail_ids: + base_domain = [('mail_mail_id', 'in', mail_mail_ids)] + elif not ids and mail_message_ids: + base_domain = [('message_id', 'in', mail_message_ids)] + else: + base_domain = [('id', 'in', ids or [])] + if domain: + base_domain = ['&'] + domain + base_domain + return self.search(cr, uid, base_domain, context=context) + + def set_opened(self, cr, uid, ids=None, mail_mail_ids=None, mail_message_ids=None, context=None): + stat_ids = self._get_ids(cr, uid, ids, mail_mail_ids, mail_message_ids, [('opened', '=', False)], context) + self.write(cr, uid, stat_ids, {'opened': fields.datetime.now()}, context=context) + return stat_ids + + def set_replied(self, cr, uid, ids=None, mail_mail_ids=None, mail_message_ids=None, context=None): + stat_ids = self._get_ids(cr, uid, ids, mail_mail_ids, mail_message_ids, [('replied', '=', False)], context) + self.write(cr, uid, stat_ids, {'replied': fields.datetime.now()}, context=context) + return stat_ids + + def set_bounced(self, cr, uid, ids=None, mail_mail_ids=None, mail_message_ids=None, context=None): + stat_ids = self._get_ids(cr, uid, ids, mail_mail_ids, mail_message_ids, [('bounced', '=', False)], context) + self.write(cr, uid, stat_ids, {'bounced': fields.datetime.now()}, context=context) + return stat_ids + diff --git a/addons/mass_mailing/views/mass_mailing.xml b/addons/mass_mailing/views/mass_mailing.xml index 52f55eaed35..6e1f73a0965 100644 --- a/addons/mass_mailing/views/mass_mailing.xml +++ b/addons/mass_mailing/views/mass_mailing.xml @@ -185,13 +185,13 @@ class="oe_highlight" string="Send to All"/> +
+

+ emails are in queue + and will be sent soon. +

+
-
-

- emails are in queue - and will be sent soon. -

-