Create basic recipe stuff.

This commit is contained in:
Chris Giacofei 2024-06-17 13:56:13 -04:00
parent d31d53ca9b
commit e0ac1f96db
4 changed files with 444 additions and 5 deletions

View File

@ -3,8 +3,8 @@ from django.urls import reverse
from django.utils.html import format_html from django.utils.html import format_html
from django.apps import apps from django.apps import apps
from beer.models import Batch, BatchRecipe, BatchRecipe from beer.models import Batch, BatchRecipe, Mash, MashStep, RecipeFermentable, RecipeHop, RecipeMisc, RecipeYeast
from yeast.models import Yeast from yeast.models import Yeast, Strain
from config.extras import BREWFATHER_APP_ROOT from config.extras import BREWFATHER_APP_ROOT
@ -12,9 +12,31 @@ class SampleInline(admin.TabularInline):
model = Yeast model = Yeast
extra = 0 extra = 0
class FermentableInline(admin.TabularInline):
model = RecipeFermentable
extra = 1
class HopInline(admin.TabularInline):
model = RecipeHop
extra = 1
class MiscInline(admin.TabularInline):
model = RecipeMisc
extra = 1
class StrainInline(admin.TabularInline):
model = RecipeYeast
extra = 1
@admin.register(BatchRecipe) @admin.register(BatchRecipe)
class BatchRecipeAdmin(admin.ModelAdmin): class BatchRecipeAdmin(admin.ModelAdmin):
list_display = ['name'] list_display = ['name']
inlines = [
FermentableInline,
HopInline,
MiscInline,
StrainInline
]
@admin.register(Batch) @admin.register(Batch)
class BeerBatchAdmin(admin.ModelAdmin): class BeerBatchAdmin(admin.ModelAdmin):
@ -30,6 +52,17 @@ class BeerBatchAdmin(admin.ModelAdmin):
return format_html("<a href='{root}/tabs/batches/batch/{batch_id}'>Brewfather App: {batch_id}</a>", batch_id=bf_id, root=BREWFATHER_APP_ROOT) return format_html("<a href='{root}/tabs/batches/batch/{batch_id}'>Brewfather App: {batch_id}</a>", batch_id=bf_id, root=BREWFATHER_APP_ROOT)
class MashStepInline(admin.TabularInline):
model = MashStep
extra = 1
@admin.register(Mash)
class MashAdmin(admin.ModelAdmin):
list_display = ['name', ]
inlines = [
MashStepInline,
]
app = apps.get_app_config('beer') app = apps.get_app_config('beer')
for model_name, model in app.models.items(): for model_name, model in app.models.items():

View File

@ -9,9 +9,45 @@ RECIPE_URL = 'https://api.brewfather.app/v2/recipes'
BATCH_URL = 'https://api.brewfather.app/v2/batches' BATCH_URL = 'https://api.brewfather.app/v2/batches'
PULL_LIMIT = 50 PULL_LIMIT = 50
BREWFATHER_CONVERT_LOOKUP = { # local_name: brewfather_name
'all': {
'name': 'name',
'unit_cost': 'costPerAmount',
'supplier': 'supplier',
'notes': 'notes',
'user_notes': 'userNotes',
},
'fermentable': {
'grain_category': 'grainCategory',
'fermentable_type': 'type',
'diastatic_power': 'diastaticPower',
'potential': 'potential',
'protein': 'protein',
'attenuation': 'attenuation',
'lovibond': 'lovibond',
'max_in_batch': 'maxInBatch',
'moisture': 'moisture',
'non_fermentable': 'notFermentable',
'ibu_per_unit': 'ibuPerAmount',
},
'hop': {
'ibu': 'ibu',
'use': 'use',
'hop_type': 'type',
'alpha': 'alpha',
},
'misc': {
'use': 'use',
'misc_type': 'type',
'water_adjustment': 'waterAdjustment',
}
}
def get_batches(api_user, api_key, batch=''): def get_batches(api_user, api_key, batch=''):
auth_string = api_user + ':' + api_key auth_string = api_user + ':' + api_key
auth64 = base64.b64encode(auth_string.encode("utf-8")) auth64 = base64.b64encode(auth_string.encode("utf-8"))
batch_array = [] batch_array = []
@ -19,7 +55,7 @@ def get_batches(api_user, api_key, batch=''):
lastbatch = '&start_after=' + batch lastbatch = '&start_after=' + batch
else: else:
lastbatch = '' lastbatch = ''
query = '{batch_url}?limit={pull_limit}&complete=True&include=recipe,recipe.batchSize&status=Planning{last_batch}'.format( query = '{batch_url}?limit={pull_limit}&complete=True&include=recipe,recipe.batchSize&status=Planning{last_batch}'.format(
batch_url=BATCH_URL, batch_url=BATCH_URL,
pull_limit=PULL_LIMIT, pull_limit=PULL_LIMIT,

View File

@ -0,0 +1,217 @@
# Generated by Django 5.0.6 on 2024-06-17 17:13
import django.db.models.deletion
import django.utils.timezone
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('beer', '0003_delete_recipe_batchrecipe_batch_recipe_and_more'),
('yeast', '0004_alter_propogation_options'),
]
operations = [
migrations.CreateModel(
name='Fermentable',
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)),
('unit_cost', models.DecimalField(blank=True, decimal_places=2, max_digits=6, null=True)),
('notes', models.TextField(blank=True, max_length=500, null=True)),
('user_notes', models.TextField(blank=True, max_length=500, null=True)),
('grain_category', models.IntegerField(choices=[(1, 'Base'), (2, 'Wheat/Oat'), (3, 'Crystal'), (4, 'Roasted'), (5, 'Acid')], default=1)),
('fermentable_type', models.IntegerField(choices=[(1, 'Grain'), (2, 'Adjunct')], default=1)),
('diastatic_power', models.DecimalField(blank=True, decimal_places=4, max_digits=6, null=True)),
('potential', models.DecimalField(decimal_places=4, max_digits=6)),
('protein', models.DecimalField(blank=True, decimal_places=4, max_digits=6, null=True)),
('attenuation', models.DecimalField(blank=True, decimal_places=4, max_digits=6, null=True)),
('lovibond', models.DecimalField(blank=True, decimal_places=4, max_digits=6, null=True)),
('max_in_batch', models.DecimalField(blank=True, decimal_places=4, max_digits=6, null=True)),
('moisture', models.DecimalField(blank=True, decimal_places=4, max_digits=6, null=True)),
('non_fermentable', models.BooleanField(blank=True, null=True)),
('ibu_per_unit', models.DecimalField(decimal_places=4, default=0, max_digits=6)),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='Hop',
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)),
('unit_cost', models.DecimalField(blank=True, decimal_places=2, max_digits=6, null=True)),
('notes', models.TextField(blank=True, max_length=500, null=True)),
('user_notes', models.TextField(blank=True, max_length=500, null=True)),
('ibu', models.DecimalField(decimal_places=4, default=0, max_digits=6)),
('use', models.IntegerField(choices=[(1, 'Bittering'), (2, 'Aroma'), (3, 'Both')], default=1)),
('hop_type', models.IntegerField(choices=[(1, 'Pellet'), (2, 'Leaf'), (3, 'Cryo'), (4, 'CO2 Extract')], default=1)),
('alpha', models.DecimalField(decimal_places=2, default=0, max_digits=6)),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='Mash',
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)),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='Misc',
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)),
('unit_cost', models.DecimalField(blank=True, decimal_places=2, max_digits=6, null=True)),
('notes', models.TextField(blank=True, max_length=500, null=True)),
('user_notes', models.TextField(blank=True, max_length=500, null=True)),
('use', models.IntegerField(choices=[(1, 'Mash'), (2, 'Sparge'), (3, 'Boil'), (4, 'Flamout'), (5, 'Primary'), (6, 'Secondary'), (7, 'Cold Crash'), (8, 'Bottling')], default=1)),
('misc_type', models.IntegerField(choices=[(1, 'Spice'), (2, 'Fining'), (3, 'Water Agent'), (4, 'Herb'), (5, 'Flavor'), (6, 'Other')], default=1)),
('water_adjustment', models.BooleanField(blank=True, null=True)),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='Supplier',
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)),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='Unit',
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)),
('unit_type', models.CharField(choices=[('WT', 'Weight'), ('VL', 'Volume')], default='WT', max_length=3)),
],
options={
'abstract': False,
},
),
migrations.AddField(
model_name='batchrecipe',
name='efficiency',
field=models.DecimalField(blank=True, decimal_places=4, max_digits=6, null=True),
),
migrations.AddField(
model_name='batchrecipe',
name='mash',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='beer.mash'),
),
migrations.CreateModel(
name='MashStep',
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)),
('step_temp', models.DecimalField(decimal_places=2, max_digits=6)),
('ramp_time', models.DecimalField(decimal_places=2, max_digits=6)),
('step_time', models.DecimalField(decimal_places=2, max_digits=6)),
('step_type', models.IntegerField(choices=[(1, 'infusion'), (2, 'temperature'), (3, 'decoction')], default=1)),
('parent_mash', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='beer.mash')),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='RecipeFermentable',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_date', models.DateTimeField(default=django.utils.timezone.now)),
('quantity', models.DecimalField(decimal_places=4, max_digits=6)),
('fermentable', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='beer.fermentable')),
('recipe', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='beer.batchrecipe')),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='RecipeHop',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_date', models.DateTimeField(default=django.utils.timezone.now)),
('quantity', models.DecimalField(decimal_places=4, max_digits=6)),
('hop', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='beer.hop')),
('recipe', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='beer.batchrecipe')),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='RecipeMisc',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_date', models.DateTimeField(default=django.utils.timezone.now)),
('quantity', models.DecimalField(decimal_places=4, max_digits=6)),
('misc', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='beer.misc')),
('recipe', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='beer.batchrecipe')),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='RecipeYeast',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_date', models.DateTimeField(default=django.utils.timezone.now)),
('recipe', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='beer.batchrecipe')),
('yeast', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='yeast.strain')),
],
options={
'abstract': False,
},
),
migrations.AddField(
model_name='misc',
name='supplier',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='beer.supplier'),
),
migrations.AddField(
model_name='hop',
name='supplier',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='beer.supplier'),
),
migrations.AddField(
model_name='fermentable',
name='supplier',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='beer.supplier'),
),
migrations.AddField(
model_name='misc',
name='units',
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='beer.unit'),
),
migrations.AddField(
model_name='hop',
name='units',
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='beer.unit'),
),
migrations.AddField(
model_name='fermentable',
name='units',
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='beer.unit'),
),
]

View File

@ -37,9 +37,162 @@ class Batch(CustomModel):
# Return a string that represents the instance # Return a string that represents the instance
return 'BF #{num}: {name}'.format(name=self.brewfather_name, num=self.brewfather_num) 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')
class Supplier(CustomModel):
name = models.CharField(max_length=50)
class CustomIngredient(CustomModel):
""" Custom model class with default fields to use. """
created_date = models.DateTimeField(default=timezone.now)
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)
class Meta:
abstract = True
class BatchRecipe(CustomModel): class BatchRecipe(CustomModel):
""" Recipe to be stored with a batch.""" """ Recipe to be stored with a batch."""
name = models.CharField(max_length=50) name = models.CharField(max_length=50)
batch_recipe = models.BooleanField(null=True) batch_recipe = models.BooleanField(null=True)
recipe_json = models.TextField(null=True, blank=True) recipe_json = models.TextField(null=True, blank=True)
mash = models.ForeignKey('Mash' , on_delete=models.PROTECT, null=True, blank=True)
efficiency = models.DecimalField(max_digits=6, decimal_places=4, null=True, blank=True)
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',
}
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)
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)
non_fermentable = models.BooleanField(null=True, blank=True)
ibu_per_unit = models.DecimalField(max_digits=6, decimal_places=4, default=0)
def __str__(self):
return self.name
class RecipeFermentable(CustomModel):
recipe = models.ForeignKey(BatchRecipe, on_delete=models.CASCADE)
fermentable = models.ForeignKey(Fermentable, on_delete=models.CASCADE)
quantity = models.DecimalField(max_digits=6, decimal_places=4)
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):
recipe = models.ForeignKey(BatchRecipe, on_delete=models.CASCADE)
hop = models.ForeignKey(Hop, on_delete=models.CASCADE)
quantity = models.DecimalField(max_digits=6, decimal_places=4)
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(BatchRecipe, on_delete=models.CASCADE)
misc = models.ForeignKey(Misc, on_delete=models.CASCADE)
quantity = models.DecimalField(max_digits=6, decimal_places=4)
class RecipeYeast(CustomModel):
recipe = models.ForeignKey(BatchRecipe, on_delete=models.CASCADE)
yeast = models.ForeignKey('yeast.Strain', on_delete=models.CASCADE)
class Mash(CustomModel):
name = models.CharField(max_length=50)
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