Skip to content

Commit bc9de13

Browse files
Merge pull request #2518 from IFRCGo/feature/dref-final-report
Dref Final Report new Workflow
2 parents a388c71 + 4934463 commit bc9de13

File tree

5 files changed

+376
-21
lines changed

5 files changed

+376
-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+
]
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Generated by Django 4.2.19 on 2025-07-22 10:05
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
("dref", "0082_dreffinalreport_indirect_cost_and_more"),
10+
]
11+
12+
operations = [
13+
migrations.AddField(
14+
model_name="dreffinalreport",
15+
name="total_operation_timeframe_imminent",
16+
field=models.IntegerField(blank=True, null=True, verbose_name="total operation timeframe for imminent type"),
17+
),
18+
]

dref/models.py

Lines changed: 50 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}"
@@ -1309,6 +1315,10 @@ class DrefFinalReport(models.Model):
13091315
total_dref_allocation = models.IntegerField(verbose_name=_("Total dref allocation"), null=True, blank=True)
13101316
date_of_publication = models.DateField(verbose_name=_("Date of publication"), blank=True, null=True)
13111317
total_operation_timeframe = models.IntegerField(verbose_name=_("Total Operation Timeframe"), null=True, blank=True)
1318+
# NOTE: Total operation Timeframe for Imminent Type: Days
1319+
total_operation_timeframe_imminent = models.IntegerField(
1320+
verbose_name=_("total operation timeframe for imminent type"), null=True, blank=True
1321+
)
13121322
operation_start_date = models.DateField(verbose_name=_("Operation Start Date"), null=True, blank=True)
13131323
appeal_code = models.CharField(verbose_name=_("appeal code"), max_length=255, null=True, blank=True)
13141324
glide_code = models.CharField(verbose_name=_("glide number"), max_length=255, null=True, blank=True)
@@ -1582,6 +1592,46 @@ class DrefFinalReport(models.Model):
15821592
main_donors = models.TextField(verbose_name=_("Main Donors"), null=True, blank=True)
15831593
operation_end_date = models.DateField(verbose_name=_("Operation End Date"), null=True, blank=True)
15841594
source_information = models.ManyToManyField(SourceInformation, blank=True, verbose_name=_("Source Information"))
1595+
# NOTE: Flag to indicate if this is an new dref imminent type
1596+
is_dref_imminent_v2 = models.BooleanField(
1597+
verbose_name=_("Is DREF Imminent V2?"),
1598+
default=False,
1599+
)
1600+
mitigation_efforts_and_achievements = models.TextField(
1601+
verbose_name=_("Mitigation Efforts and Achievements"),
1602+
null=True,
1603+
blank=True,
1604+
)
1605+
lessons_learned_and_challenges = models.TextField(
1606+
verbose_name=_("Lessons learnt and challenges"),
1607+
blank=True,
1608+
null=True,
1609+
)
1610+
proposed_action = models.ManyToManyField(ProposedAction, verbose_name=_("Proposed Action"), blank=True)
1611+
sub_total_cost = models.PositiveIntegerField(verbose_name=_("Sub total Cost"), blank=True, null=True)
1612+
sub_total_expenditure_cost = models.PositiveIntegerField(
1613+
verbose_name=_("Sub total Expenditure Cost"),
1614+
blank=True,
1615+
null=True,
1616+
)
1617+
surge_deployment_cost = models.PositiveIntegerField(verbose_name=_("Surge Deployment Cost"), null=True, blank=True)
1618+
surge_deployment_expenditure_cost = models.PositiveIntegerField(
1619+
verbose_name=_("Surge Deployment Expenditure Cost"),
1620+
null=True,
1621+
blank=True,
1622+
)
1623+
indirect_cost = models.PositiveIntegerField(verbose_name=_("Indirect Cost"), null=True, blank=True)
1624+
indirect_expenditure_cost = models.PositiveIntegerField(
1625+
verbose_name=_("Indirect Expenditure Cost"),
1626+
null=True,
1627+
blank=True,
1628+
)
1629+
total_cost = models.PositiveIntegerField(verbose_name=_("Total Cost"), null=True, blank=True)
1630+
total_expenditure_cost = models.PositiveIntegerField(
1631+
verbose_name=_("Total Expenditure Cost"),
1632+
null=True,
1633+
blank=True,
1634+
)
15851635
__financial_report_id = None
15861636

15871637
class Meta:

dref/serializers.py

Lines changed: 98 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):
@@ -1166,6 +1232,16 @@ def create(self, validated_data):
11661232
DrefOperationalUpdate.objects.filter(dref=dref, is_published=True).order_by("-operational_update_number").first()
11671233
)
11681234
validated_data["created_by"] = self.context["request"].user
1235+
# NOTE: Checks and common fields for the new dref final reports of new dref imminents
1236+
if dref.type_of_dref == Dref.DrefType.IMMINENT and dref.is_dref_imminent_v2:
1237+
validated_data["is_dref_imminent_v2"] = True
1238+
validated_data["sub_total_cost"] = dref.sub_total_cost
1239+
validated_data["surge_deployment_cost"] = dref.surge_deployment_cost
1240+
validated_data["surge_deployment_expenditure_cost"] = dref.surge_deployment_cost
1241+
validated_data["indirect_cost"] = dref.indirect_cost
1242+
validated_data["indirect_expenditure_cost"] = dref.indirect_cost
1243+
validated_data["total_cost"] = dref.total_cost
1244+
11691245
if dref_operational_update:
11701246
validated_data["title"] = dref_operational_update.title
11711247
validated_data["title_prefix"] = dref_operational_update.title_prefix
@@ -1274,11 +1350,6 @@ def create(self, validated_data):
12741350
if validated_data["type_of_dref"] == Dref.DrefType.LOAN:
12751351
raise serializers.ValidationError(gettext("Can't create final report for dref type %s" % Dref.DrefType.LOAN))
12761352

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-
)
12821353
dref_final_report = super().create(validated_data)
12831354
# XXX: Copy files from DREF (Only nested serialized fields)
12841355
nested_serialized_file_fields = [
@@ -1299,6 +1370,8 @@ def create(self, validated_data):
12991370
dref_final_report.risk_security.add(*dref_operational_update.risk_security.all())
13001371
dref_final_report.users.add(*dref_operational_update.users.all())
13011372
dref_final_report.source_information.add(*dref_operational_update.source_information.all())
1373+
if dref_final_report.is_dref_imminent_v2:
1374+
dref_final_report.proposed_action.add(*dref.proposed_action.all())
13021375
else:
13031376
validated_data["title"] = dref.title
13041377
validated_data["title_prefix"] = dref.title_prefix
@@ -1322,7 +1395,7 @@ def create(self, validated_data):
13221395
validated_data["total_dref_allocation"] = dref.total_cost
13231396
# NOTE:These field should be blank for final report
13241397
validated_data["total_operation_timeframe"] = None
1325-
validated_data["operation_end_date"] = None
1398+
validated_data["total_operation_timeframe_imminent"] = dref.operation_timeframe_imminent
13261399
validated_data["glide_code"] = dref.glide_code
13271400
validated_data["ifrc_appeal_manager_name"] = dref.ifrc_appeal_manager_name
13281401
validated_data["ifrc_appeal_manager_email"] = dref.ifrc_appeal_manager_email
@@ -1336,6 +1409,13 @@ def create(self, validated_data):
13361409
validated_data["national_society_contact_email"] = dref.national_society_contact_email
13371410
validated_data["national_society_contact_title"] = dref.national_society_contact_title
13381411
validated_data["national_society_contact_phone_number"] = dref.national_society_contact_phone_number
1412+
validated_data["national_society_integrity_contact_name"] = dref.national_society_integrity_contact_name
1413+
validated_data["national_society_integrity_contact_email"] = dref.national_society_integrity_contact_email
1414+
validated_data["national_society_integrity_contact_title"] = dref.national_society_integrity_contact_title
1415+
validated_data["national_society_integrity_contact_phone_number"] = (
1416+
dref.national_society_integrity_contact_phone_number
1417+
)
1418+
validated_data["national_society_hotline_phone_number"] = dref.national_society_hotline_phone_number
13391419
validated_data["media_contact_name"] = dref.media_contact_name
13401420
validated_data["media_contact_email"] = dref.media_contact_email
13411421
validated_data["media_contact_title"] = dref.media_contact_title
@@ -1396,11 +1476,16 @@ def create(self, validated_data):
13961476
gettext("Can't create final report for dref type %s" % Dref.DrefType.LOAN.label)
13971477
)
13981478

1399-
# TODO: Remove me! After final report is implemented for drefs IMMINENT
1479+
# NOTE: Checks for the new dref final reports of new dref imminents
14001480
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-
)
1481+
validated_data["is_dref_imminent_v2"] = True
1482+
validated_data["sub_total_cost"] = dref.sub_total_cost
1483+
validated_data["surge_deployment_cost"] = dref.surge_deployment_cost
1484+
validated_data["surge_deployment_expenditure_cost"] = dref.surge_deployment_cost
1485+
validated_data["indirect_cost"] = dref.indirect_cost
1486+
validated_data["indirect_expenditure_cost"] = dref.indirect_cost
1487+
validated_data["total_cost"] = dref.total_cost
1488+
14041489
dref_final_report = super().create(validated_data)
14051490
# XXX: Copy files from DREF (Only nested serialized fields)
14061491
nested_serialized_file_fields = [
@@ -1420,6 +1505,8 @@ def create(self, validated_data):
14201505
dref_final_report.users.add(*dref.users.all())
14211506
dref_final_report.national_society_actions.add(*dref.national_society_actions.all())
14221507
dref_final_report.source_information.add(*dref.source_information.all())
1508+
if dref_final_report.type_of_dref == Dref.DrefType.IMMINENT and dref_final_report.is_dref_imminent_v2:
1509+
dref_final_report.proposed_action.add(*dref.proposed_action.all())
14231510
# also update is_final_report_created for dref
14241511
dref.is_final_report_created = True
14251512
dref.save(update_fields=["is_final_report_created"])

0 commit comments

Comments
 (0)