bzr revid: fp@tinyerp.com-20140412130132-6dncdvhrxsciyr7l
This commit is contained in:
Fabien Pinckaers 2014-04-12 15:01:32 +02:00
commit 915ac89024
84 changed files with 3140 additions and 1784 deletions

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,42 @@ 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'):
if template.model and hasattr(self.pool[template.model], 'message_get_default_recipients'):
default_recipients = self.pool[template.model].message_get_default_recipients(cr, uid, res_ids, context=context)
elif template.model:
ctx = dict(context, thread_model=template.model)
default_recipients = self.pool['mail.thread'].message_get_default_recipients(cr, uid, res_ids, context=ctx)
else:
default_recipients = {}
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 +461,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 +529,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,28 @@ 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
def get_recipients_data(self, cr, uid, values, context=None):
if values['composition_mode'] != 'mass_mail':
return super(mail_compose_message, self).get_recipients_data(cr, uid, values, context=context)
model, res_id, template_id = values['model'], values['res_id'], values.get('template_id')
active_ids = context.get('active_ids', list())
if not active_ids or not template_id:
return False
template = self.pool['email.template'].browse(cr, uid, template_id, context=context)
partner_to = self.render_template_batch(cr, uid, template.partner_to, model, active_ids[:3], context=context)
partner_ids = [int(data) for key, data in partner_to.iteritems() if data]
rec_names = [rec_name[1] for rec_name in self.pool['res.partner'].name_get(cr, SUPERUSER_ID, partner_ids, context=context)]
recipients = ', '.join(rec_names)
recipients += ' and %d more.' % (len(active_ids) - 3) if len(active_ids) > 3 else '.'
return recipients
_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 +102,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 +119,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 +157,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(
@ -197,16 +188,15 @@ class mail_compose_message(osv.TransientModel):
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)
for res_id in res_ids:
if wizard.template_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)
# 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)
# 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

@ -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

@ -76,27 +76,12 @@ class mail_message(osv.Model):
_message_read_more_limit = 1024
def default_get(self, cr, uid, fields, context=None):
# print '\tmail_message: default_get on', fields
# protection for `default_type` values leaking from menu action context (e.g. for invoices)
if context and context.get('default_type') and context.get('default_type') not in self._columns['type'].selection:
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 +120,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 +147,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 +756,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 +821,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 +870,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 +886,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 +919,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

@ -661,15 +661,30 @@ class mail_thread(osv.AbstractModel):
# Email specific
#------------------------------------------------------
def message_get_default_recipients(self, cr, uid, ids, context=None):
model = self
if context and context.get('thread_model'):
model = self.pool[context['thread_model']]
res = {}
for record in model.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.parent_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

@ -53,4 +53,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,23 @@ 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))
result['recipients_data'] = self.get_recipients_data(cr, uid, result, context=context)
for field in vals:
if field in fields:
@ -102,13 +95,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 +111,21 @@ 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'),
'recipients_data': fields.text(string='Recipients Data',
help='Helper field used in mass mailing to display a sample of recipients'),
'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 +133,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 +164,53 @@ 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_recipients_data(self, cr, uid, values, context=None):
""" Returns a string explaining the targetted recipients, to ease the use
of the wizard. """
composition_mode, model, res_id = values['composition_mode'], values['model'], values['res_id']
if composition_mode == 'comment' and model and res_id:
doc_name = self.pool[model].name_get(cr, uid, [res_id], context=context)
return doc_name and 'Followers of %s' % doc_name[0][1] or False
elif composition_mode == 'mass_post' and model:
if 'active_domain' in context:
active_ids = self.pool[model].search(cr, uid, eval(context['active_domain']), limit=100, context=context)
else:
active_ids = context.get('active_ids', list())
if active_ids:
name_gets = [rec_name[1] for rec_name in self.pool[model].name_get(cr, uid, active_ids[:3], context=context)]
return 'Followers of selected documents (' + ', '.join(name_gets) + len(active_ids) > 3 and ', ...' or '' + ')'
return False
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,60 +222,56 @@ class mail_compose_message(osv.TransientModel):
email(s), rendering any template patterns on the fly if needed. """
if context is None:
context = {}
# import datetime
# print '--> beginning sending email', datetime.datetime.now()
# 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)
# print '----> before computing values', datetime.datetime.now()
# print '----> after computing values', datetime.datetime.now()
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)
# print '--> finished sending email', datetime.datetime.now()
return {'type': 'ir.actions.act_window_close'}
def get_mail_values(self, cr, uid, wizard, res_ids, context=None):
"""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 +286,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 +335,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 +350,14 @@ 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)
if wizard.model and hasattr(self.pool[wizard.model], 'message_get_default_recipients'):
default_recipients = self.pool[wizard.model].message_get_default_recipients(cr, uid, res_ids, context=context)
elif wizard.model:
ctx = dict(context, thread_model=wizard.model)
default_recipients = self.pool['mail.thread'].message_get_default_recipients(cr, uid, res_ids, context=ctx)
else:
default_recipients = {}
results = dict.fromkeys(res_ids, False)
for res_id in res_ids:
results[res_id] = {
@ -365,6 +366,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,30 @@
<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>.
<br />
<span>The following contacts will be mailed: </span>
</span>
<field name="recipients_data" class="oe_inline oe_compose_recipients" readonly="1" attrs="{'invisible':[('recipients_data', '=', False)]}"/>
<span attrs="{'invisible':['|', ('composition_mode', '!=', 'comment'), ('recipients_data', '=', False)]}">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,35 @@
{
'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',
'wizard/mailing_list_confirm.xml',
'views/mass_mailing_report.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 +57,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,35 @@
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:
record_ids = request.registry[mailing.mailing_model].search(cr, SUPERUSER_ID, [('id', '=', res_id), ('email', 'ilike', email)], context=context)
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,157 @@
<?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>
<record id="mass_mail_template_1" model="email.template">
<field name="name">Partner Newsletter</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="reply_to"><![CDATA[Info <info@yourcompany.example.com>]]></field>
<field name="use_in_mass_mailing" eval="True"/>
<field name="use_default_to" eval="True"/>
<field name="attachment_ids" eval="[(4, ref('mass_mail_attach_1'))]"/>
<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>
</record>
<!-- Create mailing lists -->
<record id="mass_mail_list_1" model="mail.mass_mailing.list">
<field name="name">Imported Contacts</field>
<field name="model">mail.mass_mailing.contact</field>
</record>
<record id="mass_mail_list_2" model="mail.mass_mailing.list">
<field name="name">Customers</field>
<field name="model">res.partner</field>
<field name="domain">[('customer', '=', True)]</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" eval="ref('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" eval="ref('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" eval="ref('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_id" eval="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="template_id" eval="ref('mass_mail_template_1')"/>
<field name="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="contact_list_ids" eval="[(4, ref('mass_mail_list_2'))]"/>
</record>
<record id="mass_mail_2" model="mail.mass_mailing">
<field name="name">Second Newsletter</field>
<field name="state">test</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_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>
</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,8 @@
# -*- coding: utf-8 -*-
import mass_mailing
import mail_mail
import mail_thread
import email_template
import res_config
import mass_mailing_report

View File

@ -0,0 +1,36 @@
# -*- coding: utf-8 -*-
from openerp.tools.translate import _
from openerp.osv import osv, fields
class EmailTemplate(osv.Model):
"""Add the mass mailing campaign data to mail"""
_name = 'email.template'
_inherit = ['email.template']
_columns = {
'use_in_mass_mailing': fields.boolean('Available for marketing and mailing'),
}
def action_new_mailing(self, cr, uid, ids, context=None):
template = self.browse(cr, uid, ids[0], context=context)
ctx = dict(context)
ctx.update({
'default_mailing_model': template.model,
'default_template_id': ids[0],
})
return {
'name': _('Create a Mass Mailing'),
'type': 'ir.actions.act_window',
'view_type': 'form',
'view_mode': 'form',
'res_model': 'mail.mass_mailing',
'views': [(False, 'form')],
'context': ctx,
}
class email_template_preview(osv.TransientModel):
""" Reinitialize email template preview model to have access to all email.template
new fields. """
_name = "email_template.preview"
_inherit = ['email.template', 'email_template.preview']

View File

@ -19,7 +19,8 @@
#
##############################################################################
from urlparse import urljoin
import urllib
import urlparse
from openerp import tools
from openerp import SUPERUSER_ID
@ -50,7 +51,12 @@ 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 send_get_mail_body(self, cr, uid, mail, partner=None, context=None):
@ -63,3 +69,10 @@ class MailMail(osv.Model):
if tracking_url:
body = tools.append_content_to_html(body, tracking_url, plaintext=False, container_tag='div')
return body
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,789 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# OpenERP, Open Source Management Solution
# Copyright (C) 2013-today OpenERP SA (<http://www.openerp.com>)
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>
#
##############################################################################
from datetime import datetime
from dateutil import relativedelta
import random
try:
import simplejson as json
except ImportError:
import json
import urllib
import urlparse
from openerp import tools
from openerp.exceptions import Warning
from openerp.tools.safe_eval import safe_eval as eval
from openerp.tools.translate import _
from openerp.osv import osv, fields
class MassMailingCategory(osv.Model):
"""Model of categories of mass mailing, i.e. marketing, newsletter, ... """
_name = 'mail.mass_mailing.category'
_description = 'Mass Mailing Category'
_order = 'name'
_columns = {
'name': fields.char('Name', required=True),
}
class MassMailingContact(osv.Model):
"""Model of a contact. This model is different from the partner model
because it holds only some basic information: name, email. The purpose is to
be able to deal with large contact list to email without bloating the partner
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),
'list_id': fields.many2one(
'mail.mass_mailing.list', string='Mailing List',
ondelete='cascade',
),
'opt_out': fields.boolean('Opt Out', help='The contact has chosen not to receive mails anymore from this list'),
}
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):
"""Compute the number of contacts linked to the mailing list. """
results = dict.fromkeys(ids, 0)
mlc = self.pool.get('mail.mass_mailing.contact').
result = dict(lambda x: (x,0), ids)
for m in mlc.read_group(cr, uid, [('list_id','in',ids)], ['list_id'], ['list_id'], context=context):
result[m['list_id']] = m['__count']
return results
_columns = {
'name': fields.char('Mailing List', required=True),
'contact_nbr': fields.function(
_get_contact_nbr, type='integer',
string='Contact Number',
),
}
# TODO: remove this?
def action_see_records(self, cr, uid, ids, context=None):
contact_list = self.browse(cr, uid, ids[0], context=context)
ctx = dict(context)
ctx['search_default_not_opt_out'] = True
return {
'name': _('See Contact List'),
'type': 'ir.actions.act_window',
'view_type': 'form',
'view_mode': 'tree,form',
'res_model': contact_list.model,
'views': [(False, 'tree'), (False, 'form')],
'view_id': False,
'target': 'current',
'context': ctx,
'domain': contact_list.domain,
}
# TODO: remove this?
def action_add_to_mailing(self, cr, uid, ids, context=None):
mass_mailing_id = context.get('default_mass_mailing_id')
if not mass_mailing_id:
return False
self.pool['mail.mass_mailing'].write(cr, uid, [mass_mailing_id], {'contact_list_ids': [(4, list_id) for list_id in ids]}, context=context)
return {
'name': _('Mass Mailing'),
'type': 'ir.actions.act_window',
'view_type': 'form',
'view_mode': 'form',
'res_model': 'mail.mass_mailing',
'res_id': mass_mailing_id,
'context': context,
}
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]['sent'] or 1)
results[cid]['opened_ratio'] = 100.0 * results[cid]['opened'] / (results[cid]['sent'] or 1)
results[cid]['replied_ratio'] = 100.0 * results[cid]['replied'] / (results[cid]['sent'] or 1)
return results
_columns = {
'name': fields.char('Name', required=True),
'stage_id': fields.many2one('mail.mass_mailing.stage', 'Stage', required=True),
'user_id': fields.many2one(
'res.users', 'Responsible',
required=True,
),
'category_ids': fields.many2many(
'mail.mass_mailing.category', 'Categories'),
'mass_mailing_ids': fields.one2many(
'mail.mass_mailing', 'mass_mailing_campaign_id',
'Mass 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, cr, uid, ctx=None: self._get_default_stage_id(cr, uid, context=ctx),
}
#------------------------------------------------------
# API
#------------------------------------------------------
# def get_recipients(self, cr, uid, ids, model=None, context=None):
# """Return the recipints of a mailing campaign. This is based on the statistics
# build for each mailing. """
# Statistics = self.pool['mail.mail.statistics']
# res = dict.fromkeys(ids, False)
# for cid in ids:
# domain = [('mass_mailing_campaign_id', '=', cid)]
# if model:
# domain += [('model', '=', model)]
# stat_ids = Statistics.search(cr, uid, domain, context=context)
# res[cid] = set(stat.res_id for stat in Statistics.browse(cr, uid, stat_ids, context=context))
# return res
class MassMailing(osv.Model):
""" 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 = 'id DESC'
def __get_bar_values(self, cr, uid, id, obj, domain, read_fields, value_field, groupby_field, context=None):
""" Generic method to generate data for bar chart values using SparklineBarWidget.
This method performs obj.read_group(cr, uid, domain, read_fields, groupby_field).
:param obj: the target model (i.e. crm_lead)
:param domain: the domain applied to the read_group
:param list read_fields: the list of fields to read in the read_group
:param str value_field: the field used to compute the value of the bar slice
:param str groupby_field: the fields used to group
:return list section_result: a list of dicts: [
{ 'value': (int) bar_column_value,
'tootip': (str) bar_column_tooltip,
}
]
"""
date_begin = datetime.strptime(self.browse(cr, uid, id, context=context).date, tools.DEFAULT_SERVER_DATETIME_FORMAT).date()
section_result = [{'value': 0,
'tooltip': (date_begin + relativedelta.relativedelta(days=i)).strftime('%d %B %Y'),
} for i in range(0, self._period_number)]
group_obj = obj.read_group(cr, uid, domain, read_fields, groupby_field, context=context)
field_col_info = obj._all_columns.get(groupby_field.split(':')[0])
pattern = tools.DEFAULT_SERVER_DATE_FORMAT if field_col_info.column._type == 'date' else tools.DEFAULT_SERVER_DATETIME_FORMAT
for group in group_obj:
group_begin_date = datetime.strptime(group['__domain'][0][2], pattern).date()
timedelta = relativedelta.relativedelta(group_begin_date, date_begin)
section_result[timedelta.days] = {'value': group.get(value_field, 0), 'tooltip': group.get(groupby_field)}
return section_result
def _get_daily_statistics(self, cr, uid, ids, field_name, arg, context=None):
""" Get the daily statistics of the mass mailing. This is done by a grouping
on opened and replied fields. Using custom format in context, we obtain
results for the next 6 days following the mass mailing date. """
obj = self.pool['mail.mail.statistics']
res = {}
for id in ids:
res[id] = {}
date_begin = datetime.strptime(self.browse(cr, uid, id, context=context).date, tools.DEFAULT_SERVER_DATETIME_FORMAT)
date_end = date_begin + relativedelta.relativedelta(days=self._period_number - 1)
date_begin_str = date_begin.strftime(tools.DEFAULT_SERVER_DATETIME_FORMAT)
date_end_str = date_end.strftime(tools.DEFAULT_SERVER_DATETIME_FORMAT)
domain = [('mass_mailing_id', '=', id), ('opened', '>=', date_begin_str), ('opened', '<=', date_end_str)]
res[id]['opened_dayly'] = json.dumps(self.__get_bar_values(cr, uid, id, obj, domain, ['opened'], 'opened_count', 'opened:day', context=context))
domain = [('mass_mailing_id', '=', id), ('replied', '>=', date_begin_str), ('replied', '<=', date_end_str)]
res[id]['replied_dayly'] = json.dumps(self.__get_bar_values(cr, uid, id, obj, domain, ['replied'], 'replied_count', 'replied:day', context=context))
return res
def _get_statistics(self, cr, uid, ids, name, arg, context=None):
""" Compute statistics of the mass mailing campaign """
Statistics = self.pool['mail.mail.statistics']
results = dict.fromkeys(ids, False)
for mid in ids:
stat_ids = Statistics.search(cr, uid, [('mass_mailing_id', '=', mid)], context=context)
stats = Statistics.browse(cr, uid, stat_ids, context=context)
results[mid] = {
'total': len(stats),
'failed': len([s for s in stats if not s.scheduled is False and s.sent is False and not s.exception is False]),
'scheduled': len([s for s in stats if not s.scheduled is False and s.sent is False and s.exception is False]),
'sent': len([s for s in stats if not s.sent is False]),
'opened': len([s for s in stats if not s.opened is False]),
'replied': len([s for s in stats if not s.replied is False]),
'bounced': len([s for s in stats if not s.bounced is False]),
}
results[mid]['delivered'] = results[mid]['sent'] - results[mid]['bounced']
results[mid]['received_ratio'] = 100.0 * results[mid]['delivered'] / (results[mid]['sent'] or 1)
results[mid]['opened_ratio'] = 100.0 * results[mid]['opened'] / (results[mid]['sent'] or 1)
results[mid]['replied_ratio'] = 100.0 * results[mid]['replied'] / (results[mid]['sent'] or 1)
return results
# To improve
def _get_contact_nbr(self, cr, uid, ids, name, arg, context=None):
res = dict.fromkeys(ids, False)
for mailing in self.browse(cr, uid, ids, context=context):
val = {'contact_nbr': 0, 'contact_ab_nbr': 0, 'contact_ab_done': 0}
val['contact_nbr'] = self.pool[mailing.mailing_model].search(
cr, uid,
self.pool['mail.mass_mailing.list'].get_global_domain(cr, uid, [c.id for c in mailing.contact_list_ids], context=context)[mailing.mailing_model],
count=True, context=context
)
val['contact_ab_nbr'] = int(val['contact_nbr'] * mailing.contact_ab_pc / 100.0)
if mailing.mass_mailing_campaign_id and mailing.ab_testing:
val['contact_ab_done'] = len(self.pool['mail.mass_mailing.campaign'].get_recipients(cr, uid, [mailing.mass_mailing_campaign_id.id], context=context)[mailing.mass_mailing_campaign_id.id])
res[mailing.id] = val
return res
def _get_private_models(self, context=None):
return ['res.partner', 'mail.mass_mailing.contact']
def _get_auto_reply_to_available(self, cr, uid, ids, name, arg, context=None):
res = dict.fromkeys(ids, False)
for mailing in self.browse(cr, uid, ids, context=context):
res[mailing.id] = mailing.mailing_model not in self._get_private_models(context=context)
return res
def _get_mailing_model(self, cr, uid, context=None):
return [
('res.partner', 'Customers'),
('mail.mass_mailing.contact', 'Contacts')
]
_columns = {
'name': fields.char('Subject', required=True),
'email_from': fields.char('From'),
'date': fields.datetime('Date'),
'state': fields.selection(
[('draft', 'Schedule'), ('test', 'Tested'), ('done', 'Sent')], string='Status', required=True,
),
# 'template_id': fields.many2one(
# 'email.template', 'Email Template',
# domain="[('use_in_mass_mailing', '=', True), ('model', '=', mailing_model)]",
# ),
'body_html': fields.html('Body'),
'mass_mailing_campaign_id': fields.many2one(
'mail.mass_mailing.campaign', 'Mass Mailing Campaign',
ondelete='set null',
),
# TODO: to remove
'ab_testing': fields.related(
'mass_mailing_campaign_id', 'ab_testing',
type='boolean', string='AB Testing'
),
'contact_ab_pc': fields.integer(
'AB Testing percentage',
help='Percentage of the contacts that will be mailed. Recipients will be taken randomly.'
),
'contact_ab_nbr': fields.function(
_get_contact_nbr, type='integer', multi='_get_contact_nbr',
string='Contact Number in AB Testing'
),
'contact_ab_done': fields.function(
_get_contact_nbr, type='integer', multi='_get_contact_nbr',
string='Number of already mailed contacts'
),
'color': fields.related(
'mass_mailing_campaign_id', 'color',
type='integer', string='Color Index',
),
# mailing options
'reply_in_thread': fields.boolean('Reply in thread'),
'reply_specified': fields.boolean('Specific Reply-To'),
'auto_reply_to_available': fields.function(
_get_auto_reply_to_available,
type='boolean', string='Reply in thread available'
),
'reply_to': fields.char('Reply To'),
'mailing_model': fields.selection(_get_mailing_model, string='Model', required=True),
'contact_list_ids': fields.many2many(
'mail.mass_mailing.list', 'mail_mass_mailing_list_rel',
string='Mailing Lists',
domain="[('model', '=', mailing_model)]",
),
'contact_nbr': fields.function(
_get_contact_nbr, type='integer', multi='_get_contact_nbr',
string='Contact Number'
),
# statistics data
'statistics_ids': fields.one2many(
'mail.mail.statistics', 'mass_mailing_id',
'Emails Statistics',
),
'total': fields.function(
_get_statistics, string='Total',
type='integer', multi='_get_statistics',
),
'scheduled': fields.function(
_get_statistics, string='Scheduled',
type='integer', multi='_get_statistics',
),
'failed': fields.function(
_get_statistics, string='Failed',
type='integer', multi='_get_statistics',
),
'sent': fields.function(
_get_statistics, string='Sent',
type='integer', multi='_get_statistics',
),
'delivered': fields.function(
_get_statistics, string='Delivered',
type='integer', multi='_get_statistics',
),
'opened': fields.function(
_get_statistics, string='Opened',
type='integer', multi='_get_statistics',
),
'replied': fields.function(
_get_statistics, string='Replied',
type='integer', multi='_get_statistics',
),
'bounced': fields.function(
_get_statistics, string='Bounced',
type='integer', multi='_get_statistics',
),
'received_ratio': fields.function(
_get_statistics, string='Received Ratio',
type='integer', multi='_get_statistics',
),
'opened_ratio': fields.function(
_get_statistics, string='Opened Ratio',
type='integer', multi='_get_statistics',
),
'replied_ratio': fields.function(
_get_statistics, string='Replied Ratio',
type='integer', multi='_get_statistics',
),
# dayly ratio
'opened_dayly': fields.function(
_get_daily_statistics, string='Opened',
type='char', multi='_get_daily_statistics',
oldname='opened_monthly',
),
'replied_dayly': fields.function(
_get_daily_statistics, string='Replied',
type='char', multi='_get_daily_statistics',
oldname='replied_monthly',
),
}
_defaults = {
'state': 'draft',
'date': fields.datetime.now,
'email_from': lambda self, cr, uid, ctx=None: self.pool['mail.message']._get_default_from(cr, uid, context=ctx),
'mailing_model': 'res.partner',
'contact_ab_pc': 100,
}
#------------------------------------------------------
# Technical stuff
#------------------------------------------------------
def copy_data(self, cr, uid, id, default=None, context=None):
if default is None:
default = {}
mailing = self.browse(cr, uid, id, context=context)
default.update({
'state': 'draft',
'statistics_ids': [],
'name': _('%s (duplicate)') % mailing.name,
})
return super(MassMailing, self).copy_data(cr, uid, id, default, context=context)
#------------------------------------------------------
# Views & Actions
#------------------------------------------------------
def on_change_mailing_model(self, cr, uid, ids, mailing_model, context=None):
values = {
'contact_list_ids': [],
'template_id': False,
'contact_nbr': 0,
'auto_reply_to_available': not mailing_model in self._get_private_models(context),
'reply_in_thread': not mailing_model in self._get_private_models(context),
'reply_specified': mailing_model in self._get_private_models(context)
}
return {'value': values}
def on_change_reply_specified(self, cr, uid, ids, reply_specified, reply_in_thread, context=None):
if reply_specified == reply_in_thread:
return {'value': {'reply_in_thread': not reply_specified}}
return {}
def on_change_reply_in_thread(self, cr, uid, ids, reply_specified, reply_in_thread, context=None):
if reply_in_thread == reply_specified:
return {'value': {'reply_specified': not reply_in_thread}}
return {}
def on_change_contact_list_ids(self, cr, uid, ids, mailing_model, contact_list_ids, context=None):
values = {}
list_ids = []
for command in contact_list_ids:
if command[0] == 6:
list_ids += command[2]
if list_ids:
values['contact_nbr'] = self.pool[mailing_model].search(
cr, uid,
self.pool['mail.mass_mailing.list'].get_global_domain(cr, uid, list_ids, context=context)[mailing_model],
count=True, context=context
)
return {'value': values}
def on_change_template_id(self, cr, uid, ids, template_id, context=None):
values = {}
if template_id:
template = self.pool['email.template'].browse(cr, uid, template_id, context=context)
if template.email_from:
values['email_from'] = template.email_from
if template.reply_to:
values['reply_to'] = template.reply_to
values['body_html'] = template.body_html
else:
values['email_from'] = self.pool['mail.message']._get_default_from(cr, uid, context=context)
values['reply_to'] = False
values['body_html'] = False
return {'value': values}
def on_change_contact_ab_pc(self, cr, uid, ids, contact_ab_pc, contact_nbr, context=None):
return {'value': {'contact_ab_nbr': contact_nbr * contact_ab_pc / 100.0}}
def action_duplicate(self, cr, uid, ids, context=None):
copy_id = None
for mid in ids:
copy_id = self.copy(cr, uid, mid, context=context)
if copy_id:
return {
'type': 'ir.actions.act_window',
'view_type': 'form',
'view_mode': 'form',
'res_model': 'mail.mass_mailing',
'res_id': copy_id,
'context': context,
}
return False
def _get_model_to_list_action_id(self, cr, uid, model, context=None):
if model == 'res.partner':
return self.pool['ir.model.data'].xmlid_to_res_id(cr, uid, 'mass_mailing.action_partner_to_mailing_list')
else:
return self.pool['ir.model.data'].xmlid_to_res_id(cr, uid, 'mass_mailing.action_contact_to_mailing_list')
def action_new_list(self, cr, uid, ids, context=None):
mailing = self.browse(cr, uid, ids[0], context=context)
action_id = self._get_model_to_list_action_id(cr, uid, mailing.mailing_model, context=context)
ctx = dict(context,
search_default_not_opt_out=True,
view_manager_highlight=[action_id],
default_name=mailing.name,
default_mass_mailing_id=ids[0],
default_model=mailing.mailing_model)
return {
'name': _('Choose Recipients'),
'type': 'ir.actions.act_window',
'view_type': 'form',
'view_mode': 'tree,form',
'res_model': mailing.mailing_model,
'context': ctx,
}
def action_see_recipients(self, cr, uid, ids, context=None):
mailing = self.browse(cr, uid, ids[0], context=context)
domain = self.pool['mail.mass_mailing.list'].get_global_domain(cr, uid, [c.id for c in mailing.contact_list_ids], context=context)[mailing.mailing_model]
return {
'name': _('See Recipients'),
'type': 'ir.actions.act_window',
'view_type': 'form',
'view_mode': 'tree,form',
'res_model': mailing.mailing_model,
'target': 'new',
'domain': domain,
'context': context,
}
def action_test_mailing(self, cr, uid, ids, context=None):
ctx = dict(context, default_mass_mailing_id=ids[0])
return {
'name': _('Test Mailing'),
'type': 'ir.actions.act_window',
'view_mode': 'form',
'res_model': 'mail.mass_mailing.test',
'target': 'new',
'context': ctx,
}
def action_edit_html(self, cr, uid, ids, context=None):
url = '/website_mail/email_designer?model=mail.mass_mailing&res_id=%d' % ids[0]
return {
'name': _('Open with Visual Editor'),
'type': 'ir.actions.act_url',
'url': url,
'target': 'self',
}
#------------------------------------------------------
# Email Sending
#------------------------------------------------------
def get_recipients_data(self, cr, uid, mailing, res_ids, context=None):
# tde todo: notification link ?
if mailing.mailing_model == 'mail.mass_mailing.contact':
contacts = self.pool['mail.mass_mailing.contact'].browse(cr, uid, res_ids, context=context)
return dict((contact.id, {'partner_id': False, 'name': contact.name, 'email': contact.email}) for contact in contacts)
else:
partners = self.pool['res.partner'].browse(cr, uid, res_ids, context=context)
return dict((partner.id, {'partner_id': partner.id, 'name': partner.name, 'email': partner.email}) for partner in partners)
def get_recipients(self, cr, uid, mailing, context=None):
domain = self.pool['mail.mass_mailing.list'].get_global_domain(
cr, uid, [l.id for l in mailing.contact_list_ids], context=context
)[mailing.mailing_model]
res_ids = self.pool[mailing.mailing_model].search(cr, uid, domain, context=context)
# randomly choose a fragment
if mailing.contact_ab_pc != 100:
topick = mailing.contact_ab_nbr
if mailing.mass_mailing_campaign_id and mailing.ab_testing:
already_mailed = self.pool['mail.mass_mailing.campaign'].get_recipients(cr, uid, [mailing.mass_mailing_campaign_id.id], context=context)[mailing.mass_mailing_campaign_id.id]
else:
already_mailed = set([])
remaining = set(res_ids).difference(already_mailed)
if topick > len(remaining):
topick = len(remaining)
res_ids = random.sample(remaining, topick)
return res_ids
def get_unsubscribe_url(self, cr, uid, mailing_id, res_id, email, msg=None, context=None):
base_url = self.pool.get('ir.config_parameter').get_param(cr, uid, 'web.base.url')
url = urlparse.urljoin(
base_url, 'mail/mailing/%(mailing_id)s/unsubscribe?%(params)s' % {
'mailing_id': mailing_id,
'params': urllib.urlencode({'db': cr.dbname, 'res_id': res_id, 'email': email})
}
)
return '<small><a href="%s">%s</a></small>' % (url, msg or 'Click to unsubscribe')
def send_mail(self, cr, uid, ids, context=None):
author_id = self.pool['res.users'].browse(cr, uid, uid, context=context).partner_id.id
for mailing in self.browse(cr, uid, ids, context=context):
if not mailing.contact_nbr:
raise Warning('Please select recipients.')
# instantiate an email composer + send emails
res_ids = self.get_recipients(cr, uid, mailing, context=context)
comp_ctx = dict(context, active_ids=res_ids)
composer_values = {
'author_id': author_id,
'body': mailing.body_html,
'subject': mailing.name,
'model': mailing.mailing_model,
'email_from': mailing.email_from,
'record_name': False,
'composition_mode': 'mass_mail',
'mass_mailing_id': mailing.id,
'mailing_list_ids': [(4, l.id) for l in mailing.contact_list_ids],
}
if mailing.reply_specified:
composer_values['reply_to'] = mailing.reply_to
composer_id = self.pool['mail.compose.message'].create(cr, uid, composer_values, context=comp_ctx)
self.pool['mail.compose.message'].send_mail(cr, uid, [composer_id], context=comp_ctx)
self.write(cr, uid, [mailing.id], {'date': fields.datetime.now(), 'state': 'done'}, context=context)
return True
# Merge this on emails
class MailMailStats(osv.Model):
""" MailMailStats models the statistics collected about emails. Those statistics
are stored in a separated model and table to avoid bloating the mail_mail table
with statistics values. This also allows to delete emails send with mass mailing
without loosing the statistics about them. """
_name = 'mail.mail.statistics'
_description = 'Email Statistics'
_rec_name = 'message_id'
_order = 'message_id'
_columns = {
'mail_mail_id': fields.integer(
'Mail ID',
help='ID of the related mail_mail. This field is an integer field because'
'the related mail_mail can be deleted separately from its statistics.'
),
'message_id': fields.char('Message-ID'),
'model': fields.char('Document model'),
'res_id': fields.integer('Document ID'),
# campaign / wave data
'mass_mailing_id': fields.many2one(
'mail.mass_mailing', 'Mass Mailing',
ondelete='set null',
),
'mass_mailing_campaign_id': fields.related(
'mass_mailing_id', 'mass_mailing_campaign_id',
type='many2one', ondelete='set null',
relation='mail.mass_mailing.campaign',
string='Mass Mailing Campaign',
store=True, readonly=True,
),
'template_id': fields.related(
'mass_mailing_id', 'template_id',
type='many2one', ondelete='set null',
relation='email.template',
string='Email Template',
store=True, readonly=True,
),
# Bounce and tracking
'scheduled': fields.datetime('Scheduled', help='Date when the email has been created'),
'sent': fields.datetime('Sent', help='Date when the email has been sent'),
'exception': fields.datetime('Exception', help='Date of technical error leading to the email not being sent'),
'opened': fields.datetime('Opened', help='Date when the email has been opened the first time'),
'replied': fields.datetime('Replied', help='Date when this email has been replied for the first time.'),
'bounced': fields.datetime('Bounced', help='Date when this email has bounced.'),
}
_defaults = {
'scheduled': fields.datetime.now,
}
def _get_ids(self, cr, uid, ids=None, mail_mail_ids=None, mail_message_ids=None, domain=None, context=None):
if not ids and mail_mail_ids:
base_domain = [('mail_mail_id', 'in', mail_mail_ids)]
elif not ids and mail_message_ids:
base_domain = [('message_id', 'in', mail_message_ids)]
else:
base_domain = [('id', 'in', ids or [])]
if domain:
base_domain = ['&'] + domain + base_domain
return self.search(cr, uid, base_domain, context=context)
def set_opened(self, cr, uid, ids=None, mail_mail_ids=None, mail_message_ids=None, context=None):
stat_ids = self._get_ids(cr, uid, ids, mail_mail_ids, mail_message_ids, [('opened', '=', False)], context)
self.write(cr, uid, stat_ids, {'opened': fields.datetime.now()}, context=context)
return stat_ids
def set_replied(self, cr, uid, ids=None, mail_mail_ids=None, mail_message_ids=None, context=None):
stat_ids = self._get_ids(cr, uid, ids, mail_mail_ids, mail_message_ids, [('replied', '=', False)], context)
self.write(cr, uid, stat_ids, {'replied': fields.datetime.now()}, context=context)
return stat_ids
def set_bounced(self, cr, uid, ids=None, mail_mail_ids=None, mail_message_ids=None, context=None):
stat_ids = self._get_ids(cr, uid, ids, mail_mail_ids, mail_message_ids, [('bounced', '=', False)], context)
self.write(cr, uid, stat_ids, {'bounced': fields.datetime.now()}, context=context)
return stat_ids

View File

@ -0,0 +1,30 @@
# -*- coding: utf-8 -*-
from openerp.osv import fields, osv
from openerp import tools
class MassMailingnReport(osv.Model):
_name = 'mail.mass_mailing.report'
_auto = False
_description = 'Mass Mailing Analysis'
_rec_name = 'mailing_date'
_columns = {
'mailing_date': fields.datetime('Mailing Date', readonly=True),
}
def init(self, cr):
""" Mass Mailing Report: based on mail.mass_mailing model that models the
various mailing performed, and mail.mail.statistics that models the various
statistics collected for each mailing. """
tools.drop_view_if_exists(cr, 'mail_mass_mailing_report')
cr.execute("""
CREATE OR REPLACE VIEW mail_mass_mailing_report AS (
SELECT
id,
date_trunc('day', m.date) as mailing_date
FROM
mail_mass_mailing m
)""")

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,6 +1,11 @@
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
access_mass_mailing_system,mail.mass_mailing.system,model_mail_mass_mailing,base.group_system,1,1,1,1
access_mail_mail_statistics,mail.mail.statistics,model_mail_mail_statistics,base.group_user,1,1,1,1
access_mail_mail_statistics,mail.mail.statistics,model_mail_mail_statistics,base.group_user,1,1,1,1
access_mass_mailing_report,mail.mass_mailing.report,model_mail_mass_mailing_report,base.group_user,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_mass_mailing_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
9 access_mass_mailing_system mail.mass_mailing.system model_mail_mass_mailing base.group_system 1 1 1 1
10 access_mail_mail_statistics mail.mail.statistics model_mail_mail_statistics base.group_user 1 1 1 1
11 access_mass_mailing_report mail.mass_mailing.report model_mail_mass_mailing_report base.group_user 1 1 1 1

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,11 @@
.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;
}
.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;
}
.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

@ -3,7 +3,7 @@ openerp.mass_mailing = function(openerp) {
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,127 @@
<?xml version="1.0"?>
<openerp>
<data>
<!-- Email Templates -->
<record id="email_template_form_mass_mailing" model="ir.ui.view">
<field name="name">email.template.form.mass.mailing</field>
<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_id']" position="after">
<field name="use_in_mass_mailing"/>
</xpath>
</field>
</record>
<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"/>
<field name="use_in_mass_mailing" 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 id="view_email_template_search_mass_mailing" model="ir.ui.view">
<field name="name">email.template.search.mass.mailing</field>
<field name="model">email.template</field>
<field name="inherit_id" ref="email_template.view_email_template_search"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='model_id']" position="after">
<separator/>
<filter string="Available for Marketing and Mailing" name="use_in_mass_mailing" domain="[('use_in_mass_mailing', '=', 1)]"
help="Available for use in mass mailings"/>
</xpath>
</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>
<li><a name="action_new_mailing" type="object">New Mailing</a></li>
</t>
<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="domain" eval="[('use_in_mass_mailing', '=', True)]"/>
<field name="context">{
'form_view_ref': 'mass_mailing.email_template_form_minimal',
'default_use_default_to': True,
'default_use_in_mass_mailing': 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,703 @@
<?xml version="1.0"?>
<openerp>
<data>
<!-- Marketing / Mass Mailing -->
<menuitem name="Mass Mailing" id="mass_mailing_campaign"
parent="base.marketing_menu" sequence="1"/>
<!-- Marketing / Mailing Lists -->
<menuitem name="Contact Lists" id="mass_mailing_list"
parent="base.marketing_menu" sequence="2"/>
<!-- Marketing / Configuration -->
<menuitem name="Configuration" id="marketing_configuration"
parent="base.marketing_menu" sequence="99"/>
<!-- MASS MAILING CONTACT !-->
<record model="ir.ui.view" id="view_mail_mass_mailing_contact_search">
<field name="name">mail.mass_mailing.contact.search</field>
<field name="model">mail.mass_mailing.contact</field>
<field name="arch" type="xml">
<search string="Mass Mailings">
<field name="name"/>
<field name="email"/>
<field name="list_id"/>
<separator/>
<filter string="Available for Mass Mailing" name="not_opt_out" domain="[('opt_out', '=', False)]"
help="Contact is not opt-out"/>
<group expand="0" string="Group By...">
<filter string="Mailing Lists" name="group_list_id"
context="{'group_by': 'list_id'}"/>
</group>
</search>
</field>
</record>
<record model="ir.ui.view" id="view_mail_mass_mailing_contact_tree">
<field name="name">mail.mass_mailing.contact.tree</field>
<field name="model">mail.mass_mailing.contact</field>
<field name="priority">10</field>
<field name="arch" type="xml">
<tree string="Mass Mailings">
<field name="name"/>
<field name="email"/>
<field name="list_id"/>
<field name="opt_out"/>
</tree>
</field>
</record>
<record model="ir.ui.view" id="view_mail_mass_mailing_contact_form">
<field name="name">mail.mass_mailing.contact.form</field>
<field name="model">mail.mass_mailing.contact</field>
<field name="arch" type="xml">
<form string="Mass Mailing" version="7.0">
<sheet>
<group>
<field name="name"/>
<field name="email"/>
<field name="list_id"/>
<field name="opt_out"/>
</group>
</sheet>
</form>
</field>
</record>
<record id="action_view_mass_mailing_contacts" model="ir.actions.act_window">
<field name="name">Mass Mailing Contacts</field>
<field name="res_model">mail.mass_mailing.contact</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form</field>
<field name="context">{'search_default_not_opt_out': 1}</field>
</record>
<menuitem name="Contacts" id="menu_email_mass_mailing_contacts" groups="base.group_no_one"
parent="mass_mailing_list" sequence="50"
action="action_view_mass_mailing_contacts"/>
<!-- Create a Mailing List from Contacts -->
<act_window name="Create Mailing List"
res_model="mail.mass_mailing.list.confirm"
src_model="mail.mass_mailing.contact"
view_mode="form"
multi="True"
target="new"
key2="client_action_multi"
id="action_contact_to_mailing_list"
context="{
'default_mass_mailing_id': context.get('default_mass_mailing_id'),
'default_model': context.get('default_model', 'mail.mass_mailing.contact'),
'default_name': context.get('default_name', False)}"/>
<!-- MASS MAILING LIST !-->
<record model="ir.ui.view" id="view_mail_mass_mailing_list_search">
<field name="name">mail.mass_mailing.list.search</field>
<field name="model">mail.mass_mailing.list</field>
<field name="arch" type="xml">
<search string="Mass Mailings">
<field name="name"/>
<separator/>
</search>
</field>
</record>
<record model="ir.ui.view" id="view_mail_mass_mailing_list_tree">
<field name="name">mail.mass_mailing.list.tree</field>
<field name="model">mail.mass_mailing.list</field>
<field name="priority">10</field>
<field name="arch" type="xml">
<tree string="Contact Lists">
<field name="name"/>
<field name="model"/>
<field name="contact_nbr"/>
</tree>
</field>
</record>
<record model="ir.ui.view" id="view_mail_mass_mailing_list_form">
<field name="name">mail.mass_mailing.list.form</field>
<field name="model">mail.mass_mailing.list</field>
<field name="arch" type="xml">
<form string="Contact List" version="7.0">
<header>
<button name="action_add_to_mailing" type="object"
class="oe_highlight" string="Continue to Mailing"
invisible="not context.get('default_mass_mailing_id')"/>
</header>
<sheet>
<group>
<field name="name" string="Mailing List Name"/>
<label for="contact_nbr"/>
<div>
<field name="contact_nbr" nolabel="1" class="oe_inline"/>
<field name="model" class="oe_inline"
on_change="on_change_model(model, context)" nolabel="1"/>
<button string="See Recipients" class="oe_inline oe_link" style="margin-left: 8px;"
name="action_see_records" type="object"/>
</div>
<field name="filter_id" groups="base.group_no_one"
on_change="on_change_filter_id(filter_id, context)"/>
<field name="domain" groups="base.group_no_one"
on_change="on_change_domain(domain, model, context)"/>
</group>
</sheet>
</form>
</field>
</record>
<record id="action_view_mass_mailing_lists" model="ir.actions.act_window">
<field name="name">Contact Lists</field>
<field name="res_model">mail.mass_mailing.list</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form</field>
<field name="help" type="html">
<p class="oe_view_nocontent_create">
Click here to create a new mailing list.
</p><p>
Mailing lists allows you to to manage customers and contacts easily and to send to mailings in a single click.
</p></field>
</record>
<menuitem name="Contact Lists" id="menu_email_mass_mailing_lists"
parent="mass_mailing_list" sequence="40"
action="action_view_mass_mailing_lists"/>
<!-- MASS MAILING !-->
<record model="ir.ui.view" id="view_mail_mass_mailing_search">
<field name="name">mail.mass_mailing.search</field>
<field name="model">mail.mass_mailing</field>
<field name="arch" type="xml">
<search string="Mass Mailings">
<field name="name" string="Mailings"/>
<field name="mass_mailing_campaign_id"/>
<field name="template_id"/>
<group expand="0" string="Group By...">
<filter string="State" name="group_state"
context="{'group_by': 'state'}"/>
<filter string="Campaign" name="group_mass_mailing_campaign_id"
groups="mass_mailing.group_mass_mailing_campaign"
context="{'group_by': 'mass_mailing_campaign_id'}"/>
<filter string="Template" name="group_template_id"
context="{'group_by': 'template_id'}"/>
</group>
</search>
</field>
</record>
<record model="ir.ui.view" id="view_mail_mass_mailing_tree">
<field name="name">mail.mass_mailing.tree</field>
<field name="model">mail.mass_mailing</field>
<field name="priority">10</field>
<field name="arch" type="xml">
<tree string="Mass Mailings">
<field name="name"/>
<field name="sent"/>
<field name="delivered"/>
<field name="opened"/>
<field name="replied"/>
<field name="mass_mailing_campaign_id"
groups="mass_mailing.group_mass_mailing_campaign"/>
<field name="template_id" invisible="1"/>
</tree>
</field>
</record>
<record model="ir.ui.view" id="view_mail_mass_mailing_form">
<field name="name">mail.mass_mailing.form</field>
<field name="model">mail.mass_mailing</field>
<field name="arch" type="xml">
<form string="Mass Mailing" version="7.0">
<header>
<button name="action_test_mailing" type="object"
class="oe_highlight" string="Test Mailing"/>
<button name="send_mail" type="object"
class="oe_highlight" string="Send to All"/>
<field name="state" widget="statusbar" clickable="True"/>
</header>
<sheet>
<div colspan="2" class="oe_form_box_info oe_text_center"
attrs="{'invisible': [('scheduled', '=', 0)]}">
<p>
<strong><field name="scheduled" class="oe_inline"/>emails are in queue
and will be sent soon.</strong>
</p>
</div>
<group>
<group>
<field name="email_from"/>
<field name="name"/>
</group>
<group>
<div class="oe_right oe_button_box" name="buttons">
<div>
<button name="action_see_recipients" type="object"
icon="fa-user" class="oe_stat_button">
<field name="contact_nbr" string="Recipients" widget="statinfo"/>
</button>
<button name="%(action_mail_mass_mailing_report)d" type="action"
icon="fa-envelope-o" class="oe_stat_button">
<field name="total" string="Emails" widget="statinfo"/>
</button>
</div>
<div style="margin-top: 8px;"
attrs="{'invisible': [('total', '=', 0)]}">
<button name="%(action_mail_mass_mailing_report)d"
type="action" class="oe_stat_button">
<field name="received_ratio" string="Received" widget="percentpie"/>
</button>
<button name="%(action_mail_mass_mailing_report)d"
type="action" class="oe_stat_button">
<field name="opened_ratio" string="Opened" widget="percentpie"/>
</button>
<button name="%(action_mail_mass_mailing_report)d"
type="action" class="oe_stat_button">
<field name="replied_ratio" string="Replied" widget="percentpie"/>
</button>
</div>
<div style="margin-top: 8px;"
attrs="{'invisible': [('total', '=', 0)]}">
<button name="%(action_mail_mass_mailing_report)d"
type="action" class="oe_stat_button oe_inline">
<field name="opened_dayly" string="Opened Daily" widget="barchart"/>
</button>
<button name="%(action_mail_mass_mailing_report)d"
type="action" class="oe_stat_button oe_inline">
<field name="replied_dayly" string="Replied Daily" widget="barchart"/>
</button>
</div>
</div>
</group>
</group>
<group>
<label for="reply_to"/>
<div>
<field name="auto_reply_to_available" invisible="1"/>
<field name="reply_in_thread" class="oe_inline"
on_change="on_change_reply_in_thread(reply_specified, reply_in_thread, context)"
attrs="{'readonly': [('auto_reply_to_available', '=', False)]}"/>
<span attrs="{'invisible': [('auto_reply_to_available', '=', False)]}">
Replies go into the original document
</span>
<span class="oe_grey" attrs="{'invisible': [('auto_reply_to_available', '=', True)]}">
Replies go into the original document (not available for those recipients)
</span>
<br />
<field name="reply_specified" class="oe_inline"
on_change="on_change_reply_specified(reply_specified, reply_in_thread, context)"/> Use a specific reply-to address
<field name="reply_to" class="oe_inline"
style="margin-left: 8px;"
attrs="{'required': [('reply_specified', '=', True)]}"/>
</div>
<label for="mailing_model" string="Recipients"/>
<div>
<field name="mailing_model" widget="radio"
on_change='on_change_mailing_model(mailing_model, context)'/>
<label for="contact_list_ids" string="Mailing Lists" style="display: inline-block; min-width: 90px;"/>
<field name="contact_list_ids" widget="many2many_tags" options="{'no_create': True}"
class="oe_inline" placeholder="Choose mailing lists"
on_change="on_change_contact_list_ids(mailing_model, contact_list_ids, context)"/>
<span style="margin-left: 8px; margin-right: 8px">or</span>
<button string='Create a New List' class="oe_link" type='object' name='action_new_list'/><br />
<div groups="mass_mailing.group_mass_mailing_campaign" style="display: inline;">
<field name="ab_testing" invisible="1"/>
<label for="contact_ab_pc" string="AB Testing" style="display: inline-block; min-width: 90px;"/>
Email <field name="contact_ab_pc" class="oe_inline"
on_change="on_change_contact_ab_pc(contact_ab_pc, contact_nbr, context)"/>
<strong>%</strong> of recipients
(<field name="contact_ab_nbr" class="oe_inline"/> recipients)
<div attrs="{'invisible': [('ab_testing', '=', False)]}" style="display: inline;">
<span>(</span>
<field name="contact_ab_done" class="oe_inline"
attrs="{'invisible': [('ab_testing', '=', False)]}"/> already mailed
<span>)</span>
</div>
</div>
</div>
<field name="date" readonly="True" groups="mass_mailing.group_mass_mailing_campaign"
attrs="{'invisible': [('state', '!=', 'done')]}"/>
<field name="mass_mailing_campaign_id" groups="mass_mailing.group_mass_mailing_campaign"/>
<label for="body_html" string="Email"/>
<div>
<label for="template_id" string="Template"/>
<field name="template_id" string="Select Template"
class="oe_inline" options="{'no_create': True, 'no_open': True}"
on_change="on_change_template_id(template_id, context)"/><br />
<button name="action_edit_html" type="object" string="Edit Mail Content"
class="oe_link" style="margin-left: 8px"/>
<field name="body_html"/>
</div>
</group>
</sheet>
</form>
</field>
</record>
<record model="ir.ui.view" id="view_mail_mass_mailing_kanban">
<field name="name">mail.mass_mailing.kanban</field>
<field name="model">mail.mass_mailing</field>
<field name="arch" type="xml">
<kanban default_group_by='state'>
<field name='color'/>
<field name='total'/>
<templates>
<t t-name="kanban-box">
<div t-attf-class="oe_kanban_color_#{kanban_getcolor(record.color.raw_value)} oe_kanban_card oe_kanban_global_click oe_kanban_mass_mailing">
<div class="oe_dropdown_toggle oe_dropdown_kanban">
<span class="oe_e">i</span>
<ul class="oe_dropdown_menu">
<t t-if="widget.view.is_action_enabled('delete')">
<li><a type="delete">Delete</a></li>
</t>
</ul>
</div>
<div class="oe_kanban_content">
<div>
<h3><field name="name"/></h3>
<h4 style="display: inline;"><field name="mass_mailing_campaign_id" groups="mass_mailing.group_mass_mailing_campaign"/></h4>
<t t-if="record.mass_mailing_campaign_id.raw_value" groups="mass_mailing.group_mass_mailing_campaign"> - </t><field name="date"/>
</div>
<div>
<div style="display: inline-block">
<field name="delivered" widget="gauge" style="width:120px; height: 90px;"
options="{'max_field': 'total'}"/>
</div>
<div style="display: inline-block; vertical-align: top;">
<strong>Opened</strong> <field name="opened_ratio"/> %<br />
<strong>Replied</strong> <field name="replied_ratio"/> %
</div>
</div>
</div>
<div class="oe_clear"></div>
</div>
</t>
</templates>
</kanban>
</field>
</record>
<record id="action_view_mass_mailings" model="ir.actions.act_window">
<field name="name">Mass Mailings</field>
<field name="res_model">mail.mass_mailing</field>
<field name="view_type">form</field>
<field name="view_mode">kanban,tree,form</field>
<field name="help" type="html">
<p class="oe_view_nocontent_create">
Click here to create a new mailing.
</p><p>
Mass mailing allows you to to easily design and send mass mailings to your contacts, customers or leads using mailing lists.
</p></field>
</record>
<record id="action_view_mass_mailings_from_campaign" model="ir.actions.act_window">
<field name="name">Mass Mailings</field>
<field name="res_model">mail.mass_mailing</field>
<field name="view_type">form</field>
<field name="view_mode">kanban,tree,form</field>
<field name="context">{
'search_default_mass_mailing_campaign_id': [active_id],
'default_mass_mailing_campaign_id': active_id,
}
</field>
<field name="help" type="html">
<p class="oe_view_nocontent_create">
Click here to create a new mailing.
</p><p>
Mass mailing allows you to to easily design and send mass mailings to your contacts, customers or leads using mailing lists.
</p></field>
</record>
<menuitem name="Mass Mailings" id="menu_email_mass_mailings"
parent="mass_mailing_campaign" sequence="2"
action="action_view_mass_mailings"/>
<!-- MASS MAILING CAMPAIGN STAGE !-->
<record model="ir.ui.view" id="view_mail_mass_mailing_stage_search">
<field name="name">mail.mass_mailing.stage.search</field>
<field name="model">mail.mass_mailing.stage</field>
<field name="arch" type="xml">
<search string="Mass Mailings">
<field name="name"/>
</search>
</field>
</record>
<record model="ir.ui.view" id="view_mail_mass_mailing_stage_tree">
<field name="name">mail.mass_mailing.stage.tree</field>
<field name="model">mail.mass_mailing.stage</field>
<field name="priority">10</field>
<field name="arch" type="xml">
<tree string="Mass Mailings">
<field name="name"/>
<field name="sequence"/>
</tree>
</field>
</record>
<record model="ir.ui.view" id="view_mail_mass_mailing_stage_form">
<field name="name">mail.mass_mailing.stage.form</field>
<field name="model">mail.mass_mailing.stage</field>
<field name="arch" type="xml">
<form string="Mass Mailing" version="7.0">
<sheet>
<group>
<field name="name"/>
<field name="sequence"/>
</group>
</sheet>
</form>
</field>
</record>
<record id="action_view_mass_mailing_stages" model="ir.actions.act_window">
<field name="name">Mass Mailing Stages</field>
<field name="res_model">mail.mass_mailing.stage</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form</field>
</record>
<menuitem name="Campaign Stages" id="menu_view_mass_mailing_stages"
parent="marketing_configuration" sequence="1"
action="action_view_mass_mailing_stages"/>
<!-- MASS MAILING CAMPAIGNS !-->
<record model="ir.ui.view" id="view_mail_mass_mailing_campaign_search">
<field name="name">mail.mass_mailing.campaign.search</field>
<field name="model">mail.mass_mailing.campaign</field>
<field name="arch" type="xml">
<search string="Mass Mailing Campaigns">
<field name="name" string="Campaigns"/>
<field name="category_id"/>
<field name="user_id"/>
<group expand="0" string="Group By...">
<filter string="Stage" name="group_stage_id"
context="{'group_by': 'stage_id'}"/>
<filter string="Responsible" name="group_user_id"
context="{'group_by': 'user_id'}"/>
<filter string="Category" name="group_category_id"
context="{'group_by': 'category_id'}"/>
</group>
</search>
</field>
</record>
<record model="ir.ui.view" id="view_mail_mass_mailing_campaign_tree">
<field name="name">mail.mass_mailing.campaign.tree</field>
<field name="model">mail.mass_mailing.campaign</field>
<field name="priority">10</field>
<field name="arch" type="xml">
<tree string="Mass Mailing Campaigns">
<field name="name"/>
<field name="user_id"/>
<field name="stage_id"/>
<field name="category_id"/>
</tree>
</field>
</record>
<record model="ir.ui.view" id="view_mail_mass_mailing_campaign_form">
<field name="name">mail.mass_mailing.campaign.form</field>
<field name="model">mail.mass_mailing.campaign</field>
<field name="arch" type="xml">
<form string="Mass Mailing Campaign" version="7.0">
<header>
<button name="action_new_mailing" type="object" class="oe_highlight" string="New Mailing"/>
<field name="stage_id" widget="statusbar" clickable="True"/>
</header>
<sheet>
<group>
<group>
<field name="name"/>
<field name="user_id"/>
<field name="category_id"/>
</group>
<group>
<field name="total" invisible="1"/>
<div class="oe_right oe_button_box" name="buttons"
attrs="{'invisible': [('total', '=', 0)]}">
<button name="%(action_mail_mass_mailing_report)d"
type="action" class="oe_stat_button oe_inline">
<field name="received_ratio" widget="percentpie"/>
<span>Received</span>
</button>
<button name="%(action_mail_mass_mailing_report)d"
type="action" class="oe_stat_button oe_inline">
<field name="opened_ratio" widget="percentpie"/>
<span>Opened</span>
</button>
<button name="%(action_mail_mass_mailing_report)d"
type="action" class="oe_stat_button oe_inline">
<field name="replied_ratio" widget="percentpie"/>
<span>Replied</span>
</button>
</div>
</group>
</group>
<strong>Related Mailing(s)</strong>
<field name="mass_mailing_ids" readonly="1" string="Related Mailing(s)">
<tree>
<field name="name"/>
<field name="date"/>
<field name="state"/>
<field name="delivered"/>
<field name="opened"/>
<field name="replied"/>
<field name="bounced"/>
<button name="action_duplicate" type="object" string="Duplicate"/>
</tree>
</field>
</sheet>
</form>
</field>
</record>
<record model="ir.ui.view" id="view_mail_mass_mailing_campaign_kanban">
<field name="name">mail.mass_mailing.campaign.kanban</field>
<field name="model">mail.mass_mailing.campaign</field>
<field name="arch" type="xml">
<kanban default_group_by='stage_id'>
<field name='total'/>
<field name='color'/>
<field name='user_id'/>
<field name='mass_mailing_ids'/>
<templates>
<t t-name="kanban-box">
<div t-attf-class="oe_kanban_color_#{kanban_getcolor(record.color.raw_value)} oe_kanban_card oe_kanban_global_click oe_kanban_mass_mailing_campaign">
<div class="oe_dropdown_toggle oe_dropdown_kanban">
<span class="oe_e">i</span>
<ul class="oe_dropdown_menu">
<t t-if="widget.view.is_action_enabled('edit')">
<li><a type="edit">Settings</a></li>
</t>
<t t-if="widget.view.is_action_enabled('delete')">
<li><a type="delete">Delete</a></li>
</t>
<li><ul class="oe_kanban_colorpicker" data-field="color"/></li>
</ul>
</div>
<div class="oe_kanban_content">
<div>
<img t-att-src="kanban_image('res.users', 'image_small', record.user_id.raw_value)"
t-att-title="record.user_id.value" width="24" height="24" class="oe_kanban_avatar oe_kanban_header_right"/>
<h3 style="margin-bottom: 8px;"><field name="name"/></h3>
<span class="oe_tag"><field name="category_id"/></span>
<a name="%(action_view_mass_mailings_from_campaign)d" type="action"
class="oe_mailings">
<h4 style="margin-top: 8px;"><t t-raw="record.mass_mailing_ids.raw_value.length"/> Mailings</h4>
</a>
</div>
<div class="oe_clear"></div>
<div>
<div style="display: inline-block">
<field name="delivered" widget="gauge" style="width:120px; height: 90px;"
options="{'max_field': 'total'}"/>
</div>
<div style="display: inline-block; vertical-align: top;">
<strong>Opened</strong> <field name="opened_ratio"/> %<br />
<strong>Replied</strong> <field name="replied_ratio"/> %
</div>
</div>
</div>
<div class="oe_clear"></div>
</div>
</t>
</templates>
</kanban>
</field>
</record>
<record id="action_view_mass_mailing_campaigns" model="ir.actions.act_window">
<field name="name">Mass Mailing Campaigns</field>
<field name="res_model">mail.mass_mailing.campaign</field>
<field name="view_type">form</field>
<field name="view_mode">kanban,tree,form</field>
<field name="help" type="html">
<p class="oe_view_nocontent_create">
Click to define a new mass mailing campaign.
</p><p>
Create a campaign to structure mass mailing and get analysis from email status.
</p>
</field>
</record>
<menuitem name="Campaigns" id="menu_email_campaigns"
parent="mass_mailing_campaign" sequence="1"
action="action_view_mass_mailing_campaigns"
groups="mass_mailing.group_mass_mailing_campaign"/>
<!-- MAIL MAIL STATISTICS !-->
<record model="ir.ui.view" id="view_mail_mail_statistics_search">
<field name="name">mail.mail.statistics.search</field>
<field name="model">mail.mail.statistics</field>
<field name="arch" type="xml">
<search string="Mail Statistics">
<field name="mail_mail_id"/>
<field name="message_id"/>
</search>
</field>
</record>
<record model="ir.ui.view" id="view_mail_mail_statistics_tree">
<field name="name">mail.mail.statistics.tree</field>
<field name="model">mail.mail.statistics</field>
<field name="arch" type="xml">
<tree string="Mail Statistics">
<field name="mail_mail_id"/>
<field name="message_id"/>
<field name="sent"/>
<field name="opened"/>
<field name="replied"/>
<field name="bounced"/>
</tree>
</field>
</record>
<record model="ir.ui.view" id="view_mail_mail_statistics_form">
<field name="name">mail.mail.statistics.form</field>
<field name="model">mail.mail.statistics</field>
<field name="arch" type="xml">
<form string="Mail Statistics" version="7.0">
<group>
<group>
<field name="mail_mail_id"/>
<field name="message_id"/>
<field name="exception"/>
<field name="sent"/>
<field name="opened"/>
<field name="replied"/>
<field name="bounced"/>
</group>
<group>
<field name="mass_mailing_id"/>
<field name="mass_mailing_campaign_id"/>
<field name="template_id"/>
<field name="model"/>
<field name="res_id"/>
</group>
</group>
</form>
</field>
</record>
<record id="action_view_mail_mail_statistics" model="ir.actions.act_window">
<field name="name">Mail Statistics</field>
<field name="res_model">mail.mail.statistics</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form</field>
</record>
<!-- Add in Technical/Email -->
<menuitem name="Mail Statistics" id="menu_email_statistics"
parent="base.menu_email" sequence="50"
action="action_view_mail_mail_statistics"/>
<!-- MISC -->
<!-- Mailing List Create Wizard -->
<menuitem name="Create a new List" id="menu_mail_mass_mailing_create"
parent="mass_mailing_list" sequence="10"
action="action_mail_mass_mailing_create"/>
</data>
</openerp>

View File

@ -0,0 +1,58 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data>
<record id="view_mail_mass_mailing_report_graph" model="ir.ui.view">
<field name="name">mail.mass_mailing.report.graph</field>
<field name="model">mail.mass_mailing.report</field>
<field name="arch" type="xml">
<graph string="Mass Mailing Analysis" type="pivot" stacked="True">
<field name="mailing_date" interval="day" type="col"/>
</graph>
</field>
</record>
<record id="view_mail_mass_mailing_report_search" model="ir.ui.view">
<field name="name">mail.mass_mailing.report.search</field>
<field name="model">mail.mass_mailing.report</field>
<field name="arch" type="xml">
<search string="Mass Mailing Analysis">
<group expand="0" string="Extended Filters...">
<!-- <separator orientation="vertical"/> -->
<!-- <newline/> -->
<field name="mailing_date"/>
</group>
<group expand="1" string="Group By...">
<!-- <separator orientation="vertical" /> -->
<filter string="Mailing Date (day)"
domain="[]" context="{'group_by':'mailing_date:day'}"/>
<filter string="Mailing Date (week)"
domain="[]" context="{'group_by':'mailing_date:week'}"/>
<filter string="Mailing Date (month)" name="mailing_date_month"
domain="[]" context="{'group_by':'mailing_date:month'}"/>
<filter string="Mailing Date (year)"
domain="[]" context="{'group_by':'mailing_date:year'}"/>
</group>
</search>
</field>
</record>
<!-- Actions and Menuitems -->
<record id="action_mail_mass_mailing_report" model="ir.actions.act_window">
<field name="name">Mass Mailing Analysis</field>
<field name="res_model">mail.mass_mailing.report</field>
<field name="view_type">form</field>
<field name="view_mode">graph</field>
<field name="help">Mass Mailing Analysis allows you to check different mailing related information like the opened ratio or the mailing performances. You can sort out your analysis by different groups to get accurate grained analysis.</field>
</record>
<record id="action_mail_mass_mailing_report_graph" model="ir.actions.act_window.view">
<field name="view_mode">graph</field>
<field name="view_id" ref="view_mail_mass_mailing_report_graph"/>
<field name="act_window_id" ref="action_mail_mass_mailing_report"/>
</record>
<menuitem name="Mass Mailing Analysis" id="menu_mass_mailing_report" sequence="1"
parent="base.marketing_reporting_menu" action="action_mail_mass_mailing_report"/>
</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,20 @@
<?xml version="1.0"?>
<openerp>
<data>
<!-- Create a Mailing List from Partners -->
<act_window name="Create Mailing List"
res_model="mail.mass_mailing.list.confirm"
src_model="res.partner"
view_mode="form"
multi="True"
target="new"
key2="client_action_multi"
id="action_partner_to_mailing_list"
context="{
'default_mass_mailing_id': context.get('default_mass_mailing_id'),
'default_model': context.get('default_model', 'res.partner'),
'default_name': context.get('default_name', False)}"/>
</data>
</openerp>

View File

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

View File

@ -19,6 +19,7 @@
#
##############################################################################
from openerp import tools
from openerp.osv import osv, fields
@ -29,11 +30,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 +46,45 @@ 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)]:
if wizard.mailing_list_ids:
list_ids = [l.id for l in wizard.mailing_list_ids]
if not list_ids:
list_ids = [self.pool['mail.mass_mailing.list'].create(
cr, uid, {
'name': wizard.mass_mailing_name,
'model': wizard.model,
'domain': wizard.active_domain,
}, 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,
'contact_list_ids': [(4, list_id) for list_id in list_ids],
}, context=context)
mass_mailing = self.pool['mail.mass_mailing'].browse(cr, uid, mass_mailing_id, context=context)
recipient_values = self.pool['mail.mass_mailing'].get_recipients_data(cr, uid, mass_mailing, res_ids, context=context)
for res_id in res_ids:
mail_values = res[res_id]
recipient = recipient_values[res_id]
unsubscribe_url = self.pool['mail.mass_mailing'].get_unsubscribe_url(cr, uid, mass_mailing.id, res_id, recipient['email'], context=context)
if unsubscribe_url:
mail_values['body_html'] = tools.append_content_to_html(mail_values['body_html'], unsubscribe_url, plaintext=False, container_tag='p')
mail_values.update({
'email_to': '"%s" <%s>' % (recipient['name'], recipient['email'])
})
recipient = recipient_values[res_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,57 @@
# -*- coding: utf-8 -*-
from openerp.osv import osv, fields
from openerp.tools.translate import _
class MailingListConfirm(osv.TransientModel):
"""A wizard that acts as a confirmation when creating a new mailing list coming
from a list view. This allows to have a lightweight flow to create mailing
lists without having to go through multiple screens."""
_inherit = 'mail.mass_mailing.list'
_name = 'mail.mass_mailing.list.confirm'
_columns = {
'mass_mailing_id': fields.many2one('mail.mass_mailing', 'Mailing'),
}
def create(self, cr, uid, values, context=None):
if context is None:
context = {}
context.update(no_contact_to_list=True)
return super(MailingListConfirm, self).create(cr, uid, values, context=context)
def action_confirm(self, cr, uid, ids, context=None):
wizard = self.browse(cr, uid, ids[0], context=context)
mailing_list_id = self.pool['mail.mass_mailing.list'].create(
cr, uid, {'name': wizard.name, 'model': wizard.model}, context=context)
res = self.pool['mail.mass_mailing.list'].action_add_to_mailing(cr, uid, [mailing_list_id], context=context)
if not res:
return {
'type': 'ir.actions.act_window',
'view_type': 'form',
'view_mode': 'form',
'res_model': 'mail.mass_mailing.list',
'res_id': mailing_list_id,
'context': context,
}
return res
def action_new_list(self, cr, uid, ids, context=None):
wizard = self.browse(cr, uid, ids[0], context=context)
action_id = self.pool['mail.mass_mailing']._get_model_to_list_action_id(cr, uid, wizard.model, context=context)
if wizard.model == 'mail.mass_mailing.contact':
domain = [('list_id', '=', False)]
else:
domain = []
ctx = dict(context, search_default_not_opt_out=True, view_manager_highlight=[action_id], default_name=wizard.name, default_model=wizard.model)
return {
'name': _('Choose Recipients'),
'type': 'ir.actions.act_window',
'view_type': 'form',
'view_mode': 'tree,form',
'res_model': wizard.model,
'context': ctx,
'domain': domain,
}

View File

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

View File

@ -0,0 +1,43 @@
# -*- coding: utf-8 -*-
from openerp import tools
from openerp.osv import osv, fields
class TestMassMailing(osv.TransientModel):
_name = 'mail.mass_mailing.test'
_description = 'Tets Mailing Wizard'
_columns = {
'email_to': fields.char(
'Emails', required=True,
help='Comma-separated list of email addresses.'),
'mass_mailing_id': fields.many2one('mail.mass_mailing', 'Mailing', required=True),
}
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
if not mailing.template_id:
raise Warning('Please specify on your mailing the template to use.')
test_emails = tools.email_split(wizard.email_to)
if not test_emails:
raise Warning('Please specify test email adresses.')
mail_ids = []
for test_mail in test_emails:
body = mailing.template_id.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,31 @@
<?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="Mailing Test" version="7.0">
<group>
<field name="email_to"/>
</group>
<footer>
<button string="Test Mailing" 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

@ -0,0 +1,23 @@
# -*- 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/>.
#
##############################################################################
import models
import controllers

View File

@ -0,0 +1,19 @@
# -*- coding: utf-8 -*-
{
'name': 'Mass Mailing with Recruitment',
'version': '1.0',
'depends': ['mass_mailing', 'hr_recruitment'],
'author': 'OpenERP SA',
'category': 'Hidden/Dependency',
'description': """
Bridge module between Mass Mailing and HR Recruitment
""",
'website': 'http://www.openerp.com',
'data': [
'views/hr_applicant.xml',
],
'demo': [],
'installable': True,
'auto_install': True,
}

View File

@ -0,0 +1 @@
import main

View File

@ -0,0 +1,21 @@
from openerp import http, SUPERUSER_ID
from openerp.addons.mass_mailing.controllers import main
from openerp.http import request
class MassMailController(main.MassMailController):
@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 super(MassMailController, self).mailing(mailing_id, email=email, res_id=res_id, **post)
mailing = MassMailing.browse(cr, SUPERUSER_ID, mailing_ids[0], context=context)
if mailing.mailing_model == 'hr.applicant':
record_ids = request.registry[mailing.mailing_model].search(cr, SUPERUSER_ID, [('id', '=', res_id), ('email_from', 'ilike', email)], context=context)
# request.registry[mailing.mailing_model].write(cr, SUPERUSER_ID, record_ids, {'opt_out': True}, context=context)
return 'OK'
return super(MassMailController, self).mailing(mailing_id, email=email, res_id=res_id, **post)

View File

@ -0,0 +1 @@
import mass_mailing

View File

@ -0,0 +1,32 @@
# -*- coding: utf-8 -*-
from openerp.osv import osv
class MassMailing(osv.Model):
"""Inherit to add hr.applicant objects available for mass mailing """
_name = 'mail.mass_mailing'
_inherit = 'mail.mass_mailing'
def _get_mailing_model(self, cr, uid, context=None):
res = super(MassMailing, self)._get_mailing_model(cr, uid, context=context)
res.append(('hr.applicant', 'Applicants'))
return res
def _get_model_to_list_action_id(self, cr, uid, model, context=None):
if model == 'hr.applicant':
return self.pool['ir.model.data'].xmlid_to_res_id(cr, uid, 'mass_mailing_applicant.action_applicant_to_mailing_list')
else:
return super(MassMailing, self)._get_model_to_list_action_id(cr, uid, model, context=context)
def get_recipients_data(self, cr, uid, mailing, res_ids, context=None):
if mailing.mailing_model == 'hr.applicant':
res = {}
for applicant in self.pool['hr.applicant'].browse(cr, uid, res_ids, context=context):
if applicant.partner_id:
res[applicant.id] = {'partner_id': applicant.partner_id.id, 'name': applicant.partner_id.name, 'email': applicant.partner_id.email}
else:
name, email = self.pool['res.partner']._parse_partner_name(applicant.email_from, context=context)
res[applicant.id] = {'partner_id': False, 'name': name or email, 'email': email}
return res
return super(MassMailing, self).get_recipients_data(cr, uid, mailing, res_ids, context=context)

View File

@ -0,0 +1,20 @@
<?xml version="1.0"?>
<openerp>
<data>
<!-- Create a Mailing List from Leads -->
<act_window name="Create Mailing List"
res_model="mail.mass_mailing.list"
src_model="hr.applicant"
view_mode="form"
multi="True"
target="current"
key2="client_action_multi"
id="action_applicant_to_mailing_list"
context="{
'default_mass_mailing_id': context.get('default_mass_mailing_id'),
'default_model': context.get('default_model', 'hr.applicant'),
'default_name': context.get('default_name', False)}"/>
</data>
</openerp>

View File

@ -0,0 +1,23 @@
# -*- 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/>.
#
##############################################################################
import models
import controllers

View File

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

View File

@ -0,0 +1 @@
import main

View File

@ -0,0 +1,21 @@
from openerp import http, SUPERUSER_ID
from openerp.addons.mass_mailing.controllers import main
from openerp.http import request
class MassMailController(main.MassMailController):
@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 super(MassMailController, self).mailing(mailing_id, email=email, res_id=res_id, **post)
mailing = MassMailing.browse(cr, SUPERUSER_ID, mailing_ids[0], context=context)
if mailing.mailing_model == 'crm.lead':
record_ids = request.registry[mailing.mailing_model].search(cr, SUPERUSER_ID, [('id', '=', res_id), ('email_from', 'ilike', email)], context=context)
request.registry[mailing.mailing_model].write(cr, SUPERUSER_ID, record_ids, {'opt_out': True}, context=context)
return 'OK'
return super(MassMailController, self).mailing(mailing_id, email=email, res_id=res_id, **post)

View File

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

View File

@ -0,0 +1,32 @@
# -*- coding: utf-8 -*-
from openerp.osv import osv
class MassMailing(osv.Model):
"""Inherit to add crm.lead objects available for mass mailing """
_name = 'mail.mass_mailing'
_inherit = 'mail.mass_mailing'
def _get_mailing_model(self, cr, uid, context=None):
res = super(MassMailing, self)._get_mailing_model(cr, uid, context=context)
res.append(('crm.lead', 'Leads / Opportunities'))
return res
def _get_model_to_list_action_id(self, cr, uid, model, context=None):
if model == 'crm.lead':
return self.pool['ir.model.data'].xmlid_to_res_id(cr, uid, 'mass_mailing_crm.action_lead_to_mailing_list')
else:
return super(MassMailing, self)._get_model_to_list_action_id(cr, uid, model, context=context)
def get_recipients_data(self, cr, uid, mailing, res_ids, context=None):
if mailing.mailing_model == 'crm.lead':
res = {}
for lead in self.pool['crm.lead'].browse(cr, uid, res_ids, context=context):
if lead.partner_id:
res[lead.id] = {'partner_id': lead.partner_id.id, 'name': lead.partner_id.name, 'email': lead.partner_id.email}
else:
name, email = self.pool['res.partner']._parse_partner_name(lead.email_from, context=context)
res[lead.id] = {'partner_id': False, 'name': name or email, 'email': email}
return res
return super(MassMailing, self).get_recipients_data(cr, uid, mailing, res_ids, context=context)

View File

@ -0,0 +1,20 @@
<?xml version="1.0"?>
<openerp>
<data>
<!-- Create a Mailing List from Leads -->
<act_window name="Create Mailing List"
res_model="mail.mass_mailing.list"
src_model="crm.lead"
view_mode="form"
multi="True"
target="current"
key2="client_action_multi"
id="action_lead_to_mailing_list"
context="{
'default_mass_mailing_id': context.get('default_mass_mailing_id'),
'default_model': context.get('default_model', 'res.partner'),
'default_name': context.get('default_name', False)}"/>
</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

@ -1248,10 +1248,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

@ -7,12 +7,21 @@ 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=None, res_id=None, **kw):
if not model or not model in request.registry or not res_id:
return request.redirect('/')
if not 'body' in request.registry[model]._all_columns and not 'body_html' in request.registry[model]._all_columns:
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('/')
values = {
'template': template,
'object': request.registry[model].browse(request.cr, request.uid, obj_ids[0], context=request.context),
'model': request.registry[model],
'model_name': model,
'res_id': res_id,
}
print template
return request.website.render("website_mail.designer_index", values)
@http.route(['/website_mail/snippets'], type='json', auth="user", website=True)

View File

@ -27,7 +27,7 @@ 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)
return dict((id, _('<a href="website_mail/email_designer?model=email.template&res_id=%d">Open with visual editor</a>') % id) for id in ids)
_columns = {
'website_link': fields.function(

View File

@ -6,7 +6,7 @@
<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">
<xpath expr="//h1" position="after">
<br />
<field name="website_link" widget='html' radonly='1'
style='margin: 0px; padding: 0px;'/>

View File

@ -13,38 +13,38 @@
<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>
t-att-href="'/web#return_label=Website&amp;model=%s&amp;id=%s&amp;view_type=form' % (model_name, res_id)">
<button class="btn btn-primary">Back to Form</button>
</a>
<h1 t-field="template.name"/>
<h1 t-field="object.name"/>
<div class="row" style="width: 600px;">
<div class="row">
<div t-if="'email_from' in model._all_columns" 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 class="col-lg-9"><span t-field="object.email_from"/></div>
</div>
<div class="row">
<div t-if="'email_to' in model._all_columns" 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 class="col-lg-9"><span t-field="object.email_to"/></div>
</div>
<div class="row">
<div t-if="'partner_to' in model._all_columns" 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 class="col-lg-9"><span t-field="object.partner_to"/></div>
</div>
<div class="row">
<div t-if="'reply_to' in model._all_columns" 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 class="col-lg-9"><span t-field="object.reply_to"/></div>
</div>
<div class="row">
<div t-if="'subject' in model._all_columns" class="row">
<div class="col-lg-3"><b>Subject</b></div>
<div class="col-lg-9"><span t-field="template.subject"/></div>
<div class="col-lg-9"><span t-field="object.subject"/></div>
</div>
<div class="row well">
<div t-field="template.body_html" style="position: relative;"/>
<div t-field="object.body_html" style="position: relative;"/>
</div>
</div>
</div>