diff --git a/partner_archive_propagate/README.rst b/partner_archive_propagate/README.rst new file mode 100644 index 00000000000..d2110937cf1 --- /dev/null +++ b/partner_archive_propagate/README.rst @@ -0,0 +1,128 @@ +========================= +Partner Archive Propagate +========================= + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:82494c9a9bbd7ccf2fe16046dff9b9f7966caf9a1adecad44a99ebb59f9a0a6c + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fpartner--contact-lightgray.png?logo=github + :target: https://github.com/OCA/partner-contact/tree/18.0/partner_archive_propagate + :alt: OCA/partner-contact +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/partner-contact-18-0/partner-contact-18-0-partner_archive_propagate + :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/partner-contact&target_branch=18.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +Partner Archive Propagation +--------------------------- + +This module extends the native archiving mechanism for partners. + +When archiving a company or parent contact, it will also handle its +descendants according to business rules — with user control and +safeguards. + +Features +~~~~~~~~ + +- Adds a new **"Archive Contact and Children"** button on the Partner + form. +- Shows a **wizard** listing contact-type descendants before archiving. +- Automatically skips descendants linked to active users. +- Adds a technical Many2one field (propagated_from_id) that records + which parent partner caused the automatic archiving, making + propagation fully traceable and reversible. +- Ensures automatic unarchive propagation. +- Includes a system setting to enforce propagation even for non-UI + actions (imports, RPC, automated jobs, etc.). + +**Table of contents** + +.. contents:: + :local: + +Configuration +============= + +- Go to **Settings → Technical → General Settings → Partner Archive + Propagate**. +- Enable *"Force propagation outside UI"* to automatically apply + propagation when partners are archived through automation or imports. + +Usage +===== + +1. Open a partner (company or main contact). +2. Click the **"Archive Contact and Children"** button. +3. Review the list of child contacts to be archived. +4. Confirm the action. + + - Non-contact types (e.g., invoice/delivery addresses) are archived + silently. + - Contact-type descendants appear in the wizard for review. + +Unarchiving also follows propagation rules: if a parent partner is +unarchived, its propagated descendants are unarchived as well. + +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 +------- + +* Therp BV + +Contributors +------------ + +- Nikos Tsirintanis + +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. + +.. |maintainer-ntsirintanis| image:: https://github.com/ntsirintanis.png?size=40px + :target: https://github.com/ntsirintanis + :alt: ntsirintanis + +Current `maintainer `__: + +|maintainer-ntsirintanis| + +This module is part of the `OCA/partner-contact `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/partner_archive_propagate/__init__.py b/partner_archive_propagate/__init__.py new file mode 100644 index 00000000000..9b4296142f4 --- /dev/null +++ b/partner_archive_propagate/__init__.py @@ -0,0 +1,2 @@ +from . import models +from . import wizard diff --git a/partner_archive_propagate/__manifest__.py b/partner_archive_propagate/__manifest__.py new file mode 100644 index 00000000000..9618e78a70a --- /dev/null +++ b/partner_archive_propagate/__manifest__.py @@ -0,0 +1,21 @@ +# Copyright 2025 Therp BV +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). +{ + "name": "Partner Archive Propagate", + "summary": "Archive/unarchive partner contacts hierarchically", + "version": "18.0.1.0.0", + "category": "Partner Management", + "author": "Therp BV, Odoo Community Association (OCA)", + "website": "https://github.com/OCA/partner-contact", + "license": "AGPL-3", + "depends": ["base_setup", "mail"], + "data": [ + "security/ir.model.access.csv", + "views/res_config_settings_views.xml", + "views/res_partner_views.xml", + "wizard/archive_propagate_wizard_views.xml", + ], + "installable": True, + "application": False, + "maintainers": ["ntsirintanis"], +} diff --git a/partner_archive_propagate/i18n/it.po b/partner_archive_propagate/i18n/it.po new file mode 100644 index 00000000000..55a18ca7b1a --- /dev/null +++ b/partner_archive_propagate/i18n/it.po @@ -0,0 +1,212 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * partner_archive_propagate +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: it\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" + +#. module: partner_archive_propagate +#: model_terms:ir.ui.view,arch_db:partner_archive_propagate.view_archive_propagate_wizard_form +msgid "Archive" +msgstr "" + +#. module: partner_archive_propagate +#: model_terms:ir.ui.view,arch_db:partner_archive_propagate.view_partner_form_inherit_archive_propagate +msgid "Archive Contact and Children" +msgstr "" + +#. module: partner_archive_propagate +#. odoo-python +#: code:addons/partner_archive_propagate/models/res_partner.py:0 +#: model_terms:ir.ui.view,arch_db:partner_archive_propagate.view_archive_propagate_wizard_form +#, python-format +msgid "Archive Contacts" +msgstr "" + +#. module: partner_archive_propagate +#: model:ir.model,name:partner_archive_propagate.model_res_partner_archive_propagate_wizard +msgid "Archive partner and children of partner " +msgstr "" + +#. module: partner_archive_propagate +#: model:ir.model.fields,field_description:partner_archive_propagate.field_res_partner__propagated_from_id +#: model:ir.model.fields,field_description:partner_archive_propagate.field_res_users__propagated_from_id +msgid "Archived Due To" +msgstr "" + +#. module: partner_archive_propagate +#: model_terms:ir.ui.view,arch_db:partner_archive_propagate.view_archive_propagate_wizard_form +msgid "Cancel" +msgstr "" + +#. module: partner_archive_propagate +#: model:ir.model,name:partner_archive_propagate.model_res_config_settings +msgid "Config Settings" +msgstr "" + +#. module: partner_archive_propagate +#: model:ir.model,name:partner_archive_propagate.model_res_partner +#: model:ir.model.fields,field_description:partner_archive_propagate.field_res_partner_archive_propagate_line__partner_id +msgid "Contact" +msgstr "" + +#. module: partner_archive_propagate +#: model:ir.model.fields,field_description:partner_archive_propagate.field_res_partner_archive_propagate_wizard__line_ids +msgid "Contacts to archive" +msgstr "" + +#. module: partner_archive_propagate +#: model:ir.model.fields,field_description:partner_archive_propagate.field_res_partner_archive_propagate_line__create_uid +#: model:ir.model.fields,field_description:partner_archive_propagate.field_res_partner_archive_propagate_wizard__create_uid +msgid "Created by" +msgstr "" + +#. module: partner_archive_propagate +#: model:ir.model.fields,field_description:partner_archive_propagate.field_res_partner_archive_propagate_line__create_date +#: model:ir.model.fields,field_description:partner_archive_propagate.field_res_partner_archive_propagate_wizard__create_date +msgid "Created on" +msgstr "" + +#. module: partner_archive_propagate +#: model:ir.model.fields,field_description:partner_archive_propagate.field_res_partner_archive_propagate_line__display_name +#: model:ir.model.fields,field_description:partner_archive_propagate.field_res_partner_archive_propagate_wizard__display_name +msgid "Display Name" +msgstr "" + +#. module: partner_archive_propagate +#: model:ir.model.fields,field_description:partner_archive_propagate.field_res_partner_archive_propagate_line__email +msgid "Email" +msgstr "" + +#. module: partner_archive_propagate +#: model:ir.model.fields,field_description:partner_archive_propagate.field_res_config_settings__partner_archive_force_outside_ui +msgid "Force propagation outside UI" +msgstr "" + +#. module: partner_archive_propagate +#: model:ir.model.fields,field_description:partner_archive_propagate.field_res_partner_archive_propagate_line__id +#: model:ir.model.fields,field_description:partner_archive_propagate.field_res_partner_archive_propagate_wizard__id +msgid "ID" +msgstr "" + +#. module: partner_archive_propagate +#: model:ir.model.fields,help:partner_archive_propagate.field_res_config_settings__partner_archive_force_outside_ui +msgid "" +"If enabled, archiving a partner via any non-UI method (imports, RPC, " +"automated) will also archive all descendants and mark them as propagated." +msgstr "" + +#. module: partner_archive_propagate +#: model:ir.model.fields,field_description:partner_archive_propagate.field_res_partner_archive_propagate_line____last_update +#: model:ir.model.fields,field_description:partner_archive_propagate.field_res_partner_archive_propagate_wizard____last_update +msgid "Last Modified on" +msgstr "" + +#. module: partner_archive_propagate +#: model:ir.model.fields,field_description:partner_archive_propagate.field_res_partner_archive_propagate_line__write_uid +#: model:ir.model.fields,field_description:partner_archive_propagate.field_res_partner_archive_propagate_wizard__write_uid +msgid "Last Updated by" +msgstr "" + +#. module: partner_archive_propagate +#: model:ir.model.fields,field_description:partner_archive_propagate.field_res_partner_archive_propagate_line__write_date +#: model:ir.model.fields,field_description:partner_archive_propagate.field_res_partner_archive_propagate_wizard__write_date +msgid "Last Updated on" +msgstr "" + +#. module: partner_archive_propagate +#: model:ir.model.fields,field_description:partner_archive_propagate.field_res_partner_archive_propagate_line__name +msgid "Name" +msgstr "" + +#. module: partner_archive_propagate +#: model:ir.model.fields,field_description:partner_archive_propagate.field_res_partner_archive_propagate_wizard__partner_id +msgid "Partner" +msgstr "" + +#. module: partner_archive_propagate +#: model_terms:ir.ui.view,arch_db:partner_archive_propagate.view_res_config_settings_partner_archive +msgid "Partner Archive Propagate" +msgstr "" + +#. module: partner_archive_propagate +#: model:ir.model,name:partner_archive_propagate.model_res_partner_archive_propagate_line +msgid "Partner Archive Propagate Line" +msgstr "" + +#. module: partner_archive_propagate +#: model:ir.model.fields,field_description:partner_archive_propagate.field_res_partner_archive_propagate_line__phone +msgid "Phone" +msgstr "" + +#. module: partner_archive_propagate +#: model:ir.model.fields,field_description:partner_archive_propagate.field_res_partner__show_prop_wizard_button +#: model:ir.model.fields,field_description:partner_archive_propagate.field_res_users__show_prop_wizard_button +msgid "Show Prop Wizard Button" +msgstr "" + +#. module: partner_archive_propagate +#: model:ir.model.fields,help:partner_archive_propagate.field_res_partner__show_prop_wizard_button +#: model:ir.model.fields,help:partner_archive_propagate.field_res_users__show_prop_wizard_button +msgid "Show or hide the wizard button, depending on child_ids" +msgstr "" + +#. module: partner_archive_propagate +#. odoo-python +#: code:addons/partner_archive_propagate/models/res_partner.py:0 +#, python-format +msgid "" +"Skipped archiving the following contacts because they are linked to active " +"users: %s" +msgstr "" + +#. module: partner_archive_propagate +#. odoo-python +#: code:addons/partner_archive_propagate/wizard/archive_propagate_wizard.py:0 +#, python-format +msgid "Skipped contacts linked to active users: %s" +msgstr "" + +#. module: partner_archive_propagate +#. odoo-python +#: code:addons/partner_archive_propagate/wizard/archive_propagate_wizard.py:0 +#, python-format +msgid "Some contacts were skipped" +msgstr "" + +#. module: partner_archive_propagate +#: model:ir.model.fields,help:partner_archive_propagate.field_res_partner__propagated_from_id +#: model:ir.model.fields,help:partner_archive_propagate.field_res_users__propagated_from_id +msgid "" +"Technical field. Set when this partner was archived automatically because " +"the source partner was archived. Cleared again when it is unarchived by the " +"opposite propagation." +msgstr "" + +#. module: partner_archive_propagate +#: model_terms:ir.ui.view,arch_db:partner_archive_propagate.view_archive_propagate_wizard_form +msgid "" +"The following contact-type descendants will be archived as well.\n" +" Remove any you want to keep active." +msgstr "" + +#. module: partner_archive_propagate +#: model_terms:ir.ui.view,arch_db:partner_archive_propagate.view_res_config_settings_partner_archive +msgid "" +"When enabled, archiving a partner via imports/RPC/automation will also\n" +" archive all descendants and mark them as propagated." +msgstr "" + +#. module: partner_archive_propagate +#: model:ir.model.fields,field_description:partner_archive_propagate.field_res_partner_archive_propagate_line__wizard_id +msgid "Wizard" +msgstr "" diff --git a/partner_archive_propagate/i18n/partner_archive_propagate.pot b/partner_archive_propagate/i18n/partner_archive_propagate.pot new file mode 100644 index 00000000000..255a5336fd2 --- /dev/null +++ b/partner_archive_propagate/i18n/partner_archive_propagate.pot @@ -0,0 +1,211 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * partner_archive_propagate +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.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: partner_archive_propagate +#: model_terms:ir.ui.view,arch_db:partner_archive_propagate.view_archive_propagate_wizard_form +msgid "Archive" +msgstr "" + +#. module: partner_archive_propagate +#: model_terms:ir.ui.view,arch_db:partner_archive_propagate.view_partner_form_inherit_archive_propagate +msgid "Archive Contact and Children" +msgstr "" + +#. module: partner_archive_propagate +#. odoo-python +#: code:addons/partner_archive_propagate/models/res_partner.py:0 +#: model_terms:ir.ui.view,arch_db:partner_archive_propagate.view_archive_propagate_wizard_form +#, python-format +msgid "Archive Contacts" +msgstr "" + +#. module: partner_archive_propagate +#: model:ir.model,name:partner_archive_propagate.model_res_partner_archive_propagate_wizard +msgid "Archive partner and children of partner " +msgstr "" + +#. module: partner_archive_propagate +#: model:ir.model.fields,field_description:partner_archive_propagate.field_res_partner__propagated_from_id +#: model:ir.model.fields,field_description:partner_archive_propagate.field_res_users__propagated_from_id +msgid "Archived Due To" +msgstr "" + +#. module: partner_archive_propagate +#: model_terms:ir.ui.view,arch_db:partner_archive_propagate.view_archive_propagate_wizard_form +msgid "Cancel" +msgstr "" + +#. module: partner_archive_propagate +#: model:ir.model,name:partner_archive_propagate.model_res_config_settings +msgid "Config Settings" +msgstr "" + +#. module: partner_archive_propagate +#: model:ir.model,name:partner_archive_propagate.model_res_partner +#: model:ir.model.fields,field_description:partner_archive_propagate.field_res_partner_archive_propagate_line__partner_id +msgid "Contact" +msgstr "" + +#. module: partner_archive_propagate +#: model:ir.model.fields,field_description:partner_archive_propagate.field_res_partner_archive_propagate_wizard__line_ids +msgid "Contacts to archive" +msgstr "" + +#. module: partner_archive_propagate +#: model:ir.model.fields,field_description:partner_archive_propagate.field_res_partner_archive_propagate_line__create_uid +#: model:ir.model.fields,field_description:partner_archive_propagate.field_res_partner_archive_propagate_wizard__create_uid +msgid "Created by" +msgstr "" + +#. module: partner_archive_propagate +#: model:ir.model.fields,field_description:partner_archive_propagate.field_res_partner_archive_propagate_line__create_date +#: model:ir.model.fields,field_description:partner_archive_propagate.field_res_partner_archive_propagate_wizard__create_date +msgid "Created on" +msgstr "" + +#. module: partner_archive_propagate +#: model:ir.model.fields,field_description:partner_archive_propagate.field_res_partner_archive_propagate_line__display_name +#: model:ir.model.fields,field_description:partner_archive_propagate.field_res_partner_archive_propagate_wizard__display_name +msgid "Display Name" +msgstr "" + +#. module: partner_archive_propagate +#: model:ir.model.fields,field_description:partner_archive_propagate.field_res_partner_archive_propagate_line__email +msgid "Email" +msgstr "" + +#. module: partner_archive_propagate +#: model:ir.model.fields,field_description:partner_archive_propagate.field_res_config_settings__partner_archive_force_outside_ui +msgid "Force propagation outside UI" +msgstr "" + +#. module: partner_archive_propagate +#: model:ir.model.fields,field_description:partner_archive_propagate.field_res_partner_archive_propagate_line__id +#: model:ir.model.fields,field_description:partner_archive_propagate.field_res_partner_archive_propagate_wizard__id +msgid "ID" +msgstr "" + +#. module: partner_archive_propagate +#: model:ir.model.fields,help:partner_archive_propagate.field_res_config_settings__partner_archive_force_outside_ui +msgid "" +"If enabled, archiving a partner via any non-UI method (imports, RPC, " +"automated) will also archive all descendants and mark them as propagated." +msgstr "" + +#. module: partner_archive_propagate +#: model:ir.model.fields,field_description:partner_archive_propagate.field_res_partner_archive_propagate_line____last_update +#: model:ir.model.fields,field_description:partner_archive_propagate.field_res_partner_archive_propagate_wizard____last_update +msgid "Last Modified on" +msgstr "" + +#. module: partner_archive_propagate +#: model:ir.model.fields,field_description:partner_archive_propagate.field_res_partner_archive_propagate_line__write_uid +#: model:ir.model.fields,field_description:partner_archive_propagate.field_res_partner_archive_propagate_wizard__write_uid +msgid "Last Updated by" +msgstr "" + +#. module: partner_archive_propagate +#: model:ir.model.fields,field_description:partner_archive_propagate.field_res_partner_archive_propagate_line__write_date +#: model:ir.model.fields,field_description:partner_archive_propagate.field_res_partner_archive_propagate_wizard__write_date +msgid "Last Updated on" +msgstr "" + +#. module: partner_archive_propagate +#: model:ir.model.fields,field_description:partner_archive_propagate.field_res_partner_archive_propagate_line__name +msgid "Name" +msgstr "" + +#. module: partner_archive_propagate +#: model:ir.model.fields,field_description:partner_archive_propagate.field_res_partner_archive_propagate_wizard__partner_id +msgid "Partner" +msgstr "" + +#. module: partner_archive_propagate +#: model_terms:ir.ui.view,arch_db:partner_archive_propagate.view_res_config_settings_partner_archive +msgid "Partner Archive Propagate" +msgstr "" + +#. module: partner_archive_propagate +#: model:ir.model,name:partner_archive_propagate.model_res_partner_archive_propagate_line +msgid "Partner Archive Propagate Line" +msgstr "" + +#. module: partner_archive_propagate +#: model:ir.model.fields,field_description:partner_archive_propagate.field_res_partner_archive_propagate_line__phone +msgid "Phone" +msgstr "" + +#. module: partner_archive_propagate +#: model:ir.model.fields,field_description:partner_archive_propagate.field_res_partner__show_prop_wizard_button +#: model:ir.model.fields,field_description:partner_archive_propagate.field_res_users__show_prop_wizard_button +msgid "Show Prop Wizard Button" +msgstr "" + +#. module: partner_archive_propagate +#: model:ir.model.fields,help:partner_archive_propagate.field_res_partner__show_prop_wizard_button +#: model:ir.model.fields,help:partner_archive_propagate.field_res_users__show_prop_wizard_button +msgid "Show or hide the wizard button, depending on child_ids" +msgstr "" + +#. module: partner_archive_propagate +#. odoo-python +#: code:addons/partner_archive_propagate/models/res_partner.py:0 +#, python-format +msgid "" +"Skipped archiving the following contacts because they are linked to active " +"users: %s" +msgstr "" + +#. module: partner_archive_propagate +#. odoo-python +#: code:addons/partner_archive_propagate/wizard/archive_propagate_wizard.py:0 +#, python-format +msgid "Skipped contacts linked to active users: %s" +msgstr "" + +#. module: partner_archive_propagate +#. odoo-python +#: code:addons/partner_archive_propagate/wizard/archive_propagate_wizard.py:0 +#, python-format +msgid "Some contacts were skipped" +msgstr "" + +#. module: partner_archive_propagate +#: model:ir.model.fields,help:partner_archive_propagate.field_res_partner__propagated_from_id +#: model:ir.model.fields,help:partner_archive_propagate.field_res_users__propagated_from_id +msgid "" +"Technical field. Set when this partner was archived automatically because " +"the source partner was archived. Cleared again when it is unarchived by the " +"opposite propagation." +msgstr "" + +#. module: partner_archive_propagate +#: model_terms:ir.ui.view,arch_db:partner_archive_propagate.view_archive_propagate_wizard_form +msgid "" +"The following contact-type descendants will be archived as well.\n" +" Remove any you want to keep active." +msgstr "" + +#. module: partner_archive_propagate +#: model_terms:ir.ui.view,arch_db:partner_archive_propagate.view_res_config_settings_partner_archive +msgid "" +"When enabled, archiving a partner via imports/RPC/automation will also\n" +" archive all descendants and mark them as propagated." +msgstr "" + +#. module: partner_archive_propagate +#: model:ir.model.fields,field_description:partner_archive_propagate.field_res_partner_archive_propagate_line__wizard_id +msgid "Wizard" +msgstr "" diff --git a/partner_archive_propagate/models/__init__.py b/partner_archive_propagate/models/__init__.py new file mode 100644 index 00000000000..0d7a048a3c9 --- /dev/null +++ b/partner_archive_propagate/models/__init__.py @@ -0,0 +1,2 @@ +from . import res_partner +from . import res_config_settings diff --git a/partner_archive_propagate/models/res_config_settings.py b/partner_archive_propagate/models/res_config_settings.py new file mode 100644 index 00000000000..ea699d9bbda --- /dev/null +++ b/partner_archive_propagate/models/res_config_settings.py @@ -0,0 +1,16 @@ +# Copyright 2025 Therp BV +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). +from odoo import fields, models + + +class ResConfigSettings(models.TransientModel): + _inherit = "res.config.settings" + + partner_archive_force_outside_ui = fields.Boolean( + string="Force propagation outside UI", + help="If enabled, archiving a partner via any non-UI method" + " (imports, RPC, automated) " + "will also archive all descendants and mark them as propagated.", + config_parameter="partner_archive_propagate.force_outside_ui", + default=False, + ) diff --git a/partner_archive_propagate/models/res_partner.py b/partner_archive_propagate/models/res_partner.py new file mode 100644 index 00000000000..63793269709 --- /dev/null +++ b/partner_archive_propagate/models/res_partner.py @@ -0,0 +1,208 @@ +# Copyright 2025 Therp BV +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +from odoo import _, api, fields, models + + +class ResPartner(models.Model): + _inherit = "res.partner" + + propagated_from_id = fields.Many2one( + "res.partner", + string="Archived Due To", + index=True, + help=( + "Technical field. Set when this partner was archived automatically " + "because the source partner was archived. " + "Cleared again when it is unarchived by the opposite propagation." + ), + ) + + show_prop_wizard_button = fields.Boolean( + compute="_compute_show_prop_wizard_button", + help="Show or hide the wizard button, depending on child_ids", + ) + + def _compute_show_prop_wizard_button(self): + for partner in self: + partner.show_prop_wizard_button = bool(partner.child_ids) + + def _get_archive_propagation_candidates(self): + """To be overidden""" + self.ensure_one() + return self._get_descendants() + + def write(self, vals): + """Enable (un)archiving propagation: + - detect which partners are being (un)archived + - calls _(un)archive_propagate() on them. + """ + archiving_partners = self.browse() + unarchiving_partners = self.browse() + if "active" in vals: + if vals["active"] is False: + archiving_partners = self.filtered(lambda self: self.active) + elif vals["active"] is True: + unarchiving_partners = self.filtered(lambda self: not self.active) + res = super().write(vals) + from_wizard = self.env.context.get("partner_archive_propagate_ui") + non_ui = self._force_non_ui() + # Archiving + if archiving_partners: + if from_wizard: + archiving_partners._archive_propagate_wizard() + elif non_ui: + archiving_partners._archive_propagate_external() + # Unarchiving, always propagate + if unarchiving_partners: + unarchiving_partners._unarchive_propagate() + return res + + def _archive_propagate_wizard(self): + """Archive descendants according to propagation rules.""" + # Wizard: only non-contact descendants are silently archived here. + for partner in self: + non_contacts = ( + partner._get_archive_propagation_candidates() + .sudo() + .filtered(lambda p: p.active and p.type != "contact") + ) + if not non_contacts: + continue + ( + archivable, + unarchivable, + ) = non_contacts._split_archivable_unarchivable_user() + archivable.write( + { + "active": False, + "propagated_from_id": partner.id, + } + ) + partner._notify_skipped_partners(unarchivable) + + def _archive_propagate_external(self): + """Archive descendants according to propagation rules.""" + # Non-UI: archive all children + for partner in self: + descendants = ( + partner._get_archive_propagation_candidates() + .sudo() + .filtered(lambda p: p.active) + ) + if not descendants: + continue + ( + archivable, + unarchivable, + ) = descendants._split_archivable_unarchivable_user() + archivable.write( + { + "active": False, + "propagated_from_id": partner.id, + } + ) + partner._notify_skipped_partners(unarchivable) + + def _unarchive_propagate(self): + """Unarchive descendants archived via propagation. + Only touches partners where propagated_from_id points to the unarchived + parent, and that are currently inactive. + """ + to_unarchive = ( + self.env["res.partner"] + .with_context(active_test=False) + .sudo() + .search( + [ + ("propagated_from_id", "in", self.ids), + ("active", "=", False), + ] + ) + ) + if not to_unarchive: + return + to_unarchive.write( + { + "active": True, + "propagated_from_id": False, + } + ) + + def _get_descendants(self): + """Return only active children.""" + self.ensure_one() + return self.env["res.partner"].search( + [("id", "child_of", self.ids), ("id", "!=", self.id)] + ) + + def _split_archivable_unarchivable_user(self): + """ + Find which partners are archivable + (i.e., no active users) and unarchivable. + """ + archivable = self.browse() + unarchivable = self.browse() + for partner in self: + if partner.sudo().user_ids.filtered(lambda u: u.active): + unarchivable |= partner + continue + archivable |= partner + return archivable, unarchivable + + def _notify_skipped_partners(self, unarchivable_partners): + """Message listing partners skipped due to active users.""" + unarchivable_partners = unarchivable_partners or self.browse() + if not unarchivable_partners: + return + self.message_post( + body=_( + "Skipped archiving the following contacts " + "because they are linked to active users: %s" + ) + % ", ".join(unarchivable_partners.mapped("name")) + ) + + @api.model + def _force_non_ui(self): + """Read system setting controlling external propagation.""" + return self.env["ir.config_parameter"].sudo().get_param( + "partner_archive_propagate.force_outside_ui", default="0" + ) in ("1", "true", "True") + + def action_archive_with_contacts(self): + """Show wizard only if there are type contact descendants. + + - If there are no type contact descendants: archive immediately. + - If there are type contact descendants: open wizard listing only those, + allowing the user to exclude some. + """ + self.ensure_one() + candidates = self._get_archive_propagation_candidates().filtered( + lambda p: p.active + ) + contacts_to_show = candidates.filtered(lambda p: p.type == "contact") + if not contacts_to_show: + # No wizard window opens, archive right away + return self.with_context(partner_archive_propagate_ui=True).write( + {"active": False} + ) + ( + archivable, + unarchivable, + ) = contacts_to_show._split_archivable_unarchivable_user() + wiz = self.env["res.partner.archive.propagate.wizard"].create( + { + "partner_id": self.id, + "line_ids": [(0, 0, {"partner_id": p.id}) for p in archivable], + } + ) + self._notify_skipped_partners(unarchivable) + return { + "type": "ir.actions.act_window", + "name": _("Archive Contacts"), + "res_model": "res.partner.archive.propagate.wizard", + "view_mode": "form", + "target": "new", + "res_id": wiz.id, + } diff --git a/partner_archive_propagate/pyproject.toml b/partner_archive_propagate/pyproject.toml new file mode 100644 index 00000000000..4231d0cccb3 --- /dev/null +++ b/partner_archive_propagate/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/partner_archive_propagate/readme/CONFIGURE.md b/partner_archive_propagate/readme/CONFIGURE.md new file mode 100644 index 00000000000..3912d7eb64f --- /dev/null +++ b/partner_archive_propagate/readme/CONFIGURE.md @@ -0,0 +1,4 @@ +- Go to **Settings → Technical → General Settings → Partner Archive + Propagate**. +- Enable *"Force propagation outside UI"* to automatically apply + propagation when partners are archived through automation or imports. diff --git a/partner_archive_propagate/readme/CONTRIBUTORS.md b/partner_archive_propagate/readme/CONTRIBUTORS.md new file mode 100644 index 00000000000..2d66b8d1486 --- /dev/null +++ b/partner_archive_propagate/readme/CONTRIBUTORS.md @@ -0,0 +1 @@ +- Nikos Tsirintanis \<\> diff --git a/partner_archive_propagate/readme/DESCRIPTION.md b/partner_archive_propagate/readme/DESCRIPTION.md new file mode 100644 index 00000000000..0f27995f775 --- /dev/null +++ b/partner_archive_propagate/readme/DESCRIPTION.md @@ -0,0 +1,20 @@ +## Partner Archive Propagation + +This module extends the native archiving mechanism for partners. + +When archiving a company or parent contact, it will also handle its +descendants according to business rules — with user control and +safeguards. + +### Features + +- Adds a new **"Archive Contact and Children"** button on the Partner + form. +- Shows a **wizard** listing contact-type descendants before archiving. +- Automatically skips descendants linked to active users. +- Adds a technical Many2one field (propagated_from_id) that records + which parent partner caused the automatic archiving, making + propagation fully traceable and reversible. +- Ensures automatic unarchive propagation. +- Includes a system setting to enforce propagation even for non-UI + actions (imports, RPC, automated jobs, etc.). diff --git a/partner_archive_propagate/readme/USAGE.md b/partner_archive_propagate/readme/USAGE.md new file mode 100644 index 00000000000..8c6eac7761e --- /dev/null +++ b/partner_archive_propagate/readme/USAGE.md @@ -0,0 +1,10 @@ +1. Open a partner (company or main contact). +2. Click the **"Archive Contact and Children"** button. +3. Review the list of child contacts to be archived. +4. Confirm the action. + - Non-contact types (e.g., invoice/delivery addresses) are archived + silently. + - Contact-type descendants appear in the wizard for review. + +Unarchiving also follows propagation rules: if a parent partner is +unarchived, its propagated descendants are unarchived as well. diff --git a/partner_archive_propagate/security/ir.model.access.csv b/partner_archive_propagate/security/ir.model.access.csv new file mode 100644 index 00000000000..e17837ca69a --- /dev/null +++ b/partner_archive_propagate/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_archive_line,access_archive_line,model_res_partner_archive_propagate_line,base.group_user,1,1,1,1 +access_archive_wizard,access_archive_wizard,model_res_partner_archive_propagate_wizard,base.group_user,1,1,1,1 diff --git a/partner_archive_propagate/static/description/icon.png b/partner_archive_propagate/static/description/icon.png new file mode 100644 index 00000000000..3a0328b516c Binary files /dev/null and b/partner_archive_propagate/static/description/icon.png differ diff --git a/partner_archive_propagate/static/description/index.html b/partner_archive_propagate/static/description/index.html new file mode 100644 index 00000000000..2fd3d66719b --- /dev/null +++ b/partner_archive_propagate/static/description/index.html @@ -0,0 +1,468 @@ + + + + + +Partner Archive Propagate + + + +
+

Partner Archive Propagate

+ + +

Beta License: AGPL-3 OCA/partner-contact Translate me on Weblate Try me on Runboat

+
+

Partner Archive Propagation

+

This module extends the native archiving mechanism for partners.

+

When archiving a company or parent contact, it will also handle its +descendants according to business rules — with user control and +safeguards.

+
+

Features

+
    +
  • Adds a new “Archive Contact and Children” button on the Partner +form.
  • +
  • Shows a wizard listing contact-type descendants before archiving.
  • +
  • Automatically skips descendants linked to active users.
  • +
  • Adds a technical Many2one field (propagated_from_id) that records +which parent partner caused the automatic archiving, making +propagation fully traceable and reversible.
  • +
  • Ensures automatic unarchive propagation.
  • +
  • Includes a system setting to enforce propagation even for non-UI +actions (imports, RPC, automated jobs, etc.).
  • +
+

Table of contents

+ +
+

Configuration

+
    +
  • Go to Settings → Technical → General Settings → Partner Archive +Propagate.
  • +
  • Enable “Force propagation outside UI” to automatically apply +propagation when partners are archived through automation or imports.
  • +
+
+
+

Usage

+
    +
  1. Open a partner (company or main contact).
  2. +
  3. Click the “Archive Contact and Children” button.
  4. +
  5. Review the list of child contacts to be archived.
  6. +
  7. Confirm the action.
      +
    • Non-contact types (e.g., invoice/delivery addresses) are archived +silently.
    • +
    • Contact-type descendants appear in the wizard for review.
    • +
    +
  8. +
+

Unarchiving also follows propagation rules: if a parent partner is +unarchived, its propagated descendants are unarchived as well.

+
+
+

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.

+
+ +
+
+
+

Authors

+
    +
  • Therp BV
  • +
+
+
+

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.

+

Current maintainer:

+

ntsirintanis

+

This module is part of the OCA/partner-contact project on GitHub.

+

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

+
+
+ + diff --git a/partner_archive_propagate/tests/__init__.py b/partner_archive_propagate/tests/__init__.py new file mode 100644 index 00000000000..1b03810aab1 --- /dev/null +++ b/partner_archive_propagate/tests/__init__.py @@ -0,0 +1 @@ +from . import test_archive diff --git a/partner_archive_propagate/tests/test_archive.py b/partner_archive_propagate/tests/test_archive.py new file mode 100644 index 00000000000..7a1a418581b --- /dev/null +++ b/partner_archive_propagate/tests/test_archive.py @@ -0,0 +1,274 @@ +# Copyright 2025 Therp BV +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). +from odoo.tests.common import TransactionCase + + +class TestPartnerArchivePropagate(TransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + # just sudo everywhere + cls.env = cls.env(su=True) + Partners = cls.env["res.partner"] + # Parent: A (company) + # Children: B(contact), C(delivery), D(invoice), E(child of B, contact) + cls.A = Partners.create({"name": "A", "is_company": True}) + cls.B = Partners.create({"name": "B", "parent_id": cls.A.id, "type": "contact"}) + cls.C = Partners.create( + {"name": "C", "parent_id": cls.A.id, "type": "delivery"} + ) + cls.D = Partners.create({"name": "D", "parent_id": cls.A.id, "type": "invoice"}) + cls.E = Partners.create({"name": "E", "parent_id": cls.B.id, "type": "contact"}) + + def _create_active_user_for_partner(self, partner): + """ + Create an active user linked to given + partner to test unarchivable situations + """ + return ( + self.env["res.users"] + .sudo() + .create( + { + "name": partner.name, + "login": "test@test.test", + "partner_id": partner.id, + "email": "test@tset.org", + } + ) + ) + + def test_archive_non_contact_type(self): + """If there are no type contact descendants, + the wizard should not open and archiving should proceed immediately. + """ + # turn B/E into non-contact types so only non-contacts exist + self.B.write({"type": "delivery"}) + self.E.write({"type": "invoice"}) + # launch wizard from company, but no children are type contact + res = self.A.action_archive_with_contacts() + self.assertTrue(res in (True, None)) + self.assertFalse(self.A.active) + # All descendants of non-contact type should be archived and flagged + for p in (self.B, self.C, self.D, self.E): + self.assertFalse(p.active) + self.assertEqual( + p.propagated_from_id, + self.A, + "Descendant should be archived with propagated_from_id=A", + ) + # Revert types for other tests + self.B.write({"type": "contact"}) + self.E.write({"type": "contact"}) + # Unarchive company, flagged descendants must be restored + self.A.write({"active": True}) + for p in (self.B, self.C, self.D, self.E): + self.assertTrue(p.active) + self.assertFalse( + bool(p.propagated_from_id), + "propagated_from_id should be cleared on unarchive", + ) + + def test_archive_contact_types_skip_active_user(self): + """Wizard opens for type contact children. + Children linked to active users are skipped. + """ + # Ensure we have contact descendants + self.B.write({"type": "contact"}) + self.E.write({"type": "contact"}) + # Block B by linking an active user + self._create_active_user_for_partner(self.B) + action = self.A.action_archive_with_contacts() + self.assertEqual( + action.get("res_model"), "res.partner.archive.propagate.wizard" + ) + # Load wizard and confirm + Wiz = ( + self.env["res.partner.archive.propagate.wizard"] + .sudo() + .browse(action["res_id"]) + ) + # Only archivable contacts should be in the lines + line_partners = Wiz.line_ids.mapped("partner_id") + self.assertIn(self.E, line_partners) + self.assertNotIn(self.B, line_partners) + result = Wiz.action_confirm() + # The window closes, silently + self.assertEqual(result, {"type": "ir.actions.act_window_close"}) + # Parent should be inactive + self.assertFalse(self.A.active) + # B is linked to active user, partner remains active and unflagged + self.assertTrue(self.B.active) + self.assertFalse(bool(self.B.propagated_from_id)) + # E should be archived and flagged from A + self.assertFalse(self.E.active) + self.assertEqual(self.E.propagated_from_id, self.A) + # C and D (type is not contact) archived silently and flagged from A + for p in (self.C, self.D): + self.assertFalse(p.active) + self.assertEqual(p.propagated_from_id, self.A) + # Unarchive parent: flagged ones E/C/D should + # become active, B stays active anyway + self.A.write({"active": True}) + for p in (self.E, self.C, self.D): + self.assertTrue(p.active) + self.assertFalse(bool(p.propagated_from_id)) + self.assertTrue(self.B.active) + self.assertFalse(bool(self.B.propagated_from_id)) + + def test_archive_no_wizard_force_setting_propagates(self): + """When force_outside_ui is enabled, + all operations except wizard stuff can archive children + """ + icp = self.env["ir.config_parameter"].sudo() + icp.set_param("partner_archive_propagate.force_outside_ui", "1") + # Ensure clean active state + for p in (self.A, self.B, self.C, self.D, self.E): + p.write({"active": True, "propagated_from_id": False}) + # Archive without UI + self.A.write({"active": False}) + self.assertFalse(self.A.active) + for p in (self.B, self.C, self.D, self.E): + self.assertFalse(p.active) + self.assertEqual(p.propagated_from_id, self.A) + # Unarchive parent restores flagged children + self.A.write({"active": True}) + for p in (self.B, self.C, self.D, self.E): + self.assertTrue(p.active) + self.assertFalse(bool(p.propagated_from_id)) + # Reset the setting + icp.set_param("partner_archive_propagate.force_outside_ui", "0") + + def test_manual_archive_does_not_toggle_flag(self): + """ + Archiving a partner manually should NOT toggle + propagated_from_id on that record. + """ + # Ensure clean + self.B.write({"active": True, "propagated_from_id": False}) + # Manual archive of B, force_outside_ui is off + self.env["ir.config_parameter"].sudo().set_param( + "partner_archive_propagate.force_outside_ui", "0" + ) + self.B.write({"active": False}) + self.assertFalse(self.B.active) + self.assertFalse(bool(self.B.propagated_from_id)) + # Manual unarchive keeps it False + self.B.write({"active": True}) + self.assertTrue(self.B.active) + self.assertFalse(bool(self.B.propagated_from_id)) + + def test_notify_skipped_partners_posts_message(self): + """_notify_skipped_partners should post a message listing skipped partners.""" + before = len(self.A.message_ids) + # Calling with empty recordset should do exactly nothing + empty_partners = self.env["res.partner"] + self.A._notify_skipped_partners(empty_partners) + self.assertEqual( + len(self.A.message_ids), + before, + ) + # Calling with some skipped partners should create exactly one message + skipped = self.B | self.E + self.A._notify_skipped_partners(skipped) + self.assertEqual( + len(self.A.message_ids), + before + 1, + ) + msg = self.A.message_ids.sorted("id")[-1] + self.assertIn( + "Skipped archiving the following contacts", + msg.body, + ) + self.assertIn(self.B.name, msg.body) + self.assertIn(self.E.name, msg.body) + + def test_wizard_confirm_mixed_notification(self): + """action_confirm should use split_archivable_unarchivable_user and + return a display_notification when there are unarchivable partners. + """ + self.B.write({"type": "contact", "active": True, "propagated_from_id": False}) + self.E.write({"type": "contact", "active": True, "propagated_from_id": False}) + self._create_active_user_for_partner(self.B) + Wiz = ( + self.env["res.partner.archive.propagate.wizard"] + .sudo() + .create( + { + "partner_id": self.A.id, + "line_ids": [ + (0, 0, {"partner_id": self.B.id}), + (0, 0, {"partner_id": self.E.id}), + ], + } + ) + ) + + result = Wiz.action_confirm() + # Parent should be archived by the wizard + self.assertFalse(self.A.active) + # E must be archived and flagged from A + self.assertFalse(self.E.active) + self.assertEqual(self.E.propagated_from_id, self.A) + # B must stay active and unflagged + self.assertTrue(self.B.active) + self.assertFalse(bool(self.B.propagated_from_id)) + # Result must be a display_notification about skipped contacts + self.assertIsInstance(result, dict) + self.assertEqual(result.get("type"), "ir.actions.client") + self.assertEqual(result.get("tag"), "display_notification") + params = result.get("params", {}) + self.assertIn("message", params) + self.assertIn("Skipped contacts linked to active users", params["message"]) + # for partner B + self.assertIn(self.B.name, params["message"]) + + def test_wizard_deleted_line_not_archived(self): + self.B.write({"type": "contact", "active": True, "propagated_from_id": False}) + self.E.write({"type": "contact", "active": True, "propagated_from_id": False}) + self.A.write({"active": True}) + # Simulate user removing B from the list: only E remains in line_ids. + Wiz = ( + self.env["res.partner.archive.propagate.wizard"] + .sudo() + .create( + { + "partner_id": self.A.id, + "line_ids": [(0, 0, {"partner_id": self.E.id})], + } + ) + ) + result = Wiz.action_confirm() + self.assertEqual(result, {"type": "ir.actions.act_window_close"}) + # Organisation must be archived + self.assertFalse(self.A.active) + # E was in the list and must be archived + self.assertFalse(self.E.active) + self.assertEqual(self.E.propagated_from_id, self.A) + # B was removed from the list and must remain as it was + self.assertTrue(self.B.active) + self.assertFalse(bool(self.B.propagated_from_id)) + + def test_unarchive_skips_already_active_partners(self): + """When writing active=True on a mixed recordset, only currently + inactive partners are processed — the filtered() branch is exercised.""" + p1 = self.env["res.partner"].create({"name": "P1 parent"}) + p2 = self.env["res.partner"].create({"name": "P2 child"}) + # Archive both, p2 flagged as propagated from p1 + p1.write({"active": False}) + p2.write({"active": False, "propagated_from_id": p1.id}) + + # p3 is a red herring — already active, should be skipped by filtered() + p3 = self.env["res.partner"].create({"name": "P3 already active"}) + + # Write active=True on a mix: p3 (already active) + p1 (inactive) + # filtered() produces only p1 -> _unarchive_propagate runs on p1 -> restores p2 + (p3 | p1).write({"active": True}) + + p1.invalidate_recordset() + p2.invalidate_recordset() + self.assertTrue(p1.active) + self.assertTrue(p2.active) + self.assertFalse(bool(p2.propagated_from_id)) + # p3 remains active and untouched + self.assertTrue(p3.active) diff --git a/partner_archive_propagate/views/res_config_settings_views.xml b/partner_archive_propagate/views/res_config_settings_views.xml new file mode 100644 index 00000000000..ac0ecd6fa85 --- /dev/null +++ b/partner_archive_propagate/views/res_config_settings_views.xml @@ -0,0 +1,17 @@ + + + res.config.settings.view.form.partner.archive + res.config.settings + + + + + + + + + + diff --git a/partner_archive_propagate/views/res_partner_views.xml b/partner_archive_propagate/views/res_partner_views.xml new file mode 100644 index 00000000000..ac4b38f97e2 --- /dev/null +++ b/partner_archive_propagate/views/res_partner_views.xml @@ -0,0 +1,21 @@ + + + res.partner.form.archive.propagate + res.partner + + + +
+ +
+
+
+
+
diff --git a/partner_archive_propagate/wizard/__init__.py b/partner_archive_propagate/wizard/__init__.py new file mode 100644 index 00000000000..c888ea59064 --- /dev/null +++ b/partner_archive_propagate/wizard/__init__.py @@ -0,0 +1 @@ +from . import archive_propagate_wizard diff --git a/partner_archive_propagate/wizard/archive_propagate_wizard.py b/partner_archive_propagate/wizard/archive_propagate_wizard.py new file mode 100644 index 00000000000..f7ddcd17546 --- /dev/null +++ b/partner_archive_propagate/wizard/archive_propagate_wizard.py @@ -0,0 +1,64 @@ +# Copyright 2025 Therp BV +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +from odoo import _, fields, models + + +class ArchivePropagateWizard(models.TransientModel): + _name = "res.partner.archive.propagate.wizard" + _description = "Archive partner and children of partner " + + partner_id = fields.Many2one("res.partner", required=True, readonly=True) + line_ids = fields.One2many( + "res.partner.archive.propagate.line", "wizard_id", string="Contacts to archive" + ) + + def action_confirm(self): + """Archive main partner from the ui: + - silently archive NON-contact type descendants (invoice/delivery/etc) + - skip descendants linked to active users and notify + """ + self.ensure_one() + self.partner_id.with_context(partner_archive_propagate_ui=True).write( + {"active": False} + ) + partners = ( + self.line_ids.mapped("partner_id").sudo().filtered(lambda p: p.active) + ) + archivable, unarchivable = partners._split_archivable_unarchivable_user() + archivable.write( + { + "active": False, + "propagated_from_id": self.partner_id.id, + } + ) + self.partner_id._notify_skipped_partners(unarchivable) + if unarchivable: + # return a nice message on ui too + return { + "type": "ir.actions.client", + "tag": "display_notification", + "params": { + "title": _("Some contacts were skipped"), + "message": _("Skipped contacts linked to active users: %s") + % ", ".join(unarchivable.mapped("name")), + "type": "warning", + "sticky": False, + }, + } + return {"type": "ir.actions.act_window_close"} + + +class ArchiveLine(models.TransientModel): + _name = "res.partner.archive.propagate.line" + _description = "Partner Archive Propagate Line" + + wizard_id = fields.Many2one( + "res.partner.archive.propagate.wizard", + required=True, + ondelete="cascade", + ) + partner_id = fields.Many2one("res.partner", required=True, string="Contact") + name = fields.Char(related="partner_id.name") + email = fields.Char(related="partner_id.email") + phone = fields.Char(related="partner_id.phone") diff --git a/partner_archive_propagate/wizard/archive_propagate_wizard_views.xml b/partner_archive_propagate/wizard/archive_propagate_wizard_views.xml new file mode 100644 index 00000000000..8cf751ec7af --- /dev/null +++ b/partner_archive_propagate/wizard/archive_propagate_wizard_views.xml @@ -0,0 +1,39 @@ + + + res.partner.archive.propagate.wizard.form + res.partner.archive.propagate.wizard + +
+ + + + + + + + +
+
+
+