From 360c5dc258c068be1f3a420b190c57f60d9190d2 Mon Sep 17 00:00:00 2001 From: joshua160miller Date: Tue, 4 Feb 2025 09:47:59 +0530 Subject: [PATCH] Added Priority List --- README.md | 3 +- g2p_programs_priority_list/README.md | 3 + g2p_programs_priority_list/__init__.py | 2 + g2p_programs_priority_list/__manifest__.py | 28 ++++ g2p_programs_priority_list/models/__init__.py | 6 + g2p_programs_priority_list/models/cycle.py | 20 +++ .../models/cycle_manager.py | 152 ++++++++++++++++++ .../models/cycle_membership.py | 7 + .../models/program_manager.py | 48 ++++++ g2p_programs_priority_list/models/programs.py | 76 +++++++++ .../models/sorting_criteria.py | 17 ++ g2p_programs_priority_list/pyproject.toml | 3 + .../security/ir.model.access.csv | 12 ++ .../static/description/icon.png | Bin 0 -> 3985 bytes .../static/src/css/style.css | 25 +++ .../views/cycle_manager_view.xml | 28 ++++ .../views/cycle_membership_view.xml | 16 ++ .../views/cycle_view.xml | 32 ++++ .../views/program_manager_view.xml | 16 ++ g2p_programs_priority_list/wizard/__init__.py | 4 + .../wizard/create_cycle_wizard.py | 61 +++++++ .../wizard/create_cycle_wizard.xml | 44 +++++ .../wizard/create_sorting_wizard.py | 15 ++ 23 files changed, 617 insertions(+), 1 deletion(-) create mode 100644 g2p_programs_priority_list/README.md create mode 100644 g2p_programs_priority_list/__init__.py create mode 100644 g2p_programs_priority_list/__manifest__.py create mode 100644 g2p_programs_priority_list/models/__init__.py create mode 100644 g2p_programs_priority_list/models/cycle.py create mode 100644 g2p_programs_priority_list/models/cycle_manager.py create mode 100644 g2p_programs_priority_list/models/cycle_membership.py create mode 100644 g2p_programs_priority_list/models/program_manager.py create mode 100644 g2p_programs_priority_list/models/programs.py create mode 100644 g2p_programs_priority_list/models/sorting_criteria.py create mode 100644 g2p_programs_priority_list/pyproject.toml create mode 100644 g2p_programs_priority_list/security/ir.model.access.csv create mode 100644 g2p_programs_priority_list/static/description/icon.png create mode 100644 g2p_programs_priority_list/static/src/css/style.css create mode 100644 g2p_programs_priority_list/views/cycle_manager_view.xml create mode 100644 g2p_programs_priority_list/views/cycle_membership_view.xml create mode 100644 g2p_programs_priority_list/views/cycle_view.xml create mode 100644 g2p_programs_priority_list/views/program_manager_view.xml create mode 100644 g2p_programs_priority_list/wizard/__init__.py create mode 100644 g2p_programs_priority_list/wizard/create_cycle_wizard.py create mode 100644 g2p_programs_priority_list/wizard/create_cycle_wizard.xml create mode 100644 g2p_programs_priority_list/wizard/create_sorting_wizard.py diff --git a/README.md b/README.md index 4e536b48..7c584fef 100644 --- a/README.md +++ b/README.md @@ -46,8 +46,9 @@ addon | version | maintainers | summary [g2p_program_registrant_info](g2p_program_registrant_info/) | 17.0.1.3.0 | | G2P Program: Registrant Info [g2p_program_reimbursement](g2p_program_reimbursement/) | 17.0.1.3.0 | | OpenG2P Programs: Reimbursement [g2p_programs](g2p_programs/) | 17.0.1.3.0 | | OpenG2P Programs +[g2p_programs_priority_list](g2p_programs_priority_list/) | 17.0.1.3.0 | | OpenG2P Programs Priority List [g2p_proxy_means_test](g2p_proxy_means_test/) | 17.0.1.3.0 | | G2P: Proxy Means Test -[g2p_reimbursement_portal](g2p_reimbursement_portal/) | 17.0.0.0.0 | | G2P Reimbursement Portal +[g2p_reimbursement_portal](g2p_reimbursement_portal/) | 17.0.1.3.0 | | G2P Reimbursement Portal [g2p_social_registry_importer](g2p_social_registry_importer/) | 17.0.1.3.0 | | Import records from Social Registry [g2p_theme](g2p_theme/) | 17.0.1.3.0 | | OpenG2P Theme diff --git a/g2p_programs_priority_list/README.md b/g2p_programs_priority_list/README.md new file mode 100644 index 00000000..3dccfde9 --- /dev/null +++ b/g2p_programs_priority_list/README.md @@ -0,0 +1,3 @@ +# OpenG2P Programs Priority List + +Refer to https://docs.openg2p.org. diff --git a/g2p_programs_priority_list/__init__.py b/g2p_programs_priority_list/__init__.py new file mode 100644 index 00000000..9b429614 --- /dev/null +++ b/g2p_programs_priority_list/__init__.py @@ -0,0 +1,2 @@ +from . import models +from . import wizard diff --git a/g2p_programs_priority_list/__manifest__.py b/g2p_programs_priority_list/__manifest__.py new file mode 100644 index 00000000..890d1f90 --- /dev/null +++ b/g2p_programs_priority_list/__manifest__.py @@ -0,0 +1,28 @@ +{ + "name": "OpenG2P Programs Priority List", + "category": "G2P/G2P", + "version": "17.0.1.3.0", + "sequence": 1, + "author": "OpenG2P", + "website": "https://openg2p.org", + "license": "LGPL-3", + "depends": ["g2p_programs"], + "data": [ + "security/ir.model.access.csv", + "views/cycle_view.xml", + "views/program_manager_view.xml", + "views/cycle_manager_view.xml", + "views/cycle_membership_view.xml", + "wizard/create_cycle_wizard.xml", + ], + "assets": { + "web.assets_backend": [ + "/g2p_programs_priority_list/static/src/css/style.css", + ], + }, + "demo": [], + "images": [], + "application": True, + "installable": True, + "auto_install": False, +} diff --git a/g2p_programs_priority_list/models/__init__.py b/g2p_programs_priority_list/models/__init__.py new file mode 100644 index 00000000..648b8d3d --- /dev/null +++ b/g2p_programs_priority_list/models/__init__.py @@ -0,0 +1,6 @@ +from . import cycle +from . import cycle_manager +from . import programs +from . import program_manager +from . import cycle_membership +from . import sorting_criteria diff --git a/g2p_programs_priority_list/models/cycle.py b/g2p_programs_priority_list/models/cycle.py new file mode 100644 index 00000000..68520181 --- /dev/null +++ b/g2p_programs_priority_list/models/cycle.py @@ -0,0 +1,20 @@ +from odoo import api, fields, models + + +class G2PCycleInherited(models.Model): + _inherit = "g2p.cycle" + + inclusion_limit = fields.Integer(default=0) + eligibility_domain = fields.Text(string="Domain", default="[]") + is_not_disbursement = fields.Boolean(string="Not Disbursement", default=True) + sorting_criteria_ids = fields.One2many("g2p.sorting.criteria", "cycle_id", string="Sorting Order") + + @api.model + def create(self, vals): + if "program_id" in vals: + program = self.env["g2p.program"].browse(vals["program_id"]) + if program.program_managers.manager_ref_id: + vals[ + "is_not_disbursement" + ] = not program.program_managers.manager_ref_id.is_disbursement_through_priority_list + return super().create(vals) diff --git a/g2p_programs_priority_list/models/cycle_manager.py b/g2p_programs_priority_list/models/cycle_manager.py new file mode 100644 index 00000000..1ecb7325 --- /dev/null +++ b/g2p_programs_priority_list/models/cycle_manager.py @@ -0,0 +1,152 @@ +import logging + +from odoo import _, api, fields, models +from odoo.tools.safe_eval import safe_eval + +_logger = logging.getLogger(__name__) + + +class DefaultCycleManagerInherited(models.Model): + _inherit = "g2p.cycle.manager.default" + + eligibility_domain = fields.Text(string="Domain", default="[]") + inclusion_limit = fields.Integer(default=0) + + sorting_criteria_ids = fields.One2many("g2p.sorting.criteria", "manager_id", string="Sorting Order") + + @api.model + def create(self, vals): + if "program_id" in vals: + vals[ + "eligibility_domain" + ] = f"[('program_membership_ids.program_id', 'in', [{vals['program_id']}])]" + return super().create(vals) + + def new_cycle(self, name, new_start_date, sequence): + cycle = super().new_cycle(name, new_start_date, sequence) + + for rec in self: + is_disbursement = ( + self.program_id.program_managers.manager_ref_id.is_disbursement_through_priority_list + ) + if is_disbursement: + for sorting_criterion in self.sorting_criteria_ids: + self.env["g2p.sorting.criteria"].create( + { + "cycle_id": cycle.id, + "sequence": sorting_criterion.sequence, + "field_name": sorting_criterion.field_name.id, + "order": sorting_criterion.order, + } + ) + + cycle.write( + {"inclusion_limit": rec.inclusion_limit, "eligibility_domain": rec.eligibility_domain} + ) + return cycle + + def add_beneficiaries(self, cycle, beneficiaries, state="draft"): + self.ensure_one() + self._ensure_can_edit_cycle(cycle) + _logger.debug("Adding beneficiaries to the cycle %s", cycle.name) + _logger.debug("Beneficiaries: %s", len(beneficiaries)) + + # Only add beneficiaries not added yet + existing_ids = cycle.cycle_membership_ids.mapped("partner_id.id") + _logger.debug("Existing IDs: %s", len(existing_ids)) + beneficiaries = list(set(beneficiaries) - set(existing_ids)) + + is_disbursement = ( + self.program_id.program_managers.manager_ref_id.is_disbursement_through_priority_list + ) + if is_disbursement: + # Convert beneficiaries to recordset first + if beneficiaries: + if isinstance(beneficiaries, list): + ids = beneficiaries + else: + ids = beneficiaries.mapped("partner_id.id") + + domain = safe_eval(cycle.eligibility_domain) + + domain += [("id", "in", ids), ("disabled", "=", False)] + if self.program_id.target_type == "group": + domain += [("is_group", "=", True), ("is_registrant", "=", True)] + if self.program_id.target_type == "individual": + domain += [("is_group", "=", False), ("is_registrant", "=", True)] + + remaining_limit = max(0, cycle.inclusion_limit - len(existing_ids)) + + sorted_criteria = cycle.sorting_criteria_ids.sorted("sequence") + + order = [] + for criterion in sorted_criteria: + field_name = criterion.field_name.name + reverse_flag = criterion.order == "desc" + order_direction = "desc" if reverse_flag else "asc" + order.append(f"{field_name} {order_direction}") + + # Join the order list into a comma-separated string + order_str = ",".join(order) + + # Query partners with sorting applied + sorted_beneficiaries = self.env["res.partner"].search(domain, order=order_str) + + if len(sorted_beneficiaries) > remaining_limit: + beneficiaries = sorted_beneficiaries[:remaining_limit].ids + else: + beneficiaries = sorted_beneficiaries.ids + + if len(beneficiaries) == 0: + message = _("No beneficiaries to import.") + kind = "warning" + sticky = False + elif len(beneficiaries) < self.MIN_ROW_JOB_QUEUE: + self._add_beneficiaries(cycle, beneficiaries, state, do_count=True) + message = _("%s beneficiaries imported.", len(beneficiaries)) + kind = "success" + sticky = False + else: + self._add_beneficiaries_async(cycle, beneficiaries, state) + message = _("Import of %s beneficiaries started.", len(beneficiaries)) + kind = "warning" + sticky = True + + return { + "type": "ir.actions.client", + "tag": "display_notification", + "params": { + "title": _("Enrollment"), + "message": message, + "sticky": sticky, + "type": kind, + "next": { + "type": "ir.actions.act_window_close", + }, + }, + } + + def _add_beneficiaries(self, cycle, beneficiaries, state="draft", do_count=False): + """Add Beneficiaries with Rank + + :param cycle: Recordset of cycle + :param beneficiaries: Recordset of beneficiaries + :param state: String state to be set to beneficiary + :return: None + """ + new_beneficiaries = [] + for index, r in enumerate(beneficiaries): + new_beneficiaries.append( + [ + 0, + 0, + { + "partner_id": r, + "enrollment_date": fields.Date.today(), + "state": state, + "rank": index + 1, + }, + ] + ) + cycle.update({"cycle_membership_ids": new_beneficiaries}) + cycle._compute_members_count() diff --git a/g2p_programs_priority_list/models/cycle_membership.py b/g2p_programs_priority_list/models/cycle_membership.py new file mode 100644 index 00000000..1447668b --- /dev/null +++ b/g2p_programs_priority_list/models/cycle_membership.py @@ -0,0 +1,7 @@ +from odoo import fields, models + + +class G2PCycleMembershipInherited(models.Model): + _inherit = "g2p.cycle.membership" + + rank = fields.Integer(index=True) diff --git a/g2p_programs_priority_list/models/program_manager.py b/g2p_programs_priority_list/models/program_manager.py new file mode 100644 index 00000000..94ec21be --- /dev/null +++ b/g2p_programs_priority_list/models/program_manager.py @@ -0,0 +1,48 @@ +import logging +from datetime import datetime, timedelta + +from odoo import fields, models + +from odoo.addons.g2p_programs.models.programs import G2PProgram + +_logger = logging.getLogger(__name__) + + +class DefaultProgramManagerInherited(models.Model): + _inherit = "g2p.program.manager.default" + + is_disbursement_through_priority_list = fields.Boolean( + string="Disbursement Through Priority List", default=False + ) + + def new_cycle(self): + """ + Create the next cycle of the program. + If `is_disbursement_through_priority_list` is False, it copies enrolled beneficiaries. + """ + self.ensure_one() + + for rec in self: + cycles = self.env["g2p.cycle"].search([("program_id", "=", rec.program_id.id)]) + _logger.debug("Cycles found: %s", cycles) + + cm = rec.program_id.get_manager(G2PProgram.MANAGER_CYCLE) + + if not cycles: + _logger.debug("Creating first cycle with cycle manager: %s", cm) + new_cycle = cm.new_cycle("Cycle 1", datetime.now(), 1) + else: + last_cycle = rec.last_cycle() + new_sequence = last_cycle.sequence + 1 + start_date = last_cycle.end_date + timedelta(days=1) + new_cycle = cm.new_cycle(f"Cycle {new_sequence}", start_date, new_sequence) + + # Only copy beneficiaries if disbursement is NOT based on priority list + if not rec.is_disbursement_through_priority_list: + if new_cycle: + program_beneficiaries = rec.program_id.get_beneficiaries("enrolled").mapped( + "partner_id.id" + ) + cm.add_beneficiaries(new_cycle, program_beneficiaries, "enrolled") + + return new_cycle diff --git a/g2p_programs_priority_list/models/programs.py b/g2p_programs_priority_list/models/programs.py new file mode 100644 index 00000000..df6681b6 --- /dev/null +++ b/g2p_programs_priority_list/models/programs.py @@ -0,0 +1,76 @@ +import logging + +from odoo import _, models +from odoo.exceptions import UserError + +_logger = logging.getLogger(__name__) + + +class G2PProgramInherit(models.Model): + _inherit = "g2p.program" + + def create_new_cycle(self): + if self.beneficiaries_count <= 0: + raise UserError( + _("No enrolled registrants. Enroll registrants to program to create a new cycle.") + ) + + for rec in self: + message = None + kind = "success" + cycle_manager = rec.get_manager(self.MANAGER_CYCLE) + program_manager = rec.get_manager(self.MANAGER_PROGRAM) + + if not cycle_manager: + raise UserError(_("No Cycle Manager defined.")) + if not program_manager: + raise UserError(_("No Program Manager defined.")) + + _logger.debug("-" * 80) + _logger.debug("pm: %s", program_manager) + + new_cycle = program_manager.new_cycle() + message = _("New cycle %s created.", new_cycle.name) + + if new_cycle.is_not_disbursement: + return { + "type": "ir.actions.client", + "tag": "display_notification", + "params": { + "title": _("Cycle"), + "message": message, + "sticky": False, + "type": kind, + "next": { + "type": "ir.actions.act_window_close", + }, + }, + } + else: + wizard = self.env["cycle.creation.wizard"].create( + { + "cycle_id": new_cycle.id, + "name": new_cycle.name, + "program_id": new_cycle.program_id.id, + "eligibility_domain": new_cycle.eligibility_domain, + "inclusion_limit": new_cycle.inclusion_limit, + } + ) + + for criterion in new_cycle.sorting_criteria_ids: + self.env["cycle.creation.wizard.criteria"].create( + { + "wizard_id": wizard.id, + "field_name": criterion.field_name.id, + "order": criterion.order, + "sequence": criterion.sequence, + } + ) + return { + "name": _("Update Priority Configuration"), + "type": "ir.actions.act_window", + "res_model": "cycle.creation.wizard", + "view_mode": "form", + "res_id": wizard.id, + "target": "new", + } diff --git a/g2p_programs_priority_list/models/sorting_criteria.py b/g2p_programs_priority_list/models/sorting_criteria.py new file mode 100644 index 00000000..35ab6ec5 --- /dev/null +++ b/g2p_programs_priority_list/models/sorting_criteria.py @@ -0,0 +1,17 @@ +from odoo import fields, models + + +class SortingCriteria(models.Model): + _name = "g2p.sorting.criteria" + _description = "Sorting Criteria" + + cycle_id = fields.Many2one("g2p.cycle", string="Cycle") + manager_id = fields.Many2one("g2p.cycle.manager.default", string="Cycle Manager") + + sequence = fields.Integer() + field_name = fields.Many2one( + "ir.model.fields", + domain="[('model', '=', 'res.partner')]", + help="Select a field from res.partner for sorting", + ) + order = fields.Selection([("asc", "Ascending"), ("desc", "Descending")], required=True) diff --git a/g2p_programs_priority_list/pyproject.toml b/g2p_programs_priority_list/pyproject.toml new file mode 100644 index 00000000..4231d0cc --- /dev/null +++ b/g2p_programs_priority_list/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/g2p_programs_priority_list/security/ir.model.access.csv b/g2p_programs_priority_list/security/ir.model.access.csv new file mode 100644 index 00000000..4924f56b --- /dev/null +++ b/g2p_programs_priority_list/security/ir.model.access.csv @@ -0,0 +1,12 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +g2p_cycle_creation_wizard_admin,Cycle Creation Wizard Admin Access,g2p_programs_priority_list.model_cycle_creation_wizard,g2p_registry_base.group_g2p_admin,1,1,1,1 +g2p_cycle_creation_wizard_program_manager,Cycle Creation Wizard Program Manager Access,g2p_programs_priority_list.model_cycle_creation_wizard,g2p_programs.g2p_program_manager,1,1,1,1 +g2p_cycle_creation_wizard_program_validator,Cycle Creation Wizard Program Validator Access,g2p_programs_priority_list.model_cycle_creation_wizard,g2p_programs.g2p_program_validator,1,1,1,0 + +g2p_sorting_criteria_admin,Sorting Criteria Admin Access,model_g2p_sorting_criteria,g2p_registry_base.group_g2p_admin,1,1,1,1 +g2p_sorting_criteria_program_manager,Sorting Criteria Program Manager Access,model_g2p_sorting_criteria,g2p_programs.g2p_program_manager,1,1,1,1 +g2p_sorting_criteria_program_validator,Sorting Criteria Program Validator Access,model_g2p_sorting_criteria,g2p_programs.g2p_program_validator,1,1,1,0 + +g2p_cycle_creation_wizard_criteria_admin,Cycle Creation Wizard Criteria Admin Access,g2p_programs_priority_list.model_cycle_creation_wizard_criteria,g2p_registry_base.group_g2p_admin,1,1,1,1 +g2p_cycle_creation_wizard_criteria_program_manager,Cycle Creation Wizard Criteria Program Manager Access,g2p_programs_priority_list.model_cycle_creation_wizard_criteria,g2p_programs.g2p_program_manager,1,1,1,1 +g2p_cycle_creation_wizard_criteria_program_validator,Cycle Creation Wizard Criteria Program Validator Access,g2p_programs_priority_list.model_cycle_creation_wizard_criteria,g2p_programs.g2p_program_validator,1,1,1,0 diff --git a/g2p_programs_priority_list/static/description/icon.png b/g2p_programs_priority_list/static/description/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..5ecb429ea9ceb3863d46ea852dc9c9f78d4e901b GIT binary patch literal 3985 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 literal 0 HcmV?d00001 diff --git a/g2p_programs_priority_list/static/src/css/style.css b/g2p_programs_priority_list/static/src/css/style.css new file mode 100644 index 00000000..1cd1c5e9 --- /dev/null +++ b/g2p_programs_priority_list/static/src/css/style.css @@ -0,0 +1,25 @@ +.o_list_view .o_list_table thead > tr > th.o_list_number_th { + text-align: left !important; +} + +.o_list_view .o_list_table tbody > tr > td.o_list_number { + text-align: left !important; +} + +.o_list_view .o_list_table th.o_list_number_th .d-block { + text-align: left !important; +} + +.o_list_view .o_list_table thead > tr > th .d-flex { + flex-direction: row !important; +} + +.o_list_view .o_list_table thead > tr > th[data-name="sequence"] { + width: 119px !important; + min-width: 119px !important; + max-width: 119px !important; +} + +.o-checkbox.form-check.form-switch { + display: none; +} diff --git a/g2p_programs_priority_list/views/cycle_manager_view.xml b/g2p_programs_priority_list/views/cycle_manager_view.xml new file mode 100644 index 00000000..6359600f --- /dev/null +++ b/g2p_programs_priority_list/views/cycle_manager_view.xml @@ -0,0 +1,28 @@ + + + + + Cycle Manager Default Form + g2p.cycle.manager.default + + + + + + + + + + + + + + + + + + + + diff --git a/g2p_programs_priority_list/views/cycle_membership_view.xml b/g2p_programs_priority_list/views/cycle_membership_view.xml new file mode 100644 index 00000000..9f2c4ee5 --- /dev/null +++ b/g2p_programs_priority_list/views/cycle_membership_view.xml @@ -0,0 +1,16 @@ + + + + + view_cycle_membership_tree + g2p.cycle.membership + + + + + + + + diff --git a/g2p_programs_priority_list/views/cycle_view.xml b/g2p_programs_priority_list/views/cycle_view.xml new file mode 100644 index 00000000..ef932f91 --- /dev/null +++ b/g2p_programs_priority_list/views/cycle_view.xml @@ -0,0 +1,32 @@ + + + + + view_cycle_form_inherit + g2p.cycle + + + + + + + + + + + + + + + + + + + diff --git a/g2p_programs_priority_list/views/program_manager_view.xml b/g2p_programs_priority_list/views/program_manager_view.xml new file mode 100644 index 00000000..701d314b --- /dev/null +++ b/g2p_programs_priority_list/views/program_manager_view.xml @@ -0,0 +1,16 @@ + + + + + view_program_manager_default_form_inherit + g2p.program.manager.default + + + + + + + + diff --git a/g2p_programs_priority_list/wizard/__init__.py b/g2p_programs_priority_list/wizard/__init__.py new file mode 100644 index 00000000..4e629448 --- /dev/null +++ b/g2p_programs_priority_list/wizard/__init__.py @@ -0,0 +1,4 @@ +# Part of OpenG2P. See LICENSE file for full copyright and licensing details. + +from . import create_cycle_wizard +from . import create_sorting_wizard diff --git a/g2p_programs_priority_list/wizard/create_cycle_wizard.py b/g2p_programs_priority_list/wizard/create_cycle_wizard.py new file mode 100644 index 00000000..6c5f2ff1 --- /dev/null +++ b/g2p_programs_priority_list/wizard/create_cycle_wizard.py @@ -0,0 +1,61 @@ +# Part of OpenG2P. See LICENSE file for full copyright and licensing details. + +from odoo import fields, models + + +class CycleCreationWizard(models.TransientModel): + _name = "cycle.creation.wizard" + _description = "Cycle Creation Wizard" + + cycle_id = fields.Many2one("g2p.cycle", string="Cycle", readonly=True) + name = fields.Char(string="Cycle Name", required=True, readonly=True) + program_id = fields.Many2one("g2p.program", string="Program", required=True, readonly=True) + + inclusion_limit = fields.Integer(default=0) + eligibility_domain = fields.Text(string="Domain", default="[]") + + sorting_criteria_ids = fields.One2many( + "cycle.creation.wizard.criteria", "wizard_id", string="Sorting Order" + ) + + def action_confirm(self): + if self.cycle_id: + sorting_criteria = self.sorting_criteria_ids + cycle_sorting_criteria = self.cycle_id.sorting_criteria_ids + + # Store existing field names to check which ones are removed + new_field_names = sorting_criteria.mapped("field_name.id") + + for criterion in sorting_criteria: + existing_criteria = cycle_sorting_criteria.filtered( + lambda c: c.field_name.id == criterion.field_name.id + ) + + if existing_criteria: + existing_criteria.write( + { + "order": criterion.order, + "sequence": criterion.sequence, + } + ) + else: + self.env["g2p.sorting.criteria"].create( + { + "cycle_id": self.cycle_id.id, + "field_name": criterion.field_name.id, + "order": criterion.order, + "sequence": criterion.sequence, + } + ) + + # Remove criteria that are no longer in sorting_criteria + to_remove = cycle_sorting_criteria.filtered(lambda c: c.field_name.id not in new_field_names) + if to_remove: + to_remove.unlink() + + self.cycle_id.write( + {"inclusion_limit": self.inclusion_limit, "eligibility_domain": self.eligibility_domain} + ) + + self.cycle_id.copy_beneficiaries_from_program() + return {"type": "ir.actions.act_window_close"} diff --git a/g2p_programs_priority_list/wizard/create_cycle_wizard.xml b/g2p_programs_priority_list/wizard/create_cycle_wizard.xml new file mode 100644 index 00000000..f3ce1d45 --- /dev/null +++ b/g2p_programs_priority_list/wizard/create_cycle_wizard.xml @@ -0,0 +1,44 @@ + + + + + cycle.creation.wizard.form + cycle.creation.wizard + +
+ + + + + + + + + + + + + + + + + + + +
+
+
+
+
+ + + Cycle Created + cycle.creation.wizard + form + + new + +
diff --git a/g2p_programs_priority_list/wizard/create_sorting_wizard.py b/g2p_programs_priority_list/wizard/create_sorting_wizard.py new file mode 100644 index 00000000..99bbc796 --- /dev/null +++ b/g2p_programs_priority_list/wizard/create_sorting_wizard.py @@ -0,0 +1,15 @@ +from odoo import fields, models + + +class CycleCreationWizardCriteria(models.TransientModel): + _name = "cycle.creation.wizard.criteria" + _description = "Temporary Sorting Criteria for Cycle Creation Wizard" + + wizard_id = fields.Many2one("cycle.creation.wizard", string="Wizard", ondelete="cascade") + field_name = fields.Many2one( + "ir.model.fields", + domain="[('model', '=', 'res.partner')]", + help="Select a field from res.partner for sorting", + ) + order = fields.Selection([("asc", "Ascending"), ("desc", "Descending")], required=True) + sequence = fields.Integer()