diff --git a/.docker_files/main/__manifest__.py b/.docker_files/main/__manifest__.py index b80f4ef..046789f 100644 --- a/.docker_files/main/__manifest__.py +++ b/.docker_files/main/__manifest__.py @@ -14,6 +14,8 @@ "web", "resize_observer_error_catcher", "web_custom_label", + "web_custom_modifier", + ], "installable": True, } diff --git a/Dockerfile b/Dockerfile index d8d18cb..554d3aa 100644 --- a/Dockerfile +++ b/Dockerfile @@ -19,6 +19,7 @@ USER odoo COPY resize_observer_error_catcher /mnt/extra-addons/resize_observer_error_catcher COPY web_custom_label /mnt/extra-addons/web_custom_label +COPY web_custom_modifier /mnt/extra-addons/web_custom_modifier COPY .docker_files/main /mnt/extra-addons/main COPY .docker_files/odoo.conf /etc/odoo diff --git a/web_custom_modifier/README.rst b/web_custom_modifier/README.rst new file mode 100644 index 0000000..79bdec5 --- /dev/null +++ b/web_custom_modifier/README.rst @@ -0,0 +1,117 @@ +=================== +Web Custom Modifier +=================== +This module allows to customize modifiers on form view nodes. + +For example, it allows to make a field readonly, invisible or required. + +.. contents:: Table of Contents + +Usage +----- +As system administrator, I go to `Settings / Technical / User Interface / Custom Modifiers`. + +.. image:: static/description/custom_modifier_menu.png + +I create a new custom modifier. + +.. image:: 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. + +.. image:: static/description/product_form.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. + +.. image:: static/description/button_modifier.png + +The example above hides the a button in the form view of a product. + +Hide Selection Item +------------------- +The module allows to hide an item (option) of a selection field. + +.. image:: static/description/hide_selection_item_modifier.png + +The above example hides the type of address ``Other``. + +.. image:: 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. + +.. image:: 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. + +.. image:: 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. + +Excluded Groups +--------------- +A new field ``Excluded Groups`` is available. + +.. image:: 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. + +Custom Widget +------------- +It is possible to customize the widget used for a given field. + +.. image:: static/description/custom_widget.png + +.. image:: static/description/task_form_with_custom_widget.png + +Optional +-------- +Also, it is possible to customize the optional of a given field on tree view. +The Optional modifier takes 2 possible keys: "show" or "hide", +- "show": To make the fiead displayed by default in the tree view by default. +- "hide": To make the field hidden in the 3 dotes of a tree view by default. + +*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` (label as `Number` in quotation list) of the model `sale.order`. +I select the modifier `Optional` and then set the key `show`. So the field will be shown by default but now, +it is possible now to hide it. + +.. image:: static/description/optional_modifier.png + +I go to quotation list view, I refresh the page, and now I see the change as wanted. + +.. image:: static/description/optional_modifier_applied.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. + +.. image:: static/description/number_lines_per_page_modifier.png + +Result: + +.. image:: static/description/sale_order_with_limited_sol_per_page.png + +Contributors +------------ +* Numigi (tm) and all its contributors (https://bit.ly/numigiens) diff --git a/web_custom_modifier/__init__.py b/web_custom_modifier/__init__.py new file mode 100644 index 0000000..1a9d411 --- /dev/null +++ b/web_custom_modifier/__init__.py @@ -0,0 +1,4 @@ +# Copyright 2023-today Numigi 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 0000000..7ced3c9 --- /dev/null +++ b/web_custom_modifier/__manifest__.py @@ -0,0 +1,19 @@ +# Copyright 2023-today Numigi 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": "16.0.1.0.0", + "author": "Numigi", + "maintainer": "Numigi", + "website": "https://bit.ly/numigi-com", + "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 0000000..ff81ed2 --- /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 16.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-06-15 15:07+0000\n" +"PO-Revision-Date: 2023-06-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 0000000..63dc313 --- /dev/null +++ b/web_custom_modifier/models/__init__.py @@ -0,0 +1,8 @@ +# Copyright 2023-today Numigi 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 0000000..56c76a8 --- /dev/null +++ b/web_custom_modifier/models/base.py @@ -0,0 +1,38 @@ +# Copyright 2023-today Numigi 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 .common 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 + + +class Partner(models.Model): + _inherit = "res.partner" + + def name_get(self): + """ + This avoid to load removed selection option in modifiers + that would raise an error when trying to display them. Display instead, + the name of the record. + This could be improved or fixed for each case if needed. + """ + try: + res = super().name_get() + except: # noqa: E722 + res = [] + for partner in self: + name = partner.name + res.append((partner.id, name)) + return res diff --git a/web_custom_modifier/models/common.py b/web_custom_modifier/models/common.py new file mode 100644 index 0000000..041610f --- /dev/null +++ b/web_custom_modifier/models/common.py @@ -0,0 +1,26 @@ +# Copyright 2023-today Numigi 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 0000000..9b39483 --- /dev/null +++ b/web_custom_modifier/models/ir_ui_view.py @@ -0,0 +1,91 @@ +# Copyright 2023-today Numigi and all its contributors (https://bit.ly/numigiens) +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +import json +from odoo import models +from .common import set_custom_modifiers_on_fields + +STANDARD_MODIFIERS = ( + "invisible", + "column_invisible", + "readonly", + "required", +) + + +class ViewWithCustomModifiers(models.Model): + _inherit = "ir.ui.view" + + def postprocess_and_fields(self, node, model=None, **options): + modifiers = self.env["web.custom.modifier"].get(model) + node_with_custom_modifiers = _add_custom_modifiers_to_view_arch(modifiers, node) + self.clear_caches() # Clear the cache in order to recompute _get_active_rules + return super().postprocess_and_fields( + node_with_custom_modifiers, model, **options + ) + + def _postprocess_view( + self, node, model_name, editable=True, parent_name_manager=None, **options + ): + name_manager = super()._postprocess_view( + node, + model_name, + editable=editable, + parent_name_manager=parent_name_manager, + **options + ) + modifiers = self.env["web.custom.modifier"].get(model_name) + set_custom_modifiers_on_fields(modifiers, name_manager.available_fields) + return name_manager + + +def _add_custom_modifiers_to_view_arch(modifiers, arch): + """Add custom modifiers to the given view architecture.""" + if not modifiers: + return arch + 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 = ( + "//field[@name='{field_name}'] | //modifier[@for='{field_name}']".format( + field_name=modifier["reference"] + ) + if modifier["type_"] == "field" + else 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/models/web_custom_modifier.py b/web_custom_modifier/models/web_custom_modifier.py new file mode 100644 index 0000000..0c219a0 --- /dev/null +++ b/web_custom_modifier/models/web_custom_modifier.py @@ -0,0 +1,98 @@ +# Copyright 2023-today Numigi 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, modules, 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", + ) + + +class WebCustomModifierWithCachedModifiers(models.Model): + """Add a cache for getting the modifiers. + + The system cache is invalidated when any modifier record is added / modified / deleted. + """ + + _inherit = "web.custom.modifier" + + @api.model + def create(self, vals): + new_record = super().create(vals) + modules.registry.Registry(self.env.cr.dbname).clear_caches() + return new_record + + def write(self, vals): + super().write(vals) + modules.registry.Registry(self.env.cr.dbname).clear_caches() + return True + + def unlink(self): + super().unlink() + modules.registry.Registry(self.env.cr.dbname).clear_caches() + return True + + @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/security/ir.model.access.csv b/web_custom_modifier/security/ir.model.access.csv new file mode 100644 index 0000000..7b3d798 --- /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 0000000..53f48dd 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 0000000..b0c4f4f 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 0000000..987f808 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 0000000..9556f56 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 0000000..910070b 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 0000000..a4c3251 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 0000000..fac9f38 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 0000000..156905f 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 0000000..92a86b1 Binary files /dev/null and b/web_custom_modifier/static/description/icon.png differ 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 0000000..7575bf1 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 0000000..9542d3c 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 0000000..fa5684f 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 0000000..a778739 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 0000000..97e9830 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 0000000..0733b50 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 0000000..bff5787 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 0000000..e5db8af --- /dev/null +++ b/web_custom_modifier/tests/__init__.py @@ -0,0 +1,4 @@ +# Copyright 2023-today Numigi 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 0000000..da64c5a --- /dev/null +++ b/web_custom_modifier/tests/test_view_rendering.py @@ -0,0 +1,169 @@ +# Copyright 2023-today Numigi and all its contributors (https://bit.ly/numigiens) +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +import json +from ddt import data, ddt +from lxml import etree +from odoo.tests import common + + +MODIFIERS = ( + "invisible", + "readonly", + "required", +) + + +def _extract_modifier_value(el, modifier): + return json.loads(el.attrib.get("modifiers") or "{}").get(modifier) + + +@ddt +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"].get_view(view_id=self.view.id)["arch"] + return etree.fromstring(arch) + + @data(*MODIFIERS) + def test_field_modifier(self, modifier): + self.email_modifier.modifier = modifier + tree = self._get_rendered_view_tree() + el = tree.xpath("//field[@name='email']")[0] + assert _extract_modifier_value(el, modifier) is True + + 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] + assert 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] + assert ( + el.attrib.get("column_invisible") == "1" + ) # FIXME: column_invisible is moved outside of modifiers + assert _extract_modifier_value(el, "readonly") is True + assert _extract_modifier_value(el, "invisible") is True + + @data(*MODIFIERS) + def test_xpath_modifier(self, modifier): + self.street_modifier.modifier = modifier + tree = self._get_rendered_view_tree() + el = tree.xpath("//field[@name='street']")[0] + assert _extract_modifier_value(el, modifier) is True + + 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] + assert not _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] + assert _extract_modifier_value(el, modifier) + + 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"]} + assert self.hidden_option not in options + + def test_widget(self): + tree = self._get_rendered_view_tree() + el = tree.xpath("//field[@name='parent_id']")[0] + assert el.attrib.get("widget") == "custom_widget" + + def test_optional(self): + tree = self._get_rendered_view_tree() + el = tree.xpath("//field[@name='name']")[0] + assert 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"].get_view(view_id=model_view.id)["arch"] + tree = etree.fromstring(arch) + el = tree.xpath("//tree")[0] + assert el.get("limit") == "20" 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 0000000..38a8744 --- /dev/null +++ b/web_custom_modifier/views/web_custom_modifier.xml @@ -0,0 +1,51 @@ + + + + + Custom Modifier List View + web.custom.modifier + + + + + + + + + + + + + + + Custom Modifier Search View + web.custom.modifier + + + + + + + + + + + + + + + + + + Custom Modifiers + ir.actions.act_window + web.custom.modifier + tree,form + + + + + +