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