367 lines
12 KiB
Python
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
|