[ADD] website_sale_link_tracker: Display utm statistics on website dashboard
Purpose ======= Utm links are really useful for website owners but this feature is hidden in Odoo apps. There is nothing pointing to that. Specification ============= - Add new module to generate utm pie chart in website_sale dashboard to track website utm link - Add new option in website settings to install "website_links" Title: Link Tracker Subtitle: Track clicks on UTM links Tooltip: Analyze the efficiency of your marketing campaigns by using trackable UTM trackers (campaigns, medium, sources). Create trackers and follow clicks from the Promote menu of your website. Those trackers can be used in Google Analytics or in Odoo reports where you can see the opportunities and sales revenues generated thanks to your links. - Add utm chart to dashboard, using nvd3 such pie chart should be easy to implement - If no data to display > Display a default picture with "There isn't any UTM tag detected in orders". - Set a real icon to app "utm" since it shows up in homepage debug mode
This commit is contained in:
committed by
Yannick Tivisse
parent
e2342a562d
commit
a924cbf983
@@ -391,6 +391,7 @@
|
||||
<group string="Reporting" name="technical" groups="base.group_no_one">
|
||||
<field groups="base.group_no_one" name="origin"/>
|
||||
</group>
|
||||
<group name="utm_link" groups="base.group_no_one"/>
|
||||
</group>
|
||||
</page>
|
||||
</notebook>
|
||||
|
||||
@@ -31,10 +31,14 @@
|
||||
<field name="user_id" position="after">
|
||||
<field name="tag_ids" widget="many2many_tags"/>
|
||||
</field>
|
||||
<xpath expr="//group[@name='utm_link']" position="replace">
|
||||
<group string="Marketing" name="utm_link" groups="base.group_no_one">
|
||||
<field name="campaign_id"/>
|
||||
<field name="medium_id"/>
|
||||
<field name="source_id"/>
|
||||
</group>
|
||||
</xpath>
|
||||
<xpath expr="//group[@name='technical']" position="inside">
|
||||
<field name="campaign_id" groups="base.group_no_one" />
|
||||
<field name="medium_id" groups="base.group_no_one"/>
|
||||
<field name="source_id" groups="base.group_no_one"/>
|
||||
<field name="opportunity_id" help="Log in the chatter from which opportunity the order originates" groups="base.group_no_one"/>
|
||||
</xpath>
|
||||
</field>
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 4.1 KiB |
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<menuitem id="menu_link_tracker_root" name="Link Tracker" groups="base.group_no_one" sequence="30"/>
|
||||
<menuitem id="menu_link_tracker_root" name="Link Tracker" groups="base.group_no_one" sequence="30" web_icon="utm,static/description/icon.png"/>
|
||||
<menuitem name="UTMs" id="marketing_utm" parent="menu_link_tracker_root" sequence="99" groups="base.group_no_one"/>
|
||||
|
||||
<!-- utm.campaign -->
|
||||
|
||||
@@ -27,6 +27,7 @@ class ResConfigSettings(models.TransientModel):
|
||||
cdn_url = fields.Char(related='website_id.cdn_url')
|
||||
cdn_filters = fields.Text(related='website_id.cdn_filters')
|
||||
module_website_version = fields.Boolean("A/B Testing")
|
||||
module_website_links = fields.Boolean(string="Link Trackers")
|
||||
|
||||
favicon = fields.Binary('Favicon', related='website_id.favicon')
|
||||
# Set as global config parameter since methods using it are not website-aware. To be changed
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
|
||||
h4 { margin: 0 0 0 8px; }
|
||||
|
||||
.col-md-5, .col-md-7, .col-md-12 {
|
||||
.col-md-6, .col-md-12 {
|
||||
padding: 15px;
|
||||
|
||||
.js_field_selection {
|
||||
|
||||
@@ -180,6 +180,17 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xs-12 col-md-6 o_setting_box" id="utm_tracker_settings" title="Analyze the efficiency of your marketing campaigns by using trackable UTM trackers (campaigns, medium, sources). Create trackers and follow clicks from the Promote menu of your website. Those trackers can be used in Google Analytics or in Odoo reports where you can see the opportunities and sales revenue generated thanks to your links.">
|
||||
<div class="o_setting_left_pane">
|
||||
<field name="module_website_links"/>
|
||||
</div>
|
||||
<div class="o_setting_right_pane">
|
||||
<label for="module_website_links"/>
|
||||
<div class="text-muted">
|
||||
Track clicks on UTM links
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</xpath>
|
||||
|
||||
@@ -30,4 +30,6 @@ WebsiteBackend.include({
|
||||
});
|
||||
},
|
||||
});
|
||||
return WebsiteBackend;
|
||||
|
||||
});
|
||||
|
||||
@@ -30,14 +30,14 @@
|
||||
<div t-if="widget.groups.sale_salesman" class="row o_dashboard_sales">
|
||||
<div class="col-xs-12 o_box">
|
||||
<div t-if="widget.dashboards_data.sales.summary.order_count">
|
||||
<div class="col-md-7 col-xs-12">
|
||||
<div class="col-md-6 col-xs-12">
|
||||
<t t-if="widget.date_range">
|
||||
<h2>Sales Since Last <t t-esc="widget.date_range"></t></h2>
|
||||
</t>
|
||||
<t t-else=""><h2>Sales</h2></t>
|
||||
<div id="o_graph_sales" class="o_graph" data-type="sales"/>
|
||||
</div>
|
||||
<div class="col-md-5 col-xs-12">
|
||||
<div class="col-md-6 col-xs-12">
|
||||
<t t-call="website_sale.products_table"/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -62,58 +62,62 @@
|
||||
<div class="row">
|
||||
<div class="col-md-12 col-xs-12">
|
||||
<div class="row">
|
||||
<h4>AT A GLANCE</h4>
|
||||
<a href="#" class="o_dashboard_action" name="website_sale.sale_report_action_dashboard">
|
||||
<div class="col-sm-4 o_link_enable" title="Orders">
|
||||
<div class="o_highlight">
|
||||
<t t-esc="widget.dashboards_data.sales.summary.order_count"/>
|
||||
<div class="col-md-12 col-xs-12">
|
||||
<h4>AT A GLANCE</h4>
|
||||
<a href="#" class="o_dashboard_action" name="website_sale.sale_report_action_dashboard">
|
||||
<div class="col-sm-4 o_link_enable" title="Orders">
|
||||
<div class="o_highlight">
|
||||
<t t-esc="widget.dashboards_data.sales.summary.order_count"/>
|
||||
</div>
|
||||
Orders
|
||||
</div>
|
||||
Orders
|
||||
</div>
|
||||
</a>
|
||||
<a href="#" class="o_dashboard_action" name="website_sale.sale_report_action_dashboard">
|
||||
<div class="col-sm-4 o_link_enable" title="Untaxed Total Sold">
|
||||
<div class="o_highlight">
|
||||
<t t-esc="widget.render_monetary_field(widget.dashboards_data.sales.summary.total_sold, widget.data.currency)"/>
|
||||
</a>
|
||||
<a href="#" class="o_dashboard_action" name="website_sale.sale_report_action_dashboard">
|
||||
<div class="col-sm-4 o_link_enable" title="Untaxed Total Sold">
|
||||
<div class="o_highlight">
|
||||
<t t-esc="widget.render_monetary_field(widget.dashboards_data.sales.summary.total_sold, widget.data.currency)"/>
|
||||
</div>
|
||||
Sold
|
||||
</div>
|
||||
Sold
|
||||
</a>
|
||||
<a href="#" class="o_dashboard_action" name="website_sale.sale_report_action_carts">
|
||||
<div class="col-sm-4 o_link_enable o_invisible_border" title="Carts">
|
||||
<div class="o_highlight"><t t-esc="widget.dashboards_data.sales.summary.order_carts_count"/></div>
|
||||
Carts
|
||||
</div>
|
||||
</a>
|
||||
<div class="col-sm-4 o_link_disable" title="Orders/Day">
|
||||
<div class="o_highlight"><t t-esc="widget.dashboards_data.sales.summary.order_per_day_ratio"/></div>
|
||||
Orders/Day
|
||||
</div>
|
||||
</a>
|
||||
<a href="#" class="o_dashboard_action" name="website_sale.sale_report_action_carts">
|
||||
<div class="col-sm-4 o_link_enable o_invisible_border" title="Carts">
|
||||
<div class="o_highlight"><t t-esc="widget.dashboards_data.sales.summary.order_carts_count"/></div>
|
||||
Carts
|
||||
<div class="col-sm-4 o_link_disable" title="Average Order">
|
||||
<div class="o_highlight"><t t-esc="widget.render_monetary_field(widget.dashboards_data.sales.summary.order_sold_ratio, widget.data.currency)"/></div>
|
||||
Average Order
|
||||
</div>
|
||||
<div class="col-sm-4 o_link_disable o_invisible_border" title="Conversion">
|
||||
<div class="o_highlight"><t t-esc="widget.format_number(widget.dashboards_data.sales.summary.order_convertion_pctg, 'float', [3, 2], '%')"/></div>
|
||||
Conversion
|
||||
</div>
|
||||
</a>
|
||||
<div class="col-sm-4 o_link_disable" title="Orders/Day">
|
||||
<div class="o_highlight"><t t-esc="widget.dashboards_data.sales.summary.order_per_day_ratio"/></div>
|
||||
Orders/Day
|
||||
</div>
|
||||
<div class="col-sm-4 o_link_disable" title="Average Order">
|
||||
<div class="o_highlight"><t t-esc="widget.render_monetary_field(widget.dashboards_data.sales.summary.order_sold_ratio, widget.data.currency)"/></div>
|
||||
Average Order
|
||||
</div>
|
||||
<div class="col-sm-4 o_link_disable o_invisible_border" title="Conversion">
|
||||
<div class="o_highlight"><t t-esc="widget.format_number(widget.dashboards_data.sales.summary.order_convertion_pctg, 'float', [3, 2], '%')"/></div>
|
||||
Conversion
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-12 col-xs-12 o_top_margin">
|
||||
<div class="row">
|
||||
<h4>Best Sellers</h4>
|
||||
<table class="table table-responsive table-hover">
|
||||
<tr>
|
||||
<th>Product</th>
|
||||
<th>Quantity</th>
|
||||
<th>Sold</th>
|
||||
</tr>
|
||||
<tr class="o_product_template" t-foreach="widget.dashboards_data.sales.best_sellers" t-as="product" t-att-data-product-id="product.id">
|
||||
<td><t t-esc="product.name"/></td>
|
||||
<td><t t-esc="product.qty"/></td>
|
||||
<td><t t-esc="widget.render_monetary_field(product.sales, widget.data.currency)"/></td>
|
||||
</tr>
|
||||
</table>
|
||||
<div class="col-md-12 col-xs-12">
|
||||
<h4>Best Sellers</h4>
|
||||
<table class="table table-responsive table-hover">
|
||||
<tr>
|
||||
<th>Product</th>
|
||||
<th>Quantity</th>
|
||||
<th>Sold</th>
|
||||
</tr>
|
||||
<tr class="o_product_template" t-foreach="widget.dashboards_data.sales.best_sellers" t-as="product" t-att-data-product-id="product.id">
|
||||
<td><t t-esc="product.name"/></td>
|
||||
<td><t t-esc="product.qty"/></td>
|
||||
<td><t t-esc="widget.render_monetary_field(product.sales, widget.data.currency)"/></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import models
|
||||
from . import controllers
|
||||
@@ -0,0 +1,18 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
{
|
||||
'name': 'eCommerce Link Tracker',
|
||||
'description': """
|
||||
View Link Tracker Statistics on eCommerce dashboard
|
||||
=====================================================
|
||||
|
||||
""",
|
||||
'depends': ['website_links', 'website_sale'],
|
||||
'data': [
|
||||
'views/sale_order_views.xml',
|
||||
'views/assets.xml',
|
||||
],
|
||||
'qweb': ['static/src/xml/*.xml'],
|
||||
'auto_install': True,
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import backend
|
||||
@@ -0,0 +1,50 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import http
|
||||
from odoo.addons.website_sale.controllers.backend import WebsiteSaleBackend
|
||||
from odoo.http import request
|
||||
|
||||
|
||||
class WebsiteSaleLinkTrackerBackend(WebsiteSaleBackend):
|
||||
|
||||
@http.route()
|
||||
def fetch_dashboard_data(self, date_from, date_to):
|
||||
results = super(WebsiteSaleLinkTrackerBackend, self).fetch_dashboard_data(date_from, date_to)
|
||||
results['dashboards']['sales']['utm_graph'] = self.fetch_utm_data(date_from, date_to)
|
||||
return results
|
||||
|
||||
def fetch_utm_data(self, date_from, date_to):
|
||||
sale_utm_domain = [
|
||||
('team_id.team_type', '=', 'website'),
|
||||
('state', 'in', ['sale', 'done']),
|
||||
('confirmation_date', '>=', date_from),
|
||||
('confirmation_date', '<=', date_to)
|
||||
]
|
||||
|
||||
orders_data_groupby_campaign_id = request.env['sale.order'].read_group(
|
||||
domain=sale_utm_domain + [('campaign_id', '!=', False)],
|
||||
fields=['amount_total', 'id', 'campaign_id'],
|
||||
groupby='campaign_id')
|
||||
|
||||
orders_data_groupby_medium_id = request.env['sale.order'].read_group(
|
||||
domain=sale_utm_domain + [('medium_id', '!=', False)],
|
||||
fields=['amount_total', 'id', 'medium_id'],
|
||||
groupby='medium_id')
|
||||
|
||||
orders_data_groupby_source_id = request.env['sale.order'].read_group(
|
||||
domain=sale_utm_domain + [('source_id', '!=', False)],
|
||||
fields=['amount_total', 'id', 'source_id'],
|
||||
groupby='source_id')
|
||||
|
||||
return {
|
||||
'campaign_id': self.compute_utm_graph_data('campaign_id', orders_data_groupby_campaign_id),
|
||||
'medium_id': self.compute_utm_graph_data('medium_id', orders_data_groupby_medium_id),
|
||||
'source_id': self.compute_utm_graph_data('source_id', orders_data_groupby_source_id),
|
||||
}
|
||||
|
||||
def compute_utm_graph_data(self, utm_type, utm_graph_data):
|
||||
return [{
|
||||
'utm_type': data[utm_type][1],
|
||||
'amount_total': data['amount_total']
|
||||
} for data in utm_graph_data]
|
||||
@@ -0,0 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import sale_order
|
||||
@@ -0,0 +1,9 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import models
|
||||
|
||||
|
||||
class SaleOrder(models.Model):
|
||||
_name = "sale.order"
|
||||
_inherit = ['utm.mixin', 'sale.order']
|
||||
BIN
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
@@ -0,0 +1,89 @@
|
||||
odoo.define('website_sale_link_tracker.backend', function (require) {
|
||||
"use strict";
|
||||
|
||||
var WebsiteSaleBackend = require('website_sale.backend');
|
||||
|
||||
WebsiteSaleBackend.include({
|
||||
|
||||
events: _.defaults({
|
||||
'click .js_utm_selector': '_onClickUtmButton', // Click event on select UTM drop-down button
|
||||
}, WebsiteSaleBackend.prototype.events),
|
||||
|
||||
/**
|
||||
* @override method from website backendDashboard
|
||||
* @private
|
||||
*/
|
||||
render_graphs: function() {
|
||||
this._super();
|
||||
this.utmGraphData = this.dashboards_data.sales.utm_graph;
|
||||
this._renderUtmGraph();
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Method used to generate Pie chart, depending on user selected UTM option(campaign, medium, source)
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_renderUtmGraph: function() {
|
||||
var self = this;
|
||||
this.$(".utm_button_name").html(this.btnName); // change drop-down button name
|
||||
var utmDataType = this.utmType || 'campaign_id';
|
||||
var graphData = this.utmGraphData[utmDataType];
|
||||
if (graphData.length) {
|
||||
this.$(".o_utm_no_data_img").hide();
|
||||
this.$(".o_utm_data_graph").show();
|
||||
|
||||
this.$(".o_utm_data_graph").empty();
|
||||
nv.addGraph(function() {
|
||||
var utmChart = nv.models.pieChart()
|
||||
.x(function(d) {return d.utm_type; })
|
||||
.y(function(d) {return d.amount_total; })
|
||||
.showLabels(true)
|
||||
.labelThreshold(0.1)
|
||||
.labelType("percent")
|
||||
.showLegend(false)
|
||||
.margin({ "left": 0, "right": 0, "top": 0, "bottom": 0 })
|
||||
.color(['#875a7b', '#21b799', '#E4A900', '#D5653E', '#5B899E', '#E46F78', '#8F8F8F']);
|
||||
|
||||
utmChart.tooltip.valueFormatter(function(value, i) {
|
||||
return self.render_monetary_field(value, self.data.currency);
|
||||
});
|
||||
|
||||
var svg = d3.select(".o_utm_data_graph").append("svg");
|
||||
|
||||
svg
|
||||
.attr("height", "15em")
|
||||
.datum(graphData)
|
||||
.call(utmChart);
|
||||
|
||||
nv.utils.windowResize(utmChart.update);
|
||||
return utmChart;
|
||||
});
|
||||
} else {
|
||||
this.$(".o_utm_no_data_img").show();
|
||||
this.$(".o_utm_data_graph").hide();
|
||||
}
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Handlers
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Onchange on UTM dropdown button, this method is called.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_onClickUtmButton: function(ev) {
|
||||
this.utmType = $(ev.currentTarget).attr('name');
|
||||
this.btnName = $(ev.currentTarget).text();
|
||||
this._renderUtmGraph();
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
@@ -0,0 +1,19 @@
|
||||
.o_dashboard_utms {
|
||||
.o_utm_data_graph {
|
||||
.nv-pieLabels text {
|
||||
fill: white !important;
|
||||
}
|
||||
.nvd3.nv-pie path {
|
||||
fill-opacity: .9;
|
||||
}
|
||||
}
|
||||
|
||||
.utm_chart_image {
|
||||
display: block;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.utm_dropdown {
|
||||
margin-top: -5px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<templates>
|
||||
<t t-extend="website_sale.products_table">
|
||||
<t t-jquery=".o_top_margin .row .col-xs-12" t-operation="attributes">
|
||||
<attribute name="class">col-md-6 col-xs-12</attribute>
|
||||
</t>
|
||||
<t t-jquery=".o_top_margin .row" t-operation="append">
|
||||
<div class="col-md-6 col-xs-12 o_dashboard_utms">
|
||||
<div>
|
||||
<h4 class="pull-left">REVENUE BY</h4>
|
||||
<t t-call="website_sale.LinkTrackersDropDown"/>
|
||||
</div>
|
||||
<div class="o_utm_no_data_img">
|
||||
<img src="website_sale_link_tracker/static/src/img/website_sale_link_tracker_chart_demo.png" alt="There isn't any UTM tag detected in orders" class="utm_chart_image image-responsive mt8"/>
|
||||
</div>
|
||||
<div class="o_utm_data_graph"/>
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
|
||||
<t t-name="website_sale.LinkTrackersDropDown">
|
||||
<div class="dropdown">
|
||||
<button class="btn btn-default btn-sm dropdown-toggle utm_dropdown ml4" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true"><span class="utm_button_name">Campaigns</span><span class="caret ml4"/>
|
||||
</button>
|
||||
<ul class="dropdown-menu" role="menu" aria-labelledby="utm_dropdown">
|
||||
<li><a class="js_utm_selector" name="campaign_id">Campaigns</a></li>
|
||||
<li><a class="js_utm_selector" name="medium_id">Medium</a></li>
|
||||
<li><a class="js_utm_selector" name="source_id">Sources</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<template id="assets_backend" inherit_id="web.assets_backend">
|
||||
<xpath expr="." position="inside">
|
||||
<script type="text/javascript" src="/website_sale_link_tracker/static/src/js/website_sale_dashborad_utm_chart.js"></script>
|
||||
<link rel="stylesheet" href="/website_sale_link_tracker/static/src/less/website_sale_link_tracker.less"/>
|
||||
</xpath>
|
||||
</template>
|
||||
</odoo>
|
||||
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="sale_order_from_website_link_tracker" model="ir.ui.view">
|
||||
<field name="name">sale.order.form.inherit.sale</field>
|
||||
<field name="model">sale.order</field>
|
||||
<field name="inherit_id" ref="sale.view_order_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//group[@name='utm_link']" position="replace">
|
||||
<group string="Marketing" name="utm_link" groups="base.group_no_one">
|
||||
<field name="campaign_id"/>
|
||||
<field name="medium_id"/>
|
||||
<field name="source_id"/>
|
||||
</group>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
Reference in New Issue
Block a user