[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:
axj-odoo
2017-10-24 14:48:57 +02:00
committed by Yannick Tivisse
parent e2342a562d
commit a924cbf983
21 changed files with 329 additions and 50 deletions
+1
View File
@@ -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>
+7 -3
View File
@@ -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 -1
View File
@@ -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']
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>