From aec8f85a4dc510e8814d0d04759b25b73c602195 Mon Sep 17 00:00:00 2001 From: Jadowyne Ulve Date: Sat, 31 May 2025 17:37:17 -0500 Subject: [PATCH] Transferred posting new item to new api --- application/database_payloads.py | 461 ++++++++++++++++++ application/items/database_items.py | 207 +++++++- application/items/items_API.py | 96 ++-- application/items/items_processes.py | 93 ++++ application/items/sql/insertFoodInfoTuple.sql | 4 + application/items/sql/insertItemInfoTuple.sql | 4 + .../sql/insertItemLocationsTuple copy.sql | 4 + application/items/sql/insertItemTuple.sql | 5 + .../items/sql/insertLogisticsInfoTuple.sql | 4 + application/items/static/ItemListHandler.js | 8 +- application/items/static/itemEditHandler.js | 34 +- .../items/static/transactionsHandler.js | 4 +- application/items/templates/item_new.html | 2 +- application/items/templates/transactions.html | 2 +- application/postsqldb.py | 61 +++ webserver.py | 19 - 16 files changed, 927 insertions(+), 81 deletions(-) create mode 100644 application/database_payloads.py create mode 100644 application/items/sql/insertFoodInfoTuple.sql create mode 100644 application/items/sql/insertItemInfoTuple.sql create mode 100644 application/items/sql/insertItemLocationsTuple copy.sql create mode 100644 application/items/sql/insertItemTuple.sql create mode 100644 application/items/sql/insertLogisticsInfoTuple.sql diff --git a/application/database_payloads.py b/application/database_payloads.py new file mode 100644 index 0000000..ba7907e --- /dev/null +++ b/application/database_payloads.py @@ -0,0 +1,461 @@ +from dataclasses import dataclass, field +import json, datetime +from database import lst2pgarr + +@dataclass +class LogisticsInfoPayload: + barcode: str + primary_location: int + primary_zone: int + auto_issue_location: int + auto_issue_zone: int + + def payload(self): + return (self.barcode, + self.primary_location, + self.primary_zone, + self.auto_issue_location, + self.auto_issue_zone) + +@dataclass +class ItemInfoPayload: + barcode: str + packaging: str = "" + uom_quantity: float = 1.0 + uom: int = 1 + cost: float = 0.0 + safety_stock: float = 0.0 + 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 ( + self.barcode, + self.packaging, + self.uom_quantity, + self.uom, + self.cost, + self.safety_stock, + self.lead_time_days, + self.ai_pick, + lst2pgarr(self.prefixes) + ) + +@dataclass +class FoodInfoPayload: + food_groups: list = field(default_factory=list) + ingrediants: list = field(default_factory=list) + nutrients: dict = field(default_factory=dict) + expires: bool = False + default_expiration: float = 0.0 + + def payload(self): + return ( + lst2pgarr(self.food_groups), + lst2pgarr(self.ingrediants), + json.dumps(self.nutrients), + self.expires, + self.default_expiration + ) + + +@dataclass +class ItemsPayload: + barcode: str + item_name: str + item_info_id: int + logistics_info_id: int + food_info_id: int + brand: int = 0 + description: str = "" + tags: list = field(default_factory=list) + links: dict = field(default_factory=dict) + row_type: str = "" + item_type: str = "" + search_string: str ="" + + + def payload(self): + return ( + self.barcode, + self.item_name, + self.brand, + self.description, + lst2pgarr(self.tags), + json.dumps(self.links), + self.item_info_id, + self.logistics_info_id, + self.food_info_id, + self.row_type, + self.item_type, + self.search_string + ) + + # done +@dataclass +class TransactionPayload: + timestamp: datetime.datetime + logistics_info_id: int + barcode: str + name: str + transaction_type: str + quantity: float + description: str + user_id: int + data: dict = field(default_factory=dict) + + def payload(self): + return ( + self.timestamp, + self.logistics_info_id, + self.barcode, + self.name, + self.transaction_type, + self.quantity, + self.description, + self.user_id, + json.dumps(self.data) + ) + +@dataclass +class CostLayerPayload: + aquisition_date: datetime.datetime + quantity: float + cost: float + currency_type: str + vendor: int = 0 + expires: datetime.datetime = None + + def payload(self): + return ( + self.aquisition_date, + self.quantity, + self.cost, + self.currency_type, + self.expires, + self.vendor + ) + +@dataclass +class ItemLinkPayload: + barcode: str + link: int + data: dict = field(default_factory=dict) + conv_factor: float = 1 + + def __post_init__(self): + if not isinstance(self.barcode, str): + raise TypeError(f"barcode must be of type str; not {type(self.barocde)}") + if not isinstance(self.link, int): + raise TypeError(f"link must be of type str; not {type(self.link)}") + + def payload(self): + return ( + self.barcode, + self.link, + json.dumps(self.data), + self.conv_factor + ) + +@dataclass +class GroupPayload: + name: str + description: str + group_type: str = "plain" + + def payload(self): + return ( + self.name, + self.description, + self.group_type + ) + +@dataclass +class GroupItemPayload: + uuid: str + gr_id: int + item_type: str + item_name:str + uom: str + qty: float = 0.0 + item_id: int = None + links: dict = field(default_factory=dict) + + def payload(self): + return ( + self.uuid, + self.gr_id, + self.item_type, + self.item_name, + self.uom, + self.qty, + self.item_id, + json.dumps(self.links) + ) + +@dataclass +class RecipeItemPayload: + uuid: str + rp_id: int + item_type: str + item_name:str + uom: str + qty: float = 0.0 + item_id: int = None + links: dict = field(default_factory=dict) + + def payload(self): + return ( + self.uuid, + self.rp_id, + self.item_type, + self.item_name, + self.uom, + self.qty, + self.item_id, + json.dumps(self.links) + ) + +@dataclass +class RecipePayload: + name: str + author: int + description: str + creation_date: datetime.datetime = field(init=False) + instructions: list = field(default_factory=list) + picture_path: str = "" + + def __post_init__(self): + self.creation_date = datetime.datetime.now() + + def payload(self): + return ( + self.name, + self.author, + self.description, + self.creation_date, + lst2pgarr(self.instructions), + self.picture_path + ) + +@dataclass +class ReceiptItemPayload: + type: str + receipt_id: int + barcode: str + name: str + qty: float = 1.0 + uom: str = "each" + data: dict = field(default_factory=dict) + status: str = "Unresolved" + + def payload(self): + return ( + self.type, + self.receipt_id, + self.barcode, + self.name, + self.qty, + self.uom, + json.dumps(self.data), + self.status + ) + +@dataclass +class ReceiptPayload: + receipt_id: str + receipt_status: str = "Unresolved" + date_submitted: datetime.datetime = field(init=False) + submitted_by: int = 0 + vendor_id: int = 1 + files: dict = field(default_factory=dict) + + def __post_init__(self): + self.date_submitted = datetime.datetime.now() + + def payload(self): + return ( + self.receipt_id, + self.receipt_status, + self.date_submitted, + self.submitted_by, + self.vendor_id, + json.dumps(self.files) + ) + +@dataclass +class ShoppingListItemPayload: + uuid: str + sl_id: int + item_type: str + item_name: str + uom: str + qty: float + item_id: int = None + links: dict = field(default_factory=dict) + + def payload(self): + return ( + self.uuid, + self.sl_id, + self.item_type, + self.item_name, + self.uom, + self.qty, + self.item_id, + json.dumps(self.links) + ) + +@dataclass +class ShoppingListPayload: + name: str + description: str + author: int + type: str = "plain" + creation_date: datetime.datetime = field(init=False) + + def __post_init__(self): + self.creation_date = datetime.datetime.now() + + def payload(self): + return ( + self.name, + self.description, + self.author, + self.creation_date, + self.type + ) + + +# DONE +@dataclass +class SitePayload: + site_name: str + site_description: str + site_owner_id: int + default_zone: str = None + default_auto_issue_location: str = None + default_primary_location: str = None + creation_date: datetime.datetime = field(init=False) + flags: dict = field(default_factory=dict) + + def __post_init__(self): + self.creation_date = datetime.datetime.now() + + def payload(self): + return ( + self.site_name, + self.site_description, + self.creation_date, + self.site_owner_id, + json.dumps(self.flags), + self.default_zone, + self.default_auto_issue_location, + self.default_primary_location + ) + +#DONE +@dataclass +class RolePayload: + role_name:str + role_description:str + site_id: int + flags: dict = field(default_factory=dict) + + def payload(self): + return ( + self.role_name, + self.role_description, + self.site_id, + json.dumps(self.flags) + ) + +@dataclass +class ItemLocationPayload: + part_id: int + location_id: int + quantity_on_hand: float = 0.0 + cost_layers: list = field(default_factory=list) + + def __post_init__(self): + if not isinstance(self.part_id, int): + raise TypeError(f"part_id must be of type int; not {type(self.part_id)}") + if not isinstance(self.location_id, int): + raise TypeError(f"part_id must be of type int; not {type(self.part_id)}") + + def payload(self): + return ( + self.part_id, + self.location_id, + self.quantity_on_hand, + lst2pgarr(self.cost_layers) + ) + +@dataclass +class SiteManager: + site_name: str + admin_user: tuple + default_zone: int + default_location: int + description: str + create_order: list = field(init=False) + drop_order: list = field(init=False) + + def __post_init__(self): + self.create_order = [ + "logins", + "sites", + "roles", + "units", + "cost_layers", + "linked_items", + "brands", + "food_info", + "item_info", + "zones", + "locations", + "logistics_info", + "transactions", + "item", + "vendors", + "groups", + "group_items", + "receipts", + "receipt_items", + "recipes", + "recipe_items", + "shopping_lists", + "shopping_list_items", + "item_locations", + "conversions", + "sku_prefix" + ] + self.drop_order = [ + "item_info", + "items", + "cost_layers", + "linked_items", + "transactions", + "brands", + "food_info", + "logistics_info", + "zones", + "locations", + "vendors", + "group_items", + "groups", + "receipt_items", + "receipts", + "recipe_items", + "recipes", + "shopping_list_items", + "shopping_lists", + "item_locations", + "conversions", + "sku_prefix" + ] \ No newline at end of file diff --git a/application/items/database_items.py b/application/items/database_items.py index deb2aea..bf87643 100644 --- a/application/items/database_items.py +++ b/application/items/database_items.py @@ -3,7 +3,6 @@ import config import psycopg2 import datetime - def getTransactions(site:str, payload: tuple, convert:bool=True): database_config = config.config() sql = f"SELECT * FROM {site}_transactions WHERE logistics_info_id=%s LIMIT %s OFFSET %s;" @@ -181,6 +180,23 @@ def getLocation(site:str, payload:tuple, convert:bool=True): return selected except Exception as error: raise postsqldb.DatabaseError(error, payload, sql) + +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 paginateZonesBySku(site: str, payload: tuple, convert=True): database_config = config.config() @@ -513,6 +529,162 @@ def insertItemLocationsTuple(conn, site, payload, convert=True): except Exception as error: raise postsqldb.DatabaseError(error, payload, sql) +def insertLogisticsInfoTuple(conn, site, payload, convert=False): + """insert payload into logistics_info table for site + + Args: + conn (_T_connector@connect): Postgresql Connector + site (str): + payload (tuple): (barcode[str], primary_location[str], auto_issue_location[str], dynamic_locations[jsonb], + location_data[jsonb], quantity_on_hand[float]) + convert (bool, optional): Determines if to return tuple as dictionary. Defaults to False. + + Raises: + DatabaseError: + + Returns: + tuple or dict: inserted tuple + """ + logistics_info = () + with open(f"application/items/sql/insertLogisticsInfoTuple.sql", "r+") as file: + sql = file.read().replace("%%site_name%%", site) + try: + 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 + except Exception as error: + raise postsqldb.DatabaseError(error, payload, sql) + + return logistics_info + +def insertItemInfoTuple(conn, site, payload, convert=False): + """inserts payload into the item_info table of site + + Args: + conn (_T_connector@connect): Postgresql Connector + site_name (str): + 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]) + convert (bool optional): Determines if to return tuple as dictionary. DEFAULTS to False. + + Raises: + DatabaseError: + + Returns: + tuple or dict: inserted tuple + """ + item_info = () + with open(f"application/items/sql/insertItemInfoTuple.sql", "r+") as file: + sql = file.read().replace("%%site_name%%", site) + try: + 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 + except Exception as error: + raise postsqldb.DatabaseError(error, payload, sql) + return item_info + +def insertFoodInfoTuple(conn, site, payload, convert=False): + """insert payload into food_info table for site + + Args: + conn (_T_connector@connect): Postgresql Connector + site (str): + payload (_type_): (ingrediants[lst2pgarr], food_groups[lst2pgarr], nutrients[jsonstr], expires[bool]) + convert (bool, optional): Determines if to return tuple as dictionary. Defaults to False. + + Raises: + DatabaseError: + + Returns: + tuple or dict: inserted tuple + """ + food_info = () + with open(f"application/items/sql/insertFoodInfoTuple.sql", "r+") as file: + sql = file.read().replace("%%site_name%%", site) + try: + 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 + except Exception as error: + raise postsqldb.DatabaseError(error, payload, sql) + return food_info + +def insertItemTuple(conn, site, payload, convert=False): + """insert payload into items table for site + + Args: + conn (_T_connector@connect): Postgresql Connector + site (str): + 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]) + convert (bool, optional): Determines if to return tuple as a dictionary. Defaults to False. + + Raises: + DatabaseError: + + Returns: + tuple or dict: inserted tuple + """ + item = () + with open(f"application/items/sql/insertItemTuple.sql", "r+") as file: + sql = file.read().replace("%%site_name%%", site) + try: + 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 + except Exception as error: + raise postsqldb.DatabaseError(error, payload, sql) + + return item + +def insertItemLocationsTuple(conn, site, payload, convert=False): + """insert payload into item_locations table for site + + Args: + conn (_T_connector@connect): Postgresql Connector + site (str): + payload (tuple): (part_id[int], location_id[int], quantity_on_hand[float], cost_layers[lst2pgarr]) + convert (bool, optional): Determines if to return tuple as dictionary. Defaults to False. + + Raises: + DatabaseError: + + Returns: + tuple or dict: inserted tuple + """ + location = () + with open(f"application/items/sql/insertItemLocationsTuple.sql", "r+") as file: + sql = file.read().replace("%%site_name%%", site) + try: + 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 + except Exception as error: + raise postsqldb.DatabaseError(error, payload, sql) + return location + def selectItemLocationsTuple(site_name, payload, convert=True): """select a single tuple from ItemLocations table for site_name @@ -569,7 +741,37 @@ def selectCostLayersTuple(site_name, payload, convert=True): return cost_layers except Exception as error: return error - + +def selectSiteTuple(payload, convert=True): + """Select a single Site from sites using site_name + + Args: + conn (_T_connector@connect): Postgresql Connector + payload (tuple): (site_name,) + convert (bool, optional): determines if to return tuple as dictionary. Defaults to False. + + Raises: + DatabaseError: + + Returns: + tuple or dict: selected tuples + """ + 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 postDeleteCostLayer(site_name, payload, convert=True, conn=None): """ payload (tuple): (table_to_delete_from, tuple_id) @@ -660,7 +862,6 @@ def postAddTransaction(site, payload, convert=False, conn=None): except Exception as error: raise postsqldb.DatabaseError(error, payload, sql) - def postInsertItemLink(site, payload, convert=True, conn=None): """insert payload into itemlinks table of site diff --git a/application/items/items_API.py b/application/items/items_API.py index 55499aa..40b0d7c 100644 --- a/application/items/items_API.py +++ b/application/items/items_API.py @@ -1,7 +1,22 @@ -from flask import Blueprint, request, render_template, redirect, session, url_for, send_file, jsonify, Response -import psycopg2, math, json, datetime, main, copy, requests, process, database, pprint, MyDataclasses +# 3rd Party imports +from flask import ( + Blueprint, request, render_template, redirect, session, url_for, send_file, jsonify, Response + ) +import psycopg2 +import math +import json +import datetime +import copy +import requests +import pprint + +# applications imports from config import config, sites_config from main import unfoldCostLayers +import process +import database +import main +import MyDataclasses from user_api import login_required import application.postsqldb as db from application.items import database_items @@ -25,14 +40,14 @@ def items(): current_site=session['selected_site'], sites=sites) -@items_api.route("/item/") +@items_api.route("/") @login_required def item(id): sites = [site[1] for site in main.get_sites(session['user']['sites'])] database_config = config() with psycopg2.connect(**database_config) as conn: units = db.UnitsTable.getAll(conn) - return render_template("items/item_new.html", id=id, units=units, current_site=session['selected_site'], sites=sites) + return render_template("item_new.html", id=id, units=units, current_site=session['selected_site'], sites=sites) @items_api.route("/transaction") @login_required @@ -43,14 +58,31 @@ def transaction(): units = db.UnitsTable.getAll(conn) return render_template("transaction.html", units=units, current_site=session['selected_site'], sites=sites, proto={'referrer': request.referrer}) +@items_api.route("/transactions/") +@login_required +def transactions(id): + """This is the main endpoint to reach the webpage for an items transaction history + --- + parameters: + - name: id + in: path + type: integer + required: true + default: all + responses: + 200: + description: Returns the transactions.html webpage for the item with passed ID + """ + sites = [site[1] for site in main.get_sites(session['user']['sites'])] + return render_template("transactions.html", id=id, current_site=session['selected_site'], sites=sites) -@items_api.route("/item//itemLink/") +@items_api.route("//itemLink/") @login_required def itemLink(parent_id, id): sites = [site[1] for site in main.get_sites(session['user']['sites'])] return render_template("itemlink.html", current_site=session['selected_site'], sites=sites, proto={'referrer': request.referrer}, id=id) -@items_api.route("/item/getTransactions", methods=["GET"]) +@items_api.route("/getTransactions", methods=["GET"]) @login_required def getTransactions(): """ GET a subquery of transactions by passing a logistics_info_id, limit, and page @@ -71,7 +103,7 @@ def getTransactions(): return jsonify({"transactions": recordset, "end": math.ceil(count/limit), "error": False, "message": ""}) return jsonify({"transactions": recordset, "end": math.ceil(count/limit), "error": True, "message": f"method {request.method} is not allowed."}) -@items_api.route("/item/getTransaction", methods=["GET"]) +@items_api.route("/getTransaction", methods=["GET"]) @login_required def getTransaction(): """ GET a transaction from the system by passing an ID @@ -97,7 +129,7 @@ def getTransaction(): return jsonify({"transaction": transaction, "error": False, "message": ""}) return jsonify({"transaction": transaction, "error": True, "message": f"method {request.method} is not allowed."}) -@items_api.route("/item/getItem", methods=["GET"]) +@items_api.route("/getItem", methods=["GET"]) @login_required def get_item(): """ GET item from system by passing its ID @@ -183,7 +215,7 @@ def pagninate_items(): return jsonify({'items': items, "end": math.ceil(count/limit), 'error':False, 'message': 'Items Loaded Successfully!'}) return jsonify({'items': items, "end": math.ceil(count/limit), 'error':True, 'message': 'There was a problem loading the items!'}) -@items_api.route('/item/getModalItems', methods=["GET"]) +@items_api.route('/getModalItems', methods=["GET"]) @login_required def getModalItems(): """ GET items from the system by passing a page, limit, search_string. For select modals @@ -223,7 +255,7 @@ def getModalItems(): return jsonify({"items":recordset, "end":math.ceil(count/limit), "error":False, "message":"items fetched succesfully!"}) return jsonify({"items":recordset, "end":math.ceil(count/limit), "error":True, "message": f"method {request.method} is not allowed."}) -@items_api.route('/item/getPrefixes', methods=["GET"]) +@items_api.route('/getPrefixes', methods=["GET"]) @login_required def getModalPrefixes(): """ GET prefixes from the system by passing page and limit. @@ -259,7 +291,7 @@ def getModalPrefixes(): return jsonify({"prefixes":recordset, "end":math.ceil(count/limit), "error":False, "message":"items fetched succesfully!"}) return jsonify({"prefixes":recordset, "end":math.ceil(count/limit), "error":True, "message":f"method {request.method} is not allowed!"}) -@items_api.route('/item/getZonesBySku', methods=["GET"]) +@items_api.route('/getZonesBySku', methods=["GET"]) @login_required def getZonesbySku(): """ GET zones by sku by passing page, limit, item_id @@ -301,7 +333,7 @@ def getZonesbySku(): return jsonify({'zones': zones, 'endpage': math.ceil(count/limit), 'error':False, 'message': f''}) return jsonify({'zones': zones, 'endpage': math.ceil(count/limit), 'error':False, 'message': f'method {request.method} not allowed.'}) -@items_api.route('/item/getLocationsBySkuZone', methods=['GET']) +@items_api.route('/getLocationsBySkuZone', methods=['GET']) @login_required def getLocationsBySkuZone(): """ GET locations by sku by passing page, limit, item_id, zone_id @@ -351,7 +383,7 @@ def getLocationsBySkuZone(): return jsonify({'locations': locations, 'endpage': math.ceil(count/limit), 'error': False, 'message': f''}) return jsonify({'locations': locations, 'endpage': math.ceil(count/limit), 'error': True, 'message': f'method {request.method} is not allowed.'}) -@items_api.route('/item/getBrands', methods=['GET']) +@items_api.route('/getBrands', methods=['GET']) @login_required def getBrands(): """ GET brands from the system by passing page, limit @@ -386,7 +418,7 @@ def getBrands(): return jsonify({'brands': brands, 'endpage': math.ceil(count/limit), 'error': True, 'message': f'method {request.method} is not allowed.'}) -@items_api.route('/item/updateItem', methods=['POST']) +@items_api.route('/updateItem', methods=['POST']) @login_required def updateItem(): """ POST update to item in the system by passing item_id, data @@ -414,7 +446,7 @@ def updateItem(): return jsonify({'error': False, 'message': f'Item was updated successfully!'}) return jsonify({'error': True, 'message': f'method {request.method} is not allowed!'}) -@items_api.route('/item/updateItemLink', methods=['POST']) +@items_api.route('/updateItemLink', methods=['POST']) @login_required def updateItemLink(): """ UPDATE item link by passing id, conv_factor, barcode, old_conv @@ -462,7 +494,7 @@ def updateItemLink(): return jsonify({'error': True, 'message': f"method {request.method} not allowed."}) -@items_api.route('/item/getPossibleLocations', methods=["GET"]) +@items_api.route('/getPossibleLocations', methods=["GET"]) @login_required def getPossibleLocations(): """ GET locations with zones by passing a page and limit @@ -496,7 +528,7 @@ def getPossibleLocations(): return jsonify({'locations': locations, 'end':math.ceil(count/limit), 'error':False, 'message': f'Locations received successfully!'}) return jsonify({'locations': locations, 'end':math.ceil(count/limit), 'error':True, 'message': f'method {request.method} not allowed.'}) -@items_api.route('/item/getLinkedItem', methods=["GET"]) +@items_api.route('/getLinkedItem', methods=["GET"]) @login_required def getLinkedItem(): """ GET itemlink from system by passing an ID @@ -521,7 +553,7 @@ def getLinkedItem(): return jsonify({'linked_item': linked_item, 'error': False, 'message': 'Linked Item added!!'}) return jsonify({'linked_item': linked_item, 'error': True, 'message': f'method {request.method} not allowed'}) -@items_api.route('/item/addLinkedItem', methods=["POST"]) +@items_api.route('/addLinkedItem', methods=["POST"]) @login_required def addLinkedItem(): """ POST a link between items by passing a parent_id, a child_id, conv_factor @@ -569,7 +601,7 @@ def addLinkedItem(): return jsonify({'error': False, 'message': 'Linked Item added!!'}) return jsonify({'error': True, 'message': 'These was an error with adding to the linked list!'}) -@items_api.route('/items/addBlankItem', methods=["POST"]) +@items_api.route('/addBlankItem', methods=["POST"]) def addBlankItem(): if request.method == "POST": data = { @@ -577,20 +609,16 @@ def addBlankItem(): 'name': request.get_json()['name'], 'subtype': request.get_json()['subtype'] } - pprint.pprint(data) database_config = config() site_name = session['selected_site'] user_id = session['user_id'] - try: - with psycopg2.connect(**database_config) as conn: - process.postNewBlankItem(conn, site_name, user_id, data) - except Exception as error: - conn.rollback() - return jsonify({'error': True, 'message': error}) + + items_processes.postNewBlankItem(site_name, user_id, data) + return jsonify({'error': False, 'message': 'Item added!!'}) return jsonify({'error': True, 'message': 'These was an error with adding Item!'}) -@items_api.route('/items/addSKUPrefix', methods=["POST"]) +@items_api.route('/addSKUPrefix', methods=["POST"]) def addSKUPrefix(): if request.method == "POST": database_config = config() @@ -609,7 +637,7 @@ def addSKUPrefix(): return jsonify({'error': False, 'message': 'Prefix added!!'}) return jsonify({'error': True, 'message': 'These was an error with adding this Prefix!'}) -@items_api.route('/item/addConversion', methods=['POST']) +@items_api.route('/addConversion', methods=['POST']) def addConversion(): if request.method == "POST": item_id = request.get_json()['parent_id'] @@ -627,7 +655,7 @@ def addConversion(): return jsonify(error=False, message="Conversion was added successfully") return jsonify(error=True, message="Unable to save this conversion, ERROR!") -@items_api.route('/item/deleteConversion', methods=['POST']) +@items_api.route('/deleteConversion', methods=['POST']) def deleteConversion(): if request.method == "POST": conversion_id = request.get_json()['conversion_id'] @@ -640,7 +668,7 @@ def deleteConversion(): return jsonify(error=False, message="Conversion was deleted successfully") return jsonify(error=True, message="Unable to delete this conversion, ERROR!") -@items_api.route('/item/updateConversion', methods=['POST']) +@items_api.route('/updateConversion', methods=['POST']) def updateConversion(): if request.method == "POST": conversion_id = request.get_json()['conversion_id'] @@ -653,7 +681,7 @@ def updateConversion(): return jsonify(error=False, message="Conversion was updated successfully") return jsonify(error=True, message="Unable to save this conversion, ERROR!") -@items_api.route('/item/addPrefix', methods=['POST']) +@items_api.route('/addPrefix', methods=['POST']) def addPrefix(): if request.method == "POST": item_info_id = request.get_json()['parent_id'] @@ -670,7 +698,7 @@ def addPrefix(): return jsonify(error=False, message="Prefix was added successfully") return jsonify(error=True, message="Unable to save this prefix, ERROR!") -@items_api.route('/item/deletePrefix', methods=['POST']) +@items_api.route('/deletePrefix', methods=['POST']) def deletePrefix(): if request.method == "POST": item_info_id = request.get_json()['item_info_id'] @@ -685,7 +713,7 @@ def deletePrefix(): return jsonify(error=False, message="Prefix was deleted successfully") return jsonify(error=True, message="Unable to delete this prefix, ERROR!") -@items_api.route('/item/refreshSearchString', methods=['POST']) +@items_api.route('/refreshSearchString', methods=['POST']) def refreshSearchString(): if request.method == "POST": item_id = request.get_json()['item_id'] @@ -706,7 +734,7 @@ def refreshSearchString(): return jsonify(error=False, message="Search String was updated successfully") return jsonify(error=True, message="Unable to update this search string, ERROR!") -@items_api.route('/item/postNewItemLocation', methods=['POST']) +@items_api.route('/postNewItemLocation', methods=['POST']) def postNewItemLocation(): if request.method == "POST": item_id = request.get_json()['item_id'] diff --git a/application/items/items_processes.py b/application/items/items_processes.py index 1d3d1c8..8aca57c 100644 --- a/application/items/items_processes.py +++ b/application/items/items_processes.py @@ -1,9 +1,102 @@ from application.items import database_items import application.postsqldb as db +import application.database_payloads as dbPayloads import config import datetime import psycopg2 +import json + +def postNewBlankItem(site_name: str, user_id: int, data: dict, conn=None): + """ data = {'barcode', 'name', 'subtype'}""" + self_conn = False + if not conn: + database_config = config.config() + conn = psycopg2.connect(**database_config) + conn.autocommit = False + self_conn = True + + site = database_items.selectSiteTuple((site_name,)) + default_zone = database_items.getZone(site_name,(site['default_zone'], )) + default_location = database_items.getLocation(site_name, (site['default_primary_location'],)) + uuid = f"{default_zone['name']}@{default_location['name']}" + + # create logistics info + logistics_info = dbPayloads.LogisticsInfoPayload( + barcode=data['barcode'], + 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 = dbPayloads.ItemInfoPayload(data['barcode']) + + # create Food Info + food_info = dbPayloads.FoodInfoPayload() + + logistics_info_id = 0 + item_info_id = 0 + food_info_id = 0 + brand_id = 1 + + + logistics_info = database_items.insertLogisticsInfoTuple(conn, site_name, logistics_info.payload(), convert=True) + item_info = database_items.insertItemInfoTuple(conn, site_name, item_info.payload(), convert=True) + food_info = database_items.insertFoodInfoTuple(conn, site_name, food_info.payload(), convert=True) + + name = data['name'] + name = name.replace("'", "@&apostraphe&") + description = "" + tags = db.lst2pgarr([]) + links = json.dumps({}) + search_string = f"&&{data['barcode']}&&{name}&&" + + + item = dbPayloads.ItemsPayload( + data['barcode'], + data['name'], + item_info['id'], + logistics_info['id'], + food_info['id'], + brand=brand_id, + row_type="single", + item_type=data['subtype'], + search_string=search_string + ) + + item = database_items.insertItemTuple(conn, site_name, item.payload(), convert=True) + + with conn.cursor() as cur: + cur.execute(f"SELECT id FROM {site_name}_locations WHERE uuid=%s;", (uuid, )) + location_id = cur.fetchone()[0] + + dbPayloads.ItemLocationPayload + item_location = dbPayloads.ItemLocationPayload(item['id'], location_id) + database_items.insertItemLocationsTuple(conn, site_name, item_location.payload()) + + + creation_tuple = dbPayloads.TransactionPayload( + datetime.datetime.now(), + logistics_info['id'], + item['barcode'], + item['item_name'], + "SYSTEM", + 0.0, + "Item added to the System!", + user_id, + {'location': uuid} + ) + + database_items.postAddTransaction(site_name, creation_tuple.payload(), conn=conn) + + if self_conn: + conn.commit() + conn.close() + return False + + return conn def postLinkedItem(site, payload): """ diff --git a/application/items/sql/insertFoodInfoTuple.sql b/application/items/sql/insertFoodInfoTuple.sql new file mode 100644 index 0000000..08afdf2 --- /dev/null +++ b/application/items/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/items/sql/insertItemInfoTuple.sql b/application/items/sql/insertItemInfoTuple.sql new file mode 100644 index 0000000..154e9d3 --- /dev/null +++ b/application/items/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/items/sql/insertItemLocationsTuple copy.sql b/application/items/sql/insertItemLocationsTuple copy.sql new file mode 100644 index 0000000..67abbd4 --- /dev/null +++ b/application/items/sql/insertItemLocationsTuple copy.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/items/sql/insertItemTuple.sql b/application/items/sql/insertItemTuple.sql new file mode 100644 index 0000000..4c9b940 --- /dev/null +++ b/application/items/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/items/sql/insertLogisticsInfoTuple.sql b/application/items/sql/insertLogisticsInfoTuple.sql new file mode 100644 index 0000000..312ee1c --- /dev/null +++ b/application/items/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/items/static/ItemListHandler.js b/application/items/static/ItemListHandler.js index 3c6e326..72077d0 100644 --- a/application/items/static/ItemListHandler.js +++ b/application/items/static/ItemListHandler.js @@ -214,12 +214,12 @@ async function updateTableElements(){ let viewOp = document.createElement('a') viewOp.innerHTML = `edit ` viewOp.setAttribute('class', 'uk-button uk-button-default uk-button-small') - viewOp.href = `/item/${items[i].id}` + viewOp.href = `/items/${items[i].id}` let historyOp = document.createElement('a') historyOp.innerHTML = `history ` historyOp.setAttribute('class', 'uk-button uk-button-default uk-button-small') - historyOp.href = `/transactions/${items[i].id}` + historyOp.href = `/items/transactions/${items[i].id}` buttonGroup.append(viewOp, historyOp) opsCell.append(buttonGroup) @@ -263,8 +263,8 @@ async function updateListElements(){ let footer = document.createElement('div') footer.classList.add('uk-card-footer') - footer.innerHTML = `edit - History` + footer.innerHTML = `edit + History` listItem.append(header) if(!items[i].description == ""){ diff --git a/application/items/static/itemEditHandler.js b/application/items/static/itemEditHandler.js index a7e594e..88edcfc 100644 --- a/application/items/static/itemEditHandler.js +++ b/application/items/static/itemEditHandler.js @@ -581,7 +581,7 @@ async function updateLinkedItemsTable() { let editOp = document.createElement('a') editOp.setAttribute('class', 'uk-button uk-button-default') editOp.setAttribute('uk-icon', 'icon: pencil') - editOp.setAttribute('href', `/item/${item['id']}/itemLink/${linked_items[i].id}`) + editOp.setAttribute('href', `/items/${item['id']}/itemLink/${linked_items[i].id}`) opCell.append(editOp) @@ -755,7 +755,7 @@ async function openEditConversionsModal(conversion) { async function postConversion() { - const response = await fetch(`/item/addConversion`, { + const response = await fetch(`/items/addConversion`, { method: 'POST', headers: { 'Content-Type': 'application/json', @@ -786,7 +786,7 @@ async function postConversion() { } async function postConversionUpdate(id, update) { - const response = await fetch(`/item/updateConversion`, { + const response = await fetch(`/items/updateConversion`, { method: 'POST', headers: { 'Content-Type': 'application/json', @@ -816,7 +816,7 @@ async function postConversionUpdate(id, update) { } async function deleteConversion(conversion_id) { - const response = await fetch(`/item/deleteConversion`, { + const response = await fetch(`/items/deleteConversion`, { method: 'POST', headers: { 'Content-Type': 'application/json', @@ -858,7 +858,7 @@ async function openAddPrefixesModal() { } async function postPrefix(id) { - const response = await fetch(`/item/addPrefix`, { + const response = await fetch(`/items/addPrefix`, { method: 'POST', headers: { 'Content-Type': 'application/json', @@ -887,7 +887,7 @@ async function postPrefix(id) { } async function deletePrefix(prefix_id) { - const response = await fetch(`/item/deletePrefix`, { + const response = await fetch(`/items/deletePrefix`, { method: 'POST', headers: { 'Content-Type': 'application/json', @@ -920,7 +920,7 @@ async function deletePrefix(prefix_id) { let prefix_limit = 2; async function fetchPrefixes() { - const url = new URL('/item/getPrefixes', window.location.origin); + const url = new URL('/items/getPrefixes', window.location.origin); url.searchParams.append('page', current_page); url.searchParams.append('limit', prefix_limit); const response = await fetch(url); @@ -930,7 +930,7 @@ async function fetchPrefixes() { let brands_limit = 25; async function fetchBrands() { - const url = new URL('/item/getBrands', window.location.origin); + const url = new URL('/items/getBrands', window.location.origin); url.searchParams.append('page', current_page); url.searchParams.append('limit', brands_limit); const response = await fetch(url); @@ -940,7 +940,7 @@ async function fetchBrands() { let items_limit = 25; async function fetchItems() { - const url = new URL('/item/getModalItems', window.location.origin); + const url = new URL('/items/getModalItems', window.location.origin); url.searchParams.append('page', current_page); url.searchParams.append('limit', items_limit); url.searchParams.append('search_string', search_string); @@ -951,7 +951,7 @@ async function fetchItems() { let zones_limit = 20; async function fetchZones(){ - const url = new URL('/item/getZonesBySku', window.location.origin); + const url = new URL('/items/getZonesBySku', window.location.origin); url.searchParams.append('page', current_page); url.searchParams.append('limit', zones_limit); url.searchParams.append('item_id', item.id); @@ -962,7 +962,7 @@ async function fetchZones(){ let locations_limit = 10; async function fetchLocations(logis) { - const url = new URL('/item/getLocationsBySkuZone', window.location.origin); + const url = new URL('/items/getLocationsBySkuZone', window.location.origin); url.searchParams.append('page', current_page); url.searchParams.append('limit', locations_limit); url.searchParams.append('part_id', item.id); @@ -977,7 +977,7 @@ async function fetchLocations(logis) { } async function fetchItem() { - const url = new URL('/item/getItem', window.location.origin); + const url = new URL('/items/getItem', window.location.origin); url.searchParams.append('id', item_id); const response = await fetch(url); data = await response.json(); @@ -1106,7 +1106,7 @@ async function addLinkedItem(parent_id, child_id) { if(Number.isInteger(conversion_factor)){ document.getElementById('conversion_factor').classList.remove('uk-form-danger') - const response = await fetch(`/item/addLinkedItem`, { + const response = await fetch(`/items/addLinkedItem`, { method: 'POST', headers: { 'Content-Type': 'application/json', @@ -1145,7 +1145,7 @@ async function addLinkedItem(parent_id, child_id) { } async function saveUpdated() { - const response = await fetch(`/item/updateItem`, { + const response = await fetch(`/items/updateItem`, { method: 'POST', headers: { 'Content-Type': 'application/json', @@ -1175,7 +1175,7 @@ async function saveUpdated() { }; async function refreshSearchString() { - const response = await fetch(`/item/refreshSearchString`, { + const response = await fetch(`/items/refreshSearchString`, { method: 'POST', headers: { 'Content-Type': 'application/json', @@ -1370,7 +1370,7 @@ var new_locations_current_page = 1 var new_locations_end_page = 1 var new_locations_limit = 25 async function fetch_new_locations() { - const url = new URL('/item/getPossibleLocations', window.location.origin); + const url = new URL('/items/getPossibleLocations', window.location.origin); url.searchParams.append('page', new_locations_current_page); url.searchParams.append('limit', new_locations_limit); const response = await fetch(url); @@ -1380,7 +1380,7 @@ async function fetch_new_locations() { }; async function postNewItemLocation(location_id) { - const response = await fetch(`/item/postNewItemLocation`, { + const response = await fetch(`/items/postNewItemLocation`, { method: 'POST', headers: { 'Content-Type': 'application/json', diff --git a/application/items/static/transactionsHandler.js b/application/items/static/transactionsHandler.js index b517777..e2cbca7 100644 --- a/application/items/static/transactionsHandler.js +++ b/application/items/static/transactionsHandler.js @@ -105,7 +105,7 @@ async function getItem(id) { } async function getTransaction(id) { - const url = new URL('/item/getTransaction', window.location.origin); + const url = new URL('/items/getTransaction', window.location.origin); url.searchParams.append('id', id); const response = await fetch(url); data = await response.json(); @@ -114,7 +114,7 @@ async function getTransaction(id) { } async function getTransactions(){ - const url = new URL('/item/getTransactions', window.location.origin); + const url = new URL('/items/getTransactions', window.location.origin); url.searchParams.append('page', pagination_current); url.searchParams.append('limit', limit); url.searchParams.append('logistics_info_id', item.logistics_info_id) diff --git a/application/items/templates/item_new.html b/application/items/templates/item_new.html index ddbeafe..d4e4a11 100644 --- a/application/items/templates/item_new.html +++ b/application/items/templates/item_new.html @@ -691,6 +691,6 @@ - + \ No newline at end of file diff --git a/application/items/templates/transactions.html b/application/items/templates/transactions.html index 928ef6b..dc8cc35 100644 --- a/application/items/templates/transactions.html +++ b/application/items/templates/transactions.html @@ -115,6 +115,6 @@ - + \ No newline at end of file diff --git a/application/postsqldb.py b/application/postsqldb.py index 9a8e00c..64844f8 100644 --- a/application/postsqldb.py +++ b/application/postsqldb.py @@ -2409,3 +2409,64 @@ class ItemLinkPayload: json.dumps(self.data), self.conv_factor ) + +@dataclass +class LogisticsInfoPayload: + barcode: str + primary_location: int + primary_zone: int + auto_issue_location: int + auto_issue_zone: int + + def payload(self): + return (self.barcode, + self.primary_location, + self.primary_zone, + self.auto_issue_location, + self.auto_issue_zone) + +@dataclass +class ItemInfoPayload: + barcode: str + packaging: str = "" + uom_quantity: float = 1.0 + uom: int = 1 + cost: float = 0.0 + safety_stock: float = 0.0 + 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 ( + self.barcode, + self.packaging, + self.uom_quantity, + self.uom, + self.cost, + self.safety_stock, + self.lead_time_days, + self.ai_pick, + lst2pgarr(self.prefixes) + ) + +@dataclass +class FoodInfoPayload: + food_groups: list = field(default_factory=list) + ingrediants: list = field(default_factory=list) + nutrients: dict = field(default_factory=dict) + expires: bool = False + default_expiration: float = 0.0 + + def payload(self): + return ( + lst2pgarr(self.food_groups), + lst2pgarr(self.ingrediants), + json.dumps(self.nutrients), + self.expires, + self.default_expiration + ) \ No newline at end of file diff --git a/webserver.py b/webserver.py index 0345489..8965fea 100644 --- a/webserver.py +++ b/webserver.py @@ -67,25 +67,6 @@ def inject_user(): return dict(username="") -@app.route("/transactions/") -@login_required -def transactions(id): - """This is the main endpoint to reach the webpage for an items transaction history - --- - parameters: - - name: id - in: path - type: integer - required: true - default: all - responses: - 200: - description: Returns the transactions.html webpage for the item with passed ID - """ - sites = [site[1] for site in main.get_sites(session['user']['sites'])] - return render_template("items/transactions.html", id=id, current_site=session['selected_site'], sites=sites) - - @app.route("/api/push-subscriptions", methods=["POST"]) def create_push_subscription(): json_data = request.get_json()