[REF] mass_mailing: first refactor

Mail statistics are now stored onto a separated object (mail.mail.statistics), allowing to
handle emails separately from statistics (among other removing mail.mail entries while keeping
statistics).

Everything linnked to opened/replied/bounce is not managed by mass_mailing, removed added code
in mail module.

bzr revid: tde@openerp.com-20130913115408-322cyjipdg680as6
This commit is contained in:
Thibault Delavallée 2013-09-13 13:54:08 +02:00
parent 23f9324b94
commit ed62d1dac7
24 changed files with 431 additions and 347 deletions

View File

@ -5,7 +5,6 @@ import openerp
from openerp import SUPERUSER_ID from openerp import SUPERUSER_ID
import openerp.addons.web.http as http import openerp.addons.web.http as http
from openerp.addons.web.controllers.main import content_disposition from openerp.addons.web.controllers.main import content_disposition
from openerp.addons.web.http import request
class MailController(http.Controller): class MailController(http.Controller):
@ -38,10 +37,3 @@ class MailController(http.Controller):
except psycopg2.Error: except psycopg2.Error:
pass pass
return True return True
@http.route('/mail/track/<int:mail_id>/blank.gif', type='http', auth='admin')
def track_read_email(self, mail_id):
""" Email tracking. """
mail_mail = request.registry.get('mail.mail')
mail_mail.set_opened(request.cr, request.uid, [mail_id])
return False

View File

@ -51,12 +51,6 @@
<field name="value">catchall</field> <field name="value">catchall</field>
</record> </record>
<!-- Bounce Email Alias -->
<record id="icp_mail_bounce_alias" model="ir.config_parameter">
<field name="key">mail.bounce.alias</field>
<field name="value">bounce</field>
</record>
<!-- Discussion subtype for messaging / Chatter --> <!-- Discussion subtype for messaging / Chatter -->
<record id="mt_comment" model="mail.message.subtype"> <record id="mt_comment" model="mail.message.subtype">
<field name="name">Discussions</field> <field name="name">Discussions</field>

View File

@ -62,17 +62,6 @@ class mail_mail(osv.Model):
# and during unlink() we will not cascade delete the parent and its attachments # and during unlink() we will not cascade delete the parent and its attachments
'notification': fields.boolean('Is Notification', 'notification': fields.boolean('Is Notification',
help='Mail has been created to notify people of an existing mail.message'), help='Mail has been created to notify people of an existing mail.message'),
# Bounce and tracking
'opened': fields.datetime(
'Opened',
help='Date when this email has been opened for the first time.'),
'replied': fields.datetime(
'Replied',
help='Date when this email has been replied for the first time.'),
'bounced': fields.datetime(
'Bounced',
help='Date when this email has bounced.'
),
} }
_defaults = { _defaults = {
@ -106,30 +95,6 @@ class mail_mail(osv.Model):
def cancel(self, cr, uid, ids, context=None): def cancel(self, cr, uid, ids, context=None):
return self.write(cr, uid, ids, {'state': 'cancel'}, context=context) return self.write(cr, uid, ids, {'state': 'cancel'}, context=context)
def set_opened(self, cr, uid, ids, context=None):
""" Set as opened """
existing_ids = self.exists(cr, uid, ids, context=context)
for mail in self.browse(cr, uid, existing_ids, context=context):
if not mail.opened:
self.write(cr, uid, [mail.id], {'opened': fields.datetime.now()}, context=context)
return True
def set_replied(self, cr, uid, ids, context=None):
""" Set as replied """
existing_ids = self.exists(cr, uid, ids, context=context)
for mail in self.browse(cr, uid, existing_ids, context=context):
if not mail.replied:
self.write(cr, uid, [mail.id], {'replied': fields.datetime.now()}, context=context)
return True
def set_bounced(self, cr, uid, ids, context=None):
""" Set as bounced """
existing_ids = self.exists(cr, uid, ids, context=context)
for mail in self.browse(cr, uid, existing_ids, context=context):
if not mail.bounced:
self.write(cr, uid, [mail.id], {'bounced': fields.datetime.now()}, context=context)
return True
def process_email_queue(self, cr, uid, ids=None, context=None): def process_email_queue(self, cr, uid, ids=None, context=None):
"""Send immediately queued messages, committing after each """Send immediately queued messages, committing after each
message is sent - this is not transactional and should message is sent - this is not transactional and should
@ -200,15 +165,6 @@ class mail_mail(osv.Model):
else: else:
return None return None
def _get_tracking_url(self, cr, uid, mail, partner=None, context=None):
if not mail.auto_delete:
base_url = self.pool.get('ir.config_parameter').get_param(cr, uid, 'web.base.url')
track_url = urljoin(base_url, 'mail/track/%d/blank.gif' % mail.id)
print base_url, track_url
return '<img src="%s" alt=""/>' % track_url
else:
return ''
def send_get_mail_subject(self, cr, uid, mail, force=False, partner=None, context=None): def send_get_mail_subject(self, cr, uid, mail, force=False, partner=None, context=None):
""" If subject is void and record_name defined: '<Author> posted on <Resource>' """ If subject is void and record_name defined: '<Author> posted on <Resource>'
@ -233,11 +189,8 @@ class mail_mail(osv.Model):
# generate footer # generate footer
link = self._get_partner_access_link(cr, uid, mail, partner, context=context) link = self._get_partner_access_link(cr, uid, mail, partner, context=context)
tracking_url = self._get_tracking_url(cr, uid, mail, partner, context=context)
if link: if link:
body = tools.append_content_to_html(body, link, plaintext=False, container_tag='div') body = tools.append_content_to_html(body, link, plaintext=False, container_tag='div')
if tracking_url:
body = tools.append_content_to_html(body, tracking_url, plaintext=False, container_tag='div')
return body return body
def send_get_email_dict(self, cr, uid, mail, partner=None, context=None): def send_get_email_dict(self, cr, uid, mail, partner=None, context=None):

View File

@ -41,11 +41,6 @@
<field name="model"/> <field name="model"/>
<field name="res_id"/> <field name="res_id"/>
</group> </group>
<group string="Tracking">
<field name="opened"/>
<field name="replied"/>
<field name="bounced"/>
</group>
</div> </div>
<div> <div>
<group string="Headers"> <group string="Headers">

View File

@ -778,7 +778,6 @@ class mail_thread(osv.AbstractModel):
""" """
assert isinstance(message, Message), 'message must be an email.message.Message at this point' assert isinstance(message, Message), 'message must be an email.message.Message at this point'
fallback_model = model fallback_model = model
bounce_alias = self.pool['ir.config_parameter'].get_param(cr, uid, "mail.bounce.alias", context=context)
# Get email.message.Message variables for future processing # Get email.message.Message variables for future processing
message_id = message.get('Message-Id') message_id = message.get('Message-Id')
@ -787,25 +786,6 @@ class mail_thread(osv.AbstractModel):
references = decode_header(message, 'References') references = decode_header(message, 'References')
in_reply_to = decode_header(message, 'In-Reply-To') in_reply_to = decode_header(message, 'In-Reply-To')
# 0. Verify whether this is a bounced email (wrong destination,...) -> use it to collect data, such as dead leads
if bounce_alias in email_to:
bounce_match = tools.bounce_re.search(email_to)
if bounce_match:
bounced_mail_id = bounce_match.group(1)
self.pool['mail.mail'].set_bounced(cr, uid, [bounced_mail_id], context=context)
if self.pool['mail.mail'].exists(cr, uid, bounced_mail_id):
mail = self.pool['mail.mail'].browse(cr, uid, bounced_mail_id, context=context)
bounced_model = mail.model
bounced_thread_id = mail.res_id
else:
bounced_model = bounce_match.group(2)
bounced_thread_id = int(bounce_match.group(3)) if bounce_match.group(3) else 0
_logger.info('Routing mail from %s to %s with Message-Id %s: bounced mail from mail %s, model: %s, thread_id: %s',
email_from, email_to, message_id, bounced_mail_id, bounced_model, bounced_thread_id)
if bounced_model and bounced_model in self.pool and hasattr(self.pool[bounced_model], 'message_receive_bounce'):
self.pool[bounced_model].message_receive_bounce(cr, uid, [bounced_thread_id], mail_id=bounced_mail_id, context=context)
return []
# 1. Verify if this is a reply to an existing thread # 1. Verify if this is a reply to an existing thread
thread_references = references or in_reply_to thread_references = references or in_reply_to
ref_match = thread_references and tools.reference_re.search(thread_references) ref_match = thread_references and tools.reference_re.search(thread_references)
@ -894,6 +874,40 @@ class mail_thread(osv.AbstractModel):
"No possible route found for incoming message from %s to %s (Message-Id %s:)." \ "No possible route found for incoming message from %s to %s (Message-Id %s:)." \
"Create an appropriate mail.alias or force the destination model." % (email_from, email_to, message_id) "Create an appropriate mail.alias or force the destination model." % (email_from, email_to, message_id)
def message_route_process(self, cr, uid, msg, routes, context=None):
# postpone setting msg.partner_ids after message_post, to avoid double notifications
partner_ids = msg.pop('partner_ids', [])
thread_id = False
for model, thread_id, custom_values, user_id, alias in routes:
if self._name == 'mail.thread':
context.update({'thread_model': model})
if model:
model_pool = self.pool[model]
assert thread_id and hasattr(model_pool, 'message_update') or hasattr(model_pool, 'message_new'), \
"Undeliverable mail with Message-Id %s, model %s does not accept incoming emails" % \
(msg['message_id'], model)
# disabled subscriptions during message_new/update to avoid having the system user running the
# email gateway become a follower of all inbound messages
nosub_ctx = dict(context, mail_create_nosubscribe=True, mail_create_nolog=True)
if thread_id and hasattr(model_pool, 'message_update'):
model_pool.message_update(cr, user_id, [thread_id], msg, context=nosub_ctx)
else:
thread_id = model_pool.message_new(cr, user_id, msg, custom_values, context=nosub_ctx)
else:
assert thread_id == 0, "Posting a message without model should be with a null res_id, to create a private message."
model_pool = self.pool.get('mail.thread')
if not hasattr(model_pool, 'message_post'):
context['thread_model'] = model
model_pool = self.pool['mail.thread']
new_msg_id = model_pool.message_post(cr, uid, [thread_id], context=context, subtype='mail.mt_comment', **msg)
if partner_ids:
# postponed after message_post, because this is an external message and we don't want to create
# duplicate emails due to notifications
self.pool.get('mail.message').write(cr, uid, [new_msg_id], {'partner_ids': partner_ids}, context=context)
return thread_id
def message_process(self, cr, uid, model, message, custom_values=None, def message_process(self, cr, uid, model, message, custom_values=None,
save_original=False, strip_attachments=False, save_original=False, strip_attachments=False,
thread_id=None, context=None): thread_id=None, context=None):
@ -946,8 +960,7 @@ class mail_thread(osv.AbstractModel):
msg = self.message_parse(cr, uid, msg_txt, save_original=save_original, context=context) msg = self.message_parse(cr, uid, msg_txt, save_original=save_original, context=context)
if strip_attachments: if strip_attachments:
msg.pop('attachments', None) msg.pop('attachments', None)
# postpone setting msg.partner_ids after message_post, to avoid double notifications
partner_ids = msg.pop('partner_ids', [])
if msg.get('message_id'): # should always be True as message_parse generate one if missing if msg.get('message_id'): # should always be True as message_parse generate one if missing
existing_msg_ids = self.pool.get('mail.message').search(cr, SUPERUSER_ID, [ existing_msg_ids = self.pool.get('mail.message').search(cr, SUPERUSER_ID, [
('message_id', '=', msg.get('message_id')), ('message_id', '=', msg.get('message_id')),
@ -959,36 +972,7 @@ class mail_thread(osv.AbstractModel):
# find possible routes for the message # find possible routes for the message
routes = self.message_route(cr, uid, msg_txt, msg, model, thread_id, custom_values, context=context) routes = self.message_route(cr, uid, msg_txt, msg, model, thread_id, custom_values, context=context)
thread_id = False thread_id = self.message_route_process(cr, uid, msg, routes, context=context)
for model, thread_id, custom_values, user_id, alias in routes:
if self._name == 'mail.thread':
context.update({'thread_model': model})
if model:
model_pool = self.pool[model]
assert thread_id and hasattr(model_pool, 'message_update') or hasattr(model_pool, 'message_new'), \
"Undeliverable mail with Message-Id %s, model %s does not accept incoming emails" % \
(msg['message_id'], model)
# disabled subscriptions during message_new/update to avoid having the system user running the
# email gateway become a follower of all inbound messages
nosub_ctx = dict(context, mail_create_nosubscribe=True, mail_create_nolog=True)
if thread_id and hasattr(model_pool, 'message_update'):
model_pool.message_update(cr, user_id, [thread_id], msg, context=nosub_ctx)
else:
thread_id = model_pool.message_new(cr, user_id, msg, custom_values, context=nosub_ctx)
else:
assert thread_id == 0, "Posting a message without model should be with a null res_id, to create a private message."
model_pool = self.pool.get('mail.thread')
if not hasattr(model_pool, 'message_post'):
context['thread_model'] = model
model_pool = self.pool['mail.thread']
new_msg_id = model_pool.message_post(cr, uid, [thread_id], context=context, subtype='mail.mt_comment', **msg)
if partner_ids:
# postponed after message_post, because this is an external message and we don't want to create
# duplicate emails due to notifications
self.pool.get('mail.message').write(cr, uid, [new_msg_id], {'partner_ids': partner_ids}, context=context)
return thread_id return thread_id
def message_new(self, cr, uid, msg_dict, custom_values=None, context=None): def message_new(self, cr, uid, msg_dict, custom_values=None, context=None):
@ -1044,15 +1028,6 @@ class mail_thread(osv.AbstractModel):
self.write(cr, uid, ids, update_vals, context=context) self.write(cr, uid, ids, update_vals, context=context)
return True return True
def message_receive_bounce(self, cr, uid, ids, mail_id=None, context=None):
"""Called by ``message_process`` when a bounce email (such as Undelivered
Mail Returned to Sender) is received for an existing thread. The default
behavior is to check is an integer ``message_bounce`` column exists.
If it is the case, its content is incremented. """
if self._all_columns.get('message_bounce'):
for obj in self.browse(cr, uid, ids, context=context):
self.write(cr, uid, [obj.id], {'message_bounce': obj.message_bounce + 1}, context=context)
def _message_extract_payload(self, message, save_original=False): def _message_extract_payload(self, message, save_original=False):
"""Extract body as HTML and attachments from the mail message""" """Extract body as HTML and attachments from the mail message"""
attachments = [] attachments = []
@ -1303,8 +1278,8 @@ class mail_thread(osv.AbstractModel):
return result return result
def message_post(self, cr, uid, thread_id, body='', subject=None, type='notification', def message_post(self, cr, uid, thread_id, body='', subject=None, type='notification',
subtype=None, parent_id=False, attachments=None, context=None, subtype=None, parent_id=False, attachments=None, context=None,
content_subtype='html', **kwargs): content_subtype='html', **kwargs):
""" Post a new message in an existing thread, returning the new """ Post a new message in an existing thread, returning the new
mail.message ID. mail.message ID.

View File

@ -32,14 +32,6 @@ class project_configuration(osv.TransientModel):
'Alias Domain', 'Alias Domain',
help="If you have setup a catch-all email domain redirected to the OpenERP server, enter the domain name here." help="If you have setup a catch-all email domain redirected to the OpenERP server, enter the domain name here."
), ),
'alias_bounce': fields.char(
'Return-Path for Emails',
help="Return-Path of send Emails. Used to compute bounced emails.",
),
'alias_catchall': fields.char(
'Default Alias',
help='Default email alias',
),
} }
def get_default_alias_domain(self, cr, uid, ids, context=None): def get_default_alias_domain(self, cr, uid, ids, context=None):
@ -56,21 +48,3 @@ class project_configuration(osv.TransientModel):
config_parameters = self.pool.get("ir.config_parameter") config_parameters = self.pool.get("ir.config_parameter")
for record in self.browse(cr, uid, ids, context=context): for record in self.browse(cr, uid, ids, context=context):
config_parameters.set_param(cr, uid, "mail.catchall.domain", record.alias_domain or '', context=context) config_parameters.set_param(cr, uid, "mail.catchall.domain", record.alias_domain or '', context=context)
def get_default_alias_bounce(self, cr, uid, ids, context=None):
alias_bounce = self.pool.get("ir.config_parameter").get_param(cr, uid, "mail.bounce.alias", context=context)
return {'alias_bounce': alias_bounce}
def set_alias_bounce(self, cr, uid, ids, context=None):
config_parameters = self.pool.get("ir.config_parameter")
for record in self.browse(cr, uid, ids, context=context):
config_parameters.set_param(cr, uid, "mail.bounce.alias", record.alias_bounce or '', context=context)
def get_default_alias_catchall(self, cr, uid, ids, context=None):
alias_catchall = self.pool.get("ir.config_parameter").get_param(cr, uid, "mail.catchall.alias", context=context)
return {'alias_catchall': alias_catchall}
def set_alias_catchall(self, cr, uid, ids, context=None):
config_parameters = self.pool.get("ir.config_parameter")
for record in self.browse(cr, uid, ids, context=context):
config_parameters.set_param(cr, uid, "mail.catchall.alias", record.alias_catchall or '', context=context)

View File

@ -11,14 +11,6 @@
<label for="alias_domain" class="oe_inline"/> <label for="alias_domain" class="oe_inline"/>
<field name="alias_domain" placeholder="mycompany.my.openerp.com" class="oe_inline"/> <field name="alias_domain" placeholder="mycompany.my.openerp.com" class="oe_inline"/>
</div> </div>
<div>
<label for="alias_bounce" class="oe_inline"/>
<field name="alias_bounce" placeholder="bounce" class="oe_inline"/>
</div>
<div>
<label for="alias_catchall" class="oe_inline"/>
<field name="alias_catchall" placeholder="catchall" class="oe_inline"/>
</div>
</xpath> </xpath>
</field> </field>
</record> </record>

View File

@ -19,10 +19,9 @@
# #
############################################################################## ##############################################################################
from . import test_mail_mail, test_mail_group, test_mail_message, test_mail_features, test_mail_gateway, test_message_read, test_invite from . import test_mail_group, test_mail_message, test_mail_features, test_mail_gateway, test_message_read, test_invite
checks = [ checks = [
# test_mail_mail,
test_mail_group, test_mail_group,
test_mail_message, test_mail_message,
test_mail_features, test_mail_features,

View File

@ -22,3 +22,4 @@
import mass_mailing import mass_mailing
import mail_mail import mail_mail
import wizard import wizard
import controllers

View File

@ -21,6 +21,7 @@
{ {
'name': 'Mass Mailing Campaigns', 'name': 'Mass Mailing Campaigns',
'description': """TODO""",
'version': '1.0', 'version': '1.0',
'author': 'OpenERP', 'author': 'OpenERP',
'website': 'http://www.openerp.com', 'website': 'http://www.openerp.com',
@ -31,11 +32,10 @@
'web_kanban_gauge', 'web_kanban_gauge',
'web_kanban_sparkline', 'web_kanban_sparkline',
], ],
'description': """TODO""",
'data': [ 'data': [
'mail_data.xml',
'mass_mailing_view.xml', 'mass_mailing_view.xml',
'mass_mailing_demo.xml', 'mass_mailing_demo.xml',
'mail_mail_view.xml',
'wizard/mail_compose_message_view.xml', 'wizard/mail_compose_message_view.xml',
'wizard/mail_mass_mailing_create_segment.xml', 'wizard/mail_mass_mailing_create_segment.xml',
'security/ir.model.access.csv', 'security/ir.model.access.csv',

View File

@ -0,0 +1,3 @@
import main
# vim:expandtab:tabstop=4:softtabstop=4:shiftwidth=4:

View File

@ -0,0 +1,12 @@
import openerp.addons.web.http as http
from openerp.addons.web.http import request
class MassMailController(http.Controller):
@http.route('/mail/track/<int:mail_id>/blank.gif', type='http', auth='admin')
def track_mail_open(self, mail_id):
""" Email tracking. """
mail_mail_stats = request.registry.get('mail.mail.statistics')
mail_mail_stats.set_opened(request.cr, request.uid, mail_ids=[mail_id])
return False

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data noupdate="1">
<!-- Bounce Email Alias -->
<record id="icp_mail_bounce_alias" model="ir.config_parameter">
<field name="key">mail.bounce.alias</field>
<field name="value">bounce</field>
</record>
</data>
</openerp>

View File

@ -2,7 +2,7 @@
############################################################################## ##############################################################################
# #
# OpenERP, Open Source Management Solution # OpenERP, Open Source Management Solution
# Copyright (C) 2013-today OpenERP SA (<http://www.openerp.com>) # Copyright (C) 2013-Today OpenERP SA (<http://www.openerp.com>)
# #
# This program is free software: you can redistribute it and/or modify # This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as # it under the terms of the GNU Affero General Public License as
@ -19,7 +19,11 @@
# #
############################################################################## ##############################################################################
from openerp.osv import osv, fields from urlparse import urljoin
from openerp import tools
from openerp import SUPERUSER_ID
from openerp.osv import osv
class MailMail(osv.Model): class MailMail(osv.Model):
@ -27,23 +31,29 @@ class MailMail(osv.Model):
_name = 'mail.mail' _name = 'mail.mail'
_inherit = ['mail.mail'] _inherit = ['mail.mail']
_columns = { def create(self, cr, uid, values, context=None):
'mass_mailing_segment_id': fields.many2one( """ Override mail_mail creation to create an entry in mail.mail.statistics """
'mail.mass_mailing.segment', 'Mass Mailing Segment', # TDE note: should be after 'all values computed', to have values (FIXME after merging other branch holding create refactoring)
ondelete='set null', mail_id = super(MailMail, self).create(cr, uid, values, context=context)
), message_id = self.browse(cr, SUPERUSER_ID, mail_id).message_id
'mass_mailing_campaign_id': fields.related( self.pool['mail.mail.statistics'].create(
'mass_mailing_segment_id', 'mass_mailing_campaign_id', cr, uid, {
type='many2one', ondelete='set null', 'mail_mail_id': mail_id,
relation='mail.mass_mailing.campaign', 'message_id': message_id,
string='Mass Mailing Campaign', }, context=context)
store=True, readonly=True, return mail_id
),
'template_id': fields.related( def _get_tracking_url(self, cr, uid, mail, partner=None, context=None):
'mass_mailing_segment_id', 'template_id', base_url = self.pool.get('ir.config_parameter').get_param(cr, uid, 'web.base.url')
type='many2one', ondelete='set null', track_url = urljoin(base_url, 'mail/track/%d/blank.gif' % mail.id)
relation='email.template', return '<img src="%s" alt=""/>' % track_url
string='Email Template',
store=True, readonly=True, def send_get_mail_body(self, cr, uid, mail, partner=None, context=None):
), """ Override to add the tracking URL to the body. """
} body = super(MailMail, self).send_get_mail_body(cr, uid, mail, partner=partner, context=context)
# generate tracking URL
tracking_url = self._get_tracking_url(cr, uid, mail, partner, context=context)
if tracking_url:
body = tools.append_content_to_html(body, tracking_url, plaintext=False, container_tag='div')
return body

View File

@ -1,20 +0,0 @@
<?xml version="1.0"?>
<openerp>
<data>
<!-- FOLLOWERS !-->
<record model="ir.ui.view" id="mail_mail_form_mass_mailing">
<field name="name">mail.mail.form.mass_mailing</field>
<field name="model">mail.mail</field>
<field name="inherit_id" ref="mail.view_mail_form"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='opened']" position="before">
<field name="mass_mailing_campaign_id"/>
<field name="mass_mailing_segment_id"/>
<field name="template_id"/>
</xpath>
</field>
</record>
</data>
</openerp>

View File

@ -0,0 +1,82 @@
# -*- 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/>
#
##############################################################################
import logging
from openerp import tools
from openerp.addons.mail.mail_thread import decode_header
from openerp.osv import osv
_logger = logging.getLogger(__name__)
class MailThread(osv.Model):
""" Update MailThread to add the feature of bounced emails and replied emails
in message_process. """
_name = 'mail.thread'
_inherit = ['mail.thread']
def message_route_check_bounce(self, cr, uid, message, context=None):
bounce_alias = self.pool['ir.config_parameter'].get_param(cr, uid, "mail.bounce.alias", context=context)
message_id = message.get('Message-Id')
email_from = decode_header(message, 'From')
email_to = decode_header(message, 'To')
# 0. Verify whether this is a bounced email (wrong destination,...) -> use it to collect data, such as dead leads
if bounce_alias in email_to:
bounce_match = tools.bounce_re.search(email_to)
if bounce_match:
bounced_mail_id = bounce_match.group(1)
self.pool['mail.mail'].set_bounced(cr, uid, [bounced_mail_id], context=context)
if self.pool['mail.mail'].exists(cr, uid, bounced_mail_id):
mail = self.pool['mail.mail'].browse(cr, uid, bounced_mail_id, context=context)
bounced_model = mail.model
bounced_thread_id = mail.res_id
else:
bounced_model = bounce_match.group(2)
bounced_thread_id = int(bounce_match.group(3)) if bounce_match.group(3) else 0
_logger.info('Routing mail from %s to %s with Message-Id %s: bounced mail from mail %s, model: %s, thread_id: %s',
email_from, email_to, message_id, bounced_mail_id, bounced_model, bounced_thread_id)
if bounced_model and bounced_model in self.pool and hasattr(self.pool[bounced_model], 'message_receive_bounce'):
self.pool[bounced_model].message_receive_bounce(cr, uid, [bounced_thread_id], mail_id=bounced_mail_id, context=context)
return False
return True
def message_route(self, cr, uid, message, message_dict, model=None, thread_id=None,
custom_values=None, context=None):
if not self.message_route_check_bounce(cr, uid, message, context=context):
return []
return super(MailThread, self).message_route(cr, uid, message, message_dict, model, thread_id, custom_values, context)
def message_receive_bounce(self, cr, uid, ids, mail_id=None, context=None):
"""Called by ``message_process`` when a bounce email (such as Undelivered
Mail Returned to Sender) is received for an existing thread. The default
behavior is to check is an integer ``message_bounce`` column exists.
If it is the case, its content is incremented. """
if self._all_columns.get('message_bounce'):
for obj in self.browse(cr, uid, ids, context=context):
self.write(cr, uid, [obj.id], {'message_bounce': obj.message_bounce + 1}, context=context)
def message_route_process(self, cr, uid, msg, routes, context=None):
if msg.get('message_id'):
self.pool['mail.mail.statistics'].set_replied(cr, uid, mail_message_ids=[msg.get('message_id')], context=context)
return super(MailThread, self).message_route_process(cr, uid, msg, routes, context=context)

View File

@ -38,42 +38,46 @@ class MassMailingCampaign(osv.Model):
results = dict.fromkeys(ids, False) results = dict.fromkeys(ids, False)
for campaign in self.browse(cr, uid, ids, context=context): for campaign in self.browse(cr, uid, ids, context=context):
results[campaign.id] = { results[campaign.id] = {
'sent': len(campaign.mail_ids), 'sent': len(campaign.statistics_ids),
'opened': len([mail for mail in campaign.mail_ids if mail.opened]),
'replied': len([mail for mail in campaign.mail_ids if mail.replied]),
'bounced': len([mail for mail in campaign.mail_ids if mail.bounced]),
# delivered: shouldn't be: all mails - (failed + bounced) ? # delivered: shouldn't be: all mails - (failed + bounced) ?
'delivered': len([mail for mail in campaign.mail_ids if mail.state == 'sent' and not mail.bounced]), 'delivered': len([stat for stat in campaign.statistics_ids if not stat.bounced]), # stat.state == 'sent' and
'opened': len([stat for stat in campaign.statistics_ids if stat.opened]),
'replied': len([stat for stat in campaign.statistics_ids if stat.replied]),
'bounced': len([stat for stat in campaign.statistics_ids if stat.bounced]),
} }
return results return results
def _get_segment_kanban_ids(self, cr, uid, ids, name, arg, context=None): def _get_mass_mailing_kanban_ids(self, cr, uid, ids, name, arg, context=None):
results = dict.fromkeys(ids, '') results = dict.fromkeys(ids, '')
for campaign in self.browse(cr, uid, ids, context=context): for campaign in self.browse(cr, uid, ids, context=context):
segment_results = [] mass_mailing_results = []
for segment in campaign.segment_ids: for mass_mailing in campaign.mass_mailing_ids:
segment_object = {} mass_mailing_object = {}
for attr in ['name', 'sent', 'delivered', 'opened', 'replied', 'bounced']: for attr in ['name', 'sent', 'delivered', 'opened', 'replied', 'bounced']:
segment_object[attr] = getattr(segment, attr) mass_mailing_object[attr] = getattr(mass_mailing, attr)
segment_results.append(segment_object) mass_mailing_results.append(mass_mailing_object)
results[campaign.id] = segment_results results[campaign.id] = mass_mailing_results
return results return results
_columns = { _columns = {
'name': fields.char( 'name': fields.char(
'Campaign Name', required=True, 'Campaign Name', required=True,
), ),
'segment_ids': fields.one2many( 'user_id': fields.many2one(
'mail.mass_mailing.segment', 'mass_mailing_campaign_id', 'res.users', 'Responsible',
'Segments', required=True,
), ),
'segment_kanban_ids': fields.function( 'mass_mailing_ids': fields.one2many(
_get_segment_kanban_ids, 'mail.mass_mailing', 'mass_mailing_campaign_id',
type='text', string='Segments (kanban data)', 'Mass Mailings',
help='This field has for purpose to gather data about segment to display them in kanban view as nested kanban views is not possible currently',
), ),
'mail_ids': fields.one2many( 'mass_mailing_kanban_ids': fields.function(
'mail.mail', 'mass_mailing_campaign_id', _get_mass_mailing_kanban_ids,
type='text', string='Mass Mailings (kanban data)',
help='This field has for purpose to gather data about mass mailings to display them in kanban view as nested kanban views is not possible currently',
),
'statistics_ids': fields.one2many(
'mail.mail.statistics', 'mass_mailing_campaign_id',
'Sent Emails', 'Sent Emails',
), ),
'color': fields.integer('Color Index'), 'color': fields.integer('Color Index'),
@ -105,17 +109,21 @@ class MassMailingCampaign(osv.Model):
), ),
} }
def launch_segment_create_wizard(self, cr, uid, ids, context=None): # _defaults = {
# 'user_id': lambda self, cr, uid, ctx=None: uid,
# },
def launch_mass_mailing_create_wizard(self, cr, uid, ids, context=None):
ctx = dict(context) ctx = dict(context)
ctx.update({ ctx.update({
'default_mass_mailing_campaign_id': ids[0], 'default_mass_mailing_campaign_id': ids[0],
}) })
return { return {
'name': _('Create a Segment for the Campaign'), 'name': _('Create a Mass Mailing for the Campaign'),
'type': 'ir.actions.act_window', 'type': 'ir.actions.act_window',
'view_type': 'form', 'view_type': 'form',
'view_mode': 'form', 'view_mode': 'form',
'res_model': 'mail.mass_mailing.segment.create', 'res_model': 'mail.mass_mailing.create',
'views': [(False, 'form')], 'views': [(False, 'form')],
'view_id': False, 'view_id': False,
'target': 'new', 'target': 'new',
@ -123,12 +131,12 @@ class MassMailingCampaign(osv.Model):
} }
class MassMailingSegment(osv.Model): class MassMailing(osv.Model):
""" MassMailingSegment models a segment for a mass mailign campaign. A segment """ MassMailing models a wave of emails for a mass mailign campaign.
is an occurence of sending emails. """ A mass mailing is an occurence of sending emails. """
_name = 'mail.mass_mailing.segment' _name = 'mail.mass_mailing'
_description = 'Segment of a mass mailing campaign' _description = 'Wave of sending emails'
# number of periods for tracking mail_mail statistics # number of periods for tracking mail_mail statistics
_period_number = 6 _period_number = 6
@ -162,7 +170,7 @@ class MassMailingSegment(osv.Model):
def _get_monthly_statistics(self, cr, uid, ids, field_name, arg, context=None): def _get_monthly_statistics(self, cr, uid, ids, field_name, arg, context=None):
""" TODO """ TODO
""" """
obj = self.pool['mail.mail'] obj = self.pool['mail.mail.statistics']
res = {} res = {}
context['datetime_format'] = { context['datetime_format'] = {
'opened': { 'opened': {
@ -179,22 +187,22 @@ class MassMailingSegment(osv.Model):
for id in ids: for id in ids:
res[id] = {} res[id] = {}
date_begin = self.browse(cr, uid, id, context=context).date date_begin = self.browse(cr, uid, id, context=context).date
domain = [('mass_mailing_segment_id', '=', id), ('opened', '>=', date_begin)] domain = [('mass_mailing_id', '=', id), ('opened', '>=', date_begin)]
res[id]['opened_monthly'] = self.__get_bar_values(cr, uid, id, obj, domain, ['opened'], 'opened_count', 'opened', context=context) res[id]['opened_monthly'] = self.__get_bar_values(cr, uid, id, obj, domain, ['opened'], 'opened_count', 'opened', context=context)
domain = [('mass_mailing_segment_id', '=', id), ('replied', '>=', date_begin)] domain = [('mass_mailing_id', '=', id), ('replied', '>=', date_begin)]
res[id]['replied_monthly'] = self.__get_bar_values(cr, uid, id, obj, domain, ['replied'], 'replied_count', 'replied', context=context) res[id]['replied_monthly'] = self.__get_bar_values(cr, uid, id, obj, domain, ['replied'], 'replied_count', 'replied', context=context)
return res return res
def _get_statistics(self, cr, uid, ids, name, arg, context=None): def _get_statistics(self, cr, uid, ids, name, arg, context=None):
""" Compute statistics of the mass mailing campaign """ """ Compute statistics of the mass mailing campaign """
results = dict.fromkeys(ids, False) results = dict.fromkeys(ids, False)
for segment in self.browse(cr, uid, ids, context=context): for mass_mailing in self.browse(cr, uid, ids, context=context):
results[segment.id] = { results[mass_mailing.id] = {
'sent': len(segment.mail_ids), 'sent': len(mass_mailing.statistics_ids),
'delivered': len([mail for mail in segment.mail_ids if mail.state == 'sent' and not mail.bounced]), 'delivered': len([stat for stat in mass_mailing.statistics_ids if not stat.bounced]), # mail.state == 'sent' and
'opened': len([mail for mail in segment.mail_ids if mail.opened]), 'opened': len([stat for stat in mass_mailing.statistics_ids if stat.opened]),
'replied': len([mail for mail in segment.mail_ids if mail.replied]), 'replied': len([stat for stat in mass_mailing.statistics_ids if stat.replied]),
'bounced': len([mail for mail in segment.mail_ids if mail.bounced]), 'bounced': len([stat for stat in mass_mailing.statistics_ids if stat.bounced]),
} }
return results return results
@ -214,9 +222,9 @@ class MassMailingSegment(osv.Model):
'mass_mailing_campaign_id', 'color', 'mass_mailing_campaign_id', 'color',
type='integer', string='Color Index', type='integer', string='Color Index',
), ),
# mail_mail data # statistics data
'mail_ids': fields.one2many( 'statistics_ids': fields.one2many(
'mail.mail', 'mass_mailing_segment_id', 'mail.mail.statistics', 'mass_mailing_id',
'Send Emails', 'Send Emails',
), ),
'sent': fields.function( 'sent': fields.function(
@ -260,3 +268,95 @@ class MassMailingSegment(osv.Model):
_defaults = { _defaults = {
'date': fields.datetime.now(), 'date': fields.datetime.now(),
} }
class MailMailStats(osv.Model):
""" MailMailStats models the statistics collected about emails. Those statistics
are stored in a separated model and table to avoid bloating the mail_mail table
with statistics values. This also allows to delete emails send with mass mailing
without loosing the statistics about them. """
_name = 'mail.mail.statistics'
_description = 'Email Statistics'
_rec_name = 'message_id'
_order = 'message_id'
_columns = {
'mail_mail_id': fields.integer(
'Mail ID',
help='ID of the related mail_mail. This field is an integer field because'
'the related mail_mail can be deleted separately from its statistics.'
),
'message_id': fields.char(
'Message-ID', required=True,
),
# campaign / wave data
'mass_mailing_id': fields.many2one(
'mail.mass_mailing', 'Mass Mailing',
ondelete='set null',
),
'mass_mailing_campaign_id': fields.related(
'mass_mailing_id', 'mass_mailing_campaign_id',
type='many2one', ondelete='set null',
relation='mail.mass_mailing.campaign',
string='Mass Mailing Campaign',
store=True, readonly=True,
),
'template_id': fields.related(
'mass_mailing_id', 'template_id',
type='many2one', ondelete='set null',
relation='email.template',
string='Email Template',
store=True, readonly=True,
),
# Bounce and tracking
'opened': fields.datetime(
'Opened',
help='Date when this email has been opened for the first time.'),
'replied': fields.datetime(
'Replied',
help='Date when this email has been replied for the first time.'),
'bounced': fields.datetime(
'Bounced',
help='Date when this email has bounced.'
),
}
def set_opened(self, cr, uid, ids=None, mail_mail_ids=None, mail_message_ids=None, context=None):
""" Set as opened """
if not ids and mail_mail_ids:
ids = self.search(cr, uid, [('mail_mail_id', 'in', mail_mail_ids)], context=context)
elif not ids and mail_message_ids:
ids = self.search(cr, uid, [('message_id', 'in', mail_message_ids)], context=context)
else:
ids = []
for stat in self.browse(cr, uid, ids, context=context):
if not stat.opened:
self.write(cr, uid, [stat.id], {'opened': fields.datetime.now()}, context=context)
return True
def set_replied(self, cr, uid, ids=None, mail_mail_ids=None, mail_message_ids=None, context=None):
""" Set as replied """
if not ids and mail_mail_ids:
ids = self.search(cr, uid, [('mail_mail_id', 'in', mail_mail_ids)], context=context)
elif not ids and mail_message_ids:
ids = self.search(cr, uid, [('message_id', 'in', mail_message_ids)], context=context)
else:
ids = []
for stat in self.browse(cr, uid, ids, context=context):
if not stat.replied:
self.write(cr, uid, [stat.id], {'replied': fields.datetime.now()}, context=context)
return True
def set_bounced(self, cr, uid, ids=None, mail_mail_ids=None, mail_message_ids=None, context=None):
""" Set as bounced """
if not ids and mail_mail_ids:
ids = self.search(cr, uid, [('mail_mail_id', 'in', mail_mail_ids)], context=context)
elif not ids and mail_message_ids:
ids = self.search(cr, uid, [('message_id', 'in', mail_message_ids)], context=context)
else:
ids = []
for stat in self.browse(cr, uid, ids, context=context):
if not stat.bounced:
self.write(cr, uid, [stat.id], {'bounced': fields.datetime.now()}, context=context)
return True

View File

@ -20,60 +20,69 @@
<record id="mass_mail_campaign_1" model="mail.mass_mailing.campaign"> <record id="mass_mail_campaign_1" model="mail.mass_mailing.campaign">
<field name="name">Partners Newsletter</field> <field name="name">Partners Newsletter</field>
<field name="user_id" eval="ref('base.user_root')"/>
</record> </record>
<record id="mass_mail_segment_1" model="mail.mass_mailing.segment"> <record id="mass_mail_1" model="mail.mass_mailing">
<field name="name">First Newsletter</field> <field name="name">First Newsletter</field>
<field name="template_id" eval="ref('mass_mail_template_1')"/> <field name="template_id" eval="ref('mass_mail_template_1')"/>
<field name="date" eval="(DateTime.today() - relativedelta(days=3)).strftime('%Y-%m-%d %H:%M:%S')"/> <field name="date" eval="(DateTime.today() - relativedelta(days=3)).strftime('%Y-%m-%d %H:%M:%S')"/>
<field name="mass_mailing_campaign_id" eval="ref('mass_mail_campaign_1')"/> <field name="mass_mailing_campaign_id" eval="ref('mass_mail_campaign_1')"/>
</record> </record>
<record id="mass_mail_segment_2" model="mail.mass_mailing.segment"> <record id="mass_mail_2" model="mail.mass_mailing">
<field name="name">Second Newsletter</field> <field name="name">Second Newsletter</field>
<field name="template_id" eval="ref('mass_mail_template_2')"/> <field name="template_id" eval="ref('mass_mail_template_2')"/>
<field name="date" eval="(DateTime.today() - relativedelta(days=3)).strftime('%Y-%m-%d %H:%M:%S')"/> <field name="date" eval="(DateTime.today() - relativedelta(days=3)).strftime('%Y-%m-%d %H:%M:%S')"/>
<field name="mass_mailing_campaign_id" eval="ref('mass_mail_campaign_1')"/> <field name="mass_mailing_campaign_id" eval="ref('mass_mail_campaign_1')"/>
</record> </record>
<record id="mass_mail_email_1" model="mail.mail"> <record id="mass_mail_email_1" model="mail.mail.statistics">
<field name="mass_mailing_segment_id" eval="ref('mass_mail_segment_1')"/> <field name="mass_mailing_id" eval="ref('mass_mail_1')"/>
<field name="message_id">1111000@OpenERP.com</field>
<field name="opened" eval="(DateTime.today() - relativedelta(days=2)).strftime('%Y-%m-%d %H:%M:%S')"/> <field name="opened" eval="(DateTime.today() - relativedelta(days=2)).strftime('%Y-%m-%d %H:%M:%S')"/>
<field name="replied" eval="(DateTime.today() - relativedelta(days=1)).strftime('%Y-%m-%d %H:%M:%S')"/> <field name="replied" eval="(DateTime.today() - relativedelta(days=1)).strftime('%Y-%m-%d %H:%M:%S')"/>
<field name="state">sent</field> <field name="state">sent</field>
</record> </record>
<record id="mass_mail_email_2" model="mail.mail"> <record id="mass_mail_email_2" model="mail.mail.statistics">
<field name="mass_mailing_segment_id" eval="ref('mass_mail_segment_1')"/> <field name="mass_mailing_id" eval="ref('mass_mail_1')"/>
<field name="message_id">1111001@OpenERP.com</field>
<field name="opened" eval="(DateTime.today() - relativedelta(days=2)).strftime('%Y-%m-%d %H:%M:%S')"/> <field name="opened" eval="(DateTime.today() - relativedelta(days=2)).strftime('%Y-%m-%d %H:%M:%S')"/>
<field name="replied" eval="(DateTime.today() - relativedelta(days=0)).strftime('%Y-%m-%d %H:%M:%S')"/> <field name="replied" eval="(DateTime.today() - relativedelta(days=0)).strftime('%Y-%m-%d %H:%M:%S')"/>
<field name="state">sent</field> <field name="state">sent</field>
</record> </record>
<record id="mass_mail_email_3" model="mail.mail"> <record id="mass_mail_email_3" model="mail.mail.statistics">
<field name="mass_mailing_segment_id" eval="ref('mass_mail_segment_1')"/> <field name="mass_mailing_id" eval="ref('mass_mail_1')"/>
<field name="message_id">1111002@OpenERP.com</field>
<field name="opened" eval="(DateTime.today() - relativedelta(days=1)).strftime('%Y-%m-%d %H:%M:%S')"/> <field name="opened" eval="(DateTime.today() - relativedelta(days=1)).strftime('%Y-%m-%d %H:%M:%S')"/>
<field name="state">sent</field> <field name="state">sent</field>
</record> </record>
<record id="mass_mail_email_4" model="mail.mail"> <record id="mass_mail_email_4" model="mail.mail.statistics">
<field name="mass_mailing_segment_id" eval="ref('mass_mail_segment_1')"/> <field name="mass_mailing_id" eval="ref('mass_mail_1')"/>
<field name="message_id">1111003@OpenERP.com</field>
<field name="state">sent</field> <field name="state">sent</field>
</record> </record>
<record id="mass_mail_email_5" model="mail.mail"> <record id="mass_mail_email_5" model="mail.mail.statistics">
<field name="mass_mailing_segment_id" eval="ref('mass_mail_segment_1')"/> <field name="mass_mailing_id" eval="ref('mass_mail_1')"/>
<field name="message_id">1111004@OpenERP.com</field>
<field name="bounced" eval="(DateTime.today() - relativedelta(days=3)).strftime('%Y-%m-%d %H:%M:%S')"/> <field name="bounced" eval="(DateTime.today() - relativedelta(days=3)).strftime('%Y-%m-%d %H:%M:%S')"/>
<field name="state">sent</field> <field name="state">sent</field>
</record> </record>
<record id="mass_mail_email_2_1" model="mail.mail"> <record id="mass_mail_email_2_1" model="mail.mail.statistics">
<field name="mass_mailing_segment_id" eval="ref('mass_mail_segment_2')"/> <field name="mass_mailing_id" eval="ref('mass_mail_2')"/>
<field name="message_id">1111005@OpenERP.com</field>
<field name="opened" eval="(DateTime.today() - relativedelta(days=3)).strftime('%Y-%m-%d %H:%M:%S')"/> <field name="opened" eval="(DateTime.today() - relativedelta(days=3)).strftime('%Y-%m-%d %H:%M:%S')"/>
<field name="state">sent</field> <field name="state">sent</field>
</record> </record>
<record id="mass_mail_email_2_2" model="mail.mail"> <record id="mass_mail_email_2_2" model="mail.mail.statistics">
<field name="mass_mailing_segment_id" eval="ref('mass_mail_segment_2')"/> <field name="mass_mailing_id" eval="ref('mass_mail_2')"/>
<field name="message_id">1111006@OpenERP.com</field>
<field name="opened" eval="(DateTime.today() - relativedelta(days=2)).strftime('%Y-%m-%d %H:%M:%S')"/> <field name="opened" eval="(DateTime.today() - relativedelta(days=2)).strftime('%Y-%m-%d %H:%M:%S')"/>
<field name="state">sent</field> <field name="state">sent</field>
</record> </record>
<record id="mass_mail_email_2_3" model="mail.mail"> <record id="mass_mail_email_2_3" model="mail.mail.statistics">
<field name="mass_mailing_segment_id" eval="ref('mass_mail_segment_2')"/> <field name="mass_mailing_id" eval="ref('mass_mail_2')"/>
<field name="message_id">1111007@OpenERP.com</field>
<field name="state">sent</field> <field name="state">sent</field>
</record> </record>

View File

@ -20,8 +20,8 @@
<field name="arch" type="xml"> <field name="arch" type="xml">
<form string="Mass Mailing Campaign" version="7.0"> <form string="Mass Mailing Campaign" version="7.0">
<header> <header>
<button name="launch_segment_create_wizard" type="object" <button name="launch_mass_mailing_create_wizard" type="object"
class="oe_highlight" string="Create a New Segment"/> class="oe_highlight" string="Create a New Mailing"/>
</header> </header>
<sheet> <sheet>
<group> <group>
@ -32,8 +32,8 @@
<field name="replied"/> <field name="replied"/>
<field name="bounced"/> <field name="bounced"/>
</group> </group>
<label for="segment_ids"/> <label for="mass_mailing_ids"/>
<field name="segment_ids"/> <field name="mass_mailing_ids"/>
</sheet> </sheet>
</form> </form>
</field> </field>
@ -46,29 +46,29 @@
<field name="model">mail.mass_mailing.campaign</field> <field name="model">mail.mass_mailing.campaign</field>
<field name="arch" type="xml"> <field name="arch" type="xml">
<kanban> <kanban>
<field name="segment_kanban_ids"/> <field name="mass_mailing_kanban_ids"/>
<field name='sent'/> <field name='sent'/>
<field name='color'/> <field name='color'/>
<templates> <templates>
<t t-name="mass_mailing.segment"> <t t-name="mass_mailing.mass_mailing">
<div> <div>
<h4><t t-raw="segment.name"/></h4> <h4><t t-raw="mass_mailing.name"/></h4>
</div> </div>
<div> <div>
<p class="oe_mail_stats"> <p class="oe_mail_stats">
<span class="oe_mail_result"><t t-raw="segment.sent"/></span><br /> <span class="oe_mail_result"><t t-raw="mass_mailing.sent"/></span><br />
Sent Sent
</p> </p>
<p class="oe_mail_stats"> <p class="oe_mail_stats">
<span class="oe_mail_result"><t t-raw="segment.delivered"/></span><br /> <span class="oe_mail_result"><t t-raw="mass_mailing.delivered"/></span><br />
Delivered Delivered
</p> </p>
<p class="oe_mail_stats"> <p class="oe_mail_stats">
<span class="oe_mail_result"><t t-raw="segment.opened"/></span><br /> <span class="oe_mail_result"><t t-raw="mass_mailing.opened"/></span><br />
Opened Opened
</p> </p>
<p class="oe_mail_stats"> <p class="oe_mail_stats">
<span class="oe_mail_result"><t t-raw="segment.replied"/></span><br /> <span class="oe_mail_result"><t t-raw="mass_mailing.replied"/></span><br />
Replied Replied
</p> </p>
</div> </div>
@ -93,8 +93,8 @@
<field name="replied" widget="gauge" style="width:160px; height: 120px;" <field name="replied" widget="gauge" style="width:160px; height: 120px;"
options="{'max_field': 'sent'}"/> options="{'max_field': 'sent'}"/>
</div> </div>
<t t-foreach='record.segment_kanban_ids.value' t-as='segment'> <t t-foreach='record.mass_mailing_kanban_ids.value' t-as='mass_mailing'>
<t t-call="mass_mailing.segment"/> <t t-call="mass_mailing.mass_mailing"/>
</t> </t>
</div> </div>
<div class="oe_clear"></div> <div class="oe_clear"></div>
@ -112,13 +112,13 @@
<field name="view_mode">kanban,tree,form</field> <field name="view_mode">kanban,tree,form</field>
</record> </record>
<!-- MASS MAILING SEGMENTS !--> <!-- MASS MAILING !-->
<record model="ir.ui.view" id="view_mail_mass_mailing_segment_tree"> <record model="ir.ui.view" id="view_mail_mass_mailing_tree">
<field name="name">mail.mass_mailing.segment.tree</field> <field name="name">mail.mass_mailing.tree</field>
<field name="model">mail.mass_mailing.segment</field> <field name="model">mail.mass_mailing</field>
<field name="priority">10</field> <field name="priority">10</field>
<field name="arch" type="xml"> <field name="arch" type="xml">
<tree string="Mass Mailing Segments"> <tree string="Mass Mailings">
<field name="name"/> <field name="name"/>
<field name="sent"/> <field name="sent"/>
<field name="delivered"/> <field name="delivered"/>
@ -128,11 +128,11 @@
</field> </field>
</record> </record>
<record model="ir.ui.view" id="view_mail_mass_mailing_segment_form"> <record model="ir.ui.view" id="view_mail_mass_mailing_form">
<field name="name">mail.mass_mailing.segment.form</field> <field name="name">mail.mass_mailing.form</field>
<field name="model">mail.mass_mailing.segment</field> <field name="model">mail.mass_mailing</field>
<field name="arch" type="xml"> <field name="arch" type="xml">
<form string="Mass Mailing Segment" version="7.0"> <form string="Mass Mailing" version="7.0">
<sheet> <sheet>
<group> <group>
<group> <group>
@ -150,12 +150,12 @@
</field> </field>
</record> </record>
<record model="ir.ui.view" id="view_mail_mass_mailing_segment_form_readonly"> <record model="ir.ui.view" id="view_mail_mass_mailing_form_readonly">
<field name="name">mail.mass_mailing.segment.form</field> <field name="name">mail.mass_mailing.form</field>
<field name="model">mail.mass_mailing.segment</field> <field name="model">mail.mass_mailing</field>
<field name="priority">18</field> <field name="priority">18</field>
<field name="arch" type="xml"> <field name="arch" type="xml">
<form string="Mass Mailing Segment" version="7.0"> <form string="Mass Mailing" version="7.0">
<sheet> <sheet>
<group> <group>
<group> <group>
@ -173,9 +173,9 @@
</field> </field>
</record> </record>
<record model="ir.ui.view" id="view_mail_mass_mailing_segment_kanban"> <record model="ir.ui.view" id="view_mail_mass_mailing_kanban">
<field name="name">mail.mass_mailing.segment.kanban</field> <field name="name">mail.mass_mailing.kanban</field>
<field name="model">mail.mass_mailing.segment</field> <field name="model">mail.mass_mailing</field>
<field name="arch" type="xml"> <field name="arch" type="xml">
<kanban> <kanban>
<field name='color'/> <field name='color'/>
@ -229,9 +229,9 @@
</field> </field>
</record> </record>
<record id="action_view_mass_mailing_segments" model="ir.actions.act_window"> <record id="action_view_mass_mailings" model="ir.actions.act_window">
<field name="name">Mass Mailing Segments</field> <field name="name">Mass Mailings</field>
<field name="res_model">mail.mass_mailing.segment</field> <field name="res_model">mail.mass_mailing</field>
<field name="view_type">form</field> <field name="view_type">form</field>
<field name="view_mode">kanban,tree,form</field> <field name="view_mode">kanban,tree,form</field>
</record> </record>
@ -245,9 +245,9 @@
<menuitem name="Campaigns" id="menu_email_campaigns" <menuitem name="Campaigns" id="menu_email_campaigns"
parent="mass_mailing_campaign" sequence="1" parent="mass_mailing_campaign" sequence="1"
action="action_view_mass_mailing_campaigns"/> action="action_view_mass_mailing_campaigns"/>
<menuitem name="Segments" id="menu_email_segments" <menuitem name="Mass Mailings" id="menu_email_mass_mailings"
parent="mass_mailing_campaign" sequence="2" parent="mass_mailing_campaign" sequence="2"
action="action_view_mass_mailing_segments"/> action="action_view_mass_mailings"/>
</data> </data>
</openerp> </openerp>

View File

@ -1,5 +1,6 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_mass_mailing_campaign,mail.mass_mailing.campaign,model_mail_mass_mailing_campaign,,1,1,1,0 access_mass_mailing_campaign,mail.mass_mailing.campaign,model_mail_mass_mailing_campaign,,1,1,1,0
access_mass_mailing_campaign_system,mail.mass_mailing.campaign.system,model_mail_mass_mailing_campaign,base.group_system,1,1,1,1 access_mass_mailing_campaign_system,mail.mass_mailing.campaign.system,model_mail_mass_mailing_campaign,base.group_system,1,1,1,1
access_mass_mailing_segment,mail.mass_mailing.segment,model_mail_mass_mailing_segment,,1,1,1,0 access_mass_mailing,mail.mass_mailing,model_mail_mass_mailing,,1,1,1,0
access_mass_mailing_segment_system,mail.mass_mailing.segment.system,model_mail_mass_mailing_segment,base.group_system,1,1,1,1 access_mass_mailing_system,mail.mass_mailing.system,model_mail_mass_mailing,base.group_system,1,1,1,1
access_mail_mail_statistics,mail.mail.statistics,model_mail_mail_statistics,,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_mass_mailing_campaign mail.mass_mailing.campaign model_mail_mass_mailing_campaign 1 1 1 0
3 access_mass_mailing_campaign_system mail.mass_mailing.campaign.system model_mail_mass_mailing_campaign base.group_system 1 1 1 1
4 access_mass_mailing_segment access_mass_mailing mail.mass_mailing.segment mail.mass_mailing model_mail_mass_mailing_segment model_mail_mass_mailing 1 1 1 0
5 access_mass_mailing_segment_system access_mass_mailing_system mail.mass_mailing.segment.system mail.mass_mailing.system model_mail_mass_mailing_segment model_mail_mass_mailing base.group_system 1 1 1 1
6 access_mail_mail_statistics mail.mail.statistics model_mail_mail_statistics 1 1 1 1

View File

@ -34,8 +34,8 @@ class MailComposeMessage(osv.TransientModel):
'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',
), ),
'mass_mailing_segment_id': fields.many2one( 'mass_mailing_id': fields.many2one(
'mail.mass_mailing.segment', 'Mass mailing segment', 'mail.mass_mailing', 'Mass mailing',
domain="[('mass_mailing_campaign_id', '=', mass_mailing_campaign_id)]", domain="[('mass_mailing_campaign_id', '=', mass_mailing_campaign_id)]",
), ),
} }
@ -44,18 +44,18 @@ class MailComposeMessage(osv.TransientModel):
'use_mass_mailing_campaign': False, 'use_mass_mailing_campaign': False,
} }
def onchange_mass_mail_campaign_id(self, cr, uid, ids, mass_mailing_campaign_id, mass_mail_segment_id, context=None): def onchange_mass_mail_campaign_id(self, cr, uid, ids, mass_mailing_campaign_id, mass_mailing_id, context=None):
if mass_mail_segment_id: if mass_mailing_id:
segment = self.pool['mail.mass_mailing.segment'].browse(cr, uid, mass_mail_segment_id, context=context) mass_mailing = self.pool['mail.mass_mailing'].browse(cr, uid, mass_mailing_id, context=context)
if segment.mass_mailing_campaign_id.id == mass_mailing_campaign_id: if mass_mailing.mass_mailing_campaign_id.id == mass_mailing_campaign_id:
return {} return {}
return {'value': {'mass_mailing_segment_id': False}} return {'value': {'mass_mailing_id': False}}
def render_message_batch(self, cr, uid, wizard, res_ids, context=None): def render_message_batch(self, cr, uid, wizard, res_ids, context=None):
""" Override method that generated the mail content by adding the mass """ Override method that generated the mail content by adding the mass
mailing campaign, when doing pure email mass mailing. """ mailing campaign, when doing pure email mass mailing. """
res = super(MailComposeMessage, self).render_message_batch(cr, uid, wizard, res_ids, context=context) res = super(MailComposeMessage, self).render_message_batch(cr, uid, wizard, res_ids, context=context)
if wizard.composition_mode == 'mass_mail' and wizard.use_mass_mailing_campaign and wizard.mass_mailing_segment_id: # TODO: which kind of mass mailing ? if wizard.composition_mode == 'mass_mail' and wizard.use_mass_mailing_campaign and wizard.mass_mailing_id: # TODO: which kind of mass mailing ?
for res_id in res_ids: for res_id in res_ids:
res[res_id]['mass_mailing_segment_id'] = wizard.mass_mailing_segment_id.id res[res_id]['mass_mailing_id'] = wizard.mass_mailing_id.id
return res return res

View File

@ -15,16 +15,16 @@
<div> <div>
<group> <group>
<field name="mass_mailing_campaign_id" <field name="mass_mailing_campaign_id"
on_change="onchange_mass_mail_campaign_id(mass_mailing_campaign_id, mass_mailing_segment_id, context)" on_change="onchange_mass_mail_campaign_id(mass_mailing_campaign_id, mass_mailing_id, context)"
attrs="{'invisible': ['|', ('composition_mode', '!=', 'mass_mail'), ('use_mass_mailing_campaign', '=', False)], attrs="{'invisible': ['|', ('composition_mode', '!=', 'mass_mail'), ('use_mass_mailing_campaign', '=', False)],
'required': [('composition_mode', '=', 'mass_mail'), ('use_mass_mailing_campaign', '=', True)]}"/> 'required': [('composition_mode', '=', 'mass_mail'), ('use_mass_mailing_campaign', '=', True)]}"/>
<field name="mass_mailing_segment_id" <field name="mass_mailing_id"
attrs="{'invisible': ['|', ('composition_mode', '!=', 'mass_mail'), ('use_mass_mailing_campaign', '=', False)], attrs="{'invisible': ['|', ('composition_mode', '!=', 'mass_mail'), ('use_mass_mailing_campaign', '=', False)],
'required': [('composition_mode', '=', 'mass_mail'), ('use_mass_mailing_campaign', '=', True)]}" 'required': [('composition_mode', '=', 'mass_mail'), ('use_mass_mailing_campaign', '=', True)]}"
context="{'default_mass_mailing_campaign_id': mass_mailing_campaign_id, context="{'default_mass_mailing_campaign_id': mass_mailing_campaign_id,
'default_template_id': template_id, 'default_template_id': template_id,
'default_domain': active_domain, 'default_domain': active_domain,
'form_view_ref': 'mass_mailing.view_mail_mass_mailing_segment_form_readonly'}"/> 'form_view_ref': 'mass_mailing.view_mail_mass_mailing_form_readonly'}"/>
</group> </group>
</div> </div>
</xpath> </xpath>

View File

@ -24,11 +24,11 @@ from openerp.osv import osv, fields
from openerp.tools.translate import _ from openerp.tools.translate import _
class MailMassMailingSegmentCreate(osv.TransientModel): class MailMassMailingCreate(osv.TransientModel):
"""Wizard to help creating mass mailing segments for a campaign. """ """Wizard to help creating mass mailing waves for a campaign. """
_name = 'mail.mass_mailing.segment.create' _name = 'mail.mass_mailing.create'
_description = 'Mass mailing segment creation' _description = 'Mass mailing creation'
_columns = { _columns = {
'mass_mailing_campaign_id': fields.many2one( 'mass_mailing_campaign_id': fields.many2one(
@ -55,11 +55,11 @@ class MailMassMailingSegmentCreate(osv.TransientModel):
'email.template', 'Template', required=True, 'email.template', 'Template', required=True,
domain="[('model_id', '=', model_id)]", domain="[('model_id', '=', model_id)]",
), ),
'segment_name': fields.char( 'name': fields.char(
'Segment name', required=True, 'Name', required=True,
), ),
'mass_mailing_segment_id': fields.many2one( 'mass_mailing_id': fields.many2one(
'mail.mass_mailing.segment', 'Mass Mailing Segment', 'mail.mass_mailing', 'Mass Mailing',
), ),
} }
@ -80,23 +80,23 @@ class MailMassMailingSegmentCreate(osv.TransientModel):
domain = False domain = False
return {'value': {'domain': domain}} return {'value': {'domain': domain}}
def create_segment(self, cr, uid, ids, context=None): def create_mass_mailing(self, cr, uid, ids, context=None):
""" Create a segment based on wizard data, and update the wizard """ """ Create a mass mailing based on wizard data, and update the wizard """
for wizard in self.browse(cr, uid, ids, context=context): for wizard in self.browse(cr, uid, ids, context=context):
segment_values = { mass_mailing_values = {
'name': wizard.segment_name, 'name': wizard.name,
'mass_mailing_campaign_id': wizard.mass_mailing_campaign_id.id, 'mass_mailing_campaign_id': wizard.mass_mailing_campaign_id.id,
'domain': wizard.domain, 'domain': wizard.domain,
'template_id': wizard.template_id.id, 'template_id': wizard.template_id.id,
} }
segment_id = self.pool['mail.mass_mailing.segment'].create(cr, uid, segment_values, context=context) mass_mailing_id = self.pool['mail.mass_mailing'].create(cr, uid, mass_mailing_values, context=context)
self.write(cr, uid, [wizard.id], {'mass_mailing_segment_id': segment_id}, context=context) self.write(cr, uid, [wizard.id], {'mass_mailing_id': mass_mailing_id}, context=context)
return True return True
def launch_composer(self, cr, uid, ids, context=None): def launch_composer(self, cr, uid, ids, context=None):
""" Main wizard action: create a new segment and launch the mail.compose.message """ Main wizard action: create a new mailing and launch the mail.compose.message
email composer with wizard data. """ email composer with wizard data. """
self.create_segment(cr, uid, ids, context=context) self.create_mass_mailing(cr, uid, ids, context=context)
wizard = self.browse(cr, uid, ids[0], context=context) wizard = self.browse(cr, uid, ids[0], context=context)
ctx = dict(context) ctx = dict(context)
@ -107,7 +107,7 @@ class MailMassMailingSegmentCreate(osv.TransientModel):
'default_use_active_domain': True, 'default_use_active_domain': True,
'default_active_domain': wizard.domain, 'default_active_domain': wizard.domain,
'default_mass_mailing_campaign_id': wizard.mass_mailing_campaign_id.id, 'default_mass_mailing_campaign_id': wizard.mass_mailing_campaign_id.id,
'default_mass_mailing_segment_id': wizard.mass_mailing_segment_id.id, 'default_mass_mailing_id': wizard.mass_mailing_id.id,
}) })
return { return {
'name': _('Compose Email for Mass Mailing'), 'name': _('Compose Email for Mass Mailing'),

View File

@ -3,17 +3,17 @@
<data> <data>
<!-- Wizard form view --> <!-- Wizard form view -->
<record model="ir.ui.view" id="view_mail_mass_mailing_segment_create_form"> <record model="ir.ui.view" id="view_mail_mass_mailing_create_form">
<field name="name">mail.mass_mailing.segment.create.form</field> <field name="name">mail.mass_mailing.create.form</field>
<field name="model">mail.mass_mailing.segment.create</field> <field name="model">mail.mass_mailing.create</field>
<field name="arch" type="xml"> <field name="arch" type="xml">
<form string="Create a Mass Mailing Segment" version="7.0"> <form string="Create a Mass Mailing" version="7.0">
<group> <group>
<field name="model_model" invisible="1"/> <field name="model_model" invisible="1"/>
<field name="domain" invisible="1"/> <field name="domain" invisible="1"/>
<p class="oe_grey" colspan="2" <p class="oe_grey" colspan="2"
attrs="{'invisible': [('mass_mailing_campaign_id', '!=', False)]}"> attrs="{'invisible': [('mass_mailing_campaign_id', '!=', False)]}">
Please choose a mass mailing campaign that will hold the new segment. Please choose a mass mailing campaign that will hold the new mailing.
</p> </p>
<field name="mass_mailing_campaign_id"/> <field name="mass_mailing_campaign_id"/>
@ -43,23 +43,23 @@
attrs="{'invisible': [('filter_id', '=', False)]}"/> attrs="{'invisible': [('filter_id', '=', False)]}"/>
<p class="oe_grey" colspan="2" <p class="oe_grey" colspan="2"
attrs="{'invisible': ['|', ('segment_name', '!=', False), ('template_id', '=', False)]}"> attrs="{'invisible': ['|', ('name', '!=', False), ('template_id', '=', False)]}">
Please choose the name of the campaign segment. Please choose the name of the mailing.
</p> </p>
<field name="segment_name" <field name="name"
attrs="{'invisible': [('template_id', '=', False)]}"/> attrs="{'invisible': [('template_id', '=', False)]}"/>
<button name="launch_composer" type="object" <button name="launch_composer" type="object"
string="Create segment and launch email composer" string="Create mailing and launch email composer"
attrs="{'invisible': [('segment_name', '=', False)]}"/> attrs="{'invisible': [('name', '=', False)]}"/>
</group> </group>
</form> </form>
</field> </field>
</record> </record>
<record model="ir.actions.act_window" id="action_mail_mass_mailing_segment_create"> <record model="ir.actions.act_window" id="action_mail_mass_mailing_create">
<field name="name">Create Mass Mailing Segment</field> <field name="name">Create Mass Mailing</field>
<field name="res_model">mail.mass_mailing.segment.create</field> <field name="res_model">mail.mass_mailing.create</field>
<field name="src_model">mail.mass_mailing.campaign</field> <field name="src_model">mail.mass_mailing.campaign</field>
<field name="type">ir.actions.act_window</field> <field name="type">ir.actions.act_window</field>
<field name="view_type">form</field> <field name="view_type">form</field>