From 676df5a7aba11e8d6da3b01ea756098cea3ff705 Mon Sep 17 00:00:00 2001 From: Girum Bizuayehu Date: Fri, 29 Nov 2024 04:29:52 +0300 Subject: [PATCH 1/2] Make optional some fields in LivelihoodActivity sub-classes see HEA-596 --- ...er_foodpurchase_times_per_year_and_more.py | 103 ++++++++ apps/baseline/models.py | 142 +++++++---- apps/baseline/tests/test_models.py | 238 +++++++++++++++++- 3 files changed, 439 insertions(+), 44 deletions(-) create mode 100644 apps/baseline/migrations/0019_alter_foodpurchase_times_per_year_and_more.py diff --git a/apps/baseline/migrations/0019_alter_foodpurchase_times_per_year_and_more.py b/apps/baseline/migrations/0019_alter_foodpurchase_times_per_year_and_more.py new file mode 100644 index 00000000..336712a6 --- /dev/null +++ b/apps/baseline/migrations/0019_alter_foodpurchase_times_per_year_and_more.py @@ -0,0 +1,103 @@ +# Generated by Django 5.1.1 on 2024-11-29 01:26 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("baseline", "0018_alter_livelihoodactivity_price_and_more"), + ] + + operations = [ + migrations.AlterField( + model_name="foodpurchase", + name="times_per_year", + field=models.PositiveSmallIntegerField( + blank=True, + help_text="Number of times in a year that the purchase is made", + null=True, + verbose_name="Times per year", + ), + ), + migrations.AlterField( + model_name="foodpurchase", + name="unit_multiple", + field=models.PositiveSmallIntegerField( + blank=True, + help_text="Multiple of the unit of measure in a single purchase", + null=True, + verbose_name="Unit Multiple", + ), + ), + migrations.AlterField( + model_name="othercashincome", + name="times_per_year", + field=models.PositiveSmallIntegerField( + blank=True, + help_text="Number of times in a year that the income is received", + null=True, + verbose_name="Times per year", + ), + ), + migrations.AlterField( + model_name="otherpurchase", + name="times_per_year", + field=models.PositiveSmallIntegerField( + blank=True, + help_text="Number of times in a year that the product is purchased", + null=True, + verbose_name="Times per year", + ), + ), + migrations.AlterField( + model_name="paymentinkind", + name="months_per_year", + field=models.PositiveSmallIntegerField( + blank=True, + help_text="Number of months in a year that the labor is performed", + null=True, + verbose_name="Months per year", + ), + ), + migrations.AlterField( + model_name="paymentinkind", + name="people_per_household", + field=models.PositiveSmallIntegerField( + blank=True, + help_text="Number of household members who perform the labor", + null=True, + verbose_name="People per household", + ), + ), + migrations.AlterField( + model_name="paymentinkind", + name="times_per_year", + field=models.PositiveSmallIntegerField( + blank=True, + help_text="Number of times in a year that the labor is performed", + null=True, + verbose_name="Times per year", + ), + ), + migrations.AlterField( + model_name="reliefgiftother", + name="times_per_year", + field=models.PositiveSmallIntegerField( + blank=True, + help_text="Number of times in a year that the item is received", + null=True, + verbose_name="Times per year", + ), + ), + migrations.AlterField( + model_name="reliefgiftother", + name="unit_multiple", + field=models.PositiveSmallIntegerField( + blank=True, + help_text="Multiple of the unit of measure received each time", + null=True, + verbose_name="Unit Multiple", + ), + ), + ] diff --git a/apps/baseline/models.py b/apps/baseline/models.py index 92a8f677..1e7f7805 100644 --- a/apps/baseline/models.py +++ b/apps/baseline/models.py @@ -1553,7 +1553,10 @@ class FoodPurchase(LivelihoodActivity): # unit_multiple has names like wt_of_measure in the BSS. unit_multiple = models.PositiveSmallIntegerField( - verbose_name=_("Unit Multiple"), help_text=_("Multiple of the unit of measure in a single purchase") + blank=True, + null=True, + verbose_name=_("Unit Multiple"), + help_text=_("Multiple of the unit of measure in a single purchase"), ) # This is a float field because data may be captured as "once per week", # which equates to "52 per year", which is "4.33 per month". @@ -1565,17 +1568,25 @@ class FoodPurchase(LivelihoodActivity): help_text=_("Number of months in a year that the product is purchased"), ) times_per_year = models.PositiveSmallIntegerField( + blank=True, + null=True, verbose_name=_("Times per year"), help_text=_("Number of times in a year that the purchase is made"), ) def validate_quantity_produced(self): - if self.quantity_produced != self.unit_multiple * self.times_per_month * self.months_per_year: - raise ValidationError( - _( - "Quantity produced for a Food Purchase must be purchase amount * purchases per month * months per year" # NOQA: E501 + if ( + self.quantity_produced is not None + and self.unit_multiple is not None + and self.times_per_month is not None + and self.months_per_year is not None + ): + if self.quantity_produced != self.unit_multiple * self.times_per_month * self.months_per_year: + raise ValidationError( + _( + "Quantity produced for a Food Purchase must be purchase amount * purchases per month * months per year" # NOQA: E501 + ) ) - ) class Meta: verbose_name = LivelihoodStrategyType.FOOD_PURCHASE.label @@ -1598,15 +1609,23 @@ class PaymentInKind(LivelihoodActivity): help_text=_("Amount of item received each time the labor is performed"), ) people_per_household = models.PositiveSmallIntegerField( - verbose_name=_("People per household"), help_text=_("Number of household members who perform the labor") + blank=True, + null=True, + verbose_name=_("People per household"), + help_text=_("Number of household members who perform the labor"), ) # This is a float field because data may be captured as "once per week", # which equates to "52 per year", which is "4.33 per month". times_per_month = models.FloatField(blank=True, null=True, verbose_name=_("Labor per month")) months_per_year = models.PositiveSmallIntegerField( - verbose_name=_("Months per year"), help_text=_("Number of months in a year that the labor is performed") + blank=True, + null=True, + verbose_name=_("Months per year"), + help_text=_("Number of months in a year that the labor is performed"), ) times_per_year = models.PositiveSmallIntegerField( + blank=True, + null=True, verbose_name=_("Times per year"), help_text=_("Number of times in a year that the labor is performed"), ) @@ -1621,15 +1640,21 @@ def clean(self): def validate_quantity_produced(self): if ( - self.quantity_produced - and self.quantity_produced - != self.payment_per_time * self.people_per_household * self.times_per_month * self.months_per_year + self.quantity_produced is not None + and self.payment_per_time is not None + and self.people_per_household is not None + and self.times_per_month is not None + and self.months_per_year is not None ): - raise ValidationError( - _( - "Quantity produced for Payment In Kind must be payment per time * number of people * labor per month * months per year" # NOQA: E501 + if ( + self.quantity_produced + != self.payment_per_time * self.people_per_household * self.times_per_month * self.months_per_year + ): + raise ValidationError( + _( + "Quantity produced for Payment In Kind must be payment per time * number of people * labor per month * months per year" # NOQA: E501 + ) ) - ) class Meta: verbose_name = LivelihoodStrategyType.PAYMENT_IN_KIND.label @@ -1647,7 +1672,10 @@ class ReliefGiftOther(LivelihoodActivity): # Production calculation /validation is `unit_of_measure * unit_multiple * times_per_year` # Also used for the number of children receiving school meals. unit_multiple = models.PositiveSmallIntegerField( - verbose_name=_("Unit Multiple"), help_text=_("Multiple of the unit of measure received each time") + blank=True, + null=True, + verbose_name=_("Unit Multiple"), + help_text=_("Multiple of the unit of measure received each time"), ) # This is a float field because data may be captured as "once per week", # which equates to "52 per year", which is "4.33 per month". @@ -1661,14 +1689,18 @@ class ReliefGiftOther(LivelihoodActivity): help_text=_("Number of months in a year that the item is received"), ) times_per_year = models.PositiveSmallIntegerField( - verbose_name=_("Times per year"), help_text=_("Number of times in a year that the item is received") + blank=True, + null=True, + verbose_name=_("Times per year"), + help_text=_("Number of times in a year that the item is received"), ) def validate_quantity_produced(self): - if self.quantity_produced != self.unit_multiple * self.times_per_year: - raise ValidationError( - _("Quantity produced for Relief, Gifts, Other must be amount received * times per year") - ) + if self.quantity_produced is not None and self.unit_multiple is not None and self.times_per_year is not None: + if self.quantity_produced != self.unit_multiple * self.times_per_year: + raise ValidationError( + _("Quantity produced for Relief, Gifts, Other must be amount received * times per year") + ) class Meta: verbose_name = LivelihoodStrategyType.RELIEF_GIFT_OTHER.label @@ -1752,6 +1784,8 @@ class OtherCashIncome(LivelihoodActivity): help_text=_("Number of months in a year that the labor is performed"), ) times_per_year = models.PositiveSmallIntegerField( + blank=True, + null=True, verbose_name=_("Times per year"), help_text=_("Number of times in a year that the income is received"), ) @@ -1766,19 +1800,24 @@ def clean(self): def validate_income(self): if ( - self.people_per_household - and self.income - != self.payment_per_time * self.people_per_household * self.times_per_month * self.months_per_year + self.people_per_household is not None + and self.income is not None + and self.payment_per_time is not None + and self.times_per_month is not None + and self.months_per_year is not None ): - raise ValidationError( - _( - "Quantity produced for Other Cash Income must be payment per time * number of people * labor per month * months per year" # NOQA: E501 + if ( + self.income + != self.payment_per_time * self.people_per_household * self.times_per_month * self.months_per_year + ): + raise ValidationError( + _( + "Quantity produced for Other Cash Income must be payment per time * number of people * labor per month * months per year" # NOQA: E501 + ) ) - ) - if self.income != self.payment_per_time * self.times_per_year: - raise ValidationError( - _("Quantity produced for Other Cash Income must be payment per time * times per year") - ) + if self.income is not None and self.payment_per_time is not None and self.times_per_year is not None: + if self.income != self.payment_per_time * self.times_per_year: + raise ValidationError(_("Income for 'Other Cash Income' must be payment per time * times per year")) def calculate_fields(self): self.times_per_year = self.people_per_household * self.times_per_month * self.months_per_year @@ -1817,21 +1856,40 @@ class OtherPurchase(LivelihoodActivity): help_text=_("Number of months in a year that the product is purchased"), ) times_per_year = models.PositiveSmallIntegerField( + blank=True, + null=True, verbose_name=_("Times per year"), help_text=_("Number of times in a year that the product is purchased"), ) def validate_expenditure(self): - if ( - self.times_per_month - and self.months_per_year - and self.times_per_month * self.months_per_year != self.times_per_year - ): - raise ValidationError(_("Times per year must be times per month * months per year")) - if self.expenditure != self.price * self.unit_multiple * self.times_per_year: - raise ValidationError( - _("Expenditure for Other Purchases must be price * unit multiple * purchases per year") - ) + errors = [] + if self.times_per_month is not None and self.months_per_year is not None: + expected_times_per_year = self.times_per_month * self.months_per_year + if self.times_per_year is not None and self.times_per_year != expected_times_per_year: + errors.append( + _( + "Times per year must be times per month * months per year. Expected: %(expected)s, Found: %(found)s" + ) + % { + "expected": expected_times_per_year, + "found": self.times_per_year, + } + ) + if self.price is not None and self.unit_multiple is not None and self.times_per_year is not None: + expected_expenditure = self.price * self.unit_multiple * self.times_per_year + if self.expenditure is not None and self.expenditure != expected_expenditure: + errors.append( + _( + "Expenditure for Other Purchases must be price * unit multiple * purchases per year. Expected: %(expected)s, Found: %(found)s" + ) + % { + "expected": expected_expenditure, + "found": self.expenditure, + } + ) + if errors: + raise ValidationError(errors) class Meta: verbose_name = LivelihoodStrategyType.OTHER_PURCHASE.label diff --git a/apps/baseline/tests/test_models.py b/apps/baseline/tests/test_models.py index 11a38c8b..5bf03eb7 100644 --- a/apps/baseline/tests/test_models.py +++ b/apps/baseline/tests/test_models.py @@ -1,11 +1,21 @@ from django.core.exceptions import ValidationError from django.test import TestCase -from baseline.models import WealthGroupCharacteristicValue +from baseline.models import ( + FoodPurchase, + OtherCashIncome, + OtherPurchase, + PaymentInKind, + ReliefGiftOther, + WealthGroupCharacteristicValue, +) from common.tests.factories import ClassifiedProductFactory from common.utils import conditional_logging -from .factories import CommunityFactory, WealthGroupCharacteristicValueFactory +from .factories import ( + CommunityFactory, + WealthGroupCharacteristicValueFactory, +) class WealthGroupCharacteristicValueTestCase(TestCase): @@ -118,3 +128,227 @@ def test_get_by_natural_key_for_a_baseline_wealth_group(self): full_name="", ) self.assertEqual(instance, wealth_group_characteristic_value) + + +class FoodPurchaseTestCase(TestCase): + @classmethod + def setUpTestData(cls): + # Create different instances without saving + cls.foodpurchase1 = FoodPurchase( + unit_multiple=None, + times_per_month=None, + months_per_year=None, + quantity_produced=None, + ) + cls.foodpurchase2 = FoodPurchase( + unit_multiple=2, + times_per_month=5, + months_per_year=12, + quantity_produced=120, + ) + # Incorrect: 2 * 5 * 12 = 120 + cls.foodpurchase3 = FoodPurchase( + unit_multiple=2, + times_per_month=5, + months_per_year=12, + quantity_produced=100, + ) + + def test_validate_quantity_produced(self): + """ + Test validate_quantity_produced method + """ + # Missing data should not raise ValidationError + self.foodpurchase1.validate_quantity_produced() + # Expected consistant values, should not raise + self.foodpurchase2.validate_quantity_produced() + # Incorrect: 2 * 5 * 12 = 120 + with conditional_logging(): + self.assertRaises(ValidationError, self.foodpurchase3.validate_quantity_produced) + + +class PaymentInKindTestCase(TestCase): + @classmethod + def setUpTestData(cls): + # Create different instances without saving + cls.paymentinkind1 = PaymentInKind( + payment_per_time=None, + people_per_household=None, + times_per_month=None, + months_per_year=None, + quantity_produced=None, + ) + cls.paymentinkind2 = PaymentInKind( + payment_per_time=10, + people_per_household=2, + times_per_month=5, + months_per_year=12, + quantity_produced=1200, # 10 * 2 * 5 * 12 = 1200 + ) + cls.paymentinkind3 = PaymentInKind( + payment_per_time=10, + people_per_household=2, + times_per_month=5, + months_per_year=12, + quantity_produced=1000, # Incorrect: should be 1200 + ) + + def test_validate_quantity_produced(self): + """ + Test validate_quantity_produced method + """ + # Missing data should not raise ValidationError + self.paymentinkind1.validate_quantity_produced() + + # Expected consistent values, should not raise ValidationError + self.paymentinkind2.validate_quantity_produced() + + # Incorrect: 10 * 2 * 5 * 12 = 1200 + with self.assertRaises(ValidationError): + self.paymentinkind3.validate_quantity_produced() + + def test_payment_per_time_required(self): + """ + Test that clean enforces payment_per_time when required. + """ + instance = PaymentInKind( + payment_per_time=None, + people_per_household=2, + times_per_month=5, + months_per_year=12, + ) + with self.assertRaises(ValidationError): + instance.clean() + + +class ReliefGiftOtherTestCase(TestCase): + @classmethod + def setUpTestData(cls): + # Create different instances without saving + cls.reliefgift1 = ReliefGiftOther( + unit_multiple=None, + times_per_year=None, + quantity_produced=None, + ) + cls.reliefgift2 = ReliefGiftOther( + unit_multiple=10, + times_per_year=12, + quantity_produced=120, # 10 * 12 = 120 + ) + cls.reliefgift3 = ReliefGiftOther( + unit_multiple=10, + times_per_year=12, + quantity_produced=100, # Incorrect: should be 10 * 12 = 120 + ) + + def test_validate_quantity_produced(self): + """ + Test validate_quantity_produced method + """ + # Missing data should not raise ValidationError + self.reliefgift1.validate_quantity_produced() + + # Expected consistent values, should not raise ValidationError + self.reliefgift2.validate_quantity_produced() + + # Incorrect: 10 * 12 = 120 + with self.assertRaises(ValidationError): + self.reliefgift3.validate_quantity_produced() + + +class OtherCashIncomeTestCase(TestCase): + @classmethod + def setUpTestData(cls): + # Create instances for testing + cls.othercashincome1 = OtherCashIncome( + payment_per_time=None, + people_per_household=None, + times_per_month=None, + months_per_year=None, + income=None, + ) + cls.othercashincome2 = OtherCashIncome( + payment_per_time=100, + people_per_household=2, + times_per_month=5, + months_per_year=12, + income=12000, # 100 * 2 * 5 * 12 = 12000 + ) + cls.othercashincome3 = OtherCashIncome( + payment_per_time=100, + people_per_household=2, + times_per_month=5, + months_per_year=12, + income=10000, # Incorrect: should be 12000 + ) + cls.othercashincome4 = OtherCashIncome( + payment_per_time=100, + times_per_year=24, + income=2400, # 100 * 24 = 2400 + ) + cls.othercashincome5 = OtherCashIncome( + payment_per_time=100, + times_per_year=24, + income=2000, # Incorrect: should be 2400 + ) + + def test_validate_income(self): + # Missing data should not raise ValidationError + self.othercashincome1.validate_income() + # Correct data should not raise ValidationError + self.othercashincome2.validate_income() + self.othercashincome4.validate_income() + # Incorrect data should raise ValidationError. + with self.assertRaises(ValidationError): + self.othercashincome3.validate_income() + with self.assertRaises(ValidationError): + self.othercashincome5.validate_income() + + +class OtherPurchaseTestCase(TestCase): + @classmethod + def setUpTestData(cls): + # Create instances for testing + cls.otherpurchase1 = OtherPurchase( + unit_multiple=None, + times_per_month=None, + months_per_year=None, + times_per_year=None, + price=None, + expenditure=None, + ) + cls.otherpurchase2 = OtherPurchase( + unit_multiple=2, + times_per_month=5, + months_per_year=12, + times_per_year=60, # 5 * 12 = 60 + price=10, + expenditure=1200, # 10 * 2 * 60 = 1200 + ) + cls.otherpurchase3 = OtherPurchase( + unit_multiple=2, + times_per_month=5, + months_per_year=12, + times_per_year=50, # Incorrect: should be 5 * 12 = 60 + price=10, + expenditure=1200, + ) + cls.otherpurchase4 = OtherPurchase( + unit_multiple=2, + times_per_month=5, + months_per_year=12, + times_per_year=60, + price=10, + expenditure=1000, # Incorrect: should be 10 * 2 * 60 = 1200 + ) + + def test_validate_expenditure(self): + # Missing data should not raise ValidationError + self.otherpurchase1.validate_expenditure() + # Correct data should not raise ValidationError + self.otherpurchase2.validate_expenditure() + # Incorrect data should raise ValidationError + with self.assertRaises(ValidationError): + self.otherpurchase3.validate_expenditure() + with self.assertRaises(ValidationError): + self.otherpurchase4.validate_expenditure() From 8c5449cd45605128357ecd24de88c52a2d1374a0 Mon Sep 17 00:00:00 2001 From: Girum Bizuayehu Date: Fri, 29 Nov 2024 15:32:26 +0300 Subject: [PATCH 2/2] Make optional some fields in LivelihoodActivity sub-classes see HEA-596 --- apps/baseline/tests/test_models.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/apps/baseline/tests/test_models.py b/apps/baseline/tests/test_models.py index 5bf03eb7..3c6e20e2 100644 --- a/apps/baseline/tests/test_models.py +++ b/apps/baseline/tests/test_models.py @@ -12,10 +12,7 @@ from common.tests.factories import ClassifiedProductFactory from common.utils import conditional_logging -from .factories import ( - CommunityFactory, - WealthGroupCharacteristicValueFactory, -) +from .factories import CommunityFactory, WealthGroupCharacteristicValueFactory class WealthGroupCharacteristicValueTestCase(TestCase):