diff --git a/application/poe/__pycache__/poe_processes.cpython-313.pyc b/application/poe/__pycache__/poe_processes.cpython-313.pyc index e02cea0..a0870be 100644 Binary files a/application/poe/__pycache__/poe_processes.cpython-313.pyc and b/application/poe/__pycache__/poe_processes.cpython-313.pyc differ diff --git a/application/recipes/__pycache__/database_recipes.cpython-313.pyc b/application/recipes/__pycache__/database_recipes.cpython-313.pyc index 147a3b5..6d5ab8f 100644 Binary files a/application/recipes/__pycache__/database_recipes.cpython-313.pyc and b/application/recipes/__pycache__/database_recipes.cpython-313.pyc differ diff --git a/application/recipes/__pycache__/recipe_processes.cpython-313.pyc b/application/recipes/__pycache__/recipe_processes.cpython-313.pyc new file mode 100644 index 0000000..05e07d3 Binary files /dev/null and b/application/recipes/__pycache__/recipe_processes.cpython-313.pyc differ diff --git a/application/recipes/__pycache__/recipes_api.cpython-313.pyc b/application/recipes/__pycache__/recipes_api.cpython-313.pyc index 241808b..2dff7db 100644 Binary files a/application/recipes/__pycache__/recipes_api.cpython-313.pyc and b/application/recipes/__pycache__/recipes_api.cpython-313.pyc differ diff --git a/application/recipes/database_recipes.py b/application/recipes/database_recipes.py index b6fad27..18251a4 100644 --- a/application/recipes/database_recipes.py +++ b/application/recipes/database_recipes.py @@ -91,20 +91,30 @@ def getRecipes(site:str, payload:tuple, convert=True): raise postsqldb.DatabaseError(error, payload, sql) return recordset, count -def getRecipe(site, payload:tuple, convert=True): - database_config = config.config() +def getRecipe(site, payload:tuple, convert=True, conn=None): + self_conn = False + record = () with open(f"application/recipes/sql/getRecipeByID.sql", "r+") as file: sql = file.read().replace("%%site_name%%", site) try: - with psycopg2.connect(**database_config) as conn: - with conn.cursor() as cur: - cur.execute(sql, payload) - rows = cur.fetchone() - if rows and convert: - record = postsqldb.tupleDictionaryFactory(cur.description, rows) - if rows and not convert: - record = rows - return record + if not conn: + database_config = config.config() + conn = psycopg2.connect(**database_config) + conn.autocommit = True + self_conn = True + + with conn.cursor() as cur: + cur.execute(sql, payload) + rows = cur.fetchone() + if rows and convert: + record = postsqldb.tupleDictionaryFactory(cur.description, rows) + if rows and not convert: + record = rows + + if self_conn: + conn.close() + + return record except (Exception, psycopg2.DatabaseError) as error: raise postsqldb.DatabaseError(error, payload, sql) @@ -116,6 +126,164 @@ def getPicturePath(site:str, payload:tuple): rows = cur.fetchone()[0] return rows +def selectItemLocationsTuple(site_name, payload, convert=True, conn=None): + item_locations = () + self_conn = False + select_item_location_sql = f"SELECT * FROM {site_name}_item_locations WHERE part_id = %s AND location_id = %s;" + try: + if not conn: + database_config = config.config() + conn = psycopg2.connect(**database_config) + conn.autocommit = True + self_conn = True + + with conn.cursor() as cur: + cur.execute(select_item_location_sql, payload) + rows = cur.fetchone() + if rows and convert: + item_locations = postsqldb.tupleDictionaryFactory(cur.description, rows) + elif rows and not convert: + item_locations = rows + + if self_conn: + conn.commit() + conn.close() + + return item_locations + except Exception as error: + return error + +def selectCostLayersTuple(site_name, payload, convert=True): + cost_layers = () + database_config = config.config() + select_cost_layers_sql = f"SELECT cl.* FROM {site_name}_item_locations il JOIN {site_name}_cost_layers cl ON cl.id = ANY(il.cost_layers) where il.id=%s;" + try: + with psycopg2.connect(**database_config) as conn: + with conn.cursor() as cur: + cur.execute(select_cost_layers_sql, payload) + rows = cur.fetchall() + if rows and convert: + cost_layers = rows + cost_layers = [postsqldb.tupleDictionaryFactory(cur.description, layer) for layer in rows] + elif rows and not convert: + cost_layers = rows + return cost_layers + except Exception as error: + return error + +def selectLocationsTuple(site, payload, convert=True, conn=None): + selected = () + self_conn = False + sql = f"SELECT * FROM {site}_locations WHERE id=%s;" + try: + if not conn: + database_config = config.config() + conn = psycopg2.connect(**database_config) + conn.autocommit = True + self_conn = True + + with conn.cursor() as cur: + cur.execute(sql, payload) + rows = cur.fetchone() + if rows and convert: + selected = postsqldb.tupleDictionaryFactory(cur.description, rows) + elif rows and not convert: + selected = rows + + if self_conn: + conn.commit() + conn.close() + + return selected + except Exception as error: + raise postsqldb.DatabaseError(error, payload, sql) + +def selectItemTupleByUUID(site, payload, convert=True, conn=None): + selected = () + self_conn = False + with open(f"application/recipes/sql/getItemTupleByUUID.sql", "r+") as file: + sql = file.read().replace("%%site_name%%", site) + try: + if not conn: + database_config = config.config() + conn = psycopg2.connect(**database_config) + conn.autocommit = True + self_conn = True + + with conn.cursor() as cur: + cur.execute(sql, payload) + rows = cur.fetchone() + if rows and convert: + selected = postsqldb.tupleDictionaryFactory(cur.description, rows) + elif rows and not convert: + selected = rows + + if self_conn: + conn.commit() + conn.close() + + return selected + except Exception as error: + raise postsqldb.DatabaseError(error, payload, sql) + +def insertCostLayersTuple(site, payload, convert=True, conn=None): + cost_layer = () + self_conn = False + + with open(f"application/recipes/sql/insertCostLayersTuple.sql", "r+") as file: + sql = file.read().replace("%%site_name%%", site) + try: + if not conn: + database_config = config.config() + conn = psycopg2.connect(**database_config) + conn.autocommit = True + self_conn = True + + with conn.cursor() as cur: + cur.execute(sql, payload) + rows = cur.fetchone() + if rows and convert: + cost_layer = postsqldb.tupleDictionaryFactory(cur.description, rows) + elif rows and not convert: + cost_layer = rows + + if self_conn: + conn.commit() + conn.close() + + return cost_layer + except Exception as error: + raise postsqldb.DatabaseError(error, payload, sql) + +def insertTransactionsTuple(site, payload, convert=True, conn=None): + # payload (tuple): (timestamp[timestamp], logistics_info_id[int], barcode[str], name[str], + transaction = () + self_conn = False + with open(f"application/recipes/sql/insertTransactionsTuple.sql", "r+") as file: + sql = file.read().replace("%%site_name%%", site) + try: + if not conn: + database_config = config.config() + conn = psycopg2.connect(**database_config) + conn.autocommit = True + self_conn = True + + with conn.cursor() as cur: + cur.execute(sql, payload) + rows = cur.fetchone() + if rows and convert: + transaction = postsqldb.tupleDictionaryFactory(cur.description, rows) + elif rows and not convert: + transaction = rows + + if self_conn: + conn.commit() + conn.close() + + except Exception as error: + raise postsqldb.DatabaseError(error, payload, sql) + return transaction + def postAddRecipe(site:str, payload:tuple, convert:bool=True): database_config = config.config() record = () @@ -200,6 +368,93 @@ def postDeleteRecipeItem(site:str, payload:tuple, convert:bool=True): deleted = rows return deleted +def updateCostLayersTuple(site, payload, convert=True, conn=None): + cost_layer = () + self_conn = False + + set_clause, values = postsqldb.updateStringFactory(payload['update']) + values.append(payload['id']) + sql = f"UPDATE {site}_cost_layers SET {set_clause} WHERE id=%s RETURNING *;" + + try: + if not conn: + database_config = config.config() + conn = psycopg2.connect(**database_config) + conn.autocommit = True + self_conn = True + + with conn.cursor() as cur: + cur.execute(sql, values) + rows = cur.fetchone() + if rows and convert: + cost_layer = postsqldb.tupleDictionaryFactory(cur.description, rows) + elif rows and not convert: + cost_layer = rows + + if self_conn: + conn.commit() + conn.close() + + return cost_layer + except Exception as error: + return error + +def updateItemLocation(site, payload, convert=True, conn=None): + item_location = () + self_conn = False + + with open(f"application/recipes/sql/updateItemLocation.sql", "r+") as file: + sql = file.read().replace("%%site_name%%", site) + try: + if not conn: + database_config = config.config() + conn = psycopg2.connect(**database_config) + conn.autocommit = True + self_conn = True + + with conn.cursor() as cur: + cur.execute(sql, payload) + rows = cur.fetchone() + if rows and convert: + item_location = postsqldb.tupleDictionaryFactory(cur.description, rows) + elif rows and not convert: + item_location = rows + + if self_conn: + conn.commit() + conn.close() + + return item_location + except Exception as error: + return error + +def deleteCostLayersTuple(site, payload, convert=True, conn=None): + deleted = () + self_conn = False + sql = f"WITH deleted_rows AS (DELETE FROM {site}_cost_layers WHERE id IN ({','.join(['%s'] * len(payload))}) RETURNING *) SELECT * FROM deleted_rows;" + try: + if not conn: + database_config = config.config() + conn = psycopg2.connect(**database_config) + conn.autocommit = True + self_conn = True + + with conn.cursor() as cur: + cur.execute(sql, payload) + rows = cur.fetchall() + if rows and convert: + deleted = [postsqldb.tupleDictionaryFactory(cur.description, r) for r in rows] + elif rows and not convert: + deleted = rows + + if self_conn: + conn.commit() + conn.close() + + return deleted + except Exception as error: + raise postsqldb.DatabaseError(error, payload, sql) + def deleteRecipe(site:str, payload:tuple, convert:bool=True): database_config = config.config() deleted = () diff --git a/application/recipes/recipe_processes.py b/application/recipes/recipe_processes.py new file mode 100644 index 0000000..0894b20 --- /dev/null +++ b/application/recipes/recipe_processes.py @@ -0,0 +1,143 @@ +import psycopg2 +import datetime + +from application import database_payloads, postsqldb +from application.recipes import database_recipes +import config + +def postTransaction(site_name, user_id, data: dict, conn=None): + """ dict_keys(['item_id', 'logistics_info_id', 'barcode', 'item_name', 'transaction_type', + 'quantity', 'description', 'cost', 'vendor', 'expires', 'location_id'])""" + def quantityFactory(quantity_on_hand:float, quantity:float, transaction_type:str): + if transaction_type == "Adjust In": + quantity_on_hand += quantity + return quantity_on_hand + if transaction_type == "Adjust Out": + quantity_on_hand -= quantity + return quantity_on_hand + raise Exception("The transaction type is wrong!") + + self_conn = False + if not conn: + database_config = config.config() + conn = psycopg2.connect(**database_config) + conn.autocommit = False + self_conn = True + + + transaction_time = datetime.datetime.now() + + cost_layer = postsqldb.CostLayerPayload( + aquisition_date=transaction_time, + quantity=float(data['quantity']), + cost=float(data['cost']), + currency_type="USD", + vendor=int(data['vendor']), + expires=data['expires'] + ) + transaction = postsqldb.TransactionPayload( + timestamp=transaction_time, + logistics_info_id=int(data['logistics_info_id']), + barcode=data['barcode'], + name=data['item_name'], + transaction_type=data['transaction_type'], + quantity=float(data['quantity']), + description=data['description'], + user_id=user_id, + ) + + location = database_recipes.selectItemLocationsTuple(site_name, (data['item_id'], data['location_id']), conn=conn) + site_location = database_recipes.selectLocationsTuple(site_name, (location['location_id'], ), conn=conn) + print(location) + cost_layers: list = location['cost_layers'] + if data['transaction_type'] == "Adjust In": + cost_layer = database_recipes.insertCostLayersTuple(site_name, cost_layer.payload(), conn=conn) + cost_layers.append(cost_layer['id']) + + if data['transaction_type'] == "Adjust Out": + if float(location['quantity_on_hand']) < float(data['quantity']): + raise Exception(f"The quantity on hand for {data['item_name']} in {site_location['uuid']} is not enough to satisfy your transaction!") + cost_layers = database_recipes.selectCostLayersTuple(site_name, payload=(location['id'], )) + + new_cost_layers = [] + qty = float(data['quantity']) + for layer in cost_layers: + if qty == 0.0: + new_cost_layers.append(layer['id']) + elif qty >= float(layer['quantity']): + qty -= float(layer['quantity']) + layer['quantity'] = 0.0 + else: + layer['quantity'] -= qty + new_cost_layers.append(layer['id']) + database_recipes.updateCostLayersTuple(site_name, {'id': layer['id'], 'update': {'quantity': layer['quantity']}}, conn=conn) + qty = 0.0 + + if layer['quantity'] == 0.0: + database_recipes.deleteCostLayersTuple(site_name, (layer['id'],), conn=conn) + + cost_layers = new_cost_layers + + quantity_on_hand = quantityFactory(float(location['quantity_on_hand']), data['quantity'], data['transaction_type']) + + updated_item_location_payload = (cost_layers, quantity_on_hand, data['item_id'], data['location_id']) + database_recipes.updateItemLocation(site_name, updated_item_location_payload, conn=conn) + + #site_location = database_recipes.selectLocationsTuple(site_name, (location['location_id'], ), conn=conn) + + transaction.data = {'location': site_location['uuid']} + + database_recipes.insertTransactionsTuple(site_name, transaction.payload(), conn=conn) + + if self_conn: + conn.commit() + conn.close() + return conn + + return {"error": False, "message":f"Transaction Successful!"} + +def process_recipe_receipt(site_name, user_id, data:dict, conn=None): + """data={'recipe_id': recipe_id}""" + + self_conn = False + if not conn: + database_config = config.config() + conn = psycopg2.connect(**database_config) + conn.autocommit = False + self_conn = True + + recipe = database_recipes.getRecipe(site_name, (data['recipe_id'],), conn=conn) + + sku_items = [rp_item for rp_item in recipe['recipe_items'] if rp_item['item_type'] == "sku"] + for item in sku_items: + """ dict_keys(['item_id', 'logistics_info_id', 'barcode', 'item_name', 'transaction_type', + 'quantity', 'description', 'cost', 'vendor', 'expires', 'location_id'])""" + item_stuff = database_recipes.selectItemTupleByUUID(site_name, (item['item_uuid'],), conn=conn) + print(item_stuff) + payload = { + 'item_id': item_stuff['item_id'], + 'logistics_info_id': item_stuff['logistics_info_id'], + 'barcode': "", + 'item_name': item_stuff['item_name'], + 'transaction_type': "Adjust Out", + 'quantity': item['qty'], + 'description': f"Recipe Receipt - {data['recipe_id']}", + 'cost': 0.00, + 'vendor': 0, + 'expires': False, + 'location_id': item_stuff['auto_issue_location'] + } + print(payload) + + try: + postTransaction(site_name, user_id, payload, conn=conn) + except Exception as error: + conn.rollback() + conn.close() + return False, str(error) + + if self_conn: + conn.commit() + conn.close() + + return True, "" diff --git a/application/recipes/recipes_api.py b/application/recipes/recipes_api.py index 45a4b83..0a79bc6 100644 --- a/application/recipes/recipes_api.py +++ b/application/recipes/recipes_api.py @@ -8,7 +8,7 @@ import math import main import webpush from application.access_module import access_api -from application.recipes import database_recipes +from application.recipes import database_recipes, recipe_processes from application import postsqldb as db recipes_api = Blueprint('recipes_api', __name__, template_folder="templates", static_folder="static") @@ -198,4 +198,17 @@ def saveRecipeItem(): updated_line = database_recipes.postUpdateRecipeItem(site_name, {'id': id, 'update': update}) recipe = database_recipes.getRecipe(site_name, (int(updated_line['rp_id']), )) return jsonify({'recipe': recipe, 'error': False, 'message': f'Recipe Item {updated_line['item_name']} was updated successful!'}) - return jsonify({'recipe': recipe, 'error': True, 'message': f'method {request.method} not allowed!'}) \ No newline at end of file + return jsonify({'recipe': recipe, 'error': True, 'message': f'method {request.method} not allowed!'}) + + +@recipes_api.route('/api/receiptRecipe', methods=["POST"]) +@access_api.login_required +def receiptRecipe(): + if request.method == "POST": + site_name = session['selected_site'] + user_id = session['user_id'] + status, message = recipe_processes.process_recipe_receipt(site_name, user_id, request.get_json()) + if not status: + return jsonify(status=400, message=message) + return jsonify(status=201, message="Recipe Transacted Successfully!") + return jsonify(status=405, message=f"{request.method} is not an allowed method on this endpoint!") \ No newline at end of file diff --git a/application/recipes/sql/getItemTupleByUUID.sql b/application/recipes/sql/getItemTupleByUUID.sql new file mode 100644 index 0000000..e481bc8 --- /dev/null +++ b/application/recipes/sql/getItemTupleByUUID.sql @@ -0,0 +1,6 @@ +SELECT items.id AS item_id, +items.item_name as item_name, +items.logistics_info_id as logistics_info_id, +lginf.auto_issue_location as auto_issue_location +FROM %%site_name%%_items items +LEFT JOIN %%site_name%%_logistics_info lginf ON lginf.id = items.logistics_info_id WHERE item_uuid=%s; \ No newline at end of file diff --git a/application/recipes/sql/insertCostLayersTuple.sql b/application/recipes/sql/insertCostLayersTuple.sql new file mode 100644 index 0000000..c3d381f --- /dev/null +++ b/application/recipes/sql/insertCostLayersTuple.sql @@ -0,0 +1,4 @@ +INSERT INTO %%site_name%%_cost_layers +(aquisition_date, quantity, cost, currency_type, expires, vendor) +VALUES (%s, %s, %s, %s, %s, %s) +RETURNING *; \ No newline at end of file diff --git a/application/recipes/sql/insertTransactionsTuple.sql b/application/recipes/sql/insertTransactionsTuple.sql new file mode 100644 index 0000000..d8ee48d --- /dev/null +++ b/application/recipes/sql/insertTransactionsTuple.sql @@ -0,0 +1,5 @@ +INSERT INTO %%site_name%%_transactions +(timestamp, logistics_info_id, barcode, name, transaction_type, +quantity, description, user_id, data) +VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s) +RETURNING *; \ No newline at end of file diff --git a/application/recipes/sql/updateItemLocation.sql b/application/recipes/sql/updateItemLocation.sql new file mode 100644 index 0000000..7316d94 --- /dev/null +++ b/application/recipes/sql/updateItemLocation.sql @@ -0,0 +1,4 @@ +UPDATE %%site_name%%_item_locations +SET cost_layers = %s, quantity_on_hand = %s +WHERE part_id=%s AND location_id=%s +RETURNING *; \ No newline at end of file diff --git a/application/recipes/static/js/recipeViewHandler.js b/application/recipes/static/js/recipeViewHandler.js index 4c6ad65..6ad1f26 100644 --- a/application/recipes/static/js/recipeViewHandler.js +++ b/application/recipes/static/js/recipeViewHandler.js @@ -21,6 +21,7 @@ async function replenishRecipe() { await replenishIngrediantsTable() await replenishInstructions() + await replenishTransactionsTable() await getImage() @@ -32,16 +33,21 @@ async function replenishIngrediantsTable() { for(let i=0; iHave` - } else { + if (qty_needed <= quantity_on_hand && item_type === "sku"){ + markerCell.innerHTML = `On Hand` + } else if (qty_needed > quantity_on_hand && item_type === "sku") { markerCell.innerHTML = `Missing` + } else { + markerCell.innerHTML = "" } - let nameCell = document.createElement('td') nameCell.innerHTML = `${recipe.recipe_items[i].item_name}` @@ -54,6 +60,27 @@ async function replenishIngrediantsTable() { } +async function replenishTransactionsTable() { + let receiptRecipeTableBody = document.getElementById('receiptRecipeTableBody') + receiptRecipeTableBody.innerHTML = "" + + for(let i=0; i < recipe.recipe_items.length; i++){ + if (recipe.recipe_items[i].item_type === "sku"){ + let tableRow = document.createElement('tr') + + let nameCell = document.createElement('td') + nameCell.innerHTML = `${recipe.recipe_items[i].item_name}` + + let qtyUOMCell = document.createElement('td') + qtyUOMCell.innerHTML = `${recipe.recipe_items[i].qty}` + + tableRow.append(nameCell, qtyUOMCell) + receiptRecipeTableBody.append(tableRow) + } + } + +} + async function replenishInstructions() { let tileList = document.getElementById('tileList') tileList.innerHTML = "" @@ -89,4 +116,28 @@ async function getImage(){ const imageURL = URL.createObjectURL(imageBlob); document.getElementById('recipeImage').src = imageURL; }); +} + +async function receiptRecipe(){ + let recipe_id = recipe.id + const response = await fetch(`/recipes/api/receiptRecipe`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + recipe_id: recipe_id + }), + }); + data = await response.json() + message_type = "primary" + if(data.error){ + message_type = "danger" + } + UIkit.notification({ + message: data.message, + status: message_type, + pos: 'top-right', + timeout: 5000 + }); } \ No newline at end of file diff --git a/application/recipes/templates/recipe_view.html b/application/recipes/templates/recipe_view.html index d2edb8b..50048f4 100644 --- a/application/recipes/templates/recipe_view.html +++ b/application/recipes/templates/recipe_view.html @@ -140,11 +140,36 @@
- +
+
+
+ + +
+
+

Recipe Receipt Transaction

+

You are about to receipt these items from the system, please confirm before completing these Transaction as once they have been completed + this cannot be reversed.

+ + + + + + + + + +
ItemQty
+

+ + +

+
+