From 4f170182a1359ecf73195ce76f6f52710f75465f Mon Sep 17 00:00:00 2001 From: Arnau Date: Tue, 16 Sep 2025 17:50:22 +0200 Subject: [PATCH] [IMP] base_tier_validation: Added password confirmation --- base_tier_validation/README.rst | 6 +- base_tier_validation/__manifest__.py | 1 + .../models/tier_definition.py | 4 ++ base_tier_validation/models/tier_review.py | 4 ++ .../models/tier_validation.py | 33 ++++++++++ .../security/ir.model.access.csv | 1 + .../static/description/index.html | 62 ++++++++--------- .../tests/test_tier_validation.py | 66 +++++++++++++++++++ .../views/tier_definition_view.xml | 2 + base_tier_validation/wizard/__init__.py | 1 + base_tier_validation/wizard/comment_wizard.py | 17 +++++ .../wizard/password_wizard.py | 36 ++++++++++ .../wizard/password_wizard_view.xml | 24 +++++++ 13 files changed, 218 insertions(+), 39 deletions(-) create mode 100644 base_tier_validation/wizard/password_wizard.py create mode 100644 base_tier_validation/wizard/password_wizard_view.xml diff --git a/base_tier_validation/README.rst b/base_tier_validation/README.rst index 9e7da43fb8..a5e3c2f1b8 100644 --- a/base_tier_validation/README.rst +++ b/base_tier_validation/README.rst @@ -1,7 +1,3 @@ -.. image:: https://odoo-community.org/readme-banner-image - :target: https://odoo-community.org/get-involved?utm_source=readme - :alt: Odoo Community Association - ==================== Base Tier Validation ==================== @@ -17,7 +13,7 @@ Base Tier Validation .. |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 +.. |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%2Fserver--ux-lightgray.png?logo=github diff --git a/base_tier_validation/__manifest__.py b/base_tier_validation/__manifest__.py index 588e7a61dd..56ddea6ab8 100644 --- a/base_tier_validation/__manifest__.py +++ b/base_tier_validation/__manifest__.py @@ -23,6 +23,7 @@ "views/tier_review_view.xml", "views/tier_validation_exception_view.xml", "wizard/comment_wizard_view.xml", + "wizard/password_wizard_view.xml", "templates/tier_validation_templates.xml", ], "assets": { diff --git a/base_tier_validation/models/tier_definition.py b/base_tier_validation/models/tier_definition.py index 5f5d221774..c0d2110c39 100644 --- a/base_tier_validation/models/tier_definition.py +++ b/base_tier_validation/models/tier_definition.py @@ -108,6 +108,10 @@ def _get_tier_validation_model_names(self): help="Bypassed (auto validated), if previous tier was validated " "by same reviewer", ) + require_password = fields.Boolean( + help="If checked, the user will be asked to enter " + "the password to validate the tier.", + ) @api.onchange("review_type") def onchange_review_type(self): diff --git a/base_tier_validation/models/tier_review.py b/base_tier_validation/models/tier_review.py index cd5dc3bf4f..3af56da5e9 100644 --- a/base_tier_validation/models/tier_review.py +++ b/base_tier_validation/models/tier_review.py @@ -68,6 +68,10 @@ class TierReview(models.Model): related="definition_id.approve_sequence_bypass" ) last_reminder_date = fields.Datetime(readonly=True) + require_password = fields.Boolean( + related="definition_id.require_password", readonly=True + ) + password_confirmed = fields.Boolean() @api.depends("status") def _compute_display_status(self): diff --git a/base_tier_validation/models/tier_validation.py b/base_tier_validation/models/tier_validation.py index 6479bb5bff..042ab76384 100644 --- a/base_tier_validation/models/tier_validation.py +++ b/base_tier_validation/models/tier_validation.py @@ -79,6 +79,14 @@ class TierValidation(models.AbstractModel): ) next_review = fields.Char(compute="_compute_next_review") hide_reviews = fields.Boolean(compute="_compute_hide_reviews") + require_password = fields.Boolean(compute="_compute_require_password") + + def _compute_require_password(self): + for rec in self: + require_password = rec.review_ids.filtered( + lambda r: r.status == "pending" and (self.env.user in r.reviewer_ids) + ).mapped("require_password") + rec.require_password = True in require_password def _compute_has_comment(self): for rec in self: @@ -623,6 +631,24 @@ def _add_comment(self, validate_reject, reviews): }, } + def _confirm_password(self, validate_reject, reviews): + wizard = self.env.ref("base_tier_validation.view_password_confirm_wizard") + return { + "name": self.env._("Password Confirmation"), + "type": "ir.actions.act_window", + "view_mode": "form", + "res_model": "password.wizard", + "views": [(wizard.id, "form")], + "view_id": wizard.id, + "target": "new", + "context": { + "default_res_id": self.id, + "default_res_model": self._name, + "default_review_ids": reviews.ids, + "default_validate_reject": validate_reject, + }, + } + def validate_tier(self): self.ensure_one() sequences = self._get_sequences_to_approve(self.env.user) @@ -634,6 +660,11 @@ def validate_tier(self): lambda r: r.status == "pending" and (self.env.user in r.reviewer_ids) ) return self._add_comment("validate", user_reviews) + elif self.require_password: + user_reviews = reviews.filtered( + lambda r: r.status == "pending" and (self.env.user in r.reviewer_ids) + ) + return self._confirm_password("validate", user_reviews) self._validate_tier(reviews) self._update_counter({"review_deleted": True}) @@ -643,6 +674,8 @@ def reject_tier(self): reviews = self.review_ids.filtered(lambda x: x.sequence in sequences) if self.has_comment: return self._add_comment("reject", reviews) + elif self.require_password: + return self._confirm_password("reject", reviews) self._rejected_tier(reviews) self._update_counter({"review_deleted": True}) diff --git a/base_tier_validation/security/ir.model.access.csv b/base_tier_validation/security/ir.model.access.csv index 82c76c65ba..6e68f5518b 100644 --- a/base_tier_validation/security/ir.model.access.csv +++ b/base_tier_validation/security/ir.model.access.csv @@ -7,5 +7,6 @@ access_tier_review,access.tier.review,model_tier_review,base.group_user,1,1,1,1 access_tier_definition_all,tier.definition.all,model_tier_definition,base.group_user,1,0,0,0 access_tier_definition_settings,tier.definition.settings,model_tier_definition,base.group_erp_manager,1,1,1,1 access_comment_wizard,access.comment.wizard,model_comment_wizard,base.group_user,1,1,1,1 +access_password_wizard,access.password.wizard,model_password_wizard,base.group_user,1,1,1,1 access_tier_validation_exceptions_all,tier.validation.exceptions,model_tier_validation_exception,base.group_user,1,0,0,0 access_tier_validation_exceptions_settings,tier.validation.exceptions,model_tier_validation_exception,base.group_system,1,1,1,1 diff --git a/base_tier_validation/static/description/index.html b/base_tier_validation/static/description/index.html index a9c8064f6d..b1d5d01e8c 100644 --- a/base_tier_validation/static/description/index.html +++ b/base_tier_validation/static/description/index.html @@ -3,7 +3,7 @@ -README.rst +Base Tier Validation -
+
+

Base Tier Validation

- - -Odoo Community Association - -
-

Base Tier Validation

-

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

+

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

Validating some operations is a common need across different areas in a company and sometimes it also involves several people and stages in the process. With this module you will be able to define your custom @@ -426,7 +421,7 @@

Base Tier Validation

-

Configuration

+

Configuration

To configure this module, you need to:

  1. Go to Settings > Technical > Tier Validations > Tier Definition.
  2. @@ -470,7 +465,7 @@

    Configuration

-

Known issues / Roadmap

+

Known issues / Roadmap

This is the list of known issues for this module. Any proposal for improvement will be very valuable.

-

Changelog

+

Changelog

-

17.0.1.0.0 (2024-01-10)

+

17.0.1.0.0 (2024-01-10)

Migrated to Odoo 17. Merged module with tier_validation_waiting. To support sending messages in a validation sequence when it is their turn to validate.

-

13.0.1.2.2 (2020-08-30)

+

13.0.1.2.2 (2020-08-30)

Fixes:

  • When using approve_sequence option in any tier.definition there can be @@ -515,7 +510,7 @@

    13.0.1.2.2 (2020-08-30)

-

12.0.3.3.1 (2019-12-02)

+

12.0.3.3.1 (2019-12-02)

Fixes:

-

12.0.3.3.0 (2019-11-27)

+

12.0.3.3.0 (2019-11-27)

New features:

-

12.0.3.2.1 (2019-11-26)

+

12.0.3.2.1 (2019-11-26)

Fixes:

  • Remove message_subscribe_users
-

12.0.3.2.0 (2019-11-25)

+

12.0.3.2.0 (2019-11-25)

New features:

  • Notify reviewers
-

12.0.3.0.0 (2019-12-02)

+

12.0.3.0.0 (2019-12-02)

Fixes:

  • Edit Reviews Table
-

12.0.2.1.0 (2019-05-29)

+

12.0.2.1.0 (2019-05-29)

Fixes:

  • Edit drop-down style width and position
-

12.0.2.0.0 (2019-05-28)

+

12.0.2.0.0 (2019-05-28)

New features:

-

Bug Tracker

+

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 @@ -599,15 +594,15 @@

Bug Tracker

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

-

Credits

+

Credits

-

Authors

+

Authors

  • ForgeFlow
-

Contributors

+

Contributors

-

Other credits

+

Other credits

The migration of this module from 17.0 to 18.0 was financially supported by Camptocamp.

-

Maintainers

+

Maintainers

This module is maintained by the OCA.

Odoo Community Association @@ -652,6 +647,5 @@

Maintainers

-
diff --git a/base_tier_validation/tests/test_tier_validation.py b/base_tier_validation/tests/test_tier_validation.py index 26e228a3f7..7b53f2ea6a 100644 --- a/base_tier_validation/tests/test_tier_validation.py +++ b/base_tier_validation/tests/test_tier_validation.py @@ -1202,6 +1202,72 @@ def test_31_request_validation(self): self.test_user_3_multi_company.partner_id, followers.mapped("partner_id") ) + def test_32_add_comment_and_confirm_password(self): + # Set user password for validation + self.test_user_1.password = "test_user_1" + + # Create new test record + test_record = self.test_model.create({"test_field": 2.5}) + + # Create tier definition with comment + password required + self.tier_def_obj.create( + { + "model_id": self.tester_model.id, + "review_type": "individual", + "reviewer_id": self.test_user_1.id, + "definition_domain": "[('test_field', '>', 1.0)]", + "has_comment": True, + "require_password": True, + } + ) + + # Request validation + review = test_record.with_user(self.test_user_2).request_validation() + self.assertTrue(review) + + # Comment Wizard + record = test_record.with_user(self.test_user_1) + res = record.reject_tier() + ctx = res["context"] + + comment_wizard = Form( + self.env["comment.wizard"].with_user(self.test_user_1).with_context(**ctx) + ) + comment_wizard.comment = "Test Comment" + res = comment_wizard.save().add_comment() + + # Password confirmation wizard + pw_ctx = res["context"] + pw_wizard = Form( + self.env["password.wizard"] + .with_user(self.test_user_1) + .with_context(**pw_ctx) + ) + + # Wrong password should fail + pw_wizard.password = "wrong_password" + pw_wiz = pw_wizard.save() + with self.assertRaises(ValidationError): + pw_wiz.confirm_password() + + # Correct password should pass + pw_wizard.password = "test_user_1" + pw_wiz = pw_wizard.save() + pw_wiz.confirm_password() + + # Ensure review has comment + self.assertTrue(test_record.review_ids.filtered("comment")) + + # Check notifications + accepted_msg = test_record.with_user( + self.test_user_1 + )._notify_accepted_reviews_body() + rejected_msg = test_record.with_user( + self.test_user_1 + )._notify_rejected_review_body() + self.assertEqual(accepted_msg, "A review was accepted. (Test Comment)") + self.assertEqual(rejected_msg, "A review was rejected by John. (Test Comment)") + @tagged("at_install") class TierTierValidationView(CommonTierValidation): diff --git a/base_tier_validation/views/tier_definition_view.xml b/base_tier_validation/views/tier_definition_view.xml index a93765cfff..5afa54406d 100644 --- a/base_tier_validation/views/tier_definition_view.xml +++ b/base_tier_validation/views/tier_definition_view.xml @@ -17,6 +17,7 @@ + + diff --git a/base_tier_validation/wizard/__init__.py b/base_tier_validation/wizard/__init__.py index 92bd0df50b..712d7a5a8b 100644 --- a/base_tier_validation/wizard/__init__.py +++ b/base_tier_validation/wizard/__init__.py @@ -1,3 +1,4 @@ # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). from . import comment_wizard +from . import password_wizard diff --git a/base_tier_validation/wizard/comment_wizard.py b/base_tier_validation/wizard/comment_wizard.py index 5c5a6c933d..516ac3ee93 100644 --- a/base_tier_validation/wizard/comment_wizard.py +++ b/base_tier_validation/wizard/comment_wizard.py @@ -18,8 +18,25 @@ def add_comment(self): self.ensure_one() rec = self.env[self.res_model].browse(self.res_id) self.review_ids.write({"comment": self.comment}) + if self.review_ids.require_password: + return self._confirm_password() if self.validate_reject == "validate": rec._validate_tier(self.review_ids) if self.validate_reject == "reject": rec._rejected_tier(self.review_ids) rec._update_counter({"review_deleted": True}) + + def _confirm_password(self): + return { + "name": "Password Confirmation", + "type": "ir.actions.act_window", + "res_model": "password.wizard", + "view_mode": "form", + "target": "new", + "context": { + "default_validate_reject": self.validate_reject, + "default_res_model": self.res_model, + "default_res_id": self.res_id, + "default_review_ids": [(6, 0, self.review_ids.ids)], + }, + } diff --git a/base_tier_validation/wizard/password_wizard.py b/base_tier_validation/wizard/password_wizard.py new file mode 100644 index 0000000000..6fd799eafe --- /dev/null +++ b/base_tier_validation/wizard/password_wizard.py @@ -0,0 +1,36 @@ +# Copyright 2025 ForgeFlow S.L. (https://www.forgeflow.com) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import _, fields, models +from odoo.exceptions import AccessDenied, ValidationError + + +class PasswordWizard(models.TransientModel): + _name = "password.wizard" + _description = "Password Wizard" + + validate_reject = fields.Char() + res_model = fields.Char() + res_id = fields.Integer() + review_ids = fields.Many2many(comodel_name="tier.review") + password = fields.Char(required=True) + + def confirm_password(self): + self.ensure_one() + user = self.env.user + try: + credentials = { + "login": user.login, + "password": self.password, + "type": "password", + } + user._check_credentials(credentials, {"interactive": True}) + except AccessDenied as e: + raise ValidationError(_("Incorrect password. Please try again.")) from e + rec = self.env[self.res_model].browse(self.res_id) + self.review_ids.write({"password_confirmed": True}) + if self.validate_reject == "validate": + rec._validate_tier(self.review_ids) + if self.validate_reject == "reject": + rec._rejected_tier(self.review_ids) + rec._update_counter({"review_deleted": True}) diff --git a/base_tier_validation/wizard/password_wizard_view.xml b/base_tier_validation/wizard/password_wizard_view.xml new file mode 100644 index 0000000000..337fce9cea --- /dev/null +++ b/base_tier_validation/wizard/password_wizard_view.xml @@ -0,0 +1,24 @@ + + + + Password Wizard + password.wizard + form + +
+ + + + +
+
+
+