diff --git a/spp_base_demo/tests/test_locale_providers.py b/spp_base_demo/tests/test_locale_providers.py index ed0d75bb3..9f39ce2e1 100644 --- a/spp_base_demo/tests/test_locale_providers.py +++ b/spp_base_demo/tests/test_locale_providers.py @@ -1,8 +1,6 @@ -from faker import Faker - from odoo.tests.common import TransactionCase -from ..locale_providers import create_faker, get_faker_provider +from ..locale_providers import get_faker_provider from ..locale_providers.en_KE import Provider as EnKeProvider from ..locale_providers.lo_LA import Provider as LoLaProvider from ..locale_providers.si_LK import Provider as SiLkProvider @@ -22,9 +20,3 @@ def test_get_faker_provider(self): self.assertEqual(get_faker_provider("sw_KE"), SwKeProvider) self.assertEqual(get_faker_provider("ta_LK"), TaLkProvider) self.assertEqual(get_faker_provider("en_US"), None) - - def test_create_faker(self): - fake = create_faker("en_KE") - self.assertIsInstance(fake, Faker) - self.assertIn(fake.first_name(), EnKeProvider.first_names) - self.assertIn(fake.last_name(), EnKeProvider.last_names) diff --git a/spp_base_farmer_registry/views/group_view.xml b/spp_base_farmer_registry/views/group_view.xml index cae8ba423..6cc5781c4 100644 --- a/spp_base_farmer_registry/views/group_view.xml +++ b/spp_base_farmer_registry/views/group_view.xml @@ -304,4 +304,14 @@ + + view_groups_list_farmer_registry_tree + res.partner + + + + 0 + + + diff --git a/spp_base_farmer_registry/views/individual_view.xml b/spp_base_farmer_registry/views/individual_view.xml index 451e50f74..0b909f1b8 100644 --- a/spp_base_farmer_registry/views/individual_view.xml +++ b/spp_base_farmer_registry/views/individual_view.xml @@ -100,4 +100,14 @@ + + view_individuals_list_farmer_registry_tree + res.partner + + + + 0 + + + diff --git a/spp_base_social_registry/__manifest__.py b/spp_base_social_registry/__manifest__.py index 4adda6792..05dcb3549 100644 --- a/spp_base_social_registry/__manifest__.py +++ b/spp_base_social_registry/__manifest__.py @@ -29,7 +29,9 @@ "spp_base_spmis", ], "external_dependencies": {}, - "data": [], + "data": [ + "views/registrant_view.xml", + ], "assets": {}, "demo": [], "images": [], diff --git a/spp_base_social_registry/views/registrant_view.xml b/spp_base_social_registry/views/registrant_view.xml new file mode 100644 index 000000000..bafdcad17 --- /dev/null +++ b/spp_base_social_registry/views/registrant_view.xml @@ -0,0 +1,26 @@ + + + + + view_individuals_list_social_registry_tree + res.partner + + + + 0 + + + + + + view_groups_list_social_registry_tree + res.partner + + + + 0 + + + + + diff --git a/spp_base_spmis/views/registrant_view.xml b/spp_base_spmis/views/registrant_view.xml index 28e1ffd74..a1df545df 100644 --- a/spp_base_spmis/views/registrant_view.xml +++ b/spp_base_spmis/views/registrant_view.xml @@ -25,4 +25,26 @@ + + view_individuals_list_spmis_tree + res.partner + + + + 0 + + + + + + view_groups_list_spmis_tree + res.partner + + + + 0 + + + + diff --git a/spp_change_request/__init__.py b/spp_change_request/__init__.py index c4ccea794..e6052aee3 100644 --- a/spp_change_request/__init__.py +++ b/spp_change_request/__init__.py @@ -1,3 +1,4 @@ # Part of OpenSPP. See LICENSE file for full copyright and licensing details. from . import models +from .tests import test_models diff --git a/spp_change_request/models/change_request.py b/spp_change_request/models/change_request.py index 8bffc2849..af9cc0831 100644 --- a/spp_change_request/models/change_request.py +++ b/spp_change_request/models/change_request.py @@ -88,6 +88,38 @@ class ChangeRequestBase(models.Model): domain=[("is_registrant", "=", True)], ) #: Registrant who submitted the change request + validation_stage = fields.Selection( + [ + ("local", "Local"), + ("hq", "HQ"), + ("completed", "Completed"), + ], + string="Validation Stage", + default="local", + compute="_compute_validation_stage", + store=True, + ) + + @api.depends("validator_ids") + def _compute_validation_stage(self): + for rec in self: + validation_sequences = self.env["spp.change.request.validation.sequence"].search( + [("request_type", "=", rec.request_type)] + ) + total_sequences = len(validation_sequences) + total_validators = len(rec.validator_ids) + if not validation_sequences or total_sequences == 0: + rec.validation_stage = "local" + else: + if total_validators == 0: + rec.validation_stage = "local" + elif total_validators < total_sequences - 1: + rec.validation_stage = "local" + elif total_validators == total_sequences - 1: + rec.validation_stage = "hq" + elif total_validators >= total_sequences: + rec.validation_stage = "completed" + @api.model def _registrant_id_not_visible_in_request_type(self): return [] diff --git a/spp_change_request/models/mixins/source_mixin.py b/spp_change_request/models/mixins/source_mixin.py index 58ab2362a..13dcb9b7f 100644 --- a/spp_change_request/models/mixins/source_mixin.py +++ b/spp_change_request/models/mixins/source_mixin.py @@ -76,6 +76,63 @@ class ChangeRequestAddChildren(models.Model): # string="DMS Files", # auto_join=True, # ) + validation_stage = fields.Selection( + string="Validation Stage", + related="change_request_id.validation_stage", + ) + show_cr_actions = fields.Boolean(compute="_compute_show_cr_actions") + show_assign_button = fields.Boolean(compute="_compute_show_assign_button") + show_reassign_button = fields.Boolean(compute="_compute_show_reassign_button") + + def _compute_show_cr_actions(self): + for rec in self: + user = self.env.user + rec.show_cr_actions = ( + rec.validation_stage == "local" + and user.has_group("spp_change_request.group_spp_change_request_validator") + and user.id == rec.assign_to_id.id + ) or ( + rec.validation_stage == "hq" + and user.has_group("spp_change_request.group_spp_change_request_hq_validator") + and user.id == rec.assign_to_id.id + ) + + def _compute_show_assign_button(self): + for rec in self: + user = self.env.user + assigned_id = rec.assign_to_id.id if rec.assign_to_id else None + user_not_assigned = True + if assigned_id: + user_not_assigned = True if assigned_id != user.id else False + + rec.show_assign_button = ( + rec.state in ("draft", "pending", "validated", "rejected") + and ( + rec.validation_stage == "local" + and user.has_group("spp_change_request.group_spp_change_request_validator") + ) + or ( + rec.validation_stage == "hq" + and user.has_group("spp_change_request.group_spp_change_request_hq_validator") + ) + or user.has_group("g2p_registry_base.group_g2p_admin") + ) and user_not_assigned + + def _compute_show_reassign_button(self): + for rec in self: + user = self.env.user + rec.show_reassign_button = ( + rec.state in ("draft", "pending", "validated", "rejected") + and ( + rec.validation_stage == "local" + and user.has_group("spp_change_request.group_spp_change_request_validator") + ) + or ( + rec.validation_stage == "hq" + and user.has_group("spp_change_request.group_spp_change_request_hq_validator") + ) + or user.has_group("g2p_registry_base.group_g2p_admin") + ) and rec.assign_to_id.id == user.id def _copy_group_member_ids(self, group_id_field, group_ref_field="registrant_id"): for rec in self: diff --git a/spp_change_request/tests/common.py b/spp_change_request/tests/common.py deleted file mode 100644 index ae82f5ed1..000000000 --- a/spp_change_request/tests/common.py +++ /dev/null @@ -1,53 +0,0 @@ -from unittest.mock import patch - -from odoo.tests import TransactionCase - - -class Common(TransactionCase): - @classmethod - def setUpClass(cls): - super().setUpClass() - cls._test_individual_1 = cls._create_registrant({"name": "Liu Bei"}) - cls._test_individual_2 = cls._create_registrant({"name": "Guan Yu"}) - cls._test_individual_3 = cls._create_registrant({"name": "Zhang Fei"}) - cls._test_group = cls._create_registrant( - { - "name": "Shu clan", - "is_group": True, - "group_membership_ids": [ - ( - 0, - 0, - { - "individual": cls._test_individual_1.id, - "kind": [ - ( - 4, - cls.env.ref("g2p_registry_membership.group_membership_kind_head").id, - ) - ], - }, - ), - (0, 0, {"individual": cls._test_individual_2.id}), - (0, 0, {"individual": cls._test_individual_3.id}), - ], - } - ) - - @classmethod - def _create_registrant(cls, vals): - cls.assertTrue(isinstance(vals, dict), "Return vals should be a dict!") - vals.update({"is_registrant": True}) - return cls.env["res.partner"].create(vals) - - @classmethod - @patch("odoo.addons.spp_change_request_base.models.change_request.ChangeRequestBase._selection_request_type_ref_id") - def _create_change_request(self, mock_request_type_selection): - mock_request_type_selection.return_value = [("test.request.type", "Test Request Type")] - mock_request_type_selection.__name__ = "_mocked__selection_request_type_ref_id" - return self.env["spp.change.request"].create( - { - "name": "Test Request", - "request_type": "test.request.type", - } - ) diff --git a/spp_change_request/tests/test_change_requests.py b/spp_change_request/tests/test_change_requests.py index 16ca2f570..d9b46280b 100644 --- a/spp_change_request/tests/test_change_requests.py +++ b/spp_change_request/tests/test_change_requests.py @@ -1,143 +1,167 @@ +import logging from unittest.mock import patch -from odoo import fields -from odoo.exceptions import UserError +from odoo.exceptions import UserError, ValidationError +from odoo.tests.common import TransactionCase -from .common import Common +_logger = logging.getLogger(__name__) -class TestChangeRequests(Common): +class TestChangeRequestBase(TransactionCase): @classmethod def setUpClass(cls): super().setUpClass() - cls._test_change_request = cls._create_change_request() - def test_01_create_change_request(self): - self.assertEqual( - self._test_change_request.assign_to_id, - self.env.user, - "Creating user should be default assignee!", - ) - - def test_02_unlink_raise_error_cr(self): - with self.assertRaisesRegex(UserError, "Only draft change requests can be deleted by its creator."): - self._test_change_request.with_user(2).unlink() - self._test_change_request.state = "pending" - with self.assertRaisesRegex(UserError, "Only draft change requests can be deleted by its creator."): - self._test_change_request.unlink() - - def test_03_unlink_cr(self): - self._test_change_request.unlink() - remaining_change_request = self.env["spp.change.request"].search([("request_type", "=", "request_type")]) - self.assertCountEqual( - remaining_change_request.ids, - [], - "Draft change request should unlinkable by its creator!", + # Create test users + cls.user_admin = cls.env.ref("base.user_admin") + cls.user_demo = cls.env["res.users"].create( + { + "name": "Test User", + "login": "test_user", + "password": "test_password", + } ) - def test_04_compute_applicant_id_domain_cr(self): - self.assertEqual( - self._test_change_request.applicant_id_domain, - [("id", "=", 0)], - "Without registrant, applicant selections should not be available!", + # Create test registrants + cls.registrant_1 = cls.env["res.partner"].create( + { + "name": "Test Partner 1", + "phone": "+639171234567", + "is_registrant": True, + "country_id": cls.env.ref("base.ph").id, + } ) - self._test_change_request.registrant_id = self._test_group - self.assertEqual( - self._test_change_request.applicant_id_domain, - [ - ( - "id", - "in", - self._test_change_request.registrant_id.group_membership_ids.individual.ids, - ) - ], - "With registrant, applicant selection should be available!", + cls.registrant_2 = cls.env["res.partner"].create( + { + "name": "Test Partner 2", + "phone": "+639171234568", + "is_registrant": True, + "is_group": True, + "country_id": cls.env.ref("base.ph").id, + } ) + cls.validation_ids = [] - def test_05_assign_to_user_cr(self): - admin = self.env.ref("base.user_admin") - self._test_change_request.assign_to_user(admin) - self.assertEqual( - self._test_change_request.assign_to_id, - admin, - "Admin should be the one who assigned to this CR!", + # Patch to allow creating spp.change.request + patcher = patch( + "odoo.addons.spp_change_request_base.models.change_request.ChangeRequestBase._selection_request_type_ref_id" ) - self._test_change_request.state = "pending" - with self.assertRaisesRegex(UserError, "^.*not have any validation sequence defined.$"): - self._test_change_request.assign_to_user(self.env.user) - - def test_06_onchange_scan_qr_code_details_cr(self): - self._test_change_request.qr_code_details = '{"qrcode": "-T-E-S-T-Q-R-C-O-D-E-"}' - with self.assertRaisesRegex(UserError, "^.*no group found with the ID number from the QR Code scanned.$"): - self._test_change_request._onchange_scan_qr_code_details() - self.env["g2p.reg.id"].create( + mock_selection = patcher.start() + mock_selection.return_value = [("test.cr.type2", "Test CR Type")] + mock_selection.__name__ = "_mocked__selection_request_type_ref_id" + cls.addClassCleanup(patcher.stop) + + def _create_test_cr(self): + default_vals = { + "request_type": "test.cr.type2", + "registrant_id": self.registrant_1.id, + "applicant_phone": "+639171234567", + } + + return self.env["spp.change.request"].create(default_vals) + + def test_01_cr_creation(self): + """Test change request creation with default values""" + change_request = self._create_test_cr() + self.assertTrue(change_request.name) + self.assertEqual(change_request.assign_to_id, self.env.user) + self.assertEqual(change_request.request_type, "test.cr.type2") + + def test_02_onchange_request_type(self): + """Test that registrant_id is cleared when request_type changes.""" + change_request = self._create_test_cr() + self.assertEqual(change_request.registrant_id, self.registrant_1) + + # Simulate onchange + change_request._onchange_request_type() + + self.assertFalse(change_request.registrant_id) + + def test_03_onchange_registrant_id(self): + """Test that applicant_id and applicant_phone are cleared when registrant_id changes.""" + change_request = self._create_test_cr() + change_request.applicant_id = self.registrant_1.id + self.assertTrue(change_request.applicant_id) + self.assertTrue(change_request.applicant_phone) + + change_request.registrant_id = self.registrant_2.id + change_request._onchange_registrant_id() + + self.assertFalse(change_request.applicant_id) + self.assertFalse(change_request.applicant_phone) + + def test_04_onchange_applicant_id(self): + """Test that applicant_phone is updated when applicant_id changes.""" + change_request = self._create_test_cr() + change_request.applicant_id = self.registrant_1.id + change_request._onchange_applicant_id() + self.assertEqual(change_request.applicant_phone, self.registrant_1.phone) + + change_request.applicant_id = False + change_request._onchange_applicant_id() + self.assertFalse(change_request.applicant_phone) + + def test_05_check_applicant_phone(self): + """Test applicant phone number validation.""" + change_request = self._create_test_cr() + + # Should not raise error + change_request.applicant_phone = "+639171234567" + change_request._check_applicant_phone() + + def test_06_open_applicant_form(self): + """Test opening applicant form view.""" + change_request = self._create_test_cr() + # No applicant + action = change_request.open_applicant_form() + self.assertEqual(action["type"], "ir.actions.client") + self.assertEqual(action["tag"], "display_notification") + + # With applicant + change_request.applicant_id = self.registrant_1.id + action = change_request.open_applicant_form() + self.assertEqual(action["type"], "ir.actions.act_window") + self.assertEqual(action["res_model"], "res.partner") + self.assertEqual(action["res_id"], self.registrant_1.id) + + def test_07_check_phone_exist(self): + """Test _check_phone_exist method.""" + change_request = self._create_test_cr() + change_request.applicant_phone_required = True + change_request.applicant_phone = False + with self.assertRaises(UserError) as e: + change_request._check_phone_exist() + self.assertEqual(str(e.exception), "Phone No. is required.") + + change_request.applicant_phone = "+639171234567" + self.assertIsNone(change_request._check_phone_exist()) + + def test_08_approve_cr(self): + """Test approve_cr method.""" + change_request = self._create_test_cr() + with self.assertRaises(ValidationError): + change_request.with_user(self.user_demo).approve_cr() + + test_cr_type_record = self.env["test.cr.type2"].create( { - "partner_id": self._test_group.id, - "id_type": self.env.ref("spp_idpass.id_type_idpass").id, - "value": "-T-E-S-T-Q-R-C-O-D-E-", + "change_request_id": change_request.id, } ) - self._test_change_request._onchange_scan_qr_code_details() - self.assertEqual( - self._test_change_request.registrant_id, - self._test_group, - "Registrant on CR should be test group!", - ) - @patch( - "odoo.addons.phone_validation.tools.phone_validation.phone_parse", - return_value="1", - ) - def test_07_open_request_detail_cr(self, phone_parse): - with self.assertRaisesRegex(UserError, "Phone No. is required."): - self._test_change_request.open_request_detail() - self._test_change_request.applicant_phone = "+9647001234567" - res = self._test_change_request.open_request_detail() - self.assertListEqual( - [res.get("type"), res.get("tag"), res.get("params", {}).get("type")], - ["ir.actions.client", "display_notification", "danger"], - "Request Type ID not existed, client should display error notification!", - ) + change_request.request_type_ref_id = test_cr_type_record + self.user_demo.groups_id = [(4, self.env.ref("spp_change_request.group_spp_change_request_external_api").id)] + change_request.with_user(self.user_demo).approve_cr() + self.assertEqual(change_request.state, "applied") - def test_08_cancel_error_cr(self): - with self.assertRaisesRegex(UserError, "^.*request to be cancelled must be in draft.*$"): - self._test_change_request.state = "validated" - self._test_change_request._cancel(self._test_change_request) - - def test_09_cancel_cr(self): - self.assertListEqual( - [ - self._test_change_request.state, - self._test_change_request.cancelled_by_id.id, - self._test_change_request.date_cancelled, - ], - ["draft", False, False], - "Draft CR should not have cancelling info.!", - ) - self._test_change_request._cancel(self._test_change_request) - self.assertListEqual( - [ - self._test_change_request.state, - self._test_change_request.cancelled_by_id, - ], - ["cancelled", self.env.user], - "Cancelled CR should have cancelling info.!", - ) - self.assertLessEqual( - self._test_change_request.date_cancelled, - fields.Datetime.now(), - "Cancelled CR should have date cancelled info.!", - ) + def test_09_action_cancel(self): + """Test action_cancel method.""" + change_request = self._create_test_cr() + action = change_request.action_cancel() + self.assertEqual(action["res_model"], "spp.change.request.cancel.wizard") + self.assertEqual(action["context"]["change_request_id"], change_request.id) - def test_10_check_user_error_cr(self): - self._test_change_request.assign_to_id = None - with self.assertRaisesRegex(UserError, "^.*no user assigned.*$"): - self._test_change_request._check_user(process="Apply") - - def test_11_check_user_cr(self): - with self.assertRaisesRegex(UserError, "^You are not allowed.*$"): - self._test_change_request.with_user(2)._check_user(process="Apply") - self.assertTrue( - self._test_change_request._check_user(process="Apply"), - "Change request creator / assignee should have access!", - ) + def test_10_action_reject(self): + """Test action_reject method.""" + change_request = self._create_test_cr() + action = change_request.action_reject() + self.assertEqual(action["res_model"], "spp.change.request.reject.wizard") diff --git a/spp_change_request/tests/test_models.py b/spp_change_request/tests/test_models.py new file mode 100644 index 000000000..e3102af30 --- /dev/null +++ b/spp_change_request/tests/test_models.py @@ -0,0 +1,48 @@ +from odoo import api, fields, models + + +class TestCRType(models.Model): + _name = "test.cr.type2" + _inherit = "spp.change.request.source.mixin" + _description = "Test CR Type for Source Mixin" + _test = True # Mark this as a test model + + dms_directory_ids = fields.One2many( + "spp.dms.directory", + "change_request_test_cr_id", + string="DMS Directories", + auto_join=True, + ) + dms_file_ids = fields.One2many( + "spp.dms.file", + "change_request_test_cr_id", + string="DMS Files", + auto_join=True, + ) + + validation_ids = fields.Many2many("spp.change.request.validation.sequence", string="Validations") + + def update_live_data(self): + return + + @api.onchange("registrant_id") + def _onchange_registrant_id(self): + pass + + +class SPPDMSDirectory(models.Model): + _inherit = "spp.dms.directory" + + change_request_test_cr_id = fields.Many2one( + "test.cr.type2", + string="Change Request (Test CR Type)", + ) + + +class SPPDMSFile(models.Model): + _inherit = "spp.dms.file" + + change_request_test_cr_id = fields.Many2one( + "test.cr.type2", + string="Change Request (Test CR Type)", + ) diff --git a/spp_change_request/views/change_request_view.xml b/spp_change_request/views/change_request_view.xml index 86d7c321a..236c98b00 100644 --- a/spp_change_request/views/change_request_view.xml +++ b/spp_change_request/views/change_request_view.xml @@ -313,6 +313,7 @@ Part of OpenSPP. See LICENSE file for full copyright and licensing details. force_save="1" readonly="state != 'draft'" /> + diff --git a/spp_change_request_add_children_demo/views/change_request_add_children_view.xml b/spp_change_request_add_children_demo/views/change_request_add_children_view.xml index 794b918a3..18ad8a5e6 100644 --- a/spp_change_request_add_children_demo/views/change_request_add_children_view.xml +++ b/spp_change_request_add_children_demo/views/change_request_add_children_view.xml @@ -40,6 +40,7 @@
+ diff --git a/spp_dms/views/dms_file_views.xml b/spp_dms/views/dms_file_views.xml index 1d4df602f..7106f7971 100644 --- a/spp_dms/views/dms_file_views.xml +++ b/spp_dms/views/dms_file_views.xml @@ -45,6 +45,8 @@ + + diff --git a/spp_event_data/__init__.py b/spp_event_data/__init__.py index ebdc65039..9252b5be7 100644 --- a/spp_event_data/__init__.py +++ b/spp_event_data/__init__.py @@ -2,3 +2,4 @@ from . import models from . import wizard +from .tests import test_model_event_data_type diff --git a/spp_event_data/tests/__init__.py b/spp_event_data/tests/__init__.py index 11e66a378..bc0496a1c 100644 --- a/spp_event_data/tests/__init__.py +++ b/spp_event_data/tests/__init__.py @@ -1 +1,2 @@ from . import test_event_data +from . import test_create_event_wizard diff --git a/spp_event_data/tests/test_create_event_wizard.py b/spp_event_data/tests/test_create_event_wizard.py new file mode 100644 index 000000000..c655ce4a1 --- /dev/null +++ b/spp_event_data/tests/test_create_event_wizard.py @@ -0,0 +1,62 @@ +from datetime import date +from unittest.mock import patch + +from odoo.tests import TransactionCase + + +class TestCreateEventWizard(TransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.registrant = cls.env["res.partner"].create( + { + "name": "Test Registrant for Event", + "is_registrant": True, + } + ) + cls.mock_wizard_model_name = "spp.create.event.data.test.wizard" + cls.mock_model_name = "spp.event.data.test" + + def test_next_page_flow(self): + """Test the full flow of the create_event_wizard.""" + # Patch the selection field to include our mock model for the test + selection_patcher = patch.object( + type(self.env["spp.create.event.wizard"]).event_data_model, + "selection", + new=[("default", "None"), (self.mock_model_name, "Test Event")], + ) + + with selection_patcher: + # Create the wizard *after* the selection field has been patched + wizard = self.env["spp.create.event.wizard"].create( + { + "event_data_model": self.mock_model_name, + "partner_id": self.registrant.id, + "registrar": "Test Registrar", + "collection_date": date(2024, 1, 15), + "expiry_date": date(2025, 1, 15), + } + ) + + # Create a simple view for the mock wizard model + self.env["ir.ui.view"].create( + { + "name": "Test Event Data Wizard Form", + "type": "form", + "model": self.mock_wizard_model_name, + "arch_db": """""", + } + ) + action = wizard.next_page() + + # 1. Verify that an spp.event.data record was created with correct values + event_data = self.env["spp.event.data"].search([("partner_id", "=", self.registrant.id)]) + self.assertEqual(len(event_data), 1, "An spp.event.data record should have been created.") + self.assertEqual(event_data.model, self.mock_model_name) + self.assertEqual(event_data.registrar, "Test Registrar") + + # 2. Verify that the action to open the next wizard is correct + self.assertEqual(action["res_model"], self.mock_wizard_model_name) + self.assertEqual(action["view_mode"], "form") + self.assertEqual(action["target"], "new") + self.assertEqual(action["type"], "ir.actions.act_window") diff --git a/spp_event_data/tests/test_model_event_data_type.py b/spp_event_data/tests/test_model_event_data_type.py new file mode 100644 index 000000000..6e1cb13ee --- /dev/null +++ b/spp_event_data/tests/test_model_event_data_type.py @@ -0,0 +1,14 @@ +from odoo import fields, models + + +class TestEventDataType(models.Model): + _name = "spp.event.data.test" + _description = "Test Event Data Type" + + +class SPPCreateEventDataTestWizard(models.TransientModel): + _name = "spp.create.event.data.test.wizard" + _description = "Test Create Event Data Wizard" + + name = fields.Char(string="Name") + event_id = fields.Many2one("spp.event.data", string="Event Data", required=True) diff --git a/spp_idqueue/views/id_batch_view.xml b/spp_idqueue/views/id_batch_view.xml index aae7644fc..76ea3ee1d 100644 --- a/spp_idqueue/views/id_batch_view.xml +++ b/spp_idqueue/views/id_batch_view.xml @@ -405,7 +405,10 @@ list code action = records.multi_approve_batch() - + @@ -415,7 +418,10 @@ list code action = records.multi_generate_batch() - + @@ -425,7 +431,10 @@ list code action = records.multi_print_batch() - + @@ -435,7 +444,10 @@ list code action = records.multi_printed_batch() - + @@ -445,6 +457,9 @@ list code action = records.multi_distribute_batch() - + diff --git a/spp_idqueue/views/id_queue_view.xml b/spp_idqueue/views/id_queue_view.xml index 58eca71ee..bc904b47d 100644 --- a/spp_idqueue/views/id_queue_view.xml +++ b/spp_idqueue/views/id_queue_view.xml @@ -290,7 +290,10 @@ list code action = model.open_wizard() - + @@ -300,7 +303,10 @@ list code action = records.validate_requests() - + @@ -310,7 +316,10 @@ list code action = records.generate_validate_requests() - + @@ -320,7 +329,10 @@ list code action = records.print_requests() - + @@ -330,7 +342,10 @@ list code action = records.distribute_requests() - + diff --git a/spp_programs/tests/test_cycle.py b/spp_programs/tests/test_cycle.py index 1d39826c7..5fc77fb32 100644 --- a/spp_programs/tests/test_cycle.py +++ b/spp_programs/tests/test_cycle.py @@ -1,11 +1,9 @@ -from freezegun import freeze_time - +from odoo import fields from odoo.exceptions import ValidationError from .common import Common -@freeze_time("2024-07-19") class TestCycle(Common): def test_check_dates_constrains(self): with self.assertRaisesRegex(ValidationError, 'The "End Date" cannot be earlier than the "Start Date".'): @@ -21,3 +19,59 @@ def test_check_dates_constrains(self): "start_date": "2024-07-18", } ) + + def test_get_previous_and_next_cycle(self): + # The `cycle` from `Common` is created first. + # To test previous/next, we need to control creation order. + # Cycles are sorted by `create_date`. + + # Create a new program for this test to avoid interference from other tests + self.program = self.env["g2p.program"].create( + { + "name": "Test Program for Cycle Navigation", + } + ) + + # This will be the first cycle chronologically by create_date + first_cycle = self.env["g2p.cycle"].create( + { + "name": "First Cycle", + "program_id": self.program.id, + "start_date": fields.Date.today(), + "end_date": fields.Date.today(), + } + ) + + # This is the middle cycle, created after first_cycle + middle_cycle = self.env["g2p.cycle"].create( + { + "name": "Middle Cycle", + "program_id": self.program.id, + "start_date": fields.Date.today(), + "end_date": fields.Date.today(), + } + ) + + # The `self.cycle` from `setUp` is now the last one created. + # Let's rename it for clarity in this test. + last_cycle = self.env["g2p.cycle"].create( + { + "name": "Last Cycle", + "program_id": self.program.id, + "start_date": fields.Date.today(), + "end_date": fields.Date.today(), + } + ) + + self.assertIsNone(first_cycle.get_previous_cycle(), "First cycle should have no previous cycle.") + self.assertEqual( + first_cycle.get_next_cycle(), middle_cycle, "Next cycle for first_cycle should be middle_cycle." + ) + self.assertEqual( + middle_cycle.get_previous_cycle(), first_cycle, "Previous cycle for middle_cycle should be first_cycle." + ) + self.assertEqual(middle_cycle.get_next_cycle(), last_cycle, "Next cycle for middle_cycle should be last_cycle.") + self.assertEqual( + last_cycle.get_previous_cycle(), middle_cycle, "Previous cycle for last_cycle should be middle_cycle." + ) + self.assertIsNone(last_cycle.get_next_cycle(), "Last cycle should have no next cycle.") diff --git a/spp_programs/tests/test_entitlement.py b/spp_programs/tests/test_entitlement.py index aeccea703..80fee621f 100644 --- a/spp_programs/tests/test_entitlement.py +++ b/spp_programs/tests/test_entitlement.py @@ -249,3 +249,96 @@ def test_12_approve_entitlement(self): self.cycle.write({"state": "approved"}) with self.assertRaisesRegex(UserError, "No Entitlement Manager defined."): entitlement_id.approve_entitlement() + + def test_13_reject_entitlement_wizard(self): + """Test that `reject_entitlement` returns the correct wizard action.""" + entitlement_id = self.env["g2p.entitlement"].create( + { + "partner_id": self.registrant.id, + "initial_amount": 1.0, + "cycle_id": self.cycle.id, + "state": "draft", + "valid_until": fields.Date.add(fields.Date.today(), days=1), + } + ) + self.entitlement = entitlement_id + action = self.entitlement.reject_entitlement() + self.assertEqual(action["res_model"], "spp.reject.entitlement.wizard") + self.assertEqual(action["view_mode"], "form") + self.assertEqual(action["target"], "new") + self.assertIn("to_state", action["context"]) + self.assertEqual(action["context"]["to_state"], "reject") + + def test_14_internal_reject_entitlement(self): + """Test the internal `_reject_entitlement` method.""" + # Test rejection from a valid state ('draft') + entitlement_id = self.env["g2p.entitlement"].create( + { + "partner_id": self.registrant.id, + "initial_amount": 1.0, + "cycle_id": self.cycle.id, + "state": "draft", + "valid_until": fields.Date.add(fields.Date.today(), days=1), + } + ) + self.entitlement = entitlement_id + self.entitlement.state = "draft" + rejection_reason = "Invalid data provided." + + with patch("odoo.fields.Date.today", return_value=date(2024, 1, 1)): + action = self.entitlement._reject_entitlement(to_state="reject", reject_reason=rejection_reason) + + self.assertEqual(self.entitlement.state, "reject") + self.assertEqual(self.entitlement.rejected_reason, rejection_reason) + self.assertEqual(self.entitlement.date_rejected, date(2024, 1, 1)) + + # Check notification + self.assertEqual(action["type"], "ir.actions.client") + self.assertEqual(action["tag"], "display_notification") + self.assertEqual(action["params"]["type"], "danger") + self.assertEqual(action["params"]["message"], "Entitlement Rejected") + + # Test rejection from an invalid state ('approved') + self.entitlement.state = "approved" + self.entitlement._reject_entitlement(to_state="reject", reject_reason="Should not work") + self.assertEqual(self.entitlement.state, "approved", "Entitlement in 'approved' state should not be rejected.") + + def test_15_reset_to_pending_wizard(self): + """Test that `reset_to_pending` returns the correct wizard action.""" + entitlement_id = self.env["g2p.entitlement"].create( + { + "partner_id": self.registrant.id, + "initial_amount": 1.0, + "cycle_id": self.cycle.id, + "state": "draft", + "valid_until": fields.Date.add(fields.Date.today(), days=1), + } + ) + self.entitlement = entitlement_id + action = self.entitlement.reset_to_pending() + self.assertEqual(action["res_model"], "spp.reset.pending.entitlement.wizard") + self.assertEqual(action["view_mode"], "form") + self.assertEqual(action["target"], "new") + + def test_16_internal_reset_to_pending(self): + """Test the internal `_reset_to_pending` method.""" + entitlement_id = self.env["g2p.entitlement"].create( + { + "partner_id": self.registrant.id, + "initial_amount": 1.0, + "cycle_id": self.cycle.id, + "state": "draft", + "valid_until": fields.Date.add(fields.Date.today(), days=1), + } + ) + self.entitlement = entitlement_id + self.entitlement.state = "reject" + action = self.entitlement._reset_to_pending() + + self.assertEqual(self.entitlement.state, "pending_validation") + + # Check notification + self.assertEqual(action["type"], "ir.actions.client") + self.assertEqual(action["tag"], "display_notification") + self.assertEqual(action["params"]["type"], "success") + self.assertEqual(action["params"]["message"], "Entitlement Reset to Pending") diff --git a/spp_programs/views/cycle_view.xml b/spp_programs/views/cycle_view.xml index e7da744a9..60f9ba8a8 100644 --- a/spp_programs/views/cycle_view.xml +++ b/spp_programs/views/cycle_view.xml @@ -14,6 +14,20 @@ Part of OpenSPP. See LICENSE file for full copyright and licensing details. + + 0 + + + + + + view_cycle_membership_custom_tree + g2p.cycle.membership + + + + 0 + diff --git a/spp_programs/views/entitlement_view.xml b/spp_programs/views/entitlement_view.xml index 912b26350..4f10efce3 100644 --- a/spp_programs/views/entitlement_view.xml +++ b/spp_programs/views/entitlement_view.xml @@ -8,7 +8,7 @@ Part of OpenSPP. See LICENSE file for full copyright and licensing details. g2p.entitlement.inkind 1 - + diff --git a/spp_programs/views/g2p_entitlement_view.xml b/spp_programs/views/g2p_entitlement_view.xml index c11f279b1..dea22c309 100644 --- a/spp_programs/views/g2p_entitlement_view.xml +++ b/spp_programs/views/g2p_entitlement_view.xml @@ -11,6 +11,9 @@ + + 0 + diff --git a/spp_programs/views/programs_view.xml b/spp_programs/views/programs_view.xml index def4191e7..b58e5b316 100644 --- a/spp_programs/views/programs_view.xml +++ b/spp_programs/views/programs_view.xml @@ -166,4 +166,15 @@ Part of OpenSPP. See LICENSE file for full copyright and licensing details. + + + view_program_membership_custom_tree + g2p.program_membership + + + + 0 + + + diff --git a/spp_registry_approval/tests/__init__.py b/spp_registry_approval/tests/__init__.py new file mode 100644 index 000000000..d57d215f9 --- /dev/null +++ b/spp_registry_approval/tests/__init__.py @@ -0,0 +1 @@ +from . import test_res_partner diff --git a/spp_registry_approval/tests/test_res_partner.py b/spp_registry_approval/tests/test_res_partner.py new file mode 100644 index 000000000..44dc2ae5b --- /dev/null +++ b/spp_registry_approval/tests/test_res_partner.py @@ -0,0 +1,287 @@ +import logging + +from odoo.tests.common import TransactionCase + +_logger = logging.getLogger(__name__) + + +class SppRegistryApprovalTest(TransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + # Set context to avoid job queue delay + cls.env = cls.env( + context=dict( + cls.env.context, + test_queue_job_no_delay=True, + ) + ) + + # Get the security groups + cls.approve_group = cls.env.ref("spp_registry_approval.approve_registry") + cls.reject_group = cls.env.ref("spp_registry_approval.reject_registry") + cls.reset_group = cls.env.ref("spp_registry_approval.reset_to_draft_registry") + + # Create test users with different permissions + cls.user_with_approve = cls.env["res.users"].create( + { + "name": "User with Approve Permission", + "login": "approve_user", + "email": "approve@test.com", + "groups_id": [(6, 0, [cls.approve_group.id])], + } + ) + + cls.user_with_reject = cls.env["res.users"].create( + { + "name": "User with Reject Permission", + "login": "reject_user", + "email": "reject@test.com", + "groups_id": [(6, 0, [cls.reject_group.id])], + } + ) + + cls.user_with_reset = cls.env["res.users"].create( + { + "name": "User with Reset Permission", + "login": "reset_user", + "email": "reset@test.com", + "groups_id": [(6, 0, [cls.reset_group.id])], + } + ) + + cls.user_without_permissions = cls.env["res.users"].create( + { + "name": "User without Permissions", + "login": "no_perms_user", + "email": "noperms@test.com", + "groups_id": [(6, 0, [])], + } + ) + + # Create test registries + cls.registry_draft = cls.env["res.partner"].create( + { + "name": "Test Registry Draft", + "is_registrant": True, + "is_group": False, + } + ) + + cls.registry_approved = cls.env["res.partner"].create( + { + "name": "Test Registry Approved", + "is_registrant": True, + "is_group": False, + "state": "approved", + } + ) + + cls.registry_rejected = cls.env["res.partner"].create( + { + "name": "Test Registry Rejected", + "is_registrant": True, + "is_group": False, + "state": "rejected", + } + ) + + def test_01_default_state(self): + """Test that new registries default to draft state""" + new_registry = self.env["res.partner"].create( + { + "name": "New Test Registry", + "is_registrant": True, + "is_group": False, + } + ) + self.assertEqual( + new_registry.state, + "draft", + "New registries should default to draft state", + ) + + def test_02_approve_registry_with_permission(self): + """Test approve_registry method with proper permissions""" + # Test with user having approve permission + self.registry_draft.with_user(self.user_with_approve).approve_registry() + self.assertEqual( + self.registry_draft.state, + "approved", + "Registry should be approved when user has permission", + ) + + def test_03_approve_registry_without_permission(self): + """Test approve_registry method without proper permissions""" + # Reset to draft first + self.registry_draft.state = "draft" + + # Test with user without approve permission + self.registry_draft.with_user(self.user_without_permissions).approve_registry() + self.assertEqual( + self.registry_draft.state, + "draft", + "Registry should remain draft when user lacks permission", + ) + + def test_04_reject_registry_with_permission(self): + """Test reject_registry method with proper permissions""" + # Test with user having reject permission + self.registry_draft.with_user(self.user_with_reject).reject_registry() + self.assertEqual( + self.registry_draft.state, + "rejected", + "Registry should be rejected when user has permission", + ) + + def test_05_reject_registry_without_permission(self): + """Test reject_registry method without proper permissions""" + # Reset to draft first + self.registry_draft.state = "draft" + + # Test with user without reject permission + self.registry_draft.with_user(self.user_without_permissions).reject_registry() + self.assertEqual( + self.registry_draft.state, + "draft", + "Registry should remain draft when user lacks permission", + ) + + def test_06_reset_to_draft_registry_with_permission(self): + """Test reset_to_draft_registry method with proper permissions""" + # Test with user having reset permission + self.registry_approved.with_user(self.user_with_reset).reset_to_draft_registry() + self.assertEqual( + self.registry_approved.state, + "draft", + "Registry should be reset to draft when user has permission", + ) + + def test_07_reset_to_draft_registry_without_permission(self): + """Test reset_to_draft_registry method without proper permissions""" + # Reset to approved first + self.registry_approved.state = "approved" + + # Test with user without reset permission + self.registry_approved.with_user(self.user_without_permissions).reset_to_draft_registry() + self.assertEqual( + self.registry_approved.state, + "approved", + "Registry should remain approved when user lacks permission", + ) + + def test_08_multiple_records_approval(self): + """Test approve_registry method with multiple records""" + # Create multiple registries + registries = self.env["res.partner"].create( + [ + { + "name": "Registry 1", + "is_registrant": True, + "is_group": False, + "state": "draft", + }, + { + "name": "Registry 2", + "is_registrant": True, + "is_group": False, + "state": "draft", + }, + ] + ) + + # Approve all registries + registries.with_user(self.user_with_approve).approve_registry() + + # Verify all are approved + for registry in registries: + self.assertEqual( + registry.state, + "approved", + f"Registry {registry.name} should be approved", + ) + + def test_09_state_constants(self): + """Test that state constants are properly defined""" + self.assertEqual( + self.env["res.partner"].DRAFT, + "draft", + "DRAFT constant should be 'draft'", + ) + self.assertEqual( + self.env["res.partner"].APPROVED, + "approved", + "APPROVED constant should be 'approved'", + ) + self.assertEqual( + self.env["res.partner"].REJECTED, + "rejected", + "REJECTED constant should be 'rejected'", + ) + + def test_10_edge_case_empty_recordset(self): + """Test methods with empty recordset""" + empty_recordset = self.env["res.partner"].browse([]) + + # These should not raise errors + empty_recordset.with_user(self.user_with_approve).approve_registry() + empty_recordset.with_user(self.user_with_reject).reject_registry() + empty_recordset.with_user(self.user_with_reset).reset_to_draft_registry() + + def test_11_mixed_permissions_user(self): + """Test user with multiple permissions""" + # Create user with multiple permissions + user_multi = self.env["res.users"].create( + { + "name": "User with Multiple Permissions", + "login": "multi_user", + "email": "multi@test.com", + "groups_id": [(6, 0, [self.approve_group.id, self.reject_group.id, self.reset_group.id])], + } + ) + + # Test all operations with multi-permission user + registry = self.env["res.partner"].create( + { + "name": "Multi Test Registry", + "is_registrant": True, + "is_group": False, + "state": "draft", + } + ) + + # Approve + registry.with_user(user_multi).approve_registry() + self.assertEqual(registry.state, "approved") + + # Reject + registry.with_user(user_multi).reject_registry() + self.assertEqual(registry.state, "rejected") + + # Reset to draft + registry.with_user(user_multi).reset_to_draft_registry() + self.assertEqual(registry.state, "draft") + + def test_12_sudo_usage(self): + """Test that sudo() is properly used in methods""" + # This test verifies that the methods use sudo() correctly + # by checking that the state changes are applied even when + # the user doesn't have direct write access to the record + + # Create a registry with restricted access + registry = self.env["res.partner"].create( + { + "name": "Restricted Registry", + "is_registrant": True, + "is_group": False, + "state": "draft", + } + ) + + # Test that sudo() allows the operation to succeed + registry.with_user(self.user_with_approve).approve_registry() + self.assertEqual( + registry.state, + "approved", + "sudo() should allow approval even with restricted access", + ) diff --git a/spp_user_roles/security/ir.model.access.csv b/spp_user_roles/security/ir.model.access.csv index 8b0b5a06c..9d7a66b36 100644 --- a/spp_user_roles/security/ir.model.access.csv +++ b/spp_user_roles/security/ir.model.access.csv @@ -2,3 +2,6 @@ id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink spp_res_users_local_registrar,SPP Users Local Registrar Access,base.model_res_users,spp_user_roles.group_local_registrar,1,0,0,0 spp_res_users_role_local_registrar,SPP Users Role Local Registrar Access,base_user_role.model_res_users_role,spp_user_roles.group_local_registrar,1,0,0,0 spp_res_users_role_line_local_registrar,SPP Users Role Lines Local Registrar Access,base_user_role.model_res_users_role_line,spp_user_roles.group_local_registrar,1,0,0,0 + +spp_res_users_role_admin,SPP Users Role Admin Access,base_user_role.model_res_users_role,g2p_registry_base.group_g2p_admin,1,1,1,1 +spp_res_users_role_line_admin,SPP Users Role Lines Admin Access,base_user_role.model_res_users_role_line,g2p_registry_base.group_g2p_admin,1,1,1,1