---
myst:
html_meta:
"title": "Program cycles"
"description": "Learn how to modify OpenSPP program cycles, their states, and transitions through custom module development"
"keywords": "OpenSPP, program cycles, cycle management, program states, module development, cycle customization"
---
# Program cycles
In OpenSPP, the creation and scheduling of program cycles are handled by a flexible system called **Cycle Managers**.
A Cycle Manager is a self-contained component that defines the logic for how and when new cycles are created for a program.
This allows for creating reusable and complex cycle generation rules that can be easily attached to any program.
This guide will walk you through creating a custom Cycle Manager module from scratch.
We will build a new manager based on the default one, but with a specific business rule: creating cycles with a fixed six-month duration, regardless of other settings.
By the end of this guide, you will be able to:
- Understand the role and structure of a Cycle Manager.
- Create a new model for your custom cycle generation rules.
- Implement the core logic to override cycle date calculations.
- Create a user interface for configuring your manager.
- Register your new manager so it can be used in any program.
- Extend the Program Creation Wizard to pre-configure your manager.
- Set up the necessary security access for your new model.
## Prerequisites
- Solid understanding of Odoo 17 module development, including Python, XML, and XPath.
- Familiarity with the OpenG2P and OpenSPP core modules, especially `OpenG2P Programs` ({doc}`g2p_programs `) and `OpenSPP Programs` ({doc}`spp_programs `).
- To set up OpenSPP for development, please refer to the {doc}`Development Setup Guide <../setup>`.
## Module Structure
A typical Cycle Manager module follows the standard Odoo module structure.
Here's the complete structure of the module we will build, `spp_cycle_manager_fixed_interval`:
```text
spp_cycle_manager_fixed_interval/
├── __init__.py
├── __manifest__.py
├── models/
│ ├── __init__.py
│ └── cycle_manager.py # The core manager logic & registration
├── security/
│ └── ir.model.access.csv
├── views/
│ └── cycle_manager_view.xml # The manager's UI
└── wizard/
├── __init__.py
└── create_program_wizard.py # Extends the program creation wizard
```
---
## Step-by-Step Guide
### Create the Module Scaffold
Start by creating a new directory for your module (e.g., `spp_cycle_manager_fixed_interval`) and populate it with the basic Odoo module files and the directory structure shown above.
### Define the Manifest (`__manifest__.py`)
The manifest file declares your module's metadata and dependencies.
Our cycle manager depends on {doc}`g2p_programs ` and {doc}`spp_programs ` for the base manager framework.
```python
# From: spp_cycle_manager_fixed_interval/__manifest__.py
{
"name": "OpenSPP Fixed Interval Cycle Manager",
"summary": "A cycle manager that creates cycles with a fixed six-month duration.",
"category": "OpenSPP",
"version": "17.0.1.0.0",
"author": "OpenSPP.org",
"website": "https://github.com/OpenSPP/openspp-modules",
"license": "LGPL-3",
"depends": [
"g2p_programs",
"spp_programs",
],
"data": [
"security/ir.model.access.csv",
"views/cycle_manager_view.xml",
"wizard/create_program_wizard.xml",
],
"application": True,
"installable": True,
"auto_install": False,
}
```
### Create the Cycle Manager Model
This is the core of your module.
You will create a new model that inherits from the default cycle manager and overrides its behavior.
1. **Create the model file**: In your `models/` directory, create a Python file named `cycle_manager.py`. Remember to import it in `models/__init__.py`.
2. **Define the model**:
- The model name (`_name`) should be descriptive, like `g2p.cycle.manager.fixed.interval`.
- Inherit from `g2p.cycle.manager.default`. This provides the essential framework and fields of the default cycle manager.
- Override the `_get_end_date` method. This is where we'll inject our custom logic to enforce a six-month duration.
```python
# From: spp_cycle_manager_fixed_interval/models/cycle_manager.py
from dateutil.relativedelta import relativedelta
from odoo import api, models
class FixedIntervalCycleManager(models.Model):
_name = "g2p.cycle.manager.fixed.interval"
_inherit = "g2p.cycle.manager.default"
_description = "Fixed Interval Cycle Manager"
def _get_end_date(self, start_date):
"""Override to set a fixed 6-month end date, ignoring cycle_duration."""
return start_date + relativedelta(months=6, days=-1)
```
### Register the New Manager
To make OpenSPP aware of your new manager, you must add it to the list of available cycle managers.
1. **Extend the `g2p.cycle.manager` model**: In the same file, `models/cycle_manager.py`, inherit from `g2p.cycle.manager`.
2. **Extend the selection method**: Override the `_selection_manager_ref_id` method to add your new manager's model name and a user-friendly label to the selection list.
```python
# From: spp_cycle_manager_fixed_interval/models/cycle_manager.py
class CycleManager(models.Model):
_inherit = "g2p.cycle.manager"
@api.model
def _selection_manager_ref_id(self):
selection = super()._selection_manager_ref_id()
new_manager = ("g2p.cycle.manager.fixed.interval", "Fixed 6-Month Interval")
if new_manager not in selection:
selection.append(new_manager)
return selection
```
### Create the User Interface
Create a form view for your manager.
Since we are inheriting from the default manager, we can also inherit its view and modify it to hide the fields that are no longer relevant (like the recurrence rules).
```xml
view_cycle_manager_fixed_interval_formg2p.cycle.manager.fixed.interval11
This manager automatically creates cycles with a fixed six-month duration. The recurrence settings below are ignored.
```
### Extend the Program Creation Wizard
To improve user experience, add a selection to the "Create Program" wizard so users can choose your new manager from the start.
1. **Extend the wizard model**: In `wizard/create_program_wizard.py`, inherit from `g2p.program.create.wizard`, add a `cycle_manager_kind` selection, and override `create_program` to handle the creation of your custom manager.
```python
# In: spp_cycle_manager_fixed_interval/wizard/create_program_wizard.py
from odoo import _, fields, models
class CustomCycleManagerWizard(models.TransientModel):
_inherit = "g2p.program.create.wizard"
cycle_manager_kind = fields.Selection(
selection_add=[("fixed_interval", "Fixed 6-Month Interval")],
string="Cycle Manager Type",
default='default'
)
def create_program(self):
if self.cycle_manager_kind != 'fixed_interval':
return super().create_program()
self._check_required_fields()
rec = self
program_vals = rec.get_program_vals()
program = self.env["g2p.program"].create(program_vals)
program_id = program.id
vals = {}
vals.update(rec._get_eligibility_manager(program_id))
# Create our custom cycle manager
cycle_manager_default_val = rec.get_cycle_manager_default_val(program_id)
fixed_interval_mgr = self.env["g2p.cycle.manager.fixed.interval"].create(cycle_manager_default_val)
mgr = self.env["g2p.cycle.manager"].create({
"program_id": program_id,
"manager_ref_id": f"{fixed_interval_mgr._name},{str(fixed_interval_mgr.id)}",
})
vals.update({"cycle_managers": [(4, mgr.id)]})
vals.update(rec._get_entitlement_manager(program_id))
vals.update({"is_one_time_distribution": rec.is_one_time_distribution})
program.update(vals)
if rec.import_beneficiaries == "yes":
rec.program_wizard_import_beneficiaries(program)
if rec.is_one_time_distribution:
program.create_new_cycle()
view_id = self.env.ref("g2p_programs.view_program_list_form")
if rec.view_id:
view_id = rec.view_id
program.view_id = view_id.id
return {
"name": _("Programs"),
"view_mode": "form",
"res_model": "g2p.program",
"res_id": program_id,
"view_id": view_id.id,
"type": "ir.actions.act_window",
"target": "current",
}
```
2. **Extend the wizard view**: In `wizard/create_program_wizard.xml`, extend the form to show your new selection field and hide the default recurrence rules when your manager is selected.
```xml
create_program_wizard_form_view_custom_cycleg2p.program.create.wizardcycle_manager_kind == 'fixed_interval'
```
### Set Up Security
Grant users access to your new model in `security/ir.model.access.csv`.
```csv
# From: spp_cycle_manager_fixed_interval/security/ir.model.access.csv
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
g2p_cycle_manager_fixed_interval_admin,Cycle Manager Fixed Interval Admin Access,spp_cycle_manager_fixed_interval.model_g2p_cycle_manager_fixed_interval,g2p_registry_base.group_g2p_admin,1,1,1,1
g2p_cycle_manager_fixed_interval_program_manager,Cycle Manager Fixed Interval Program Manager Access,spp_cycle_manager_fixed_interval.model_g2p_cycle_manager_fixed_interval,g2p_programs.g2p_program_manager,1,1,1,0
```
### Install and Test
1. Install or upgrade the module through the Apps menu.
2. Navigate to **Programs** and click **Create Program**.
3. In the wizard, on the **Cycle** page, select your new **"Fixed 6-Month Interval"** manager type.
4. Notice that the recurrence settings disappear.
5. Complete the wizard and click **Create**.
A new program will be created, and an instance of your cycle manager will be automatically created and configured.
When new cycles are created for this program, they will automatically have a six-month duration.
## References
For more information on extending OpenSPP modules, refer to:
- [Odoo 17 Developer Documentation](https://www.odoo.com/documentation/17.0/developer/)
- [OpenSPP Programs Module Source](https://github.com/OpenSPP/openspp-modules/tree/17.0/spp_programs)