Skip to content

Commit 8bef52a

Browse files
feat: add spp_hazard_programs, spp_import_match, and spp_oauth modules
Add three new modules with comprehensive test coverage: - spp_hazard_programs: links hazard incidents to program eligibility, enabling emergency response targeting based on verified impacts and damage levels (24 tests) - spp_import_match: CSV import matching and deduplication with async queue job support for large files (23 tests) - spp_oauth: JWT-based OAuth with RSA key management via system parameters (10 tests)
1 parent a3fec68 commit 8bef52a

68 files changed

Lines changed: 5160 additions & 0 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ openpyxl
1515
parse-accept-language
1616
pydantic
1717
pyjwt
18+
pyjwt>=2.4.0
1819
pyproj
1920
python-dateutil
2021
python-magic

spp_hazard_programs/README.rst

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
===================================
2+
OpenSPP Hazard Programs Integration
3+
===================================
4+
5+
..
6+
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
7+
!! This file is generated by oca-gen-addon-readme !!
8+
!! changes will be overwritten. !!
9+
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
10+
!! source digest: sha256:e4ca3f5b5e8c998b833ccd6526f575c07f53a38ac49875511c31201f13122916
11+
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
12+
13+
.. |badge1| image:: https://img.shields.io/badge/maturity-Alpha-red.png
14+
:target: https://odoo-community.org/page/development-status
15+
:alt: Alpha
16+
.. |badge2| image:: https://img.shields.io/badge/license-LGPL--3-blue.png
17+
:target: http://www.gnu.org/licenses/lgpl-3.0-standalone.html
18+
:alt: License: LGPL-3
19+
.. |badge3| image:: https://img.shields.io/badge/github-OpenSPP%2FOpenSPP2-lightgray.png?logo=github
20+
:target: https://github.com/OpenSPP/OpenSPP2/tree/19.0/spp_hazard_programs
21+
:alt: OpenSPP/OpenSPP2
22+
23+
|badge1| |badge2| |badge3|
24+
25+
Links hazard incidents to emergency response programs. Enables programs
26+
to target affected populations using verified impact data, filter
27+
registrants by damage severity, and automatically enable emergency mode
28+
when responding to active incidents.
29+
30+
Key Capabilities
31+
~~~~~~~~~~~~~~~~
32+
33+
- Link programs to one or more hazard incidents via many-to-many
34+
relation
35+
- Automatically flag programs as emergency when linked to incidents in
36+
alert/active/recovery status
37+
- Filter eligible registrants by damage level threshold (any, moderate+,
38+
severe+, critical only)
39+
- Count affected registrants based on verified impacts matching damage
40+
criteria
41+
- Track which programs are responding to each incident (bidirectional
42+
navigation)
43+
44+
Key Models
45+
~~~~~~~~~~
46+
47+
+----------------------------------+-----------------------------------+
48+
| Model | Description |
49+
+==================================+===================================+
50+
| ``spp.program`` (extend) | Adds target incidents, emergency |
51+
| | mode, damage filter |
52+
+----------------------------------+-----------------------------------+
53+
| ``spp.hazard.incident`` (extend) | Adds reverse relation to response |
54+
| | programs |
55+
+----------------------------------+-----------------------------------+
56+
57+
UI Location
58+
~~~~~~~~~~~
59+
60+
- **Programs**: Programs > Programs > "Emergency Response" tab
61+
- **Incidents**: Hazard & Emergency > Incidents > All Incidents >
62+
"Response Programs" tab
63+
- **Stat buttons**: Programs show incident count and affected registrant
64+
count; incidents show response program count
65+
- **Filters**: "Emergency Programs" and "Has Target Incidents" filters
66+
in program search view
67+
68+
Security
69+
~~~~~~~~
70+
71+
No new ACL entries. Access inherited from base models:
72+
73+
- ``spp.program``: Controlled by ``spp_programs`` security groups
74+
- ``spp.hazard.incident``: Controlled by ``spp_hazard`` security groups
75+
76+
Extension Points
77+
~~~~~~~~~~~~~~~~
78+
79+
- Override ``get_emergency_eligible_registrants()`` to customize
80+
eligibility logic beyond damage levels
81+
- Override ``_get_damage_level_domain()`` to add custom damage filtering
82+
rules
83+
- Inherit ``spp.program`` to add fields used in emergency calculations
84+
- Use ``is_emergency_program`` and ``is_emergency_mode`` flags in
85+
downstream program logic
86+
87+
Dependencies
88+
~~~~~~~~~~~~
89+
90+
``spp_hazard``, ``spp_programs``
91+
92+
.. IMPORTANT::
93+
This is an alpha version, the data model and design can change at any time without warning.
94+
Only for development or testing purpose, do not use in production.
95+
96+
**Table of contents**
97+
98+
.. contents::
99+
:local:
100+
101+
Bug Tracker
102+
===========
103+
104+
Bugs are tracked on `GitHub Issues <https://github.com/OpenSPP/OpenSPP2/issues>`_.
105+
In case of trouble, please check there if your issue has already been reported.
106+
If you spotted it first, help us to smash it by providing a detailed and welcomed
107+
`feedback <https://github.com/OpenSPP/OpenSPP2/issues/new?body=module:%20spp_hazard_programs%0Aversion:%2019.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.
108+
109+
Do not contact contributors directly about support or help with technical issues.
110+
111+
Credits
112+
=======
113+
114+
Authors
115+
-------
116+
117+
* OpenSPP.org
118+
119+
Maintainers
120+
-----------
121+
122+
.. |maintainer-jeremi| image:: https://github.com/jeremi.png?size=40px
123+
:target: https://github.com/jeremi
124+
:alt: jeremi
125+
.. |maintainer-gonzalesedwin1123| image:: https://github.com/gonzalesedwin1123.png?size=40px
126+
:target: https://github.com/gonzalesedwin1123
127+
:alt: gonzalesedwin1123
128+
.. |maintainer-reichie020212| image:: https://github.com/reichie020212.png?size=40px
129+
:target: https://github.com/reichie020212
130+
:alt: reichie020212
131+
132+
Current maintainers:
133+
134+
|maintainer-jeremi| |maintainer-gonzalesedwin1123| |maintainer-reichie020212|
135+
136+
This module is part of the `OpenSPP/OpenSPP2 <https://github.com/OpenSPP/OpenSPP2/tree/19.0/spp_hazard_programs>`_ project on GitHub.
137+
138+
You are welcome to contribute.

spp_hazard_programs/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Part of OpenSPP. See LICENSE file for full copyright and licensing details.
2+
3+
from . import models
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# pylint: disable=pointless-statement
2+
# Part of OpenSPP. See LICENSE file for full copyright and licensing details.
3+
4+
5+
{
6+
"name": "OpenSPP Hazard Programs Integration",
7+
"summary": "Links hazard impacts to program eligibility and entitlements. "
8+
"Enables emergency programs to use hazard data for targeting and benefit calculation.",
9+
"category": "OpenSPP/Targeting",
10+
"version": "19.0.1.0.0",
11+
"sequence": 1,
12+
"author": "OpenSPP.org",
13+
"website": "https://github.com/OpenSPP/OpenSPP2",
14+
"license": "LGPL-3",
15+
"development_status": "Alpha",
16+
"maintainers": ["jeremi", "gonzalesedwin1123", "reichie020212"],
17+
"depends": [
18+
"spp_hazard",
19+
"spp_programs",
20+
],
21+
"data": [
22+
"security/ir.model.access.csv",
23+
"views/program_views.xml",
24+
"views/incident_views.xml",
25+
],
26+
"demo": [],
27+
"assets": {},
28+
"application": False,
29+
"installable": True,
30+
"auto_install": True,
31+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# Part of OpenSPP. See LICENSE file for full copyright and licensing details.
2+
3+
from . import program
4+
from . import incident
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
# Part of OpenSPP. See LICENSE file for full copyright and licensing details.
2+
3+
import logging
4+
5+
from odoo import _, fields, models
6+
7+
_logger = logging.getLogger(__name__)
8+
9+
10+
class HazardIncident(models.Model):
11+
"""
12+
Extends spp.hazard.incident to add program integration.
13+
14+
This extension links incidents to programs, enabling tracking of
15+
which programs are responding to which incidents.
16+
"""
17+
18+
_inherit = "spp.hazard.incident"
19+
20+
program_ids = fields.Many2many(
21+
"spp.program",
22+
"spp_program_hazard_incident_rel",
23+
"incident_id",
24+
"program_id",
25+
string="Response Programs",
26+
help="Programs responding to this incident",
27+
)
28+
program_count = fields.Integer(
29+
compute="_compute_program_count",
30+
string="Number of Programs",
31+
)
32+
33+
def _compute_program_count(self):
34+
"""Compute the number of programs responding to this incident."""
35+
for rec in self:
36+
rec.program_count = len(rec.program_ids)
37+
38+
def action_view_programs(self):
39+
"""Open a list view of programs responding to this incident."""
40+
self.ensure_one()
41+
return {
42+
"name": _("Response Programs - %s", self.name),
43+
"type": "ir.actions.act_window",
44+
"res_model": "spp.program",
45+
"view_mode": "list,form",
46+
"domain": [("id", "in", self.program_ids.ids)],
47+
"context": {},
48+
}
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
# Part of OpenSPP. See LICENSE file for full copyright and licensing details.
2+
3+
import logging
4+
5+
from odoo import _, api, fields, models
6+
7+
_logger = logging.getLogger(__name__)
8+
9+
10+
class SppProgram(models.Model):
11+
"""
12+
Extends spp.program to add hazard/emergency response capabilities.
13+
14+
This extension links programs to hazard incidents, enabling:
15+
- Emergency program eligibility based on hazard impacts
16+
- Targeting of affected populations
17+
- Emergency mode with relaxed compliance rules
18+
"""
19+
20+
_inherit = "spp.program"
21+
22+
target_incident_ids = fields.Many2many(
23+
"spp.hazard.incident",
24+
"spp_program_hazard_incident_rel",
25+
"program_id",
26+
"incident_id",
27+
string="Target Incidents",
28+
help="Incidents this program responds to. Registrants with verified "
29+
"impacts from these incidents may be eligible for this program.",
30+
)
31+
is_emergency_program = fields.Boolean(
32+
compute="_compute_is_emergency_program",
33+
store=True,
34+
help="Whether this program is responding to any active emergency incidents",
35+
)
36+
qualifying_damage_levels = fields.Selection(
37+
[
38+
("any", "Any Damage Level"),
39+
("moderate_up", "Moderate and Above"),
40+
("severe_up", "Severe and Above"),
41+
("critical_only", "Critical/Totally Damaged Only"),
42+
],
43+
default="any",
44+
help="Minimum damage level required for emergency eligibility",
45+
)
46+
is_emergency_mode = fields.Boolean(
47+
string="Emergency Mode",
48+
default=False,
49+
help="When enabled, relaxed compliance rules apply for this program. "
50+
"Automatically enabled when linked to active incidents.",
51+
)
52+
target_incident_count = fields.Integer(
53+
compute="_compute_target_incident_count",
54+
string="Number of Target Incidents",
55+
store=True,
56+
)
57+
affected_registrant_count = fields.Integer(
58+
compute="_compute_affected_registrant_count",
59+
string="Potentially Affected Registrants",
60+
)
61+
62+
@api.depends("target_incident_ids")
63+
def _compute_target_incident_count(self):
64+
"""Compute the number of target incidents."""
65+
for rec in self:
66+
rec.target_incident_count = len(rec.target_incident_ids)
67+
68+
@api.depends("target_incident_ids.status")
69+
def _compute_is_emergency_program(self):
70+
"""Compute whether this is an emergency program based on linked incidents."""
71+
for rec in self:
72+
rec.is_emergency_program = bool(
73+
rec.target_incident_ids.filtered(lambda i: i.status in ("alert", "active", "recovery"))
74+
)
75+
76+
@api.depends("target_incident_ids", "qualifying_damage_levels")
77+
def _compute_affected_registrant_count(self):
78+
"""Compute the number of potentially affected registrants."""
79+
for rec in self:
80+
if not rec.target_incident_ids:
81+
rec.affected_registrant_count = 0
82+
continue
83+
84+
# Build domain for qualifying damage levels
85+
damage_domain = rec._get_damage_level_domain()
86+
87+
# Count unique registrants with qualifying impacts
88+
impacts = self.env["spp.hazard.impact"].search(
89+
[
90+
("incident_id", "in", rec.target_incident_ids.ids),
91+
("verification_status", "=", "verified"),
92+
]
93+
+ damage_domain
94+
)
95+
rec.affected_registrant_count = len(impacts.mapped("registrant_id"))
96+
97+
def _get_damage_level_domain(self):
98+
"""Get the domain filter for qualifying damage levels."""
99+
self.ensure_one()
100+
if self.qualifying_damage_levels == "any":
101+
return []
102+
elif self.qualifying_damage_levels == "moderate_up":
103+
return [("damage_level", "in", ("moderate", "severe", "critical", "partially_damaged", "totally_damaged"))]
104+
elif self.qualifying_damage_levels == "severe_up":
105+
return [("damage_level", "in", ("severe", "critical", "totally_damaged"))]
106+
elif self.qualifying_damage_levels == "critical_only":
107+
return [("damage_level", "in", ("critical", "totally_damaged"))]
108+
return []
109+
110+
def get_emergency_eligible_registrants(self):
111+
"""
112+
Get registrants eligible for this emergency program based on hazard impacts.
113+
114+
Returns registrants who:
115+
- Have verified impact from one of the target incidents
116+
- Meet the qualifying damage level threshold
117+
118+
:return: recordset of res.partner
119+
"""
120+
self.ensure_one()
121+
if not self.target_incident_ids:
122+
return self.env["res.partner"].browse()
123+
124+
damage_domain = self._get_damage_level_domain()
125+
126+
# Find qualifying impacts
127+
impacts = self.env["spp.hazard.impact"].search(
128+
[
129+
("incident_id", "in", self.target_incident_ids.ids),
130+
("verification_status", "=", "verified"),
131+
]
132+
+ damage_domain
133+
)
134+
135+
return impacts.mapped("registrant_id")
136+
137+
def action_view_target_incidents(self):
138+
"""Open a list view of target incidents."""
139+
self.ensure_one()
140+
return {
141+
"name": _("Target Incidents - %s", self.name),
142+
"type": "ir.actions.act_window",
143+
"res_model": "spp.hazard.incident",
144+
"view_mode": "list,form",
145+
"domain": [("id", "in", self.target_incident_ids.ids)],
146+
"context": {},
147+
}
148+
149+
def action_view_affected_registrants(self):
150+
"""Open a list view of potentially affected registrants."""
151+
self.ensure_one()
152+
registrants = self.get_emergency_eligible_registrants()
153+
return {
154+
"name": _("Affected Registrants - %s", self.name),
155+
"type": "ir.actions.act_window",
156+
"res_model": "res.partner",
157+
"view_mode": "list,form",
158+
"domain": [("id", "in", registrants.ids)],
159+
"context": {},
160+
}

0 commit comments

Comments
 (0)