diff --git a/base_tier_validation/models/tier_definition.py b/base_tier_validation/models/tier_definition.py
index b36a82f6..886a3d44 100644
--- a/base_tier_validation/models/tier_definition.py
+++ b/base_tier_validation/models/tier_definition.py
@@ -105,6 +105,12 @@ def _get_tier_validation_model_names(self):
default=False,
help="Approval order by the specified sequence number",
)
+ allow_bulk_approve = fields.Boolean(
+ default=False,
+ help="When set, reviewers can approve several reviews at once from the "
+ "tier review list view. Reviews whose definition does not have this "
+ "flag are skipped from a bulk selection.",
+ )
approve_sequence_bypass = fields.Boolean(
help="Bypassed (auto validated), if previous tier was validated "
"by same reviewer",
diff --git a/base_tier_validation/models/tier_review.py b/base_tier_validation/models/tier_review.py
index bed4caaf..1e53b5c4 100644
--- a/base_tier_validation/models/tier_review.py
+++ b/base_tier_validation/models/tier_review.py
@@ -67,6 +67,7 @@ class TierReview(models.Model):
approve_sequence_bypass = fields.Boolean(
related="definition_id.approve_sequence_bypass"
)
+ allow_bulk_approve = fields.Boolean(related="definition_id.allow_bulk_approve")
last_reminder_date = fields.Datetime(readonly=True)
@api.depends("status")
@@ -212,3 +213,42 @@ def _schedule_review_reminder_activity(self, record):
note=self._notify_review_reminder_body(),
user_id=self.reviewer_ids.id,
)
+
+ def _filter_bulk_validatable(self):
+ """Return the subset of self that the current user may bulk-approve.
+
+ A review is eligible when its definition opts in via
+ ``allow_bulk_approve``, the review is pending and assigned to the
+ current user, the user is the reviewer-of-the-moment (``can_review``),
+ and the definition does not require a per-review comment (which
+ cannot be collected in a bulk action).
+ """
+ return self.filtered(
+ lambda r: r.allow_bulk_approve
+ and r.status == "pending"
+ and r.can_review
+ and self.env.user in r.reviewer_ids
+ and not r.has_comment
+ )
+
+ def action_bulk_validate(self):
+ """Approve every eligible review in ``self`` in a single call.
+
+ Reviews are grouped by their underlying record so that each record's
+ own ``_validate_tier`` runs once -- preserving notifications, the
+ approve-sequence promotion logic, and the review counter update.
+ Ineligible reviews are skipped silently; the action is meant to be
+ wired to the list view header.
+ """
+ eligible = self._filter_bulk_validatable()
+ if not eligible:
+ return False
+ by_key = {}
+ for review in eligible:
+ by_key.setdefault((review.model, review.res_id), self.browse())
+ by_key[(review.model, review.res_id)] |= review
+ for (model, res_id), reviews in by_key.items():
+ record = self.env[model].browse(res_id)
+ record._validate_tier(reviews)
+ record._update_counter({"review_deleted": True})
+ return True
diff --git a/base_tier_validation/readme/CONFIGURE.md b/base_tier_validation/readme/CONFIGURE.md
index 4bd817e6..944fc755 100644
--- a/base_tier_validation/readme/CONFIGURE.md
+++ b/base_tier_validation/readme/CONFIGURE.md
@@ -15,6 +15,9 @@ To configure this module, you need to:
Reject.
- If check *Approve by sequence*, reviewers is forced to review by
specified sequence.
+- If check *Allow Bulk Approve*, reviewers can validate several reviews at
+ once from the Tier Review list. Reviews on definitions without this flag
+ are skipped from a bulk selection.
To configure Tier Validation Exceptions, you need to:
diff --git a/base_tier_validation/tests/test_tier_validation.py b/base_tier_validation/tests/test_tier_validation.py
index 344004fd..5b425edd 100644
--- a/base_tier_validation/tests/test_tier_validation.py
+++ b/base_tier_validation/tests/test_tier_validation.py
@@ -1457,6 +1457,52 @@ def test_34_test_duplicate_new_user_should_not_have_review_ids(self):
@tagged("at_install")
class TierTierValidationView(CommonTierValidation):
+ def test_bulk_validate_approves_eligible_reviews(self):
+ """Reviews on definitions with allow_bulk_approve=True get approved;
+ those without are left untouched."""
+ self.definition_1.allow_bulk_approve = True
+ rec_a = self.test_model.create({"test_field": 1.0})
+ rec_b = self.test_model.create({"test_field": 1.0})
+ rec_a.with_user(self.test_user_2).request_validation()
+ rec_b.with_user(self.test_user_2).request_validation()
+ reviews = (rec_a.review_ids | rec_b.review_ids).with_user(self.test_user_1)
+ self.assertEqual(set(reviews.mapped("status")), {"pending"})
+ reviews.action_bulk_validate()
+ self.assertEqual(set(reviews.mapped("status")), {"approved"})
+ self.assertEqual(rec_a.validation_status, "validated")
+ self.assertEqual(rec_b.validation_status, "validated")
+
+ def test_bulk_validate_skips_definitions_without_flag(self):
+ """A review on a definition without allow_bulk_approve stays pending
+ even when included in a bulk selection."""
+ self.assertFalse(self.definition_1.allow_bulk_approve)
+ rec = self.test_model.create({"test_field": 1.0})
+ rec.with_user(self.test_user_2).request_validation()
+ review = rec.review_ids.with_user(self.test_user_1)
+ review.action_bulk_validate()
+ self.assertEqual(review.status, "pending")
+
+ def test_bulk_validate_skips_other_users_reviews(self):
+ """The action only touches reviews assigned to the current user."""
+ self.definition_1.allow_bulk_approve = True
+ rec = self.test_model.create({"test_field": 1.0})
+ rec.with_user(self.test_user_2).request_validation()
+ review = rec.review_ids.with_user(self.test_user_2)
+ self.assertFalse(review._filter_bulk_validatable())
+ review.action_bulk_validate()
+ self.assertEqual(review.status, "pending")
+
+ def test_bulk_validate_skips_reviews_requiring_comment(self):
+ """A comment-required definition cannot be approved in bulk because
+ no comment can be collected; such reviews are skipped."""
+ self.definition_1.allow_bulk_approve = True
+ self.definition_1.has_comment = True
+ rec = self.test_model.create({"test_field": 1.0})
+ rec.with_user(self.test_user_2).request_validation()
+ review = rec.review_ids.with_user(self.test_user_1)
+ review.action_bulk_validate()
+ self.assertEqual(review.status, "pending")
+
def test_view_manual(self):
view = self.env[self.test_record._name].get_view(False, "form")
with Form(self.test_record) as f:
diff --git a/base_tier_validation/views/tier_definition_view.xml b/base_tier_validation/views/tier_definition_view.xml
index 922727ed..e5622516 100644
--- a/base_tier_validation/views/tier_definition_view.xml
+++ b/base_tier_validation/views/tier_definition_view.xml
@@ -76,6 +76,7 @@
options="{'no_create': True}"
/>
+