diff --git a/web_widget_x2many_2d_matrix/README.rst b/web_widget_x2many_2d_matrix/README.rst
new file mode 100644
index 000000000..0b145aaf6
--- /dev/null
+++ b/web_widget_x2many_2d_matrix/README.rst
@@ -0,0 +1,78 @@
+2D matrix for x2many fields
+===========================
+
+This module allows to show an x2many field with 3-tuples
+($x_value, $y_value, $value) in a table
+
+ $x_value1 $x_value2
+========= =========== ===========
+$y_value1 $value(1/1) $value(2/1)
+$y_value2 $value(1/2) $value(2/2)
+========= =========== ===========
+
+where `value(n/n)` is editable.
+
+An example use case would be: Select some projects and some employees so that
+a manager can easily fill in the planned_hours for one task per employee. The
+result could look like this:
+
+.. image:: /web_widget_x2many_2d_matrix/static/description/screenshot.png
+ :alt: Screenshot
+
+The beauty of this is that you have an arbitrary amount of columns with this widget, trying to get this in standard x2many lists involves some quite agly hacks.
+
+Usage
+=====
+
+Use this widget by saying::
+
+
+
+This assumes that my_field refers to a model with the fields `x`, `y` and
+`value`. If your fields are named differently, pass the correct names as
+attributes::
+
+
+
+You can pass the following parameters:
+
+field_x_axis
+ The field that indicates the x value of a point
+field_y_axis
+ The field that indicates the y value of a point
+field_label_x_axis
+ Use another field to display in the table header
+field_label_y_axis
+ Use another field to display in the table header
+field_value
+ Show this field as value
+show_row_totals
+ If field_value is a numeric field, calculate row totals
+show_column_totals
+ If field_value is a numeric field, calculate column totals
+
+Known issues / Roadmap
+======================
+
+* it would be worth trying to instantiate the proper field widget and let it render the input
+
+Credits
+=======
+
+Contributors
+------------
+
+* Holger Brunn
+
+Maintainer
+----------
+
+.. image:: http://odoo-community.org/logo.png
+ :alt: Odoo Community Association
+ :target: http://odoo-community.org
+
+This module is maintained by the OCA.
+
+OCA, or the Odoo Community Association, is a nonprofit organization whose mission is to support the collaborative development of Odoo features and promote its widespread use.
+
+To contribute to this module, please visit http://odoo-community.org.
diff --git a/web_widget_x2many_2d_matrix/__init__.py b/web_widget_x2many_2d_matrix/__init__.py
new file mode 100644
index 000000000..faef9dac0
--- /dev/null
+++ b/web_widget_x2many_2d_matrix/__init__.py
@@ -0,0 +1,20 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# OpenERP, Open Source Management Solution
+# This module copyright (C) 2015 Therp BV .
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+#
+##############################################################################
diff --git a/web_widget_x2many_2d_matrix/__openerp__.py b/web_widget_x2many_2d_matrix/__openerp__.py
new file mode 100644
index 000000000..1cbc4aad7
--- /dev/null
+++ b/web_widget_x2many_2d_matrix/__openerp__.py
@@ -0,0 +1,45 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# OpenERP, Open Source Management Solution
+# This module copyright (C) 2015 Therp BV .
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+#
+##############################################################################
+{
+ "name": "2D matrix for x2many fields",
+ "version": "1.0",
+ "author": "Therp BV",
+ "license": "AGPL-3",
+ "category": "Hidden/Dependency",
+ "summary": "Show list fields as a matrix",
+ "depends": [
+ 'web',
+ ],
+ "data": [
+ 'views/templates.xml',
+ ],
+ "qweb": [
+ 'static/src/xml/web_widget_x2many_2d_matrix.xml',
+ ],
+ "test": [
+ ],
+ "auto_install": False,
+ "installable": True,
+ "application": False,
+ "external_dependencies": {
+ 'python': [],
+ },
+}
diff --git a/web_widget_x2many_2d_matrix/static/description/icon.png b/web_widget_x2many_2d_matrix/static/description/icon.png
new file mode 100644
index 000000000..d7cdcec3b
Binary files /dev/null and b/web_widget_x2many_2d_matrix/static/description/icon.png differ
diff --git a/web_widget_x2many_2d_matrix/static/description/screenshot.png b/web_widget_x2many_2d_matrix/static/description/screenshot.png
new file mode 100644
index 000000000..47c2a40d6
Binary files /dev/null and b/web_widget_x2many_2d_matrix/static/description/screenshot.png differ
diff --git a/web_widget_x2many_2d_matrix/static/src/css/web_widget_x2many_2d_matrix.css b/web_widget_x2many_2d_matrix/static/src/css/web_widget_x2many_2d_matrix.css
new file mode 100644
index 000000000..d33d4f21b
--- /dev/null
+++ b/web_widget_x2many_2d_matrix/static/src/css/web_widget_x2many_2d_matrix.css
@@ -0,0 +1,8 @@
+.oe_form_field_x2many_2d_matrix th.oe_link
+{
+ cursor: pointer;
+}
+.openerp .oe_form_field_x2many_2d_matrix .oe_list_content > tbody > tr > td.oe_list_field_cell
+{
+ white-space: normal;
+}
diff --git a/web_widget_x2many_2d_matrix/static/src/js/web_widget_x2many_2d_matrix.js b/web_widget_x2many_2d_matrix/static/src/js/web_widget_x2many_2d_matrix.js
new file mode 100644
index 000000000..5d4ce7854
--- /dev/null
+++ b/web_widget_x2many_2d_matrix/static/src/js/web_widget_x2many_2d_matrix.js
@@ -0,0 +1,380 @@
+//-*- coding: utf-8 -*-
+//############################################################################
+//
+// OpenERP, Open Source Management Solution
+// This module copyright (C) 2015 Therp BV .
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+//
+//############################################################################
+
+openerp.web_widget_x2many_2d_matrix = function(instance)
+{
+ instance.web.form.widgets.add(
+ 'x2many_2d_matrix',
+ 'instance.web_widget_x2many_2d_matrix.FieldX2Many2dMatrix');
+ instance.web_widget_x2many_2d_matrix.FieldX2Many2dMatrix = instance.web.form.FieldOne2Many.extend({
+ template: 'FieldX2Many2dMatrix',
+ widget_class: 'oe_form_field_x2many_2d_matrix',
+
+ // those will be filled with rows from the dataset
+ by_x_axis: {},
+ by_y_axis: {},
+ field_x_axis: 'x',
+ field_label_x_axis: 'x',
+ field_y_axis: 'y',
+ field_label_y_axis: 'y',
+ field_value: 'value',
+ // 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: {},
+
+ // 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.field_value = node.attrs.field_value || this.field_value;
+ this.show_row_totals = node.attrs.show_row_totals || this.show_row_totals;
+ this.show_column_totals = node.attrs.show_column_totals || this.show_column_totals;
+ return this._super.apply(this, arguments);
+ },
+
+ // 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()
+ {
+ var self = this,
+ result = this._super.apply(this, arguments);
+
+ self.by_x_axis = {};
+ self.by_y_axis = {};
+
+ return jQuery.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;
+ }).then(function()
+ {
+ return self.dataset.read_ids(self.dataset.ids).then(function(rows)
+ {
+ var read_many2one = {},
+ many2one_fields = [
+ self.field_x_axis, self.field_y_axis,
+ self.field_label_x_axis, self.field_label_y_axis
+ ];
+ // prepare to read many2one names if necessary (we can get (id, name) or just id as value)
+ _.each(many2one_fields, function(field)
+ {
+ if(self.fields[field].type == 'many2one')
+ {
+ read_many2one[field] = {};
+ }
+ });
+ // setup data structure
+ _.each(rows, function(row)
+ {
+ self.add_xy_row(row);
+ _.each(read_many2one, function(rows, field)
+ {
+ if(!_.isArray(row[field]))
+ {
+ rows[row[field]] = rows[row[field]] || []
+ rows[row[field]].push(row);
+ }
+ });
+ });
+ // read many2one fields if necessary
+ var deferrends = [];
+ _.each(read_many2one, function(rows, field)
+ {
+ if(_.isEmpty(rows))
+ {
+ return;
+ }
+ var model = new instance.web.Model(self.fields[field].relation);
+ deferrends.push(model.call(
+ 'name_get',
+ [_.map(_.keys(rows), function(key) {return parseInt(key)})])
+ .then(function(names)
+ {
+ _.each(names, function(name)
+ {
+ _.each(rows[name[0]], function(row)
+ {
+ row[field] = name;
+ });
+ });
+ }));
+ })
+ if(self.is_started && !self.no_rerender)
+ {
+ self.renderElement();
+ self.compute_totals();
+ self.setup_many2one_axes();
+ self.$el.find('.edit').on(
+ 'change', self.proxy(self.xy_value_change));
+ self.effective_readonly_change();
+ }
+ return jQuery.when.apply(jQuery, deferrends);
+ });
+ });
+ });
+ },
+
+ // to 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);
+ 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;
+ },
+
+ // 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'];
+ },
+
+ // 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 instance.web.parse_value(
+ val, {'type': this.fields[this.field_value].type});
+ },
+
+ // format a value from the database for display
+ format_xy_value: function(val)
+ {
+ return instance.web.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 = {};
+ return self.dataset.read_ids(self.dataset.ids).then(function(rows)
+ {
+ _.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);
+ });
+ }).then(function()
+ {
+ _.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))
+ return {
+ totals_x: totals_x,
+ totals_y: totals_y,
+ grand_total: grand_total,
+ };
+ });
+ },
+
+ setup_many2one_axes: function()
+ {
+ if(this.fields[this.field_x_axis].type == 'many2one')
+ {
+ 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.$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: jQuery(e.currentTarget).data(id_attribute),
+ views: [[false, 'form']],
+ target: 'current',
+ })
+ },
+
+ start: function()
+ {
+ var self = this;
+ this.$el.find('.edit').on(
+ 'change', self.proxy(this.xy_value_change));
+ 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.apply(this, arguments);
+ },
+
+ xy_value_change: function(e)
+ {
+ var $this = jQuery(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;
+ },
+
+ // deactivate view related functions
+ load_views: function() {},
+ reload_current_view: function() {},
+ get_active_view: function() {},
+ });
+}
diff --git a/web_widget_x2many_2d_matrix/static/src/xml/web_widget_x2many_2d_matrix.xml b/web_widget_x2many_2d_matrix/static/src/xml/web_widget_x2many_2d_matrix.xml
new file mode 100644
index 000000000..35f1669bc
--- /dev/null
+++ b/web_widget_x2many_2d_matrix/static/src/xml/web_widget_x2many_2d_matrix.xml
@@ -0,0 +1,36 @@
+
+
+