@@ -348,22 +348,23 @@ class Language(models.TextChoices):
348348 verbose_name = _ ("Currency" ),
349349 help_text = _ ("Default currency for income or expenditure from Livelihood Activities within this Baseline." ),
350350 )
351+ # Derived field - always set by calculate_fields()
352+ _annual_kcals_cost = models .FloatField (
353+ blank = True ,
354+ null = True ,
355+ verbose_name = _ ("Annual kcals cost" ),
356+ help_text = _ ("Annual cost per person of 100% of recommended kcals in the baseline currency." ),
357+ )
351358 objects = LivelihoodZoneBaselineManager ()
352359
353- def _get_annual_kcals_cost (self ):
360+ def _get_annual_kcals_cost_sql (self ):
354361 """
355362 Calculate the annual cost per person of 100% of recommended kcals.
356363
357- This is required to calculate the combined food and income as kcals.
358-
359- It is defined as the cost of providing 100% of required kcals for the P
360- Wealth Group divided by the average_household_size. The cost is
361- calculated as the cost of Basket 2 (Other food survival) plus the cost
362- of providing the remainder of kcals by purchasing the main staple. All
363- costs are in the currency for the BSS.
364-
365- This calculation is based on the formulae in the Graphs worksheet.
364+ This method uses a stored SQL function and is used for troubleshooting.
366365 """
366+ if not self .pk :
367+ return None
367368 return (
368369 type (self )
369370 .objects .filter (pk = self .pk )
@@ -377,10 +378,22 @@ def _get_annual_kcals_cost(self):
377378 .get ()
378379 )
379380
380- def _get_annual_kcals_cost_python (self ):
381+ def _get_annual_kcals_cost (self ):
381382 """
382- A Python implementation of the annual_kcals_cost calculation, used for troubleshooting.
383+ Calculate the annual cost per person of 100% of recommended kcals.
384+
385+ This is required to calculate the combined food and income as kcals.
386+
387+ It is defined as the cost of providing 100% of required kcals for the P
388+ Wealth Group divided by the average_household_size. The cost is
389+ calculated as the cost of Basket 2 (Other food survival) plus the cost
390+ of providing the remainder of kcals by purchasing the main staple. All
391+ costs are in the currency for the BSS.
392+
393+ This calculation is based on the formulae in the Graphs worksheet.
383394 """
395+ if not self .pk :
396+ return None
384397 poor_main_staple = LivelihoodProductCategory .objects .filter (
385398 basket = LivelihoodProductCategory .ProductBasket .MAIN_STAPLE ,
386399 baseline_livelihood_activity__wealth_group__livelihood_zone_baseline = self ,
@@ -400,6 +413,10 @@ def _get_annual_kcals_cost_python(self):
400413 )
401414 )
402415 poor_main_staple = poor_main_staple [0 ]
416+ poor_household_size = poor_main_staple .baseline_livelihood_activity .wealth_group .average_household_size
417+ if not poor_household_size :
418+ # Cannot calculate without household size
419+ return None
403420 poor_other_food = LivelihoodProductCategory .objects .filter (
404421 basket = LivelihoodProductCategory .ProductBasket .SURVIVAL_OTHER_FOOD ,
405422 baseline_livelihood_activity__wealth_group__livelihood_zone_baseline = self ,
@@ -412,12 +429,11 @@ def _get_annual_kcals_cost_python(self):
412429 F ("baseline_livelihood_activity__expenditure" ) * F ("percentage_allocation_to_basket" )
413430 ),
414431 )
415- poor_household_size = poor_main_staple .baseline_livelihood_activity .wealth_group .average_household_size
416432 main_staple_kcals_per_unit = poor_main_staple .baseline_livelihood_activity .extra .get (
417433 "product__kcals_per_unit" ,
418434 poor_main_staple .baseline_livelihood_activity .livelihood_strategy .product .kcals_per_unit ,
419435 )
420- main_staple_percentage_kcals_required = 1 - poor_other_food ["total_percentage_kcals" ]
436+ main_staple_percentage_kcals_required = 1 - ( poor_other_food ["total_percentage_kcals" ] or 0 )
421437 main_staple_cost = (
422438 2100 # kcals per person per day
423439 * 365 # days per year
@@ -426,17 +442,29 @@ def _get_annual_kcals_cost_python(self):
426442 / main_staple_kcals_per_unit
427443 * poor_main_staple .baseline_livelihood_activity .price
428444 )
429- total_cost = main_staple_cost + poor_other_food ["total_expenditure" ]
445+ total_cost = main_staple_cost + ( poor_other_food ["total_expenditure" ] or 0 )
430446 total_food_cost_per_person = total_cost / poor_household_size
431447 return total_food_cost_per_person
432448
433- def get_annual_kcals_cost (self ):
434- key = f"livelihood_zone_baseline~{ self .pk } ~annual_kcals_cost"
435- annual_kcals_cost = cache .get (key )
436- if not annual_kcals_cost :
437- annual_kcals_cost = self ._get_annual_kcals_cost ()
438- cache .set (key , annual_kcals_cost , 60 * 60 * 24 )
439- return annual_kcals_cost
449+ @property
450+ def annual_kcals_cost (self ):
451+ return self ._annual_kcals_cost
452+
453+ def calculate_fields (self ):
454+ self ._annual_kcals_cost = self ._get_annual_kcals_cost ()
455+
456+ def save (self , * args , ** kwargs ):
457+ self .calculate_fields ()
458+ self .full_clean (
459+ exclude = [field .name for field in self ._meta .fields if isinstance (field , models .ForeignKey )],
460+ validate_unique = False ,
461+ )
462+
463+ # Make sure that _annual_kcals_cost is included in the update_fields if the update_fields argument is provided.
464+ update_fields = kwargs .get ("update_fields" )
465+ if update_fields is not None :
466+ kwargs ["update_fields" ] = set (update_fields ) | {"_annual_kcals_cost" }
467+ super ().save (* args , ** kwargs )
440468
441469 def natural_key (self ):
442470 try :
@@ -734,7 +762,7 @@ class WealthGroup(common_models.Model):
734762
735763 @cached_property
736764 def household_annual_kcals_cost (self ):
737- annual_kcals_cost = self .livelihood_zone_baseline .get_annual_kcals_cost ()
765+ annual_kcals_cost = self .livelihood_zone_baseline .annual_kcals_cost
738766 if annual_kcals_cost and self .average_household_size :
739767 return annual_kcals_cost * self .average_household_size
740768
@@ -759,7 +787,7 @@ def _get_survival_threshold_as_percentage_kcals(self):
759787 This basket is defined for the Poor Wealth Group, and other values use
760788 the same expenditure, scaled according to average household size.
761789 """
762- annual_kcals_cost = self .livelihood_zone_baseline .get_annual_kcals_cost ()
790+ annual_kcals_cost = self .livelihood_zone_baseline .annual_kcals_cost
763791 poor_survival_non_food = (
764792 LivelihoodProductCategory .objects .filter (
765793 basket = LivelihoodProductCategory .ProductBasket .SURVIVAL_NON_FOOD ,
@@ -804,7 +832,7 @@ def survival_threshold_as_percentage_kcals(self):
804832
805833 @cached_property
806834 def survival_threshold_as_cash (self ):
807- annual_kcals_cost = self .livelihood_zone_baseline .get_annual_kcals_cost ()
835+ annual_kcals_cost = self .livelihood_zone_baseline .annual_kcals_cost
808836 if annual_kcals_cost and self .survival_threshold_as_percentage_kcals and self .average_household_size :
809837 return self .survival_threshold_as_percentage_kcals * self .average_household_size * annual_kcals_cost
810838
@@ -847,7 +875,7 @@ def _get_livelihoods_protection_threshold_as_percentage_kcals(self):
847875 Livelihoods Protection Basket then the amount is inherited from the Poor wealth group,
848876 rather than based on typical consumption for the Wealth Group in the Reference Year.
849877 """
850- annual_kcals_cost = self .livelihood_zone_baseline .get_annual_kcals_cost ()
878+ annual_kcals_cost = self .livelihood_zone_baseline .annual_kcals_cost
851879
852880 livelihood_protection_qs = LivelihoodProductCategory .objects .filter (
853881 basket = LivelihoodProductCategory .ProductBasket .LIVELIHOODS_PROTECTION ,
@@ -981,7 +1009,7 @@ def livelihoods_protection_threshold_as_percentage_kcals(self):
9811009
9821010 @cached_property
9831011 def livelihoods_protection_threshold_as_cash (self ):
984- annual_kcals_cost = self .livelihood_zone_baseline .get_annual_kcals_cost ()
1012+ annual_kcals_cost = self .livelihood_zone_baseline .annual_kcals_cost
9851013 if (
9861014 annual_kcals_cost
9871015 and self .livelihoods_protection_threshold_as_percentage_kcals
@@ -1754,7 +1782,7 @@ def total_income_as_percentage_kcals(self):
17541782 """
17551783 The total food consumed and income received as percentage of required household kcals.
17561784 """
1757- annual_kcals_cost = self .livelihood_zone_baseline .get_annual_kcals_cost ()
1785+ annual_kcals_cost = self .livelihood_zone_baseline .annual_kcals_cost
17581786 percentage_kcals = self .percentage_kcals or 0
17591787 income = self .income or 0
17601788 if self .wealth_group .average_household_size :
@@ -1769,7 +1797,7 @@ def total_income_as_cash(self):
17691797 """
17701798 The total food consumed and income received as an amount in the BSS currency.
17711799 """
1772- annual_kcals_cost = self .livelihood_zone_baseline .get_annual_kcals_cost ()
1800+ annual_kcals_cost = self .livelihood_zone_baseline .annual_kcals_cost
17731801 percentage_kcals = self .percentage_kcals or 0
17741802 income = self .income or 0
17751803 if self .wealth_group .average_household_size :
@@ -1854,15 +1882,17 @@ def validate_expenditure(self):
18541882
18551883 However, some LivelihoodActivity subclasses, such as FoodPurchase and
18561884 OtherPurchase, involve spending money to acquire the item, in which
1857- case we must validate that expenditure = quantity_produced * price
1885+ case we must validate that expenditure = quantity_purchased * price
18581886 """
1859- quantity_produced = self .quantity_produced or 0
1887+ quantity_produced_or_purchased = self .quantity_produced or self . quantity_purchased or 0
18601888 price = self .price or 0
18611889 expenditure = self .expenditure or 0
18621890
1863- if self .expenditure and not math .isclose (expenditure , quantity_produced * price ):
1891+ if self .expenditure and not math .isclose (expenditure , quantity_produced_or_purchased * price ):
18641892 raise ValidationError (
1865- _ ("Expenditure for a Livelihood Activity must be quantity produced multiplied by price" )
1893+ _ (
1894+ "Expenditure for a Livelihood Activity must be quantity produced or quantity purchased multiplied by price"
1895+ )
18661896 )
18671897
18681898 def validate_kcals_consumed (self ):
0 commit comments