brewery-website/beer/models.py

334 lines
11 KiB
Python

from django.db import models
from django.utils import timezone
from django_cryptography.fields import encrypt
from django.core.validators import MinValueValidator
from config.extras import BREWFATHER_APP_ROOT
from django.conf import settings
import logging
logger = logging.getLogger('django')
class CustomModel(models.Model):
""" Custom model class with default fields to use. """
created_date = models.DateTimeField(default=timezone.now)
class Meta:
abstract = True
class UserProfile(CustomModel):
user = models.OneToOneField(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
brewfather_api_user = encrypt(models.TextField(max_length=128))
brewfather_api_key = encrypt(models.TextField(max_length=128))
def __str__(self):
return self.user.username
class Batch(CustomModel):
brewfather_id = models.CharField(max_length=50)
brewfather_num = models.IntegerField(default=1)
brewfather_name = models.CharField(max_length=500, default='name')
recipe = models.OneToOneField('BatchRecipe', on_delete=models.CASCADE, default=1)
@property
def brewfather_url(self):
return '{}/tabs/batches/batch/{}'.format(BREWFATHER_APP_ROOT, self.brewfather_id)
def __str__(self):
# Return a string that represents the instance
return 'BF #{num}: {name}'.format(name=self.brewfather_name, num=self.brewfather_num)
#-----------------------------------------------------------------------
# Recipe Stuff
#-----------------------------------------------------------------------
class Unit(CustomModel):
unit_types = {
'WT': 'Weight',
'VL': 'Volume',
}
""" Recipe to be stored with a batch."""
name = models.CharField(max_length=50)
unit_type = models.CharField(max_length=3, choices=unit_types, default='WT')
def __str__(self):
return self.name
class Supplier(CustomModel):
name = models.CharField(max_length=50)
def __str__(self):
return self.name
class CustomIngredient(CustomModel):
""" Custom model class with default fields to use. """
created_date = models.DateTimeField(default=timezone.now)
name = models.CharField(max_length=50)
units = models.ForeignKey(Unit, on_delete=models.PROTECT)
unit_cost = models.DecimalField(max_digits=6, decimal_places=2, null=True, blank=True)
supplier = models.ForeignKey(Supplier, on_delete=models.PROTECT, null=True, blank=True)
notes = models.TextField(max_length=500, blank=True, null=True)
user_notes = models.TextField(max_length=500, blank=True, null=True)
class Meta:
abstract = True
class BatchRecipe(CustomModel):
""" Recipe to be stored with a batch."""
name = models.CharField(max_length=50)
batch_recipe = models.BooleanField(null=True)
recipe_json = models.TextField(null=True, blank=True)
mash = models.ForeignKey('Mash' , on_delete=models.PROTECT, null=True, blank=True)
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
class Fermentable(CustomIngredient):
categories = {
1: 'Base',
2: 'Wheat/Oat',
3: 'Crystal',
4: 'Roasted',
5: 'Acid',
}
types = {
1: 'Grain',
2: 'Adjunct',
3: 'Sugar'
}
grain_category = models.IntegerField(choices=categories, default=1)
fermentable_type = models.IntegerField(choices=types, default=1)
diastatic_power = models.DecimalField(max_digits=6, decimal_places=4, null=True, blank=True)
potential = models.DecimalField(max_digits=6, decimal_places=4)
protein = models.DecimalField(max_digits=6, decimal_places=4, null=True, blank=True)
attenuation = models.DecimalField(max_digits=6, decimal_places=4, null=True, blank=True)
lovibond = models.DecimalField(max_digits=6, decimal_places=4, null=True, blank=True)
max_in_batch = models.DecimalField(max_digits=6, decimal_places=4, null=True, blank=True)
moisture = models.DecimalField(max_digits=6, decimal_places=4, null=True, blank=True)
non_fermentable = models.BooleanField(null=True, blank=True)
ibu_per_unit = models.DecimalField(max_digits=6, decimal_places=4, default=0)
def __str__(self):
return self.name
class RecipeFermentable(CustomModel):
recipe = models.ForeignKey(BatchRecipe, on_delete=models.CASCADE)
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',
2: 'Aroma',
3: 'Both',
}
types = {
1: 'Pellet',
2: 'Leaf',
3: 'Cryo',
4: 'CO2 Extract',
}
ibu = models.DecimalField(max_digits=6, decimal_places=4, default=0)
use = models.IntegerField(choices=uses, default=1)
hop_type = models.IntegerField(choices=types, default=1)
alpha = models.DecimalField(max_digits=6, decimal_places=2, default=0)
def __str__(self):
return self.name
class RecipeHop(CustomModel):
uses = {
1: 'Boil',
2: 'Dry Hop',
3: 'Aroma (Hop Stand)',
4: 'Mash',
5: 'First Wort'
}
recipe = models.ForeignKey(BatchRecipe, on_delete=models.CASCADE)
hop = models.ForeignKey(Hop, on_delete=models.CASCADE)
quantity = models.DecimalField(max_digits=6, decimal_places=4)
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',
2: 'Sparge',
3: 'Boil',
4: 'Flamout',
5: 'Primary',
6: 'Secondary',
7: 'Cold Crash',
8: 'Bottling',
}
types = {
1: 'Spice',
2: 'Fining',
3: 'Water Agent',
4: 'Herb',
5: 'Flavor',
6: 'Other',
}
use = models.IntegerField(choices=uses, default=1)
misc_type = models.IntegerField(choices=types, default=1)
water_adjustment = models.BooleanField(null=True, blank=True)
def __str__(self):
return self.name
class RecipeMisc(CustomModel):
recipe = models.ForeignKey(BatchRecipe, on_delete=models.CASCADE)
misc = models.ForeignKey(Misc, on_delete=models.CASCADE)
quantity = models.DecimalField(max_digits=6, decimal_places=4)
def __str__(self):
return self.name
class RecipeYeast(CustomModel):
recipe = models.ForeignKey(BatchRecipe, on_delete=models.CASCADE)
yeast = models.ForeignKey('yeast.Strain', on_delete=models.CASCADE)
class Mash(CustomModel):
name = models.CharField(max_length=50)
def __str__(self):
return self.name
class MashStep(CustomModel):
step_types = {
1: 'infusion',
2: 'temperature',
3: 'decoction',
}
name = models.CharField(max_length=50)
step_temp = models.DecimalField(max_digits=6, decimal_places=2)
ramp_time = models.DecimalField(max_digits=6, decimal_places=2)
step_time = models.DecimalField(max_digits=6, decimal_places=2)
step_type = models.IntegerField(choices=step_types, default=1)
parent_mash = models.ForeignKey(Mash, on_delete=models.CASCADE)
def __str__(self):
return self.name