diff --git a/chained_swapper/README.rst b/chained_swapper/README.rst new file mode 100644 index 0000000000..bbfe97808c --- /dev/null +++ b/chained_swapper/README.rst @@ -0,0 +1,159 @@ +.. image:: https://odoo-community.org/readme-banner-image + :target: https://odoo-community.org/get-involved?utm_source=readme + :alt: Odoo Community Association + +=============== +Chained Swapper +=============== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:aed245b59dd77904c9db486ed5c337b4e0ac803e5643ca7ba2c26064ea2458f6 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Mature-brightgreen.png + :target: https://odoo-community.org/page/development-status + :alt: Mature +.. |badge2| image:: https://img.shields.io/badge/license-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%2Fserver--ux-lightgray.png?logo=github + :target: https://github.com/OCA/server-ux/tree/19.0/chained_swapper + :alt: OCA/server-ux +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/server-ux-19-0/server-ux-19-0-chained_swapper + :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/server-ux&target_branch=19.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module allows to swap the value of a field and propagate it in a +chained way to linked records. Example: changing the delivery address in +a confirmed sales order, it should be changed in its delivery orders as +well. + +It also allows to apply constraints for not allowing to do that change +according rules, so the business logic is not broken. Example: Don't +allow to change the delivery address if the delivery order is validated. + +This module requires some technical knowledge for setting the chained +swap and the constraint, as it's defined through technical names and +Python code. + +**WARNING**: Use this module with care, as it can screw up database +consistency if swaps are not properly designed . + +**Table of contents** + +.. contents:: + :local: + +Configuration +============= + +To configure this module, you need to: + +1. Go to *Setting > Field Swaps > Field Swaps*. +2. Create a new object and set the following data as an example: + + - Name for identifying it and use it for the action name. + + - Select the source model where the swap will be started. + + - Select the starting field for which the swap will be done. + + - Add several chained fields. They are expressed as a string using + dot notation, taking the source model as beginning for looking + there the first field, and continuing from there drilling through. + Example: picking_ids.partner_id for sale.order model will go to the + linked deliveries orders, and change the customer there. + + - Add possible constraints for restricting the chained swap. They are + Python expressions that must be one line that is evaluated as + boolean. If the evaluation is true, then a message will be thrown + and no swap will be allowed. You can use the variable records in + your code, that will be referring the selected records for doing + the swap. Example: for restricting sales orders that have a + delivery order validated: + + any(p.state == 'done' for p in records.mapped('picking_ids.state')) + + Other variables you can use are env, date and datetime. Each + constraint has a name for identifying it, but also for showing that + name when displaying the error trying to do the swap. + +3. Click on 'Add action' smart button to add a new action in the source + model. + +On demo databases, you can check the example "Language", that changes +the language of a contact, and propagate it to children contacts. + +Usage +===== + +To use this module, you need to: + +1. Go to the source document in list mode. +2. Select one or several records. +3. Click on Action and locate the option "Chained swap: ". +4. If one of the selected records doesn't comply with one of the + constraints, a message will be shown, and the swap won't continue. +5. If everything is OK, a popup will arise, and you will see a field for + filling the new value. +6. Click on "Change", and the swap will be done. +7. On the chatter of the source document, an entry will be logged for + reflecting the done swap. + +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 +------- + +* Tecnativa + +Contributors +------------ + +- `Tecnativa `__: + + - Ernesto Tejeda + - Pedro M. Baeza + - César A. Sánchez + - Juan Carlos Oñate + - Carlos Lopez + +- Sunanda Chhatbar + +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/server-ux `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/chained_swapper/__init__.py b/chained_swapper/__init__.py new file mode 100644 index 0000000000..b23af66646 --- /dev/null +++ b/chained_swapper/__init__.py @@ -0,0 +1,5 @@ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from . import models +from . import wizard +from .hooks import uninstall_hook diff --git a/chained_swapper/__manifest__.py b/chained_swapper/__manifest__.py new file mode 100644 index 0000000000..dd8298d29c --- /dev/null +++ b/chained_swapper/__manifest__.py @@ -0,0 +1,20 @@ +# Copyright 2020 Tecnativa - Ernesto Tejeda +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +{ + "name": "Chained Swapper", + "summary": "Chained Swapper", + "version": "19.0.1.0.0", + "development_status": "Mature", + "author": "Tecnativa, Odoo Community Association (OCA)", + "category": "Tools", + "website": "https://github.com/OCA/server-ux", + "license": "AGPL-3", + "depends": ["base"], + "data": [ + "security/ir.model.access.csv", + "views/chained_swapper_views.xml", + "wizard/chained_swapper_wizard_views.xml", + ], + "demo": ["demo/chained_swapper_demo.xml"], + "uninstall_hook": "uninstall_hook", +} diff --git a/chained_swapper/demo/chained_swapper_demo.xml b/chained_swapper/demo/chained_swapper_demo.xml new file mode 100644 index 0000000000..31821e8a8d --- /dev/null +++ b/chained_swapper/demo/chained_swapper_demo.xml @@ -0,0 +1,44 @@ + + + + + + Language + + + + + + child_ids.lang + + + + Only parent company + bool(records.mapped('parent_id')) + + + + Chained swap: Language + chained.swapper.wizard + form + new + + form + + + + + + + + diff --git a/chained_swapper/hooks.py b/chained_swapper/hooks.py new file mode 100644 index 0000000000..a0b24db7c8 --- /dev/null +++ b/chained_swapper/hooks.py @@ -0,0 +1,11 @@ +# Copyright 2020 Tecnativa - Ernesto Tejeda +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + + +def uninstall_hook(env): + """Delete the actions that were created with chained_swapper when + the module is uninstalled""" + env["ir.actions.act_window"].search( + [("res_model", "=", "chained.swapper.wizard")] + ).unlink() + return True diff --git a/chained_swapper/i18n/chained_swapper.pot b/chained_swapper/i18n/chained_swapper.pot new file mode 100644 index 0000000000..cb9025eb6b --- /dev/null +++ b/chained_swapper/i18n/chained_swapper.pot @@ -0,0 +1,352 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * chained_swapper +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 18.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: chained_swapper +#. odoo-python +#: code:addons/chained_swapper/wizard/chained_swapper_wizard.py:0 +msgid "Chained swap done:
%(field)s: %(old)s ⇒ %(new)s" +msgstr "" + +#. module: chained_swapper +#: model_terms:ir.ui.view,arch_db:chained_swapper.chained_swapper_view_form +msgid "date: The current date." +msgstr "" + +#. module: chained_swapper +#: model_terms:ir.ui.view,arch_db:chained_swapper.chained_swapper_view_form +msgid "datetime: The current date and time." +msgstr "" + +#. module: chained_swapper +#: model_terms:ir.ui.view,arch_db:chained_swapper.chained_swapper_view_form +msgid "env: The Odoo environment." +msgstr "" + +#. module: chained_swapper +#: model:ir.model.fields,field_description:chained_swapper.field_chained_swapper__ref_ir_act_window_id +msgid "Action" +msgstr "" + +#. module: chained_swapper +#: model:ir.model.fields,help:chained_swapper.field_chained_swapper__ref_ir_act_window_id +msgid "" +"Action to make this template available on records of the related document " +"model." +msgstr "" + +#. module: chained_swapper +#: model_terms:ir.ui.view,arch_db:chained_swapper.chained_swapper_view_form +msgid "Add Action" +msgstr "" + +#. module: chained_swapper +#: model_terms:ir.ui.view,arch_db:chained_swapper.chained_swapper_view_form +msgid "" +"Add a new contextual action of related documents to open a composition " +"wizard" +msgstr "" + +#. module: chained_swapper +#: model_terms:ir.ui.view,arch_db:chained_swapper.chained_swapper_view_form +msgid "Adding Constraints for Restricting the Chained Swap" +msgstr "" + +#. module: chained_swapper +#: model_terms:ir.ui.view,arch_db:chained_swapper.chained_swapper_view_form +msgid "Advanced" +msgstr "" + +#. module: chained_swapper +#: model:ir.model.fields,field_description:chained_swapper.field_chained_swapper__allowed_field_ids_domain +msgid "Allowed Field Ids Domain" +msgstr "" + +#. module: chained_swapper +#: model:ir.model.fields,help:chained_swapper.field_chained_swapper_constraint__expression +msgid "" +"Boolean python expression. You can use the keyword 'records' as the records " +"selected to execute the contextual action. Ex.: " +"bool(records.mapped('parent_id'))" +msgstr "" + +#. module: chained_swapper +#: model:ir.model,name:chained_swapper.model_chained_swapper +#: model:ir.model.fields,field_description:chained_swapper.field_chained_swapper_constraint__chained_swapper_id +#: model:ir.model.fields,field_description:chained_swapper.field_chained_swapper_sub_field__chained_swapper_id +msgid "Chained Swapper" +msgstr "" + +#. module: chained_swapper +#: model:ir.model,name:chained_swapper.model_chained_swapper_constraint +msgid "Chained Swapper Constraint" +msgstr "" + +#. module: chained_swapper +#: model:ir.model,name:chained_swapper.model_chained_swapper_sub_field +msgid "Chained Swapper Sub-field" +msgstr "" + +#. module: chained_swapper +#. odoo-python +#: code:addons/chained_swapper/models/chained_swapper.py:0 +msgid "Chained swap" +msgstr "" + +#. module: chained_swapper +#: model:ir.actions.act_window,name:chained_swapper.partner_chained_swap_lang_action_demo +msgid "Chained swap: Language" +msgstr "" + +#. module: chained_swapper +#: model_terms:ir.ui.view,arch_db:chained_swapper.chained_swapper_wizard_view_form +msgid "Change" +msgstr "" + +#. module: chained_swapper +#: model_terms:ir.ui.view,arch_db:chained_swapper.chained_swapper_wizard_view_form +msgid "Close" +msgstr "" + +#. module: chained_swapper +#: model:ir.model.fields,field_description:chained_swapper.field_chained_swapper_wizard__company_id +msgid "Company" +msgstr "" + +#. module: chained_swapper +#: model_terms:ir.ui.view,arch_db:chained_swapper.chained_swapper_view_form +msgid "Constraint Name" +msgstr "" + +#. module: chained_swapper +#: model:ir.model.fields,field_description:chained_swapper.field_chained_swapper_constraint__expression +#: model_terms:ir.ui.view,arch_db:chained_swapper.chained_swapper_constraint_view_form +msgid "Constraint expression" +msgstr "" + +#. module: chained_swapper +#: model:ir.model.fields,field_description:chained_swapper.field_chained_swapper__constraint_ids +#: model_terms:ir.ui.view,arch_db:chained_swapper.chained_swapper_view_form +msgid "Constraints" +msgstr "" + +#. module: chained_swapper +#: model:ir.model.fields,field_description:chained_swapper.field_chained_swapper__create_uid +#: model:ir.model.fields,field_description:chained_swapper.field_chained_swapper_constraint__create_uid +#: model:ir.model.fields,field_description:chained_swapper.field_chained_swapper_sub_field__create_uid +#: model:ir.model.fields,field_description:chained_swapper.field_chained_swapper_wizard__create_uid +msgid "Created by" +msgstr "" + +#. module: chained_swapper +#: model:ir.model.fields,field_description:chained_swapper.field_chained_swapper__create_date +#: model:ir.model.fields,field_description:chained_swapper.field_chained_swapper_constraint__create_date +#: model:ir.model.fields,field_description:chained_swapper.field_chained_swapper_sub_field__create_date +#: model:ir.model.fields,field_description:chained_swapper.field_chained_swapper_wizard__create_date +msgid "Created on" +msgstr "" + +#. module: chained_swapper +#: model:ir.model.fields,field_description:chained_swapper.field_chained_swapper__display_name +#: model:ir.model.fields,field_description:chained_swapper.field_chained_swapper_constraint__display_name +#: model:ir.model.fields,field_description:chained_swapper.field_chained_swapper_sub_field__display_name +#: model:ir.model.fields,field_description:chained_swapper.field_chained_swapper_wizard__display_name +msgid "Display Name" +msgstr "" + +#. module: chained_swapper +#: model_terms:ir.ui.view,arch_db:chained_swapper.chained_swapper_view_form +msgid "" +"Each constraint should have a name for identifying it. This" +" name is also used when displaying the error message in case the swap is not" +" allowed." +msgstr "" + +#. module: chained_swapper +#: model:ir.model.fields,field_description:chained_swapper.field_chained_swapper__field_id +msgid "Field" +msgstr "" + +#. module: chained_swapper +#: model:ir.actions.act_window,name:chained_swapper.chained_swapper_action +#: model:ir.ui.menu,name:chained_swapper.chained_swapper_menu +#: model:ir.ui.menu,name:chained_swapper.chained_swapper_submenu +msgid "Field Swaps" +msgstr "" + +#. module: chained_swapper +#: model:ir.model.fields,field_description:chained_swapper.field_chained_swapper__group_ids +msgid "Groups" +msgstr "" + +#. module: chained_swapper +#: model:ir.model.fields,field_description:chained_swapper.field_chained_swapper__id +#: model:ir.model.fields,field_description:chained_swapper.field_chained_swapper_constraint__id +#: model:ir.model.fields,field_description:chained_swapper.field_chained_swapper_sub_field__id +#: model:ir.model.fields,field_description:chained_swapper.field_chained_swapper_wizard__id +msgid "ID" +msgstr "" + +#. module: chained_swapper +#: model_terms:ir.ui.view,arch_db:chained_swapper.chained_swapper_view_form +msgid "" +"In this case, the constraint restricts the swap if any of the related " +"picking orders has been validated (state 'done')." +msgstr "" + +#. module: chained_swapper +#. odoo-python +#: code:addons/chained_swapper/models/chained_swapper.py:0 +msgid "Incorrect sub-field expression: %(sub_field_chain)s. %(error)s" +msgstr "" + +#. module: chained_swapper +#. odoo-python +#: code:addons/chained_swapper/models/chained_swapper.py:0 +msgid "Invalid constraint expression: %(expression)s. %(error)s." +msgstr "" + +#. module: chained_swapper +#. odoo-python +#: code:addons/chained_swapper/models/chained_swapper.py:0 +msgid "Invalid value for %(sub_field_chain)s. %(error)s" +msgstr "" + +#. module: chained_swapper +#: model:chained.swapper,name:chained_swapper.chained_swapper_demo +msgid "Language" +msgstr "" + +#. module: chained_swapper +#: model:ir.model.fields,field_description:chained_swapper.field_chained_swapper__write_uid +#: model:ir.model.fields,field_description:chained_swapper.field_chained_swapper_constraint__write_uid +#: model:ir.model.fields,field_description:chained_swapper.field_chained_swapper_sub_field__write_uid +#: model:ir.model.fields,field_description:chained_swapper.field_chained_swapper_wizard__write_uid +msgid "Last Updated by" +msgstr "" + +#. module: chained_swapper +#: model:ir.model.fields,field_description:chained_swapper.field_chained_swapper__write_date +#: model:ir.model.fields,field_description:chained_swapper.field_chained_swapper_constraint__write_date +#: model:ir.model.fields,field_description:chained_swapper.field_chained_swapper_sub_field__write_date +#: model:ir.model.fields,field_description:chained_swapper.field_chained_swapper_wizard__write_date +msgid "Last Updated on" +msgstr "" + +#. module: chained_swapper +#: model:ir.model.fields,field_description:chained_swapper.field_chained_swapper__model_id +msgid "Model" +msgstr "" + +#. module: chained_swapper +#: model:ir.model.fields,field_description:chained_swapper.field_chained_swapper_sub_field__model_name +msgid "Model Name" +msgstr "" + +#. module: chained_swapper +#: model:ir.model.constraint,message:chained_swapper.constraint_chained_swapper_model_id_field_id_unique +msgid "Model and Field must be unique!" +msgstr "" + +#. module: chained_swapper +#: model:ir.model.fields,help:chained_swapper.field_chained_swapper__model_id +msgid "" +"Model is used for Selecting Field. This is editable until Contextual Action " +"is not created." +msgstr "" + +#. module: chained_swapper +#: model:ir.model.fields,field_description:chained_swapper.field_chained_swapper__name +#: model:ir.model.fields,field_description:chained_swapper.field_chained_swapper_constraint__name +msgid "Name" +msgstr "" + +#. module: chained_swapper +#. odoo-python +#: code:addons/chained_swapper/wizard/chained_swapper_wizard.py:0 +msgid "Not possible to swap the field due to the constraint" +msgstr "" + +#. module: chained_swapper +#: model:chained.swapper.constraint,name:chained_swapper.chained_swapper_constraint_demo +msgid "Only parent company" +msgstr "" + +#. module: chained_swapper +#: model_terms:ir.ui.view,arch_db:chained_swapper.chained_swapper_view_form +msgid "Other Variables You Can Use" +msgstr "" + +#. module: chained_swapper +#: model_terms:ir.ui.view,arch_db:chained_swapper.chained_swapper_view_form +msgid "Remove Action" +msgstr "" + +#. module: chained_swapper +#: model_terms:ir.ui.view,arch_db:chained_swapper.chained_swapper_view_form +msgid "Remove the contextual action to use this template on related documents" +msgstr "" + +#. module: chained_swapper +#: model_terms:ir.ui.view,arch_db:chained_swapper.chained_swapper_view_form +msgid "Security" +msgstr "" + +#. module: chained_swapper +#: model:ir.model.fields,field_description:chained_swapper.field_chained_swapper_sub_field__sub_field_chain +msgid "Sub Field Chain" +msgstr "" + +#. module: chained_swapper +#: model:ir.model.fields,field_description:chained_swapper.field_chained_swapper__sub_field_ids +#: model_terms:ir.ui.view,arch_db:chained_swapper.chained_swapper_view_form +msgid "Sub-fields" +msgstr "" + +#. module: chained_swapper +#. odoo-python +#: code:addons/chained_swapper/models/chained_swapper.py:0 +msgid "The sub-field '%s' is not compatible with the main field." +msgstr "" + +#. module: chained_swapper +#: model:ir.model,name:chained_swapper.model_chained_swapper_wizard +msgid "Wizard chained swapper" +msgstr "" + +#. module: chained_swapper +#: model_terms:ir.ui.view,arch_db:chained_swapper.chained_swapper_view_form +msgid "" +"You can add possible constraints to restrict the chained swap. These constraints are Python expressions that must be evaluated as a boolean value.\n" +" If the evaluation returns True, a message will be thrown and no swap will be allowed." +msgstr "" + +#. module: chained_swapper +#: model:ir.model.fields,help:chained_swapper.field_chained_swapper_sub_field__sub_field_chain +msgid "" +"You can specify here a field of related fields as dotted names. Ex.: " +"'child_ids.lang'." +msgstr "" + +#. module: chained_swapper +#: model_terms:ir.ui.view,arch_db:chained_swapper.chained_swapper_view_form +msgid "" +"You can use the variable records in your expression. This will " +"refer to the selected records for performing the swap. For example:" +msgstr "" + +#. module: chained_swapper +#: model_terms:ir.ui.view,arch_db:chained_swapper.chained_swapper_view_form +msgid "any(p.state == 'done' for p in records.picking_ids)" +msgstr "" diff --git a/chained_swapper/i18n/es.po b/chained_swapper/i18n/es.po new file mode 100644 index 0000000000..245385b64e --- /dev/null +++ b/chained_swapper/i18n/es.po @@ -0,0 +1,301 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * chained_swapper +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 15.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2022-06-28 21:43+0000\n" +"PO-Revision-Date: 2022-06-28 21:43+0000\n" +"Last-Translator: \n" +"Language-Team: \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: chained_swapper +#: code:addons/chained_swapper/wizard/chained_swapper_wizard.py:0 +#, python-format +msgid "Chained swap done:" +msgstr "Cambio encadenado realizado:" + +#. module: chained_swapper +#: model:ir.model.fields,field_description:chained_swapper.field_chained_swapper__ref_ir_act_window_id +msgid "Action" +msgstr "Acción" + +#. module: chained_swapper +#: model:ir.model.fields,help:chained_swapper.field_chained_swapper__ref_ir_act_window_id +msgid "" +"Action to make this template available on records of the related document " +"model." +msgstr "" +"Acción contextual para hacer que esta plantilla esté disponible en los " +"registros del modelo de documento relacionado." + +#. module: chained_swapper +#: model_terms:ir.ui.view,arch_db:chained_swapper.chained_swapper_view_form +msgid "Add Action" +msgstr "Añadir acción" + +#. module: chained_swapper +#: model_terms:ir.ui.view,arch_db:chained_swapper.chained_swapper_view_form +msgid "" +"Add a new contextual action of related documents to open a composition wizard" +msgstr "" +"Añade una acción contextual para los documentos relacionados para abrir un " +"asistente de intercambio encadenado" + +#. module: chained_swapper +#: model_terms:ir.ui.view,arch_db:chained_swapper.chained_swapper_view_form +msgid "Advanced" +msgstr "Avanzado" + +#. module: chained_swapper +#: model:ir.model.fields,field_description:chained_swapper.field_chained_swapper__allowed_field_ids +msgid "Allowed Field" +msgstr "Campos permitidos" + +#. module: chained_swapper +#: model:ir.model.fields,help:chained_swapper.field_chained_swapper_constraint__expression +msgid "" +"Boolean python expression. You can use the keyword 'records' as the records " +"selected to execute the contextual action. Ex.: bool(records." +"mapped('parent_id'))" +msgstr "" +"Expresión Python booleana. Puede usar la palabra clave 'records' para hacer " +"referencia a los registros seleccionados al ejecutar la acción contextual. " +"Ej.: bool(records.mapped('parent_id'))" + +#. module: chained_swapper +#: model:ir.model,name:chained_swapper.model_chained_swapper +#: model:ir.model.fields,field_description:chained_swapper.field_chained_swapper_constraint__chained_swapper_id +#: model:ir.model.fields,field_description:chained_swapper.field_chained_swapper_sub_field__chained_swapper_id +msgid "Chained Swapper" +msgstr "Intercambiador encadenado" + +#. module: chained_swapper +#: model:ir.model,name:chained_swapper.model_chained_swapper_constraint +msgid "Chained Swapper Constraint" +msgstr "Intercambiador encadenado - Restricciones" + +#. module: chained_swapper +#: model:ir.model,name:chained_swapper.model_chained_swapper_sub_field +msgid "Chained Swapper Sub-field" +msgstr "Intercambiador encadenado - Sub-campos" + +#. module: chained_swapper +#: code:addons/chained_swapper/models/chained_swapper.py:0 +#, python-format +msgid "Chained swap" +msgstr "Cambio encadenado" + +#. module: chained_swapper +#: model:ir.actions.act_window,name:chained_swapper.partner_chained_swap_lang_action_demo +msgid "Chained swap: Language" +msgstr "Cambio encadenado: Idioma" + +#. module: chained_swapper +#: model_terms:ir.ui.view,arch_db:chained_swapper.chained_swapper_wizard_view_form +msgid "Change" +msgstr "Cambiar" + +#. module: chained_swapper +#: model_terms:ir.ui.view,arch_db:chained_swapper.chained_swapper_wizard_view_form +msgid "Close" +msgstr "Cerrar" + +#. module: chained_swapper +#: model:ir.model.fields,field_description:chained_swapper.field_chained_swapper_constraint__expression +#: model_terms:ir.ui.view,arch_db:chained_swapper.chained_swapper_constraint_view_form +msgid "Constraint expression" +msgstr "Expresión de restricción" + +#. module: chained_swapper +#: model:ir.model.fields,field_description:chained_swapper.field_chained_swapper__constraint_ids +#: model_terms:ir.ui.view,arch_db:chained_swapper.chained_swapper_view_form +msgid "Constraints" +msgstr "Restricciones" + +#. module: chained_swapper +#: model:ir.model.fields,field_description:chained_swapper.field_chained_swapper__create_uid +#: model:ir.model.fields,field_description:chained_swapper.field_chained_swapper_constraint__create_uid +#: model:ir.model.fields,field_description:chained_swapper.field_chained_swapper_sub_field__create_uid +#: model:ir.model.fields,field_description:chained_swapper.field_chained_swapper_wizard__create_uid +msgid "Created by" +msgstr "Creado por" + +#. module: chained_swapper +#: model:ir.model.fields,field_description:chained_swapper.field_chained_swapper__create_date +#: model:ir.model.fields,field_description:chained_swapper.field_chained_swapper_constraint__create_date +#: model:ir.model.fields,field_description:chained_swapper.field_chained_swapper_sub_field__create_date +#: model:ir.model.fields,field_description:chained_swapper.field_chained_swapper_wizard__create_date +msgid "Created on" +msgstr "Creado el" + +#. module: chained_swapper +#: model:ir.model.fields,field_description:chained_swapper.field_chained_swapper__display_name +#: model:ir.model.fields,field_description:chained_swapper.field_chained_swapper_constraint__display_name +#: model:ir.model.fields,field_description:chained_swapper.field_chained_swapper_sub_field__display_name +#: model:ir.model.fields,field_description:chained_swapper.field_chained_swapper_wizard__display_name +msgid "Display Name" +msgstr "Nombre mostrado" + +#. module: chained_swapper +#: model:ir.model.fields,field_description:chained_swapper.field_chained_swapper__field_id +msgid "Field" +msgstr "Campo" + +#. module: chained_swapper +#: model:ir.actions.act_window,name:chained_swapper.chained_swapper_action +#: model:ir.ui.menu,name:chained_swapper.chained_swapper_menu +#: model:ir.ui.menu,name:chained_swapper.chained_swapper_submenu +msgid "Field Swaps" +msgstr "Intercambios de campo" + +#. module: chained_swapper +#: model:ir.model.fields,field_description:chained_swapper.field_chained_swapper__group_ids +msgid "Groups" +msgstr "Grupos" + +#. module: chained_swapper +#: model:ir.model.fields,field_description:chained_swapper.field_chained_swapper__id +#: model:ir.model.fields,field_description:chained_swapper.field_chained_swapper_constraint__id +#: model:ir.model.fields,field_description:chained_swapper.field_chained_swapper_sub_field__id +#: model:ir.model.fields,field_description:chained_swapper.field_chained_swapper_wizard__id +msgid "ID" +msgstr "ID (Identificador)" + +#. module: chained_swapper +#: code:addons/chained_swapper/models/chained_swapper.py:0 +#, python-format +msgid "Incorrect sub-field expression: %(sub_field_chain)s. %(error)s" +msgstr "Expresión incorrecta: %(sub_field_chain)s. %(error)s" + +#. module: chained_swapper +#: code:addons/chained_swapper/models/chained_swapper.py:0 +#, python-format +msgid "Invalid constraint expression: %(expression)s. %(error)s." +msgstr "Expresión incorrecta: %(expression)s. %(error)s." + +#. module: chained_swapper +#: code:addons/chained_swapper/models/chained_swapper.py:0 +#, python-format +msgid "Invalid value for %(sub_field_chain)s. %(error)s" +msgstr "Valor incorrecto para %(sub_field_chain)s. %(error)s" + +#. module: chained_swapper +#: model:chained.swapper,name:chained_swapper.chained_swapper_demo +msgid "Language" +msgstr "Idioma" + +#. module: chained_swapper +#: model:ir.model.fields,field_description:chained_swapper.field_chained_swapper____last_update +#: model:ir.model.fields,field_description:chained_swapper.field_chained_swapper_constraint____last_update +#: model:ir.model.fields,field_description:chained_swapper.field_chained_swapper_sub_field____last_update +#: model:ir.model.fields,field_description:chained_swapper.field_chained_swapper_wizard____last_update +msgid "Last Modified on" +msgstr "Última modificación en" + +#. module: chained_swapper +#: model:ir.model.fields,field_description:chained_swapper.field_chained_swapper__write_uid +#: model:ir.model.fields,field_description:chained_swapper.field_chained_swapper_constraint__write_uid +#: model:ir.model.fields,field_description:chained_swapper.field_chained_swapper_sub_field__write_uid +#: model:ir.model.fields,field_description:chained_swapper.field_chained_swapper_wizard__write_uid +msgid "Last Updated by" +msgstr "Última actualización por" + +#. module: chained_swapper +#: model:ir.model.fields,field_description:chained_swapper.field_chained_swapper__write_date +#: model:ir.model.fields,field_description:chained_swapper.field_chained_swapper_constraint__write_date +#: model:ir.model.fields,field_description:chained_swapper.field_chained_swapper_sub_field__write_date +#: model:ir.model.fields,field_description:chained_swapper.field_chained_swapper_wizard__write_date +msgid "Last Updated on" +msgstr "Última actualización el" + +#. module: chained_swapper +#: model:ir.model.fields,field_description:chained_swapper.field_chained_swapper__model_id +msgid "Model" +msgstr "Modelo" + +#. module: chained_swapper +#: model:ir.model.constraint,message:chained_swapper.constraint_chained_swapper_model_id_field_id_unique +msgid "Model and Field must be unique!" +msgstr "La combinación Modelo-Campo debe ser única!" + +#. module: chained_swapper +#: model:ir.model.fields,help:chained_swapper.field_chained_swapper__model_id +msgid "" +"Model is used for Selecting Field. This is editable until Contextual Action " +"is not created." +msgstr "" +"El modelo se utiliza para seleccionar el campo. Este es editable mientras la " +"acción contextual no esté creada." + +#. module: chained_swapper +#: model:ir.model.fields,field_description:chained_swapper.field_chained_swapper__name +#: model:ir.model.fields,field_description:chained_swapper.field_chained_swapper_constraint__name +msgid "Name" +msgstr "Nombre" + +#. module: chained_swapper +#: code:addons/chained_swapper/wizard/chained_swapper_wizard.py:0 +#, python-format +msgid "Not possible to swap the field due to the constraint" +msgstr "No es posible cambiar el campo debido a la restricción" + +#. module: chained_swapper +#: model:chained.swapper.constraint,name:chained_swapper.chained_swapper_constraint_demo +msgid "Only parent company" +msgstr "Solo compañías padres" + +#. module: chained_swapper +#: model_terms:ir.ui.view,arch_db:chained_swapper.chained_swapper_view_form +msgid "Remove Action" +msgstr "Eliminar acción" + +#. module: chained_swapper +#: model_terms:ir.ui.view,arch_db:chained_swapper.chained_swapper_view_form +msgid "Remove the contextual action to use this template on related documents" +msgstr "" +"Borrar la acción conceptual para usar esta plantilla en documentos " +"relacionados" + +#. module: chained_swapper +#: model_terms:ir.ui.view,arch_db:chained_swapper.chained_swapper_view_form +msgid "Security" +msgstr "Seguridad" + +#. module: chained_swapper +#: model:ir.model.fields,field_description:chained_swapper.field_chained_swapper_sub_field__sub_field_chain +msgid "Sub Field Chain" +msgstr "Cadena de sub-campo" + +#. module: chained_swapper +#: model:ir.model.fields,field_description:chained_swapper.field_chained_swapper__sub_field_ids +#: model_terms:ir.ui.view,arch_db:chained_swapper.chained_swapper_view_form +msgid "Sub-fields" +msgstr "Sub-campo" + +#. module: chained_swapper +#: code:addons/chained_swapper/models/chained_swapper.py:0 +#, python-format +msgid "The sub-field '%s' is not compatible with the main field." +msgstr "El sub-campo '%s' no es compatible con el campo principal." + +#. module: chained_swapper +#: model:ir.model,name:chained_swapper.model_chained_swapper_wizard +msgid "Wizard chained swapper" +msgstr "Asistente de intercambiador encadenado" + +#. module: chained_swapper +#: model:ir.model.fields,help:chained_swapper.field_chained_swapper_sub_field__sub_field_chain +msgid "" +"You can specify here a field of related fields as dotted names. Ex.: " +"'child_ids.lang'." +msgstr "" +"Puede especificar aquí un campo de campos relacionados como una cadena de " +"nombres separada por puntos. Ej.: 'child_ids.lang'." diff --git a/chained_swapper/i18n/it.po b/chained_swapper/i18n/it.po new file mode 100644 index 0000000000..28358f52fe --- /dev/null +++ b/chained_swapper/i18n/it.po @@ -0,0 +1,302 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * chained_swapper +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 15.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2022-06-28 21:43+0000\n" +"PO-Revision-Date: 2026-01-11 16:43+0000\n" +"Last-Translator: mymage \n" +"Language-Team: \n" +"Language: it\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 5.10.4\n" + +#. module: chained_swapper +#: code:addons/chained_swapper/wizard/chained_swapper_wizard.py:0 +#, python-format +msgid "Chained swap done:" +msgstr "Sostituzione a catena effettuata:" + +#. module: chained_swapper +#: model:ir.model.fields,field_description:chained_swapper.field_chained_swapper__ref_ir_act_window_id +msgid "Action" +msgstr "Azione" + +#. module: chained_swapper +#: model:ir.model.fields,help:chained_swapper.field_chained_swapper__ref_ir_act_window_id +msgid "" +"Action to make this template available on records of the related document " +"model." +msgstr "" +"Azione per rendere questo template disponibile sui record del modello del " +"documento relazionato." + +#. module: chained_swapper +#: model_terms:ir.ui.view,arch_db:chained_swapper.chained_swapper_view_form +msgid "Add Action" +msgstr "Aggiungi azione" + +#. module: chained_swapper +#: model_terms:ir.ui.view,arch_db:chained_swapper.chained_swapper_view_form +msgid "" +"Add a new contextual action of related documents to open a composition wizard" +msgstr "" +"Aggiungi una nuova azione contestuale per i documenti relazionati per aprire " +"una wizard di sostituzione a catena" + +#. module: chained_swapper +#: model_terms:ir.ui.view,arch_db:chained_swapper.chained_swapper_view_form +msgid "Advanced" +msgstr "Avanzato" + +#. module: chained_swapper +#: model:ir.model.fields,field_description:chained_swapper.field_chained_swapper__allowed_field_ids +msgid "Allowed Field" +msgstr "Campi permessi" + +#. module: chained_swapper +#: model:ir.model.fields,help:chained_swapper.field_chained_swapper_constraint__expression +msgid "" +"Boolean python expression. You can use the keyword 'records' as the records " +"selected to execute the contextual action. Ex.: bool(records." +"mapped('parent_id'))" +msgstr "" +"Espressione Python booleana. Puoi utilizzare la parola chiave 'records' per " +"riferirti ai record selezionati per eseguire l'azione contestuale. Es: " +"bool(records.mapped('parent_id'))" + +#. module: chained_swapper +#: model:ir.model,name:chained_swapper.model_chained_swapper +#: model:ir.model.fields,field_description:chained_swapper.field_chained_swapper_constraint__chained_swapper_id +#: model:ir.model.fields,field_description:chained_swapper.field_chained_swapper_sub_field__chained_swapper_id +msgid "Chained Swapper" +msgstr "Sostitutore a catena" + +#. module: chained_swapper +#: model:ir.model,name:chained_swapper.model_chained_swapper_constraint +msgid "Chained Swapper Constraint" +msgstr "Restrizione Sostitutore a catena" + +#. module: chained_swapper +#: model:ir.model,name:chained_swapper.model_chained_swapper_sub_field +msgid "Chained Swapper Sub-field" +msgstr "Sottocampo Sostitutore a catena" + +#. module: chained_swapper +#: code:addons/chained_swapper/models/chained_swapper.py:0 +#, python-format +msgid "Chained swap" +msgstr "Sostituzione a catena" + +#. module: chained_swapper +#: model:ir.actions.act_window,name:chained_swapper.partner_chained_swap_lang_action_demo +msgid "Chained swap: Language" +msgstr "Sostituzione a catena: Lingua" + +#. module: chained_swapper +#: model_terms:ir.ui.view,arch_db:chained_swapper.chained_swapper_wizard_view_form +msgid "Change" +msgstr "Modifica" + +#. module: chained_swapper +#: model_terms:ir.ui.view,arch_db:chained_swapper.chained_swapper_wizard_view_form +msgid "Close" +msgstr "Chiudi" + +#. module: chained_swapper +#: model:ir.model.fields,field_description:chained_swapper.field_chained_swapper_constraint__expression +#: model_terms:ir.ui.view,arch_db:chained_swapper.chained_swapper_constraint_view_form +msgid "Constraint expression" +msgstr "Espressione di restrizione" + +#. module: chained_swapper +#: model:ir.model.fields,field_description:chained_swapper.field_chained_swapper__constraint_ids +#: model_terms:ir.ui.view,arch_db:chained_swapper.chained_swapper_view_form +msgid "Constraints" +msgstr "Restrizioni" + +#. module: chained_swapper +#: model:ir.model.fields,field_description:chained_swapper.field_chained_swapper__create_uid +#: model:ir.model.fields,field_description:chained_swapper.field_chained_swapper_constraint__create_uid +#: model:ir.model.fields,field_description:chained_swapper.field_chained_swapper_sub_field__create_uid +#: model:ir.model.fields,field_description:chained_swapper.field_chained_swapper_wizard__create_uid +msgid "Created by" +msgstr "Creato da" + +#. module: chained_swapper +#: model:ir.model.fields,field_description:chained_swapper.field_chained_swapper__create_date +#: model:ir.model.fields,field_description:chained_swapper.field_chained_swapper_constraint__create_date +#: model:ir.model.fields,field_description:chained_swapper.field_chained_swapper_sub_field__create_date +#: model:ir.model.fields,field_description:chained_swapper.field_chained_swapper_wizard__create_date +msgid "Created on" +msgstr "Creato il" + +#. module: chained_swapper +#: model:ir.model.fields,field_description:chained_swapper.field_chained_swapper__display_name +#: model:ir.model.fields,field_description:chained_swapper.field_chained_swapper_constraint__display_name +#: model:ir.model.fields,field_description:chained_swapper.field_chained_swapper_sub_field__display_name +#: model:ir.model.fields,field_description:chained_swapper.field_chained_swapper_wizard__display_name +msgid "Display Name" +msgstr "Nome visualizzato" + +#. module: chained_swapper +#: model:ir.model.fields,field_description:chained_swapper.field_chained_swapper__field_id +msgid "Field" +msgstr "Campo" + +#. module: chained_swapper +#: model:ir.actions.act_window,name:chained_swapper.chained_swapper_action +#: model:ir.ui.menu,name:chained_swapper.chained_swapper_menu +#: model:ir.ui.menu,name:chained_swapper.chained_swapper_submenu +msgid "Field Swaps" +msgstr "Sostituzioni campi" + +#. module: chained_swapper +#: model:ir.model.fields,field_description:chained_swapper.field_chained_swapper__group_ids +msgid "Groups" +msgstr "Gruppi" + +#. module: chained_swapper +#: model:ir.model.fields,field_description:chained_swapper.field_chained_swapper__id +#: model:ir.model.fields,field_description:chained_swapper.field_chained_swapper_constraint__id +#: model:ir.model.fields,field_description:chained_swapper.field_chained_swapper_sub_field__id +#: model:ir.model.fields,field_description:chained_swapper.field_chained_swapper_wizard__id +msgid "ID" +msgstr "ID" + +#. module: chained_swapper +#: code:addons/chained_swapper/models/chained_swapper.py:0 +#, python-format +msgid "Incorrect sub-field expression: %(sub_field_chain)s. %(error)s" +msgstr "Espressione sotto-campo errata: %(sub_field_chain)s. %(error)s" + +#. module: chained_swapper +#: code:addons/chained_swapper/models/chained_swapper.py:0 +#, python-format +msgid "Invalid constraint expression: %(expression)s. %(error)s." +msgstr "Espressione vincolo errata: %(expression)s. %(error)s." + +#. module: chained_swapper +#: code:addons/chained_swapper/models/chained_swapper.py:0 +#, python-format +msgid "Invalid value for %(sub_field_chain)s. %(error)s" +msgstr "Valore non valido per %(sub_field_chain)s. %(error)s" + +#. module: chained_swapper +#: model:chained.swapper,name:chained_swapper.chained_swapper_demo +msgid "Language" +msgstr "Lingua" + +#. module: chained_swapper +#: model:ir.model.fields,field_description:chained_swapper.field_chained_swapper____last_update +#: model:ir.model.fields,field_description:chained_swapper.field_chained_swapper_constraint____last_update +#: model:ir.model.fields,field_description:chained_swapper.field_chained_swapper_sub_field____last_update +#: model:ir.model.fields,field_description:chained_swapper.field_chained_swapper_wizard____last_update +msgid "Last Modified on" +msgstr "Ultima modifica il" + +#. module: chained_swapper +#: model:ir.model.fields,field_description:chained_swapper.field_chained_swapper__write_uid +#: model:ir.model.fields,field_description:chained_swapper.field_chained_swapper_constraint__write_uid +#: model:ir.model.fields,field_description:chained_swapper.field_chained_swapper_sub_field__write_uid +#: model:ir.model.fields,field_description:chained_swapper.field_chained_swapper_wizard__write_uid +msgid "Last Updated by" +msgstr "Ultimo aggiornamento di" + +#. module: chained_swapper +#: model:ir.model.fields,field_description:chained_swapper.field_chained_swapper__write_date +#: model:ir.model.fields,field_description:chained_swapper.field_chained_swapper_constraint__write_date +#: model:ir.model.fields,field_description:chained_swapper.field_chained_swapper_sub_field__write_date +#: model:ir.model.fields,field_description:chained_swapper.field_chained_swapper_wizard__write_date +msgid "Last Updated on" +msgstr "Ultimo aggiornamento il" + +#. module: chained_swapper +#: model:ir.model.fields,field_description:chained_swapper.field_chained_swapper__model_id +msgid "Model" +msgstr "Modello" + +#. module: chained_swapper +#: model:ir.model.constraint,message:chained_swapper.constraint_chained_swapper_model_id_field_id_unique +msgid "Model and Field must be unique!" +msgstr "Campo e Modello devono essere univoci!" + +#. module: chained_swapper +#: model:ir.model.fields,help:chained_swapper.field_chained_swapper__model_id +msgid "" +"Model is used for Selecting Field. This is editable until Contextual Action " +"is not created." +msgstr "" +"Il modello è utilizzato per selezionare il campo. Questo è modificabile solo " +"fino alla creazione di un'azione contestuale." + +#. module: chained_swapper +#: model:ir.model.fields,field_description:chained_swapper.field_chained_swapper__name +#: model:ir.model.fields,field_description:chained_swapper.field_chained_swapper_constraint__name +msgid "Name" +msgstr "Nome" + +#. module: chained_swapper +#: code:addons/chained_swapper/wizard/chained_swapper_wizard.py:0 +#, python-format +msgid "Not possible to swap the field due to the constraint" +msgstr "Non è possibile sostituire il campo a causa della restrizione" + +#. module: chained_swapper +#: model:chained.swapper.constraint,name:chained_swapper.chained_swapper_constraint_demo +msgid "Only parent company" +msgstr "Solo azienda padre" + +#. module: chained_swapper +#: model_terms:ir.ui.view,arch_db:chained_swapper.chained_swapper_view_form +msgid "Remove Action" +msgstr "Rimuovi azione" + +#. module: chained_swapper +#: model_terms:ir.ui.view,arch_db:chained_swapper.chained_swapper_view_form +msgid "Remove the contextual action to use this template on related documents" +msgstr "" +"Rimuove l'azione contestuale per usare questo template in documenti " +"relazionati" + +#. module: chained_swapper +#: model_terms:ir.ui.view,arch_db:chained_swapper.chained_swapper_view_form +msgid "Security" +msgstr "Sicurezza" + +#. module: chained_swapper +#: model:ir.model.fields,field_description:chained_swapper.field_chained_swapper_sub_field__sub_field_chain +msgid "Sub Field Chain" +msgstr "Catena del sottocampo" + +#. module: chained_swapper +#: model:ir.model.fields,field_description:chained_swapper.field_chained_swapper__sub_field_ids +#: model_terms:ir.ui.view,arch_db:chained_swapper.chained_swapper_view_form +msgid "Sub-fields" +msgstr "Sottocampi" + +#. module: chained_swapper +#: code:addons/chained_swapper/models/chained_swapper.py:0 +#, python-format +msgid "The sub-field '%s' is not compatible with the main field." +msgstr "Il sottocampo '%s' non è compatibile con il campo principale." + +#. module: chained_swapper +#: model:ir.model,name:chained_swapper.model_chained_swapper_wizard +msgid "Wizard chained swapper" +msgstr "Wizard sostitutore a catena" + +#. module: chained_swapper +#: model:ir.model.fields,help:chained_swapper.field_chained_swapper_sub_field__sub_field_chain +msgid "" +"You can specify here a field of related fields as dotted names. Ex.: " +"'child_ids.lang'." +msgstr "" +"Puoi specificare qui un campo di campi relazionati usando un nome separato " +"da punti. Es: 'child_ids.lang'." diff --git a/chained_swapper/models/__init__.py b/chained_swapper/models/__init__.py new file mode 100644 index 0000000000..10e942d94e --- /dev/null +++ b/chained_swapper/models/__init__.py @@ -0,0 +1,3 @@ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from . import chained_swapper diff --git a/chained_swapper/models/chained_swapper.py b/chained_swapper/models/chained_swapper.py new file mode 100644 index 0000000000..330dad5936 --- /dev/null +++ b/chained_swapper/models/chained_swapper.py @@ -0,0 +1,214 @@ +# Copyright 2020 Tecnativa - Ernesto Tejeda +# Copyright 2020 Tecnativa - Pedro M. Baeza +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import api, exceptions, fields, models +from odoo.exceptions import ValidationError +from odoo.tools.safe_eval import safe_eval + + +class ChainedSwapper(models.Model): + _name = "chained.swapper" + _description = "Chained Swapper" + + name = fields.Char(required=True, translate=True, index="trigram") + model_id = fields.Many2one( + comodel_name="ir.model", + required=True, + ondelete="cascade", + help="Model is used for Selecting Field. This is editable " + "until Contextual Action is not created.", + ) + allowed_field_ids_domain = fields.Binary( + compute="_compute_allowed_field_ids_domain" + ) + field_id = fields.Many2one( + comodel_name="ir.model.fields", + required=True, + ondelete="cascade", + domain="allowed_field_ids_domain", + ) + sub_field_ids = fields.One2many( + comodel_name="chained.swapper.sub.field", + inverse_name="chained_swapper_id", + string="Sub-fields", + ) + constraint_ids = fields.One2many( + comodel_name="chained.swapper.constraint", + inverse_name="chained_swapper_id", + string="Constraints", + ) + ref_ir_act_window_id = fields.Many2one( + comodel_name="ir.actions.act_window", + string="Action", + readonly=True, + help="Action to make this template available on records " + "of the related document model.", + ) + group_ids = fields.Many2many( + comodel_name="res.groups", + relation="mass_group_rel", + column1="mass_id", + column2="group_id", + string="Groups", + ) + + _model_id_field_id_unique = models.Constraint( + "unique (model_id, field_id)", + "Model and Field must be unique!", + ) + + @api.depends("model_id") + def _compute_allowed_field_ids_domain(self): + for record in self: + domain = [] + if record.model_id: + all_models = record.model_id + active_model_obj = self.env[record.model_id.model] + if active_model_obj._inherits: + inherited_models = list(active_model_obj._inherits.keys()) + all_models |= self.env["ir.model"].search( + [("model", "in", inherited_models)] + ) + domain = [ + ("ttype", "not in", ["reference", "function", "one2many"]), + ("model_id", "in", all_models.ids), + ] + record.allowed_field_ids_domain = domain + + @api.constrains("model_id", "field_id") + def _check_sub_field_ids(self): + self.mapped("sub_field_ids")._check_sub_field_chain() + + @api.onchange("model_id") + def _onchange_model_id(self): + self.field_id = False + + def write(self, vals): + res = super().write(vals) + if "name" in vals: + self.mapped("ref_ir_act_window_id").write({"name": vals["name"]}) + return res + + def unlink(self): + self.unlink_action() + return super().unlink() + + def add_action(self): + self.ensure_one() + action = self.env["ir.actions.act_window"].create( + { + "name": self.env._("Chained swap") + ": " + self.name, + "type": "ir.actions.act_window", + "res_model": "chained.swapper.wizard", + "group_ids": [(4, x.id) for x in self.group_ids], + "context": {"chained_swapper_id": self.id}, + "view_mode": "form", + "target": "new", + "binding_model_id": self.model_id.id, + "binding_type": "action", + } + ) + self.write({"ref_ir_act_window_id": action.id}) + return True + + def unlink_action(self): + self.mapped("ref_ir_act_window_id").unlink() + return True + + +class ChainedSwapperSubField(models.Model): + _name = "chained.swapper.sub.field" + _description = "Chained Swapper Sub-field" + + chained_swapper_id = fields.Many2one( + comodel_name="chained.swapper", ondelete="cascade" + ) + model_name = fields.Char( + related="chained_swapper_id.model_id.model", string="Model Name" + ) + sub_field_chain = fields.Char( + required=True, + help="You can specify here a field of related fields as " + "dotted names. Ex.: 'child_ids.lang'.", + ) + + @api.constrains("chained_swapper_id", "sub_field_chain") + def _check_sub_field_chain(self): + for rec in self: + # Check sub-field exist + try: + chain_list = rec.sub_field_chain.split(".") + chain_field_name = chain_list.pop() + chain_model = self.env[rec.chained_swapper_id.model_id.model] + for name in chain_list: + chain_model = chain_model[name] + chain_model[chain_field_name] # pylint: disable=W0104 + except KeyError as err: + raise exceptions.ValidationError( + self.env._( + "Incorrect sub-field expression:" + " %(sub_field_chain)s. %(error)s", + sub_field_chain=rec.sub_field_chain, + error=err, + ) + ) from err + except Exception as err: + raise ValidationError( + self.env._( + "Invalid value for %(sub_field_chain)s. %(error)s", + sub_field_chain=rec.sub_field_chain, + error=err, + ) + ) from err + # Check sub-field and original field are the same type + swap_field = rec.chained_swapper_id.field_id + chain_field = self.env["ir.model.fields"].search( + [ + ("model_id.model", "=", chain_model._name), + ("name", "=", chain_field_name), + ] + ) + if ( + chain_field.ttype != swap_field.ttype + or chain_field.relation != swap_field.relation + ): + raise exceptions.ValidationError( + self.env._( + "The sub-field '%s' is not compatible with the main field.", + rec.sub_field_chain, + ) + ) + + +class ChainedSwapperConstraint(models.Model): + _name = "chained.swapper.constraint" + _description = "Chained Swapper Constraint" + + chained_swapper_id = fields.Many2one( + comodel_name="chained.swapper", ondelete="cascade" + ) + name = fields.Char(required=True, translate=True) + expression = fields.Text( + string="Constraint expression", + required=True, + help="Boolean python expression. You can use the keyword " + "'records' as the records selected to execute the " + "contextual action. Ex.: bool(records.mapped('parent_id'))", + default="True", + ) + + @api.constrains("expression") + def _check_expression(self): + for record in self: + model = self.env[record.chained_swapper_id.model_id.model] + try: + safe_eval(record.expression, {"records": model}) + except Exception as err: + raise exceptions.ValidationError( + self.env._( + "Invalid constraint expression: %(expression)s. %(error)s.", + expression=record.expression, + error=err, + ) + ) from err diff --git a/chained_swapper/pyproject.toml b/chained_swapper/pyproject.toml new file mode 100644 index 0000000000..4231d0cccb --- /dev/null +++ b/chained_swapper/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/chained_swapper/readme/CONFIGURE.md b/chained_swapper/readme/CONFIGURE.md new file mode 100644 index 0000000000..b390fdf81d --- /dev/null +++ b/chained_swapper/readme/CONFIGURE.md @@ -0,0 +1,35 @@ +To configure this module, you need to: + +1. Go to *Setting \> Field Swaps \> Field Swaps*. +2. Create a new object and set the following data as an example: + - Name for identifying it and use it for the action name. + + - Select the source model where the swap will be started. + + - Select the starting field for which the swap will be done. + + - Add several chained fields. They are expressed as a string using + dot notation, taking the source model as beginning for looking + there the first field, and continuing from there drilling through. + Example: picking_ids.partner_id for sale.order model will go to + the linked deliveries orders, and change the customer there. + + - Add possible constraints for restricting the chained swap. They + are Python expressions that must be one line that is evaluated as + boolean. If the evaluation is true, then a message will be thrown + and no swap will be allowed. You can use the variable records in + your code, that will be referring the selected records for doing + the swap. Example: for restricting sales orders that have a + delivery order validated: + + any(p.state == 'done' for p in + records.mapped('picking_ids.state')) + + Other variables you can use are env, date and datetime. Each + constraint has a name for identifying it, but also for showing + that name when displaying the error trying to do the swap. +3. Click on 'Add action' smart button to add a new action in the source + model. + +On demo databases, you can check the example "Language", that changes +the language of a contact, and propagate it to children contacts. diff --git a/chained_swapper/readme/CONTRIBUTORS.md b/chained_swapper/readme/CONTRIBUTORS.md new file mode 100644 index 0000000000..05b2f20122 --- /dev/null +++ b/chained_swapper/readme/CONTRIBUTORS.md @@ -0,0 +1,7 @@ +- [Tecnativa](https://www.tecnativa.com): + - Ernesto Tejeda + - Pedro M. Baeza + - César A. Sánchez + - Juan Carlos Oñate + - Carlos Lopez +- Sunanda Chhatbar \<\> diff --git a/chained_swapper/readme/DESCRIPTION.md b/chained_swapper/readme/DESCRIPTION.md new file mode 100644 index 0000000000..10bc0dc244 --- /dev/null +++ b/chained_swapper/readme/DESCRIPTION.md @@ -0,0 +1,15 @@ +This module allows to swap the value of a field and propagate it in a +chained way to linked records. Example: changing the delivery address in +a confirmed sales order, it should be changed in its delivery orders as +well. + +It also allows to apply constraints for not allowing to do that change +according rules, so the business logic is not broken. Example: Don't +allow to change the delivery address if the delivery order is validated. + +This module requires some technical knowledge for setting the chained +swap and the constraint, as it's defined through technical names and +Python code. + +**WARNING**: Use this module with care, as it can screw up database +consistency if swaps are not properly designed . diff --git a/chained_swapper/readme/USAGE.md b/chained_swapper/readme/USAGE.md new file mode 100644 index 0000000000..10b6864500 --- /dev/null +++ b/chained_swapper/readme/USAGE.md @@ -0,0 +1,13 @@ +To use this module, you need to: + +1. Go to the source document in list mode. +2. Select one or several records. +3. Click on Action and locate the option "Chained swap: \". +4. If one of the selected records doesn't comply with one of the + constraints, a message will be shown, and the swap won't continue. +5. If everything is OK, a popup will arise, and you will see a field + for filling the new value. +6. Click on "Change", and the swap will be done. +7. On the chatter of the source document, an entry will be logged for + reflecting the done swap. diff --git a/chained_swapper/security/ir.model.access.csv b/chained_swapper/security/ir.model.access.csv new file mode 100644 index 0000000000..4bd1828850 --- /dev/null +++ b/chained_swapper/security/ir.model.access.csv @@ -0,0 +1,8 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_chained_swapper_user,chained.swapper.user,model_chained_swapper,base.group_user,1,0,0,0 +access_chained_swapper_erp_manager,chained.swapper.erp_manager,model_chained_swapper,base.group_erp_manager,1,1,1,1 +access_chained_swapper_sub_field_user,chained.swapper.sub.field.user,model_chained_swapper_sub_field,base.group_user,1,0,0,0 +access_chained_swapper_sub_field_erp_manager,chained.swapper.sub.field.erp_manager,model_chained_swapper_sub_field,base.group_erp_manager,1,1,1,1 +access_chained_swapper_constraint_user,chained.swapper.constraint.user,model_chained_swapper_constraint,base.group_user,1,0,0,0 +access_chained_swapper_constraint_erp_manager,chained.swapper.constraint.erp_manager,model_chained_swapper_constraint,base.group_erp_manager,1,1,1,1 +access_chained_swapper_wizard,chained.swapper.wizard.constraint.erp_manager,model_chained_swapper_wizard,base.group_user,1,1,1,1 diff --git a/chained_swapper/static/description/icon.png b/chained_swapper/static/description/icon.png new file mode 100644 index 0000000000..3a0328b516 Binary files /dev/null and b/chained_swapper/static/description/icon.png differ diff --git a/chained_swapper/static/description/index.html b/chained_swapper/static/description/index.html new file mode 100644 index 0000000000..926979177e --- /dev/null +++ b/chained_swapper/static/description/index.html @@ -0,0 +1,508 @@ + + + + + +README.rst + + + +
+ + + +Odoo Community Association + +
+

Chained Swapper

+ +

Mature License: AGPL-3 OCA/server-ux Translate me on Weblate Try me on Runboat

+

This module allows to swap the value of a field and propagate it in a +chained way to linked records. Example: changing the delivery address in +a confirmed sales order, it should be changed in its delivery orders as +well.

+

It also allows to apply constraints for not allowing to do that change +according rules, so the business logic is not broken. Example: Don’t +allow to change the delivery address if the delivery order is validated.

+

This module requires some technical knowledge for setting the chained +swap and the constraint, as it’s defined through technical names and +Python code.

+

WARNING: Use this module with care, as it can screw up database +consistency if swaps are not properly designed .

+

Table of contents

+ +
+

Configuration

+

To configure this module, you need to:

+
    +
  1. Go to Setting > Field Swaps > Field Swaps.

    +
  2. +
  3. Create a new object and set the following data as an example:

    +
      +
    • Name for identifying it and use it for the action name.

      +
    • +
    • Select the source model where the swap will be started.

      +
    • +
    • Select the starting field for which the swap will be done.

      +
    • +
    • Add several chained fields. They are expressed as a string using +dot notation, taking the source model as beginning for looking +there the first field, and continuing from there drilling through. +Example: picking_ids.partner_id for sale.order model will go to the +linked deliveries orders, and change the customer there.

      +
    • +
    • Add possible constraints for restricting the chained swap. They are +Python expressions that must be one line that is evaluated as +boolean. If the evaluation is true, then a message will be thrown +and no swap will be allowed. You can use the variable records in +your code, that will be referring the selected records for doing +the swap. Example: for restricting sales orders that have a +delivery order validated:

      +

      any(p.state == ‘done’ for p in records.mapped(‘picking_ids.state’))

      +

      Other variables you can use are env, date and datetime. Each +constraint has a name for identifying it, but also for showing that +name when displaying the error trying to do the swap.

      +
    • +
    +
  4. +
  5. Click on ‘Add action’ smart button to add a new action in the source +model.

    +
  6. +
+

On demo databases, you can check the example “Language”, that changes +the language of a contact, and propagate it to children contacts.

+
+
+

Usage

+

To use this module, you need to:

+
    +
  1. Go to the source document in list mode.
  2. +
  3. Select one or several records.
  4. +
  5. Click on Action and locate the option “Chained swap: <name of the +swap>”.
  6. +
  7. If one of the selected records doesn’t comply with one of the +constraints, a message will be shown, and the swap won’t continue.
  8. +
  9. If everything is OK, a popup will arise, and you will see a field for +filling the new value.
  10. +
  11. Click on “Change”, and the swap will be done.
  12. +
  13. On the chatter of the source document, an entry will be logged for +reflecting the done swap.
  14. +
+
+
+

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

+
    +
  • Tecnativa
  • +
+
+
+

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/server-ux project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+
+ + diff --git a/chained_swapper/tests/__init__.py b/chained_swapper/tests/__init__.py new file mode 100644 index 0000000000..ccd7e321fe --- /dev/null +++ b/chained_swapper/tests/__init__.py @@ -0,0 +1,3 @@ +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from . import test_chained_swapper diff --git a/chained_swapper/tests/test_chained_swapper.py b/chained_swapper/tests/test_chained_swapper.py new file mode 100644 index 0000000000..9cb507b9c0 --- /dev/null +++ b/chained_swapper/tests/test_chained_swapper.py @@ -0,0 +1,105 @@ +# Copyright 2020 Tecnativa - Ernesto Tejeda +# Copyright 2020 Tecnativa - Pedro M. Baeza +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import exceptions +from odoo.tests import Form, tagged + +from odoo.addons.base.tests.common import BaseCommon + +from ..hooks import uninstall_hook + + +@tagged("post_install", "-at_install") +class TestChainedSwapper(BaseCommon): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.env["res.lang"]._activate_lang("es_ES") + res_partner = cls.env["res.partner"] + cls.partner_parent = res_partner.create( + {"name": "parent partner cs", "lang": "en_US"} + ) + cls.partner_child_1 = res_partner.create( + {"name": "partner child1 cs", "parent_id": cls.partner_parent.id} + ) + cls.partner_child_2 = res_partner.create( + {"name": "partner child2 cs", "parent_id": cls.partner_parent.id} + ) + # Prevent duplicate error removing demo data if exists + record = cls.env.ref("chained_swapper.chained_swapper_demo", False) + if record: + record.unlink() + + chained_swapper_form = Form(cls.env["chained.swapper"]) + chained_swapper_form.name = "Language" + chained_swapper_form.model_id = cls.env.ref("base.model_res_partner") + chained_swapper_form.field_id = cls.env.ref("base.field_res_partner__lang") + with chained_swapper_form.sub_field_ids.new() as sub_field_form: + sub_field_form.sub_field_chain = "child_ids.lang" + with chained_swapper_form.constraint_ids.new() as constraint_form: + constraint_form.name = "Only parent company" + constraint_form.expression = "bool(records.mapped('parent_id'))" + + cls.chained_swapper = chained_swapper_form.save() + cls.chained_swapper.add_action() + + def test_create_unlink_action(self): + """Test if Sidebar Action is added / removed to / from given object.""" + action = ( + self.chained_swapper.ref_ir_act_window_id + and self.chained_swapper.ref_ir_act_window_id.binding_model_id + ) + self.assertTrue(action) + # Remove the action + self.chained_swapper.unlink_action() + action = self.chained_swapper.ref_ir_act_window_id + self.assertFalse(action) + # Add an action + self.chained_swapper.add_action() + action = ( + self.chained_swapper.ref_ir_act_window_id + and self.chained_swapper.ref_ir_act_window_id.binding_model_id + ) + self.assertTrue(action) + + def test_unlink_chained_swapper(self): + """Test if related actions are removed when a chained swapper + record is unlinked.""" + action_id = self.chained_swapper.ref_ir_act_window_id.id + self.chained_swapper.unlink() + action = self.env["ir.actions.act_window"].search([("id", "=", action_id)]) + self.assertFalse(action) + + def test_change_constrained_partner_language(self): + with self.assertRaises(exceptions.UserError): + self.env["chained.swapper.wizard"].with_context( + active_model="res.partner", + active_id=self.partner_parent.id, + active_ids=(self.partner_parent | self.partner_child_1).ids, + chained_swapper_id=self.chained_swapper.id, + ).create({"lang": "es_ES"}) + + def test_change_partner_language(self): + self.env["chained.swapper.wizard"].with_context( + active_model="res.partner", + active_id=self.partner_parent.id, + active_ids=[self.partner_parent.id], + chained_swapper_id=self.chained_swapper.id, + ).create({"lang": "es_ES"}) + self.assertEqual(self.partner_parent.lang, "es_ES") + self.assertEqual(self.partner_child_1.lang, "es_ES") + self.assertEqual(self.partner_child_2.lang, "es_ES") + + def test_uninstall_hook(self): + """Test if related actions are removed when mass editing + record is uninstalled.""" + action_id = self.chained_swapper.ref_ir_act_window_id.id + uninstall_hook(self.env) + self.assertFalse(self.env["ir.actions.act_window"].browse(action_id).exists()) + + def test_invalid_constraint(self): + with self.assertRaises(exceptions.ValidationError): + self.chained_swapper.constraint_ids.write( + {"expression": "Something incorrect"} + ) diff --git a/chained_swapper/views/chained_swapper_views.xml b/chained_swapper/views/chained_swapper_views.xml new file mode 100644 index 0000000000..efba01bbcd --- /dev/null +++ b/chained_swapper/views/chained_swapper_views.xml @@ -0,0 +1,177 @@ + + + + + + chained.swapper.form + chained.swapper + +
+ +
+ + +
+
+
+ + + + + + + + + + + + +

Adding Constraints for Restricting the Chained Swap

+

+ You can add possible constraints to restrict the chained swap. These constraints are Python expressions that must be evaluated as a boolean value. + If the evaluation returns True, a message will be thrown and no swap will be allowed. +

+

+ You can use the variable records in your expression. This will refer to the selected records for performing the swap. For example: +

+
+        any(p.state == 'done' for p in records.picking_ids)
+                            
+

+ In this case, the constraint restricts the swap if any of the related picking orders has been validated (state 'done'). +

+

Other Variables You Can Use

+
    +
  • + env: The Odoo environment.
  • +
  • + date: The current date.
  • +
  • + datetime: The current date and time.
  • +
+

Constraint Name

+

+ Each constraint should have a name for identifying it. This name is also used when displaying the error message in case the swap is not allowed. +

+
+ + + + + + + + +
+ +
+
+
+
+ + chained.swapper.tree + chained.swapper + + + + + + + + + + Field Swaps + chained.swapper + list,form + + + + chained.swapper.sub.field.tree + chained.swapper.sub.field + + + + + + + + + + chained.swapper.constraint.tree + chained.swapper.constraint + + + + + + + + + chained.swapper.constraint.form + chained.swapper.constraint + +
+ + + + + + +
+
+
+ + + +
diff --git a/chained_swapper/wizard/__init__.py b/chained_swapper/wizard/__init__.py new file mode 100644 index 0000000000..f841eb5e04 --- /dev/null +++ b/chained_swapper/wizard/__init__.py @@ -0,0 +1,3 @@ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from . import chained_swapper_wizard diff --git a/chained_swapper/wizard/chained_swapper_wizard.py b/chained_swapper/wizard/chained_swapper_wizard.py new file mode 100644 index 0000000000..c478bbe12c --- /dev/null +++ b/chained_swapper/wizard/chained_swapper_wizard.py @@ -0,0 +1,182 @@ +# Copyright 2020 Tecnativa - Ernesto Tejeda +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +import ast +from datetime import date, datetime + +from lxml import etree +from markupsafe import Markup + +from odoo import api, fields, models +from odoo.exceptions import UserError +from odoo.tools.safe_eval import safe_eval + + +class ChainedSwapperWizard(models.TransientModel): + _name = "chained.swapper.wizard" + _description = "Wizard chained swapper" + + company_id = fields.Many2one( + "res.company", string="Company", default=lambda self: self.env.company + ) + + @api.model + def default_get(self, fields): + context = self.env.context + if context.get("chained_swapper_id"): + records = self.env[context.get("active_model")].browse( + context.get("active_ids") + ) + exp_dict = { + "records": records, + "env": self.env, + "date": date, + "datetime": datetime, + } + chained_swapper = self.env["chained.swapper"].browse( + context.get("chained_swapper_id") + ) + for constraint in chained_swapper.constraint_ids: + if safe_eval(constraint.expression, exp_dict): + raise UserError( + self.env._( + "Not possible to swap the field due to the constraint" + ) + + ": " + + constraint.name + ) + return super().default_get(fields) + + @api.model + def fields_get(self, allfields=None, attributes=None): + chained_swapper_id = self.env.context.get("chained_swapper_id") + chained_swapper = self.env["chained.swapper"].browse(chained_swapper_id) + res = super().fields_get(allfields, attributes) + field = chained_swapper.field_id + model = self.env[field.model] + field_info = model.fields_get() + res.update({field.name: field_info[field.name]}) + return res + + def onchange(self, values, field_names, fields_spec): + fields_spec = {k: v for k, v in fields_spec.items() if k in self._fields} + return super().onchange(values, field_names, fields_spec) + + @api.model + def get_views(self, views, options=None): + action = self.env["ir.actions.act_window"].browse(options.get("action_id")) + context = ast.literal_eval(action.context) + self = self.with_context(chained_swapper_id=context.get("chained_swapper_id")) + res = super().get_views(views, options) + return res + + @api.model + def get_view(self, view_id=None, view_type="form", **options): + """As we don't have any field in this model, result['fields'] + and result['arch'] are modified to add dynamically the + corresponding field. + """ + action = self.env["ir.actions.act_window"].browse(options.get("action_id")) + context = ast.literal_eval(action.context) + chained_swapper = self.env["chained.swapper"].browse( + context.get("chained_swapper_id") + ) + self = self.with_context(chained_swapper_id=context.get("chained_swapper_id")) + field = chained_swapper.field_id + res = super().get_view(view_id, view_type, **options) + # XML view definition + doc = etree.XML(res["arch"]) + group_node = doc.xpath("//group[@name='swap_field_group']")[0] + etree.SubElement(group_node, "field", {"name": field.name}) + if field.ttype in ["one2many", "many2many", "text"]: + group_node.set("string", field.field_description) + group_node.set("nolabel", "1") + res.update(arch=etree.tostring(doc, encoding="unicode")) + return res + + @api.model_create_multi + def create(self, vals_list): + """As we don't have any field in this model, the key-value pair + received in vals dict are only used to change the value in the active + models. + """ + for vals in vals_list: + model_obj = self.env[self.env.context.get("active_model")] + context = self.env.context + field_name, new_value = list(vals.items())[0] + # write the active model + model = model_obj.browse(self.env.context.get("active_ids")) + original_values = {m.id: m[field_name] for m in model} + model.write(vals) + if hasattr(model, "message_post"): + self.post_chained_swap(model, field_name, original_values, new_value) + # write chained models + chained_swapper_obj = self.env["chained.swapper"] + chained_swapper = chained_swapper_obj.browse( + context.get("chained_swapper_id") + ) + for sub_field in chained_swapper.sub_field_ids: + chain_fields = sub_field.sub_field_chain.split(".") + field_name = chain_fields.pop() + chain_model = model + for chain_field in chain_fields: + chain_model = chain_model.mapped(chain_field) + original_values = {cm.id: cm[field_name] for cm in chain_model} + chain_model.write({field_name: new_value}) + # post swap + if hasattr(chain_model, "message_post"): + self.post_chained_swap( + chain_model, field_name, original_values, new_value + ) + return super().create([{} for _ in vals_list]) + + def change_action(self): + return {"type": "ir.actions.act_window_close"} + + @api.model + def post_chained_swap(self, model, field_name, original_values, new_value): + def human_readable_field(value): + result = value + field_def = model._fields[field_name] + if field_def.type == "selection": + if type(field_def.selection) is list: + selection = field_def.selection + else: + selection = field_def.selection(self) + for selection_item in selection: + if selection_item[0] == value: + result = selection_item[1] + break + elif field_def.type == "many2one": + if type(value) is int: + result = self.env[field_def.comodel_name].browse(value) + result = result.display_name + elif field_def.type == "many2many": + if type(value) is list: + ids = value[0][2] + value = self.env[field_def.comodel_name].browse(ids) + result = str(value.mapped("display_name")) + return result + + field_desc = model._fields[field_name].string + new_value = human_readable_field(new_value) + for m in model: + original_value = human_readable_field(original_values[m.id]) + body = Markup( + self.env._( + "Chained swap done:
%(field)s: %(old)s ⇒ %(new)s", + field=field_desc, + old=original_value, + new=new_value, + ) + ) + m.message_post(body=body) + + def read(self, fields, load="_classic_read"): + """Without this call, dynamic fields build by get_view() + generate a crash and warning, i.e.: read() with unknown field 'myfield' + """ + real_fields = set(fields) & set(self._fields) + result = super().read(list(real_fields), load=load) + result[0].update({x: False for x in set(fields) - real_fields}) + return result diff --git a/chained_swapper/wizard/chained_swapper_wizard_views.xml b/chained_swapper/wizard/chained_swapper_wizard_views.xml new file mode 100644 index 0000000000..9c9651b8a6 --- /dev/null +++ b/chained_swapper/wizard/chained_swapper_wizard_views.xml @@ -0,0 +1,24 @@ + + + + + chained.swapper.wizard.form + chained.swapper.wizard + +
+ + + + + +
+