714 lines
25 KiB
Python
714 lines
25 KiB
Python
from django.db import models
|
|
from django.db.models import Sum, Q
|
|
from django.contrib.staticfiles import finders
|
|
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 beer.extras import sg_plato, plato_sg, kg_extract, convert
|
|
from django.conf import settings
|
|
|
|
import json
|
|
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,
|
|
editable=False
|
|
)
|
|
|
|
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(
|
|
'Recipe', on_delete=models.CASCADE, default=1)
|
|
|
|
# -------------------------------------------------------------------------
|
|
# Brewday Measurements
|
|
# -------------------------------------------------------------------------
|
|
|
|
# Mash
|
|
first_runnings = models.DecimalField(
|
|
max_digits=8, decimal_places=4, null=True, blank=True)
|
|
mash_ph = models.DecimalField(
|
|
max_digits=4, decimal_places=3, null=True, blank=True)
|
|
|
|
# Boil
|
|
pre_boil_vol = models.DecimalField(
|
|
max_digits=8, decimal_places=4, null=True, blank=True)
|
|
pre_boil_sg = models.DecimalField(
|
|
max_digits=5, decimal_places=4, null=True, blank=True)
|
|
post_boil_vol = models.DecimalField(
|
|
max_digits=8, decimal_places=4, null=True, blank=True)
|
|
post_boil_sg = models.DecimalField(
|
|
max_digits=5, decimal_places=4, null=True, blank=True)
|
|
|
|
# Ferment
|
|
fermenter_topup_vol = models.DecimalField(
|
|
max_digits=8, decimal_places=4, null=True, blank=True)
|
|
fermenter_vol = models.DecimalField(
|
|
max_digits=8, decimal_places=4, null=True, blank=True)
|
|
original_sg = models.DecimalField(
|
|
max_digits=5, decimal_places=4, null=True, blank=True)
|
|
final_sg = models.DecimalField(
|
|
max_digits=5, decimal_places=4, null=True, blank=True)
|
|
|
|
# Properties Needed:
|
|
# braukaiser.com/wiki/index.php/Troubleshooting_Brewhouse_Efficiency
|
|
# - Mash Efficiency
|
|
# - ABV
|
|
# - Attenuation
|
|
# - Actual Trub/Chiller Loss
|
|
|
|
# -------------------------------------------------------------------------
|
|
# Batch Stats
|
|
# -------------------------------------------------------------------------
|
|
@property
|
|
def brewhouse_efficiency(self):
|
|
try:
|
|
return round(self.boil_extract_kg/self.recipe.total_extract_kg, 4)
|
|
except ZeroDivisionError:
|
|
return 0
|
|
|
|
@property
|
|
def boil_extract_kg(self):
|
|
return kg_extract(float(self.post_boil_vol)*.96, self.post_boil_sg)
|
|
|
|
@property
|
|
def conversion_efficiency(self):
|
|
""" Calculate conversion efficiency of mash."""
|
|
if (self.first_runnings is None
|
|
or self.recipe.fermentable_weight_kg == 0):
|
|
return '-'
|
|
|
|
return round((sg_plato(self.first_runnings)/self.recipe.fw_max)
|
|
* (100-self.recipe.fw_max)
|
|
/ (100-sg_plato(self.first_runnings)), 4)
|
|
|
|
@property
|
|
def boil_off_calcualted(self):
|
|
return float(self.pre_boil_vol - self.post_boil_vol) * .96
|
|
|
|
@property
|
|
def trub_loss_calculated(self):
|
|
transfered_volume = self.fermenter_vol - self.fermenter_topup_vol
|
|
return self.post_boil_vol-transfered_volume
|
|
|
|
@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,
|
|
editable=False
|
|
)
|
|
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)
|
|
parent = models.ForeignKey(
|
|
'self', on_delete=models.SET_NULL, null=True, blank=True)
|
|
|
|
class Meta:
|
|
abstract = True
|
|
|
|
|
|
class Recipe(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)
|
|
equipment = models.ForeignKey(
|
|
'EquipmentProfile', 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)
|
|
fermentables = models.ManyToManyField(
|
|
'Fermentable', through='RecipeFermentable')
|
|
|
|
with open(finders.find('bjcp/2021.json'), encoding='utf-8', errors="ignore") as bjcp_file:
|
|
bjcp = json.load(bjcp_file)
|
|
styles = bjcp['styles']
|
|
style_ids = dict([
|
|
(x['style_id'], '{}: {}'.format(x['style_id'], x['name']))
|
|
for x in styles
|
|
])
|
|
|
|
bjcp_style_id = models.CharField(
|
|
max_length=3, choices=style_ids, default='1A')
|
|
|
|
class Meta:
|
|
verbose_name = 'Recipe'
|
|
verbose_name_plural = 'Recipes'
|
|
|
|
@property
|
|
def batch_size_display(self):
|
|
return convert(self.batch_size, 'l', 'gal')
|
|
|
|
@property
|
|
def fw_max(self):
|
|
potential = 0
|
|
weight = 0
|
|
for ferm in self.recipefermentable_set.all():
|
|
potential += ferm.fermentable.extract_percent*float(ferm.quantity)
|
|
weight += float(ferm.quantity)
|
|
|
|
e_grain = potential / weight
|
|
|
|
ratio = float(self.equipment.mash_ratio)
|
|
return 100*e_grain / (ratio+e_grain)
|
|
|
|
@property
|
|
def total_fermentable_display(self):
|
|
return convert(self.fermentable_weight_kg, 'kg', 'lb')
|
|
|
|
@property
|
|
def fermentable_weight_kg(self):
|
|
"""Weight of all fermentables attached to recipe."""
|
|
aggregate = self.recipefermentable_set.all().aggregate(Sum('quantity'))
|
|
|
|
if aggregate['quantity__sum']:
|
|
return aggregate['quantity__sum']
|
|
else:
|
|
return 0
|
|
|
|
@property
|
|
def total_extract_kg(self):
|
|
extract = 0
|
|
for f in self.recipefermentable_set.all():
|
|
extract += f.extract_weight_kg
|
|
|
|
return extract
|
|
|
|
@property
|
|
def total_hop_display(self):
|
|
return convert(self.hop_weight_g, 'g', 'oz')
|
|
|
|
@property
|
|
def hop_weight_g(self):
|
|
"""Weight of all fermentables attached to recipe."""
|
|
aggregate = self.recipehop_set.all().aggregate(Sum('quantity'))
|
|
|
|
if aggregate['quantity__sum']:
|
|
return aggregate['quantity__sum']
|
|
else:
|
|
return 0
|
|
|
|
@property
|
|
def final_volume(self):
|
|
"""Return final volume (after boil)."""
|
|
return (float(self.batch_size)
|
|
+ self.hop_water_loss
|
|
+ self.net_kettle_deadspace
|
|
+ self.kettle_hose_loss)
|
|
|
|
@property
|
|
def sugar_yield(self):
|
|
"""Return point yield of all non-mashed ingredients."""
|
|
|
|
ferms = self.recipefermentable_set.all().select_related('fermentable')
|
|
sugars = ferms.filter(fermentable__fermentable_type=3)
|
|
|
|
ferm_yield = 0
|
|
for f in sugars:
|
|
ferm_yield += f.extract_weight_kg
|
|
|
|
return float(ferm_yield)
|
|
|
|
@property
|
|
def mash_yield(self):
|
|
"""Return point yield of all mashed ingredients."""
|
|
|
|
ferms = self.recipefermentable_set.all().select_related('fermentable')
|
|
mashed = ferms.filter(~Q(fermentable__fermentable_type=3))
|
|
|
|
mash_yield = 0
|
|
for f in mashed:
|
|
mash_yield += f.extract_weight_kg * float(self.efficiency/100)
|
|
|
|
return float(mash_yield)
|
|
|
|
@property
|
|
def original_sg(self):
|
|
"""Return original gravity."""
|
|
total_extract = self.sugar_yield + self.mash_yield
|
|
plato = 100 * total_extract / (self.final_volume + total_extract)
|
|
return round(plato_sg(plato), 3)
|
|
|
|
@property
|
|
def pre_boil_sg(self):
|
|
"""Return pre-boil gravity."""
|
|
total_extract = self.sugar_yield + self.mash_yield
|
|
total_water = self.final_volume+self.boil_off_gph
|
|
plato = 100 * total_extract / (total_water + total_extract)
|
|
return round(plato_sg(plato), 3)
|
|
|
|
@property
|
|
def hop_water_loss(self):
|
|
return float(sum(
|
|
x.quantity*x.trub_volume for x in self.recipehop_set.all()
|
|
))
|
|
|
|
@property
|
|
def net_kettle_deadspace(self):
|
|
if self.equipment is None:
|
|
return 0
|
|
|
|
# If hops in kettle deadspace
|
|
# No deadspace if its all filled with hop trub
|
|
if self.equipment.hops_remain_kettle:
|
|
result = self.kettle_dead_space - self.hop_water_loss
|
|
return float(max(0, result))
|
|
else:
|
|
return 0
|
|
|
|
@property
|
|
def kettle_hose_loss(self):
|
|
if self.equipment is None:
|
|
return 0
|
|
return float(self.equipment.kettle_plumbing_loss)
|
|
|
|
@property
|
|
def kettle_dead_space(self):
|
|
if self.equipment is None:
|
|
return 0
|
|
return float(self.equipment.kettle_deadspace)
|
|
|
|
@property
|
|
def boil_off_gph(self):
|
|
if self.equipment is None:
|
|
return 0
|
|
return float(self.equipment.kettle_boil_rate)
|
|
|
|
@property
|
|
def ibu(self): # TODO: Multiple IBU formulas
|
|
return self.ibu_tinseth
|
|
|
|
@property
|
|
def ibu_tinseth(self):
|
|
return sum(x.ibu_tinseth for x in self.recipehop_set.all())
|
|
|
|
@property
|
|
def srm(self):
|
|
color_total = sum(x.lovibond_contributed
|
|
for x in self.recipefermentable_set.all())
|
|
return 1.4922*(color_total**0.6859)
|
|
|
|
@property
|
|
def srm_hex(self):
|
|
srm_hex_lookup = {
|
|
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', 0: 'C1963C'
|
|
}
|
|
|
|
return '#{}'.format(srm_hex_lookup[int(self.srm)])
|
|
|
|
@property
|
|
def bu_gu(self):
|
|
gu = (self.original_sg - 1) * 1000
|
|
try:
|
|
return self.ibu_tinseth / gu
|
|
except ZeroDivisionError:
|
|
return 0
|
|
|
|
@property
|
|
def rbr(self): # .75 needs to be calculated number...
|
|
return self.bu_gu * (1 + (.75 - 0.7655))
|
|
|
|
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, default=0.80)
|
|
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, default=0.04)
|
|
non_fermentable = models.BooleanField(null=True, blank=True)
|
|
ibu_per_unit = models.DecimalField(
|
|
max_digits=6, decimal_places=4, default=0)
|
|
|
|
@property
|
|
def extract_percent(self):
|
|
return (((float(self.potential)-1)*1000/46.17)
|
|
* (1-float(self.moisture)/100))
|
|
|
|
def __str__(self):
|
|
return self.name
|
|
|
|
|
|
class RecipeFermentable(CustomModel):
|
|
recipe = models.ForeignKey(Recipe, on_delete=models.CASCADE)
|
|
fermentable = models.ForeignKey(Fermentable, on_delete=models.CASCADE)
|
|
quantity = models.DecimalField(max_digits=6, decimal_places=4)
|
|
|
|
# Properties Needed:
|
|
# - The weight of extract in the grist
|
|
# extract in grist in kg = weight of grist in kg * extract potential
|
|
|
|
@property
|
|
def quantity_display(self):
|
|
return convert(self.quantity, 'kg', 'lb')
|
|
|
|
@property
|
|
def extract_weight_kg(self):
|
|
return float(self.quantity) * self.fermentable.extract_percent
|
|
|
|
@property
|
|
def percent(self):
|
|
return float(100 * self.quantity / self.recipe.fermentable_weight_kg)
|
|
|
|
@property
|
|
def lovibond_contributed(self):
|
|
srm_calc = (float(self.fermentable.lovibond)
|
|
* convert(self.quantity, 'kg', 'lb')
|
|
/ convert(self.recipe.final_volume, 'l', 'gal'))
|
|
|
|
return round(srm_calc, 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(Recipe, 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 quantity_display(self):
|
|
"""Convert grams to ounces."""
|
|
return convert(self.quantity, 'g', 'oz')
|
|
|
|
@property
|
|
def ibu_tinseth(self):
|
|
type_bonus = {
|
|
1: 1.1, # Pellet
|
|
2: 1.0, # Leaf
|
|
3: 1.1, # Cryo
|
|
4: 1.4, # CO2 Extract
|
|
}
|
|
|
|
hop_bonus = type_bonus[self.hop.hop_type]
|
|
|
|
average_wort_sg = (((self.recipe.pre_boil_sg-1)
|
|
+ (self.recipe.original_sg-1)) / 2)
|
|
|
|
if self.use == 1:
|
|
conc = (float(self.hop.alpha/100)
|
|
* convert(self.quantity, 'g', 'oz')
|
|
* 7490/convert(self.recipe.final_volume, 'l', 'gal'))
|
|
util = (hop_bonus*1.65*0.000125**average_wort_sg
|
|
* ((1-2.71828182845904**(-0.04*self.time)) / 4.15))
|
|
ibu = conc * util
|
|
else:
|
|
ibu = 0
|
|
|
|
return float(ibu)
|
|
|
|
@property
|
|
def trub_volume(self):
|
|
if self.hop.hop_type == 1:
|
|
return self.recipe.equipment.pellet_hop_trub
|
|
elif self.hop.hop_type in [2, 3]:
|
|
return self.recipe.equipment.leaf_hop_trub
|
|
else:
|
|
return 0
|
|
|
|
|
|
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(Recipe, 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(Recipe, on_delete=models.CASCADE)
|
|
yeast = models.ForeignKey('yeast.Strain', on_delete=models.CASCADE)
|
|
|
|
|
|
class Mash(CustomModel):
|
|
name = models.CharField(max_length=50)
|
|
parent = models.ForeignKey(
|
|
'self', on_delete=models.SET_NULL, null=True, blank=True)
|
|
|
|
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
|
|
|
|
|
|
class EquipmentProfile(CustomModel):
|
|
name = models.CharField(max_length=50)
|
|
parent = models.ForeignKey(
|
|
'self', on_delete=models.SET_NULL, null=True, blank=True)
|
|
|
|
# Water managment stuff
|
|
hlt_deadspace = models.DecimalField(
|
|
max_digits=6, decimal_places=2, default=0.25, help_text='liters')
|
|
mt_deadspace = models.DecimalField(
|
|
max_digits=6, decimal_places=2, default=0.25, help_text='liters')
|
|
mt_capacity = models.DecimalField(
|
|
max_digits=6, decimal_places=2, default=10, help_text='liters')
|
|
grain_absorption = models.DecimalField(
|
|
max_digits=6, decimal_places=2,
|
|
default=0.12, help_text='liters/kilogram')
|
|
kettle_deadspace = models.DecimalField(
|
|
max_digits=6, decimal_places=2, default=0.25, help_text='liters')
|
|
kettle_plumbing_loss = models.DecimalField(
|
|
max_digits=6, decimal_places=2, default=0.25, help_text='liters')
|
|
kettle_boil_rate = models.DecimalField(
|
|
max_digits=6, decimal_places=2,
|
|
default=0.5, help_text='liters/hr')
|
|
batch_volume = models.DecimalField(
|
|
max_digits=6, decimal_places=2, default=5.5, help_text='liters')
|
|
leaf_hop_trub = models.DecimalField(
|
|
max_digits=12, decimal_places=10,
|
|
default=0.0083454045, help_text='liters/gram')
|
|
pellet_hop_trub = models.DecimalField(
|
|
max_digits=12, decimal_places=10,
|
|
default=0.0033381620, help_text='liters/gram')
|
|
hops_remain_kettle = models.BooleanField(default=True)
|
|
mash_ratio = models.DecimalField(
|
|
max_digits=6, decimal_places=2,
|
|
default=2.6, help_text='liters/kilogram')
|
|
|
|
# Thermal Properties
|
|
mt_initial_hear = models.DecimalField(
|
|
max_digits=6, decimal_places=4, default=0.74)
|
|
mt_heat_loss_hour = models.DecimalField(
|
|
max_digits=6, decimal_places=4, default=2.0)
|
|
|
|
class Meta:
|
|
db_table_comment = 'Volumes in liters and weights in kg.'
|
|
|
|
class BjcpStyle(models.Model):
|
|
name = models.CharField(max_length=50, blank=True, null=True)
|
|
category = models.CharField(max_length=50, blank=True, null=True)
|
|
category_id = models.CharField(max_length=5, blank=True, null=True)
|
|
style_id = models.CharField(max_length=5, blank=True, null=True)
|
|
category_description = models.TextField(blank=True, null=True)
|
|
overall_impression = models.TextField(blank=True, null=True)
|
|
aroma = models.TextField(blank=True, null=True)
|
|
appearance = models.TextField(blank=True, null=True)
|
|
flavor = models.TextField(blank=True, null=True)
|
|
mouthfeel = models.TextField(blank=True, null=True)
|
|
comments = models.TextField(blank=True, null=True)
|
|
history = models.TextField(blank=True, null=True)
|
|
style_comparison = models.TextField(blank=True, null=True)
|
|
tags = models.TextField(blank=True, null=True)
|
|
original_gravity_minimum_unit = models.CharField(max_length=10, blank=True, null=True)
|
|
original_gravity_minimum_value = models.DecimalField(max_digits=10, decimal_places=5, blank=True, null=True) # max_digits and decimal_places have been guessed, as this database handles decimal fields as float
|
|
original_gravity_maximum_unit = models.CharField(max_length=10, blank=True, null=True)
|
|
original_gravity_maximum_value = models.DecimalField(max_digits=10, decimal_places=5, blank=True, null=True) # max_digits and decimal_places have been guessed, as this database handles decimal fields as float
|
|
international_bitterness_units_minimum_unit = models.CharField(max_length=10, blank=True, null=True)
|
|
international_bitterness_units_minimum_value = models.DecimalField(max_digits=10, decimal_places=5, blank=True, null=True) # max_digits and decimal_places have been guessed, as this database handles decimal fields as float
|
|
international_bitterness_units_maximum_unit = models.CharField(max_length=10, blank=True, null=True)
|
|
international_bitterness_units_maximum_value = models.DecimalField(max_digits=10, decimal_places=5, blank=True, null=True) # max_digits and decimal_places have been guessed, as this database handles decimal fields as float
|
|
final_gravity_minimum_unit = models.CharField(max_length=10, blank=True, null=True)
|
|
final_gravity_minimum_value = models.DecimalField(max_digits=10, decimal_places=5, blank=True, null=True) # max_digits and decimal_places have been guessed, as this database handles decimal fields as float
|
|
final_gravity_maximum_unit = models.CharField(max_length=10, blank=True, null=True)
|
|
final_gravity_maximum_value = models.DecimalField(max_digits=10, decimal_places=5, blank=True, null=True) # max_digits and decimal_places have been guessed, as this database handles decimal fields as float
|
|
alcohol_by_volume_minimum_unit = models.CharField(max_length=10, blank=True, null=True)
|
|
alcohol_by_volume_minimum_value = models.DecimalField(max_digits=10, decimal_places=5, blank=True, null=True) # max_digits and decimal_places have been guessed, as this database handles decimal fields as float
|
|
alcohol_by_volume_maximum_unit = models.CharField(max_length=10, blank=True, null=True)
|
|
alcohol_by_volume_maximum_value = models.DecimalField(max_digits=10, decimal_places=5, blank=True, null=True) # max_digits and decimal_places have been guessed, as this database handles decimal fields as float
|
|
color_minimum_unit = models.CharField(max_length=10, blank=True, null=True)
|
|
color_minimum_value = models.DecimalField(max_digits=10, decimal_places=5, blank=True, null=True) # max_digits and decimal_places have been guessed, as this database handles decimal fields as float
|
|
color_maximum_unit = models.CharField(max_length=10, blank=True, null=True)
|
|
color_maximum_value = models.DecimalField(max_digits=10, decimal_places=5, blank=True, null=True) # max_digits and decimal_places have been guessed, as this database handles decimal fields as float
|
|
ingredients = models.TextField(blank=True, null=True)
|
|
examples = models.TextField(blank=True, null=True)
|
|
style_guide = models.CharField(max_length=20, blank=True, null=True)
|
|
type = models.TextField(blank=True, null=True)
|
|
entry_instructions = models.TextField(blank=True, null=True)
|
|
notes = models.TextField(blank=True, null=True)
|
|
currently_defined_types = models.TextField(blank=True, null=True)
|
|
strength_classifications = models.TextField(blank=True, null=True)
|
|
vital_statistics = models.TextField(blank=True, null=True)
|
|
profile = models.TextField(blank=True, null=True)
|
|
comparison = models.TextField(blank=True, null=True)
|
|
|
|
def __str__(self):
|
|
return '{} {}: {}'.format(
|
|
self.category,
|
|
self.style_id,
|
|
self.name
|
|
)
|
|
|
|
class Meta:
|
|
db_table = 'beer_bjcp'
|