diff --git a/web_widget_x2many_2d_matrix/README.rst b/web_widget_x2many_2d_matrix/README.rst
new file mode 100644
index 000000000..2f503f3d1
--- /dev/null
+++ b/web_widget_x2many_2d_matrix/README.rst
@@ -0,0 +1,237 @@
+===========================
+2D matrix for x2many fields
+===========================
+
+.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+ !! This file is generated by oca-gen-addon-readme !!
+ !! changes will be overwritten. !!
+ !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+
+.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png
+ :target: https://odoo-community.org/page/development-status
+ :alt: Beta
+.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png
+ :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
+ :alt: License: AGPL-3
+.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fweb-lightgray.png?logo=github
+ :target: https://github.com/OCA/web/tree/13.0/web_widget_x2many_2d_matrix
+ :alt: OCA/web
+.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
+ :target: https://translation.odoo-community.org/projects/web-13-0/web-13-0-web_widget_x2many_2d_matrix
+ :alt: Translate me on Weblate
+.. |badge5| image:: https://img.shields.io/badge/runbot-Try%20me-875A7B.png
+ :target: https://runbot.odoo-community.org/runbot/162/13.0
+ :alt: Try me on Runbot
+
+|badge1| |badge2| |badge3| |badge4| |badge5|
+
+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:: https://raw.githubusercontent.com/OCA/web/12.0/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 ugly
+hacks.
+
+**Table of contents**
+
+.. contents::
+ :local:
+
+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:
+
+.. code-block:: xml
+
+
+
+
+
+
+
+
+
+
+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, it indicates if you want to calculate
+ row totals. True by default
+show_column_totals
+ If field_value is a numeric field, it indicates if you want to calculate
+ column totals. True by default
+
+Example
+~~~~~~~
+
+You need a data structure already filled with values. Let's assume we want to
+use this widget in a wizard that lets the user fill in planned hours for one
+task per project per user. In this case, we can use ``project.task`` as our
+data model and point to it from our wizard. The crucial part is that we fill
+the field in the default function:
+
+.. code-block:: python
+
+ from odoo import fields, models
+
+ class MyWizard(models.TransientModel):
+ _name = 'my.wizard'
+
+ def _default_task_ids(self):
+ # your list of project should come from the context, some selection
+ # in a previous wizard or wherever else
+ projects = self.env['project.project'].browse([1, 2, 3])
+ # same with users
+ users = self.env['res.users'].browse([1, 2, 3])
+ return [
+ (0, 0, {
+ 'name': 'Sample task name',
+ 'project_id': p.id,
+ 'user_id': u.id,
+ 'planned_hours': 0,
+ 'message_needaction': False,
+ 'date_deadline': fields.Date.today(),
+ })
+ # if the project doesn't have a task for the user,
+ # create a new one
+ if not p.task_ids.filtered(lambda x: x.user_id == u) else
+ # otherwise, return the task
+ (4, p.task_ids.filtered(lambda x: x.user_id == u)[0].id)
+ for p in projects
+ for u in users
+ ]
+
+ task_ids = fields.Many2many('project.task', default=_default_task_ids)
+
+Now in our wizard, we can use:
+
+.. code-block:: xml
+
+
+
+
+
+
+
+
+
+
+Known issues / Roadmap
+======================
+
+* Support extra attributes on each field cell via `field_extra_attrs` param.
+ We could set a cell as not editable, required or readonly for instance.
+ The `readonly` case will also give the ability
+ to click on m2o to open related records.
+
+* Support limit total records in the matrix. Ref: https://github.com/OCA/web/issues/901
+
+* Support cell traversal through keyboard arrows.
+
+* Entering the widget from behind by pressing ``Shift+TAB`` in your keyboard
+ will enter into the 1st cell until https://github.com/odoo/odoo/pull/26490
+ is merged.
+
+* Support extra invisible fields inside each cell.
+
+* Support kanban mode. Current behaviour forces list mode.
+
+Changelog
+=========
+
+12.0.1.0.1 (2018-12-07)
+~~~~~~~~~~~~~~~~~~~~~~~
+
+* [FIX] Cells are unable to render property.
+ (`#1126 `_)
+
+12.0.1.0.0 (2018-11-20)
+~~~~~~~~~~~~~~~~~~~~~~~
+
+* [12.0][MIG] web_widget_x2many_2d_matrix
+ (`#1101 `_)
+
+Bug Tracker
+===========
+
+Bugs are tracked on `GitHub Issues `_.
+In case of trouble, please check there if your issue has already been reported.
+If you spotted it first, help us smashing it by providing a detailed and welcomed
+`feedback `_.
+
+Do not contact contributors directly about support or help with technical issues.
+
+Credits
+=======
+
+Authors
+~~~~~~~
+
+* Therp BV
+* Tecnativa
+* Camptocamp
+* Brainbean Apps
+
+Contributors
+~~~~~~~~~~~~
+
+* Holger Brunn
+* Pedro M. Baeza
+* Artem Kostyuk
+* Simone Orsi
+* Timon Tschanz
+* Jairo Llopis
+* Dennis Sluijk
+* Alexey Pelykh
+* Adrià Gil Sorribes
+
+Maintainers
+~~~~~~~~~~~
+
+This module is maintained by the OCA.
+
+.. image:: https://odoo-community.org/logo.png
+ :alt: Odoo Community Association
+ :target: https://odoo-community.org
+
+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.
+
+This module is part of the `OCA/web `_ project on GitHub.
+
+You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.
diff --git a/web_widget_x2many_2d_matrix/__init__.py b/web_widget_x2many_2d_matrix/__init__.py
new file mode 100644
index 000000000..ef5ae3587
--- /dev/null
+++ b/web_widget_x2many_2d_matrix/__init__.py
@@ -0,0 +1 @@
+# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
diff --git a/web_widget_x2many_2d_matrix/__manifest__.py b/web_widget_x2many_2d_matrix/__manifest__.py
new file mode 100644
index 000000000..d0e3fde8b
--- /dev/null
+++ b/web_widget_x2many_2d_matrix/__manifest__.py
@@ -0,0 +1,22 @@
+# Copyright 2015 Holger Brunn
+# Copyright 2016 Pedro M. Baeza
+# Copyright 2018 Simone Orsi
+# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
+{
+ "name": "2D matrix for x2many fields",
+ "version": "13.0.1.0.0",
+ "author": (
+ "Therp BV, "
+ "Tecnativa, "
+ "Camptocamp, "
+ "Brainbean Apps, "
+ "Odoo Community Association (OCA)"
+ ),
+ "website": "https://github.com/OCA/web",
+ "license": "AGPL-3",
+ "category": "Hidden/Dependency",
+ "summary": "Show list fields as a matrix",
+ "depends": ["web"],
+ "data": ["views/assets.xml"],
+ "installable": True,
+}
diff --git a/web_widget_x2many_2d_matrix/i18n/ar.po b/web_widget_x2many_2d_matrix/i18n/ar.po
new file mode 100644
index 000000000..c0f18b7f1
--- /dev/null
+++ b/web_widget_x2many_2d_matrix/i18n/ar.po
@@ -0,0 +1,43 @@
+# Translation of Odoo Server.
+# This file contains the translation of the following modules:
+# * web_widget_x2many_2d_matrix
+#
+# Translators:
+# SaFi J. , 2015
+msgid ""
+msgstr ""
+"Project-Id-Version: web (8.0)\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2015-12-16 07:41+0000\n"
+"PO-Revision-Date: 2015-12-16 17:24+0000\n"
+"Last-Translator: SaFi J. \n"
+"Language-Team: Arabic (http://www.transifex.com/oca/OCA-web-8-0/language/"
+"ar/)\n"
+"Language: ar\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: \n"
+"Plural-Forms: nplurals=6; plural=n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 "
+"&& n%100<=10 ? 3 : n%100>=11 && n%100<=99 ? 4 : 5;\n"
+
+#. module: web_widget_x2many_2d_matrix
+#. openerp-web
+#: code:addons/web_widget_x2many_2d_matrix/static/src/js/2d_matrix_renderer.js:65
+#, python-format
+msgid "Sorry no matrix data to display."
+msgstr ""
+
+#. module: web_widget_x2many_2d_matrix
+#. openerp-web
+#: code:addons/web_widget_x2many_2d_matrix/static/src/js/2d_matrix_renderer.js:400
+#: code:addons/web_widget_x2many_2d_matrix/static/src/js/2d_matrix_renderer.js:475
+#, python-format
+msgid "Sum"
+msgstr ""
+
+#. module: web_widget_x2many_2d_matrix
+#. openerp-web
+#: code:addons/web_widget_x2many_2d_matrix/static/src/js/2d_matrix_renderer.js:394
+#, fuzzy, python-format
+msgid "Sum Total"
+msgstr "المجموع الاجمالي"
diff --git a/web_widget_x2many_2d_matrix/i18n/de.po b/web_widget_x2many_2d_matrix/i18n/de.po
new file mode 100644
index 000000000..2482e2c85
--- /dev/null
+++ b/web_widget_x2many_2d_matrix/i18n/de.po
@@ -0,0 +1,42 @@
+# Translation of Odoo Server.
+# This file contains the translation of the following modules:
+# * web_widget_x2many_2d_matrix
+#
+# Translators:
+# Rudolf Schnapka , 2016
+msgid ""
+msgstr ""
+"Project-Id-Version: web (8.0)\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2016-01-10 07:31+0000\n"
+"PO-Revision-Date: 2016-01-18 20:15+0000\n"
+"Last-Translator: Rudolf Schnapka \n"
+"Language-Team: German (http://www.transifex.com/oca/OCA-web-8-0/language/"
+"de/)\n"
+"Language: de\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: \n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#. module: web_widget_x2many_2d_matrix
+#. openerp-web
+#: code:addons/web_widget_x2many_2d_matrix/static/src/js/2d_matrix_renderer.js:65
+#, python-format
+msgid "Sorry no matrix data to display."
+msgstr ""
+
+#. module: web_widget_x2many_2d_matrix
+#. openerp-web
+#: code:addons/web_widget_x2many_2d_matrix/static/src/js/2d_matrix_renderer.js:400
+#: code:addons/web_widget_x2many_2d_matrix/static/src/js/2d_matrix_renderer.js:475
+#, python-format
+msgid "Sum"
+msgstr ""
+
+#. module: web_widget_x2many_2d_matrix
+#. openerp-web
+#: code:addons/web_widget_x2many_2d_matrix/static/src/js/2d_matrix_renderer.js:394
+#, fuzzy, python-format
+msgid "Sum Total"
+msgstr "Gesamt"
diff --git a/web_widget_x2many_2d_matrix/i18n/es.po b/web_widget_x2many_2d_matrix/i18n/es.po
new file mode 100644
index 000000000..1bcc69039
--- /dev/null
+++ b/web_widget_x2many_2d_matrix/i18n/es.po
@@ -0,0 +1,41 @@
+# Translation of Odoo Server.
+# This file contains the translation of the following modules:
+# * web_widget_x2many_2d_matrix
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: web (8.0)\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2015-11-23 13:46+0000\n"
+"PO-Revision-Date: 2015-11-07 11:29+0000\n"
+"Last-Translator: Pedro M. Baeza \n"
+"Language-Team: Spanish (http://www.transifex.com/oca/OCA-web-8-0/language/"
+"es/)\n"
+"Language: es\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: \n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#. module: web_widget_x2many_2d_matrix
+#. openerp-web
+#: code:addons/web_widget_x2many_2d_matrix/static/src/js/2d_matrix_renderer.js:65
+#, python-format
+msgid "Sorry no matrix data to display."
+msgstr ""
+
+#. module: web_widget_x2many_2d_matrix
+#. openerp-web
+#: code:addons/web_widget_x2many_2d_matrix/static/src/js/2d_matrix_renderer.js:400
+#: code:addons/web_widget_x2many_2d_matrix/static/src/js/2d_matrix_renderer.js:475
+#, python-format
+msgid "Sum"
+msgstr ""
+
+#. module: web_widget_x2many_2d_matrix
+#. openerp-web
+#: code:addons/web_widget_x2many_2d_matrix/static/src/js/2d_matrix_renderer.js:394
+#, fuzzy, python-format
+msgid "Sum Total"
+msgstr "Total"
diff --git a/web_widget_x2many_2d_matrix/i18n/fi.po b/web_widget_x2many_2d_matrix/i18n/fi.po
new file mode 100644
index 000000000..81516261f
--- /dev/null
+++ b/web_widget_x2many_2d_matrix/i18n/fi.po
@@ -0,0 +1,42 @@
+# Translation of Odoo Server.
+# This file contains the translation of the following modules:
+# * web_widget_x2many_2d_matrix
+#
+# Translators:
+# Jarmo Kortetjärvi , 2016
+msgid ""
+msgstr ""
+"Project-Id-Version: web (8.0)\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2016-01-10 07:31+0000\n"
+"PO-Revision-Date: 2016-02-01 09:54+0000\n"
+"Last-Translator: Jarmo Kortetjärvi \n"
+"Language-Team: Finnish (http://www.transifex.com/oca/OCA-web-8-0/language/"
+"fi/)\n"
+"Language: fi\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: \n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#. module: web_widget_x2many_2d_matrix
+#. openerp-web
+#: code:addons/web_widget_x2many_2d_matrix/static/src/js/2d_matrix_renderer.js:65
+#, python-format
+msgid "Sorry no matrix data to display."
+msgstr ""
+
+#. module: web_widget_x2many_2d_matrix
+#. openerp-web
+#: code:addons/web_widget_x2many_2d_matrix/static/src/js/2d_matrix_renderer.js:400
+#: code:addons/web_widget_x2many_2d_matrix/static/src/js/2d_matrix_renderer.js:475
+#, python-format
+msgid "Sum"
+msgstr ""
+
+#. module: web_widget_x2many_2d_matrix
+#. openerp-web
+#: code:addons/web_widget_x2many_2d_matrix/static/src/js/2d_matrix_renderer.js:394
+#, fuzzy, python-format
+msgid "Sum Total"
+msgstr "Yhteensä"
diff --git a/web_widget_x2many_2d_matrix/i18n/fr.po b/web_widget_x2many_2d_matrix/i18n/fr.po
new file mode 100644
index 000000000..e473d0379
--- /dev/null
+++ b/web_widget_x2many_2d_matrix/i18n/fr.po
@@ -0,0 +1,42 @@
+# Translation of Odoo Server.
+# This file contains the translation of the following modules:
+# * web_widget_x2many_2d_matrix
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: web (8.0)\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2016-05-06 15:50+0000\n"
+"PO-Revision-Date: 2019-08-06 12:44+0000\n"
+"Last-Translator: Nicolas JEUDY \n"
+"Language-Team: French (http://www.transifex.com/oca/OCA-web-8-0/language/fr/)"
+"\n"
+"Language: fr\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: \n"
+"Plural-Forms: nplurals=2; plural=n > 1;\n"
+"X-Generator: Weblate 3.7.1\n"
+
+#. module: web_widget_x2many_2d_matrix
+#. openerp-web
+#: code:addons/web_widget_x2many_2d_matrix/static/src/js/2d_matrix_renderer.js:65
+#, python-format
+msgid "Sorry no matrix data to display."
+msgstr "Désolé il n'y a pas de donnée matrice à afficher."
+
+#. module: web_widget_x2many_2d_matrix
+#. openerp-web
+#: code:addons/web_widget_x2many_2d_matrix/static/src/js/2d_matrix_renderer.js:400
+#: code:addons/web_widget_x2many_2d_matrix/static/src/js/2d_matrix_renderer.js:475
+#, python-format
+msgid "Sum"
+msgstr "Somme"
+
+#. module: web_widget_x2many_2d_matrix
+#. openerp-web
+#: code:addons/web_widget_x2many_2d_matrix/static/src/js/2d_matrix_renderer.js:394
+#, python-format
+msgid "Sum Total"
+msgstr "Total"
diff --git a/web_widget_x2many_2d_matrix/i18n/hr.po b/web_widget_x2many_2d_matrix/i18n/hr.po
new file mode 100644
index 000000000..214fbea23
--- /dev/null
+++ b/web_widget_x2many_2d_matrix/i18n/hr.po
@@ -0,0 +1,44 @@
+# Translation of Odoo Server.
+# This file contains the translation of the following modules:
+# * web_widget_x2many_2d_matrix
+#
+# Translators:
+# Ana-Maria Olujić , 2016
+msgid ""
+msgstr ""
+"Project-Id-Version: web (8.0)\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2016-08-25 00:51+0000\n"
+"PO-Revision-Date: 2019-11-14 10:34+0000\n"
+"Last-Translator: Bole \n"
+"Language-Team: Croatian (http://www.transifex.com/oca/OCA-web-8-0/language/"
+"hr/)\n"
+"Language: hr\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: \n"
+"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<="
+"4 && (n%100<10 || n%100>=20) ? 1 : 2;\n"
+"X-Generator: Weblate 3.8\n"
+
+#. module: web_widget_x2many_2d_matrix
+#. openerp-web
+#: code:addons/web_widget_x2many_2d_matrix/static/src/js/2d_matrix_renderer.js:65
+#, python-format
+msgid "Sorry no matrix data to display."
+msgstr "Oprostite, nema matrice podataka za prikaz."
+
+#. module: web_widget_x2many_2d_matrix
+#. openerp-web
+#: code:addons/web_widget_x2many_2d_matrix/static/src/js/2d_matrix_renderer.js:400
+#: code:addons/web_widget_x2many_2d_matrix/static/src/js/2d_matrix_renderer.js:475
+#, python-format
+msgid "Sum"
+msgstr "Suma"
+
+#. module: web_widget_x2many_2d_matrix
+#. openerp-web
+#: code:addons/web_widget_x2many_2d_matrix/static/src/js/2d_matrix_renderer.js:394
+#, python-format
+msgid "Sum Total"
+msgstr "Ukupno"
diff --git a/web_widget_x2many_2d_matrix/i18n/it.po b/web_widget_x2many_2d_matrix/i18n/it.po
new file mode 100644
index 000000000..052122935
--- /dev/null
+++ b/web_widget_x2many_2d_matrix/i18n/it.po
@@ -0,0 +1,42 @@
+# Translation of Odoo Server.
+# This file contains the translation of the following modules:
+# * web_widget_x2many_2d_matrix
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: web (8.0)\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2016-03-17 07:30+0000\n"
+"PO-Revision-Date: 2019-05-03 11:03+0000\n"
+"Last-Translator: gslabit \n"
+"Language-Team: Italian (http://www.transifex.com/oca/OCA-web-8-0/language/"
+"it/)\n"
+"Language: it\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: \n"
+"Plural-Forms: nplurals=2; plural=n != 1;\n"
+"X-Generator: Weblate 3.5.1\n"
+
+#. module: web_widget_x2many_2d_matrix
+#. openerp-web
+#: code:addons/web_widget_x2many_2d_matrix/static/src/js/2d_matrix_renderer.js:65
+#, python-format
+msgid "Sorry no matrix data to display."
+msgstr "Spiacenti, nessun dato da visualizzare."
+
+#. module: web_widget_x2many_2d_matrix
+#. openerp-web
+#: code:addons/web_widget_x2many_2d_matrix/static/src/js/2d_matrix_renderer.js:400
+#: code:addons/web_widget_x2many_2d_matrix/static/src/js/2d_matrix_renderer.js:475
+#, python-format
+msgid "Sum"
+msgstr "Somma"
+
+#. module: web_widget_x2many_2d_matrix
+#. openerp-web
+#: code:addons/web_widget_x2many_2d_matrix/static/src/js/2d_matrix_renderer.js:394
+#, python-format
+msgid "Sum Total"
+msgstr "Totale"
diff --git a/web_widget_x2many_2d_matrix/i18n/lt.po b/web_widget_x2many_2d_matrix/i18n/lt.po
new file mode 100644
index 000000000..eb6bdc747
--- /dev/null
+++ b/web_widget_x2many_2d_matrix/i18n/lt.po
@@ -0,0 +1,42 @@
+# Translation of Odoo Server.
+# This file contains the translation of the following modules:
+# * web_widget_x2many_2d_matrix
+#
+# Translators:
+# Viktoras Norkus , 2018
+msgid ""
+msgstr ""
+"Project-Id-Version: Odoo Server 11.0\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2018-01-25 01:58+0000\n"
+"PO-Revision-Date: 2018-02-15 12:40+0200\n"
+"Last-Translator: Viktoras Norkus , 2018\n"
+"Language-Team: Lithuanian (https://www.transifex.com/oca/teams/23907/lt/)\n"
+"Language: lt\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: \n"
+"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && (n"
+"%100<10 || n%100>=20) ? 1 : 2);\n"
+
+#. module: web_widget_x2many_2d_matrix
+#. openerp-web
+#: code:addons/web_widget_x2many_2d_matrix/static/src/js/2d_matrix_renderer.js:65
+#, python-format
+msgid "Sorry no matrix data to display."
+msgstr ""
+
+#. module: web_widget_x2many_2d_matrix
+#. openerp-web
+#: code:addons/web_widget_x2many_2d_matrix/static/src/js/2d_matrix_renderer.js:400
+#: code:addons/web_widget_x2many_2d_matrix/static/src/js/2d_matrix_renderer.js:475
+#, python-format
+msgid "Sum"
+msgstr ""
+
+#. module: web_widget_x2many_2d_matrix
+#. openerp-web
+#: code:addons/web_widget_x2many_2d_matrix/static/src/js/2d_matrix_renderer.js:394
+#, fuzzy, python-format
+msgid "Sum Total"
+msgstr "Suma"
diff --git a/web_widget_x2many_2d_matrix/i18n/nl_NL.po b/web_widget_x2many_2d_matrix/i18n/nl_NL.po
new file mode 100644
index 000000000..71f8d8726
--- /dev/null
+++ b/web_widget_x2many_2d_matrix/i18n/nl_NL.po
@@ -0,0 +1,42 @@
+# Translation of Odoo Server.
+# This file contains the translation of the following modules:
+# * web_widget_x2many_2d_matrix
+#
+# Translators:
+# Peter Hageman , 2017
+msgid ""
+msgstr ""
+"Project-Id-Version: Odoo Server 11.0\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2018-01-03 03:50+0000\n"
+"PO-Revision-Date: 2018-02-15 12:39+0200\n"
+"Last-Translator: Peter Hageman , 2017\n"
+"Language-Team: Dutch (Netherlands) (https://www.transifex.com/oca/"
+"teams/23907/nl_NL/)\n"
+"Language: nl_NL\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: \n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#. module: web_widget_x2many_2d_matrix
+#. openerp-web
+#: code:addons/web_widget_x2many_2d_matrix/static/src/js/2d_matrix_renderer.js:65
+#, python-format
+msgid "Sorry no matrix data to display."
+msgstr ""
+
+#. module: web_widget_x2many_2d_matrix
+#. openerp-web
+#: code:addons/web_widget_x2many_2d_matrix/static/src/js/2d_matrix_renderer.js:400
+#: code:addons/web_widget_x2many_2d_matrix/static/src/js/2d_matrix_renderer.js:475
+#, python-format
+msgid "Sum"
+msgstr ""
+
+#. module: web_widget_x2many_2d_matrix
+#. openerp-web
+#: code:addons/web_widget_x2many_2d_matrix/static/src/js/2d_matrix_renderer.js:394
+#, fuzzy, python-format
+msgid "Sum Total"
+msgstr "Totaal"
diff --git a/web_widget_x2many_2d_matrix/i18n/pt_BR.po b/web_widget_x2many_2d_matrix/i18n/pt_BR.po
new file mode 100644
index 000000000..86d0f57b3
--- /dev/null
+++ b/web_widget_x2many_2d_matrix/i18n/pt_BR.po
@@ -0,0 +1,42 @@
+# Translation of Odoo Server.
+# This file contains the translation of the following modules:
+# * web_widget_x2many_2d_matrix
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: web (8.0)\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2016-03-11 02:18+0000\n"
+"PO-Revision-Date: 2019-09-03 01:23+0000\n"
+"Last-Translator: Rodrigo Macedo \n"
+"Language-Team: Portuguese (Brazil) (http://www.transifex.com/oca/OCA-web-8-0/"
+"language/pt_BR/)\n"
+"Language: pt_BR\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: \n"
+"Plural-Forms: nplurals=2; plural=n > 1;\n"
+"X-Generator: Weblate 3.8\n"
+
+#. module: web_widget_x2many_2d_matrix
+#. openerp-web
+#: code:addons/web_widget_x2many_2d_matrix/static/src/js/2d_matrix_renderer.js:65
+#, python-format
+msgid "Sorry no matrix data to display."
+msgstr "Desculpe não há dados de matriz para exibir."
+
+#. module: web_widget_x2many_2d_matrix
+#. openerp-web
+#: code:addons/web_widget_x2many_2d_matrix/static/src/js/2d_matrix_renderer.js:400
+#: code:addons/web_widget_x2many_2d_matrix/static/src/js/2d_matrix_renderer.js:475
+#, python-format
+msgid "Sum"
+msgstr "Soma"
+
+#. module: web_widget_x2many_2d_matrix
+#. openerp-web
+#: code:addons/web_widget_x2many_2d_matrix/static/src/js/2d_matrix_renderer.js:394
+#, python-format
+msgid "Sum Total"
+msgstr "Soma Total"
diff --git a/web_widget_x2many_2d_matrix/i18n/sl.po b/web_widget_x2many_2d_matrix/i18n/sl.po
new file mode 100644
index 000000000..b99c46ebf
--- /dev/null
+++ b/web_widget_x2many_2d_matrix/i18n/sl.po
@@ -0,0 +1,42 @@
+# Translation of Odoo Server.
+# This file contains the translation of the following modules:
+# * web_widget_x2many_2d_matrix
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: web (8.0)\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2015-11-23 13:46+0000\n"
+"PO-Revision-Date: 2015-11-08 05:48+0000\n"
+"Last-Translator: Matjaž Mozetič \n"
+"Language-Team: Slovenian (http://www.transifex.com/oca/OCA-web-8-0/language/"
+"sl/)\n"
+"Language: sl\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: \n"
+"Plural-Forms: nplurals=4; plural=(n%100==1 ? 0 : n%100==2 ? 1 : n%100==3 || n"
+"%100==4 ? 2 : 3);\n"
+
+#. module: web_widget_x2many_2d_matrix
+#. openerp-web
+#: code:addons/web_widget_x2many_2d_matrix/static/src/js/2d_matrix_renderer.js:65
+#, python-format
+msgid "Sorry no matrix data to display."
+msgstr ""
+
+#. module: web_widget_x2many_2d_matrix
+#. openerp-web
+#: code:addons/web_widget_x2many_2d_matrix/static/src/js/2d_matrix_renderer.js:400
+#: code:addons/web_widget_x2many_2d_matrix/static/src/js/2d_matrix_renderer.js:475
+#, python-format
+msgid "Sum"
+msgstr ""
+
+#. module: web_widget_x2many_2d_matrix
+#. openerp-web
+#: code:addons/web_widget_x2many_2d_matrix/static/src/js/2d_matrix_renderer.js:394
+#, fuzzy, python-format
+msgid "Sum Total"
+msgstr "Skupaj"
diff --git a/web_widget_x2many_2d_matrix/i18n/tr.po b/web_widget_x2many_2d_matrix/i18n/tr.po
new file mode 100644
index 000000000..20ef19466
--- /dev/null
+++ b/web_widget_x2many_2d_matrix/i18n/tr.po
@@ -0,0 +1,42 @@
+# Translation of Odoo Server.
+# This file contains the translation of the following modules:
+# * web_widget_x2many_2d_matrix
+#
+# Translators:
+# Ahmet Altınışık , 2015
+msgid ""
+msgstr ""
+"Project-Id-Version: web (8.0)\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2016-01-08 21:34+0000\n"
+"PO-Revision-Date: 2015-12-30 22:00+0000\n"
+"Last-Translator: Ahmet Altınışık \n"
+"Language-Team: Turkish (http://www.transifex.com/oca/OCA-web-8-0/language/"
+"tr/)\n"
+"Language: tr\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: \n"
+"Plural-Forms: nplurals=2; plural=(n > 1);\n"
+
+#. module: web_widget_x2many_2d_matrix
+#. openerp-web
+#: code:addons/web_widget_x2many_2d_matrix/static/src/js/2d_matrix_renderer.js:65
+#, python-format
+msgid "Sorry no matrix data to display."
+msgstr ""
+
+#. module: web_widget_x2many_2d_matrix
+#. openerp-web
+#: code:addons/web_widget_x2many_2d_matrix/static/src/js/2d_matrix_renderer.js:400
+#: code:addons/web_widget_x2many_2d_matrix/static/src/js/2d_matrix_renderer.js:475
+#, python-format
+msgid "Sum"
+msgstr ""
+
+#. module: web_widget_x2many_2d_matrix
+#. openerp-web
+#: code:addons/web_widget_x2many_2d_matrix/static/src/js/2d_matrix_renderer.js:394
+#, fuzzy, python-format
+msgid "Sum Total"
+msgstr "Toplam"
diff --git a/web_widget_x2many_2d_matrix/i18n/web_widget_x2many_2d_matrix.pot b/web_widget_x2many_2d_matrix/i18n/web_widget_x2many_2d_matrix.pot
new file mode 100644
index 000000000..0171482c8
--- /dev/null
+++ b/web_widget_x2many_2d_matrix/i18n/web_widget_x2many_2d_matrix.pot
@@ -0,0 +1,37 @@
+# Translation of Odoo Server.
+# This file contains the translation of the following modules:
+# * web_widget_x2many_2d_matrix
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Odoo Server 12.0\n"
+"Report-Msgid-Bugs-To: \n"
+"Last-Translator: <>\n"
+"Language-Team: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: \n"
+"Plural-Forms: \n"
+
+#. module: web_widget_x2many_2d_matrix
+#. openerp-web
+#: code:addons/web_widget_x2many_2d_matrix/static/src/js/2d_matrix_renderer.js:65
+#, python-format
+msgid "Sorry no matrix data to display."
+msgstr ""
+
+#. module: web_widget_x2many_2d_matrix
+#. openerp-web
+#: code:addons/web_widget_x2many_2d_matrix/static/src/js/2d_matrix_renderer.js:400
+#: code:addons/web_widget_x2many_2d_matrix/static/src/js/2d_matrix_renderer.js:475
+#, python-format
+msgid "Sum"
+msgstr ""
+
+#. module: web_widget_x2many_2d_matrix
+#. openerp-web
+#: code:addons/web_widget_x2many_2d_matrix/static/src/js/2d_matrix_renderer.js:394
+#, python-format
+msgid "Sum Total"
+msgstr ""
+
diff --git a/web_widget_x2many_2d_matrix/i18n/zh_CN.po b/web_widget_x2many_2d_matrix/i18n/zh_CN.po
new file mode 100644
index 000000000..783e9d43d
--- /dev/null
+++ b/web_widget_x2many_2d_matrix/i18n/zh_CN.po
@@ -0,0 +1,39 @@
+# Translation of Odoo Server.
+# This file contains the translation of the following modules:
+# * web_widget_x2many_2d_matrix
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Odoo Server 12.0\n"
+"Report-Msgid-Bugs-To: \n"
+"PO-Revision-Date: 2019-09-01 17:23+0000\n"
+"Last-Translator: 黎伟杰 <674416404@qq.com>\n"
+"Language-Team: none\n"
+"Language: zh_CN\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: \n"
+"Plural-Forms: nplurals=1; plural=0;\n"
+"X-Generator: Weblate 3.8\n"
+
+#. module: web_widget_x2many_2d_matrix
+#. openerp-web
+#: code:addons/web_widget_x2many_2d_matrix/static/src/js/2d_matrix_renderer.js:65
+#, python-format
+msgid "Sorry no matrix data to display."
+msgstr "抱歉没有要显示的矩阵数据。"
+
+#. module: web_widget_x2many_2d_matrix
+#. openerp-web
+#: code:addons/web_widget_x2many_2d_matrix/static/src/js/2d_matrix_renderer.js:400
+#: code:addons/web_widget_x2many_2d_matrix/static/src/js/2d_matrix_renderer.js:475
+#, python-format
+msgid "Sum"
+msgstr "总和"
+
+#. module: web_widget_x2many_2d_matrix
+#. openerp-web
+#: code:addons/web_widget_x2many_2d_matrix/static/src/js/2d_matrix_renderer.js:394
+#, python-format
+msgid "Sum Total"
+msgstr "总和"
diff --git a/web_widget_x2many_2d_matrix/readme/CONTRIBUTORS.rst b/web_widget_x2many_2d_matrix/readme/CONTRIBUTORS.rst
new file mode 100644
index 000000000..88111d553
--- /dev/null
+++ b/web_widget_x2many_2d_matrix/readme/CONTRIBUTORS.rst
@@ -0,0 +1,9 @@
+* Holger Brunn
+* Pedro M. Baeza
+* Artem Kostyuk
+* Simone Orsi
+* Timon Tschanz
+* Jairo Llopis
+* Dennis Sluijk
+* Alexey Pelykh
+* Adrià Gil Sorribes
diff --git a/web_widget_x2many_2d_matrix/readme/DESCRIPTION.rst b/web_widget_x2many_2d_matrix/readme/DESCRIPTION.rst
new file mode 100644
index 000000000..d3f10cae8
--- /dev/null
+++ b/web_widget_x2many_2d_matrix/readme/DESCRIPTION.rst
@@ -0,0 +1,23 @@
+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:: https://raw.githubusercontent.com/OCA/web/12.0/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 ugly
+hacks.
diff --git a/web_widget_x2many_2d_matrix/readme/HISTORY.rst b/web_widget_x2many_2d_matrix/readme/HISTORY.rst
new file mode 100644
index 000000000..8bad8c46c
--- /dev/null
+++ b/web_widget_x2many_2d_matrix/readme/HISTORY.rst
@@ -0,0 +1,11 @@
+12.0.1.0.1 (2018-12-07)
+~~~~~~~~~~~~~~~~~~~~~~~
+
+* [FIX] Cells are unable to render property.
+ (`#1126 `_)
+
+12.0.1.0.0 (2018-11-20)
+~~~~~~~~~~~~~~~~~~~~~~~
+
+* [12.0][MIG] web_widget_x2many_2d_matrix
+ (`#1101 `_)
diff --git a/web_widget_x2many_2d_matrix/readme/ROADMAP.rst b/web_widget_x2many_2d_matrix/readme/ROADMAP.rst
new file mode 100644
index 000000000..1dc1a84da
--- /dev/null
+++ b/web_widget_x2many_2d_matrix/readme/ROADMAP.rst
@@ -0,0 +1,16 @@
+* Support extra attributes on each field cell via `field_extra_attrs` param.
+ We could set a cell as not editable, required or readonly for instance.
+ The `readonly` case will also give the ability
+ to click on m2o to open related records.
+
+* Support limit total records in the matrix. Ref: https://github.com/OCA/web/issues/901
+
+* Support cell traversal through keyboard arrows.
+
+* Entering the widget from behind by pressing ``Shift+TAB`` in your keyboard
+ will enter into the 1st cell until https://github.com/odoo/odoo/pull/26490
+ is merged.
+
+* Support extra invisible fields inside each cell.
+
+* Support kanban mode. Current behaviour forces list mode.
diff --git a/web_widget_x2many_2d_matrix/readme/USAGE.rst b/web_widget_x2many_2d_matrix/readme/USAGE.rst
new file mode 100644
index 000000000..514257d00
--- /dev/null
+++ b/web_widget_x2many_2d_matrix/readme/USAGE.rst
@@ -0,0 +1,92 @@
+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:
+
+.. code-block:: xml
+
+
+
+
+
+
+
+
+
+
+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, it indicates if you want to calculate
+ row totals. True by default
+show_column_totals
+ If field_value is a numeric field, it indicates if you want to calculate
+ column totals. True by default
+
+Example
+~~~~~~~
+
+You need a data structure already filled with values. Let's assume we want to
+use this widget in a wizard that lets the user fill in planned hours for one
+task per project per user. In this case, we can use ``project.task`` as our
+data model and point to it from our wizard. The crucial part is that we fill
+the field in the default function:
+
+.. code-block:: python
+
+ from odoo import fields, models
+
+ class MyWizard(models.TransientModel):
+ _name = 'my.wizard'
+
+ def _default_task_ids(self):
+ # your list of project should come from the context, some selection
+ # in a previous wizard or wherever else
+ projects = self.env['project.project'].browse([1, 2, 3])
+ # same with users
+ users = self.env['res.users'].browse([1, 2, 3])
+ return [
+ (0, 0, {
+ 'name': 'Sample task name',
+ 'project_id': p.id,
+ 'user_id': u.id,
+ 'planned_hours': 0,
+ 'message_needaction': False,
+ 'date_deadline': fields.Date.today(),
+ })
+ # if the project doesn't have a task for the user,
+ # create a new one
+ if not p.task_ids.filtered(lambda x: x.user_id == u) else
+ # otherwise, return the task
+ (4, p.task_ids.filtered(lambda x: x.user_id == u)[0].id)
+ for p in projects
+ for u in users
+ ]
+
+ task_ids = fields.Many2many('project.task', default=_default_task_ids)
+
+Now in our wizard, we can use:
+
+.. code-block:: xml
+
+
+
+
+
+
+
+
+
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..a501fbf83
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/index.html b/web_widget_x2many_2d_matrix/static/description/index.html
new file mode 100644
index 000000000..8b501c4b2
--- /dev/null
+++ b/web_widget_x2many_2d_matrix/static/description/index.html
@@ -0,0 +1,595 @@
+
+
+
+
+
+
+2D matrix for x2many fields
+
+
+
+
+
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:
+
+
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 ugly
+hacks.
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 need a data structure already filled with values. Let’s assume we want to
+use this widget in a wizard that lets the user fill in planned hours for one
+task per project per user. In this case, we can use project.task as our
+data model and point to it from our wizard. The crucial part is that we fill
+the field in the default function:
+
+fromodooimportfields,models
+
+classMyWizard(models.TransientModel):
+ _name='my.wizard'
+
+ def_default_task_ids(self):
+ # your list of project should come from the context, some selection
+ # in a previous wizard or wherever else
+ projects=self.env['project.project'].browse([1,2,3])
+ # same with users
+ users=self.env['res.users'].browse([1,2,3])
+ return[
+ (0,0,{
+ 'name':'Sample task name',
+ 'project_id':p.id,
+ 'user_id':u.id,
+ 'planned_hours':0,
+ 'message_needaction':False,
+ 'date_deadline':fields.Date.today(),
+ })
+ # if the project doesn't have a task for the user,
+ # create a new one
+ ifnotp.task_ids.filtered(lambdax:x.user_id==u)else
+ # otherwise, return the task
+ (4,p.task_ids.filtered(lambdax:x.user_id==u)[0].id)
+ forpinprojects
+ foruinusers
+ ]
+
+ task_ids=fields.Many2many('project.task',default=_default_task_ids)
+
Support extra attributes on each field cell via field_extra_attrs param.
+We could set a cell as not editable, required or readonly for instance.
+The readonly case will also give the ability
+to click on m2o to open related records.
Entering the widget from behind by pressing Shift+TAB in your keyboard
+will enter into the 1st cell until https://github.com/odoo/odoo/pull/26490
+is merged.
+
Support extra invisible fields inside each cell.
+
Support kanban mode. Current behaviour forces list mode.
Bugs are tracked on GitHub Issues.
+In case of trouble, please check there if your issue has already been reported.
+If you spotted it first, help us smashing it by providing a detailed and welcomed
+feedback.
+
Do not contact contributors directly about support or help with technical issues.
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.
+
This module is part of the OCA/web project on GitHub.
+
+
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..4b75baa8a
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/js/2d_matrix_renderer.js b/web_widget_x2many_2d_matrix/static/src/js/2d_matrix_renderer.js
new file mode 100644
index 000000000..e0bf72f28
--- /dev/null
+++ b/web_widget_x2many_2d_matrix/static/src/js/2d_matrix_renderer.js
@@ -0,0 +1,622 @@
+/* Copyright 2018 Simone Orsi
+ * Copyright 2018 Brainbean Apps
+ * License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). */
+
+odoo.define("web_widget_x2many_2d_matrix.X2Many2dMatrixRenderer", function(require) {
+ "use strict";
+
+ var BasicRenderer = require("web.BasicRenderer");
+ var config = require("web.config");
+ var core = require("web.core");
+ var field_utils = require("web.field_utils");
+ var utils = require("web.utils");
+ var _t = core._t;
+
+ var FIELD_CLASSES = {
+ float: "o_list_number",
+ integer: "o_list_number",
+ monetary: "o_list_number",
+ text: "o_list_text",
+ };
+
+ // X2Many2dMatrixRenderer is heavily inspired by Odoo's ListRenderer
+ // and is reusing portions of code from list_renderer.js
+ var X2Many2dMatrixRenderer = BasicRenderer.extend({
+ /**
+ * @override
+ */
+ init: function(parent, state, params) {
+ this._super.apply(this, arguments);
+ this.editable = params.editable;
+ this._saveMatrixData(params.matrix_data);
+ },
+
+ /**
+ * Update matrix data in current renderer instance.
+ *
+ * @param {Object} matrixData Contains the matrix data
+ */
+ _saveMatrixData: function(matrixData) {
+ this.columns = matrixData.columns;
+ this.rows = matrixData.rows;
+ this.matrix_data = matrixData;
+ },
+
+ /**
+ * Main render function for the matrix widget.
+ *
+ * It is rendered as a table. For now,
+ * this method does not wait for the field widgets to be ready.
+ *
+ * @override
+ * @private
+ * @returns {Deferred} this deferred is resolved immediately
+ */
+ _renderView: function() {
+ var self = this;
+
+ this.$el.removeClass("table-responsive").empty();
+
+ // Display a nice message if there's no data to display
+ if (!self.rows.length) {
+ var $alert = $("
", {class: "alert alert-info"});
+ $alert.text(_t("Sorry no matrix data to display."));
+ this.$el.append($alert);
+ return this._super();
+ }
+
+ var $table = $("
").addClass(
+ "o_list_view table table-condensed table-striped " +
+ "o_x2many_2d_matrix "
+ );
+ this.$el.addClass("table-responsive").append($table);
+
+ this._computeColumnAggregates();
+ this._computeRowAggregates();
+
+ // We need to initialize the deferred list object for inherited functions that use this.defs even if it
+ // is empty at the moment.
+ var defs = [];
+ this.defs = defs;
+
+ $table.append(this._renderHeader()).append(this._renderBody());
+ if (self.matrix_data.show_column_totals) {
+ $table.append(this._renderFooter());
+ }
+ delete this.defs;
+ return this._super();
+ },
+
+ /**
+ * Render the table body.
+ *
+ * Looks for the table body and renders the rows in it.
+ * Also it sets the tabindex on every input element.
+ *
+ * @private
+ * @returns {jQueryElement} The table body element just filled.
+ */
+ _renderBody: function() {
+ var $body = $("").append(this._renderRows());
+ _.each($body.find("input"), function(td, i) {
+ $(td).attr("tabindex", i);
+ });
+ return $body;
+ },
+
+ /**
+ * Render the table head of our matrix. Looks for the first table head
+ * and inserts the header into it.
+ *
+ * @private
+ * @returns {jQueryElement} The thead element that was inserted into.
+ */
+ _renderHeader: function() {
+ var $tr = $("
", {class: "total"}));
+ }
+ return $("").append($tr);
+ },
+
+ /**
+ * Render a single header cell.
+ *
+ * Creates a th and adds the description as text.
+ *
+ * @private
+ * @param {jQueryElement} node
+ * @returns {jQueryElement} the created
node.
+ */
+ _renderHeaderCell: function(node) {
+ var name = node.attrs.name;
+ var field = this.state.fields[name];
+ var $th = $("
");
+ if (!field) {
+ return $th;
+ }
+ var description = null;
+ if (node.attrs.widget) {
+ description = this.state.fieldsInfo.list[name].Widget.prototype
+ .description;
+ }
+ if (_.isNull(description)) {
+ description = node.attrs.string || field.string;
+ }
+ $th.text(description).data("name", name);
+
+ if (
+ field.type === "float" ||
+ field.type === "integer" ||
+ field.type === "monetary"
+ ) {
+ $th.addClass("text-right");
+ } else {
+ $th.addClass("text-center");
+ }
+
+ if (config.debug) {
+ var fieldDescr = {
+ field: field,
+ name: name,
+ string: description || name,
+ record: this.state,
+ attrs: node.attrs,
+ };
+ this._addFieldTooltip(fieldDescr, $th);
+ }
+ return $th;
+ },
+
+ /**
+ * Proxy call to function rendering single row.
+ *
+ * @private
+ * @returns {String} a string with the generated html.
+ */
+ _renderRows: function() {
+ return _.map(
+ this.rows,
+ function(row) {
+ row.attrs.name = this.matrix_data.field_value;
+ return this._renderRow(row);
+ }.bind(this)
+ );
+ },
+
+ /**
+ * Render a single row with all its columns.
+ * Renders all the cells and then wraps them with a
.
+ * If aggregate is set on the row it also will generate
+ * the aggregate cell.
+ *
+ * @private
+ * @param {Object} row The row that will be rendered.
+ * @returns {jQueryElement} the
element that has been rendered.
+ */
+ _renderRow: function(row) {
+ var $tr = $("
", {class: "o_data_row"}),
+ _data = _.without(row.data, undefined);
+ $tr = $tr.append(this._renderLabelCell(_data[0]));
+ var $cells = this.columns.map(
+ function(column, index) {
+ var record = row.data[index];
+ // Make the widget use our field value for each cell
+ column.attrs.name = this.matrix_data.field_value;
+ return this._renderBodyCell(record, column, index, {mode: ""});
+ }.bind(this)
+ );
+ $tr = $tr.append($cells);
+ if (row.aggregate) {
+ $tr.append(this._renderAggregateRowCell(row));
+ }
+ return $tr;
+ },
+
+ /**
+ * Renders the label for a specific row.
+ *
+ * @private
+ * @param {Object} record Contains the information about the record.
+ * @returns {jQueryElement} the cell that was rendered.
+ */
+ _renderLabelCell: function(record) {
+ var $td = $("
");
+ var value = record.data[this.matrix_data.field_y_axis];
+ if (value.type === "record") {
+ // We have a related record
+ value = value.data.display_name;
+ }
+ // Get 1st column filled w/ Y label
+ $td.text(value);
+ return $td;
+ },
+
+ /**
+ * Create a cell and fill it with the aggregate value.
+ *
+ * @private
+ * @param {Object} row the row object to aggregate.
+ * @returns {jQueryElement} The rendered cell.
+ */
+ _renderAggregateRowCell: function(row) {
+ var $cell = $("
", {class: "row-total"});
+ this.applyAggregateValue($cell, row);
+ return $cell;
+ },
+
+ /**
+ * Render a single body Cell.
+ * Gets the field and renders the widget. We force the edit mode, since
+ * we always want the widget to be editable.
+ *
+ * @private
+ * @param {Object} record Contains the data for this cell
+ * @param {jQueryElement} node The HTML of the field.
+ * @param {int} colIndex The index of the current column.
+ * @param {Object} options The obtions used for the widget
+ * @returns {jQueryElement} the rendered cell.
+ */
+ _renderBodyCell: function(record, node, colIndex, options) {
+ var tdClassName = "o_data_cell";
+ if (node.tag === "field") {
+ var typeClass = FIELD_CLASSES[this.state.fields[node.attrs.name].type];
+ if (typeClass) {
+ tdClassName += " " + typeClass;
+ }
+ if (node.attrs.widget) {
+ tdClassName += " o_" + node.attrs.widget + "_cell";
+ }
+ }
+
+ // TODO roadmap: here we should collect possible extra params
+ // the user might want to attach to each single cell.
+
+ var $td = $("
", {
+ class: tdClassName,
+ });
+
+ if (_.isUndefined(record)) {
+ // Without record, nothing elese to do
+ return $td;
+ }
+ $td.attr({
+ "data-form-id": record.id,
+ "data-id": record.data.id,
+ });
+
+ // We register modifiers on the
element so that it gets
+ // the correct modifiers classes (for styling)
+ var modifiers = this._registerModifiers(
+ node,
+ record,
+ $td,
+ _.pick(options, "mode")
+ );
+ // If the invisible modifiers is true, the
element is
+ // left empty. Indeed, if the modifiers was to change the
+ // whole cell would be rerendered anyway.
+ if (modifiers.invisible && !(options && options.renderInvisible)) {
+ return $td;
+ }
+
+ // Enforce mode of the parent
+ options.mode = this.getParent().mode;
+
+ if (node.tag === "widget") {
+ return $td.append(this._renderWidget(record, node));
+ }
+ var $el = this._renderFieldWidget(node, record, _.pick(options, "mode"));
+ return $td.append($el);
+ },
+
+ /**
+ * Wraps the column aggregate with a tfoot element
+ *
+ * @private
+ * @returns {jQueryElement} The footer element with the cells in it.
+ */
+ _renderFooter: function() {
+ var $cells = this._renderAggregateColCells();
+ if ($cells) {
+ var $tr = $("
")
+ .append("
")
+ .append($cells);
+ var $total_cell = this._renderTotalCell();
+ if ($total_cell) {
+ $tr.append($total_cell);
+ }
+ return $("
").append($tr);
+ }
+ },
+
+ /**
+ * Renders the total cell (of all rows / columns)
+ *
+ * @private
+ * @returns {jQueryElement} The td element with the total in it.
+ */
+ _renderTotalCell: function() {
+ if (
+ !this.matrix_data.show_column_totals ||
+ !this.matrix_data.show_row_totals
+ ) {
+ return;
+ }
+
+ var $cell = $("
", {class: "col-total"});
+ this.applyAggregateValue($cell, this.total);
+ return $cell;
+ },
+
+ /**
+ * Render the Aggregate cells for the column.
+ *
+ * @private
+ * @returns {List} the rendered cells
+ */
+ _renderAggregateColCells: function() {
+ var self = this;
+
+ return _.map(this.columns, function(column) {
+ var $cell = $("
");
+ if (config.debug) {
+ $cell.addClass(column.attrs.name);
+ }
+ if (column.aggregate) {
+ self.applyAggregateValue($cell, column);
+ }
+ return $cell;
+ });
+ },
+
+ /**
+ * Compute the column aggregates.
+ * This function is called everytime the value is changed.
+ *
+ * @private
+ */
+ _computeColumnAggregates: function() {
+ if (!this.matrix_data.show_column_totals) {
+ return;
+ }
+ var fname = this.matrix_data.field_value,
+ field = this.state.fields[fname];
+ if (!field) {
+ return;
+ }
+ var type = field.type;
+ if (!~["integer", "float", "monetary"].indexOf(type)) {
+ return;
+ }
+ this.total = {
+ attrs: {
+ name: fname,
+ },
+ aggregate: {
+ help: _t("Sum Total"),
+ value: 0,
+ },
+ };
+ _.each(
+ this.columns,
+ function(column, index) {
+ column.aggregate = {
+ help: _t("Sum"),
+ value: 0,
+ };
+ _.each(this.rows, function(row) {
+ // TODO Use only one _.propertyOf in underscore 1.9.0+
+ try {
+ column.aggregate.value += row.data[index].data[fname];
+ } catch (error) {
+ // Nothing to do
+ }
+ });
+ this.total.aggregate.value += column.aggregate.value;
+ }.bind(this)
+ );
+ },
+
+ _getRecord: function(recordId) {
+ var record = null;
+ utils.traverse_records(this.state, function(r) {
+ if (r.id === recordId) {
+ record = r;
+ }
+ });
+ return record;
+ },
+
+ /**
+ * @override
+ */
+ updateState: function(state, params) {
+ if (params.matrix_data) {
+ this._saveMatrixData(params.matrix_data);
+ }
+ return this._super.apply(this, arguments);
+ },
+
+ /**
+ * Traverse the fields matrix with the keyboard
+ *
+ * @override
+ * @private
+ * @param {OdooEvent} event "navigation_move" event
+ */
+ _onNavigationMove: function(event) {
+ var widgets = this.__parentedChildren,
+ index = widgets.indexOf(event.target),
+ first = index === 0,
+ last = index === widgets.length - 1,
+ move = 0;
+ // Guess if we have to move the focus
+ if (event.data.direction === "next" && !last) {
+ move = 1;
+ } else if (event.data.direction === "previous" && !first) {
+ move = -1;
+ }
+ // Move focus
+ if (move) {
+ var target = widgets[index + move];
+ index = this.allFieldWidgets[target.record.id].indexOf(target);
+ this._activateFieldWidget(target.record, index, {inc: 0});
+ event.stopPropagation();
+ }
+ },
+
+ /**
+ * Compute the row aggregates.
+ *
+ * This function is called everytime the value is changed.
+ *
+ * @private
+ */
+ _computeRowAggregates: function() {
+ if (!this.matrix_data.show_row_totals) {
+ return;
+ }
+ var fname = this.matrix_data.field_value,
+ field = this.state.fields[fname];
+ if (!field) {
+ return;
+ }
+ var type = field.type;
+ if (!~["integer", "float", "monetary"].indexOf(type)) {
+ return;
+ }
+ _.each(this.rows, function(row) {
+ row.aggregate = {
+ help: _t("Sum"),
+ value: 0,
+ };
+ _.each(row.data, function(col) {
+ // TODO Use _.property in underscore 1.9+
+ try {
+ row.aggregate.value += col.data[fname];
+ } catch (error) {
+ // Nothing to do
+ }
+ });
+ });
+ },
+
+ /**
+ * Takes the given Value, formats it and adds it to the given cell.
+ *
+ * @private
+ *
+ * @param {jQueryElement} $cell
+ * The Cell where the aggregate should be added.
+ *
+ * @param {Object} axis
+ * The object which contains the information about the aggregate value axis
+ */
+ applyAggregateValue: function($cell, axis) {
+ var field = this.state.fields[axis.attrs.name];
+ var value = axis.aggregate.value;
+ var help = axis.aggregate.help;
+ var fieldInfo = this.state.fieldsInfo.list[axis.attrs.name];
+ var formatFunc =
+ field_utils.format[fieldInfo.widget ? fieldInfo.widget : field.type];
+ var formattedValue = formatFunc(value, field, {escape: true});
+ $cell
+ .addClass("o_list_number")
+ .attr("title", help)
+ .html(formattedValue);
+ },
+
+ /**
+ * Check if the change was successful and then update the grid.
+ * This function is required on relational fields.
+ *
+ * @param {Object} state
+ * Contains the current state of the field & all the data
+ *
+ * @param {String} id
+ * the id of the updated object.
+ *
+ * @param {Array} fields
+ * The fields we have in the view.
+ *
+ * @param {Object} ev
+ * The event object.
+ *
+ * @returns {Deferred}
+ * The deferred object thats gonna be resolved when the change is made.
+ */
+ confirmUpdate: function(state, id, fields, ev) {
+ var self = this;
+ this.state = state;
+ return this.confirmChange(state, id, fields, ev).then(function() {
+ self._refresh(id);
+ });
+ },
+
+ /**
+ * Refresh our grid.
+ *
+ * @private
+ * @param {String} id Datapoint ID
+ */
+ _refresh: function(id) {
+ this._updateRow(id);
+ this._refreshColTotals();
+ this._refreshRowTotals();
+ },
+
+ /**
+ *Update row data in our internal rows.
+ *
+ * @param {String} id: The id of the row that needs to be updated.
+ */
+ _updateRow: function(id) {
+ var record = _.findWhere(this.state.data, {id: id}),
+ _id = _.property("id");
+ _.each(this.rows, function(row) {
+ _.each(row.data, function(col, i) {
+ if (_id(col) === id) {
+ row.data[i] = record;
+ }
+ });
+ });
+ },
+
+ /**
+ * Update the row total.
+ */
+ _refreshColTotals: function() {
+ this._computeColumnAggregates();
+ this.$("tfoot").replaceWith(this._renderFooter());
+ },
+
+ /**
+ * Update the column total.
+ */
+ _refreshRowTotals: function() {
+ var self = this;
+ this._computeRowAggregates();
+ var $rows = self.$el.find("tr.o_data_row");
+ _.each(self.rows, function(row, i) {
+ if (row.aggregate) {
+ $($rows[i])
+ .find(".row-total")
+ .replaceWith(self._renderAggregateRowCell(row));
+ }
+ });
+ },
+
+ /**
+ * X2many fields expect this
+ *
+ * @returns {null}
+ */
+ getEditableRecordID: function() {
+ return null;
+ },
+ });
+
+ return X2Many2dMatrixRenderer;
+});
diff --git a/web_widget_x2many_2d_matrix/static/src/js/2d_matrix_view.js b/web_widget_x2many_2d_matrix/static/src/js/2d_matrix_view.js
new file mode 100644
index 000000000..dd3dadbbd
--- /dev/null
+++ b/web_widget_x2many_2d_matrix/static/src/js/2d_matrix_view.js
@@ -0,0 +1,22 @@
+/* Copyright 2019 Alexandre Díaz
+ * License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). */
+
+odoo.define("web_widget_x2many_2d_matrix.X2Many2dMatrixView", function(require) {
+ "use strict";
+
+ var BasicView = require("web.BasicView");
+
+ BasicView.include({
+ _processField: function(viewType, field, attrs) {
+ // Workaround for kanban mode rendering.
+ // Source of the issue: https://github.com/OCA/OCB/blob/12.0/addons/web/static/src/js/views/basic/basic_view.js#L303 .
+ // See https://github.com/OCA/web/pull/1404#pullrequestreview-305813206 .
+ // In the long term we should a way to handle kanban mode
+ // better (eg: a specific renderer).
+ if (attrs.widget === "x2many_2d_matrix") {
+ attrs.mode = "tree";
+ }
+ return this._super(viewType, field, attrs);
+ },
+ });
+});
diff --git a/web_widget_x2many_2d_matrix/static/src/js/abstract_view_matrix_limit_extend.js b/web_widget_x2many_2d_matrix/static/src/js/abstract_view_matrix_limit_extend.js
new file mode 100644
index 000000000..6ef957777
--- /dev/null
+++ b/web_widget_x2many_2d_matrix/static/src/js/abstract_view_matrix_limit_extend.js
@@ -0,0 +1,16 @@
+odoo.define("web_widget_x2many_2d_matrix.matrix_limit_extend", function(require) {
+ "use strict";
+
+ var FormView = require("web.FormView");
+
+ FormView.include({
+ // We extend this method so that the view is not limited to
+ // just 40 cells when the 'x2many_2d_matrix' widget is used.
+ _setSubViewLimit: function(attrs) {
+ this._super(attrs);
+ if (attrs.widget === "x2many_2d_matrix") {
+ attrs.limit = Infinity;
+ }
+ },
+ });
+});
diff --git a/web_widget_x2many_2d_matrix/static/src/js/widget_x2many_2d_matrix.js b/web_widget_x2many_2d_matrix/static/src/js/widget_x2many_2d_matrix.js
new file mode 100644
index 000000000..6c8ff957f
--- /dev/null
+++ b/web_widget_x2many_2d_matrix/static/src/js/widget_x2many_2d_matrix.js
@@ -0,0 +1,261 @@
+/* Copyright 2015 Holger Brunn
+ * Copyright 2016 Pedro M. Baeza
+ * Copyright 2018 Simone Orsi
+ * 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 field_registry = require("web.field_registry");
+ var relational_fields = require("web.relational_fields");
+ var X2Many2dMatrixRenderer = require("web_widget_x2many_2d_matrix.X2Many2dMatrixRenderer");
+
+ var WidgetX2Many2dMatrix = relational_fields.FieldOne2Many.extend({
+ widget_class: "o_form_field_x2many_2d_matrix",
+
+ /**
+ * Initialize the widget & parameters.
+ *
+ * @param {Object} parent contains the form view.
+ * @param {String} name the name of the field.
+ * @param {Object} record information about the database records.
+ * @param {Object} options view options.
+ */
+ init: function(parent, name, record, options) {
+ this._super(parent, name, record, options);
+ this.init_params();
+ },
+
+ /**
+ * Initialize the widget specific parameters.
+ * Sets the axis and the values.
+ */
+ init_params: function() {
+ var node = this.attrs;
+ this.by_y_axis = {};
+ this.x_axis = [];
+ this.y_axis = [];
+ this.field_x_axis = node.field_x_axis || this.field_x_axis;
+ this.field_y_axis = node.field_y_axis || this.field_y_axis;
+ this.field_label_x_axis = node.field_label_x_axis || this.field_x_axis;
+ this.field_label_y_axis = node.field_label_y_axis || this.field_y_axis;
+ this.x_axis_clickable = this.parse_boolean(node.x_axis_clickable || "1");
+ this.y_axis_clickable = this.parse_boolean(node.y_axis_clickable || "1");
+ this.field_value = node.field_value || this.field_value;
+ // TODO: is this really needed? Holger?
+ for (var property in node) {
+ if (property.startsWith("field_att_")) {
+ this.fields_att[property.substring(10)] = node[property];
+ }
+ }
+ var field_defs = this.recordData[this.name].fields;
+ // TODO: raise when any of the fields above don't exist with a
+ // helpful error message
+ if (!field_defs[this.field_value]) {
+ throw new Error(
+ _.str.sprintf(
+ "You need to include %s in your view definition",
+ this.field_value
+ )
+ );
+ }
+ this.show_row_totals = this.parse_boolean(
+ node.show_row_totals ||
+ this.is_aggregatable(field_defs[this.field_value])
+ ? "1"
+ : ""
+ );
+ this.show_column_totals = this.parse_boolean(
+ node.show_column_totals ||
+ this.is_aggregatable(field_defs[this.field_value])
+ ? "1"
+ : ""
+ );
+ },
+
+ /**
+ * Initializes the Value matrix.
+ *
+ * Puts the values in the grid.
+ * If we have related items we use the display name.
+ */
+ init_matrix: function() {
+ var records = this.recordData[this.name].data;
+ // Wipe the content if something still exists
+ this.by_y_axis = {};
+ this.x_axis = [];
+ this.y_axis = [];
+ _.each(
+ records,
+ function(record) {
+ var x = record.data[this.field_x_axis],
+ y = record.data[this.field_y_axis];
+ if (x.type === "record") {
+ // We have a related record
+ x = x.data.display_name;
+ }
+ if (y.type === "record") {
+ // We have a related record
+ y = y.data.display_name;
+ }
+ this.by_y_axis[y] = this.by_y_axis[y] || {};
+ this.by_y_axis[y][x] = record;
+ if (this.y_axis.indexOf(y) === -1) {
+ this.y_axis.push(y);
+ }
+ if (this.x_axis.indexOf(x) === -1) {
+ this.x_axis.push(x);
+ }
+ }.bind(this)
+ );
+ // Init columns
+ this.columns = [];
+ _.each(
+ this.x_axis,
+ function(x) {
+ this.columns.push(this._make_column(x));
+ }.bind(this)
+ );
+ this.rows = [];
+ _.each(
+ this.y_axis,
+ function(y) {
+ this.rows.push(this._make_row(y));
+ }.bind(this)
+ );
+ this.matrix_data = {
+ field_value: this.field_value,
+ field_x_axis: this.field_x_axis,
+ field_y_axis: this.field_y_axis,
+ columns: this.columns,
+ rows: this.rows,
+ show_row_totals: this.show_row_totals,
+ show_column_totals: this.show_column_totals,
+ };
+ console.log(this.matrix_data);
+ },
+
+ /**
+ * Create scaffold for a column.
+ *
+ * @param {String} x The string used as a column title
+ * @returns {Object}
+ */
+ _make_column: function(x) {
+ return {
+ // Simulate node parsed on xml arch
+ tag: "field",
+ attrs: {
+ name: this.field_x_axis,
+ string: x,
+ },
+ };
+ },
+
+ /**
+ * Create scaffold for a row.
+ *
+ * @param {String} y The string used as a row title
+ * @returns {Object}
+ */
+ _make_row: function(y) {
+ var self = this;
+ // Use object so that we can attach more data if needed
+ var row = {
+ tag: "field",
+ attrs: {
+ name: this.field_y_axis,
+ string: y,
+ },
+ data: [],
+ };
+ _.each(self.x_axis, function(x) {
+ row.data.push(self.by_y_axis[y][x]);
+ });
+ return row;
+ },
+
+ /**
+ * Determine if a field represented by field_def can be aggregated
+ */
+ is_aggregatable: function(field_def) {
+ return field_def.type in {float: 1, monetary: 1, integer: 1};
+ },
+
+ /**
+ * Parse a String containing a bool and convert it to a JS bool.
+ *
+ * @param {String} val: the string to be parsed.
+ * @returns {Boolean} The parsed boolean.
+ */
+ parse_boolean: function(val) {
+ if (val.toLowerCase() === "true" || val === "1") {
+ return true;
+ }
+ return false;
+ },
+
+ /**
+ * Create the matrix renderer and add its output to our element
+ *
+ * @returns {Deferred}
+ * A deferred object to be completed when it finished rendering.
+ */
+ _render: function() {
+ if (!this.view) {
+ return this._super();
+ }
+ // Ensure widget is re initiated when rendering
+ this.init_matrix();
+ var arch = this.view.arch;
+ // Update existing renderer
+ if (!_.isUndefined(this.renderer)) {
+ return this.renderer.updateState(this.value, {
+ matrix_data: this.matrix_data,
+ });
+ }
+ // Create a new matrix renderer
+ this.renderer = new X2Many2dMatrixRenderer(this, this.value, {
+ arch: arch,
+ editable: this.mode === "edit" && arch.attrs.editable,
+ viewType: "list",
+ matrix_data: this.matrix_data,
+ });
+ this.$el.addClass("o_field_x2many o_field_x2many_2d_matrix");
+ return this.renderer.appendTo(this.$el);
+ },
+
+ /**
+ * Activate the widget.
+ *
+ * @override
+ */
+ activate: function(options) {
+ // Won't work fine without https://github.com/odoo/odoo/pull/26490
+ // TODO Use _.propertyOf in underscore 1.9+
+ try {
+ this._backwards = options.event.data.direction === "previous";
+ } catch (error) {
+ this._backwards = false;
+ }
+ var result = this._super.apply(this, arguments);
+ delete this._backwards;
+ return result;
+ },
+
+ /**
+ * Get first element to focus.
+ *
+ * @override
+ */
+ getFocusableElement: function() {
+ return this.$(".o_input:" + (this._backwards ? "last" : "first"));
+ },
+ });
+
+ field_registry.add("x2many_2d_matrix", WidgetX2Many2dMatrix);
+
+ return {
+ WidgetX2Many2dMatrix: WidgetX2Many2dMatrix,
+ };
+});
diff --git a/web_widget_x2many_2d_matrix/static/src/scss/web_widget_x2many_2d_matrix.scss b/web_widget_x2many_2d_matrix/static/src/scss/web_widget_x2many_2d_matrix.scss
new file mode 100644
index 000000000..8596f01ef
--- /dev/null
+++ b/web_widget_x2many_2d_matrix/static/src/scss/web_widget_x2many_2d_matrix.scss
@@ -0,0 +1,72 @@
+$x2many_2d_matrix_max_height: 450px;
+
+.o_form_view .o_field_x2many_2d_matrix {
+ .table-responsive {
+ max-height: $x2many_2d_matrix_max_height;
+ overflow-y: auto;
+ }
+
+ .o_x2many_2d_matrix.o_list_view {
+ > thead > tr > th {
+ white-space: pre-line;
+ position: sticky;
+ top: 0;
+ z-index: 1;
+ background-color: $o-list-footer-bg-color;
+
+ &.total {
+ right: 0;
+ }
+ }
+
+ > tbody {
+ > tr {
+ &:nth-of-type(2n + 1) td.row-total,
+ &:nth-of-type(2n + 1) td:first-child {
+ background-color: mix(#000, #fff, 1%);
+ }
+ &:nth-of-type(2n) td.row-total,
+ &:nth-of-type(2n) td:first-child {
+ background-color: white;
+ }
+
+ > td {
+ text-align: left;
+
+ &:first-child {
+ position: sticky;
+ left: 0;
+ border-right-width: 1px;
+ border-right-color: $gray-300;
+ border-right-style: solid;
+ box-shadow: -1px 5px 10px $gray-300;
+ }
+ &.row-total {
+ font-weight: bold;
+ position: sticky;
+ right: 0;
+ border-left-width: 1px;
+ border-left-color: $gray-300;
+ border-left-style: solid;
+ box-shadow: -1px 5px 10px $gray-300;
+ }
+ }
+ }
+ }
+
+ > tfoot > tr > td {
+ padding: 0.75rem;
+ text-align: left;
+ background-color: $o-list-footer-bg-color;
+ position: sticky;
+ bottom: 0;
+
+ &.col-total {
+ right: 0;
+ border-left-width: 1px;
+ border-left-color: $gray-300;
+ border-left-style: solid;
+ }
+ }
+ }
+}
diff --git a/web_widget_x2many_2d_matrix/views/assets.xml b/web_widget_x2many_2d_matrix/views/assets.xml
new file mode 100644
index 000000000..9f2ebd386
--- /dev/null
+++ b/web_widget_x2many_2d_matrix/views/assets.xml
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/web_widget_x2many_2d_matrix_example/README.rst b/web_widget_x2many_2d_matrix_example/README.rst
new file mode 100644
index 000000000..67843ee97
--- /dev/null
+++ b/web_widget_x2many_2d_matrix_example/README.rst
@@ -0,0 +1,74 @@
+===================================
+web_widget_x2many_2d_matrix example
+===================================
+
+.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+ !! This file is generated by oca-gen-addon-readme !!
+ !! changes will be overwritten. !!
+ !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+
+.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png
+ :target: https://odoo-community.org/page/development-status
+ :alt: Beta
+.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png
+ :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
+ :alt: License: AGPL-3
+.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fweb-lightgray.png?logo=github
+ :target: https://github.com/OCA/web/tree/13.0/web_widget_x2many_2d_matrix_example
+ :alt: OCA/web
+.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
+ :target: https://translation.odoo-community.org/projects/web-13-0/web-13-0-web_widget_x2many_2d_matrix_example
+ :alt: Translate me on Weblate
+.. |badge5| image:: https://img.shields.io/badge/runbot-Try%20me-875A7B.png
+ :target: https://runbot.odoo-community.org/runbot/162/13.0
+ :alt: Try me on Runbot
+
+|badge1| |badge2| |badge3| |badge4| |badge5|
+
+Install it and click on the menu item `Demo x2m matrix widget`.
+
+**Table of contents**
+
+.. contents::
+ :local:
+
+Bug Tracker
+===========
+
+Bugs are tracked on `GitHub Issues `_.
+In case of trouble, please check there if your issue has already been reported.
+If you spotted it first, help us smashing it by providing a detailed and welcomed
+`feedback `_.
+
+Do not contact contributors directly about support or help with technical issues.
+
+Credits
+=======
+
+Authors
+~~~~~~~
+
+* Camptocamp
+
+Contributors
+~~~~~~~~~~~~
+
+* Simone Orsi
+* Anand Kansagra
+
+Maintainers
+~~~~~~~~~~~
+
+This module is maintained by the OCA.
+
+.. image:: https://odoo-community.org/logo.png
+ :alt: Odoo Community Association
+ :target: https://odoo-community.org
+
+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.
+
+This module is part of the `OCA/web `_ project on GitHub.
+
+You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.
diff --git a/web_widget_x2many_2d_matrix_example/__init__.py b/web_widget_x2many_2d_matrix_example/__init__.py
new file mode 100644
index 000000000..d6c56a95f
--- /dev/null
+++ b/web_widget_x2many_2d_matrix_example/__init__.py
@@ -0,0 +1,3 @@
+# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
+from . import models
+from . import wizard
diff --git a/web_widget_x2many_2d_matrix_example/__manifest__.py b/web_widget_x2many_2d_matrix_example/__manifest__.py
new file mode 100644
index 000000000..df95e5e9a
--- /dev/null
+++ b/web_widget_x2many_2d_matrix_example/__manifest__.py
@@ -0,0 +1,18 @@
+# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
+{
+ "name": "web_widget_x2many_2d_matrix example",
+ "summary": "A small example on how to use `web_widget_x2many_2d_matrix`.",
+ "version": "13.0.1.0.0",
+ "author": "Camptocamp, " "Odoo Community Association (OCA)",
+ "website": "https://github.com/OCA/web",
+ "license": "AGPL-3",
+ "category": "Hidden/Dependency",
+ "depends": ["web_widget_x2many_2d_matrix"],
+ "data": [
+ "security/ir.model.access.csv",
+ "demo/x2m.demo.csv",
+ "views/x2m_demo.xml",
+ "wizard/x2m_matrix.xml",
+ ],
+ "installable": True,
+}
diff --git a/web_widget_x2many_2d_matrix_example/demo/x2m.demo.csv b/web_widget_x2many_2d_matrix_example/demo/x2m.demo.csv
new file mode 100644
index 000000000..9a8b5aa11
--- /dev/null
+++ b/web_widget_x2many_2d_matrix_example/demo/x2m.demo.csv
@@ -0,0 +1,20 @@
+id,name,line_ids/user_id/id,line_ids/name,line_ids/value
+web_widget_x2many_2d_matrix_example.x2m_demo_5,One,,,
+,,base.user_demo,A,1
+,,base.user_demo,B,2
+,,base.user_demo,C,3
+web_widget_x2many_2d_matrix_example.x2m_demo_3,Two,,,
+,,base.user_demo,E,5
+,,base.user_demo,F,6
+web_widget_x2many_2d_matrix_example.x2m_demo_2,Three,,,
+,,base.user_root,G,8
+,,base.user_demo,H,9
+,,base.user_root,I,10
+web_widget_x2many_2d_matrix_example.x2m_demo_1,Four,,,
+,,base.user_root,L,12
+,,base.user_demo,M,13
+,,base.user_demo,N,14
+,,base.user_demo,O,15
+,,base.user_root,P,16
+web_widget_x2many_2d_matrix_example.x2m_demo_4,Five,,,
+,,base.user_demo,Q,18
diff --git a/web_widget_x2many_2d_matrix_example/i18n/web_widget_x2many_2d_matrix_example.pot b/web_widget_x2many_2d_matrix_example/i18n/web_widget_x2many_2d_matrix_example.pot
new file mode 100644
index 000000000..2a35dc195
--- /dev/null
+++ b/web_widget_x2many_2d_matrix_example/i18n/web_widget_x2many_2d_matrix_example.pot
@@ -0,0 +1,147 @@
+# Translation of Odoo Server.
+# This file contains the translation of the following modules:
+# * web_widget_x2many_2d_matrix_example
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Odoo Server 12.0\n"
+"Report-Msgid-Bugs-To: \n"
+"Last-Translator: <>\n"
+"Language-Team: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: \n"
+"Plural-Forms: \n"
+
+#. module: web_widget_x2many_2d_matrix_example
+#: model:ir.model.fields,field_description:web_widget_x2many_2d_matrix_example.field_x2m_demo__create_uid
+#: model:ir.model.fields,field_description:web_widget_x2many_2d_matrix_example.field_x2m_demo_line__create_uid
+#: model:ir.model.fields,field_description:web_widget_x2many_2d_matrix_example.field_x2m_matrix_demo_wiz__create_uid
+msgid "Created by"
+msgstr ""
+
+#. module: web_widget_x2many_2d_matrix_example
+#: model:ir.model.fields,field_description:web_widget_x2many_2d_matrix_example.field_x2m_demo__create_date
+#: model:ir.model.fields,field_description:web_widget_x2many_2d_matrix_example.field_x2m_demo_line__create_date
+#: model:ir.model.fields,field_description:web_widget_x2many_2d_matrix_example.field_x2m_matrix_demo_wiz__create_date
+msgid "Created on"
+msgstr ""
+
+#. module: web_widget_x2many_2d_matrix_example
+#: model:ir.actions.act_window,name:web_widget_x2many_2d_matrix_example.action_x2m_demo
+#: model:ir.model.fields,field_description:web_widget_x2many_2d_matrix_example.field_x2m_demo_line__demo_id
+msgid "Demo"
+msgstr ""
+
+#. module: web_widget_x2many_2d_matrix_example
+#: model:ir.ui.menu,name:web_widget_x2many_2d_matrix_example.base_matrix_widget_menu
+msgid "Demo x2m matrix widget"
+msgstr ""
+
+#. module: web_widget_x2many_2d_matrix_example
+#: model:ir.model.fields,field_description:web_widget_x2many_2d_matrix_example.field_x2m_demo__display_name
+#: model:ir.model.fields,field_description:web_widget_x2many_2d_matrix_example.field_x2m_demo_line__display_name
+#: model:ir.model.fields,field_description:web_widget_x2many_2d_matrix_example.field_x2m_matrix_demo_wiz__display_name
+msgid "Display Name"
+msgstr ""
+
+#. module: web_widget_x2many_2d_matrix_example
+#: model:ir.model.fields,field_description:web_widget_x2many_2d_matrix_example.field_x2m_demo__id
+#: model:ir.model.fields,field_description:web_widget_x2many_2d_matrix_example.field_x2m_demo_line__id
+#: model:ir.model.fields,field_description:web_widget_x2many_2d_matrix_example.field_x2m_matrix_demo_wiz__id
+msgid "ID"
+msgstr ""
+
+#. module: web_widget_x2many_2d_matrix_example
+#: model:ir.model.fields,field_description:web_widget_x2many_2d_matrix_example.field_x2m_demo____last_update
+#: model:ir.model.fields,field_description:web_widget_x2many_2d_matrix_example.field_x2m_demo_line____last_update
+#: model:ir.model.fields,field_description:web_widget_x2many_2d_matrix_example.field_x2m_matrix_demo_wiz____last_update
+msgid "Last Modified on"
+msgstr ""
+
+#. module: web_widget_x2many_2d_matrix_example
+#: model:ir.model.fields,field_description:web_widget_x2many_2d_matrix_example.field_x2m_demo__write_uid
+#: model:ir.model.fields,field_description:web_widget_x2many_2d_matrix_example.field_x2m_demo_line__write_uid
+#: model:ir.model.fields,field_description:web_widget_x2many_2d_matrix_example.field_x2m_matrix_demo_wiz__write_uid
+msgid "Last Updated by"
+msgstr ""
+
+#. module: web_widget_x2many_2d_matrix_example
+#: model:ir.model.fields,field_description:web_widget_x2many_2d_matrix_example.field_x2m_demo__write_date
+#: model:ir.model.fields,field_description:web_widget_x2many_2d_matrix_example.field_x2m_demo_line__write_date
+#: model:ir.model.fields,field_description:web_widget_x2many_2d_matrix_example.field_x2m_matrix_demo_wiz__write_date
+msgid "Last Updated on"
+msgstr ""
+
+#. module: web_widget_x2many_2d_matrix_example
+#: model:ir.model.fields,field_description:web_widget_x2many_2d_matrix_example.field_x2m_demo__line_ids
+#: model:ir.model.fields,field_description:web_widget_x2many_2d_matrix_example.field_x2m_matrix_demo_wiz__line_ids
+msgid "Line"
+msgstr ""
+
+#. module: web_widget_x2many_2d_matrix_example
+#: model:ir.model.fields,field_description:web_widget_x2many_2d_matrix_example.field_x2m_demo__name
+#: model:ir.model.fields,field_description:web_widget_x2many_2d_matrix_example.field_x2m_demo_line__name
+msgid "Name"
+msgstr ""
+
+#. module: web_widget_x2many_2d_matrix_example
+#: model_terms:ir.ui.view,arch_db:web_widget_x2many_2d_matrix_example.view_x2m_demo_form
+msgid "Try x2m 2d matrix"
+msgstr ""
+
+#. module: web_widget_x2many_2d_matrix_example
+#: model_terms:ir.ui.view,arch_db:web_widget_x2many_2d_matrix_example.view_x2m_demo_form
+msgid "Try x2m 2d matrix (many2one)"
+msgstr ""
+
+#. module: web_widget_x2many_2d_matrix_example
+#: model_terms:ir.ui.view,arch_db:web_widget_x2many_2d_matrix_example.view_x2m_demo_form
+msgid "Try x2m 2d matrix (selection)"
+msgstr ""
+
+#. module: web_widget_x2many_2d_matrix_example
+#: model:ir.model.fields,field_description:web_widget_x2many_2d_matrix_example.field_x2m_demo_line__user_id
+msgid "User"
+msgstr ""
+
+#. module: web_widget_x2many_2d_matrix_example
+#: model:ir.model.fields,field_description:web_widget_x2many_2d_matrix_example.field_x2m_demo_line__value
+msgid "Value"
+msgstr ""
+
+#. module: web_widget_x2many_2d_matrix_example
+#: selection:x2m.demo.line,value_selection:0
+msgid "Value 1"
+msgstr ""
+
+#. module: web_widget_x2many_2d_matrix_example
+#: selection:x2m.demo.line,value_selection:0
+msgid "Value 2"
+msgstr ""
+
+#. module: web_widget_x2many_2d_matrix_example
+#: model:ir.model.fields,field_description:web_widget_x2many_2d_matrix_example.field_x2m_demo_line__value_many2one
+msgid "Value Many2One"
+msgstr ""
+
+#. module: web_widget_x2many_2d_matrix_example
+#: model:ir.model.fields,field_description:web_widget_x2many_2d_matrix_example.field_x2m_demo_line__value_selection
+msgid "Value Selection"
+msgstr ""
+
+#. module: web_widget_x2many_2d_matrix_example
+#: model:ir.model,name:web_widget_x2many_2d_matrix_example.model_x2m_demo
+msgid "X2Many Demo"
+msgstr ""
+
+#. module: web_widget_x2many_2d_matrix_example
+#: model:ir.model,name:web_widget_x2many_2d_matrix_example.model_x2m_demo_line
+msgid "X2Many Demo Line"
+msgstr ""
+
+#. module: web_widget_x2many_2d_matrix_example
+#: model:ir.model,name:web_widget_x2many_2d_matrix_example.model_x2m_matrix_demo_wiz
+msgid "X2Many Matrix Demo Wizard"
+msgstr ""
+
diff --git a/web_widget_x2many_2d_matrix_example/i18n/zh_CN.po b/web_widget_x2many_2d_matrix_example/i18n/zh_CN.po
new file mode 100644
index 000000000..db6dcf62e
--- /dev/null
+++ b/web_widget_x2many_2d_matrix_example/i18n/zh_CN.po
@@ -0,0 +1,149 @@
+# Translation of Odoo Server.
+# This file contains the translation of the following modules:
+# * web_widget_x2many_2d_matrix_example
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Odoo Server 12.0\n"
+"Report-Msgid-Bugs-To: \n"
+"PO-Revision-Date: 2019-09-17 19:24+0000\n"
+"Last-Translator: 黎伟杰 <674416404@qq.com>\n"
+"Language-Team: none\n"
+"Language: zh_CN\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: \n"
+"Plural-Forms: nplurals=1; plural=0;\n"
+"X-Generator: Weblate 3.8\n"
+
+#. module: web_widget_x2many_2d_matrix_example
+#: model:ir.model.fields,field_description:web_widget_x2many_2d_matrix_example.field_x2m_demo__create_uid
+#: model:ir.model.fields,field_description:web_widget_x2many_2d_matrix_example.field_x2m_demo_line__create_uid
+#: model:ir.model.fields,field_description:web_widget_x2many_2d_matrix_example.field_x2m_matrix_demo_wiz__create_uid
+msgid "Created by"
+msgstr "创建者"
+
+#. module: web_widget_x2many_2d_matrix_example
+#: model:ir.model.fields,field_description:web_widget_x2many_2d_matrix_example.field_x2m_demo__create_date
+#: model:ir.model.fields,field_description:web_widget_x2many_2d_matrix_example.field_x2m_demo_line__create_date
+#: model:ir.model.fields,field_description:web_widget_x2many_2d_matrix_example.field_x2m_matrix_demo_wiz__create_date
+msgid "Created on"
+msgstr "创建时间"
+
+#. module: web_widget_x2many_2d_matrix_example
+#: model:ir.actions.act_window,name:web_widget_x2many_2d_matrix_example.action_x2m_demo
+#: model:ir.model.fields,field_description:web_widget_x2many_2d_matrix_example.field_x2m_demo_line__demo_id
+msgid "Demo"
+msgstr "演示"
+
+#. module: web_widget_x2many_2d_matrix_example
+#: model:ir.ui.menu,name:web_widget_x2many_2d_matrix_example.base_matrix_widget_menu
+msgid "Demo x2m matrix widget"
+msgstr "演示x2m矩阵小部件"
+
+#. module: web_widget_x2many_2d_matrix_example
+#: model:ir.model.fields,field_description:web_widget_x2many_2d_matrix_example.field_x2m_demo__display_name
+#: model:ir.model.fields,field_description:web_widget_x2many_2d_matrix_example.field_x2m_demo_line__display_name
+#: model:ir.model.fields,field_description:web_widget_x2many_2d_matrix_example.field_x2m_matrix_demo_wiz__display_name
+msgid "Display Name"
+msgstr "显示名称"
+
+#. module: web_widget_x2many_2d_matrix_example
+#: model:ir.model.fields,field_description:web_widget_x2many_2d_matrix_example.field_x2m_demo__id
+#: model:ir.model.fields,field_description:web_widget_x2many_2d_matrix_example.field_x2m_demo_line__id
+#: model:ir.model.fields,field_description:web_widget_x2many_2d_matrix_example.field_x2m_matrix_demo_wiz__id
+msgid "ID"
+msgstr "ID"
+
+#. module: web_widget_x2many_2d_matrix_example
+#: model:ir.model.fields,field_description:web_widget_x2many_2d_matrix_example.field_x2m_demo____last_update
+#: model:ir.model.fields,field_description:web_widget_x2many_2d_matrix_example.field_x2m_demo_line____last_update
+#: model:ir.model.fields,field_description:web_widget_x2many_2d_matrix_example.field_x2m_matrix_demo_wiz____last_update
+msgid "Last Modified on"
+msgstr "最后修改时间"
+
+#. module: web_widget_x2many_2d_matrix_example
+#: model:ir.model.fields,field_description:web_widget_x2many_2d_matrix_example.field_x2m_demo__write_uid
+#: model:ir.model.fields,field_description:web_widget_x2many_2d_matrix_example.field_x2m_demo_line__write_uid
+#: model:ir.model.fields,field_description:web_widget_x2many_2d_matrix_example.field_x2m_matrix_demo_wiz__write_uid
+msgid "Last Updated by"
+msgstr "最后更新者"
+
+#. module: web_widget_x2many_2d_matrix_example
+#: model:ir.model.fields,field_description:web_widget_x2many_2d_matrix_example.field_x2m_demo__write_date
+#: model:ir.model.fields,field_description:web_widget_x2many_2d_matrix_example.field_x2m_demo_line__write_date
+#: model:ir.model.fields,field_description:web_widget_x2many_2d_matrix_example.field_x2m_matrix_demo_wiz__write_date
+msgid "Last Updated on"
+msgstr "最后更新时间"
+
+#. module: web_widget_x2many_2d_matrix_example
+#: model:ir.model.fields,field_description:web_widget_x2many_2d_matrix_example.field_x2m_demo__line_ids
+#: model:ir.model.fields,field_description:web_widget_x2many_2d_matrix_example.field_x2m_matrix_demo_wiz__line_ids
+msgid "Line"
+msgstr "行"
+
+#. module: web_widget_x2many_2d_matrix_example
+#: model:ir.model.fields,field_description:web_widget_x2many_2d_matrix_example.field_x2m_demo__name
+#: model:ir.model.fields,field_description:web_widget_x2many_2d_matrix_example.field_x2m_demo_line__name
+msgid "Name"
+msgstr "名称"
+
+#. module: web_widget_x2many_2d_matrix_example
+#: model_terms:ir.ui.view,arch_db:web_widget_x2many_2d_matrix_example.view_x2m_demo_form
+msgid "Try x2m 2d matrix"
+msgstr "试试x2m 2d矩阵"
+
+#. module: web_widget_x2many_2d_matrix_example
+#: model_terms:ir.ui.view,arch_db:web_widget_x2many_2d_matrix_example.view_x2m_demo_form
+msgid "Try x2m 2d matrix (many2one)"
+msgstr "试试x2m 2d矩阵(many2one)"
+
+#. module: web_widget_x2many_2d_matrix_example
+#: model_terms:ir.ui.view,arch_db:web_widget_x2many_2d_matrix_example.view_x2m_demo_form
+msgid "Try x2m 2d matrix (selection)"
+msgstr "试试x2m 2d矩阵 (selection)"
+
+#. module: web_widget_x2many_2d_matrix_example
+#: model:ir.model.fields,field_description:web_widget_x2many_2d_matrix_example.field_x2m_demo_line__user_id
+msgid "User"
+msgstr "用户"
+
+#. module: web_widget_x2many_2d_matrix_example
+#: model:ir.model.fields,field_description:web_widget_x2many_2d_matrix_example.field_x2m_demo_line__value
+msgid "Value"
+msgstr "值"
+
+#. module: web_widget_x2many_2d_matrix_example
+#: selection:x2m.demo.line,value_selection:0
+msgid "Value 1"
+msgstr "值1"
+
+#. module: web_widget_x2many_2d_matrix_example
+#: selection:x2m.demo.line,value_selection:0
+msgid "Value 2"
+msgstr "值2"
+
+#. module: web_widget_x2many_2d_matrix_example
+#: model:ir.model.fields,field_description:web_widget_x2many_2d_matrix_example.field_x2m_demo_line__value_many2one
+msgid "Value Many2One"
+msgstr "值 Many2One"
+
+#. module: web_widget_x2many_2d_matrix_example
+#: model:ir.model.fields,field_description:web_widget_x2many_2d_matrix_example.field_x2m_demo_line__value_selection
+msgid "Value Selection"
+msgstr "值Selection"
+
+#. module: web_widget_x2many_2d_matrix_example
+#: model:ir.model,name:web_widget_x2many_2d_matrix_example.model_x2m_demo
+msgid "X2Many Demo"
+msgstr "X2Many演示"
+
+#. module: web_widget_x2many_2d_matrix_example
+#: model:ir.model,name:web_widget_x2many_2d_matrix_example.model_x2m_demo_line
+msgid "X2Many Demo Line"
+msgstr "X2Many演示行"
+
+#. module: web_widget_x2many_2d_matrix_example
+#: model:ir.model,name:web_widget_x2many_2d_matrix_example.model_x2m_matrix_demo_wiz
+msgid "X2Many Matrix Demo Wizard"
+msgstr "X2Many Matrix演示向导"
diff --git a/web_widget_x2many_2d_matrix_example/models/__init__.py b/web_widget_x2many_2d_matrix_example/models/__init__.py
new file mode 100644
index 000000000..bf8b6ac47
--- /dev/null
+++ b/web_widget_x2many_2d_matrix_example/models/__init__.py
@@ -0,0 +1,2 @@
+# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
+from . import x2m_demo
diff --git a/web_widget_x2many_2d_matrix_example/models/x2m_demo.py b/web_widget_x2many_2d_matrix_example/models/x2m_demo.py
new file mode 100644
index 000000000..c8a1dd25c
--- /dev/null
+++ b/web_widget_x2many_2d_matrix_example/models/x2m_demo.py
@@ -0,0 +1,48 @@
+# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
+from odoo import fields, models
+
+
+class X2MDemo(models.Model):
+ _name = "x2m.demo"
+ _description = "X2Many Demo"
+
+ name = fields.Char()
+ line_ids = fields.One2many("x2m.demo.line", "demo_id")
+
+ def _open_x2m_matrix(self, view_xmlid):
+ wiz = self.env["x2m.matrix.demo.wiz"].create({})
+ view_id = self.env.ref(
+ "web_widget_x2many_2d_matrix_example.%s" % view_xmlid,
+ ).id
+ return {
+ "name": "Try x2many 2D matrix widget",
+ "type": "ir.actions.act_window",
+ "view_type": "form",
+ "view_mode": "form",
+ "res_model": "x2m.matrix.demo.wiz",
+ "target": "new",
+ "res_id": wiz.id,
+ "view_id": view_id,
+ "context": self.env.context,
+ }
+
+ def open_x2m_matrix(self):
+ return self._open_x2m_matrix("x2many_2d_matrix_demo")
+
+ def open_x2m_matrix_selection(self):
+ return self._open_x2m_matrix("x2many_2d_matrix_demo_selection")
+
+ def open_x2m_matrix_many2one(self):
+ return self._open_x2m_matrix("x2many_2d_matrix_demo_many2one")
+
+
+class X2MDemoLine(models.Model):
+ _name = "x2m.demo.line"
+ _description = "X2Many Demo Line"
+
+ name = fields.Char()
+ demo_id = fields.Many2one("x2m.demo")
+ user_id = fields.Many2one("res.users")
+ value = fields.Integer()
+ value_selection = fields.Selection([("val1", "Value 1"), ("val2", "Value 2")],)
+ value_many2one = fields.Many2one("res.groups")
diff --git a/web_widget_x2many_2d_matrix_example/readme/CONTRIBUTORS.rst b/web_widget_x2many_2d_matrix_example/readme/CONTRIBUTORS.rst
new file mode 100644
index 000000000..1fee00cb0
--- /dev/null
+++ b/web_widget_x2many_2d_matrix_example/readme/CONTRIBUTORS.rst
@@ -0,0 +1,2 @@
+* Simone Orsi
+* Anand Kansagra
diff --git a/web_widget_x2many_2d_matrix_example/readme/DESCRIPTION.rst b/web_widget_x2many_2d_matrix_example/readme/DESCRIPTION.rst
new file mode 100644
index 000000000..1af8b59ef
--- /dev/null
+++ b/web_widget_x2many_2d_matrix_example/readme/DESCRIPTION.rst
@@ -0,0 +1 @@
+Install it and click on the menu item `Demo x2m matrix widget`.
diff --git a/web_widget_x2many_2d_matrix_example/security/ir.model.access.csv b/web_widget_x2many_2d_matrix_example/security/ir.model.access.csv
new file mode 100644
index 000000000..ff2b0388d
--- /dev/null
+++ b/web_widget_x2many_2d_matrix_example/security/ir.model.access.csv
@@ -0,0 +1,5 @@
+id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
+access_x2m_demo_line,access_x2m_demo_line,model_x2m_demo_line,base.group_user,1,0,0,0
+access_x2m_demo_line_admin,access_x2m_demo_line_admin,model_x2m_demo_line,base.group_system,1,1,1,1
+access_x2m_demo,access_x2m_demo,model_x2m_demo,base.group_user,1,0,0,0
+access_x2m_demo_admin,access_x2m_demo_admin,model_x2m_demo,base.group_system,1,1,1,1
diff --git a/web_widget_x2many_2d_matrix_example/static/description/icon.png b/web_widget_x2many_2d_matrix_example/static/description/icon.png
new file mode 100644
index 000000000..3a0328b51
Binary files /dev/null and b/web_widget_x2many_2d_matrix_example/static/description/icon.png differ
diff --git a/web_widget_x2many_2d_matrix_example/static/description/index.html b/web_widget_x2many_2d_matrix_example/static/description/index.html
new file mode 100644
index 000000000..59b905f00
--- /dev/null
+++ b/web_widget_x2many_2d_matrix_example/static/description/index.html
@@ -0,0 +1,420 @@
+
+
+
+
+
+
+web_widget_x2many_2d_matrix example
+
+
+
+
+
web_widget_x2many_2d_matrix example
+
+
+
+
Install it and click on the menu item Demo x2m matrix widget.
Bugs are tracked on GitHub Issues.
+In case of trouble, please check there if your issue has already been reported.
+If you spotted it first, help us smashing it by providing a detailed and welcomed
+feedback.
+
Do not contact contributors directly about support or help with technical issues.
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.
+
This module is part of the OCA/web project on GitHub.