More batch info.

Please enter the commit message for your changes. Lines starting
This commit is contained in:
Chris Giacofei 2024-06-24 16:25:18 -04:00
parent 1986d9fbd0
commit 1d830eb22d
8 changed files with 312 additions and 7 deletions

View File

@ -0,0 +1,23 @@
# Generated by Django 5.0.6 on 2024-06-24 18:56
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('beer', '0012_recipe_fermentables'),
]
operations = [
migrations.AddField(
model_name='batch',
name='first_runnings',
field=models.DecimalField(blank=True, decimal_places=4, max_digits=8, null=True),
),
migrations.AddField(
model_name='equipmentprofile',
name='mash_ratio',
field=models.DecimalField(decimal_places=2, default=2.6, max_digits=6),
),
]

View File

@ -36,6 +36,48 @@ class Batch(CustomModel):
recipe = models.OneToOneField(
'Recipe', on_delete=models.CASCADE, default=1)
first_runnings = models.DecimalField(
max_digits=8, decimal_places=4, 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
# 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
@property
def conversion_efficiency(self):
""" Calculate conversion efficiency of mash."""
strike_volume = (self.recipe.equipment.mash_ratio
* self.recipe.fermentable_weight)
pot_yield = 0
for ferm in self.recipefermentable_set.all():
pot_yield += ferm.extract_potential * ferm.quantity
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 brewfather_url(self):
return '{}/tabs/batches/batch/{}'.format(
@ -213,6 +255,10 @@ class Recipe(CustomModel):
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())
@ -298,6 +344,14 @@ class RecipeFermentable(CustomModel):
fermentable = models.ForeignKey(Fermentable, on_delete=models.CASCADE)
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
@property
def percent(self):
return float(100 * self.quantity / self.recipe.fermentable_weight)
@ -486,6 +540,8 @@ class EquipmentProfile(CustomModel):
pellet_hop_trub = models.DecimalField(
max_digits=6, decimal_places=4, default=0.025)
hops_remain_kettle = models.BooleanField(default=True)
mash_ratio = models.DecimalField(
max_digits=6, decimal_places=2, default=2.6) # 1.25 qt/lb
# Thermal Properties
mt_initial_hear = models.DecimalField(

View File

@ -0,0 +1,65 @@
{% extends "no_jumbo_base.html" %}
{% load mathfilters %}
{% block title %}Yeast Samples{% endblock %}
{% block style %}
.table td.fit,
.table th.fit {
white-space: nowrap;
width: 1%;
}
{% endblock %}
{% block content %}
<!-- Information Header -->
<div class="container">
<h1>{{ batch.brewfather_name }}</h1>
</div> <!-- /container -->
<!-- End Information Header -->
<!-- Main Data Container -->
<div class="container mt-4 mb-4">
<div class="row">
<!-- Tabbed Pages -->
<!-- Nav tabs -->
<ul class="nav nav-tabs nav-fill" role="tablist">
<li class="nav-item">
<a class="nav-link active" data-bs-toggle="tab" href="#planning">Planning</a>
</li>
<li class="nav-item">
<a class="nav-link" data-bs-toggle="tab" href="#brewing">Brewing</a>
</li>
<li class="nav-item">
<a class="nav-link" data-bs-toggle="tab" href="#fermenting">Fermenting</a>
</li>
<li class="nav-item">
<a class="nav-link" data-bs-toggle="tab" href="#complete">Complete</a>
</li>
</ul>
<!-- Tab panes -->
<div class="tab-content">
<div id="planning" class="container tab-pane active"><br>
{% include "beer/planning.html" %}
</div>
<div id="brewing" class="container tab-pane fade"><br>
</div>
<div id="fermenting" class="container tab-pane fade"><br>
</div>
<div id="complete" class="container tab-pane fade"><br>
</div>
</div>
<!-- End Tabbed Pages -->
</div>
</div>
<!-- End Main Data Container -->
{% endblock %}

View File

@ -0,0 +1,58 @@
<div class="row">
<div class="col-lg-6">
<div class="d-flex justify-content-between bg-dark text-white ms-2 my-1">
Recipe Ingredients
</div>
<div class="container-fluid">
<table class="table table-sm table-hover">
<tbody>
{% for f in batch.recipe.recipefermentable_set.all %}
<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>
</div>
</div>
<div class="col-lg-6">
<div class="d-flex justify-content-between bg-dark text-white ms-2 my-1">
Batch Recipe
</div>
<div class="container-fluid">
<table class="table table-sm table-hover">
<tbody>
<tr onclick="window.location='recipe/{{ batch.recipe.id }}';">
<td><b>{{ batch.recipe.name }}</b><br>
<span class="text-muted"><b>OG:</b> {{ batch.recipe.original_sg }} <b>IBU:</b> {{ batch.recipe.ibu|floatformat:0 }}</span>
</td>
</tr>
</tbody>
</table>
</div>
<div class="d-flex justify-content-between bg-dark text-white ms-2 my-1">
Yeast
</div>
<div class="container-fluid">
<table class="table table-sm table-hover">
<tbody>
{% for y in batch.recipe.recipeyeast_set.all %}
<tr>
<td>{{ y.yeast.manufacturer.name }} {{ y.yeast.name }}<br>
<span class="text-muted">{{ y.yeast.long_name }}</span>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>

View File

@ -1,12 +1,14 @@
from django.urls import path
from django.urls import path, re_path
from django.conf import settings
from django.conf.urls.static import static
from .views import home, view_recipe, update_ferm, update_hop
from .views import home, view_recipe, view_batch, update_ferm, update_hop
urlpatterns = [
path('', home, name='home'),
path('recipes/<int:recipe_id>/', view_recipe, name='recipe'),
path('batches/<int:batch_id>/recipe/<int:recipe_id>', view_recipe),
path('batches/<int:batch_id>/', view_batch, name='batch'),
path('ingredients/fermentables/<int:ferm_id>',
update_ferm, name='update_fermentable'),
path('ingredients/hops/<int:hop_id>', update_hop, name='update_hop'),

View File

@ -41,15 +41,18 @@ def home(request):
return render(request, 'beer/home.html', context)
def view_recipe(request, recipe_id):
recipe = get_object_or_404(Recipe, pk=recipe_id)
def view_recipe(request, recipe_id, batch_id=None):
context = {
'recipe': recipe,
'recipe': get_object_or_404(Recipe, pk=recipe_id),
}
return render(request, 'beer/recipe.html', context)
def view_batch(request, batch_id):
context = {
'batch': get_object_or_404(Batch, pk=batch_id)
}
return render(request, 'beer/batch.html', context)
def update_ferm(request, ferm_id):
fermentable = get_object_or_404(Fermentable, pk=ferm_id)

2
run.sh
View File

@ -1,6 +1,6 @@
#! /usr/bin/env bash
source .env/bin/activate
source .env/bin/activate || source .env/Scripts/activate
find -name "*.py" -not -name "manage.py" -not -path "./.env/*" -not -path "*/migrations/*" -exec python -m flake8 {} \;
pip install --upgrade -r requirements.txt > /dev/null

View File

@ -0,0 +1,98 @@
<html lang="en">
<head>
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{% block title %}{% endblock %}</title>
<!-- CSS only -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
{% load static %}
<link href="{% static 'fontawesomefree/css/fontawesome.css' %}" rel="stylesheet" type="text/css">
<link href="{% static 'fontawesomefree/css/solid.css' %}" rel="stylesheet" type="text/css">
<!-- JS, Popper.js, and jQuery -->
<!-- <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.min.js" -->
<!-- integrity="sha384-0pUGZvbkm6XF6gxjEnlmuGrJXVbNuzT9qBBavbLwCsOGabYfZo0T0to5eqruptLy" -->
<!-- crossorigin="anonymous"></script> -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.7.1/jquery.min.js"></script>
{% block script %}{% endblock %}
<style>
{% block style %}{% endblock %}
body {
margin-top: 10px;
}
label {
margin-right: 10px;
}
.table-borderless > tbody > tr > td,
.table-borderless > tbody > tr > th,
.table-borderless > tfoot > tr > td,
.table-borderless > tfoot > tr > th,
.table-borderless > thead > tr > td,
.table-borderless > thead > tr > th {
border: none;
background-color: rgba(0,0,0, 0.0) !important;
}
</style>
</head>
<body>
<div class="container">
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container-fluid">
<a class="navbar-brand" href="/">Damn Yankee Brewing</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent"
aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav mr-auto">
<li class="nav-item active">
<a class="nav-link" href="/">Home</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{% url 'yeast:batches' %}">Batches</a>
</li>
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" id="dropdown01" data-bs-toggle="dropdown" aria-haspopup="true"
aria-expanded="false">Brewery Operations</a>
<div class="dropdown-menu" aria-labelledby="dropdown01">
<a class="dropdown-item" href="/yeast/">Yeast</a>
<a class="dropdown-item" href="/equipment/">Equipment</a>
<a class="dropdown-item" href="/beer/">Beer</a>
</div>
</li>
</ul>
{% if user.is_authenticated %}
<div class="dropdown ms-auto">
<button class="btn btn-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
{{ user.username }}
</button>
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="#" onclick="document.getElementById('LougoutForm').submit();">Log Out</a></li>
<li><a class="dropdown-item" href="{% url 'password_change' %}">Change Password</a></li>
{% if user.is_superuser %}
<div class="dropdown-divider"></div>
<li><a class="dropdown-item" href="{% url 'admin:index' %}">Admin</a></li>
{% endif %}
</ul>
</div>
{% else %}
<a href="{% url 'login' %}">Log In</a>
{% endif %}
<form id="LougoutForm" action="{% url 'logout' %}" method="POST" class="form-inline my-2 my-lg-0">
{% csrf_token %}
</form>
</div>
</div>
</nav>
</div>
<main role="main">
{% block content %}{% endblock %}
</main>
</body>
{% block endscript %}{% endblock %}
</html>