[WIP] started to reduce code

bzr revid: fp@tinyerp.com-20140412153339-rr3b1r4cmdeigp0v
This commit is contained in:
Fabien Pinckaers 2014-04-12 17:33:39 +02:00
commit 2feab20dbb
8 changed files with 79 additions and 755 deletions

View File

@ -44,7 +44,6 @@ professional emails and reuse templates in a few clicks.
'data/mass_mailing_data.xml', 'data/mass_mailing_data.xml',
'wizard/mail_compose_message_view.xml', 'wizard/mail_compose_message_view.xml',
'wizard/test_mailing.xml', 'wizard/test_mailing.xml',
'wizard/mailing_list_confirm.xml',
'views/mass_mailing_report.xml', 'views/mass_mailing_report.xml',
'views/mass_mailing.xml', 'views/mass_mailing.xml',
'views/res_config.xml', 'views/res_config.xml',

View File

@ -1,6 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import mass_mailing import mass_mailing
import mass_mailing_stats
import mail_mail import mail_mail
import mail_thread import mail_thread
import email_template import email_template

View File

@ -8,11 +8,9 @@ class EmailTemplate(osv.Model):
"""Add the mass mailing campaign data to mail""" """Add the mass mailing campaign data to mail"""
_name = 'email.template' _name = 'email.template'
_inherit = ['email.template'] _inherit = ['email.template']
_columns = { _columns = {
'use_in_mass_mailing': fields.boolean('Available for marketing and mailing'), 'use_in_mass_mailing': fields.boolean('Available for marketing and mailing'),
} }
def action_new_mailing(self, cr, uid, ids, context=None): def action_new_mailing(self, cr, uid, ids, context=None):
template = self.browse(cr, uid, ids[0], context=context) template = self.browse(cr, uid, ids[0], context=context)
ctx = dict(context) ctx = dict(context)

View File

@ -22,10 +22,7 @@
from datetime import datetime from datetime import datetime
from dateutil import relativedelta from dateutil import relativedelta
import random import random
try: import json
import simplejson as json
except ImportError:
import json
import urllib import urllib
import urlparse import urlparse
@ -40,191 +37,57 @@ class MassMailingCategory(osv.Model):
"""Model of categories of mass mailing, i.e. marketing, newsletter, ... """ """Model of categories of mass mailing, i.e. marketing, newsletter, ... """
_name = 'mail.mass_mailing.category' _name = 'mail.mass_mailing.category'
_description = 'Mass Mailing Category' _description = 'Mass Mailing Category'
_order = 'name'
_columns = { _columns = {
'name': fields.char('Name', required=True), 'name': fields.char('Name', required=True),
} }
class MassMailingContact(osv.Model): class MassMailingContact(osv.Model):
"""Model of a contact. This model is different from the partner 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 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 be able to deal with large contact list to email without bloating the partner
database. """ base."""
_name = 'mail.mass_mailing.contact' _name = 'mail.mass_mailing.contact'
_description = 'Mass Mailing Contact' _description = 'Mass Mailing Contact'
_order = 'email'
_rec_name = 'email'
_columns = { _columns = {
'name': fields.char('Name', required=True), 'name': fields.char('Name'),
'email': fields.char('Email', required=True), 'email': fields.char('Email', required=True),
'list_id': fields.many2one( 'list_id': fields.many2one(
'mail.mass_mailing.list', string='Mailing List', 'mail.mass_mailing.list', string='Mailing List',
domain=[('model', '=', 'mail.mass_mailing.contact')],
ondelete='cascade', 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): def _get_latest_list(self, cr, uid, context={}):
name, email = self.pool['res.partner']._parse_partner_name(name, context=context) lid = self.pool.get('mail.mass_mailing.list').search(cr, uid, [], limit=1, order='id desc', context=context)
if name and not email: return lid and lid[0] or False
email = name _defaults = {
if email and not name: 'list_id': _get_latest_list
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): class MassMailingList(osv.Model):
"""Model of a contact list. """ """Model of a contact list. """
_name = 'mail.mass_mailing.list' _name = 'mail.mass_mailing.list'
_description = 'Contact List' _order = 'name'
_description = 'Mailing 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
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
)
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 = { _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
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,
}
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): class MassMailingStage(osv.Model):
"""Stage for mass mailing campaigns. """ """Stage for mass mailing campaigns. """
_name = 'mail.mass_mailing.stage' _name = 'mail.mass_mailing.stage'
_description = 'Mass Mailing Campaign Stage' _description = 'Mass Mailing Campaign Stage'
_order = 'sequence ASC' _order = 'sequence'
_columns = { _columns = {
'name': fields.char('Name', required=True), 'name': fields.char('Name', required=True, translate=True),
'sequence': fields.integer('Sequence'), 'sequence': fields.integer('Sequence'),
} }
_defaults = { _defaults = {
'sequence': 0, 'sequence': 0,
} }
@ -234,29 +97,6 @@ class MassMailingCampaign(osv.Model):
"""Model of mass mailing campaigns. """ """Model of mass mailing campaigns. """
_name = "mail.mass_mailing.campaign" _name = "mail.mass_mailing.campaign"
_description = '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 = { _columns = {
'name': fields.char('Name', required=True), 'name': fields.char('Name', required=True),
'stage_id': fields.many2one('mail.mass_mailing.stage', 'Stage', required=True), 'stage_id': fields.many2one('mail.mass_mailing.stage', 'Stage', required=True),
@ -264,201 +104,31 @@ class MassMailingCampaign(osv.Model):
'res.users', 'Responsible', 'res.users', 'Responsible',
required=True, required=True,
), ),
'category_id': fields.many2one( 'category_ids': fields.many2many(
'mail.mass_mailing.category', 'Category', 'mail.mass_mailing.category', 'Categories'),
help='Category'),
'mass_mailing_ids': fields.one2many( 'mass_mailing_ids': fields.one2many(
'mail.mass_mailing', 'mass_mailing_campaign_id', 'mail.mass_mailing', 'mass_mailing_campaign_id',
'Mass Mailings', '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'), '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): 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) 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 = { _defaults = {
'user_id': lambda self, cr, uid, ctx=None: uid, '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), 'stage_id': lambda self, *args: self._get_default_stage_id(*args),
} }
#------------------------------------------------------
# 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
class MassMailing(osv.Model): class MassMailing(osv.Model):
""" MassMailing models a wave of emails for a mass mailign campaign. """ MassMailing models a wave of emails for a mass mailign campaign.
A mass mailing is an occurence of sending emails. """ A mass mailing is an occurence of sending emails. """
_name = 'mail.mass_mailing' _name = 'mail.mass_mailing'
_description = 'Mass Mailing' _description = 'Mass Mailing'
# number of periods for tracking mail_mail statistics _order = 'id DESC'
_period_number = 6
_order = 'date 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
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): def _get_private_models(self, context=None):
return ['res.partner', 'mail.mass_mailing.contact'] return ['res.partner', 'mail.mass_mailing.contact']
@ -475,38 +145,26 @@ class MassMailing(osv.Model):
('mail.mass_mailing.contact', 'Contacts') ('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 = { _columns = {
'name': fields.char('Subject', required=True), 'name': fields.char('Subject', required=True),
'email_from': fields.char('From'),
'date': fields.datetime('Date'), '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)]",
),
'body_html': fields.html('Body'), 'body_html': fields.html('Body'),
'mass_mailing_campaign_id': fields.many2one( 'mass_mailing_campaign_id': fields.many2one(
'mail.mass_mailing.campaign', 'Mass Mailing Campaign', 'mail.mass_mailing.campaign', 'Mass Mailing Campaign',
ondelete='set null', ondelete='set null',
), ),
'ab_testing': fields.related( 'state': fields.selection(
'mass_mailing_campaign_id', 'ab_testing', [('draft', 'Schedule'), ('test', 'Tested'), ('done', 'Sent')], string='Status', required=True,
type='boolean', string='AB Testing'
), ),
'color': fields.related( 'color': fields.related(
'mass_mailing_campaign_id', 'color', 'mass_mailing_campaign_id', 'color',
type='integer', string='Color Index', type='integer', string='Color Index',
), ),
# mailing options # mailing options
'email_from': fields.char('From'), # TODO: simplify these 4 fields
'reply_in_thread': fields.boolean('Reply in thread'), 'reply_in_thread': fields.boolean('Reply in thread'),
'reply_specified': fields.boolean('Specific Reply-To'), 'reply_specified': fields.boolean('Specific Reply-To'),
'auto_reply_to_available': fields.function( 'auto_reply_to_available': fields.function(
@ -514,88 +172,18 @@ class MassMailing(osv.Model):
type='boolean', string='Reply in thread available' type='boolean', string='Reply in thread available'
), ),
'reply_to': fields.char('Reply To'), 'reply_to': fields.char('Reply To'),
'mailing_model': fields.selection(_mailing_model, string='Type', required=True),
# Target Emails
'mailing_model': fields.selection(_get_mailing_model, string='Model', required=True),
'mailing_domain': fields.char('Domain', required=True),
'contact_list_ids': fields.many2many( 'contact_list_ids': fields.many2many(
'mail.mass_mailing.list', 'mail_mass_mailing_list_rel', 'mail.mass_mailing.list', 'mail_mass_mailing_list_rel',
string='Mailing Lists', 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( 'contact_ab_pc': fields.integer(
'AB Testing percentage', 'AB Testing percentage',
help='Percentage of the contacts that will be mailed. Recipients will be taken randomly.' 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 = { _defaults = {
@ -611,42 +199,15 @@ class MassMailing(osv.Model):
#------------------------------------------------------ #------------------------------------------------------
def copy_data(self, cr, uid, id, default=None, context=None): def copy_data(self, cr, uid, id, default=None, context=None):
if default is None: default = default or {}
default = {}
mailing = self.browse(cr, uid, id, context=context) mailing = self.browse(cr, uid, id, context=context)
default.update({ default.update({
'state': 'draft', 'state': 'draft',
'statistics_ids': [], 'statistics_ids': [],
'state': 'draft',
'name': _('%s (duplicate)') % mailing.name, 'name': _('%s (duplicate)') % mailing.name,
}) })
return super(MassMailing, self).copy_data(cr, uid, id, default, context=context) 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 # Views & Actions
#------------------------------------------------------ #------------------------------------------------------
@ -846,80 +407,3 @@ class MassMailing(osv.Model):
return True return True
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

View File

@ -12,17 +12,17 @@
<menuitem name="Configuration" id="marketing_configuration" <menuitem name="Configuration" id="marketing_configuration"
parent="base.marketing_menu" sequence="99"/> parent="base.marketing_menu" sequence="99"/>
<!-- MASS MAILING CONTACT !--> <!-- MASS MAILING CONTACT -->
<record model="ir.ui.view" id="view_mail_mass_mailing_contact_search"> <record model="ir.ui.view" id="view_mail_mass_mailing_contact_search">
<field name="name">mail.mass_mailing.contact.search</field> <field name="name">mail.mass_mailing.contact.search</field>
<field name="model">mail.mass_mailing.contact</field> <field name="model">mail.mass_mailing.contact</field>
<field name="arch" type="xml"> <field name="arch" type="xml">
<search string="Mass Mailings"> <search string="Mailing Lists Subscribers">
<field name="name"/> <field name="name"/>
<field name="email"/> <field name="email"/>
<field name="list_id"/> <field name="list_id"/>
<separator/> <separator/>
<filter string="Available for Mass Mailing" name="not_opt_out" domain="[('opt_out', '=', False)]" <filter string="Exclude Opt Out" name="not_opt_out" domain="[('opt_out', '=', False)]"
help="Contact is not opt-out"/> help="Contact is not opt-out"/>
<group expand="0" string="Group By..."> <group expand="0" string="Group By...">
<filter string="Mailing Lists" name="group_list_id" <filter string="Mailing Lists" name="group_list_id"
@ -37,7 +37,7 @@
<field name="model">mail.mass_mailing.contact</field> <field name="model">mail.mass_mailing.contact</field>
<field name="priority">10</field> <field name="priority">10</field>
<field name="arch" type="xml"> <field name="arch" type="xml">
<tree string="Mass Mailings"> <tree string="Mailing Lists Subscribers" editable="top">
<field name="name"/> <field name="name"/>
<field name="email"/> <field name="email"/>
<field name="list_id"/> <field name="list_id"/>
@ -46,58 +46,26 @@
</field> </field>
</record> </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"> <record id="action_view_mass_mailing_contacts" model="ir.actions.act_window">
<field name="name">Mass Mailing Contacts</field> <field name="name">Mailing List Subscribers</field>
<field name="res_model">mail.mass_mailing.contact</field> <field name="res_model">mail.mass_mailing.contact</field>
<field name="view_type">form</field> <field name="view_type">form</field>
<field name="view_mode">tree,form</field> <field name="view_mode">tree</field>
<field name="context">{'search_default_not_opt_out': 1}</field> <field name="context">{'search_default_not_opt_out': 1}</field>
</record> </record>
<menuitem name="Contacts" id="menu_email_mass_mailing_contacts" groups="base.group_no_one" <menuitem name="Contacts" id="menu_email_mass_mailing_contacts"
parent="mass_mailing_list" sequence="50" parent="mass_mailing_list" sequence="50"
action="action_view_mass_mailing_contacts"/> action="action_view_mass_mailing_contacts"/>
<!-- Create a Mailing List from Contacts --> <!-- MASS MAILING LIST -->
<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"> <record model="ir.ui.view" id="view_mail_mass_mailing_list_search">
<field name="name">mail.mass_mailing.list.search</field> <field name="name">mail.mass_mailing.list.search</field>
<field name="model">mail.mass_mailing.list</field> <field name="model">mail.mass_mailing.list</field>
<field name="arch" type="xml"> <field name="arch" type="xml">
<search string="Mass Mailings"> <search string="Mailing Lists">
<field name="name"/> <field name="name"/>
<separator/> </search>
</search>
</field> </field>
</record> </record>
@ -106,39 +74,40 @@
<field name="model">mail.mass_mailing.list</field> <field name="model">mail.mass_mailing.list</field>
<field name="priority">10</field> <field name="priority">10</field>
<field name="arch" type="xml"> <field name="arch" type="xml">
<tree string="Contact Lists"> <tree string="Mailing Lists">
<field name="name"/> <field name="name"/>
<field name="model"/>
<field name="contact_nbr"/> <field name="contact_nbr"/>
</tree> </tree>
</field> </field>
</record> </record>
<record id="mass_mailing_list_open_contacts" model="ir.actions.act_window">
<field name="name">Recipients</field>
<field name="res_model">mail.mass_mailing.contact</field>
<field name="view_type">form</field>
<field name="view_mode">tree</field>
<field name="context">{'search_default_list_id': active_id}</field>
<field name="help" type="html">
<p class="oe_view_nocontent_create">
Click to create a recipient.
</p>
</field>
</record>
<record model="ir.ui.view" id="view_mail_mass_mailing_list_form"> <record model="ir.ui.view" id="view_mail_mass_mailing_list_form">
<field name="name">mail.mass_mailing.list.form</field> <field name="name">mail.mass_mailing.list.form</field>
<field name="model">mail.mass_mailing.list</field> <field name="model">mail.mass_mailing.list</field>
<field name="arch" type="xml"> <field name="arch" type="xml">
<form string="Contact List" version="7.0"> <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> <sheet>
<!-- TODO: stat_get button and relate link -->
<div class="oe_button_box" nname="buttons">
<button name="%(mass_mailing.mass_mailing_list_open_contacts)d" type="action"
string="Recipients"/>
</div>
<group> <group>
<field name="name" string="Mailing List Name"/> <field name="name"/>
<label for="contact_nbr"/> <field 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> </group>
</sheet> </sheet>
</form> </form>
@ -150,15 +119,17 @@
<field name="res_model">mail.mass_mailing.list</field> <field name="res_model">mail.mass_mailing.list</field>
<field name="view_type">form</field> <field name="view_type">form</field>
<field name="view_mode">tree,form</field> <field name="view_mode">tree,form</field>
<field name="help" type="html"> <field name="help" type="html">
<p class="oe_view_nocontent_create"> <p class="oe_view_nocontent_create">
Click here to create a new mailing list. Click here to create a new mailing list.
</p><p> </p><p>
Mailing lists allows you to to manage customers and contacts easily and to send to mailings in a single click. Mailing lists allows you to to manage customers and
</p></field> contacts easily and to send to mailings in a single click.
</p>
</field>
</record> </record>
<menuitem name="Contact Lists" id="menu_email_mass_mailing_lists" <menuitem name="Mailing Lists" id="menu_email_mass_mailing_lists"
parent="mass_mailing_list" sequence="40" parent="mass_mailing_list" sequence="40"
action="action_view_mass_mailing_lists"/> action="action_view_mass_mailing_lists"/>
@ -170,15 +141,12 @@
<search string="Mass Mailings"> <search string="Mass Mailings">
<field name="name" string="Mailings"/> <field name="name" string="Mailings"/>
<field name="mass_mailing_campaign_id"/> <field name="mass_mailing_campaign_id"/>
<field name="template_id"/>
<group expand="0" string="Group By..."> <group expand="0" string="Group By...">
<filter string="State" name="group_state" <filter string="State" name="group_state"
context="{'group_by': 'state'}"/> context="{'group_by': 'state'}"/>
<filter string="Campaign" name="group_mass_mailing_campaign_id" <filter string="Campaign" name="group_mass_mailing_campaign_id"
groups="mass_mailing.group_mass_mailing_campaign" groups="mass_mailing.group_mass_mailing_campaign"
context="{'group_by': 'mass_mailing_campaign_id'}"/> context="{'group_by': 'mass_mailing_campaign_id'}"/>
<filter string="Template" name="group_template_id"
context="{'group_by': 'template_id'}"/>
</group> </group>
</search> </search>
</field> </field>
@ -197,7 +165,6 @@
<field name="replied"/> <field name="replied"/>
<field name="mass_mailing_campaign_id" <field name="mass_mailing_campaign_id"
groups="mass_mailing.group_mass_mailing_campaign"/> groups="mass_mailing.group_mass_mailing_campaign"/>
<field name="template_id" invisible="1"/>
</tree> </tree>
</field> </field>
</record> </record>
@ -208,20 +175,21 @@
<field name="arch" type="xml"> <field name="arch" type="xml">
<form string="Mass Mailing" version="7.0"> <form string="Mass Mailing" version="7.0">
<header> <header>
<field name="state" widget="statusbar" clickable="True"/>
<button name="action_test_mailing" type="object" <button name="action_test_mailing" type="object"
class="oe_highlight" string="Test Mailing"/> class="oe_highlight" string="Test Mailing"/>
<button name="send_mail" type="object" <button name="send_mail" type="object"
class="oe_highlight" string="Send to All"/> class="oe_highlight" string="Send to All"/>
<field name="state" widget="statusbar" clickable="True"/>
</header> </header>
<sheet> <sheet>
<div colspan="2" class="oe_form_box_info oe_text_center" <div class="oe_form_box_info oe_text_center"
attrs="{'invisible': [('scheduled', '=', 0)]}"> attrs="{'invisible': [('scheduled', '=', 0)]}">
<p> <p>
<strong><field name="scheduled" class="oe_inline"/>emails are in queue <strong><field name="scheduled" class="oe_inline"/>emails are in queue
and will be sent soon.</strong> and will be sent soon.</strong>
</p> </p>
</div> </div>
<group> <group>
<group> <group>
<field name="email_from"/> <field name="email_from"/>
@ -512,7 +480,6 @@
<field name="name"/> <field name="name"/>
<field name="user_id"/> <field name="user_id"/>
<field name="category_id"/> <field name="category_id"/>
<field name="ab_testing"/>
</group> </group>
<group> <group>
<field name="total" invisible="1"/> <field name="total" invisible="1"/>

View File

@ -2,4 +2,3 @@
import test_mailing import test_mailing
import mail_compose_message import mail_compose_message
import mailing_list_confirm

View File

@ -1,57 +0,0 @@
# -*- coding: utf-8 -*-
from openerp.osv import osv, fields
from openerp.tools.translate import _
class MailingListConfirm(osv.TransientModel):
"""A wizard that acts as a confirmation when creating a new mailing list coming
from a list view. This allows to have a lightweight flow to create mailing
lists without having to go through multiple screens."""
_inherit = 'mail.mass_mailing.list'
_name = 'mail.mass_mailing.list.confirm'
_columns = {
'mass_mailing_id': fields.many2one('mail.mass_mailing', 'Mailing'),
}
def create(self, cr, uid, values, context=None):
if context is None:
context = {}
context.update(no_contact_to_list=True)
return super(MailingListConfirm, self).create(cr, uid, values, context=context)
def action_confirm(self, cr, uid, ids, context=None):
wizard = self.browse(cr, uid, ids[0], context=context)
mailing_list_id = self.pool['mail.mass_mailing.list'].create(
cr, uid, {'name': wizard.name, 'model': wizard.model}, context=context)
res = self.pool['mail.mass_mailing.list'].action_add_to_mailing(cr, uid, [mailing_list_id], context=context)
if not res:
return {
'type': 'ir.actions.act_window',
'view_type': 'form',
'view_mode': 'form',
'res_model': 'mail.mass_mailing.list',
'res_id': mailing_list_id,
'context': context,
}
return res
def action_new_list(self, cr, uid, ids, context=None):
wizard = self.browse(cr, uid, ids[0], context=context)
action_id = self.pool['mail.mass_mailing']._get_model_to_list_action_id(cr, uid, wizard.model, context=context)
if wizard.model == 'mail.mass_mailing.contact':
domain = [('list_id', '=', False)]
else:
domain = []
ctx = dict(context, search_default_not_opt_out=True, view_manager_highlight=[action_id], default_name=wizard.name, default_model=wizard.model)
return {
'name': _('Choose Recipients'),
'type': 'ir.actions.act_window',
'view_type': 'form',
'view_mode': 'tree,form',
'res_model': wizard.model,
'context': ctx,
'domain': domain,
}

View File

@ -1,67 +0,0 @@
<?xml version="1.0"?>
<openerp>
<data>
<record model="ir.ui.view" id="view_mail_mass_mailing_list_confirm_form">
<field name="name">mail.mass_mailing.list.confirm.form</field>
<field name="model">mail.mass_mailing.list.confirm</field>
<field name="arch" type="xml">
<form string="Confirm Mailing List" version="7.0">
<group>
<field name="model" invisible="1"/>
<field name="domain" invisible="1"
on_change="on_change_domain(domain, model, context)"/>
<div attrs="{'invisible': [('mass_mailing_id', '=', False)]}" colspan="2">
Adding <field name="contact_nbr" class="oe_inline"/> contacts
to the mailing <field name="mass_mailing_id" class="oe_inline" readonly="1"/>.
</div>
<field name="name" attrs="{'invisible': [('mass_mailing_id', '!=', False)]}"/>
</group>
<footer>
<button string="Confirm" name="action_confirm" type="object" class="oe_highlight"/>
or
<button string="Cancel" class="oe_link" special="cancel" />
</footer>
</form>
</field>
</record>
<record model="ir.ui.view" id="view_mail_mass_mailing_list_create_form">
<field name="name">mail.mass_mailing.list.confirm.form</field>
<field name="model">mail.mass_mailing.list.confirm</field>
<field name="priority">32</field>
<field name="arch" type="xml">
<form string="Create a Mailing List" version="7.0">
<header>
<button string='Create a New List' class="oe_highlight"
type='object' name='action_new_list'/>
</header>
<sheet>
<group>
<field name="name"/>
<field name="model" widget="radio"/>
</group>
</sheet>
</form>
</field>
</record>
<record id="action_mail_mass_mailing_list_confirm" model="ir.actions.act_window">
<field name="name">Create a Mailing List</field>
<field name="res_model">mail.mass_mailing.list.confirm</field>
<field name="view_type">form</field>
<field name="view_mode">form</field>
<field name="target">new</field>
</record>
<record id="action_mail_mass_mailing_create" model="ir.actions.act_window">
<field name="name">Create a Mailing List</field>
<field name="res_model">mail.mass_mailing.list.confirm</field>
<field name="view_type">form</field>
<field name="view_mode">form</field>
<field name="view_id" ref="view_mail_mass_mailing_list_create_form"/>
<field name="target">new</field>
</record>
</data>
</openerp>