Report updated

This commit is contained in:
projectsodoo 2021-01-25 12:02:34 +05:30
parent b02030d36e
commit 5bbdf18b18
6 changed files with 579 additions and 6 deletions

View File

@ -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):

View File

@ -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,))

View File

@ -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>

View File

@ -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,
}

View File

@ -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;
},
});
});

View File

@ -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>