You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
414 lines
14 KiB
414 lines
14 KiB
/* Copyright 2015 Holger Brunn <hbrunn@therp.nl>
|
|
* Copyright 2016 Pedro M. Baeza <pedro.baeza@tecnativa.com>
|
|
* License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). */
|
|
|
|
odoo.define('web_widget_x2many_2d_matrix.widget', function (require) {
|
|
"use strict";
|
|
|
|
var core = require('web.core');
|
|
var formats = require('web.formats');
|
|
var FieldOne2Many = core.form_widget_registry.get('one2many');
|
|
var Model = require('web.Model');
|
|
var data = require('web.data');
|
|
var _ = require('_');
|
|
var $ = require('$');
|
|
|
|
var WidgetX2Many2dMatrix = FieldOne2Many.extend({
|
|
template: 'FieldX2Many2dMatrix',
|
|
widget_class: 'oe_form_field_x2many_2d_matrix',
|
|
events: {
|
|
"change .edit": "xy_value_change",
|
|
},
|
|
|
|
// those will be filled with rows from the dataset
|
|
by_x_axis: {},
|
|
by_y_axis: {},
|
|
by_id: {},
|
|
// configuration values
|
|
field_x_axis: 'x',
|
|
field_label_x_axis: 'x',
|
|
field_y_axis: 'y',
|
|
field_label_y_axis: 'y',
|
|
field_value: 'value',
|
|
x_axis_clickable: true,
|
|
y_axis_clickable: true,
|
|
// information about our datatype
|
|
is_numeric: false,
|
|
show_row_totals: true,
|
|
show_column_totals: true,
|
|
// this will be filled with the model's fields_get
|
|
fields: {},
|
|
// Store fields used to fill HTML attributes
|
|
fields_att: {},
|
|
|
|
parse_boolean: function(val)
|
|
{
|
|
if (val.toLowerCase() === 'true' || val === '1') {
|
|
return true;
|
|
}
|
|
return false;
|
|
},
|
|
|
|
// read parameters
|
|
init: function(field_manager, node)
|
|
{
|
|
this.field_x_axis = node.attrs.field_x_axis || this.field_x_axis;
|
|
this.field_y_axis = node.attrs.field_y_axis || this.field_y_axis;
|
|
this.field_label_x_axis = node.attrs.field_label_x_axis || this.field_x_axis;
|
|
this.field_label_y_axis = node.attrs.field_label_y_axis || this.field_y_axis;
|
|
this.x_axis_clickable = this.parse_boolean(node.attrs.x_axis_clickable || '1');
|
|
this.y_axis_clickable = this.parse_boolean(node.attrs.y_axis_clickable || '1');
|
|
this.field_value = node.attrs.field_value || this.field_value;
|
|
for (var property in node.attrs) {
|
|
if (property.startsWith("field_att_")) {
|
|
this.fields_att[property.substring(10)] = node.attrs[property];
|
|
}
|
|
}
|
|
this.field_editability = node.attrs.field_editability || this.field_editability;
|
|
this.show_row_totals = this.parse_boolean(node.attrs.show_row_totals || '1');
|
|
this.show_column_totals = this.parse_boolean(node.attrs.show_column_totals || '1');
|
|
return this._super(field_manager, node);
|
|
},
|
|
|
|
// return a field's value, id in case it's a one2many field
|
|
get_field_value: function(row, field, many2one_as_name)
|
|
{
|
|
if(this.fields[field].type == 'many2one' && _.isArray(row[field]))
|
|
{
|
|
if(many2one_as_name)
|
|
{
|
|
return row[field][1];
|
|
}
|
|
else
|
|
{
|
|
return row[field][0];
|
|
}
|
|
}
|
|
return row[field];
|
|
},
|
|
|
|
// setup our datastructure for simple access in the template
|
|
set_value: function(value_)
|
|
{
|
|
var self = this,
|
|
result = this._super(value_);
|
|
|
|
self.by_x_axis = {};
|
|
self.by_y_axis = {};
|
|
self.by_id = {};
|
|
|
|
return $.when(result).then(function()
|
|
{
|
|
return self.dataset._model.call('fields_get').then(function(fields)
|
|
{
|
|
self.fields = fields;
|
|
self.is_numeric = fields[self.field_value].type == 'float';
|
|
self.show_row_totals &= self.is_numeric;
|
|
self.show_column_totals &= self.is_numeric;
|
|
})
|
|
// if there are cached writes on the parent dataset, read below
|
|
// only returns the written data, which is not enough to properly
|
|
// set up our data structure. Read those ids here and patch the
|
|
// cache
|
|
.then(function()
|
|
{
|
|
var ids_written = _.map(
|
|
self.dataset.to_write, function(x) { return x.id });
|
|
if(!ids_written.length)
|
|
{
|
|
return;
|
|
}
|
|
return (new data.Query(self.dataset._model))
|
|
.filter([['id', 'in', ids_written]])
|
|
.all()
|
|
.then(function(rows)
|
|
{
|
|
_.each(rows, function(row)
|
|
{
|
|
var cache = _.find(
|
|
self.dataset.cache,
|
|
function(x) { return x.id == row.id }
|
|
);
|
|
_.extend(cache.values, row, _.clone(cache.values));
|
|
})
|
|
})
|
|
})
|
|
.then(function()
|
|
{
|
|
return self.dataset.read_ids(self.dataset.ids).then(function(rows)
|
|
{
|
|
// setup data structure
|
|
_.each(rows, function(row)
|
|
{
|
|
self.add_xy_row(row);
|
|
});
|
|
if(self.is_started && !self.no_rerender)
|
|
{
|
|
self.renderElement();
|
|
self.compute_totals();
|
|
self.setup_many2one_axes();
|
|
self.effective_readonly_change();
|
|
}
|
|
});
|
|
});
|
|
});
|
|
},
|
|
|
|
// do whatever needed to setup internal data structure
|
|
add_xy_row: function(row)
|
|
{
|
|
var x = this.get_field_value(row, this.field_x_axis),
|
|
y = this.get_field_value(row, this.field_y_axis);
|
|
// row is a *copy* of a row in dataset.cache, fetch
|
|
// a reference to this row in order to have the
|
|
// internal data structure point to the same data
|
|
// the dataset manipulates
|
|
_.every(this.dataset.cache, function(cached_row)
|
|
{
|
|
if(cached_row.id == row.id)
|
|
{
|
|
row = cached_row.values;
|
|
// new rows don't have that
|
|
row.id = cached_row.id;
|
|
return false;
|
|
}
|
|
return true;
|
|
});
|
|
this.by_x_axis[x] = this.by_x_axis[x] || {};
|
|
this.by_y_axis[y] = this.by_y_axis[y] || {};
|
|
this.by_x_axis[x][y] = row;
|
|
this.by_y_axis[y][x] = row;
|
|
this.by_id[row.id] = row;
|
|
},
|
|
|
|
// get x axis values in the correct order
|
|
get_x_axis_values: function()
|
|
{
|
|
return _.keys(this.by_x_axis);
|
|
},
|
|
|
|
// get y axis values in the correct order
|
|
get_y_axis_values: function()
|
|
{
|
|
return _.keys(this.by_y_axis);
|
|
},
|
|
|
|
// get the label for a value on the x axis
|
|
get_x_axis_label: function(x)
|
|
{
|
|
return this.get_field_value(
|
|
_.first(_.values(this.by_x_axis[x])),
|
|
this.field_label_x_axis, true);
|
|
},
|
|
|
|
// get the label for a value on the y axis
|
|
get_y_axis_label: function(y)
|
|
{
|
|
return this.get_field_value(
|
|
_.first(_.values(this.by_y_axis[y])),
|
|
this.field_label_y_axis, true);
|
|
},
|
|
|
|
// return the class(es) the inputs should have
|
|
get_xy_value_class: function()
|
|
{
|
|
var classes = 'oe_form_field oe_form_required';
|
|
if(this.is_numeric)
|
|
{
|
|
classes += ' oe_form_field_float';
|
|
}
|
|
return classes;
|
|
},
|
|
|
|
// return row id of a coordinate
|
|
get_xy_id: function(x, y)
|
|
{
|
|
return this.by_x_axis[x][y]['id'];
|
|
},
|
|
|
|
get_xy_att: function(x, y)
|
|
{
|
|
var vals = {};
|
|
for (var att in this.fields_att) {
|
|
var val = this.get_field_value(
|
|
this.by_x_axis[x][y], this.fields_att[att]);
|
|
// Discard empty values
|
|
if (val) {
|
|
vals[att] = val;
|
|
}
|
|
}
|
|
return vals;
|
|
},
|
|
|
|
// return the value of a coordinate
|
|
get_xy_value: function(x, y)
|
|
{
|
|
return this.get_field_value(
|
|
this.by_x_axis[x][y], this.field_value);
|
|
},
|
|
|
|
// validate a value
|
|
validate_xy_value: function(val)
|
|
{
|
|
try
|
|
{
|
|
this.parse_xy_value(val);
|
|
}
|
|
catch(e)
|
|
{
|
|
return false;
|
|
}
|
|
return true;
|
|
},
|
|
|
|
// parse a value from user input
|
|
parse_xy_value: function(val)
|
|
{
|
|
return formats.parse_value(
|
|
val, {'type': this.fields[this.field_value].type});
|
|
},
|
|
|
|
// format a value from the database for display
|
|
format_xy_value: function(val)
|
|
{
|
|
return formats.format_value(
|
|
val, {'type': this.fields[this.field_value].type});
|
|
},
|
|
|
|
// compute totals
|
|
compute_totals: function()
|
|
{
|
|
var self = this,
|
|
grand_total = 0,
|
|
totals_x = {},
|
|
totals_y = {},
|
|
rows = this.by_id,
|
|
deferred = $.Deferred();
|
|
_.each(rows, function(row)
|
|
{
|
|
var key_x = self.get_field_value(row, self.field_x_axis),
|
|
key_y = self.get_field_value(row, self.field_y_axis);
|
|
totals_x[key_x] = (totals_x[key_x] || 0) + self.get_field_value(row, self.field_value);
|
|
totals_y[key_y] = (totals_y[key_y] || 0) + self.get_field_value(row, self.field_value);
|
|
grand_total += self.get_field_value(row, self.field_value);
|
|
});
|
|
_.each(totals_y, function(total, y)
|
|
{
|
|
self.$el.find(
|
|
_.str.sprintf('td.row_total[data-y="%s"]', y)).text(
|
|
self.format_xy_value(total));
|
|
});
|
|
_.each(totals_x, function(total, x)
|
|
{
|
|
self.$el.find(
|
|
_.str.sprintf('td.column_total[data-x="%s"]', x)).text(
|
|
self.format_xy_value(total));
|
|
});
|
|
self.$el.find('.grand_total').text(
|
|
self.format_xy_value(grand_total))
|
|
deferred.resolve({
|
|
totals_x: totals_x,
|
|
totals_y: totals_y,
|
|
grand_total: grand_total,
|
|
rows: rows,
|
|
});
|
|
return deferred;
|
|
},
|
|
|
|
setup_many2one_axes: function()
|
|
{
|
|
if(this.fields[this.field_x_axis].type == 'many2one' && this.x_axis_clickable)
|
|
{
|
|
this.$el.find('th[data-x]').addClass('oe_link')
|
|
.click(_.partial(
|
|
this.proxy(this.many2one_axis_click),
|
|
this.field_x_axis, 'x'));
|
|
}
|
|
if(this.fields[this.field_y_axis].type == 'many2one' && this.y_axis_clickable)
|
|
{
|
|
this.$el.find('tr[data-y] th').addClass('oe_link')
|
|
.click(_.partial(
|
|
this.proxy(this.many2one_axis_click),
|
|
this.field_y_axis, 'y'));
|
|
}
|
|
},
|
|
|
|
many2one_axis_click: function(field, id_attribute, e)
|
|
{
|
|
this.do_action({
|
|
type: 'ir.actions.act_window',
|
|
name: this.fields[field].string,
|
|
res_model: this.fields[field].relation,
|
|
res_id: $(e.currentTarget).data(id_attribute),
|
|
views: [[false, 'form']],
|
|
target: 'current',
|
|
})
|
|
},
|
|
|
|
start: function()
|
|
{
|
|
var self = this;
|
|
this.compute_totals();
|
|
this.setup_many2one_axes();
|
|
this.on("change:effective_readonly",
|
|
this, this.proxy(this.effective_readonly_change));
|
|
this.effective_readonly_change();
|
|
return this._super();
|
|
},
|
|
|
|
xy_value_change: function(e)
|
|
{
|
|
var $this = $(e.currentTarget),
|
|
val = $this.val();
|
|
if(this.validate_xy_value(val))
|
|
{
|
|
var data = {}, value = this.parse_xy_value(val);
|
|
data[this.field_value] = value;
|
|
|
|
$this.siblings('.read').text(this.format_xy_value(value));
|
|
$this.val(this.format_xy_value(value));
|
|
|
|
this.dataset.write($this.data('id'), data);
|
|
$this.parent().removeClass('oe_form_invalid');
|
|
this.compute_totals();
|
|
}
|
|
else
|
|
{
|
|
$this.parent().addClass('oe_form_invalid');
|
|
}
|
|
|
|
},
|
|
|
|
effective_readonly_change: function()
|
|
{
|
|
this.$el
|
|
.find('tbody td.oe_list_field_cell span.oe_form_field .edit')
|
|
.toggle(!this.get('effective_readonly'));
|
|
this.$el
|
|
.find('tbody td.oe_list_field_cell span.oe_form_field .read')
|
|
.toggle(this.get('effective_readonly'));
|
|
this.$el.find('.edit').first().focus();
|
|
},
|
|
|
|
is_syntax_valid: function()
|
|
{
|
|
return this.$el.find('.oe_form_invalid').length == 0;
|
|
},
|
|
|
|
load_views: function() {
|
|
// Needed for removing the initial empty tree view when the widget
|
|
// is loaded
|
|
var self = this,
|
|
result = this._super();
|
|
|
|
return $.when(result).then(function()
|
|
{
|
|
self.renderElement();
|
|
});
|
|
},
|
|
});
|
|
|
|
core.form_widget_registry.add('x2many_2d_matrix', WidgetX2Many2dMatrix);
|
|
|
|
return WidgetX2Many2dMatrix;
|
|
});
|