[MERGE][REF] utm, mass_mailing: improve and use utm.campaign

This merge removes the mass_mailing.campaign model. Instead of having a
fully fledged model, we will simply inherit utm.campaign.

This change implies that mass_mailing.tag and mass_mailing.stage move to
the utm model along their associated views/data.

These changes were made so that campaigns could be used in the future by
social, mass_mailing and mass_sms and available in the same view

This commit also removes the source_id and the medium_id fields on the
campaign. Indeed those depends on the flow, not the campaign (mass mailing
is a source, email is a medium for example).

In this merge we also add statistics from crm and sales to the campaign
model like what has been done for mass mailing previously.

Task ID: 2002029&
PR: #34015

closes odoo/odoo#34452

Signed-off-by: Thibault Delavallee (tde) <tde@openerp.com>
This commit is contained in:
Robot Odoo
2019-08-02 17:49:45 +02:00
committed by GitHub
53 changed files with 857 additions and 627 deletions
+1
View File
@@ -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',
+1
View File
@@ -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
+42
View File
@@ -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
+1
View File
@@ -781,6 +781,7 @@
<field name="probability"/>
<field name="lost_reason"/>
<field name="date_conversion"/>
<field name="campaign_id"/>
<separator/>
<filter string="My Pipeline" name="assigned_to_me"
domain="[('user_id', '=', uid)]"
+50
View File
@@ -0,0 +1,50 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record model="ir.ui.view" id="utm_campaign_view_kanban">
<field name="name">utm.campaign.view.kanban</field>
<field name="model">utm.campaign</field>
<field name="inherit_id" ref="utm.utm_campaign_view_kanban"/>
<field name="arch" type="xml">
<xpath expr="//div[@id='utm_statistics']" position="inside">
<field name="crm_lead_activated" invisible="1"/>
<div class="d-flex justify-content-between align-items-center w-100 order-3 mb-1"
groups="sales_team.group_sale_salesman"
attrs="{'invisible': [('crm_lead_activated', '=', False)]}">
<strong>Leads</strong> <span><field name="lead_count"/></span>
</div>
<div class="d-flex justify-content-between align-items-center w-100 order-3 mb-1"
groups="sales_team.group_sale_salesman"
attrs="{'invisible': [('crm_lead_activated', '=', True)]}">
<strong>Opportunities</strong> <span><field name="opportunity_count"/></span>
</div>
</xpath>
</field>
</record>
<record model="ir.ui.view" id="utm_campaign_view_form">
<field name="name">utm.campaign.view.form</field>
<field name="model">utm.campaign</field>
<field name="inherit_id" ref="utm.utm_campaign_view_form"/>
<field name="arch" type="xml">
<xpath expr="//div[hasclass('oe_button_box')]" position="inside">
<field name="crm_lead_activated" invisible="1"/>
<button name="action_redirect_to_leads"
type="object"
class="oe_stat_button order-3"
icon="fa-star"
groups="sales_team.group_sale_salesman"
attrs="{'invisible': [('crm_lead_activated', '=', False)]}">
<field name="lead_count" widget="statinfo" string="Leads"/>
</button>
<button name="action_redirect_to_opportunities"
type="object"
class="oe_stat_button order-3"
icon="fa-star"
groups="sales_team.group_sale_salesman"
attrs="{'invisible': [('crm_lead_activated', '=', True)]}">
<field name="opportunity_count" widget="statinfo" string="Opportunities"/>
</button>
</xpath>
</field>
</record>
</odoo>
+1
View File
@@ -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',
],
}
+1
View File
@@ -2,3 +2,4 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import link_tracker
from . import utm
@@ -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')
+22
View File
@@ -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
@@ -9,7 +9,13 @@
<search string="Links">
<field name="url" string="Title and URL" filter_domain="['|', ('title', 'ilike', self), ('url', 'ilike', self)]"/>
<field name="title"/>
<field name="campaign_id"/>
<field name="medium_id"/>
<field name="source_id"/>
<group expand="0" string="Group By">
<filter string="Campaign" name="groupby_campaign_id" context="{'group_by': 'campaign_id'}"/>
<filter string="Medium" name="groupby_medium_id" context="{'group_by': 'medium_id'}"/>
<filter string="Source" name="groupby_source_id" context="{'group_by': 'source_id'}"/>
</group>
</search>
</field>
@@ -147,6 +153,19 @@
<field name="context">{'search_default_groupby_country_id': 1}</field>
</record>
<record id="link_tracker_action_campaign" model="ir.actions.act_window">
<field name="name">Statistics of Clicks</field>
<field name="res_model">link.tracker</field>
<field name="view_mode">tree,form,graph</field>
<field name="view_id" ref="link_tracker.link_tracker_view_tree"/>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Create a new link tracker
</p>
</field>
<field name="context">{'search_default_campaign_id': active_id}</field>
</record>
<!-- MENUS -->
<menuitem id="link_tracker_menu_main"
name="Link Tracker"
@@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="utm_campaign_view_form" model="ir.ui.view">
<field name="name">utm.campaign.view.form</field>
<field name="model">utm.campaign</field>
<field name="inherit_id" ref="utm.utm_campaign_view_form"/>
<field name="arch" type="xml">
<xpath expr="//div[hasclass('oe_button_box')]" position="inside">
<button name="%(link_tracker_action_campaign)d"
type="action" class="oe_stat_button order-12">
<field name="clicks_ratio" widget="percentpie" string="Clicks"/>
</button>
</xpath>
</field>
</record>
<record id="utm_campaign_view_kanban" model="ir.ui.view">
<field name="name">utm.campaign.view.form</field>
<field name="model">utm.campaign</field>
<field name="inherit_id" ref="utm.utm_campaign_view_kanban"/>
<field name="arch" type="xml">
<xpath expr="//div[@id='utm_statistics']" position="inside">
<div class="d-flex justify-content-between align-items-center w-100 order-1 mb-1">
<strong>Clicks</strong> <span><field name="clicks_ratio"/> %</span>
</div>
</xpath>
</field>
</record>
</odoo>
+1
View File
@@ -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',
@@ -1,21 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="1">
<!-- Default stages of mass mailing campaigns -->
<record id="campaign_stage_1" model="mailing.stage">
<field name="name">Schedule</field>
<field name="sequence">10</field>
</record>
<record id="campaign_stage_2" model="mailing.stage">
<field name="name">Design</field>
<field name="sequence">20</field>
</record>
<record id="campaign_stage_3" model="mailing.stage">
<field name="name">Sent</field>
<field name="sequence">30</field>
</record>
<!-- Cron that process the mass mailing queue -->
<record id="ir_cron_mass_mailing_queue" model="ir.cron">
<field name="name">Mass Mailing: Process queue</field>
+4 -12
View File
@@ -51,21 +51,14 @@
</record>
<!-- Create campaign and mailings -->
<record id="mass_mail_tag_1" model="mailing.tag">
<field name="name">Marketing</field>
<field name="color" eval="1"/>
</record>
<record id="utm_source_0" model="utm.source">
<field name="name">Newsletter 1</field>
</record>
<record id="mass_mail_campaign_1" model="mail.mass_mailing.campaign">
<record id="mass_mail_campaign_1" model="utm.campaign">
<field name="name">Newsletter</field>
<field name="stage_id" ref="mass_mailing.campaign_stage_1"/>
<field name="stage_id" ref="utm.campaign_stage_1"/>
<field name="user_id" eval="ref('base.user_admin')"/>
<field name="campaign_id" eval="ref('utm.utm_campaign_email_campaign_products')"/>
<field name="medium_id" eval="ref('utm.utm_medium_email')"/>
<field name="source_id" eval="ref('mass_mailing.utm_source_0')"/>
<field name="tag_ids" eval="[(6,0,[ref('mass_mailing.mass_mail_tag_1')])]"/>
<field name="tag_ids" eval="[(6,0,[ref('utm.utm_tag_1')])]"/>
</record>
<record id="mass_mail_1" model="mailing.mailing">
@@ -75,8 +68,7 @@
<field name="user_id" eval="ref('base.user_admin')"/>
<field name="email_from">info@yourcompany.example.com</field>
<field name="sent_date" eval="(DateTime.today() - relativedelta(days=5)).strftime('%Y-%m-%d %H:%M:%S')"/>
<field name="mass_mailing_campaign_id" eval="ref('mass_mail_campaign_1')"/>
<field name="campaign_id" ref="utm.utm_campaign_email_campaign_products"/>
<field name="campaign_id" eval="ref('mass_mail_campaign_1')"/>
<field name="source_id" ref="mass_mailing.utm_source_0"/>
<field name="mailing_model_id" ref="base.model_res_partner"/>
<field name="mailing_domain" eval="[('parent_id', '=', ref('base.res_partner_4'))]"/>
+1
View File
@@ -10,3 +10,4 @@ from . import mail_mail
from . import mail_template
from . import mail_thread
from . import res_config_settings
from . import utm
+2 -4
View File
@@ -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
+25 -179
View File
@@ -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'])
+2 -2
View File
@@ -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
+101
View File
@@ -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
@@ -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
)""")
@@ -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
access_link_tracker_mailing,access.link.tracker.mailing,link_tracker.model_link_tracker,mass_mailing.group_mass_mailing_user,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_mailing_tag access_utm_tag_mass_mailing_campaign access.mailing.tag utm.tag model_mailing_tag utm.model_utm_tag mass_mailing.group_mass_mailing_campaign 1 1 1 1
3 access_mailing_contact_mm_user access.mailing.contact.mm.user model_mailing_contact mass_mailing.group_mass_mailing_user 1 1 1 1
4 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
5 access_mailing_list_mm_user access.mailing.list.mm.user model_mailing_list mass_mailing.group_mass_mailing_user 1 1 1 1
6 access_mailing_stage access_utm_stage access.mailing.stage.mm.user utm.stage model_mailing_stage utm.model_utm_stage mass_mailing.group_mass_mailing_user 1 1 1 1
7 access_mass_mailing_campaign access_utm_campaign_mass_mailing_user mail.mass_mailing.campaign utm.campaign model_mail_mass_mailing_campaign utm.model_utm_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
8 access_mailing_mailing_mm_user access.mailing.mailing.mm.user model_mailing_mailing mass_mailing.group_mass_mailing_user 1 1 1 1
9 access_mailing_mailing_system access.mailing.mailing.system model_mailing_mailing base.group_system 1 1 1 1
10 access_mailing_trace_user mailing.trace.user model_mailing_trace base.group_user 1 1 1 1
13 access_utm_source access_utm_source utm.model_utm_source mass_mailing.group_mass_mailing_user 1 1 1 0
14 access_ir_mail_server access_ir_mail_server base.model_ir_mail_server mass_mailing.group_mass_mailing_user 1 0 0 0
15 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
16 access_link_tracker_mailing access.link.tracker.mailing link_tracker.model_link_tracker mass_mailing.group_mass_mailing_user 1 1 1 1
@@ -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);
@@ -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;
});
@@ -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;
});
@@ -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;
});
@@ -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;
}
}
}
@@ -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;
}
}
@@ -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": """
+1 -3
View File
@@ -3,15 +3,13 @@
<template id="assets_backend" name="mass_mailing assets" inherit_id="web.assets_backend">
<xpath expr="." position="inside">
<link rel="stylesheet" type="text/scss" href="/mass_mailing/static/src/scss/mass_mailing.scss"/>
<link rel="stylesheet" type="text/scss" href="/mass_mailing/static/src/scss/mass_mailing_mobile.scss"/>
<link rel="stylesheet" href="/mass_mailing/static/src/css/email_template.css"/>
<script type="text/javascript" src="/mass_mailing/static/src/js/mass_mailing.js"></script>
<script type="text/javascript" src="/mass_mailing/static/src/js/mass_mailing_widget.js"></script>
</xpath>
<xpath expr="//script[last()]" position="after">
<script type="text/javascript" src="/mass_mailing/static/src/js/mass_mailing_campaign_kanban_record.js"></script>
<script type="text/javascript" src="/mass_mailing/static/src/js/mass_mailing_campaign_kanban_renderer.js"></script>
<script type="text/javascript" src="/mass_mailing/static/src/js/mass_mailing_campaign_kanban_view.js"></script>
<script type="text/javascript" src="/mass_mailing/static/src/js/mass_mailing_list_kanban_record.js"></script>
<script type="text/javascript" src="/mass_mailing/static/src/js/mass_mailing_list_kanban_renderer.js"></script>
<script type="text/javascript" src="/mass_mailing/static/src/js/mass_mailing_list_kanban_view.js"></script>
@@ -8,11 +8,11 @@
<field name="arch" type="xml">
<xpath expr="//field[@name='title']" position="after">
<field name="mass_mailing_id"/>
<field name="mass_mailing_campaign_id"/>
<field name="campaign_id"/>
</xpath>
<xpath expr="//group" position="inside">
<filter string="Mass Mailing" name="groupby_mass_mailing_id" context="{'group_by': 'mass_mailing_id'}"/>
<filter string="Mailing Campaign" name="groupby_mass_mailing_campaign_id" domain="[]" context="{'group_by': 'mass_mailing_campaign_id'}"/>
<filter string="Mailing Campaign" name="groupby_mass_mailing_campaign_id" domain="[]" context="{'group_by': 'campaign_id'}"/>
</xpath>
</field>
</record>
@@ -24,7 +24,6 @@
<field name="arch" type="xml">
<xpath expr="//field[@name='campaign_id']" position="before">
<field name="mass_mailing_id"/>
<field name="mass_mailing_campaign_id"/>
</xpath>
</field>
</record>
@@ -62,12 +61,12 @@
<field name="inherit_id" ref="link_tracker.link_tracker_click_view_search"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='country_id']" position="after">
<field name="mass_mailing_campaign_id"/>
<field name="campaign_id"/>
<field name="mass_mailing_id"/>
</xpath>
<xpath expr="//filter[@name='groupby_country_id']" position="after">
<filter string="Mass Mailing" name="groupby_mass_mailing_id" context="{'group_by': 'mass_mailing_id'}"/>
<filter string="Mailing Campaign" name="groupby_mass_mailing_campaign_id" domain="[]" context="{'group_by': 'mass_mailing_campaign_id'}"/>
<filter string="Mailing Campaign" name="groupby_mass_mailing_campaign_id" domain="[]" context="{'group_by': 'campaign_id'}"/>
</xpath>
</field>
</record>
@@ -78,7 +77,7 @@
<field name="inherit_id" ref="link_tracker.link_tracker_click_view_form"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='country_id']" position="after">
<field name="mass_mailing_campaign_id"/>
<field name="campaign_id"/>
<field name="mass_mailing_id"/>
<field name="mailing_trace_id"/>
</xpath>
@@ -91,7 +90,7 @@
<field name="inherit_id" ref="link_tracker.link_tracker_click_view_tree"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='link_id']" position="after">
<field name="mass_mailing_campaign_id"/>
<field name="campaign_id"/>
<field name="mass_mailing_id"/>
</xpath>
</field>
@@ -103,7 +102,7 @@
<field name="inherit_id" ref="link_tracker.link_tracker_click_view_graph"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='country_id']" position="after">
<field name="mass_mailing_campaign_id"/>
<field name="campaign_id"/>
<field name="mass_mailing_id"/>
</xpath>
</field>
@@ -7,7 +7,7 @@
<field name="arch" type="xml">
<search string="Mailings">
<field name="name" string="Mailings"/>
<field name="mass_mailing_campaign_id"/>
<field name="campaign_id"/>
<filter string="My Mailings" name="assigned_to_me"
domain="[('user_id', '=', uid)]"
help="Mailings that are assigned to me"/>
@@ -36,7 +36,7 @@
<field name="delivered"/>
<field name="opened"/>
<field name="replied"/>
<field name="mass_mailing_campaign_id"
<field name="campaign_id"
groups="mass_mailing.group_mass_mailing_campaign"/>
</tree>
</field>
@@ -133,12 +133,6 @@
class="oe_stat_button">
<field name="replied_ratio" string="Replied" widget="percentpie"/>
</button>
<button name="action_view_clicked"
type="object"
attrs="{'invisible': [('state', 'in', ('draft','test'))]}"
class="oe_stat_button">
<field name="clicks_ratio" string="Clicks" widget="percentpie"/>
</button>
<button name="action_view_bounced"
type="object"
context="{'search_default_filter_bounced': True}"
@@ -150,8 +144,8 @@
<group>
<field name="active" invisible="1"/>
<field name="name" required="1" string="Mailing Name"/>
<field name="subject" string="Subject"/>
<field name="name" required="1" string="Mailing Name"/>
<label for="mailing_model_id" string="Recipients"/>
<div>
<field name="mailing_model_id" widget="selection" required="1"/>
@@ -183,6 +177,7 @@
<group>
<group string="Mailing">
<field name="user_id"/>
<field name="unique_ab_testing" groups="base.group_no_one"/>
<field name="email_from"/>
<label for="reply_to"/>
<div>
@@ -202,20 +197,18 @@
<field name="sent_date" readonly="1" attrs="{'invisible': [('sent_date', '=', False)]}"/>
</group>
<group string="Marketing" groups="base.group_no_one,mass_mailing.group_mass_mailing_campaign">
<field name="mass_mailing_campaign_id"
<field name="campaign_id"
string="Mailing Campaign"
groups="mass_mailing.group_mass_mailing_campaign"/>
<field name="campaign_id"
string="UTM Campaign"
groups="base.group_no_one"/>
<field name="source_id"
readonly="1"
string="Source"
readonly="1"
required="False"
groups="base.group_no_one"/>
<field name="medium_id"
string="Medium"
groups="base.group_no_one"/>
required="True"
groups="base.group_no_one"/>
<label for="contact_ab_pc" groups="mass_mailing.group_mass_mailing_campaign"/>
<div groups="mass_mailing.group_mass_mailing_campaign">
<field name="contact_ab_pc" class="oe_inline"/> %
@@ -264,8 +257,8 @@
<div class="o_kanban_record_headings">
<h3 class="mt4 mb4"><field name="name"/></h3>
<div class="o_kanban_record_subtitle" attrs="{'invisible': [('sent_date', '=', False)]}">
<h5 style="display: inline;"><field name="mass_mailing_campaign_id" groups="mass_mailing.group_mass_mailing_campaign"/></h5>
<t t-if="record.mass_mailing_campaign_id.raw_value" groups="mass_mailing.group_mass_mailing_campaign"> - </t><field name="sent_date"/>
<h5 style="display: inline;"><field name="campaign_id" groups="mass_mailing.group_mass_mailing_campaign"/></h5>
<t t-if="record.campaign_id.raw_value" groups="mass_mailing.group_mass_mailing_campaign"> - </t><field name="sent_date"/>
</div>
</div>
</div>
@@ -348,8 +341,8 @@
<field name="view_mode">kanban,tree,form</field>
<field name="context">{
'search_default_assigned_to_me': 1,
'search_default_mass_mailing_campaign_id': [active_id],
'default_mass_mailing_campaign_id': active_id,
'search_default_campaign_id': [active_id],
'default_campaign_id': active_id,
'default_user_id': uid,
}
</field>
@@ -369,8 +362,8 @@
<field name="view_mode">form,kanban,tree</field>
<field name="context">{
'search_default_assigned_to_me': 1,
'search_default_mass_mailing_campaign_id': [active_id],
'default_mass_mailing_campaign_id': active_id,
'search_default_campaign_id': [active_id],
'default_campaign_id': active_id,
'default_user_id': uid,
}
</field>
@@ -390,8 +383,8 @@
<field name="view_mode">form,kanban,tree</field>
<field name="context">{
'search_default_assigned_to_me': 1,
'search_default_mass_mailing_campaign_id': [active_id],
'default_mass_mailing_campaign_id': active_id,
'search_default_campaign_id': [active_id],
'default_campaign_id': active_id,
'default_contact_ab_pc': 10,
'default_user_id': uid,
}
@@ -409,258 +402,4 @@
<menuitem name="Mailings" id="mass_mailing_menu" parent="mass_mailing_menu_root"
sequence="1" action="action_view_mass_mailings" groups="mass_mailing.group_mass_mailing_user"/>
<!-- MAILING CAMPAIGN STAGE !-->
<record model="ir.ui.view" id="view_mail_mass_mailing_stage_search">
<field name="name">mailing.stage.search</field>
<field name="model">mailing.stage</field>
<field name="arch" type="xml">
<search string="Mailings">
<field name="name"/>
</search>
</field>
</record>
<record model="ir.ui.view" id="view_mail_mass_mailing_stage_tree">
<field name="name">mailing.stage.tree</field>
<field name="model">mailing.stage</field>
<field name="priority">10</field>
<field name="arch" type="xml">
<tree string="Mailings" editable="top">
<field name="sequence" widget="handle"/>
<field name="name"/>
</tree>
</field>
</record>
<record id="action_view_mass_mailing_stages" model="ir.actions.act_window">
<field name="name">Mailing Stages</field>
<field name="res_model">mailing.stage</field>
<field name="view_mode">tree,form</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Create a new mass mailing stage
</p>
</field>
</record>
<menuitem name="Campaign Stages" id="menu_view_mass_mailing_stages"
parent="mass_mailing_configuration" sequence="1"
groups="mass_mailing.group_mass_mailing_campaign"
action="action_view_mass_mailing_stages"/>
<!-- MAILING CAMPAIGN TAGS !-->
<record id="mail_mass_mailing_tag_view_form" model="ir.ui.view">
<field name="name">mailing.tag.form</field>
<field name="model">mailing.tag</field>
<field name="arch" type="xml">
<form string="Campaign Tags">
<sheet>
<group>
<field name="name"/>
</group>
</sheet>
</form>
</field>
</record>
<record id="mass_mailing_tag_action" model="ir.actions.act_window">
<field name="name">Campaign Tags</field>
<field name="res_model">mailing.tag</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Add a new tag
</p>
</field>
</record>
<menuitem id="mass_mailing_tag_menu" parent="mass_mailing_configuration" action="mass_mailing_tag_action" sequence="2" groups="base.group_no_one"/>
<!-- MAILING CAMPAIGNS !-->
<record model="ir.ui.view" id="view_mail_mass_mailing_campaign_search">
<field name="name">mail.mass_mailing.campaign.search</field>
<field name="model">mail.mass_mailing.campaign</field>
<field name="arch" type="xml">
<search string="Mailing Campaigns">
<field name="name" string="Campaigns"/>
<field name="tag_ids"/>
<field name="user_id"/>
<group expand="0" string="Group By">
<filter string="Stage" name="group_stage_id"
context="{'group_by': 'stage_id'}"/>
<filter string="Responsible" name="group_user_id"
context="{'group_by': 'user_id'}"/>
</group>
</search>
</field>
</record>
<record model="ir.ui.view" id="view_mail_mass_mailing_campaign_tree">
<field name="name">mail.mass_mailing.campaign.tree</field>
<field name="model">mail.mass_mailing.campaign</field>
<field name="priority">10</field>
<field name="arch" type="xml">
<tree string="Mailing Campaigns">
<field name="name"/>
<field name="user_id"/>
<field name="stage_id"/>
<field name="tag_ids"/>
</tree>
</field>
</record>
<record model="ir.ui.view" id="view_mail_mass_mailing_campaign_form">
<field name="name">mail.mass_mailing.campaign.form</field>
<field name="model">mail.mass_mailing.campaign</field>
<field name="arch" type="xml">
<form string="Mailing Campaign">
<header style="min-height:31px;">
<button name="%(action_create_mass_mailings_from_campaign)d" type="action" class="oe_highlight" string="Send new Mailing"/>
<button name="%(action_create_ab_mass_mailings_from_campaign)d" type="action" class="oe_highlight" string="Send new A/B Testing Mailing"/>
<span><field name="stage_id" widget="statusbar" options="{'clickable': '1'}"/></span>
</header>
<sheet>
<field name="total" invisible="1"/>
<div class="oe_button_box" name="button_box"
attrs="{'invisible': [('total', '=', 0)]}">
<button name="%(action_view_mass_mailings_from_campaign)d"
type="action" class="oe_stat_button" icon="fa-envelope-o">
<field name="total_mailings" widget="statinfo" string="Mailings"/>
</button>
<button name="%(action_view_mail_mail_statistics_mailing)d" type="action"
icon="fa-envelope-o" class="oe_stat_button"
attrs="{'invisible': [('sent', '=', 0)]}" >
<field name="sent" string="Emails Sent" widget="statinfo"/>
</button>
<button name="%(action_view_mass_mailing_contacts)d"
type="action" class="oe_stat_button">
<field name="received_ratio" widget="percentpie" string="Delivered"/>
</button>
<button name="%(action_view_mass_mailing_contacts)d"
type="action" class="oe_stat_button">
<field name="opened_ratio" widget="percentpie" string="Opened"/>
</button>
<button name="%(action_view_mass_mailing_contacts)d"
type="action" class="oe_stat_button">
<field name="replied_ratio" widget="percentpie" string="Replied"/>
</button>
<button name="%(link_tracker_action_mass_mailing_campaign)d"
type="action" class="oe_stat_button">
<field name="clicks_ratio" widget="percentpie" string="Clicks"/>
</button>
</div>
<group>
<field name="name" string="Campaign Name"/>
<field name="user_id"/>
<field name="tag_ids" widget="many2many_tags" options="{'color_field': 'color', 'no_create_edit': True}"/>
<field name="unique_ab_testing" groups="base.group_no_one"/>
</group>
<notebook>
<page string="Tracking">
<group>
<field name="medium_id" />
<field name="source_id" />
</group>
</page>
<page string="Related Mailings" groups="base.group_no_one">
<field name="mass_mailing_ids" readonly="1" string="Related Mailing(s)">
<tree>
<field name="name"/>
<field name="sent_date"/>
<field name="state"/>
<field name="delivered"/>
<field name="opened"/>
<field name="replied"/>
<field name="bounced"/>
<button name="action_duplicate" type="object" string="Duplicate"/>
</tree>
</field>
</page>
</notebook>
</sheet>
</form>
</field>
</record>
<record model="ir.ui.view" id="view_mail_mass_mailing_campaign_kanban">
<field name="name">mail.mass_mailing.campaign.kanban</field>
<field name="model">mail.mass_mailing.campaign</field>
<field name="arch" type="xml">
<kanban js_class="mass_mailing_campaign_kanban" default_group_by='stage_id'>
<field name='total'/>
<field name='color'/>
<field name='user_id'/>
<field name='mass_mailing_ids'/>
<field name="stage_id"/>
<templates>
<t t-name="kanban-box">
<div t-attf-class="oe_kanban_color_#{kanban_getcolor(record.color.raw_value)} oe_kanban_card oe_kanban_global_click oe_kanban_mass_mailing_campaign">
<div class="o_dropdown_kanban dropdown">
<a role="button" class="dropdown-toggle o-no-caret btn" data-toggle="dropdown" href="#" aria-label="Dropdown menu" title="Dropdown menu">
<span class="fa fa-ellipsis-v"/>
</a>
<div class="dropdown-menu" role="menu">
<t t-if="widget.editable">
<a role="menuitem" type="edit" class="dropdown-item">Settings</a>
</t>
<t t-if="widget.deletable">
<a role="menuitem" type="delete" class="dropdown-item">Delete</a>
</t>
<div role="separator" class="dropdown-divider"/>
<div role="separator" class="dropdown-header">Colour</div>
<ul class="oe_kanban_colorpicker" data-field="color"/>
</div>
</div>
<div class="oe_kanban_content">
<div class="o_kanban_record_top mb16">
<div class="o_kanban_record_headings">
<h3 class="oe_margin_bottom_8 o_kanban_record_title"><field name="name"/></h3>
<a name="%(action_view_mass_mailings_from_campaign)d" type="action"
class="oe_mailings o_kanban_record_subtitle">
<t t-raw="record.mass_mailing_ids.raw_value.length"/> Mailings
</a>
</div>
</div>
<div class="o_kanban_record_body">
<field name="tag_ids" widget="many2many_tags" options="{'color_field': 'color'}"/>
</div>
<div class="o_kanban_record_bottom mt16" style="height:90px">
<div class="oe_kanban_bottom_left" style="height:90px">
<field name="delivered" widget="gauge" options="{'max_field': 'total', 'style': 'width:120px; height: 90px;'}"/>
<div>
<strong>Opened</strong> <field name="opened_ratio"/> %<br />
<strong>Replied</strong> <field name="replied_ratio"/> % <br />
<strong>Clicks</strong> <field name="clicks_ratio"/> % <br />
</div>
</div>
<div class="oe_kanban_bottom_right" style="height:90px">
<img t-att-src="kanban_image('res.users', 'image_small', record.user_id.raw_value)" t-att-title="record.user_id.value" t-att-alt="record.user_id.value" width="30" height="30" class="oe_kanban_avatar"/>
</div>
</div>
</div>
<div class="oe_clear"></div>
</div>
</t>
</templates>
</kanban>
</field>
</record>
<record id="action_view_mass_mailing_campaigns" model="ir.actions.act_window">
<field name="name">Mailing Campaigns</field>
<field name="res_model">mail.mass_mailing.campaign</field>
<field name="view_mode">kanban,tree,form</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Create a new mailing campaign
</p><p>
Create a campaign to structure mailing and get analysis from email status.
</p>
</field>
</record>
<menuitem name="Campaigns" id="menu_email_campaigns"
parent="mass_mailing_menu_root" sequence="5"
action="action_view_mass_mailing_campaigns"
groups="mass_mailing.group_mass_mailing_campaign"/>
</odoo>
@@ -65,7 +65,7 @@
<group>
<field name="email" string="Email"/>
<field name="mass_mailing_id"/>
<field name="mass_mailing_campaign_id" groups="mass_mailing.group_mass_mailing_campaign"/>
<field name="campaign_id" groups="mass_mailing.group_mass_mailing_campaign"/>
<field name="mail_mail_id_int" groups="base.group_no_one"/>
<field name="message_id" groups="base.group_no_one"/>
<field name="model" groups="base.group_no_one"/>
@@ -21,7 +21,7 @@
</div>
<div class="content-group" attrs="{'invisible': [('group_mass_mailing_campaign', '=', False)]}">
<div class="mt16">
<button name="%(mass_mailing.action_view_mass_mailing_stages)d"
<button name="%(utm.action_view_utm_stage)d"
icon="fa-arrow-right" type="action" string="Campaign Stages"
class="btn-link"/>
</div>
@@ -0,0 +1,126 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record model="ir.ui.view" id="utm_campaign_view_form">
<field name="name">utm.campaign.view.form</field>
<field name="model">utm.campaign</field>
<field name="inherit_id" ref="utm.utm_campaign_view_form"/>
<field name="arch" type="xml">
<xpath expr="//sheet" position="before">
<header style="min-height:31px;">
<button name="%(action_create_mass_mailings_from_campaign)d" type="action" class="oe_highlight" string="Send new Mailing"/>
<button name="%(action_create_ab_mass_mailings_from_campaign)d" type="action" class="oe_highlight" string="Send new A/B Testing Mailing"/>
<span><field name="stage_id" widget="statusbar" options="{'clickable': '1'}"/></span>
</header>
</xpath>
<xpath expr="//div[hasclass('oe_button_box')]" position="inside">
<button name="%(action_view_mass_mailings_from_campaign)d"
type="action" class="oe_stat_button order-11" icon="fa-envelope-o">
<field name="mailing_items" widget="statinfo" string="Mailings"/>
</button>
</xpath>
<xpath expr="//notebook" position="inside">
<page string="Mailings" attrs="{}">
<group groups="base.group_no_one">
<field name="mailing_ids" readonly="1" nolabel="1">
<tree>
<field name="name"/>
<field name="sent_date"/>
<field name="state"/>
<field name="delivered"/>
<field name="opened"/>
<field name="replied"/>
<field name="bounced"/>
<button name="action_duplicate" type="object" string="Duplicate"/>
</tree>
</field>
</group>
<div class="o_utm_campaign_mass_mailing_substats d-flex justify-content-end align-items-center">
<div class="d-flex justify-content-end align-items-center flex-column">
<label for="received_ratio" string="Delivered" class="m-0"/>
<div class="m-0">
<span class="text-right">
<field name="received_ratio"/>
<span>%</span>
</span>
</div>
</div>
<div class="d-flex justify-content-end align-items-center flex-column">
<label for="opened_ratio" string="Opened" class="m-0"/>
<div class="m-0">
<span class="text-right">
<field name="opened_ratio"/>
<span>%</span>
</span>
</div>
</div>
<div class="d-flex justify-content-end align-items-center flex-column">
<label for="replied_ratio" string="Replied" class="m-0"/>
<div class="m-0">
<span class="text-right">
<field name="replied_ratio"/>
<span>%</span>
</span>
</div>
</div>
<div class="d-flex justify-content-end align-items-center flex-column">
<label for="bounced_ratio" string="Bounced" class="m-0"/>
<div class="m-0">
<span class="text-right">
<field name="bounced_ratio"/>
<span>%</span>
</span>
</div>
</div>
</div>
</page>
</xpath>
</field>
</record>
<record model="ir.ui.view" id="utm_campaign_view_kanban">
<field name="name">utm.campaign.view.kanban</field>
<field name="model">utm.campaign</field>
<field name="inherit_id" ref="utm.utm_campaign_view_kanban"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='user_id']" position="after">
<field name="mailing_ids"/>
</xpath>
<xpath expr="//div[hasclass('o_kanban_record_headings')]">
<a name="%(action_view_mass_mailings_from_campaign)d" type="action"
class="oe_mailings o_kanban_record_subtitle d-inline-block">
<t t-raw="record.mailing_ids.raw_value.length"/> Mailings
</a>
</xpath>
</field>
</record>
<record id="action_view_utm_campaigns" model="ir.actions.act_window">
<field name="name">UTM Campaigns</field>
<field name="res_model">utm.campaign</field>
<field name="view_mode">kanban,tree,form</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Create a new UTM campaign
</p><p>
Create a campaign to structure mailing and get analysis from email status.
</p>
</field>
<field name="domain">[('is_website', '=', False)]</field>
</record>
<menuitem name="Campaigns" id="menu_email_campaigns"
parent="mass_mailing_menu_root" sequence="5"
action="action_view_utm_campaigns"
groups="mass_mailing.group_mass_mailing_campaign"/>
<menuitem name="Campaign Stages" id="menu_view_mass_mailing_stages"
parent="mass_mailing_configuration" sequence="1"
groups="mass_mailing.group_mass_mailing_campaign"
action="utm.action_view_utm_stage"/>
<menuitem id="mass_mailing_tag_menu"
parent="mass_mailing_configuration"
action="utm.action_view_utm_tag"
sequence="2"
groups="base.group_no_one"/>
</odoo>
@@ -8,8 +8,8 @@ from odoo.tools import email_re
class MailComposeMessage(models.TransientModel):
_inherit = 'mail.compose.message'
mass_mailing_campaign_id = fields.Many2one('mail.mass_mailing.campaign', string='Mass Mailing Campaign')
mass_mailing_id = fields.Many2one('mailing.mailing', string='Mass Mailing', ondelete='cascade')
campaign_id = fields.Many2one('utm.campaign', string='Mass Mailing Campaign')
mass_mailing_name = fields.Char(string='Mass Mailing Name')
mailing_list_ids = fields.Many2many('mailing.list', string='Mailing List')
@@ -28,7 +28,7 @@ class MailComposeMessage(models.TransientModel):
reply_to_mode = 'email' if self.no_auto_thread else 'thread'
reply_to = self.reply_to if self.no_auto_thread else False
mass_mailing = self.env['mailing.mailing'].create({
'mass_mailing_campaign_id': self.mass_mailing_campaign_id.id,
'campaign_id': self.campaign_id.id,
'name': self.mass_mailing_name,
'template_id': self.template_id.id,
'state': 'done',
@@ -8,7 +8,7 @@
<field name="inherit_id" ref="mail.email_compose_message_wizard_form"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='notify']" position="after">
<field name="mass_mailing_campaign_id" groups="mass_mailing.group_mass_mailing_campaign"
<field name="campaign_id" groups="mass_mailing.group_mass_mailing_campaign"
attrs="{'invisible': [('composition_mode', '!=', 'mass_mail')]}"/>
<field name="mass_mailing_name"
attrs="{'invisible': [('composition_mode', '!=', 'mass_mail')]}"/>
@@ -7,8 +7,7 @@
<field name="state">in_queue</field>
<field name="user_id" ref="base.user_admin"/>
<field name="schedule_date" eval="(DateTime.today() + relativedelta(days=5)).strftime('%Y-%m-%d %H:%M:%S')"/>
<field name="mass_mailing_campaign_id" eval="ref('mass_mailing.mass_mail_campaign_1')"/>
<field name="campaign_id" ref="utm.utm_campaign_email_campaign_products"/>
<field name="campaign_id" eval="ref('mass_mailing.mass_mail_campaign_1')"/>
<field name="source_id" ref="utm.utm_source_mailing"/>
<field name="mailing_model_id" ref="crm.model_crm_lead"/>
<field name="mailing_domain">[]</field>
@@ -7,8 +7,7 @@
<field name="state">in_queue</field>
<field name="user_id" ref="base.user_admin"/>
<field name="schedule_date" eval="(DateTime.today() + relativedelta(days=5)).strftime('%Y-%m-%d %H:%M:%S')"/>
<field name="mass_mailing_campaign_id" eval="ref('mass_mailing.mass_mail_campaign_1')"/>
<field name="campaign_id" ref="utm.utm_campaign_email_campaign_products"/>
<field name="campaign_id" eval="ref('mass_mailing.mass_mail_campaign_1')"/>
<field name="source_id" ref="sale.utm_source_sale_order_0"/>
<field name="mailing_model_id" ref="sale.model_sale_order"/>
<field name="mailing_domain">[]</field>
@@ -36,17 +36,21 @@ class MassMailing(models.Model):
def action_redirect_to_quotations(self):
action = self.env.ref('sale.action_quotations_with_onboarding').read()[0]
action['domain'] = self._get_sale_utm_domain()
action['context'] = {'default_type': 'lead', 'create': False}
action['context'] = {'create': False}
return action
def action_redirect_to_invoiced(self):
action = self.env.ref('account.view_move_form').read()[0]
action = self.env.ref('account.action_move_journal_line').read()[0]
invoices = self.env['sale.order'].search(self._get_sale_utm_domain()).mapped('invoice_ids')
action['context'] = {
'create': False,
'edit': False,
'view_no_maturity': True
}
action['domain'] = [
('id', 'in', invoices.ids),
('type', 'in', ('out_invoice', 'out_refund')),
('type', '=', 'posted'),
('partner_id', 'child_of', self.id),
('type', 'in', ('out_invoice', 'out_refund', 'in_invoice', 'in_refund', 'out_receipt', 'in_receipt')),
('state', 'not in', ['draft', 'cancel'])
]
action['context'] = {'create': False}
return action
+1
View File
@@ -33,6 +33,7 @@ This module contains all the common features of Sales Management and eCommerce.
'views/res_config_settings_views.xml',
'views/payment_views.xml',
'views/product_attribute_views.xml',
'views/utm_campaign_views.xml'
],
'demo': [
'data/sale_demo.xml',
+1
View File
@@ -13,3 +13,4 @@ from . import sale
from . import res_config_settings
from . import sales_team
from . import payment
from . import utm
+61
View File
@@ -0,0 +1,61 @@
# -*- 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'
quotation_count = fields.Integer('Quotation Count', groups='sales_team.group_sale_salesman', compute="_compute_quotation_count")
invoiced_amount = fields.Integer(default=0, compute="_compute_sale_invoiced_amount", string="Revenues generated by the campaign")
company_id = fields.Many2one('res.company', string='Company', readonly=True, states={'draft': [('readonly', False)], 'refused': [('readonly', False)]}, default=lambda self: self.env.company)
currency_id = fields.Many2one('res.currency', related='company_id.currency_id', string='Currency')
def _compute_quotation_count(self):
quotation_data = self.env['sale.order'].read_group([
('campaign_id', 'in', self.ids)],
['campaign_id'], ['campaign_id'])
data_map = {datum['campaign_id'][0]: datum['campaign_id_count'] for datum in quotation_data}
for campaign in self:
campaign.quotation_count = data_map.get(campaign.id, 0)
def _compute_sale_invoiced_amount(self):
query = """SELECT move.campaign_id, -SUM(line.balance) as price_subtotal
FROM account_move_line line
INNER JOIN account_move move ON line.move_id = move.id
WHERE move.state not in ('draft', 'cancel')
AND move.campaign_id IN %s
AND move.type IN ('out_invoice', 'out_refund', 'in_invoice', 'in_refund', 'out_receipt', 'in_receipt')
AND line.account_id IS NOT NULL
AND NOT line.exclude_from_invoice_tab
GROUP BY move.campaign_id
"""
self._cr.execute(query, [tuple(self.ids)])
query_res = self._cr.dictfetchall()
for datum in query_res:
campaign = self.browse(datum['campaign_id'])
campaign.invoiced_amount = datum['price_subtotal']
def action_redirect_to_quotations(self):
action = self.env.ref('sale.action_quotations_with_onboarding').read()[0]
action['domain'] = [('campaign_id', '=', self.id)]
action['context'] = {'default_campaign_id': self.id}
return action
def action_redirect_to_invoiced(self):
action = self.env.ref('account.action_move_journal_line').read()[0]
invoices = self.env['account.move'].search([('campaign_id', '=', self.id)])
action['context'] = {
'create': False,
'edit': False,
'view_no_maturity': True
}
action['domain'] = [
('id', 'in', invoices.ids),
('type', 'in', ('out_invoice', 'out_refund', 'in_invoice', 'in_refund', 'out_receipt', 'in_receipt')),
('state', 'not in', ['draft', 'cancel'])
]
return action
+1
View File
@@ -701,6 +701,7 @@
<field name="inherit_id" ref="sale.view_sales_order_filter"/>
<field name="arch" type="xml">
<xpath expr="//filter[@name='my_sale_orders_filter']" position="replace">
<field name="campaign_id"/>
<separator/>
<filter string="My Quotations" name="my_quotation" domain="[('user_id', '=', uid)]"/>
<separator/>
+37
View File
@@ -0,0 +1,37 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record model="ir.ui.view" id="utm_campaign_view_kanban">
<field name="name">utm.campaign.view.kanban</field>
<field name="model">utm.campaign</field>
<field name="inherit_id" ref="utm.utm_campaign_view_kanban"/>
<field name="arch" type="xml">
<xpath expr="//div[@id='utm_statistics']" position="inside">
<div class="d-flex justify-content-between align-items-center w-100 order-2 mb-1">
<field name="currency_id" invisible="True"/>
<strong>Revenues</strong> <span><field name="invoiced_amount" widget="monetary" options="{'currency_field': 'currency_id'}"/></span>
</div>
<div class="d-flex justify-content-between align-items-center w-100 order-4 mb-1">
<strong>Quotations</strong> <span><field name="quotation_count"/></span>
</div>
</xpath>
</field>
</record>
<record model="ir.ui.view" id="utm_campaign_view_form">
<field name="name">utm.campaign.view.form</field>
<field name="model">utm.campaign</field>
<field name="inherit_id" ref="utm.utm_campaign_view_form"/>
<field name="arch" type="xml">
<xpath expr="//div[hasclass('oe_button_box')]" position="inside">
<button name="action_redirect_to_invoiced"
type="object" class="oe_stat_button order-1" icon="fa-money">
<field name="invoiced_amount" widget="statinfo" string="Revenues"/>
</button>
<button name="action_redirect_to_quotations"
type="object" class="oe_stat_button order-2" icon="fa-usd">
<field name="quotation_count" widget="statinfo" string="Quotations"/>
</button>
</xpath>
</field>
</record>
</odoo>
@@ -50,7 +50,7 @@ class TestMassMailPerformance(TestMassMailPerformanceBase):
'mailing_domain': [('id', 'in', self.mm_recs.ids)],
})
with self.assertQueryCount(__system__=2477, marketing=3128):
with self.assertQueryCount(__system__=2478, marketing=3128):
mailing.action_send_mail()
self.assertEqual(mailing.sent, 50)
@@ -88,7 +88,7 @@ class TestMassMailBlPerformance(TestMassMailPerformanceBase):
'mailing_domain': [('id', 'in', self.mm_recs.ids)],
})
with self.assertQueryCount(__system__=2864, marketing=3611):
with self.assertQueryCount(__system__=2865, marketing=3611):
mailing.action_send_mail()
self.assertEqual(mailing.sent, 50)
+1
View File
@@ -11,6 +11,7 @@ Enable management of UTM trackers: campaign, medium, source.
'depends': ['base'],
'data': [
'data/utm_data.xml',
'views/utm_campaign_views.xml',
'views/utm_views.xml',
'security/ir.model.access.csv',
],
+32
View File
@@ -1,17 +1,43 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- UTM Stage -->
<record id="default_utm_stage" model="utm.stage">
<field name="name">Default</field>
<field name="sequence">10</field>
</record>
<record id="campaign_stage_1" model="utm.stage">
<field name="name">Schedule</field>
<field name="sequence">10</field>
</record>
<record id="campaign_stage_2" model="utm.stage">
<field name="name">Design</field>
<field name="sequence">20</field>
</record>
<record id="campaign_stage_3" model="utm.stage">
<field name="name">Sent</field>
<field name="sequence">30</field>
</record>
<!-- UTM Campain -->
<record model="utm.campaign" id="utm_campaign_fall_drive">
<field name="name">Sale</field>
<field name="stage_id" ref="utm.default_utm_stage"/>
<field name="is_website" eval="True"/>
</record>
<record model="utm.campaign" id="utm_campaign_christmas_special">
<field name="name">Christmas Special</field>
<field name="stage_id" ref="utm.default_utm_stage"/>
<field name="is_website" eval="True"/>
</record>
<record model="utm.campaign" id="utm_campaign_email_campaign_services">
<field name="name">Email Campaign - Services</field>
<field name="stage_id" ref="utm.default_utm_stage"/>
<field name="is_website" eval="True"/>
</record>
<record model="utm.campaign" id="utm_campaign_email_campaign_products">
<field name="name">Email Campaign - Products</field>
<field name="stage_id" ref="utm.default_utm_stage"/>
<field name="is_website" eval="True"/>
</record>
<!-- UTM Source -->
@@ -62,4 +88,10 @@
<record model="utm.medium" id="utm_medium_google_adwords">
<field name="name">Google Adwords</field>
</record>
<!-- UTM Tag -->
<record id="utm_tag_1" model="utm.tag">
<field name="name">Marketing</field>
<field name="color" eval="1"/>
</record>
</odoo>
+44 -1
View File
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, models
from odoo import fields, models, api, SUPERUSER_ID
class UtmMedium(models.Model):
@@ -21,9 +21,52 @@ class UtmCampaign(models.Model):
name = fields.Char(string='Campaign Name', required=True, translate=True)
user_id = fields.Many2one(
'res.users', string='Responsible',
required=True, default=lambda self: self.env.uid)
stage_id = fields.Many2one('utm.stage', string='Stage', ondelete='restrict', required=True,
default=lambda self: self.env['utm.stage'].search([], limit=1),
group_expand='_group_expand_stage_ids')
tag_ids = fields.Many2many(
'utm.tag', 'utm_tag_rel',
'tag_id', 'campaign_id', string='Tags')
is_website = fields.Boolean(default=False, help="Allows us to filter relevant Campaign")
color = fields.Integer(string='Color Index')
@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 UtmSource(models.Model):
_name = 'utm.source'
_description = 'UTM Source'
name = fields.Char(string='Source Name', required=True, translate=True)
class UtmStage(models.Model):
"""Stage for utm campaigns. """
_name = 'utm.stage'
_description = 'Campaign Stage'
_order = 'sequence'
name = fields.Char(required=True, translate=True)
sequence = fields.Integer()
class UtmTag(models.Model):
"""Model of categories of utm campaigns, i.e. marketing, newsletter, ... """
_name = 'utm.tag'
_description = 'UTM 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 !"),
]
+1 -1
View File
@@ -37,7 +37,7 @@ class UtmMixin(models.AbstractModel):
Model = self.env[field.comodel_name]
records = Model.search([('name', '=', value)], limit=1)
if not records:
records = Model.create({'name': value})
records = Model.create({'name': value, 'is_website': True})
value = records.id
if value:
values[field_name] = value
+5
View File
@@ -1,7 +1,12 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_utm_campaign_user,access_utm_campaign_user,model_utm_campaign,base.group_user,1,1,1,0
access_utm_campaign_system,utm.campaign.system,model_utm_campaign,base.group_system,1,1,1,1
access_utm_medium_user,access_utm_medium_user,model_utm_medium,base.group_user,1,1,1,0
access_utm_source_user,access_utm_source_user,model_utm_source,base.group_user,1,1,1,0
access_utm_stage_user,mail.utm.stage,model_utm_stage,base.group_user,1,0,0,0
access_utm_stage_system,mail.utm.stage,model_utm_stage,base.group_system,1,1,1,1
access_utm_campaign,access_utm_campaign,model_utm_campaign,,1,0,1,0
access_utm_medium,access_utm_medium,model_utm_medium,,1,0,1,0
access_utm_source,access_utm_source,model_utm_source,,1,0,1,0
access_utm_tag_user,utm.tag,model_utm_tag,base.group_user,1,0,0,0
access_utm_tag_system,utm.tag,model_utm_tag,base.group_system,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_utm_campaign_user access_utm_campaign_user model_utm_campaign base.group_user 1 1 1 0
3 access_utm_campaign_system utm.campaign.system model_utm_campaign base.group_system 1 1 1 1
4 access_utm_medium_user access_utm_medium_user model_utm_medium base.group_user 1 1 1 0
5 access_utm_source_user access_utm_source_user model_utm_source base.group_user 1 1 1 0
6 access_utm_stage_user mail.utm.stage model_utm_stage base.group_user 1 0 0 0
7 access_utm_stage_system mail.utm.stage model_utm_stage base.group_system 1 1 1 1
8 access_utm_campaign access_utm_campaign model_utm_campaign 1 0 1 0
9 access_utm_medium access_utm_medium model_utm_medium 1 0 1 0
10 access_utm_source access_utm_source model_utm_source 1 0 1 0
11 access_utm_tag_user utm.tag model_utm_tag base.group_user 1 0 0 0
12 access_utm_tag_system utm.tag model_utm_tag base.group_system 1 1 1 1
+160
View File
@@ -0,0 +1,160 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record model="ir.ui.view" id="view_utm_campaign_view_search">
<field name="name">utm.campaign.view.search</field>
<field name="model">utm.campaign</field>
<field name="arch" type="xml">
<search string="UTM Campaigns">
<field name="name" string="Campaigns"/>
<field name="tag_ids"/>
<field name="user_id"/>
<group expand="0" string="Group By">
<filter string="Stage" name="group_stage_id"
context="{'group_by': 'stage_id'}"/>
<filter string="Responsible" name="group_user_id"
context="{'group_by': 'user_id'}"/>
</group>
</search>
</field>
</record>
<record model="ir.ui.view" id="utm_campaign_view_form">
<field name="name">utm.campaign.view.form</field>
<field name="model">utm.campaign</field>
<field name="arch" type="xml">
<form string="UTM Campaign">
<sheet>
<div class="oe_button_box d-flex justify-content-end" name="button_box">
</div>
<group id="top-group">
<field name="name" string="Campaign Name"/>
<field name="user_id"/>
<field name="tag_ids" widget="many2many_tags" options="{'color_field': 'color', 'no_create_edit': True}"/>
</group>
<notebook>
</notebook>
</sheet>
</form>
</field>
</record>
<record model="ir.ui.view" id="utm_campaign_view_tree">
<field name="name">utm.campaign.view.tree</field>
<field name="model">utm.campaign</field>
<field name="arch" type="xml">
<tree string="UTM Campaigns">
<field name="name"/>
<field name="user_id"/>
<field name="stage_id"/>
<field name="tag_ids"/>
</tree>
</field>
</record>
<record model="ir.ui.view" id="utm_campaign_view_kanban">
<field name="name">utm.campaign.view.kanban</field>
<field name="model">utm.campaign</field>
<field name="arch" type="xml">
<kanban default_group_by='stage_id'>
<field name='color'/>
<field name='user_id'/>
<field name="stage_id"/>
<templates>
<t t-name="kanban-box">
<div t-attf-class="oe_kanban_color_#{kanban_getcolor(record.color.raw_value)} oe_kanban_card oe_kanban_global_click">
<div class="o_dropdown_kanban dropdown">
<a role="button" class="dropdown-toggle o-no-caret btn" data-toggle="dropdown" href="#" aria-label="Dropdown menu" title="Dropdown menu">
<span class="fa fa-ellipsis-v"/>
</a>
<div class="dropdown-menu" role="menu">
<t t-if="widget.editable">
<a role="menuitem" type="edit" class="dropdown-item">Settings</a>
</t>
<t t-if="widget.deletable">
<a role="menuitem" type="delete" class="dropdown-item">Delete</a>
</t>
<div role="separator" class="dropdown-divider"/>
<div role="separator" class="dropdown-header">Colour</div>
<ul class="oe_kanban_colorpicker" data-field="color"/>
</div>
</div>
<div class="oe_kanban_content">
<div class="o_kanban_record_top mb16">
<div class="o_kanban_record_headings">
<h3 class="oe_margin_bottom_8 o_kanban_record_title"><field name="name"/></h3>
</div>
</div>
<div class="o_kanban_record_body">
<field name="tag_ids" widget="many2many_tags" options="{'color_field': 'color'}"/>
</div>
<div class="o_kanban_record_bottom mt16" style="height:90px">
<div id="utm_statistics" class="d-flex flex-column justify-content-start align-items-start w-100"/>
</div>
</div>
<div class="oe_clear"></div>
</div>
</t>
</templates>
</kanban>
</field>
</record>
<!-- CAMPAIGN TAGS !-->
<record id="utm_tag_view_form" model="ir.ui.view">
<field name="name">utm.tag.view.form</field>
<field name="model">utm.tag</field>
<field name="arch" type="xml">
<form string="Campaign Tags">
<sheet>
<group>
<field name="name"/>
</group>
</sheet>
</form>
</field>
</record>
<record id="action_view_utm_tag" model="ir.actions.act_window">
<field name="name">Campaign Tags</field>
<field name="res_model">utm.tag</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Add a new tag
</p>
</field>
</record>
<!-- CAMPAIGN STAGE !-->
<record model="ir.ui.view" id="utm_stage_view_search">
<field name="name">utm.stage.view.search</field>
<field name="model">utm.stage</field>
<field name="arch" type="xml">
<search string="Stages">
<field name="name"/>
</search>
</field>
</record>
<record model="ir.ui.view" id="utm_stage_view_tree">
<field name="name">utm.stage.view.tree</field>
<field name="model">utm.stage</field>
<field name="priority">10</field>
<field name="arch" type="xml">
<tree string="Stages" editable="top">
<field name="sequence" widget="handle"/>
<field name="name"/>
</tree>
</field>
</record>
<record id="action_view_utm_stage" model="ir.actions.act_window">
<field name="name">UTM Stages</field>
<field name="res_model">utm.stage</field>
<field name="view_mode">tree,form</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Create a new utm stage
</p>
</field>
</record>
</odoo>
+1 -25
View File
@@ -11,34 +11,10 @@
sequence="99"
groups="base.group_no_one"/>
<!-- utm.campaign -->
<record id="utm_campaign_view_tree" model="ir.ui.view">
<field name="name">utm.campaign.view.tree</field>
<field name="model">utm.campaign</field>
<field name="arch" type="xml">
<tree string="Campaign" editable="bottom">
<field name="name"/>
</tree>
</field>
</record>
<record id="utm_campaign_view_form" model="ir.ui.view">
<field name="name">utm.campaign.view.form</field>
<field name="model">utm.campaign</field>
<field name="arch" type="xml">
<form string="Campaign">
<sheet>
<group>
<field name="name"/>
</group>
</sheet>
</form>
</field>
</record>
<record id="utm_campaign_action" model="ir.actions.act_window">
<field name="name">Campaigns</field>
<field name="res_model">utm.campaign</field>
<field name="view_id" ref="utm_campaign_view_tree"/>
<field name="view_mode">kanban,tree,form</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Define a new UTM campaign
@@ -77,10 +77,16 @@ var SelectBox = publicWidget.Widget.extend({
*/
_createObject: function (name) {
var self = this;
var args = {
name: name
};
if (this.obj === "utm.campaign"){
args.is_website = true;
}
return this._rpc({
model: this.obj,
method: 'create',
args: [{name: name}],
args: [args],
}).then(function (record) {
self.$el.attr('value', record);
self.objects.push({'id': record, 'text': name});