Skip to content

Commit 4af5a08

Browse files
committed
feat(dref-final): Add flag on dref final report, proposed action
- Add Validation checks for the new dref imminent
1 parent a388c71 commit 4af5a08

File tree

4 files changed

+317
-21
lines changed

4 files changed

+317
-21
lines changed
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
# Generated by Django 4.2.19 on 2025-07-08 10:51
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
("dref", "0081_remove_dref_hazard_date_and_more"),
10+
]
11+
12+
operations = [
13+
migrations.AddField(
14+
model_name="dreffinalreport",
15+
name="indirect_cost",
16+
field=models.PositiveIntegerField(blank=True, null=True, verbose_name="Indirect Cost"),
17+
),
18+
migrations.AddField(
19+
model_name="dreffinalreport",
20+
name="indirect_expenditure_cost",
21+
field=models.PositiveIntegerField(blank=True, null=True, verbose_name="Indirect Expenditure Cost"),
22+
),
23+
migrations.AddField(
24+
model_name="dreffinalreport",
25+
name="is_dref_imminent_v2",
26+
field=models.BooleanField(default=False, verbose_name="Is DREF Imminent V2?"),
27+
),
28+
migrations.AddField(
29+
model_name="dreffinalreport",
30+
name="lessons_learned_and_challenges",
31+
field=models.TextField(blank=True, null=True, verbose_name="Lessons learnt and challenges"),
32+
),
33+
migrations.AddField(
34+
model_name="dreffinalreport",
35+
name="mitigation_efforts_and_achievements",
36+
field=models.TextField(blank=True, null=True, verbose_name="Mitigation Efforts and Achievements"),
37+
),
38+
migrations.AddField(
39+
model_name="dreffinalreport",
40+
name="proposed_action",
41+
field=models.ManyToManyField(blank=True, to="dref.proposedaction", verbose_name="Proposed Action"),
42+
),
43+
migrations.AddField(
44+
model_name="dreffinalreport",
45+
name="sub_total_cost",
46+
field=models.PositiveIntegerField(blank=True, null=True, verbose_name="Sub total Cost"),
47+
),
48+
migrations.AddField(
49+
model_name="dreffinalreport",
50+
name="sub_total_expenditure_cost",
51+
field=models.PositiveIntegerField(blank=True, null=True, verbose_name="Sub total Expenditure Cost"),
52+
),
53+
migrations.AddField(
54+
model_name="dreffinalreport",
55+
name="surge_deployment_cost",
56+
field=models.PositiveIntegerField(blank=True, null=True, verbose_name="Surge Deployment Cost"),
57+
),
58+
migrations.AddField(
59+
model_name="dreffinalreport",
60+
name="surge_deployment_expenditure_cost",
61+
field=models.PositiveIntegerField(blank=True, null=True, verbose_name="Surge Deployment Expenditure Cost"),
62+
),
63+
migrations.AddField(
64+
model_name="dreffinalreport",
65+
name="total_cost",
66+
field=models.PositiveIntegerField(blank=True, null=True, verbose_name="Total Cost"),
67+
),
68+
migrations.AddField(
69+
model_name="dreffinalreport",
70+
name="total_expenditure_cost",
71+
field=models.PositiveIntegerField(blank=True, null=True, verbose_name="Total Expenditure Cost"),
72+
),
73+
migrations.AddField(
74+
model_name="proposedaction",
75+
name="total_expenditure",
76+
field=models.PositiveIntegerField(
77+
blank=True, help_text="Total expenditure for the proposed action", null=True, verbose_name="Expenditure"
78+
),
79+
),
80+
]

dref/models.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,12 @@ class Action(models.IntegerChoices):
235235
)
236236
activities = models.ManyToManyField(ProposedActionActivities, verbose_name=_("Activities"), blank=True)
237237
total_budget = models.PositiveIntegerField(verbose_name=_("Total Purpose Action Budget"), blank=True, null=True)
238+
total_expenditure = models.PositiveIntegerField(
239+
verbose_name=_("Expenditure"),
240+
blank=True,
241+
null=True,
242+
help_text=_("Total expenditure for the proposed action"),
243+
)
238244

239245
def __str__(self) -> str:
240246
return f"{self.get_proposed_type_display()}-{self.total_budget}"
@@ -1582,6 +1588,46 @@ class DrefFinalReport(models.Model):
15821588
main_donors = models.TextField(verbose_name=_("Main Donors"), null=True, blank=True)
15831589
operation_end_date = models.DateField(verbose_name=_("Operation End Date"), null=True, blank=True)
15841590
source_information = models.ManyToManyField(SourceInformation, blank=True, verbose_name=_("Source Information"))
1591+
# NOTE: Flag to indicate if this is an new dref imminent type
1592+
is_dref_imminent_v2 = models.BooleanField(
1593+
verbose_name=_("Is DREF Imminent V2?"),
1594+
default=False,
1595+
)
1596+
mitigation_efforts_and_achievements = models.TextField(
1597+
verbose_name=_("Mitigation Efforts and Achievements"),
1598+
null=True,
1599+
blank=True,
1600+
)
1601+
lessons_learned_and_challenges = models.TextField(
1602+
verbose_name=_("Lessons learnt and challenges"),
1603+
blank=True,
1604+
null=True,
1605+
)
1606+
proposed_action = models.ManyToManyField(ProposedAction, verbose_name=_("Proposed Action"), blank=True)
1607+
sub_total_cost = models.PositiveIntegerField(verbose_name=_("Sub total Cost"), blank=True, null=True)
1608+
sub_total_expenditure_cost = models.PositiveIntegerField(
1609+
verbose_name=_("Sub total Expenditure Cost"),
1610+
blank=True,
1611+
null=True,
1612+
)
1613+
surge_deployment_cost = models.PositiveIntegerField(verbose_name=_("Surge Deployment Cost"), null=True, blank=True)
1614+
surge_deployment_expenditure_cost = models.PositiveIntegerField(
1615+
verbose_name=_("Surge Deployment Expenditure Cost"),
1616+
null=True,
1617+
blank=True,
1618+
)
1619+
indirect_cost = models.PositiveIntegerField(verbose_name=_("Indirect Cost"), null=True, blank=True)
1620+
indirect_expenditure_cost = models.PositiveIntegerField(
1621+
verbose_name=_("Indirect Expenditure Cost"),
1622+
null=True,
1623+
blank=True,
1624+
)
1625+
total_cost = models.PositiveIntegerField(verbose_name=_("Total Cost"), null=True, blank=True)
1626+
total_expenditure_cost = models.PositiveIntegerField(
1627+
verbose_name=_("Total Expenditure Cost"),
1628+
null=True,
1629+
blank=True,
1630+
)
15851631
__financial_report_id = None
15861632

15871633
class Meta:

dref/serializers.py

Lines changed: 88 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -429,6 +429,7 @@ class Meta:
429429
"modified_by",
430430
"created_by",
431431
"budget_file_preview",
432+
"is_dref_imminent_v2",
432433
)
433434
exclude = (
434435
"cover_image",
@@ -1083,6 +1084,7 @@ def update(self, instance, validated_data):
10831084

10841085
class DrefFinalReportSerializer(NestedUpdateMixin, NestedCreateMixin, ModelSerializer):
10851086
MAX_NUMBER_OF_PHOTOS = 4
1087+
SUB_TOTAL_COST = 75000
10861088
national_society_actions = NationalSocietyActionSerializer(many=True, required=False)
10871089
needs_identified = IdentifiedNeedSerializer(many=True, required=False)
10881090
planned_interventions = PlannedInterventionSerializer(many=True, required=False)
@@ -1105,13 +1107,15 @@ class DrefFinalReportSerializer(NestedUpdateMixin, NestedCreateMixin, ModelSeria
11051107
modified_by_details = UserNameSerializer(source="modified_by", read_only=True)
11061108
disaster_type_details = DisasterTypeSerializer(source="disaster_type", read_only=True)
11071109
source_information = SourceInformationSerializer(many=True, required=False)
1110+
proposed_action = ProposedActionSerializer(many=True, required=False)
11081111

11091112
class Meta:
11101113
model = DrefFinalReport
11111114
read_only_fields = (
11121115
"modified_by",
11131116
"created_by",
11141117
"financial_report_preview",
1118+
"is_dref_imminent_v2",
11151119
)
11161120
exclude = (
11171121
"images",
@@ -1123,7 +1127,7 @@ class Meta:
11231127

11241128
def validate(self, data):
11251129
dref = data.get("dref")
1126-
# check if dref is published and operational_update associated with it is also published
1130+
# Check if dref is published and operational_update associated with it is also published
11271131
if not self.instance and dref:
11281132
if not dref.is_published:
11291133
raise serializers.ValidationError(gettext("Can't create Final Report for not published dref %s." % dref.id))
@@ -1141,6 +1145,68 @@ def validate(self, data):
11411145

11421146
if self.instance and self.instance.is_published:
11431147
raise serializers.ValidationError(gettext("Can't update published final report %s." % self.instance.id))
1148+
1149+
# NOTE: Validation for type DREF Imminent
1150+
if self.instance and self.instance.is_dref_imminent_v2 and data.get("type_of_dref") == Dref.DrefType.IMMINENT:
1151+
sub_total_cost = data.get("sub_total_cost")
1152+
sub_total_expenditure_cost = data.get("sub_total_expenditure_cost")
1153+
surge_deployment_expenditure_cost = data.get("surge_deployment_expenditure_cost") or 0
1154+
indirect_cost = data.get("indirect_cost")
1155+
indirect_expenditure_cost = data.get("indirect_expenditure_cost")
1156+
total_cost = data.get("total_cost")
1157+
total_expenditure_cost = data.get("total_expenditure_cost")
1158+
proposed_actions = data.get("proposed_action", [])
1159+
1160+
if not proposed_actions:
1161+
raise serializers.ValidationError(
1162+
{"proposed_action": gettext("Proposed Action is required for type DREF Imminent")}
1163+
)
1164+
if not sub_total_cost:
1165+
raise serializers.ValidationError({"sub_total_cost": gettext("Sub-total is required for Imminent DREF")})
1166+
if not sub_total_expenditure_cost:
1167+
raise serializers.ValidationError(
1168+
{"sub_total_expenditure_cost": gettext("Sub-total Expenditure is required for Imminent DREF")}
1169+
)
1170+
if sub_total_cost != self.SUB_TOTAL_COST:
1171+
raise serializers.ValidationError(
1172+
{"sub_total": gettext("Sub-total should be equal to %s for Imminent DREF" % self.SUB_TOTAL_COST)}
1173+
)
1174+
if not indirect_cost:
1175+
raise serializers.ValidationError({"indirect_cost": gettext("Indirect Cost is required for Imminent DREF")})
1176+
if not indirect_expenditure_cost:
1177+
raise serializers.ValidationError(
1178+
{"indirect_expenditure_cost": gettext("Indirect Expenditure is required for Imminent DREF")}
1179+
)
1180+
if not total_cost:
1181+
raise serializers.ValidationError({"total_cost": gettext("Total is required for Imminent DREF")})
1182+
if not total_expenditure_cost:
1183+
raise serializers.ValidationError(
1184+
{"total_expenditure_cost": gettext("Total Expenditure is required for Imminent DREF")}
1185+
)
1186+
1187+
total_proposed_budget: int = 0
1188+
total_proposed_expenditure: int = 0
1189+
for action in proposed_actions:
1190+
total_proposed_budget += action.get("total_budget", 0)
1191+
total_proposed_expenditure += action.get("total_expenditure", 0)
1192+
if total_proposed_budget != sub_total_cost:
1193+
raise serializers.ValidationError({"sub_total_cost": gettext("Sub-total should be equal to proposed budget.")})
1194+
if total_proposed_expenditure != sub_total_expenditure_cost:
1195+
raise serializers.ValidationError(
1196+
{"sub_total_expenditure_cost": gettext("Sub-total Expenditure should be equal to proposed expenditure.")}
1197+
)
1198+
expected_total_expenditure_cost: int = (
1199+
sub_total_expenditure_cost + surge_deployment_expenditure_cost + indirect_expenditure_cost
1200+
)
1201+
if expected_total_expenditure_cost != total_expenditure_cost:
1202+
raise serializers.ValidationError(
1203+
{
1204+
"total_expenditure_cost": gettext(
1205+
"Total Expenditure Cost should be equal to sum of Sub-total Expenditure, "
1206+
"Surge Deployment Expenditure and Indirect Expenditure Cost."
1207+
)
1208+
}
1209+
)
11441210
return data
11451211

11461212
def validate_photos(self, photos):
@@ -1274,11 +1340,6 @@ def create(self, validated_data):
12741340
if validated_data["type_of_dref"] == Dref.DrefType.LOAN:
12751341
raise serializers.ValidationError(gettext("Can't create final report for dref type %s" % Dref.DrefType.LOAN))
12761342

1277-
# TODO: Remove me! After final report is implemented for drefs IMMINENT
1278-
if validated_data["type_of_dref"] == Dref.DrefType.IMMINENT and dref.is_dref_imminent_v2:
1279-
raise serializers.ValidationError(
1280-
gettext("Can't create final report for newly created dref type %s" % Dref.DrefType.IMMINENT.label)
1281-
)
12821343
dref_final_report = super().create(validated_data)
12831344
# XXX: Copy files from DREF (Only nested serialized fields)
12841345
nested_serialized_file_fields = [
@@ -1395,12 +1456,26 @@ def create(self, validated_data):
13951456
raise serializers.ValidationError(
13961457
gettext("Can't create final report for dref type %s" % Dref.DrefType.LOAN.label)
13971458
)
1398-
1399-
# TODO: Remove me! After final report is implemented for drefs IMMINENT
1459+
# NOTE: Checks for the new dref final reports of new dref imminents
14001460
if validated_data["type_of_dref"] == Dref.DrefType.IMMINENT and dref.is_dref_imminent_v2:
1401-
raise serializers.ValidationError(
1402-
gettext("Can't create final report for newly created dref type %s" % Dref.DrefType.IMMINENT.label)
1403-
)
1461+
validated_data["is_dref_imminent_v2"] = True
1462+
validated_data["sub_total_cost"] = dref.sub_total_cost
1463+
validated_data["surge_deployment_cost"] = dref.surge_deployment_cost
1464+
validated_data["surge_deployment_expenditure_cost"] = dref.surge_deployment_cost
1465+
validated_data["indirect_cost"] = dref.indirect_cost
1466+
validated_data["indirect_expenditure_cost"] = dref.indirect_cost
1467+
validated_data["total_cost"] = dref.total_cost
1468+
1469+
# NOTE: Checks for the new dref final reports of new dref imminents
1470+
if validated_data["type_of_dref"] == Dref.DrefType.IMMINENT and dref.is_dref_imminent_v2:
1471+
validated_data["is_dref_imminent_v2"] = True
1472+
validated_data["sub_total_cost"] = dref.sub_total_cost
1473+
validated_data["surge_deployment_cost"] = dref.surge_deployment_cost
1474+
validated_data["surge_deployment_expenditure_cost"] = dref.surge_deployment_cost
1475+
validated_data["indirect_cost"] = dref.indirect_cost
1476+
validated_data["indirect_expenditure_cost"] = dref.indirect_cost
1477+
validated_data["total_cost"] = dref.total_cost
1478+
14041479
dref_final_report = super().create(validated_data)
14051480
# XXX: Copy files from DREF (Only nested serialized fields)
14061481
nested_serialized_file_fields = [
@@ -1420,6 +1495,8 @@ def create(self, validated_data):
14201495
dref_final_report.users.add(*dref.users.all())
14211496
dref_final_report.national_society_actions.add(*dref.national_society_actions.all())
14221497
dref_final_report.source_information.add(*dref.source_information.all())
1498+
if dref_final_report.type_of_dref == Dref.DrefType.IMMINENT and dref_final_report.is_dref_imminent_v2:
1499+
dref_final_report.proposed_action.add(*dref.proposed_action.all())
14231500
# also update is_final_report_created for dref
14241501
dref.is_final_report_created = True
14251502
dref.save(update_fields=["is_final_report_created"])

0 commit comments

Comments
 (0)