Compare commits
3 Commits
1d830eb22d
...
90bf91c3ed
Author | SHA1 | Date | |
---|---|---|---|
90bf91c3ed | |||
570ede627f | |||
07c881bdfe |
@ -7,6 +7,7 @@ from beer.models import Batch, Recipe, Mash, MashStep, \
|
||||
from yeast.models import Yeast
|
||||
|
||||
from config.extras import BREWFATHER_APP_ROOT
|
||||
from beer.extras import plato_sg
|
||||
|
||||
|
||||
class SampleInline(admin.TabularInline):
|
||||
@ -36,7 +37,7 @@ class StrainInline(admin.TabularInline):
|
||||
|
||||
@admin.register(Recipe)
|
||||
class RecipeAdmin(admin.ModelAdmin):
|
||||
list_display = ['name']
|
||||
list_display = ['name', 'total_extract_kg']
|
||||
inlines = [
|
||||
FermentableInline,
|
||||
HopInline,
|
||||
@ -47,11 +48,40 @@ class RecipeAdmin(admin.ModelAdmin):
|
||||
|
||||
@admin.register(Batch)
|
||||
class BeerBatchAdmin(admin.ModelAdmin):
|
||||
list_display = ['brewfather_id', 'batch_url']
|
||||
list_display = [
|
||||
'brewfather_id',
|
||||
'batch_url',
|
||||
'brewhouse_efficiency',
|
||||
'conversion_efficiency',
|
||||
]
|
||||
inlines = [
|
||||
SampleInline,
|
||||
]
|
||||
|
||||
fieldsets = [
|
||||
[None, {
|
||||
'fields': [
|
||||
('brewfather_id', 'brewfather_num', 'brewfather_name'),
|
||||
'recipe',
|
||||
]
|
||||
}],
|
||||
['Mash', {
|
||||
'fields': ['first_runnings', 'mash_ph'],
|
||||
}],
|
||||
['Boil', {
|
||||
'fields': [
|
||||
('pre_boil_vol', 'pre_boil_sg'),
|
||||
('post_boil_vol', 'post_boil_sg'),
|
||||
],
|
||||
}],
|
||||
['Ferment', {
|
||||
'fields': [
|
||||
('fermenter_topup_vol', 'fermenter_vol'),
|
||||
('original_sg', 'final_sg'),
|
||||
],
|
||||
}],
|
||||
]
|
||||
|
||||
def batch_url(self, obj):
|
||||
url_string = ('<a href="{root}/tabs/batches/batch/{batch_id}">'
|
||||
'Brewfather Batch ID: {batch_id}</a>')
|
||||
|
@ -9,6 +9,7 @@ RECIPE_URL = 'https://api.brewfather.app/v2/recipes'
|
||||
BATCH_URL = 'https://api.brewfather.app/v2/batches'
|
||||
PULL_LIMIT = 50
|
||||
|
||||
|
||||
BREWFATHER_CONVERT_LOOKUP = { # local_name: brewfather_name
|
||||
'all': {
|
||||
'name': 'name',
|
||||
@ -86,3 +87,77 @@ def get_batches(api_user, api_key, batch=''):
|
||||
data = data + get_batches(batch=last_id)
|
||||
|
||||
return data
|
||||
|
||||
|
||||
def sg_plato(sg):
|
||||
"""Convert specific gravtiy to °P."""
|
||||
sg = float(sg)
|
||||
return (-1*616.868) + (1111.14*sg) - (630.272*sg**2) + (135.997*sg**3)
|
||||
|
||||
|
||||
def plato_sg(plato):
|
||||
"""Convert °P to specific gravtiy."""
|
||||
return 1 + (plato / (258.6 - (plato/258.2) * 227.1))
|
||||
|
||||
|
||||
def kg_extract(v, sg):
|
||||
"""Calculate kg of extract based on volume and SG."""
|
||||
return float(v) * float(sg) * sg_plato(sg)/100
|
||||
|
||||
|
||||
def convert(value, start_unit, end_unit):
|
||||
"""Convert units."""
|
||||
|
||||
family_dict = unit_family(start_unit)
|
||||
|
||||
if family_dict:
|
||||
start = [val for key, val in family_dict.items()
|
||||
if start_unit in key.split(',')][0]
|
||||
|
||||
end = [val for key, val in family_dict.items()
|
||||
if end_unit in key.split(',')][0]
|
||||
|
||||
return float(value) * end / start
|
||||
|
||||
|
||||
def unit_family(unit_name):
|
||||
"""Find unit family base on unit name."""
|
||||
|
||||
unit_lookup = [
|
||||
{'name': 'length',
|
||||
'units': {
|
||||
'm,meter,meters': 1.0,
|
||||
'cm,centimeter,centimeters': 100,
|
||||
'mm,millimeter,millimeters': 1000,
|
||||
'in,inch,inches': 39.3701,
|
||||
'yd,yard,yards': 1.09361,
|
||||
'ft,foot,feet': 3.28084,
|
||||
'mi,mile,miles': 0.000621371,
|
||||
'km,killometer,killometers': .001,
|
||||
}},
|
||||
{'name': 'mass',
|
||||
'units': {
|
||||
'kg,kilogram,kilograms': 1.0,
|
||||
'g,gram,grams': 1000,
|
||||
'lb,pound,pounds': 2.20462,
|
||||
'oz,ounce,ounces': 35.274,
|
||||
'st,stone': 0.157473,
|
||||
}},
|
||||
{'name': 'volume',
|
||||
'units': {
|
||||
'l,liter,liters': 1.0,
|
||||
'ml,milliliter,milliliters': 1000,
|
||||
'floz,fluid ounce,fluid ounces': 33.814,
|
||||
'cup,cups': 4.22675,
|
||||
'qt,quart,quarts': 1.05669,
|
||||
'gal,gallon,gallons': 0.264172,
|
||||
'ft^3,cubic foot,cubic feet': 0.0353147,
|
||||
'pt,pint,pints': 4.22675/2,
|
||||
'tsp,teaspoon,teaspoons': 202.884,
|
||||
'tbsp,tablespoon,tablespoons': 202.884/3,
|
||||
}},
|
||||
]
|
||||
|
||||
for family in unit_lookup:
|
||||
if [key for key in family['units'] if unit_name in key.split(',')]:
|
||||
return family['units']
|
||||
|
@ -0,0 +1,33 @@
|
||||
# Generated by Django 5.0.6 on 2024-06-25 15:42
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('beer', '0013_batch_first_runnings_equipmentprofile_mash_ratio'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='batch',
|
||||
name='post_boil_sg',
|
||||
field=models.DecimalField(blank=True, decimal_places=4, max_digits=6, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='batch',
|
||||
name='post_boil_vol',
|
||||
field=models.DecimalField(blank=True, decimal_places=4, max_digits=8, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='fermentable',
|
||||
name='moisture',
|
||||
field=models.DecimalField(blank=True, decimal_places=4, default=0.04, max_digits=6, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='fermentable',
|
||||
name='potential',
|
||||
field=models.DecimalField(decimal_places=4, default=0.8, max_digits=6),
|
||||
),
|
||||
]
|
@ -0,0 +1,53 @@
|
||||
# Generated by Django 5.0.6 on 2024-06-26 14:11
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('beer', '0014_batch_post_boil_sg_batch_post_boil_vol_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='batch',
|
||||
name='fermenter_topup_vol',
|
||||
field=models.DecimalField(blank=True, decimal_places=4, max_digits=8, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='batch',
|
||||
name='fermenter_vol',
|
||||
field=models.DecimalField(blank=True, decimal_places=4, max_digits=8, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='batch',
|
||||
name='final_sg',
|
||||
field=models.DecimalField(blank=True, decimal_places=4, max_digits=5, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='batch',
|
||||
name='mash_ph',
|
||||
field=models.DecimalField(blank=True, decimal_places=3, max_digits=4, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='batch',
|
||||
name='original_sg',
|
||||
field=models.DecimalField(blank=True, decimal_places=4, max_digits=5, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='batch',
|
||||
name='pre_boil_sg',
|
||||
field=models.DecimalField(blank=True, decimal_places=4, max_digits=5, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='batch',
|
||||
name='pre_boil_vol',
|
||||
field=models.DecimalField(blank=True, decimal_places=4, max_digits=8, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='batch',
|
||||
name='post_boil_sg',
|
||||
field=models.DecimalField(blank=True, decimal_places=4, max_digits=5, null=True),
|
||||
),
|
||||
]
|
169
beer/models.py
169
beer/models.py
@ -5,6 +5,7 @@ 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 logging
|
||||
@ -36,47 +37,70 @@ class Batch(CustomModel):
|
||||
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)
|
||||
|
||||
# Batch measurements to add:
|
||||
# - Mash pH
|
||||
# - First Runnings gravity (include lookup table fo rmash thickness)
|
||||
# - Boil Vol
|
||||
# - Pre-Boil Gravity
|
||||
# - Post-Boil Vol
|
||||
# - Post-Boil Gravity
|
||||
# - Original Gravity
|
||||
# - Final Gravity
|
||||
# - Fermenter Top-Up
|
||||
# - Fermenter Vol
|
||||
## 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: (https://braukaiser.com/wiki/index.php/Troubleshooting_Brewhouse_Efficiency)
|
||||
# - Conversion Efficiency
|
||||
# - Mash Efficiency
|
||||
# - Brewhouse Efficiency
|
||||
# kettle extract weight in kg = volume in liter * SG * Plato / 100
|
||||
# brewhouse efficiency in % = 100% * kettle extract weight in kg / extract in grist in kg
|
||||
# - ABV
|
||||
# - Attenuation
|
||||
# - Actual Boil-Off Rate
|
||||
# - 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."""
|
||||
strike_volume = (self.recipe.equipment.mash_ratio
|
||||
* self.recipe.fermentable_weight)
|
||||
if self.first_runnings is None or self.recipe.fermentable_weight_kg == 0:
|
||||
return '-'
|
||||
|
||||
pot_yield = 0
|
||||
for ferm in self.recipefermentable_set.all():
|
||||
pot_yield += ferm.extract_potential * ferm.quantity
|
||||
return round((sg_plato(self.first_runnings)/self.recipe.fw_max)
|
||||
* (100-self.recipe.fw_max) / (100-sg_plato(self.first_runnings))
|
||||
, 4)
|
||||
|
||||
pot_yield = pot_yield / self.recipe.fermentable_weight
|
||||
fw_max = pot_yield / (strike_volume+pot_yield)
|
||||
|
||||
return (100 * (self.first_runnings/fw_max)
|
||||
* (100-fw_max) / (100-self.first_runnings))
|
||||
@property
|
||||
def boil_off_calcualted(self):
|
||||
return float(self.pre_boil_vol - self.post_boil_vol) * .96
|
||||
|
||||
@property
|
||||
def brewfather_url(self):
|
||||
@ -157,16 +181,41 @@ class Recipe(CustomModel):
|
||||
verbose_name_plural = 'Recipes'
|
||||
|
||||
@property
|
||||
def fermentable_weight(self):
|
||||
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 fermentable_weight_kg(self):
|
||||
"""Weight of all fermentables attached to recipe."""
|
||||
aggregate = self.recipefermentable_set.all().aggregate(Sum('quantity'))
|
||||
return aggregate['quantity__sum']
|
||||
|
||||
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 hop_weight(self):
|
||||
"""Weight of all fermentables attached to recipe."""
|
||||
aggregate = self.recipehop_set.all().aggregate(Sum('quantity'))
|
||||
return aggregate['quantity__sum']
|
||||
return float(aggregate['quantity__sum']) * 0.0352739619
|
||||
|
||||
@property
|
||||
def final_volume(self):
|
||||
@ -179,44 +228,43 @@ class Recipe(CustomModel):
|
||||
@property
|
||||
def sugar_yield(self):
|
||||
"""Return point yield of all non-mashed ingredients."""
|
||||
ferm_yield = 0
|
||||
|
||||
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.ferm_yield
|
||||
ferm_yield += f.extract_weight_kg
|
||||
|
||||
return float(ferm_yield)
|
||||
|
||||
@property
|
||||
def mash_yield(self):
|
||||
"""Return point yield of all mashed ingredients."""
|
||||
mash_yield = 0
|
||||
|
||||
ferms = self.recipefermentable_set.all().select_related('fermentable')
|
||||
# Is not sugar (3)
|
||||
mashed = ferms.filter(~Q(fermentable__fermentable_type=3))
|
||||
|
||||
mash_yield = 0
|
||||
for f in mashed:
|
||||
mash_yield += f.ferm_yield
|
||||
mash_yield += f.extract_weight_kg * float(self.efficiency/100)
|
||||
|
||||
return float(mash_yield)
|
||||
|
||||
@property
|
||||
def original_sg(self):
|
||||
"""Return original gravity."""
|
||||
total_yield = self.sugar_yield + self.mash_yield
|
||||
gravity_points = total_yield/self.final_volume
|
||||
return round(1 + gravity_points/1000, 3)
|
||||
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_yield = self.sugar_yield + self.mash_yield
|
||||
total_extract = self.sugar_yield + self.mash_yield
|
||||
total_water = self.final_volume+self.boil_off_gph
|
||||
gravity_points = total_yield/total_water
|
||||
return round(1 + gravity_points/1000, 3)
|
||||
plato = 100 * total_extract / (total_water + total_extract)
|
||||
return round(plato_sg(plato), 3)
|
||||
|
||||
@property
|
||||
def hop_water_loss(self):
|
||||
@ -320,7 +368,8 @@ class Fermentable(CustomIngredient):
|
||||
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)
|
||||
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(
|
||||
@ -330,11 +379,15 @@ class Fermentable(CustomIngredient):
|
||||
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)
|
||||
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
|
||||
|
||||
@ -345,32 +398,29 @@ class RecipeFermentable(CustomModel):
|
||||
quantity = models.DecimalField(max_digits=6, decimal_places=4)
|
||||
|
||||
# Properties Needed:
|
||||
# - Extract Potential (1-moisture percent as decimal) * potential
|
||||
# - The weight of extract in the grist
|
||||
# extract in grist in kg = weight of grist in kg * extract potential
|
||||
|
||||
@property
|
||||
def extract_potential(self):
|
||||
return .8 * .96
|
||||
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)
|
||||
return float(100 * self.quantity / self.recipe.fermentable_weight_kg)
|
||||
|
||||
@property
|
||||
def lovibond_contributed(self):
|
||||
srm_calc = (float(self.fermentable.lovibond)
|
||||
* float(self.quantity) / self.recipe.final_volume)
|
||||
* convert(self.quantity, 'kg', 'lb')
|
||||
/ convert(self.recipe.final_volume, 'l', 'gal'))
|
||||
|
||||
return round(srm_calc, 1)
|
||||
|
||||
@property
|
||||
def ferm_yield(self):
|
||||
potential_yield = self.quantity * (self.fermentable.potential-1) * 1000
|
||||
|
||||
if self.fermentable.fermentable_type == 3:
|
||||
return potential_yield
|
||||
else:
|
||||
return potential_yield * (self.recipe.efficiency/100)
|
||||
|
||||
|
||||
class Hop(CustomIngredient):
|
||||
uses = {
|
||||
@ -409,6 +459,11 @@ class RecipeHop(CustomModel):
|
||||
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 = {
|
||||
@ -424,8 +479,8 @@ class RecipeHop(CustomModel):
|
||||
+ (self.recipe.original_sg-1)) / 2)
|
||||
|
||||
if self.use == 1:
|
||||
conc = (float((self.hop.alpha/100) * self.quantity)
|
||||
* 7490/self.recipe.final_volume)
|
||||
conc = (float((self.hop.alpha/100)) * float(self.quantity)*0.0352739619
|
||||
* 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
|
||||
|
@ -88,8 +88,8 @@ font-size: .8em;
|
||||
|
||||
<caption>
|
||||
<p class="text-end smaller">
|
||||
Pre-Boil Gravity: <b>{{ recipe.pre_boil_sg }}</b><br>
|
||||
Original Gravity: <b>{{ recipe.original_sg }}</b><br>
|
||||
Pre-Boil Gravity: <b>{{ recipe.pre_boil_sg|floatformat:3 }}</b><br>
|
||||
Original Gravity: <b>{{ recipe.original_sg|floatformat:3 }}</b><br>
|
||||
Color: <b>{{ recipe.srm|floatformat:0 }} SRM</b>
|
||||
</p>
|
||||
</caption>
|
||||
@ -97,9 +97,9 @@ font-size: .8em;
|
||||
<tbody>
|
||||
{% for f in recipe.recipefermentable_set.all %}
|
||||
<tr onclick="window.location='{% url 'beer:update_fermentable' f.fermentable.id %}';">
|
||||
<td>{{ f.quantity|floatformat:2 }} lb</td>
|
||||
<td>{{ f.quantity_display|floatformat:2 }} lb</td>
|
||||
<td>{{ f.fermentable.name }}<br>
|
||||
<span class="text-muted">{{ f.fermentable.get_fermentable_type_display }} {{ f.srm }} SRM</span>
|
||||
<span class="text-muted">{{ f.fermentable.get_fermentable_type_display }} {{ f.lovibond_contributed }} L</span>
|
||||
</td>
|
||||
<td>{{ f.percent|floatformat:1 }} %</td>
|
||||
</tr>
|
||||
@ -132,7 +132,7 @@ font-size: .8em;
|
||||
<tbody>
|
||||
{% for h in recipe.recipehop_set.all %}
|
||||
<tr onclick="window.location='{% url 'beer:update_hop' h.hop.id %}';">
|
||||
<td>{{ h.quantity|floatformat:2 }} oz</td>
|
||||
<td>{{ h.quantity_display|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>
|
||||
|
Loading…
Reference in New Issue
Block a user