[MERGE] [REF] mass_mailing refactoring. Mass mailing is refactored to be easier to use, introducing an easy-to-use way of handling recipients, mailing lists and statistics. This branch comes with a web branch that introduce the char_domain widget, that is a widget on a char field holding a domain. It allows to select and count records without having to deal with the complexity of domains.

This branch comes with a cleaning of marketing modules :
- marketing now holds only the basic stuff for marketing related modules: mainly marketing settings + menu definition
- marketing_crm new module is a bridge between crm and marketing and holds the crm-related stuff previously present in marketing module.

This branch also holds some mail and template improvement in order to speedup the mass mailing process. The template edition in website_mail has also be improved. It is now a page that allows to edit email-like content (content with a subject, an email_from and a body), like templates, emails or mass mailing.

Misc :
- mail_compose_message: removed unnecessary fields coming from the template (partner_to, ...) because they are confusing -> composer should be easier to understand and use; also cleaned method generating the email values in the composer that was splitted in two methods
- fixed removed double body computation when using templates (one for template, then the wizard -> not necessary)
- mail_message: record_name is not a function field anymore, but a char field + a method in create, allowing to speedup mass mailing by avoiding browsing all records to get their name

bzr revid: tde@openerp.com-20140416115152-tnitidd4v6w37hyp
This commit is contained in:
Thibault Delavallée 2014-04-16 13:51:52 +02:00
commit fef7f9adfd
71 changed files with 2516 additions and 1877 deletions

View File

@ -79,6 +79,7 @@ class crm_lead(format_address, osv.osv):
'crm.mt_lead_lost': lambda self, cr, uid, obj, ctx=None: obj.probability == 0 and obj.stage_id and obj.stage_id.fold and obj.stage_id.sequence > 1,
},
}
_mail_mass_mailing = _('Leads / Opportunities')
def get_empty_list_help(self, cr, uid, help, context=None):
if context.get('default_type') == 'lead':

View File

@ -63,7 +63,7 @@ campaigns on any OpenERP document.
'wizard/mail_compose_message_view.xml',
'security/ir.model.access.csv'
],
'demo': ['res_partner_demo.yml'],
'demo': [],
'installable': True,
'auto_install': True,
'images': ['images/1_email_account.jpeg','images/2_email_template.jpeg','images/3_emails.jpeg'],

View File

@ -231,6 +231,11 @@ class email_template(osv.osv):
'email_from': fields.char('From',
help="Sender address (placeholders may be used here). If not set, the default "
"value will be the author's email alias if configured, or email address."),
'use_default_to': fields.boolean(
'Default recipients',
help="Default recipients of the record:\n"
"- partner (using id on a partner or the partner_id field) OR\n"
"- email (using email_from or email field)"),
'email_to': fields.char('To (Emails)', help="Comma-separated recipient addresses (placeholders may be used here)"),
'partner_to': fields.char('To (Partners)',
help="Comma-separated ids of recipient partners (placeholders may be used here)",
@ -386,6 +391,37 @@ class email_template(osv.osv):
})
return {'value': result}
def generate_recipients_batch(self, cr, uid, results, template_id, res_ids, context=None):
"""Generates the recipients of the template. Default values can ben generated
instead of the template values if requested by template or context.
Emails (email_to, email_cc) can be transformed into partners if requested
in the context. """
if context is None:
context = {}
template = self.browse(cr, uid, template_id, context=context)
if template.use_default_to or context.get('tpl_force_default_to'):
ctx = dict(context, thread_model=template.model)
default_recipients = self.pool['mail.thread'].message_get_default_recipients(cr, uid, res_ids, context=ctx)
for res_id, recipients in default_recipients.iteritems():
results[res_id].pop('partner_to', None)
results[res_id].update(recipients)
for res_id, values in results.iteritems():
partner_ids = values.get('partner_ids', list())
if context and context.get('tpl_partners_only'):
mails = tools.email_split(values.pop('email_to', '')) + tools.email_split(values.pop('email_cc', ''))
for mail in mails:
partner_id = self.pool.get('res.partner').find_or_create(cr, uid, mail, context=context)
partner_ids.append(partner_id)
partner_to = values.pop('partner_to', '')
if partner_to:
# placeholders could generate '', 3, 2 due to some empty field values
tpl_partner_ids = [pid for pid in partner_to.split(',') if pid]
partner_ids += self.pool['res.partner'].exists(cr, SUPERUSER_ID, tpl_partner_ids, context=context)
results[res_id]['partner_ids'] = partner_ids
return results
def generate_email_batch(self, cr, uid, template_id, res_ids, context=None, fields=None):
"""Generates an email from the template for given the given model based on
records given by res_ids.
@ -420,14 +456,18 @@ class email_template(osv.osv):
context=context)
for res_id, field_value in generated_field_values.iteritems():
results.setdefault(res_id, dict())[field] = field_value
# compute recipients
results = self.generate_recipients_batch(cr, uid, results, template.id, template_res_ids, context=context)
# update values for all res_ids
for res_id in template_res_ids:
values = results[res_id]
# body: add user signature, sanitize
if 'body_html' in fields and template.user_signature:
signature = self.pool.get('res.users').browse(cr, uid, uid, context).signature
values['body_html'] = tools.append_content_to_html(values['body_html'], signature)
if values.get('body_html'):
values['body'] = tools.html_sanitize(values['body_html'])
# technical settings
values.update(
mail_server_id=template.mail_server_id.id or False,
auto_delete=template.auto_delete,
@ -484,17 +524,8 @@ class email_template(osv.osv):
# create a mail_mail based on values, without attachments
values = self.generate_email(cr, uid, template_id, res_id, context=context)
if not values.get('email_from'):
raise osv.except_osv(_('Warning!'),_("Sender email is missing or empty after template rendering. Specify one to deliver your message"))
# process partner_to field that is a comma separated list of partner_ids -> recipient_ids
# NOTE: only usable if force_send is True, because otherwise the value is
# not stored on the mail_mail, and therefore lost -> fixed in v8
values['recipient_ids'] = []
partner_to = values.pop('partner_to', '')
if partner_to:
# placeholders could generate '', 3, 2 due to some empty field values
tpl_partner_ids = [pid for pid in partner_to.split(',') if pid]
values['recipient_ids'] += [(4, pid) for pid in self.pool['res.partner'].exists(cr, SUPERUSER_ID, tpl_partner_ids, context=context)]
raise osv.except_osv(_('Warning!'), _("Sender email is missing or empty after template rendering. Specify one to deliver your message"))
values['recipient_ids'] = [(4, pid) for pid in values.get('partner_ids', list())]
attachment_ids = values.pop('attachment_ids', [])
attachments = values.pop('attachments', [])
msg_id = mail_mail.create(cr, uid, values, context=context)

View File

@ -9,8 +9,10 @@
<sheet>
<div class="oe_title">
<label for="name" class="oe_edit_only"/><h1><field name="name" required="1"/></h1>
<label for="model_id"/><field name="model_id" required="1" on_change="onchange_model_id(model_id)" class="oe_inline"/>
<field name="model" invisible="1"/>
<group>
<field name="model_id" required="1" on_change="onchange_model_id(model_id)"/>
<field name="model" invisible="1"/>
</group>
</div>
<div class="oe_right oe_button_box" name="buttons">
<field name="ref_ir_act_window" invisible="1"/>
@ -25,43 +27,28 @@
context="{'template_id':active_id}"/>
</div>
<notebook>
<page string="Mailing Template">
<group>
<group>
<field name="email_from"
placeholder="Override author's email"/>
<field name="email_to"
placeholder="Comma-separated recipient addresses"/>
<field name="partner_to"
placeholder="Comma-separated ids of recipient partners"/>
<field name="email_cc"
placeholder="Comma-separated carbon copy recipients addresses"/>
<field name="reply_to"
placeholder="Preferred reply address"/>
<field name="subject"
placeholder="Subject (placeholders may be used here)"/>
<field name="user_signature" string="Author Signature (mass mail only)"/>
</group>
<group class="oe_edit_only">
<h3 colspan="2">Dynamic placeholder generator</h3>
<field name="model_object_field"
domain="[('model_id','=',model_id),('ttype','!=','one2many'),('ttype','!=','many2many')]"
on_change="onchange_sub_model_object_value_field(model_object_field)"/>
<field name="sub_object" readonly="1"/>
<field name="sub_model_object_field"
domain="[('model_id','=',sub_object),('ttype','!=','one2many'),('ttype','!=','many2many')]"
attrs="{'readonly':[('sub_object','=',False)],'required':[('sub_object','!=',False)]}"
on_change="onchange_sub_model_object_value_field(model_object_field,sub_model_object_field)"/>
<field name="null_value"
on_change="onchange_sub_model_object_value_field(model_object_field,sub_model_object_field,null_value)"/>
<field name="copyvalue"/>
</group>
</group>
<h3>Body</h3>
<field name="body_html" width="250" height="450"
placeholder="Rich-text/HTML content of the message (placeholders may be used here)"/>
<page string="Content">
<label for="subject"/>
<h2 style="display: inline-block;"><field name="subject" placeholder="Subject (placeholders may be used here)"/></h2>
<field name="body_html"/>
<field name="attachment_ids" widget="many2many_binary"/>
</page>
<page string="Email Configuration">
<group>
<field name="email_from"
placeholder="Override author's email"/>
<field name="use_default_to"/>
<field name="email_to" attrs="{'invisible': [('use_default_to', '=', True)]}"
placeholder="Comma-separated recipient addresses"/>
<field name="partner_to" attrs="{'invisible': [('use_default_to', '=', True)]}"
placeholder="Comma-separated ids of recipient partners"/>
<field name="email_cc" attrs="{'invisible': [('use_default_to', '=', True)]}"
placeholder="Comma-separated carbon copy recipients addresses"/>
<field name="reply_to"
placeholder="Preferred reply address"/>
<field name="user_signature" string="Author Signature (mass mail only)"/>
</group>
</page>
<page string="Advanced Settings">
<group>
<field name="lang"/>
@ -72,6 +59,21 @@
attrs="{'invisible':[('report_template','=',False)]}"/>
</group>
</page>
<page string="Dynamic Placeholder Generator">
<group>
<field name="model_object_field"
domain="[('model_id','=',model_id),('ttype','!=','one2many'),('ttype','!=','many2many')]"
on_change="onchange_sub_model_object_value_field(model_object_field)"/>
<field name="sub_object" readonly="1"/>
<field name="sub_model_object_field"
domain="[('model_id','=',sub_object),('ttype','!=','one2many'),('ttype','!=','many2many')]"
attrs="{'readonly':[('sub_object','=',False)],'required':[('sub_object','!=',False)]}"
on_change="onchange_sub_model_object_value_field(model_object_field,sub_model_object_field)"/>
<field name="null_value"
on_change="onchange_sub_model_object_value_field(model_object_field,sub_model_object_field,null_value)"/>
<field name="copyvalue"/>
</group>
</page>
</notebook>
</sheet>
</form>

View File

@ -1,9 +0,0 @@
-
Set opt-out to True on all demo partners
-
!python {model: res.partner}: |
partner_ids = self.search(cr, uid, [])
# assume partners with an external ID come from demo data
ext_ids = self._get_external_ids(cr, uid, partner_ids)
ids_to_update = [k for (k,v) in ext_ids.iteritems() if v]
self.write(cr, uid, ids_to_update, {'opt_out': True})

View File

@ -74,7 +74,7 @@ class test_message_compose(TestMail):
# 1. Comment on pigs
compose_id = mail_compose.create(cr, uid,
{'subject': 'Forget me subject', 'body': '<p>Dummy body</p>', 'post': True},
{'subject': 'Forget me subject', 'body': '<p>Dummy body</p>'},
{'default_composition_mode': 'comment',
'default_model': 'mail.group',
'default_res_id': self.group_pigs_id,
@ -102,7 +102,7 @@ class test_message_compose(TestMail):
'default_template_id': email_template_id,
'active_ids': [self.group_pigs_id, self.group_bird_id]
}
compose_id = mail_compose.create(cr, uid, {'subject': 'Forget me subject', 'body': 'Dummy body', 'post': True}, context)
compose_id = mail_compose.create(cr, uid, {'subject': 'Forget me subject', 'body': 'Dummy body'}, context)
compose = mail_compose.browse(cr, uid, compose_id, context)
onchange_res = compose.onchange_template_id(email_template_id, 'comment', 'mail.group', self.group_pigs_id)['value']
onchange_res['partner_ids'] = [(4, partner_id) for partner_id in onchange_res.pop('partner_ids', [])]
@ -146,7 +146,7 @@ class test_message_compose(TestMail):
'default_partner_ids': [p_a_id],
'active_ids': [self.group_pigs_id, self.group_bird_id]
}
compose_id = mail_compose.create(cr, uid, {'subject': 'Forget me subject', 'body': 'Dummy body', 'post': True}, context)
compose_id = mail_compose.create(cr, uid, {'subject': 'Forget me subject', 'body': 'Dummy body'}, context)
compose = mail_compose.browse(cr, uid, compose_id, context)
onchange_res = compose.onchange_template_id(email_template_id, 'mass_mail', 'mail.group', self.group_pigs_id)['value']
onchange_res['partner_ids'] = [(4, partner_id) for partner_id in onchange_res.pop('partner_ids', [])]
@ -172,12 +172,12 @@ class test_message_compose(TestMail):
self.assertIn(_body_html1, message_pigs.body, 'mail.message body on Pigs incorrect')
self.assertIn(_body_html2, message_bird.body, 'mail.message body on Bird incorrect')
# Test: partner_ids: p_a_id (default) + 3 newly created partners
message_pigs_pids = [partner.id for partner in message_pigs.notified_partner_ids]
message_bird_pids = [partner.id for partner in message_bird.notified_partner_ids]
partner_ids = self.res_partner.search(cr, uid, [('email', 'in', ['b@b.b', 'c@c.c', 'd@d.d'])])
partner_ids.append(p_a_id)
self.assertEqual(set(message_pigs_pids), set(partner_ids), 'mail.message on pigs incorrect number of notified_partner_ids')
self.assertEqual(set(message_bird_pids), set(partner_ids), 'mail.message on bird notified_partner_ids incorrect')
# message_pigs_pids = [partner.id for partner in message_pigs.notified_partner_ids]
# message_bird_pids = [partner.id for partner in message_bird.notified_partner_ids]
# partner_ids = self.res_partner.search(cr, uid, [('email', 'in', ['b@b.b', 'c@c.c', 'd@d.d'])])
# partner_ids.append(p_a_id)
# self.assertEqual(set(message_pigs_pids), set(partner_ids), 'mail.message on pigs incorrect number of notified_partner_ids')
# self.assertEqual(set(message_bird_pids), set(partner_ids), 'mail.message on bird notified_partner_ids incorrect')
# ----------------------------------------
# CASE4: test newly introduced partner_to field
@ -237,8 +237,8 @@ class test_message_compose(TestMail):
email_template.send_mail(cr, uid, email_template_id, self.group_pigs_id, force_send=True, context=context)
sent_emails = self._build_email_kwargs_list
email_to_lst = [
['b@b.b', 'c@c.c'], ['"Followers of Pigs" <admin@yourcompany.example.com>'],
['"Followers of Pigs" <raoul@raoul.fr>'], ['"Followers of Pigs" <bert@bert.fr>']]
['b@b.b', 'c@c.c'], ['Administrator <admin@yourcompany.example.com>'],
['Raoul Grosbedon <raoul@raoul.fr>'], ['Bert Tartignole <bert@bert.fr>']]
self.assertEqual(len(sent_emails), 4, 'email_template: send_mail: 3 valid email recipients + email_to -> should send 4 emails')
for email in sent_emails:
self.assertIn(email['email_to'], email_to_lst, 'email_template: send_mail: wrong email_recipients')

View File

@ -66,6 +66,7 @@ class email_template_preview(osv.osv_memory):
_columns = {
'res_id': fields.selection(_get_records, 'Sample Document'),
'partner_ids': fields.many2many('res.partner', string='Recipients'),
}
def on_change_res_id(self, cr, uid, ids, res_id, context=None):
@ -80,7 +81,7 @@ class email_template_preview(osv.osv_memory):
# generate and get template values
mail_values = email_template.generate_email(cr, uid, template_id, res_id, context=context)
vals = dict((field, mail_values.get(field, False)) for field in ('email_from', 'email_to', 'email_cc', 'reply_to', 'subject', 'body_html', 'partner_to'))
vals = dict((field, mail_values.get(field, False)) for field in ('email_from', 'email_to', 'email_cc', 'reply_to', 'subject', 'body_html', 'partner_to', 'partner_ids', 'attachment_ids'))
vals['name'] = template.name
return {'value': vals}

View File

@ -8,14 +8,17 @@
<field name="arch" type="xml">
<form string="Email Preview" version="7.0">
<field name="model_id" invisible="1"/>
<h2 style="color: #7c7bad;">Preview of <field name="name" readonly="1" nolabel="1" class="oe_inline"/></h2>
Using sample document <field name="res_id" on_change="on_change_res_id(res_id, context)" class="oe_inline"/>
<h3>Preview of <field name="name" readonly="1" nolabel="1" class="oe_inline"/></h3>
Choose an example <field name="model_id" class="oe_inline" readonly="1"/> record:
<field name="res_id" on_change="on_change_res_id(res_id, context)" class="oe_inline"
style="margin-left: 8px;"/>
<group>
<field name="subject" readonly="1"/>
<field name="email_from" readonly="1"
attrs="{'invisible':[('email_from','=',False)]}"/>
<field name="email_to" readonly="1"/>
<field name="partner_to" readonly="1"/>
<field name="partner_ids" widget="many2many_tags" readonly="1"/>
<field name="email_to" readonly="1"
attrs="{'invisible':[('email_to','=',False)]}"/>
<field name="email_cc" readonly="1"
attrs="{'invisible':[('email_cc','=',False)]}"/>
<field name="reply_to" readonly="1"
@ -23,6 +26,7 @@
</group>
<field name="body_html" widget="html" readonly="1"
nolabel="1" options='{"safe": True}'/>
<field name="attachment_ids" widget="many2many_binary" radonly="1"/>
</form>
</field>
</record>
@ -30,10 +34,11 @@
<record id="wizard_email_template_preview" model="ir.actions.act_window">
<field name="name">Template Preview</field>
<field name="res_model">email_template.preview</field>
<field name="src_model">email_template.preview</field>
<field name="src_model">email.template</field>
<field name="type">ir.actions.act_window</field>
<field name="view_type">form</field>
<field name="view_mode">form</field>
<field name="view_id" ref="email_template_preview_form"/>
<field name="auto_refresh" eval="1" />
<field name="target">new</field>
<field name="context">{'template_id':active_id}</field>

View File

@ -42,7 +42,8 @@ class mail_compose_message(osv.TransientModel):
_inherit = 'mail.compose.message'
def default_get(self, cr, uid, fields, context=None):
""" Override to pre-fill the data when having a template in single-email mode """
""" Override to pre-fill the data when having a template in single-email mode
and not going through the view: the on_change is not called in that case. """
if context is None:
context = {}
res = super(mail_compose_message, self).default_get(cr, uid, fields, context=context)
@ -50,19 +51,13 @@ class mail_compose_message(osv.TransientModel):
res.update(
self.onchange_template_id(
cr, uid, [], context['default_template_id'], res.get('composition_mode'),
res.get('model'), res.get('res_id', context.get('active_id')), context=context
res.get('model'), res.get('res_id'), context=context
)['value']
)
return res
_columns = {
'template_id': fields.many2one('email.template', 'Use template', select=True),
'partner_to': fields.char('To (Partner IDs)',
help="Comma-separated list of recipient partners ids (placeholders may be used here)"),
'email_to': fields.char('To (Emails)',
help="Comma-separated recipient addresses (placeholders may be used here)",),
'email_cc': fields.char('Cc (Emails)',
help="Carbon copy recipients (placeholders may be used here)"),
}
def send_mail(self, cr, uid, ids, context=None):
@ -92,14 +87,13 @@ class mail_compose_message(osv.TransientModel):
""" - mass_mailing: we cannot render, so return the template values
- normal mode: return rendered values """
if template_id and composition_mode == 'mass_mail':
fields = ['subject', 'body_html', 'email_from', 'email_to', 'partner_to', 'email_cc', 'reply_to', 'attachment_ids', 'mail_server_id']
fields = ['subject', 'body_html', 'email_from', 'reply_to', 'attachment_ids', 'mail_server_id']
template_values = self.pool.get('email.template').read(cr, uid, template_id, fields, context)
values = dict((field, template_values[field]) for field in fields if template_values.get(field))
elif template_id:
values = self.generate_email_for_composer_batch(cr, uid, template_id, [res_id], context=context)[res_id]
# transform attachments into attachment_ids; not attached to the document because this will
# be done further in the posting process, allowing to clean database if email not send
values['attachment_ids'] = values.pop('attachment_ids', [])
ir_attach_obj = self.pool.get('ir.attachment')
for attach_fname, attach_datas in values.pop('attachments', []):
data_attach = {
@ -110,7 +104,7 @@ class mail_compose_message(osv.TransientModel):
'res_id': 0,
'type': 'binary', # override default_type from context, possibly meant for another model!
}
values['attachment_ids'].append(ir_attach_obj.create(cr, uid, data_attach, context=context))
values.setdefault('attachment_ids', list()).append(ir_attach_obj.create(cr, uid, data_attach, context=context))
else:
values = self.default_get(cr, uid, ['subject', 'body', 'email_from', 'email_to', 'email_cc', 'partner_to', 'reply_to', 'attachment_ids', 'mail_server_id'], context=context)
@ -148,47 +142,29 @@ class mail_compose_message(osv.TransientModel):
# Wizard validation and send
#------------------------------------------------------
def _get_or_create_partners_from_values(self, cr, uid, rendered_values, context=None):
""" Check for email_to, email_cc, partner_to """
partner_ids = []
mails = tools.email_split(rendered_values.pop('email_to', '')) + tools.email_split(rendered_values.pop('email_cc', ''))
for mail in mails:
partner_id = self.pool.get('res.partner').find_or_create(cr, uid, mail, context=context)
partner_ids.append(partner_id)
partner_to = rendered_values.pop('partner_to', '')
if partner_to:
# placeholders could generate '', 3, 2 due to some empty field values
tpl_partner_ids = [pid for pid in partner_to.split(',') if pid]
partner_ids += self.pool['res.partner'].exists(cr, SUPERUSER_ID, tpl_partner_ids, context=context)
return partner_ids
def generate_email_for_composer_batch(self, cr, uid, template_id, res_ids, context=None, fields=None):
""" Call email_template.generate_email(), get fields relevant for
mail.compose.message, transform email_cc and email_to into partner_ids """
# filter template values
if context is None:
context = {}
if fields is None:
fields = ['subject', 'body_html', 'email_from', 'email_to', 'partner_to', 'email_cc', 'reply_to', 'attachment_ids', 'mail_server_id']
returned_fields = fields + ['attachments']
returned_fields = fields + ['partner_ids', 'attachments']
values = dict.fromkeys(res_ids, False)
template_values = self.pool.get('email.template').generate_email_batch(cr, uid, template_id, res_ids, fields=fields, context=context)
ctx = dict(context, tpl_partners_only=True)
template_values = self.pool.get('email.template').generate_email_batch(cr, uid, template_id, res_ids, fields=fields, context=ctx)
for res_id in res_ids:
res_id_values = dict((field, template_values[res_id][field]) for field in returned_fields if template_values[res_id].get(field))
res_id_values['body'] = res_id_values.pop('body_html', '')
# transform email_to, email_cc into partner_ids
ctx = dict((k, v) for k, v in (context or {}).items() if not k.startswith('default_'))
partner_ids = self._get_or_create_partners_from_values(cr, uid, res_id_values, context=ctx)
# legacy template behavior: void values do not erase existing values and the
# related key is removed from the values dict
if partner_ids:
res_id_values['partner_ids'] = list(partner_ids)
values[res_id] = res_id_values
return values
def render_message_batch(self, cr, uid, wizard, res_ids, context=None):
""" Override to handle templates. """
# generate composer values
composer_values = super(mail_compose_message, self).render_message_batch(cr, uid, wizard, res_ids, context)
# generate template-based values
if wizard.template_id:
template_values = self.generate_email_for_composer_batch(
@ -196,17 +172,18 @@ class mail_compose_message(osv.TransientModel):
fields=['email_to', 'partner_to', 'email_cc', 'attachment_ids', 'mail_server_id'],
context=context)
else:
template_values = dict.fromkeys(res_ids, dict())
# generate composer values
composer_values = super(mail_compose_message, self).render_message_batch(cr, uid, wizard, res_ids, context)
template_values = {}
for res_id in res_ids:
# remove attachments from template values as they should not be rendered
template_values[res_id].pop('attachment_ids', None)
# remove some keys from composer that are readonly
composer_values[res_id].pop('email_to', None)
composer_values[res_id].pop('email_cc', None)
composer_values[res_id].pop('partner_to', None)
if template_values.get(res_id):
# recipients are managed by the template
composer_values[res_id].pop('partner_ids')
composer_values[res_id].pop('email_to')
composer_values[res_id].pop('email_cc')
# remove attachments from template values as they should not be rendered
template_values[res_id].pop('attachment_ids', None)
else:
template_values[res_id] = dict()
# update template values by composer values
template_values[res_id].update(composer_values[res_id])
return template_values

View File

@ -7,22 +7,6 @@
<field name="model">mail.compose.message</field>
<field name="inherit_id" ref="mail.email_compose_message_wizard_form"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='subject']" position="after">
<label string="Template Recipients" for="partner_to"
groups="base.group_no_one"
attrs="{'invisible':[('composition_mode', '!=', 'mass_mail')]}"/>
<div groups="base.group_no_one" name="template_recipients"
attrs="{'invisible':[('composition_mode', '!=', 'mass_mail')]}">
<group class="oe_grey">
<!-- <label string="Partners" for="partner_to"/> -->
<field name="partner_to" readonly="1"/>
<!-- <label string="Email To" for="email_to"/> -->
<field name="email_to" readonly="1"/>
<!-- <label string="Email CC" for="email_cc"/> -->
<field name="email_cc" readonly="1"/>
</group>
</div>
</xpath>
<xpath expr="//footer" position="inside">
<group class="oe_right oe_form" col="1">
<div>Use template

View File

@ -80,6 +80,7 @@ class hr_applicant(osv.Model):
_description = "Applicant"
_order = "id desc"
_inherit = ['mail.thread', 'ir.needaction_mixin']
_track = {
'stage_id': {
# this is only an heuristics; depending on your particular stage configuration it may not match all 'new' stages
@ -87,6 +88,7 @@ class hr_applicant(osv.Model):
'hr_recruitment.mt_applicant_stage_changed': lambda self, cr, uid, obj, ctx=None: obj.stage_id and obj.stage_id.sequence > 1,
},
}
_mail_mass_mailing = _('Applicants')
def _get_default_department_id(self, cr, uid, context=None):
""" Gives default department by checking if present in the context """

View File

@ -126,7 +126,7 @@ class mail_mail(osv.Model):
_logger.exception("Failed processing mail queue")
return res
def _postprocess_sent_message(self, cr, uid, mail, context=None):
def _postprocess_sent_message(self, cr, uid, mail, context=None, mail_sent=True):
"""Perform any post-processing necessary after sending ``mail``
successfully, including deleting it completely along with its
attachment if the ``auto_delete`` flag of the mail was set.
@ -145,9 +145,8 @@ class mail_mail(osv.Model):
#------------------------------------------------------
def _get_partner_access_link(self, cr, uid, mail, partner=None, context=None):
""" Generate URLs for links in mails:
- partner is an user and has read access to the document: direct link to document with model, res_id
"""
"""Generate URLs for links in mails: partner has access (is user):
link to action_mail_redirect action that will redirect to doc or Inbox """
if partner and partner.user_ids:
base_url = self.pool.get('ir.config_parameter').get_param(cr, uid, 'web.base.url')
# the parameters to encode for the query and fragment part of url
@ -167,11 +166,10 @@ class mail_mail(osv.Model):
return 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, set the subject as 'Re: <Resource>' or
'Re: <mail.parent_id.subject>'
:param boolean force: force the subject replacement
:param browse_record mail: mail.mail browse_record
:param browse_record partner: specific recipient partner
"""
if (force or not mail.subject) and mail.record_name:
return 'Re: %s' % (mail.record_name)
@ -180,12 +178,8 @@ class mail_mail(osv.Model):
return mail.subject
def send_get_mail_body(self, cr, uid, mail, partner=None, context=None):
""" Return a specific ir_email body. The main purpose of this method
is to be inherited to add custom content depending on some module.
:param browse_record mail: mail.mail browse_record
:param browse_record partner: specific recipient partner
"""
"""Return a specific ir_email body. The main purpose of this method
is to be inherited to add custom content depending on some module."""
body = mail.body_html
# generate footer
@ -194,34 +188,34 @@ class mail_mail(osv.Model):
body = tools.append_content_to_html(body, link, plaintext=False, container_tag='div')
return body
def send_get_email_dict(self, cr, uid, mail, partner=None, context=None):
""" Return a dictionary for specific email values, depending on a
partner, or generic to the whole recipients given by mail.email_to.
:param browse_record mail: mail.mail browse_record
:param browse_record partner: specific recipient partner
"""
body = self.send_get_mail_body(cr, uid, mail, partner=partner, context=context)
subject = self.send_get_mail_subject(cr, uid, mail, partner=partner, context=context)
body_alternative = tools.html2plaintext(body)
# generate email_to, heuristic:
# 1. if 'partner' is specified and there is a related document: Followers of 'Doc' <email>
# 2. if 'partner' is specified, but no related document: Partner Name <email>
# 3; fallback on mail.email_to that we split to have an email addresses list
if partner and mail.record_name:
def send_get_mail_to(self, cr, uid, mail, partner=None, context=None):
"""Forge the email_to with the following heuristic:
- if 'partner' and mail is a notification on a document: followers (Followers of 'Doc' <email>)
- elif 'partner', no notificatoin or no doc: recipient specific (Partner Name <email>)
- else fallback on mail.email_to splitting """
if partner and mail.notification and mail.record_name:
sanitized_record_name = re.sub(r'[^\w+.]+', '-', mail.record_name)
email_to = [_('"Followers of %s" <%s>') % (sanitized_record_name, partner.email)]
elif partner:
email_to = ['%s <%s>' % (partner.name, partner.email)]
else:
email_to = tools.email_split(mail.email_to)
return email_to
def send_get_email_dict(self, cr, uid, mail, partner=None, context=None):
"""Return a dictionary for specific email values, depending on a
partner, or generic to the whole recipients given by mail.email_to.
:param browse_record mail: mail.mail browse_record
:param browse_record partner: specific recipient partner
"""
body = self.send_get_mail_body(cr, uid, mail, partner=partner, context=context)
body_alternative = tools.html2plaintext(body)
return {
'body': body,
'body_alternative': body_alternative,
'subject': subject,
'email_to': email_to,
'subject': self.send_get_mail_subject(cr, uid, mail, partner=partner, context=context),
'email_to': self.send_get_mail_to(cr, uid, mail, partner=partner, context=context),
}
def send(self, cr, uid, ids, auto_commit=False, raise_exception=False, context=None):
@ -240,7 +234,7 @@ class mail_mail(osv.Model):
:return: True
"""
ir_mail_server = self.pool.get('ir.mail_server')
for mail in self.browse(cr, SUPERUSER_ID, ids, context=context):
try:
# handle attachments
@ -284,7 +278,7 @@ class mail_mail(osv.Model):
res = ir_mail_server.send_email(cr, uid, msg,
mail_server_id=mail.mail_server_id.id,
context=context)
if res:
mail.write({'state': 'sent', 'message_id': res})
mail_sent = True
@ -294,11 +288,11 @@ class mail_mail(osv.Model):
# /!\ can't use mail.state here, as mail.refresh() will cause an error
# see revid:odo@openerp.com-20120622152536-42b2s28lvdv3odyr in 6.1
if mail_sent:
self._postprocess_sent_message(cr, uid, mail, context=context)
self._postprocess_sent_message(cr, uid, mail, context=context, mail_sent=mail_sent)
except Exception as e:
_logger.exception('failed sending mail.mail %s', mail.id)
mail.write({'state': 'exception'})
self._postprocess_sent_message(cr, uid, mail, context=context, mail_sent=False)
if raise_exception:
if isinstance(e, AssertionError):
# get the args of the original error, wrap into a value and throw a MailDeliveryException
@ -307,6 +301,6 @@ class mail_mail(osv.Model):
raise MailDeliveryException(_("Mail Delivery Failed"), value)
raise
if auto_commit == True:
if auto_commit is True:
cr.commit()
return True

View File

@ -36,6 +36,7 @@
<div>
<group string="Status">
<field name="auto_delete"/>
<field name="notification"/>
<field name="type"/>
<field name="state"/>
<field name="mail_server_id"/>

View File

@ -81,22 +81,6 @@ class mail_message(osv.Model):
context = dict(context, default_type=None)
return super(mail_message, self).default_get(cr, uid, fields, context=context)
def _shorten_name(self, name):
if len(name) <= (self._message_record_name_length + 3):
return name
return name[:self._message_record_name_length] + '...'
def _get_record_name(self, cr, uid, ids, name, arg, context=None):
""" Return the related document name, using name_get. It is done using
SUPERUSER_ID, to be sure to have the record name correctly stored. """
# TDE note: regroup by model/ids, to have less queries to perform
result = dict.fromkeys(ids, False)
for message in self.read(cr, uid, ids, ['model', 'res_id'], context=context):
if not message.get('model') or not message.get('res_id') or message['model'] not in self.pool:
continue
result[message['id']] = self.pool[message['model']].name_get(cr, SUPERUSER_ID, [message['res_id']], context=context)[0][1]
return result
def _get_to_read(self, cr, uid, ids, name, arg, context=None):
""" Compute if the message is unread by the current user. """
res = dict((id, False) for id in ids)
@ -135,16 +119,6 @@ class mail_message(osv.Model):
inversed because we search unread message on a read column. """
return ['&', ('notification_ids.partner_id.user_ids', 'in', [uid]), ('notification_ids.starred', '=', domain[0][2])]
def name_get(self, cr, uid, ids, context=None):
# name_get may receive int id instead of an id list
if isinstance(ids, (int, long)):
ids = [ids]
res = []
for message in self.browse(cr, uid, ids, context=context):
name = '%s: %s' % (message.subject or '', strip_tags(message.body or '') or '')
res.append((message.id, self._shorten_name(name.lstrip(' :'))))
return res
_columns = {
'type': fields.selection([
('email', 'Email'),
@ -172,9 +146,7 @@ class mail_message(osv.Model):
'child_ids': fields.one2many('mail.message', 'parent_id', 'Child Messages'),
'model': fields.char('Related Document Model', size=128, select=1),
'res_id': fields.integer('Related Document ID', select=1),
'record_name': fields.function(_get_record_name, type='char',
store=True, string='Message Record Name',
help="Name get of the related document."),
'record_name': fields.char('Message Record Name', help="Name get of the related document."),
'notification_ids': fields.one2many('mail.notification', 'message_id',
string='Notifications', auto_join=True,
help='Technical field holding the message notifications. Use notified_partner_ids to access notified partners.'),
@ -783,6 +755,13 @@ class mail_message(osv.Model):
_('The requested operation cannot be completed due to security restrictions. Please contact your system administrator.\n\n(Document type: %s, Operation: %s)') % \
(self._description, operation))
def _get_record_name(self, cr, uid, values, context=None):
""" Return the related document name, using name_get. It is done using
SUPERUSER_ID, to be sure to have the record name correctly stored. """
if not values.get('model') or not values.get('res_id') or values['model'] not in self.pool:
return False
return self.pool[values['model']].name_get(cr, SUPERUSER_ID, [values['res_id']], context=context)[0][1]
def _get_reply_to(self, cr, uid, values, context=None):
""" Return a specific reply_to: alias of the document through message_get_reply_to
or take the email_from
@ -841,8 +820,11 @@ class mail_message(osv.Model):
values['message_id'] = self._get_message_id(cr, uid, values, context=context)
if 'reply_to' not in values:
values['reply_to'] = self._get_reply_to(cr, uid, values, context=context)
if 'record_name' not in values and 'default_record_name' not in context:
values['record_name'] = self._get_record_name(cr, uid, values, context=context)
newid = super(mail_message, self).create(cr, uid, values, context)
self._notify(cr, uid, newid, context=context,
force_send=context.get('mail_notify_force_send', True),
user_signature=context.get('mail_notify_user_signature', True))
@ -887,78 +869,6 @@ class mail_message(osv.Model):
# Messaging API
#------------------------------------------------------
# TDE note: this code is not used currently, will be improved in a future merge, when quoted context
# will be added to email send for notifications. Currently only WIP.
MAIL_TEMPLATE = """<div>
% if message:
${display_message(message)}
% endif
% for ctx_msg in context_messages:
${display_message(ctx_msg)}
% endfor
% if add_expandable:
${display_expandable()}
% endif
${display_message(header_message)}
</div>
<%def name="display_message(message)">
<div>
Subject: ${message.subject}<br />
Body: ${message.body}
</div>
</%def>
<%def name="display_expandable()">
<div>This is an expandable.</div>
</%def>
"""
def message_quote_context(self, cr, uid, id, context=None, limit=3, add_original=False):
"""
1. message.parent_id = False: new thread, no quote_context
2. get the lasts messages in the thread before message
3. get the message header
4. add an expandable between them
:param dict quote_context: options for quoting
:return string: html quote
"""
add_expandable = False
message = self.browse(cr, uid, id, context=context)
if not message.parent_id:
return ''
context_ids = self.search(cr, uid, [
('parent_id', '=', message.parent_id.id),
('id', '<', message.id),
], limit=limit, context=context)
if len(context_ids) >= limit:
add_expandable = True
context_ids = context_ids[0:-1]
context_ids.append(message.parent_id.id)
context_messages = self.browse(cr, uid, context_ids, context=context)
header_message = context_messages.pop()
try:
if not add_original:
message = False
result = MakoTemplate(self.MAIL_TEMPLATE).render_unicode(message=message,
context_messages=context_messages,
header_message=header_message,
add_expandable=add_expandable,
# context kw would clash with mako internals
ctx=context,
format_exceptions=True)
result = result.strip()
return result
except Exception:
_logger.exception("failed to render mako template for quoting message")
return ''
return result
def _notify(self, cr, uid, newid, context=None, force_send=False, user_signature=True):
""" Add the related record followers to the destination partner_ids if is not a private message.
Call mail_notification.notify to manage the email sending
@ -975,9 +885,11 @@ class mail_message(osv.Model):
cr, SUPERUSER_ID, [
('res_model', '=', message.model),
('res_id', '=', message.res_id),
('subtype_ids', 'in', message.subtype_id.id)
], context=context)
partners_to_notify |= set(fo.partner_id.id for fo in fol_obj.browse(cr, SUPERUSER_ID, fol_ids, context=context))
partners_to_notify |= set(
fo.partner_id.id for fo in fol_obj.browse(cr, SUPERUSER_ID, fol_ids, context=context)
if message.subtype_id.id in [st.id for st in fo.subtype_ids]
)
# remove me from notified partners, unless the message is written on my own wall
if message.subtype_id and message.author_id and message.model == "res.partner" and message.res_id == message.author_id.id:
partners_to_notify |= set([message.author_id.id])
@ -1006,25 +918,3 @@ class mail_message(osv.Model):
'partner_id': partner.id,
'read': True,
}, context=context)
#------------------------------------------------------
# Tools
#------------------------------------------------------
def check_partners_email(self, cr, uid, partner_ids, context=None):
""" Verify that selected partner_ids have an email_address defined.
Otherwise throw a warning. """
partner_wo_email_lst = []
for partner in self.pool.get('res.partner').browse(cr, uid, partner_ids, context=context):
if not partner.email:
partner_wo_email_lst.append(partner)
if not partner_wo_email_lst:
return {}
warning_msg = _('The following partners chosen as recipients for the email have no email address linked :')
for partner in partner_wo_email_lst:
warning_msg += '\n- %s' % (partner.name)
return {'warning': {
'title': _('Partners email addresses not found'),
'message': warning_msg,
}
}

View File

@ -97,6 +97,9 @@ class mail_thread(osv.AbstractModel):
# :param function lambda: returns whether the tracking should record using this subtype
_track = {}
# Mass mailing feature
_mail_mass_mailing = False
def get_empty_list_help(self, cr, uid, help, context=None):
""" Override of BaseModel.get_empty_list_help() to generate an help message
that adds alias information. """
@ -662,15 +665,31 @@ class mail_thread(osv.AbstractModel):
# Email specific
#------------------------------------------------------
def message_get_default_recipients(self, cr, uid, ids, context=None):
if context and context.get('thread_model') and context['thread_model'] in self.pool and context['thread_model'] != self._name:
sub_ctx = dict(context)
sub_ctx.pop('thread_model')
return self.pool[context['thread_model']].message_get_default_recipients(cr, uid, ids, context=sub_ctx)
res = {}
for record in self.browse(cr, SUPERUSER_ID, ids, context=context):
recipient_ids, email_to, email_cc = set(), False, False
if 'partner_id' in self._all_columns and record.partner_id:
recipient_ids.add(record.partner_id.id)
elif 'email_from' in self._all_columns and record.email_from:
email_to = record.email_from
elif 'email' in self._all_columns:
email_to = record.email
res[record.id] = {'partner_ids': list(recipient_ids), 'email_to': email_to, 'email_cc': email_cc}
return res
def message_get_reply_to(self, cr, uid, ids, context=None):
""" Returns the preferred reply-to email address that is basically
the alias of the document, if it exists. """
if not self._inherits.get('mail.alias'):
return [False for id in ids]
return ["%s@%s" % (record['alias_name'], record['alias_domain'])
if record.get('alias_domain') and record.get('alias_name')
else False
for record in self.read(cr, SUPERUSER_ID, ids, ['alias_name', 'alias_domain'], context=context)]
return ["%s@%s" % (record.alias_name, record.alias_domain)
if record.alias_domain and record.alias_name else False
for record in self.browse(cr, SUPERUSER_ID, ids, context=context)]
#------------------------------------------------------
# Mail gateway

View File

@ -28,6 +28,7 @@ class res_partner_mail(osv.Model):
_name = "res.partner"
_inherit = ['res.partner', 'mail.thread']
_mail_flat_thread = False
_mail_mass_mailing = _('Customers')
_columns = {
'notification_email_send': fields.selection([
@ -53,4 +54,5 @@ class res_partner_mail(osv.Model):
self._message_add_suggested_recipient(cr, uid, recipients, partner, partner=partner, reason=_('Partner Profile'))
return recipients
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
def message_get_default_recipients(self, cr, uid, ids, context=None):
return dict((id, {'partner_ids': [id], 'email_to': False, 'email_cc': False}) for id in ids)

View File

@ -507,18 +507,15 @@ openerp.mail = function (session) {
}
$.when(recipient_done).done(function (partner_ids) {
var context = {
'default_composition_mode': default_composition_mode,
'default_parent_id': self.id,
'default_body': mail.ChatterUtils.get_text2html(self.$el ? (self.$el.find('textarea:not(.oe_compact)').val() || '') : ''),
'default_attachment_ids': _.map(self.attachment_ids, function (file) {return file.id;}),
'default_partner_ids': partner_ids,
'default_is_log': self.is_log,
'mail_post_autofollow': true,
'mail_post_autofollow_partner_ids': partner_ids,
'is_private': self.is_private
};
if (self.is_log) {
_.extend(context, {'mail_compose_log': true});
}
if (default_composition_mode != 'reply' && self.context.default_model && self.context.default_res_id) {
context.default_model = self.context.default_model;
context.default_res_id = self.context.default_res_id;

View File

@ -210,24 +210,6 @@ class test_mail(TestMail):
self.assertTrue(subtype_data['mt_mg_nodef']['followed'], 'Admin should follow mt_mg_nodef in pigs')
self.assertTrue(subtype_data['mt_all_nodef']['followed'], 'Admin should follow mt_all_nodef in pigs')
def test_10_message_quote_context(self):
""" Tests designed for message_post. """
cr, uid, user_admin, group_pigs = self.cr, self.uid, self.user_admin, self.group_pigs
msg1_id = self.mail_message.create(cr, uid, {'body': 'Thread header about Zap Brannigan', 'subject': 'My subject'})
msg2_id = self.mail_message.create(cr, uid, {'body': 'First answer, should not be displayed', 'subject': 'Re: My subject', 'parent_id': msg1_id})
msg3_id = self.mail_message.create(cr, uid, {'body': 'Second answer', 'subject': 'Re: My subject', 'parent_id': msg1_id})
msg4_id = self.mail_message.create(cr, uid, {'body': 'Third answer', 'subject': 'Re: My subject', 'parent_id': msg1_id})
msg_new_id = self.mail_message.create(cr, uid, {'body': 'My answer I am propagating', 'subject': 'Re: My subject', 'parent_id': msg1_id})
result = self.mail_message.message_quote_context(cr, uid, msg_new_id, limit=3)
self.assertIn('Thread header about Zap Brannigan', result, 'Thread header content should be in quote.')
self.assertIn('Second answer', result, 'Answer should be in quote.')
self.assertIn('Third answer', result, 'Answer should be in quote.')
self.assertIn('expandable', result, 'Expandable should be present.')
self.assertNotIn('First answer, should not be displayed', result, 'Old answer should not be in quote.')
self.assertNotIn('My answer I am propagating', result, 'Thread header content should be in quote.')
def test_11_notification_url(self):
""" Tests designed to test the URL added in notification emails. """
cr, uid, group_pigs = self.cr, self.uid, self.group_pigs
@ -674,7 +656,6 @@ class test_mail(TestMail):
'attachment_ids': [(0, 0, _attachments[0]), (0, 0, _attachments[1])]
}, context={
'default_composition_mode': 'reply',
'default_model': 'mail.thread',
'default_res_id': self.group_pigs_id,
'default_parent_id': message.id
})
@ -699,11 +680,10 @@ class test_mail(TestMail):
# --------------------------------------------------
# Do: Compose in mass_mail_mode on pigs and bird
compose_id = mail_compose.create(cr, user_raoul.id,
{
compose_id = mail_compose.create(
cr, user_raoul.id, {
'subject': _subject,
'body': '${object.description}',
'post': True,
'partner_ids': [(4, p_c_id), (4, p_d_id)],
}, context={
'default_composition_mode': 'mass_mail',
@ -718,6 +698,13 @@ class test_mail(TestMail):
'default_res_id': -1,
'active_ids': [self.group_pigs_id, group_bird_id]
})
# check mail_mail
mail_mail_ids = self.mail_mail.search(cr, uid, [('subject', '=', _subject)])
for mail_mail in self.mail_mail.browse(cr, uid, mail_mail_ids):
self.assertEqual(set([p.id for p in mail_mail.recipient_ids]), set([p_c_id, p_d_id]),
'compose wizard: mail_mail mass mailing: mail.mail in mass mail incorrect recipients')
# check logged messages
group_pigs.refresh()
group_bird.refresh()
message1 = group_pigs.message_ids[0]
@ -733,14 +720,14 @@ class test_mail(TestMail):
'compose wizard: message_post: mail.message in mass mail subject incorrect')
self.assertEqual(message1.body, '<p>%s</p>' % group_pigs.description,
'compose wizard: message_post: mail.message in mass mail body incorrect')
self.assertEqual(set([p.id for p in message1.notified_partner_ids]), set([p_c_id, p_d_id]),
'compose wizard: message_post: mail.message in mass mail incorrect notified partners')
# self.assertEqual(set([p.id for p in message1.notified_partner_ids]), set([p_c_id, p_d_id]),
# 'compose wizard: message_post: mail.message in mass mail incorrect notified partners')
self.assertEqual(message2.subject, _subject,
'compose wizard: message_post: mail.message in mass mail subject incorrect')
self.assertEqual(message2.body, '<p>%s</p>' % group_bird.description,
'compose wizard: message_post: mail.message in mass mail body incorrect')
self.assertEqual(set([p.id for p in message2.notified_partner_ids]), set([p_c_id, p_d_id]),
'compose wizard: message_post: mail.message in mass mail incorrect notified partners')
# self.assertEqual(set([p.id for p in message2.notified_partner_ids]), set([p_c_id, p_d_id]),
# 'compose wizard: message_post: mail.message in mass mail incorrect notified partners')
# Test: mail.group followers: author not added as follower in mass mail mode
pigs_pids = [p.id for p in group_pigs.message_follower_ids]
@ -757,7 +744,6 @@ class test_mail(TestMail):
{
'subject': _subject,
'body': '${object.description}',
'post': True,
'partner_ids': [(4, p_c_id), (4, p_d_id)],
}, context={
'default_composition_mode': 'mass_mail',

View File

@ -38,10 +38,7 @@ class mail_compose_message(osv.TransientModel):
at model and view levels to provide specific features.
The behavior of the wizard depends on the composition_mode field:
- 'reply': reply to a previous message. The wizard is pre-populated
via ``get_message_data``.
- 'comment': new post on a record. The wizard is pre-populated via
``get_record_data``
- 'comment': post on a record. The wizard is pre-populated via ``get_record_data``
- 'mass_mail': wizard in mass mailing mode where the mail details can
contain template placeholders that will be merged with actual data
before being sent to each recipient.
@ -50,6 +47,7 @@ class mail_compose_message(osv.TransientModel):
_inherit = 'mail.message'
_description = 'Email composition wizard'
_log_access = True
_batch_size = 500
def default_get(self, cr, uid, fields, context=None):
""" Handle composition mode. Some details about context keys:
@ -68,28 +66,22 @@ class mail_compose_message(osv.TransientModel):
if context is None:
context = {}
result = super(mail_compose_message, self).default_get(cr, uid, fields, context=context)
# get some important values from context
composition_mode = context.get('default_composition_mode', context.get('mail.compose.message.mode'))
model = context.get('default_model', context.get('active_model'))
res_id = context.get('default_res_id', context.get('active_id'))
message_id = context.get('default_parent_id', context.get('message_id', context.get('active_id')))
active_ids = context.get('active_ids')
# v6.1 compatibility mode
result['composition_mode'] = result.get('composition_mode', context.get('mail.compose.message.mode'))
result['model'] = result.get('model', context.get('active_model'))
result['res_id'] = result.get('res_id', context.get('active_id'))
result['parent_id'] = result.get('parent_id', context.get('message_id'))
# default values according to composition mode - NOTE: reply is deprecated, fall back on comment
if result['composition_mode'] == 'reply':
result['composition_mode'] = 'comment'
vals = {}
if 'active_domain' in context: # not context.get() because we want to keep global [] domains
result['use_active_domain'] = True
result['active_domain'] = '%s' % context.get('active_domain')
elif not result.get('active_domain'):
result['active_domain'] = ''
# get default values according to the composition mode
if composition_mode == 'reply':
vals = self.get_message_data(cr, uid, message_id, context=context)
elif composition_mode == 'comment' and model and res_id:
vals = self.get_record_data(cr, uid, model, res_id, context=context)
elif composition_mode == 'mass_mail' and model and active_ids:
vals = {'model': model, 'res_id': res_id}
else:
vals = {'model': model, 'res_id': res_id}
if composition_mode:
vals['composition_mode'] = composition_mode
vals['use_active_domain'] = True
vals['active_domain'] = '%s' % context.get('active_domain')
if result['composition_mode'] == 'comment':
vals.update(self.get_record_data(cr, uid, result, context=context))
for field in vals:
if field in fields:
@ -102,13 +94,15 @@ class mail_compose_message(osv.TransientModel):
# but when creating the mail.message to create the mail.compose.message
# access rights issues may rise
# We therefore directly change the model and res_id
if result.get('model') == 'res.users' and result.get('res_id') == uid:
if result['model'] == 'res.users' and result['res_id'] == uid:
result['model'] = 'res.partner'
result['res_id'] = self.pool.get('res.users').browse(cr, uid, uid).partner_id.id
return result
def _get_composition_mode_selection(self, cr, uid, context=None):
return [('comment', 'Comment a document'), ('reply', 'Reply to a message'), ('mass_mail', 'Mass mailing')]
return [('comment', 'Post on a document'),
('mass_mail', 'Email Mass Mailing'),
('mass_post', 'Post on Multiple Documents')]
_columns = {
'composition_mode': fields.selection(
@ -116,19 +110,19 @@ class mail_compose_message(osv.TransientModel):
string='Composition mode'),
'partner_ids': fields.many2many('res.partner',
'mail_compose_message_res_partner_rel',
'wizard_id', 'partner_id', 'Additional contacts'),
'wizard_id', 'partner_id', 'Additional Contacts'),
'use_active_domain': fields.boolean('Use active domain'),
'active_domain': fields.char('Active domain', readonly=True),
'post': fields.boolean('Post a copy in the document',
help='Post a copy of the message on the document communication history.'),
'notify': fields.boolean('Notify followers',
help='Notify followers of the document'),
'same_thread': fields.boolean('Replies in the document',
help='Replies to the messages will go into the selected document.'),
'attachment_ids': fields.many2many('ir.attachment',
'mail_compose_message_ir_attachments_rel',
'wizard_id', 'attachment_id', 'Attachments'),
'filter_id': fields.many2one('ir.filters', 'Filters'),
'is_log': fields.boolean('Log an Internal Note',
help='Whether the message is an internal note (comment mode only)'),
# mass mode options
'notify': fields.boolean('Notify followers',
help='Notify followers of the document (mass post only)'),
'same_thread': fields.boolean('Replies in the document',
help='Replies to the messages will go into the selected document (mass mail only)'),
}
#TODO change same_thread to False in trunk (Require view update)
_defaults = {
@ -136,8 +130,6 @@ class mail_compose_message(osv.TransientModel):
'body': lambda self, cr, uid, ctx={}: '',
'subject': lambda self, cr, uid, ctx={}: False,
'partner_ids': lambda self, cr, uid, ctx={}: [],
'post': False,
'notify': False,
'same_thread': True,
}
@ -169,61 +161,36 @@ class mail_compose_message(osv.TransientModel):
not want that feature in the wizard. """
return
def get_record_data(self, cr, uid, model, res_id, context=None):
def get_record_data(self, cr, uid, values, context=None):
""" Returns a defaults-like dict with initial values for the composition
wizard when sending an email related to the document record
identified by ``model`` and ``res_id``.
:param str model: model name of the document record this mail is
related to.
:param int res_id: id of the document record this mail is related to
"""
doc_name_get = self.pool[model].name_get(cr, uid, [res_id], context=context)
record_name = False
if doc_name_get:
record_name = doc_name_get[0][1]
values = {
'model': model,
'res_id': res_id,
'record_name': record_name,
}
if record_name:
values['subject'] = 'Re: %s' % record_name
return values
def get_message_data(self, cr, uid, message_id, context=None):
""" Returns a defaults-like dict with initial values for the composition
wizard when replying to the given message (e.g. including the quote
of the initial message, and the correct recipients).
:param int message_id: id of the mail.message to which the user
is replying.
"""
if not message_id:
return {}
wizard when sending an email related a previous email (parent_id) or
a document (model, res_id). This is based on previously computed default
values. """
if context is None:
context = {}
message_data = self.pool.get('mail.message').browse(cr, uid, message_id, context=context)
result, subject = {}, False
if values.get('parent_id'):
parent = self.pool.get('mail.message').browse(cr, uid, values.get('parent_id'), context=context)
result['record_name'] = parent.record_name,
subject = tools.ustr(parent.subject or parent.record_name or '')
if not values.get('model'):
result['model'] = parent.model
if not values.get('res_id'):
result['res_id'] = parent.res_id
partner_ids = values.get('partner_ids', list()) + [partner.id for partner in parent.partner_ids]
if context.get('is_private') and parent.author_id: # check message is private then add author also in partner list.
partner_ids += [parent.author_id.id]
result['partner_ids'] = partner_ids
elif values.get('model') and values.get('res_id'):
doc_name_get = self.pool[values.get('model')].name_get(cr, uid, [values.get('res_id')], context=context)
result['record_name'] = doc_name_get and doc_name_get[0][1] or ''
subject = tools.ustr(result['record_name'])
# create subject
re_prefix = _('Re:')
reply_subject = tools.ustr(message_data.subject or message_data.record_name or '')
if not (reply_subject.startswith('Re:') or reply_subject.startswith(re_prefix)) and message_data.subject:
reply_subject = "%s %s" % (re_prefix, reply_subject)
# get partner_ids from original message
partner_ids = [partner.id for partner in message_data.partner_ids] if message_data.partner_ids else []
partner_ids += context.get('default_partner_ids', [])
if context.get('is_private',False) and message_data.author_id : #check message is private then add author also in partner list.
partner_ids += [message_data.author_id.id]
# update the result
result = {
'record_name': message_data.record_name,
'model': message_data.model,
'res_id': message_data.res_id,
'parent_id': message_data.id,
'subject': reply_subject,
'partner_ids': partner_ids,
}
if subject and not (subject.startswith('Re:') or subject.startswith(re_prefix)):
subject = "%s %s" % (re_prefix, subject)
result['subject'] = subject
return result
#------------------------------------------------------
@ -235,53 +202,42 @@ class mail_compose_message(osv.TransientModel):
email(s), rendering any template patterns on the fly if needed. """
if context is None:
context = {}
# clean the context (hint: mass mailing sets some default values that
# could be wrongly interpreted by mail_mail)
context.pop('default_email_to', None)
context.pop('default_partner_ids', None)
active_ids = context.get('active_ids')
is_log = context.get('mail_compose_log', False)
for wizard in self.browse(cr, uid, ids, context=context):
mass_mail_mode = wizard.composition_mode == 'mass_mail'
mass_mode = wizard.composition_mode in ('mass_mail', 'mass_post')
active_model_pool = self.pool[wizard.model if wizard.model else 'mail.thread']
if not hasattr(active_model_pool, 'message_post'):
context['thread_model'] = wizard.model
active_model_pool = self.pool['mail.thread']
# wizard works in batch mode: [res_id] or active_ids or active_domain
if mass_mail_mode and wizard.use_active_domain and wizard.model:
if mass_mode and wizard.use_active_domain and wizard.model:
res_ids = self.pool[wizard.model].search(cr, uid, eval(wizard.active_domain), context=context)
elif mass_mail_mode and wizard.model and active_ids:
res_ids = active_ids
elif mass_mode and wizard.model and context.get('active_ids'):
res_ids = context['active_ids']
else:
res_ids = [wizard.res_id]
all_mail_values = self.get_mail_values(cr, uid, wizard, res_ids, context=context)
for res_id, mail_values in all_mail_values.iteritems():
if mass_mail_mode and not wizard.post:
m2m_attachment_ids = self.pool['mail.thread']._message_preprocess_attachments(
cr, uid, mail_values.pop('attachments', []),
mail_values.pop('attachment_ids', []),
'mail.message', 0,
context=context)
mail_values['attachment_ids'] = m2m_attachment_ids
if not mail_values.get('reply_to'):
mail_values['reply_to'] = mail_values['email_from']
self.pool.get('mail.mail').create(cr, uid, mail_values, context=context)
else:
subtype = 'mail.mt_comment'
if is_log: # log a note: subtype is False
subtype = False
elif mass_mail_mode: # mass mail: is a log pushed to recipients unless specified, author not added
if not wizard.notify:
sliced_res_ids = [res_ids[i:i + self._batch_size] for i in range(0, len(res_ids), self._batch_size)]
for res_ids in sliced_res_ids:
all_mail_values = self.get_mail_values(cr, uid, wizard, res_ids, context=context)
for res_id, mail_values in all_mail_values.iteritems():
if wizard.composition_mode == 'mass_mail':
self.pool['mail.mail'].create(cr, uid, mail_values, context=context)
else:
subtype = 'mail.mt_comment'
if context.get('mail_compose_log') or (wizard.composition_mode == 'mass_post' and not wizard.notify): # log a note: subtype is False
subtype = False
context = dict(context,
mail_notify_force_send=False, # do not send emails directly but use the queue instead
mail_create_nosubscribe=True) # add context key to avoid subscribing the author
active_model_pool.message_post(cr, uid, [res_id], type='comment', subtype=subtype, context=context, **mail_values)
if wizard.composition_mode == 'mass_post':
context = dict(context,
mail_notify_force_send=False, # do not send emails directly but use the queue instead
mail_create_nosubscribe=True) # add context key to avoid subscribing the author
active_model_pool.message_post(cr, uid, [res_id], type='comment', subtype=subtype, context=context, **mail_values)
return {'type': 'ir.actions.act_window_close'}
@ -289,6 +245,7 @@ class mail_compose_message(osv.TransientModel):
"""Generate the values that will be used by send_mail to create mail_messages
or mail_mails. """
results = dict.fromkeys(res_ids, False)
rendered_values, default_recipients = {}, {}
mass_mail_mode = wizard.composition_mode == 'mass_mail'
# render all template-based value at once
@ -303,40 +260,46 @@ class mail_compose_message(osv.TransientModel):
'parent_id': wizard.parent_id and wizard.parent_id.id,
'partner_ids': [partner.id for partner in wizard.partner_ids],
'attachment_ids': [attach.id for attach in wizard.attachment_ids],
'author_id': wizard.author_id.id,
'email_from': wizard.email_from,
'record_name': wizard.record_name,
}
# mass mailing: rendering override wizard static values
if mass_mail_mode and wizard.model:
# always keep a copy, reset record name (avoid browsing records)
mail_values.update(notification=True, model=wizard.model, res_id=res_id, record_name=False)
# auto deletion of mail_mail
if 'mail_auto_delete' in context:
mail_values['auto_delete'] = context.get('mail_auto_delete')
# rendered values using template
email_dict = rendered_values[res_id]
mail_values['partner_ids'] += email_dict.pop('partner_ids', [])
mail_values.update(email_dict)
if wizard.same_thread:
mail_values.pop('reply_to')
elif not mail_values.get('reply_to'):
mail_values['reply_to'] = mail_values['email_from']
# mail_mail values: body -> body_html, partner_ids -> recipient_ids
mail_values['body_html'] = mail_values.get('body', '')
mail_values['recipient_ids'] = [(4, id) for id in mail_values.pop('partner_ids', [])]
# process attachments: should not be encoded before being processed by message_post / mail_mail create
attachments = []
if email_dict.get('attachments'):
for name, enc_cont in email_dict.pop('attachments'):
attachments.append((name, base64.b64decode(enc_cont)))
mail_values['attachments'] = attachments
mail_values['attachments'] = [(name, base64.b64decode(enc_cont)) for name, enc_cont in email_dict.pop('attachments', list())]
attachment_ids = []
for attach_id in mail_values.pop('attachment_ids'):
new_attach_id = self.pool.get('ir.attachment').copy(cr, uid, attach_id, {'res_model': self._name, 'res_id': wizard.id}, context=context)
attachment_ids.append(new_attach_id)
mail_values['attachment_ids'] = attachment_ids
# email_from: mass mailing only can specify another email_from
if email_dict.get('email_from'):
mail_values['email_from'] = email_dict.pop('email_from')
# replies redirection: mass mailing only
if wizard.same_thread and wizard.post:
email_dict.pop('reply_to', None)
else:
mail_values['reply_to'] = email_dict.pop('reply_to', None)
mail_values.update(email_dict)
# mass mailing without post: mail_mail values
if mass_mail_mode and not wizard.post:
if 'mail_auto_delete' in context:
mail_values['auto_delete'] = context.get('mail_auto_delete')
mail_values['body_html'] = mail_values.get('body', '')
mail_values['recipient_ids'] = [(4, id) for id in mail_values.pop('partner_ids', [])]
mail_values['attachment_ids'] = self.pool['mail.thread']._message_preprocess_attachments(
cr, uid, mail_values.pop('attachments', []),
attachment_ids, 'mail.message', 0, context=context)
results[res_id] = mail_values
return results
#------------------------------------------------------
# Template rendering
#------------------------------------------------------
def render_message_batch(self, cr, uid, wizard, res_ids, context=None):
"""Generate template-based values of wizard, for the document records given
by res_ids. This method is meant to be inherited by email_template that
@ -346,6 +309,10 @@ class mail_compose_message(osv.TransientModel):
once, and render it multiple times. This is useful for mass mailing where
template rendering represent a significant part of the process.
Default recipients are also computed, based on mail_thread method
message_get_default_recipients. This allows to ensure a mass mailing has
always some recipients specified.
:param browse wizard: current mail.compose.message browse record
:param list res_ids: list of record ids
@ -357,6 +324,9 @@ class mail_compose_message(osv.TransientModel):
emails_from = self.render_template_batch(cr, uid, wizard.email_from, wizard.model, res_ids, context=context)
replies_to = self.render_template_batch(cr, uid, wizard.reply_to, wizard.model, res_ids, context=context)
ctx = dict(context, thread_model=wizard.model)
default_recipients = self.pool['mail.thread'].message_get_default_recipients(cr, uid, res_ids, context=ctx)
results = dict.fromkeys(res_ids, False)
for res_id in res_ids:
results[res_id] = {
@ -365,6 +335,7 @@ class mail_compose_message(osv.TransientModel):
'email_from': emails_from[res_id],
'reply_to': replies_to[res_id],
}
results[res_id].update(default_recipients.get(res_id, dict()))
return results
def render_template_batch(self, cr, uid, template, model, res_ids, context=None, post_process=False):

View File

@ -11,6 +11,7 @@
<field name="composition_mode" invisible="1"/>
<field name="model" invisible="1"/>
<field name="res_id" invisible="1"/>
<field name="is_log" invisible="1"/>
<field name="parent_id" invisible="1"/>
<field name="mail_server_id" invisible="1"/>
<!-- Various warnings -->
@ -28,29 +29,27 @@
<field name="email_from"
attrs="{'invisible':[('composition_mode', '!=', 'mass_mail')]}"/>
<field name="subject" placeholder="Subject..." required="True"/>
<!-- classic message composer -->
<label for="partner_ids" string="Recipients"
attrs="{'invisible':[('composition_mode', '=', 'mass_mail')]}"/>
<div groups="base.group_user"
attrs="{'invisible':[('composition_mode', '=', 'mass_mail')]}">
<span attrs="{'invisible':[('model', '=', False)]}">
Followers of
<field name="record_name" readonly="1" class="oe_inline oe_compose_recipients"/>
and
<!-- recipients -->
<label for="partner_ids" string="Recipients" attrs="{'invisible': [('is_log', '=', True)]}" groups="base.group_user"/>
<div groups="base.group_user" attrs="{'invisible': [('is_log', '=', True)]}">
<span attrs="{'invisible': [('composition_mode', '!=', 'mass_mail')]}">
<strong>Email mass mailing</strong> on
<span attrs="{'invisible': [('use_active_domain', '=', True)]}">the selected records</span>
<span attrs="{'invisible': [('use_active_domain', '=', False)]}">the current search filter</span>.
</span>
<span attrs="{'invisible':[('composition_mode', '!=', 'comment')]}">Followers of the document and</span>
<field name="partner_ids" widget="many2many_tags_email" placeholder="Add contacts to notify..."
context="{'force_email':True, 'show_email':True}"/>
context="{'force_email':True, 'show_email':True}"
attrs="{'invisible': [('composition_mode', '!=', 'comment')]}"/>
</div>
<!-- mass post / mass mailing -->
<field name="post"
attrs="{'invisible':[('composition_mode', '!=', 'mass_mail')]}"/>
<!-- mass post -->
<field name="notify"
attrs="{'invisible':['|', ('post', '!=', True), ('composition_mode', '!=', 'mass_mail')]}"/>
<field name="same_thread"
attrs="{'invisible':['|', ('composition_mode', '!=', 'mass_mail'), ('post', '=', False)]}"/>
<field name="reply_to" placeholder="Email address te redirect replies..."
attrs="{'invisible':['|', '&amp;', ('same_thread', '=', True), ('post', '=', True), ('composition_mode', '!=', 'mass_mail')],
'required':['&amp;', '|', ('post', '=', False), ('same_thread', '=', False), ('composition_mode', '=', 'mass_mail')]}"/>
attrs="{'invisible':['|', ('composition_mode', '!=', 'mass_post')]}"/>
<!-- mass mailing -->
<field name="same_thread" attrs="{'invisible':[('composition_mode', '!=', 'mass_mail')]}"/>
<field name="reply_to" placeholder="Email address to redirect replies..."
attrs="{'invisible':['|', ('same_thread', '=', True), ('composition_mode', '!=', 'mass_mail')],
'required':[('same_thread', '!=', True), ('composition_mode', '=', 'mass_mail')]}"/>
</group>
<field name="body"/>
<field name="attachment_ids" widget="many2many_binary" string="Attach a file"/>

View File

@ -1,8 +1,8 @@
# -*- coding: utf-8 -*-
##############################################################################
#
#
# OpenERP, Open Source Management Solution
# Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
# Copyright (C) 2004-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
@ -15,7 +15,7 @@
# 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/>.
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################

View File

@ -1,29 +1,9 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# OpenERP, Open Source Management Solution
# Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
#
# 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/>.
#
##############################################################################
{
'name': 'Marketing',
'version': '1.1',
'depends': ['base', 'base_setup', 'crm'],
'depends': ['base', 'base_setup'],
'author': 'OpenERP SA',
'category': 'Hidden/Dependency',
'description': """
@ -35,7 +15,6 @@ Contains the installer for marketing-related modules.
'website': 'http://www.openerp.com',
'data': [
'security/marketing_security.xml',
'security/ir.model.access.csv',
'marketing_view.xml',
'res_config_view.xml',
],
@ -44,4 +23,3 @@ Contains the installer for marketing-related modules.
'auto_install': False,
'images': ['images/config_marketing.jpeg'],
}
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:

View File

@ -3,39 +3,12 @@
<data>
<!-- Top menu item -->
<menuitem name="Marketing"
id="base.marketing_menu"
groups="base.group_user"
sequence="85"/>
<menuitem name="Marketing" id="base.marketing_menu" sequence="85"
groups="base.group_user"/>
<record id="view_crm_lead_form" model="ir.ui.view">
<field name="name">crm.lead.inherit.form</field>
<field name="model">crm.lead</field>
<field name="inherit_id" ref="crm.crm_case_form_view_leads"/>
<field name="arch" type="xml">
<xpath expr="//group[@name='categorization']" position="attributes">
<attribute name="string">Marketing</attribute>
<attribute name="groups"></attribute>
</xpath>
<xpath expr="//field[@name='company_id']" position="after">
<field name="type_id"/>
<field name="channel_id" widget="selection"/>
</xpath>
</field>
</record>
<!-- Reporting for Marketing -->
<menuitem name="Marketing" id="base.marketing_reporting_menu" sequence="10"
parent="base.menu_reporting" />
<record id="view_crm_opportunity_form" model="ir.ui.view">
<field name="name">crm.lead.inherit.form</field>
<field name="model">crm.lead</field>
<field name="inherit_id" ref="crm.crm_case_form_view_oppor"/>
<field name="arch" type="xml">
<xpath expr="//group[@name='mailings']" position="before">
<group string="Marketing">
<field name="type_id" />
<field name="channel_id" widget="selection"/>
</group>
</xpath>
</field>
</record>
</data>
</openerp>

View File

@ -1,40 +1,19 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# OpenERP, Open Source Business Applications
# Copyright (C) 2004-2012 OpenERP S.A. (<http://openerp.com>).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
from openerp.osv import fields, osv
class marketing_config_settings(osv.osv_memory):
class marketing_config_settings(osv.TransientModel):
_name = 'marketing.config.settings'
_inherit = 'res.config.settings'
_columns = {
'module_marketing_campaign': fields.boolean('Marketing campaigns',
'module_mass_mailing': fields.boolean(
'Mass Mailing',
help='Provide a way to perform mass mailings.\n'
'-This installs the module mass_mailing.'),
'module_marketing_campaign': fields.boolean(
'Marketing campaigns',
help='Provides leads automation through marketing campaigns. '
'Campaigns can in fact be defined on any resource, not just CRM leads.\n'
'-This installs the module marketing_campaign.'),
'module_marketing_campaign_crm_demo': fields.boolean('Demo data for marketing campaigns',
help='Installs demo data like leads, campaigns and segments for Marketing Campaigns.\n'
'-This installs the module marketing_campaign_crm_demo.'),
'module_crm_profiling': fields.boolean('Track customer profile to focus your campaigns',
help='Allows users to perform segmentation within partners.\n'
'-This installs the module crm_profiling.'),
}
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:

View File

@ -11,24 +11,25 @@
<button string="Apply" type="object" name="execute" class="oe_highlight"/>
or
<button string="Cancel" type="object" name="cancel" class="oe_link"/>
</header>
<separator string="Campaigns"/>
<separator string="Mass Mailing"/>
<group>
<label for="id" string="Campaigns Settings"/>
<label for="id" string="Settings"/>
<div>
<div>
<div name="module_mass_mailing">
<field name="module_mass_mailing" class="oe_inline"/>
<label for="module_mass_mailing"/>
</div>
</div>
</group>
<separator string="Marketing Campaigns"/>
<group>
<label for="id" string="Settings"/>
<div>
<div name="module_marketing_campaign">
<field name="module_marketing_campaign" class="oe_inline"/>
<label for="module_marketing_campaign"/>
</div>
<div attrs="{'invisible':[('module_marketing_campaign','=',False)]}">
<field name="module_marketing_campaign_crm_demo" class="oe_inline"/>
<label for="module_marketing_campaign_crm_demo"/>
</div>
<div>
<field name="module_crm_profiling" class="oe_inline"/>
<label for="module_crm_profiling"/>
</div>
</div>
</group>
</form>

View File

@ -1 +0,0 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink

View File

@ -0,0 +1,22 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# OpenERP, Open Source Management Solution
# Copyright (C) 2004-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 models

View File

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
{
'name': 'Marketing in CRM',
'version': '1.0',
'depends': ['marketing', 'crm'],
'author': 'OpenERP SA',
'category': 'Hidden/Dependency',
'description': """
Bridge module between marketing and CRM
""",
'website': 'http://www.openerp.com',
'data': [
'views/crm.xml',
'views/res_config.xml',
],
'demo': [],
'installable': True,
'auto_install': True,
}

View File

@ -0,0 +1,3 @@
# -*- coding: utf-8 -*-
import res_config

View File

@ -0,0 +1,19 @@
# -*- coding: utf-8 -*-
from openerp.osv import fields, osv
class CrmMarketingConfig(osv.TransientModel):
_name = 'marketing.config.settings'
_inherit = 'marketing.config.settings'
_columns = {
'module_marketing_campaign_crm_demo': fields.boolean(
'Demo data for marketing campaigns',
help='Installs demo data like leads, campaigns and segments for Marketing Campaigns.\n'
'-This installs the module marketing_campaign_crm_demo.'),
'module_crm_profiling': fields.boolean(
'Track customer profile to focus your campaigns',
help='Allows users to perform segmentation within partners.\n'
'-This installs the module crm_profiling.'),
}

View File

@ -0,0 +1,36 @@
<?xml version="1.0"?>
<openerp>
<data>
<record id="view_crm_lead_form" model="ir.ui.view">
<field name="name">crm.lead.inherit.form</field>
<field name="model">crm.lead</field>
<field name="inherit_id" ref="crm.crm_case_form_view_leads"/>
<field name="arch" type="xml">
<xpath expr="//group[@name='categorization']" position="attributes">
<attribute name="string">Marketing</attribute>
<attribute name="groups"></attribute>
</xpath>
<xpath expr="//field[@name='company_id']" position="after">
<field name="type_id"/>
<field name="channel_id" widget="selection"/>
</xpath>
</field>
</record>
<record id="view_crm_opportunity_form" model="ir.ui.view">
<field name="name">crm.lead.inherit.form</field>
<field name="model">crm.lead</field>
<field name="inherit_id" ref="crm.crm_case_form_view_oppor"/>
<field name="arch" type="xml">
<xpath expr="//group[@name='mailings']" position="before">
<group string="Marketing">
<field name="type_id" />
<field name="channel_id" widget="selection"/>
</group>
</xpath>
</field>
</record>
</data>
</openerp>

View File

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data>
<record id="view_marketing_configuration" model="ir.ui.view">
<field name="name">marketing.config.settings.crm</field>
<field name="model">marketing.config.settings</field>
<field name="inherit_id" ref="marketing.view_marketing_configuration"/>
<field name="arch" type="xml">
<xpath expr="//div[@name='module_marketing_campaign']" position="after">
<div attrs="{'invisible':[('module_marketing_campaign','=',False)]}">
<field name="module_marketing_campaign_crm_demo" class="oe_inline"/>
<label for="module_marketing_campaign_crm_demo"/>
</div>
<div>
<field name="module_crm_profiling" class="oe_inline"/>
<label for="module_crm_profiling"/>
</div>
</xpath>
</field>
</record>
</data>
</openerp>

View File

@ -1,26 +1,5 @@
# -*- 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 mass_mailing
import mail_mail
import mail_thread
import models
import wizard
import controllers

View File

@ -21,26 +21,33 @@
{
'name': 'Mass Mailing Campaigns',
'summary': 'Design, send and track emails',
'description': """
Easily send mass mailing to your leads, opportunities or customers. Track
marketing campaigns performance to improve conversion rates. Design
professional emails and reuse templates in a few clicks.
""",
'version': '1.0',
'version': '2.0',
'author': 'OpenERP',
'website': 'http://www.openerp.com',
'category': 'Marketing',
'depends': [
'mail',
'email_template',
'marketing',
'web_kanban_gauge',
'web_kanban_sparkline',
'website_mail',
],
'data': [
'mail_data.xml',
'data/mail_data.xml',
'data/mass_mailing_data.xml',
'wizard/mail_compose_message_view.xml',
'wizard/mail_mass_mailing_create_segment.xml',
'mass_mailing_view.xml',
'wizard/test_mailing.xml',
'views/mass_mailing.xml',
'views/res_config.xml',
'views/res_partner.xml',
'views/email_template.xml',
'security/ir.model.access.csv',
],
'js': [
@ -48,10 +55,11 @@ professional emails and reuse templates in a few clicks.
],
'qweb': [],
'css': [
'static/src/css/mass_mailing.css'
'static/src/css/mass_mailing.css',
'static/src/css/email_template.css'
],
'demo': [
'mass_mailing_demo.xml',
'data/mass_mailing_demo.xml',
],
'installable': True,
'auto_install': False,

View File

@ -1,11 +1,42 @@
import werkzeug
from openerp import http, SUPERUSER_ID
from openerp.http import request
class MassMailController(http.Controller):
@http.route('/mail/track/<int:mail_id>/blank.gif', type='http', auth='none')
def track_mail_open(self, mail_id):
def track_mail_open(self, mail_id, **post):
""" Email tracking. """
mail_mail_stats = request.registry.get('mail.mail.statistics')
mail_mail_stats.set_opened(request.cr, SUPERUSER_ID, mail_mail_ids=[mail_id])
return "data:image/gif;base64,R0lGODlhAQABAIAAANvf7wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw=="
response = werkzeug.wrappers.Response()
response.mimetype = 'image/gif'
response.set_data('R0lGODlhAQABAIAAANvf7wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw=='.decode('base64'))
return response
@http.route(['/mail/mailing/<int:mailing_id>/unsubscribe'], type='http', auth='none')
def mailing(self, mailing_id, email=None, res_id=None, **post):
cr, uid, context = request.cr, request.uid, request.context
MassMailing = request.registry['mail.mass_mailing']
mailing_ids = MassMailing.exists(cr, SUPERUSER_ID, [mailing_id], context=context)
if not mailing_ids:
return 'KO'
mailing = MassMailing.browse(cr, SUPERUSER_ID, mailing_ids[0], context=context)
if mailing.mailing_model == 'mail.mass_mailing.contact':
list_ids = [l.id for l in mailing.contact_list_ids]
record_ids = request.registry[mailing.mailing_model].search(cr, SUPERUSER_ID, [('list_id', 'in', list_ids), ('id', '=', res_id), ('email', 'ilike', email)], context=context)
request.registry[mailing.mailing_model].write(cr, SUPERUSER_ID, record_ids, {'opt_out': True}, context=context)
else:
email_fname = None
if 'email_from' in request.registry[mailing.mailing_model]._all_columns:
email_fname = 'email_from'
elif 'email' in request.registry[mailing.mailing_model]._all_columns:
email_fname = 'email'
if email_fname:
record_ids = request.registry[mailing.mailing_model].search(cr, SUPERUSER_ID, [('id', '=', res_id), (email_fname, 'ilike', email)], context=context)
if 'opt_out' in request.registry[mailing.mailing_model]._all_columns:
request.registry[mailing.mailing_model].write(cr, SUPERUSER_ID, record_ids, {'opt_out': True}, context=context)
return 'OK'

View File

@ -0,0 +1,37 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data noupdate="1">
<!-- After installation of the module, open the related menu -->
<record id="action_client_marketing_menu" model="ir.actions.client">
<field name="name">Open Marketing Menu</field>
<field name="tag">reload</field>
<field name="params" eval="{'menu_id': ref('base.marketing_menu')}"/>
</record>
<record id="base.open_menu" model="ir.actions.todo">
<field name="action_id" ref="action_client_marketing_menu"/>
<field name="state">open</field>
</record>
<!-- Group to manage campaigns -->
<record id="group_mass_mailing_campaign" model="res.groups">
<field name="name">Manage Mass Mailing Campaigns</field>
<field name="category_id" ref="base.module_category_hidden"/>
</record>
<!-- Default stages of mass mailing campaigns -->
<record id="campaign_stage_1" model="mail.mass_mailing.stage">
<field name="name">Schedule</field>
<field name="sequence">10</field>
</record>
<record id="campaign_stage_2" model="mail.mass_mailing.stage">
<field name="name">Design</field>
<field name="sequence">20</field>
</record>
<record id="campaign_stage_3" model="mail.mass_mailing.stage">
<field name="name">Sent</field>
<field name="sequence">30</field>
</record>
</data>
</openerp>

View File

@ -0,0 +1,146 @@
<?xml version="1.0"?>
<openerp>
<data noupdate="1">
<record id="mass_mail_attach_1" model="ir.attachment">
<field name="datas">bWlncmF0aW9uIHRlc3Q=</field>
<field name="datas_fname">SampleDoc.doc</field>
<field name="name">SampleDoc.doc</field>
</record>
<!-- Create mailing lists -->
<record id="mass_mail_list_1" model="mail.mass_mailing.list">
<field name="name">Imported Contacts</field>
</record>
<!-- Create Contacts -->
<record id="mass_mail_contact_1" model="mail.mass_mailing.contact">
<field name="name">Aristide Antario</field>
<field name="email">aa@example.com</field>
<field name="list_id" ref="mass_mailing.mass_mail_list_1"/>
</record>
<record id="mass_mail_contact_2" model="mail.mass_mailing.contact">
<field name="name">Beverly Bridge</field>
<field name="email">bb@example.com</field>
<field name="list_id" ref="mass_mailing.mass_mail_list_1"/>
</record>
<record id="mass_mail_contact_3" model="mail.mass_mailing.contact">
<field name="name">Carol Cartridge</field>
<field name="email">cc@example.com</field>
<field name="list_id" ref="mass_mailing.mass_mail_list_1"/>
<field name="opt_out" eval="True"/>
</record>
<!-- Create campaign and mailings -->
<record id="mass_mail_category_1" model="mail.mass_mailing.category">
<field name="name">Marketing</field>
</record>
<record id="mass_mail_campaign_1" model="mail.mass_mailing.campaign">
<field name="name">Newsletter</field>
<field name="stage_id" ref="mass_mailing.campaign_stage_1"/>
<field name="user_id" eval="ref('base.user_root')"/>
<field name="category_ids" eval="[(6,0,[ref('mass_mailing.mass_mail_category_1')])]"/>
</record>
<record id="mass_mail_1" model="mail.mass_mailing">
<field name="name">First Newsletter</field>
<field name="state">done</field>
<field name="sent_date" eval="(DateTime.today() - relativedelta(days=5)).strftime('%Y-%m-%d %H:%M:%S')"/>
<field name="mass_mailing_campaign_id" eval="ref('mass_mail_campaign_1')"/>
<field name="mailing_model">res.partner</field>
<field name="mailing_domain">[('customer', '=', True)]</field>
<field name="reply_to_mode">email</field>
<field name="reply_to"><![CDATA[Info <info@yourcompany.example.com>]]></field>
<field name="body_html"><![CDATA[<div data-snippet-id="big-picture" style="padding:0px; margin:0px">
<table cellpadding="0" cellspacing="0" style="margin:10px 0px 0px;vertical-align:top;padding:0px;font-family:arial;font-size:12px;color:rgb(51,51,51)">
<tbody>
<tr>
<td style="width:600px" valign="top">
<h2 style="text-align: center; padding:0px 5px">A Punchy Headline</h2>
</td>
</tr>
<tr>
<td style="width:600px" valign="top"><img src="/website/static/src/img/big_picture.png" style="display:block;border:none;min-height:250px;margin:0 auto;" width="500"></td>
</tr>
<tr>
<td style="width:600px" valign="top">
<p style="text-align: center; overflow:hidden"></p>
<h3 style="text-align: center; padding:0px 5px">A Small Subtitle for ${object.name}</h3>
<p></p>
<p style="text-align: center; overflow:hidden">Choose a vibrant image and write an inspiring paragraph about it. It does not have to be long, but it should reinforce your image.</p>
</td>
</tr>
</tbody>
</table>
</div>
<div data-snippet-id="three-columns" style="padding:0px; margin:0px">
<table cellpadding="0" cellspacing="0" style="margin:10px 0px 0px;vertical-align:top;padding:0px;font-family:arial;font-size:12px;color:rgb(51,51,51)">
<tbody>
<tr>
<td style="width:300px" valign="top"><img src="/website/static/src/img/desert_thumb.jpg" style="display:block;border:none;min-height:50px" width="275"></td>
<td style="width:300px" valign="top"><img src="/website/static/src/img/deers_thumb.jpg" style="display:block;border:none;min-height:50px" width="275"></td>
</tr>
<tr>
<td style="width:300px" valign="top">
<h3 style="text-align: center; padding:0px 5px">Feature One</h3>
<p style="overflow:hidden">Choose a vibrant image and write an inspiring paragraph about it. It does not have to be long, but it should reinforce your image.</p>
</td>
<td style="width:300px" valign="top">
<h3 style="text-align: center; padding:0px 5px">Feature Two</h3>
<p style="overflow:hidden">Choose a vibrant image and write an inspiring paragraph about it. It does not have to be long, but it should reinforce your image.</p>
</td>
</tr>
</tbody>
</table>
</div>]]></field>
<field name="attachment_ids" eval="[(4, ref('mass_mail_attach_1'))]"/>
</record>
<record id="mass_mail_2" model="mail.mass_mailing">
<field name="name">Second Newsletter</field>
<field name="state">test</field>
<field name="mass_mailing_campaign_id" eval="ref('mass_mail_campaign_1')"/>
<field name="mailing_model">res.partner</field>
<field name="mailing_domain">[('customer', '=', True)]</field>
<field name="reply_to_mode">email</field>
<field name="reply_to"><![CDATA[Info <info@yourcompany.example.com>]]></field>
</record>
<record id="mass_mail_email_1" model="mail.mail.statistics">
<field name="mass_mailing_id" eval="ref('mass_mail_1')"/>
<field name="message_id">1111000@OpenERP.com</field>
<field name="sent" eval="(DateTime.today() - relativedelta(days=5)).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')"/>
</record>
<record id="mass_mail_email_2" model="mail.mail.statistics">
<field name="mass_mailing_id" eval="ref('mass_mail_1')"/>
<field name="message_id">1111001@OpenERP.com</field>
<field name="sent" eval="(DateTime.today() - relativedelta(days=5)).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')"/>
</record>
<record id="mass_mail_email_3" model="mail.mail.statistics">
<field name="mass_mailing_id" eval="ref('mass_mail_1')"/>
<field name="message_id">1111002@OpenERP.com</field>
<field name="sent" eval="(DateTime.today() - relativedelta(days=5)).strftime('%Y-%m-%d %H:%M:%S')"/>
<field name="opened" eval="(DateTime.today() - relativedelta(days=1)).strftime('%Y-%m-%d %H:%M:%S')"/>
</record>
<record id="mass_mail_email_4" model="mail.mail.statistics">
<field name="mass_mailing_id" eval="ref('mass_mail_1')"/>
<field name="message_id">1111003@OpenERP.com</field>
<field name="exception" eval="(DateTime.today() - relativedelta(days=5)).strftime('%Y-%m-%d %H:%M:%S')"/>
</record>
<record id="mass_mail_email_5" model="mail.mail.statistics">
<field name="mass_mailing_id" eval="ref('mass_mail_1')"/>
<field name="message_id">1111004@OpenERP.com</field>
<field name="sent" eval="(DateTime.today() - relativedelta(days=5)).strftime('%Y-%m-%d %H:%M:%S')"/>
<field name="bounced" eval="(DateTime.today() - relativedelta(days=3)).strftime('%Y-%m-%d %H:%M:%S')"/>
</record>
</data>
</openerp>

View File

@ -1,369 +0,0 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# OpenERP, Open Source Management Solution
# Copyright (C) 2013-today OpenERP SA (<http://www.openerp.com>)
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>
#
##############################################################################
from datetime import datetime
from dateutil import relativedelta
from openerp import tools
from openerp.tools.translate import _
from openerp.osv import osv, fields
class MassMailingCampaign(osv.Model):
"""Model of mass mailing campaigns.
"""
_name = "mail.mass_mailing.campaign"
_description = 'Mass Mailing Campaign'
# number of embedded mailings in kanban view
_kanban_mailing_nbr = 4
def _get_statistics(self, cr, uid, ids, name, arg, context=None):
""" Compute statistics of the mass mailing campaign """
results = dict.fromkeys(ids, False)
for campaign in self.browse(cr, uid, ids, context=context):
results[campaign.id] = {
'sent': len(campaign.statistics_ids),
# delivered: shouldn't be: all mails - (failed + bounced) ?
'delivered': len([stat for stat in campaign.statistics_ids if not stat.bounced]), # stat.state == 'sent' and
'opened': len([stat for stat in campaign.statistics_ids if stat.opened]),
'replied': len([stat for stat in campaign.statistics_ids if stat.replied]),
'bounced': len([stat for stat in campaign.statistics_ids if stat.bounced]),
}
return results
def _get_mass_mailing_kanban_ids(self, cr, uid, ids, name, arg, context=None):
""" Gather data about mass mailings to display them in kanban view as
nested kanban views is not possible currently. """
results = dict.fromkeys(ids, '')
for campaign in self.browse(cr, uid, ids, context=context):
mass_mailing_results = []
for mass_mailing in campaign.mass_mailing_ids[:self._kanban_mailing_nbr]:
mass_mailing_object = {}
for attr in ['name', 'sent', 'delivered', 'opened', 'replied', 'bounced']:
mass_mailing_object[attr] = getattr(mass_mailing, attr)
mass_mailing_results.append(mass_mailing_object)
results[campaign.id] = mass_mailing_results
return results
_columns = {
'name': fields.char(
'Campaign Name', required=True,
),
'user_id': fields.many2one(
'res.users', 'Responsible',
required=True,
),
'mass_mailing_ids': fields.one2many(
'mail.mass_mailing', 'mass_mailing_campaign_id',
'Mass Mailings',
),
'mass_mailing_kanban_ids': fields.function(
_get_mass_mailing_kanban_ids,
type='text', string='Mass Mailings (kanban data)',
help='This field has for purpose to gather data about mass mailings '
'to display them in kanban view as nested kanban views is not '
'possible currently',
),
'statistics_ids': fields.one2many(
'mail.mail.statistics', 'mass_mailing_campaign_id',
'Sent Emails',
),
'color': fields.integer('Color Index'),
# stat fields
'sent': fields.function(
_get_statistics,
string='Sent Emails',
type='integer', multi='_get_statistics'
),
'delivered': fields.function(
_get_statistics,
string='Delivered',
type='integer', multi='_get_statistics',
),
'opened': fields.function(
_get_statistics,
string='Opened',
type='integer', multi='_get_statistics',
),
'replied': fields.function(
_get_statistics,
string='Replied',
type='integer', multi='_get_statistics'
),
'bounced': fields.function(
_get_statistics,
string='Bounced',
type='integer', multi='_get_statistics'
),
}
_defaults = {
'user_id': lambda self, cr, uid, ctx=None: uid,
}
def launch_mass_mailing_create_wizard(self, cr, uid, ids, context=None):
ctx = dict(context)
ctx.update({
'default_mass_mailing_campaign_id': ids[0],
})
return {
'name': _('Create a Mass Mailing for the Campaign'),
'type': 'ir.actions.act_window',
'view_type': 'form',
'view_mode': 'form',
'res_model': 'mail.mass_mailing.create',
'views': [(False, 'form')],
'view_id': False,
'target': 'new',
'context': ctx,
}
class MassMailing(osv.Model):
""" MassMailing models a wave of emails for a mass mailign campaign.
A mass mailing is an occurence of sending emails. """
_name = 'mail.mass_mailing'
_description = 'Wave of sending emails'
# number of periods for tracking mail_mail statistics
_period_number = 6
_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_monthly'] = 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_monthly'] = 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 """
results = dict.fromkeys(ids, False)
for mass_mailing in self.browse(cr, uid, ids, context=context):
results[mass_mailing.id] = {
'sent': len(mass_mailing.statistics_ids),
'delivered': len([stat for stat in mass_mailing.statistics_ids if not stat.bounced]), # mail.state == 'sent' and
'opened': len([stat for stat in mass_mailing.statistics_ids if stat.opened]),
'replied': len([stat for stat in mass_mailing.statistics_ids if stat.replied]),
'bounced': len([stat for stat in mass_mailing.statistics_ids if stat.bounced]),
}
return results
_columns = {
'name': fields.char('Name', required=True),
'mass_mailing_campaign_id': fields.many2one(
'mail.mass_mailing.campaign', 'Mass Mailing Campaign',
ondelete='cascade', required=True,
),
'template_id': fields.many2one(
'email.template', 'Email Template',
ondelete='set null',
),
'domain': fields.char('Domain'),
'date': fields.datetime('Date'),
'color': fields.related(
'mass_mailing_campaign_id', 'color',
type='integer', string='Color Index',
),
# statistics data
'statistics_ids': fields.one2many(
'mail.mail.statistics', 'mass_mailing_id',
'Emails 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='Bounce',
type='integer', multi='_get_statistics'
),
# monthly ratio
'opened_monthly': fields.function(
_get_daily_statistics,
string='Opened',
type='char', multi='_get_daily_statistics',
),
'replied_monthly': fields.function(
_get_daily_statistics,
string='Replied',
type='char', multi='_get_daily_statistics',
),
}
_defaults = {
'date': fields.datetime.now,
}
class MailMailStats(osv.Model):
""" MailMailStats models the statistics collected about emails. Those statistics
are stored in a separated model and table to avoid bloating the mail_mail table
with statistics values. This also allows to delete emails send with mass mailing
without loosing the statistics about them. """
_name = 'mail.mail.statistics'
_description = 'Email Statistics'
_rec_name = 'message_id'
_order = 'message_id'
_columns = {
'mail_mail_id': fields.integer(
'Mail ID',
help='ID of the related mail_mail. This field is an integer field because'
'the related mail_mail can be deleted separately from its statistics.'
),
'message_id': fields.char(
'Message-ID',
),
'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
'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 ids
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 ids
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 ids

View File

@ -1,82 +0,0 @@
<?xml version="1.0"?>
<openerp>
<!-- <data noupdate="1"> -->
<data>
<record id="mass_mail_template_1" model="email.template">
<field name="name">Partner Newsletter 1</field>
<field name="model_id" ref="base.model_res_partner"/>
<field name="auto_delete" eval="False"/>
<field name="partner_to">${object.id}</field>
<field name="body_html"><![CDATA[<p>Hello</p>]]></field>
</record>
<record id="mass_mail_template_2" model="email.template">
<field name="name">Partner Newsletter 2</field>
<field name="model_id" ref="base.model_res_partner"/>
<field name="auto_delete" eval="False"/>
<field name="partner_to">${object.id}</field>
<field name="body_html"><![CDATA[<p>Hello</p>]]></field>
</record>
<record id="mass_mail_campaign_1" model="mail.mass_mailing.campaign">
<field name="name">Partners Newsletter</field>
<field name="user_id" eval="ref('base.user_root')"/>
</record>
<record id="mass_mail_1" model="mail.mass_mailing">
<field name="name">First Newsletter</field>
<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="mass_mailing_campaign_id" eval="ref('mass_mail_campaign_1')"/>
</record>
<record id="mass_mail_2" model="mail.mass_mailing">
<field name="name">Second Newsletter</field>
<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="mass_mailing_campaign_id" eval="ref('mass_mail_campaign_1')"/>
</record>
<record id="mass_mail_email_1" model="mail.mail.statistics">
<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="replied" eval="(DateTime.today() - relativedelta(days=1)).strftime('%Y-%m-%d %H:%M:%S')"/>
</record>
<record id="mass_mail_email_2" model="mail.mail.statistics">
<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="replied" eval="(DateTime.today() - relativedelta(days=0)).strftime('%Y-%m-%d %H:%M:%S')"/>
</record>
<record id="mass_mail_email_3" model="mail.mail.statistics">
<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')"/>
</record>
<record id="mass_mail_email_4" model="mail.mail.statistics">
<field name="mass_mailing_id" eval="ref('mass_mail_1')"/>
<field name="message_id">1111003@OpenERP.com</field>
</record>
<record id="mass_mail_email_5" model="mail.mail.statistics">
<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')"/>
</record>
<record id="mass_mail_email_2_1" model="mail.mail.statistics">
<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')"/>
</record>
<record id="mass_mail_email_2_2" model="mail.mail.statistics">
<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')"/>
</record>
<record id="mass_mail_email_2_3" model="mail.mail.statistics">
<field name="mass_mailing_id" eval="ref('mass_mail_2')"/>
<field name="message_id">1111007@OpenERP.com</field>
</record>
</data>
</openerp>

View File

@ -1,379 +0,0 @@
<?xml version="1.0"?>
<openerp>
<data>
<!-- MASS MAILING !-->
<record model="ir.ui.view" id="view_mail_mass_mailing_search">
<field name="name">mail.mass_mailing.search</field>
<field name="model">mail.mass_mailing</field>
<field name="arch" type="xml">
<search string="Mass Mailings">
<field name="name" string="Mailings"/>
<field name="mass_mailing_campaign_id"/>
<field name="template_id"/>
<group expand="0" string="Group By...">
<filter string="Campaign" name="group_mass_mailing_campaign_id"
context="{'group_by': 'mass_mailing_campaign_id'}"/>
<filter string="Template" name="group_template_id"
context="{'group_by': 'template_id'}"/>
</group>
</search>
</field>
</record>
<record model="ir.ui.view" id="view_mail_mass_mailing_tree">
<field name="name">mail.mass_mailing.tree</field>
<field name="model">mail.mass_mailing</field>
<field name="priority">10</field>
<field name="arch" type="xml">
<tree string="Mass Mailings">
<field name="name"/>
<field name="sent"/>
<field name="delivered"/>
<field name="opened"/>
<field name="replied"/>
<field name="mass_mailing_campaign_id" invisible="1"/>
<field name="template_id" invisible="1"/>
</tree>
</field>
</record>
<record model="ir.ui.view" id="view_mail_mass_mailing_form">
<field name="name">mail.mass_mailing.form</field>
<field name="model">mail.mass_mailing</field>
<field name="arch" type="xml">
<form string="Mass Mailing" version="7.0">
<sheet>
<group>
<group>
<field name="name"/>
<field name="mass_mailing_campaign_id" readonly="True"/>
</group>
<group>
<field name="template_id"/>
<field name="domain"/>
<field name="date"/>
</group>
</group>
<group string="Email Statistics">
<field name="statistics_ids" nolabel="1" colspan="2"/>
<group>
<field name="sent"/>
<field name="opened"/>
<field name="bounced"/>
</group>
<group>
<field name="delivered"/>
<field name="replied"/>
</group>
</group>
</sheet>
</form>
</field>
</record>
<record model="ir.ui.view" id="view_mail_mass_mailing_kanban">
<field name="name">mail.mass_mailing.kanban</field>
<field name="model">mail.mass_mailing</field>
<field name="arch" type="xml">
<kanban>
<field name='color'/>
<templates>
<t t-name="kanban-box">
<div t-attf-class="oe_kanban_color_#{kanban_getcolor(record.color.raw_value)} oe_kanban_card oe_kanban_global_click oe_kanban_mass_mailing oe_kanban_mass_mailing_segment">
<div class="oe_kanban_content">
<div>
<h3>
<field name="name"/>
</h3>
<p style="margin-left: 10px; margin-top: 8px;">
Sent: <field name="date"/><br />
Campaign: <field name="mass_mailing_campaign_id"/>
</p>
</div>
<div>
<p class="oe_mail_stats">
<span class="oe_mail_result"><field name="sent"/></span><br />
Sent
</p>
<p class="oe_mail_stats">
<span class="oe_mail_result"><field name="delivered"/></span><br />
Delivered
</p>
<p class="oe_mail_stats">
<span class="oe_mail_result"><field name="opened"/></span><br />
Opened
</p>
<p class="oe_mail_stats">
<span class="oe_mail_result"><field name="replied"/></span><br />
Replied
</p>
</div>
<div>
<div class="oe_sparkline_container">
<h4 class="oe_sparkline_bar_title">Opened</h4><br />
<field name="opened_monthly" widget="sparkline_bar" options="{'height': '50px', 'barWidth': 10, 'barSpacing': 5}"/>
</div>
<div class="oe_sparkline_container">
<h4 class="oe_sparkline_bar_title">Replied</h4><br />
<field name="replied_monthly" widget="sparkline_bar" options="{'height': '50px', 'barWidth': 10, 'barSpacing': 5}"/>
</div>
</div>
</div>
<div class="oe_clear"></div>
</div>
</t>
</templates>
</kanban>
</field>
</record>
<record id="action_view_mass_mailings" model="ir.actions.act_window">
<field name="name">Mass Mailings</field>
<field name="res_model">mail.mass_mailing</field>
<field name="view_type">form</field>
<field name="view_mode">kanban,tree,form</field>
</record>
<record id="action_view_mass_mailings_from_campaign" model="ir.actions.act_window">
<field name="name">Mass Mailings</field>
<field name="res_model">mail.mass_mailing</field>
<field name="view_type">form</field>
<field name="view_mode">kanban,tree,form</field>
<field name="context">{
'search_default_mass_mailing_campaign_id': [active_id],
'default_mass_mailing_campaign_id': active_id,
}
</field>
</record>
<!-- MASS MAILING CAMPAIGNS !-->
<record model="ir.ui.view" id="view_mail_mass_mailing_campaign_search">
<field name="name">mail.mass_mailing.campaign.search</field>
<field name="model">mail.mass_mailing.campaign</field>
<field name="arch" type="xml">
<search string="Mass Mailing Campaigns">
<field name="name" string="Campaigns"/>
<field name="user_id"/>
<group expand="0" string="Group By...">
<filter string="Responsibles" name="group_user_id"
context="{'group_by': 'user_id'}"/>
</group>
</search>
</field>
</record>
<record model="ir.ui.view" id="view_mail_mass_mailing_campaign_tree">
<field name="name">mail.mass_mailing.campaign.tree</field>
<field name="model">mail.mass_mailing.campaign</field>
<field name="priority">10</field>
<field name="arch" type="xml">
<tree string="Mass Mailing Campaigns">
<field name="name"/>
<field name="user_id"/>
</tree>
</field>
</record>
<record model="ir.ui.view" id="view_mail_mass_mailing_campaign_form">
<field name="name">mail.mass_mailing.campaign.form</field>
<field name="model">mail.mass_mailing.campaign</field>
<field name="arch" type="xml">
<form string="Mass Mailing Campaign" version="7.0">
<header>
<button name="launch_mass_mailing_create_wizard" type="object"
class="oe_highlight" string="Create a New Mailing"/>
</header>
<sheet>
<group>
<field name="name"/>
<field name="user_id"/>
</group>
<group>
<group>
<field name="sent"/>
<field name="opened"/>
<field name="bounced"/>
</group>
<group>
<field name="delivered"/>
<field name="replied"/>
</group>
</group>
<group>
<field name="mass_mailing_ids" readonly="1"/>
</group>
</sheet>
</form>
</field>
</record>
<record model="ir.ui.view" id="view_mail_mass_mailing_campaign_kanban">
<field name="name">mail.mass_mailing.campaign.kanban</field>
<field name="model">mail.mass_mailing.campaign</field>
<field name="arch" type="xml">
<kanban>
<field name="mass_mailing_kanban_ids"/>
<field name='sent'/>
<field name='color'/>
<templates>
<t t-name="mass_mailing.mass_mailing">
<div class="oe_mass_mailings">
<div>
<a name="%(action_view_mass_mailings_from_campaign)d" type="action">
<h4><t t-raw="mass_mailing.name"/></h4>
</a>
</div>
<div>
<p class="oe_mail_stats">
<span class="oe_mail_result"><t t-raw="mass_mailing.sent"/></span><br />
Sent
</p>
<p class="oe_mail_stats">
<span class="oe_mail_result"><t t-raw="mass_mailing.delivered"/></span><br />
Delivered
</p>
<p class="oe_mail_stats">
<span class="oe_mail_result"><t t-raw="mass_mailing.opened"/></span><br />
Opened
</p>
<p class="oe_mail_stats">
<span class="oe_mail_result"><t t-raw="mass_mailing.replied"/></span><br />
Replied
</p>
</div>
</div>
</t>
<t t-name="kanban-box">
<div t-attf-class="oe_kanban_color_#{kanban_getcolor(record.color.raw_value)} oe_kanban_card oe_kanban_global_click oe_kanban_mass_mailing oe_kanban_mass_mailing_campaign">
<div class="oe_dropdown_toggle oe_dropdown_kanban">
<span class="oe_e">i</span>
<ul class="oe_dropdown_menu">
<t t-if="widget.view.is_action_enabled('edit')">
<li><a type="edit">Settings</a></li>
</t>
<t t-if="widget.view.is_action_enabled('edit')">
<li><a name="%(action_mail_mass_mailing_create)d" type="action">New Wave</a></li>
</t>
<t t-if="widget.view.is_action_enabled('delete')">
<li><a type="delete">Delete</a></li>
</t>
<li><ul class="oe_kanban_colorpicker" data-field="color"/></li>
</ul>
</div>
<div class="oe_kanban_content">
<h3>
<field name="name"/>
</h3>
<div>
<field name="delivered" widget="gauge" style="width:160px; height: 120px;"
options="{'max_field': 'sent'}"/>
<field name="opened" widget="gauge" style="width:160px; height: 120px;"
options="{'max_field': 'sent'}"/>
<field name="replied" widget="gauge" style="width:160px; height: 120px;"
options="{'max_field': 'sent'}"/>
</div>
<t t-foreach='record.mass_mailing_kanban_ids.value' t-as='mass_mailing'>
<t t-call="mass_mailing.mass_mailing"/>
</t>
</div>
<div class="oe_clear"></div>
</div>
</t>
</templates>
</kanban>
</field>
</record>
<record id="action_view_mass_mailing_campaigns" model="ir.actions.act_window">
<field name="name">Mass Mailing Campaigns</field>
<field name="res_model">mail.mass_mailing.campaign</field>
<field name="view_type">form</field>
<field name="view_mode">kanban,tree,form</field>
<field name="help" type="html">
<p class="oe_view_nocontent_create">
Click to define a new mass mailing campaign.
</p><p>
Create a campaign to structure mass mailing and get analysis from email status.
</p>
</field>
</record>
<!-- MAIL MAIL STATISTICS !-->
<record model="ir.ui.view" id="view_mail_mail_statistics_search">
<field name="name">mail.mail.statistics.search</field>
<field name="model">mail.mail.statistics</field>
<field name="arch" type="xml">
<search string="Mail Statistics">
<field name="mail_mail_id"/>
<field name="message_id"/>
</search>
</field>
</record>
<record model="ir.ui.view" id="view_mail_mail_statistics_tree">
<field name="name">mail.mail.statistics.tree</field>
<field name="model">mail.mail.statistics</field>
<field name="arch" type="xml">
<tree string="Mail Statistics">
<field name="mail_mail_id"/>
<field name="message_id"/>
<field name="opened"/>
<field name="replied"/>
<field name="bounced"/>
</tree>
</field>
</record>
<record model="ir.ui.view" id="view_mail_mail_statistics_form">
<field name="name">mail.mail.statistics.form</field>
<field name="model">mail.mail.statistics</field>
<field name="arch" type="xml">
<form string="Mail Statistics" version="7.0">
<group>
<group>
<field name="mail_mail_id"/>
<field name="message_id"/>
<field name="opened"/>
<field name="replied"/>
<field name="bounced"/>
</group>
<group>
<field name="mass_mailing_id"/>
<field name="mass_mailing_campaign_id"/>
<field name="template_id"/>
<field name="model"/>
<field name="res_id"/>
</group>
</group>
</form>
</field>
</record>
<record id="action_view_mail_mail_statistics" model="ir.actions.act_window">
<field name="name">Mail Statistics</field>
<field name="res_model">mail.mail.statistics</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form</field>
</record>
<!-- Top menu item -->
<menuitem name="Marketing" id="base.marketing_menu" sequence="85" groups="base.group_user"/>
<!-- Add in marketing -->
<menuitem name="Mass Mailing" id="mass_mailing_campaign"
parent="base.marketing_menu" sequence="1"/>
<menuitem name="Campaigns" id="menu_email_campaigns"
parent="mass_mailing_campaign" sequence="1"
action="action_view_mass_mailing_campaigns"/>
<menuitem name="Mass Mailings" id="menu_email_mass_mailings"
parent="mass_mailing_campaign" sequence="2"
action="action_view_mass_mailings"/>
<!-- Add in Technical/Email -->
<menuitem name="Mail Statistics" id="menu_email_statistics"
parent="base.menu_email" sequence="50"
action="action_view_mail_mail_statistics"/>
</data>
</openerp>

View File

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

View File

@ -19,7 +19,8 @@
#
##############################################################################
from urlparse import urljoin
import urllib
import urlparse
from openerp import tools
from openerp import SUPERUSER_ID
@ -32,6 +33,7 @@ class MailMail(osv.Model):
_inherit = ['mail.mail']
_columns = {
'mailing_id': fields.many2one('mail.mass_mailing', 'Mass Mailing'),
'statistics_ids': fields.one2many(
'mail.mail.statistics', 'mail_mail_id',
string='Statistics',
@ -50,9 +52,24 @@ class MailMail(osv.Model):
def _get_tracking_url(self, cr, uid, mail, partner=None, context=None):
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)
track_url = urlparse.urljoin(
base_url, 'mail/track/%(mail_id)s/blank.gif?%(params)s' % {
'mail_id': mail.id,
'params': urllib.urlencode({'db': cr.dbname})
}
)
return '<img src="%s" alt=""/>' % track_url
def _get_unsubscribe_url(self, cr, uid, mail, email_to, msg=None, context=None):
base_url = self.pool.get('ir.config_parameter').get_param(cr, uid, 'web.base.url')
url = urlparse.urljoin(
base_url, 'mail/mailing/%(mailing_id)s/unsubscribe?%(params)s' % {
'mailing_id': mail.mailing_id.id,
'params': urllib.urlencode({'db': cr.dbname, 'res_id': mail.res_id, 'email': email_to})
}
)
return '<small><a href="%s">%s</a></small>' % (url, msg or 'Click to unsubscribe')
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)
@ -63,3 +80,19 @@ class MailMail(osv.Model):
if tracking_url:
body = tools.append_content_to_html(body, tracking_url, plaintext=False, container_tag='div')
return body
def send_get_email_dict(self, cr, uid, mail, partner=None, context=None):
res = super(MailMail, self).send_get_email_dict(cr, uid, mail, partner, context=context)
if mail.mailing_id and res.get('body') and res.get('email_to'):
email_to = tools.email_split(res.get('email_to')[0])
unsubscribe_url = self._get_unsubscribe_url(cr, uid, mail, email_to, context=context)
if unsubscribe_url:
res['body'] = tools.append_content_to_html(res['body'], unsubscribe_url, plaintext=False, container_tag='p')
return res
def _postprocess_sent_message(self, cr, uid, mail, context=None, mail_sent=True):
if mail_sent is True and mail.statistics_ids:
self.pool['mail.mail.statistics'].write(cr, uid, [s.id for s in mail.statistics_ids], {'sent': fields.datetime.now()}, context=context)
elif mail_sent is False and mail.statistics_ids:
self.pool['mail.mail.statistics'].write(cr, uid, [s.id for s in mail.statistics_ids], {'exception': fields.datetime.now()}, context=context)
return super(MailMail, self)._postprocess_sent_message(cr, uid, mail, context=context, mail_sent=mail_sent)

View File

@ -0,0 +1,571 @@
# -*- coding: utf-8 -*-
from datetime import datetime
from dateutil import relativedelta
import json
import random
import urllib
import urlparse
from openerp import tools
from openerp.tools.safe_eval import safe_eval as eval
from openerp.tools.translate import _
from openerp.osv import osv, fields
class MassMailingCategory(osv.Model):
"""Model of categories of mass mailing, i.e. marketing, newsletter, ... """
_name = 'mail.mass_mailing.category'
_description = 'Mass Mailing Category'
_order = 'name'
_columns = {
'name': fields.char('Name', required=True),
}
class MassMailingContact(osv.Model):
"""Model of a contact. This model is different from the partner model
because it holds only some basic information: name, email. The purpose is to
be able to deal with large contact list to email without bloating the partner
base."""
_name = 'mail.mass_mailing.contact'
_description = 'Mass Mailing Contact'
_order = 'email'
_rec_name = 'email'
_columns = {
'name': fields.char('Name'),
'email': fields.char('Email', required=True),
'create_date': fields.datetime('Create Date'),
'list_id': fields.many2one(
'mail.mass_mailing.list', string='Mailing List',
ondelete='cascade', required=True,
),
'opt_out': fields.boolean('Opt Out', help='The contact has chosen not to receive mails anymore from this list'),
}
def _get_latest_list(self, cr, uid, context={}):
lid = self.pool.get('mail.mass_mailing.list').search(cr, uid, [], limit=1, order='id desc', context=context)
return lid and lid[0] or False
_defaults = {
'list_id': _get_latest_list
}
def name_create(self, cr, uid, name, context=None):
name, email = self.pool['res.partner']._parse_partner_name(name, context=context)
if name and not email:
email = name
if email and not name:
name = email
rec_id = self.create(cr, uid, {'name': name, 'email': email}, context=context)
return self.name_get(cr, uid, [rec_id], context)[0]
class MassMailingList(osv.Model):
"""Model of a contact list. """
_name = 'mail.mass_mailing.list'
_order = 'name'
_description = 'Mailing List'
def _get_contact_nbr(self, cr, uid, ids, name, arg, context=None):
result = dict.fromkeys(ids, 0)
Contacts = self.pool.get('mail.mass_mailing.contact')
for group in Contacts.read_group(cr, uid, [('list_id', 'in', ids), ('opt_out', '!=', True)], ['list_id'], ['list_id'], context=context):
result[group['list_id'][0]] = group['list_id_count']
return result
_columns = {
'name': fields.char('Mailing List', required=True),
'contact_nbr': fields.function(
_get_contact_nbr, type='integer',
string='Number of Contacts',
),
}
class MassMailingStage(osv.Model):
"""Stage for mass mailing campaigns. """
_name = 'mail.mass_mailing.stage'
_description = 'Mass Mailing Campaign Stage'
_order = 'sequence'
_columns = {
'name': fields.char('Name', required=True, translate=True),
'sequence': fields.integer('Sequence'),
}
_defaults = {
'sequence': 0,
}
class MassMailingCampaign(osv.Model):
"""Model of mass mailing campaigns. """
_name = "mail.mass_mailing.campaign"
_description = 'Mass Mailing Campaign'
def _get_statistics(self, cr, uid, ids, name, arg, context=None):
""" Compute statistics of the mass mailing campaign """
Statistics = self.pool['mail.mail.statistics']
results = dict.fromkeys(ids, False)
for cid in ids:
stat_ids = Statistics.search(cr, uid, [('mass_mailing_campaign_id', '=', cid)], context=context)
stats = Statistics.browse(cr, uid, stat_ids, context=context)
results[cid] = {
'total': len(stats),
'failed': len([s for s in stats if not s.scheduled is False and s.sent is False and not s.exception is False]),
'scheduled': len([s for s in stats if not s.scheduled is False and s.sent is False and s.exception is False]),
'sent': len([s for s in stats if not s.sent is False]),
'opened': len([s for s in stats if not s.opened is False]),
'replied': len([s for s in stats if not s.replied is False]),
'bounced': len([s for s in stats if not s.bounced is False]),
}
results[cid]['delivered'] = results[cid]['sent'] - results[cid]['bounced']
results[cid]['received_ratio'] = 100.0 * results[cid]['delivered'] / (results[cid]['total'] or 1)
results[cid]['opened_ratio'] = 100.0 * results[cid]['opened'] / (results[cid]['total'] or 1)
results[cid]['replied_ratio'] = 100.0 * results[cid]['replied'] / (results[cid]['total'] or 1)
return results
_columns = {
'name': fields.char('Name', required=True),
'stage_id': fields.many2one('mail.mass_mailing.stage', 'Stage', required=True),
'user_id': fields.many2one(
'res.users', 'Responsible',
required=True,
),
'category_ids': fields.many2many(
'mail.mass_mailing.category', 'mail_mass_mailing_category_rel',
'category_id', 'campaign_id', string='Categories'),
'mass_mailing_ids': fields.one2many(
'mail.mass_mailing', 'mass_mailing_campaign_id',
'Mass Mailings',
),
'unique_ab_testing': fields.boolean(
'AB Testing',
help='If checked, recipients will be mailed only once, allowing to send'
'various mailings in a single campaign to test the effectiveness'
'of the mailings.'),
'color': fields.integer('Color Index'),
# stat fields
'total': fields.function(
_get_statistics, string='Total',
type='integer', multi='_get_statistics'
),
'scheduled': fields.function(
_get_statistics, string='Scheduled',
type='integer', multi='_get_statistics'
),
'failed': fields.function(
_get_statistics, string='Failed',
type='integer', multi='_get_statistics'
),
'sent': fields.function(
_get_statistics, string='Sent Emails',
type='integer', multi='_get_statistics'
),
'delivered': fields.function(
_get_statistics, string='Delivered',
type='integer', multi='_get_statistics',
),
'opened': fields.function(
_get_statistics, string='Opened',
type='integer', multi='_get_statistics',
),
'replied': fields.function(
_get_statistics, string='Replied',
type='integer', multi='_get_statistics'
),
'bounced': fields.function(
_get_statistics, string='Bounced',
type='integer', multi='_get_statistics'
),
'received_ratio': fields.function(
_get_statistics, string='Received Ratio',
type='integer', multi='_get_statistics',
),
'opened_ratio': fields.function(
_get_statistics, string='Opened Ratio',
type='integer', multi='_get_statistics',
),
'replied_ratio': fields.function(
_get_statistics, string='Replied Ratio',
type='integer', multi='_get_statistics',
),
}
def _get_default_stage_id(self, cr, uid, context=None):
stage_ids = self.pool['mail.mass_mailing.stage'].search(cr, uid, [], limit=1, context=context)
return stage_ids and stage_ids[0] or False
_defaults = {
'user_id': lambda self, cr, uid, ctx=None: uid,
'stage_id': lambda self, *args: self._get_default_stage_id(*args),
}
def get_recipients(self, cr, uid, ids, model=None, context=None):
"""Return the recipients of a mailing campaign. This is based on the statistics
build for each mailing. """
Statistics = self.pool['mail.mail.statistics']
res = dict.fromkeys(ids, False)
for cid in ids:
domain = [('mass_mailing_campaign_id', '=', cid)]
if model:
domain += [('model', '=', model)]
stat_ids = Statistics.search(cr, uid, domain, context=context)
res[cid] = set(stat.res_id for stat in Statistics.browse(cr, uid, stat_ids, context=context))
return res
class MassMailing(osv.Model):
""" MassMailing models a wave of emails for a mass mailign campaign.
A mass mailing is an occurence of sending emails. """
_name = 'mail.mass_mailing'
_description = 'Mass Mailing'
# number of periods for tracking mail_mail statistics
_period_number = 6
_order = 'sent_date DESC'
def __get_bar_values(self, cr, uid, obj, domain, read_fields, value_field, groupby_field, date_begin, 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 = date_begin.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 mailing in self.browse(cr, uid, ids, context=context):
res[mailing.id] = {}
date = mailing.sent_date if mailing.sent_date else mailing.create_date
date_begin = datetime.strptime(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', '=', mailing.id), ('opened', '>=', date_begin_str), ('opened', '<=', date_end_str)]
res[mailing.id]['opened_dayly'] = json.dumps(self.__get_bar_values(cr, uid, obj, domain, ['opened'], 'opened_count', 'opened:day', date_begin, context=context))
domain = [('mass_mailing_id', '=', mailing.id), ('replied', '>=', date_begin_str), ('replied', '<=', date_end_str)]
res[mailing.id]['replied_dayly'] = json.dumps(self.__get_bar_values(cr, uid, obj, domain, ['replied'], 'replied_count', 'replied:day', date_begin, 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]['total'] or 1)
results[mid]['opened_ratio'] = 100.0 * results[mid]['opened'] / (results[mid]['total'] or 1)
results[mid]['replied_ratio'] = 100.0 * results[mid]['replied'] / (results[mid]['total'] or 1)
return results
def _get_mailing_model(self, cr, uid, context=None):
res = []
for model_name in self.pool:
model = self.pool[model_name]
if hasattr(model, '_mail_mass_mailing') and getattr(model, '_mail_mass_mailing'):
res.append((model._name, getattr(model, '_mail_mass_mailing')))
res.append(('mail.mass_mailing.contact', _('Mailing List')))
return res
# indirections for inheritance
_mailing_model = lambda self, *args, **kwargs: self._get_mailing_model(*args, **kwargs)
_columns = {
'name': fields.char('Subject', required=True),
'email_from': fields.char('From', required=True),
'create_date': fields.datetime('Creation Date'),
'sent_date': fields.datetime('Sent Date'),
'body_html': fields.html('Body'),
'attachment_ids': fields.many2many(
'ir.attachment', 'mass_mailing_ir_attachments_rel',
'mass_mailing_id', 'attachment_id', 'Attachments'
),
'mass_mailing_campaign_id': fields.many2one(
'mail.mass_mailing.campaign', 'Mass Mailing Campaign',
ondelete='set null',
),
'state': fields.selection(
[('draft', 'Draft'), ('test', 'Tested'), ('done', 'Sent')],
string='Status', required=True,
),
'color': fields.related(
'mass_mailing_campaign_id', 'color',
type='integer', string='Color Index',
),
# mailing options
'reply_to_mode': fields.selection(
[('thread', 'In Document'), ('email', 'Specified Email Address')],
string='Reply-To Mode', required=True,
),
'reply_to': fields.char('Reply To', help='Preferred Reply-To Address'),
# recipients
'mailing_model': fields.selection(_mailing_model, string='Recipients Model', required=True),
'mailing_domain': fields.char('Domain'),
'contact_list_ids': fields.many2many(
'mail.mass_mailing.list', 'mail_mass_mailing_list_rel',
string='Mailing Lists',
),
'contact_ab_pc': fields.integer(
'AB Testing percentage',
help='Percentage of the contacts that will be mailed. Recipients will be taken randomly.'
),
# 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',
)
}
def default_get(self, cr, uid, fields, context=None):
res = super(MassMailing, self).default_get(cr, uid, fields, context=context)
if 'reply_to_mode' in fields and not 'reply_to_mode' in res and res.get('mailing_model'):
if res['mailing_model'] in ['res.partner', 'mail.mass_mailing.contact']:
res['reply_to_mode'] = 'email'
else:
res['reply_to_mode'] = 'thread'
return res
_defaults = {
'state': 'draft',
'email_from': lambda self, cr, uid, ctx=None: self.pool['mail.message']._get_default_from(cr, uid, context=ctx),
'reply_to': lambda self, cr, uid, ctx=None: self.pool['mail.message']._get_default_from(cr, uid, context=ctx),
'mailing_model': 'mail.mass_mailing.contact',
'contact_ab_pc': 100,
}
#------------------------------------------------------
# Technical stuff
#------------------------------------------------------
def copy_data(self, cr, uid, id, default=None, context=None):
if default is None:
default = {}
mailing = self.browse(cr, uid, id, context=context)
default.update({
'state': 'draft',
'statistics_ids': [],
'name': _('%s (duplicate)') % mailing.name,
'sent_date': False,
})
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)
states = [('draft', 'Draft'), ('test', 'Tested'), ('done', 'Sent')]
read_group_all_states = [{
'__context': {'group_by': groupby[1:]},
'__domain': domain + [('state', '=', state_value)],
'state': state_value,
'state_count': 0,
} for state_value, state_name in states]
# Get standard results
read_group_res = super(MassMailing, self).read_group(cr, uid, domain, fields, groupby, offset=offset, limit=limit, context=context, orderby=orderby)
# Update standard results with default results
result = []
for state_value, state_name in states:
res = filter(lambda x: x['state'] == state_value, read_group_res)
if not res:
res = filter(lambda x: x['state'] == state_value, read_group_all_states)
res[0]['state'] = [state_value, state_name]
result.append(res[0])
return result
else:
return super(MassMailing, self).read_group(cr, uid, domain, fields, groupby, offset=offset, limit=limit, context=context, orderby=orderby)
#------------------------------------------------------
# Views & Actions
#------------------------------------------------------
def on_change_model_and_list(self, cr, uid, ids, mailing_model, list_ids, context=None):
value = {}
if mailing_model == 'mail.mass_mailing.contact':
list_ids = map(lambda item: item if isinstance(item, (int, long)) else [lid for lid in item[2]], list_ids)
if list_ids:
value['mailing_domain'] = "[('list_id', 'in', %s)]" % list_ids
else:
value['mailing_domain'] = "[('list_id', '=', False)]"
else:
value['mailing_domain'] = False
return {'value': value}
def action_duplicate(self, cr, uid, ids, context=None):
copy_id = None
for mid in ids:
copy_id = self.copy(cr, uid, mid, context=context)
if copy_id:
return {
'type': 'ir.actions.act_window',
'view_type': 'form',
'view_mode': 'form',
'res_model': 'mail.mass_mailing',
'res_id': copy_id,
'context': context,
}
return False
def action_test_mailing(self, cr, uid, ids, context=None):
ctx = dict(context, default_mass_mailing_id=ids[0])
return {
'name': _('Test Mailing'),
'type': 'ir.actions.act_window',
'view_mode': 'form',
'res_model': 'mail.mass_mailing.test',
'target': 'new',
'context': ctx,
}
def action_edit_html(self, cr, uid, ids, context=None):
if not len(ids) == 1:
raise ValueError('One and only one ID allowed for this action')
mail = self.browse(cr, uid, ids[0], context=context)
url = '/website_mail/email_designer?model=mail.mass_mailing&res_id=%d&template_model=%s&enable_editor=1' % (ids[0], mail.mailing_model)
return {
'name': _('Open with Visual Editor'),
'type': 'ir.actions.act_url',
'url': url,
'target': 'self',
}
#------------------------------------------------------
# Email Sending
#------------------------------------------------------
def get_recipients(self, cr, uid, mailing, context=None):
domain = eval(mailing.mailing_domain)
res_ids = self.pool[mailing.mailing_model].search(cr, uid, domain, context=context)
# randomly choose a fragment
if mailing.contact_ab_pc < 100:
contact_nbr = self.pool[mailing.mailing_model].search(cr, uid, domain, count=True, context=context)
topick = int(contact_nbr / 100.0 * mailing.contact_ab_pc)
if mailing.mass_mailing_campaign_id and mailing.mass_mailing_campaign_id.unique_ab_testing:
already_mailed = self.pool['mail.mass_mailing.campaign'].get_recipients(cr, uid, [mailing.mass_mailing_campaign_id.id], context=context)[mailing.mass_mailing_campaign_id.id]
else:
already_mailed = set([])
remaining = set(res_ids).difference(already_mailed)
if topick > len(remaining):
topick = len(remaining)
res_ids = random.sample(remaining, topick)
return res_ids
def send_mail(self, cr, uid, ids, context=None):
author_id = self.pool['res.users'].browse(cr, uid, uid, context=context).partner_id.id
for mailing in self.browse(cr, uid, ids, context=context):
# instantiate an email composer + send emails
res_ids = self.get_recipients(cr, uid, mailing, context=context)
comp_ctx = dict(context, active_ids=res_ids)
composer_values = {
'author_id': author_id,
'body': mailing.body_html,
'subject': mailing.name,
'model': mailing.mailing_model,
'email_from': mailing.email_from,
'record_name': False,
'composition_mode': 'mass_mail',
'mass_mailing_id': mailing.id,
'mailing_list_ids': [(4, l.id) for l in mailing.contact_list_ids],
}
if mailing.reply_to_mode == 'email':
composer_values['reply_to'] = mailing.reply_to
composer_id = self.pool['mail.compose.message'].create(cr, uid, composer_values, context=comp_ctx)
self.pool['mail.compose.message'].send_mail(cr, uid, [composer_id], context=comp_ctx)
self.write(cr, uid, [mailing.id], {'sent_date': fields.datetime.now(), 'state': 'done'}, context=context)
return True

View File

@ -0,0 +1,109 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# OpenERP, Open Source Management Solution
# Copyright (C) 2013-today OpenERP SA (<http://www.openerp.com>)
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>
#
##############################################################################
from datetime import datetime
from dateutil import relativedelta
import random
try:
import simplejson as json
except ImportError:
import json
import urllib
import urlparse
from openerp import tools
from openerp.exceptions import Warning
from openerp.tools.safe_eval import safe_eval as eval
from openerp.tools.translate import _
from openerp.osv import osv, fields
class MailMailStats(osv.Model):
""" MailMailStats models the statistics collected about emails. Those statistics
are stored in a separated model and table to avoid bloating the mail_mail table
with statistics values. This also allows to delete emails send with mass mailing
without loosing the statistics about them. """
_name = 'mail.mail.statistics'
_description = 'Email Statistics'
_rec_name = 'message_id'
_order = 'message_id'
_columns = {
'mail_mail_id': fields.integer(
'Mail ID',
help='ID of the related mail_mail. This field is an integer field because'
'the related mail_mail can be deleted separately from its statistics.'
),
'message_id': fields.char('Message-ID'),
'model': fields.char('Document model'),
'res_id': fields.integer('Document ID'),
# campaign / wave data
'mass_mailing_id': fields.many2one(
'mail.mass_mailing', 'Mass Mailing',
ondelete='set null',
),
'mass_mailing_campaign_id': fields.related(
'mass_mailing_id', 'mass_mailing_campaign_id',
type='many2one', ondelete='set null',
relation='mail.mass_mailing.campaign',
string='Mass Mailing Campaign',
store=True, readonly=True,
),
# Bounce and tracking
'scheduled': fields.datetime('Scheduled', help='Date when the email has been created'),
'sent': fields.datetime('Sent', help='Date when the email has been sent'),
'exception': fields.datetime('Exception', help='Date of technical error leading to the email not being sent'),
'opened': fields.datetime('Opened', help='Date when the email has been opened the first time'),
'replied': fields.datetime('Replied', help='Date when this email has been replied for the first time.'),
'bounced': fields.datetime('Bounced', help='Date when this email has bounced.'),
}
_defaults = {
'scheduled': fields.datetime.now,
}
def _get_ids(self, cr, uid, ids=None, mail_mail_ids=None, mail_message_ids=None, domain=None, context=None):
if not ids and mail_mail_ids:
base_domain = [('mail_mail_id', 'in', mail_mail_ids)]
elif not ids and mail_message_ids:
base_domain = [('message_id', 'in', mail_message_ids)]
else:
base_domain = [('id', 'in', ids or [])]
if domain:
base_domain = ['&'] + domain + base_domain
return self.search(cr, uid, base_domain, context=context)
def set_opened(self, cr, uid, ids=None, mail_mail_ids=None, mail_message_ids=None, context=None):
stat_ids = self._get_ids(cr, uid, ids, mail_mail_ids, mail_message_ids, [('opened', '=', False)], context)
self.write(cr, uid, stat_ids, {'opened': fields.datetime.now()}, context=context)
return stat_ids
def set_replied(self, cr, uid, ids=None, mail_mail_ids=None, mail_message_ids=None, context=None):
stat_ids = self._get_ids(cr, uid, ids, mail_mail_ids, mail_message_ids, [('replied', '=', False)], context)
self.write(cr, uid, stat_ids, {'replied': fields.datetime.now()}, context=context)
return stat_ids
def set_bounced(self, cr, uid, ids=None, mail_mail_ids=None, mail_message_ids=None, context=None):
stat_ids = self._get_ids(cr, uid, ids, mail_mail_ids, mail_message_ids, [('bounced', '=', False)], context)
self.write(cr, uid, stat_ids, {'bounced': fields.datetime.now()}, context=context)
return stat_ids

View File

@ -0,0 +1,15 @@
# -*- coding: utf-8 -*-
from openerp.osv import fields, osv
class MassMailingConfiguration(osv.TransientModel):
_name = 'marketing.config.settings'
_inherit = 'marketing.config.settings'
_columns = {
'group_mass_mailing_campaign': fields.boolean(
'Manage Mass Mailing using Campaign',
implied_group='mass_mailing.group_mass_mailing_campaign',
help="""Manage mass mailign using Campaigns"""),
}

View File

@ -1,4 +1,8 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_mass_mailing_category,mail.mass_mailing.category,model_mail_mass_mailing_category,base.group_user,1,1,1,1
access_mass_mailing_contact,mail.mass_mailing.contact,model_mail_mass_mailing_contact,base.group_user,1,1,1,1
access_mass_mailing_list,mail.mass_mailing.list,model_mail_mass_mailing_list,base.group_user,1,1,1,1
access_mass_mailing_stage,mail.mass_mailing.stage,model_mail_mass_mailing_stage,base.group_user,1,1,1,1
access_mass_mailing_campaign,mail.mass_mailing.campaign,model_mail_mass_mailing_campaign,base.group_user,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,mail.mass_mailing,model_mail_mass_mailing,base.group_user,1,1,1,0

1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_mass_mailing_category mail.mass_mailing.category model_mail_mass_mailing_category base.group_user 1 1 1 1
3 access_mass_mailing_contact mail.mass_mailing.contact model_mail_mass_mailing_contact base.group_user 1 1 1 1
4 access_mass_mailing_list mail.mass_mailing.list model_mail_mass_mailing_list base.group_user 1 1 1 1
5 access_mass_mailing_stage mail.mass_mailing.stage model_mail_mass_mailing_stage base.group_user 1 1 1 1
6 access_mass_mailing_campaign mail.mass_mailing.campaign model_mail_mass_mailing_campaign base.group_user 1 1 1 0
7 access_mass_mailing_campaign_system mail.mass_mailing.campaign.system model_mail_mass_mailing_campaign base.group_system 1 1 1 1
8 access_mass_mailing mail.mass_mailing model_mail_mass_mailing base.group_user 1 1 1 0

View File

@ -0,0 +1,17 @@
.openerp .oe_kanban_email_template {
width: 360px;
min-height: 270px !important;
}
.kanban_html_preview {
width: 600px;
height: 600px;
-webkit-transform: scale(.50);
-ms-transform: scale(.50);
transform: scale(.50);
-webkit-transform-origin: 0 0;
-ms-transform-origin: 0 0;
transform-origin: 0 0;
margin: 0 0px -300px 0;
overflow: hidden !important;
}

View File

@ -1,61 +1,13 @@
.openerp .oe_kanban_view .oe_kanban_mass_mailing.oe_kanban_mass_mailing_campaign {
/* Customize to manage content */
width: 552px;
min-height: 278px !important;
/* End of customize */
.openerp .oe_kanban_view .oe_kanban_mass_mailing_campaign {
width: 280px;
min-height: 141px;
}
.openerp .oe_kanban_view .oe_kanban_mass_mailing.oe_kanban_mass_mailing_segment {
/* Customize to manage content */
width: 282px;
min-height: 246px !important;
/* End of customize */
.openerp .oe_kanban_view .oe_kanban_mass_mailing_campaign .oe_kanban_header_right {
float: right;
}
.openerp .oe_kanban_view .oe_kanban_mass_mailing .oe_mail_stats {
width: 122px; /* Manage space in between stats */
display: inline-block;
margin: 2px 5px 0px 5px;
text-align: center;
border: 1px solid rgba(0, 0, 0, 0.16);
-webkit-border-radius: 2px;
border-radius: 2px;
background-color: #FFFFFF;
.openerp .oe_kanban_view .oe_kanban_mass_mailing {
width: 280px;
min-height: 141px;
}
.openerp .oe_kanban_view .oe_kanban_mass_mailing .oe_mail_result {
font-weight: bold;
}
.openerp .oe_kanban_view .oe_kanban_mass_mailing .oe_gauge {
width: 120px;
height: 120px;
display: inline-block;
}
.openerp .oe_kanban_view .oe_kanban_mass_mailing .oe_kanban_content div.oe_sparkline_container {
height: 60px;
width: 120px;
display: inline-block;
margin: 8px 5px 0px 5px;
}
.openerp .oe_kanban_view .oe_kanban_mass_mailing .oe_sparkline_bar_title {
text-align: center;
display: inline;
}
.openerp .oe_kanban_view .oe_kanban_mass_mailing .oe_sparkline_bar {
width: 100px;
height: 60px;
display: inline-block;
}
/*
* Campaign related CSS
*/
/*
* Segment related CSS
*/

View File

@ -1,13 +1,13 @@
openerp.mass_mailing = function(openerp) {
openerp.mass_mailing = function (instance) {
var _t = instance.web._t;
openerp.web_kanban.KanbanRecord.include({
on_card_clicked: function (event) {
if (this.view.dataset.model === 'mail.mass_mailing.campaign') {
this.$('.oe_mass_mailings a').first().click();
this.$('.oe_mailings').click();
} else {
this._super.apply(this, arguments);
}
},
});
};

View File

@ -0,0 +1,97 @@
<?xml version="1.0"?>
<openerp>
<data>
<!-- Email Templates -->
<record model="ir.ui.view" id="email_template_form_minimal">
<field name="name">email.template.form.minimal</field>
<field name="model">email.template</field>
<field name="priority">32</field>
<field name="arch" type="xml">
<form string="Templates" version="7.0">
<sheet>
<group>
<group>
<field name="name" required="True"/>
<field name="model_id" required="1" options="{'no_open': True, 'no_create': True}"
on_change="onchange_model_id(model_id)"
domain="[('model', 'in', ['res.partner', 'mail.mass_mailing.contact'])]"/>
<field name="model" invisible="True"/>
<field name="use_default_to" invisible="1"/>
</group>
<group>
<div class="oe_right oe_button_box" name="buttons">
<button name="%(email_template.wizard_email_template_preview)d" string="Preview"
type="action" target="new"
context="{'template_id':active_id}"/>
<br />
<!-- <field name="website_link" widget='html' radonly='1'
style='margin: 0px; padding: 0px;'/> -->
</div>
</group>
</group>
<notebook>
<page string="Body">
<field name="body_html" nolabel="1"/>
<field name="attachment_ids" widget="many2many_binary"/>
</page>
</notebook>
</sheet>
</form>
</field>
</record>
<record model="ir.ui.view" id="view_email_template_kanban">
<field name="name">email.template.kanban</field>
<field name="model">email.template</field>
<field name="arch" type="xml">
<kanban>
<field name="body_html"/>
<templates>
<t t-name="kanban-box">
<div t-attf-class="oe_kanban_card oe_kanban_global_click oe_kanban_email_template">
<div class="oe_dropdown_toggle oe_dropdown_kanban">
<span class="oe_e">i</span>
<ul class="oe_dropdown_menu">
<t t-if="widget.view.is_action_enabled('edit')">
<li><a type="edit">Edit</a></li>
</t>
<t t-if="widget.view.is_action_enabled('delete')">
<li><a type="delete">Delete</a></li>
</t>
</ul>
</div>
<div class="oe_kanban_content">
<h3>
<field name="name"/>
</h3>
<div class="kanban_html_preview">
<t t-raw="record.body_html.raw_value"/>
</div>
</div>
<div class="oe_clear"></div>
</div>
</t>
</templates>
</kanban>
</field>
</record>
<record model="ir.actions.act_window" id="action_email_template_marketing">
<field name="name">Templates</field>
<field name="res_model">email.template</field>
<field name="view_type">form</field>
<field name="view_mode">kanban,tree,form</field>
<field name="context">{
'form_view_ref': 'mass_mailing.email_template_form_minimal',
'default_use_default_to': True,
}</field>
</record>
<!-- Add Templates in Marketing / Mass mailing menu -->
<menuitem name="Mail Templates" id="menu_email_template"
parent="mass_mailing_campaign" sequence="3"
action="action_email_template_marketing"/>
</data>
</openerp>

View File

@ -0,0 +1,626 @@
<?xml version="1.0"?>
<openerp>
<data>
<!-- Marketing / Mass Mailing -->
<menuitem name="Mass Mailing" id="mass_mailing_campaign"
parent="base.marketing_menu" sequence="1"/>
<!-- Marketing / Mailing Lists -->
<menuitem name="Mailing Lists" id="mass_mailing_list"
parent="base.marketing_menu" sequence="2"/>
<!-- Marketing / Configuration -->
<menuitem name="Configuration" id="marketing_configuration"
parent="base.marketing_menu" sequence="99"/>
<!-- MASS MAILING CONTACT -->
<record model="ir.ui.view" id="view_mail_mass_mailing_contact_search">
<field name="name">mail.mass_mailing.contact.search</field>
<field name="model">mail.mass_mailing.contact</field>
<field name="arch" type="xml">
<search string="Mailing Lists Subscribers">
<field name="name"/>
<field name="email"/>
<field name="list_id"/>
<separator/>
<filter string="Exclude Opt Out" name="not_opt_out" domain="[('opt_out', '=', False)]"/>
<group expand="0" string="Group By...">
<filter string="Creation Date" name="group_create_date"
context="{'group_by': 'create_date'}"/>
<filter string="Mailing Lists" name="group_list_id"
context="{'group_by': 'list_id'}"/>
</group>
</search>
</field>
</record>
<record model="ir.ui.view" id="view_mail_mass_mailing_contact_tree">
<field name="name">mail.mass_mailing.contact.tree</field>
<field name="model">mail.mass_mailing.contact</field>
<field name="priority">10</field>
<field name="arch" type="xml">
<tree string="Mailing Lists Subscribers" editable="top">
<field name="email"/>
<field name="name"/>
<field name="list_id"/>
<field name="opt_out"/>
</tree>
</field>
</record>
<record model="ir.actions.act_window" id="action_view_mass_mailing_contacts">
<field name="name">Mailing List Subscribers</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_not_opt_out': 1}</field>
</record>
<record model="ir.actions.act_window" id="action_view_mass_mailing_contacts_from_list">
<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, 'search_default_not_opt_out': 1}</field>
<field name="help" type="html">
<p class="oe_view_nocontent_create">
Click to create a recipient.
</p>
</field>
</record>
<menuitem name="Contacts" id="menu_email_mass_mailing_contacts"
parent="mass_mailing_list" sequence="50"
action="action_view_mass_mailing_contacts"/>
<!-- MASS MAILING LIST -->
<record model="ir.ui.view" id="view_mail_mass_mailing_list_search">
<field name="name">mail.mass_mailing.list.search</field>
<field name="model">mail.mass_mailing.list</field>
<field name="arch" type="xml">
<search string="Mailing Lists">
<field name="name"/>
</search>
</field>
</record>
<record model="ir.ui.view" id="view_mail_mass_mailing_list_tree">
<field name="name">mail.mass_mailing.list.tree</field>
<field name="model">mail.mass_mailing.list</field>
<field name="priority">10</field>
<field name="arch" type="xml">
<tree string="Mailing Lists">
<field name="name"/>
<field name="contact_nbr"/>
</tree>
</field>
</record>
<record model="ir.ui.view" id="view_mail_mass_mailing_list_form">
<field name="name">mail.mass_mailing.list.form</field>
<field name="model">mail.mass_mailing.list</field>
<field name="arch" type="xml">
<form string="Contact List" version="7.0">
<sheet>
<div class="oe_right oe_button_box" name="buttons">
<button name="%(mass_mailing.action_view_mass_mailing_contacts_from_list)d"
type="action" icon="fa-user" class="oe_stat_button pull-right">
<field name="contact_nbr" string="Recipients" widget="statinfo"/>
</button>
</div>
<div class="oe_title">
<label for="name" class="oe_edit_only"/>
<h1>
<field name="name"/>
</h1>
</div>
</sheet>
</form>
</field>
</record>
<record model="ir.actions.act_window" id="action_view_mass_mailing_lists">
<field name="name">Contact Lists</field>
<field name="res_model">mail.mass_mailing.list</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form</field>
<field name="help" type="html">
<p class="oe_view_nocontent_create">
Click here to create a new mailing list.
</p><p>
Mailing lists allows you to to manage customers and
contacts easily and to send to mailings in a single click.
</p></field>
</record>
<menuitem name="Mailing Lists" id="menu_email_mass_mailing_lists"
parent="mass_mailing_list" sequence="40"
action="action_view_mass_mailing_lists"/>
<!-- MASS MAILING !-->
<record model="ir.ui.view" id="view_mail_mass_mailing_search">
<field name="name">mail.mass_mailing.search</field>
<field name="model">mail.mass_mailing</field>
<field name="arch" type="xml">
<search string="Mass Mailings">
<field name="name" string="Mailings"/>
<field name="mass_mailing_campaign_id"/>
<group expand="0" string="Group By...">
<filter string="State" name="group_state"
context="{'group_by': 'state'}"/>
<filter string="Campaign" name="group_mass_mailing_campaign_id"
groups="mass_mailing.group_mass_mailing_campaign"
context="{'group_by': 'mass_mailing_campaign_id'}"/>
</group>
</search>
</field>
</record>
<record model="ir.ui.view" id="view_mail_mass_mailing_tree">
<field name="name">mail.mass_mailing.tree</field>
<field name="model">mail.mass_mailing</field>
<field name="priority">10</field>
<field name="arch" type="xml">
<tree string="Mass Mailings">
<field name="name"/>
<field name="sent"/>
<field name="delivered"/>
<field name="opened"/>
<field name="replied"/>
<field name="mass_mailing_campaign_id"
groups="mass_mailing.group_mass_mailing_campaign"/>
</tree>
</field>
</record>
<record model="ir.ui.view" id="view_mail_mass_mailing_form">
<field name="name">mail.mass_mailing.form</field>
<field name="model">mail.mass_mailing</field>
<field name="arch" type="xml">
<form string="Mass Mailing" version="7.0">
<header>
<button name="action_test_mailing" type="object"
class="oe_highlight" string="Test Mailing" states="draft"/>
<button name="send_mail" type="object" states="draft,test"
class="oe_highlight" string="Send to All"/>
<button name="action_test_mailing" type="object" states="test,done"
string="Send Test Sample"/>
<field name="state" widget="statusbar"/>
</header>
<div class="oe_form_box_info oe_text_center" attrs="{'invisible': [('scheduled', '=', 0)]}">
<p><strong>
<field name="scheduled" class="oe_inline"/>
emails are in queue and will be sent soon.
</strong></p>
</div>
<sheet>
<div class="oe_button_box pull-right" attrs="{'invisible': [('state', 'in', ('draft','test'))]}">
<button name="%(action_view_mass_mailing_contacts)d"
type="action" class="oe_stat_button">
<field name="received_ratio" string="Received" widget="percentpie"/>
</button>
<button name="%(action_view_mass_mailing_contacts)d"
type="action" class="oe_stat_button">
<field name="opened_ratio" string="Opened" widget="percentpie"/>
</button>
<button name="%(action_view_mass_mailing_contacts)d"
type="action" class="oe_stat_button">
<field name="replied_ratio" string="Replied" widget="percentpie"/>
</button>
<button name="%(action_view_mass_mailing_contacts)d"
type="action" class="oe_stat_button oe_inline">
<field name="opened_dayly" string="Opened Daily" widget="barchart"/>
</button>
<button name="%(action_view_mass_mailing_contacts)d"
type="action" class="oe_stat_button oe_inline">
<field name="replied_dayly" string="Replied Daily" widget="barchart"/>
</button>
</div>
<div class="oe_button_box" attrs="{'invisible': [('total', '=', 0)]}" style="margin-bottom: 32px">
<button name="%(action_view_mass_mailing_contacts)d" type="action"
icon="fa-envelope-o" class="oe_stat_button">
<field name="total" string="Emails" widget="statinfo"/>
</button>
</div>
<group>
<field name="email_from"/>
<field name="name"/>
<label for="mailing_model" string="Recipients"/>
<div>
<field name="mailing_model" widget="radio" style="margin-bottom: 8px"
on_change="on_change_model_and_list(mailing_model, contact_list_ids)"/>
<field name="mailing_domain" widget="char_domain"
placeholder="Select recipients"
options="{'model_field': 'mailing_model'}"/>
<div attrs="{'invisible': [('mailing_model', '&lt;&gt;', 'mail.mass_mailing.contact')]}">
<label for="contact_list_ids" string="Select mailing lists:" class="oe_edit_only"/>
<field name="contact_list_ids" widget="many2many_tags"
placeholder="Select mailing lists..." class="oe_inline"
on_change="on_change_model_and_list(mailing_model, contact_list_ids)"/>
</div>
</div>
</group>
<notebook>
<page string="Mail Body">
<button name="action_edit_html" type="object" string="Design Email" class="oe_highlight" states="draft"/>
<button name="action_edit_html" type="object" string="Change Email Design" states="test"/>
<div attrs="{'invisible' : ['|', '|', ('state', '=', 'done'), ('body_html','!=',False), ('mailing_domain', '=', False)]}" class="oe_view_nocontent oe_clear">
<p class="oe_view_nocontent_create oe_edit_only">
Click to design your email.
</p>
</div>
<field name="body_html" readonly="1"/>
<field name="attachment_ids" widget="many2many_binary" string="Attach a file"/>
</page>
<page string="Options">
<group>
<group string="Mailing">
<label for="reply_to"/>
<div>
<p class="alert alert-danger"
attrs="{'invisible': ['|', ('reply_to_mode', '!=', 'thread'), ('mailing_model', 'not in', ['mail.mass_mailing.contact', 'res.partner'])]}">
This option is not available for the recipients you selected.
Please use a specific reply-to email address.
</p>
<field name="reply_to_mode" widget="radio"/>
<field name="reply_to" style="margin-left: 16px;"
attrs="{'required': [('reply_to_mode', '=', 'email')]}"/>
</div>
<field name="create_date" readonly="1"/>
<field name="sent_date" readonly="1"/>
</group>
<group string="Campaign">
<field name="mass_mailing_campaign_id" groups="mass_mailing.group_mass_mailing_campaign"/>
<label for="contact_ab_pc" groups="mass_mailing.group_mass_mailing_campaign"/>
<div>
<field name="contact_ab_pc" class="oe_inline"/> %
</div>
</group>
</group>
</page>
</notebook>
</sheet>
</form>
</field>
</record>
<record model="ir.ui.view" id="view_mail_mass_mailing_kanban">
<field name="name">mail.mass_mailing.kanban</field>
<field name="model">mail.mass_mailing</field>
<field name="arch" type="xml">
<kanban default_group_by='state'>
<field name='color'/>
<field name='total'/>
<templates>
<t t-name="kanban-box">
<div t-attf-class="oe_kanban_color_#{kanban_getcolor(record.color.raw_value)} oe_kanban_card oe_kanban_global_click oe_kanban_mass_mailing">
<div class="oe_dropdown_toggle oe_dropdown_kanban">
<span class="oe_e">i</span>
<ul class="oe_dropdown_menu">
<t t-if="widget.view.is_action_enabled('delete')">
<li><a type="delete">Delete</a></li>
</t>
</ul>
</div>
<div class="oe_kanban_content">
<div>
<h3><field name="name"/></h3>
<h4 style="display: inline;"><field name="mass_mailing_campaign_id" groups="mass_mailing.group_mass_mailing_campaign"/></h4>
<t t-if="record.mass_mailing_campaign_id.raw_value" groups="mass_mailing.group_mass_mailing_campaign"> - </t><field name="sent_date"/>
</div>
<div>
<div style="display: inline-block">
<field name="delivered" widget="gauge" style="width:120px; height: 90px;"
options="{'max_field': 'total'}"/>
</div>
<div style="display: inline-block; vertical-align: top;">
<strong>Opened</strong> <field name="opened_ratio"/> %<br />
<strong>Replied</strong> <field name="replied_ratio"/> %
</div>
</div>
</div>
<div class="oe_clear"></div>
</div>
</t>
</templates>
</kanban>
</field>
</record>
<record id="action_view_mass_mailings" model="ir.actions.act_window">
<field name="name">Mass Mailings</field>
<field name="res_model">mail.mass_mailing</field>
<field name="view_type">form</field>
<field name="view_mode">kanban,tree,form</field>
<field name="help" type="html">
<p class="oe_view_nocontent_create">
Click here to create a new mailing.
</p><p>
Mass mailing allows you to to easily design and send mass mailings to your contacts, customers or leads using mailing lists.
</p></field>
</record>
<record id="action_view_mass_mailings_from_campaign" model="ir.actions.act_window">
<field name="name">Mass Mailings</field>
<field name="res_model">mail.mass_mailing</field>
<field name="view_type">form</field>
<field name="view_mode">kanban,tree,form</field>
<field name="context">{
'search_default_mass_mailing_campaign_id': [active_id],
'default_mass_mailing_campaign_id': active_id,
}
</field>
<field name="help" type="html">
<p class="oe_view_nocontent_create">
Click here to create a new mailing.
</p><p>
Mass mailing allows you to to easily design and send mass mailings to your contacts, customers or leads using mailing lists.
</p></field>
</record>
<menuitem name="Mass Mailings" id="menu_email_mass_mailings"
parent="mass_mailing_campaign" sequence="2"
action="action_view_mass_mailings"/>
<!-- MASS MAILING CAMPAIGN STAGE !-->
<record model="ir.ui.view" id="view_mail_mass_mailing_stage_search">
<field name="name">mail.mass_mailing.stage.search</field>
<field name="model">mail.mass_mailing.stage</field>
<field name="arch" type="xml">
<search string="Mass Mailings">
<field name="name"/>
</search>
</field>
</record>
<record model="ir.ui.view" id="view_mail_mass_mailing_stage_tree">
<field name="name">mail.mass_mailing.stage.tree</field>
<field name="model">mail.mass_mailing.stage</field>
<field name="priority">10</field>
<field name="arch" type="xml">
<tree string="Mass Mailings" editable="top">
<field name="sequence" widget="handle"/>
<field name="name"/>
</tree>
</field>
</record>
<record id="action_view_mass_mailing_stages" model="ir.actions.act_window">
<field name="name">Mass Mailing Stages</field>
<field name="res_model">mail.mass_mailing.stage</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form</field>
</record>
<menuitem name="Campaign Stages" id="menu_view_mass_mailing_stages"
parent="marketing_configuration" sequence="1"
groups="mass_mailing.group_mass_mailing_campaign"
action="action_view_mass_mailing_stages"/>
<!-- MASS MAILING CAMPAIGNS !-->
<record model="ir.ui.view" id="view_mail_mass_mailing_campaign_search">
<field name="name">mail.mass_mailing.campaign.search</field>
<field name="model">mail.mass_mailing.campaign</field>
<field name="arch" type="xml">
<search string="Mass Mailing Campaigns">
<field name="name" string="Campaigns"/>
<field name="category_ids"/>
<field name="user_id"/>
<group expand="0" string="Group By...">
<filter string="Stage" name="group_stage_id"
context="{'group_by': 'stage_id'}"/>
<filter string="Responsible" name="group_user_id"
context="{'group_by': 'user_id'}"/>
</group>
</search>
</field>
</record>
<record model="ir.ui.view" id="view_mail_mass_mailing_campaign_tree">
<field name="name">mail.mass_mailing.campaign.tree</field>
<field name="model">mail.mass_mailing.campaign</field>
<field name="priority">10</field>
<field name="arch" type="xml">
<tree string="Mass Mailing Campaigns">
<field name="name"/>
<field name="user_id"/>
<field name="stage_id"/>
<field name="category_ids"/>
</tree>
</field>
</record>
<record model="ir.ui.view" id="view_mail_mass_mailing_campaign_form">
<field name="name">mail.mass_mailing.campaign.form</field>
<field name="model">mail.mass_mailing.campaign</field>
<field name="arch" type="xml">
<form string="Mass Mailing Campaign" version="7.0">
<header>
<field name="stage_id" widget="statusbar" clickable="True"/>
</header>
<sheet>
<group>
<group>
<field name="name"/>
<field name="user_id"/>
<field name="category_ids" widget="many2many_tags"/>
</group>
<group>
<field name="total" invisible="1"/>
<div class="oe_right oe_button_box" name="buttons"
attrs="{'invisible': [('total', '=', 0)]}">
<button name="%(action_view_mass_mailing_contacts)d"
type="action" class="oe_stat_button oe_inline">
<field name="received_ratio" widget="percentpie" string="Received"/>
</button>
<button name="%(action_view_mass_mailing_contacts)d"
type="action" class="oe_stat_button oe_inline">
<field name="opened_ratio" widget="percentpie" string="Opened"/>
</button>
<button name="%(action_view_mass_mailing_contacts)d"
type="action" class="oe_stat_button oe_inline">
<field name="replied_ratio" widget="percentpie" string="Replied"/>
</button>
</div>
</group>
</group>
<strong>Related Mailing(s)</strong>
<field name="mass_mailing_ids" readonly="1" string="Related Mailing(s)">
<tree>
<field name="name"/>
<field name="sent_date"/>
<field name="state"/>
<field name="delivered"/>
<field name="opened"/>
<field name="replied"/>
<field name="bounced"/>
<button name="action_duplicate" type="object" string="Duplicate"/>
</tree>
</field>
</sheet>
</form>
</field>
</record>
<record model="ir.ui.view" id="view_mail_mass_mailing_campaign_kanban">
<field name="name">mail.mass_mailing.campaign.kanban</field>
<field name="model">mail.mass_mailing.campaign</field>
<field name="arch" type="xml">
<kanban default_group_by='stage_id'>
<field name='total'/>
<field name='color'/>
<field name='user_id'/>
<field name='mass_mailing_ids'/>
<templates>
<t t-name="kanban-box">
<div t-attf-class="oe_kanban_color_#{kanban_getcolor(record.color.raw_value)} oe_kanban_card oe_kanban_global_click oe_kanban_mass_mailing_campaign">
<div class="oe_dropdown_toggle oe_dropdown_kanban">
<span class="oe_e">i</span>
<ul class="oe_dropdown_menu">
<t t-if="widget.view.is_action_enabled('edit')">
<li><a type="edit">Settings</a></li>
</t>
<t t-if="widget.view.is_action_enabled('delete')">
<li><a type="delete">Delete</a></li>
</t>
<li><ul class="oe_kanban_colorpicker" data-field="color"/></li>
</ul>
</div>
<div class="oe_kanban_content">
<div>
<img t-att-src="kanban_image('res.users', 'image_small', record.user_id.raw_value)"
t-att-title="record.user_id.value" width="24" height="24" class="oe_kanban_avatar oe_kanban_header_right"/>
<h3 style="margin-bottom: 8px;"><field name="name"/></h3>
<field name="category_ids"/>
<a name="%(action_view_mass_mailings_from_campaign)d" type="action"
class="oe_mailings">
<h4 style="margin-top: 8px;"><t t-raw="record.mass_mailing_ids.raw_value.length"/> Mailings</h4>
</a>
</div>
<div class="oe_clear"></div>
<div>
<div style="display: inline-block">
<field name="delivered" widget="gauge" style="width:120px; height: 90px;"
options="{'max_field': 'total'}"/>
</div>
<div style="display: inline-block; vertical-align: top;">
<strong>Opened</strong> <field name="opened_ratio"/> %<br />
<strong>Replied</strong> <field name="replied_ratio"/> %
</div>
</div>
</div>
<div class="oe_clear"></div>
</div>
</t>
</templates>
</kanban>
</field>
</record>
<record id="action_view_mass_mailing_campaigns" model="ir.actions.act_window">
<field name="name">Mass Mailing Campaigns</field>
<field name="res_model">mail.mass_mailing.campaign</field>
<field name="view_type">form</field>
<field name="view_mode">kanban,tree,form</field>
<field name="help" type="html">
<p class="oe_view_nocontent_create">
Click to define a new mass mailing campaign.
</p><p>
Create a campaign to structure mass mailing and get analysis from email status.
</p>
</field>
</record>
<menuitem name="Campaigns" id="menu_email_campaigns"
parent="mass_mailing_campaign" sequence="1"
action="action_view_mass_mailing_campaigns"
groups="mass_mailing.group_mass_mailing_campaign"/>
<!-- MAIL MAIL STATISTICS !-->
<record model="ir.ui.view" id="view_mail_mail_statistics_search">
<field name="name">mail.mail.statistics.search</field>
<field name="model">mail.mail.statistics</field>
<field name="arch" type="xml">
<search string="Mail Statistics">
<field name="mail_mail_id"/>
<field name="message_id"/>
</search>
</field>
</record>
<record model="ir.ui.view" id="view_mail_mail_statistics_tree">
<field name="name">mail.mail.statistics.tree</field>
<field name="model">mail.mail.statistics</field>
<field name="arch" type="xml">
<tree string="Mail Statistics">
<field name="mail_mail_id"/>
<field name="message_id"/>
<field name="sent"/>
<field name="opened"/>
<field name="replied"/>
<field name="bounced"/>
</tree>
</field>
</record>
<record model="ir.ui.view" id="view_mail_mail_statistics_form">
<field name="name">mail.mail.statistics.form</field>
<field name="model">mail.mail.statistics</field>
<field name="arch" type="xml">
<form string="Mail Statistics" version="7.0">
<group>
<group>
<field name="mail_mail_id"/>
<field name="message_id"/>
<field name="exception"/>
<field name="sent"/>
<field name="opened"/>
<field name="replied"/>
<field name="bounced"/>
</group>
<group>
<field name="mass_mailing_id"/>
<field name="mass_mailing_campaign_id"/>
<field name="model"/>
<field name="res_id"/>
</group>
</group>
</form>
</field>
</record>
<record id="action_view_mail_mail_statistics" model="ir.actions.act_window">
<field name="name">Mail Statistics</field>
<field name="res_model">mail.mail.statistics</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form</field>
</record>
<!-- Add in Technical/Email -->
<menuitem name="Mail Statistics" id="menu_email_statistics"
parent="base.menu_email" sequence="50"
action="action_view_mail_mail_statistics"/>
</data>
</openerp>

View File

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data>
<record id="view_marketing_configuration_mass_mailing" model="ir.ui.view">
<field name="name">marketing.config.settings.mass.mailing</field>
<field name="model">marketing.config.settings</field>
<field name="inherit_id" ref="marketing.view_marketing_configuration"/>
<field name="arch" type="xml">
<xpath expr="//div[@name='module_mass_mailing']" position="after">
<div>
<field name="group_mass_mailing_campaign" class="oe_inline"/>
<label for="group_mass_mailing_campaign"/>
</div>
</xpath>
</field>
</record>
</data>
</openerp>

View File

@ -0,0 +1,6 @@
<?xml version="1.0"?>
<openerp>
<data>
</data>
</openerp>

View File

@ -1,23 +1,4 @@
# -*- 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 test_mailing
import mail_compose_message
import mail_mass_mailing_create_segment

View File

@ -1,23 +1,4 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# OpenERP, Open Source Management Solution
# Copyright (C) 2013-today OpenERP SA (<http://www.openerp.com>)
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>
#
##############################################################################
from openerp.osv import osv, fields
@ -29,11 +10,14 @@ class MailComposeMessage(osv.TransientModel):
_columns = {
'mass_mailing_campaign_id': fields.many2one(
'mail.mass_mailing.campaign', 'Mass mailing campaign',
'mail.mass_mailing.campaign', 'Mass Mailing Campaign',
),
'mass_mailing_id': fields.many2one(
'mail.mass_mailing', 'Mass mailing',
domain="[('mass_mailing_campaign_id', '=', mass_mailing_campaign_id)]",
'mail.mass_mailing', 'Mass Mailing'
),
'mass_mailing_name': fields.char('Mass Mailing'),
'mailing_list_ids': fields.many2many(
'mail.mass_mailing.list', string='Mailing List'
),
}
@ -42,23 +26,27 @@ class MailComposeMessage(osv.TransientModel):
mail.mail.statistics values in the o2m of mail_mail, when doing pure
email mass mailing. """
res = super(MailComposeMessage, self).get_mail_values(cr, uid, wizard, res_ids, context=context)
if wizard.composition_mode == 'mass_mail' and wizard.mass_mailing_campaign_id: # TODO: which kind of mass mailing ?
if wizard.mass_mailing_id:
mass_mailing_id = wizard.mass_mailing_id.id
else:
current_date = fields.datetime.now()
# use only for allowed models in mass mailing
if wizard.composition_mode == 'mass_mail' and \
(wizard.mass_mailing_name or wizard.mass_mailing_id) and \
wizard.model in [item[0] for item in self.pool['mail.mass_mailing']._get_mailing_model(cr, uid, context=context)]:
mass_mailing = wizard.mass_mailing_id
if not mass_mailing:
mass_mailing_id = self.pool['mail.mass_mailing'].create(
cr, uid, {
'mass_mailing_campaign_id': wizard.mass_mailing_campaign_id.id,
'name': '%s-%s' % (wizard.mass_mailing_campaign_id.name, current_date),
'date': current_date,
'domain': wizard.active_domain,
'mass_mailing_campaign_id': wizard.mass_mailing_campaign_id and wizard.mass_mailing_campaign_id.id or False,
'name': wizard.mass_mailing_name,
'template_id': wizard.template_id and wizard.template_id.id or False,
'state': 'done',
'mailing_type': wizard.model,
'mailing_domain': wizard.active_domain,
}, context=context)
mass_mailing = self.pool['mail.mass_mailing'].browse(cr, uid, mass_mailing_id, context=context)
for res_id in res_ids:
res[res_id]['mailing_id'] = mass_mailing.id
res[res_id]['statistics_ids'] = [(0, 0, {
'model': wizard.model,
'res_id': res_id,
'mass_mailing_id': mass_mailing_id,
'mass_mailing_id': mass_mailing.id,
})]
return res

View File

@ -9,7 +9,9 @@
<field name="inherit_id" ref="mail.email_compose_message_wizard_form"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='notify']" position="after">
<field name="mass_mailing_campaign_id"
<field name="mass_mailing_campaign_id" groups="mass_mailing.group_mass_mailing_campaign"
attrs="{'invisible': [('composition_mode', '!=', 'mass_mail')]}"/>
<field name="mass_mailing_name"
attrs="{'invisible': [('composition_mode', '!=', 'mass_mail')]}"/>
</xpath>
</field>

View File

@ -1,135 +0,0 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# OpenERP, Open Source Management Solution
# Copyright (C) 2013-Today OpenERP SA (<http://www.openerp.com>)
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>
#
##############################################################################
from openerp.osv import osv, fields
from openerp.tools.translate import _
class MailMassMailingCreate(osv.TransientModel):
"""Wizard to help creating mass mailing waves for a campaign. """
_name = 'mail.mass_mailing.create'
_description = 'Mass mailing creation'
_columns = {
'mass_mailing_campaign_id': fields.many2one(
'mail.mass_mailing.campaign', 'Mass mailing campaign',
required=True,
),
'model_id': fields.many2one(
'ir.model', 'Document Type',
required=True,
help='Document on which the mass mailing will run. This must be a '
'valid OpenERP model.',
),
'model_model': fields.related(
'model_id', 'name',
type='char', string='Model Name'
),
'filter_id': fields.many2one(
'ir.filters', 'Filter',
required=True,
domain="[('model_id', '=', model_model)]",
help='Filter to be applied on the document to find the records to be '
'mailed.',
),
'domain': fields.related(
'filter_id', 'domain',
type='char', string='Domain',
),
'template_id': fields.many2one(
'email.template', 'Template', required=True,
domain="[('model_id', '=', model_id)]",
),
'name': fields.char(
'Mailing Name', required=True,
help='Name of the mass mailing.',
),
'mass_mailing_id': fields.many2one(
'mail.mass_mailing', 'Mass Mailing',
),
}
def _get_default_model_id(self, cr, uid, context=None):
model_ids = self.pool['ir.model'].search(cr, uid, [('model', '=', 'res.partner')], context=context)
return model_ids and model_ids[0] or False
_defaults = {
'model_id': lambda self, cr, uid, ctx=None: self._get_default_model_id(cr, uid, context=ctx),
}
def on_change_model_id(self, cr, uid, ids, model_id, context=None):
if model_id:
model_model = self.pool['ir.model'].browse(cr, uid, model_id, context=context).model
else:
model_model = False
return {'value': {'model_model': model_model}}
def on_change_filter_id(self, cr, uid, ids, filter_id, context=None):
if filter_id:
domain = self.pool['ir.filters'].browse(cr, uid, filter_id, context=context).domain
else:
domain = False
return {'value': {'domain': domain}}
def create_mass_mailing(self, cr, uid, ids, context=None):
""" Create a mass mailing based on wizard data, and update the wizard """
for wizard in self.browse(cr, uid, ids, context=context):
mass_mailing_values = {
'name': wizard.name,
'mass_mailing_campaign_id': wizard.mass_mailing_campaign_id.id,
'domain': wizard.domain,
'template_id': wizard.template_id.id,
}
mass_mailing_id = self.pool['mail.mass_mailing'].create(cr, uid, mass_mailing_values, context=context)
self.write(cr, uid, [wizard.id], {'mass_mailing_id': mass_mailing_id}, context=context)
return True
def launch_composer(self, cr, uid, ids, context=None):
""" Main wizard action: create a new mailing and launch the mail.compose.message
email composer with wizard data. """
self.create_mass_mailing(cr, uid, ids, context=context)
wizard = self.browse(cr, uid, ids[0], context=context)
ctx = dict(context)
ctx.update({
'default_composition_mode': 'mass_mail',
'default_template_id': wizard.template_id.id,
'default_use_mass_mailing_campaign': True,
'default_use_active_domain': True,
'default_model': wizard.model_id.model,
'default_res_id': False,
'default_active_domain': wizard.domain,
'default_mass_mailing_campaign_id': wizard.mass_mailing_campaign_id.id,
'default_mass_mailing_id': wizard.mass_mailing_id.id,
})
return {
'name': _('Compose Email for Mass Mailing'),
'type': 'ir.actions.act_window',
'view_type': 'form',
'view_mode': 'form',
'res_model': 'mail.compose.message',
'views': [(False, 'form')],
'view_id': False,
'target': 'new',
'context': ctx,
}

View File

@ -1,82 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data>
<!-- Wizard form view -->
<record model="ir.ui.view" id="view_mail_mass_mailing_create_form">
<field name="name">mail.mass_mailing.create.form</field>
<field name="model">mail.mass_mailing.create</field>
<field name="arch" type="xml">
<form string="Create a Mass Mailing" version="7.0">
<group>
<field name="model_model" invisible="1"/>
<field name="domain" invisible="1"/>
<label for="mass_mailing_campaign_id"/>
<div>
<field name="mass_mailing_campaign_id"/>
<p class="oe_grey"
attrs="{'invisible': [('mass_mailing_campaign_id', '!=', False)]}">
Please choose a mass mailing campaign that will hold the new mailing.
</p>
</div>
<label for="model_id"/>
<div>
<field name="model_id"
on_change="on_change_model_id(model_id, context)"/>
<p class="oe_grey"
attrs="{'invisible': [('model_id', '!=', False)]}">
Please choose a model on which you will run the mass mailing.
</p>
</div>
<label for="filter_id"/>
<div>
<field name="filter_id"
on_change="on_change_filter_id(filter_id, context)"/>
<p class="oe_grey"
attrs="{'invisible': [('filter_id', '!=', False)]}">
Please choose a filter that will be applied on the model
to find the records on which you will run the mass mailing.
</p>
</div>
<label for="template_id"/>
<div>
<field name="template_id"/>
<p class="oe_grey"
attrs="{'invisible': [('template_id', '!=', False)]}">
Please choose the template to use to render the emails
to send.
</p>
</div>
<label for="name"/>
<div>
<field name="name"/>
<p class="oe_grey">
Please choose the name of the mailing.
</p>
</div>
<button name="launch_composer" type="object"
string="Create mailing and launch email composer"/>
</group>
</form>
</field>
</record>
<record model="ir.actions.act_window" id="action_mail_mass_mailing_create">
<field name="name">Create Mass Mailing</field>
<field name="res_model">mail.mass_mailing.create</field>
<field name="src_model">mail.mass_mailing.campaign</field>
<field name="type">ir.actions.act_window</field>
<field name="view_type">form</field>
<field name="view_mode">form</field>
<field name="target">new</field>
<field name="context">{'default_mass_mailing_campaign_id': active_id}</field>
</record>
</data>
</openerp>

View File

@ -0,0 +1,42 @@
# -*- coding: utf-8 -*-
from openerp import tools
from openerp.osv import osv, fields
class TestMassMailing(osv.TransientModel):
_name = 'mail.mass_mailing.test'
_description = 'Sample Mail Wizard'
_columns = {
'email_to': fields.char('Recipients', required=True,
help='Comma-separated list of email addresses.'),
'mass_mailing_id': fields.many2one('mail.mass_mailing', 'Mailing', required=True),
}
_defaults = {
'email_to': lambda self, cr, uid, ctx=None: self.pool['mail.message']._get_default_from(cr, uid, context=ctx),
}
def send_mail_test(self, cr, uid, ids, context=None):
Mail = self.pool['mail.mail']
for wizard in self.browse(cr, uid, ids, context=context):
mailing = wizard.mass_mailing_id
test_emails = tools.email_split(wizard.email_to)
mail_ids = []
for test_mail in test_emails:
body = mailing.body_html
unsubscribe_url = self.pool['mail.mass_mailing'].get_unsubscribe_url(cr, uid, mailing.id, 0, email=test_mail, context=context)
body = tools.append_content_to_html(body, unsubscribe_url, plaintext=False, container_tag='p')
mail_values = {
'email_from': mailing.email_from,
'reply_to': mailing.reply_to,
'email_to': test_mail,
'subject': mailing.name,
'body_html': body,
'auto_delete': True,
}
mail_ids.append(Mail.create(cr, uid, mail_values, context=context))
Mail.send(cr, uid, mail_ids, context=context)
self.pool['mail.mass_mailing'].write(cr, uid, [mailing.id], {'state': 'test'}, context=context)
return True

View File

@ -0,0 +1,34 @@
<?xml version="1.0"?>
<openerp>
<data>
<record model="ir.ui.view" id="view_mail_mass_mailing_test_form">
<field name="name">mail.mass_mailing.test.form</field>
<field name="model">mail.mass_mailing.test</field>
<field name="arch" type="xml">
<form string="Send a Sample Mail" version="7.0">
<p class="text-muted">
Send a sample of this mailing to the above of email addresses for test purpose.
</p>
<group>
<field name="email_to"/>
</group>
<footer>
<button string="Send Sample Mail" name="send_mail_test" type="object" class="oe_highlight"/>
or
<button string="Cancel" class="oe_link" special="cancel" />
</footer>
</form>
</field>
</record>
<record id="action_mail_mass_mailing_test" model="ir.actions.act_window">
<field name="name">Mailing Test</field>
<field name="res_model">mail.mass_mailing.test</field>
<field name="view_type">form</field>
<field name="view_mode">form</field>
<field name="target">new</field>
</record>
</data>
</openerp>

View File

@ -126,8 +126,8 @@ class account_invoice(osv.Model):
class mail_mail(osv.osv):
_inherit = 'mail.mail'
def _postprocess_sent_message(self, cr, uid, mail, context=None):
if mail.model == 'sale.order':
def _postprocess_sent_message(self, cr, uid, mail, context=None, mail_sent=True):
if mail_sent and mail.model == 'sale.order':
so_obj = self.pool.get('sale.order')
order = so_obj.browse(cr, uid, mail.res_id, context=context)
partner = order.partner_id
@ -138,4 +138,4 @@ class mail_mail(osv.osv):
for p in mail.partner_ids:
if p.id not in order.message_follower_ids:
so_obj.message_subscribe(cr, uid, [mail.res_id], [p.id], context=context)
return super(mail_mail, self)._postprocess_sent_message(cr, uid, mail=mail, context=context)
return super(mail_mail, self)._postprocess_sent_message(cr, uid, mail=mail, context=context, mail_sent=mail_sent)

View File

@ -1245,10 +1245,10 @@ class mail_mail(osv.Model):
_name = 'mail.mail'
_inherit = 'mail.mail'
def _postprocess_sent_message(self, cr, uid, mail, context=None):
if mail.model == 'purchase.order':
def _postprocess_sent_message(self, cr, uid, mail, context=None, mail_sent=True):
if mail_sent and mail.model == 'purchase.order':
self.pool.get('purchase.order').signal_send_rfq(cr, uid, [mail.res_id])
return super(mail_mail, self)._postprocess_sent_message(cr, uid, mail=mail, context=context)
return super(mail_mail, self)._postprocess_sent_message(cr, uid, mail=mail, context=context, mail_sent=mail_sent)
class product_template(osv.Model):

View File

@ -427,6 +427,7 @@
editor.edit();
}
});
website.editor_bar = editor;
};
/* ----- TOP EDITOR BAR FOR ADMIN ---- */

View File

@ -1,19 +1,71 @@
# -*- coding: utf-8 -*-
# from openerp import SUPERUSER_ID
from urllib import urlencode
from openerp.addons.web import http
from openerp.addons.web.http import request
class WebsiteEmailDesigner(http.Controller):
@http.route('/website_mail/email_designer/<model("email.template"):template>', type='http', auth="user", website=True, multilang=True)
def index(self, template, **kw):
@http.route('/website_mail/email_designer', type='http', auth="user", website=True, multilang=True)
def index(self, model, res_id, template_model=None, **kw):
if not model or not model in request.registry or not res_id:
return request.redirect('/')
model_cols = request.registry[model]._all_columns
if 'body' not in model_cols and 'body_html' not in model_cols or \
'email' not in model_cols and 'email_from' not in model_cols or \
'name' not in model_cols and 'subject' not in model_cols:
return request.redirect('/')
obj_ids = request.registry[model].exists(request.cr, request.uid, [res_id], context=request.context)
if not obj_ids:
return request.redirect('/')
# try to find fields to display / edit -> as t-field is static, we have to limit
# the available fields to a given subset
email_from_field = 'email'
if 'email_from' in model_cols:
email_from_field = 'email_from'
subject_field = 'name'
if 'subject' in model_cols:
subject_field = 'subject'
body_field = 'body'
if 'body_html' in model_cols:
body_field = 'body_html'
cr, uid, context = request.cr, request.uid, request.context
res_id = int(res_id)
record = request.registry[model].browse(cr, uid, res_id, context=context)
values = {
'template': template,
'record': record,
'templates': None,
'model': model,
'res_id': res_id,
'email_from_field': email_from_field,
'subject_field': subject_field,
'body_field': body_field,
}
print template
return request.website.render("website_mail.designer_index", values)
if getattr(record, body_field):
values['mode'] = 'email_designer'
else:
if kw.get('enable_editor'):
kw.pop('enable_editor')
fragments = dict(model=model, res_id=res_id, **kw)
if template_model:
fragments['template_model'] = template_model
return request.redirect('/website_mail/email_designer?%s' % urlencode(fragments))
values['mode'] = 'email_template'
tmpl_obj = request.registry['email.template']
if template_model:
tids = tmpl_obj.search(cr, uid, [('model', '=', template_model)], context=context)
else:
tids = tmpl_obj.search(cr, uid, [], context=context)
templates = tmpl_obj.browse(cr, uid, tids, context=context)
values['templates'] = templates
return request.website.render("website_mail.email_designer", values)
@http.route(['/website_mail/snippets'], type='json', auth="user", website=True)
def snippets(self):

View File

@ -1,38 +1,19 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# OpenERP, Open Source Management Solution
# Copyright (C) 2014-Today OpenERP SA (<http://www.openerp.com>).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
from openerp.osv import osv, fields
from openerp.osv import osv
from openerp.tools.translate import _
class EmailTemplate(osv.Model):
_inherit = 'email.template'
def _get_website_link(self, cr, uid, ids, name, args, context=None):
return dict((id, _('<a href="website_mail/email_designer/%s">Open with visual editor</a>') % id) for id in ids)
_columns = {
'website_link': fields.function(
_get_website_link, type='text',
string='Website Link',
help='Link to the website',
),
}
def action_edit_html(self, cr, uid, ids, context=None):
if not len(ids) == 1:
raise ValueError('One and only one ID allowed for this action')
url = '/website_mail/email_designer?model=email.template&res_id=%d&enable_editor=1' % (ids[0],)
return {
'name': _('Edit Template'),
'type': 'ir.actions.act_url',
'url': url,
'target': 'self',
}

View File

@ -1,4 +1,19 @@
.js_follow[data-follow='on'] .js_follow_btn ,
.js_follow[data-follow='off'] .js_unfollow_btn {
display: none;
}
}
.email_preview_border {
overflow: hidden !important;
border: 2px solid grey;
height: 300px;
}
.email_preview {
-webkit-transform: scale(.50);
-ms-transform: scale(.50);
transform: scale(.50);
-webkit-transform-origin: 0 0;
-ms-transform-origin: 0 0;
transform-origin: 0 0;
margin: 0 0px -300px 0;
}

View File

@ -1,17 +1,23 @@
(function () {
'use strict';
var website = openerp.website;
website.snippet.BuildingBlock.include({
// init: function (parent) {
// this._super.apply(this, arguments);
// },
_get_snippet_url: function () {
return '/website_mail/snippets';
}
});
// Copy the template to the body of the email
$(document).ready(function () {
$('.js_template_set').click(function(ev) {
$('#email_designer').show();
$('#email_template').hide();
$(".js_content", $(this).parent()).children().clone().appendTo('#email_body');
openerp.website.editor_bar.edit();
event.preventDefault();
});
});
})();

View File

@ -6,10 +6,8 @@
<field name="model">email.template</field>
<field name="inherit_id" ref="email_template.email_template_form"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='model']" position="before">
<br />
<field name="website_link" widget='html' radonly='1'
style='margin: 0px; padding: 0px;'/>
<xpath expr="//h1" position="after">
<button string="Edit Template" name="action_edit_html" type="object"/>
</xpath>
</field>
</record>

View File

@ -1,53 +1,74 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data>
<data>
<!-- Email Designer main page -->
<template id="designer_index" name="Email Designer">
<!-- Template Choice page -->
<template id="email_designer" name="Email Designer">
<t t-call="website.layout">
<t t-set="head">
<script type="text/javascript" src="/website_mail/static/src/js/website_email_designer.js"></script>
</t>
<div id="wrap">
<div class="container">
<div id="wrap" class="container" t-ignore="True">
<div id="email_template" class="mb32" t-att-style="mode != 'email_template' and 'display: none' or ''">
<a class="mt16 btn btn-default pull-right"
t-attf-href="/web#return_label=Website&amp;model=#{model}&amp;id=#{res_id}&amp;view_type=form">
Back
</a>
<h1 class="page-header mt16">
Choose an Email Template
</h1>
<div class="row">
<div class="col-md-8">
<a class="pull-right mt32"
t-att-href="'/web#return_label=Website&amp;action=email_template.action_email_template_tree_all&amp;view_type=form&amp;id=%d' % template.id">
<button class="btn btn-primary">Back to Template Form</button>
</a>
<h1 t-field="template.name"/>
<div class="row" style="width: 600px;">
<div class="row">
<div class="col-lg-3"><b>Email From</b></div>
<div class="col-lg-9"><span t-field="template.email_from"/></div>
</div>
<div class="row">
<div class="col-lg-3"><b>To (Email)</b></div>
<div class="col-lg-9"><span t-field="template.email_to"/></div>
</div>
<div class="row">
<div class="col-lg-3"><b>To (Partners)</b></div>
<div class="col-lg-9"><span t-field="template.partner_to"/></div>
</div>
<div class="row">
<div class="col-lg-3"><b>Reply To</b></div>
<div class="col-lg-9"><span t-field="template.reply_to"/></div>
</div>
<div class="row">
<div class="col-lg-3"><b>Subject</b></div>
<div class="col-lg-9"><span t-field="template.subject"/></div>
</div>
<div class="row well">
<div t-field="template.body_html" style="position: relative;"/>
</div>
<div class="col-md-3 col-sm-4 text-center img-border">
<div class="email_preview_border">
<div class="email_preview js_content"/>
</div>
<h4>New Template</h4>
<button class="btn btn-primary js_template_set">Select</button>
</div>
<div t-foreach="templates" t-as="template" class="col-md-3 col-sm-4 text-center">
<div class="email_preview_border">
<div t-field="template.body_html" class="email_preview js_content"/>
</div>
<h4 t-field="template.name"/>
<button class="btn btn-primary js_template_set">Select</button>
</div>
</div>
</div>
<div id="email_designer" class="mb32" t-att-style="mode != 'email_designer' and 'display: none' or ''">
<a class="mt16 btn btn-primary pull-right"
t-attf-href="/web#return_label=Website&amp;model=#{model}&amp;id=#{res_id}&amp;view_type=form">
Save and Continue
</a>
<h1 class="page-header mt16">
Design Your Email
</h1>
<div class="form-horizontal">
<!-- email_from fields-->
<div class="form-group" t-if="email_from_field == 'email_from'">
<label class="col-sm-2 control-label">From:</label>
<div class="col-sm-7"><span t-field="record.email_from" class="form-control"/></div>
</div>
<div class="form-group" t-if="email_from_field == 'email'">
<label class="col-sm-2 control-label">From:</label>
<div class="col-sm-7"><span t-field="record.email" class="form-control"/></div>
</div>
<!-- email_from fields-->
<div class="form-group" t-if="subject_field == 'subject'">
<label class="col-sm-2 control-label">Subject:</label>
<div class="col-sm-7"><span t-field="record.subject" class="form-control"/></div>
</div>
<div class="form-group" t-if="subject_field == 'name'">
<label class="col-sm-2 control-label">Subject:</label>
<div class="col-sm-7"><span t-field="record.name" class="form-control"/></div>
</div>
</div>
<hr/>
<!-- body fields -->
<div t-if="body_field == 'body_html'">
<div t-field="record.body_html" id="email_body_html"/>
</div>
<div t-if="body_field == 'body'">
<div t-field="record.body" id="email_body"/>
</div>
</div>
</div>
@ -55,7 +76,7 @@
<t t-set="website.footer"></t>
</template>
</data>
</data>
</openerp>