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

+ + +

Beta License: AGPL-3 OCA/web Translate me on Weblate Try me on Runbot

+

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:

+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

+ +
+

Usage

+

Use this widget by saying:

+
+<field name="my_field" widget="x2many_2d_matrix" />
+
+

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:

+
+<field name="my_field" widget="x2many_2d_matrix" field_x_axis="my_field1" field_y_axis="my_field2" field_value="my_field3">
+    <tree>
+        <field name="my_field"/>
+        <field name="my_field1"/>
+        <field name="my_field2"/>
+        <field name="my_field3"/>
+    </tree>
+</field>
+
+

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:

+
+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:

+
+<field name="task_ids" widget="x2many_2d_matrix" field_x_axis="project_id" field_y_axis="user_id" field_value="planned_hours">
+    <tree>
+        <field name="task_ids"/>
+        <field name="project_id"/>
+        <field name="user_id"/>
+        <field name="planned_hours"/>
+    </tree>
+</field>
+
+
+
+
+

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

+ +
+
+

Maintainers

+

This module is maintained by the OCA.

+Odoo Community Association +

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/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 = $("").append("").append($tr); + }, + + /** + * Render a single header cell. + * + * Creates a th and adds the description as text. + * + * @private + * @param {jQueryElement} node + * @returns {jQueryElement} the created . + * 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 = $("") + .append("").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 = $("
"); + $tr = $tr.append(_.map(this.columns, this._renderHeaderCell.bind(this))); + if (this.matrix_data.show_row_totals) { + $tr.append($("", {class: "total"})); + } + return $("
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
"); + 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($cells); + var $total_cell = this._renderTotalCell(); + if ($total_cell) { + $tr.append($total_cell); + } + return $("
", {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 @@ + + + +