--- /dev/null
+ # -*- coding: utf-8 -*-
+ ##############################################################################
+ #
+ # OpenERP, Open Source Management Solution
+ # Copyright (C) 2013-today OpenERP SA (<http://www.openerp.com>)
+ #
+ # 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 <http://www.gnu.org/licenses/>
+ #
+ ##############################################################################
+
+ 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 MassMailingCategory(osv.Model):
+ """Model of categories of mass mailing, i.e. marketing, newsletter, ... """
+ _name = 'mail.mass_mailing.category'
+ _description = 'Mass Mailing Category'
-
++ _order = 'name'
+ _columns = {
+ 'name': fields.char('Name', required=True),
+ }
+
-
+ class MassMailingContact(osv.Model):
+ """Model of a contact. This model is different from the partner model
+ because it holds only some basic information: name, email. The purpose is to
+ be able to deal with large contact list to email without bloating the partner
- database. """
++ base."""
+ _name = 'mail.mass_mailing.contact'
+ _description = 'Mass Mailing Contact'
-
++ _order = 'email'
++ _rec_name = 'email'
+ _columns = {
- 'name': fields.char('Name', required=True),
++ 'name': fields.char('Name'),
+ 'email': fields.char('Email', required=True),
+ 'list_id': fields.many2one(
+ 'mail.mass_mailing.list', string='Mailing List',
- domain=[('model', '=', 'mail.mass_mailing.contact')],
+ ondelete='cascade',
+ ),
- 'opt_out': fields.boolean('Opt Out', help='The contact has chosen not to receive news anymore from this mailing list'),
++ 'opt_out': fields.boolean('Opt Out', help='The contact has chosen not to receive mails anymore from this list'),
+ }
+
- def name_create(self, cr, uid, name, context=None):
- name, email = self.pool['res.partner']._parse_partner_name(name, context=context)
- if name and not email:
- email = name
- if email and not name:
- name = email
- rec_id = self.create(cr, uid, {'name': name, 'email': email}, context=context)
- return self.name_get(cr, uid, [rec_id], context)[0]
-
+
+ class MassMailingList(osv.Model):
+ """Model of a contact list. """
+ _name = 'mail.mass_mailing.list'
- _description = 'Contact List'
-
- def default_get(self, cr, uid, fields, context=None):
- """Override default_get to handle active_domain coming from the list view. """
- res = super(MassMailingList, self).default_get(cr, uid, fields, context=context)
- if 'domain' in fields:
- if not 'model' in res and context.get('active_model'):
- res['model'] = context['active_model']
- if 'active_domain' in context:
- res['domain'] = '%s' % context['active_domain']
- elif 'active_ids' in context:
- res['domain'] = '%s' % [('id', 'in', context['active_ids'])]
- else:
- res['domain'] = False
- return res
++ _order = 'name'
++ _description = 'Mailing List'
+
+ def _get_contact_nbr(self, cr, uid, ids, name, arg, context=None):
+ """Compute the number of contacts linked to the mailing list. """
+ results = dict.fromkeys(ids, 0)
- for contact_list in self.browse(cr, uid, ids, context=context):
- results[contact_list.id] = self.pool[contact_list.model].search(
- cr, uid,
- self._get_domain(cr, uid, [contact_list.id], context=context)[contact_list.id],
- count=True, context=context
- )
++ mlc = self.pool.get('mail.mass_mailing.contact').
++ result = dict(lambda x: (x,0), ids)
++ for m in mlc.read_group(cr, uid, [('list_id','in',ids)], ['list_id'], ['list_id'], context=context):
++ result[m['list_id']] = m['__count']
+ return results
+
- def _get_model_list(self, cr, uid, context=None):
- return self.pool['mail.mass_mailing']._get_mailing_model(cr, uid, context=context)
-
- # indirections for inheritance
- _model_list = lambda self, *args, **kwargs: self._get_model_list(*args, **kwargs)
-
+ _columns = {
- 'name': fields.char('Name', required=True),
++ 'name': fields.char('Mailing List', required=True),
+ 'contact_nbr': fields.function(
+ _get_contact_nbr, type='integer',
+ string='Contact Number',
+ ),
- 'model': fields.selection(
- _model_list, type='char', required=True,
- string='Applies To'
- ),
- 'filter_id': fields.many2one(
- 'ir.filters', string='Custom Filter',
- domain="[('model_id.model', '=', model)]",
- ),
- 'domain': fields.text('Domain'),
+ }
+
- def on_change_model(self, cr, uid, ids, model, context=None):
- return {'value': {'filter_id': False}}
-
- def on_change_filter_id(self, cr, uid, ids, filter_id, context=None):
- values = {}
- if filter_id:
- ir_filter = self.pool['ir.filters'].browse(cr, uid, filter_id, context=context)
- values['domain'] = ir_filter.domain
- return {'value': values}
-
- def on_change_domain(self, cr, uid, ids, domain, model, context=None):
- if domain is False:
- return {'value': {'contact_nbr': 0}}
- else:
- domain = eval(domain)
- return {'value': {'contact_nbr': self.pool[model].search(cr, uid, domain, context=context, count=True)}}
-
- def create(self, cr, uid, values, context=None):
- new_id = super(MassMailingList, self).create(cr, uid, values, context=context)
- if values.get('model') == 'mail.mass_mailing.contact' and (not context or not context.get('no_contact_to_list')):
- domain = values.get('domain')
- if domain is None or domain is False:
- return new_id
- contact_ids = self.pool['mail.mass_mailing.contact'].search(cr, uid, eval(domain), context=context)
- self.pool['mail.mass_mailing.contact'].write(cr, uid, contact_ids, {'list_id': new_id}, context=context)
- self.pool['mail.mass_mailing.list'].write(cr, uid, [new_id], {'domain': [('list_id', '=', new_id)]}, context=context)
- return new_id
-
++ # TODO: remove this?
+ def action_see_records(self, cr, uid, ids, context=None):
+ contact_list = self.browse(cr, uid, ids[0], context=context)
+ ctx = dict(context)
+ ctx['search_default_not_opt_out'] = True
+ return {
+ 'name': _('See Contact List'),
+ 'type': 'ir.actions.act_window',
+ 'view_type': 'form',
+ 'view_mode': 'tree,form',
+ 'res_model': contact_list.model,
+ 'views': [(False, 'tree'), (False, 'form')],
+ 'view_id': False,
+ 'target': 'current',
+ 'context': ctx,
+ 'domain': contact_list.domain,
+ }
+
++ # TODO: remove this?
+ def action_add_to_mailing(self, cr, uid, ids, context=None):
+ mass_mailing_id = context.get('default_mass_mailing_id')
+ if not mass_mailing_id:
+ return False
+ self.pool['mail.mass_mailing'].write(cr, uid, [mass_mailing_id], {'contact_list_ids': [(4, list_id) for list_id in ids]}, context=context)
+ return {
+ 'name': _('Mass Mailing'),
+ 'type': 'ir.actions.act_window',
+ 'view_type': 'form',
+ 'view_mode': 'form',
+ 'res_model': 'mail.mass_mailing',
+ 'res_id': mass_mailing_id,
+ 'context': context,
+ }
+
- def _get_domain(self, cr, uid, ids, context=None):
- domains = {}
- for contact_list in self.browse(cr, uid, ids, context=context):
- if contact_list.domain is False or contact_list.domain is None: # domain is a string like False or None -> void list
- domain = [('id', '=', '0')]
- else:
- domain = eval(contact_list.domain)
- domains[contact_list.id] = domain
- return domains
-
- def get_global_domain(self, cr, uid, ids, context=None):
- model_to_domains = dict((mailing_model[0], list())
- for mailing_model in self.pool['mail.mass_mailing']._get_mailing_model(cr, uid, context=context))
- for contact_list in self.browse(cr, uid, ids, context=context):
- domain = self._get_domain(cr, uid, [contact_list.id], context=context)[contact_list.id]
- if domain is not False:
- model_to_domains[contact_list.model].append(domain)
- for model, domains in model_to_domains.iteritems():
- if domains:
- final_domain = ['|'] * (len(domains) - 1) + [leaf for dom in domains for leaf in dom]
- else:
- final_domain = [('id', '=', '0')]
- model_to_domains[model] = final_domain
- return model_to_domains
-
+
+ class MassMailingStage(osv.Model):
+ """Stage for mass mailing campaigns. """
+ _name = 'mail.mass_mailing.stage'
+ _description = 'Mass Mailing Campaign Stage'
- _order = 'sequence ASC'
-
++ _order = 'sequence'
+ _columns = {
- 'name': fields.char('Name', required=True),
++ 'name': fields.char('Name', required=True, translate=True),
+ 'sequence': fields.integer('Sequence'),
+ }
-
+ _defaults = {
+ 'sequence': 0,
+ }
+
+
+ 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 """
+ 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 = {
+ 'name': fields.char('Name', required=True),
+ 'stage_id': fields.many2one('mail.mass_mailing.stage', 'Stage', required=True),
+ 'user_id': fields.many2one(
+ 'res.users', 'Responsible',
+ required=True,
+ ),
- 'category_id': fields.many2one(
- 'mail.mass_mailing.category', 'Category',
- help='Category'),
++ 'category_ids': fields.many2many(
++ 'mail.mass_mailing.category', 'Categories'),
+ 'mass_mailing_ids': fields.one2many(
+ 'mail.mass_mailing', 'mass_mailing_campaign_id',
+ 'Mass Mailings',
+ ),
- 'ab_testing': fields.boolean(
- 'AB Testing',
- help='If checked, recipients will be mailed only once, allowing to send'
- 'various mailings in a single campaign to test the effectiveness'
- 'of the mailings.'),
+ 'color': fields.integer('Color Index'),
+ # stat fields
+ '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',
+ ),
+ }
+
+ def _get_default_stage_id(self, cr, uid, context=None):
+ stage_ids = self.pool['mail.mass_mailing.stage'].search(cr, uid, [], limit=1, context=context)
- return stage_ids and stage_ids[0]
++ return stage_ids and stage_ids[0] or False
+
+ _defaults = {
+ 'user_id': lambda self, cr, uid, ctx=None: uid,
+ 'stage_id': lambda self, cr, uid, ctx=None: self._get_default_stage_id(cr, uid, context=ctx),
+ }
+
+ #------------------------------------------------------
- # Actions
- #------------------------------------------------------
-
- def action_new_mailing(self, cr, uid, ids, context=None):
- 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',
- 'views': [(False, 'form')],
- 'context': dict(context, default_mass_mailing_campaign_id=ids[0]),
- }
-
- #------------------------------------------------------
+ # API
+ #------------------------------------------------------
-
- def get_recipients(self, cr, uid, ids, model=None, context=None):
- """Return the recipints of a mailing campaign. This is based on the statistics
- build for each mailing. """
- Statistics = self.pool['mail.mail.statistics']
- res = dict.fromkeys(ids, False)
- for cid in ids:
- domain = [('mass_mailing_campaign_id', '=', cid)]
- if model:
- domain += [('model', '=', model)]
- stat_ids = Statistics.search(cr, uid, domain, context=context)
- res[cid] = set(stat.res_id for stat in Statistics.browse(cr, uid, stat_ids, context=context))
- return res
++ # def get_recipients(self, cr, uid, ids, model=None, context=None):
++ # """Return the recipints of a mailing campaign. This is based on the statistics
++ # build for each mailing. """
++ # Statistics = self.pool['mail.mail.statistics']
++ # res = dict.fromkeys(ids, False)
++ # for cid in ids:
++ # domain = [('mass_mailing_campaign_id', '=', cid)]
++ # if model:
++ # domain += [('model', '=', model)]
++ # stat_ids = Statistics.search(cr, uid, domain, context=context)
++ # res[cid] = set(stat.res_id for stat in Statistics.browse(cr, uid, stat_ids, context=context))
++ # return res
+
+
+ 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 = 'Mass Mailing'
+ # number of periods for tracking mail_mail statistics
+ _period_number = 6
- _order = 'date DESC'
++ _order = 'id DESC'
+
+ 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):
+ val = {'contact_nbr': 0, 'contact_ab_nbr': 0, 'contact_ab_done': 0}
+ val['contact_nbr'] = self.pool[mailing.mailing_model].search(
+ cr, uid,
+ self.pool['mail.mass_mailing.list'].get_global_domain(cr, uid, [c.id for c in mailing.contact_list_ids], context=context)[mailing.mailing_model],
+ count=True, context=context
+ )
+ val['contact_ab_nbr'] = int(val['contact_nbr'] * mailing.contact_ab_pc / 100.0)
+ if mailing.mass_mailing_campaign_id and mailing.ab_testing:
+ val['contact_ab_done'] = len(self.pool['mail.mass_mailing.campaign'].get_recipients(cr, uid, [mailing.mass_mailing_campaign_id.id], context=context)[mailing.mass_mailing_campaign_id.id])
+ res[mailing.id] = val
+ return res
+
+ def _get_private_models(self, context=None):
+ return ['res.partner', 'mail.mass_mailing.contact']
+
+ def _get_auto_reply_to_available(self, cr, uid, ids, name, arg, context=None):
+ res = dict.fromkeys(ids, False)
+ for mailing in self.browse(cr, uid, ids, context=context):
+ res[mailing.id] = mailing.mailing_model not in self._get_private_models(context=context)
+ return res
+
+ def _get_mailing_model(self, cr, uid, context=None):
+ return [
+ ('res.partner', 'Customers'),
+ ('mail.mass_mailing.contact', 'Contacts')
+ ]
+
- def _get_state_list(self, cr, uid, context=None):
- return [('draft', 'Schedule'), ('test', 'Tested'), ('done', 'Sent')]
-
- # indirections for inheritance
- _mailing_model = lambda self, *args, **kwargs: self._get_mailing_model(*args, **kwargs)
- _state = lambda self, *args, **kwargs: self._get_state_list(*args, **kwargs)
-
+ _columns = {
+ 'name': fields.char('Subject', required=True),
++ 'email_from': fields.char('From'),
+ 'date': fields.datetime('Date'),
++
+ 'state': fields.selection(
- _state, string='Status', required=True,
- ),
- 'template_id': fields.many2one(
- 'email.template', 'Email Template',
- domain="[('use_in_mass_mailing', '=', True), ('model', '=', mailing_model)]",
++ [('draft', 'Schedule'), ('test', 'Tested'), ('done', 'Sent')], string='Status', required=True,
+ ),
++ # 'template_id': fields.many2one(
++ # 'email.template', 'Email Template',
++ # domain="[('use_in_mass_mailing', '=', True), ('model', '=', mailing_model)]",
++ # ),
+ 'body_html': fields.html('Body'),
+ 'mass_mailing_campaign_id': fields.many2one(
+ 'mail.mass_mailing.campaign', 'Mass Mailing Campaign',
+ ondelete='set null',
+ ),
++
++
++ # TODO: to remove
+ 'ab_testing': fields.related(
+ 'mass_mailing_campaign_id', 'ab_testing',
+ type='boolean', string='AB Testing'
+ ),
++ 'contact_ab_pc': fields.integer(
++ 'AB Testing percentage',
++ help='Percentage of the contacts that will be mailed. Recipients will be taken randomly.'
++ ),
++ 'contact_ab_nbr': fields.function(
++ _get_contact_nbr, type='integer', multi='_get_contact_nbr',
++ string='Contact Number in AB Testing'
++ ),
++ 'contact_ab_done': fields.function(
++ _get_contact_nbr, type='integer', multi='_get_contact_nbr',
++ string='Number of already mailed contacts'
++ ),
++
++
+ 'color': fields.related(
+ 'mass_mailing_campaign_id', 'color',
+ type='integer', string='Color Index',
+ ),
++
+ # mailing options
- 'email_from': fields.char('From'),
+ 'reply_in_thread': fields.boolean('Reply in thread'),
+ 'reply_specified': fields.boolean('Specific Reply-To'),
+ 'auto_reply_to_available': fields.function(
+ _get_auto_reply_to_available,
+ type='boolean', string='Reply in thread available'
+ ),
++
+ 'reply_to': fields.char('Reply To'),
- 'mailing_model': fields.selection(_mailing_model, string='Type', required=True),
++
++ 'mailing_model': fields.selection(_get_mailing_model, string='Model', required=True),
++
+ 'contact_list_ids': fields.many2many(
+ 'mail.mass_mailing.list', 'mail_mass_mailing_list_rel',
+ string='Mailing Lists',
+ domain="[('model', '=', mailing_model)]",
+ ),
+ 'contact_nbr': fields.function(
+ _get_contact_nbr, type='integer', multi='_get_contact_nbr',
+ string='Contact Number'
+ ),
- 'contact_ab_pc': fields.integer(
- 'AB Testing percentage',
- help='Percentage of the contacts that will be mailed. Recipients will be taken randomly.'
- ),
- 'contact_ab_nbr': fields.function(
- _get_contact_nbr, type='integer', multi='_get_contact_nbr',
- string='Contact Number in AB Testing'
- ),
- 'contact_ab_done': fields.function(
- _get_contact_nbr, type='integer', multi='_get_contact_nbr',
- string='Number of already mailed contacts'
- ),
+ # 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',
+ ),
+ }
+
+ _defaults = {
+ 'state': 'draft',
+ 'date': fields.datetime.now,
+ 'email_from': lambda self, cr, uid, ctx=None: self.pool['mail.message']._get_default_from(cr, uid, context=ctx),
+ 'mailing_model': 'res.partner',
+ 'contact_ab_pc': 100,
+ }
+
+ #------------------------------------------------------
+ # Technical stuff
+ #------------------------------------------------------
+
+ def copy_data(self, cr, uid, id, default=None, context=None):
+ if default is None:
+ default = {}
+ mailing = self.browse(cr, uid, id, context=context)
+ default.update({
+ 'state': 'draft',
+ 'statistics_ids': [],
- 'state': 'draft',
+ 'name': _('%s (duplicate)') % mailing.name,
+ })
+ return super(MassMailing, self).copy_data(cr, uid, id, default, context=context)
+
- def read_group(self, cr, uid, domain, fields, groupby, offset=0, limit=None, context=None, orderby=False, lazy=True):
- """ Override read_group to always display all states. """
- if groupby and groupby[0] == "state":
- # Default result structure
- states = self._get_state_list(cr, uid, context=context)
- read_group_all_states = [{
- '__context': {'group_by': groupby[1:]},
- '__domain': domain + [('state', '=', state_value)],
- 'state': state_value,
- 'state_count': 0,
- } for state_value, state_name in states]
- # Get standard results
- read_group_res = super(MassMailing, self).read_group(cr, uid, domain, fields, groupby, offset=offset, limit=limit, context=context, orderby=orderby)
- # Update standard results with default results
- result = []
- for state_value, state_name in states:
- res = filter(lambda x: x['state'] == state_value, read_group_res)
- if not res:
- res = filter(lambda x: x['state'] == state_value, read_group_all_states)
- res[0]['state'] = [state_value, state_name]
- result.append(res[0])
- return result
- else:
- return super(MassMailing, self).read_group(cr, uid, domain, fields, groupby, offset=offset, limit=limit, context=context, orderby=orderby)
-
+ #------------------------------------------------------
+ # Views & Actions
+ #------------------------------------------------------
+
+ def on_change_mailing_model(self, cr, uid, ids, mailing_model, context=None):
+ values = {
+ 'contact_list_ids': [],
+ 'template_id': False,
+ 'contact_nbr': 0,
+ 'auto_reply_to_available': not mailing_model in self._get_private_models(context),
+ 'reply_in_thread': not mailing_model in self._get_private_models(context),
+ 'reply_specified': mailing_model in self._get_private_models(context)
+ }
+ return {'value': values}
+
+ def on_change_reply_specified(self, cr, uid, ids, reply_specified, reply_in_thread, context=None):
+ if reply_specified == reply_in_thread:
+ return {'value': {'reply_in_thread': not reply_specified}}
+ return {}
+
+ def on_change_reply_in_thread(self, cr, uid, ids, reply_specified, reply_in_thread, context=None):
+ if reply_in_thread == reply_specified:
+ return {'value': {'reply_specified': not reply_in_thread}}
+ return {}
+
+ def on_change_contact_list_ids(self, cr, uid, ids, mailing_model, contact_list_ids, context=None):
+ values = {}
+ list_ids = []
+ for command in contact_list_ids:
+ if command[0] == 6:
+ list_ids += command[2]
+ if list_ids:
+ values['contact_nbr'] = self.pool[mailing_model].search(
+ cr, uid,
+ self.pool['mail.mass_mailing.list'].get_global_domain(cr, uid, list_ids, context=context)[mailing_model],
+ count=True, context=context
+ )
+ return {'value': values}
+
+ def on_change_template_id(self, cr, uid, ids, template_id, context=None):
+ values = {}
+ if template_id:
+ template = self.pool['email.template'].browse(cr, uid, template_id, context=context)
+ if template.email_from:
+ values['email_from'] = template.email_from
+ if template.reply_to:
+ values['reply_to'] = template.reply_to
+ values['body_html'] = template.body_html
+ else:
+ values['email_from'] = self.pool['mail.message']._get_default_from(cr, uid, context=context)
+ values['reply_to'] = False
+ values['body_html'] = False
+ return {'value': values}
+
+ def on_change_contact_ab_pc(self, cr, uid, ids, contact_ab_pc, contact_nbr, context=None):
+ return {'value': {'contact_ab_nbr': contact_nbr * contact_ab_pc / 100.0}}
+
+ def action_duplicate(self, cr, uid, ids, context=None):
+ copy_id = None
+ for mid in ids:
+ copy_id = self.copy(cr, uid, mid, context=context)
+ if copy_id:
+ return {
+ 'type': 'ir.actions.act_window',
+ 'view_type': 'form',
+ 'view_mode': 'form',
+ 'res_model': 'mail.mass_mailing',
+ 'res_id': copy_id,
+ 'context': context,
+ }
+ return False
+
+ def _get_model_to_list_action_id(self, cr, uid, model, context=None):
+ if model == 'res.partner':
+ return self.pool['ir.model.data'].xmlid_to_res_id(cr, uid, 'mass_mailing.action_partner_to_mailing_list')
+ else:
+ return self.pool['ir.model.data'].xmlid_to_res_id(cr, uid, 'mass_mailing.action_contact_to_mailing_list')
+
+ def action_new_list(self, cr, uid, ids, context=None):
+ mailing = self.browse(cr, uid, ids[0], context=context)
+ action_id = self._get_model_to_list_action_id(cr, uid, mailing.mailing_model, context=context)
+ ctx = dict(context,
+ search_default_not_opt_out=True,
+ view_manager_highlight=[action_id],
+ default_name=mailing.name,
+ default_mass_mailing_id=ids[0],
+ default_model=mailing.mailing_model)
+ return {
+ 'name': _('Choose Recipients'),
+ 'type': 'ir.actions.act_window',
+ 'view_type': 'form',
+ 'view_mode': 'tree,form',
+ 'res_model': mailing.mailing_model,
+ 'context': ctx,
+ }
+
+ def action_see_recipients(self, cr, uid, ids, context=None):
+ mailing = self.browse(cr, uid, ids[0], context=context)
+ domain = self.pool['mail.mass_mailing.list'].get_global_domain(cr, uid, [c.id for c in mailing.contact_list_ids], context=context)[mailing.mailing_model]
+ return {
+ 'name': _('See Recipients'),
+ 'type': 'ir.actions.act_window',
+ 'view_type': 'form',
+ 'view_mode': 'tree,form',
+ 'res_model': mailing.mailing_model,
+ 'target': 'new',
+ 'domain': domain,
+ 'context': context,
+ }
+
+ def action_test_mailing(self, cr, uid, ids, context=None):
+ ctx = dict(context, default_mass_mailing_id=ids[0])
+ return {
+ 'name': _('Test Mailing'),
+ 'type': 'ir.actions.act_window',
+ 'view_mode': 'form',
+ 'res_model': 'mail.mass_mailing.test',
+ 'target': 'new',
+ 'context': ctx,
+ }
+
+ def action_edit_html(self, cr, uid, ids, context=None):
+ url = '/website_mail/email_designer?model=mail.mass_mailing&res_id=%d' % ids[0]
+ return {
+ 'name': _('Open with Visual Editor'),
+ 'type': 'ir.actions.act_url',
+ 'url': url,
+ 'target': 'self',
+ }
+
+ #------------------------------------------------------
+ # Email Sending
+ #------------------------------------------------------
+
+ def get_recipients_data(self, cr, uid, mailing, res_ids, context=None):
+ # tde todo: notification link ?
+ if mailing.mailing_model == 'mail.mass_mailing.contact':
+ contacts = self.pool['mail.mass_mailing.contact'].browse(cr, uid, res_ids, context=context)
+ return dict((contact.id, {'partner_id': False, 'name': contact.name, 'email': contact.email}) for contact in contacts)
+ else:
+ partners = self.pool['res.partner'].browse(cr, uid, res_ids, context=context)
+ return dict((partner.id, {'partner_id': partner.id, 'name': partner.name, 'email': partner.email}) for partner in partners)
+
+ def get_recipients(self, cr, uid, mailing, context=None):
+ domain = self.pool['mail.mass_mailing.list'].get_global_domain(
+ cr, uid, [l.id for l in mailing.contact_list_ids], context=context
+ )[mailing.mailing_model]
+ res_ids = self.pool[mailing.mailing_model].search(cr, uid, domain, context=context)
+
+ # randomly choose a fragment
+ if mailing.contact_ab_pc != 100:
+ topick = mailing.contact_ab_nbr
+ if mailing.mass_mailing_campaign_id and mailing.ab_testing:
+ already_mailed = self.pool['mail.mass_mailing.campaign'].get_recipients(cr, uid, [mailing.mass_mailing_campaign_id.id], context=context)[mailing.mass_mailing_campaign_id.id]
+ else:
+ already_mailed = set([])
+ remaining = set(res_ids).difference(already_mailed)
+ if topick > len(remaining):
+ topick = len(remaining)
+ res_ids = random.sample(remaining, topick)
+ return res_ids
+
+ def get_unsubscribe_url(self, cr, uid, mailing_id, res_id, email, msg=None, context=None):
+ base_url = self.pool.get('ir.config_parameter').get_param(cr, uid, 'web.base.url')
+ url = urlparse.urljoin(
+ base_url, 'mail/mailing/%(mailing_id)s/unsubscribe?%(params)s' % {
+ 'mailing_id': mailing_id,
+ 'params': urllib.urlencode({'db': cr.dbname, 'res_id': res_id, 'email': email})
+ }
+ )
+ return '<small><a href="%s">%s</a></small>' % (url, msg or 'Click to unsubscribe')
+
+ def send_mail(self, cr, uid, ids, context=None):
+ author_id = self.pool['res.users'].browse(cr, uid, uid, context=context).partner_id.id
+ for mailing in self.browse(cr, uid, ids, context=context):
+ if not mailing.contact_nbr:
+ raise Warning('Please select recipients.')
+ # instantiate an email composer + send emails
+ res_ids = self.get_recipients(cr, uid, mailing, context=context)
+ comp_ctx = dict(context, active_ids=res_ids)
+ composer_values = {
+ 'author_id': author_id,
+ 'body': mailing.body_html,
+ 'subject': mailing.name,
+ 'model': mailing.mailing_model,
+ 'email_from': mailing.email_from,
+ 'record_name': False,
+ 'composition_mode': 'mass_mail',
+ 'mass_mailing_id': mailing.id,
+ 'mailing_list_ids': [(4, l.id) for l in mailing.contact_list_ids],
+ }
+ if mailing.reply_specified:
+ composer_values['reply_to'] = mailing.reply_to
+ composer_id = self.pool['mail.compose.message'].create(cr, uid, composer_values, context=comp_ctx)
+ self.pool['mail.compose.message'].send_mail(cr, uid, [composer_id], context=comp_ctx)
+ self.write(cr, uid, [mailing.id], {'date': fields.datetime.now(), 'state': 'done'}, context=context)
+ return True
+
+
++# 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,
+ ),
+ '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
+ '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
--- /dev/null
+ <?xml version="1.0"?>
+ <openerp>
+ <data>
+
+ <!-- Marketing / Mass Mailing -->
+ <menuitem name="Mass Mailing" id="mass_mailing_campaign"
+ parent="base.marketing_menu" sequence="1"/>
+ <!-- Marketing / Mailing Lists -->
+ <menuitem name="Contact Lists" id="mass_mailing_list"
+ parent="base.marketing_menu" sequence="2"/>
+ <!-- Marketing / Configuration -->
+ <menuitem name="Configuration" id="marketing_configuration"
+ parent="base.marketing_menu" sequence="99"/>
+
+ <!-- MASS MAILING CONTACT !-->
+ <record model="ir.ui.view" id="view_mail_mass_mailing_contact_search">
+ <field name="name">mail.mass_mailing.contact.search</field>
+ <field name="model">mail.mass_mailing.contact</field>
+ <field name="arch" type="xml">
+ <search string="Mass Mailings">
+ <field name="name"/>
+ <field name="email"/>
+ <field name="list_id"/>
+ <separator/>
+ <filter string="Available for Mass Mailing" name="not_opt_out" domain="[('opt_out', '=', False)]"
+ help="Contact is not opt-out"/>
+ <group expand="0" string="Group By...">
+ <filter string="Mailing Lists" name="group_list_id"
+ context="{'group_by': 'list_id'}"/>
+ </group>
+ </search>
+ </field>
+ </record>
+
+ <record model="ir.ui.view" id="view_mail_mass_mailing_contact_tree">
+ <field name="name">mail.mass_mailing.contact.tree</field>
+ <field name="model">mail.mass_mailing.contact</field>
+ <field name="priority">10</field>
+ <field name="arch" type="xml">
+ <tree string="Mass Mailings">
+ <field name="name"/>
+ <field name="email"/>
+ <field name="list_id"/>
+ <field name="opt_out"/>
+ </tree>
+ </field>
+ </record>
+
+ <record model="ir.ui.view" id="view_mail_mass_mailing_contact_form">
+ <field name="name">mail.mass_mailing.contact.form</field>
+ <field name="model">mail.mass_mailing.contact</field>
+ <field name="arch" type="xml">
+ <form string="Mass Mailing" version="7.0">
+ <sheet>
+ <group>
+ <field name="name"/>
+ <field name="email"/>
+ <field name="list_id"/>
+ <field name="opt_out"/>
+ </group>
+ </sheet>
+ </form>
+ </field>
+ </record>
+
+ <record id="action_view_mass_mailing_contacts" model="ir.actions.act_window">
+ <field name="name">Mass Mailing Contacts</field>
+ <field name="res_model">mail.mass_mailing.contact</field>
+ <field name="view_type">form</field>
+ <field name="view_mode">tree,form</field>
+ <field name="context">{'search_default_not_opt_out': 1}</field>
+ </record>
+
+ <menuitem name="Contacts" id="menu_email_mass_mailing_contacts" groups="base.group_no_one"
+ parent="mass_mailing_list" sequence="50"
+ action="action_view_mass_mailing_contacts"/>
+
+ <!-- Create a Mailing List from Contacts -->
+ <act_window name="Create Mailing List"
+ res_model="mail.mass_mailing.list.confirm"
+ src_model="mail.mass_mailing.contact"
+ view_mode="form"
+ multi="True"
+ target="new"
+ key2="client_action_multi"
+ id="action_contact_to_mailing_list"
+ context="{
+ 'default_mass_mailing_id': context.get('default_mass_mailing_id'),
+ 'default_model': context.get('default_model', 'mail.mass_mailing.contact'),
+ 'default_name': context.get('default_name', False)}"/>
+
+ <!-- MASS MAILING LIST !-->
+ <record model="ir.ui.view" id="view_mail_mass_mailing_list_search">
+ <field name="name">mail.mass_mailing.list.search</field>
+ <field name="model">mail.mass_mailing.list</field>
+ <field name="arch" type="xml">
+ <search string="Mass Mailings">
+ <field name="name"/>
+ <separator/>
+ </search>
+ </field>
+ </record>
+
+ <record model="ir.ui.view" id="view_mail_mass_mailing_list_tree">
+ <field name="name">mail.mass_mailing.list.tree</field>
+ <field name="model">mail.mass_mailing.list</field>
+ <field name="priority">10</field>
+ <field name="arch" type="xml">
+ <tree string="Contact Lists">
+ <field name="name"/>
+ <field name="model"/>
+ <field name="contact_nbr"/>
+ </tree>
+ </field>
+ </record>
+
+ <record model="ir.ui.view" id="view_mail_mass_mailing_list_form">
+ <field name="name">mail.mass_mailing.list.form</field>
+ <field name="model">mail.mass_mailing.list</field>
+ <field name="arch" type="xml">
+ <form string="Contact List" version="7.0">
+ <header>
+ <button name="action_add_to_mailing" type="object"
+ class="oe_highlight" string="Continue to Mailing"
+ invisible="not context.get('default_mass_mailing_id')"/>
+ </header>
+ <sheet>
+ <group>
+ <field name="name" string="Mailing List Name"/>
+ <label for="contact_nbr"/>
+ <div>
+ <field name="contact_nbr" nolabel="1" class="oe_inline"/>
+ <field name="model" class="oe_inline"
+ on_change="on_change_model(model, context)" nolabel="1"/>
+ <button string="See Recipients" class="oe_inline oe_link" style="margin-left: 8px;"
+ name="action_see_records" type="object"/>
+ </div>
+ <field name="filter_id" groups="base.group_no_one"
+ on_change="on_change_filter_id(filter_id, context)"/>
+ <field name="domain" groups="base.group_no_one"
+ on_change="on_change_domain(domain, model, context)"/>
+ </group>
+ </sheet>
+ </form>
+ </field>
+ </record>
+
+ <record id="action_view_mass_mailing_lists" model="ir.actions.act_window">
+ <field name="name">Contact Lists</field>
+ <field name="res_model">mail.mass_mailing.list</field>
+ <field name="view_type">form</field>
+ <field name="view_mode">tree,form</field>
+ <field name="help" type="html">
+ <p class="oe_view_nocontent_create">
+ Click here to create a new mailing list.
+ </p><p>
+ Mailing lists allows you to to manage customers and contacts easily and to send to mailings in a single click.
+ </p></field>
+ </record>
+
+ <menuitem name="Contact Lists" id="menu_email_mass_mailing_lists"
+ parent="mass_mailing_list" sequence="40"
+ action="action_view_mass_mailing_lists"/>
+
+ <!-- MASS MAILING !-->
+ <record model="ir.ui.view" id="view_mail_mass_mailing_search">
+ <field name="name">mail.mass_mailing.search</field>
+ <field name="model">mail.mass_mailing</field>
+ <field name="arch" type="xml">
+ <search string="Mass Mailings">
+ <field name="name" string="Mailings"/>
+ <field name="mass_mailing_campaign_id"/>
+ <field name="template_id"/>
+ <group expand="0" string="Group By...">
+ <filter string="State" name="group_state"
+ context="{'group_by': 'state'}"/>
+ <filter string="Campaign" name="group_mass_mailing_campaign_id"
+ groups="mass_mailing.group_mass_mailing_campaign"
+ context="{'group_by': 'mass_mailing_campaign_id'}"/>
+ <filter string="Template" name="group_template_id"
+ context="{'group_by': 'template_id'}"/>
+ </group>
+ </search>
+ </field>
+ </record>
+
+ <record model="ir.ui.view" id="view_mail_mass_mailing_tree">
+ <field name="name">mail.mass_mailing.tree</field>
+ <field name="model">mail.mass_mailing</field>
+ <field name="priority">10</field>
+ <field name="arch" type="xml">
+ <tree string="Mass Mailings">
+ <field name="name"/>
+ <field name="sent"/>
+ <field name="delivered"/>
+ <field name="opened"/>
+ <field name="replied"/>
+ <field name="mass_mailing_campaign_id"
+ groups="mass_mailing.group_mass_mailing_campaign"/>
+ <field name="template_id" invisible="1"/>
+ </tree>
+ </field>
+ </record>
+
+ <record model="ir.ui.view" id="view_mail_mass_mailing_form">
+ <field name="name">mail.mass_mailing.form</field>
+ <field name="model">mail.mass_mailing</field>
+ <field name="arch" type="xml">
+ <form string="Mass Mailing" version="7.0">
+ <header>
+ <button name="action_test_mailing" type="object"
+ class="oe_highlight" string="Test Mailing"/>
+ <button name="send_mail" type="object"
+ class="oe_highlight" string="Send to All"/>
+ <field name="state" widget="statusbar" clickable="True"/>
+ </header>
+ <sheet>
+ <div colspan="2" class="oe_form_box_info oe_text_center"
+ attrs="{'invisible': [('scheduled', '=', 0)]}">
+ <p>
+ <strong><field name="scheduled" class="oe_inline"/>emails are in queue
+ and will be sent soon.</strong>
+ </p>
+ </div>
+ <group>
+ <group>
+ <field name="email_from"/>
+ <field name="name"/>
+ </group>
+ <group>
+ <div class="oe_right oe_button_box" name="buttons">
+ <div>
+ <button name="action_see_recipients" type="object"
+ icon="fa-user" class="oe_stat_button">
+ <field name="contact_nbr" string="Recipients" widget="statinfo"/>
+ </button>
+ <button name="%(action_mail_mass_mailing_report)d" type="action"
+ icon="fa-envelope-o" class="oe_stat_button">
+ <field name="total" string="Emails" widget="statinfo"/>
+ </button>
+ </div>
+ <div style="margin-top: 8px;"
+ attrs="{'invisible': [('total', '=', 0)]}">
+ <button name="%(action_mail_mass_mailing_report)d"
+ type="action" class="oe_stat_button">
+ <field name="received_ratio" string="Received" widget="percentpie"/>
+ </button>
+ <button name="%(action_mail_mass_mailing_report)d"
+ type="action" class="oe_stat_button">
+ <field name="opened_ratio" string="Opened" widget="percentpie"/>
+ </button>
+ <button name="%(action_mail_mass_mailing_report)d"
+ type="action" class="oe_stat_button">
+ <field name="replied_ratio" string="Replied" widget="percentpie"/>
+ </button>
+ </div>
+ <div style="margin-top: 8px;"
+ attrs="{'invisible': [('total', '=', 0)]}">
+ <button name="%(action_mail_mass_mailing_report)d"
+ type="action" class="oe_stat_button oe_inline">
+ <field name="opened_dayly" string="Opened Daily" widget="barchart"/>
+ </button>
+ <button name="%(action_mail_mass_mailing_report)d"
+ type="action" class="oe_stat_button oe_inline">
+ <field name="replied_dayly" string="Replied Daily" widget="barchart"/>
+ </button>
+ </div>
+ </div>
+ </group>
+ </group>
+ <group>
+ <label for="reply_to"/>
+ <div>
+ <field name="auto_reply_to_available" invisible="1"/>
+ <field name="reply_in_thread" class="oe_inline"
+ on_change="on_change_reply_in_thread(reply_specified, reply_in_thread, context)"
+ attrs="{'readonly': [('auto_reply_to_available', '=', False)]}"/>
+ <span attrs="{'invisible': [('auto_reply_to_available', '=', False)]}">
+ Replies go into the original document
+ </span>
+ <span class="oe_grey" attrs="{'invisible': [('auto_reply_to_available', '=', True)]}">
+ Replies go into the original document (not available for those recipients)
+ </span>
+ <br />
+ <field name="reply_specified" class="oe_inline"
+ on_change="on_change_reply_specified(reply_specified, reply_in_thread, context)"/> Use a specific reply-to address
+ <field name="reply_to" class="oe_inline"
+ style="margin-left: 8px;"
+ attrs="{'required': [('reply_specified', '=', True)]}"/>
+ </div>
+ <label for="mailing_model" string="Recipients"/>
+ <div>
+ <field name="mailing_model" widget="radio"
+ on_change='on_change_mailing_model(mailing_model, context)'/>
+
+ <label for="contact_list_ids" string="Mailing Lists" style="display: inline-block; min-width: 90px;"/>
+ <field name="contact_list_ids" widget="many2many_tags" options="{'no_create': True}"
+ class="oe_inline" placeholder="Choose mailing lists"
+ on_change="on_change_contact_list_ids(mailing_model, contact_list_ids, context)"/>
+ <span style="margin-left: 8px; margin-right: 8px">or</span>
+ <button string='Create a New List' class="oe_link" type='object' name='action_new_list'/><br />
+
+ <div groups="mass_mailing.group_mass_mailing_campaign" style="display: inline;">
+ <field name="ab_testing" invisible="1"/>
+ <label for="contact_ab_pc" string="AB Testing" style="display: inline-block; min-width: 90px;"/>
+ Email <field name="contact_ab_pc" class="oe_inline"
+ on_change="on_change_contact_ab_pc(contact_ab_pc, contact_nbr, context)"/>
+ <strong>%</strong> of recipients
+ (<field name="contact_ab_nbr" class="oe_inline"/> recipients)
+ <div attrs="{'invisible': [('ab_testing', '=', False)]}" style="display: inline;">
+ <span>(</span>
+ <field name="contact_ab_done" class="oe_inline"
+ attrs="{'invisible': [('ab_testing', '=', False)]}"/> already mailed
+ <span>)</span>
+ </div>
+ </div>
+
+ </div>
+ <field name="date" readonly="True" groups="mass_mailing.group_mass_mailing_campaign"
+ attrs="{'invisible': [('state', '!=', 'done')]}"/>
+ <field name="mass_mailing_campaign_id" groups="mass_mailing.group_mass_mailing_campaign"/>
+ <label for="body_html" string="Email"/>
+ <div>
+ <label for="template_id" string="Template"/>
+ <field name="template_id" string="Select Template"
+ class="oe_inline" options="{'no_create': True, 'no_open': True}"
+ on_change="on_change_template_id(template_id, context)"/><br />
+ <button name="action_edit_html" type="object" string="Edit Mail Content"
+ class="oe_link" style="margin-left: 8px"/>
+ <field name="body_html"/>
+ </div>
+ </group>
+ </sheet>
+ </form>
+ </field>
+ </record>
+
+ <record model="ir.ui.view" id="view_mail_mass_mailing_kanban">
+ <field name="name">mail.mass_mailing.kanban</field>
+ <field name="model">mail.mass_mailing</field>
+ <field name="arch" type="xml">
+ <kanban default_group_by='state'>
+ <field name='color'/>
+ <field name='total'/>
+ <templates>
+ <t t-name="kanban-box">
+ <div t-attf-class="oe_kanban_color_#{kanban_getcolor(record.color.raw_value)} oe_kanban_card oe_kanban_global_click oe_kanban_mass_mailing">
+ <div class="oe_dropdown_toggle oe_dropdown_kanban">
+ <span class="oe_e">i</span>
+ <ul class="oe_dropdown_menu">
+ <t t-if="widget.view.is_action_enabled('delete')">
+ <li><a type="delete">Delete</a></li>
+ </t>
+ </ul>
+ </div>
+ <div class="oe_kanban_content">
+ <div>
+ <h3><field name="name"/></h3>
+ <h4 style="display: inline;"><field name="mass_mailing_campaign_id" groups="mass_mailing.group_mass_mailing_campaign"/></h4>
+ <t t-if="record.mass_mailing_campaign_id.raw_value" groups="mass_mailing.group_mass_mailing_campaign"> - </t><field name="date"/>
+ </div>
+ <div>
+ <div style="display: inline-block">
+ <field name="delivered" widget="gauge" style="width:120px; height: 90px;"
+ options="{'max_field': 'total'}"/>
+ </div>
+ <div style="display: inline-block; vertical-align: top;">
+ <strong>Opened</strong> <field name="opened_ratio"/> %<br />
+ <strong>Replied</strong> <field name="replied_ratio"/> %
+ </div>
+ </div>
+ </div>
+ <div class="oe_clear"></div>
+ </div>
+ </t>
+ </templates>
+ </kanban>
+ </field>
+ </record>
+
+ <record id="action_view_mass_mailings" model="ir.actions.act_window">
+ <field name="name">Mass Mailings</field>
+ <field name="res_model">mail.mass_mailing</field>
+ <field name="view_type">form</field>
+ <field name="view_mode">kanban,tree,form</field>
+ <field name="help" type="html">
+ <p class="oe_view_nocontent_create">
+ Click here to create a new mailing.
+ </p><p>
+ Mass mailing allows you to to easily design and send mass mailings to your contacts, customers or leads using mailing lists.
+ </p></field>
+ </record>
+
+ <record id="action_view_mass_mailings_from_campaign" model="ir.actions.act_window">
+ <field name="name">Mass Mailings</field>
+ <field name="res_model">mail.mass_mailing</field>
+ <field name="view_type">form</field>
+ <field name="view_mode">kanban,tree,form</field>
+ <field name="context">{
+ 'search_default_mass_mailing_campaign_id': [active_id],
+ 'default_mass_mailing_campaign_id': active_id,
+ }
+ </field>
+ <field name="help" type="html">
+ <p class="oe_view_nocontent_create">
+ Click here to create a new mailing.
+ </p><p>
+ Mass mailing allows you to to easily design and send mass mailings to your contacts, customers or leads using mailing lists.
+ </p></field>
+ </record>
+
+ <menuitem name="Mass Mailings" id="menu_email_mass_mailings"
+ parent="mass_mailing_campaign" sequence="2"
+ action="action_view_mass_mailings"/>
+
+ <!-- MASS MAILING CAMPAIGN STAGE !-->
+ <record model="ir.ui.view" id="view_mail_mass_mailing_stage_search">
+ <field name="name">mail.mass_mailing.stage.search</field>
+ <field name="model">mail.mass_mailing.stage</field>
+ <field name="arch" type="xml">
+ <search string="Mass Mailings">
+ <field name="name"/>
+ </search>
+ </field>
+ </record>
+
+ <record model="ir.ui.view" id="view_mail_mass_mailing_stage_tree">
+ <field name="name">mail.mass_mailing.stage.tree</field>
+ <field name="model">mail.mass_mailing.stage</field>
+ <field name="priority">10</field>
+ <field name="arch" type="xml">
+ <tree string="Mass Mailings">
+ <field name="name"/>
+ <field name="sequence"/>
+ </tree>
+ </field>
+ </record>
+
+ <record model="ir.ui.view" id="view_mail_mass_mailing_stage_form">
+ <field name="name">mail.mass_mailing.stage.form</field>
+ <field name="model">mail.mass_mailing.stage</field>
+ <field name="arch" type="xml">
+ <form string="Mass Mailing" version="7.0">
+ <sheet>
+ <group>
+ <field name="name"/>
+ <field name="sequence"/>
+ </group>
+ </sheet>
+ </form>
+ </field>
+ </record>
+
+ <record id="action_view_mass_mailing_stages" model="ir.actions.act_window">
+ <field name="name">Mass Mailing Stages</field>
+ <field name="res_model">mail.mass_mailing.stage</field>
+ <field name="view_type">form</field>
+ <field name="view_mode">tree,form</field>
+ </record>
+
+ <menuitem name="Campaign Stages" id="menu_view_mass_mailing_stages"
+ parent="marketing_configuration" sequence="1"
+ action="action_view_mass_mailing_stages"/>
+
+ <!-- MASS MAILING CAMPAIGNS !-->
+ <record model="ir.ui.view" id="view_mail_mass_mailing_campaign_search">
+ <field name="name">mail.mass_mailing.campaign.search</field>
+ <field name="model">mail.mass_mailing.campaign</field>
+ <field name="arch" type="xml">
+ <search string="Mass Mailing Campaigns">
+ <field name="name" string="Campaigns"/>
+ <field name="category_id"/>
+ <field name="user_id"/>
+ <group expand="0" string="Group By...">
+ <filter string="Stage" name="group_stage_id"
+ context="{'group_by': 'stage_id'}"/>
+ <filter string="Responsible" name="group_user_id"
+ context="{'group_by': 'user_id'}"/>
+ <filter string="Category" name="group_category_id"
+ context="{'group_by': 'category_id'}"/>
+ </group>
+ </search>
+ </field>
+ </record>
+
+ <record model="ir.ui.view" id="view_mail_mass_mailing_campaign_tree">
+ <field name="name">mail.mass_mailing.campaign.tree</field>
+ <field name="model">mail.mass_mailing.campaign</field>
+ <field name="priority">10</field>
+ <field name="arch" type="xml">
+ <tree string="Mass Mailing Campaigns">
+ <field name="name"/>
+ <field name="user_id"/>
+ <field name="stage_id"/>
+ <field name="category_id"/>
+ </tree>
+ </field>
+ </record>
+
+ <record model="ir.ui.view" id="view_mail_mass_mailing_campaign_form">
+ <field name="name">mail.mass_mailing.campaign.form</field>
+ <field name="model">mail.mass_mailing.campaign</field>
+ <field name="arch" type="xml">
+ <form string="Mass Mailing Campaign" version="7.0">
+ <header>
+ <button name="action_new_mailing" type="object" class="oe_highlight" string="New Mailing"/>
+ <field name="stage_id" widget="statusbar" clickable="True"/>
+ </header>
+ <sheet>
+ <group>
+ <group>
+ <field name="name"/>
+ <field name="user_id"/>
+ <field name="category_id"/>
- <field name="ab_testing"/>
+ </group>
+ <group>
+ <field name="total" invisible="1"/>
+ <div class="oe_right oe_button_box" name="buttons"
+ attrs="{'invisible': [('total', '=', 0)]}">
+ <button name="%(action_mail_mass_mailing_report)d"
+ type="action" class="oe_stat_button oe_inline">
+ <field name="received_ratio" widget="percentpie"/>
+ <span>Received</span>
+ </button>
+ <button name="%(action_mail_mass_mailing_report)d"
+ type="action" class="oe_stat_button oe_inline">
+ <field name="opened_ratio" widget="percentpie"/>
+ <span>Opened</span>
+ </button>
+ <button name="%(action_mail_mass_mailing_report)d"
+ type="action" class="oe_stat_button oe_inline">
+ <field name="replied_ratio" widget="percentpie"/>
+ <span>Replied</span>
+ </button>
+ </div>
+ </group>
+ </group>
+ <strong>Related Mailing(s)</strong>
+ <field name="mass_mailing_ids" readonly="1" string="Related Mailing(s)">
+ <tree>
+ <field name="name"/>
+ <field name="date"/>
+ <field name="state"/>
+ <field name="delivered"/>
+ <field name="opened"/>
+ <field name="replied"/>
+ <field name="bounced"/>
+ <button name="action_duplicate" type="object" string="Duplicate"/>
+ </tree>
+ </field>
+ </sheet>
+ </form>
+ </field>
+ </record>
+
+ <record model="ir.ui.view" id="view_mail_mass_mailing_campaign_kanban">
+ <field name="name">mail.mass_mailing.campaign.kanban</field>
+ <field name="model">mail.mass_mailing.campaign</field>
+ <field name="arch" type="xml">
+ <kanban default_group_by='stage_id'>
+ <field name='total'/>
+ <field name='color'/>
+ <field name='user_id'/>
+ <field name='mass_mailing_ids'/>
+ <templates>
+ <t t-name="kanban-box">
+ <div t-attf-class="oe_kanban_color_#{kanban_getcolor(record.color.raw_value)} oe_kanban_card oe_kanban_global_click oe_kanban_mass_mailing_campaign">
+ <div class="oe_dropdown_toggle oe_dropdown_kanban">
+ <span class="oe_e">i</span>
+ <ul class="oe_dropdown_menu">
+ <t t-if="widget.view.is_action_enabled('edit')">
+ <li><a type="edit">Settings</a></li>
+ </t>
+ <t t-if="widget.view.is_action_enabled('delete')">
+ <li><a type="delete">Delete</a></li>
+ </t>
+ <li><ul class="oe_kanban_colorpicker" data-field="color"/></li>
+ </ul>
+ </div>
+ <div class="oe_kanban_content">
+ <div>
+ <img t-att-src="kanban_image('res.users', 'image_small', record.user_id.raw_value)"
+ t-att-title="record.user_id.value" width="24" height="24" class="oe_kanban_avatar oe_kanban_header_right"/>
+ <h3 style="margin-bottom: 8px;"><field name="name"/></h3>
+ <span class="oe_tag"><field name="category_id"/></span>
+ <a name="%(action_view_mass_mailings_from_campaign)d" type="action"
+ class="oe_mailings">
+ <h4 style="margin-top: 8px;"><t t-raw="record.mass_mailing_ids.raw_value.length"/> Mailings</h4>
+ </a>
+ </div>
+ <div class="oe_clear"></div>
+ <div>
+ <div style="display: inline-block">
+ <field name="delivered" widget="gauge" style="width:120px; height: 90px;"
+ options="{'max_field': 'total'}"/>
+ </div>
+ <div style="display: inline-block; vertical-align: top;">
+ <strong>Opened</strong> <field name="opened_ratio"/> %<br />
+ <strong>Replied</strong> <field name="replied_ratio"/> %
+ </div>
+ </div>
+ </div>
+ <div class="oe_clear"></div>
+ </div>
+ </t>
+ </templates>
+ </kanban>
+ </field>
+ </record>
+
+ <record id="action_view_mass_mailing_campaigns" model="ir.actions.act_window">
+ <field name="name">Mass Mailing Campaigns</field>
+ <field name="res_model">mail.mass_mailing.campaign</field>
+ <field name="view_type">form</field>
+ <field name="view_mode">kanban,tree,form</field>
+ <field name="help" type="html">
+ <p class="oe_view_nocontent_create">
+ Click to define a new mass mailing campaign.
+ </p><p>
+ Create a campaign to structure mass mailing and get analysis from email status.
+ </p>
+ </field>
+ </record>
+
+ <menuitem name="Campaigns" id="menu_email_campaigns"
+ parent="mass_mailing_campaign" sequence="1"
+ action="action_view_mass_mailing_campaigns"
+ groups="mass_mailing.group_mass_mailing_campaign"/>
+
+ <!-- MAIL MAIL STATISTICS !-->
+ <record model="ir.ui.view" id="view_mail_mail_statistics_search">
+ <field name="name">mail.mail.statistics.search</field>
+ <field name="model">mail.mail.statistics</field>
+ <field name="arch" type="xml">
+ <search string="Mail Statistics">
+ <field name="mail_mail_id"/>
+ <field name="message_id"/>
+ </search>
+ </field>
+ </record>
+
+ <record model="ir.ui.view" id="view_mail_mail_statistics_tree">
+ <field name="name">mail.mail.statistics.tree</field>
+ <field name="model">mail.mail.statistics</field>
+ <field name="arch" type="xml">
+ <tree string="Mail Statistics">
+ <field name="mail_mail_id"/>
+ <field name="message_id"/>
+ <field name="sent"/>
+ <field name="opened"/>
+ <field name="replied"/>
+ <field name="bounced"/>
+ </tree>
+ </field>
+ </record>
+
+ <record model="ir.ui.view" id="view_mail_mail_statistics_form">
+ <field name="name">mail.mail.statistics.form</field>
+ <field name="model">mail.mail.statistics</field>
+ <field name="arch" type="xml">
+ <form string="Mail Statistics" version="7.0">
+ <group>
+ <group>
+ <field name="mail_mail_id"/>
+ <field name="message_id"/>
+ <field name="exception"/>
+ <field name="sent"/>
+ <field name="opened"/>
+ <field name="replied"/>
+ <field name="bounced"/>
+ </group>
+ <group>
+ <field name="mass_mailing_id"/>
+ <field name="mass_mailing_campaign_id"/>
+ <field name="template_id"/>
+ <field name="model"/>
+ <field name="res_id"/>
+ </group>
+ </group>
+ </form>
+ </field>
+ </record>
+
+ <record id="action_view_mail_mail_statistics" model="ir.actions.act_window">
+ <field name="name">Mail Statistics</field>
+ <field name="res_model">mail.mail.statistics</field>
+ <field name="view_type">form</field>
+ <field name="view_mode">tree,form</field>
+ </record>
+
+ <!-- Add in Technical/Email -->
+ <menuitem name="Mail Statistics" id="menu_email_statistics"
+ parent="base.menu_email" sequence="50"
+ action="action_view_mail_mail_statistics"/>
+
+ <!-- MISC -->
+ <!-- Mailing List Create Wizard -->
+ <menuitem name="Create a new List" id="menu_mail_mass_mailing_create"
+ parent="mass_mailing_list" sequence="10"
+ action="action_mail_mass_mailing_create"/>
+
+ </data>
+ </openerp>