mirror of
https://github.com/OCA/web.git
synced 2025-02-22 13:21:25 +02:00
[ADD] web_pivot_computed_measure
This commit is contained in:
committed by
CarlosRoca13
parent
07b7590fc0
commit
b9bc3561ce
145
web_pivot_computed_measure/static/src/js/pivot_controller.js
Normal file
145
web_pivot_computed_measure/static/src/js/pivot_controller.js
Normal file
@@ -0,0 +1,145 @@
|
||||
/* Copyright 2020 Tecnativa - Alexandre Díaz
|
||||
* License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) */
|
||||
|
||||
odoo.define('web_pivot_computed_measure.PivotController', function (require) {
|
||||
"use strict";
|
||||
|
||||
var core = require('web.core');
|
||||
var config = require('web.config');
|
||||
var PivotController = require('web.PivotController');
|
||||
|
||||
var QWeb = core.qweb;
|
||||
|
||||
|
||||
PivotController.include({
|
||||
custom_events: {
|
||||
'add_measure': '_onAddMeasure',
|
||||
'remove_measure': '_onRemoveMeasure',
|
||||
},
|
||||
|
||||
computed_measures_open: false,
|
||||
|
||||
/**
|
||||
* Add the computed measures to the context. This
|
||||
* will be used when save a filter.
|
||||
*
|
||||
* @override
|
||||
*/
|
||||
getContext: function () {
|
||||
var res = this._super.apply(this, arguments);
|
||||
var state = this.model.get();
|
||||
res.pivot_computed_measures = state.computed_measures;
|
||||
return res;
|
||||
},
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
renderButtons: function($node) {
|
||||
this._super.apply(this, arguments);
|
||||
if ($node) {
|
||||
this._renderComputedMeasures();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Handle click event on measures menu to support computed measures sub-menu
|
||||
*
|
||||
* @override
|
||||
*/
|
||||
_onButtonClick: function (event) {
|
||||
var $target = $(event.target);
|
||||
if ($target.parents("div[data-id='__computed__']").length) {
|
||||
var hideMenu = false;
|
||||
event.preventDefault();
|
||||
|
||||
if ($target.hasClass('dropdown-item') || $target.hasClass('o_submenu_switcher')) {
|
||||
this.computed_measures_open = !this.computed_measures_open;
|
||||
this._renderComputedMeasures();
|
||||
} else if ($target.hasClass('o_add_computed_measure')) {
|
||||
hideMenu = true;
|
||||
var field1 = this.$buttons_measures_ex.find('#computed_measure_field_1').val();
|
||||
var field2 = this.$buttons_measures_ex.find('#computed_measure_field_2').val();
|
||||
var oper = this.$buttons_measures_ex.find('#computed_measure_operation').val();
|
||||
if (oper === "custom") {
|
||||
oper = this.$buttons_measures_ex.find('#computed_measure_operation_custom').val();
|
||||
}
|
||||
var name = this.$buttons_measures_ex.find('#computed_measure_name').val();
|
||||
var format = this.$buttons_measures_ex.find('#computed_measure_format').val();
|
||||
var uniqueId = (new Date()).getTime();
|
||||
this.model.createComputedMeasure(uniqueId, field1, field2, oper, name, format)
|
||||
.then(this.update.bind(this, {}, {reload: false}));
|
||||
}
|
||||
|
||||
if (!hideMenu) {
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this._super.apply(this, arguments);
|
||||
},
|
||||
|
||||
/**
|
||||
* Render computed measures menu
|
||||
*/
|
||||
_renderComputedMeasures: function() {
|
||||
if (this.$buttons_measures_ex && this.$buttons_measures_ex.length) {
|
||||
this.$buttons_measures_ex.remove();
|
||||
}
|
||||
var self = this;
|
||||
var measures = _.sortBy(_.pairs(_.omit(this.measures, '__count')), function (x) { return x[1].string.toLowerCase(); });
|
||||
this.$buttons_measures_ex = $(QWeb.render('web_pivot_computed_measure.ExtendedMenu', {
|
||||
isOpen: this.computed_measures_open,
|
||||
debug: config.debug,
|
||||
measures: measures,
|
||||
computed_measures: _.map(_.reject(measures, function(item) { return !item[1].__computed_id; }), function(item) {
|
||||
item[1].active = _.contains(self.model.data.measures, item[0]);
|
||||
return item;
|
||||
}),
|
||||
}));
|
||||
this.$buttons_measures_ex.find('#computed_measure_operation').on('change', this._onChangeComputedMeasureOperation.bind(this));
|
||||
this.$buttons.find('.o_pivot_measures_list').append(this.$buttons_measures_ex);
|
||||
},
|
||||
|
||||
/**
|
||||
* Custom event to add a new measure
|
||||
*
|
||||
* @param {CustomEvent} ev
|
||||
*/
|
||||
_onAddMeasure: function(ev) {
|
||||
this.measures[ev.data.id] = ev.data.def;
|
||||
this._renderComputedMeasures();
|
||||
},
|
||||
|
||||
/**
|
||||
* Custom event to remove a measure
|
||||
*
|
||||
* @param {CustomEvent} ev
|
||||
*/
|
||||
_onRemoveMeasure: function(ev) {
|
||||
delete this.measures[ev.data.id];
|
||||
this._renderComputedMeasures();
|
||||
},
|
||||
|
||||
/**
|
||||
* Set default values related with the selected operation
|
||||
*
|
||||
* @param {ChangeEvent} ev
|
||||
*/
|
||||
_onChangeComputedMeasureOperation: function(ev) {
|
||||
var $option = $(ev.target.options[ev.target.selectedIndex]);
|
||||
if ($(ev.target).val() === "custom") {
|
||||
this.$buttons_measures_ex.find('#container_computed_measure_operation_custom').removeClass('d-none');
|
||||
} else {
|
||||
var format = $option.data('format');
|
||||
if (format) {
|
||||
this.$buttons_measures_ex.find('#computed_measure_format').val(format);
|
||||
}
|
||||
this.$buttons_measures_ex.find('#container_computed_measure_operation_custom').addClass('d-none');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
256
web_pivot_computed_measure/static/src/js/pivot_model.js
Normal file
256
web_pivot_computed_measure/static/src/js/pivot_model.js
Normal file
@@ -0,0 +1,256 @@
|
||||
/* Copyright 2020 Tecnativa - Alexandre Díaz
|
||||
* License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) */
|
||||
|
||||
odoo.define('web_pivot_computed_measure.PivotModel', function (require) {
|
||||
"use strict";
|
||||
|
||||
var core = require('web.core');
|
||||
var PivotModel = require('web.PivotModel');
|
||||
|
||||
var _t = core._t;
|
||||
|
||||
|
||||
PivotModel.include({
|
||||
_computed_measures: [],
|
||||
|
||||
/**
|
||||
* Create a new computed measure
|
||||
*
|
||||
* @param {string} id
|
||||
* @param {string} field1
|
||||
* @param {string} field2
|
||||
* @param {string} operation
|
||||
* @param {string} name
|
||||
* @param {string} format
|
||||
*/
|
||||
createComputedMeasure: function(id, field1, field2, operation, name, format) {
|
||||
var measure = _.find(this._computed_measures, function(item) {
|
||||
return item.field1 === field1 && item.field2 === field2 && item.operation === operation;
|
||||
});
|
||||
if (measure) {
|
||||
return $.Deferred(function(d) {
|
||||
d.resolve();
|
||||
});
|
||||
}
|
||||
var fieldM1 = this.fields[field1];
|
||||
var fieldM2 = this.fields[field2];
|
||||
var cmId = '__computed_' + id;
|
||||
var oper = operation.replace(/m1/g, field1).replace(/m2/g, field2);
|
||||
var oper_human = operation.replace(
|
||||
/m1/g,
|
||||
fieldM1.__computed_id?"("+fieldM1.string+")":fieldM1.string).replace(
|
||||
/m2/g,
|
||||
fieldM2.__computed_id?"("+fieldM2.string+")":fieldM2.string);
|
||||
var cmTotal = this._computed_measures.push({
|
||||
field1: field1,
|
||||
field2: field2,
|
||||
operation: oper,
|
||||
name: name || oper_human,
|
||||
id: cmId,
|
||||
format: format,
|
||||
});
|
||||
|
||||
return this._createVirtualMeasure(this._computed_measures[cmTotal-1]);
|
||||
},
|
||||
|
||||
/**
|
||||
* Create and enable a measure based on a 'fake' field
|
||||
*
|
||||
* @param {Object} cmDef
|
||||
* @param {List} fields *Optional*
|
||||
*/
|
||||
_createVirtualMeasure: function(cmDef, fields) {
|
||||
var arrFields = fields || this.fields;
|
||||
// This is a minimal 'fake' field info
|
||||
arrFields[cmDef.id] = {
|
||||
type: cmDef.format, // Used to format the value
|
||||
string: cmDef.name, // Used to print the header name
|
||||
__computed_id: cmDef.id, // Used to know if is a computed measure field
|
||||
}
|
||||
this.trigger_up("add_measure", {
|
||||
id: cmDef.id,
|
||||
def: arrFields[cmDef.id],
|
||||
});
|
||||
return this._activeMeasures([cmDef.field1, cmDef.field2, cmDef.id]);
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {List of Strings} fields
|
||||
*/
|
||||
_activeMeasures: function(fields) {
|
||||
var needLoad = false;
|
||||
var l = fields.length;
|
||||
for (var x = 0; x < l; ++x) {
|
||||
var field = fields[x];
|
||||
if (!this._isMeasureEnabled(field)) {
|
||||
this.data.measures.push(field);
|
||||
needLoad = true;
|
||||
}
|
||||
}
|
||||
if (needLoad) {
|
||||
return this._loadData();
|
||||
}
|
||||
return $.Deferred(function(d) {
|
||||
d.resolve();
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {String} field
|
||||
*/
|
||||
_isMeasureEnabled: function(field) {
|
||||
return _.contains(this.data.measures, field);
|
||||
},
|
||||
|
||||
/**
|
||||
* Fill the dataPoints with the computed measures values
|
||||
*
|
||||
* @override
|
||||
*/
|
||||
_mergeData: function (data, comparisonData, groupBys) {
|
||||
var res = this._super.apply(this, arguments);
|
||||
var l = groupBys.length; // Cached loop (This is not python! hehe)
|
||||
for (var index = 0; index < l; ++index) {
|
||||
if (data.length) {
|
||||
var l2 = data[index].length;
|
||||
for (var k = 0; k < l2; ++k) {
|
||||
var dataPoint = data[index][k];
|
||||
if (_.isEmpty(dataPoint)) {
|
||||
break;
|
||||
}
|
||||
var l3 = this._computed_measures.length;
|
||||
for (var x = 0; x < l3; ++x) {
|
||||
var cm = this._computed_measures[x];
|
||||
if (!this._isMeasureEnabled(cm.id)) {
|
||||
continue;
|
||||
}
|
||||
dataPoint[cm.id] = py.eval(cm.operation, dataPoint);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return res;
|
||||
},
|
||||
|
||||
/**
|
||||
* Load the computed measures in context. This is used by filters.
|
||||
*
|
||||
* @override
|
||||
*/
|
||||
load: function (params) {
|
||||
var self = this;
|
||||
this._computed_measures = params.context.pivot_computed_measures || params.computed_measures || [];
|
||||
var toActive = [];
|
||||
var l = this._computed_measures.length;
|
||||
for (var x = 0; x < l; ++x) {
|
||||
var cmDef = this._computed_measures[x];
|
||||
params.fields[cmDef.id] = {
|
||||
type: cmDef.format,
|
||||
string: cmDef.name,
|
||||
__computed_id: cmDef.id,
|
||||
}
|
||||
toActive.push(cmDef.field1, cmDef.field2, cmDef.id);
|
||||
}
|
||||
return this._super(params).then(function() {
|
||||
_.defer(function() {
|
||||
for (var x = 0; x < l; ++x) {
|
||||
var cmDef = self._computed_measures[x];
|
||||
self.trigger_up("add_measure", {
|
||||
id: cmDef.id,
|
||||
def: self.fields[cmDef.id],
|
||||
});
|
||||
}
|
||||
});
|
||||
self._activeMeasures(toActive);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Load the computed measures in context. This is used by filters.
|
||||
*
|
||||
* @override
|
||||
*/
|
||||
reload: function (handle, params) {
|
||||
if ('context' in params) {
|
||||
this._computed_measures = params.context.pivot_computed_measures || params.computed_measures || [];
|
||||
}
|
||||
var l = this._computed_measures.length;
|
||||
for (var x = 0; x < l; ++x) {
|
||||
this._createVirtualMeasure(this._computed_measures[x]);
|
||||
}
|
||||
// Clean unused 'fake' fields
|
||||
var fieldNames = Object.keys(this.fields);
|
||||
for (var x = 0; x < fieldNames.length; ++x) {
|
||||
var field = this.fields[fieldNames[x]];
|
||||
if (field.__computed_id) {
|
||||
var cm = _.find(this._computed_measures, {id:field.__computed_id});
|
||||
if (!cm) {
|
||||
delete this.fields[fieldNames[x]];
|
||||
this.data.measures = _.without(this.data.measures, fieldNames[x]);
|
||||
this.trigger_up("remove_measure", {
|
||||
id: fieldNames[x],
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
|
||||
/**
|
||||
* Add the computed measures to the state. This is used by filters.
|
||||
*
|
||||
* @override
|
||||
*/
|
||||
get: function () {
|
||||
var res = this._super.apply(this, arguments);
|
||||
res.computed_measures = this._computed_measures;
|
||||
return res;
|
||||
},
|
||||
|
||||
/**
|
||||
* Adds a rule to deny that measures can be disabled if are being used by a computed measure.
|
||||
* In the other hand, when enables a measure analyzes it to active all involved measures.
|
||||
*
|
||||
* @override
|
||||
*/
|
||||
toggleMeasure: function (field) {
|
||||
if (this._isMeasureEnabled(field)) {
|
||||
// Measure is disabled
|
||||
var umeasures = _.filter(this._computed_measures, function(item) {
|
||||
return item.field1 === field || item.field2 === field;
|
||||
})
|
||||
if (umeasures.length && this._isMeasureEnabled(umeasures[0].id)) {
|
||||
return $.Deferred(function(d) {
|
||||
d.reject(_t("This measure is currently used by a 'computed measure'. Please, disable the computed measure first."));
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// Mesaure is enabled
|
||||
var toEnable = [];
|
||||
var toAnalize = [field];
|
||||
while (toAnalize.length) {
|
||||
var afield = toAnalize.shift();
|
||||
var fieldDef = this.fields[afield];
|
||||
if (fieldDef.__computed_id) {
|
||||
var cm = _.find(this._computed_measures, {id:fieldDef.__computed_id});
|
||||
toAnalize.push(cm.field1, cm.field2);
|
||||
var toEnableFields = [];
|
||||
if (!this.fields[cm.field1].__computed_id) {
|
||||
toEnableFields.push(cm.field1);
|
||||
}
|
||||
if (!this.fields[cm.field2].__computed_id) {
|
||||
toEnableFields.push(cm.field2);
|
||||
}
|
||||
toEnableFields.push(afield);
|
||||
toEnable.push(toEnableFields);
|
||||
}
|
||||
}
|
||||
if (toEnable.length) {
|
||||
return this._activeMeasures(_.flatten(toEnable.reverse()));
|
||||
}
|
||||
}
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
});
|
||||
|
||||
});
|
||||
@@ -0,0 +1,102 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
Copyright 2020 Tecnativa - Alexandre Díaz
|
||||
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
|
||||
-->
|
||||
<templates>
|
||||
|
||||
<t t-name="web_pivot_computed_measure.ComputedMeasureOperations">
|
||||
<option name="sum" value="m1+m2">
|
||||
Sum (m1 + m2)
|
||||
</option>
|
||||
<option name="sub" value="m1-m2">
|
||||
Sub (m1 - m2)
|
||||
</option>
|
||||
<option name="mult" value="m1*m2">
|
||||
Mult (m1 * m2)
|
||||
</option>
|
||||
<option name="div" data-format="float" value="m1/m2">
|
||||
Div (m1 / m2)
|
||||
</option>
|
||||
<option name="perc" data-format="percentage" value="m1/m2">
|
||||
Perc (m1 * 100 / m2)
|
||||
</option>
|
||||
<option t-if="debug" name="custom" value="custom">
|
||||
Custom
|
||||
</option>
|
||||
</t>
|
||||
|
||||
<t t-name="web_pivot_computed_measure.ComputedMeasureFormats">
|
||||
<option name="int" value="integer">
|
||||
Integer
|
||||
</option>
|
||||
<option name="float" value="float" selected="selected">
|
||||
Float
|
||||
</option>
|
||||
<option name="percentage" value="percentage">
|
||||
Percentage
|
||||
</option>
|
||||
</t>
|
||||
|
||||
<t t-name="web_pivot_computed_measure.ExtendedMenu">
|
||||
<div role="separator" class="dropdown-divider"/>
|
||||
<t t-foreach="computed_measures" t-as="cm">
|
||||
<a role="menuitem" href="#" t-attf-class="dropdown-item {{cm[1].active and 'selected' or ''}}" t-data-computed="1" t-att-data-field="cm[0]"><t t-esc="cm[1].string"/></a>
|
||||
</t>
|
||||
<div class="o_menu_item" data-id="__computed__">
|
||||
<a href="#" role="menuitem" class="dropdown-item">
|
||||
Computed Measure
|
||||
|
||||
<span class="o_submenu_switcher" data-id="__computed__">
|
||||
<span t-att-class="isOpen ? 'fa fa-caret-down' : 'fa fa-caret-right'"></span>
|
||||
</span>
|
||||
</a>
|
||||
<t t-if="isOpen">
|
||||
<div class="dropdown-item-text">
|
||||
<label for="computed_measure_field_1">Measure 1</label>
|
||||
<select class="o_input o_date_field_selector" id="computed_measure_field_1">
|
||||
<t t-foreach="measures" t-as="measure">
|
||||
<option t-att-value="measure[0]">
|
||||
<t t-esc="measure[1].string" />
|
||||
</option>
|
||||
</t>
|
||||
</select>
|
||||
</div>
|
||||
<div class="dropdown-item-text">
|
||||
<label for="computed_measure_field_2">Measure 2</label>
|
||||
<select class="o_input o_time_range_selector" id="computed_measure_field_2">
|
||||
<t t-foreach="measures" t-as="measure">
|
||||
<option t-att-value="measure[0]">
|
||||
<t t-esc="measure[1].string" />
|
||||
</option>
|
||||
</t>
|
||||
</select>
|
||||
</div>
|
||||
<div class="dropdown-item-text">
|
||||
<label for="computed_measure_operation">Operation</label>
|
||||
<select class="o_input o_time_range_selector" id="computed_measure_operation">
|
||||
<t t-call="web_pivot_computed_measure.ComputedMeasureOperations" />
|
||||
</select>
|
||||
</div>
|
||||
<div t-if="debug" class="dropdown-item-text d-none" id="container_computed_measure_operation_custom">
|
||||
<label for="computed_measure_operation_custom">Formula</label>
|
||||
<input type="text" id="computed_measure_operation_custom" />
|
||||
</div>
|
||||
<div class="dropdown-item-text">
|
||||
<label for="computed_measure_name">Name</label>
|
||||
<input placeholder="Can be empty" type="text" id="computed_measure_name" />
|
||||
</div>
|
||||
<div class="dropdown-item-text">
|
||||
<label for="computed_measure_format">Format</label>
|
||||
<select class="o_input o_time_range_selector" id="computed_measure_format">
|
||||
<t t-call="web_pivot_computed_measure.ComputedMeasureFormats" />
|
||||
</select>
|
||||
</div>
|
||||
<div class="dropdown-item-text">
|
||||
<button class="btn btn-primary o_add_computed_measure" type="button">Add</button>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
Reference in New Issue
Block a user