Feature: Download nutritional information and store description and allergen information in products
This commit is contained in:
parent
cd6a5e129f
commit
88f0c35b4d
27
brewman/alembic/versions/66abfc21db73_allergen.py
Normal file
27
brewman/alembic/versions/66abfc21db73_allergen.py
Normal file
@ -0,0 +1,27 @@
|
||||
"""allergen
|
||||
|
||||
Revision ID: 66abfc21db73
|
||||
Revises: 2438cd581f00
|
||||
Create Date: 2023-12-28 12:45:01.275322
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "66abfc21db73"
|
||||
down_revision = "2438cd581f00"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.add_column("products", sa.Column("allergen", sa.Text(), server_default="", nullable=False))
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_column("products", "allergen")
|
||||
# ### end Alembic commands ###
|
@ -39,6 +39,8 @@ class Product:
|
||||
product_group: Mapped["ProductGroup"] = relationship("ProductGroup", back_populates="products")
|
||||
account: Mapped["Account"] = relationship("Account", back_populates="products")
|
||||
|
||||
allergen: Mapped[str] = mapped_column(Text, nullable=False)
|
||||
|
||||
protein: Mapped[Decimal] = mapped_column(Numeric(precision=15, scale=5), nullable=False)
|
||||
carbohydrate: Mapped[Decimal] = mapped_column(Numeric(precision=15, scale=5), nullable=False)
|
||||
total_sugar: Mapped[Decimal] = mapped_column(Numeric(precision=15, scale=5), nullable=False)
|
||||
@ -56,12 +58,14 @@ class Product:
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
description: str | None,
|
||||
fraction_units: str,
|
||||
product_group_id: uuid.UUID,
|
||||
account_id: uuid.UUID,
|
||||
is_active: bool,
|
||||
is_purchased: bool,
|
||||
is_sold: bool,
|
||||
allergen: str = "",
|
||||
protein: Decimal = 0,
|
||||
carbohydrate: Decimal = 0,
|
||||
total_sugar: Decimal = 0,
|
||||
@ -82,6 +86,7 @@ class Product:
|
||||
if code is not None:
|
||||
self.code = code
|
||||
self.name = name
|
||||
self.description = description
|
||||
self.fraction_units = fraction_units
|
||||
self.product_group_id = product_group_id
|
||||
self.account_id = account_id
|
||||
@ -89,6 +94,7 @@ class Product:
|
||||
self.is_purchased = is_purchased
|
||||
self.is_sold = is_sold
|
||||
|
||||
self.allergen = allergen
|
||||
self.protein = protein
|
||||
self.carbohydrate = carbohydrate
|
||||
self.total_sugar = total_sugar
|
||||
|
249
brewman/brewman/routers/calculate_nutrition.py
Normal file
249
brewman/brewman/routers/calculate_nutrition.py
Normal file
@ -0,0 +1,249 @@
|
||||
import uuid
|
||||
|
||||
from ..schemas.nutritional_information import NutritionalInformation
|
||||
from ..models.product_group import ProductGroup
|
||||
|
||||
from fastapi import HTTPException, status
|
||||
from sqlalchemy import distinct, or_, select, update
|
||||
from sqlalchemy.exc import SQLAlchemyError
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from ..models.product import Product
|
||||
from ..models.recipe import Recipe
|
||||
from ..models.recipe_item import RecipeItem
|
||||
from ..models.stock_keeping_unit import StockKeepingUnit
|
||||
|
||||
|
||||
def calculate_nutrition(db: Session):
|
||||
try:
|
||||
# Get all recipes that have nutritional values
|
||||
products = set(
|
||||
db.execute(
|
||||
select(distinct(StockKeepingUnit.product_id))
|
||||
.join(StockKeepingUnit.recipes)
|
||||
.join(StockKeepingUnit.product)
|
||||
.join(Product.product_group)
|
||||
.where(or_(ProductGroup.nutritional == True, ProductGroup.ice_cream == True)) # noqa: E712
|
||||
)
|
||||
.scalars()
|
||||
.all()
|
||||
)
|
||||
db.flush()
|
||||
while len(products) > 0:
|
||||
calculate(products, db)
|
||||
except SQLAlchemyError as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=str(e),
|
||||
)
|
||||
|
||||
|
||||
def calculate(products: set[uuid.UUID], db: Session) -> None:
|
||||
sq = select(RecipeItem.recipe_id).where(RecipeItem.product_id.in_(products))
|
||||
recipes = (
|
||||
db.execute(
|
||||
select(Recipe).join(Recipe.sku).where(StockKeepingUnit.product_id.in_(products), Recipe.id.notin_(sq))
|
||||
)
|
||||
.scalars()
|
||||
.all()
|
||||
)
|
||||
for recipe in recipes:
|
||||
protein = sum(i.quantity * i.product.protein for i in recipe.items) / recipe.sku.fraction
|
||||
carbohydrate = sum(i.quantity * i.product.carbohydrate for i in recipe.items) / recipe.sku.fraction
|
||||
total_sugar = sum(i.quantity * i.product.total_sugar for i in recipe.items) / recipe.sku.fraction
|
||||
added_sugar = sum(i.quantity * i.product.added_sugar for i in recipe.items) / recipe.sku.fraction
|
||||
total_fat = sum(i.quantity * i.product.total_fat for i in recipe.items) / recipe.sku.fraction
|
||||
saturated_fat = sum(i.quantity * i.product.saturated_fat for i in recipe.items) / recipe.sku.fraction
|
||||
trans_fat = sum(i.quantity * i.product.trans_fat for i in recipe.items) / recipe.sku.fraction
|
||||
cholestrol = sum(i.quantity * i.product.cholestrol for i in recipe.items) / recipe.sku.fraction
|
||||
sodium = sum(i.quantity * i.product.sodium for i in recipe.items) / recipe.sku.fraction
|
||||
msnf = sum(i.quantity * i.product.msnf for i in recipe.items) / recipe.sku.fraction
|
||||
other_solids = sum(i.quantity * i.product.other_solids for i in recipe.items) / recipe.sku.fraction
|
||||
total_solids = sum(i.quantity * i.product.total_solids for i in recipe.items) / recipe.sku.fraction
|
||||
water = sum(i.quantity * i.product.water for i in recipe.items) / recipe.sku.fraction
|
||||
|
||||
db.execute(
|
||||
update(Product)
|
||||
.where(Product.id == recipe.sku.product_id)
|
||||
.values(
|
||||
protein=protein,
|
||||
carbohydrate=carbohydrate,
|
||||
total_sugar=total_sugar,
|
||||
added_sugar=added_sugar,
|
||||
total_fat=total_fat,
|
||||
saturated_fat=saturated_fat,
|
||||
trans_fat=trans_fat,
|
||||
cholestrol=cholestrol,
|
||||
sodium=sodium,
|
||||
msnf=msnf,
|
||||
other_solids=other_solids,
|
||||
total_solids=total_solids,
|
||||
water=water,
|
||||
)
|
||||
)
|
||||
|
||||
products.remove(recipe.sku.product_id)
|
||||
db.flush()
|
||||
|
||||
|
||||
def report_nutrition(db: Session) -> list[NutritionalInformation]:
|
||||
try:
|
||||
# Get all recipes that have nutritional values
|
||||
products = set(
|
||||
db.execute(
|
||||
select(distinct(StockKeepingUnit.product_id))
|
||||
.join(StockKeepingUnit.recipes)
|
||||
.join(StockKeepingUnit.product)
|
||||
.join(Product.product_group)
|
||||
)
|
||||
.scalars()
|
||||
.all()
|
||||
)
|
||||
final, semi, ingredients = nut_final(db), nut_semi(db), nut_ingredients(db)
|
||||
return report(products, final, semi, ingredients, db)
|
||||
except SQLAlchemyError as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=str(e),
|
||||
)
|
||||
|
||||
|
||||
def report(
|
||||
products: set[uuid.UUID], final: set[uuid.UUID], semi: set[uuid.UUID], ingredients: set[uuid.UUID], db: Session
|
||||
) -> list[NutritionalInformation]:
|
||||
_list: list[NutritionalInformation] = []
|
||||
ingredient_dict: dict[str, set[str]] = {}
|
||||
allergen_dict: dict[str, set[str]] = {}
|
||||
ings = db.execute(select(Product).where(Product.id.in_(ingredients))).scalars().all()
|
||||
for ingredient in ings:
|
||||
if ingredient.id in products:
|
||||
raise ValueError("Ingredient cannot be in a recipe")
|
||||
ingredients.remove(ingredient.id)
|
||||
ingredient_dict[ingredient.name] = set([ingredient.name])
|
||||
allergen_dict[ingredient.name] = set(a.strip() for a in ingredient.allergen.split(",")) - set([""])
|
||||
_list.append(
|
||||
NutritionalInformation(
|
||||
name=ingredient.name,
|
||||
units=ingredient.fraction_units,
|
||||
product_group="Ingredients",
|
||||
description=ingredient.description,
|
||||
ingredients=[ingredient.name],
|
||||
allergen=list(allergen_dict[ingredient.name]),
|
||||
protein=ingredient.protein,
|
||||
carbohydrate=ingredient.carbohydrate,
|
||||
total_sugar=ingredient.total_sugar,
|
||||
added_sugar=ingredient.added_sugar,
|
||||
total_fat=ingredient.total_fat,
|
||||
saturated_fat=ingredient.saturated_fat,
|
||||
trans_fat=ingredient.trans_fat,
|
||||
cholestrol=ingredient.cholestrol,
|
||||
sodium=ingredient.sodium,
|
||||
msnf=ingredient.msnf,
|
||||
other_solids=ingredient.other_solids,
|
||||
total_solids=ingredient.total_solids,
|
||||
water=ingredient.water,
|
||||
)
|
||||
)
|
||||
while len(semi) > 0:
|
||||
semi_products = (
|
||||
db.execute(select(Recipe).join(Recipe.sku).where(StockKeepingUnit.product_id.in_(semi))).scalars().all()
|
||||
)
|
||||
for semi_recipe in semi_products:
|
||||
semi_product = semi_recipe.sku.product
|
||||
products.remove(semi_product.id)
|
||||
semi.remove(semi_product.id)
|
||||
ingredient_dict[semi_product.name] = set.union(
|
||||
*[ingredient_dict[i.product.name] for i in semi_recipe.items]
|
||||
)
|
||||
allergen_dict[semi_product.name] = set.union(
|
||||
*[allergen_dict[i.product.name] for i in semi_recipe.items]
|
||||
) - set([""])
|
||||
|
||||
_list.append(
|
||||
NutritionalInformation(
|
||||
name=semi_product.name,
|
||||
units=semi_product.fraction_units,
|
||||
product_group="Semi",
|
||||
description=semi_product.description,
|
||||
ingredients=list(ingredient_dict[semi_product.name]),
|
||||
allergen=list(allergen_dict[semi_product.name]),
|
||||
protein=semi_product.protein,
|
||||
carbohydrate=semi_product.carbohydrate,
|
||||
total_sugar=semi_product.total_sugar,
|
||||
added_sugar=semi_product.added_sugar,
|
||||
total_fat=semi_product.total_fat,
|
||||
saturated_fat=semi_product.saturated_fat,
|
||||
trans_fat=semi_product.trans_fat,
|
||||
cholestrol=semi_product.cholestrol,
|
||||
sodium=semi_product.sodium,
|
||||
msnf=semi_product.msnf,
|
||||
other_solids=semi_product.other_solids,
|
||||
total_solids=semi_product.total_solids,
|
||||
water=semi_product.water,
|
||||
)
|
||||
)
|
||||
recipes = db.execute(select(Recipe).join(Recipe.sku).where(StockKeepingUnit.product_id.in_(final))).scalars().all()
|
||||
for recipe in recipes:
|
||||
recipe_product = recipe.sku.product
|
||||
products.remove(recipe_product.id)
|
||||
final.remove(recipe_product.id)
|
||||
ingredient_dict[recipe_product.name] = set.union(*[ingredient_dict[i.product.name] for i in recipe.items])
|
||||
allergen_dict[recipe_product.name] = set.union(*[allergen_dict[i.product.name] for i in recipe.items]) - set(
|
||||
[""]
|
||||
)
|
||||
_list.append(
|
||||
NutritionalInformation(
|
||||
name=recipe_product.name,
|
||||
units=recipe_product.fraction_units,
|
||||
product_group=recipe_product.product_group.name,
|
||||
description=recipe_product.description,
|
||||
ingredients=list(ingredient_dict[recipe_product.name]),
|
||||
allergen=list(allergen_dict[recipe_product.name]),
|
||||
protein=recipe_product.protein,
|
||||
carbohydrate=recipe_product.carbohydrate,
|
||||
total_sugar=recipe_product.total_sugar,
|
||||
added_sugar=recipe_product.added_sugar,
|
||||
total_fat=recipe_product.total_fat,
|
||||
saturated_fat=recipe_product.saturated_fat,
|
||||
trans_fat=recipe_product.trans_fat,
|
||||
cholestrol=recipe_product.cholestrol,
|
||||
sodium=recipe_product.sodium,
|
||||
msnf=recipe_product.msnf,
|
||||
other_solids=recipe_product.other_solids,
|
||||
total_solids=recipe_product.total_solids,
|
||||
water=recipe_product.water,
|
||||
)
|
||||
)
|
||||
if len(products) > 0 or len(final) > 0 or len(semi) > 0 or len(ingredients) > 0:
|
||||
raise ValueError("They cannot be more than 0")
|
||||
return _list
|
||||
|
||||
|
||||
def nut_final(db: Session) -> set[uuid.UUID]:
|
||||
sq = select(StockKeepingUnit.product_id).where(StockKeepingUnit.id.in_(select(Recipe.sku_id)))
|
||||
ingredients = (
|
||||
db.execute(select(Product.id).where(Product.id.notin_(select(RecipeItem.product_id)), Product.id.in_(sq)))
|
||||
.scalars()
|
||||
.all()
|
||||
)
|
||||
return set(ingredients)
|
||||
|
||||
|
||||
def nut_ingredients(db: Session) -> set[uuid.UUID]:
|
||||
sq = select(StockKeepingUnit.product_id).where(StockKeepingUnit.id.in_(select(Recipe.sku_id)))
|
||||
ingredients = (
|
||||
db.execute(select(Product.id).where(Product.id.in_(select(RecipeItem.product_id)), Product.id.notin_(sq)))
|
||||
.scalars()
|
||||
.all()
|
||||
)
|
||||
return set(ingredients)
|
||||
|
||||
|
||||
def nut_semi(db: Session) -> set[uuid.UUID]:
|
||||
sq = select(StockKeepingUnit.product_id).where(StockKeepingUnit.id.in_(select(Recipe.sku_id)))
|
||||
ingredients = (
|
||||
db.execute(select(Product.id).where(Product.id.in_(select(RecipeItem.product_id)), Product.id.in_(sq)))
|
||||
.scalars()
|
||||
.all()
|
||||
)
|
||||
return set(ingredients)
|
@ -34,12 +34,14 @@ def save(
|
||||
with SessionFuture() as db:
|
||||
item = Product(
|
||||
name=data.name,
|
||||
description=data.description,
|
||||
fraction_units=data.fraction_units,
|
||||
product_group_id=data.product_group.id_,
|
||||
account_id=Account.all_purchases(),
|
||||
is_active=data.is_active,
|
||||
is_purchased=data.is_purchased,
|
||||
is_sold=data.is_sold,
|
||||
allergen=data.allergen,
|
||||
protein=data.protein,
|
||||
carbohydrate=data.carbohydrate,
|
||||
total_sugar=data.total_sugar,
|
||||
@ -96,6 +98,7 @@ def update_route(
|
||||
detail=f"{item.name} is a fixture and cannot be edited or deleted.",
|
||||
)
|
||||
item.name = data.name
|
||||
item.description = data.description
|
||||
item.fraction_units = data.fraction_units
|
||||
item.product_group_id = data.product_group.id_
|
||||
item.account_id = Account.all_purchases()
|
||||
@ -103,6 +106,7 @@ def update_route(
|
||||
item.is_purchased = data.is_purchased
|
||||
item.is_sold = data.is_sold
|
||||
|
||||
item.allergen = data.allergen
|
||||
item.protein = data.protein
|
||||
item.carbohydrate = data.carbohydrate
|
||||
item.total_sugar = data.total_sugar
|
||||
@ -305,6 +309,7 @@ def product_info(product: Product) -> schemas.Product:
|
||||
id_=product.id,
|
||||
code=product.code,
|
||||
name=product.name,
|
||||
description=product.description,
|
||||
fraction_units=product.fraction_units,
|
||||
skus=[
|
||||
schemas.StockKeepingUnit(
|
||||
@ -322,6 +327,7 @@ def product_info(product: Product) -> schemas.Product:
|
||||
is_purchased=product.is_purchased,
|
||||
is_sold=product.is_sold,
|
||||
product_group=schemas.ProductGroupLink(id_=product.product_group.id, name=product.product_group.name),
|
||||
allergen=product.allergen,
|
||||
protein=product.protein,
|
||||
carbohydrate=product.carbohydrate,
|
||||
total_sugar=product.total_sugar,
|
||||
@ -341,12 +347,14 @@ def product_info(product: Product) -> schemas.Product:
|
||||
def product_blank() -> schemas.ProductBlank:
|
||||
return schemas.ProductBlank(
|
||||
name="",
|
||||
description="",
|
||||
fraction_units="",
|
||||
skus=[],
|
||||
is_active=True,
|
||||
is_purchased=True,
|
||||
is_sold=False,
|
||||
is_fixture=False,
|
||||
allergen="",
|
||||
protein=0,
|
||||
carbohydrate=0,
|
||||
total_sugar=0,
|
||||
|
@ -8,11 +8,14 @@ from decimal import Decimal
|
||||
from io import BytesIO
|
||||
from typing import Sequence
|
||||
|
||||
from ..schemas.nutritional_information import NutritionalInformation
|
||||
from ..routers.calculate_nutrition import calculate_nutrition, report_nutrition
|
||||
|
||||
import brewman.schemas.recipe as schemas
|
||||
import brewman.schemas.recipe_item as rischemas
|
||||
|
||||
from brewman.models.price import Price
|
||||
from brewman.routers.calculate_prices import calculate_prices
|
||||
from ..models.price import Price
|
||||
from ..routers.calculate_prices import calculate_prices
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request, Security, status
|
||||
from fastapi.responses import FileResponse, StreamingResponse
|
||||
from openpyxl import Workbook
|
||||
@ -26,7 +29,6 @@ from ..db.session import SessionFuture
|
||||
from ..models.product import Product
|
||||
from ..models.recipe import Recipe
|
||||
from ..models.recipe_item import RecipeItem
|
||||
from ..models.recipe_template import RecipeTemplate
|
||||
from ..models.stock_keeping_unit import StockKeepingUnit
|
||||
from ..schemas.user import UserToken
|
||||
|
||||
@ -352,6 +354,89 @@ def excel(prices: list[tuple[str, str, Decimal, Decimal, Decimal]], recipes: lis
|
||||
return virtual_workbook
|
||||
|
||||
|
||||
@router.get("/nutrition", response_class=StreamingResponse)
|
||||
def get_nutrition(
|
||||
p: uuid.UUID | None = None,
|
||||
) -> StreamingResponse:
|
||||
with SessionFuture() as db:
|
||||
calculate_nutrition(db)
|
||||
db.commit()
|
||||
list_: list[NutritionalInformation] = []
|
||||
with SessionFuture() as db:
|
||||
list_ = report_nutrition(db)
|
||||
# q = (
|
||||
# select(Recipe)
|
||||
# .join(Recipe.sku)
|
||||
# .join(StockKeepingUnit.product)
|
||||
# .join(Product.product_group)
|
||||
# .where(or_(ProductGroup.nutritional == True, ProductGroup.ice_cream == True)) # noqa: E712
|
||||
# .options(
|
||||
# contains_eager(Recipe.sku, StockKeepingUnit.product, Product.product_group),
|
||||
# )
|
||||
# )
|
||||
# list_ = [
|
||||
# NutritionalInformation(
|
||||
# name=i.sku.product.name,
|
||||
# units=i.sku.units,
|
||||
# product_group=i.sku.product.product_group.name,
|
||||
# **i.sku.product,
|
||||
# )
|
||||
# for i in db.execute(q).unique().scalars().all()
|
||||
# ]
|
||||
e = nut(sorted(list_, key=lambda r: r.name))
|
||||
e.seek(0)
|
||||
|
||||
headers = {"Content-Disposition": "attachment; filename = nutritional.xlsx"}
|
||||
return StreamingResponse(e, media_type="text/xlsx", headers=headers)
|
||||
|
||||
|
||||
def nut(products: list[NutritionalInformation]) -> BytesIO:
|
||||
wb = Workbook()
|
||||
wb.active.title = "Ingredients"
|
||||
pgs = set([x.product_group for x in products])
|
||||
pgs.remove("Ingredients")
|
||||
for pg in pgs:
|
||||
wb.create_sheet(pg)
|
||||
rows = defaultdict(lambda: 1)
|
||||
register_styles(wb)
|
||||
for item in products:
|
||||
ws = wb[item.product_group]
|
||||
row = rows[item.product_group]
|
||||
if row == 1:
|
||||
ws.cell(row=row, column=1, value="Product").style = "header"
|
||||
ws.cell(row=row, column=2, value="Description").style = "header"
|
||||
ws.cell(row=row, column=3, value="Ingredients").style = "header"
|
||||
ws.cell(row=row, column=4, value="Allergen").style = "header"
|
||||
ws.cell(row=row, column=5, value="Protein").style = "header"
|
||||
ws.cell(row=row, column=6, value="Carbohydrate").style = "header"
|
||||
ws.cell(row=row, column=7, value="Total Sugar").style = "header"
|
||||
ws.cell(row=row, column=8, value="Added Sugar").style = "header"
|
||||
ws.cell(row=row, column=9, value="Total Fat").style = "header"
|
||||
ws.cell(row=row, column=10, value="Saturated Fat").style = "header"
|
||||
ws.cell(row=row, column=11, value="Trans Fat").style = "header"
|
||||
ws.cell(row=row, column=12, value="Cholestrol").style = "header"
|
||||
ws.cell(row=row, column=13, value="Sodium").style = "header"
|
||||
row += 1
|
||||
ws.cell(row=row, column=1, value=f"{item.name} ({item.units})").style = "ing"
|
||||
ws.cell(row=row, column=2, value=item.description).style = "ing"
|
||||
ws.cell(row=row, column=3, value=", ".join(item.ingredients)).style = "ing"
|
||||
ws.cell(row=row, column=4, value=", ".join(item.allergen)).style = "ing"
|
||||
ws.cell(row=row, column=5, value=item.protein).style = "ing"
|
||||
ws.cell(row=row, column=6, value=item.carbohydrate).style = "ing"
|
||||
ws.cell(row=row, column=7, value=item.total_sugar).style = "ing"
|
||||
ws.cell(row=row, column=8, value=item.added_sugar).style = "ing"
|
||||
ws.cell(row=row, column=9, value=item.total_fat).style = "ing"
|
||||
ws.cell(row=row, column=10, value=item.saturated_fat).style = "ing"
|
||||
ws.cell(row=row, column=11, value=item.trans_fat).style = "ing"
|
||||
ws.cell(row=row, column=12, value=item.cholestrol).style = "ing"
|
||||
ws.cell(row=row, column=13, value=item.sodium).style = "ing"
|
||||
row += 1
|
||||
rows[item.product_group] = row + 1
|
||||
virtual_workbook = BytesIO()
|
||||
wb.save(virtual_workbook)
|
||||
return virtual_workbook
|
||||
|
||||
|
||||
def register_styles(wb: Workbook) -> tuple[NamedStyle, NamedStyle, NamedStyle, NamedStyle, NamedStyle]:
|
||||
bd = Side(style="thin", color="000000")
|
||||
|
||||
|
28
brewman/brewman/schemas/nutritional_information.py
Normal file
28
brewman/brewman/schemas/nutritional_information.py
Normal file
@ -0,0 +1,28 @@
|
||||
from decimal import Decimal
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class NutritionalInformation(BaseModel):
|
||||
name: str
|
||||
units: str
|
||||
product_group: str
|
||||
|
||||
description: str | None
|
||||
ingredients: list[str]
|
||||
|
||||
allergen: list[str]
|
||||
protein: Decimal
|
||||
carbohydrate: Decimal
|
||||
total_sugar: Decimal
|
||||
added_sugar: Decimal
|
||||
total_fat: Decimal
|
||||
saturated_fat: Decimal
|
||||
trans_fat: Decimal
|
||||
cholestrol: Decimal
|
||||
sodium: Decimal
|
||||
|
||||
msnf: Decimal
|
||||
other_solids: Decimal
|
||||
total_solids: Decimal
|
||||
water: Decimal
|
@ -17,6 +17,7 @@ class ProductLink(BaseModel):
|
||||
|
||||
class ProductIn(BaseModel):
|
||||
name: str = Field(..., min_length=1)
|
||||
description: str | None
|
||||
fraction_units: str = Field(..., min_length=1)
|
||||
skus: list[StockKeepingUnit]
|
||||
product_group: ProductGroupLink = Field(...)
|
||||
@ -24,6 +25,7 @@ class ProductIn(BaseModel):
|
||||
is_purchased: bool
|
||||
is_sold: bool
|
||||
|
||||
allergen: str
|
||||
protein: Decimal
|
||||
carbohydrate: Decimal
|
||||
total_sugar: Decimal
|
||||
|
@ -21,6 +21,7 @@ export class Product {
|
||||
id: string | undefined;
|
||||
code: number;
|
||||
name: string;
|
||||
description: string | undefined;
|
||||
skus: StockKeepingUnit[];
|
||||
fractionUnits: string | undefined;
|
||||
|
||||
@ -30,6 +31,7 @@ export class Product {
|
||||
isSold: boolean;
|
||||
productGroup?: ProductGroup;
|
||||
|
||||
allergen: string;
|
||||
protein: number;
|
||||
carbohydrate: number;
|
||||
totalSugar: number;
|
||||
@ -55,6 +57,7 @@ export class Product {
|
||||
this.isPurchased = true;
|
||||
this.isSold = false;
|
||||
|
||||
this.allergen = '';
|
||||
this.protein = 0;
|
||||
this.carbohydrate = 0;
|
||||
this.totalSugar = 0;
|
||||
|
@ -20,6 +20,12 @@
|
||||
<input matInput formControlName="fractionUnits" />
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="flex flex-row justify-around content-start items-start">
|
||||
<mat-form-field class="flex-auto">
|
||||
<mat-label>Description</mat-label>
|
||||
<input matInput #nameElement formControlName="description" />
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="flex flex-row justify-around content-start items-start">
|
||||
<mat-checkbox formControlName="isPurchased" class="flex-auto mr-5">Is Purchased?</mat-checkbox>
|
||||
<mat-checkbox formControlName="isSold" class="flex-auto mr-5">Is Sold?</mat-checkbox>
|
||||
@ -35,6 +41,12 @@
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="flex flex-row justify-around content-start items-start">
|
||||
<mat-form-field class="flex-auto">
|
||||
<mat-label>Allergen</mat-label>
|
||||
<input matInput #nameElement formControlName="allergen" />
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<h2 *ngIf="item.productGroup?.nutritional ?? false">Nutritional Information</h2>
|
||||
<div
|
||||
class="flex flex-row justify-around content-start items-start"
|
||||
|
@ -23,6 +23,7 @@ export class ProductDetailComponent implements OnInit, AfterViewInit {
|
||||
form: FormGroup<{
|
||||
code: FormControl<string | number>;
|
||||
name: FormControl<string | null>;
|
||||
description: FormControl<string | null>;
|
||||
fractionUnits: FormControl<string | null>;
|
||||
addRow: FormGroup<{
|
||||
units: FormControl<string | null>;
|
||||
@ -36,6 +37,7 @@ export class ProductDetailComponent implements OnInit, AfterViewInit {
|
||||
isActive: FormControl<boolean>;
|
||||
productGroup: FormControl<string | null>;
|
||||
|
||||
allergen: FormControl<string | null>;
|
||||
protein: FormControl<number>;
|
||||
carbohydrate: FormControl<number>;
|
||||
totalSugar: FormControl<number>;
|
||||
@ -69,6 +71,7 @@ export class ProductDetailComponent implements OnInit, AfterViewInit {
|
||||
this.form = new FormGroup({
|
||||
code: new FormControl<string | number>({ value: 0, disabled: true }, { nonNullable: true }),
|
||||
name: new FormControl<string | null>(null),
|
||||
description: new FormControl<string | null>(null),
|
||||
fractionUnits: new FormControl<string | null>(null),
|
||||
addRow: new FormGroup({
|
||||
units: new FormControl<string | null>(null),
|
||||
@ -82,6 +85,7 @@ export class ProductDetailComponent implements OnInit, AfterViewInit {
|
||||
isActive: new FormControl<boolean>(true, { nonNullable: true }),
|
||||
productGroup: new FormControl<string | null>(null),
|
||||
|
||||
allergen: new FormControl<string | null>(null),
|
||||
protein: new FormControl<number>(0, { nonNullable: true }),
|
||||
carbohydrate: new FormControl<number>(0, { nonNullable: true }),
|
||||
totalSugar: new FormControl<number>(0, { nonNullable: true }),
|
||||
@ -116,6 +120,7 @@ export class ProductDetailComponent implements OnInit, AfterViewInit {
|
||||
this.form.setValue({
|
||||
code: this.item.code || '(Auto)',
|
||||
name: this.item.name,
|
||||
description: this.item.description || '',
|
||||
fractionUnits: this.item.fractionUnits ?? '',
|
||||
addRow: {
|
||||
units: '',
|
||||
@ -129,6 +134,7 @@ export class ProductDetailComponent implements OnInit, AfterViewInit {
|
||||
isActive: this.item.isActive,
|
||||
productGroup: this.item.productGroup ? this.item.productGroup.id ?? '' : '',
|
||||
|
||||
allergen: this.item.allergen ?? '',
|
||||
protein: this.item.protein,
|
||||
carbohydrate: this.item.carbohydrate,
|
||||
totalSugar: this.item.totalSugar,
|
||||
@ -266,11 +272,13 @@ export class ProductDetailComponent implements OnInit, AfterViewInit {
|
||||
getItem(): Product {
|
||||
const formModel = this.form.value;
|
||||
this.item.name = formModel.name ?? '';
|
||||
this.item.description = formModel.description ?? '';
|
||||
this.item.fractionUnits = formModel.fractionUnits ?? '';
|
||||
this.item.isPurchased = formModel.isPurchased ?? true;
|
||||
this.item.isSold = formModel.isSold ?? false;
|
||||
this.item.isActive = formModel.isActive ?? true;
|
||||
|
||||
this.item.allergen = formModel.allergen ?? '';
|
||||
this.item.protein = formModel.protein ?? 0;
|
||||
this.item.carbohydrate = formModel.carbohydrate ?? 0;
|
||||
this.item.totalSugar = formModel.totalSugar ?? 0;
|
||||
|
@ -5,6 +5,9 @@
|
||||
<a mat-icon-button [href]="'/api/recipes/xlsx?t=' + period.id">
|
||||
<mat-icon>save_alt</mat-icon>
|
||||
</a>
|
||||
<a mat-icon-button href="/api/recipes/nutrition">
|
||||
<mat-icon>save_alt</mat-icon>
|
||||
</a>
|
||||
<a mat-button [routerLink]="['/recipes', 'new']">
|
||||
<mat-icon>add_box</mat-icon>
|
||||
Add
|
||||
|
Loading…
Reference in New Issue
Block a user