Skip to content

Commit 91a890a

Browse files
authored
Merge pull request #101 from OpenSPP/fix/spp-hazard-review-fixes
fix(spp_hazard performance)
2 parents 0e8f268 + 73de234 commit 91a890a

34 files changed

+4501
-297
lines changed

spp_hazard/README.rst

Lines changed: 1086 additions & 34 deletions
Large diffs are not rendered by default.

spp_hazard/__manifest__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
"spp_gis",
2424
],
2525
"data": [
26+
"security/privileges.xml",
2627
"security/groups.xml",
2728
"security/ir.model.access.csv",
2829
"data/impact_type_data.xml",

spp_hazard/demo/hazard_demo.xml

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,4 +195,145 @@
195195
<p>Below-normal rainfall for 6+ months.</p>
196196
]]></field>
197197
</record>
198+
199+
<!-- Demo Areas (spp_area provides no demo data, so minimal records are created here) -->
200+
<record id="demo_area_northern_province" model="spp.area">
201+
<field name="draft_name">Northern Province</field>
202+
<field name="code">DEMO-AREA-NORTH</field>
203+
</record>
204+
205+
<record id="demo_area_coastal_district" model="spp.area">
206+
<field name="draft_name">Coastal District</field>
207+
<field name="code">DEMO-AREA-COAST</field>
208+
</record>
209+
210+
<record id="demo_area_inland_valley" model="spp.area">
211+
<field name="draft_name">Inland Valley</field>
212+
<field name="code">DEMO-AREA-INLAND</field>
213+
</record>
214+
215+
<!-- Demo Incident Area Links (Task 6.1) -->
216+
<record id="incident_area_typhoon_northern" model="spp.hazard.incident.area">
217+
<field name="incident_id" ref="incident_typhoon_demo" />
218+
<field name="area_id" ref="demo_area_northern_province" />
219+
<field name="severity_override">5</field>
220+
<field name="affected_population_estimate">12500</field>
221+
<field
222+
name="notes"
223+
>Direct hit from typhoon eye; severe structural damage reported.</field>
224+
</record>
225+
226+
<record id="incident_area_typhoon_coastal" model="spp.hazard.incident.area">
227+
<field name="incident_id" ref="incident_typhoon_demo" />
228+
<field name="area_id" ref="demo_area_coastal_district" />
229+
<field name="severity_override">4</field>
230+
<field name="affected_population_estimate">8200</field>
231+
<field
232+
name="notes"
233+
>Storm surge and coastal flooding; fishing communities heavily impacted.</field>
234+
</record>
235+
236+
<record id="incident_area_flood_coastal" model="spp.hazard.incident.area">
237+
<field name="incident_id" ref="incident_flood_demo" />
238+
<field name="area_id" ref="demo_area_coastal_district" />
239+
<field name="severity_override">3</field>
240+
<field name="affected_population_estimate">3400</field>
241+
<field
242+
name="notes"
243+
>Riverine flooding submerged low-lying settlements for 14 days.</field>
244+
</record>
245+
246+
<record id="incident_area_drought_inland" model="spp.hazard.incident.area">
247+
<field name="incident_id" ref="incident_drought_demo" />
248+
<field name="area_id" ref="demo_area_inland_valley" />
249+
<field name="severity_override">3</field>
250+
<field name="affected_population_estimate">6700</field>
251+
<field
252+
name="notes"
253+
>Agricultural zone with total crop failure reported in two consecutive seasons.</field>
254+
</record>
255+
256+
<!-- Demo Registrants (spp_registry provides no demo data, so minimal records are created here) -->
257+
<record id="demo_registrant_santos" model="res.partner">
258+
<field name="name">SANTOS, MARIA</field>
259+
<field name="is_registrant">True</field>
260+
<field name="is_group">False</field>
261+
<field name="area_id" ref="demo_area_northern_province" />
262+
</record>
263+
264+
<record id="demo_registrant_reyes" model="res.partner">
265+
<field name="name">REYES, JOSE</field>
266+
<field name="is_registrant">True</field>
267+
<field name="is_group">False</field>
268+
<field name="area_id" ref="demo_area_coastal_district" />
269+
</record>
270+
271+
<record id="demo_registrant_cruz" model="res.partner">
272+
<field name="name">CRUZ, ANA</field>
273+
<field name="is_registrant">True</field>
274+
<field name="is_group">False</field>
275+
<field name="area_id" ref="demo_area_inland_valley" />
276+
</record>
277+
278+
<!-- Demo Impact Records (Task 6.2) -->
279+
<!-- Typhoon impacts - mix of verification statuses and damage levels -->
280+
<record id="impact_typhoon_santos_displacement" model="spp.hazard.impact">
281+
<field name="incident_id" ref="incident_typhoon_demo" />
282+
<field name="registrant_id" ref="demo_registrant_santos" />
283+
<field name="impact_type_id" ref="spp_hazard.impact_type_displacement" />
284+
<field name="damage_level">severe</field>
285+
<field name="impact_date">2024-11-15</field>
286+
<field name="verification_status">verified</field>
287+
<field
288+
name="notes"
289+
>Family evacuated to barangay hall; home partially submerged.</field>
290+
</record>
291+
292+
<record id="impact_typhoon_santos_property" model="spp.hazard.impact">
293+
<field name="incident_id" ref="incident_typhoon_demo" />
294+
<field name="registrant_id" ref="demo_registrant_santos" />
295+
<field name="impact_type_id" ref="spp_hazard.impact_type_property_damage" />
296+
<field name="damage_level">totally_damaged</field>
297+
<field name="impact_date">2024-11-16</field>
298+
<field name="verification_status">verified</field>
299+
<field name="notes">Roof collapsed; walls structurally compromised.</field>
300+
</record>
301+
302+
<record id="impact_typhoon_reyes_livelihood" model="spp.hazard.impact">
303+
<field name="incident_id" ref="incident_typhoon_demo" />
304+
<field name="registrant_id" ref="demo_registrant_reyes" />
305+
<field name="impact_type_id" ref="spp_hazard.impact_type_livelihood_loss" />
306+
<field name="damage_level">moderate</field>
307+
<field name="impact_date">2024-11-18</field>
308+
<field name="verification_status">reported</field>
309+
<field
310+
name="notes"
311+
>Fishing boat damaged; unable to work for several weeks.</field>
312+
</record>
313+
314+
<!-- Flood impact -->
315+
<record id="impact_flood_reyes_property" model="spp.hazard.impact">
316+
<field name="incident_id" ref="incident_flood_demo" />
317+
<field name="registrant_id" ref="demo_registrant_reyes" />
318+
<field name="impact_type_id" ref="spp_hazard.impact_type_property_damage" />
319+
<field name="damage_level">partially_damaged</field>
320+
<field name="impact_date">2024-10-03</field>
321+
<field name="verification_status">closed</field>
322+
<field
323+
name="notes"
324+
>Ground floor flooded; damage assessed and assistance provided.</field>
325+
</record>
326+
327+
<!-- Drought impacts -->
328+
<record id="impact_drought_cruz_crop_loss" model="spp.hazard.impact">
329+
<field name="incident_id" ref="incident_drought_demo" />
330+
<field name="registrant_id" ref="demo_registrant_cruz" />
331+
<field name="impact_type_id" ref="spp_hazard.impact_type_crop_loss" />
332+
<field name="damage_level">critical</field>
333+
<field name="impact_date">2024-05-01</field>
334+
<field name="verification_status">verified</field>
335+
<field
336+
name="notes"
337+
>Total loss of rice crop due to lack of irrigation water.</field>
338+
</record>
198339
</odoo>

spp_hazard/models/hazard_category.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,7 @@
11
# Part of OpenSPP. See LICENSE file for full copyright and licensing details.
22

3-
import logging
4-
53
from odoo import api, fields, models
64

7-
_logger = logging.getLogger(__name__)
8-
95

106
class HazardCategory(models.Model):
117
"""
@@ -83,8 +79,14 @@ def _compute_complete_name(self):
8379
@api.depends("incident_ids")
8480
def _compute_incident_count(self):
8581
"""Compute the number of incidents linked to this category."""
82+
data = self.env["spp.hazard.incident"].read_group(
83+
[("category_id", "in", self.ids)],
84+
["category_id"],
85+
["category_id"],
86+
)
87+
mapped = {d["category_id"][0]: d["category_id_count"] for d in data}
8688
for rec in self:
87-
rec.incident_count = len(rec.incident_ids)
89+
rec.incident_count = mapped.get(rec.id, 0)
8890

8991
def action_view_incidents(self):
9092
"""Open a list view of incidents for this category."""

spp_hazard/models/hazard_impact.py

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77

88
_logger = logging.getLogger(__name__)
99

10+
BATCH_SIZE = 500
11+
1012

1113
class HazardImpact(models.Model):
1214
"""
@@ -19,7 +21,7 @@ class HazardImpact(models.Model):
1921

2022
_name = "spp.hazard.impact"
2123
_description = "Hazard Impact"
22-
_order = "impact_date desc, incident_id"
24+
_order = "impact_date desc, id"
2325
_inherit = ["mail.thread", "mail.activity.mixin"]
2426

2527
incident_id = fields.Many2one(
@@ -125,8 +127,6 @@ class HazardImpact(models.Model):
125127
@api.constrains("impact_date", "incident_id")
126128
def _check_impact_date(self):
127129
"""Validate that impact_date is not before the incident start_date."""
128-
# Prefetch incidents to avoid N+1 queries
129-
self.mapped("incident_id")
130130
for rec in self:
131131
if rec.impact_date and rec.incident_id.start_date:
132132
if rec.impact_date < rec.incident_id.start_date:
@@ -222,5 +222,15 @@ def bulk_create_impacts(self, incident, area, impact_type, damage_level):
222222
)
223223

224224
if vals_list:
225-
return self.create(vals_list)
225+
created = self.browse()
226+
for i in range(0, len(vals_list), BATCH_SIZE):
227+
batch = vals_list[i : i + BATCH_SIZE]
228+
created |= self.create(batch)
229+
_logger.info(
230+
"Created %d impact records for incident '%s' in area '%s'",
231+
len(created),
232+
incident.name,
233+
area.name,
234+
)
235+
return created
226236
return self.browse()

spp_hazard/models/hazard_impact_type.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,7 @@
11
# Part of OpenSPP. See LICENSE file for full copyright and licensing details.
22

3-
import logging
4-
53
from odoo import api, fields, models
64

7-
_logger = logging.getLogger(__name__)
8-
95

106
class HazardImpactType(models.Model):
117
"""
@@ -68,5 +64,11 @@ class HazardImpactType(models.Model):
6864
@api.depends("impact_ids")
6965
def _compute_impact_count(self):
7066
"""Compute the number of impact records using this type."""
67+
data = self.env["spp.hazard.impact"].read_group(
68+
[("impact_type_id", "in", self.ids)],
69+
["impact_type_id"],
70+
["impact_type_id"],
71+
)
72+
mapped = {d["impact_type_id"][0]: d["impact_type_id_count"] for d in data}
7173
for rec in self:
72-
rec.impact_count = len(rec.impact_ids)
74+
rec.impact_count = mapped.get(rec.id, 0)

spp_hazard/models/hazard_incident.py

Lines changed: 51 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -141,23 +141,44 @@ def _compute_is_ongoing(self):
141141
"recovery",
142142
)
143143

144-
@api.depends("area_ids")
144+
@api.depends("area_ids", "incident_area_ids.area_id")
145145
def _compute_area_count(self):
146-
"""Compute the number of affected areas."""
146+
"""Compute the number of affected areas from both M2M and detail records."""
147147
for rec in self:
148-
rec.area_count = len(rec.area_ids)
148+
detail_areas = rec.incident_area_ids.mapped("area_id")
149+
all_areas = rec.area_ids | detail_areas
150+
rec.area_count = len(all_areas)
149151

150152
@api.depends("impact_ids")
151153
def _compute_impact_count(self):
152154
"""Compute the number of impact records."""
155+
data = self.env["spp.hazard.impact"].read_group(
156+
[("incident_id", "in", self.ids)],
157+
["incident_id"],
158+
["incident_id"],
159+
)
160+
mapped = {d["incident_id"][0]: d["incident_id_count"] for d in data}
153161
for rec in self:
154-
rec.impact_count = len(rec.impact_ids)
162+
rec.impact_count = mapped.get(rec.id, 0)
155163

156164
@api.depends("impact_ids.registrant_id")
157165
def _compute_affected_registrant_count(self):
158166
"""Compute the number of unique affected registrants."""
167+
if not self.ids:
168+
self.affected_registrant_count = 0
169+
return
170+
self.env.cr.execute(
171+
"""
172+
SELECT incident_id, COUNT(DISTINCT registrant_id)
173+
FROM spp_hazard_impact
174+
WHERE incident_id IN %s
175+
GROUP BY incident_id
176+
""",
177+
[tuple(self.ids)],
178+
)
179+
mapped = dict(self.env.cr.fetchall())
159180
for rec in self:
160-
rec.affected_registrant_count = len(rec.impact_ids.mapped("registrant_id"))
181+
rec.affected_registrant_count = mapped.get(rec.id, 0)
161182

162183
def action_set_active(self):
163184
"""Set incident status to active."""
@@ -169,11 +190,17 @@ def action_set_recovery(self):
169190

170191
def action_close(self):
171192
"""Close the incident."""
172-
self.write(
173-
{
174-
"status": "closed",
175-
"end_date": self.end_date or fields.Date.today(),
176-
}
193+
for rec in self:
194+
rec.write(
195+
{
196+
"status": "closed",
197+
"end_date": rec.end_date or fields.Date.today(),
198+
}
199+
)
200+
_logger.info(
201+
"Closed %d incident(s): %s",
202+
len(self),
203+
", ".join(self.mapped("name")),
177204
)
178205

179206
def action_view_impacts(self):
@@ -188,6 +215,12 @@ def action_view_impacts(self):
188215
"context": {"default_incident_id": self.id},
189216
}
190217

218+
def _get_all_area_ids(self):
219+
"""Get all affected area IDs from both M2M and detail records."""
220+
self.ensure_one()
221+
detail_areas = self.incident_area_ids.mapped("area_id")
222+
return (self.area_ids | detail_areas).ids
223+
191224
def action_view_areas(self):
192225
"""Open a list view of affected areas."""
193226
self.ensure_one()
@@ -196,7 +229,7 @@ def action_view_areas(self):
196229
"type": "ir.actions.act_window",
197230
"res_model": "spp.area",
198231
"view_mode": "list,form",
199-
"domain": [("id", "in", self.area_ids.ids)],
232+
"domain": [("id", "in", self._get_all_area_ids())],
200233
}
201234

202235
def identify_potentially_affected_registrants(self):
@@ -207,14 +240,15 @@ def identify_potentially_affected_registrants(self):
207240
affected based on their location in the incident's geographic scope.
208241
"""
209242
self.ensure_one()
210-
if not self.area_ids:
243+
all_area_ids = self._get_all_area_ids()
244+
if not all_area_ids:
211245
return self.env["res.partner"].browse()
212246

213247
# Find registrants in affected areas
214248
return self.env["res.partner"].search(
215249
[
216250
("is_registrant", "=", True),
217-
("area_id", "in", self.area_ids.ids),
251+
("area_id", "in", all_area_ids),
218252
]
219253
)
220254

@@ -230,6 +264,7 @@ class HazardIncidentArea(models.Model):
230264
_name = "spp.hazard.incident.area"
231265
_description = "Hazard Incident Area"
232266
_order = "incident_id, area_id"
267+
_rec_name = "display_name"
233268

234269
incident_id = fields.Many2one(
235270
"spp.hazard.incident",
@@ -267,13 +302,7 @@ class HazardIncidentArea(models.Model):
267302
"This area is already linked to this incident!",
268303
)
269304

270-
def name_get(self):
271-
"""Return a descriptive name for the record."""
272-
# Prefetch related records to avoid N+1 queries
273-
self.mapped("incident_id")
274-
self.mapped("area_id")
275-
result = []
305+
@api.depends("incident_id.name", "area_id.name")
306+
def _compute_display_name(self):
276307
for rec in self:
277-
name = f"{rec.incident_id.name} - {rec.area_id.name}"
278-
result.append((rec.id, name))
279-
return result
308+
rec.display_name = f"{rec.incident_id.name} - {rec.area_id.name}"

0 commit comments

Comments
 (0)