diff --git a/setup/web_custom_modifier/odoo/addons/web_custom_modifier b/setup/web_custom_modifier/odoo/addons/web_custom_modifier new file mode 120000 index 000000000..a8a681d3e --- /dev/null +++ b/setup/web_custom_modifier/odoo/addons/web_custom_modifier @@ -0,0 +1 @@ +../../../../web_custom_modifier \ No newline at end of file diff --git a/setup/web_custom_modifier/setup.py b/setup/web_custom_modifier/setup.py new file mode 100644 index 000000000..28c57bb64 --- /dev/null +++ b/setup/web_custom_modifier/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +) diff --git a/web_custom_modifier/README.rst b/web_custom_modifier/README.rst new file mode 100644 index 000000000..ead290e24 --- /dev/null +++ b/web_custom_modifier/README.rst @@ -0,0 +1,215 @@ +=================== +Web Custom Modifier +=================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:3cda9a1f864e81bc69284b8cd6c0219a277f33993b92eba42c626b37f033d8cc + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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-LGPL--3-blue.png + :target: http://www.gnu.org/licenses/lgpl-3.0-standalone.html + :alt: License: LGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fweb-lightgray.png?logo=github + :target: https://github.com/OCA/web/tree/14.0/web_custom_modifier + :alt: OCA/web +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/web-14-0/web-14-0-web_custom_modifier + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png + :target: https://runboat.odoo-community.org/builds?repo=OCA/web&target_branch=14.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module allows to customize modifiers on form and tree view nodes. + +For example, it allows to make a field readonly, invisible or required. + +**Table of contents** + +.. contents:: + :local: + +Use Cases / Context +=================== + +In Odoo, when you want to customize some fields or views such as hiding +a field, changing the number of line of a list view or make a field +mandatory, you need to have technical knowledge. + +This module allows functional people to apply some customizations +without having to code. + +Usage +===== + +Access to the module +-------------------- + +As system administrator, I go to *Settings / Technical / User Interface +/ Custom Modifiers*. |Custom Modifier Menu| + +Requiered Field +--------------- + +I create a new custom modifier. |New Modifier| + +The modifier is configured to make the field ``default_code`` of a +product required. + +After refreshing my screen, I go to the form view of a product. + +I notice that the field ``default_code`` is required. |Product form| + +Hide selection item +------------------- + +The module allows to hide an item (option) of a selection field. |Hide +Selction Item Modifier| + +The above example hides the type of address ``Other``. |Contact Form +Witout Selection Item| + +Beware that if the hidden option is already selected on a record, it +will look as it was never set. |Contact Form Type not Selected| + +Therefore, this feature should only be used to hide options that are +never used. + +Force Save +---------- + +A new option ``Force Save`` is available. |Force Save Modifier| + +This modifier may be used along with the ``Readonly`` modifier so that +the field value is saved to the server. + +Custom Widget +------------- + +It is possible to customize the widget used for a given field. |Custom +Widget| |Task Form with Custom Widget| + +Number of lines per page (List Views) +------------------------------------- + +A new modifier is added to set the number of lines per page in list +view. + +In the following example, we set a limit of 20 sale order lines per page +on a sale order form view. + +|Number of lines per Page Modifier| |Sale Order with Limited SOL per +Page| + +Advanced Usage +-------------- + +In the field ``Type``, I can select ``Xpath``. This allows to set a +modifier for a specific view node, such as a button. |Button Modifier| +The example above hides the a button in the form view of a product. + +Excluded Groups +--------------- + +A new field ``Excluded Groups`` is available. |Excluded Groups| + +If at least one group of users is selected, the modifier is not applied +for users that are member of any of these groups. + +This is useful when rendering an element readonly or invisible only for +a subset of users. + +Optional +-------- + +Since the version **14.0.2.0.1**, it is possible to customize the +optional of a given field on a tree view. + +The ``Optional`` modifier takes 2 possible keys: + +- show : To make the field displayed by default in the tree view. +- hide : To make the field hidden by default in the 3 dots of a tree + view. + +.. important:: + + The field must be present by default on the tree view and displayed. + +Example: + +As system administrator, I go to +``Settings / Technical / User Interface / Custom Modifiers``. + +I add the field name (labelled as ``Number`` in Quotation List View) of +the model ``sale.order``. + +I select the modifier ``Optional`` and then set the key ``show``. +|Optional Modifier| + +I go to the Quotation List View and notice that the field is now shown +by default and that it is possible to hide it. |Optional Modifier +Applied| + +.. |Custom Modifier Menu| image:: https://raw.githubusercontent.com/OCA/web/14.0/web_custom_modifier/static/description/custom_modifier_menu.png +.. |New Modifier| image:: https://raw.githubusercontent.com/OCA/web/14.0/web_custom_modifier/static/description/new_custom_modifier.png +.. |Product form| image:: https://raw.githubusercontent.com/OCA/web/14.0/web_custom_modifier/static/description/product_form.png +.. |Hide Selction Item Modifier| image:: https://raw.githubusercontent.com/OCA/web/14.0/web_custom_modifier/static/description/hide_selection_item_modifier.png +.. |Contact Form Witout Selection Item| image:: https://raw.githubusercontent.com/OCA/web/14.0/web_custom_modifier/static/description/contact_form_without_selection_item.png +.. |Contact Form Type not Selected| image:: https://raw.githubusercontent.com/OCA/web/14.0/web_custom_modifier/static/description/contact_form_type_not_selected.png +.. |Force Save Modifier| image:: https://raw.githubusercontent.com/OCA/web/14.0/web_custom_modifier/static/description/force_save_modifier.png +.. |Custom Widget| image:: https://raw.githubusercontent.com/OCA/web/14.0/web_custom_modifier/static/description/custom_widget.png +.. |Task Form with Custom Widget| image:: https://raw.githubusercontent.com/OCA/web/14.0/web_custom_modifier/static/description/task_form_with_custom_widget.png +.. |Number of lines per Page Modifier| image:: https://raw.githubusercontent.com/OCA/web/14.0/web_custom_modifier/static/description/number_lines_per_page_modifier.png +.. |Sale Order with Limited SOL per Page| image:: https://raw.githubusercontent.com/OCA/web/14.0/web_custom_modifier/static/description/sale_order_with_limited_sol_per_page.png +.. |Button Modifier| image:: https://raw.githubusercontent.com/OCA/web/14.0/web_custom_modifier/static/description/button_modifier.png +.. |Excluded Groups| image:: https://raw.githubusercontent.com/OCA/web/14.0/web_custom_modifier/static/description/excluded_groups.png +.. |Optional Modifier| image:: https://raw.githubusercontent.com/OCA/web/14.0/web_custom_modifier/static/description/optional_modifier.png +.. |Optional Modifier Applied| image:: https://raw.githubusercontent.com/OCA/web/14.0/web_custom_modifier/static/description/optional_modifier_applied.png + +Known issues / Roadmap +====================== + +- Set a default value on a field for all users. + +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 to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +------- + +* Numigi + +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_custom_modifier/__init__.py b/web_custom_modifier/__init__.py new file mode 100644 index 000000000..ee214276d --- /dev/null +++ b/web_custom_modifier/__init__.py @@ -0,0 +1,4 @@ +# Copyright 2023 Numigi (tm) and all its contributors (https://bit.ly/numigiens) +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +from . import models diff --git a/web_custom_modifier/__manifest__.py b/web_custom_modifier/__manifest__.py new file mode 100644 index 000000000..d30076185 --- /dev/null +++ b/web_custom_modifier/__manifest__.py @@ -0,0 +1,19 @@ +# Copyright 2023 Numigi (tm) and all its contributors (https://bit.ly/numigiens) +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +{ + "name": "Web Custom Modifier", + "version": "14.0.2.0.1", + "author": "Numigi, " "Odoo Community Association (OCA)", + "maintainer": "Numigi", + "website": "https://github.com/OCA/web", + "license": "LGPL-3", + "category": "Project", + "summary": "Enable easily customizing view modifiers.", + "depends": ["base"], + "data": [ + "views/web_custom_modifier.xml", + "security/ir.model.access.csv", + ], + "installable": True, +} diff --git a/web_custom_modifier/i18n/fr.po b/web_custom_modifier/i18n/fr.po new file mode 100644 index 000000000..ff4e76805 --- /dev/null +++ b/web_custom_modifier/i18n/fr.po @@ -0,0 +1,178 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * web_custom_modifier +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 12.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2022-07-15 15:07+0000\n" +"PO-Revision-Date: 2022-07-15 15:07+0000\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_custom_modifier +#: model:ir.model.fields,field_description:web_custom_modifier.field_web_custom_modifier__active +msgid "Active" +msgstr "Actif" + +#. module: web_custom_modifier +#: model_terms:ir.ui.view,arch_db:web_custom_modifier.custom_modifier_search +msgid "Archived" +msgstr "Archivé" + +#. module: web_custom_modifier +#: model:ir.model,name:web_custom_modifier.model_base +msgid "Base" +msgstr "Base" + +#. module: web_custom_modifier +#: model:ir.model.fields,field_description:web_custom_modifier.field_web_custom_modifier__create_uid +msgid "Created by" +msgstr "Créé par" + +#. module: web_custom_modifier +#: model:ir.model.fields,field_description:web_custom_modifier.field_web_custom_modifier__create_date +msgid "Created on" +msgstr "Créé le" + +#. module: web_custom_modifier +#: model:ir.actions.act_window,name:web_custom_modifier.custom_modifier_action +#: model:ir.ui.menu,name:web_custom_modifier.custom_modifier_menu +#: model_terms:ir.ui.view,arch_db:web_custom_modifier.custom_modifier_list +#: model_terms:ir.ui.view,arch_db:web_custom_modifier.custom_modifier_search +msgid "Custom Modifiers" +msgstr "Modificateurs personnalisés" + +#. module: web_custom_modifier +#: model:ir.model,name:web_custom_modifier.model_web_custom_modifier +msgid "Custom View Modifier" +msgstr "Modificateurs de vue personnalisés" + +#. module: web_custom_modifier +#: model:ir.model.fields,field_description:web_custom_modifier.field_web_custom_modifier__display_name +msgid "Display Name" +msgstr "Nom affiché" + +#. module: web_custom_modifier +#: model:ir.model.fields,field_description:web_custom_modifier.field_web_custom_modifier__excluded_group_ids +msgid "Excluded Groups" +msgstr "Groupes exclus" + +#. module: web_custom_modifier +#: model_terms:ir.ui.view,arch_db:web_custom_modifier.custom_modifier_list +#: model_terms:ir.ui.view,arch_db:web_custom_modifier.custom_modifier_search +#: selection:web.custom.modifier,type_:0 +msgid "Field" +msgstr "Champ" + +#. module: web_custom_modifier +#: selection:web.custom.modifier,modifier:0 +msgid "Force Save" +msgstr "Forcer la sauvegarde" + +#. module: web_custom_modifier +#: model_terms:ir.ui.view,arch_db:web_custom_modifier.custom_modifier_search +msgid "Group By" +msgstr "Grouper par" + +#. module: web_custom_modifier +#: selection:web.custom.modifier,modifier:0 +msgid "Hide Selection Item" +msgstr "Cacher un choix de sélection" + +#. module: web_custom_modifier +#: model:ir.model.fields,field_description:web_custom_modifier.field_web_custom_modifier__id +msgid "ID" +msgstr "ID" + +#. module: web_custom_modifier +#: selection:web.custom.modifier,modifier:0 +msgid "Invisible" +msgstr "Invisible" + +#. module: web_custom_modifier +#: selection:web.custom.modifier,modifier:0 +msgid "Invisible (List Views)" +msgstr "Invisible (vues listes)" + +#. module: web_custom_modifier +#: model:ir.model.fields,field_description:web_custom_modifier.field_web_custom_modifier__key +msgid "Key" +msgstr "Clé" + +#. module: web_custom_modifier +#: model:ir.model.fields,field_description:web_custom_modifier.field_web_custom_modifier____last_update +msgid "Last Modified on" +msgstr "Dernière modification le" + +#. module: web_custom_modifier +#: model:ir.model.fields,field_description:web_custom_modifier.field_web_custom_modifier__write_uid +msgid "Last Updated by" +msgstr "Dernière mise à jour par" + +#. module: web_custom_modifier +#: model:ir.model.fields,field_description:web_custom_modifier.field_web_custom_modifier__write_date +msgid "Last Updated on" +msgstr "Dernière mise à jour le" + +#. module: web_custom_modifier +#: model:ir.model.fields,field_description:web_custom_modifier.field_web_custom_modifier__model_ids +msgid "Model" +msgstr "Modèle" + +#. module: web_custom_modifier +#: model:ir.model.fields,field_description:web_custom_modifier.field_web_custom_modifier__modifier +#: model_terms:ir.ui.view,arch_db:web_custom_modifier.custom_modifier_search +msgid "Modifier" +msgstr "Modificateur" + +#. module: web_custom_modifier +#: selection:web.custom.modifier,modifier:0 +msgid "Number of lines per page (List Views)" +msgstr "Nombre de lignes par page (Vues listes)" + +#. module: web_custom_modifier +#: selection:web.custom.modifier,modifier:0 +msgid "Readonly" +msgstr "Lecture seule" + +#. module: web_custom_modifier +#: model:ir.model.fields,field_description:web_custom_modifier.field_web_custom_modifier__reference +msgid "Reference" +msgstr "Référence" + +#. module: web_custom_modifier +#: selection:web.custom.modifier,modifier:0 +msgid "Required" +msgstr "Obligatoire" + +#. module: web_custom_modifier +#: model:ir.model.fields,field_description:web_custom_modifier.field_web_custom_modifier__type_ +#: model_terms:ir.ui.view,arch_db:web_custom_modifier.custom_modifier_search +msgid "Type" +msgstr "Type" + +#. module: web_custom_modifier +#: model:ir.model,name:web_custom_modifier.model_ir_ui_view +msgid "View" +msgstr "View" + +#. module: web_custom_modifier +#: selection:web.custom.modifier,modifier:0 +msgid "Widget" +msgstr "Widget" + +#. module: web_custom_modifier +#: selection:web.custom.modifier,type_:0 +msgid "Xpath" +msgstr "Xpath" + +#. module: web_custom_modifier +#: model:ir.model.fields.selection,name:web_custom_modifier.selection__web_custom_modifier__modifier__optional +msgid "Optional" +msgstr "Optionnel" diff --git a/web_custom_modifier/models/__init__.py b/web_custom_modifier/models/__init__.py new file mode 100644 index 000000000..5f2756331 --- /dev/null +++ b/web_custom_modifier/models/__init__.py @@ -0,0 +1,8 @@ +# Copyright 2023 Numigi (tm) and all its contributors (https://bit.ly/numigiens) +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +from . import ( + base, + ir_ui_view, + web_custom_modifier, +) diff --git a/web_custom_modifier/models/base.py b/web_custom_modifier/models/base.py new file mode 100644 index 000000000..124907f18 --- /dev/null +++ b/web_custom_modifier/models/base.py @@ -0,0 +1,19 @@ +# Copyright 2023 - today Numigi (tm) and all its contributors (https://bit.ly/numigiens) +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +from odoo import api, models + +from ..utils import set_custom_modifiers_on_fields + + +class Base(models.AbstractModel): + + _inherit = "base" + + @api.model + def fields_get(self, allfields=None, attributes=None): + """Add the custom modifiers to the fields metadata.""" + fields = super().fields_get(allfields, attributes) + modifiers = self.env["web.custom.modifier"].get(self._name) + set_custom_modifiers_on_fields(modifiers, fields) + return fields diff --git a/web_custom_modifier/models/common.py b/web_custom_modifier/models/common.py new file mode 100644 index 000000000..bd820b3f7 --- /dev/null +++ b/web_custom_modifier/models/common.py @@ -0,0 +1,26 @@ +# © 2023 - today Numigi (tm) and all its contributors (https://bit.ly/numigiens) +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +from typing import List, Mapping + + +def set_custom_modifiers_on_fields(modifiers: List[dict], fields: Mapping[str, dict]): + _hide_selection_items(modifiers, fields) + + +def _hide_selection_items(modifiers, fields): + hidden_items = ( + m + for m in modifiers + if m["type_"] == "field" and m["modifier"] == "selection_hide" + ) + for item in hidden_items: + _hide_single_selection_item(item, fields) + + +def _hide_single_selection_item(modifier, fields): + field = fields.get(modifier["reference"]) + if field and "selection" in field: + field["selection"] = [ + (k, v) for k, v in field["selection"] if k != modifier["key"] + ] diff --git a/web_custom_modifier/models/ir_ui_view.py b/web_custom_modifier/models/ir_ui_view.py new file mode 100644 index 000000000..fce1c2f0f --- /dev/null +++ b/web_custom_modifier/models/ir_ui_view.py @@ -0,0 +1,24 @@ +# Copyright 2023 Numigi (tm) and all its contributors (https://bit.ly/numigiens) +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +from odoo import models + +from ..utils import add_custom_modifiers_to_view_arch, set_custom_modifiers_on_fields + + +class ViewWithCustomModifiers(models.Model): + _inherit = "ir.ui.view" + + def postprocess(self, node, current_node_path, editable, name_manager): + """Add custom modifiers to the view xml. + + This method is called in Odoo when generating the final xml of a view. + """ + model_name = name_manager.Model._name + modifiers = self.env["web.custom.modifier"].get(model_name) + node_with_custom_modifiers = add_custom_modifiers_to_view_arch(modifiers, node) + set_custom_modifiers_on_fields(modifiers, name_manager.available_fields) + self.clear_caches() # Clear the cache in order to recompute _get_active_rules + return super().postprocess( + node_with_custom_modifiers, current_node_path, editable, name_manager + ) diff --git a/web_custom_modifier/models/web_custom_modifier.py b/web_custom_modifier/models/web_custom_modifier.py new file mode 100644 index 000000000..fa39c7e46 --- /dev/null +++ b/web_custom_modifier/models/web_custom_modifier.py @@ -0,0 +1,95 @@ +# Copyright 2023 Numigi (tm) and all its contributors (https://bit.ly/numigiens) +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +from odoo import api, fields, models, tools + + +class WebCustomModifier(models.Model): + + _name = "web.custom.modifier" + _description = "Custom View Modifier" + + model_ids = fields.Many2many( + "ir.model", "ir_model_custom_modifier", "modifier_id", "model_id", "Model" + ) + type_ = fields.Selection( + [ + ("field", "Field"), + ("xpath", "Xpath"), + ], + string="Type", + default="field", + required=True, + ) + modifier = fields.Selection( + [ + ("invisible", "Invisible"), + ("column_invisible", "Invisible (List Views)"), + ("readonly", "Readonly"), + ("force_save", "Force Save"), + ("required", "Required"), + ("selection_hide", "Hide Selection Item"), + ("widget", "Widget"), + ("limit", "Number of lines per page (List Views)"), + ("optional", "Optional"), + ], + required=True, + ) + reference = fields.Char(required=True) + key = fields.Char() + active = fields.Boolean(default=True) + excluded_group_ids = fields.Many2many( + "res.groups", + "web_custom_modifier_excluded_group_rel", + "modifier_id", + "group_id", + "Excluded Groups", + ) + + @api.model + def create(self, vals): + new_record = super().create(vals) + self._clear_modifier_cache() + return new_record + + def write(self, vals): + super().write(vals) + self._clear_modifier_cache() + return True + + def unlink(self): + super().unlink() + self._clear_modifier_cache() + return True + + def _clear_modifier_cache(self): + for model in ( + self.sudo().env["web.custom.modifier"].search([]).mapped("model_ids.model") + ): + self.env[model].clear_caches() + + @tools.ormcache() + def _get_cache(self): + return [ + el._to_dict() for el in self.sudo().env["web.custom.modifier"].search([]) + ] + + def _to_dict(self): + return { + "models": self.mapped("model_ids.model"), + "key": self.key, + "type_": self.type_, + "modifier": self.modifier, + "reference": self.reference, + "excluded_group_ids": self.excluded_group_ids.ids, + } + + def get(self, model): + cache = self._get_cache() + user_group_ids = self.env.user.groups_id.ids + return [ + el + for el in cache + if model in el["models"] + and all(id_ not in user_group_ids for id_ in el["excluded_group_ids"]) + ] diff --git a/web_custom_modifier/readme/CONTEXT.md b/web_custom_modifier/readme/CONTEXT.md new file mode 100644 index 000000000..e313b2b60 --- /dev/null +++ b/web_custom_modifier/readme/CONTEXT.md @@ -0,0 +1,3 @@ +In Odoo, when you want to customize some fields or views such as hiding a field, changing the number of line of a list view or make a field mandatory, you need to have technical knowledge. + +This module allows functional people to apply some customizations without having to code. \ No newline at end of file diff --git a/web_custom_modifier/readme/CONTRIBUTOR.md b/web_custom_modifier/readme/CONTRIBUTOR.md new file mode 100644 index 000000000..e86d4e78a --- /dev/null +++ b/web_custom_modifier/readme/CONTRIBUTOR.md @@ -0,0 +1,5 @@ +- David Dufresne david.dufresne@numigi.com (www.numigi.com) +- Majda El Mariouli majda.elmariouli@numigi.com (www.numigi.com) +- Lanto Razafindrabe lanto.razafindrabe@numigi.com (www.numigi.com) +- Julie LeBrun julie.lebrun@numigi.com (www.numigi.com) +- Abdellatif Benzbiria abdellatif.benzbiria@numigi.com (www.numigi.com) diff --git a/web_custom_modifier/readme/DESCRIPTION.md b/web_custom_modifier/readme/DESCRIPTION.md new file mode 100644 index 000000000..8dd71eef7 --- /dev/null +++ b/web_custom_modifier/readme/DESCRIPTION.md @@ -0,0 +1,3 @@ +This module allows to customize modifiers on form and tree view nodes. + +For example, it allows to make a field readonly, invisible or required. \ No newline at end of file diff --git a/web_custom_modifier/readme/ROADMAP.md b/web_custom_modifier/readme/ROADMAP.md new file mode 100644 index 000000000..c828040c7 --- /dev/null +++ b/web_custom_modifier/readme/ROADMAP.md @@ -0,0 +1 @@ +- Set a default value on a field for all users. \ No newline at end of file diff --git a/web_custom_modifier/readme/USAGE.md b/web_custom_modifier/readme/USAGE.md new file mode 100644 index 000000000..5bfa09ca9 --- /dev/null +++ b/web_custom_modifier/readme/USAGE.md @@ -0,0 +1,91 @@ +## Access to the module +As system administrator, I go to *Settings / Technical / User Interface / Custom Modifiers*. +![Custom Modifier Menu](../static/description/custom_modifier_menu.png) + +## Requiered Field +I create a new custom modifier. +![New Modifier](../static/description/new_custom_modifier.png) + +The modifier is configured to make the field `default_code` of a product required. + +After refreshing my screen, I go to the form view of a product. + +I notice that the field `default_code` is required. +![Product form](../static/description/product_form.png) + +## Hide selection item +The module allows to hide an item (option) of a selection field. +![Hide Selction Item Modifier](../static/description/hide_selection_item_modifier.png) + +The above example hides the type of address `Other`. +![Contact Form Witout Selection Item](../static/description/contact_form_without_selection_item.png) + +Beware that if the hidden option is already selected on a record, it will look as it was never set. +![Contact Form Type not Selected](../static/description/contact_form_type_not_selected.png) + +Therefore, this feature should only be used to hide options that are never used. + +## Force Save +A new option `Force Save` is available. +![Force Save Modifier](../static/description/force_save_modifier.png) + +This modifier may be used along with the `Readonly` modifier so that the field value is saved to the server. + +## Custom Widget +It is possible to customize the widget used for a given field. +![Custom Widget](../static/description/custom_widget.png) +![Task Form with Custom Widget](../static/description/task_form_with_custom_widget.png) + + +## Number of lines per page (List Views) +A new modifier is added to set the number of lines per page in list view. + +In the following example, we set a limit of 20 sale order lines per page on a sale order form view. + +![Number of lines per Page Modifier](../static/description/number_lines_per_page_modifier.png) +![Sale Order with Limited SOL per Page](../static/description/sale_order_with_limited_sol_per_page.png) + +## Advanced Usage +In the field `Type`, I can select `Xpath`. This allows to set a modifier for a specific view node, such as a button. +![Button Modifier](../static/description/button_modifier.png) +The example above hides the a button in the form view of a product. + +## Excluded Groups +A new field `Excluded Groups` is available. +![Excluded Groups](../static/description/excluded_groups.png) + +If at least one group of users is selected, the modifier is not applied for users that are member of any of these groups. + +This is useful when rendering an element readonly or invisible only for a subset of users. + + + +## Optional +Since the version **14.0.2.0.1**, it is possible to customize the optional of a given field on a tree view. + +The `Optional` modifier takes 2 possible keys: +- show : To make the field displayed by default in the tree view. +- hide : To make the field hidden by default in the 3 dots of a tree view. + +> [!IMPORTANT] +> The field must be present by default on the tree view and displayed. + +Example: + +As system administrator, I go to `Settings / Technical / User Interface / Custom Modifiers`. + +I add the field name (labelled as `Number` in Quotation List View) of the model `sale.order`. + +I select the modifier `Optional` and then set the key `show`. +![Optional Modifier](../static/description/optional_modifier.png) + +I go to the Quotation List View and notice that the field is now shown by default and that it is possible to hide it. +![Optional Modifier Applied](../static/description/optional_modifier_applied.png) + + + + + + + + diff --git a/web_custom_modifier/security/ir.model.access.csv b/web_custom_modifier/security/ir.model.access.csv new file mode 100644 index 000000000..7b3d7984f --- /dev/null +++ b/web_custom_modifier/security/ir.model.access.csv @@ -0,0 +1,3 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_web_custom_modifier,access_web_custom_modifier,model_web_custom_modifier,base.group_user,1,0,0,0 +access_web_custom_modifier_admin,access_web_custom_modifier_admin,model_web_custom_modifier,base.group_erp_manager,1,1,1,1 diff --git a/web_custom_modifier/static/description/button_modifier.png b/web_custom_modifier/static/description/button_modifier.png new file mode 100644 index 000000000..14e97bff4 Binary files /dev/null and b/web_custom_modifier/static/description/button_modifier.png differ diff --git a/web_custom_modifier/static/description/contact_form_type_not_selected.png b/web_custom_modifier/static/description/contact_form_type_not_selected.png new file mode 100644 index 000000000..95012d7a4 Binary files /dev/null and b/web_custom_modifier/static/description/contact_form_type_not_selected.png differ diff --git a/web_custom_modifier/static/description/contact_form_without_selection_item.png b/web_custom_modifier/static/description/contact_form_without_selection_item.png new file mode 100644 index 000000000..721b600cb Binary files /dev/null and b/web_custom_modifier/static/description/contact_form_without_selection_item.png differ diff --git a/web_custom_modifier/static/description/custom_modifier_menu.png b/web_custom_modifier/static/description/custom_modifier_menu.png new file mode 100644 index 000000000..f14e0a3d1 Binary files /dev/null and b/web_custom_modifier/static/description/custom_modifier_menu.png differ diff --git a/web_custom_modifier/static/description/custom_widget.png b/web_custom_modifier/static/description/custom_widget.png new file mode 100644 index 000000000..8abe3ee56 Binary files /dev/null and b/web_custom_modifier/static/description/custom_widget.png differ diff --git a/web_custom_modifier/static/description/excluded_groups.png b/web_custom_modifier/static/description/excluded_groups.png new file mode 100644 index 000000000..ff7693492 Binary files /dev/null and b/web_custom_modifier/static/description/excluded_groups.png differ diff --git a/web_custom_modifier/static/description/force_save_modifier.png b/web_custom_modifier/static/description/force_save_modifier.png new file mode 100644 index 000000000..cf09ac54b Binary files /dev/null and b/web_custom_modifier/static/description/force_save_modifier.png differ diff --git a/web_custom_modifier/static/description/hide_selection_item_modifier.png b/web_custom_modifier/static/description/hide_selection_item_modifier.png new file mode 100644 index 000000000..5c9401edf Binary files /dev/null and b/web_custom_modifier/static/description/hide_selection_item_modifier.png differ diff --git a/web_custom_modifier/static/description/icon.png b/web_custom_modifier/static/description/icon.png new file mode 100644 index 000000000..3a0328b51 Binary files /dev/null and b/web_custom_modifier/static/description/icon.png differ diff --git a/web_custom_modifier/static/description/index.html b/web_custom_modifier/static/description/index.html new file mode 100644 index 000000000..07881527a --- /dev/null +++ b/web_custom_modifier/static/description/index.html @@ -0,0 +1,524 @@ + + + + + + +Web Custom Modifier + + + +
+

Web Custom Modifier

+ + +

Beta License: LGPL-3 OCA/web Translate me on Weblate Try me on Runboat

+

This module allows to customize modifiers on form and tree view nodes.

+

For example, it allows to make a field readonly, invisible or required.

+

Table of contents

+ +
+

Use Cases / Context

+

In Odoo, when you want to customize some fields or views such as hiding +a field, changing the number of line of a list view or make a field +mandatory, you need to have technical knowledge.

+

This module allows functional people to apply some customizations +without having to code.

+
+
+

Usage

+
+

Access to the module

+

As system administrator, I go to Settings / Technical / User Interface +/ Custom Modifiers. Custom Modifier Menu

+
+
+

Requiered Field

+

I create a new custom modifier. New Modifier

+

The modifier is configured to make the field default_code of a +product required.

+

After refreshing my screen, I go to the form view of a product.

+

I notice that the field default_code is required. Product form

+
+
+

Hide selection item

+

The module allows to hide an item (option) of a selection field. Hide Selction Item Modifier

+

The above example hides the type of address Other. Contact Form Witout Selection Item

+

Beware that if the hidden option is already selected on a record, it +will look as it was never set. Contact Form Type not Selected

+

Therefore, this feature should only be used to hide options that are +never used.

+
+
+

Force Save

+

A new option Force Save is available. Force Save Modifier

+

This modifier may be used along with the Readonly modifier so that +the field value is saved to the server.

+
+
+

Custom Widget

+

It is possible to customize the widget used for a given field. Custom Widget Task Form with Custom Widget

+
+
+

Number of lines per page (List Views)

+

A new modifier is added to set the number of lines per page in list +view.

+

In the following example, we set a limit of 20 sale order lines per page +on a sale order form view.

+

Number of lines per Page Modifier Sale Order with Limited SOL per Page

+
+
+

Advanced Usage

+

In the field Type, I can select Xpath. This allows to set a +modifier for a specific view node, such as a button. Button Modifier +The example above hides the a button in the form view of a product.

+
+
+

Excluded Groups

+

A new field Excluded Groups is available. Excluded Groups

+

If at least one group of users is selected, the modifier is not applied +for users that are member of any of these groups.

+

This is useful when rendering an element readonly or invisible only for +a subset of users.

+
+
+

Optional

+

Since the version 14.0.2.0.1, it is possible to customize the +optional of a given field on a tree view.

+

The Optional modifier takes 2 possible keys:

+
    +
  • show : To make the field displayed by default in the tree view.
  • +
  • hide : To make the field hidden by default in the 3 dots of a tree +view.
  • +
+
+

Important

+

The field must be present by default on the tree view and displayed.

+
+

Example:

+

As system administrator, I go to +Settings / Technical / User Interface / Custom Modifiers.

+

I add the field name (labelled as Number in Quotation List View) of +the model sale.order.

+

I select the modifier Optional and then set the key show. +Optional Modifier

+

I go to the Quotation List View and notice that the field is now shown +by default and that it is possible to hide it. Optional Modifier Applied

+
+
+
+

Known issues / Roadmap

+
    +
  • Set a default value on a field for all users.
  • +
+
+
+

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 to smash it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • Numigi
  • +
+
+
+

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_custom_modifier/static/description/new_custom_modifier.png b/web_custom_modifier/static/description/new_custom_modifier.png new file mode 100644 index 000000000..76eb94170 Binary files /dev/null and b/web_custom_modifier/static/description/new_custom_modifier.png differ diff --git a/web_custom_modifier/static/description/number_lines_per_page_modifier.png b/web_custom_modifier/static/description/number_lines_per_page_modifier.png new file mode 100644 index 000000000..10fa4a12b Binary files /dev/null and b/web_custom_modifier/static/description/number_lines_per_page_modifier.png differ diff --git a/web_custom_modifier/static/description/optional_modifier.png b/web_custom_modifier/static/description/optional_modifier.png new file mode 100644 index 000000000..9fcff3516 Binary files /dev/null and b/web_custom_modifier/static/description/optional_modifier.png differ diff --git a/web_custom_modifier/static/description/optional_modifier_applied.png b/web_custom_modifier/static/description/optional_modifier_applied.png new file mode 100644 index 000000000..05599293b Binary files /dev/null and b/web_custom_modifier/static/description/optional_modifier_applied.png differ diff --git a/web_custom_modifier/static/description/product_form.png b/web_custom_modifier/static/description/product_form.png new file mode 100644 index 000000000..ea6c82625 Binary files /dev/null and b/web_custom_modifier/static/description/product_form.png differ diff --git a/web_custom_modifier/static/description/sale_order_with_limited_sol_per_page.png b/web_custom_modifier/static/description/sale_order_with_limited_sol_per_page.png new file mode 100644 index 000000000..2c49d27ef Binary files /dev/null and b/web_custom_modifier/static/description/sale_order_with_limited_sol_per_page.png differ diff --git a/web_custom_modifier/static/description/task_form_with_custom_widget.png b/web_custom_modifier/static/description/task_form_with_custom_widget.png new file mode 100644 index 000000000..3a1b83df1 Binary files /dev/null and b/web_custom_modifier/static/description/task_form_with_custom_widget.png differ diff --git a/web_custom_modifier/tests/__init__.py b/web_custom_modifier/tests/__init__.py new file mode 100644 index 000000000..615f37a40 --- /dev/null +++ b/web_custom_modifier/tests/__init__.py @@ -0,0 +1,4 @@ +# Copyright 2023 Numigi (tm) and all its contributors (https://bit.ly/numigiens) +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +from . import test_view_rendering diff --git a/web_custom_modifier/tests/test_view_rendering.py b/web_custom_modifier/tests/test_view_rendering.py new file mode 100644 index 000000000..bcd430fe4 --- /dev/null +++ b/web_custom_modifier/tests/test_view_rendering.py @@ -0,0 +1,159 @@ +# Copyright 2023 Numigi (tm) and all its contributors (https://bit.ly/numigiens) +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +import json + +from lxml import etree + +from odoo.tests import common + + +def _extract_modifier_value(el, modifier): + return json.loads(el.attrib.get("modifiers") or "{}").get(modifier) + + +class TestViewRendering(common.SavepointCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.view = cls.env.ref("base.view_partner_form") + cls.email_modifier = cls.env["web.custom.modifier"].create( + { + "model_ids": [(4, cls.env.ref("base.model_res_partner").id)], + "type_": "field", + "reference": "email", + "modifier": "invisible", + } + ) + + cls.xpath = "//field[@name='street']" + cls.street_modifier = cls.env["web.custom.modifier"].create( + { + "model_ids": [(4, cls.env.ref("base.model_res_partner").id)], + "type_": "xpath", + "reference": cls.xpath, + "modifier": "invisible", + } + ) + + cls.hidden_option = "other" + cls.env["web.custom.modifier"].create( + { + "model_ids": [(4, cls.env.ref("base.model_res_partner").id)], + "type_": "field", + "reference": "type", + "modifier": "selection_hide", + "key": cls.hidden_option, + } + ) + + cls.env["web.custom.modifier"].create( + { + "model_ids": [(4, cls.env.ref("base.model_res_partner").id)], + "type_": "field", + "reference": "parent_id", + "modifier": "widget", + "key": "custom_widget", + } + ) + + cls.env["web.custom.modifier"].create( + { + "model_ids": [(4, cls.env.ref("base.model_ir_model").id)], + "type_": "xpath", + "reference": "//field[@name='field_id']//tree", + "modifier": "limit", + "key": "20", + } + ) + + cls.env["web.custom.modifier"].create( + { + "model_ids": [(4, cls.env.ref("base.model_res_partner").id)], + "type_": "field", + "reference": "name", + "modifier": "optional", + "key": "show", + } + ) + + def _get_rendered_view_tree(self): + arch = self.env["res.partner"].fields_view_get(view_id=self.view.id)["arch"] + return etree.fromstring(arch) + + def test_field_modifier(self, modifier="invisible"): + self.email_modifier.modifier = modifier + tree = self._get_rendered_view_tree() + el = tree.xpath("//field[@name='email']")[0] + self.assertTrue(_extract_modifier_value(el, modifier)) + + def test_field_force_save(self): + self.email_modifier.modifier = "force_save" + tree = self._get_rendered_view_tree() + el = tree.xpath("//field[@name='email']")[0] + self.assertEqual(el.attrib["force_save"], "1") + + def test_two_modifier_same_field(self): + self.email_modifier.modifier = "invisible" + self.email_modifier.copy().modifier = "readonly" + self.email_modifier.copy().modifier = "column_invisible" + tree = self._get_rendered_view_tree() + el = tree.xpath("//field[@name='email']")[0] + self.assertTrue(_extract_modifier_value(el, "column_invisible")) + self.assertTrue(_extract_modifier_value(el, "readonly")) + self.assertTrue(_extract_modifier_value(el, "invisible")) + + def test_xpath_modifier(self, modifier="invisible"): + self.street_modifier.modifier = modifier + tree = self._get_rendered_view_tree() + el = tree.xpath("//field[@name='street']")[0] + self.assertTrue(_extract_modifier_value(el, modifier)) + + def test_user_in_excluded_groups(self): + modifier = "invisible" + group = self.env.ref("base.group_system") + self.street_modifier.modifier = modifier + self.street_modifier.excluded_group_ids = group + self.env.user.groups_id |= group + tree = self._get_rendered_view_tree() + el = tree.xpath("//field[@name='street']")[0] + self.assertFalse(_extract_modifier_value(el, modifier)) + + def test_user_not_in_excluded_groups(self): + modifier = "invisible" + group = self.env.ref("base.group_system") + self.street_modifier.modifier = modifier + self.street_modifier.excluded_group_ids = group + self.env.user.groups_id -= group + tree = self._get_rendered_view_tree() + el = tree.xpath("//field[@name='street']")[0] + self.assertTrue(_extract_modifier_value(el, modifier)) + + def test_selection_hide__fields_view_get(self): + fields = self.env["res.partner"].fields_view_get(view_id=self.view.id)["fields"] + options = {i[0]: i[1] for i in fields["type"]["selection"]} + self.assertNotIn(self.hidden_option, options) + + def test_selection_hide__fields_get(self): + fields = self.env["res.partner"].fields_get() + options = {i[0]: i[1] for i in fields["type"]["selection"]} + self.assertNotIn(self.hidden_option, options) + + def test_widget(self): + tree = self._get_rendered_view_tree() + el = tree.xpath("//field[@name='parent_id']")[0] + self.assertEqual(el.attrib.get("widget"), "custom_widget") + + def test_optional(self): + tree = self._get_rendered_view_tree() + el = tree.xpath("//field[@name='name']")[0] + self.assertEqual(el.attrib.get("optional"), "show") + + def test_nbr_line_per_page(self): + model_view = self.env.ref("base.view_model_form") + arch = self.env["ir.model"].fields_view_get(view_id=model_view.id)["fields"][ + "field_id" + ]["views"]["tree"]["arch"] + tree = etree.fromstring(arch) + el = tree.xpath("//tree")[0] + self.assertEqual(el.attrib.get("limit"), "20") diff --git a/web_custom_modifier/utils.py b/web_custom_modifier/utils.py new file mode 100644 index 000000000..b1e12520f --- /dev/null +++ b/web_custom_modifier/utils.py @@ -0,0 +1,88 @@ +# Copyright 2023 - today Numigi (tm) and all its contributors (https://bit.ly/numigiens) +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +import json + +STANDARD_MODIFIERS = ( + "invisible", + "column_invisible", + "readonly", + "required", +) + + +def set_custom_modifiers_on_fields(modifiers, fields): + """ + :param modifiers: list[dict] of modifiers to apply on the fields + :param fields: dict[str, dict] of model's fields and their attributes. + :return: + """ + _hide_selection_items(modifiers, fields) + + +def _hide_selection_items(modifiers, fields): + hidden_items = ( + m + for m in modifiers + if m["type_"] == "field" and m["modifier"] == "selection_hide" + ) + for item in hidden_items: + _hide_single_selection_item(item, fields) + + +def _hide_single_selection_item(modifier, fields): + field = fields.get(modifier["reference"]) + if field and "selection" in field: + field["selection"] = [ + (k, v) for k, v in field["selection"] if k != modifier["key"] + ] + + +def add_custom_modifiers_to_view_arch(modifiers, arch): + """Add custom modifiers to the given view architecture.""" + for modifier in modifiers: + _add_custom_modifier_to_view_tree(modifier, arch) + return arch + + +def _add_custom_modifier_to_view_tree(modifier, tree): + """Add a custom modifier to the given view architecture.""" + xpath_expr = modifier["reference"] + if modifier["type_"] == "field": + xpath_expr = ( + "//field[@name='{field_name}'] | //modifier[@for='{field_name}']".format( + field_name=modifier["reference"] + ) + ) + for node in tree.xpath(xpath_expr): + _add_custom_modifier_to_node(node, modifier) + + +def _add_custom_modifier_to_node(node, modifier): + key = modifier["modifier"] + if key == "widget": + node.attrib["widget"] = modifier["key"] + + if key == "optional": + node.attrib["optional"] = modifier["key"] + + elif key == "force_save": + node.attrib["force_save"] = "1" + + elif key == "limit": + node.attrib["limit"] = modifier["key"] + + elif key in STANDARD_MODIFIERS: + node.set(key, "1") + modifiers = _get_node_modifiers(node) + modifiers[key] = True + _set_node_modifiers(modifiers, node) + + +def _get_node_modifiers(node): + modifiers = node.get("modifiers") + return json.loads(modifiers) if modifiers else {} + + +def _set_node_modifiers(modifiers, node): + node.set("modifiers", json.dumps(modifiers)) diff --git a/web_custom_modifier/views/web_custom_modifier.xml b/web_custom_modifier/views/web_custom_modifier.xml new file mode 100644 index 000000000..22873a65d --- /dev/null +++ b/web_custom_modifier/views/web_custom_modifier.xml @@ -0,0 +1,77 @@ + + + + + Custom Modifier List View + web.custom.modifier + + + + + + + + + + + + + + + Custom Modifier Search View + web.custom.modifier + + + + + + + + + + + + + + + + + + Custom Modifiers + ir.actions.act_window + web.custom.modifier + tree + + + + +