diff --git a/beer/models.py b/beer/models.py index 9ad8624..c21f83b 100644 --- a/beer/models.py +++ b/beer/models.py @@ -72,7 +72,7 @@ class CustomIngredient(CustomModel): class Meta: abstract = True - + class BatchRecipe(CustomModel): """ Recipe to be stored with a batch.""" name = models.CharField(max_length=50) @@ -82,6 +82,97 @@ class BatchRecipe(CustomModel): efficiency = models.DecimalField(max_digits=6, decimal_places=2, default=75) batch_size = models.DecimalField(max_digits=6, decimal_places=2, default=11) + @property + def fermentables(self): + return [x for x in list(self.recipefermentable_set.all())] + + @property + def hops(self): + return [x for x in list(self.recipehop_set.all())] + + @property + def final_volume(self): + return float(self.batch_size) + self.hop_water_loss + self.net_kettle_deadspace + self.kettle_hose_loss + + @property + def ferm_yield(self): + ferm_yield = 0 + + for f in self.fermentables: + if f.fermentable.fermentable_type == 3: # Is sugar + ferm_yield += f.quantity * (f.fermentable.potential - 1) * 1000 + else: + ferm_yield += f.quantity * (self.efficiency / 100) * (f.fermentable.potential - 1) * 1000 + + return float(ferm_yield) + + @property + def mash_yield(self): + mash_yield = 0 + + for f in self.fermentables: + if f.fermentable.fermentable_type != 3: # Is not sugar + mash_yield += f.quantity * (self.efficiency / 100) * (f.fermentable.potential - 1) * 1000 + + return float(mash_yield) + + @property + def original_sg(self): + return round(1 + self.ferm_yield / self.final_volume / 1000, 3) + + @property + def pre_boil_sg(self): + return self.ferm_yield / (self.final_volume + self.boil_off_gph) + + @property + def hop_water_loss(self): + hop_absorption = .025 # gallons per ounce + return sum([float(x.quantity) * hop_absorption for x in self.hops]) + + @property + def net_kettle_deadspace(self): + # If hops in kettle deadspace + result = self.kettle_dead_space - self.hop_water_loss + return float(max(0, result)) # No deadspace if its all filled with hop trub) + + # Else hops in bag or removed + return 0 + + @property + def kettle_hose_loss(self): + return .25 # TODO + + @property + def kettle_dead_space(self): + return .25 # TODO + + @property + def boil_off_gph(self): + return .8 # TODO + + @property + def ibu_tinseth(self): + return sum([x.ibu_tinseth for x in self.hops]) + + @property + def srm(self): + color_total = sum([x.srm for x in self.fermentables]) + return 1.4922*(color_total**0.6859) + + @property + def srm_hex(self): + SRM_HEX = { + 1: 'F3F993',2: 'F5F75C',3: 'F6F513',4: 'EAE615',5: 'E0D01B', + 6: 'D5BC26',7: 'CDAA37',8: 'C1963C',9: 'BE8C3A',10: 'BE823A', + 11: 'C17A37',12: 'BF7138',13: 'BC6733',14: 'B26033',15: 'A85839', + 16: '985336',17: '8D4C32',18: '7C452D',19: '6B3A1E',20: '5D341A', + 21: '4E2A0C',22: '4A2727',23: '361F1B',24: '261716',25: '231716', + 26: '19100F',27: '16100F',28: '120D0C',29: '100B0A',30: '050B0A' + } + + return '#{}'.format(SRM_HEX[int(self.srm)]) + + def __str__(self): return self.name @@ -97,6 +188,7 @@ class Fermentable(CustomIngredient): types = { 1: 'Grain', 2: 'Adjunct', + 3: 'Sugar' } grain_category = models.IntegerField(choices=categories, default=1) @@ -119,6 +211,10 @@ class RecipeFermentable(CustomModel): fermentable = models.ForeignKey(Fermentable, on_delete=models.CASCADE) quantity = models.DecimalField(max_digits=6, decimal_places=4) + @property + def srm(self): + return round(float(self.fermentable.lovibond) * float(self.quantity) / self.recipe.final_volume, 1) + class Hop(CustomIngredient): uses = { 1: 'Bittering', @@ -155,6 +251,25 @@ class RecipeHop(CustomModel): time = models.IntegerField(default=60, validators=[MinValueValidator(0)]) use = models.IntegerField(choices=uses, default=1) + @property + def ibu_tinseth(self): + type_bonus = { + 1: 1.1, # Pellet + 2: 1.0, # Leaf + 3: 1.1, # Cryo + 4: 1.4, # CO2 Extract + } + + ibu = 0 + + average_wort_sg = (self.recipe.pre_boil_sg/1000 + (self.recipe.original_sg-1)) / 2 + if self.use == 1: + conc = ((float(self.hop.alpha) / 100) * float(self.quantity)) * 7490 / self.recipe.final_volume + util = (type_bonus[self.hop.hop_type] * 1.65 * (0.000125**average_wort_sg)) * ((1-2.71828182845904**(-0.04 * self.time))/4.15) + ibu = conc * util + + return float(ibu) + class Misc(CustomIngredient): uses = { 1: 'Mash',