# -*- 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 from openerp import tools from openerp.tools.translate import _ from openerp.osv import osv, fields class MassMailingCampaign(osv.Model): """Model of mass mailing campaigns. """ _name = "mail.mass_mailing.campaign" _description = 'Mass Mailing Campaign' def _get_statistics(self, cr, uid, ids, name, arg, context=None): """ Compute statistics of the mass mailing campaign """ results = dict.fromkeys(ids, False) for campaign in self.browse(cr, uid, ids, context=context): results[campaign.id] = { 'sent': len(campaign.statistics_ids), # delivered: shouldn't be: all mails - (failed + bounced) ? 'delivered': len([stat for stat in campaign.statistics_ids if not stat.bounced]), # stat.state == 'sent' and 'opened': len([stat for stat in campaign.statistics_ids if stat.opened]), 'replied': len([stat for stat in campaign.statistics_ids if stat.replied]), 'bounced': len([stat for stat in campaign.statistics_ids if stat.bounced]), } return results def _get_mass_mailing_kanban_ids(self, cr, uid, ids, name, arg, context=None): results = dict.fromkeys(ids, '') for campaign in self.browse(cr, uid, ids, context=context): mass_mailing_results = [] for mass_mailing in campaign.mass_mailing_ids: mass_mailing_object = {} for attr in ['name', 'sent', 'delivered', 'opened', 'replied', 'bounced']: mass_mailing_object[attr] = getattr(mass_mailing, attr) mass_mailing_results.append(mass_mailing_object) results[campaign.id] = mass_mailing_results return results _columns = { 'name': fields.char( 'Campaign Name', required=True, ), 'user_id': fields.many2one( 'res.users', 'Responsible', required=True, ), 'mass_mailing_ids': fields.one2many( 'mail.mass_mailing', 'mass_mailing_campaign_id', 'Mass Mailings', ), 'mass_mailing_kanban_ids': fields.function( _get_mass_mailing_kanban_ids, type='text', string='Mass Mailings (kanban data)', help='This field has for purpose to gather data about mass mailings to display them in kanban view as nested kanban views is not possible currently', ), 'statistics_ids': fields.one2many( 'mail.mail.statistics', 'mass_mailing_campaign_id', 'Sent Emails', ), 'color': fields.integer('Color Index'), # stat fields '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' ), } # _defaults = { # 'user_id': lambda self, cr, uid, ctx=None: uid, # }, def launch_mass_mailing_create_wizard(self, cr, uid, ids, context=None): ctx = dict(context) ctx.update({ 'default_mass_mailing_campaign_id': ids[0], }) return { 'name': _('Create a Mass Mailing for the Campaign'), 'type': 'ir.actions.act_window', 'view_type': 'form', 'view_mode': 'form', 'res_model': 'mail.mass_mailing.create', 'views': [(False, 'form')], 'view_id': False, 'target': 'new', 'context': ctx, } class MassMailing(osv.Model): """ MassMailing models a wave of emails for a mass mailign campaign. A mass mailing is an occurence of sending emails. """ _name = 'mail.mass_mailing' _description = 'Wave of sending emails' # number of periods for tracking mail_mail statistics _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) for group in group_obj: group_begin_date = datetime.strptime(group['__domain'][0][2], tools.DEFAULT_SERVER_DATE_FORMAT).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_monthly_statistics(self, cr, uid, ids, field_name, arg, context=None): """ TODO """ obj = self.pool['mail.mail.statistics'] res = {} context['datetime_format'] = { 'opened': { 'interval': 'day', 'groupby_format': 'yyyy-mm-dd', 'display_format': 'dd MMMM YYYY' }, 'replied': { 'interval': 'day', 'groupby_format': 'yyyy-mm-dd', 'display_format': 'dd MMMM YYYY' }, } for id in ids: res[id] = {} date_begin = self.browse(cr, uid, id, context=context).date domain = [('mass_mailing_id', '=', id), ('opened', '>=', date_begin)] res[id]['opened_monthly'] = self.__get_bar_values(cr, uid, id, obj, domain, ['opened'], 'opened_count', 'opened', context=context) domain = [('mass_mailing_id', '=', id), ('replied', '>=', date_begin)] res[id]['replied_monthly'] = self.__get_bar_values(cr, uid, id, obj, domain, ['replied'], 'replied_count', 'replied', context=context) return res def _get_statistics(self, cr, uid, ids, name, arg, context=None): """ Compute statistics of the mass mailing campaign """ results = dict.fromkeys(ids, False) for mass_mailing in self.browse(cr, uid, ids, context=context): results[mass_mailing.id] = { 'sent': len(mass_mailing.statistics_ids), 'delivered': len([stat for stat in mass_mailing.statistics_ids if not stat.bounced]), # mail.state == 'sent' and 'opened': len([stat for stat in mass_mailing.statistics_ids if stat.opened]), 'replied': len([stat for stat in mass_mailing.statistics_ids if stat.replied]), 'bounced': len([stat for stat in mass_mailing.statistics_ids if stat.bounced]), } return results _columns = { 'name': fields.char('Name', required=True), 'mass_mailing_campaign_id': fields.many2one( 'mail.mass_mailing.campaign', 'Mass Mailing Campaign', ondelete='cascade', required=True, ), 'template_id': fields.many2one( 'email.template', 'Email Template', ondelete='set null', ), 'domain': fields.char('Domain'), 'date': fields.datetime('Date'), 'color': fields.related( 'mass_mailing_campaign_id', 'color', type='integer', string='Color Index', ), # statistics data 'statistics_ids': fields.one2many( 'mail.mail.statistics', 'mass_mailing_id', 'Send Emails', ), '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='Bounce', type='integer', multi='_get_statistics' ), # monthly ratio 'opened_monthly': fields.function( _get_monthly_statistics, string='Opened', type='char', multi='_get_monthly_statistics', ), 'replied_monthly': fields.function( _get_monthly_statistics, string='Replied', type='char', multi='_get_monthly_statistics', ), } _defaults = { 'date': fields.datetime.now(), } 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', required=True, ), # 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, ), 'template_id': fields.related( 'mass_mailing_id', 'template_id', type='many2one', ondelete='set null', relation='email.template', string='Email Template', store=True, readonly=True, ), # Bounce and tracking 'opened': fields.datetime( 'Opened', help='Date when this email has been opened for 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.' ), } def set_opened(self, cr, uid, ids=None, mail_mail_ids=None, mail_message_ids=None, context=None): """ Set as opened """ if not ids and mail_mail_ids: ids = self.search(cr, uid, [('mail_mail_id', 'in', mail_mail_ids)], context=context) elif not ids and mail_message_ids: ids = self.search(cr, uid, [('message_id', 'in', mail_message_ids)], context=context) else: ids = [] for stat in self.browse(cr, uid, ids, context=context): if not stat.opened: self.write(cr, uid, [stat.id], {'opened': fields.datetime.now()}, context=context) return True def set_replied(self, cr, uid, ids=None, mail_mail_ids=None, mail_message_ids=None, context=None): """ Set as replied """ if not ids and mail_mail_ids: ids = self.search(cr, uid, [('mail_mail_id', 'in', mail_mail_ids)], context=context) elif not ids and mail_message_ids: ids = self.search(cr, uid, [('message_id', 'in', mail_message_ids)], context=context) else: ids = [] for stat in self.browse(cr, uid, ids, context=context): if not stat.replied: self.write(cr, uid, [stat.id], {'replied': fields.datetime.now()}, context=context) return True def set_bounced(self, cr, uid, ids=None, mail_mail_ids=None, mail_message_ids=None, context=None): """ Set as bounced """ if not ids and mail_mail_ids: ids = self.search(cr, uid, [('mail_mail_id', 'in', mail_mail_ids)], context=context) elif not ids and mail_message_ids: ids = self.search(cr, uid, [('message_id', 'in', mail_message_ids)], context=context) else: ids = [] for stat in self.browse(cr, uid, ids, context=context): if not stat.bounced: self.write(cr, uid, [stat.id], {'bounced': fields.datetime.now()}, context=context) return True