From 1dc6c8b2ed94bce4d11003290d1c3d1cfe919306 Mon Sep 17 00:00:00 2001 From: emjay0921 Date: Fri, 6 Feb 2026 15:53:44 +0800 Subject: [PATCH 1/4] fix(spp_programs): reliably close wizard modal after program creation --- spp_programs/tests/test_create_program_wiz.py | 18 ++-- .../tests/test_spp_cycle_compliance.py | 2 +- ...st_spp_program_create_wizard_compliance.py | 4 +- spp_programs/wizard/create_program_wizard.py | 95 +++++++++---------- .../wizard/create_program_wizard_cel.py | 2 +- .../create_program_wizard_compliance.py | 2 +- 6 files changed, 61 insertions(+), 62 deletions(-) diff --git a/spp_programs/tests/test_create_program_wiz.py b/spp_programs/tests/test_create_program_wiz.py index 70527bba..6562cb11 100644 --- a/spp_programs/tests/test_create_program_wiz.py +++ b/spp_programs/tests/test_create_program_wiz.py @@ -34,7 +34,7 @@ def setUp(self): } ) - self.program = self._program_create_wiz.create_program() + self._program_action = self._program_create_wiz.create_program() # self.cycle_manager_default = ( # self._program_create_wiz.create_cycle_manager_default(self.program.id) # ) @@ -69,19 +69,19 @@ def test_02_create_program(self): ) res = new_wiz.create_program() self.assertEqual(type(res), dict, "Action should be in json format!") - for key in ("type", "res_model", "res_id"): + for key in ("type", "tag", "params"): self.assertIn(key, res.keys(), f"Key `{key}` is missing!") self.assertEqual( res["type"], - "ir.actions.act_window", + "ir.actions.client", "Action for program should be returned!", ) - self.assertEqual(res["res_model"], "spp.program", "Action for program should be return!") - self.assertTrue(res["res_id"], "New record for program should be existed!") + self.assertEqual(res["tag"], "open_program_close_modal", "Client action tag should match!") + self.assertTrue(res["params"]["program_id"], "New record for program should be existed!") def test_03_get_eligibility_manager(self): self._program_create_wiz.eligibility_type = "default_eligibility" - res = self._program_create_wiz._get_eligibility_manager(self.program["res_id"]) + res = self._program_create_wiz._get_eligibility_manager(self._program_action["params"]["program_id"]) self.assertIn("eligibility_managers", res) @@ -115,7 +115,7 @@ def test_06_create_program(self): "entitlement_item_ids": [Command.create({"product_id": self.product.id, "quantity": 1})], } ) - program = new_wiz.create_program() + action = new_wiz.create_program() - self.assertEqual(program["res_model"], "spp.program") - self.assertIsNotNone(program) + self.assertEqual(action["type"], "ir.actions.client") + self.assertTrue(action["params"]["program_id"]) diff --git a/spp_programs/tests/test_spp_cycle_compliance.py b/spp_programs/tests/test_spp_cycle_compliance.py index ba9f0276..6a8ff8dd 100644 --- a/spp_programs/tests/test_spp_cycle_compliance.py +++ b/spp_programs/tests/test_spp_cycle_compliance.py @@ -21,7 +21,7 @@ def setUpClass(cls): } ) action = cls._test.create_program() - cls.program = cls.env["spp.program"].browse(action["res_id"]) + cls.program = cls.env["spp.program"].browse(action["params"]["program_id"]) @classmethod def _create_individual(self, vals): diff --git a/spp_programs/tests/test_spp_program_create_wizard_compliance.py b/spp_programs/tests/test_spp_program_create_wizard_compliance.py index d49ae247..cbf28cd2 100644 --- a/spp_programs/tests/test_spp_program_create_wizard_compliance.py +++ b/spp_programs/tests/test_spp_program_create_wizard_compliance.py @@ -12,7 +12,7 @@ def test_01_create_program_without_compliance_manager(self): wizard = self.program_create_wizard({}) wizard._check_compliance_manager_info() action = wizard.create_program() - program = self.env["spp.program"].browse(action["res_id"]) + program = self.env["spp.program"].browse(action["params"]["program_id"]) self.assertFalse( bool(program.compliance_manager_ids), "Should not create compliance manager for new program!", @@ -38,7 +38,7 @@ def test_03_create_program_default_compliance_manager(self): } ) action = wizard.create_program() - program = self.env["spp.program"].browse(action["res_id"]) + program = self.env["spp.program"].browse(action["params"]["program_id"]) self.assertTrue( bool(program.compliance_manager_ids), "Should create compliance manager for new program!", diff --git a/spp_programs/wizard/create_program_wizard.py b/spp_programs/wizard/create_program_wizard.py index 6c269c98..2c16b4ee 100644 --- a/spp_programs/wizard/create_program_wizard.py +++ b/spp_programs/wizard/create_program_wizard.py @@ -366,72 +366,71 @@ def _insert_domain_operator(self, domain): return new_domain def create_program(self): + self.ensure_one() self._check_required_fields() - for rec in self: - program_vals = rec.get_program_vals() - program = self.env["spp.program"].with_context(skip_default_managers=True).create(program_vals) - program_id = program.id - vals = {} + program_vals = self.get_program_vals() + program = self.env["spp.program"].with_context(skip_default_managers=True).create(program_vals) - # Set Default Eligibility Manager settings - vals.update(rec._get_eligibility_manager(program_id)) + program_id = program.id + vals = {} - # Set Default Cycle Manager settings - # Add a new record to default cycle manager model + # Set Default Eligibility Manager settings + vals.update(self._get_eligibility_manager(program_id)) - cycle_manager_default_val = rec.get_cycle_manager_default_val(program_id) - def_mgr = self.env["spp.cycle.manager.default"].create(cycle_manager_default_val) + # Set Default Cycle Manager settings + # Add a new record to default cycle manager model - # Add a new record to cycle manager parent model + cycle_manager_default_val = self.get_cycle_manager_default_val(program_id) + def_mgr = self.env["spp.cycle.manager.default"].create(cycle_manager_default_val) - cycle_manager_val = rec.get_cycle_manager_val(program_id, def_mgr) - mgr = self.env["spp.cycle.manager"].create(cycle_manager_val) + # Add a new record to cycle manager parent model - vals.update({"cycle_manager_ids": [(4, mgr.id)]}) + cycle_manager_val = self.get_cycle_manager_val(program_id, def_mgr) + mgr = self.env["spp.cycle.manager"].create(cycle_manager_val) - # Set Default Entitlement Manager - vals.update(rec._get_entitlement_manager(program_id)) + vals.update({"cycle_manager_ids": [(4, mgr.id)]}) - # Set Default Program Manager - vals.update(rec._get_program_manager(program_id)) + # Set Default Entitlement Manager + vals.update(self._get_entitlement_manager(program_id)) - # Clean legacy aliases that are not real fields on spp.program - vals.pop("eligibility_managers", None) - vals.pop("cycle_managers", None) - # Convert legacy entitlement_managers key to entitlement_manager_ids - if "entitlement_managers" in vals: - entitlement_managers = vals.pop("entitlement_managers") - if "entitlement_manager_ids" not in vals: - vals["entitlement_manager_ids"] = entitlement_managers + # Set Default Program Manager + vals.update(self._get_program_manager(program_id)) - vals.update({"is_one_time_distribution": rec.is_one_time_distribution}) + # Clean legacy aliases that are not real fields on spp.program + vals.pop("eligibility_managers", None) + vals.pop("cycle_managers", None) + # Convert legacy entitlement_managers key to entitlement_manager_ids + if "entitlement_managers" in vals: + entitlement_managers = vals.pop("entitlement_managers") + if "entitlement_manager_ids" not in vals: + vals["entitlement_manager_ids"] = entitlement_managers - # Complete the program data - program.update(vals) + vals.update({"is_one_time_distribution": self.is_one_time_distribution}) - if rec.import_beneficiaries == "yes" or rec.is_one_time_distribution: - rec.program_wizard_import_beneficiaries(program) + # Complete the program data + program.update(vals) - if rec.is_one_time_distribution: - program.create_new_cycle() + if self.import_beneficiaries == "yes" or self.is_one_time_distribution: + self.program_wizard_import_beneficiaries(program) - view_id = self.env.ref("spp_programs.view_program_list_form") - if rec.view_id: - view_id = rec.view_id + if self.is_one_time_distribution: + program.create_new_cycle() - program.view_id = view_id.id + view_id = self.env.ref("spp_programs.view_program_list_form") + if self.view_id: + view_id = self.view_id - # Open the newly created program - return { - "name": _("Programs"), - "view_mode": "form", - "res_model": "spp.program", - "res_id": program_id, - "view_id": view_id.id, - "type": "ir.actions.act_window", - "target": "current", - } + program.view_id = view_id.id + + # Close modal and open program form using client action with async/await + return { + "type": "ir.actions.client", + "tag": "open_program_close_modal", + "params": { + "program_id": program_id, + }, + } def _get_default_eligibility_manager_val(self, program_id): return { diff --git a/spp_programs/wizard/create_program_wizard_cel.py b/spp_programs/wizard/create_program_wizard_cel.py index 9a810ed6..c028000b 100644 --- a/spp_programs/wizard/create_program_wizard_cel.py +++ b/spp_programs/wizard/create_program_wizard_cel.py @@ -289,7 +289,7 @@ def create_program(self): # Create dedicated compliance manager if CEL compliance is enabled if self.enable_compliance_cel and self.compliance_cel_expression: - program = self.env["spp.program"].browse(action["res_id"]) + program = self.env["spp.program"].browse(action["params"]["program_id"]) self._create_cel_compliance_manager(program) return action diff --git a/spp_programs/wizard/create_program_wizard_compliance.py b/spp_programs/wizard/create_program_wizard_compliance.py index 162d361f..b3338c02 100644 --- a/spp_programs/wizard/create_program_wizard_compliance.py +++ b/spp_programs/wizard/create_program_wizard_compliance.py @@ -30,7 +30,7 @@ def _check_required_fields(self): def create_program(self): action = super().create_program() if self.enable_compliance_verification: - program = self.env["spp.program"].browse(action["res_id"]) + program = self.env["spp.program"].browse(action["params"]["program_id"]) self._create_compliance_manager(program) return action From cec6a1819de16ec5d50b6dad048557d7307ed04d Mon Sep 17 00:00:00 2001 From: emjay0921 Date: Fri, 6 Feb 2026 15:58:46 +0800 Subject: [PATCH 2/4] fix(spp_programs): hide Geographic Targeting from Eligibility Manager UI --- spp_programs/views/managers/eligibility_manager_view.xml | 2 +- spp_programs/wizard/create_program_wizard.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/spp_programs/views/managers/eligibility_manager_view.xml b/spp_programs/views/managers/eligibility_manager_view.xml index 61f88970..4fd39a5d 100644 --- a/spp_programs/views/managers/eligibility_manager_view.xml +++ b/spp_programs/views/managers/eligibility_manager_view.xml @@ -67,7 +67,7 @@ Part of OpenSPP. See LICENSE file for full copyright and licensing details. - + - + Date: Fri, 6 Feb 2026 16:14:08 +0800 Subject: [PATCH 3/4] fix(spp_programs): pass translated name and view_id to client action --- spp_programs/static/src/js/create_program.js | 6 ++++-- spp_programs/wizard/create_program_wizard.py | 2 ++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/spp_programs/static/src/js/create_program.js b/spp_programs/static/src/js/create_program.js index ac0dffa9..a7a1500b 100644 --- a/spp_programs/static/src/js/create_program.js +++ b/spp_programs/static/src/js/create_program.js @@ -15,6 +15,8 @@ import {useService} from "@web/core/utils/hooks"; async function openProgramCloseModal(env, action) { const actionService = env.services.action; const programId = action.params?.program_id; + const programName = action.params?.name || "Program"; + const viewId = action.params?.view_id || false; await actionService.doAction( {type: "ir.actions.act_window_close"}, @@ -24,10 +26,10 @@ async function openProgramCloseModal(env, action) { if (programId) { await actionService.doAction({ type: "ir.actions.act_window", - name: "Program", + name: programName, res_model: "spp.program", res_id: programId, - views: [[false, "form"]], + views: [[viewId, "form"]], target: "current", }); } diff --git a/spp_programs/wizard/create_program_wizard.py b/spp_programs/wizard/create_program_wizard.py index 2c16b4ee..68ffee02 100644 --- a/spp_programs/wizard/create_program_wizard.py +++ b/spp_programs/wizard/create_program_wizard.py @@ -429,6 +429,8 @@ def create_program(self): "tag": "open_program_close_modal", "params": { "program_id": program_id, + "name": _("Program"), + "view_id": view_id.id, }, } From 6fe3cc15d2f6c6b138e084737f6e45d1047efa2e Mon Sep 17 00:00:00 2001 From: emjay0921 Date: Wed, 11 Feb 2026 11:18:04 +0800 Subject: [PATCH 4/4] fix(spp_change_request_v2): reliably close wizard modal after CR creation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Use ir.actions.client with async/await pattern to close the wizard modal before navigating to the CR detail form. Same approach as spp_programs wizard — prevents the modal from staying open on slow connections. --- .../static/src/js/create_change_request.js | 30 +++++++++++++ .../wizards/create_wizard.py | 43 ++++++++++--------- 2 files changed, 53 insertions(+), 20 deletions(-) diff --git a/spp_change_request_v2/static/src/js/create_change_request.js b/spp_change_request_v2/static/src/js/create_change_request.js index 9bcf63b7..fca558cc 100644 --- a/spp_change_request_v2/static/src/js/create_change_request.js +++ b/spp_change_request_v2/static/src/js/create_change_request.js @@ -4,9 +4,39 @@ import {FormController} from "@web/views/form/form_controller"; import {ListController} from "@web/views/list/list_controller"; import {onWillStart} from "@odoo/owl"; import {patch} from "@web/core/utils/patch"; +import {registry} from "@web/core/registry"; import {useService} from "@web/core/utils/hooks"; import {user} from "@web/core/user"; +/** + * Client action to close wizard modal and then open CR detail form. + * Uses async/await to ensure the modal is fully closed before navigating. + */ +async function openCRCloseModal(env, action) { + const actionService = env.services.action; + const params = action.params || {}; + + await actionService.doAction( + {type: "ir.actions.act_window_close"}, + {clearBreadcrumbs: true} + ); + + if (params.res_id) { + await actionService.doAction({ + type: "ir.actions.act_window", + name: params.name || "Change Request Details", + res_model: params.res_model, + res_id: params.res_id, + view_mode: "form", + views: [[params.view_id || false, "form"]], + target: "current", + context: params.context || {}, + }); + } +} + +registry.category("actions").add("open_cr_close_modal", openCRCloseModal); + patch(ListController.prototype, { setup() { super.setup(); diff --git a/spp_change_request_v2/wizards/create_wizard.py b/spp_change_request_v2/wizards/create_wizard.py index d7f6ce38..811f2095 100644 --- a/spp_change_request_v2/wizards/create_wizard.py +++ b/spp_change_request_v2/wizards/create_wizard.py @@ -232,35 +232,38 @@ def action_create_draft(self): } ) - # Open the detail form directly for editing + # Close wizard modal and open detail form using client action + # The client action ensures the modal is fully closed before navigating detail = cr.get_detail() if detail: view_id = self.request_type_id.get_detail_form_view_id() return { - "type": "ir.actions.act_window", - "name": "Change Request Details", - "res_model": cr.detail_res_model, - "res_id": detail.id, - "view_mode": "form", - "view_id": view_id, - "target": "current", - "context": { - "create": False, - "delete": False, - "form_view_initial_mode": "edit", + "type": "ir.actions.client", + "tag": "open_cr_close_modal", + "params": { + "name": _("Change Request Details"), + "res_model": cr.detail_res_model, + "res_id": detail.id, + "view_id": view_id, + "context": { + "create": False, + "delete": False, + "form_view_initial_mode": "edit", + }, }, } # Fallback: open CR form if no detail model configured return { - "type": "ir.actions.act_window", - "name": "Change Request", - "res_model": "spp.change.request", - "res_id": cr.id, - "view_mode": "form", - "target": "current", - "context": { - "form_view_initial_mode": "edit", + "type": "ir.actions.client", + "tag": "open_cr_close_modal", + "params": { + "name": _("Change Request"), + "res_model": "spp.change.request", + "res_id": cr.id, + "context": { + "form_view_initial_mode": "edit", + }, }, }