Add parent field to all recipe relations.

Need to make a copy of master item when attaching
to a recipe.
Also add some more calculations.
This commit is contained in:
Chris Giacofei 2024-06-18 20:41:58 -04:00
parent 34470cbea7
commit ea4340c8ea
5 changed files with 237 additions and 16 deletions

View File

@ -0,0 +1,77 @@
# Generated by Django 5.0.6 on 2024-06-18 23:23
import django.db.models.deletion
import django.utils.timezone
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('beer', '0006_batchrecipe_batch_size_alter_batchrecipe_efficiency'),
]
operations = [
migrations.AddField(
model_name='fermentable',
name='parent',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='beer.fermentable'),
),
migrations.AddField(
model_name='hop',
name='parent',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='beer.hop'),
),
migrations.AddField(
model_name='mash',
name='parent',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='beer.mash'),
),
migrations.AddField(
model_name='misc',
name='parent',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='beer.misc'),
),
migrations.AlterField(
model_name='fermentable',
name='fermentable_type',
field=models.IntegerField(choices=[(1, 'Grain'), (2, 'Adjunct'), (3, 'Sugar')], default=1),
),
migrations.CreateModel(
name='EquipmentProfile',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_date', models.DateTimeField(default=django.utils.timezone.now)),
('name', models.CharField(max_length=50)),
('hlt_deadspace', models.DecimalField(decimal_places=2, default=0.25, max_digits=6)),
('mt_deadspace', models.DecimalField(decimal_places=2, default=0.25, max_digits=6)),
('mt_capacity', models.DecimalField(decimal_places=2, default=10, max_digits=6)),
('grain_absorption', models.DecimalField(decimal_places=2, default=0.12, max_digits=6)),
('kettle_deadspace', models.DecimalField(decimal_places=2, default=0.25, max_digits=6)),
('kettle_plumbing_loss', models.DecimalField(decimal_places=2, default=0.25, max_digits=6)),
('kettle_boil_rate', models.DecimalField(decimal_places=2, default=0.5, max_digits=6)),
('batch_volume', models.DecimalField(decimal_places=2, default=5.5, max_digits=6)),
('leaf_hop_trub', models.DecimalField(decimal_places=2, default=0.0625, max_digits=6)),
('pellet_hop_trub', models.DecimalField(decimal_places=2, default=0.025, max_digits=6)),
('hops_remain_kettle', models.BooleanField(default=True)),
('mt_initial_hear', models.DecimalField(decimal_places=2, default=0.74, max_digits=6)),
('mt_heat_loss_hour', models.DecimalField(decimal_places=2, default=2.0, max_digits=6)),
('parent', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='beer.equipmentprofile')),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='RecipeEquipment',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_date', models.DateTimeField(default=django.utils.timezone.now)),
('equipment', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='beer.equipmentprofile')),
('recipe', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='beer.batchrecipe')),
],
options={
'abstract': False,
},
),
]

View File

@ -0,0 +1,39 @@
# Generated by Django 5.0.6 on 2024-06-18 23:37
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('beer', '0007_fermentable_parent_hop_parent_mash_parent_and_more'),
]
operations = [
migrations.AlterField(
model_name='equipmentprofile',
name='parent',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='beer.equipmentprofile'),
),
migrations.AlterField(
model_name='fermentable',
name='parent',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='beer.fermentable'),
),
migrations.AlterField(
model_name='hop',
name='parent',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='beer.hop'),
),
migrations.AlterField(
model_name='mash',
name='parent',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='beer.mash'),
),
migrations.AlterField(
model_name='misc',
name='parent',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='beer.misc'),
),
]

View File

@ -0,0 +1,42 @@
# Generated by Django 5.0.6 on 2024-06-19 00:13
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('beer', '0008_alter_equipmentprofile_parent_and_more'),
]
operations = [
migrations.AddField(
model_name='batchrecipe',
name='equipment',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='beer.equipmentprofile'),
),
migrations.AlterField(
model_name='equipmentprofile',
name='leaf_hop_trub',
field=models.DecimalField(decimal_places=4, default=0.0625, max_digits=6),
),
migrations.AlterField(
model_name='equipmentprofile',
name='mt_heat_loss_hour',
field=models.DecimalField(decimal_places=4, default=2.0, max_digits=6),
),
migrations.AlterField(
model_name='equipmentprofile',
name='mt_initial_hear',
field=models.DecimalField(decimal_places=4, default=0.74, max_digits=6),
),
migrations.AlterField(
model_name='equipmentprofile',
name='pellet_hop_trub',
field=models.DecimalField(decimal_places=4, default=0.025, max_digits=6),
),
migrations.DeleteModel(
name='RecipeEquipment',
),
]

View File

@ -69,6 +69,7 @@ class CustomIngredient(CustomModel):
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
@ -78,7 +79,8 @@ class BatchRecipe(CustomModel):
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)
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)
@ -86,6 +88,10 @@ class BatchRecipe(CustomModel):
def fermentables(self):
return [x for x in list(self.recipefermentable_set.all())]
@property
def fermentable_weight(self):
return sum([x.quantity for x in self.fermentables])
@property
def hops(self):
return [x for x in list(self.recipehop_set.all())]
@ -122,7 +128,7 @@ class BatchRecipe(CustomModel):
@property
def pre_boil_sg(self):
return self.ferm_yield / (self.final_volume + self.boil_off_gph)
return round(1 + self.ferm_yield / (self.final_volume + self.boil_off_gph) / 1000, 3)
@property
def hop_water_loss(self):
@ -135,20 +141,20 @@ class BatchRecipe(CustomModel):
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
#TODO: Else hops in bag or removed
return 0
@property
def kettle_hose_loss(self):
return .25 # TODO
return float(self.equipment.kettle_plumbing_loss)
@property
def kettle_dead_space(self):
return .25 # TODO
return float(self.equipment.kettle_deadspace)
@property
def boil_off_gph(self):
return .8 # TODO
return float(self.equipment.kettle_boil_rate)
@property
def ibu_tinseth(self):
@ -172,6 +178,14 @@ class BatchRecipe(CustomModel):
return '#{}'.format(SRM_HEX[int(self.srm)])
@property
def bu_gu(self):
gu = (self.original_sg - 1) * 1000
return self.ibu_tinseth / gu
@property
def rbr(self):
return self.bu_gu * (1 + (.75 - 0.7655)) # .75 needs to be calculated number...
def __str__(self):
return self.name
@ -211,6 +225,10 @@ class RecipeFermentable(CustomModel):
fermentable = models.ForeignKey(Fermentable, on_delete=models.CASCADE)
quantity = models.DecimalField(max_digits=6, decimal_places=4)
@property
def percent(self):
return 100 * float(self.quantity) / float(self.recipe.fermentable_weight)
@property
def srm(self):
return round(float(self.fermentable.lovibond) * float(self.quantity) / self.recipe.final_volume, 1)
@ -262,7 +280,8 @@ class RecipeHop(CustomModel):
ibu = 0
average_wort_sg = (self.recipe.pre_boil_sg/1000 + (self.recipe.original_sg-1)) / 2
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) * 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)
@ -312,6 +331,7 @@ class RecipeYeast(CustomModel):
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
@ -331,3 +351,24 @@ class MashStep(CustomModel):
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)
mt_deadspace = models.DecimalField(max_digits=6, decimal_places=2, default=0.25)
mt_capacity = models.DecimalField(max_digits=6, decimal_places=2, default=10)
grain_absorption = models.DecimalField(max_digits=6, decimal_places=2, default=0.12) # gal/lb
kettle_deadspace = models.DecimalField(max_digits=6, decimal_places=2, default=0.25)
kettle_plumbing_loss = models.DecimalField(max_digits=6, decimal_places=2, default=0.25)
kettle_boil_rate = models.DecimalField(max_digits=6, decimal_places=2, default=0.5) # gal/hr
batch_volume = models.DecimalField(max_digits=6, decimal_places=2, default=5.5)
leaf_hop_trub = models.DecimalField(max_digits=6, decimal_places=4, default=0.0625)
pellet_hop_trub = models.DecimalField(max_digits=6, decimal_places=4, default=0.025)
hops_remain_kettle = models.BooleanField(default=True)
# 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)

View File

@ -32,12 +32,23 @@ input, label {
<dd>All Grain</dd>
</dl>
</div>
<div class="col-lg-3" style="border:1px solid #cecece; margin-right:.5em; margin-left:.5em">
<div>Equipment Selection</div>
<div class="col-lg-4" style="border:1px solid #cecece; margin-right:.5em; margin-left:.5em">
<div class="text-truncate">Equipment: {{ recipe.equipment.name }}</div>
<!--
<b>Batch Size:</b> {{ recipe.batch_size }} gal <b>Actual Volume:</b> {{ recipe.final_volume|floatformat:2 }}<br>
<b>Mash Efficiency:</b> {{ recipe.efficiency|floatformat:2 }} %
-->
<dl class="row">
<dt>Batch Size</dt>
<dd>{{ recipe.batch_size }} gal</dd>
<dt>Actual Volume</dt>
<dd>{{ recipe.final_volume|floatformat:2 }} gal</dd>
<dt>Mash Efficiency</dt>
<dd>{{ recipe.efficiency|floatformat:2 }} %</dd>
</dl>
</div>
<div class="col-lg-3" style="border:1px solid #cecece; margin-right:.5em; margin-left:.5em">
<div class="col-xl-3" style="border:1px solid #cecece; margin-right:.5em; margin-left:.5em">
Style Data
</div>
</div>
@ -57,14 +68,20 @@ input, label {
<table class="table table-sm ">
<tbody>
{% for f in recipe.recipefermentable_set.all %}
<tr><td>{{ f.quantity|floatformat:2 }} lb</td><td>{{ f.fermentable.name }}<br>{{ f.srm }}</td></tr>
<tr>
<td>{{ f.quantity|floatformat:2 }} lb</td>
<td>{{ f.fermentable.name }}<br>
<span class="text-muted">{{ f.fermentable.get_fermentable_type_display }} {{ f.srm }} SRM</span>
</td>
<td>{{ f.percent|floatformat:1 }} %</td>
</tr>
{% endfor %}
</tbody>
</table>
<p class="text-end small">
Pre-Boil Gravity: <b>1.XXX</b><br>
Pre-Boil Gravity: <b>{{ recipe.pre_boil_sg }}</b><br>
Original Gravity: <b>{{ recipe.original_sg }}</b><br>
Color: <b>XXX SRM</b>
Color: <b>{{ recipe.srm|floatformat:0 }} SRM</b>
</p>
</div>
</div>
@ -77,14 +94,19 @@ input, label {
<table class="table table-sm ">
<tbody>
{% for h in recipe.recipehop_set.all %}
<tr><td>{{ h.quantity|floatformat:2 }} oz</td><td>{{ h.hop.name }} {{ h.hop.alpha|floatformat:1 }} %<br>{{ h.ibu_tinseth|floatformat:1 }} IBU</td><td>{{ h.time }} min</td></tr>
<tr>
<td>{{ h.quantity|floatformat:2 }} oz</td>
<td>{{ h.hop.name }} {{ h.hop.alpha|floatformat:1 }} %<br>
<span class="text-muted">{{ h.hop.get_hop_type_display }} {{ h.ibu_tinseth|floatformat:1 }} IBU</span>
</td>
<td>{{ h.time }} min</td></tr>
{% endfor %}
</tbody>
</table>
<p class="text-end small">
Total IBU: <b>{{ recipe.ibu_tinseth|floatformat:1 }}</b><br>
BU/GU: <b>0.XX</b><br>
RBR: <b>0.XX</b>
BU/GU: <b>{{ recipe.bu_gu|floatformat:2 }}</b><br>
RBR: <b>{{ recipe.rbr|floatformat:2 }}</b>
</p>
</div>
</div>