diff --git a/application/__pycache__/database_payloads.cpython-313.pyc b/application/__pycache__/database_payloads.cpython-313.pyc index 0d26499..82e08d3 100644 Binary files a/application/__pycache__/database_payloads.cpython-313.pyc and b/application/__pycache__/database_payloads.cpython-313.pyc differ diff --git a/application/administration/sql/CREATE/food_info.sql b/application/administration/sql/CREATE/food_info.sql index 0ec79a9..49d1ec7 100644 --- a/application/administration/sql/CREATE/food_info.sql +++ b/application/administration/sql/CREATE/food_info.sql @@ -5,5 +5,6 @@ CREATE TABLE IF NOT EXISTS %%site_name%%_food_info ( ingrediants TEXT [], nutrients JSONB, expires BOOLEAN, - default_expiration FLOAT8 + default_expiration FLOAT8, + UNIQUE(food_info_uuid) ); \ No newline at end of file diff --git a/application/administration/sql/CREATE/item_info.sql b/application/administration/sql/CREATE/item_info.sql index 54a985b..3cedeb0 100644 --- a/application/administration/sql/CREATE/item_info.sql +++ b/application/administration/sql/CREATE/item_info.sql @@ -9,6 +9,6 @@ CREATE TABLE IF NOt EXISTS %%site_name%%_item_info ( safety_stock FLOAT8, lead_time_days FLOAT8, ai_pick BOOLEAN, - prefixes INTEGER [], - UNIQUE(barcode) + prefixes INTEGER [] + UNIQUE(item_info_uuid) ); \ No newline at end of file diff --git a/application/administration/sql/CREATE/logistics_info.sql b/application/administration/sql/CREATE/logistics_info.sql index 22a52fa..bc53c31 100644 --- a/application/administration/sql/CREATE/logistics_info.sql +++ b/application/administration/sql/CREATE/logistics_info.sql @@ -6,7 +6,7 @@ CREATE TABLE IF NOT EXISTS %%site_name%%_logistics_info( primary_zone INTEGER NOT NULL, auto_issue_location INTEGER NOT NULL, auto_issue_zone INTEGER NOT NULL, - UNIQUE(barcode), + UNIQUE(logistics_info_uuid), CONSTRAINT fk_primary_location FOREIGN KEY(primary_location) REFERENCES %%site_name%%_locations(id), diff --git a/application/administration/sql/CREATE/transactions.sql b/application/administration/sql/CREATE/transactions.sql index 0ddefd6..6fef0e2 100644 --- a/application/administration/sql/CREATE/transactions.sql +++ b/application/administration/sql/CREATE/transactions.sql @@ -2,7 +2,7 @@ CREATE TABLE IF NOT EXISTS %%site_name%%_Transactions ( id SERIAL PRIMARY KEY, timestamp TIMESTAMP, logistics_info_id INTEGER NOT NULL, - barcode VARCHAR(255) NOT NULL, + barcode VARCHAR(255), name VARCHAR(255), transaction_type VARCHAR(255) NOT NULL, quantity FLOAT8 NOT NULL, diff --git a/application/database_payloads.py b/application/database_payloads.py index bea0256..e4254f9 100644 --- a/application/database_payloads.py +++ b/application/database_payloads.py @@ -45,10 +45,6 @@ class ItemInfoPayload: lead_time_days: float = 0.0 ai_pick: bool = False prefixes: list = field(default_factory=list) - - def __post_init__(self): - if not isinstance(self.barcode, str): - raise TypeError(f"barcode must be of type str; not {type(self.barcode)}") def payload(self): return ( diff --git a/application/recipes/__pycache__/database_recipes.cpython-313.pyc b/application/recipes/__pycache__/database_recipes.cpython-313.pyc index f51235d..72db4bb 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 index bdd998f..8096fd1 100644 Binary files a/application/recipes/__pycache__/recipe_processes.cpython-313.pyc 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 2dff7db..caa1dd5 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 6d29857..56e374d 100644 --- a/application/recipes/database_recipes.py +++ b/application/recipes/database_recipes.py @@ -126,6 +126,59 @@ def getPicturePath(site:str, payload:tuple): rows = cur.fetchone()[0] return rows +def selectSiteTuple(payload, convert=True): + """ payload (tuple): (site_name,) """ + site = () + database_config = config.config() + select_site_sql = f"SELECT * FROM sites WHERE site_name = %s;" + try: + with psycopg2.connect(**database_config) as conn: + with conn.cursor() as cur: + cur.execute(select_site_sql, payload) + rows = cur.fetchone() + if rows and convert: + site = postsqldb.tupleDictionaryFactory(cur.description, rows) + elif rows and not convert: + site = rows + except Exception as error: + raise postsqldb.DatabaseError(error, payload, select_site_sql) + return site + +def getZone(site:str, payload:tuple, convert:bool=True): + selected = () + database_config = config.config() + sql = f"SELECT * FROM {site}_zones WHERE id=%s;" + try: + with psycopg2.connect(**database_config) as conn: + 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 + return selected + except Exception as error: + raise postsqldb.DatabaseError(error, payload, sql) + + +def getLocation(site:str, payload:tuple, convert:bool=True): + selected = () + database_config = config.config() + sql = f"SELECT * FROM {site}_locations WHERE id=%s;" + try: + with psycopg2.connect(**database_config) as conn: + 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 + return selected + except Exception as error: + raise postsqldb.DatabaseError(error, payload, sql) + def selectItemLocationsTuple(site_name, payload, convert=True, conn=None): item_locations = () self_conn = False @@ -312,6 +365,158 @@ def insertTransactionsTuple(site, payload, convert=True, conn=None): raise postsqldb.DatabaseError(error, payload, sql) return transaction +def insertLogisticsInfoTuple(site, payload, convert=True, conn=None): + """ payload (tuple): (barcode[str], primary_location[str], auto_issue_location[str], dynamic_locations[jsonb], + location_data[jsonb], quantity_on_hand[float]) """ + logistics_info = () + self_conn = False + + with open(f"application/recipes/sql/insertLogisticsInfoTuple.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: + logistics_info = postsqldb.tupleDictionaryFactory(cur.description, rows) + elif rows and not convert: + logistics_info = rows + + if self_conn: + conn.commit() + conn.close() + + return logistics_info + + except Exception as error: + raise postsqldb.DatabaseError(error, payload, sql) + +def insertItemInfoTuple(site, payload, convert=True, conn=None): + """ payload (tuple): (barcode[str], linked_items[lst2pgarr], shopping_lists[lst2pgarr], recipes[lst2pgarr], groups[lst2pgarr], + packaging[str], uom[str], cost[float], safety_stock[float], lead_time_days[float], ai_pick[bool]) """ + item_info = () + self_conn = False + with open(f"application/recipes/sql/insertItemInfoTuple.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_info = postsqldb.tupleDictionaryFactory(cur.description, rows) + elif rows and not convert: + item_info = rows + if self_conn: + conn.commit() + conn.close() + + return item_info + except Exception as error: + raise postsqldb.DatabaseError(error, payload, sql) + +def insertFoodInfoTuple(site, payload, convert=True, conn=None): + """ payload (_type_): (ingrediants[lst2pgarr], food_groups[lst2pgarr], nutrients[jsonstr], expires[bool]) """ + food_info = () + self_conn = False + with open(f"application/recipes/sql/insertFoodInfoTuple.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: + food_info = postsqldb.tupleDictionaryFactory(cur.description, rows) + elif rows and not convert: + food_info = rows + + if self_conn: + conn.commit() + conn.close() + + return food_info + except Exception as error: + raise postsqldb.DatabaseError(error, payload, sql) + +def insertItemTuple(site, payload, convert=True, conn=None): + """ payload (tuple): (barcode[str], item_name[str], brand[int], description[str], + tags[lst2pgarr], links[jsonb], item_info_id[int], logistics_info_id[int], + food_info_id[int], row_type[str], item_type[str], search_string[str]) """ + item = () + self_conn = False + with open(f"application/recipes/sql/insertItemTuple.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 = postsqldb.tupleDictionaryFactory(cur.description, rows) + elif rows and not convert: + item = rows + + if self_conn: + conn.commit() + conn.close() + + return item + except Exception as error: + raise postsqldb.DatabaseError(error, payload, sql) + + +def insertItemLocationsTuple(site, payload, convert=True, conn=None): + """ payload (tuple): (part_id[int], location_id[int], quantity_on_hand[float], cost_layers[lst2pgarr]) """ + location = () + self_conn = False + database_config = config.config() + with open(f"application/recipes/sql/insertItemLocationsTuple.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: + location = postsqldb.tupleDictionaryFactory(cur.description, rows) + elif rows and not convert: + location = rows + + if self_conn: + conn.commit() + conn.close() + + return location + except Exception as error: + raise postsqldb.DatabaseError(error, payload, sql) + def postAddRecipe(site:str, payload:tuple, convert:bool=True): database_config = config.config() record = () diff --git a/application/recipes/recipe_processes.py b/application/recipes/recipe_processes.py index 5bd19a1..1b52075 100644 --- a/application/recipes/recipe_processes.py +++ b/application/recipes/recipe_processes.py @@ -1,8 +1,10 @@ import psycopg2 import datetime +import json from application import database_payloads, postsqldb from application.recipes import database_recipes +from application.items import database_items import config def postTransaction(site_name, user_id, data: dict, conn=None): @@ -137,3 +139,95 @@ def process_recipe_receipt(site_name, user_id, data:dict, conn=None): conn.close() return True, "" + +def postNewSkuFromRecipe(site_name: str, user_id: int, data: dict, conn=None): + """ data = {'name', 'subtype', 'qty', 'uom_id', 'main_link', 'cost'}""" + self_conn = False + if not conn: + database_config = config.config() + conn = psycopg2.connect(**database_config) + conn.autocommit = False + self_conn = True + + site = database_recipes.selectSiteTuple((site_name,)) + default_zone = database_recipes.getZone(site_name,(site['default_zone'], )) + default_location = database_recipes.getLocation(site_name, (site['default_primary_location'],)) + uuid = f"{default_zone['name']}@{default_location['name']}" + + # create logistics info + logistics_info = database_payloads.LogisticsInfoPayload( + barcode=None, + primary_location=site['default_primary_location'], + primary_zone=site['default_zone'], + auto_issue_location=site['default_auto_issue_location'], + auto_issue_zone=site['default_zone'] + ) + + # create item info + item_info = database_payloads.ItemInfoPayload(barcode=None) + + # create Food Info + food_info = database_payloads.FoodInfoPayload() + + logistics_info_id = 0 + item_info_id = 0 + food_info_id = 0 + brand_id = 1 + + + logistics_info = database_recipes.insertLogisticsInfoTuple(site_name, logistics_info.payload(), conn=conn) + item_info = database_recipes.insertItemInfoTuple(site_name, item_info.payload(), conn=conn) + food_info = database_recipes.insertFoodInfoTuple(site_name, food_info.payload(), conn=conn) + + name = data['name'] + name = name.replace("'", "@&apostraphe&") + links = {'main': data['main_link']} + search_string = f"&&{name}&&" + + + item = database_payloads.ItemsPayload( + barcode=None, + item_name=data['name'], + item_info_id=item_info['id'], + logistics_info_id=logistics_info['id'], + food_info_id=food_info['id'], + links=links, + brand=brand_id, + row_type="single", + item_type=data['subtype'], + search_string=search_string + ) + + item = database_recipes.insertItemTuple(site_name, item.payload(), conn=conn) + + with conn.cursor() as cur: + cur.execute(f"SELECT id FROM {site_name}_locations WHERE uuid=%s;", (uuid, )) + location_id = cur.fetchone()[0] + + database_payloads.ItemLocationPayload + item_location = database_payloads.ItemLocationPayload(item['id'], location_id) + database_recipes.insertItemLocationsTuple(site_name, item_location.payload(), conn=conn) + + + creation_tuple = database_payloads.TransactionPayload( + datetime.datetime.now(), + logistics_info['id'], + None, + item['item_name'], + "SYSTEM", + 0.0, + "Item added to the System!", + user_id, + {'location': uuid} + ) + + database_recipes.insertTransactionsTuple(site_name, creation_tuple.payload(), conn=conn) + + item_uuid = item['item_uuid'] + + if self_conn: + conn.commit() + conn.close() + return False, item_uuid + + return conn, item_uuid diff --git a/application/recipes/recipes_api.py b/application/recipes/recipes_api.py index 0a79bc6..bb08bc8 100644 --- a/application/recipes/recipes_api.py +++ b/application/recipes/recipes_api.py @@ -157,6 +157,34 @@ def postSKUItem(): return jsonify({'recipe': recipe, 'error': False, 'message': 'Recipe Item was added successful!'}) return jsonify({'recipe': recipe, 'error': True, 'message': f'method {request.method} is not allowed!'}) +@recipes_api.route('/api/postNewSKUItem', methods=["POST"]) +@access_api.login_required +def postNewSKUItem(): + recipe = {} + if request.method == "POST": + recipe_id = int(request.get_json()['recipe_id']) + site_name = session['selected_site'] + user_id = session['user_id'] + + _, item_uuid= recipe_processes.postNewSkuFromRecipe(site_name, user_id, request.get_json()) + + item = database_recipes.selectItemTupleByUUID(site_name, (item_uuid,)) + + recipe_item = db.RecipesTable.ItemPayload( + item_uuid=item_uuid, + rp_id=recipe_id, + item_type='sku', + item_name=request.get_json()['name'], + uom=request.get_json()['uom_id'], + qty=float(request.get_json()['qty']), + item_id=item['item_id'], + links={'main': request.get_json()['main_link']} + ) + database_recipes.postAddRecipeItem(site_name, recipe_item.payload()) + recipe = database_recipes.getRecipe(site_name, (recipe_id, )) + return jsonify({'recipe': recipe, 'error': False, 'message': 'Recipe Item was added successful!'}) + return jsonify({'recipe': recipe, 'error': True, 'message': f'method {request.method} is not allowed!'}) + @recipes_api.route('/postImage/', methods=["POST"]) @access_api.login_required def uploadImage(recipe_id): diff --git a/application/recipes/sql/insertFoodInfoTuple.sql b/application/recipes/sql/insertFoodInfoTuple.sql new file mode 100644 index 0000000..08afdf2 --- /dev/null +++ b/application/recipes/sql/insertFoodInfoTuple.sql @@ -0,0 +1,4 @@ +INSERT INTO %%site_name%%_food_info +(ingrediants, food_groups, nutrients, expires, default_expiration) +VALUES (%s, %s, %s, %s, %s) +RETURNING *; \ No newline at end of file diff --git a/application/recipes/sql/insertItemInfoTuple.sql b/application/recipes/sql/insertItemInfoTuple.sql new file mode 100644 index 0000000..154e9d3 --- /dev/null +++ b/application/recipes/sql/insertItemInfoTuple.sql @@ -0,0 +1,4 @@ +INSERT INTO %%site_name%%_item_info +(barcode, packaging, uom_quantity, uom, cost, safety_stock, lead_time_days, ai_pick, prefixes) +VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s) +RETURNING *; \ No newline at end of file diff --git a/application/recipes/sql/insertItemLocationsTuple.sql b/application/recipes/sql/insertItemLocationsTuple.sql new file mode 100644 index 0000000..67abbd4 --- /dev/null +++ b/application/recipes/sql/insertItemLocationsTuple.sql @@ -0,0 +1,4 @@ +INSERT INTO %%site_name%%_item_locations +(part_id, location_id, quantity_on_hand, cost_layers) +VALUES (%s, %s, %s, %s) +RETURNING *; \ No newline at end of file diff --git a/application/recipes/sql/insertItemTuple.sql b/application/recipes/sql/insertItemTuple.sql new file mode 100644 index 0000000..4c9b940 --- /dev/null +++ b/application/recipes/sql/insertItemTuple.sql @@ -0,0 +1,5 @@ +INSERT INTO %%site_name%%_items +(barcode, item_name, brand, description, tags, links, item_info_id, logistics_info_id, +food_info_id, row_type, item_type, search_string) +VALUES(%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s) +RETURNING *; \ No newline at end of file diff --git a/application/recipes/sql/insertLogisticsInfoTuple.sql b/application/recipes/sql/insertLogisticsInfoTuple.sql new file mode 100644 index 0000000..312ee1c --- /dev/null +++ b/application/recipes/sql/insertLogisticsInfoTuple.sql @@ -0,0 +1,4 @@ +INSERT INTO %%site_name%%_logistics_info +(barcode, primary_location, primary_zone, auto_issue_location, auto_issue_zone) +VALUES (%s, %s, %s, %s, %s) +RETURNING *; \ No newline at end of file diff --git a/application/recipes/static/js/recipeEditHandler.js b/application/recipes/static/js/recipeEditHandler.js index 0213a97..f02b709 100644 --- a/application/recipes/static/js/recipeEditHandler.js +++ b/application/recipes/static/js/recipeEditHandler.js @@ -254,6 +254,47 @@ async function addSKUItem(item_id) { UIkit.modal(document.getElementById('itemsModal')).hide(); } +async function addNewSKUItem() { + let newSKUName = document.getElementById('newSKUName').value + let newSKUSubtype = document.getElementById('newSKUSubtype').value + let newSKUQty = parseFloat(document.getElementById('newSKUQty').value) + let newSKUUOM = parseInt(document.getElementById('newSKUUOM').value) + let newWeblink = document.getElementById('newWeblink').value + let newSKUCost = parseFloat(document.getElementById('newSKUCost').value) + + + const response = await fetch(`/recipes/api/postNewSKUItem`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + recipe_id: recipe.id, + name: newSKUName, + subtype: newSKUSubtype, + qty: newSKUQty, + uom_id: newSKUUOM, + main_link: newWeblink, + cost: newSKUCost + }), + }); + 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 + }); + recipe = data.recipe + await replenishRecipe() + UIkit.modal(document.getElementById('addNewSKUItem')).hide(); +} + + let updated = {} async function postUpdate() { let description = document.getElementById('recipeDescription').value @@ -326,6 +367,18 @@ async function deleteInstruction(index){ await replenishInstructions() } + +async function openNewSKUModal() { + let itemsModal = document.getElementById('addNewSKUItem') + document.getElementById('newSKUName').value = "" + document.getElementById('newSKUSubtype').value = "FOOD" + document.getElementById('newSKUQty').value = 1 + document.getElementById('newSKUUOM').value = "1" + document.getElementById('newWeblink').value = "" + document.getElementById('newSKUCost').value = 0.00 + UIkit.modal(itemsModal).show(); +} + let pagination_current = 1; let pagination_end = 10; let search_string = ""; diff --git a/application/recipes/templates/recipe_edit.html b/application/recipes/templates/recipe_edit.html index 769a4aa..36dacd5 100644 --- a/application/recipes/templates/recipe_edit.html +++ b/application/recipes/templates/recipe_edit.html @@ -174,6 +174,8 @@
+ +
@@ -318,6 +320,69 @@
+ +
+
+ +
+

Add New Item...

+
+
+
+

Adding a new sku with both create a new item in the system with the information you provide and also add it to the recipe. +

+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ + +
+
+ +
+ +
+
+
+ + +
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+