From b3e7fd040b934ee1a60ddaf8c866af3e787e484c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Mon, 15 Jul 2019 09:18:22 +0000 Subject: [PATCH 1/7] [MOV] mass_mailing: reorganize module according to guidelines PURPOSE Mass mailing is currently a bit messy. As SMS will be added as a way to notify people in mass through SMS let us take this opportunity to somehow clean this application: organization, light code cleaning, model renaming. SPECIFICATIONS Guidelines: re-organize model files and split them according to main models. Also reorganize views. Done in this commit * split big python file by main models; * split views according to python files; * move statistics report to /report; * merge some exploded view files (assets, snippets, application menus); LINKS Task ID 2037906 Preparing task ID 1997464 (SMS addition in mass mailing) PR #34938 --- addons/mass_mailing/__init__.py | 1 + addons/mass_mailing/__manifest__.py | 15 +- .../mail_data.xml} | 21 +- addons/mass_mailing/models/__init__.py | 5 +- addons/mass_mailing/models/mass_mailing.py | 244 +--------- .../models/mass_mailing_contact.py | 142 ++++++ .../mass_mailing/models/mass_mailing_list.py | 111 +++++ addons/mass_mailing/report/__init__.py | 4 + .../mass_mailing_stats_report.py} | 0 .../mass_mailing_stats_report_views.xml} | 0 addons/mass_mailing/views/assets.xml | 78 ++++ .../mass_mailing/views/editor_field_html.xml | 42 -- .../views/mass_mailing_blacklist_views.xml | 4 - .../views/mass_mailing_contact_views.xml | 267 +++++++++++ .../views/mass_mailing_list_views.xml | 167 +++++++ .../views/mass_mailing_template.xml | 21 - ....xml => mass_mailing_templates_portal.xml} | 0 .../mass_mailing/views/mass_mailing_views.xml | 430 ------------------ .../views/mass_mailing_views_menus.xml | 6 + addons/mass_mailing/views/snippets_themes.xml | 48 ++ .../views/snippets_themes_options.xml | 51 --- 21 files changed, 837 insertions(+), 820 deletions(-) rename addons/mass_mailing/{views/mass_mailing_layout.xml => data/mail_data.xml} (61%) create mode 100644 addons/mass_mailing/models/mass_mailing_contact.py create mode 100644 addons/mass_mailing/models/mass_mailing_list.py create mode 100644 addons/mass_mailing/report/__init__.py rename addons/mass_mailing/{models/mass_mailing_report.py => report/mass_mailing_stats_report.py} (100%) rename addons/mass_mailing/{views/mass_mailing_report_views.xml => report/mass_mailing_stats_report_views.xml} (100%) create mode 100644 addons/mass_mailing/views/assets.xml delete mode 100644 addons/mass_mailing/views/editor_field_html.xml delete mode 100644 addons/mass_mailing/views/mass_mailing_blacklist_views.xml create mode 100644 addons/mass_mailing/views/mass_mailing_contact_views.xml create mode 100644 addons/mass_mailing/views/mass_mailing_list_views.xml delete mode 100644 addons/mass_mailing/views/mass_mailing_template.xml rename addons/mass_mailing/views/{unsubscribe_templates.xml => mass_mailing_templates_portal.xml} (100%) delete mode 100644 addons/mass_mailing/views/snippets_themes_options.xml diff --git a/addons/mass_mailing/__init__.py b/addons/mass_mailing/__init__.py index 88710707227..4e9b019bd7a 100644 --- a/addons/mass_mailing/__init__.py +++ b/addons/mass_mailing/__init__.py @@ -3,4 +3,5 @@ from . import controllers from . import models +from . import report from . import wizard diff --git a/addons/mass_mailing/__manifest__.py b/addons/mass_mailing/__manifest__.py index 1e1986cc211..fd1e0c8b631 100644 --- a/addons/mass_mailing/__manifest__.py +++ b/addons/mass_mailing/__manifest__.py @@ -20,26 +20,25 @@ ], 'data': [ 'security/mass_mailing_security.xml', + 'security/ir.model.access.csv', + 'data/mail_data.xml', 'data/mass_mailing_data.xml', 'wizard/mail_compose_message_views.xml', 'wizard/mass_mailing_list_merge_views.xml', 'wizard/test_mailing_views.xml', 'wizard/mass_mailing_schedule_date_views.xml', 'views/mass_mailing_views_menus.xml', - 'views/mass_mailing_report_views.xml', 'views/mass_mailing_stats_views.xml', 'views/link_tracker_views.xml', - 'views/mass_mailing_template.xml', + 'views/mass_mailing_contact_views.xml', + 'views/mass_mailing_list_views.xml', 'views/mass_mailing_views.xml', 'views/res_config_settings_views.xml', - 'security/ir.model.access.csv', - 'views/mass_mailing_layout.xml', - 'views/editor_field_html.xml', + 'report/mass_mailing_stats_report_views.xml', + 'views/assets.xml', + 'views/mass_mailing_templates_portal.xml', 'views/themes_templates.xml', 'views/snippets_themes.xml', - 'views/snippets_themes_options.xml', - 'views/mass_mailing_blacklist_views.xml', - 'views/unsubscribe_templates.xml', ], 'demo': [ 'data/mass_mailing_demo.xml', diff --git a/addons/mass_mailing/views/mass_mailing_layout.xml b/addons/mass_mailing/data/mail_data.xml similarity index 61% rename from addons/mass_mailing/views/mass_mailing_layout.xml rename to addons/mass_mailing/data/mail_data.xml index eae060d21ea..6c9bd7b591d 100644 --- a/addons/mass_mailing/views/mass_mailing_layout.xml +++ b/addons/mass_mailing/data/mail_data.xml @@ -1,22 +1,5 @@ - - - + - + diff --git a/addons/mass_mailing/models/__init__.py b/addons/mass_mailing/models/__init__.py index c542abccfba..afeb0313925 100644 --- a/addons/mass_mailing/models/__init__.py +++ b/addons/mass_mailing/models/__init__.py @@ -1,11 +1,12 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. +from . import link_tracker +from . import mass_mailing_contact +from . import mass_mailing_list from . import mass_mailing from . import mass_mailing_stats from . import mail_mail from . import mail_template from . import mail_thread from . import res_config_settings -from . import mass_mailing_report -from . import link_tracker diff --git a/addons/mass_mailing/models/mass_mailing.py b/addons/mass_mailing/models/mass_mailing.py index f4f598cf3c9..2a45147ec0d 100644 --- a/addons/mass_mailing/models/mass_mailing.py +++ b/addons/mass_mailing/models/mass_mailing.py @@ -43,251 +43,9 @@ class MassMailingTag(models.Model): color = fields.Integer(string='Color Index') _sql_constraints = [ - ('name_uniq', 'unique (name)', "Tag name already exists !"), + ('name_uniq', 'unique (name)', "Tag name already exists !"), ] -class MassMailingContactListRel(models.Model): - """ Intermediate model between mass mailing list and mass mailing contact - Indicates if a contact is opted out for a particular list - """ - _name = 'mail.mass_mailing.list_contact_rel' - _description = 'Mass Mailing Subscription Information' - _table = 'mail_mass_mailing_contact_list_rel' - _rec_name = 'contact_id' - - contact_id = fields.Many2one('mail.mass_mailing.contact', string='Contact', ondelete='cascade', required=True) - list_id = fields.Many2one('mail.mass_mailing.list', string='Mailing List', ondelete='cascade', required=True) - opt_out = fields.Boolean(string='Opt Out', - help='The contact has chosen not to receive mails anymore from this list', default=False) - unsubscription_date = fields.Datetime(string='Unsubscription Date') - contact_count = fields.Integer(related='list_id.contact_nbr', store=False, readonly=False) - message_bounce = fields.Integer(related='contact_id.message_bounce', store=False, readonly=False) - is_blacklisted = fields.Boolean(related='contact_id.is_blacklisted', store=False, readonly=False) - - _sql_constraints = [ - ('unique_contact_list', 'unique (contact_id, list_id)', - 'A contact cannot be subscribed multiple times to the same list!') - ] - - @api.model - def create(self, vals): - if 'opt_out' in vals: - vals['unsubscription_date'] = vals['opt_out'] and fields.Datetime.now() - return super(MassMailingContactListRel, self).create(vals) - - def write(self, vals): - if 'opt_out' in vals: - vals['unsubscription_date'] = vals['opt_out'] and fields.Datetime.now() - return super(MassMailingContactListRel, self).write(vals) - - def action_open_mailing_list_contact(self): - """TODO DBE : To remove - Deprecated""" - contact_id = self.contact_id - action = { - 'name': _(contact_id.name), - 'type': 'ir.actions.act_window', - 'res_model': 'mail.mass_mailing.contact', - 'view_mode': 'form', - 'target': 'current', - 'res_id': contact_id.id - } - return action - - -class MassMailingList(models.Model): - """Model of a contact list. """ - _name = 'mail.mass_mailing.list' - _order = 'name' - _description = 'Mailing List' - - name = fields.Char(string='Mailing List', required=True) - active = fields.Boolean(default=True) - contact_nbr = fields.Integer(compute="_compute_contact_nbr", string='Number of Contacts') - contact_ids = fields.Many2many( - 'mail.mass_mailing.contact', 'mail_mass_mailing_contact_list_rel', 'list_id', 'contact_id', - string='Mailing Lists') - subscription_contact_ids = fields.One2many('mail.mass_mailing.list_contact_rel', 'list_id', - string='Subscription Information') - is_public = fields.Boolean(default=True, help="The mailing list can be accessible by recipient in the unsubscription" - " page to allows him to update his subscription preferences.") - - # Compute number of contacts non opt-out, non blacklisted and valid email recipient for a mailing list - def _compute_contact_nbr(self): - self.env.cr.execute(''' - select - list_id, count(*) - from - mail_mass_mailing_contact_list_rel r - left join mail_mass_mailing_contact c on (r.contact_id=c.id) - left join mail_blacklist bl on c.email_normalized = bl.email and bl.active - where - list_id in %s - AND COALESCE(r.opt_out,FALSE) = FALSE - AND c.email_normalized IS NOT NULL - AND bl.id IS NULL - group by - list_id - ''', (tuple(self.ids), )) - data = dict(self.env.cr.fetchall()) - for mailing_list in self: - mailing_list.contact_nbr = data.get(mailing_list.id, 0) - - def name_get(self): - return [(list.id, "%s (%s)" % (list.name, list.contact_nbr)) for list in self] - - def action_merge(self, src_lists, archive): - """ - Insert all the contact from the mailing lists 'src_lists' to the - mailing list in 'self'. Possibility to archive the mailing lists - 'src_lists' after the merge except the destination mailing list 'self'. - """ - # Explation of the SQL query with an example. There are the following lists - # A (id=4): yti@odoo.com; yti@example.com - # B (id=5): yti@odoo.com; yti@openerp.com - # C (id=6): nothing - # To merge the mailing lists A and B into C, we build the view st that looks - # like this with our example: - # - # contact_id | email | row_number | list_id | - # ------------+---------------------------+------------------------ - # 4 | yti@odoo.com | 1 | 4 | - # 6 | yti@odoo.com | 2 | 5 | - # 5 | yti@example.com | 1 | 4 | - # 7 | yti@openerp.com | 1 | 5 | - # - # The row_column is kind of an occurence counter for the email address. - # Then we create the Many2many relation between the destination list and the contacts - # while avoiding to insert an existing email address (if the destination is in the source - # for example) - self.ensure_one() - # Put destination is sources lists if not already the case - src_lists |= self - self.env.cr.execute(""" - INSERT INTO mail_mass_mailing_contact_list_rel (contact_id, list_id) - SELECT st.contact_id AS contact_id, %s AS list_id - FROM - ( - SELECT - contact.id AS contact_id, - contact.email AS email, - mailing_list.id AS list_id, - row_number() OVER (PARTITION BY email ORDER BY email) AS rn - FROM - mail_mass_mailing_contact contact, - mail_mass_mailing_contact_list_rel contact_list_rel, - mail_mass_mailing_list mailing_list - WHERE contact.id=contact_list_rel.contact_id - AND COALESCE(contact_list_rel.opt_out,FALSE) = FALSE - AND contact.email_normalized NOT IN (select email from mail_blacklist where active = TRUE) - AND mailing_list.id=contact_list_rel.list_id - AND mailing_list.id IN %s - AND NOT EXISTS - ( - SELECT 1 - FROM - mail_mass_mailing_contact contact2, - mail_mass_mailing_contact_list_rel contact_list_rel2 - WHERE contact2.email = contact.email - AND contact_list_rel2.contact_id = contact2.id - AND contact_list_rel2.list_id = %s - ) - ) st - WHERE st.rn = 1;""", (self.id, tuple(src_lists.ids), self.id)) - self.invalidate_cache() - if archive: - (src_lists - self).write({'active': False}) - - def close_dialog(self): - return {'type': 'ir.actions.act_window_close'} - - -class MassMailingContact(models.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' - _inherit = ['mail.thread.blacklist'] - _description = 'Mass Mailing Contact' - _order = 'email' - _rec_name = 'email' - - name = fields.Char() - company_name = fields.Char(string='Company Name') - title_id = fields.Many2one('res.partner.title', string='Title') - email = fields.Char(required=True) - is_email_valid = fields.Boolean(compute='_compute_is_email_valid', store=True) - list_ids = fields.Many2many( - 'mail.mass_mailing.list', 'mail_mass_mailing_contact_list_rel', - 'contact_id', 'list_id', string='Mailing Lists') - subscription_list_ids = fields.One2many('mail.mass_mailing.list_contact_rel', - 'contact_id', string='Subscription Information') - country_id = fields.Many2one('res.country', string='Country') - tag_ids = fields.Many2many('res.partner.category', string='Tags') - opt_out = fields.Boolean('Opt Out', compute='_compute_opt_out', search='_search_opt_out', - help='Opt out flag for a specific mailing list.' - 'This field should not be used in a view without a unique and active mailing list context.') - - @api.depends('email') - def _compute_is_email_valid(self): - for record in self: - normalized = tools.email_normalize(record.email) - record.is_email_valid = normalized if not normalized else True - - @api.model - def _search_opt_out(self, operator, value): - # Assumes operator is '=' or '!=' and value is True or False - if operator != '=': - if operator == '!=' and isinstance(value, bool): - value = not value - else: - raise NotImplementedError() - - if 'default_list_ids' in self._context and isinstance(self._context['default_list_ids'], (list, tuple)) and len(self._context['default_list_ids']) == 1: - [active_list_id] = self._context['default_list_ids'] - contacts = self.env['mail.mass_mailing.list_contact_rel'].search([('list_id', '=', active_list_id)]) - return [('id', 'in', [record.contact_id.id for record in contacts if record.opt_out == value])] - else: - raise UserError('Search opt out cannot be executed without a unique and valid active mailing list context.') - - @api.depends('subscription_list_ids') - def _compute_opt_out(self): - if 'default_list_ids' in self._context and isinstance(self._context['default_list_ids'], (list, tuple)) and len(self._context['default_list_ids']) == 1: - [active_list_id] = self._context['default_list_ids'] - for record in self: - active_subscription_list = record.subscription_list_ids.filtered(lambda l: l.list_id.id == active_list_id) - record.opt_out = active_subscription_list.opt_out - else: - for record in self: - record.opt_out = False - - def get_name_email(self, name): - name, email = self.env['res.partner']._parse_partner_name(name) - if name and not email: - email = name - if email and not name: - name = email - return name, email - - @api.model - def name_create(self, name): - name, email = self.get_name_email(name) - contact = self.create({'name': name, 'email': email}) - return contact.name_get()[0] - - @api.model - def add_to_list(self, name, list_id): - name, email = self.get_name_email(name) - contact = self.create({'name': name, 'email': email, 'list_ids': [(4, list_id)]}) - return contact.name_get()[0] - - def _message_get_default_recipients(self): - return {r.id: { - 'partner_ids': [], - 'email_to': r.email_normalized, - 'email_cc': False} - for r in self} - class MassMailingStage(models.Model): diff --git a/addons/mass_mailing/models/mass_mailing_contact.py b/addons/mass_mailing/models/mass_mailing_contact.py new file mode 100644 index 00000000000..817c30830ac --- /dev/null +++ b/addons/mass_mailing/models/mass_mailing_contact.py @@ -0,0 +1,142 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import api, fields, models, tools, _ +from odoo.exceptions import UserError + + +class MassMailingContactListRel(models.Model): + """ Intermediate model between mass mailing list and mass mailing contact + Indicates if a contact is opted out for a particular list + """ + _name = 'mail.mass_mailing.list_contact_rel' + _description = 'Mass Mailing Subscription Information' + _table = 'mail_mass_mailing_contact_list_rel' + _rec_name = 'contact_id' + + contact_id = fields.Many2one('mail.mass_mailing.contact', string='Contact', ondelete='cascade', required=True) + list_id = fields.Many2one('mail.mass_mailing.list', string='Mailing List', ondelete='cascade', required=True) + opt_out = fields.Boolean(string='Opt Out', + help='The contact has chosen not to receive mails anymore from this list', default=False) + unsubscription_date = fields.Datetime(string='Unsubscription Date') + contact_count = fields.Integer(related='list_id.contact_nbr', store=False, readonly=False) + message_bounce = fields.Integer(related='contact_id.message_bounce', store=False, readonly=False) + is_blacklisted = fields.Boolean(related='contact_id.is_blacklisted', store=False, readonly=False) + + _sql_constraints = [ + ('unique_contact_list', 'unique (contact_id, list_id)', + 'A contact cannot be subscribed multiple times to the same list!') + ] + + @api.model + def create(self, vals): + if 'opt_out' in vals: + vals['unsubscription_date'] = vals['opt_out'] and fields.Datetime.now() + return super(MassMailingContactListRel, self).create(vals) + + def write(self, vals): + if 'opt_out' in vals: + vals['unsubscription_date'] = vals['opt_out'] and fields.Datetime.now() + return super(MassMailingContactListRel, self).write(vals) + + def action_open_mailing_list_contact(self): + """TODO DBE : To remove - Deprecated""" + contact_id = self.contact_id + action = { + 'name': _(contact_id.name), + 'type': 'ir.actions.act_window', + 'res_model': 'mail.mass_mailing.contact', + 'view_mode': 'form', + 'target': 'current', + 'res_id': contact_id.id + } + return action + + +class MassMailingContact(models.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' + _inherit = ['mail.thread.blacklist'] + _description = 'Mass Mailing Contact' + _order = 'email' + _rec_name = 'email' + + name = fields.Char() + company_name = fields.Char(string='Company Name') + title_id = fields.Many2one('res.partner.title', string='Title') + email = fields.Char(required=True) + is_email_valid = fields.Boolean(compute='_compute_is_email_valid', store=True) + list_ids = fields.Many2many( + 'mail.mass_mailing.list', 'mail_mass_mailing_contact_list_rel', + 'contact_id', 'list_id', string='Mailing Lists') + subscription_list_ids = fields.One2many('mail.mass_mailing.list_contact_rel', + 'contact_id', string='Subscription Information') + country_id = fields.Many2one('res.country', string='Country') + tag_ids = fields.Many2many('res.partner.category', string='Tags') + opt_out = fields.Boolean('Opt Out', compute='_compute_opt_out', search='_search_opt_out', + help='Opt out flag for a specific mailing list.' + 'This field should not be used in a view without a unique and active mailing list context.') + + @api.depends('email') + def _compute_is_email_valid(self): + for record in self: + normalized = tools.email_normalize(record.email) + record.is_email_valid = normalized if not normalized else True + + @api.model + def _search_opt_out(self, operator, value): + # Assumes operator is '=' or '!=' and value is True or False + if operator != '=': + if operator == '!=' and isinstance(value, bool): + value = not value + else: + raise NotImplementedError() + + if 'default_list_ids' in self._context and isinstance(self._context['default_list_ids'], (list, tuple)) and len(self._context['default_list_ids']) == 1: + [active_list_id] = self._context['default_list_ids'] + contacts = self.env['mail.mass_mailing.list_contact_rel'].search([('list_id', '=', active_list_id)]) + return [('id', 'in', [record.contact_id.id for record in contacts if record.opt_out == value])] + else: + raise UserError('Search opt out cannot be executed without a unique and valid active mailing list context.') + + @api.depends('subscription_list_ids') + def _compute_opt_out(self): + if 'default_list_ids' in self._context and isinstance(self._context['default_list_ids'], (list, tuple)) and len(self._context['default_list_ids']) == 1: + [active_list_id] = self._context['default_list_ids'] + for record in self: + active_subscription_list = record.subscription_list_ids.filtered(lambda l: l.list_id.id == active_list_id) + record.opt_out = active_subscription_list.opt_out + else: + for record in self: + record.opt_out = False + + def get_name_email(self, name): + name, email = self.env['res.partner']._parse_partner_name(name) + if name and not email: + email = name + if email and not name: + name = email + return name, email + + @api.model + def name_create(self, name): + name, email = self.get_name_email(name) + contact = self.create({'name': name, 'email': email}) + return contact.name_get()[0] + + @api.model + def add_to_list(self, name, list_id): + name, email = self.get_name_email(name) + contact = self.create({'name': name, 'email': email, 'list_ids': [(4, list_id)]}) + return contact.name_get()[0] + + def _message_get_default_recipients(self): + return {r.id: { + 'partner_ids': [], + 'email_to': r.email_normalized, + 'email_cc': False} + for r in self + } diff --git a/addons/mass_mailing/models/mass_mailing_list.py b/addons/mass_mailing/models/mass_mailing_list.py new file mode 100644 index 00000000000..48cde5784c3 --- /dev/null +++ b/addons/mass_mailing/models/mass_mailing_list.py @@ -0,0 +1,111 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import api, fields, models + + +class MassMailingList(models.Model): + """Model of a contact list. """ + _name = 'mail.mass_mailing.list' + _order = 'name' + _description = 'Mailing List' + + name = fields.Char(string='Mailing List', required=True) + active = fields.Boolean(default=True) + contact_nbr = fields.Integer(compute="_compute_contact_nbr", string='Number of Contacts') + contact_ids = fields.Many2many( + 'mail.mass_mailing.contact', 'mail_mass_mailing_contact_list_rel', 'list_id', 'contact_id', + string='Mailing Lists') + subscription_contact_ids = fields.One2many('mail.mass_mailing.list_contact_rel', 'list_id', + string='Subscription Information') + is_public = fields.Boolean(default=True, help="The mailing list can be accessible by recipient in the unsubscription" + " page to allows him to update his subscription preferences.") + + # Compute number of contacts non opt-out, non blacklisted and valid email recipient for a mailing list + def _compute_contact_nbr(self): + self.env.cr.execute(''' + select + list_id, count(*) + from + mail_mass_mailing_contact_list_rel r + left join mail_mass_mailing_contact c on (r.contact_id=c.id) + left join mail_blacklist bl on c.email_normalized = bl.email and bl.active + where + list_id in %s + AND COALESCE(r.opt_out,FALSE) = FALSE + AND c.email_normalized IS NOT NULL + AND bl.id IS NULL + group by + list_id + ''', (tuple(self.ids), )) + data = dict(self.env.cr.fetchall()) + for mailing_list in self: + mailing_list.contact_nbr = data.get(mailing_list.id, 0) + + def name_get(self): + return [(list.id, "%s (%s)" % (list.name, list.contact_nbr)) for list in self] + + def action_merge(self, src_lists, archive): + """ + Insert all the contact from the mailing lists 'src_lists' to the + mailing list in 'self'. Possibility to archive the mailing lists + 'src_lists' after the merge except the destination mailing list 'self'. + """ + # Explation of the SQL query with an example. There are the following lists + # A (id=4): yti@odoo.com; yti@example.com + # B (id=5): yti@odoo.com; yti@openerp.com + # C (id=6): nothing + # To merge the mailing lists A and B into C, we build the view st that looks + # like this with our example: + # + # contact_id | email | row_number | list_id | + # ------------+---------------------------+------------------------ + # 4 | yti@odoo.com | 1 | 4 | + # 6 | yti@odoo.com | 2 | 5 | + # 5 | yti@example.com | 1 | 4 | + # 7 | yti@openerp.com | 1 | 5 | + # + # The row_column is kind of an occurence counter for the email address. + # Then we create the Many2many relation between the destination list and the contacts + # while avoiding to insert an existing email address (if the destination is in the source + # for example) + self.ensure_one() + # Put destination is sources lists if not already the case + src_lists |= self + self.env.cr.execute(""" + INSERT INTO mail_mass_mailing_contact_list_rel (contact_id, list_id) + SELECT st.contact_id AS contact_id, %s AS list_id + FROM + ( + SELECT + contact.id AS contact_id, + contact.email AS email, + mailing_list.id AS list_id, + row_number() OVER (PARTITION BY email ORDER BY email) AS rn + FROM + mail_mass_mailing_contact contact, + mail_mass_mailing_contact_list_rel contact_list_rel, + mail_mass_mailing_list mailing_list + WHERE contact.id=contact_list_rel.contact_id + AND COALESCE(contact_list_rel.opt_out,FALSE) = FALSE + AND contact.email_normalized NOT IN (select email from mail_blacklist where active = TRUE) + AND mailing_list.id=contact_list_rel.list_id + AND mailing_list.id IN %s + AND NOT EXISTS + ( + SELECT 1 + FROM + mail_mass_mailing_contact contact2, + mail_mass_mailing_contact_list_rel contact_list_rel2 + WHERE contact2.email = contact.email + AND contact_list_rel2.contact_id = contact2.id + AND contact_list_rel2.list_id = %s + ) + ) st + WHERE st.rn = 1;""", (self.id, tuple(src_lists.ids), self.id)) + self.invalidate_cache() + if archive: + (src_lists - self).write({'active': False}) + + def close_dialog(self): + return {'type': 'ir.actions.act_window_close'} diff --git a/addons/mass_mailing/report/__init__.py b/addons/mass_mailing/report/__init__.py new file mode 100644 index 00000000000..2796202ba19 --- /dev/null +++ b/addons/mass_mailing/report/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import mass_mailing_stats_report diff --git a/addons/mass_mailing/models/mass_mailing_report.py b/addons/mass_mailing/report/mass_mailing_stats_report.py similarity index 100% rename from addons/mass_mailing/models/mass_mailing_report.py rename to addons/mass_mailing/report/mass_mailing_stats_report.py diff --git a/addons/mass_mailing/views/mass_mailing_report_views.xml b/addons/mass_mailing/report/mass_mailing_stats_report_views.xml similarity index 100% rename from addons/mass_mailing/views/mass_mailing_report_views.xml rename to addons/mass_mailing/report/mass_mailing_stats_report_views.xml diff --git a/addons/mass_mailing/views/assets.xml b/addons/mass_mailing/views/assets.xml new file mode 100644 index 00000000000..03b1a1415bc --- /dev/null +++ b/addons/mass_mailing/views/assets.xml @@ -0,0 +1,78 @@ + + + + + + + + + + + + + - diff --git a/addons/mass_mailing/views/unsubscribe_templates.xml b/addons/mass_mailing/views/mass_mailing_templates_portal.xml similarity index 100% rename from addons/mass_mailing/views/unsubscribe_templates.xml rename to addons/mass_mailing/views/mass_mailing_templates_portal.xml diff --git a/addons/mass_mailing/views/mass_mailing_views.xml b/addons/mass_mailing/views/mass_mailing_views.xml index 77a8c28c35a..7bbf205497f 100644 --- a/addons/mass_mailing/views/mass_mailing_views.xml +++ b/addons/mass_mailing/views/mass_mailing_views.xml @@ -1,435 +1,5 @@ - - - - mail.mass_mailing.list_contact_rel.view.form - mail.mass_mailing.list_contact_rel - 10 - -
- - - - - - -
-
-
- - - - mail.mass_mailing.list_contact_rel.list_contact.view.tree - mail.mass_mailing.list_contact_rel - - - - - - - - - -
-
- - - - - - -
-
- - - mail.mass_mailing.list.form.simplified - mail.mass_mailing.list - -
- - -
-
-
-
- - - -
-
-
-
-
- - - Create a Mass Mailing List - mail.mass_mailing.list - form - - new - - - - mail.mass_mailing.list.kanban - mail.mass_mailing.list - - - - - - - -
-
-
-
-
- -
-
-
- -
- -
-
-
-
-
-
- - - Mailing Lists - mail.mass_mailing.list - kanban,tree,form - -

- Create a new mailing list -

- You don't need to import your mailing lists, you can easily - send emails
to any contact saved in other Odoo apps. -

-
-
- - - mail.mass_mailing.search diff --git a/addons/mass_mailing/views/mass_mailing_views_menus.xml b/addons/mass_mailing/views/mass_mailing_views_menus.xml index 57832e6850a..4d83cae5370 100644 --- a/addons/mass_mailing/views/mass_mailing_views_menus.xml +++ b/addons/mass_mailing/views/mass_mailing_views_menus.xml @@ -4,9 +4,15 @@ + + + +
\ No newline at end of file diff --git a/addons/mass_mailing/views/snippets_themes.xml b/addons/mass_mailing/views/snippets_themes.xml index 7e908e12033..ac6c75ba128 100644 --- a/addons/mass_mailing/views/snippets_themes.xml +++ b/addons/mass_mailing/views/snippets_themes.xml @@ -769,4 +769,52 @@ + + + diff --git a/addons/mass_mailing/views/snippets_themes_options.xml b/addons/mass_mailing/views/snippets_themes_options.xml deleted file mode 100644 index 7700dd00d14..00000000000 --- a/addons/mass_mailing/views/snippets_themes_options.xml +++ /dev/null @@ -1,51 +0,0 @@ - - - - - - From f5b1105c9752ba5836d24e71f3780a611ec1801f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Thu, 4 Jul 2019 11:19:53 +0000 Subject: [PATCH 2/7] [IMP] mass_mailing: perform small code fixes / move PURPOSE Mass mailing is currently a bit messy. As SMS will be added as a way to notify people in mass through SMS let us take this opportunity to somehow clean this application: organization, light code cleaning, model renaming. SPECIFICATIONS Move some code bits / fields declarations to reorder them. Purpose is to make code easier to understand and find. Funny we found two override of create that have been merged. An override of name create is not necessary considering code was present in one of the two merged create. Make internal methods private. Rename send_mail to action_send_mail to avoid confusion with composer and template send_mail methods. LINKS Task ID 2037906 Preparing task ID 1997464 (SMS addition in mass mailing) PR #34938 --- addons/mass_mailing/models/mass_mailing.py | 226 +++++++++--------- .../mass_mailing/models/mass_mailing_stats.py | 8 +- .../tests/test_mail_auto_blacklist.py | 2 +- .../tests/test_mass_mailing.py | 6 +- .../tests/test_performance.py | 4 +- 5 files changed, 120 insertions(+), 126 deletions(-) diff --git a/addons/mass_mailing/models/mass_mailing.py b/addons/mass_mailing/models/mass_mailing.py index 2a45147ec0d..c3a8c851814 100644 --- a/addons/mass_mailing/models/mass_mailing.py +++ b/addons/mass_mailing/models/mass_mailing.py @@ -161,7 +161,7 @@ class MassMailingCampaign(models.Model): for campaign in self: campaign.total_mailings = mapped_data.get(campaign.id, 0) - def get_recipients(self, model=None): + def _get_recipients(self, model=None): """Return the recipients of a mailing campaign. This is based on the statistics build for each mailing. """ res = dict.fromkeys(self.ids, {}) @@ -184,7 +184,6 @@ class MassMailingCampaign(models.Model): class MassMailing(models.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 @@ -275,7 +274,7 @@ class MassMailing(models.Model): def _compute_total(self): for mass_mailing in self: - mass_mailing.total = len(mass_mailing.sudo().get_recipients()) + mass_mailing.total = len(mass_mailing.sudo()._get_recipients()) def _compute_clicks_ratio(self): self.env.cr.execute(""" @@ -331,24 +330,6 @@ class MassMailing(models.Model): row['bounced_ratio'] = 100.0 * row['bounced'] / total self.browse(row.pop('mailing_id')).update(row) - def _unsubscribe_token(self, res_id, email): - """Generate a secure hash for this mailing list and parameters. - - This is appended to the unsubscription URL and then checked at - unsubscription time to ensure no malicious unsubscriptions are - performed. - - :param int res_id: - ID of the resource that will be unsubscribed. - - :param str email: - Email of the resource that will be unsubscribed. - """ - secret = self.env["ir.config_parameter"].sudo().get_param( - "database.secret") - token = (self.env.cr.dbname, self.id, int(res_id), tools.ustr(email)) - return hmac.new(secret.encode('utf-8'), repr(token).encode('utf-8'), hashlib.sha512).hexdigest() - def _compute_next_departure(self): cron_next_call = self.env.ref('mass_mailing.ir_cron_mass_mailing_queue').sudo().nextcall str2dt = fields.Datetime.from_string @@ -390,21 +371,22 @@ class MassMailing(models.Model): if self.subject and not self.name: self.name = self.subject - #------------------------------------------------------ - # Technical stuff - #------------------------------------------------------ + # ------------------------------------------------------ + # ORM + # ------------------------------------------------------ @api.model - def name_create(self, name): - """ _rec_name is source_id, creates a utm.source instead """ - mass_mailing = self.create({'name': name, 'subject': name}) - return mass_mailing.name_get()[0] + def create(self, values): + if values.get('name') and not values.get('subject'): + values['subject'] = values['name'] + if values.get('body_html'): + values['body_html'] = self._convert_inline_images_to_urls(values['body_html']) + return super(MassMailing, self).create(values) - @api.model - def create(self, vals): - if vals.get('name') and not vals.get('subject'): - vals['subject'] = vals['name'] - return super(MassMailing, self).create(vals) + def write(self, values): + if values.get('body_html'): + values['body_html'] = self._convert_inline_images_to_urls(values['body_html']) + return super(MassMailing, self).write(values) @api.returns('self', lambda value: value.id) def copy(self, default=None): @@ -416,74 +398,9 @@ class MassMailing(models.Model): def _group_expand_states(self, states, domain, order): return [key for key, val in type(self).state.selection] - def update_opt_out(self, email, list_ids, value): - if len(list_ids) > 0: - model = self.env['mail.mass_mailing.contact'].with_context(active_test=False) - records = model.search([('email_normalized', '=', tools.email_normalize(email))]) - opt_out_records = self.env['mail.mass_mailing.list_contact_rel'].search([ - ('contact_id', 'in', records.ids), - ('list_id', 'in', list_ids), - ('opt_out', '!=', value) - ]) - - opt_out_records.write({'opt_out': value}) - message = _('The recipient unsubscribed from %s mailing list(s)') \ - if value else _('The recipient subscribed to %s mailing list(s)') - for record in records: - # filter the list_id by record - record_lists = opt_out_records.filtered(lambda rec: rec.contact_id.id == record.id) - if len(record_lists) > 0: - record.sudo().message_post(body=_(message % ', '.join(str(list.name) for list in record_lists.mapped('list_id')))) - - @api.model - def create(self, values): - if values.get('body_html'): - values['body_html'] = self._convert_inline_images_to_urls(values['body_html']) - return super(MassMailing, self).create(values) - - def write(self, values): - if values.get('body_html'): - values['body_html'] = self._convert_inline_images_to_urls(values['body_html']) - return super(MassMailing, self).write(values) - - def _convert_inline_images_to_urls(self, body_html): - """ - Find inline base64 encoded images, make an attachement out of - them and replace the inline image with an url to the attachement. - """ - - def _image_to_url(b64image: bytes): - """Store an image in an attachement and returns an url""" - attachment = self.env['ir.attachment'].create({ - 'datas': b64image, - 'name': "cropped_image_mailing_{}".format(self.id), - 'type': 'binary',}) - - attachment.generate_access_token() - - return '/web/image/%s?access_token=%s' % ( - attachment.id, attachment.access_token) - - - modified = False - root = lxml.html.fromstring(body_html) - for node in root.iter('img'): - match = image_re.match(node.attrib.get('src', '')) - if match: - mime = match.group(1) # unsed - image = match.group(2).encode() # base64 image as bytes - - node.attrib['src'] = _image_to_url(image) - modified = True - - if modified: - return lxml.html.tostring(root) - - return body_html - - #------------------------------------------------------ - # Views & Actions - #------------------------------------------------------ + # ------------------------------------------------------ + # ACTIONS + # ------------------------------------------------------ def action_duplicate(self): self.ensure_one() @@ -528,7 +445,7 @@ class MassMailing(models.Model): failed_mails = self.env['mail.mail'].search([('mailing_id', 'in', self.ids), ('state', '=', 'exception')]) failed_mails.mapped('statistics_ids').unlink() failed_mails.sudo().unlink() - res_ids = self.get_recipients() + res_ids = self._get_recipients() except_mailed = self.env['mail.mail.statistics'].search([ ('model', '=', self.mailing_model_real), ('res_id', 'in', res_ids), @@ -571,9 +488,28 @@ class MassMailing(models.Model): 'domain': [('id', 'in', res_ids)], } - #------------------------------------------------------ + def update_opt_out(self, email, list_ids, value): + if len(list_ids) > 0: + model = self.env['mail.mass_mailing.contact'].with_context(active_test=False) + records = model.search([('email_normalized', '=', tools.email_normalize(email))]) + opt_out_records = self.env['mail.mass_mailing.list_contact_rel'].search([ + ('contact_id', 'in', records.ids), + ('list_id', 'in', list_ids), + ('opt_out', '!=', value) + ]) + + opt_out_records.write({'opt_out': value}) + message = _('The recipient unsubscribed from %s mailing list(s)') \ + if value else _('The recipient subscribed to %s mailing list(s)') + for record in records: + # filter the list_id by record + record_lists = opt_out_records.filtered(lambda rec: rec.contact_id.id == record.id) + if len(record_lists) > 0: + record.sudo().message_post(body=_(message % ', '.join(str(list.name) for list in record_lists.mapped('list_id')))) + + # ------------------------------------------------------ # Email Sending - #------------------------------------------------------ + # ------------------------------------------------------ def _get_opt_out_list(self): """Returns a set of emails opted-out in target model""" @@ -672,7 +608,7 @@ class MassMailing(models.Model): 'post_convert_links': self._get_convert_links(), } - def get_recipients(self): + def _get_recipients(self): if self.mailing_domain: domain = safe_eval(self.mailing_domain) res_ids = self.env[self.mailing_model_real].search(domain).ids @@ -685,7 +621,7 @@ class MassMailing(models.Model): contact_nbr = self.env[self.mailing_model_real].search_count(domain) topick = int(contact_nbr / 100.0 * self.contact_ab_pc) if self.mass_mailing_campaign_id and self.mass_mailing_campaign_id.unique_ab_testing: - already_mailed = self.mass_mailing_campaign_id.get_recipients()[self.mass_mailing_campaign_id.id] + already_mailed = self.mass_mailing_campaign_id._get_recipients()[self.mass_mailing_campaign_id.id] else: already_mailed = set([]) remaining = set(res_ids).difference(already_mailed) @@ -694,20 +630,21 @@ class MassMailing(models.Model): res_ids = random.sample(remaining, topick) return res_ids - def get_remaining_recipients(self): - res_ids = self.get_recipients() - already_mailed = self.env['mail.mail.statistics'].search_read([('model', '=', self.mailing_model_real), - ('res_id', 'in', res_ids), - ('mass_mailing_id', '=', self.id)], ['res_id']) - already_mailed_res_ids = [record['res_id'] for record in already_mailed] - return list(set(res_ids) - set(already_mailed_res_ids)) + def _get_remaining_recipients(self): + res_ids = self._get_recipients() + already_mailed = self.env['mail.mail.statistics'].search_read([ + ('model', '=', self.mailing_model_real), + ('res_id', 'in', res_ids), + ('mass_mailing_id', '=', self.id)], ['res_id']) + done_res_ids = [record['res_id'] for record in already_mailed] + return [rid for rid in res_ids if rid not in done_res_ids] - def send_mail(self, res_ids=None): + def action_send_mail(self, res_ids=None): author_id = self.env.user.partner_id.id for mailing in self: if not res_ids: - res_ids = mailing.get_remaining_recipients() + res_ids = mailing._get_remaining_recipients() if not res_ids: raise UserError(_('There is no recipients selected.')) @@ -765,8 +702,63 @@ class MassMailing(models.Model): for mass_mailing in mass_mailings: user = mass_mailing.write_uid or self.env.user mass_mailing = mass_mailing.with_context(**user.with_user(user).context_get()) - if len(mass_mailing.get_remaining_recipients()) > 0: + if len(mass_mailing._get_remaining_recipients()) > 0: mass_mailing.state = 'sending' - mass_mailing.send_mail() + mass_mailing.action_send_mail() else: mass_mailing.write({'state': 'done', 'sent_date': fields.Datetime.now()}) + + # ------------------------------------------------------ + # TOOLS + # ------------------------------------------------------ + + def _unsubscribe_token(self, res_id, email): + """Generate a secure hash for this mailing list and parameters. + + This is appended to the unsubscription URL and then checked at + unsubscription time to ensure no malicious unsubscriptions are + performed. + + :param int res_id: + ID of the resource that will be unsubscribed. + + :param str email: + Email of the resource that will be unsubscribed. + """ + secret = self.env["ir.config_parameter"].sudo().get_param("database.secret") + token = (self.env.cr.dbname, self.id, int(res_id), tools.ustr(email)) + return hmac.new(secret.encode('utf-8'), repr(token).encode('utf-8'), hashlib.sha512).hexdigest() + + def _convert_inline_images_to_urls(self, body_html): + """ + Find inline base64 encoded images, make an attachement out of + them and replace the inline image with an url to the attachement. + """ + + def _image_to_url(b64image: bytes): + """Store an image in an attachement and returns an url""" + attachment = self.env['ir.attachment'].create({ + 'datas': b64image, + 'name': "cropped_image_mailing_{}".format(self.id), + 'type': 'binary',}) + + attachment.generate_access_token() + + return '/web/image/%s?access_token=%s' % ( + attachment.id, attachment.access_token) + + modified = False + root = lxml.html.fromstring(body_html) + for node in root.iter('img'): + match = image_re.match(node.attrib.get('src', '')) + if match: + mime = match.group(1) # unsed + image = match.group(2).encode() # base64 image as bytes + + node.attrib['src'] = _image_to_url(image) + modified = True + + if modified: + return lxml.html.tostring(root) + + return body_html diff --git a/addons/mass_mailing/models/mass_mailing_stats.py b/addons/mass_mailing/models/mass_mailing_stats.py index f0d8a763438..0710a0e8b1a 100644 --- a/addons/mass_mailing/models/mass_mailing_stats.py +++ b/addons/mass_mailing/models/mass_mailing_stats.py @@ -15,6 +15,7 @@ class MailMailStats(models.Model): _rec_name = 'message_id' _order = 'message_id' + # mail data mail_mail_id = fields.Many2one('mail.mail', string='Mail', index=True) mail_mail_id_int = fields.Integer( string='Mail ID (tech)', @@ -23,7 +24,9 @@ class MailMailStats(models.Model): 'However the ID is needed for several action and controllers.', index=True, ) + email = fields.Char(string="Recipient email address") message_id = fields.Char(string='Message-ID') + # document model = fields.Char(string='Document model') res_id = fields.Integer(string='Document ID') # campaign / wave data @@ -54,9 +57,8 @@ class MailMailStats(models.Model): ('bounced', 'Bounced'), ('ignored', 'Ignored')], store=True) state_update = fields.Datetime(compute="_compute_state", string='State Update', - help='Last state update of the mail', - store=True) - email = fields.Char(string="Recipient email address") + help='Last state update of the mail', + store=True) @api.depends('sent', 'opened', 'clicked', 'replied', 'bounced', 'exception', 'ignored') def _compute_state(self): diff --git a/addons/test_mass_mailing/tests/test_mail_auto_blacklist.py b/addons/test_mass_mailing/tests/test_mail_auto_blacklist.py index ac04a835847..eee8c7ea964 100644 --- a/addons/test_mass_mailing/tests/test_mail_auto_blacklist.py +++ b/addons/test_mass_mailing/tests/test_mail_auto_blacklist.py @@ -54,7 +54,7 @@ class TestAutoBlacklist(common.TransactionCase): [self.mailing_contact_1.id])], 'body_html': 'This is a bounced mail for auto blacklist demo'}) self.mass_mailing.put_in_queue() - res_ids = self.mass_mailing.get_remaining_recipients() + res_ids = self.mass_mailing._get_remaining_recipients() composer_values = { 'body': self.mass_mailing.convert_links()[self.mass_mailing.id], 'subject': self.mass_mailing.name, diff --git a/addons/test_mass_mailing/tests/test_mass_mailing.py b/addons/test_mass_mailing/tests/test_mass_mailing.py index 28ba0596e6b..f9d4e0d845e 100644 --- a/addons/test_mass_mailing/tests/test_mass_mailing.py +++ b/addons/test_mass_mailing/tests/test_mass_mailing.py @@ -42,7 +42,7 @@ class TestMassMail(MassMailingCase): 'mailing_domain': [('id', 'in', self.mm_recs.ids)], }) - mailing.send_mail() + mailing.action_send_mail() # basic test emails are sent self.assertEqual(mailing.sent, 5) @@ -92,7 +92,7 @@ class TestAccessRights(TransactionCase): self.mailing_contact_4.id, self.mailing_contact_5.id])], 'body_html': 'This is mass mail marketing demo'}) self.mass_mailing.put_in_queue() - res_ids = self.mass_mailing.get_remaining_recipients() + res_ids = self.mass_mailing._get_remaining_recipients() composer_values = { 'body': self.mass_mailing.convert_links()[self.mass_mailing.id], 'subject': self.mass_mailing.name, @@ -296,7 +296,7 @@ class TestOnResPartner(TransactionCase): 'body_html': 'This is mass mail marketing demo'}) self.mass_mailing.mailing_model_real = 'res.partner' self.mass_mailing.put_in_queue() - res_ids = self.mass_mailing.get_remaining_recipients() + res_ids = self.mass_mailing._get_remaining_recipients() composer_values = { 'body': self.mass_mailing.convert_links()[self.mass_mailing.id], 'subject': self.mass_mailing.name, diff --git a/addons/test_mass_mailing/tests/test_performance.py b/addons/test_mass_mailing/tests/test_performance.py index a3b6699e2be..2bc18cf1f99 100644 --- a/addons/test_mass_mailing/tests/test_performance.py +++ b/addons/test_mass_mailing/tests/test_performance.py @@ -51,7 +51,7 @@ class TestMassMailPerformance(TestMassMailPerformanceBase): }) with self.assertQueryCount(__system__=2477, marketing=3128): - mailing.send_mail() + mailing.action_send_mail() self.assertEqual(mailing.sent, 50) self.assertEqual(mailing.delivered, 50) @@ -89,7 +89,7 @@ class TestMassMailBlPerformance(TestMassMailPerformanceBase): }) with self.assertQueryCount(__system__=2864, marketing=3611): - mailing.send_mail() + mailing.action_send_mail() self.assertEqual(mailing.sent, 50) self.assertEqual(mailing.delivered, 50) From 20dfba6e11ac4553509c1a3516c6453b8f8023e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Wed, 17 Jul 2019 10:20:25 +0000 Subject: [PATCH 3/7] [FIX] mass_mailing: fix management and propagation of UTMs PURPOSE Mass mailing is currently a bit messy. As SMS will be added as a way to notify people in mass through SMS let us take this opportunity to somehow clean this application: organization, light code cleaning, model renaming. SPECIFICATIONS Fix UTM management and propagation in mass mailing. Right UTM definition in mass mailing * mass mailing campaign -> utm.campaign * mass mailing -> utm.source * "email" -> utm.medium Therefore * remove campaign setting source and medium as each mailing is a source and medium is "email"; * ensure each mailing is a separate source (otherwise name is shared); * ensure UTM values propagate to link creation are those values and not the one coming from the mass mailing campaign; LINKS Task ID 2037906 Preparing task ID 1997464 (SMS addition in mass mailing) PR #34938 --- .../mass_mailing/data/mass_mailing_demo.xml | 1 - addons/mass_mailing/models/mass_mailing.py | 26 +++++++------------ .../report/mass_mailing_stats_report.py | 2 +- .../data/mass_mailing_demo.xml | 1 - .../data/mass_mailing_demo.xml | 1 - 5 files changed, 11 insertions(+), 20 deletions(-) diff --git a/addons/mass_mailing/data/mass_mailing_demo.xml b/addons/mass_mailing/data/mass_mailing_demo.xml index c1fdc2483f2..b2054c5636d 100644 --- a/addons/mass_mailing/data/mass_mailing_demo.xml +++ b/addons/mass_mailing/data/mass_mailing_demo.xml @@ -78,7 +78,6 @@ - email diff --git a/addons/mass_mailing/models/mass_mailing.py b/addons/mass_mailing/models/mass_mailing.py index c3a8c851814..3cc86fbebd4 100644 --- a/addons/mass_mailing/models/mass_mailing.py +++ b/addons/mass_mailing/models/mass_mailing.py @@ -228,8 +228,7 @@ class MassMailing(models.Model): help="This name helps you tracking your different campaign efforts, e.g. Fall_Drive, Christmas_Special") source_id = fields.Many2one('utm.source', string='Source', required=True, ondelete='cascade', help="This is the link source, e.g. Search Engine, another domain, or name of email list") - medium_id = fields.Many2one('utm.medium', string='Medium', - help="This is the delivery method, e.g. Postcard, Email, or Banner Ad", default=lambda self: self.env.ref('utm.utm_medium_email')) + medium_id = fields.Many2one('utm.medium', string='Medium', help="Delivery method: Email") clicks_ratio = fields.Integer(compute="_compute_clicks_ratio", string="Number of Clicks") state = fields.Selection([('draft', 'Draft'), ('in_queue', 'In Queue'), ('sending', 'Sending'), ('done', 'Sent')], string='Status', required=True, copy=False, default='draft', group_expand='_group_expand_states') @@ -344,10 +343,7 @@ class MassMailing(models.Model): @api.onchange('mass_mailing_campaign_id') def _onchange_mass_mailing_campaign_id(self): if self.mass_mailing_campaign_id: - dic = {'campaign_id': self.mass_mailing_campaign_id.campaign_id, - 'source_id': self.mass_mailing_campaign_id.source_id, - 'medium_id': self.mass_mailing_campaign_id.medium_id} - self.update(dic) + self.campaign_id = self.mass_mailing_campaign_id.campaign_id.id @api.onchange('mailing_model_id', 'contact_list_ids') def _onchange_model_and_list(self): @@ -381,6 +377,8 @@ class MassMailing(models.Model): values['subject'] = values['name'] if values.get('body_html'): values['body_html'] = self._convert_inline_images_to_urls(values['body_html']) + if 'medium_id' not in values: + values['medium_id'] = self.env.ref('utm.utm_medium_email').id return super(MassMailing, self).create(values) def write(self, values): @@ -534,18 +532,14 @@ class MassMailing(models.Model): return opt_out def _get_convert_links(self): - self.ensure_one() - utm_mixin = self.mass_mailing_campaign_id if self.mass_mailing_campaign_id else self - vals = {'mass_mailing_id': self.id} - + vals = { + 'mass_mailing_id': self.id, + 'source_id': self.source_id.id, + 'medium_id': self.medium_id.id, + } if self.mass_mailing_campaign_id: vals['mass_mailing_campaign_id'] = self.mass_mailing_campaign_id.id - if utm_mixin.campaign_id: - vals['campaign_id'] = utm_mixin.campaign_id.id - if utm_mixin.source_id: - vals['source_id'] = utm_mixin.source_id.id - if utm_mixin.medium_id: - vals['medium_id'] = utm_mixin.medium_id.id + vals['campaign_id'] = self.mass_mailing_campaign_id.campaign_id.id return vals def _get_seen_list(self): diff --git a/addons/mass_mailing/report/mass_mailing_stats_report.py b/addons/mass_mailing/report/mass_mailing_stats_report.py index de63344389c..358e90d0dfa 100644 --- a/addons/mass_mailing/report/mass_mailing_stats_report.py +++ b/addons/mass_mailing/report/mass_mailing_stats_report.py @@ -9,7 +9,7 @@ class MassMailingReport(models.Model): _auto = False _description = 'Mass Mailing Statistics' - scheduled_date = fields.Datetime(stirng='Scheduled Date', readonly=True) + scheduled_date = fields.Datetime(string='Scheduled Date', readonly=True) name = fields.Char(string='Mass Mail', readonly=True) campaign = fields.Char(string='Mass Mail Campaign', readonly=True) sent = fields.Integer(readonly=True) diff --git a/addons/mass_mailing_crm/data/mass_mailing_demo.xml b/addons/mass_mailing_crm/data/mass_mailing_demo.xml index 195ec7b5791..08366c44a86 100644 --- a/addons/mass_mailing_crm/data/mass_mailing_demo.xml +++ b/addons/mass_mailing_crm/data/mass_mailing_demo.xml @@ -10,7 +10,6 @@ - [] email diff --git a/addons/mass_mailing_sale/data/mass_mailing_demo.xml b/addons/mass_mailing_sale/data/mass_mailing_demo.xml index 9a5524c532f..3eead4e66b5 100644 --- a/addons/mass_mailing_sale/data/mass_mailing_demo.xml +++ b/addons/mass_mailing_sale/data/mass_mailing_demo.xml @@ -10,7 +10,6 @@ - [] email From 2896aad34e797b6ca65a8efe3c5d71f31500a9a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Wed, 17 Jul 2019 09:26:42 +0000 Subject: [PATCH 4/7] [REF] mass_mailing: rename mail.mail.statistics and reporting model PURPOSE Mass mailing is currently a bit messy. As SMS will be added as a way to notify people in mass through SMS let us take this opportunity to somehow clean this application: organization, light code cleaning, model renaming. SPECIFICATIONS Rename mail.mail.statistics to mailing.trace and mail.statistics.report to mail.trace.report. Rationale : * mail.mail.statistics is linked to mail.mail model. Soon this model will hold data related to SMS sending. It makes sense to be broader in the naming; * mailing.trace is more inlined with marketing.trace model that is the marketing automation model using it in marketing automation (enterprise application); * mailing.trace is shorter to write; * mail.statistics.report model should sense to be updated at the same time; MIGRATION mail.mail.statistics model -> mailing.trace mail_mail_statistics table -> mailing_trace mail.statistics.report model -> mail.trace.report fields updated (w column change) * link.tracker.click: mail_stat_id -> mailing_trace_id fields updated (no column change) * mail.mail: statistics_ids -> mailing_trace_ids * mail.mass_mailing: statistics_ids -> mailing_trace_ids LINKS Task ID 2037906 Preparing task ID 1997464 (SMS addition in mass mailing) PR #34938 --- addons/mass_mailing/__manifest__.py | 4 +- addons/mass_mailing/controllers/main.py | 8 ++-- .../mass_mailing/data/mass_mailing_demo.xml | 24 ++++++------ addons/mass_mailing/models/__init__.py | 2 +- addons/mass_mailing/models/link_tracker.py | 20 +++++----- addons/mass_mailing/models/mail_mail.py | 24 ++++++------ addons/mass_mailing/models/mail_thread.py | 20 +++++----- ...mass_mailing_stats.py => mailing_trace.py} | 38 +++++++++---------- addons/mass_mailing/models/mass_mailing.py | 38 +++++++++---------- addons/mass_mailing/report/__init__.py | 2 +- ...tats_report.py => mailing_trace_report.py} | 14 +++---- ...ews.xml => mailing_trace_report_views.xml} | 24 ++++++------ .../mass_mailing/security/ir.model.access.csv | 6 +-- .../mass_mailing/views/link_tracker_views.xml | 2 +- ...tats_views.xml => mailing_trace_views.xml} | 28 +++++++------- .../wizard/mail_compose_message.py | 10 ++--- addons/test_mass_mailing/tests/test_link.py | 4 +- .../tests/test_mail_auto_blacklist.py | 3 +- 18 files changed, 134 insertions(+), 137 deletions(-) rename addons/mass_mailing/models/{mass_mailing_stats.py => mailing_trace.py} (81%) rename addons/mass_mailing/report/{mass_mailing_stats_report.py => mailing_trace_report.py} (84%) rename addons/mass_mailing/report/{mass_mailing_stats_report_views.xml => mailing_trace_report_views.xml} (74%) rename addons/mass_mailing/views/{mass_mailing_stats_views.xml => mailing_trace_views.xml} (87%) diff --git a/addons/mass_mailing/__manifest__.py b/addons/mass_mailing/__manifest__.py index fd1e0c8b631..10c3939ea80 100644 --- a/addons/mass_mailing/__manifest__.py +++ b/addons/mass_mailing/__manifest__.py @@ -28,13 +28,13 @@ 'wizard/test_mailing_views.xml', 'wizard/mass_mailing_schedule_date_views.xml', 'views/mass_mailing_views_menus.xml', - 'views/mass_mailing_stats_views.xml', + 'views/mailing_trace_views.xml', 'views/link_tracker_views.xml', 'views/mass_mailing_contact_views.xml', 'views/mass_mailing_list_views.xml', 'views/mass_mailing_views.xml', 'views/res_config_settings_views.xml', - 'report/mass_mailing_stats_report_views.xml', + 'report/mailing_trace_report_views.xml', 'views/assets.xml', 'views/mass_mailing_templates_portal.xml', 'views/themes_templates.xml', diff --git a/addons/mass_mailing/controllers/main.py b/addons/mass_mailing/controllers/main.py index 0d3c563bc6a..4470bb75dfa 100644 --- a/addons/mass_mailing/controllers/main.py +++ b/addons/mass_mailing/controllers/main.py @@ -94,15 +94,15 @@ class MassMailController(http.Controller): @http.route('/mail/track//blank.gif', type='http', auth='public') def track_mail_open(self, mail_id, **post): """ Email tracking. """ - request.env['mail.mail.statistics'].sudo().set_opened(mail_mail_ids=[mail_id]) + request.env['mailing.trace'].sudo().set_opened(mail_mail_ids=[mail_id]) response = werkzeug.wrappers.Response() response.mimetype = 'image/gif' response.data = base64.b64decode(b'R0lGODlhAQABAIAAANvf7wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==') return response - @http.route('/r//m/', type='http', auth="public") - def full_url_redirect(self, code, stat_id, **post): + @http.route('/r//m/', type='http', auth="public") + def full_url_redirect(self, code, mailing_trace_id, **post): # don't assume geoip is set, it is part of the website module # which mass_mailing doesn't depend on country_code = request.session.get('geoip', False) and request.session.geoip.get('country_code', False) @@ -111,7 +111,7 @@ class MassMailController(http.Controller): code, ip=request.httprequest.remote_addr, country_code=country_code, - mail_stat_id=stat_id + mailing_trace_id=mailing_trace_id ) return werkzeug.utils.redirect(request.env['link.tracker'].get_url_from_code(code), 301) diff --git a/addons/mass_mailing/data/mass_mailing_demo.xml b/addons/mass_mailing/data/mass_mailing_demo.xml index b2054c5636d..172d90873bd 100644 --- a/addons/mass_mailing/data/mass_mailing_demo.xml +++ b/addons/mass_mailing/data/mass_mailing_demo.xml @@ -198,7 +198,7 @@ - + 1111000@odoo.com res.partner @@ -207,7 +207,7 @@ - + 1111001@odoo.com res.partner @@ -216,7 +216,7 @@ - + 1111002@odoo.com res.partner @@ -224,7 +224,7 @@ - + 1111003@odoo.com res.partner @@ -232,7 +232,7 @@ - + 1111004@odoo.com res.partner @@ -240,7 +240,7 @@ - + 1111004@odoo.com res.partner @@ -248,7 +248,7 @@ - + 1111004@odoo.com res.partner @@ -264,7 +264,7 @@ use="code"/> 100.01.02.03 BE - + 100.01.02.03 BE - + 100.01.02.04 BE - + 100.01.02.04 BE - + 100.01.02.05 BE - + diff --git a/addons/mass_mailing/models/__init__.py b/addons/mass_mailing/models/__init__.py index afeb0313925..d00a7590cf8 100644 --- a/addons/mass_mailing/models/__init__.py +++ b/addons/mass_mailing/models/__init__.py @@ -5,7 +5,7 @@ from . import link_tracker from . import mass_mailing_contact from . import mass_mailing_list from . import mass_mailing -from . import mass_mailing_stats +from . import mailing_trace from . import mail_mail from . import mail_template from . import mail_thread diff --git a/addons/mass_mailing/models/link_tracker.py b/addons/mass_mailing/models/link_tracker.py index 2b20b007a4b..c33dae7731e 100644 --- a/addons/mass_mailing/models/link_tracker.py +++ b/addons/mass_mailing/models/link_tracker.py @@ -14,22 +14,22 @@ class LinkTracker(models.Model): class LinkTrackerClick(models.Model): _inherit = "link.tracker.click" - mail_stat_id = fields.Many2one('mail.mail.statistics', string='Mail Statistics') + mailing_trace_id = fields.Many2one('mailing.trace', string='Mail Statistics') mass_mailing_id = fields.Many2one('mail.mass_mailing', string='Mass Mailing') mass_mailing_campaign_id = fields.Many2one('mail.mass_mailing.campaign', string='Mass Mailing Campaign') def _prepare_click_values_from_route(self, **route_values): click_values = super(LinkTrackerClick, self)._prepare_click_values_from_route(**route_values) - if click_values.get('mail_stat_id'): - stat_sudo = self.env['mail.mail.statistics'].sudo().browse(route_values['mail_stat_id']).exists() - if not stat_sudo: - click_values['mail_stat_id'] = False + if click_values.get('mailing_trace_id'): + trace_sudo = self.env['mailing.trace'].sudo().browse(route_values['mailing_trace_id']).exists() + if not trace_sudo: + click_values['mailing_trace_id'] = False else: if not click_values.get('mass_mailing_campaign_id'): - click_values['mass_mailing_campaign_id'] = stat_sudo.mass_mailing_campaign_id.id + click_values['mass_mailing_campaign_id'] = trace_sudo.mass_mailing_campaign_id.id if not click_values.get('mass_mailing_id'): - click_values['mass_mailing_id'] = stat_sudo.mass_mailing_id.id + click_values['mass_mailing_id'] = trace_sudo.mass_mailing_id.id return click_values @@ -37,8 +37,8 @@ class LinkTrackerClick(models.Model): def add_click(self, code, **route_values): click = super(LinkTrackerClick, self).add_click(code, **route_values) - if click and click.mail_stat_id: - click.mail_stat_id.set_opened() - click.mail_stat_id.set_clicked() + if click and click.mailing_trace_id: + click.mailing_trace_id.set_opened() + click.mailing_trace_id.set_clicked() return click diff --git a/addons/mass_mailing/models/mail_mail.py b/addons/mass_mailing/models/mail_mail.py index a4de6be6972..1dcb643b1e7 100644 --- a/addons/mass_mailing/models/mail_mail.py +++ b/addons/mass_mailing/models/mail_mail.py @@ -14,16 +14,16 @@ class MailMail(models.Model): _inherit = ['mail.mail'] mailing_id = fields.Many2one('mail.mass_mailing', string='Mass Mailing') - statistics_ids = fields.One2many('mail.mail.statistics', 'mail_mail_id', string='Statistics') + mailing_trace_ids = fields.One2many('mailing.trace', 'mail_mail_id', string='Statistics') @api.model def create(self, values): - """ Override mail_mail creation to create an entry in mail.mail.statistics """ + """ Override mail_mail creation to create an entry in mailing.trace """ # TDE note: should be after 'all values computed', to have values (FIXME after merging other branch holding create refactoring) mail = super(MailMail, self).create(values) - if values.get('statistics_ids'): + if values.get('mailing_trace_ids'): mail_sudo = mail.sudo() - mail_sudo.statistics_ids.write({'message_id': mail_sudo.message_id, 'state': 'outgoing'}) + mail_sudo.mailing_trace_ids.write({'message_id': mail_sudo.message_id, 'state': 'outgoing'}) return mail def _get_tracking_url(self): @@ -54,12 +54,12 @@ class MailMail(models.Model): def _send_prepare_body(self): """ Override to add the tracking URL to the body and to add - Statistic_id in shorted urls """ + trace ID in shortened urls """ # TDE: temporary addition (mail was parameter) due to semi-new-API self.ensure_one() body = super(MailMail, self)._send_prepare_body() - if self.mailing_id and body and self.statistics_ids: + if self.mailing_id and body and self.mailing_trace_ids: for match in re.findall(URL_REGEX, self.body_html): href = match[0] url = match[1] @@ -67,7 +67,7 @@ class MailMail(models.Model): parsed = werkzeug.urls.url_parse(url, scheme='http') if parsed.scheme.startswith('http') and parsed.path.startswith('/r/'): - new_href = href.replace(url, url + '/m/' + str(self.statistics_ids[0].id)) + new_href = href.replace(url, url + '/m/' + str(self.mailing_trace_ids[0].id)) body = body.replace(href, new_href) # generate tracking URL @@ -96,8 +96,8 @@ class MailMail(models.Model): mail_sent = not failure_type # we consider that a recipient error is a failure with mass mailling and show them as failed for mail in self: if mail.mailing_id: - if mail_sent is True and mail.statistics_ids: - mail.statistics_ids.write({'sent': fields.Datetime.now(), 'exception': False}) - elif mail_sent is False and mail.statistics_ids: - mail.statistics_ids.write({'exception': fields.Datetime.now()}) - return super(MailMail, self)._postprocess_sent_message(success_pids, failure_reason=failure_reason, failure_type=failure_type) + if mail_sent is True and mail.mailing_trace_ids: + mail.mailing_trace_ids.write({'sent': fields.Datetime.now(), 'exception': False}) + elif mail_sent is False and mail.mailing_trace_ids: + mail.mailing_trace_ids.write({'exception': fields.Datetime.now()}) + return super(MailMail, self)._postprocess_sent_message(success_pids, failure_reason=failure_reason, failure_type=failure_type) diff --git a/addons/mass_mailing/models/mail_thread.py b/addons/mass_mailing/models/mail_thread.py index 5195672eead..28b02921801 100644 --- a/addons/mass_mailing/models/mail_thread.py +++ b/addons/mass_mailing/models/mail_thread.py @@ -13,12 +13,12 @@ BLACKLIST_MAX_BOUNCED_LIMIT = 5 class MailThread(models.AbstractModel): - """ Update MailThread to add the support of bounce management in mass mailing statistics. """ + """ Update MailThread to add the support of bounce management in mass mailing traces. """ _inherit = 'mail.thread' @api.model def message_route(self, message, message_dict, model=None, thread_id=None, custom_values=None): - """ Override to udpate mass mailing statistics based on bounce emails """ + """ Override to udpate mass mailing traces based on bounce emails """ bounce_alias = self.env['ir.config_parameter'].sudo().get_param("mail.bounce.alias") email_to = decode_message_header(message, 'To') email_to_localpart = (tools.email_split(email_to) or [''])[0].split('@', 1)[0].lower() @@ -28,19 +28,19 @@ class MailThread(models.AbstractModel): bounce_match = bounce_re.search(email_to) if bounce_match: bounced_mail_id = bounce_match.group(1) - self.env['mail.mail.statistics'].set_bounced(mail_mail_ids=[bounced_mail_id]) + self.env['mailing.trace'].set_bounced(mail_mail_ids=[bounced_mail_id]) return super(MailThread, self).message_route(message, message_dict, model, thread_id, custom_values) @api.model def message_route_process(self, message, message_dict, routes): - """ Override to update the parent mail statistics. The parent is found + """ Override to update the parent mailing traces. The parent is found by using the References header of the incoming message and looking for - matching message_id in mail.mail.statistics. """ + matching message_id in mailing.trace. """ if message.get('References') and routes: message_ids = [x.strip() for x in decode_smtp_header(message['References']).split()] - self.env['mail.mail.statistics'].set_opened(mail_message_ids=message_ids) - self.env['mail.mail.statistics'].set_replied(mail_message_ids=message_ids) + self.env['mailing.trace'].set_opened(mail_message_ids=message_ids) + self.env['mailing.trace'].set_replied(mail_message_ids=message_ids) return super(MailThread, self).message_route_process(message, message_dict, routes) def message_post_with_template(self, template_id, **kwargs): @@ -64,11 +64,11 @@ class MailThread(models.AbstractModel): super(MailThread, self)._message_receive_bounce(email, partner, mail_id=None) three_months_ago = fields.Datetime.to_string(datetime.datetime.now() - datetime.timedelta(weeks=13)) - stats = self.env['mail.mail.statistics'] \ + traces = self.env['mailing.trace'] \ .search(['&', ('bounced', '>', three_months_ago), ('email', '=ilike', email)]) \ .mapped('bounced') - if len(stats) >= BLACKLIST_MAX_BOUNCED_LIMIT: - if max(stats) > min(stats) + datetime.timedelta(weeks=1): + if len(traces) >= BLACKLIST_MAX_BOUNCED_LIMIT: + if max(traces) > min(traces) + datetime.timedelta(weeks=1): blacklist_rec = self.env['mail.blacklist'].sudo()._add(email) blacklist_rec._message_log( body='This email has been automatically blacklisted because of too much bounced.') diff --git a/addons/mass_mailing/models/mass_mailing_stats.py b/addons/mass_mailing/models/mailing_trace.py similarity index 81% rename from addons/mass_mailing/models/mass_mailing_stats.py rename to addons/mass_mailing/models/mailing_trace.py index 0710a0e8b1a..9f6556ae3ef 100644 --- a/addons/mass_mailing/models/mass_mailing_stats.py +++ b/addons/mass_mailing/models/mailing_trace.py @@ -4,14 +4,13 @@ from odoo import api, fields, models -class MailMailStats(models.Model): - """ MailMailStats models the statistics collected about emails. Those statistics +class MailingTrace(models.Model): + """ MailingTrace 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' + _name = 'mailing.trace' + _description = 'Mailing Statistics' _rec_name = 'message_id' _order = 'message_id' @@ -45,7 +44,7 @@ class MailMailStats(models.Model): replied = fields.Datetime(help='Date when this email has been replied for the first time.') bounced = fields.Datetime(help='Date when this email has bounced.') # Link tracking - links_click_ids = fields.One2many('link.tracker.click', 'mail_stat_id', string='Links click') + links_click_ids = fields.One2many('link.tracker.click', 'mailing_trace_id', string='Links click') clicked = fields.Datetime(help='Date when customer clicked on at least one tracked link') # Status state = fields.Selection(compute="_compute_state", @@ -83,7 +82,7 @@ class MailMailStats(models.Model): def create(self, values): if 'mail_mail_id' in values: values['mail_mail_id_int'] = values['mail_mail_id'] - res = super(MailMailStats, self).create(values) + res = super(MailingTrace, self).create(values) return res def _get_records(self, mail_mail_ids=None, mail_message_ids=None, domain=None): @@ -98,22 +97,21 @@ class MailMailStats(models.Model): return self.search(base_domain) def set_opened(self, mail_mail_ids=None, mail_message_ids=None): - statistics = self._get_records(mail_mail_ids, mail_message_ids, [('opened', '=', False)]) - statistics.write({'opened': fields.Datetime.now(), 'bounced': False}) - return statistics + traces = self._get_records(mail_mail_ids, mail_message_ids, [('opened', '=', False)]) + traces.write({'opened': fields.Datetime.now(), 'bounced': False}) + return traces def set_clicked(self, mail_mail_ids=None, mail_message_ids=None): - statistics = self._get_records(mail_mail_ids, mail_message_ids, [('clicked', '=', False)]) - statistics.write({'clicked': fields.Datetime.now()}) - return statistics + traces = self._get_records(mail_mail_ids, mail_message_ids, [('clicked', '=', False)]) + traces.write({'clicked': fields.Datetime.now()}) + return traces def set_replied(self, mail_mail_ids=None, mail_message_ids=None): - statistics = self._get_records(mail_mail_ids, mail_message_ids, [('replied', '=', False)]) - statistics.write({'replied': fields.Datetime.now()}) - return statistics + traces = self._get_records(mail_mail_ids, mail_message_ids, [('replied', '=', False)]) + traces.write({'replied': fields.Datetime.now()}) + return traces def set_bounced(self, mail_mail_ids=None, mail_message_ids=None): - statistics = self._get_records( - mail_mail_ids, mail_message_ids, [('bounced', '=', False), ('opened', '=', False)]) - statistics.write({'bounced': fields.Datetime.now()}) - return statistics + traces = self._get_records(mail_mail_ids, mail_message_ids, [('bounced', '=', False), ('opened', '=', False)]) + traces.write({'bounced': fields.Datetime.now()}) + return traces diff --git a/addons/mass_mailing/models/mass_mailing.py b/addons/mass_mailing/models/mass_mailing.py index 3cc86fbebd4..d0249777baf 100644 --- a/addons/mass_mailing/models/mass_mailing.py +++ b/addons/mass_mailing/models/mass_mailing.py @@ -89,7 +89,7 @@ class MassMailingCampaign(models.Model): 'the effectiveness of the mailings, without causing duplicate messages.') color = fields.Integer(string='Color Index') clicks_ratio = fields.Integer(compute="_compute_clicks_ratio", string="Number of clicks") - # stat fields + # trace statistics fields total = fields.Integer(compute="_compute_statistics") scheduled = fields.Integer(compute="_compute_statistics") failed = fields.Integer(compute="_compute_statistics") @@ -107,9 +107,9 @@ class MassMailingCampaign(models.Model): def _compute_clicks_ratio(self): self.env.cr.execute(""" - SELECT COUNT(DISTINCT(stats.id)) AS nb_mails, COUNT(DISTINCT(clicks.mail_stat_id)) AS nb_clicks, stats.mass_mailing_campaign_id AS id - FROM mail_mail_statistics AS stats - LEFT OUTER JOIN link_tracker_click AS clicks ON clicks.mail_stat_id = stats.id + SELECT COUNT(DISTINCT(stats.id)) AS nb_mails, COUNT(DISTINCT(clicks.mailing_trace_id)) AS nb_clicks, stats.mass_mailing_campaign_id AS id + FROM mailing_trace AS stats + LEFT OUTER JOIN link_tracker_click AS clicks ON clicks.mailing_trace_id = stats.id WHERE stats.mass_mailing_campaign_id IN %s GROUP BY stats.mass_mailing_campaign_id """, (tuple(self.ids), )) @@ -134,7 +134,7 @@ class MassMailingCampaign(models.Model): COUNT(CASE WHEN s.replied is not null THEN 1 ELSE null END) AS replied , COUNT(CASE WHEN s.bounced is not null THEN 1 ELSE null END) AS bounced FROM - mail_mail_statistics s + mailing_trace s RIGHT JOIN mail_mass_mailing_campaign c ON (c.id = s.mass_mailing_campaign_id) @@ -169,7 +169,7 @@ class MassMailingCampaign(models.Model): domain = [('mass_mailing_campaign_id', '=', campaign.id)] if model: domain += [('model', '=', model)] - res[campaign.id] = set(self.env['mail.mail.statistics'].search(domain).mapped('res_id')) + res[campaign.id] = set(self.env['mailing.trace'].search(domain).mapped('res_id')) return res @api.model @@ -253,7 +253,7 @@ class MassMailing(models.Model): contact_ab_pc = fields.Integer(string='A/B Testing percentage', help='Percentage of the contacts that will be mailed. Recipients will be taken randomly.', default=100) # statistics data - statistics_ids = fields.One2many('mail.mail.statistics', 'mass_mailing_id', string='Emails Statistics') + mailing_trace_ids = fields.One2many('mailing.trace', 'mass_mailing_id', string='Emails Statistics') total = fields.Integer(compute="_compute_total") scheduled = fields.Integer(compute="_compute_statistics") expected = fields.Integer(compute="_compute_statistics") @@ -277,9 +277,9 @@ class MassMailing(models.Model): def _compute_clicks_ratio(self): self.env.cr.execute(""" - SELECT COUNT(DISTINCT(stats.id)) AS nb_mails, COUNT(DISTINCT(clicks.mail_stat_id)) AS nb_clicks, stats.mass_mailing_id AS id - FROM mail_mail_statistics AS stats - LEFT OUTER JOIN link_tracker_click AS clicks ON clicks.mail_stat_id = stats.id + SELECT COUNT(DISTINCT(stats.id)) AS nb_mails, COUNT(DISTINCT(clicks.mailing_trace_id)) AS nb_clicks, stats.mass_mailing_id AS id + FROM mailing_trace AS stats + LEFT OUTER JOIN link_tracker_click AS clicks ON clicks.mailing_trace_id = stats.id WHERE stats.mass_mailing_id IN %s GROUP BY stats.mass_mailing_id """, (tuple(self.ids), )) @@ -311,7 +311,7 @@ class MassMailing(models.Model): COUNT(CASE WHEN s.bounced is not null THEN 1 ELSE null END) AS bounced, COUNT(CASE WHEN s.exception is not null THEN 1 ELSE null END) AS failed FROM - mail_mail_statistics s + mailing_trace s RIGHT JOIN mail_mass_mailing m ON (m.id = s.mass_mailing_id) @@ -441,10 +441,10 @@ class MassMailing(models.Model): def retry_failed_mail(self): failed_mails = self.env['mail.mail'].search([('mailing_id', 'in', self.ids), ('state', '=', 'exception')]) - failed_mails.mapped('statistics_ids').unlink() + failed_mails.mapped('mailing_trace_ids').unlink() failed_mails.sudo().unlink() res_ids = self._get_recipients() - except_mailed = self.env['mail.mail.statistics'].search([ + except_mailed = self.env['mailing.trace'].search([ ('model', '=', self.mailing_model_real), ('res_id', 'in', res_ids), ('exception', '!=', False), @@ -471,11 +471,11 @@ class MassMailing(models.Model): def _action_view_documents_filtered(self, view_filter): if view_filter in ('sent', 'opened', 'replied', 'bounced', 'clicked'): - opened_stats = self.statistics_ids.filtered(lambda stat: stat[view_filter]) + opened_stats = self.mailing_trace_ids.filtered(lambda stat: stat[view_filter]) elif view_filter == ('delivered'): - opened_stats = self.statistics_ids.filtered(lambda stat: stat.sent and not stat.bounced) + opened_stats = self.mailing_trace_ids.filtered(lambda stat: stat.sent and not stat.bounced) else: - opened_stats = self.env['mail.mail.statistics'] + opened_stats = self.env['mailing.trace'] res_ids = opened_stats.mapped('res_id') model_name = self.env['ir.model']._get(self.mailing_model_real).display_name return { @@ -551,7 +551,7 @@ class MassMailing(models.Model): # + use a basic heuristic for extracting emails query = """ SELECT lower(substring(t.%(mail_field)s, '([^ ,;<@]+@[^> ,;]+)')) - FROM mail_mail_statistics s + FROM mailing_trace s JOIN %(target)s t ON (s.res_id = t.id) WHERE substring(t.%(mail_field)s, '([^ ,;<@]+@[^> ,;]+)') IS NOT NULL """ @@ -561,7 +561,7 @@ class MassMailing(models.Model): mail_field = 'email' query = """ SELECT lower(substring(p.%(mail_field)s, '([^ ,;<@]+@[^> ,;]+)')) - FROM mail_mail_statistics s + FROM mailing_trace s JOIN %(target)s t ON (s.res_id = t.id) JOIN res_partner p ON (t.partner_id = p.id) WHERE substring(p.%(mail_field)s, '([^ ,;<@]+@[^> ,;]+)') IS NOT NULL @@ -626,7 +626,7 @@ class MassMailing(models.Model): def _get_remaining_recipients(self): res_ids = self._get_recipients() - already_mailed = self.env['mail.mail.statistics'].search_read([ + already_mailed = self.env['mailing.trace'].search_read([ ('model', '=', self.mailing_model_real), ('res_id', 'in', res_ids), ('mass_mailing_id', '=', self.id)], ['res_id']) diff --git a/addons/mass_mailing/report/__init__.py b/addons/mass_mailing/report/__init__.py index 2796202ba19..cd346c5e5ca 100644 --- a/addons/mass_mailing/report/__init__.py +++ b/addons/mass_mailing/report/__init__.py @@ -1,4 +1,4 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from . import mass_mailing_stats_report +from . import mailing_trace_report diff --git a/addons/mass_mailing/report/mass_mailing_stats_report.py b/addons/mass_mailing/report/mailing_trace_report.py similarity index 84% rename from addons/mass_mailing/report/mass_mailing_stats_report.py rename to addons/mass_mailing/report/mailing_trace_report.py index 358e90d0dfa..2afa8906097 100644 --- a/addons/mass_mailing/report/mass_mailing_stats_report.py +++ b/addons/mass_mailing/report/mailing_trace_report.py @@ -1,11 +1,11 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from odoo import api, fields, models, tools +from odoo import fields, models, tools -class MassMailingReport(models.Model): - _name = 'mail.statistics.report' +class MailingTraceReport(models.Model): + _name = 'mailing.trace.report' _auto = False _description = 'Mass Mailing Statistics' @@ -23,12 +23,12 @@ class MassMailingReport(models.Model): email_from = fields.Char('From', readonly=True) def init(self): - """Mass Mail Statistical Report: based on mail.mail.statistics that models the various + """Mass Mail Statistical Report: based on mailing.trace that models the various statistics collected for each mailing, and mail.mass_mailing model that models the various mailing performed. """ - tools.drop_view_if_exists(self.env.cr, 'mail_statistics_report') + tools.drop_view_if_exists(self.env.cr, 'mailing_trace_report') self.env.cr.execute(""" - CREATE OR REPLACE VIEW mail_statistics_report AS ( + CREATE OR REPLACE VIEW mailing_trace_report AS ( SELECT min(ms.id) as id, ms.scheduled as scheduled_date, @@ -43,7 +43,7 @@ class MassMailingReport(models.Model): mm.state, mm.email_from FROM - mail_mail_statistics as ms + mailing_trace as ms left join mail_mass_mailing as mm ON (ms.mass_mailing_id=mm.id) left join mail_mass_mailing_campaign as mc ON (ms.mass_mailing_campaign_id=mc.id) left join utm_campaign as utm_campaign ON (mc.campaign_id = utm_campaign.id) diff --git a/addons/mass_mailing/report/mass_mailing_stats_report_views.xml b/addons/mass_mailing/report/mailing_trace_report_views.xml similarity index 74% rename from addons/mass_mailing/report/mass_mailing_stats_report_views.xml rename to addons/mass_mailing/report/mailing_trace_report_views.xml index 6924fdc0433..0840fcb2254 100644 --- a/addons/mass_mailing/report/mass_mailing_stats_report_views.xml +++ b/addons/mass_mailing/report/mailing_trace_report_views.xml @@ -1,8 +1,8 @@ - - mail.statistics.report.pivot - mail.statistics.report + + mailing.trace.report.view.pivot + mailing.trace.report @@ -16,9 +16,9 @@ - - mail.statistics.report.graph - mail.statistics.report + + mailing.trace.report.view.graph + mailing.trace.report @@ -27,9 +27,9 @@ - - mail.statistics.report.search - mail.statistics.report + + mailing.trace.report.view.search + mailing.trace.report @@ -51,14 +51,14 @@ - + Mass Mailing Analysis - mail.statistics.report + mailing.trace.report graph,pivot Mass Mailing Statistics allows you to check different mailing related information like number of bounced mails, opened mails, replied mails. You can sort out your analysis by different groups to get accurate grained analysis. + parent="mass_mailing_menu_root" action="mailing_trace_report_action" groups="mass_mailing.group_mass_mailing_user"/> diff --git a/addons/mass_mailing/security/ir.model.access.csv b/addons/mass_mailing/security/ir.model.access.csv index 7e3887ea576..ed137e9ba92 100644 --- a/addons/mass_mailing/security/ir.model.access.csv +++ b/addons/mass_mailing/security/ir.model.access.csv @@ -7,9 +7,9 @@ access_mass_mailing_campaign,mail.mass_mailing.campaign,model_mail_mass_mailing_ 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,mass_mailing.group_mass_mailing_user,1,1,1,1 access_mass_mailing_system,mail.mass_mailing.system,model_mail_mass_mailing,base.group_system,1,1,1,1 -access_mail_mail_statistics_user,mail.mail.statistics,model_mail_mail_statistics,base.group_user,1,1,1,1 -access_mail_mail_statistics_mass_mailing_user,mail.mail.statistics,model_mail_mail_statistics,mass_mailing.group_mass_mailing_user,1,1,1,1 -access_mail_statistics_report,mail.statistics.report,model_mail_statistics_report,mass_mailing.group_mass_mailing_user,1,1,1,1 +access_mailing_trace_user,mailing.trace.user,model_mailing_trace,base.group_user,1,1,1,1 +access_mailing_trace_mm_user,access.mailing.trace.mm.user,model_mailing_trace,mass_mailing.group_mass_mailing_user,1,1,1,1 +access_mailing_trace_report_mm_user,access.mailing.trace.report.mm.user,model_mailing_trace_report,mass_mailing.group_mass_mailing_user,1,1,1,1 access_utm_source,access_utm_source,utm.model_utm_source,mass_mailing.group_mass_mailing_user,1,1,1,0 access_ir_mail_server,access_ir_mail_server,base.model_ir_mail_server,mass_mailing.group_mass_mailing_user,1,0,0,0 access_mail_blacklist_mass_mailing_user,access.mail.blacklist.mass_mailing_user,mail.model_mail_blacklist,mass_mailing.group_mass_mailing_user,1,1,1,1 diff --git a/addons/mass_mailing/views/link_tracker_views.xml b/addons/mass_mailing/views/link_tracker_views.xml index b1e4a96a582..450d4e7bd77 100644 --- a/addons/mass_mailing/views/link_tracker_views.xml +++ b/addons/mass_mailing/views/link_tracker_views.xml @@ -80,7 +80,7 @@ - + diff --git a/addons/mass_mailing/views/mass_mailing_stats_views.xml b/addons/mass_mailing/views/mailing_trace_views.xml similarity index 87% rename from addons/mass_mailing/views/mass_mailing_stats_views.xml rename to addons/mass_mailing/views/mailing_trace_views.xml index 57f291ef557..86907f2770f 100644 --- a/addons/mass_mailing/views/mass_mailing_stats_views.xml +++ b/addons/mass_mailing/views/mailing_trace_views.xml @@ -1,9 +1,9 @@ - - - mail.mail.statistics.search - mail.mail.statistics + + + mailing.trace.search + mailing.trace @@ -30,9 +30,9 @@ - - mail.mail.statistics.tree - mail.mail.statistics + + mailing.trace.tree + mailing.trace @@ -49,9 +49,9 @@ - - mail.mail.statistics.form - mail.mail.statistics + + mailing.trace.form + mailing.trace
@@ -88,13 +88,13 @@ Mail Statistics - mail.mail.statistics + mailing.trace tree,form Mail Statistics - mail.mail.statistics + mailing.trace graph,tree,form,pivot [] {'search_default_mass_mailing_id': active_id} @@ -102,7 +102,7 @@ Emails Sent - mail.mail.statistics + mailing.trace tree,graph,form,pivot [] {'search_default_mass_mailing_id': active_id} @@ -110,7 +110,7 @@ Mail Statistics Graph - mail.mail.statistics + mailing.trace diff --git a/addons/mass_mailing/wizard/mail_compose_message.py b/addons/mass_mailing/wizard/mail_compose_message.py index 3f5c0c03e2e..fc75d7e192f 100644 --- a/addons/mass_mailing/wizard/mail_compose_message.py +++ b/addons/mass_mailing/wizard/mail_compose_message.py @@ -16,7 +16,7 @@ class MailComposeMessage(models.TransientModel): def get_mail_values(self, res_ids): """ Override method that generated the mail content by creating the - mail.mail.statistics values in the o2m of mail_mail, when doing pure + mailing.trace values in the o2m of mail_mail, when doing pure email mass mailing. """ self.ensure_one() res = super(MailComposeMessage, self).get_mail_values(res_ids) @@ -71,7 +71,7 @@ class MailComposeMessage(models.TransientModel): mail_values['state'] = 'cancel' elif seen_list is not None: seen_list.add(mail_to) - stat_vals = { + trace_vals = { 'model': self.model, 'res_id': res_id, 'mass_mailing_id': mass_mailing.id, @@ -79,12 +79,12 @@ class MailComposeMessage(models.TransientModel): } if mail_values.get('body_html') and mass_mail_layout: mail_values['body_html'] = mass_mail_layout.render({'body': mail_values['body_html']}, engine='ir.qweb', minimal_qcontext=True) - # propagate ignored state to stat when still-born + # propagate ignored state to trace when still-born if mail_values.get('state') == 'cancel': - stat_vals['ignored'] = fields.Datetime.now() + trace_vals['ignored'] = fields.Datetime.now() mail_values.update({ 'mailing_id': mass_mailing.id, - 'statistics_ids': [(0, 0, stat_vals)], + 'mailing_trace_ids': [(0, 0, trace_vals)], # email-mode: keep original message for routing 'notification': mass_mailing.reply_to_mode == 'thread', 'auto_delete': not mass_mailing.keep_archives, diff --git a/addons/test_mass_mailing/tests/test_link.py b/addons/test_mass_mailing/tests/test_link.py index bbe731413bf..80c73647bf0 100644 --- a/addons/test_mass_mailing/tests/test_link.py +++ b/addons/test_mass_mailing/tests/test_link.py @@ -48,7 +48,7 @@ class TestLinkTracker(common.MassMailingCase): mailing = self.env['mail.mass_mailing'].create({'name': 'Test Mailing', "subject": "Hi!"}) code = self.link.code self.assertEqual(self.link.count, 1) - stat = self.env['mail.mail.statistics'].create({'mass_mailing_id': mailing.id}) + stat = self.env['mailing.trace'].create({'mass_mailing_id': mailing.id}) self.assertFalse(stat.opened) self.assertFalse(stat.clicked) @@ -57,7 +57,7 @@ class TestLinkTracker(common.MassMailingCase): code, ip='100.00.00.01', country_code='BEL', - mail_stat_id=stat.id + mailing_trace_id=stat.id ) self.assertEqual(self.link.count, 2) self.assertEqual(click.mass_mailing_id, mailing) diff --git a/addons/test_mass_mailing/tests/test_mail_auto_blacklist.py b/addons/test_mass_mailing/tests/test_mail_auto_blacklist.py index eee8c7ea964..56db2616fd9 100644 --- a/addons/test_mass_mailing/tests/test_mail_auto_blacklist.py +++ b/addons/test_mass_mailing/tests/test_mail_auto_blacklist.py @@ -9,7 +9,7 @@ class TestAutoBlacklist(common.TransactionCase): mass_mailing_contacts = self.env['mail.mass_mailing.contact'] mass_mailing = self.env['mail.mass_mailing'] mail_blacklist = self.env['mail.blacklist'] - mail_statistics = self.env['mail.mail.statistics'] + mail_statistics = self.env['mailing.trace'] mail_thread = self.env['mail.thread'] # create mailing contact record @@ -45,7 +45,6 @@ class TestAutoBlacklist(common.TransactionCase): }) self.mailing_contact_1._message_receive_bounce(self.mailing_contact_1.email, self.mailing_contact_1) - # create mass mailing record self.mass_mailing = mass_mailing.create({ 'name': 'test', From 772e1c0cc4e75cec971d2b4e1c820eb54872dc93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Wed, 17 Jul 2019 10:58:39 +0000 Subject: [PATCH 5/7] [REF] mass_mailing: rename mail.mass_mailing.{.contact{_rel}, list{.merge}} models PURPOSE Mass mailing is currently a bit messy. As SMS will be added as a way to notify people in mass through SMS let us take this opportunity to somehow clean this application: organization, light code cleaning, model renaming. SPECIFICATIONS Rename mail.mass_mailing.list and mail.mass_mailing.list to mailing.list and mailing.list.merge. Rename mail.mass_mailing.contact to mailing.contact. Rename mail.mailing_list.list_contact_rel to mailing.contact.subscription. Rationale : * those new names are easier to understand: mailing.list and mailing.contact are less mail-related, especially taking into account that SMS will allow to be less mail-oriented; * those names are easier to read / find / understand; * align wizard and sub-models naming with the main naming; * have a mailing as first part of namespacing; MIGRATION mail.mass_mailing.list model -> mailing.list mail.mass_mailing.list.merge model -> mailing.list.merge mail.mass_mailing.contact model -> mailing.contact mail.mass_mailing.list_contact_rel model -> mailing.contact.subscription mail_mass_mailing_contact_list_rel table -> mailing_contact_list_rel (specific case of a decorated m2m) fields updated (no column change) * mailing.list: subscription_contact_ids -> subscription_ids LINKS Task ID 2037906 Preparing task ID 1997464 (SMS addition in mass mailing) PR #34938 --- addons/mass_mailing/__manifest__.py | 6 +-- addons/mass_mailing/controllers/main.py | 8 +-- .../mass_mailing/data/mass_mailing_demo.xml | 14 ++--- addons/mass_mailing/models/__init__.py | 4 +- ..._mailing_contact.py => mailing_contact.py} | 19 ++++--- .../{mass_mailing_list.py => mailing_list.py} | 28 +++++----- addons/mass_mailing/models/mass_mailing.py | 24 ++++----- .../mass_mailing/security/ir.model.access.csv | 6 +-- .../static/src/js/mass_mailing.js | 2 +- .../tests/test_mass_mailing_list_merge.py | 8 +-- .../tests/test_mass_mailing_shortener.py | 10 ++-- ...ct_views.xml => mailing_contact_views.xml} | 51 +++++++++---------- ..._list_views.xml => mailing_list_views.xml} | 36 ++++++------- .../mass_mailing/views/mass_mailing_views.xml | 6 +-- addons/mass_mailing/wizard/__init__.py | 2 +- .../wizard/mail_compose_message.py | 5 +- ...ng_list_merge.py => mailing_list_merge.py} | 8 +-- ...views.xml => mailing_list_merge_views.xml} | 12 ++--- .../tests/test_mail_auto_blacklist.py | 12 ++--- .../tests/test_mass_mailing.py | 20 ++++---- addons/website_mass_mailing/__manifest__.py | 3 +- .../website_mass_mailing/controllers/main.py | 18 +++---- .../website_mass_mailing/models/__init__.py | 3 +- .../models/mailing_list.py | 14 +++++ ...ailing_list.py => website_mass_mailing.py} | 12 +---- .../src/js/website_mass_mailing.editor.js | 2 +- .../views/mailing_list_views.xml | 26 ++++++++++ ...iew.xml => website_mass_mailing_views.xml} | 24 --------- 28 files changed, 193 insertions(+), 190 deletions(-) rename addons/mass_mailing/models/{mass_mailing_contact.py => mailing_contact.py} (88%) rename addons/mass_mailing/models/{mass_mailing_list.py => mailing_list.py} (82%) rename addons/mass_mailing/views/{mass_mailing_contact_views.xml => mailing_contact_views.xml} (86%) rename addons/mass_mailing/views/{mass_mailing_list_views.xml => mailing_list_views.xml} (85%) rename addons/mass_mailing/wizard/{mass_mailing_list_merge.py => mailing_list_merge.py} (79%) rename addons/mass_mailing/wizard/{mass_mailing_list_merge_views.xml => mailing_list_merge_views.xml} (75%) create mode 100644 addons/website_mass_mailing/models/mailing_list.py rename addons/website_mass_mailing/models/{mass_mailing_list.py => website_mass_mailing.py} (56%) create mode 100644 addons/website_mass_mailing/views/mailing_list_views.xml rename addons/website_mass_mailing/views/{mass_mailing_view.xml => website_mass_mailing_views.xml} (50%) diff --git a/addons/mass_mailing/__manifest__.py b/addons/mass_mailing/__manifest__.py index 10c3939ea80..82c20b4afae 100644 --- a/addons/mass_mailing/__manifest__.py +++ b/addons/mass_mailing/__manifest__.py @@ -24,14 +24,14 @@ 'data/mail_data.xml', 'data/mass_mailing_data.xml', 'wizard/mail_compose_message_views.xml', - 'wizard/mass_mailing_list_merge_views.xml', + 'wizard/mailing_list_merge_views.xml', 'wizard/test_mailing_views.xml', 'wizard/mass_mailing_schedule_date_views.xml', 'views/mass_mailing_views_menus.xml', 'views/mailing_trace_views.xml', 'views/link_tracker_views.xml', - 'views/mass_mailing_contact_views.xml', - 'views/mass_mailing_list_views.xml', + 'views/mailing_contact_views.xml', + 'views/mailing_list_views.xml', 'views/mass_mailing_views.xml', 'views/res_config_settings_views.xml', 'report/mailing_trace_report_views.xml', diff --git a/addons/mass_mailing/controllers/main.py b/addons/mass_mailing/controllers/main.py index 4470bb75dfa..56e5259cdb3 100644 --- a/addons/mass_mailing/controllers/main.py +++ b/addons/mass_mailing/controllers/main.py @@ -35,11 +35,11 @@ class MassMailController(http.Controller): if not self._valid_unsubscribe_token(mailing_id, res_id, email, str(token)): raise exceptions.AccessDenied() - if mailing.mailing_model_real == 'mail.mass_mailing.contact': + if mailing.mailing_model_real == 'mailing.contact': # Unsubscribe directly + Let the user choose his subscriptions mailing.update_opt_out(email, mailing.contact_list_ids.ids, True) - contacts = request.env['mail.mass_mailing.contact'].sudo().search([('email_normalized', '=', tools.email_normalize(email))]) + contacts = request.env['mailing.contact'].sudo().search([('email_normalized', '=', tools.email_normalize(email))]) subscription_list_ids = contacts.mapped('subscription_list_ids') # In many user are found : if user is opt_out on the list with contact_id 1 but not with contact_id 2, # assume that the user is not opt_out on both @@ -49,7 +49,7 @@ class MassMailController(http.Controller): opt_out_list_ids = set([list.id for list in opt_out_list_ids if list not in opt_in_list_ids]) unique_list_ids = set([list.list_id.id for list in subscription_list_ids]) - list_ids = request.env['mail.mass_mailing.list'].sudo().browse(unique_list_ids) + list_ids = request.env['mailing.list'].sudo().browse(unique_list_ids) unsubscribed_list = ', '.join(str(list.name) for list in mailing.contact_list_ids if list.is_public) return request.render('mass_mailing.page_unsubscribe', { 'contacts': contacts, @@ -62,7 +62,7 @@ class MassMailController(http.Controller): 'show_blacklist_button': request.env['ir.config_parameter'].sudo().get_param('mass_mailing.show_blacklist_buttons'), }) else: - opt_in_lists = request.env['mail.mass_mailing.list_contact_rel'].sudo().search([ + opt_in_lists = request.env['mailing.contact.subscription'].sudo().search([ ('contact_id.email_normalized', '=', email), ('opt_out', '=', False) ]).mapped('list_id') diff --git a/addons/mass_mailing/data/mass_mailing_demo.xml b/addons/mass_mailing/data/mass_mailing_demo.xml index 172d90873bd..825340f1618 100644 --- a/addons/mass_mailing/data/mass_mailing_demo.xml +++ b/addons/mass_mailing/data/mass_mailing_demo.xml @@ -8,38 +8,38 @@ - + Imported Contacts - + Aristide Antario aa@example.com - + Beverly Bridge bb@example.com - + Carol Cartridge cc@example.com - + David Dawson dd@example.com - + Elsa Ericson ee@example.com - + True diff --git a/addons/mass_mailing/models/__init__.py b/addons/mass_mailing/models/__init__.py index d00a7590cf8..3caef695204 100644 --- a/addons/mass_mailing/models/__init__.py +++ b/addons/mass_mailing/models/__init__.py @@ -2,8 +2,8 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. from . import link_tracker -from . import mass_mailing_contact -from . import mass_mailing_list +from . import mailing_contact +from . import mailing_list from . import mass_mailing from . import mailing_trace from . import mail_mail diff --git a/addons/mass_mailing/models/mass_mailing_contact.py b/addons/mass_mailing/models/mailing_contact.py similarity index 88% rename from addons/mass_mailing/models/mass_mailing_contact.py rename to addons/mass_mailing/models/mailing_contact.py index 817c30830ac..966a5f29aaa 100644 --- a/addons/mass_mailing/models/mass_mailing_contact.py +++ b/addons/mass_mailing/models/mailing_contact.py @@ -9,13 +9,13 @@ class MassMailingContactListRel(models.Model): """ Intermediate model between mass mailing list and mass mailing contact Indicates if a contact is opted out for a particular list """ - _name = 'mail.mass_mailing.list_contact_rel' + _name = 'mailing.contact.subscription' _description = 'Mass Mailing Subscription Information' - _table = 'mail_mass_mailing_contact_list_rel' + _table = 'mailing_contact_list_rel' _rec_name = 'contact_id' - contact_id = fields.Many2one('mail.mass_mailing.contact', string='Contact', ondelete='cascade', required=True) - list_id = fields.Many2one('mail.mass_mailing.list', string='Mailing List', ondelete='cascade', required=True) + contact_id = fields.Many2one('mailing.contact', string='Contact', ondelete='cascade', required=True) + list_id = fields.Many2one('mailing.list', string='Mailing List', ondelete='cascade', required=True) opt_out = fields.Boolean(string='Opt Out', help='The contact has chosen not to receive mails anymore from this list', default=False) unsubscription_date = fields.Datetime(string='Unsubscription Date') @@ -45,7 +45,7 @@ class MassMailingContactListRel(models.Model): action = { 'name': _(contact_id.name), 'type': 'ir.actions.act_window', - 'res_model': 'mail.mass_mailing.contact', + 'res_model': 'mailing.contact', 'view_mode': 'form', 'target': 'current', 'res_id': contact_id.id @@ -58,7 +58,7 @@ class MassMailingContact(models.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' + _name = 'mailing.contact' _inherit = ['mail.thread.blacklist'] _description = 'Mass Mailing Contact' _order = 'email' @@ -70,10 +70,9 @@ class MassMailingContact(models.Model): email = fields.Char(required=True) is_email_valid = fields.Boolean(compute='_compute_is_email_valid', store=True) list_ids = fields.Many2many( - 'mail.mass_mailing.list', 'mail_mass_mailing_contact_list_rel', + 'mailing.list', 'mailing_contact_list_rel', 'contact_id', 'list_id', string='Mailing Lists') - subscription_list_ids = fields.One2many('mail.mass_mailing.list_contact_rel', - 'contact_id', string='Subscription Information') + subscription_list_ids = fields.One2many('mailing.contact.subscription', 'contact_id', string='Subscription Information') country_id = fields.Many2one('res.country', string='Country') tag_ids = fields.Many2many('res.partner.category', string='Tags') opt_out = fields.Boolean('Opt Out', compute='_compute_opt_out', search='_search_opt_out', @@ -97,7 +96,7 @@ class MassMailingContact(models.Model): if 'default_list_ids' in self._context and isinstance(self._context['default_list_ids'], (list, tuple)) and len(self._context['default_list_ids']) == 1: [active_list_id] = self._context['default_list_ids'] - contacts = self.env['mail.mass_mailing.list_contact_rel'].search([('list_id', '=', active_list_id)]) + contacts = self.env['mailing.contact.subscription'].search([('list_id', '=', active_list_id)]) return [('id', 'in', [record.contact_id.id for record in contacts if record.opt_out == value])] else: raise UserError('Search opt out cannot be executed without a unique and valid active mailing list context.') diff --git a/addons/mass_mailing/models/mass_mailing_list.py b/addons/mass_mailing/models/mailing_list.py similarity index 82% rename from addons/mass_mailing/models/mass_mailing_list.py rename to addons/mass_mailing/models/mailing_list.py index 48cde5784c3..ad78b7ad85f 100644 --- a/addons/mass_mailing/models/mass_mailing_list.py +++ b/addons/mass_mailing/models/mailing_list.py @@ -6,7 +6,7 @@ from odoo import api, fields, models class MassMailingList(models.Model): """Model of a contact list. """ - _name = 'mail.mass_mailing.list' + _name = 'mailing.list' _order = 'name' _description = 'Mailing List' @@ -14,9 +14,9 @@ class MassMailingList(models.Model): active = fields.Boolean(default=True) contact_nbr = fields.Integer(compute="_compute_contact_nbr", string='Number of Contacts') contact_ids = fields.Many2many( - 'mail.mass_mailing.contact', 'mail_mass_mailing_contact_list_rel', 'list_id', 'contact_id', + 'mailing.contact', 'mailing_contact_list_rel', 'list_id', 'contact_id', string='Mailing Lists') - subscription_contact_ids = fields.One2many('mail.mass_mailing.list_contact_rel', 'list_id', + subscription_ids = fields.One2many('mailing.contact.subscription', 'list_id', string='Subscription Information') is_public = fields.Boolean(default=True, help="The mailing list can be accessible by recipient in the unsubscription" " page to allows him to update his subscription preferences.") @@ -27,8 +27,8 @@ class MassMailingList(models.Model): select list_id, count(*) from - mail_mass_mailing_contact_list_rel r - left join mail_mass_mailing_contact c on (r.contact_id=c.id) + mailing_contact_list_rel r + left join mailing_contact c on (r.contact_id=c.id) left join mail_blacklist bl on c.email_normalized = bl.email and bl.active where list_id in %s @@ -73,30 +73,30 @@ class MassMailingList(models.Model): # Put destination is sources lists if not already the case src_lists |= self self.env.cr.execute(""" - INSERT INTO mail_mass_mailing_contact_list_rel (contact_id, list_id) + INSERT INTO mailing_contact_list_rel (contact_id, list_id) SELECT st.contact_id AS contact_id, %s AS list_id FROM ( SELECT contact.id AS contact_id, contact.email AS email, - mailing_list.id AS list_id, + list.id AS list_id, row_number() OVER (PARTITION BY email ORDER BY email) AS rn FROM - mail_mass_mailing_contact contact, - mail_mass_mailing_contact_list_rel contact_list_rel, - mail_mass_mailing_list mailing_list + mailing_contact contact, + mailing_contact_list_rel contact_list_rel, + mailing_list list WHERE contact.id=contact_list_rel.contact_id AND COALESCE(contact_list_rel.opt_out,FALSE) = FALSE AND contact.email_normalized NOT IN (select email from mail_blacklist where active = TRUE) - AND mailing_list.id=contact_list_rel.list_id - AND mailing_list.id IN %s + AND list.id=contact_list_rel.list_id + AND list.id IN %s AND NOT EXISTS ( SELECT 1 FROM - mail_mass_mailing_contact contact2, - mail_mass_mailing_contact_list_rel contact_list_rel2 + mailing_contact contact2, + mailing_contact_list_rel contact_list_rel2 WHERE contact2.email = contact.email AND contact_list_rel2.contact_id = contact2.id AND contact_list_rel2.list_id = %s diff --git a/addons/mass_mailing/models/mass_mailing.py b/addons/mass_mailing/models/mass_mailing.py index d0249777baf..29493baa4e8 100644 --- a/addons/mass_mailing/models/mass_mailing.py +++ b/addons/mass_mailing/models/mass_mailing.py @@ -24,8 +24,8 @@ MASS_MAILING_BUSINESS_MODELS = [ 'res.partner', 'event.track', 'sale.order', - 'mail.mass_mailing.list', - 'mail.mass_mailing.contact' + 'mailing.list', + 'mailing.contact' ] # Syntax of the data URL Scheme: https://tools.ietf.org/html/rfc2397#section-3 @@ -205,7 +205,7 @@ class MassMailing(models.Model): def default_get(self, fields): res = super(MassMailing, self).default_get(fields) if 'reply_to_mode' in fields and not 'reply_to_mode' in res and res.get('mailing_model_real'): - if res['mailing_model_real'] in ['res.partner', 'mail.mass_mailing.contact']: + if res['mailing_model_real'] in ['res.partner', 'mailing.contact']: res['reply_to_mode'] = 'email' else: res['reply_to_mode'] = 'thread' @@ -240,15 +240,15 @@ class MassMailing(models.Model): reply_to = fields.Char(string='Reply To', help='Preferred Reply-To Address', default=lambda self: self.env['mail.message']._get_default_from()) # recipients - mailing_model_real = fields.Char(compute='_compute_model', string='Recipients Real Model', default='mail.mass_mailing.contact', required=True) + mailing_model_real = fields.Char(compute='_compute_model', string='Recipients Real Model', default='mailing.contact', required=True) mailing_model_id = fields.Many2one('ir.model', string='Recipients Model', domain=[('model', 'in', MASS_MAILING_BUSINESS_MODELS)], - default=lambda self: self.env.ref('mass_mailing.model_mail_mass_mailing_list').id) + default=lambda self: self.env.ref('mass_mailing.model_mailing_list').id) mailing_model_name = fields.Char(related='mailing_model_id.model', string='Recipients Model Name', readonly=True, related_sudo=True) mailing_domain = fields.Char(string='Domain', oldname='domain', default=[]) mail_server_id = fields.Many2one('ir.mail_server', string='Mail Server', default=_get_default_mail_server_id, help="Use a specific mail server in priority. Otherwise Odoo relies on the first outgoing mail server available (based on their sequencing) as it does for normal mails.") - contact_list_ids = fields.Many2many('mail.mass_mailing.list', 'mail_mass_mailing_list_rel', + contact_list_ids = fields.Many2many('mailing.list', 'mail_mass_mailing_list_rel', string='Mailing Lists') contact_ab_pc = fields.Integer(string='A/B Testing percentage', help='Percentage of the contacts that will be mailed. Recipients will be taken randomly.', default=100) @@ -292,7 +292,7 @@ class MassMailing(models.Model): @api.depends('mailing_model_id') def _compute_model(self): for record in self: - record.mailing_model_real = (record.mailing_model_name != 'mail.mass_mailing.list') and record.mailing_model_name or 'mail.mass_mailing.contact' + record.mailing_model_real = (record.mailing_model_name != 'mailing.list') and record.mailing_model_name or 'mailing.contact' def _compute_statistics(self): """ Compute statistics of the mass mailing """ @@ -349,7 +349,7 @@ class MassMailing(models.Model): def _onchange_model_and_list(self): mailing_domain = [] if self.mailing_model_name: - if self.mailing_model_name == 'mail.mass_mailing.list': + if self.mailing_model_name == 'mailing.list': if self.contact_list_ids: mailing_domain.append(('list_ids', 'in', self.contact_list_ids.ids)) else: @@ -488,9 +488,9 @@ class MassMailing(models.Model): def update_opt_out(self, email, list_ids, value): if len(list_ids) > 0: - model = self.env['mail.mass_mailing.contact'].with_context(active_test=False) + model = self.env['mailing.contact'].with_context(active_test=False) records = model.search([('email_normalized', '=', tools.email_normalize(email))]) - opt_out_records = self.env['mail.mass_mailing.list_contact_rel'].search([ + opt_out_records = self.env['mailing.contact.subscription'].search([ ('contact_id', 'in', records.ids), ('list_id', 'in', list_ids), ('opt_out', '!=', value) @@ -514,11 +514,11 @@ class MassMailing(models.Model): self.ensure_one() opt_out = {} target = self.env[self.mailing_model_real] - if self.mailing_model_real == "mail.mass_mailing.contact": + if self.mailing_model_real == "mailing.contact": # if user is opt_out on One list but not on another # or if two user with same email address, one opted in and the other one opted out, send the mail anyway # TODO DBE Fixme : Optimise the following to get real opt_out and opt_in - target_list_contacts = self.env['mail.mass_mailing.list_contact_rel'].search( + target_list_contacts = self.env['mailing.contact.subscription'].search( [('list_id', 'in', self.contact_list_ids.ids)]) opt_out_contacts = target_list_contacts.filtered(lambda rel: rel.opt_out).mapped('contact_id.email_normalized') opt_in_contacts = target_list_contacts.filtered(lambda rel: not rel.opt_out).mapped('contact_id.email_normalized') diff --git a/addons/mass_mailing/security/ir.model.access.csv b/addons/mass_mailing/security/ir.model.access.csv index ed137e9ba92..1bf09f0486a 100644 --- a/addons/mass_mailing/security/ir.model.access.csv +++ b/addons/mass_mailing/security/ir.model.access.csv @@ -1,7 +1,8 @@ id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink access_mass_mailing_tag,mail.mass_mailing.tag,model_mail_mass_mailing_tag,mass_mailing.group_mass_mailing_campaign,1,1,1,1 -access_mass_mailing_contact,mail.mass_mailing.contact,model_mail_mass_mailing_contact,mass_mailing.group_mass_mailing_user,1,1,1,1 -access_mass_mailing_list,mail.mass_mailing.list,model_mail_mass_mailing_list,mass_mailing.group_mass_mailing_user,1,1,1,1 +access_mailing_contact_mm_user,access.mailing.contact.mm.user,model_mailing_contact,mass_mailing.group_mass_mailing_user,1,1,1,1 +access_mailing_contact_subscription_mm_user,access.mailing.contact.subscription.mm.user,model_mailing_contact_subscription,mass_mailing.group_mass_mailing_user,1,1,1,1 +access_mailing_list_mm_user,access.mailing.list.mm.user,model_mailing_list,mass_mailing.group_mass_mailing_user,1,1,1,1 access_mass_mailing_stage,mail.mass_mailing.stage,model_mail_mass_mailing_stage,mass_mailing.group_mass_mailing_user,1,1,1,1 access_mass_mailing_campaign,mail.mass_mailing.campaign,model_mail_mass_mailing_campaign,mass_mailing.group_mass_mailing_user,1,1,1,1 access_mass_mailing_campaign_system,mail.mass_mailing.campaign.system,model_mail_mass_mailing_campaign,base.group_system,1,1,1,1 @@ -13,5 +14,4 @@ access_mailing_trace_report_mm_user,access.mailing.trace.report.mm.user,model_ma access_utm_source,access_utm_source,utm.model_utm_source,mass_mailing.group_mass_mailing_user,1,1,1,0 access_ir_mail_server,access_ir_mail_server,base.model_ir_mail_server,mass_mailing.group_mass_mailing_user,1,0,0,0 access_mail_blacklist_mass_mailing_user,access.mail.blacklist.mass_mailing_user,mail.model_mail_blacklist,mass_mailing.group_mass_mailing_user,1,1,1,1 -access_mail_mass_mailing_list_contact_rel,access.mail.mass_mailing_list_contact_rel,model_mail_mass_mailing_list_contact_rel,mass_mailing.group_mass_mailing_user,1,1,1,1 access_link_tracker_mailing,access.link.tracker.mailing,link_tracker.model_link_tracker,mass_mailing.group_mass_mailing_user,1,1,1,1 \ No newline at end of file diff --git a/addons/mass_mailing/static/src/js/mass_mailing.js b/addons/mass_mailing/static/src/js/mass_mailing.js index 404ed6e554b..b516af7a47a 100644 --- a/addons/mass_mailing/static/src/js/mass_mailing.js +++ b/addons/mass_mailing/static/src/js/mass_mailing.js @@ -16,7 +16,7 @@ KanbanRecord.include({ _openRecord: function () { if (this.modelName === 'mail.mass_mailing.campaign') { this.$('.oe_mailings').click(); - } else if (this.modelName === 'mail.mass_mailing.list' && + } else if (this.modelName === 'mailing.list' && this.$('.o_mailing_list_kanban_boxes a')) { this.$('.o_mailing_list_kanban_boxes a').first().click(); } else { diff --git a/addons/mass_mailing/tests/test_mass_mailing_list_merge.py b/addons/mass_mailing/tests/test_mass_mailing_list_merge.py index 3408989d77e..fe8feba73ec 100644 --- a/addons/mass_mailing/tests/test_mass_mailing_list_merge.py +++ b/addons/mass_mailing/tests/test_mass_mailing_list_merge.py @@ -9,7 +9,7 @@ class TestMassMailingCommon(common.TransactionCase): def test_00_test_mass_mailing_list_merge(self): # Data set up - mailing_list_A = self.env['mail.mass_mailing.list'].create({ + mailing_list_A = self.env['mailing.list'].create({ 'name': 'A', 'contact_ids': [ (0, 0, {'name': 'Noel Flantier', 'email': 'noel.flantier@example.com'}), @@ -18,7 +18,7 @@ class TestMassMailingCommon(common.TransactionCase): ] }) - mailing_list_B = self.env['mail.mass_mailing.list'].create({ + mailing_list_B = self.env['mailing.list'].create({ 'name': 'B', 'contact_ids': [ (0, 0, {'name': 'Icallhimtest', 'email': 'icallhimtest@example.com'}), @@ -27,7 +27,7 @@ class TestMassMailingCommon(common.TransactionCase): ] }) - mailing_list_C = self.env['mail.mass_mailing.list'].create({ + mailing_list_C = self.env['mailing.list'].create({ 'name': 'C', 'contact_ids': [ (0, 0, {'name': 'Norberto', 'email': 'norbert@example.com'}), @@ -39,7 +39,7 @@ class TestMassMailingCommon(common.TransactionCase): # This test ensure that the mailing lists are correctly merged and no # duplicates are appearing in C - result_list = self.env['mass.mailing.list.merge'].create({ + result_list = self.env['mailing.list.merge'].create({ 'src_list_ids': [(4, list_id) for list_id in [mailing_list_A.id, mailing_list_B.id]], 'dest_list_id': mailing_list_C.id, 'merge_options': 'existing', diff --git a/addons/mass_mailing/tests/test_mass_mailing_shortener.py b/addons/mass_mailing/tests/test_mass_mailing_shortener.py index d279208264d..5b7ba5cf877 100644 --- a/addons/mass_mailing/tests/test_mass_mailing_shortener.py +++ b/addons/mass_mailing/tests/test_mass_mailing_shortener.py @@ -35,23 +35,23 @@ class TestMassMailingShortener(common.TransactionCase): self.addCleanup(patcher2.stop) def test_00_test_mass_mailing_shortener(self): - mailing_list_A = self.env['mail.mass_mailing.list'].create({ + mailing_list_A = self.env['mailing.list'].create({ 'name': 'A', }) - self.env['mail.mass_mailing.contact'].create({ + self.env['mailing.contact'].create({ 'name': 'User 1', 'email': 'user1@example.com', 'list_ids': [(4, mailing_list_A.id)] }) - self.env['mail.mass_mailing.contact'].create({ + self.env['mailing.contact'].create({ 'name': 'User 2', 'email': 'user2@example.com', 'list_ids': [(4, mailing_list_A.id)] }) - self.env['mail.mass_mailing.contact'].create({ + self.env['mailing.contact'].create({ 'name': 'User 3', 'email': 'user3@example.com', 'list_ids': [(4, mailing_list_A.id)] }) mass_mailing = self.env['mail.mass_mailing'].create({ "reply_to_mode": "email", "reply_to": "Administrator ", - "mailing_model_id": self.env.ref('mass_mailing.model_mail_mass_mailing_list').id, + "mailing_model_id": self.env.ref('mass_mailing.model_mailing_list').id, "mailing_domain": "[('list_ids', 'in', [%d])]" % mailing_list_A.id, "contact_list_ids": [[6, False, [mailing_list_A.id]]], "mass_mailing_campaign_id": False, diff --git a/addons/mass_mailing/views/mass_mailing_contact_views.xml b/addons/mass_mailing/views/mailing_contact_views.xml similarity index 86% rename from addons/mass_mailing/views/mass_mailing_contact_views.xml rename to addons/mass_mailing/views/mailing_contact_views.xml index f474b76240c..9a0b8989628 100644 --- a/addons/mass_mailing/views/mass_mailing_contact_views.xml +++ b/addons/mass_mailing/views/mailing_contact_views.xml @@ -1,10 +1,9 @@ - - - - mail.mass_mailing.list_contact_rel.view.form - mail.mass_mailing.list_contact_rel + + + mailing.contact.subscription.view.form + mailing.contact.subscription 10 @@ -27,10 +26,9 @@ - - - mail.mass_mailing.list_contact_rel.list_contact.view.tree - mail.mass_mailing.list_contact_rel + + mailing.contact.subscription.view.tree + mailing.contact.subscription @@ -43,10 +41,9 @@ - - - mail.mass_mailing.list_contact_rel.view.search - mail.mass_mailing.list_contact_rel + + mailing.contact.subscription.view.search + mailing.contact.subscription @@ -71,8 +68,8 @@ - mail.mass_mailing.contact.search - mail.mass_mailing.contact + mailing.contact.search + mailing.contact - mail.mass_mailing.contact.tree - mail.mass_mailing.contact + mailing.contact.tree + mailing.contact 10 @@ -116,8 +113,8 @@ - mail.mass_mailing.contact.kanban - mail.mass_mailing.contact + mailing.contact.kanban + mailing.contact @@ -158,8 +155,8 @@ - mail.mass_mailing.contact.form - mail.mass_mailing.contact + mailing.contact.form + mailing.contact 10 @@ -213,8 +210,8 @@ - mail.mass_mailing.contact.pivot - mail.mass_mailing.contact + mailing.contact.pivot + mailing.contact 10 @@ -224,8 +221,8 @@ - mail.mass_mailing.contact.graph - mail.mass_mailing.contact + mailing.contact.graph + mailing.contact 10 @@ -236,7 +233,7 @@ Mailing List Contacts - mail.mass_mailing.contact + mailing.contact tree,kanban,form,graph,pivot {'search_default_valid_recipient': 1} @@ -250,7 +247,7 @@ Recipients - mail.mass_mailing.contact + mailing.contact tree,form,graph [('list_ids', 'in', [active_id])] {'search_default_valid_recipients': 1, 'default_list_ids': [active_id]} diff --git a/addons/mass_mailing/views/mass_mailing_list_views.xml b/addons/mass_mailing/views/mailing_list_views.xml similarity index 85% rename from addons/mass_mailing/views/mass_mailing_list_views.xml rename to addons/mass_mailing/views/mailing_list_views.xml index 3a3185f9bda..597c7b3a9ff 100644 --- a/addons/mass_mailing/views/mass_mailing_list_views.xml +++ b/addons/mass_mailing/views/mailing_list_views.xml @@ -1,9 +1,9 @@ - - mail.mass_mailing.list.search - mail.mass_mailing.list + + mailing.list.view.search + mailing.list @@ -18,9 +18,9 @@ - - mail.mass_mailing.list.tree - mail.mass_mailing.list + + mailing.list.view.tree + mailing.list 10 @@ -32,9 +32,9 @@ - - mail.mass_mailing.list.form - mail.mass_mailing.list + + mailing.list.form + mailing.list @@ -60,9 +60,9 @@ - - mail.mass_mailing.list.form.simplified - mail.mass_mailing.list + + mailing.list.form.simplified + mailing.list @@ -88,15 +88,15 @@ Create a Mass Mailing List - mail.mass_mailing.list + mailing.list form - + new - - mail.mass_mailing.list.kanban - mail.mass_mailing.list + + mailing.list.view.kanban + mailing.list @@ -149,7 +149,7 @@ Mailing Lists - mail.mass_mailing.list + mailing.list kanban,tree,form

diff --git a/addons/mass_mailing/views/mass_mailing_views.xml b/addons/mass_mailing/views/mass_mailing_views.xml index 7bbf205497f..7643974d42b 100644 --- a/addons/mass_mailing/views/mass_mailing_views.xml +++ b/addons/mass_mailing/views/mass_mailing_views.xml @@ -158,10 +158,10 @@ -

+