From 08ab922468980b5fbba0288a121822b9f5f64742 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Fri, 14 Mar 2014 17:51:13 +0100 Subject: [PATCH] [IMP] [REF] mass_mailing: first draft for the saas-4 refactoring. Improved mass mailing form view, that is used as a central point to create new mailings. Added concept of contact list (based on partner, or leads (to add)), as well as contact (a list of name / email to import). Mailings are done un contact list to simplify the way it works. Added a kanban view of templates, with a flag to filter only mass mailing templates (to avoid havign to deal with acknoledgments). Using campaigns is now an option (a group), mailings can be done without having to deal with campaigns. Mailings and campaigns now have a status, used to display their kanban view. bzr revid: tde@openerp.com-20140314165113-g4gvvifrhr2nfu15 --- addons/email_template/__openerp__.py | 2 +- addons/email_template/res_partner_demo.yml | 9 - addons/mass_mailing/__init__.py | 6 +- addons/mass_mailing/__openerp__.py | 7 +- addons/mass_mailing/email_template.py | 31 ++ addons/mass_mailing/email_template.xml | 83 ++++ addons/mass_mailing/mass_mailing.py | 378 +++++++++++++++++- addons/mass_mailing/mass_mailing_data.xml | 23 ++ addons/mass_mailing/mass_mailing_demo.xml | 160 +++++++- addons/mass_mailing/mass_mailing_view.xml | 267 +++++++++++-- addons/mass_mailing/res_config.py | 15 + addons/mass_mailing/res_config_view.xml | 20 + .../mass_mailing/security/ir.model.access.csv | 3 + .../static/src/css/email_template.css | 19 + 14 files changed, 950 insertions(+), 73 deletions(-) delete mode 100644 addons/email_template/res_partner_demo.yml create mode 100644 addons/mass_mailing/email_template.py create mode 100644 addons/mass_mailing/email_template.xml create mode 100644 addons/mass_mailing/mass_mailing_data.xml create mode 100644 addons/mass_mailing/res_config.py create mode 100644 addons/mass_mailing/res_config_view.xml create mode 100644 addons/mass_mailing/static/src/css/email_template.css diff --git a/addons/email_template/__openerp__.py b/addons/email_template/__openerp__.py index b7977660134..298754c8b8c 100644 --- a/addons/email_template/__openerp__.py +++ b/addons/email_template/__openerp__.py @@ -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'], diff --git a/addons/email_template/res_partner_demo.yml b/addons/email_template/res_partner_demo.yml deleted file mode 100644 index 8eaf48f9e87..00000000000 --- a/addons/email_template/res_partner_demo.yml +++ /dev/null @@ -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}) \ No newline at end of file diff --git a/addons/mass_mailing/__init__.py b/addons/mass_mailing/__init__.py index 9dadc456842..b71d26831ac 100644 --- a/addons/mass_mailing/__init__.py +++ b/addons/mass_mailing/__init__.py @@ -2,7 +2,7 @@ ############################################################################## # # OpenERP, Open Source Management Solution -# Copyright (C) 2013-today OpenERP SA () +# Copyright (C) 2013-Today OpenERP SA () # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as @@ -22,5 +22,7 @@ import mass_mailing import mail_mail import mail_thread +import email_template +import res_config import wizard -import controllers +import controllers \ No newline at end of file diff --git a/addons/mass_mailing/__openerp__.py b/addons/mass_mailing/__openerp__.py index 92f8b1fc3e8..b21e25bc8ba 100644 --- a/addons/mass_mailing/__openerp__.py +++ b/addons/mass_mailing/__openerp__.py @@ -33,14 +33,18 @@ professional emails and reuse templates in a few clicks. 'depends': [ 'mail', 'email_template', + 'marketing', 'web_kanban_gauge', 'web_kanban_sparkline', ], 'data': [ 'mail_data.xml', + 'mass_mailing_data.xml', 'wizard/mail_compose_message_view.xml', 'wizard/mail_mass_mailing_create_segment.xml', 'mass_mailing_view.xml', + 'res_config_view.xml', + 'email_template.xml', 'security/ir.model.access.csv', ], 'js': [ @@ -48,7 +52,8 @@ 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', diff --git a/addons/mass_mailing/email_template.py b/addons/mass_mailing/email_template.py new file mode 100644 index 00000000000..9fb2c22e0b1 --- /dev/null +++ b/addons/mass_mailing/email_template.py @@ -0,0 +1,31 @@ +# -*- 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 mass mailing campaigns'), + } + + def action_new_mailing(self, cr, uid, ids, context=None): + ctx = dict(context) + ctx.update({ + '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')], + 'view_id': False, + # 'target': 'new', + 'context': ctx, + } diff --git a/addons/mass_mailing/email_template.xml b/addons/mass_mailing/email_template.xml new file mode 100644 index 00000000000..0278223290c --- /dev/null +++ b/addons/mass_mailing/email_template.xml @@ -0,0 +1,83 @@ + + + + + + + email.template.form.mass.mailing + email.template + + + +
+
+
+ + + email.template.search.mass.mailing + email.template + + + + + + + + + + + email.template.kanban + email.template + + + + + +
+
+ i + +
+
+

+ +

+
+ +
+
+
+
+
+
+
+
+
+ + + Templates + email.template + form + kanban,tree,form + {'search_default_use_in_mass_mailing': 1} + + + + + +
+
\ No newline at end of file diff --git a/addons/mass_mailing/mass_mailing.py b/addons/mass_mailing/mass_mailing.py index e7583d8e739..79866a6b748 100644 --- a/addons/mass_mailing/mass_mailing.py +++ b/addons/mass_mailing/mass_mailing.py @@ -23,10 +23,151 @@ from datetime import datetime from dateutil import relativedelta from openerp import tools +from openerp.tools.safe_eval import safe_eval as eval from openerp.tools.translate import _ from openerp.osv import osv, fields +class MassMailingCategory(osv.Model): + """Model of categories of mass mailing, i.e. marketing, newsletter, ... """ + _name = 'mail.mass_mailing.category' + _description = 'Mass Mailing Category' + + _columns = { + 'name': fields.char('Name', required=True), + } + + +class MassMailingContact(osv.Model): + """Model of a contact. This model is different from the partner model + because it holds only some basic information: name, email. The purpose is to + be able to deal with large contact list to email without bloating the partner + database. """ + _name = 'mail.mass_mailing.contact' + _description = 'Mass Mailing Contact' + + _columns = { + 'name': fields.char('Name', required=True), + 'email': fields.char('Email', required=True), + 'list_id': fields.many2one( + 'mail.mass_mailing.list', string='Mailing List', + required=True, ondelete='cascade', + ), + 'opt_out': fields.boolean('Opt Out', help='The contact has chosen not to receive news anymore from this mailing list'), + } + + +class MassMailingList(osv.Model): + """Model of a contact list. """ + _name = 'mail.mass_mailing.list' + _description = 'Contact List' + + def default_get(self, cr, uid, fields, context=None): + """Override default_get to handle active_domain coming from the list view. """ + res = super(MassMailingList, self).default_get(cr, uid, fields, context=context) + if 'domain' in fields and 'active_domain' in context: + res['domain'] = '%s' % context['active_domain'] + res['model'] = context.get('active_model', 'res.partner') + return res + + def _get_contact_nbr(self, cr, uid, ids, name, arg, context=None): + """Compute the number of contacts linked to the mailing list. """ + results = dict.fromkeys(ids, 0) + for contact_list in self.browse(cr, uid, ids, context=context): + if contact_list.model == 'mail.mass_mailing.contact': + results[contact_list.id] = len(contact_list.contact_ids) + else: + domain = self.compute_domain(cr, uid, [contact_list.id], context=context) + results[contact_list.id] = self.pool[contact_list.model].search(cr, uid, domain, count=True, context=context) + return results + + def _get_model_list(self, cr, uid, context=None): + return [ + ('res.partner', 'Customers'), + ('mail.mass_mailing.contact', 'Mailing Contacts') + ] + + # indirections for inheritance + _model_list = lambda self, *args, **kwargs: self._get_model_list(*args, **kwargs) + + _columns = { + 'name': fields.char('Name', required=True), + 'contact_nbr': fields.function( + _get_contact_nbr, type='integer', + string='Contact Number', + ), + # contact-based list + 'contact_ids': fields.one2many( + 'mail.mass_mailing.contact', 'list_id', string='Contacts', + domain=[('opt_out', '=', False)], + ), + # filter-based list + 'model': fields.selection( + _model_list, type='char', required=True, + string='Applies To' + ), + # 'model_id': fields.many2one( + # 'ir.model', string='Related Model', + # domain="[('model', '=', model')]", + # ), + 'filter_id': fields.many2one( + 'ir.filters', string='Custom Filter', + # domain="[('model_id', '=', model_id)]", + ), + 'domain': fields.text('Domain'), + } + + def on_change_model(self, cr, uid, ids, model, context=None): + values = {} + if model == 'mail.mass_mailing.contact': + values.update(domain=False, filter_id=False) + else: + values.update(filter_id=False) + return {'value': values} + + def on_change_filter_id(self, cr, uid, ids, filter_id, context=None): + values = {} + if filter_id: + ir_filter = self.pool['ir.filters'].browse(cr, uid, filter_id, context=context) + values['domain'] = ir_filter.domain + else: + values['domain'] = False + return {'value': values} + + def action_see_records(self, cr, uid, ids, context=None): + contact_list = self.browse(cr, uid, ids[0], context=context) + ctx = dict(context) + ctx['search_default_not_opt_out'] = True + return { + 'name': _('See Contact List'), + 'type': 'ir.actions.act_window', + 'view_type': 'form', + 'view_mode': 'tree,form', + 'res_model': contact_list.model, + 'views': [(False, 'tree'), (False, 'form')], + 'view_id': False, + 'target': 'current', + 'context': ctx, + 'domain': contact_list.domain, + } + + def compute_domain(self, cr, uid, ids, context=None): + domains = [] + for contact_list in self.browse(cr, uid, ids, context=context): + domain = eval(contact_list.domain) + if domain: + domain = ['&', ('opt_out', '=', False)] + domain + else: + domain = [('opt_out', '=', False)] + if domain is not False: + domains.append(domain) + if domains: + final_domain = ['|'] * (len(domains) - 1) + [leaf for dom in domains for leaf in dom] + else: + final_domain = domains + return final_domain + + class MassMailingCampaign(osv.Model): """Model of mass mailing campaigns. """ @@ -63,6 +204,12 @@ class MassMailingCampaign(osv.Model): results[campaign.id] = mass_mailing_results return results + def _get_state_list(self, cr, uid, context=None): + return [('draft', 'Schedule'), ('design', 'Design'), ('done', 'Sent')] + + # indirections for inheritance + _state = lambda self, *args, **kwargs: self._get_state_list(*args, **kwargs) + _columns = { 'name': fields.char( 'Campaign Name', required=True, @@ -71,6 +218,12 @@ class MassMailingCampaign(osv.Model): 'res.users', 'Responsible', required=True, ), + 'state': fields.selection( + _state, string='Status', required=True, + ), + 'category_id': fields.many2one( + 'mail.mass_mailing.category', 'Category', + help='Category'), 'mass_mailing_ids': fields.one2many( 'mail.mass_mailing', 'mass_mailing_campaign_id', 'Mass Mailings', @@ -117,8 +270,42 @@ class MassMailingCampaign(osv.Model): _defaults = { 'user_id': lambda self, cr, uid, ctx=None: uid, + 'state': 'draft', } + #------------------------------------------------------ + # Technical stuff + #------------------------------------------------------ + + def read_group(self, cr, uid, domain, fields, groupby, offset=0, limit=None, context=None, orderby=False): + """ Override read_group to always display all states. """ + if groupby and groupby[0] == "state": + # Default result structure + states = self._get_state_list(cr, uid, context=context) + read_group_all_states = [{ + '__context': {'group_by': groupby[1:]}, + '__domain': domain + [('state', '=', state_value)], + 'state': state_value, + 'state_count': 0, + } for state_value, state_name in states] + # Get standard results + read_group_res = super(MassMailingCampaign, self).read_group(cr, uid, domain, fields, groupby, offset=offset, limit=limit, context=context, orderby=orderby) + # Update standard results with default results + result = [] + for state_value, state_name in states: + res = filter(lambda x: x['state'] == state_value, read_group_res) + if not res: + res = filter(lambda x: x['state'] == state_value, read_group_all_states) + res[0]['state'] = [state_value, state_name] + result.append(res[0]) + return result + else: + return super(MassMailingCampaign, self).read_group(cr, uid, domain, fields, groupby, offset=offset, limit=limit, context=context, orderby=orderby) + + #------------------------------------------------------ + # Actions + #------------------------------------------------------ + def launch_mass_mailing_create_wizard(self, cr, uid, ids, context=None): ctx = dict(context) ctx.update({ @@ -142,7 +329,7 @@ class MassMailing(osv.Model): A mass mailing is an occurence of sending emails. """ _name = 'mail.mass_mailing' - _description = 'Wave of sending emails' + _description = 'Mass Mailing' # number of periods for tracking mail_mail statistics _period_number = 6 _order = 'date DESC' @@ -189,9 +376,9 @@ class MassMailing(osv.Model): 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) + res[id]['opened_dayly'] = 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) + res[id]['replied_dayly'] = 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): @@ -207,22 +394,54 @@ class MassMailing(osv.Model): } return results + def _get_mailing_type(self, cr, uid, context=None): + return [ + ('res.partner', 'Customers'), + ('mail.mass_mailing.contact', 'Contacts') + ] + + def _get_state_list(self, cr, uid, context=None): + return [('draft', 'Schedule'), ('test', 'Tested'), ('done', 'Sent')] + + # indirections for inheritance + _mailing_type = lambda self, *args, **kwargs: self._get_mailing_type(*args, **kwargs) + _state = lambda self, *args, **kwargs: self._get_state_list(*args, **kwargs) + _columns = { - 'name': fields.char('Name', required=True), - 'mass_mailing_campaign_id': fields.many2one( - 'mail.mass_mailing.campaign', 'Mass Mailing Campaign', - ondelete='cascade', required=True, + 'name': fields.char('Subject', required=True), + 'date': fields.datetime('Date'), + 'state': fields.selection( + _state, string='Status', required=True, ), 'template_id': fields.many2one( 'email.template', 'Email Template', + domain=[('use_in_mass_mailing', '=', True)], + ondelete='set null', + ), + 'body_html': fields.related( + 'template_id', 'body_html', type='html', + string='Body', readonly='True', + help='Technical field: used only to display a view of the template in the form view', + ), + 'mass_mailing_campaign_id': fields.many2one( + 'mail.mass_mailing.campaign', 'Mass Mailing Campaign', ondelete='set null', ), - 'domain': fields.char('Domain'), - 'date': fields.datetime('Date'), 'color': fields.related( 'mass_mailing_campaign_id', 'color', type='integer', string='Color Index', ), + # mailing options + 'email_from': fields.char('From'), + 'email_to': fields.char('Send to Emails'), + 'reply_to': fields.char('Reply To'), + 'mailing_type': fields.selection(_mailing_type, string='Type', required=True), + 'contact_list_ids': fields.many2many( + 'mail.mass_mailing.list', 'mail_mass_mailing_list_rel', + string='Mailing Lists', + domain="[('model', '=', mailing_type)]", + ), + 'contact_nbr': fields.integer('Contact Number'), # statistics data 'statistics_ids': fields.one2many( 'mail.mail.statistics', 'mass_mailing_id', @@ -254,22 +473,159 @@ class MassMailing(osv.Model): type='integer', multi='_get_statistics' ), # monthly ratio - 'opened_monthly': fields.function( + 'opened_dayly': fields.function( _get_daily_statistics, string='Opened', type='char', multi='_get_daily_statistics', + oldname='opened_monthly', ), - 'replied_monthly': fields.function( + '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_type': 'res.partner', } + #------------------------------------------------------ + # Technical stuff + #------------------------------------------------------ + + def read_group(self, cr, uid, domain, fields, groupby, offset=0, limit=None, context=None, orderby=False): + """ Override read_group to always display all states. """ + if groupby and groupby[0] == "state": + # Default result structure + states = self._get_state_list(cr, uid, context=context) + read_group_all_states = [{ + '__context': {'group_by': groupby[1:]}, + '__domain': domain + [('state', '=', state_value)], + 'state': state_value, + 'state_count': 0, + } for state_value, state_name in states] + # Get standard results + read_group_res = super(MassMailing, self).read_group(cr, uid, domain, fields, groupby, offset=offset, limit=limit, context=context, orderby=orderby) + # Update standard results with default results + result = [] + for state_value, state_name in states: + res = filter(lambda x: x['state'] == state_value, read_group_res) + if not res: + res = filter(lambda x: x['state'] == state_value, read_group_all_states) + res[0]['state'] = [state_value, state_name] + result.append(res[0]) + return result + else: + return super(MassMailing, self).read_group(cr, uid, domain, fields, groupby, offset=offset, limit=limit, context=context, orderby=orderby) + + #------------------------------------------------------ + # Views & Actions + #------------------------------------------------------ + + def on_change_mailing_type(self, cr, uid, ids, mailing_type, context=None): + return {'value': {'contact_list_ids': []}} + + 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 send_mail(self, cr, uid, ids, context=None): + Mail = self.pool['mail.mail'] + for mailing in self.browse(cr, uid, ids, context=context): + contact_list_ids = [contact_list.id for contact_list in mailing.contact_list_ids] + + # contact-based list: aggregate all contacts + if mailing.mailing_type == 'mail.mass_mailing.list': + res_ids = [contact.id for contact in contact_list.contact_ids for contact_list in mailing.contact_list_ids] + elif mailing.mailing_type == 'res.partner': + domain = self.pool['mail.mass_mailing.list'].compute_domain(cr, uid, contact_list_ids, context=context) + print domain + res_ids = self.pool[contact_list.model].search(cr, uid, domain, context=context) + + all_mail_values = self.pool['mail.compose.message'].generate_email_for_composer_batch( + cr, uid, mailing.template_id.id, res_ids, + context=context, + fields=['body_html', 'attachment_ids', 'mail_server_id'] + ) + for res_id, mail_values in all_mail_values.iteritems(): + mail_values.update({ + 'email_from': mailing.email_from, + 'reply_to': mailing.reply_to, + 'subject': mailing.name, + 'body_html': mail_values.get('body'), + 'auto_delete': True, + 'statistics_ids': [(0, 0, { + 'model': mailing.mailing_type, + 'res_id': res_id, + 'mass_mailing_id': mailing.id, + })] + }) + 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 mailing.mailing_type == 'mail.mass_mailing.list': + contact = self.pool['mail.mass_mailing.contact'].browse(cr, uid, res_id, context=context) + mail_values['email_to'] = '"%s" <%s>' % (contact.name, contact.email) + elif mailing.mailing_type == 'res.partner': + mail_values['recipient_ids'] = [(4, res_id)] + Mail.create(cr, uid, mail_values, context=context) + # todo: handle email_to + return True + + def send_mail_to_myself(self, cr, uid, ids, context=None): + Mail = self.pool['mail.mail'] + for mailing in self.browse(cr, uid, ids, context=context): + mail_values = { + 'email_from': mailing.email_from, + 'reply_to': mailing.reply_to, + 'email_to': self.pool['res.users'].browse(cr, uid, uid, context=context).email, + 'subject': mailing.name, + 'body_html': mailing.template_id.body_html, + 'auto_delete': True, + } + mail_id = Mail.create(cr, uid, mail_values, context=context) + Mail.send(cr, uid, [mail_id], context=context) + return True + + def select_customers(self, cr, uid, ids, context=None): + sid = self.pool['ir.model.data'].xmlid_to_res_id(cr, uid, 'base.view_res_partner_filter') + + aid = self.pool['ir.model.data'].xmlid_to_res_id(cr, uid, 'mass_mailing.action_partner_to_mailing_list') + print sid, aid + ctx = dict(context) + ctx['view_manager_highlight'] = [aid] + return { + 'name': _('Choose Customers'), + 'type': 'ir.actions.act_window', + 'view_type': 'form', + 'view_mode': 'tree,form', + 'res_model': 'res.partner', + # 'views': [(False, 'tree'), (False, 'form')], + 'view_id': False, + 'search_view_id': sid, + # 'target': 'new', + 'context': ctx, + } + class MailMailStats(osv.Model): """ MailMailStats models the statistics collected about emails. Those statistics diff --git a/addons/mass_mailing/mass_mailing_data.xml b/addons/mass_mailing/mass_mailing_data.xml new file mode 100644 index 00000000000..6982aac566f --- /dev/null +++ b/addons/mass_mailing/mass_mailing_data.xml @@ -0,0 +1,23 @@ + + + + + + + Open Marketing Menu + reload + + + + + open + + + + + Manage Mass Mailing Campaigns + + + + + \ No newline at end of file diff --git a/addons/mass_mailing/mass_mailing_demo.xml b/addons/mass_mailing/mass_mailing_demo.xml index 5a2388846e4..3b440aab84c 100644 --- a/addons/mass_mailing/mass_mailing_demo.xml +++ b/addons/mass_mailing/mass_mailing_demo.xml @@ -8,16 +8,153 @@ ${object.id} - Hello

]]>
+ ]]> + + + + + + + + + + + + + + +
+

A Punchy Headline

+
+

+ +

A Small Subtitle for ${object.name}

+ +

+ +

Choose a vibrant image and write an inspiring paragraph about it. It does not have to be long, but it should reinforce your image.

+
+ +
+ + + + + + + + + + + +
+

Feature One

+ +

Choose a vibrant image and write an inspiring paragraph about it. It does not have to be long, but it should reinforce your image.

+
+

Feature Two

+ +

Choose a vibrant image and write an inspiring paragraph about it. It does not have to be long, but it should reinforce your image.

+
+
]]>
+ Partner Newsletter 2 ${object.id} - Hello

]]>
+ ]]> + + + + + + + + +
+
+

Sell Online. Easily.

+ +

Write one sentence to convince visitor about your message.

+
+
+ +
+ + + + + + +
+
+
+
+
+ +
+ + + + + + + + + + + + + +
+

Feature One

+ +

Choose a vibrant image and write an inspiring paragraph about it. It does not have to be long, but it should reinforce your image.

+
+

Feature Two

+ +

Choose a vibrant image and write an inspiring paragraph about it. It does not have to be long, but it should reinforce your image.

+
+

Feature Three

+ +

Choose a vibrant image and write an inspiring paragraph about it. It does not have to be long, but it should reinforce your image.

+
+
]]>
+ + + Imported Contacts + mail.mass_mailing.contact + + + Customers + res.partner + + [('customer', '=', True)] + + + + + Aristide Antario + aa@example.com + + + + Beverly Bridge + bb@example.com + + + + Carol Cartridge + cc@example.com + + + + + Partners Newsletter @@ -25,12 +162,14 @@ First Newsletter + done - + Second Newsletter + test @@ -63,20 +202,5 @@ - - - 1111005@OpenERP.com - - - - - 1111006@OpenERP.com - - - - - 1111007@OpenERP.com - - diff --git a/addons/mass_mailing/mass_mailing_view.xml b/addons/mass_mailing/mass_mailing_view.xml index e8848fba44f..b9549ebd176 100644 --- a/addons/mass_mailing/mass_mailing_view.xml +++ b/addons/mass_mailing/mass_mailing_view.xml @@ -2,6 +2,147 @@ + + + + + + + + mail.mass_mailing.contact.search + mail.mass_mailing.contact + + + + + + + + + + + + + + + + mail.mass_mailing.contact.tree + mail.mass_mailing.contact + 10 + + + + + + + + + + + + mail.mass_mailing.contact.form + mail.mass_mailing.contact + +
+ + + + + + + + +
+
+
+ + + Mass Mailing Contacts + mail.mass_mailing.contact + form + tree,form + {'search_default_not_opt_out': 1} + + + + + + + mail.mass_mailing.list.search + mail.mass_mailing.list + + + + + + + + + + mail.mass_mailing.list.tree + mail.mass_mailing.list + 10 + + + + + + + + + + + mail.mass_mailing.list.form + mail.mass_mailing.list + +
+
+
+ + + + + +
+
+
+ + + Contact Lists + mail.mass_mailing.list + form + tree,form + + + + mail.mass_mailing.search @@ -13,6 +154,7 @@ @@ -32,7 +174,8 @@ - + @@ -43,19 +186,51 @@ mail.mass_mailing
+
+
- + + + - - - +

Here be some graphs

- + + +
@@ -76,7 +251,7 @@ mail.mass_mailing.kanban mail.mass_mailing - + @@ -87,8 +262,9 @@

- Sent:
- Campaign: + Sent:
+ Campaign: +

@@ -112,11 +288,11 @@

Opened


- +

Replied


- +
@@ -147,6 +323,10 @@
+ + mail.mass_mailing.campaign.search @@ -154,10 +334,13 @@ + - + @@ -171,6 +354,8 @@ + +
@@ -182,14 +367,16 @@
+ - + - + + + + + + + + + +