More batch info.
Please enter the commit message for your changes. Lines starting
This commit is contained in:
parent
1986d9fbd0
commit
1d830eb22d
@ -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),
|
||||
),
|
||||
]
|
@ -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(
|
||||
|
65
beer/templates/beer/batch.html
Normal file
65
beer/templates/beer/batch.html
Normal 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 %}
|
58
beer/templates/beer/planning.html
Normal file
58
beer/templates/beer/planning.html
Normal 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>
|
@ -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'),
|
||||
|
@ -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
2
run.sh
@ -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
|
||||
|
98
templates/no_jumbo_base.html
Normal file
98
templates/no_jumbo_base.html
Normal 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>
|
Loading…
Reference in New Issue
Block a user