Skip to content

Commit bf14ae3

Browse files
committed
Merge branch 'main' into HEA-984/Livelihood_activity_valid_instances_errors_for_income_not_rounding_error
2 parents c5ea5fb + f5c5b92 commit bf14ae3

8 files changed

Lines changed: 595 additions & 30 deletions

File tree

apps/baseline/admin.py

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,11 @@
1313
FoodPurchaseForm,
1414
LivelihoodActivityForm,
1515
LivelihoodStrategyForm,
16+
MeatProductionForm,
1617
MilkProductionForm,
18+
OtherCashIncomeForm,
1719
OtherPurchaseForm,
20+
PaymentInKindForm,
1821
ReliefGiftOtherForm,
1922
WealthGroupCharacteristicValueForm,
2023
WealthGroupForm,
@@ -333,6 +336,134 @@ def get_extra(self, request, obj=None, **kwargs):
333336

334337
class LivelihoodActivityAdmin(admin.ModelAdmin):
335338
form = LivelihoodActivityForm
339+
340+
# Maps strategy_type to the subclass accessor name, its form, extra fieldsets,
341+
# and any extra fields to append to the base "Quantity" fieldset.
342+
_SUBCLASS_CONFIG = {
343+
LivelihoodStrategyType.MILK_PRODUCTION: {
344+
"accessor": "milkproduction",
345+
"form": MilkProductionForm,
346+
"quantity_extra": ["quantity_butter_production"],
347+
"extra_fieldsets": [
348+
(
349+
"Milk source",
350+
{
351+
"fields": [
352+
"milking_animals",
353+
"lactation_days",
354+
"daily_production",
355+
"type_of_milk_consumed",
356+
"type_of_milk_sold_or_other_uses",
357+
]
358+
},
359+
),
360+
],
361+
},
362+
LivelihoodStrategyType.MEAT_PRODUCTION: {
363+
"accessor": "meatproduction",
364+
"form": MeatProductionForm,
365+
"extra_fieldsets": [
366+
(
367+
"Meat source",
368+
{
369+
"fields": [
370+
"animals_slaughtered",
371+
"carcass_weight",
372+
]
373+
},
374+
),
375+
],
376+
},
377+
LivelihoodStrategyType.FOOD_PURCHASE: {
378+
"accessor": "foodpurchase",
379+
"form": FoodPurchaseForm,
380+
"extra_fieldsets": [
381+
(
382+
"Purchases",
383+
{
384+
"fields": [
385+
"unit_multiple",
386+
"times_per_month",
387+
"months_per_year",
388+
"times_per_year",
389+
]
390+
},
391+
),
392+
],
393+
},
394+
LivelihoodStrategyType.PAYMENT_IN_KIND: {
395+
"accessor": "paymentinkind",
396+
"form": PaymentInKindForm,
397+
"extra_fieldsets": [
398+
(
399+
"Payment",
400+
{
401+
"fields": [
402+
"payment_product",
403+
"payment_per_time",
404+
"people_per_household",
405+
"times_per_month",
406+
"months_per_year",
407+
"times_per_year",
408+
]
409+
},
410+
),
411+
],
412+
},
413+
LivelihoodStrategyType.RELIEF_GIFT_OTHER: {
414+
"accessor": "reliefgiftother",
415+
"form": ReliefGiftOtherForm,
416+
"extra_fieldsets": [
417+
(
418+
"Relief",
419+
{
420+
"fields": [
421+
"unit_multiple",
422+
"times_per_month",
423+
"months_per_year",
424+
"times_per_year",
425+
]
426+
},
427+
),
428+
],
429+
},
430+
LivelihoodStrategyType.OTHER_CASH_INCOME: {
431+
"accessor": "othercashincome",
432+
"form": OtherCashIncomeForm,
433+
"extra_fieldsets": [
434+
(
435+
"Income",
436+
{
437+
"fields": [
438+
"payment_per_time",
439+
"people_per_household",
440+
"times_per_month",
441+
"months_per_year",
442+
"times_per_year",
443+
]
444+
},
445+
),
446+
],
447+
},
448+
LivelihoodStrategyType.OTHER_PURCHASE: {
449+
"accessor": "otherpurchase",
450+
"form": OtherPurchaseForm,
451+
"extra_fieldsets": [
452+
(
453+
"Purchases",
454+
{
455+
"fields": [
456+
"unit_multiple",
457+
"times_per_month",
458+
"months_per_year",
459+
"times_per_year",
460+
]
461+
},
462+
),
463+
],
464+
},
465+
}
466+
336467
list_display = (
337468
"wealth_group",
338469
"strategy_type",
@@ -365,6 +496,41 @@ class LivelihoodActivityAdmin(admin.ModelAdmin):
365496
*translation_fields("livelihood_strategy__season__name__icontains"),
366497
)
367498

499+
def get_object(self, request, object_id, from_field=None):
500+
obj = super().get_object(request, object_id, from_field)
501+
if obj is None:
502+
return None
503+
config = self._SUBCLASS_CONFIG.get(obj.strategy_type)
504+
if config:
505+
try:
506+
return getattr(obj, config["accessor"])
507+
except AttributeError:
508+
pass
509+
return obj
510+
511+
def get_form(self, request, obj=None, change=False, **kwargs):
512+
if obj is not None:
513+
config = self._SUBCLASS_CONFIG.get(obj.strategy_type)
514+
if config and config.get("form"):
515+
return config["form"]
516+
return super().get_form(request, obj, change, **kwargs)
517+
518+
def get_fieldsets(self, request, obj=None):
519+
fieldsets = deepcopy(self.fieldsets)
520+
if obj is None:
521+
return fieldsets
522+
config = self._SUBCLASS_CONFIG.get(obj.strategy_type)
523+
if not config:
524+
return fieldsets
525+
for extra_field in config.get("quantity_extra", []):
526+
for fs in fieldsets:
527+
if fs[0] == "Quantity":
528+
fs[1]["fields"].append(extra_field)
529+
break
530+
for i, extra_fs in enumerate(config.get("extra_fieldsets", []), start=1):
531+
fieldsets.insert(i, extra_fs)
532+
return fieldsets
533+
368534
def get_queryset(self, request):
369535
qs = super().get_queryset(request)
370536
return qs.select_related(

apps/baseline/forms.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,11 @@
77
FoodPurchase,
88
LivelihoodActivity,
99
LivelihoodStrategy,
10+
MeatProduction,
1011
MilkProduction,
12+
OtherCashIncome,
1113
OtherPurchase,
14+
PaymentInKind,
1215
ReliefGiftOther,
1316
WealthGroup,
1417
WealthGroupCharacteristicValue,
@@ -116,6 +119,45 @@ def __init__(self, *args, **kwargs):
116119
self.fields["unit_multiple"].label = _("Purchase size")
117120

118121

122+
class MeatProductionForm(LivelihoodActivityForm):
123+
class Meta:
124+
model = MeatProduction
125+
exclude = [
126+
"livelihood_zone_baseline",
127+
"strategy_type",
128+
]
129+
widgets = {
130+
"livelihood_strategy": autocomplete.ModelSelect2(url="livelihoodstrategy-autocomplete"),
131+
"wealth_group": autocomplete.ModelSelect2(url="wealthgroup-autocomplete"),
132+
}
133+
134+
135+
class PaymentInKindForm(LivelihoodActivityForm):
136+
class Meta:
137+
model = PaymentInKind
138+
exclude = [
139+
"livelihood_zone_baseline",
140+
"strategy_type",
141+
]
142+
widgets = {
143+
"livelihood_strategy": autocomplete.ModelSelect2(url="livelihoodstrategy-autocomplete"),
144+
"wealth_group": autocomplete.ModelSelect2(url="wealthgroup-autocomplete"),
145+
}
146+
147+
148+
class OtherCashIncomeForm(LivelihoodActivityForm):
149+
class Meta:
150+
model = OtherCashIncome
151+
exclude = [
152+
"livelihood_zone_baseline",
153+
"strategy_type",
154+
]
155+
widgets = {
156+
"livelihood_strategy": autocomplete.ModelSelect2(url="livelihoodstrategy-autocomplete"),
157+
"wealth_group": autocomplete.ModelSelect2(url="wealthgroup-autocomplete"),
158+
}
159+
160+
119161
class WealthGroupCharacteristicValueForm(forms.ModelForm):
120162
class Meta:
121163
model = WealthGroupCharacteristicValue
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# Generated by Django 5.2.11 on 2026-02-26 07:23
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
("baseline", "0028_paymentinkind_payment_product"),
10+
]
11+
12+
operations = [
13+
migrations.AlterField(
14+
model_name="meatproduction",
15+
name="animals_slaughtered",
16+
field=models.PositiveSmallIntegerField(
17+
blank=True, null=True, verbose_name="Number of animals slaughtered"
18+
),
19+
),
20+
migrations.AlterField(
21+
model_name="meatproduction",
22+
name="carcass_weight",
23+
field=models.FloatField(blank=True, null=True, verbose_name="Carcass weight per animal"),
24+
),
25+
]

apps/baseline/models.py

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1721,14 +1721,25 @@ class MeatProduction(LivelihoodActivity):
17211721
"""
17221722

17231723
# Production calculation /validation is `input_quantity` * `item_yield`
1724-
animals_slaughtered = models.PositiveSmallIntegerField(verbose_name=_("Number of animals slaughtered"))
1725-
carcass_weight = models.FloatField(verbose_name=_("Carcass weight per animal"))
1724+
animals_slaughtered = models.PositiveSmallIntegerField(
1725+
verbose_name=_("Number of animals slaughtered"), null=True, blank=True
1726+
)
1727+
carcass_weight = models.FloatField(verbose_name=_("Carcass weight per animal"), null=True, blank=True)
1728+
1729+
def clean(self):
1730+
super().clean()
1731+
# If animals were slaughtered, carcass weight must be recorded
1732+
if self.animals_slaughtered and self.animals_slaughtered > 0 and self.carcass_weight is None:
1733+
raise ValidationError(_("Carcass weight is required when animals slaughtered"))
17261734

17271735
def validate_quantity_produced(self):
1728-
if self.quantity_produced and self.quantity_produced != self.animals_slaughtered * self.carcass_weight:
1729-
raise ValidationError(
1730-
_("Quantity Produced for a Meat Production must be animals slaughtered multiplied by carcass weight")
1731-
)
1736+
if self.quantity_produced is not None and self.animals_slaughtered and self.carcass_weight is not None:
1737+
if self.quantity_produced != self.animals_slaughtered * self.carcass_weight:
1738+
raise ValidationError(
1739+
_(
1740+
"Quantity Produced for a Meat Production must be animals slaughtered multiplied by carcass weight"
1741+
)
1742+
)
17321743

17331744
class Meta:
17341745
verbose_name = LivelihoodStrategyType.MEAT_PRODUCTION.label

apps/baseline/tests/test_viewsets.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3851,6 +3851,34 @@ def test_html(self):
38513851
df = pd.read_html(content)[0].fillna("")
38523852
self.assertEqual(len(df), self.num_records + 1)
38533853

3854+
def test_filter_by_country(self):
3855+
country = CountryFactory(
3856+
iso3166a2="AA",
3857+
iso3166a3="AAA",
3858+
iso3166n3=911,
3859+
iso_en_ro_name="A Country",
3860+
iso_en_name="AA Country",
3861+
name="AA Country",
3862+
)
3863+
PaymentInKindFactory(livelihood_zone_baseline__livelihood_zone__country=country)
3864+
response = self.client.get(self.url, {"country": country.iso3166a2})
3865+
self.assertEqual(response.status_code, 200)
3866+
self.assertEqual(len(response.json()), 1)
3867+
response = self.client.get(self.url, {"country": country.iso_en_ro_name})
3868+
self.assertEqual(response.status_code, 200)
3869+
self.assertEqual(len(response.json()), 1)
3870+
3871+
def test_filter_by_livelihood_zone(self):
3872+
livelihood_zone = LivelihoodZoneFactory(code="MW01")
3873+
PaymentInKindFactory(livelihood_zone_baseline__livelihood_zone=livelihood_zone)
3874+
response = self.client.get(self.url, {"livelihood_zone": "MW01"})
3875+
self.assertEqual(response.status_code, 200)
3876+
self.assertEqual(len(response.json()), 1)
3877+
# Case-insensitive match
3878+
response = self.client.get(self.url, {"livelihood_zone": "mw01"})
3879+
self.assertEqual(response.status_code, 200)
3880+
self.assertEqual(len(response.json()), 1)
3881+
38543882

38553883
class ReliefGiftsOtherViewSetTestCase(APITestCase):
38563884
@classmethod

0 commit comments

Comments
 (0)