Report updated
This commit is contained in:
parent
b02030d36e
commit
5bbdf18b18
|
@ -71,7 +71,7 @@ class Project(models.Model):
|
|||
comment = fields.Text(string='Comment')
|
||||
tag_ids = fields.Many2many('custom.project.tags', string='Tags')
|
||||
|
||||
@api.onchange('allowed_internal_user_ids')
|
||||
"""@api.onchange('allowed_internal_user_ids')
|
||||
def onchange_add_allowed_internal_users(self):
|
||||
user_list = []
|
||||
consultant_list = []
|
||||
|
@ -89,7 +89,7 @@ class Project(models.Model):
|
|||
if employee_obj:
|
||||
for employee in employee_obj:
|
||||
self.sale_line_employee_ids.create({'project_id': self._origin.id,
|
||||
'employee_id': employee})
|
||||
'employee_id': employee})"""
|
||||
|
||||
|
||||
def _onchange_calculate_timesheet_hours(self):
|
||||
|
|
|
@ -0,0 +1,52 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import fields, models, tools
|
||||
|
||||
|
||||
class BudgetHrsAnalysis(models.Model):
|
||||
|
||||
_name = "project.consultant.hrs.report"
|
||||
_description = "Project consultant hours analysis report"
|
||||
#_order = 'project_id desc, hours_type asc, end_date desc, employee_id'
|
||||
_order = 'project_id desc, 1, 2, hours_type, employee_id, end_date, start_date'
|
||||
_auto = False
|
||||
|
||||
project_id = fields.Many2one('project.project', string='Project', readonly=True)
|
||||
employee_id = fields.Many2one('hr.employee', string='Consultant', readonly=True)
|
||||
start_date = fields.Date(string='Start Date', readonly=True)
|
||||
end_date = fields.Date(string='End Date', readonly=True)
|
||||
hours_type = fields.Char(string="Hours Type", readonly=True)
|
||||
hours = fields.Float("Hours", digits=(16, 2), readonly=True, group_operator="sum")
|
||||
percentage = fields.Float("Percentage (%)")
|
||||
#roworder = fields.Integer("Row Order")
|
||||
#ptime = fields.Char(string="Time", readonly=True)
|
||||
|
||||
"""def _generate_order_by(self, order_spec, query):
|
||||
order_by = super(BudgetHrsAnalysis, self)._generate_order_by(order_spec, query)
|
||||
print("order by>>>>>>>>>>>>>>", order_by)
|
||||
return order_by"""
|
||||
|
||||
def init(self):
|
||||
'''Create the view'''
|
||||
tools.drop_view_if_exists(self._cr, self._table)
|
||||
self._cr.execute("""
|
||||
CREATE OR REPLACE VIEW %s AS (
|
||||
SELECT ROW_NUMBER() OVER() as id, project_id, start_date, end_date, employee_id, hours_type, hours, percentage
|
||||
from (
|
||||
select project_id as project_id, start_date as start_date, end_date as end_date,
|
||||
employee_id as employee_id, 'Budgeted Hours for period' as hours_type, budgeted_hours as hours,
|
||||
percentage as percentage
|
||||
from project_consultant_hrs ph
|
||||
union
|
||||
select project_id as project_id, start_date as start_date, end_date as end_date,
|
||||
employee_id as employee_id, 'Actual Hours for period' as hours_type, actual_hours as hours,
|
||||
actual_percentage as percentage
|
||||
from project_consultant_hrs ph
|
||||
) as res order by
|
||||
project_id desc,
|
||||
hours_type,
|
||||
employee_id, end_date, start_date
|
||||
)""" % (self._table,))
|
||||
|
||||
|
|
@ -0,0 +1,97 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<record id="view_project_consultant_hrs_report_form" model="ir.ui.view">
|
||||
<field name="name">project.consultant.hrs.report.form</field>
|
||||
<field name="model">project.consultant.hrs.report</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Consultant Allocation" create="false" edit="false" delete="0">
|
||||
<group>
|
||||
<field name="project_id"/>
|
||||
<field name="employee_id"/>
|
||||
<field name="start_date"/>
|
||||
<field name="end_date"/>
|
||||
<field name="hours_type"/>
|
||||
<field name="hours"/>
|
||||
<field name="percentage"/>
|
||||
</group>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_project_consultant_hrs_report_tree" model="ir.ui.view">
|
||||
<field name="name">project.consultant.hrs.report.tree</field>
|
||||
<field name="model">project.consultant.hrs.report</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree string="Consultant Allocation" create="false" edit="false" delete="0">
|
||||
<field name="project_id"/>
|
||||
<field name="employee_id"/>
|
||||
<field name="start_date"/>
|
||||
<field name="end_date"/>
|
||||
<field name="hours_type"/>
|
||||
<field name="hours"/>
|
||||
<field name="percentage"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_project_consultant_hrs_report_search" model="ir.ui.view">
|
||||
<field name="name">project.consultant.hrs.report.search</field>
|
||||
<field name="model">project.consultant.hrs.report</field>
|
||||
<field name="arch" type="xml">
|
||||
<search string="Consultant Allocation">
|
||||
<field name="project_id"/>
|
||||
<field name="employee_id"/>
|
||||
<group expand="0" string="Group By">
|
||||
<filter string="Project" name="project" domain="[]" context="{'group_by':'project_id'}"/>
|
||||
<filter string="Consultant" name="consultant" domain="[]" context="{'group_by':'employee_id'}"/>
|
||||
<filter string="Start Date" name="sdate" domain="[]" context="{'group_by':'start_date'}"/>
|
||||
<filter string="End Date" name="edate" domain="[]" context="{'group_by':'end_date'}"/>
|
||||
<filter string="Hours type" name="group_by_hours_type" context="{'group_by':'hours_type'}"/>
|
||||
<!--<filter string="Time" name="ptime" domain="[]" context="{'group_by':'ptime'}"/>-->
|
||||
</group>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="project_consultant_hrs_report_graph" model="ir.ui.view">
|
||||
<field name="name">project.consultant.hrs.report.graph</field>
|
||||
<field name="model">project.consultant.hrs.report</field>
|
||||
<field name="arch" type="xml">
|
||||
<graph string="Consultant Allocation" type="bar" stacked="True" sample="1" disable_linking="1">
|
||||
<field name="project_id" type="row"/>
|
||||
<field name="hours" type="measure"/>
|
||||
</graph>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_project_consultant_hrs_report" model="ir.actions.act_window">
|
||||
<field name="name">Consultant Allocation</field>
|
||||
<field name="type">ir.actions.act_window</field>
|
||||
<field name="res_model">project.consultant.hrs.report</field>
|
||||
<field name="view_mode">tree,graph</field>
|
||||
<field name="search_view_id" ref="view_project_consultant_hrs_report_search"/>
|
||||
<field name="context">{
|
||||
'search_default_project': 1,
|
||||
'search_default_consultant': 1,
|
||||
}
|
||||
</field>
|
||||
</record>
|
||||
|
||||
|
||||
<record id="project_consul_hours_report_view_form" model="ir.ui.view">
|
||||
<field name="name">Project Consul Report Hours</field>
|
||||
<field name="model">project.project</field>
|
||||
<field name="inherit_id" ref="project.edit_project"/>
|
||||
<field name="arch" type="xml">
|
||||
<div name="button_box" position="inside">
|
||||
<button class="oe_stat_button" type="action" name="cor_custom.action_project_consultant_hrs_report"
|
||||
icon="fa-tasks"
|
||||
attrs="{'invisible':['|',('pricing_type','!=','employee_rate'),('project_type','!=','hours_in_consultant')]}"
|
||||
string="View Allocation1" widget="statinfo">
|
||||
</button>
|
||||
</div>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
|
@ -20,6 +20,9 @@
|
|||
'report/project_budget_hrs_analysis_views.xml',
|
||||
'report/project_budget_amt_analysis_views.xml',
|
||||
],
|
||||
'qweb': [
|
||||
"static/src/xml/base.xml",
|
||||
],
|
||||
'auto_install': False,
|
||||
'installable': True,
|
||||
}
|
|
@ -1,10 +1,29 @@
|
|||
odoo.define("project_report.GraphRenderer", function(require) {
|
||||
var GraphRenderer = require("web.GraphRenderer");
|
||||
var config = require("web.config");
|
||||
var AbstractRenderer = require('web.AbstractRenderer');
|
||||
var config = require('web.config');
|
||||
var core = require('web.core');
|
||||
var dataComparisonUtils = require('web.dataComparisonUtils');
|
||||
var fieldUtils = require('web.field_utils');
|
||||
|
||||
// Hide top legend when too many items for device size
|
||||
var MAX_LEGEND_LENGTH = 25 * (1 + config.device.size_class);
|
||||
var _t = core._t;
|
||||
var DateClasses = dataComparisonUtils.DateClasses;
|
||||
var qweb = core.qweb;
|
||||
|
||||
var CHART_TYPES = ['pie', 'bar', 'horizontalBar', 'line'];
|
||||
|
||||
var COLORS = ["#1f77b4", "#ff7f0e", "#aec7e8", "#ffbb78", "#2ca02c", "#98df8a", "#d62728",
|
||||
"#ff9896", "#9467bd", "#c5b0d5", "#8c564b", "#c49c94", "#e377c2", "#f7b6d2",
|
||||
"#7f7f7f", "#c7c7c7", "#bcbd22", "#dbdb8d", "#17becf", "#9edae5"];
|
||||
var COLOR_NB = COLORS.length;
|
||||
|
||||
function hexToRGBA(hex, opacity) {
|
||||
var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
||||
var rgb = result.slice(1, 4).map(function (n) {
|
||||
return parseInt(n, 16);
|
||||
}).join(',');
|
||||
return 'rgba(' + rgb + ',' + opacity + ')';
|
||||
}
|
||||
|
||||
// used to format values in tooltips and yAxes.
|
||||
var FORMAT_OPTIONS = {
|
||||
|
@ -21,6 +40,13 @@ var FORMAT_OPTIONS = {
|
|||
},
|
||||
};
|
||||
|
||||
var NO_DATA = [_t('No data')];
|
||||
NO_DATA.isNoData = true;
|
||||
|
||||
// Hide top legend when too many items for device size
|
||||
var MAX_LEGEND_LENGTH = 25 * (1 + config.device.size_class);
|
||||
|
||||
|
||||
GraphRenderer.include({
|
||||
init: function(parent, state, params) {
|
||||
this._super.apply(this, arguments);
|
||||
|
@ -32,13 +58,333 @@ var FORMAT_OPTIONS = {
|
|||
},
|
||||
|
||||
_formatValue: function (value) {
|
||||
//this._super.apply(this, arguments);
|
||||
//this._super.apply(this, arguments);
|
||||
var measureField = this.fields[this.state.measure];
|
||||
var formatter = fieldUtils.format.float;
|
||||
var formatedValue = formatter(value, measureField, FORMAT_OPTIONS);
|
||||
return formatedValue;
|
||||
},
|
||||
|
||||
/**
|
||||
* Determines the initial section of the labels array
|
||||
* over a dataset has to be completed. The section only depends
|
||||
* on the datasets origins.
|
||||
*
|
||||
* @private
|
||||
* @param {number} originIndex
|
||||
* @param {number} defaultLength
|
||||
* @returns {number}
|
||||
*/
|
||||
_getDatasetDataLength: function (originIndex, defaultLength) {
|
||||
if (_.contains(['bar', 'horizontalBar', 'line'], this.state.mode) && this.state.comparisonFieldIndex === 0) {
|
||||
return this.dateClasses.dateSets[originIndex].length;
|
||||
}
|
||||
return defaultLength;
|
||||
},
|
||||
|
||||
/**
|
||||
* Determines over which label is the data point
|
||||
*
|
||||
* @private
|
||||
* @param {Object} dataPt
|
||||
* @returns {Array}
|
||||
*/
|
||||
_getLabel: function (dataPt) {
|
||||
var i = this.state.comparisonFieldIndex;
|
||||
if (_.contains(['bar', 'horizontalBar', 'line'], this.state.mode)) {
|
||||
if (i === 0) {
|
||||
return [this.dateClasses.dateClass(dataPt.originIndex, dataPt.labels[i])];
|
||||
} else {
|
||||
return dataPt.labels.slice(0, 1);
|
||||
}
|
||||
} else if (i === 0) {
|
||||
return Array.prototype.concat.apply([], [
|
||||
this.dateClasses.dateClass(dataPt.originIndex, dataPt.labels[i]),
|
||||
dataPt.labels.slice(i+1)
|
||||
]);
|
||||
} else {
|
||||
return dataPt.labels;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns an object used to style chart elements independently from the datasets.
|
||||
*
|
||||
* @private
|
||||
* @returns {Object}
|
||||
*/
|
||||
_getElementOptions: function () {
|
||||
var elementOptions = {};
|
||||
if (this.state.mode === 'bar' || this.state.mode === 'horizontalBar') {
|
||||
elementOptions.rectangle = {borderWidth: 1};
|
||||
} else if (this.state.mode === 'line') {
|
||||
elementOptions.line = {
|
||||
tension: 0,
|
||||
fill: false,
|
||||
};
|
||||
}
|
||||
return elementOptions;
|
||||
},
|
||||
|
||||
/**
|
||||
* Determines to which dataset belong the data point
|
||||
*
|
||||
* @private
|
||||
* @param {Object} dataPt
|
||||
* @returns {string}
|
||||
*/
|
||||
_getDatasetLabel: function (dataPt) {
|
||||
if (_.contains(['bar', 'horizontalBar', 'line'], this.state.mode)) {
|
||||
// ([origin] + second to last groupBys) or measure
|
||||
var datasetLabel = dataPt.labels.slice(1).join("/");
|
||||
if (this.state.origins.length > 1) {
|
||||
datasetLabel = this.state.origins[dataPt.originIndex] +
|
||||
(datasetLabel ? ('/' + datasetLabel) : '');
|
||||
}
|
||||
datasetLabel = datasetLabel || this.fields[this.state.measure].string;
|
||||
return datasetLabel;
|
||||
}
|
||||
return this.state.origins[dataPt.originIndex];
|
||||
},
|
||||
|
||||
|
||||
_filterDataPoints: function () {
|
||||
var dataPoints = [];
|
||||
if (_.contains(['bar', 'horizontalBar', 'pie'], this.state.mode)) {
|
||||
dataPoints = this.state.dataPoints.filter(function (dataPt) {
|
||||
return dataPt.count > 0;
|
||||
});
|
||||
} else if (this.state.mode === 'line') {
|
||||
var counts = 0;
|
||||
this.state.dataPoints.forEach(function (dataPt) {
|
||||
if (dataPt.labels[0] !== _t("Undefined")) {
|
||||
dataPoints.push(dataPt);
|
||||
}
|
||||
counts += dataPt.count;
|
||||
});
|
||||
// data points with zero count might have been created on purpose
|
||||
// we only remove them if there are no data point with positive count
|
||||
if (counts === 0) {
|
||||
dataPoints = [];
|
||||
}
|
||||
}
|
||||
return dataPoints;
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Returns the options used to generate the chart legend.
|
||||
*
|
||||
* @private
|
||||
* @param {Number} datasetsCount
|
||||
* @returns {Object}
|
||||
*/
|
||||
_getLegendOptions: function (datasetsCount) {
|
||||
var legendOptions = {
|
||||
display: datasetsCount <= MAX_LEGEND_LENGTH,
|
||||
// position: this.state.mode === 'pie' ? 'right' : 'top',
|
||||
position: 'top',
|
||||
onHover: this._onlegendTooltipHover.bind(this),
|
||||
onLeave: this._onLegendTootipLeave.bind(this),
|
||||
};
|
||||
var self = this;
|
||||
if (_.contains(['bar', 'horizontalBar', 'line'], this.state.mode)) {
|
||||
var referenceColor;
|
||||
if (this.state.mode === 'bar') {
|
||||
referenceColor = 'backgroundColor';
|
||||
} else {
|
||||
referenceColor = 'borderColor';
|
||||
}
|
||||
legendOptions.labels = {
|
||||
generateLabels: function (chart) {
|
||||
var data = chart.data;
|
||||
return data.datasets.map(function (dataset, i) {
|
||||
return {
|
||||
text: self._shortenLabel(dataset.label),
|
||||
fullText: dataset.label,
|
||||
fillStyle: dataset[referenceColor],
|
||||
hidden: !chart.isDatasetVisible(i),
|
||||
lineCap: dataset.borderCapStyle,
|
||||
lineDash: dataset.borderDash,
|
||||
lineDashOffset: dataset.borderDashOffset,
|
||||
lineJoin: dataset.borderJoinStyle,
|
||||
lineWidth: dataset.borderWidth,
|
||||
strokeStyle: dataset[referenceColor],
|
||||
pointStyle: dataset.pointStyle,
|
||||
datasetIndex: i,
|
||||
};
|
||||
});
|
||||
},
|
||||
};
|
||||
} else {
|
||||
legendOptions.labels = {
|
||||
generateLabels: function (chart) {
|
||||
var data = chart.data;
|
||||
var metaData = data.datasets.map(function (dataset, index) {
|
||||
return chart.getDatasetMeta(index).data;
|
||||
});
|
||||
return data.labels.map(function (label, i) {
|
||||
var hidden = metaData.reduce(
|
||||
function (hidden, data) {
|
||||
if (data[i]) {
|
||||
hidden = hidden || data[i].hidden;
|
||||
}
|
||||
return hidden;
|
||||
},
|
||||
false
|
||||
);
|
||||
var fullText = self._relabelling(label);
|
||||
var text = self._shortenLabel(fullText);
|
||||
return {
|
||||
text: text,
|
||||
fullText: fullText,
|
||||
fillStyle: label.isNoData ? '#d3d3d3' : self._getColor(i),
|
||||
hidden: hidden,
|
||||
index: i,
|
||||
};
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
return legendOptions;
|
||||
},
|
||||
|
||||
_isRedirectionEnabled: function () {
|
||||
return !this.disableLinking &&
|
||||
(this.state.mode === 'bar' || this.state.mode === 'horizontalBar' || this.state.mode === 'pie');
|
||||
},
|
||||
|
||||
/**
|
||||
* Determine how to relabel a label according to a given origin.
|
||||
* The idea is that the getLabel function is in general not invertible but
|
||||
* it is when restricted to the set of dataPoints coming from a same origin.
|
||||
|
||||
* @private
|
||||
* @param {Array} label
|
||||
* @param {Array} originIndex
|
||||
* @returns {string}
|
||||
*/
|
||||
_relabelling: function (label, originIndex) {
|
||||
if (label.isNoData) {
|
||||
return label[0];
|
||||
}
|
||||
var i = this.state.comparisonFieldIndex;
|
||||
if (_.contains(['bar', 'horizontalBar', 'line'], this.state.mode) && i === 0) {
|
||||
// here label is an array of length 1 and contains a number
|
||||
return this.dateClasses.representative(label, originIndex) || '';
|
||||
} else if (this.state.mode === 'pie' && i === 0) {
|
||||
// here label is an array of length at least one containing string or numbers
|
||||
var labelCopy = label.slice(0);
|
||||
if (originIndex !== undefined) {
|
||||
labelCopy.splice(i, 1, this.dateClasses.representative(label[i], originIndex));
|
||||
} else {
|
||||
labelCopy.splice(i, 1, this.dateClasses.dateClassMembers(label[i]));
|
||||
}
|
||||
return labelCopy.join('/');
|
||||
}
|
||||
// here label is an array containing strings or numbers.
|
||||
return label.join('/') || _t('Total');
|
||||
},
|
||||
|
||||
async _renderView() {
|
||||
if (this.chart) {
|
||||
this.chart.destroy();
|
||||
}
|
||||
this.$el.empty();
|
||||
if (!_.contains(CHART_TYPES, this.state.mode)) {
|
||||
this.trigger_up('warning', {
|
||||
title: _t('Invalid mode for chart'),
|
||||
message: _t('Cannot render chart with mode : ') + this.state.mode
|
||||
});
|
||||
}
|
||||
var dataPoints = this._filterDataPoints();
|
||||
dataPoints = this._sortDataPoints(dataPoints);
|
||||
if (this.isInDOM) {
|
||||
this._renderTitle();
|
||||
|
||||
// detect if some pathologies are still present after the filtering
|
||||
if (this.state.mode === 'pie') {
|
||||
const someNegative = dataPoints.some(dataPt => dataPt.value < 0);
|
||||
const somePositive = dataPoints.some(dataPt => dataPt.value > 0);
|
||||
if (someNegative && somePositive) {
|
||||
const context = {
|
||||
title: _t("Invalid data"),
|
||||
description: [
|
||||
_t("Pie chart cannot mix positive and negative numbers. "),
|
||||
_t("Try to change your domain to only display positive results")
|
||||
].join("")
|
||||
};
|
||||
this._renderNoContentHelper(context);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.state.isSample && !this.isEmbedded) {
|
||||
this._renderNoContentHelper();
|
||||
}
|
||||
|
||||
// only render the graph if the widget is already in the DOM (this
|
||||
// happens typically after an update), otherwise, it will be
|
||||
// rendered when the widget will be attached to the DOM (see
|
||||
// 'on_attach_callback')
|
||||
var $canvasContainer = $('<div/>', {class: 'o_graph_canvas_container'});
|
||||
var $canvas = $('<canvas/>').attr('id', this.chartId);
|
||||
$canvasContainer.append($canvas);
|
||||
this.$el.append($canvasContainer);
|
||||
|
||||
var i = this.state.comparisonFieldIndex;
|
||||
if (i === 0) {
|
||||
this.dateClasses = this._getDateClasses(dataPoints);
|
||||
}
|
||||
if (this.state.mode === 'bar') {
|
||||
this._renderBarChart(dataPoints);
|
||||
} else if (this.state.mode === 'horizontalBar') {
|
||||
this._renderhorizontalBarChart(dataPoints);
|
||||
}
|
||||
else if (this.state.mode === 'line') {
|
||||
this._renderLineChart(dataPoints);
|
||||
} else if (this.state.mode === 'pie') {
|
||||
this._renderPieChart(dataPoints);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
_getScaleHBarOptions: function () {
|
||||
var self = this;
|
||||
if (_.contains(['horizontalBar'], this.state.mode)) {
|
||||
return {
|
||||
xAxes: [{
|
||||
type: 'linear',
|
||||
scaleLabel: {
|
||||
display: !this.isEmbedded,
|
||||
labelString: this.fields[this.state.measure].string,
|
||||
},
|
||||
ticks: {
|
||||
callback: this._formatValue.bind(this),
|
||||
suggestedMax: 0,
|
||||
suggestedMin: 0,
|
||||
}
|
||||
}],
|
||||
yAxes: [{
|
||||
type: 'category',
|
||||
scaleLabel: {
|
||||
display: this.state.processedGroupBy.length && !this.isEmbedded,
|
||||
labelString: this.state.processedGroupBy.length ?
|
||||
this.fields[this.state.processedGroupBy[0].split(':')[0]].string : '',
|
||||
},
|
||||
ticks: {
|
||||
// don't use bind: callback is called with 'index' as second parameter
|
||||
// with value labels.indexOf(label)!
|
||||
callback: function (label) {
|
||||
return self._relabelling(label);
|
||||
},
|
||||
},
|
||||
}],
|
||||
};
|
||||
}
|
||||
return {};
|
||||
},
|
||||
|
||||
_animationOptions: function () {
|
||||
var animationOptions = {};
|
||||
var GraphVal = this;
|
||||
|
@ -108,5 +454,79 @@ var FORMAT_OPTIONS = {
|
|||
});
|
||||
},
|
||||
|
||||
_prepare_HBar_Options: function (datasetsCount) {
|
||||
const options = {
|
||||
maintainAspectRatio: false,
|
||||
scales: this._getScaleHBarOptions(),
|
||||
legend: this._getLegendOptions(datasetsCount),
|
||||
tooltips: this._getTooltipOptions(),
|
||||
elements: this._getElementOptions(),
|
||||
//animation: this._animationOptions(),
|
||||
};
|
||||
if (this._isRedirectionEnabled()) {
|
||||
options.onClick = this._onGraphClicked.bind(this);
|
||||
}
|
||||
return options;
|
||||
},
|
||||
|
||||
|
||||
_renderhorizontalBarChart: function (dataPoints) {
|
||||
var self = this;
|
||||
|
||||
// prepare data
|
||||
var data = this._prepareData(dataPoints);
|
||||
|
||||
data.datasets.forEach(function (dataset, index) {
|
||||
// used when stacked
|
||||
dataset.stack = self.state.stacked ? self.state.origins[dataset.originIndex] : undefined;
|
||||
// set dataset color
|
||||
var color = self._getColor(index);
|
||||
dataset.backgroundColor = color;
|
||||
});
|
||||
|
||||
// prepare options
|
||||
var options = this._prepare_HBar_Options(data.datasets.length);
|
||||
// create chart
|
||||
var ctx = document.getElementById(this.chartId);
|
||||
this.chart = new Chart(ctx, {
|
||||
type: 'horizontalBar',
|
||||
data: data,
|
||||
options: options,
|
||||
});
|
||||
},
|
||||
/**
|
||||
* Sort datapoints according to the current order (ASC or DESC).
|
||||
*
|
||||
* Note: this should be moved to the model at some point.
|
||||
*
|
||||
* @private
|
||||
* @param {Object[]} dataPoints
|
||||
* @returns {Object[]} sorted dataPoints if orderby set on state
|
||||
*/
|
||||
_sortDataPoints(dataPoints) {
|
||||
if (!Object.keys(this.state.timeRanges).length && this.state.orderBy &&
|
||||
['bar', 'horizontalBar', 'line'].includes(this.state.mode) && this.state.groupBy.length) {
|
||||
// group data by their x-axis value, and then sort datapoints
|
||||
// based on the sum of values by group in ascending/descending order
|
||||
const groupByFieldName = this.state.groupBy[0].split(':')[0];
|
||||
const groupedByMany2One = this.fields[groupByFieldName].type === 'many2one';
|
||||
const groupedDataPoints = {};
|
||||
dataPoints.forEach(function (dataPoint) {
|
||||
const key = groupedByMany2One ? dataPoint.resId : dataPoint.labels[0];
|
||||
groupedDataPoints[key] = groupedDataPoints[key] || [];
|
||||
groupedDataPoints[key].push(dataPoint);
|
||||
});
|
||||
dataPoints = _.sortBy(groupedDataPoints, function (group) {
|
||||
return group.reduce((sum, dataPoint) => sum + dataPoint.value, 0);
|
||||
});
|
||||
dataPoints = dataPoints.flat();
|
||||
if (this.state.orderBy === 'desc') {
|
||||
dataPoints = dataPoints.reverse('value');
|
||||
}
|
||||
}
|
||||
return dataPoints;
|
||||
},
|
||||
|
||||
|
||||
});
|
||||
});
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
|
||||
<template id="assets_backend" inherit_id="web.assets_backend" priority="99">
|
||||
<xpath expr="." position="inside">
|
||||
<script type="text/javascript" src="/project_report/static/src/js/graph_controller.js" />
|
||||
<script type="text/javascript" src="/project_report/static/src/js/graph_renderer.js" />
|
||||
</xpath>
|
||||
</template>
|
||||
|
|
Loading…
Reference in New Issue