Skip to content

Commit 5e0cb93

Browse files
authored
Merge pull request #259 from American-Institutes-for-Research/HEA-984/Livelihood_activity_valid_instances_errors_for_income_not_rounding_error
Hea 984/livelihood activity valid instances errors for income not rounding error
2 parents 9a9298a + 164c418 commit 5e0cb93

2 files changed

Lines changed: 107 additions & 33 deletions

File tree

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
# Generated by Django 5.2.12 on 2026-04-04 12:57
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
("baseline", "0029_alter_meatproduction_animals_slaughtered_and_more"),
10+
]
11+
12+
operations = [
13+
migrations.AlterField(
14+
model_name="foodpurchase",
15+
name="unit_multiple",
16+
field=models.FloatField(
17+
blank=True,
18+
help_text="Multiple of the unit of measure in a single purchase",
19+
null=True,
20+
verbose_name="Unit Multiple",
21+
),
22+
),
23+
migrations.AlterField(
24+
model_name="livelihoodactivity",
25+
name="quantity_consumed",
26+
field=models.FloatField(blank=True, null=True, verbose_name="Quantity Consumed"),
27+
),
28+
migrations.AlterField(
29+
model_name="livelihoodactivity",
30+
name="quantity_other_uses",
31+
field=models.FloatField(blank=True, null=True, verbose_name="Quantity Other Uses"),
32+
),
33+
migrations.AlterField(
34+
model_name="livelihoodactivity",
35+
name="quantity_produced",
36+
field=models.FloatField(blank=True, null=True, verbose_name="Quantity Produced"),
37+
),
38+
migrations.AlterField(
39+
model_name="livelihoodactivity",
40+
name="quantity_purchased",
41+
field=models.FloatField(blank=True, null=True, verbose_name="Quantity Purchased"),
42+
),
43+
migrations.AlterField(
44+
model_name="livelihoodactivity",
45+
name="quantity_sold",
46+
field=models.FloatField(blank=True, null=True, verbose_name="Quantity Sold/Exchanged"),
47+
),
48+
migrations.AlterField(
49+
model_name="milkproduction",
50+
name="quantity_butter_production",
51+
field=models.FloatField(blank=True, null=True, verbose_name="Quantity used for Butter Production"),
52+
),
53+
migrations.AlterField(
54+
model_name="otherpurchase",
55+
name="unit_multiple",
56+
field=models.FloatField(
57+
blank=True,
58+
help_text="Multiple of the unit of measure in a single purchase",
59+
null=True,
60+
verbose_name="Unit Multiple",
61+
),
62+
),
63+
migrations.AlterField(
64+
model_name="reliefgiftother",
65+
name="unit_multiple",
66+
field=models.FloatField(
67+
blank=True,
68+
help_text="Multiple of the unit of measure received each time",
69+
null=True,
70+
verbose_name="Unit Multiple",
71+
),
72+
),
73+
]

apps/baseline/models.py

Lines changed: 34 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
"""
44

55
import datetime
6+
import math
67
import numbers
78

89
from django.conf import settings
@@ -1172,13 +1173,13 @@ class LivelihoodActivity(common_models.Model):
11721173
wealth_group = models.ForeignKey(WealthGroup, on_delete=models.CASCADE, help_text=_("Wealth Group"))
11731174

11741175
# Also used for the quantity received for the PaymentInKind and ReliefGiftsOther Livelihood Strategies
1175-
quantity_produced = models.PositiveIntegerField(blank=True, null=True, verbose_name=_("Quantity Produced"))
1176-
quantity_purchased = models.PositiveIntegerField(blank=True, null=True, verbose_name=_("Quantity Purchased"))
1177-
quantity_sold = models.PositiveIntegerField(blank=True, null=True, verbose_name=_("Quantity Sold/Exchanged"))
1178-
quantity_other_uses = models.PositiveIntegerField(blank=True, null=True, verbose_name=_("Quantity Other Uses"))
1176+
quantity_produced = models.FloatField(blank=True, null=True, verbose_name=_("Quantity Produced"))
1177+
quantity_purchased = models.FloatField(blank=True, null=True, verbose_name=_("Quantity Purchased"))
1178+
quantity_sold = models.FloatField(blank=True, null=True, verbose_name=_("Quantity Sold/Exchanged"))
1179+
quantity_other_uses = models.FloatField(blank=True, null=True, verbose_name=_("Quantity Other Uses"))
11791180
# Can normally be calculated / validated as `quantity_produced + quantity_purchased - quantity_sold - quantity_other_uses` # NOQA: E501
11801181
# but there are exceptions, such as MilkProduction which also stores MilkProduction.quantity_butter_production
1181-
quantity_consumed = models.PositiveIntegerField(blank=True, null=True, verbose_name=_("Quantity Consumed"))
1182+
quantity_consumed = models.FloatField(blank=True, null=True, verbose_name=_("Quantity Consumed"))
11821183

11831184
price = models.FloatField(
11841185
blank=True,
@@ -1356,19 +1357,17 @@ def validate_quantity_consumed(self):
13561357
)
13571358

13581359
# Check if the actual quantity_consumed matches the expected quantity_consumed
1359-
if self.quantity_consumed and self.quantity_consumed != expected_quantity_consumed:
1360+
if self.quantity_consumed and not math.isclose(self.quantity_consumed, expected_quantity_consumed):
13601361
if quantity_butter_production:
13611362
message = "Quantity consumed for Milk Production must be quantity produced + quantity purchased - quantity sold - quantity used for butter production - quantity used for other things" # NOQA: E501
13621363
else:
13631364
message = "Quantity consumed for a Livelihood Activity must be quantity produced + quantity purchased - quantity sold - quantity used for other things" # NOQA: E501
13641365
raise ValidationError(_(message))
13651366

13661367
def validate_income(self):
1367-
income = self.income or 0
1368-
quantity_sold = self.quantity_sold or 0
1369-
price = self.price or 0
1370-
if self.income and income != quantity_sold * price:
1371-
raise ValidationError(_("Income for a Livelihood Activity must be quantity sold multiplied by price"))
1368+
if self.income and self.quantity_sold is not None and self.price is not None:
1369+
if not math.isclose(self.income, self.quantity_sold * self.price):
1370+
raise ValidationError(_("Income for a Livelihood Activity must be quantity sold multiplied by price"))
13721371

13731372
def validate_expenditure(self):
13741373
"""
@@ -1386,7 +1385,7 @@ def validate_expenditure(self):
13861385
price = self.price or 0
13871386
expenditure = self.expenditure or 0
13881387

1389-
if self.expenditure and expenditure != quantity_produced * price:
1388+
if self.expenditure and not math.isclose(expenditure, quantity_produced * price):
13901389
raise ValidationError(
13911390
_("Expenditure for a Livelihood Activity must be quantity produced multiplied by price")
13921391
)
@@ -1409,9 +1408,9 @@ def validate_kcals_consumed(self):
14091408
"product": self.livelihood_strategy.product,
14101409
}
14111410
)
1412-
if (
1413-
self.kcals_consumed
1414-
!= self.quantity_consumed * conversion_factor * self.livelihood_strategy.product.kcals_per_unit
1411+
if not math.isclose(
1412+
self.kcals_consumed,
1413+
self.quantity_consumed * conversion_factor * self.livelihood_strategy.product.kcals_per_unit,
14151414
):
14161415
raise ValidationError(
14171416
_(
@@ -1637,7 +1636,7 @@ class MilkType(models.TextChoices):
16371636
# https://unstats.un.org/unsd/classifications/Econ/Detail/EN/1074/22110. That LivelihoodActivity would have a
16381637
# `quantity_produced` equal to the amount of whole milk that was used for ButterProduction, and then the
16391638
# quantity_sold and the quantity_consumed could be tracked separately.
1640-
quantity_butter_production = models.PositiveIntegerField(
1639+
quantity_butter_production = models.FloatField(
16411640
blank=True, null=True, verbose_name=_("Quantity used for Butter Production")
16421641
) # NOQA: E501
16431642
type_of_milk_consumed = models.CharField(
@@ -1736,7 +1735,7 @@ def clean(self):
17361735

17371736
def validate_quantity_produced(self):
17381737
if self.quantity_produced is not None and self.animals_slaughtered and self.carcass_weight is not None:
1739-
if self.quantity_produced != self.animals_slaughtered * self.carcass_weight:
1738+
if not math.isclose(self.quantity_produced, self.animals_slaughtered * self.carcass_weight):
17401739
raise ValidationError(
17411740
_(
17421741
"Quantity Produced for a Meat Production must be animals slaughtered multiplied by carcass weight"
@@ -1789,7 +1788,7 @@ class FoodPurchase(LivelihoodActivity):
17891788
# NIO93 Row B422 tia = 2.5kg
17901789

17911790
# unit_multiple has names like wt_of_measure in the BSS.
1792-
unit_multiple = models.PositiveSmallIntegerField(
1791+
unit_multiple = models.FloatField(
17931792
blank=True,
17941793
null=True,
17951794
verbose_name=_("Unit Multiple"),
@@ -1818,7 +1817,9 @@ def validate_quantity_purchased(self):
18181817
and self.times_per_month is not None
18191818
and self.months_per_year is not None
18201819
):
1821-
if self.quantity_purchased != self.unit_multiple * self.times_per_month * self.months_per_year:
1820+
if not math.isclose(
1821+
self.quantity_purchased, self.unit_multiple * self.times_per_month * self.months_per_year
1822+
):
18221823
raise ValidationError(
18231824
_(
18241825
"Quantity purchased for a Food Purchase must be purchase amount * purchases per month * months per year" # NOQA: E501
@@ -1830,7 +1831,7 @@ def validate_expenditure(self):
18301831
price = self.price or 0
18311832
expenditure = self.expenditure or 0
18321833

1833-
if self.expenditure and expenditure != quantity_purchased * price:
1834+
if self.expenditure and not math.isclose(expenditure, quantity_purchased * price):
18341835
raise ValidationError(_("Expenditure for a Food Purchase must be quantity purchased multiplied by price"))
18351836

18361837
class Meta:
@@ -1912,9 +1913,9 @@ def validate_quantity_produced(self):
19121913
and self.times_per_month is not None
19131914
and self.months_per_year is not None
19141915
):
1915-
if (
1916-
self.quantity_produced
1917-
!= self.payment_per_time * self.people_per_household * self.times_per_month * self.months_per_year
1916+
if not math.isclose(
1917+
self.quantity_produced,
1918+
self.payment_per_time * self.people_per_household * self.times_per_month * self.months_per_year,
19181919
):
19191920
raise ValidationError(
19201921
_(
@@ -1937,7 +1938,7 @@ class ReliefGiftOther(LivelihoodActivity):
19371938

19381939
# Production calculation /validation is `unit_of_measure * unit_multiple * times_per_year`
19391940
# Also used for the number of children receiving school meals.
1940-
unit_multiple = models.PositiveSmallIntegerField(
1941+
unit_multiple = models.FloatField(
19411942
blank=True,
19421943
null=True,
19431944
verbose_name=_("Unit Multiple"),
@@ -1963,7 +1964,7 @@ class ReliefGiftOther(LivelihoodActivity):
19631964

19641965
def validate_quantity_produced(self):
19651966
if self.quantity_produced is not None and self.unit_multiple is not None and self.times_per_year is not None:
1966-
if self.quantity_produced != self.unit_multiple * self.times_per_year:
1967+
if not math.isclose(self.quantity_produced, self.unit_multiple * self.times_per_year):
19671968
raise ValidationError(
19681969
_("Quantity produced for Relief, Gifts, Other must be amount received * times per year")
19691970
)
@@ -2072,17 +2073,17 @@ def validate_income(self):
20722073
and self.times_per_month is not None
20732074
and self.months_per_year is not None
20742075
):
2075-
if (
2076-
self.income
2077-
!= self.payment_per_time * self.people_per_household * self.times_per_month * self.months_per_year
2076+
if not math.isclose(
2077+
self.income,
2078+
self.payment_per_time * self.people_per_household * self.times_per_month * self.months_per_year,
20782079
):
20792080
raise ValidationError(
20802081
_(
20812082
"Quantity produced for Other Cash Income must be payment per time * number of people * labor per month * months per year" # NOQA: E501
20822083
)
20832084
)
2084-
if self.income is not None and self.payment_per_time is not None and self.times_per_year is not None:
2085-
if self.income != self.payment_per_time * self.times_per_year:
2085+
elif self.income is not None and self.payment_per_time is not None and self.times_per_year is not None:
2086+
if not math.isclose(self.income, self.payment_per_time * self.times_per_year):
20862087
raise ValidationError(_("Income for 'Other Cash Income' must be payment per time * times per year"))
20872088

20882089
def calculate_fields(self):
@@ -2106,7 +2107,7 @@ class OtherPurchase(LivelihoodActivity):
21062107
# individual fields must be nullable
21072108
# Do we need this, or can we use combined units of measure like FDW, e.g. 5kg
21082109
# NIO93 Row B422 tia = 2.5kg
2109-
unit_multiple = models.PositiveSmallIntegerField(
2110+
unit_multiple = models.FloatField(
21102111
blank=True,
21112112
null=True,
21122113
verbose_name=_("Unit Multiple"),
@@ -2132,7 +2133,7 @@ def validate_expenditure(self):
21322133
errors = []
21332134
if self.times_per_month is not None and self.months_per_year is not None:
21342135
expected_times_per_year = self.times_per_month * self.months_per_year
2135-
if self.times_per_year is not None and self.times_per_year != expected_times_per_year:
2136+
if self.times_per_year is not None and not math.isclose(self.times_per_year, expected_times_per_year):
21362137
errors.append(
21372138
_(
21382139
"Times per year must be times per month * months per year. Expected: %(expected)s, Found: %(found)s"
@@ -2144,7 +2145,7 @@ def validate_expenditure(self):
21442145
)
21452146
if self.price is not None and self.unit_multiple is not None and self.times_per_year is not None:
21462147
expected_expenditure = self.price * self.unit_multiple * self.times_per_year
2147-
if self.expenditure is not None and self.expenditure != expected_expenditure:
2148+
if self.expenditure is not None and not math.isclose(self.expenditure, expected_expenditure):
21482149
errors.append(
21492150
_(
21502151
"Expenditure for Other Purchases must be price * unit multiple * purchases per year. Expected: %(expected)s, Found: %(found)s"

0 commit comments

Comments
 (0)