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,163 changes: 1,117 additions & 46 deletions spp_hazard/README.rst

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions spp_hazard/__manifest__.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"spp_gis",
],
"data": [
"security/privileges.xml",
"security/groups.xml",
"security/ir.model.access.csv",
"data/impact_type_data.xml",
Expand Down
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)
73 changes: 51 additions & 22 deletions spp_hazard/models/hazard_incident.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,23 +141,44 @@ def _compute_is_ongoing(self):
"recovery",
)

@api.depends("area_ids")
@api.depends("area_ids", "incident_area_ids.area_id")
def _compute_area_count(self):
"""Compute the number of affected areas."""
"""Compute the number of affected areas from both M2M and detail records."""
for rec in self:
rec.area_count = len(rec.area_ids)
detail_areas = rec.incident_area_ids.mapped("area_id")
all_areas = rec.area_ids | detail_areas
rec.area_count = len(all_areas)

@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 +190,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 All @@ -188,6 +215,12 @@ def action_view_impacts(self):
"context": {"default_incident_id": self.id},
}

def _get_all_area_ids(self):
"""Get all affected area IDs from both M2M and detail records."""
self.ensure_one()
detail_areas = self.incident_area_ids.mapped("area_id")
return (self.area_ids | detail_areas).ids

def action_view_areas(self):
"""Open a list view of affected areas."""
self.ensure_one()
Expand All @@ -196,7 +229,7 @@ def action_view_areas(self):
"type": "ir.actions.act_window",
"res_model": "spp.area",
"view_mode": "list,form",
"domain": [("id", "in", self.area_ids.ids)],
"domain": [("id", "in", self._get_all_area_ids())],
}

def identify_potentially_affected_registrants(self):
Expand All @@ -207,14 +240,15 @@ def identify_potentially_affected_registrants(self):
affected based on their location in the incident's geographic scope.
"""
self.ensure_one()
if not self.area_ids:
all_area_ids = self._get_all_area_ids()
if not all_area_ids:
return self.env["res.partner"].browse()

# Find registrants in affected areas
return self.env["res.partner"].search(
[
("is_registrant", "=", True),
("area_id", "in", self.area_ids.ids),
("area_id", "in", all_area_ids),
]
)

Expand All @@ -230,6 +264,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 +302,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}"
Loading
Loading