diff --git a/README.md b/README.md
index 97d44c0d..ad42f599 100644
--- a/README.md
+++ b/README.md
@@ -46,6 +46,7 @@ addon | version | maintainers | summary
[g2p_program_registrant_info](g2p_program_registrant_info/) | 17.0.1.2.0 | | G2P Program: Registrant Info
[g2p_program_reimbursement](g2p_program_reimbursement/) | 17.0.1.2.0 | | OpenG2P Programs: Reimbursement
[g2p_programs](g2p_programs/) | 17.0.1.2.0 | | OpenG2P Programs
+[g2p_programs_priority_list](g2p_programs_priority_list/) | 17.0.1.2.0 | | OpenG2P Programs Priority List
[g2p_proxy_means_test](g2p_proxy_means_test/) | 17.0.1.2.0 | | G2P: Proxy Means Test
[g2p_reimbursement_portal](g2p_reimbursement_portal/) | 17.0.0.0.0 | | G2P Reimbursement Portal
[g2p_social_registry_importer](g2p_social_registry_importer/) | 17.0.1.2.0 | | Import records from Social Registry
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..4ea346f2
--- /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.2.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/i18n/g2p_programs_priority_list.pot b/g2p_programs_priority_list/i18n/g2p_programs_priority_list.pot
new file mode 100644
index 00000000..95718fe1
--- /dev/null
+++ b/g2p_programs_priority_list/i18n/g2p_programs_priority_list.pot
@@ -0,0 +1,269 @@
+# Translation of Odoo Server.
+# This file contains the translation of the following modules:
+# * g2p_programs_priority_list
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Odoo Server 17.0\n"
+"Report-Msgid-Bugs-To: \n"
+"Last-Translator: \n"
+"Language-Team: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: \n"
+"Plural-Forms: \n"
+
+#. module: g2p_programs_priority_list
+#. odoo-python
+#: code:addons/g2p_programs_priority_list/models/cycle_manager.py:0
+#, python-format
+msgid "%s beneficiaries imported."
+msgstr ""
+
+#. module: g2p_programs_priority_list
+#: model:ir.model.fields.selection,name:g2p_programs_priority_list.selection__cycle_creation_wizard_criteria__order__asc
+#: model:ir.model.fields.selection,name:g2p_programs_priority_list.selection__g2p_sorting_criteria__order__asc
+msgid "Ascending"
+msgstr ""
+
+#. module: g2p_programs_priority_list
+#: model:ir.model.fields,field_description:g2p_programs_priority_list.field_cycle_creation_wizard__create_uid
+#: model:ir.model.fields,field_description:g2p_programs_priority_list.field_cycle_creation_wizard_criteria__create_uid
+#: model:ir.model.fields,field_description:g2p_programs_priority_list.field_g2p_sorting_criteria__create_uid
+msgid "Created by"
+msgstr ""
+
+#. module: g2p_programs_priority_list
+#: model:ir.model.fields,field_description:g2p_programs_priority_list.field_cycle_creation_wizard__create_date
+#: model:ir.model.fields,field_description:g2p_programs_priority_list.field_cycle_creation_wizard_criteria__create_date
+#: model:ir.model.fields,field_description:g2p_programs_priority_list.field_g2p_sorting_criteria__create_date
+msgid "Created on"
+msgstr ""
+
+#. module: g2p_programs_priority_list
+#. odoo-python
+#: code:addons/g2p_programs_priority_list/models/programs.py:0
+#: model:ir.model,name:g2p_programs_priority_list.model_g2p_cycle
+#: model:ir.model.fields,field_description:g2p_programs_priority_list.field_cycle_creation_wizard__cycle_id
+#: model:ir.model.fields,field_description:g2p_programs_priority_list.field_g2p_sorting_criteria__cycle_id
+#, python-format
+msgid "Cycle"
+msgstr ""
+
+#. module: g2p_programs_priority_list
+#: model:ir.actions.act_window,name:g2p_programs_priority_list.action_cycle_creation_wizard
+msgid "Cycle Created"
+msgstr ""
+
+#. module: g2p_programs_priority_list
+#: model:ir.model,name:g2p_programs_priority_list.model_cycle_creation_wizard
+msgid "Cycle Creation Wizard"
+msgstr ""
+
+#. module: g2p_programs_priority_list
+#: model:ir.model.fields,field_description:g2p_programs_priority_list.field_g2p_sorting_criteria__manager_id
+msgid "Cycle Manager"
+msgstr ""
+
+#. module: g2p_programs_priority_list
+#: model:ir.model,name:g2p_programs_priority_list.model_g2p_cycle_membership
+msgid "Cycle Membership"
+msgstr ""
+
+#. module: g2p_programs_priority_list
+#: model:ir.model.fields,field_description:g2p_programs_priority_list.field_cycle_creation_wizard__name
+msgid "Cycle Name"
+msgstr ""
+
+#. module: g2p_programs_priority_list
+#: model:ir.model,name:g2p_programs_priority_list.model_g2p_cycle_manager_default
+msgid "Default Cycle Manager"
+msgstr ""
+
+#. module: g2p_programs_priority_list
+#: model:ir.model,name:g2p_programs_priority_list.model_g2p_program_manager_default
+msgid "Default Program Manager"
+msgstr ""
+
+#. module: g2p_programs_priority_list
+#: model:ir.model.fields.selection,name:g2p_programs_priority_list.selection__cycle_creation_wizard_criteria__order__desc
+#: model:ir.model.fields.selection,name:g2p_programs_priority_list.selection__g2p_sorting_criteria__order__desc
+msgid "Descending"
+msgstr ""
+
+#. module: g2p_programs_priority_list
+#: model:ir.model.fields,field_description:g2p_programs_priority_list.field_g2p_program_manager_default__is_disbursement_through_priority_list
+msgid "Disbursement Through Priority List"
+msgstr ""
+
+#. module: g2p_programs_priority_list
+#: model:ir.model.fields,field_description:g2p_programs_priority_list.field_cycle_creation_wizard__display_name
+#: model:ir.model.fields,field_description:g2p_programs_priority_list.field_cycle_creation_wizard_criteria__display_name
+#: model:ir.model.fields,field_description:g2p_programs_priority_list.field_g2p_sorting_criteria__display_name
+msgid "Display Name"
+msgstr ""
+
+#. module: g2p_programs_priority_list
+#: model:ir.model.fields,field_description:g2p_programs_priority_list.field_cycle_creation_wizard__eligibility_domain
+#: model:ir.model.fields,field_description:g2p_programs_priority_list.field_g2p_cycle__eligibility_domain
+#: model:ir.model.fields,field_description:g2p_programs_priority_list.field_g2p_cycle_manager_default__eligibility_domain
+msgid "Domain"
+msgstr ""
+
+#. module: g2p_programs_priority_list
+#: model_terms:ir.ui.view,arch_db:g2p_programs_priority_list.view_cycle_creation_wizard
+msgid "Edit Cycle Details"
+msgstr ""
+
+#. module: g2p_programs_priority_list
+#. odoo-python
+#: code:addons/g2p_programs_priority_list/models/cycle_manager.py:0
+#, python-format
+msgid "Enrollment"
+msgstr ""
+
+#. module: g2p_programs_priority_list
+#: model:ir.model.fields,field_description:g2p_programs_priority_list.field_cycle_creation_wizard_criteria__field_name
+#: model:ir.model.fields,field_description:g2p_programs_priority_list.field_g2p_sorting_criteria__field_name
+msgid "Field Name"
+msgstr ""
+
+#. module: g2p_programs_priority_list
+#: model:ir.model.fields,field_description:g2p_programs_priority_list.field_cycle_creation_wizard__id
+#: model:ir.model.fields,field_description:g2p_programs_priority_list.field_cycle_creation_wizard_criteria__id
+#: model:ir.model.fields,field_description:g2p_programs_priority_list.field_g2p_sorting_criteria__id
+msgid "ID"
+msgstr ""
+
+#. module: g2p_programs_priority_list
+#. odoo-python
+#: code:addons/g2p_programs_priority_list/models/cycle_manager.py:0
+#, python-format
+msgid "Import of %s beneficiaries started."
+msgstr ""
+
+#. module: g2p_programs_priority_list
+#: model:ir.model.fields,field_description:g2p_programs_priority_list.field_cycle_creation_wizard__inclusion_limit
+#: model:ir.model.fields,field_description:g2p_programs_priority_list.field_g2p_cycle__inclusion_limit
+#: model:ir.model.fields,field_description:g2p_programs_priority_list.field_g2p_cycle_manager_default__inclusion_limit
+msgid "Inclusion Limit"
+msgstr ""
+
+#. module: g2p_programs_priority_list
+#: model:ir.model.fields,field_description:g2p_programs_priority_list.field_cycle_creation_wizard__write_uid
+#: model:ir.model.fields,field_description:g2p_programs_priority_list.field_cycle_creation_wizard_criteria__write_uid
+#: model:ir.model.fields,field_description:g2p_programs_priority_list.field_g2p_sorting_criteria__write_uid
+msgid "Last Updated by"
+msgstr ""
+
+#. module: g2p_programs_priority_list
+#: model:ir.model.fields,field_description:g2p_programs_priority_list.field_cycle_creation_wizard__write_date
+#: model:ir.model.fields,field_description:g2p_programs_priority_list.field_cycle_creation_wizard_criteria__write_date
+#: model:ir.model.fields,field_description:g2p_programs_priority_list.field_g2p_sorting_criteria__write_date
+msgid "Last Updated on"
+msgstr ""
+
+#. module: g2p_programs_priority_list
+#. odoo-python
+#: code:addons/g2p_programs_priority_list/models/programs.py:0
+#, python-format
+msgid "New cycle %s created."
+msgstr ""
+
+#. module: g2p_programs_priority_list
+#. odoo-python
+#: code:addons/g2p_programs_priority_list/models/programs.py:0
+#, python-format
+msgid "No Cycle Manager defined."
+msgstr ""
+
+#. module: g2p_programs_priority_list
+#. odoo-python
+#: code:addons/g2p_programs_priority_list/models/programs.py:0
+#, python-format
+msgid "No Program Manager defined."
+msgstr ""
+
+#. module: g2p_programs_priority_list
+#. odoo-python
+#: code:addons/g2p_programs_priority_list/models/cycle_manager.py:0
+#, python-format
+msgid "No beneficiaries to import."
+msgstr ""
+
+#. module: g2p_programs_priority_list
+#. odoo-python
+#: code:addons/g2p_programs_priority_list/models/programs.py:0
+#, python-format
+msgid ""
+"No enrolled registrants. Enroll registrants to program to create a new "
+"cycle."
+msgstr ""
+
+#. module: g2p_programs_priority_list
+#: model:ir.model.fields,field_description:g2p_programs_priority_list.field_g2p_cycle__is_not_disbursement
+msgid "Not Disbursement"
+msgstr ""
+
+#. module: g2p_programs_priority_list
+#: model:ir.model.fields,field_description:g2p_programs_priority_list.field_cycle_creation_wizard_criteria__order
+#: model:ir.model.fields,field_description:g2p_programs_priority_list.field_g2p_sorting_criteria__order
+msgid "Order"
+msgstr ""
+
+#. module: g2p_programs_priority_list
+#: model:ir.model,name:g2p_programs_priority_list.model_g2p_program
+#: model:ir.model.fields,field_description:g2p_programs_priority_list.field_cycle_creation_wizard__program_id
+msgid "Program"
+msgstr ""
+
+#. module: g2p_programs_priority_list
+#: model:ir.model.fields,field_description:g2p_programs_priority_list.field_g2p_cycle_membership__rank
+msgid "Rank"
+msgstr ""
+
+#. module: g2p_programs_priority_list
+#: model_terms:ir.ui.view,arch_db:g2p_programs_priority_list.view_cycle_creation_wizard
+msgid "Save"
+msgstr ""
+
+#. module: g2p_programs_priority_list
+#: model:ir.model.fields,help:g2p_programs_priority_list.field_cycle_creation_wizard_criteria__field_name
+#: model:ir.model.fields,help:g2p_programs_priority_list.field_g2p_sorting_criteria__field_name
+msgid "Select a field from res.partner for sorting"
+msgstr ""
+
+#. module: g2p_programs_priority_list
+#: model:ir.model.fields,field_description:g2p_programs_priority_list.field_cycle_creation_wizard_criteria__sequence
+#: model:ir.model.fields,field_description:g2p_programs_priority_list.field_g2p_sorting_criteria__sequence
+msgid "Sequence"
+msgstr ""
+
+#. module: g2p_programs_priority_list
+#: model:ir.model,name:g2p_programs_priority_list.model_g2p_sorting_criteria
+msgid "Sorting Criteria"
+msgstr ""
+
+#. module: g2p_programs_priority_list
+#: model:ir.model.fields,field_description:g2p_programs_priority_list.field_cycle_creation_wizard__sorting_criteria_ids
+#: model:ir.model.fields,field_description:g2p_programs_priority_list.field_g2p_cycle__sorting_criteria_ids
+#: model:ir.model.fields,field_description:g2p_programs_priority_list.field_g2p_cycle_manager_default__sorting_criteria_ids
+msgid "Sorting Order"
+msgstr ""
+
+#. module: g2p_programs_priority_list
+#: model:ir.model,name:g2p_programs_priority_list.model_cycle_creation_wizard_criteria
+msgid "Temporary Sorting Criteria for Cycle Creation Wizard"
+msgstr ""
+
+#. module: g2p_programs_priority_list
+#. odoo-python
+#: code:addons/g2p_programs_priority_list/models/programs.py:0
+#, python-format
+msgid "Update Priority Configuration"
+msgstr ""
+
+#. module: g2p_programs_priority_list
+#: model:ir.model.fields,field_description:g2p_programs_priority_list.field_cycle_creation_wizard_criteria__wizard_id
+msgid "Wizard"
+msgstr ""
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 00000000..5ecb429e
Binary files /dev/null and b/g2p_programs_priority_list/static/description/icon.png differ
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()