diff --git a/server_action_mass_edit/README.rst b/server_action_mass_edit/README.rst index f1f3113e09..133d82b8a2 100644 --- a/server_action_mass_edit/README.rst +++ b/server_action_mass_edit/README.rst @@ -133,7 +133,9 @@ Contributors * Jairo Llopis * Víctor Martínez + * Carlos Dauden * Tatiana Deribina +* Hieu, Vo Minh Bao Maintainers ~~~~~~~~~~~ diff --git a/server_action_mass_edit/demo/mass_editing.xml b/server_action_mass_edit/demo/mass_editing.xml index 7587bd2ed8..fbfe474aed 100644 --- a/server_action_mass_edit/demo/mass_editing.xml +++ b/server_action_mass_edit/demo/mass_editing.xml @@ -72,8 +72,28 @@ License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + + + mass_edit + Mass Edit + + + + + + + + + + + + + + - + diff --git a/server_action_mass_edit/readme/CONTRIBUTORS.rst b/server_action_mass_edit/readme/CONTRIBUTORS.rst index ce841adced..9a87fc6b77 100644 --- a/server_action_mass_edit/readme/CONTRIBUTORS.rst +++ b/server_action_mass_edit/readme/CONTRIBUTORS.rst @@ -11,4 +11,6 @@ * Jairo Llopis * Víctor Martínez + * Carlos Dauden * Tatiana Deribina +* Hieu, Vo Minh Bao diff --git a/server_action_mass_edit/static/description/index.html b/server_action_mass_edit/static/description/index.html index e97c8d1f41..1a7779a0c5 100644 --- a/server_action_mass_edit/static/description/index.html +++ b/server_action_mass_edit/static/description/index.html @@ -8,10 +8,11 @@ /* :Author: David Goodger (goodger@python.org) -:Id: $Id: html4css1.css 8954 2022-01-20 10:10:25Z milde $ +:Id: $Id: html4css1.css 9511 2024-01-13 09:50:07Z milde $ :Copyright: This stylesheet has been placed in the public domain. Default cascading style sheet for the HTML output of Docutils. +Despite the name, some widely supported CSS2 features are used. See https://docutils.sourceforge.io/docs/howto/html-stylesheets.html for how to customize this style sheet. @@ -274,7 +275,7 @@ margin-left: 2em ; margin-right: 2em } -pre.code .ln { color: grey; } /* line numbers */ +pre.code .ln { color: gray; } /* line numbers */ pre.code, code { background-color: #eeeeee } pre.code .comment, code .comment { color: #5C6576 } pre.code .keyword, code .keyword { color: #3B0D06; font-weight: bold } @@ -300,7 +301,7 @@ span.pre { white-space: pre } -span.problematic { +span.problematic, pre.problematic { color: red } span.section-subtitle { @@ -472,15 +473,19 @@

Contributors

  • Tecnativa
    • Jairo Llopis
    • Víctor Martínez
    • +
    • Carlos Dauden
  • Tatiana Deribina <tatiana.deribina@spritnit.fi>
  • +
  • Hieu, Vo Minh Bao <hieu.vmb@komit-consulting.com>
  • Maintainers

    This module is maintained by the OCA.

    -Odoo Community Association + +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.

    diff --git a/server_action_mass_edit/tests/test_mass_editing.py b/server_action_mass_edit/tests/test_mass_editing.py index 4d12de1263..2fdf770f1b 100644 --- a/server_action_mass_edit/tests/test_mass_editing.py +++ b/server_action_mass_edit/tests/test_mass_editing.py @@ -5,7 +5,7 @@ from ast import literal_eval -from odoo.exceptions import ValidationError +from odoo.exceptions import UserError, ValidationError from odoo.tests import Form, common, new_test_user from odoo.addons.base.models.ir_actions import IrActionsServer @@ -27,12 +27,16 @@ def setUp(self): self.MassEditingWizard = self.env["mass.editing.wizard"] self.ResPartnerTitle = self.env["res.partner.title"] + self.ResPartner = self.env["res.partner"] self.ResLang = self.env["res.lang"] self.IrActionsActWindow = self.env["ir.actions.act_window"] self.mass_editing_user = self.env.ref( "server_action_mass_edit.mass_editing_user" ) + self.mass_editing_partner = self.env.ref( + "server_action_mass_edit.mass_editing_partner" + ) self.mass_editing_partner_title = self.env.ref( "server_action_mass_edit.mass_editing_partner_title" ) @@ -47,6 +51,7 @@ def setUp(self): groups="base.group_system", ) self.partner_title = self._create_partner_title() + self.invoice_partner = self._create_invoice_partner() def _create_partner_title(self): """Create a Partner Title.""" @@ -62,6 +67,14 @@ def _create_partner_title(self): ) return partner_title + def _create_invoice_partner(self): + invoice_partner = self.ResPartner.create( + { + "type": "invoice", + } + ) + return invoice_partner + def _create_wizard_and_apply_values(self, server_action, items, vals): action = server_action.with_context( active_model=items._name, @@ -405,3 +418,38 @@ def test_onchange_model_id(self): result, None, ) + + def test_mass_edit_partner_user_error(self): + vals = { + "selection__parent_id": "set", + "parent_id": self.invoice_partner.id, + "write_record_by_record": True, + } + action = self.mass_editing_partner.with_context( + active_model=self.invoice_partner._name, + active_ids=self.invoice_partner.ids, + ).run() + try: + self.env[action["res_model"]].with_context( + **literal_eval(action["context"]), + ).create(vals) + except Exception as e: + self.assertEqual(type(e), UserError) + + def test_mass_edit_partner_sql_error(self): + vals = { + "selection__type": "set", + "type": "contact", + "write_record_by_record": True, + "selection__name": "remove", + } + action = self.mass_editing_partner.with_context( + active_model=self.invoice_partner._name, + active_ids=self.invoice_partner.ids, + ).run() + try: + self.env[action["res_model"]].with_context( + **literal_eval(action["context"]), + ).create(vals) + except Exception as e: + self.assertEqual(type(e), UserError) diff --git a/server_action_mass_edit/wizard/mass_editing_wizard.py b/server_action_mass_edit/wizard/mass_editing_wizard.py index bc0f688ab4..f9567d4853 100644 --- a/server_action_mass_edit/wizard/mass_editing_wizard.py +++ b/server_action_mass_edit/wizard/mass_editing_wizard.py @@ -3,10 +3,19 @@ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). import json +import logging from lxml import etree +from psycopg2 import IntegrityError from odoo import _, api, fields, models +from odoo.exceptions import ( + AccessDenied, + AccessError, + MissingError, + UserError, + ValidationError, +) from odoo.addons.base.models.ir_ui_view import ( transfer_modifiers_to_node, @@ -24,6 +33,12 @@ class MassEditingWizard(models.TransientModel): operation_description_warning = fields.Text(readonly=True) operation_description_danger = fields.Text(readonly=True) message = fields.Text(readonly=True) + write_record_by_record = fields.Boolean( + help="This option will write the records one by one, instead of all at once.\n" + "This is useful when you are editing a lot of records and one of the " + "records raises an error. \n With this option, the error message will be " + "more specific to facillitate the undertanding of the error." + ) @api.model def default_get(self, fields, active_ids=None): @@ -243,12 +258,15 @@ def _clean_check_company_field_domain(self, TargetModel, field, field_info): return field_info @api.model_create_multi - def create(self, vals_list): + def create(self, vals_list): # noqa: C901 server_action_id = self.env.context.get("server_action_id") server_action = self.env["ir.actions.server"].sudo().browse(server_action_id) active_ids = self.env.context.get("active_ids", []) if server_action and active_ids: + TargetModel = self.env[server_action.model_id.model] for vals in vals_list: + write_record_by_record = vals.pop("write_record_by_record", False) + logging.warning("write_record_by_record: %s", write_record_by_record) values = {} for key, val in vals.items(): if key.startswith("selection_"): @@ -281,9 +299,44 @@ def create(self, vals_list): values.update({split_key: m2m_list}) if values: - self.env[server_action.model_id.model].browse( - active_ids - ).with_context(mass_edit=True,).write(values) + target_records = TargetModel.browse(active_ids) + if write_record_by_record: + for target_record in target_records: + try: + target_record.with_context(mass_edit=True).write(values) + except ( + AccessDenied, + AccessError, + MissingError, + UserError, + ValidationError, + IntegrityError, + ) as oe: + if isinstance(oe, IntegrityError): + sql_error_msg_dict = ( + models.convert_pgerror_constraint( + self.env[TargetModel._name], + False, + False, + oe, + ) + ) + sql_error_message = sql_error_msg_dict.get( + "message", "" + ) + oe = Exception(sql_error_message) + raise UserError( + _( + 'Failed to process the %(model_name)s "%(name)s" ' + "[id: %(id)s]:\n\n%(ue)s", + model_name=server_action.model_id.name, + name=target_record.display_name, + id=target_record.id, + ue=str(oe), + ) + ) from oe + else: + target_records.with_context(mass_edit=True).write(values) return super().create([{}]) def _prepare_create_values(self, vals_list): diff --git a/server_action_mass_edit/wizard/mass_editing_wizard.xml b/server_action_mass_edit/wizard/mass_editing_wizard.xml index f5b8ca6b21..1fbddf21ae 100644 --- a/server_action_mass_edit/wizard/mass_editing_wizard.xml +++ b/server_action_mass_edit/wizard/mass_editing_wizard.xml @@ -50,6 +50,8 @@ License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). type="object" class="oe_highlight" /> +