brewman/brewman/brewman/routers/recipe.py

367 lines
12 KiB
Python
Raw Normal View History

import datetime
import time
2020-10-07 15:18:43 +00:00
import uuid
from decimal import Decimal, InvalidOperation
2020-10-07 15:18:43 +00:00
from brewman.models.master import CostCentre, Product, Recipe, RecipeItem
from brewman.models.voucher import Inventory, Journal, Voucher, VoucherType
from fastapi import APIRouter
2020-10-07 15:18:43 +00:00
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 = (
2020-10-07 16:59:24 +00:00
request.dbsession.query(Product)
.filter(Product.id == uuid.UUID(json["Product"]["ProductID"]))
.first()
)
try:
2020-10-07 16:59:24 +00:00
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:
2020-10-07 16:59:24 +00:00
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"]:
2020-10-07 16:59:24 +00:00
product = (
request.dbsession.query(Product)
.filter(Product.id == uuid.UUID(item["Product"]["ProductID"]))
.first()
)
quantity = round(Decimal(item["Quantity"]), 2)
if product.is_purchased:
2020-10-07 16:59:24 +00:00
ingredient_cost = get_purchased_product_cost(
product.id, valid_from, valid_to, request.dbsession
)
else:
2020-10-07 16:59:24 +00:00
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
2020-10-07 16:59:24 +00:00
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()
2020-05-12 04:25:33 +00:00
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(
2020-10-07 15:18:43 +00:00
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(
2020-10-07 16:59:24 +00:00
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:
2020-10-07 16:59:24 +00:00
ingredient_cost = get_purchased_product_cost(
item.product_id, start_date, finish_date, dbsession
)
else:
2020-10-07 16:59:24 +00:00
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
2020-10-07 16:59:24 +00:00
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:
2020-10-07 16:59:24 +00:00
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:
2020-10-07 16:59:24 +00:00
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):
2020-10-07 16:59:24 +00:00
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,
2020-05-12 04:25:33 +00:00
"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