diff --git a/spp_change_request_v2/__manifest__.py b/spp_change_request_v2/__manifest__.py index f31ac32a..a7a41763 100644 --- a/spp_change_request_v2/__manifest__.py +++ b/spp_change_request_v2/__manifest__.py @@ -1,6 +1,6 @@ { "name": "OpenSPP Change Request V2", - "version": "19.0.2.0.1", + "version": "19.0.2.0.2", "sequence": 50, "category": "OpenSPP", "summary": "Configuration-driven change request system with UX improvements, conflict detection and duplicate prevention", diff --git a/spp_change_request_v2/tests/test_ux_wizards.py b/spp_change_request_v2/tests/test_ux_wizards.py index ea8cca2c..dfebd1a5 100644 --- a/spp_change_request_v2/tests/test_ux_wizards.py +++ b/spp_change_request_v2/tests/test_ux_wizards.py @@ -225,9 +225,108 @@ def test_wizard_onchange_clears_mismatched_registrant(self): self.assertFalse(wizard.registrant_id) +@tagged("post_install", "-at_install", "cr_ux") +class TestBatchApprovalWizardBase(TestChangeRequestBase): + """Tests for batch approval wizard that don't require pending CRs.""" + + def _create_wizard(self, cr_ids, action_type="approve", **kwargs): + """Helper to create a batch wizard via create_from_selection.""" + result = ( + self.env["spp.cr.batch.approval.wizard"].with_context(active_ids=cr_ids).create_from_selection(action_type) + ) + wizard = self.env["spp.cr.batch.approval.wizard"].browse(result["res_id"]) + if kwargs: + wizard.write(kwargs) + return wizard + + def test_create_from_selection_no_active_ids(self): + """Test create_from_selection raises error with no active_ids.""" + with self.assertRaises(UserError): + self.env["spp.cr.batch.approval.wizard"].create_from_selection("approve") + + def test_create_from_selection_returns_action(self): + """Test create_from_selection returns a valid window action.""" + cr_type = self.env["spp.change.request.type"].search([], limit=1) + if not cr_type: + self.skipTest("No CR type available") + draft_cr = self.env["spp.change.request"].create( + {"request_type_id": cr_type.id, "registrant_id": self.group.id} + ) + result = ( + self.env["spp.cr.batch.approval.wizard"] + .with_context(active_ids=[draft_cr.id]) + .create_from_selection("approve") + ) + self.assertEqual(result["type"], "ir.actions.act_window") + self.assertEqual(result["res_model"], "spp.cr.batch.approval.wizard") + self.assertEqual(result["target"], "new") + self.assertTrue(result["res_id"]) + + def test_create_from_selection_action_types(self): + """Test create_from_selection sets action_type correctly.""" + cr_type = self.env["spp.change.request.type"].search([], limit=1) + if not cr_type: + self.skipTest("No CR type available") + draft_cr = self.env["spp.change.request"].create( + {"request_type_id": cr_type.id, "registrant_id": self.group.id} + ) + for action_type in ("approve", "reject", "revision"): + result = ( + self.env["spp.cr.batch.approval.wizard"] + .with_context(active_ids=[draft_cr.id]) + .create_from_selection(action_type) + ) + wizard = self.env["spp.cr.batch.approval.wizard"].browse(result["res_id"]) + self.assertEqual(wizard.action_type, action_type) + + def test_error_message_not_pending(self): + """Test error message for CR not in pending state.""" + cr_type = self.env["spp.change.request.type"].search([], limit=1) + if not cr_type: + self.skipTest("No CR type available") + draft_cr = self.env["spp.change.request"].create( + {"request_type_id": cr_type.id, "registrant_id": self.group.id} + ) + wizard = self._create_wizard([draft_cr.id]) + line = wizard.line_ids[0] + self.assertFalse(line.can_process) + self.assertIn("Not pending approval", line.error_message) + + def test_batch_approve_requires_valid_crs(self): + """Test batch approve fails if no valid CRs.""" + cr_type = self.env["spp.change.request.type"].search([], limit=1) + if not cr_type: + self.skipTest("No CR type available") + draft_cr = self.env["spp.change.request"].create( + {"request_type_id": cr_type.id, "registrant_id": self.group.id} + ) + wizard = self._create_wizard([draft_cr.id]) + self.assertEqual(wizard.valid_count, 0) + with self.assertRaises(UserError): + wizard.action_confirm() + + def test_batch_wizard_line_removal(self): + """Test that lines can be removed from a saved wizard.""" + cr_type = self.env["spp.change.request.type"].search([], limit=1) + if not cr_type: + self.skipTest("No CR type available") + crs = self.env["spp.change.request"] + for _i in range(3): + crs |= self.env["spp.change.request"].create( + {"request_type_id": cr_type.id, "registrant_id": self.group.id} + ) + wizard = self._create_wizard(crs.ids) + self.assertEqual(len(wizard.line_ids), 3) + + # Remove one line + wizard.line_ids[0].unlink() + wizard.invalidate_recordset() + self.assertEqual(len(wizard.line_ids), 2) + + @tagged("post_install", "-at_install", "cr_ux") class TestBatchApprovalWizard(TestChangeRequestBase): - """Tests for the batch approval wizard.""" + """Tests for batch approval wizard that require pending CRs.""" @classmethod def setUpClass(cls): @@ -237,7 +336,6 @@ def setUpClass(cls): def setUp(self): super().setUp() - # Create multiple pending CRs for batch testing cr_type = self.env["spp.change.request.type"].search([("approval_definition_id", "!=", False)], limit=1) if not cr_type: @@ -254,56 +352,57 @@ def setUp(self): cr.action_submit_for_approval() self.pending_crs |= cr + def _create_wizard(self, cr_ids, action_type="approve", **kwargs): + """Helper to create a batch wizard via create_from_selection.""" + result = ( + self.env["spp.cr.batch.approval.wizard"].with_context(active_ids=cr_ids).create_from_selection(action_type) + ) + wizard = self.env["spp.cr.batch.approval.wizard"].browse(result["res_id"]) + if kwargs: + wizard.write(kwargs) + return wizard + def test_batch_wizard_initialization(self): """Test batch wizard initializes with selected CRs.""" - wizard = self.env["spp.cr.batch.approval.wizard"].with_context(active_ids=self.pending_crs.ids).create({}) + wizard = self._create_wizard(self.pending_crs.ids) self.assertEqual(wizard.total_count, 3) self.assertEqual(len(wizard.line_ids), 3) def test_batch_wizard_counts(self): """Test batch wizard computes valid/invalid counts correctly.""" - wizard = self.env["spp.cr.batch.approval.wizard"].with_context(active_ids=self.pending_crs.ids).create({}) + wizard = self._create_wizard(self.pending_crs.ids) # All should be valid if user can approve self.assertEqual(wizard.total_count, wizard.valid_count + wizard.invalid_count) - def test_batch_approve_requires_valid_crs(self): - """Test batch approve fails if no valid CRs.""" - # Create wizard with draft CR (cannot be approved) - cr_type = self.env["spp.change.request.type"].search([], limit=1) - draft_cr = self.env["spp.change.request"].create( - { - "request_type_id": cr_type.id, - "registrant_id": self.group.id, - } - ) - - wizard = self.env["spp.cr.batch.approval.wizard"].with_context(active_ids=[draft_cr.id]).create({}) - - # Should have 0 valid CRs - self.assertEqual(wizard.valid_count, 0) + def test_batch_reject_requires_comment(self): + """Test batch reject requires a reason.""" + wizard = self._create_wizard(self.pending_crs.ids, action_type="reject", comment="") + # Should fail without comment with self.assertRaises(UserError): wizard.action_confirm() - def test_batch_reject_requires_comment(self): - """Test batch reject requires a reason.""" - wizard = ( - self.env["spp.cr.batch.approval.wizard"] - .with_context(active_ids=self.pending_crs.ids) - .create( - { - "action_type": "reject", - "comment": "", - } - ) - ) + def test_batch_revision_requires_comment(self): + """Test batch revision requires notes.""" + wizard = self._create_wizard(self.pending_crs.ids, action_type="revision", comment="") - # Should fail without comment with self.assertRaises(UserError): wizard.action_confirm() + def test_error_message_mixed_crs(self): + """Test error messages with mix of pending and draft CRs.""" + cr_type = self.env["spp.change.request.type"].search([], limit=1) + draft_cr = self.env["spp.change.request"].create( + {"request_type_id": cr_type.id, "registrant_id": self.group.id} + ) + wizard = self._create_wizard(self.pending_crs.ids + [draft_cr.id]) + self.assertTrue(wizard.total_count > 0) + invalid_lines = wizard.line_ids.filtered(lambda ln: not ln.can_process) + for line in invalid_lines: + self.assertTrue(line.error_message) + @tagged("post_install", "-at_install", "cr_ux") class TestConflictComparisonWizard(TestChangeRequestBase): diff --git a/spp_change_request_v2/views/batch_approval_wizard_views.xml b/spp_change_request_v2/views/batch_approval_wizard_views.xml index a133c161..a535b84b 100644 --- a/spp_change_request_v2/views/batch_approval_wizard_views.xml +++ b/spp_change_request_v2/views/batch_approval_wizard_views.xml @@ -95,7 +95,7 @@ - + -