Public release from ruodoo-project: 19.0 - 2026-05-31 21:19:12 UTC
This commit is contained in:
5
base_tier_validation/tests/__init__.py
Normal file
5
base_tier_validation/tests/__init__.py
Normal file
@ -0,0 +1,5 @@
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||
|
||||
from . import test_tier_validation
|
||||
from . import test_tier_validation_reminder
|
||||
from . import test_tier
|
||||
217
base_tier_validation/tests/common.py
Normal file
217
base_tier_validation/tests/common.py
Normal file
@ -0,0 +1,217 @@
|
||||
# Copyright 2018-19 ForgeFlow S.L. (https://www.forgeflow.com)
|
||||
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
|
||||
|
||||
from odoo_test_helper import FakeModelLoader
|
||||
|
||||
from odoo import Command
|
||||
from odoo.tests import new_test_user
|
||||
|
||||
from odoo.addons.base.tests.common import BaseCommon
|
||||
|
||||
|
||||
class CommonTierValidation(BaseCommon):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.loader = FakeModelLoader(cls.env, cls.__module__)
|
||||
cls.loader.backup_registry()
|
||||
from .tier_validation_tester import (
|
||||
TierDefinition,
|
||||
TierValidationTester,
|
||||
TierValidationTester2,
|
||||
TierValidationTesterComputed,
|
||||
)
|
||||
|
||||
cls.loader.update_registry(
|
||||
(
|
||||
TierValidationTester,
|
||||
TierValidationTester2,
|
||||
TierValidationTesterComputed,
|
||||
TierDefinition,
|
||||
)
|
||||
)
|
||||
|
||||
cls.test_model = cls.env[TierValidationTester._name]
|
||||
cls.test_model_2 = cls.env[TierValidationTester2._name]
|
||||
cls.test_model_computed = cls.env[TierValidationTesterComputed._name]
|
||||
|
||||
cls.tester_model = cls.env["ir.model"].search(
|
||||
[("model", "=", "tier.validation.tester")]
|
||||
)
|
||||
cls.tester_model_2 = cls.env["ir.model"].search(
|
||||
[("model", "=", "tier.validation.tester2")]
|
||||
)
|
||||
cls.tester_model_computed = cls.env["ir.model"].search(
|
||||
[("model", "=", "tier.validation.tester.computed")]
|
||||
)
|
||||
# Create a multi-company
|
||||
cls.main_company = cls.env.ref("base.main_company")
|
||||
cls.other_company = cls.env["res.company"].create({"name": "My Company"})
|
||||
|
||||
models = (
|
||||
cls.tester_model,
|
||||
cls.tester_model_2,
|
||||
cls.tester_model_computed,
|
||||
)
|
||||
for model in models:
|
||||
# Access record:
|
||||
cls.env["ir.model.access"].create(
|
||||
{
|
||||
"name": f"access {model.name}",
|
||||
"model_id": model.id,
|
||||
"perm_read": 1,
|
||||
"perm_write": 1,
|
||||
"perm_create": 1,
|
||||
"perm_unlink": 1,
|
||||
}
|
||||
)
|
||||
|
||||
# Define views to avoid automatic views with all fields.
|
||||
cls.env["ir.ui.view"].create(
|
||||
{
|
||||
"model": model.model,
|
||||
"name": f"Demo view for {model}",
|
||||
"arch": """<form>
|
||||
<header>
|
||||
<button name="action_confirm" type="object" string="Confirm" />
|
||||
<field name="state" widget="statusbar" />
|
||||
</header>
|
||||
<sheet>
|
||||
<field name="test_field" />
|
||||
</sheet>
|
||||
</form>""",
|
||||
}
|
||||
)
|
||||
|
||||
# Create users:
|
||||
cls.test_user_1 = new_test_user(
|
||||
cls.env, name="John", login="test1", groups="base.group_system"
|
||||
)
|
||||
cls.test_user_2 = new_test_user(cls.env, name="Mike", login="test2")
|
||||
cls.test_user_3_multi_company = new_test_user(
|
||||
cls.env,
|
||||
name="Jane",
|
||||
login="test3",
|
||||
company_ids=[Command.set([cls.main_company.id, cls.other_company.id])],
|
||||
)
|
||||
# Create groups
|
||||
cls.test_group = cls.env["res.groups"].create(
|
||||
{
|
||||
"name": "TestGroup",
|
||||
"users": [(4, cls.test_user_1.id), (4, cls.test_user_2.id)],
|
||||
}
|
||||
)
|
||||
# Create tier definitions:
|
||||
cls.tier_def_obj = cls.env["tier.definition"]
|
||||
cls.tier_definition = cls.tier_def_obj.create(
|
||||
{
|
||||
"model_id": cls.tester_model.id,
|
||||
"review_type": "individual",
|
||||
"reviewer_id": cls.test_user_1.id,
|
||||
"definition_domain": "[('test_field', '=', 1.0)]",
|
||||
"sequence": 30,
|
||||
}
|
||||
)
|
||||
|
||||
cls.test_record = cls.test_model.create({"test_field": 1.0})
|
||||
cls.test_record_2 = cls.test_model_2.create({"test_field": 1.0})
|
||||
cls.test_record_computed = cls.test_model_computed.create({"test_field": 1.0})
|
||||
|
||||
cls.tier_def_obj.create(
|
||||
{
|
||||
"model_id": cls.tester_model.id,
|
||||
"review_type": "individual",
|
||||
"reviewer_id": cls.test_user_1.id,
|
||||
"definition_domain": "[('test_field', '>', 3.0)]",
|
||||
"approve_sequence": True,
|
||||
"notify_on_pending": False,
|
||||
"sequence": 20,
|
||||
"name": "Definition for test 19 - sequence - user 1",
|
||||
}
|
||||
)
|
||||
cls.tier_def_obj.create(
|
||||
{
|
||||
"model_id": cls.tester_model.id,
|
||||
"review_type": "individual",
|
||||
"reviewer_id": cls.test_user_2.id,
|
||||
"definition_domain": "[('test_field', '>', 3.0)]",
|
||||
"approve_sequence": True,
|
||||
"notify_on_pending": True,
|
||||
"sequence": 10,
|
||||
"name": "Definition for test 19 - sequence - user 2",
|
||||
}
|
||||
)
|
||||
# Create definition for test 20
|
||||
cls.tier_def_obj.create(
|
||||
{
|
||||
"model_id": cls.tester_model.id,
|
||||
"review_type": "individual",
|
||||
"reviewer_id": cls.test_user_1.id,
|
||||
"definition_domain": "[('test_field', '=', 0.9)]",
|
||||
"approve_sequence": False,
|
||||
"notify_on_pending": True,
|
||||
"sequence": 10,
|
||||
"name": "Definition for test 20 - no sequence - user 1 - no sequence",
|
||||
}
|
||||
)
|
||||
|
||||
cls.tier_def_obj.create(
|
||||
{
|
||||
"model_id": cls.tester_model_computed.id,
|
||||
"review_type": "individual",
|
||||
"reviewer_id": cls.test_user_1.id,
|
||||
"definition_domain": "[]",
|
||||
"approve_sequence": True,
|
||||
"notify_on_pending": False,
|
||||
"sequence": 20,
|
||||
"name": "Definition for computed model",
|
||||
}
|
||||
)
|
||||
|
||||
# Create definition for test 30, 31
|
||||
# Main company tier definition
|
||||
cls.tier_def_obj.create(
|
||||
{
|
||||
"model_id": cls.tester_model_2.id,
|
||||
"review_type": "individual",
|
||||
"reviewer_id": cls.test_user_1.id,
|
||||
"definition_domain": "[('test_field', '>=', 1.0)]",
|
||||
"approve_sequence": True,
|
||||
"notify_on_pending": False,
|
||||
"sequence": 30,
|
||||
"name": "Definition for test 30 - sequence - user 1 - main company",
|
||||
"company_id": cls.main_company.id,
|
||||
}
|
||||
)
|
||||
cls.tier_def_obj.create(
|
||||
{
|
||||
"model_id": cls.tester_model_2.id,
|
||||
"review_type": "individual",
|
||||
"reviewer_id": cls.test_user_3_multi_company.id,
|
||||
"definition_domain": "[('test_field', '>=', 1.0)]",
|
||||
"approve_sequence": True,
|
||||
"notify_on_pending": False,
|
||||
"sequence": 20,
|
||||
"name": "Definition for test 30 - sequence - user 3 - main company",
|
||||
"company_id": cls.main_company.id,
|
||||
}
|
||||
)
|
||||
# Other company tier definition
|
||||
cls.tier_def_obj.create(
|
||||
{
|
||||
"model_id": cls.tester_model_2.id,
|
||||
"review_type": "individual",
|
||||
"reviewer_id": cls.test_user_3_multi_company.id,
|
||||
"definition_domain": "[('test_field', '>=', 1.0)]",
|
||||
"approve_sequence": True,
|
||||
"notify_on_pending": False,
|
||||
"sequence": 30,
|
||||
"name": "Definition for test 30 - sequence - user 3 - other company",
|
||||
"company_id": cls.other_company.id,
|
||||
}
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
cls.loader.restore_registry()
|
||||
super().tearDownClass()
|
||||
193
base_tier_validation/tests/test_tier.py
Normal file
193
base_tier_validation/tests/test_tier.py
Normal file
@ -0,0 +1,193 @@
|
||||
# Copyright 2024 DOB
|
||||
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
|
||||
|
||||
from odoo_test_helper import FakeModelLoader
|
||||
|
||||
from odoo.tests import new_test_user
|
||||
from odoo.tests.common import TransactionCase
|
||||
|
||||
from .tier_validation_tester import (
|
||||
TierDefinition,
|
||||
TierValidationTester,
|
||||
)
|
||||
|
||||
|
||||
class TestTierDefinition(TransactionCase):
|
||||
"""Tests for base_tier_validation: TierDefinition creation and workflow.
|
||||
|
||||
Validates: Requirements 8.1, 8.2, 8.3, 8.4
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.loader = FakeModelLoader(cls.env, cls.__module__)
|
||||
cls.loader.backup_registry()
|
||||
cls.loader.update_registry((TierValidationTester, TierDefinition))
|
||||
|
||||
cls.test_model = cls.env[TierValidationTester._name]
|
||||
cls.tester_model = cls.env["ir.model"].search(
|
||||
[("model", "=", TierValidationTester._name)]
|
||||
)
|
||||
|
||||
# Grant access to the fake model
|
||||
cls.env["ir.model.access"].create(
|
||||
{
|
||||
"name": "access tier.validation.tester",
|
||||
"model_id": cls.tester_model.id,
|
||||
"perm_read": 1,
|
||||
"perm_write": 1,
|
||||
"perm_create": 1,
|
||||
"perm_unlink": 1,
|
||||
}
|
||||
)
|
||||
|
||||
# Minimal form view to avoid auto-view issues
|
||||
cls.env["ir.ui.view"].create(
|
||||
{
|
||||
"model": TierValidationTester._name,
|
||||
"name": "test_tier_tester_form",
|
||||
"arch": """<form>
|
||||
<header>
|
||||
<button name="action_confirm" type="object" string="Confirm" />
|
||||
<field name="state" widget="statusbar" />
|
||||
</header>
|
||||
<sheet>
|
||||
<field name="test_field" />
|
||||
</sheet>
|
||||
</form>""",
|
||||
}
|
||||
)
|
||||
|
||||
# Users: reviewer (admin) and requester (regular)
|
||||
cls.reviewer = new_test_user(
|
||||
cls.env,
|
||||
name="Reviewer",
|
||||
login="tier_reviewer",
|
||||
groups="base.group_system",
|
||||
)
|
||||
cls.requester = new_test_user(
|
||||
cls.env,
|
||||
name="Requester",
|
||||
login="tier_requester",
|
||||
)
|
||||
|
||||
cls.tier_def_obj = cls.env["tier.definition"]
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
cls.loader.restore_registry()
|
||||
super().tearDownClass()
|
||||
|
||||
def test_01_create_tier_definition(self):
|
||||
"""WHEN a TierDefinition is created for a model with approval levels,
|
||||
base_tier_validation SHALL create the record without errors.
|
||||
|
||||
Validates: Requirement 8.1
|
||||
"""
|
||||
tier_def = self.tier_def_obj.create(
|
||||
{
|
||||
"model_id": self.tester_model.id,
|
||||
"review_type": "individual",
|
||||
"reviewer_id": self.reviewer.id,
|
||||
"definition_domain": "[]",
|
||||
"sequence": 10,
|
||||
}
|
||||
)
|
||||
self.assertTrue(tier_def.id, "TierDefinition should be created without errors")
|
||||
self.assertEqual(tier_def.model_id, self.tester_model)
|
||||
self.assertEqual(tier_def.reviewer_id, self.reviewer)
|
||||
|
||||
def test_02_request_validation_creates_tier_reviews(self):
|
||||
"""WHEN a document is submitted for approval,
|
||||
base_tier_validation SHALL create TierReview records for each level.
|
||||
|
||||
Validates: Requirement 8.2
|
||||
"""
|
||||
# Create two tier definitions (two approval levels)
|
||||
self.tier_def_obj.create(
|
||||
{
|
||||
"model_id": self.tester_model.id,
|
||||
"review_type": "individual",
|
||||
"reviewer_id": self.reviewer.id,
|
||||
"definition_domain": "[('test_field', '>', 0.0)]",
|
||||
"sequence": 10,
|
||||
}
|
||||
)
|
||||
self.tier_def_obj.create(
|
||||
{
|
||||
"model_id": self.tester_model.id,
|
||||
"review_type": "individual",
|
||||
"reviewer_id": self.reviewer.id,
|
||||
"definition_domain": "[('test_field', '>', 0.0)]",
|
||||
"sequence": 20,
|
||||
}
|
||||
)
|
||||
|
||||
record = self.test_model.create({"test_field": 1.0})
|
||||
self.assertFalse(record.review_ids, "No reviews before request")
|
||||
|
||||
reviews = record.with_user(self.requester).request_validation()
|
||||
|
||||
self.assertTrue(reviews, "Reviews should be created after request_validation")
|
||||
self.assertEqual(
|
||||
len(reviews),
|
||||
2,
|
||||
"One TierReview should be created per approval level",
|
||||
)
|
||||
|
||||
def test_03_approve_all_tiers_sets_validated_status(self):
|
||||
"""WHEN all approval levels are approved,
|
||||
base_tier_validation SHALL set the document validation_status to 'validated'.
|
||||
|
||||
Validates: Requirement 8.3
|
||||
"""
|
||||
self.tier_def_obj.create(
|
||||
{
|
||||
"model_id": self.tester_model.id,
|
||||
"review_type": "individual",
|
||||
"reviewer_id": self.reviewer.id,
|
||||
"definition_domain": "[('test_field', '>', 0.0)]",
|
||||
"sequence": 10,
|
||||
}
|
||||
)
|
||||
|
||||
record = self.test_model.create({"test_field": 1.0})
|
||||
record.with_user(self.requester).request_validation()
|
||||
|
||||
record_as_reviewer = record.with_user(self.reviewer)
|
||||
record_as_reviewer.validate_tier()
|
||||
|
||||
self.assertEqual(
|
||||
record.validation_status,
|
||||
"validated",
|
||||
"validation_status should be 'validated' after all tiers approved",
|
||||
)
|
||||
|
||||
def test_04_reject_tier_sets_rejected_status(self):
|
||||
"""IF one approval level is rejected,
|
||||
base_tier_validation SHALL set the document validation_status to 'rejected'.
|
||||
|
||||
Validates: Requirement 8.4
|
||||
"""
|
||||
self.tier_def_obj.create(
|
||||
{
|
||||
"model_id": self.tester_model.id,
|
||||
"review_type": "individual",
|
||||
"reviewer_id": self.reviewer.id,
|
||||
"definition_domain": "[('test_field', '>', 0.0)]",
|
||||
"sequence": 10,
|
||||
}
|
||||
)
|
||||
|
||||
record = self.test_model.create({"test_field": 1.0})
|
||||
record.with_user(self.requester).request_validation()
|
||||
|
||||
record_as_reviewer = record.with_user(self.reviewer)
|
||||
record_as_reviewer.reject_tier()
|
||||
|
||||
self.assertEqual(
|
||||
record.validation_status,
|
||||
"rejected",
|
||||
"validation_status should be 'rejected' after a tier is rejected",
|
||||
)
|
||||
1285
base_tier_validation/tests/test_tier_validation.py
Normal file
1285
base_tier_validation/tests/test_tier_validation.py
Normal file
File diff suppressed because it is too large
Load Diff
46
base_tier_validation/tests/test_tier_validation_reminder.py
Normal file
46
base_tier_validation/tests/test_tier_validation_reminder.py
Normal file
@ -0,0 +1,46 @@
|
||||
# Copyright 2018-19 ForgeFlow S.L. (https://www.forgeflow.com)
|
||||
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
|
||||
|
||||
from freezegun import freeze_time
|
||||
|
||||
from odoo import fields
|
||||
from odoo.tests.common import tagged
|
||||
|
||||
from .common import CommonTierValidation
|
||||
|
||||
|
||||
@tagged("post_install", "-at_install")
|
||||
class TierTierValidation(CommonTierValidation):
|
||||
def test_validation_reminder(self):
|
||||
"""Check the posting of reminder to reviews."""
|
||||
tier_definition = self.tier_definition
|
||||
tier_definition.notify_reminder_delay = 3
|
||||
|
||||
# Request a review today
|
||||
self.test_record.with_user(self.test_user_2.id).request_validation()
|
||||
review = self.env["tier.review"].search(
|
||||
[("definition_id", "=", tier_definition.id)]
|
||||
)
|
||||
self.assertTrue(review)
|
||||
self.assertEqual(review.last_reminder_date, False)
|
||||
|
||||
# 2 days later no reminder should be posted
|
||||
in_2_days = fields.Datetime.add(fields.Datetime.now(), days=2)
|
||||
with freeze_time(in_2_days):
|
||||
tier_definition._cron_send_review_reminder()
|
||||
self.assertEqual(review.last_reminder_date, False)
|
||||
# 4 days later first reminder
|
||||
in_4_days = fields.Datetime.add(fields.Datetime.now(), days=4)
|
||||
with freeze_time(in_4_days):
|
||||
self.tier_definition._cron_send_review_reminder()
|
||||
self.assertEqual(review.last_reminder_date, in_4_days)
|
||||
# 5 days later no new reminder
|
||||
in_6_days = fields.Datetime.add(fields.Datetime.now(), days=6)
|
||||
with freeze_time(in_6_days):
|
||||
self.tier_definition._cron_send_review_reminder()
|
||||
self.assertEqual(review.last_reminder_date, in_4_days)
|
||||
# 9 days later second reminder
|
||||
in_9_days = fields.Datetime.add(fields.Datetime.now(), days=9)
|
||||
with freeze_time(in_9_days):
|
||||
self.tier_definition._cron_send_review_reminder()
|
||||
self.assertEqual(review.last_reminder_date, in_9_days)
|
||||
118
base_tier_validation/tests/tier_validation_tester.py
Normal file
118
base_tier_validation/tests/tier_validation_tester.py
Normal file
@ -0,0 +1,118 @@
|
||||
# Copyright 2018-19 ForgeFlow S.L. (https://www.forgeflow.com)
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||
|
||||
from odoo import api, fields, models
|
||||
|
||||
|
||||
class TierValidationTester(models.Model):
|
||||
_name = "tier.validation.tester"
|
||||
_description = "Tier Validation Tester"
|
||||
_inherit = ["tier.validation", "mail.thread"]
|
||||
_tier_validation_manual_config = True
|
||||
|
||||
state = fields.Selection(
|
||||
selection=[
|
||||
("draft", "Draft"),
|
||||
("confirmed", "Confirmed"),
|
||||
("cancel", "Cancel"),
|
||||
],
|
||||
default="draft",
|
||||
)
|
||||
test_validation_field = fields.Integer(default=0)
|
||||
test_field = fields.Float()
|
||||
user_id = fields.Many2one(string="Assigned to:", comodel_name="res.users")
|
||||
group_id = fields.Many2one("res.groups", string="Assigned to (group)")
|
||||
menu_id = fields.Many2one(
|
||||
"ir.ui.menu", help="For testing choosing a wrong field type"
|
||||
)
|
||||
|
||||
def action_confirm(self):
|
||||
self.write({"state": "confirmed"})
|
||||
|
||||
|
||||
class TierValidationTester2(models.Model):
|
||||
_name = "tier.validation.tester2"
|
||||
_description = "Tier Validation Tester 2"
|
||||
_inherit = ["tier.validation"]
|
||||
_tier_validation_manual_config = False
|
||||
|
||||
state = fields.Selection(
|
||||
selection=[
|
||||
("draft", "Draft"),
|
||||
("confirmed", "Confirmed"),
|
||||
("cancel", "Cancel"),
|
||||
],
|
||||
default="draft",
|
||||
)
|
||||
test_field = fields.Float()
|
||||
test_validation_field = fields.Float()
|
||||
user_id = fields.Many2one(string="Assigned to:", comodel_name="res.users")
|
||||
company_id = fields.Many2one(comodel_name="res.company")
|
||||
|
||||
def action_confirm(self):
|
||||
self.write({"state": "confirmed"})
|
||||
|
||||
|
||||
class TierValidationTesterComputed(models.Model):
|
||||
_name = "tier.validation.tester.computed"
|
||||
_description = "Tier Validation Tester Computed"
|
||||
_inherit = ["tier.validation"]
|
||||
_tier_validation_manual_config = False
|
||||
_tier_validation_state_field_is_computed = True
|
||||
|
||||
confirmed = fields.Boolean()
|
||||
cancelled = fields.Boolean()
|
||||
state = fields.Selection(
|
||||
selection=[
|
||||
("draft", "Draft"),
|
||||
("confirmed", "Confirmed"),
|
||||
("cancel", "Cancel"),
|
||||
],
|
||||
compute="_compute_state",
|
||||
store=True,
|
||||
)
|
||||
test_field = fields.Float()
|
||||
test_validation_field = fields.Float()
|
||||
user_id = fields.Many2one(string="Assigned to:", comodel_name="res.users")
|
||||
|
||||
@api.model
|
||||
def _get_after_validation_exceptions(self):
|
||||
return super()._get_after_validation_exceptions() + [
|
||||
"confirmed",
|
||||
"cancelled",
|
||||
]
|
||||
|
||||
@api.model
|
||||
def _get_under_validation_exceptions(self):
|
||||
return super()._get_under_validation_exceptions() + [
|
||||
"confirmed",
|
||||
"cancelled",
|
||||
]
|
||||
|
||||
@api.depends("confirmed", "cancelled")
|
||||
def _compute_state(self):
|
||||
for rec in self:
|
||||
if rec.cancelled:
|
||||
rec.state = "cancel"
|
||||
elif rec.confirmed:
|
||||
rec.state = "confirmed"
|
||||
else:
|
||||
rec.state = "draft"
|
||||
|
||||
def action_confirm(self):
|
||||
self.write({"confirmed": True})
|
||||
|
||||
def action_cancel(self):
|
||||
self.write({"cancelled": True})
|
||||
|
||||
|
||||
class TierDefinition(models.Model):
|
||||
_inherit = "tier.definition"
|
||||
|
||||
@api.model
|
||||
def _get_tier_validation_model_names(self):
|
||||
res = super()._get_tier_validation_model_names()
|
||||
res.append("tier.validation.tester")
|
||||
res.append("tier.validation.tester2")
|
||||
res.append("tier.validation.tester.computed")
|
||||
return res
|
||||
Reference in New Issue
Block a user