diff --git a/web_widget_x2many_2d_matrix/README.rst b/web_widget_x2many_2d_matrix/README.rst
new file mode 100644
index 00000000..27a266b9
--- /dev/null
+++ b/web_widget_x2many_2d_matrix/README.rst
@@ -0,0 +1,56 @@
+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 | $value1/1 | $value2/1 |
++-----------+-----------+-----------+
+| $y_value2 | $value1/2 | $value2/2 |
++-----------+-----------+-----------+
+
+where `valuen/n` is editable.
+
+
+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::
+
+
+
+Known issues / Roadmap
+======================
+
+* ...
+
+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 00000000..faef9dac
--- /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 00000000..1cbc4aad
--- /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 00000000..4c7ab302
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/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 00000000..e69de29b
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 00000000..3a754f42
--- /dev/null
+++ b/web_widget_x2many_2d_matrix/static/src/js/web_widget_x2many_2d_matrix.js
@@ -0,0 +1,305 @@
+//-*- 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)
+ {
+ var x = self.get_field_value(row, self.field_x_axis),
+ y = self.get_field_value(row, self.field_y_axis);
+ self.by_x_axis[x] = self.by_x_axis[x] || {};
+ self.by_y_axis[y] = self.by_y_axis[y] || {};
+ self.by_x_axis[x][y] = row;
+ self.by_y_axis[y][x] = 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;
+ });
+ });
+ }));
+ })
+ return jQuery.when.apply(jQuery, deferrends);
+ });
+ });
+ });
+ },
+
+ // 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 x axis labels
+ get_x_axis_labels: function()
+ {
+ var self = this;
+ return _.map(
+ this.get_x_axis_values(),
+ function(val)
+ {
+ return self.get_field_value(
+ _.first(_.values(self.by_x_axis[val])),
+ self.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)
+ {
+ return true;
+ },
+
+ // parse a value from user input
+ parse_xy_value: function(val)
+ {
+ if(this.is_numeric)
+ {
+ return parseFloat(val);
+ }
+ else
+ {
+ return val;
+ }
+ },
+
+ // 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,
+ 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);
+ });
+ }).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));
+ });
+ });
+ },
+
+ start: function()
+ {
+ var self = this;
+ this.$el.find('input').on(
+ 'change',
+ function()
+ {
+ var $this = jQuery(this),
+ val = $this.val()
+ if(self.validate_xy_value(val))
+ {
+ data = {}
+ data[self.field_value] = self.parse_xy_value(val);
+ self.dataset.write($this.data('id'), data);
+ $this.parent().removeClass('oe_form_invalid');
+ self.compute_totals();
+ }
+ else
+ {
+ $this.parent().addClass('oe_form_invalid');
+ }
+
+ });
+ this.compute_totals();
+ return this._super.apply(this, arguments);
+ },
+
+ // 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 00000000..e29367a0
--- /dev/null
+++ b/web_widget_x2many_2d_matrix/static/src/xml/web_widget_x2many_2d_matrix.xml
@@ -0,0 +1,33 @@
+
+
+