Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions base_tier_validation/models/tier_definition.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,14 @@ def _get_tier_validation_model_names(self):
help="Bypassed (auto validated), if previous tier was validated "
"by same reviewer",
)
allow_reject = fields.Boolean(
string="Allow Rejection",
default=True,
help="When unchecked, reviewers of this tier can only validate "
"the record, not reject it. Use this to model 'sign-off' / "
"informational tiers where the reviewer is expected to "
"acknowledge but not block the workflow.",
)

@api.onchange("review_type")
def onchange_review_type(self):
Expand Down
38 changes: 38 additions & 0 deletions base_tier_validation/models/tier_validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,14 @@ class TierValidation(models.AbstractModel):
can_review = fields.Boolean(
compute="_compute_can_review", search="_search_can_review"
)
can_reject = fields.Boolean(
compute="_compute_can_reject",
help="True when the current user can act on this record AND at "
"least one of their actionable tier reviews has "
"``tier.definition.allow_reject`` set. Drives the visibility "
"of the Reject button on the validation banner so that "
"sign-off / informational tiers don't expose a Reject action.",
)
has_comment = fields.Boolean(
compute="_compute_has_comment",
help="If set, Allow the reviewer to leave a comment on the review.",
Expand Down Expand Up @@ -115,6 +123,29 @@ def _compute_can_review(self):
for rec in self:
rec.can_review = rec._get_sequences_to_approve(self.env.user)

@api.depends_context("uid")
@api.depends(
"review_ids.approve_sequence",
"review_ids.reviewer_ids",
"review_ids.sequence",
"review_ids.status",
"review_ids.definition_id.allow_reject",
)
def _compute_can_reject(self):
user = self.env.user
for rec in self:
sequences = rec._get_sequences_to_approve(user)
if not sequences:
rec.can_reject = False
continue
# The user has actionable tiers; check whether at least one
# of them is configured to allow rejection.
rec.can_reject = any(
r.definition_id.allow_reject
for r in rec.review_ids
if r.sequence in sequences
)

@api.model
def _search_can_review(self, operator, value):
domain = Domain(
Expand Down Expand Up @@ -639,6 +670,13 @@ def reject_tier(self):
self.ensure_one()
sequences = self._get_sequences_to_approve(self.env.user)
reviews = self.review_ids.filtered(lambda x: x.sequence in sequences)
# Honor per-definition ``allow_reject``: tiers flagged as
# sign-off / informational only let the reviewer validate, not
# reject. Filter them out so calling reject_tier on a mixed
# batch only rejects the tiers that actually permit it.
reviews = reviews.filtered(lambda r: r.definition_id.allow_reject)
if not reviews:
return
if self.has_comment:
return self._add_comment("reject", reviews)
self._rejected_tier(reviews)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
<button
name="reject_tier"
string="Reject"
invisible="not can_review"
invisible="not can_reject"
type="object"
class="btn-icon btn-danger"
icon="fa-thumbs-down"
Expand Down
45 changes: 45 additions & 0 deletions base_tier_validation/tests/test_tier_validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,51 @@ def test_03_request_validation_approved(self):
record.validate_tier()
self.assertEqual(record.validation_status, "validated")

def test_allow_reject_false_hides_reject_button(self):
"""When the matching tier definition has ``allow_reject=False``
the validated record reports ``can_reject=False`` while
``can_review`` is still True. The form template binds the
Reject button's visibility to ``can_reject`` so the button is
hidden for sign-off tiers."""
# Override the default definition created in CommonTierValidation
# to forbid rejection.
self.tier_definition.write({"allow_reject": False})
self.test_record.with_user(self.test_user_2).request_validation()
record = self.test_record.with_user(self.test_user_1)
self.assertTrue(record.can_review)
self.assertFalse(record.can_reject)

def test_reject_tier_skips_definitions_with_allow_reject_false(self):
"""``reject_tier`` ignores reviews whose definition forbids
rejection -- so a reviewer assigned to a mix of sign-off and
regular tiers can still reject the regular ones but the
sign-off ones stay pending."""
# Two definitions for user 1: tier 1 = sign-off (no reject),
# tier 2 = regular. Sequence configured so both reviews
# surface as pending at the same time.
self.tier_definition.write({"allow_reject": False, "sequence": 10})
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)]",
"sequence": 20,
"allow_reject": True,
}
)
self.test_record.with_user(self.test_user_2).request_validation()
record = self.test_record.with_user(self.test_user_1)
self.assertTrue(record.can_reject)
record.reject_tier()
# The sign-off review stays in `pending`; the regular review
# transitions to `rejected`.
statuses = {
r.definition_id.allow_reject: r.status for r in self.test_record.review_ids
}
self.assertEqual(statuses[False], "pending")
self.assertEqual(statuses[True], "rejected")

def test_04_request_validation_rejected(self):
"""Request validation, rejection and reset."""
self.assertFalse(self.test_record.review_ids)
Expand Down
1 change: 1 addition & 0 deletions base_tier_validation/views/tier_definition_view.xml
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@
<span
>Bypass, if previous tier was validated by same reviewer</span>
</div>
<field name="allow_reject" />
</group>
</group>
<notebook>
Expand Down
Loading