diff --git a/web_widget_x2many_2d_matrix/README.rst b/web_widget_x2many_2d_matrix/README.rst new file mode 100644 index 000000000000..9972ee0e0485 --- /dev/null +++ b/web_widget_x2many_2d_matrix/README.rst @@ -0,0 +1,218 @@ +=========================== +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/12.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-12-0/web-12-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/12.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:: /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. + +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 + +Contributors +~~~~~~~~~~~~ + +* Holger Brunn +* Pedro M. Baeza +* Artem Kostyuk +* Simone Orsi +* Timon Tschanz +* Jairo Llopis +* Dennis Sluijk +* Alexey Pelykh + +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 000000000000..ef5ae3587f59 --- /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 000000000000..bfee53ec3550 --- /dev/null +++ b/web_widget_x2many_2d_matrix/__manifest__.py @@ -0,0 +1,25 @@ +# 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': '12.0.1.0.0', + 'author': ( + 'Therp BV, ' + 'Tecnativa, ' + 'Camptocamp, ' + '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 000000000000..6b12b83b8744 --- /dev/null +++ b/web_widget_x2many_2d_matrix/i18n/ar.po @@ -0,0 +1,31 @@ +# 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:46 +#, python-format +msgid "Sorry no matrix data to display." +msgstr "" + +#~ msgid "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 000000000000..53c95477ee10 --- /dev/null +++ b/web_widget_x2many_2d_matrix/i18n/de.po @@ -0,0 +1,30 @@ +# 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:46 +#, python-format +msgid "Sorry no matrix data to display." +msgstr "" + +#~ msgid "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 000000000000..46745146710a --- /dev/null +++ b/web_widget_x2many_2d_matrix/i18n/es.po @@ -0,0 +1,29 @@ +# 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:46 +#, python-format +msgid "Sorry no matrix data to display." +msgstr "" + +#~ msgid "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 000000000000..50321d78a94e --- /dev/null +++ b/web_widget_x2many_2d_matrix/i18n/fi.po @@ -0,0 +1,30 @@ +# 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:46 +#, python-format +msgid "Sorry no matrix data to display." +msgstr "" + +#~ msgid "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 000000000000..73c195491a86 --- /dev/null +++ b/web_widget_x2many_2d_matrix/i18n/fr.po @@ -0,0 +1,29 @@ +# 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: 2015-11-07 11:22+0000\n" +"Last-Translator: <>\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" + +#. module: web_widget_x2many_2d_matrix +#. openerp-web +#: code:addons/web_widget_x2many_2d_matrix/static/src/js/2d_matrix_renderer.js:46 +#, python-format +msgid "Sorry no matrix data to display." +msgstr "" + +#~ msgid "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 000000000000..7a9c1d999c52 --- /dev/null +++ b/web_widget_x2many_2d_matrix/i18n/hr.po @@ -0,0 +1,31 @@ +# 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: 2016-08-19 11:47+0000\n" +"Last-Translator: Ana-Maria Olujić \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" + +#. module: web_widget_x2many_2d_matrix +#. openerp-web +#: code:addons/web_widget_x2many_2d_matrix/static/src/js/2d_matrix_renderer.js:46 +#, python-format +msgid "Sorry no matrix data to display." +msgstr "" + +#~ msgid "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 000000000000..09f32362839c --- /dev/null +++ b/web_widget_x2many_2d_matrix/i18n/it.po @@ -0,0 +1,29 @@ +# 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: 2015-11-07 11:22+0000\n" +"Last-Translator: <>\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" + +#. module: web_widget_x2many_2d_matrix +#. openerp-web +#: code:addons/web_widget_x2many_2d_matrix/static/src/js/2d_matrix_renderer.js:46 +#, python-format +msgid "Sorry no matrix data to display." +msgstr "" + +#~ msgid "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 000000000000..c2c47a021b21 --- /dev/null +++ b/web_widget_x2many_2d_matrix/i18n/lt.po @@ -0,0 +1,30 @@ +# 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:46 +#, python-format +msgid "Sorry no matrix data to display." +msgstr "" + +#~ msgid "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 000000000000..5a792ed3dc88 --- /dev/null +++ b/web_widget_x2many_2d_matrix/i18n/nl_NL.po @@ -0,0 +1,30 @@ +# 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:46 +#, python-format +msgid "Sorry no matrix data to display." +msgstr "" + +#~ msgid "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 000000000000..e9999626d7dd --- /dev/null +++ b/web_widget_x2many_2d_matrix/i18n/pt_BR.po @@ -0,0 +1,30 @@ +# 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: 2018-08-03 13:20+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.1.1\n" + +#. module: web_widget_x2many_2d_matrix +#. openerp-web +#: code:addons/web_widget_x2many_2d_matrix/static/src/js/2d_matrix_renderer.js:46 +#, python-format +msgid "Sorry no matrix data to display." +msgstr "Desculpe não há dados de matriz para exibir." + +#~ msgid "Total" +#~ msgstr "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 000000000000..83c518e00f8a --- /dev/null +++ b/web_widget_x2many_2d_matrix/i18n/sl.po @@ -0,0 +1,30 @@ +# 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:46 +#, python-format +msgid "Sorry no matrix data to display." +msgstr "" + +#~ msgid "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 000000000000..0e192ed2a167 --- /dev/null +++ b/web_widget_x2many_2d_matrix/i18n/tr.po @@ -0,0 +1,30 @@ +# 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:46 +#, python-format +msgid "Sorry no matrix data to display." +msgstr "" + +#~ msgid "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 000000000000..abb0c59653e3 --- /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 11.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:60 +#, 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:379 +#: code:addons/web_widget_x2many_2d_matrix/static/src/js/2d_matrix_renderer.js:456 +#, 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:372 +#, 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 000000000000..f30eb3eb1d53 --- /dev/null +++ b/web_widget_x2many_2d_matrix/readme/CONTRIBUTORS.rst @@ -0,0 +1,8 @@ +* Holger Brunn +* Pedro M. Baeza +* Artem Kostyuk +* Simone Orsi +* Timon Tschanz +* Jairo Llopis +* Dennis Sluijk +* Alexey Pelykh 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 000000000000..a84da8005ae6 --- /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:: /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/ROADMAP.rst b/web_widget_x2many_2d_matrix/readme/ROADMAP.rst new file mode 100644 index 000000000000..b5c8e912a898 --- /dev/null +++ b/web_widget_x2many_2d_matrix/readme/ROADMAP.rst @@ -0,0 +1,14 @@ +* 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. 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 000000000000..8716546d49cb --- /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 000000000000..a501fbf835ea 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 000000000000..8ee519c969bd --- /dev/null +++ b/web_widget_x2many_2d_matrix/static/description/index.html @@ -0,0 +1,568 @@ + + + + + + +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.
  • +
+
+
+

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

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 000000000000..4b75baa8aa91 Binary files /dev/null and b/web_widget_x2many_2d_matrix/static/description/screenshot.png differ diff --git a/web_widget_x2many_2d_matrix/static/src/css/web_widget_x2many_2d_matrix.css b/web_widget_x2many_2d_matrix/static/src/css/web_widget_x2many_2d_matrix.css new file mode 100644 index 000000000000..907f507d2048 --- /dev/null +++ b/web_widget_x2many_2d_matrix/static/src/css/web_widget_x2many_2d_matrix.css @@ -0,0 +1,3 @@ +.o_field_x2many_2d_matrix .row-total { + font-weight: bold; +} 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 000000000000..ac627e44c7c0 --- /dev/null +++ b/web_widget_x2many_2d_matrix/static/src/js/2d_matrix_renderer.js @@ -0,0 +1,582 @@ +/* Copyright 2018 Simone Orsi + * License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). */ + +odoo.define('web_widget_x2many_2d_matrix.X2Many2dMatrixRenderer', function (require) { + "use strict"; + + // Heavily inspired by Odoo's `ListRenderer` + var BasicRenderer = require('web.BasicRenderer'); + var config = require('web.config'); + var core = require('web.core'); + var field_utils = require('web.field_utils'); + var _t = core._t; + var FIELD_CLASSES = { + // Copied from ListRenderer + float: 'o_list_number', + integer: 'o_list_number', + monetary: 'o_list_number', + text: 'o_list_text', + }; + + 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; + + // Display a nice message if there's no data to display + this.$el.empty(); + 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' + ); + this.$el + .addClass('table-responsive') + .append($table); + + this._computeColumnAggregates(); + this._computeRowAggregates(); + + $table + .append(this._renderHeader()) + .append(this._renderBody()); + if (self.matrix_data.show_column_totals) { + $table.append(this._renderFooter()); + } + 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 = _.map(this.columns, function (node, index) { + var record = row.data[index]; + // Make the widget use our field value for each cell + node.attrs.name = this.matrix_data.field_value; + return this._renderBodyCell(record, node, 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'); + } + + 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, this._renderRow.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 text-right'}); + this._apply_aggregate_value($cell, row.aggregate); + 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 === 'button') { + tdClassName += ' o_list_button'; + } else 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; + var widget = this._renderFieldWidget( + node, record, _.pick(options, 'mode') + ); + this._handleAttributes(widget.$el, node); + return $td.append(widget.$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 text-right'}); + this._apply_aggregate_value($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 = $('', {class: 'col-total text-right'}); + if (column.aggregate) { + self._apply_aggregate_value($cell, column.aggregate); + } + 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 = { + fname: fname, + ftype: type, + help: _t('Sum Total'), + value: 0, + }; + _.each(this.columns, function (column, index) { + column.aggregate = { + fname: fname, + ftype: type, + 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.value += column.aggregate.value; + }.bind(this)); + }, + + /** + * @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 = { + fname: fname, + ftype: type, + 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} aggregate + * The object which contains the information about the aggregate value + */ + _apply_aggregate_value: function ($cell, aggregate) { + var field = this.state.fields[aggregate.fname], + formatter = field_utils.format[field.type]; + var formattedValue = formatter( + aggregate.value, field, {escape: true} + ); + $cell.addClass('total').attr('title', aggregate.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/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 000000000000..5b24c457a1ae --- /dev/null +++ b/web_widget_x2many_2d_matrix/static/src/js/abstract_view_matrix_limit_extend.js @@ -0,0 +1,17 @@ +odoo.define( "web_widget_x2many_2d_matrix.matrix_limit_extend", function (require) { +"use strict"; + + var AbstractView = require("web.AbstractView"); + + AbstractView.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 000000000000..addf7cd4087e --- /dev/null +++ b/web_widget_x2many_2d_matrix/static/src/js/widget_x2many_2d_matrix.js @@ -0,0 +1,224 @@ +/* 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_x_axis = {}; + this.by_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]; + } + } + // And this? + this.field_editability = + node.field_editability || this.field_editability; + this.show_row_totals = + this.parse_boolean(node.show_row_totals || '1'); + this.show_column_totals = + this.parse_boolean(node.show_column_totals || '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_x_axis = {}; + this.by_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_x_axis[x] = this.by_x_axis[x] || {}; + this.by_y_axis[y] = this.by_y_axis[y] || {}; + this.by_x_axis[x][y] = record; + this.by_y_axis[y][x] = record; + }.bind(this)); + // Init columns + this.columns = []; + $.each(this.by_x_axis, function (x) { + this.columns.push(this._make_column(x)); + }.bind(this)); + this.rows = []; + $.each(this.by_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, + }; + }, + + /** + * 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 = {'data': []}; + $.each(self.by_x_axis, function (x) { + row.data.push(self.by_y_axis[y][x]); + }); + return row; + }, + + /** + * 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/views/assets.xml b/web_widget_x2many_2d_matrix/views/assets.xml new file mode 100644 index 000000000000..6083a642fa0c --- /dev/null +++ b/web_widget_x2many_2d_matrix/views/assets.xml @@ -0,0 +1,16 @@ + + + + +