Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,120 changes: 1,086 additions & 34 deletions spp_hazard/README.rst

Large diffs are not rendered by default.

141 changes: 141 additions & 0 deletions spp_hazard/demo/hazard_demo.xml
Original file line number Diff line number Diff line change
Expand Up @@ -195,4 +195,145 @@
<p>Below-normal rainfall for 6+ months.</p>
]]></field>
</record>

<!-- Demo Areas (spp_area provides no demo data, so minimal records are created here) -->
<record id="demo_area_northern_province" model="spp.area">
<field name="draft_name">Northern Province</field>
<field name="code">DEMO-AREA-NORTH</field>
</record>

<record id="demo_area_coastal_district" model="spp.area">
<field name="draft_name">Coastal District</field>
<field name="code">DEMO-AREA-COAST</field>
</record>

<record id="demo_area_inland_valley" model="spp.area">
<field name="draft_name">Inland Valley</field>
<field name="code">DEMO-AREA-INLAND</field>
</record>

<!-- Demo Incident Area Links (Task 6.1) -->
<record id="incident_area_typhoon_northern" model="spp.hazard.incident.area">
<field name="incident_id" ref="incident_typhoon_demo" />
<field name="area_id" ref="demo_area_northern_province" />
<field name="severity_override">5</field>
<field name="affected_population_estimate">12500</field>
<field
name="notes"
>Direct hit from typhoon eye; severe structural damage reported.</field>
</record>

<record id="incident_area_typhoon_coastal" model="spp.hazard.incident.area">
<field name="incident_id" ref="incident_typhoon_demo" />
<field name="area_id" ref="demo_area_coastal_district" />
<field name="severity_override">4</field>
<field name="affected_population_estimate">8200</field>
<field
name="notes"
>Storm surge and coastal flooding; fishing communities heavily impacted.</field>
</record>

<record id="incident_area_flood_coastal" model="spp.hazard.incident.area">
<field name="incident_id" ref="incident_flood_demo" />
<field name="area_id" ref="demo_area_coastal_district" />
<field name="severity_override">3</field>
<field name="affected_population_estimate">3400</field>
<field
name="notes"
>Riverine flooding submerged low-lying settlements for 14 days.</field>
</record>

<record id="incident_area_drought_inland" model="spp.hazard.incident.area">
<field name="incident_id" ref="incident_drought_demo" />
<field name="area_id" ref="demo_area_inland_valley" />
<field name="severity_override">3</field>
<field name="affected_population_estimate">6700</field>
<field
name="notes"
>Agricultural zone with total crop failure reported in two consecutive seasons.</field>
</record>

<!-- Demo Registrants (spp_registry provides no demo data, so minimal records are created here) -->
<record id="demo_registrant_santos" model="res.partner">
<field name="name">SANTOS, MARIA</field>
<field name="is_registrant">True</field>
<field name="is_group">False</field>
<field name="area_id" ref="demo_area_northern_province" />
</record>

<record id="demo_registrant_reyes" model="res.partner">
<field name="name">REYES, JOSE</field>
<field name="is_registrant">True</field>
<field name="is_group">False</field>
<field name="area_id" ref="demo_area_coastal_district" />
</record>

<record id="demo_registrant_cruz" model="res.partner">
<field name="name">CRUZ, ANA</field>
<field name="is_registrant">True</field>
<field name="is_group">False</field>
<field name="area_id" ref="demo_area_inland_valley" />
</record>

<!-- Demo Impact Records (Task 6.2) -->
<!-- Typhoon impacts - mix of verification statuses and damage levels -->
<record id="impact_typhoon_santos_displacement" model="spp.hazard.impact">
<field name="incident_id" ref="incident_typhoon_demo" />
<field name="registrant_id" ref="demo_registrant_santos" />
<field name="impact_type_id" ref="spp_hazard.impact_type_displacement" />
<field name="damage_level">severe</field>
<field name="impact_date">2024-11-15</field>
<field name="verification_status">verified</field>
<field
name="notes"
>Family evacuated to barangay hall; home partially submerged.</field>
</record>

<record id="impact_typhoon_santos_property" model="spp.hazard.impact">
<field name="incident_id" ref="incident_typhoon_demo" />
<field name="registrant_id" ref="demo_registrant_santos" />
<field name="impact_type_id" ref="spp_hazard.impact_type_property_damage" />
<field name="damage_level">totally_damaged</field>
<field name="impact_date">2024-11-16</field>
<field name="verification_status">verified</field>
<field name="notes">Roof collapsed; walls structurally compromised.</field>
</record>

<record id="impact_typhoon_reyes_livelihood" model="spp.hazard.impact">
<field name="incident_id" ref="incident_typhoon_demo" />
<field name="registrant_id" ref="demo_registrant_reyes" />
<field name="impact_type_id" ref="spp_hazard.impact_type_livelihood_loss" />
<field name="damage_level">moderate</field>
<field name="impact_date">2024-11-18</field>
<field name="verification_status">reported</field>
<field
name="notes"
>Fishing boat damaged; unable to work for several weeks.</field>
</record>

<!-- Flood impact -->
<record id="impact_flood_reyes_property" model="spp.hazard.impact">
<field name="incident_id" ref="incident_flood_demo" />
<field name="registrant_id" ref="demo_registrant_reyes" />
<field name="impact_type_id" ref="spp_hazard.impact_type_property_damage" />
<field name="damage_level">partially_damaged</field>
<field name="impact_date">2024-10-03</field>
<field name="verification_status">closed</field>
<field
name="notes"
>Ground floor flooded; damage assessed and assistance provided.</field>
</record>

<!-- Drought impacts -->
<record id="impact_drought_cruz_crop_loss" model="spp.hazard.impact">
<field name="incident_id" ref="incident_drought_demo" />
<field name="registrant_id" ref="demo_registrant_cruz" />
<field name="impact_type_id" ref="spp_hazard.impact_type_crop_loss" />
<field name="damage_level">critical</field>
<field name="impact_date">2024-05-01</field>
<field name="verification_status">verified</field>
<field
name="notes"
>Total loss of rice crop due to lack of irrigation water.</field>
</record>
</odoo>
12 changes: 7 additions & 5 deletions spp_hazard/models/hazard_category.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,7 @@
# Part of OpenSPP. See LICENSE file for full copyright and licensing details.

import logging

from odoo import api, fields, models

_logger = logging.getLogger(__name__)


class HazardCategory(models.Model):
"""
Expand Down Expand Up @@ -83,8 +79,14 @@ def _compute_complete_name(self):
@api.depends("incident_ids")
def _compute_incident_count(self):
"""Compute the number of incidents linked to this category."""
data = self.env["spp.hazard.incident"].read_group(
[("category_id", "in", self.ids)],
["category_id"],
["category_id"],
)
mapped = {d["category_id"][0]: d["category_id_count"] for d in data}
for rec in self:
rec.incident_count = len(rec.incident_ids)
rec.incident_count = mapped.get(rec.id, 0)

def action_view_incidents(self):
"""Open a list view of incidents for this category."""
Expand Down
16 changes: 13 additions & 3 deletions spp_hazard/models/hazard_impact.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@

_logger = logging.getLogger(__name__)

BATCH_SIZE = 500


class HazardImpact(models.Model):
"""
Expand Down Expand Up @@ -125,8 +127,6 @@ class HazardImpact(models.Model):
@api.constrains("impact_date", "incident_id")
def _check_impact_date(self):
"""Validate that impact_date is not before the incident start_date."""
# Prefetch incidents to avoid N+1 queries
self.mapped("incident_id")
for rec in self:
if rec.impact_date and rec.incident_id.start_date:
if rec.impact_date < rec.incident_id.start_date:
Expand Down Expand Up @@ -222,5 +222,15 @@ def bulk_create_impacts(self, incident, area, impact_type, damage_level):
)

if vals_list:
return self.create(vals_list)
created = self.browse()
for i in range(0, len(vals_list), BATCH_SIZE):
batch = vals_list[i : i + BATCH_SIZE]
created |= self.create(batch)
_logger.info(
"Created %d impact records for incident '%s' in area '%s'",
len(created),
incident.name,
area.name,
)
return created
return self.browse()
12 changes: 7 additions & 5 deletions spp_hazard/models/hazard_impact_type.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,7 @@
# Part of OpenSPP. See LICENSE file for full copyright and licensing details.

import logging

from odoo import api, fields, models

_logger = logging.getLogger(__name__)


class HazardImpactType(models.Model):
"""
Expand Down Expand Up @@ -68,5 +64,11 @@ class HazardImpactType(models.Model):
@api.depends("impact_ids")
def _compute_impact_count(self):
"""Compute the number of impact records using this type."""
data = self.env["spp.hazard.impact"].read_group(
[("impact_type_id", "in", self.ids)],
["impact_type_id"],
["impact_type_id"],
)
mapped = {d["impact_type_id"][0]: d["impact_type_id_count"] for d in data}
for rec in self:
rec.impact_count = len(rec.impact_ids)
rec.impact_count = mapped.get(rec.id, 0)
52 changes: 36 additions & 16 deletions spp_hazard/models/hazard_incident.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,14 +150,33 @@ def _compute_area_count(self):
@api.depends("impact_ids")
def _compute_impact_count(self):
"""Compute the number of impact records."""
data = self.env["spp.hazard.impact"].read_group(
[("incident_id", "in", self.ids)],
["incident_id"],
["incident_id"],
)
mapped = {d["incident_id"][0]: d["incident_id_count"] for d in data}
for rec in self:
rec.impact_count = len(rec.impact_ids)
rec.impact_count = mapped.get(rec.id, 0)

@api.depends("impact_ids.registrant_id")
def _compute_affected_registrant_count(self):
"""Compute the number of unique affected registrants."""
if not self.ids:
self.affected_registrant_count = 0
return
self.env.cr.execute(
"""
SELECT incident_id, COUNT(DISTINCT registrant_id)
FROM spp_hazard_impact
WHERE incident_id IN %s
GROUP BY incident_id
""",
[tuple(self.ids)],
)
mapped = dict(self.env.cr.fetchall())
for rec in self:
rec.affected_registrant_count = len(rec.impact_ids.mapped("registrant_id"))
rec.affected_registrant_count = mapped.get(rec.id, 0)

def action_set_active(self):
"""Set incident status to active."""
Expand All @@ -169,11 +188,17 @@ def action_set_recovery(self):

def action_close(self):
"""Close the incident."""
self.write(
{
"status": "closed",
"end_date": self.end_date or fields.Date.today(),
}
for rec in self:
rec.write(
{
"status": "closed",
"end_date": rec.end_date or fields.Date.today(),
}
)
_logger.info(
"Closed %d incident(s): %s",
len(self),
", ".join(self.mapped("name")),
)

def action_view_impacts(self):
Expand Down Expand Up @@ -230,6 +255,7 @@ class HazardIncidentArea(models.Model):
_name = "spp.hazard.incident.area"
_description = "Hazard Incident Area"
_order = "incident_id, area_id"
_rec_name = "display_name"

incident_id = fields.Many2one(
"spp.hazard.incident",
Expand Down Expand Up @@ -267,13 +293,7 @@ class HazardIncidentArea(models.Model):
"This area is already linked to this incident!",
)

def name_get(self):
"""Return a descriptive name for the record."""
# Prefetch related records to avoid N+1 queries
self.mapped("incident_id")
self.mapped("area_id")
result = []
@api.depends("incident_id.name", "area_id.name")
def _compute_display_name(self):
for rec in self:
name = f"{rec.incident_id.name} - {rec.area_id.name}"
result.append((rec.id, name))
return result
rec.display_name = f"{rec.incident_id.name} - {rec.area_id.name}"
21 changes: 14 additions & 7 deletions spp_hazard/models/registrant.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,7 @@
# Part of OpenSPP. See LICENSE file for full copyright and licensing details.

import logging

from odoo import _, api, fields, models

_logger = logging.getLogger(__name__)


class ResPartner(models.Model):
"""
Expand All @@ -29,23 +25,34 @@ class ResPartner(models.Model):
)
has_active_impact = fields.Boolean(
compute="_compute_has_active_impact",
string="Has Active Impact",
store=True,
help="Whether the registrant has an impact from an active incident",
)

@api.depends("hazard_impact_ids")
def _compute_hazard_impact_count(self):
"""Compute the number of hazard impacts for this registrant."""
data = self.env["spp.hazard.impact"].read_group(
[("registrant_id", "in", self.ids)],
["registrant_id"],
["registrant_id"],
)
mapped = {d["registrant_id"][0]: d["registrant_id_count"] for d in data}
for rec in self:
rec.hazard_impact_count = len(rec.hazard_impact_ids)
rec.hazard_impact_count = mapped.get(rec.id, 0)

@api.depends("hazard_impact_ids", "hazard_impact_ids.incident_id.status")
def _compute_has_active_impact(self):
"""Compute whether the registrant has an impact from an active incident."""
for rec in self:
rec.has_active_impact = bool(
rec.hazard_impact_ids.filtered(lambda i: i.incident_id.status in ("alert", "active", "recovery"))
self.env["spp.hazard.impact"].search_count(
[
("registrant_id", "=", rec.id),
("incident_id.status", "in", ("alert", "active", "recovery")),
],
limit=1,
)
)

def action_view_hazard_impacts(self):
Expand Down
Loading
Loading