diff --git a/addons/crm/__manifest__.py b/addons/crm/__manifest__.py index 1100d1f4214..acb084220cc 100644 --- a/addons/crm/__manifest__.py +++ b/addons/crm/__manifest__.py @@ -47,6 +47,7 @@ 'report/crm_opportunity_report_views.xml', 'views/crm_team_views.xml', 'views/digest_views.xml', + 'views/utm_campaign_views.xml' ], 'demo': [ 'data/crm_demo.xml', diff --git a/addons/crm/models/__init__.py b/addons/crm/models/__init__.py index 0a441965aa8..463bcc22e85 100644 --- a/addons/crm/models/__init__.py +++ b/addons/crm/models/__init__.py @@ -10,3 +10,4 @@ from . import res_config_settings from . import res_partner from . import digest from . import crm_lead_scoring_frequency +from . import utm diff --git a/addons/crm/models/utm.py b/addons/crm/models/utm.py new file mode 100644 index 00000000000..c68f6b43365 --- /dev/null +++ b/addons/crm/models/utm.py @@ -0,0 +1,42 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import fields, models, api, SUPERUSER_ID + +class UtmCampaign(models.Model): + _inherit = 'utm.campaign' + + crm_lead_activated = fields.Boolean('Use Leads', compute='_compute_crm_lead_activated') + lead_count = fields.Integer('Lead Count', groups='sales_team.group_sale_salesman', compute="_compute_global_opportunity_and_lead_count") + opportunity_count = fields.Integer('Opportunity Count', groups='sales_team.group_sale_salesman', compute="_compute_global_opportunity_and_lead_count") + + def _compute_crm_lead_activated(self): + for campaign in self: + campaign.crm_lead_activated = self.env.user.has_group('crm.group_use_lead') + + def _compute_global_opportunity_and_lead_count(self): + lead_data = self.env['crm.lead'].with_context(active_test=False).read_group([ + ('campaign_id', 'in', self.ids)], + ['campaign_id'], ['campaign_id']) + data_map = {datum['campaign_id'][0]: datum['campaign_id_count'] for datum in lead_data} + if self.env.user.has_group('crm.group_use_lead'): + for campaign in self: + campaign.lead_count = data_map.get(campaign.id, 0) + campaign.opportunity_count = 0 + else: + for campaign in self: + campaign.lead_count = 0 + campaign.opportunity_count = data_map.get(campaign.id, 0) + + def action_redirect_to_leads(self): + action = self.env.ref('crm.crm_lead_all_leads').read()[0] + action['domain'] = [('campaign_id', '=', self.id)] + action['context'] = {'default_type': 'lead', 'active_test': False} + return action + + def action_redirect_to_opportunities(self): + action = self.env.ref('crm.crm_lead_opportunities').read()[0] + action['view_mode'] = 'tree,kanban,graph,pivot,form,calendar' + action['domain'] = [('campaign_id', '=', self.id)] + action['context'] = {'active_test': False} + return action diff --git a/addons/crm/views/crm_lead_views.xml b/addons/crm/views/crm_lead_views.xml index 2cfeb26fd0f..a10ba85000f 100644 --- a/addons/crm/views/crm_lead_views.xml +++ b/addons/crm/views/crm_lead_views.xml @@ -781,6 +781,7 @@ + + + + utm.campaign.view.kanban + utm.campaign + + + + +
+ Leads +
+
+ Opportunities +
+
+
+
+ + + utm.campaign.view.form + utm.campaign + + + + + + + + + +
diff --git a/addons/link_tracker/__manifest__.py b/addons/link_tracker/__manifest__.py index c458d23f539..3f622632cc4 100644 --- a/addons/link_tracker/__manifest__.py +++ b/addons/link_tracker/__manifest__.py @@ -11,6 +11,7 @@ Shorten URLs and use them to track clicks and UTMs 'depends': ['utm'], 'data': [ 'views/link_tracker_views.xml', + 'views/utm_campaign_views.xml', 'security/ir.model.access.csv', ], } diff --git a/addons/link_tracker/models/__init__.py b/addons/link_tracker/models/__init__.py index d0881ab6f62..847f935569e 100644 --- a/addons/link_tracker/models/__init__.py +++ b/addons/link_tracker/models/__init__.py @@ -2,3 +2,4 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. from . import link_tracker +from . import utm diff --git a/addons/link_tracker/models/link_tracker.py b/addons/link_tracker/models/link_tracker.py index 4adb4ce5b3a..ed12d1e0f01 100644 --- a/addons/link_tracker/models/link_tracker.py +++ b/addons/link_tracker/models/link_tracker.py @@ -231,6 +231,7 @@ class LinkTrackerClick(models.Model): _rec_name = "link_id" _description = "Link Tracker Click" + campaign_id = fields.Many2one(string='UTM Campaign', comodel_name="utm.campaign", related="link_id.campaign_id", store=True) link_id = fields.Many2one('link.tracker', 'Link', required=True, ondelete='cascade') ip = fields.Char(string='Internet Protocol') country_id = fields.Many2one('res.country', 'Country') diff --git a/addons/link_tracker/models/utm.py b/addons/link_tracker/models/utm.py new file mode 100644 index 00000000000..537993988c0 --- /dev/null +++ b/addons/link_tracker/models/utm.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import fields, models, api, SUPERUSER_ID + + +class UtmCampaign(models.Model): + _inherit = ['utm.campaign'] + _description = 'UTM Campaign' + + items_total = fields.Integer(string="Number of items that are part of the campaign", compute="_compute_items_total") + clicked_total = fields.Integer(string="Number of items that are part of the campaign who generated a click", compute="_compute_items_total") + clicks_ratio = fields.Integer(string="Global Clicks Ratio for the campaign", compute="_compute_clicks_ratio") + + def _compute_items_total(self): + """Empty method that will be implemented by inheriting classes""" + return + + @api.depends('items_total', 'clicked_total') + def _compute_clicks_ratio(self): + for campaign in self: + campaign.clicks_ratio = campaign.clicked_total / campaign.items_total * 100 if campaign.items_total > 0 else 0 diff --git a/addons/link_tracker/views/link_tracker_views.xml b/addons/link_tracker/views/link_tracker_views.xml index f5b27deff28..c6d54738f8a 100644 --- a/addons/link_tracker/views/link_tracker_views.xml +++ b/addons/link_tracker/views/link_tracker_views.xml @@ -9,7 +9,13 @@ + + + + + +
@@ -147,6 +153,19 @@ {'search_default_groupby_country_id': 1} + + Statistics of Clicks + link.tracker + tree,form,graph + + +

+ Create a new link tracker +

+
+ {'search_default_campaign_id': active_id} +
+ + + + utm.campaign.view.form + utm.campaign + + + + + + + + + + utm.campaign.view.form + utm.campaign + + + +
+ Clicks % +
+
+
+
+
+ diff --git a/addons/mass_mailing/__manifest__.py b/addons/mass_mailing/__manifest__.py index ab2077b97fb..cc4a7c0d0c7 100644 --- a/addons/mass_mailing/__manifest__.py +++ b/addons/mass_mailing/__manifest__.py @@ -34,6 +34,7 @@ 'views/mailing_list_views.xml', 'views/mailing_mailing_views.xml', 'views/res_config_settings_views.xml', + 'views/utm_campaign_views.xml', 'report/mailing_trace_report_views.xml', 'views/assets.xml', 'views/mass_mailing_templates_portal.xml', diff --git a/addons/mass_mailing/data/mass_mailing_data.xml b/addons/mass_mailing/data/mass_mailing_data.xml index 37d21928188..1d4bdc2ae3d 100644 --- a/addons/mass_mailing/data/mass_mailing_data.xml +++ b/addons/mass_mailing/data/mass_mailing_data.xml @@ -1,21 +1,6 @@ - - - - Schedule - 10 - - - Design - 20 - - - Sent - 30 - - Mass Mailing: Process queue diff --git a/addons/mass_mailing/data/mass_mailing_demo.xml b/addons/mass_mailing/data/mass_mailing_demo.xml index af6f9ba57a4..40d04fcd0c1 100644 --- a/addons/mass_mailing/data/mass_mailing_demo.xml +++ b/addons/mass_mailing/data/mass_mailing_demo.xml @@ -51,21 +51,14 @@ - - Marketing - - Newsletter 1 - + Newsletter - + - - - - + @@ -75,8 +68,7 @@ info@yourcompany.example.com - - + diff --git a/addons/mass_mailing/models/__init__.py b/addons/mass_mailing/models/__init__.py index 1e852942767..9797d43aeff 100644 --- a/addons/mass_mailing/models/__init__.py +++ b/addons/mass_mailing/models/__init__.py @@ -10,3 +10,4 @@ from . import mail_mail from . import mail_template from . import mail_thread from . import res_config_settings +from . import utm diff --git a/addons/mass_mailing/models/link_tracker.py b/addons/mass_mailing/models/link_tracker.py index ce3897e006a..5fa99fba774 100644 --- a/addons/mass_mailing/models/link_tracker.py +++ b/addons/mass_mailing/models/link_tracker.py @@ -8,7 +8,6 @@ class LinkTracker(models.Model): _inherit = "link.tracker" mass_mailing_id = fields.Many2one('mailing.mailing', string='Mass Mailing') - mass_mailing_campaign_id = fields.Many2one('mail.mass_mailing.campaign', string='Mass Mailing Campaign') class LinkTrackerClick(models.Model): @@ -16,7 +15,6 @@ class LinkTrackerClick(models.Model): mailing_trace_id = fields.Many2one('mailing.trace', string='Mail Statistics') mass_mailing_id = fields.Many2one('mailing.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) @@ -26,8 +24,8 @@ class LinkTrackerClick(models.Model): 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'] = trace_sudo.mass_mailing_campaign_id.id + if not click_values.get('campaign_id'): + click_values['campaign_id'] = trace_sudo.campaign_id.id if not click_values.get('mass_mailing_id'): click_values['mass_mailing_id'] = trace_sudo.mass_mailing_id.id diff --git a/addons/mass_mailing/models/mailing.py b/addons/mass_mailing/models/mailing.py index 417e7f3b6cc..2011ac6c916 100644 --- a/addons/mass_mailing/models/mailing.py +++ b/addons/mass_mailing/models/mailing.py @@ -32,155 +32,6 @@ MASS_MAILING_BUSINESS_MODELS = [ # Used to find inline images image_re = re.compile(r"data:(image/[A-Za-z]+);base64,(.*)") - -class MailingTag(models.Model): - """Model of categories of mass mailing, i.e. marketing, newsletter, ... """ - _name = 'mailing.tag' - _description = 'Mass Mailing Tag' - _order = 'name' - - name = fields.Char(required=True, translate=True) - color = fields.Integer(string='Color Index') - - _sql_constraints = [ - ('name_uniq', 'unique (name)', "Tag name already exists !"), - ] - - -class MailingStage(models.Model): - - """Stage for mass mailing campaigns. """ - _name = 'mailing.stage' - _description = 'Mass Mailing Campaign Stage' - _order = 'sequence' - - name = fields.Char(required=True, translate=True) - sequence = fields.Integer() - - -class MassMailingCampaign(models.Model): - """Model of mass mailing campaigns. """ - _name = "mail.mass_mailing.campaign" - _description = 'Mass Mailing Campaign' - _rec_name = "campaign_id" - _inherits = {'utm.campaign': 'campaign_id'} - - stage_id = fields.Many2one('mailing.stage', string='Stage', ondelete='restrict', required=True, - default=lambda self: self.env['mailing.stage'].search([], limit=1), - group_expand='_group_expand_stage_ids') - user_id = fields.Many2one( - 'res.users', string='Responsible', - required=True, default=lambda self: self.env.uid) - campaign_id = fields.Many2one('utm.campaign', 'campaign_id', - required=True, ondelete='cascade', help="This name helps you tracking your different campaign efforts, e.g. Fall_Drive, Christmas_Special") - source_id = fields.Many2one('utm.source', string='Source', - help="This is the link source, e.g. Search Engine, another domain,or name of email list", default=lambda self: self.env.ref('utm.utm_source_newsletter', False)) - 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', False)) - tag_ids = fields.Many2many( - 'mailing.tag', 'mail_mass_mailing_tag_rel', - 'tag_id', 'campaign_id', string='Tags') - mass_mailing_ids = fields.One2many( - 'mailing.mailing', 'mass_mailing_campaign_id', - string='Mass Mailings') - unique_ab_testing = fields.Boolean(string='Allow A/B Testing', default=False, - help='If checked, recipients will be mailed only once for the whole campaign. ' - 'This lets you send different mailings to randomly selected recipients and test ' - '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") - # trace statistics fields - total = fields.Integer(compute="_compute_statistics") - scheduled = fields.Integer(compute="_compute_statistics") - failed = fields.Integer(compute="_compute_statistics") - ignored = fields.Integer(compute="_compute_statistics") - sent = fields.Integer(compute="_compute_statistics", string="Sent Emails") - delivered = fields.Integer(compute="_compute_statistics") - opened = fields.Integer(compute="_compute_statistics") - replied = fields.Integer(compute="_compute_statistics") - bounced = fields.Integer(compute="_compute_statistics") - received_ratio = fields.Integer(compute="_compute_statistics", string='Received Ratio') - opened_ratio = fields.Integer(compute="_compute_statistics", string='Opened Ratio') - replied_ratio = fields.Integer(compute="_compute_statistics", string='Replied Ratio') - bounced_ratio = fields.Integer(compute="_compute_statistics", string='Bounced Ratio') - total_mailings = fields.Integer(compute="_compute_total_mailings", string='Mailings') - - def _compute_clicks_ratio(self): - self.env.cr.execute(""" - 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), )) - - campaign_data = self.env.cr.dictfetchall() - mapped_data = dict([(c['id'], 100 * c['nb_clicks'] / c['nb_mails']) for c in campaign_data]) - for campaign in self: - campaign.clicks_ratio = mapped_data.get(campaign.id, 0) - - def _compute_statistics(self): - """ Compute statistics of the mass mailing campaign """ - self.env.cr.execute(""" - SELECT - c.id as campaign_id, - COUNT(s.id) AS total, - COUNT(CASE WHEN s.sent is not null THEN 1 ELSE null END) AS sent, - COUNT(CASE WHEN s.scheduled is not null AND s.sent is null AND s.exception is null AND s.ignored is null THEN 1 ELSE null END) AS scheduled, - COUNT(CASE WHEN s.scheduled is not null AND s.sent is null AND s.exception is not null THEN 1 ELSE null END) AS failed, - COUNT(CASE WHEN s.scheduled is not null AND s.sent is null AND s.exception is null AND s.ignored is not null THEN 1 ELSE null END) AS ignored, - COUNT(CASE WHEN s.id is not null AND s.bounced is null THEN 1 ELSE null END) AS delivered, - COUNT(CASE WHEN s.opened is not null THEN 1 ELSE null END) AS opened, - 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 - mailing_trace s - RIGHT JOIN - mail_mass_mailing_campaign c - ON (c.id = s.mass_mailing_campaign_id) - WHERE - c.id IN %s - GROUP BY - c.id - """, (tuple(self.ids), )) - - for row in self.env.cr.dictfetchall(): - total = (row['total'] - row['ignored']) or 1 - row['delivered'] = row['sent'] - row['bounced'] - row['received_ratio'] = 100.0 * row['delivered'] / total - row['opened_ratio'] = 100.0 * row['opened'] / total - row['replied_ratio'] = 100.0 * row['replied'] / total - row['bounced_ratio'] = 100.0 * row['bounced'] / total - self.browse(row.pop('campaign_id')).update(row) - - def _compute_total_mailings(self): - campaign_data = self.env['mailing.mailing'].read_group( - [('mass_mailing_campaign_id', 'in', self.ids)], - ['mass_mailing_campaign_id'], ['mass_mailing_campaign_id']) - mapped_data = dict([(c['mass_mailing_campaign_id'][0], c['mass_mailing_campaign_id_count']) for c in campaign_data]) - for campaign in self: - campaign.total_mailings = mapped_data.get(campaign.id, 0) - - 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, {}) - for campaign in self: - domain = [('mass_mailing_campaign_id', '=', campaign.id)] - if model: - domain += [('model', '=', model)] - res[campaign.id] = set(self.env['mailing.trace'].search(domain).mapped('res_id')) - return res - - @api.model - def _group_expand_stage_ids(self, stages, domain, order): - """ Read group customization in order to display all the stages in the - kanban view, even if they are empty - """ - stage_ids = stages._search([], order=order, access_rights_uid=SUPERUSER_ID) - return stages.browse(stage_ids) - - class MassMailing(models.Model): """ MassMailing models a wave of emails for a mass mailign campaign. A mass mailing is an occurence of sending emails. """ @@ -223,9 +74,7 @@ class MassMailing(models.Model): attachment_ids = fields.Many2many('ir.attachment', 'mass_mailing_ir_attachments_rel', 'mass_mailing_id', 'attachment_id', string='Attachments') keep_archives = fields.Boolean(string='Keep Archives') - mass_mailing_campaign_id = fields.Many2one('mail.mass_mailing.campaign', string='Mass Mailing Campaign') - campaign_id = fields.Many2one('utm.campaign', string='Campaign', - help="This name helps you tracking your different campaign efforts, e.g. Fall_Drive, Christmas_Special") + campaign_id = fields.Many2one('utm.campaign', string='UTM Campaign') 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="Delivery method: Email") @@ -252,6 +101,10 @@ class MassMailing(models.Model): 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) + unique_ab_testing = fields.Boolean(string='Allow A/B Testing', default=False, + help='If checked, recipients will be mailed only once for the whole campaign. ' + 'This lets you send different mailings to randomly selected recipients and test ' + 'the effectiveness of the mailings, without causing duplicate messages.') # statistics data mailing_trace_ids = fields.One2many('mailing.trace', 'mass_mailing_id', string='Emails Statistics') total = fields.Integer(compute="_compute_total") @@ -340,11 +193,6 @@ class MassMailing(models.Model): else: mass_mailing.next_departure = cron_time - @api.onchange('mass_mailing_campaign_id') - def _onchange_mass_mailing_campaign_id(self): - if self.mass_mailing_campaign_id: - 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): mailing_domain = [] @@ -531,14 +379,15 @@ class MassMailing(models.Model): return opt_out def _get_convert_links(self): - 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 - vals['campaign_id'] = self.mass_mailing_campaign_id.campaign_id.id + self.ensure_one() + vals = {'mass_mailing_id': self.id} + + if self.campaign_id: + vals['campaign_id'] = self.campaign_id.id + if self.source_id: + vals['source_id'] = self.source_id.id + if self.medium_id: + vals['medium_id'] = self.medium_id.id return vals def _get_seen_list(self): @@ -576,9 +425,9 @@ class MassMailing(models.Model): else: raise UserError(_("Unsupported mass mailing model %s") % self.mailing_model_id.name) - if self.mass_mailing_campaign_id.unique_ab_testing: + if self.unique_ab_testing: query +=""" - AND s.mass_mailing_campaign_id = %%(mailing_campaign_id)s; + AND s.campaign_id = %%(mailing_campaign_id)s; """ else: query +=""" @@ -586,7 +435,7 @@ class MassMailing(models.Model): AND s.model = %%(target_model)s; """ query = query % {'target': target._table, 'mail_field': mail_field} - params = {'mailing_id': self.id, 'mailing_campaign_id': self.mass_mailing_campaign_id.id, 'target_model': self.mailing_model_real} + params = {'mailing_id': self.id, 'mailing_campaign_id': self.campaign_id.id, 'target_model': self.mailing_model_real} self._cr.execute(query, params) seen_list = set(m[0] for m in self._cr.fetchall()) _logger.info( @@ -613,8 +462,8 @@ class MassMailing(models.Model): if self.contact_ab_pc < 100: 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] + if self.campaign_id and self.unique_ab_testing: + already_mailed = self.campaign_id._get_mailing_recipients()[self.campaign_id.id] else: already_mailed = set([]) remaining = set(res_ids).difference(already_mailed) @@ -671,19 +520,16 @@ class MassMailing(models.Model): def convert_links(self): res = {} for mass_mailing in self: - utm_mixin = mass_mailing.mass_mailing_campaign_id if mass_mailing.mass_mailing_campaign_id else mass_mailing html = mass_mailing.body_html if mass_mailing.body_html else '' vals = {'mass_mailing_id': mass_mailing.id} - if mass_mailing.mass_mailing_campaign_id: - vals['mass_mailing_campaign_id'] = mass_mailing.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 + if mass_mailing.campaign_id: + vals['campaign_id'] = mass_mailing.campaign_id.id + if mass_mailing.source_id: + vals['source_id'] = mass_mailing.source_id.id + if mass_mailing.medium_id: + vals['medium_id'] = mass_mailing.medium_id.id res[mass_mailing.id] = self.env['link.tracker'].convert_links(html, vals, blacklist=['/unsubscribe_from_list']) diff --git a/addons/mass_mailing/models/mailing_trace.py b/addons/mass_mailing/models/mailing_trace.py index 7f8ce107d14..6245d0093ff 100644 --- a/addons/mass_mailing/models/mailing_trace.py +++ b/addons/mass_mailing/models/mailing_trace.py @@ -30,8 +30,8 @@ class MailingTrace(models.Model): res_id = fields.Integer(string='Document ID') # campaign / wave data mass_mailing_id = fields.Many2one('mailing.mailing', string='Mass Mailing', index=True) - mass_mailing_campaign_id = fields.Many2one( - related='mass_mailing_id.mass_mailing_campaign_id', + campaign_id = fields.Many2one( + related='mass_mailing_id.campaign_id', string='Mass Mailing Campaign', store=True, readonly=True, index=True) # Bounce and tracking diff --git a/addons/mass_mailing/models/utm.py b/addons/mass_mailing/models/utm.py new file mode 100644 index 00000000000..9418dd914f2 --- /dev/null +++ b/addons/mass_mailing/models/utm.py @@ -0,0 +1,101 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import api, fields, models + +class UtmCampaign(models.Model): + _inherit = 'utm.campaign' + + mailing_ids = fields.One2many( + 'mailing.mailing', 'campaign_id', + string='Mass Mailings') + mailing_clicks_ratio = fields.Integer(default=0, compute="_compute_mailing_clicks_ratio", string="Number of clicks") + mailing_items = fields.Integer(compute="_compute_mailing_items", string='Mailings') + mailing_clicked = fields.Integer(compute="_compute_mailing_items", string='Mailings Clicked') + # stat fields + total = fields.Integer(compute="_compute_statistics") + scheduled = fields.Integer(compute="_compute_statistics") + failed = fields.Integer(compute="_compute_statistics") + ignored = fields.Integer(compute="_compute_statistics") + sent = fields.Integer(compute="_compute_statistics", string="Sent Emails") + delivered = fields.Integer(compute="_compute_statistics") + opened = fields.Integer(compute="_compute_statistics") + replied = fields.Integer(compute="_compute_statistics") + bounced = fields.Integer(compute="_compute_statistics") + received_ratio = fields.Integer(compute="_compute_statistics", string='Received Ratio') + opened_ratio = fields.Integer(compute="_compute_statistics", string='Opened Ratio') + replied_ratio = fields.Integer(compute="_compute_statistics", string='Replied Ratio') + bounced_ratio = fields.Integer(compute="_compute_statistics", string='Bounced Ratio') + + @api.depends('mailing_items', 'mailing_clicked') + def _compute_items_total(self): + for campaign in self: + campaign.items_total += campaign.mailing_items + campaign.clicked_total += campaign.mailing_clicked + + def _compute_mailing_items(self): + super(UtmCampaign, self)._compute_clicks_ratio() + query = """SELECT trace.campaign_id AS campaign_id, COUNT(DISTINCT(trace.id)) AS items_total, COUNT(DISTINCT(click.mailing_trace_id)) AS clicked_total + FROM mailing_trace AS trace + LEFT OUTER JOIN link_tracker_click as click ON click.mailing_trace_id = trace.id + WHERE trace.campaign_id IN %s + GROUP BY trace.campaign_id """ + params = [tuple(self.ids)] + self.env.cr.execute(query, params) + clicked_data = self.env.cr.dictfetchall() + mapped_data = {datum['campaign_id']: {'clicked_total': datum['clicked_total'], 'items_total': datum['items_total']} for datum in clicked_data} + + for campaign in self: + campaign_items_values = mapped_data.get(campaign.id, {}) + campaign.mailing_items = campaign_items_values.get('items_total', 0) + campaign.mailing_clicked = campaign_items_values.get('clicked_total', 0) + + @api.depends('mailing_items', 'mailing_clicked') + def _compute_mailing_clicks_ratio(self): + for campaign in self: + campaign.mailing_clicks_ratio = campaign.mailing_clicked / campaign.mailing_items * 100 if campaign.mailing_items > 0 else 0 + + def _compute_statistics(self): + """ Compute statistics of the mass mailing campaign """ + self.env.cr.execute(""" + SELECT + c.id as campaign_id, + COUNT(s.id) AS total, + COUNT(CASE WHEN s.sent is not null THEN 1 ELSE null END) AS sent, + COUNT(CASE WHEN s.scheduled is not null AND s.sent is null AND s.exception is null AND s.ignored is null THEN 1 ELSE null END) AS scheduled, + COUNT(CASE WHEN s.scheduled is not null AND s.sent is null AND s.exception is not null THEN 1 ELSE null END) AS failed, + COUNT(CASE WHEN s.scheduled is not null AND s.sent is null AND s.exception is null AND s.ignored is not null THEN 1 ELSE null END) AS ignored, + COUNT(CASE WHEN s.id is not null AND s.bounced is null THEN 1 ELSE null END) AS delivered, + COUNT(CASE WHEN s.opened is not null THEN 1 ELSE null END) AS opened, + 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 + mailing_trace s + RIGHT JOIN + utm_campaign c + ON (c.id = s.campaign_id) + WHERE + c.id IN %s + GROUP BY + c.id + """, (tuple(self.ids), )) + + for row in self.env.cr.dictfetchall(): + total = (row['total'] - row['ignored']) or 1 + row['delivered'] = row['sent'] - row['bounced'] + row['received_ratio'] = 100.0 * row['delivered'] / total + row['opened_ratio'] = 100.0 * row['opened'] / total + row['replied_ratio'] = 100.0 * row['replied'] / total + row['bounced_ratio'] = 100.0 * row['bounced'] / total + self.browse(row.pop('campaign_id')).update(row) + + def _get_mailing_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, {}) + for campaign in self: + domain = [('campaign_id', '=', campaign.id)] + if model: + domain += [('model', '=', model)] + res[campaign.id] = set(self.env['mailing.trace'].search(domain).mapped('res_id')) + return res diff --git a/addons/mass_mailing/report/mailing_trace_report.py b/addons/mass_mailing/report/mailing_trace_report.py index be48a863b1e..66f37c83599 100644 --- a/addons/mass_mailing/report/mailing_trace_report.py +++ b/addons/mass_mailing/report/mailing_trace_report.py @@ -45,8 +45,7 @@ class MailingTraceReport(models.Model): FROM mailing_trace as ms left join mailing_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) + left join utm_campaign as utm_campaign ON (mm.campaign_id = utm_campaign.id) left join utm_source as utm_source ON (mm.source_id = utm_source.id) GROUP BY ms.scheduled, utm_source.name, utm_campaign.name, mm.state, mm.email_from )""") diff --git a/addons/mass_mailing/security/ir.model.access.csv b/addons/mass_mailing/security/ir.model.access.csv index d131c63a741..63d65ed09c7 100644 --- a/addons/mass_mailing/security/ir.model.access.csv +++ b/addons/mass_mailing/security/ir.model.access.csv @@ -1,11 +1,10 @@ id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink -access_mailing_tag,access.mailing.tag,model_mailing_tag,mass_mailing.group_mass_mailing_campaign,1,1,1,1 +access_utm_tag_mass_mailing_campaign,utm.tag,utm.model_utm_tag,mass_mailing.group_mass_mailing_campaign,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_mailing_stage,access.mailing.stage.mm.user,model_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 +access_utm_stage,utm.stage,utm.model_utm_stage,mass_mailing.group_mass_mailing_user,1,1,1,1 +access_utm_campaign_mass_mailing_user,utm.campaign,utm.model_utm_campaign,mass_mailing.group_mass_mailing_user,1,1,1,1 access_mailing_mailing_mm_user,access.mailing.mailing.mm.user,model_mailing_mailing,mass_mailing.group_mass_mailing_user,1,1,1,1 access_mailing_mailing_system,access.mailing.mailing.system,model_mailing_mailing,base.group_system,1,1,1,1 access_mailing_trace_user,mailing.trace.user,model_mailing_trace,base.group_user,1,1,1,1 @@ -14,4 +13,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_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 +access_link_tracker_mailing,access.link.tracker.mailing,link_tracker.model_link_tracker,mass_mailing.group_mass_mailing_user,1,1,1,1 diff --git a/addons/mass_mailing/static/src/js/mass_mailing.js b/addons/mass_mailing/static/src/js/mass_mailing.js index 5d156c3cd65..e74d97f562b 100644 --- a/addons/mass_mailing/static/src/js/mass_mailing.js +++ b/addons/mass_mailing/static/src/js/mass_mailing.js @@ -1,30 +1,8 @@ odoo.define('mass_mailing.mass_mailing', function (require) { "use strict"; -var KanbanRecord = require('web.KanbanRecord'); var KanbanColumn = require('web.KanbanColumn'); -KanbanRecord.include({ - //-------------------------------------------------------------------------- - // Private - //-------------------------------------------------------------------------- - - /** - * @override - * @private - */ - _openRecord: function () { - if (this.modelName === 'mail.mass_mailing.campaign') { - this.$('.oe_mailings').click(); - } else if (this.modelName === 'mailing.list' && - this.$('.o_mailing_list_kanban_boxes a')) { - this.$('.o_mailing_list_kanban_boxes a').first().click(); - } else { - this._super.apply(this, arguments); - } - }, -}); - KanbanColumn.include({ init: function () { this._super.apply(this, arguments); diff --git a/addons/mass_mailing/static/src/js/mass_mailing_campaign_kanban_record.js b/addons/mass_mailing/static/src/js/mass_mailing_campaign_kanban_record.js deleted file mode 100644 index 4a237ced9e3..00000000000 --- a/addons/mass_mailing/static/src/js/mass_mailing_campaign_kanban_record.js +++ /dev/null @@ -1,18 +0,0 @@ -odoo.define('mass_mailing.CampaignKanbanRecord', function (require) { -"use strict"; - -var KanbanRecord = require('web.KanbanRecord'); - -var MassMailingCampaignKanbanRecord = KanbanRecord.extend({ - /** - * @override - * @private - */ - _openRecord: function () { - this.$('.oe_mailings').click(); - } -}); - -return MassMailingCampaignKanbanRecord; - -}); diff --git a/addons/mass_mailing/static/src/js/mass_mailing_campaign_kanban_renderer.js b/addons/mass_mailing/static/src/js/mass_mailing_campaign_kanban_renderer.js deleted file mode 100644 index abc7dcc018a..00000000000 --- a/addons/mass_mailing/static/src/js/mass_mailing_campaign_kanban_renderer.js +++ /dev/null @@ -1,16 +0,0 @@ -odoo.define('mass_mailing.CampaignKanbanRenderer', function (require) { -"use strict"; - -var MassMailingCampaignKanbanRecord = require('mass_mailing.CampaignKanbanRecord'); - -var KanbanRenderer = require('web.KanbanRenderer'); - -var MassMailingCampaignKanbanRenderer = KanbanRenderer.extend({ - config: _.extend({}, KanbanRenderer.prototype.config, { - KanbanRecord: MassMailingCampaignKanbanRecord, - }), -}); - -return MassMailingCampaignKanbanRenderer; - -}); diff --git a/addons/mass_mailing/static/src/js/mass_mailing_campaign_kanban_view.js b/addons/mass_mailing/static/src/js/mass_mailing_campaign_kanban_view.js deleted file mode 100644 index cf7be9ed894..00000000000 --- a/addons/mass_mailing/static/src/js/mass_mailing_campaign_kanban_view.js +++ /dev/null @@ -1,19 +0,0 @@ -odoo.define('mass_mailing.CampaignKanbanView', function (require) { -"use strict"; - -var MassMailingCampaignKanbanRenderer = require('mass_mailing.CampaignKanbanRenderer'); - -var KanbanView = require('web.KanbanView'); -var view_registry = require('web.view_registry'); - -var MassMailingCampaignKanbanView = KanbanView.extend({ - config: _.extend({}, KanbanView.prototype.config, { - Renderer: MassMailingCampaignKanbanRenderer, - }), -}); - -view_registry.add('mass_mailing_campaign_kanban', MassMailingCampaignKanbanView); - -return MassMailingCampaignKanbanView; - -}); diff --git a/addons/mass_mailing/static/src/scss/mass_mailing.scss b/addons/mass_mailing/static/src/scss/mass_mailing.scss index 873e1b9dea0..8e20d437232 100644 --- a/addons/mass_mailing/static/src/scss/mass_mailing.scss +++ b/addons/mass_mailing/static/src/scss/mass_mailing.scss @@ -24,3 +24,22 @@ color: #005326; font-size: 90%; } + +@media only screen and (min-width: 1200px) { + .o_utm_campaign_mass_mailing_substats { + padding-right: 210px; + div { + margin-left: 50px; + } + } +} + +@media only screen and (min-width: 768px) and (max-width: 1200px) { + .o_utm_campaign_mass_mailing_substats { + padding-right: 180px; + div { + margin-left: 30px; + } + } +} + diff --git a/addons/mass_mailing/static/src/scss/mass_mailing_mobile.scss b/addons/mass_mailing/static/src/scss/mass_mailing_mobile.scss new file mode 100644 index 00000000000..9df458597b2 --- /dev/null +++ b/addons/mass_mailing/static/src/scss/mass_mailing_mobile.scss @@ -0,0 +1,7 @@ +@include media-breakpoint-down(sm) { + .o_utm_campaign_mass_mailing_substats { + width: 80%; + margin: 0 auto; + justify-content: space-between !important; + } +} \ No newline at end of file diff --git a/addons/mass_mailing/tests/test_mass_mailing_shortener.py b/addons/mass_mailing/tests/test_mass_mailing_shortener.py index bc55668579c..28af1d2df16 100644 --- a/addons/mass_mailing/tests/test_mass_mailing_shortener.py +++ b/addons/mass_mailing/tests/test_mass_mailing_shortener.py @@ -54,7 +54,7 @@ class TestMassMailingShortener(common.TransactionCase): "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, + "campaign_id": False, "name": "sdf", "subject": "test_00_test_mass_mailing_shortener", "body_html": """ diff --git a/addons/mass_mailing/views/assets.xml b/addons/mass_mailing/views/assets.xml index 03b1a1415bc..46a6cfcb5dc 100644 --- a/addons/mass_mailing/views/assets.xml +++ b/addons/mass_mailing/views/assets.xml @@ -3,15 +3,13 @@