brewman/brewman/brewman/routers/recipe.py

367 lines
12 KiB
Python

import datetime
import time
import uuid
from decimal import Decimal, InvalidOperation
from brewman.models.master import CostCentre, Product, Recipe, RecipeItem
from brewman.models.voucher import Inventory, Journal, Voucher, VoucherType
from fastapi import APIRouter
from sqlalchemy import desc, func, or_
from ..core.session import get_finish_date, get_start_date, set_period
router = APIRouter()
# @router.post("/new") # "Recipes"
@router.post("/{id}") # "Recipes"
def save(request):
json = request.json_body
recipe_product = (
request.dbsession.query(Product)
.filter(Product.id == uuid.UUID(json["Product"]["ProductID"]))
.first()
)
try:
valid_from = datetime.date(
*(time.strptime(request.json_body["ValidFrom"], "%d-%b-%Y")[0:3])
)
except (ValueError, KeyError, TypeError):
raise ValidationError("Valid From is not a valid date")
try:
valid_to = datetime.date(
*(time.strptime(request.json_body["ValidTo"], "%d-%b-%Y")[0:3])
)
except (ValueError, KeyError, TypeError):
raise ValidationError("Valid To is not a valid date")
if valid_to < valid_from:
raise ValidationError("Valid To cannot be less than valid from")
try:
recipe_quantity = Decimal(json.get("Quantity", 0))
if recipe_quantity < 0:
raise ValidationError("Quantity must be a decimal >= 0")
except (ValueError, InvalidOperation):
raise ValidationError("Quantity must be a decimal >= 0")
sale_price = 0
if recipe_product.is_sold:
try:
sale_price = Decimal(json.get("SalePrice", 0))
if sale_price < 0:
raise ValidationError("Sale Price must be a decimal >= 0")
except (ValueError, InvalidOperation):
raise ValidationError("Sale Price must be a decimal >= 0")
recipe_cost = 0
if len(json["RecipeItems"]) == 0:
raise ValidationError("Recipe has no ingredients")
recipe = Recipe(
product_id=recipe_product.id,
quantity=recipe_quantity,
sale_price=sale_price,
valid_from=valid_from,
valid_to=valid_to,
)
for item in json["RecipeItems"]:
product = (
request.dbsession.query(Product)
.filter(Product.id == uuid.UUID(item["Product"]["ProductID"]))
.first()
)
quantity = round(Decimal(item["Quantity"]), 2)
if product.is_purchased:
ingredient_cost = get_purchased_product_cost(
product.id, valid_from, valid_to, request.dbsession
)
else:
ingredient_cost = get_sub_product_cost(
product.id, valid_from, valid_to, request.dbsession
)
cost_per_unit = ingredient_cost / (product.fraction * product.product_yield)
recipe_cost += cost_per_unit * quantity
recipe.recipe_items.append(
RecipeItem(None, product.id, quantity, ingredient_cost)
)
recipe.cost_price = round(recipe_cost / recipe_quantity, 2)
if recipe_product.is_sold:
recipe_product.sale_price = sale_price
save_recipe(recipe, request.dbsession)
transaction.commit()
set_period(valid_from, valid_to, request)
return recipe_info(recipe.id, request)
def save_recipe(recipe, dbsession):
product = dbsession.query(Product).filter(Product.id == recipe.product_id).first()
product.price = recipe.cost_price
update_old_rows(
recipe.product_id,
recipe.valid_from,
recipe.valid_to,
recipe.effective_from,
dbsession,
)
dbsession.add(recipe)
for item in recipe.recipe_items:
item.recipe_id = recipe.id
dbsession.add(item)
def get_purchased_product_cost(product_id, start_date, finish_date, dbsession):
quantity_sum = func.sum(Journal.debit * Inventory.quantity).label("quantity")
amount_sum = func.sum(
Journal.debit
* Inventory.quantity
* Inventory.rate
* (1 + Inventory.tax)
* (1 - Inventory.discount)
).label("amount")
costing = (
dbsession.query(quantity_sum, amount_sum)
.join(Product.inventories)
.join(Inventory.voucher)
.join(Voucher.journals)
.filter(Inventory.product_id == product_id)
.filter(Voucher.date >= start_date)
.filter(Voucher.date <= finish_date)
.filter(Voucher.type == VoucherType.by_name("Issue").id)
.filter(Journal.cost_centre_id == CostCentre.cost_centre_purchase())
.group_by(Product)
.first()
)
if costing is None:
product = dbsession.query(Product).filter(Product.id == product_id).first()
return product.price
else:
quantity, amount = costing
return amount / quantity
def get_sub_product_cost(product_id, start_date, finish_date, dbsession):
product = dbsession.query(Product).filter(Product.id == product_id).first()
old_recipe = (
dbsession.query(Recipe)
.filter(Recipe.product_id == product_id)
.filter(Recipe.effective_to == None)
.filter(Recipe.valid_from == start_date)
.filter(Recipe.valid_to == finish_date)
.first()
)
if old_recipe is not None:
return old_recipe.cost_price
old_recipe = (
dbsession.query(Recipe)
.filter(Recipe.product_id == product_id)
.filter(Recipe.effective_to == None)
.order_by(desc(Recipe.valid_to))
.first()
)
if old_recipe is None:
raise ValidationError("Ingredient XXX has no recipe, cannot determine cost")
recipe_cost = 0
recipe = Recipe(
product_id=product_id,
quantity=old_recipe.quantity,
sale_price=product.sale_price,
valid_from=start_date,
valid_to=finish_date,
)
for item in old_recipe.recipe_items:
if item.product.is_purchased:
ingredient_cost = get_purchased_product_cost(
item.product_id, start_date, finish_date, dbsession
)
else:
ingredient_cost = get_sub_product_cost(
item.product_id, start_date, finish_date, dbsession
)
cost_per_unit = ingredient_cost / (
item.product.fraction * item.product.product_yield
)
recipe_cost += cost_per_unit * item.quantity
recipe.recipe_items.append(
RecipeItem(None, item.product_id, item.quantity, ingredient_cost)
)
recipe.cost_price = round(recipe_cost / old_recipe.quantity, 2)
save_recipe(recipe, dbsession)
return recipe_cost
def update_old_rows(product_id, valid_from, valid_to, effective_date, dbsession):
old = (
dbsession.query(Recipe)
.filter(Recipe.product_id == product_id)
.filter(Recipe.valid_from < valid_to)
.filter(Recipe.valid_to > valid_from)
.filter(or_(Recipe.effective_to == None, Recipe.effective_to >= effective_date))
.order_by(Recipe.effective_from)
.all()
)
new_recipes = []
for item in old:
if item.valid_from < valid_from:
recipe = Recipe(
product_id=item.product_id,
quantity=item.quantity,
cost_price=item.cost_price,
sale_price=item.sale_price,
valid_from=item.valid_from,
valid_to=valid_from - datetime.timedelta(days=1),
effective_from=effective_date,
)
for ri in item.recipe_items:
recipe.recipe_items.append(
RecipeItem(None, ri.product_id, ri.quantity, ri.price)
)
new_recipes.append(recipe)
if item.valid_to > valid_to:
recipe = Recipe(
product_id=item.product_id,
quantity=item.quantity,
cost_price=item.cost_price,
sale_price=item.sale_price,
valid_from=valid_to + datetime.timedelta(days=1),
valid_to=item.valid_to,
effective_from=effective_date,
)
for ri in item.recipe_items:
recipe.recipe_items.append(
RecipeItem(None, ri.product_id, ri.quantity, ri.price)
)
new_recipes.append(recipe)
if item.effective_from == effective_date and item.effective_to is None:
for ri in item.recipe_items:
dbsession.delete(ri)
dbsession.delete(item)
else:
item.effective_to = effective_date
for recipe in new_recipes:
dbsession.add(recipe)
for item in recipe.recipe_items:
item.recipe_id = recipe.id
dbsession.add(item)
@router.delete("/{id}") # "Recipes"
def delete(request):
recipe = (
request.dbsession.query(Recipe)
.filter(Recipe.id == uuid.UUID(request.matchdict["id"]))
.first()
)
if len(recipe.product.recipes) > 1:
request.dbsession.delete(recipe)
else:
request.dbsession.delete(recipe)
request.dbsession.delete(recipe.product)
transaction.commit()
return recipe_info(None, request)
@router.get("/{id}") # "Recipes"
def show_id(request):
return recipe_info(uuid.UUID(request.matchdict["id"]), request)
@router.get("/new") # "Recipes"
def show_blank(request):
return recipe_info(None, request)
@router.get("/") # "Authenticated"
async def show_list(l: bool):
list_ = (
request.dbsession.query(Recipe)
.join(Recipe.product)
.filter(Recipe.effective_to == None)
.order_by(Recipe.product_id)
.order_by(desc(Recipe.valid_to))
.all()
)
recipes = []
product_id = None
for item in list_:
if item.product_id != product_id:
recipe = {
"ProductID": item.product_id,
"Name": item.product.name,
"ProductGroup": item.product.product_group.name,
"Prices": [],
}
recipes.append(recipe)
product_id = item.product_id
costing = 0
if item.product.is_sold and item.sale_price != 0 and item.cost_price != 0:
costing = item.cost_price / item.sale_price
recipe["Prices"].append(
{
"ValidFrom": item.valid_from.strftime("%d-%b-%Y"),
"ValidTo": item.valid_to.strftime("%d-%b-%Y"),
"CostPrice": item.cost_price,
"SalePrice": item.sale_price,
"Costing": costing,
"Url": request.route_url("recipe_id", id=item.id),
}
)
return recipes
def recipe_info(id_, request):
if id_ is None:
info = {
"Quantity": 1,
"ValidFrom": get_start_date(request),
"ValidTo": get_finish_date(request),
"RecipeItems": [],
}
else:
recipe = request.dbsession.query(Recipe).filter(Recipe.id == id_).one()
info = {
"RecipeID": recipe.id,
"Product": {
"ProductID": recipe.product_id,
"Name": recipe.product.name,
"Units": recipe.product.units,
"SalePrice": recipe.product.sale_price,
"IsSold": recipe.product.is_sold,
},
"Quantity": recipe.quantity,
"SalePrice": recipe.sale_price,
"CostPrice": recipe.cost_price,
"ValidFrom": recipe.valid_from.strftime("%d-%b-%Y"),
"ValidTo": recipe.valid_to.strftime("%d-%b-%Y"),
"RecipeItems": [],
}
for item in recipe.recipe_items:
info["RecipeItems"].append(
{
"Product": {
"ProductID": item.product.id,
"Name": item.product.name,
"Units": item.product.units,
"FractionUnits": item.product.fraction_units,
"Fraction": item.product.fraction,
"ProductYield": item.product.product_yield,
},
"Quantity": item.quantity,
"Price": item.price,
}
)
return info