diff --git a/application/items/templates/transaction.html b/application/items/templates/transaction.html index 55d7159..7149f2c 100644 --- a/application/items/templates/transaction.html +++ b/application/items/templates/transaction.html @@ -115,7 +115,6 @@
@@ -201,47 +200,6 @@
-
-
-

Using this method of entering receipts does so by adding each barcode to a list and once the receipt has been built the - the system will then add the receipt to the system. Its important that you have the Barcode input focused and use a scanner that places the - characters into the field before it finishes up with a press of the ENTER key. -

-
-
-
- -
-
- -
-
- -
-
-
-
-
-
-
- - -
-
-
- - - - - - - - - - -
TypeBarcodeName
-
-
diff --git a/application/poe/poe_api.py b/application/poe/poe_api.py index 46ad57c..3cdfc8d 100644 --- a/application/poe/poe_api.py +++ b/application/poe/poe_api.py @@ -7,6 +7,7 @@ from queue import Queue import time, process from user_api import login_required import webpush +from application.poe import poe_processes point_of_ease = Blueprint('poe', __name__, template_folder="templates", static_folder="static") @@ -17,7 +18,11 @@ def scannerEndpoint(): return render_template('scanner.html', current_site=session['selected_site'], sites=sites) - +@point_of_ease.route('/receipts', methods=["GET"]) +def receiptsEndpoint(): + sites = [site[1] for site in main.get_sites(session['user']['sites'])] + return render_template('receipts.html', current_site=session['selected_site'], + sites=sites) @point_of_ease.route('/getItemLocations', methods=["GET"]) def getItemLocations(): @@ -86,14 +91,18 @@ def getModalItems(): @point_of_ease.route('/postTransaction', methods=["POST"]) def post_transaction(): if request.method == "POST": - database_config = config() - with psycopg2.connect(**database_config) as conn: - result = process.postTransaction( - conn=conn, - site_name=session['selected_site'], - user_id=session['user_id'], - data=dict(request.json) - ) + print('test two') + result = poe_processes.postTransaction( + site_name=session['selected_site'], + user_id=session['user_id'], + data=dict(request.json) + ) + #result = process.postTransaction( + # conn=conn, + # site_name=session['selected_site'], + # user_id=session['user_id'], + # data=dict(request.json) + #) return jsonify(result) return jsonify({"error":True, "message":"There was an error with this POST statement"}) diff --git a/application/poe/poe_database.py b/application/poe/poe_database.py new file mode 100644 index 0000000..3b93697 --- /dev/null +++ b/application/poe/poe_database.py @@ -0,0 +1,313 @@ +import psycopg2 +import config +from application import postsqldb + + +def selectItemLocationsTuple(site_name, payload, convert=True): + """select a single tuple from ItemLocations table for site_name + + Args: + conn (_T_connector@connect): + site_name (str): + payload (tuple): [item_id, location_id] + convert (bool): defaults to False, used to determine return of tuple/dict + + Returns: + tuple: the row that was returned from the table + """ + item_locations = () + database_config = config.config() + select_item_location_sql = f"SELECT * FROM {site_name}_item_locations WHERE part_id = %s AND location_id = %s;" + try: + with psycopg2.connect(**database_config) as conn: + 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 + return item_locations + except Exception as error: + return error + +def selectCostLayersTuple(site_name, payload, convert=True): + """select a single or series of cost layers from the database for site_name + + Args: + conn (_T_connector@connect): + site_name (str): + payload (tuple): (item_locations_id, ) + convert (bool): defaults to False, used for determining return as tuple/dict + + Returns: + list: list of tuples/dict from the cost_layers table for site_name + """ + 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 selectItemLocationsTuple(site_name, payload, convert=True, conn=None): + """select a single tuple from ItemLocations table for site_name + + Args: + conn (_T_connector@connect): + site_name (str): + payload (tuple): [item_id, location_id] + convert (bool): defaults to False, used to determine return of tuple/dict + + Returns: + tuple: the row that was returned from the table + """ + 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 insertCostLayersTuple(site, payload, convert=True, conn=None): + cost_layer = () + self_conn = False + + with open(f"application/poe/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): + """insert payload into transactions table for site + + Args: + conn (_T_connector@connect): Postgresql Connector + site (str): + payload (tuple): (timestamp[timestamp], logistics_info_id[int], barcode[str], name[str], + transaction_type[str], quantity[float], description[str], user_id[int], data[jsonb]) + convert (bool, optional): Determines if to return tuple as dictionary. Defaults to False. + + Raises: + DatabaseError: + + Returns: + tuple or dict: inserted tuple + """ + transaction = () + self_conn = False + with open(f"application/poe/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 updateCostLayersTuple(site, payload, convert=True, conn=None): + """_summary_ + + Args: + conn (_type_): _description_ + site (_type_): _description_ + payload (_type_): {'id': cost_layer_id, 'update': {column: data...}} + + Returns: + _type_: _description_ + """ + 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/poe/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): + """This is a basic funtion to delete a tuple from a table in site with an id. All + tables in this database has id's associated with them. + + Args: + conn (_T_connector@connect): Postgresql Connector + site_name (str): + payload (tuple): (tuple_id,) + convert (bool, optional): Determines if to return tuple as dictionary. Defaults to True. + + Raises: + DatabaseError: + + Returns: + tuple or dict: deleted tuple + """ + 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) diff --git a/application/poe/poe_processes.py b/application/poe/poe_processes.py new file mode 100644 index 0000000..dcfc930 --- /dev/null +++ b/application/poe/poe_processes.py @@ -0,0 +1,103 @@ +from application import postsqldb +from application.poe import poe_database + +import datetime +import psycopg2 + +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.selectItemLocationsTuple(conn, site_name, payload=(data['item_id'], data['location_id']), convert=True) + location = poe_database.selectItemLocationsTuple(site_name, payload=(data['item_id'], data['location_id']), conn=conn) + cost_layers: list = location['cost_layers'] + if data['transaction_type'] == "Adjust In": + cost_layer = poe_database.insertCostLayersTuple(site_name, cost_layer.payload(), conn=conn) + #cost_layer = database.insertCostLayersTuple(conn, site_name, cost_layer.payload(), convert=True) + cost_layers.append(cost_layer['id']) + + if data['transaction_type'] == "Adjust Out": + if float(location['quantity_on_hand']) < float(data['quantity']): + return {"error":True, "message":f"The quantity on hand in the chosen location is not enough to satisfy your transaction!"} + #cost_layers = database.selectCostLayersTuple(conn, site_name, (location['id'], ), convert=True) + cost_layers = poe_database.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']) + poe_database.updateCostLayersTuple(site_name, {'id': layer['id'], 'update': {'quantity': layer['quantity']}}, conn=conn) + #database.__updateTuple(conn, site_name, f"{site_name}_cost_layers", {'id': layer['id'], 'update': {'quantity': layer['quantity']}}) + qty = 0.0 + + if layer['quantity'] == 0.0: + poe_database.deleteCostLayersTuple(site_name, (layer['id'],), conn=conn) + #database.deleteCostLayersTuple(conn, site_name, (layer['id'], )) + + 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']) + poe_database.updateItemLocation(site_name, updated_item_location_payload, conn=conn) + #database.updateItemLocation(conn, site_name, updated_item_location_payload) + + site_location = poe_database.selectLocationsTuple(site_name, (location['location_id'], ), conn=conn) + #site_location = database.__selectTuple(conn, site_name, f"{site_name}_locations", (location['location_id'], ), convert=True) + + transaction.data = {'location': site_location['uuid']} + + poe_database.insertTransactionsTuple(site_name, transaction.payload(), conn=conn) + #database.insertTransactionsTuple(conn, site_name, transaction.payload()) + + if self_conn: + conn.rollback() + conn.close() + + return {"error": False, "message":f"Transaction Successful!"} \ No newline at end of file diff --git a/application/poe/sql/insertCostLayersTuple.sql b/application/poe/sql/insertCostLayersTuple.sql new file mode 100644 index 0000000..c3d381f --- /dev/null +++ b/application/poe/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/poe/sql/insertTransactionsTuple.sql b/application/poe/sql/insertTransactionsTuple.sql new file mode 100644 index 0000000..d8ee48d --- /dev/null +++ b/application/poe/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/poe/sql/updateItemLocation.sql b/application/poe/sql/updateItemLocation.sql new file mode 100644 index 0000000..7316d94 --- /dev/null +++ b/application/poe/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/poe/static/js/receiptsHandler.js b/application/poe/static/js/receiptsHandler.js new file mode 100644 index 0000000..0ecb98b --- /dev/null +++ b/application/poe/static/js/receiptsHandler.js @@ -0,0 +1,734 @@ +var pagination_current = 1; +var search_string = ''; +var defaqult_limit = 2; +var pagination_end = 1; +var item; + +async function changeSite(site){ + console.log(site) + const response = await fetch(`/changeSite`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + site: site, + }), + }); + data = await response.json(); + transaction_status = "success" + if (data.error){ + transaction_status = "danger" + } + + UIkit.notification({ + message: data.message, + status: transaction_status, + pos: 'top-right', + timeout: 5000 + }); + location.reload(true) +} + +async function replenishItemsTable(items) { + let itemsTableBody = document.getElementById("itemsTableBody") + itemsTableBody.innerHTML = "" + + for(let i = 0; i < items.length; i++){ + let tableRow = document.createElement('tr') + + + let idCell = document.createElement('td') + idCell.innerHTML = items[i].id + let barcodeCell = document.createElement('td') + barcodeCell.innerHTML = items[i].barcode + let nameCell = document.createElement('td') + nameCell.innerHTML = items[i].item_name + + tableRow.append(idCell) + tableRow.append(barcodeCell) + tableRow.append(nameCell) + + tableRow.onclick = function(){ + selectItem(items[i].id) + } + + itemsTableBody.append(tableRow) + } +} + +async function populateForm() { + if (item){ + console.log(item) + document.getElementById('database_id').value = item.id + document.getElementById('barcode').value = item.barcode + document.getElementById('name').value = item.item_name + document.getElementById('transaction_cost').value = parseFloat(item.item_info.cost) + + await selectLocation( + item.logistics_info.primary_zone.id, + item.logistics_info.primary_location.id, + item.logistics_info.primary_zone.name, + item.logistics_info.primary_location.name + ) + + + let quantity_on_hand = 0 + let locations = await getItemLocations() + for(let i = 0; i < locations.length; i++){ + quantity_on_hand = quantity_on_hand + locations[i].quantity_on_hand + } + document.getElementById('QOH').value = quantity_on_hand + document.getElementById('UOM').value = item.item_info.uom.fullname + + await replenishItemLocationsTable(locations) + + } +} + +async function selectItem(id) { + UIkit.modal(document.getElementById("itemsModal")).hide(); + item = await getItem(id) + await populateForm() +} + +var transaction_zone_id = 0 +var transaction_item_location_id = 0 +async function selectLocation(zone_id, location_id, zone_name, location_name) { + document.getElementById('zone').value = zone_name + document.getElementById('location').value = location_name + transaction_zone_id = zone_id + transaction_item_location_id = location_id +} + +async function openItemsModal(elementID){ + UIkit.modal(document.getElementById("itemsModal")).show(); + pagination_current = 1 + search_string = '' + let items = await getItems() + await replenishItemsTable(items) + await updatePaginationElement(elementID) + setFormButtonsEnabled(true) +} + +async function setFormButtonsEnabled(state) { + let item_location_button = document.getElementById("itemLocations") + + if(state){ + item_location_button.classList.remove("uk-disabled") + } else { + item_location_button.classList.add("uk-disabled") + } +} + +async function setTransactionTypeAdjustments() { + let trans_type = document.getElementById('trans_type').value + + if(trans_type=="Adjust Out"){ + document.getElementById('transaction_cost').classList.add('uk-disabled') + } + if(trans_type=="Adjust In"){ + document.getElementById('transaction_cost').classList.remove('uk-disabled') + } + +} + +async function replenishItemLocationsTable(locations) { + let itemLocationTableBody = document.getElementById('itemLocationTableBody') + itemLocationTableBody.innerHTML = "" + for(let i = 0; i < locations.length; i++){ + let tableRow = document.createElement('tr') + + let loca = locations[i].uuid.split('@') + + let zoneCell = document.createElement('td') + zoneCell.innerHTML = loca[0] + + let locationCell = document.createElement('td') + locationCell.innerHTML = loca[1] + + let qohCell = document.createElement('td') + qohCell.innerHTML = parseFloat(locations[i].quantity_on_hand) + + tableRow.append(zoneCell, locationCell, qohCell) + tableRow.onclick = async function(){ + await selectLocation( + locations[i].zone_id, + locations[i].id, + loca[0], + loca[1] + ) + } + itemLocationTableBody.append(tableRow) + } +} + +let locations_limit = 10; +async function getItemLocations() { + console.log("getting Locations") + const url = new URL('/external/getItemLocations', window.location.origin); + url.searchParams.append('page', pagination_current); + url.searchParams.append('limit', locations_limit); + url.searchParams.append('id', item.id); + const response = await fetch(url); + data = await response.json(); + pagination_end = data.end + let locations = data.locations; + console.log(locations) + return locations; +} + + +let items_limit = 50; +async function getItems() { + console.log("getting items") + const url = new URL('/external/getModalItems', window.location.origin); + url.searchParams.append('page', pagination_current); + url.searchParams.append('limit', items_limit); + url.searchParams.append('search_string', search_string) + const response = await fetch(url); + data = await response.json(); + pagination_end = data.end + let items = data.items; + return items; +} + +async function getItem(id) { + console.log(`selected item: ${id}`) + const url = new URL('/external/getItem', window.location.origin); + url.searchParams.append('id', id); + const response = await fetch(url); + data = await response.json(); + item = data.item; + return item; +} + +async function validateTransaction() { + let database_id = document.getElementById("database_id") + let transaction_type = document.getElementById("trans_type") + let transaction_zone = document.getElementById("zone") + let transaction_location = document.getElementById("location") + let transaction_quantity = document.getElementById("transaction_quantity") + let transaction_cost = document.getElementById("transaction_cost") + + + let error_count = 0 + if(database_id.value === ""){ + error_count = error_count + 1 + database_id.classList.add("uk-form-danger") + } else { + database_id.classList.remove("uk-form-danger") + } + if(transaction_type.value === "0"){ + error_count = error_count + 1 + transaction_type.classList.add("uk-form-danger") + } else { + transaction_type.classList.remove("uk-form-danger") + } + + if (transaction_zone.value === ""){ + error_count = error_count + 1 + transaction_zone.classList.add("uk-form-danger") + } else { + transaction_zone.classList.remove("uk-form-danger") + } + + if (transaction_location.value === ""){ + error_count = error_count + 1 + transaction_location.classList.add("uk-form-danger") + } else { + transaction_location.classList.remove("uk-form-danger") + } + + let transaction_quantity_int = parseFloat(transaction_quantity.value) + if (transaction_quantity_int === 0.00 || transaction_quantity_int < 0.00){ + error_count = error_count + 1 + transaction_quantity.classList.add("uk-form-danger") + } else { + transaction_quantity.classList.remove("uk-form-danger") + } + + let transaction_cost_int = parseFloat(transaction_cost.value) + if (transaction_cost_int == 0.00 && transaction_type.value == "Adjust In"){ + error_count = error_count + 1 + transaction_cost.classList.add("uk-form-danger") + } else { + transaction_cost.classList.remove("uk-form-danger") + } + + if(error_count > 0){ + return false + } + + return true +} + +async function submitTransaction() { + let validated = await validateTransaction() + if (validated){ + let cost = parseFloat(document.getElementById('transaction_cost').value.replace(/[^0-9.-]+/g, "")); + const response = await fetch(`/external/postTransaction`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + item_id: item.id, + logistics_info_id: item.logistics_info_id, + barcode: item.barcode, + item_name: item.item_name, + transaction_type: document.getElementById('trans_type').value, + quantity: parseFloat(document.getElementById('transaction_quantity').value), + description: document.getElementById('transaction_description').value, + cost: cost, + vendor: 0, + expires: null, + location_id: transaction_item_location_id + }), + }); + data = await response.json(); + transaction_status = "success" + if (data.error){ + transaction_status = "danger" + } + + UIkit.notification({ + message: data.message, + status: transaction_status, + pos: 'top-right', + timeout: 5000 + }); + + item = await getItem(item.id) + await populateForm() + document.getElementById('transaction_quantity').value = '0.00' + + } else { + UIkit.notification({ + message: 'Please verify your transaction receipt.', + status: 'warning', + pos: 'top-right', + timeout: 5000 + }) +} +} + +async function searchTable(event, logis, elementID) { + if(event.key==='Enter' && logis==='items'){ + search_string = event.srcElement.value + let items = await getItems() + await replenishItemsTable(items) + } + await updatePaginationElement(elementID) +} + +async function setPage(pageNumber, elementID){ + pagination_current = pageNumber; + + if(elementID=="itemsPage"){ + let items = await getItems() + await replenishItemsTable(items) + } + await updatePaginationElement(elementID) +} + +async function updatePaginationElement(elementID) { + let paginationElement = document.getElementById(elementID); + paginationElement.innerHTML = ""; + // previous + let previousElement = document.createElement('li') + if(pagination_current<=1){ + previousElement.innerHTML = ``; + previousElement.classList.add('uk-disabled'); + }else { + previousElement.innerHTML = ``; + } + paginationElement.append(previousElement) + + //first + let firstElement = document.createElement('li') + if(pagination_current<=1){ + firstElement.innerHTML = `1`; + firstElement.classList.add('uk-disabled'); + }else { + firstElement.innerHTML = `1`; + } + paginationElement.append(firstElement) + + // ... + if(pagination_current-2>1){ + let firstDotElement = document.createElement('li') + firstDotElement.classList.add('uk-disabled') + firstDotElement.innerHTML = ``; + paginationElement.append(firstDotElement) + } + // last + if(pagination_current-2>0){ + let lastElement = document.createElement('li') + lastElement.innerHTML = `${pagination_current-1}` + paginationElement.append(lastElement) + } + // current + if(pagination_current!=1 && pagination_current != pagination_end){ + let currentElement = document.createElement('li') + currentElement.innerHTML = `
  • ${pagination_current}
  • ` + paginationElement.append(currentElement) + } + // next + if(pagination_current+2${pagination_current+1}` + paginationElement.append(nextElement) + } + // ... + if(pagination_current+2<=pagination_end){ + let secondDotElement = document.createElement('li') + secondDotElement.classList.add('uk-disabled') + secondDotElement.innerHTML = ``; + paginationElement.append(secondDotElement) + } + //end + let endElement = document.createElement('li') + if(pagination_current>=pagination_end){ + endElement.innerHTML = `${pagination_end}`; + endElement.classList.add('uk-disabled'); + }else { + endElement.innerHTML = `${pagination_end}`; + } + paginationElement.append(endElement) + //next button + let nextElement = document.createElement('li') + if(pagination_current>=pagination_end){ + nextElement.innerHTML = ``; + nextElement.classList.add('uk-disabled'); + }else { + nextElement.innerHTML = ``; + console.log(nextElement.innerHTML) + } + paginationElement.append(nextElement) +} + +var scannedItems = Array(); +const queueLimit = 5; // 49 should be default + +async function addToQueue(event) { + if (event.key == "Enter"){ + let data = await getItemBarcode(document.getElementById('barcode-scan').value) + let scannedItem = data.item + if(data.error){ + UIkit.notification({ + message: data.message, + status: "danger", + pos: 'top-right', + timeout: 5000 + }); + } + if(scannedItems.length > queueLimit){ + scannedItems.shift() + } + if(!Array.isArray(scannedItem) && !data.error){ + let status = await submitScanTransaction(scannedItem) + scannedItems.push({'item': scannedItem, 'type': `${document.getElementById('scan_trans_type').value}`, 'error': status}) + document.getElementById('barcode-scan').value = "" + } + } + await replenishScanTable() +} + +async function getItemBarcode(barcode) { + console.log(`selected item: ${barcode}`) + const url = new URL('/external/getItem/barcode', window.location.origin); + url.searchParams.append('barcode', barcode); + const response = await fetch(url); + data = await response.json(); + return data; +} + +async function submitScanTransaction(scannedItem) { + /// I need to find the location that matches the items auto issue location id + + let trans_type = document.getElementById('scan_trans_type').value + let scan_transaction_item_location_id = 0 + let comparator = 0 + + if (trans_type === "Adjust In"){ + comparator = scannedItem.logistics_info.primary_location.id + } else if (trans_type === "Adjust Out"){ + comparator = scannedItem.logistics_info.auto_issue_location.id + } + + for (let i = 0; i < scannedItem.item_locations.length; i++){ + if (scannedItem.item_locations[i].location_id === comparator){ + scan_transaction_item_location_id = scannedItem.item_locations[i].id + } + } + + const response = await fetch(`/external/postTransaction`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + item_id: scannedItem.id, + logistics_info_id: scannedItem.logistics_info_id, + barcode: scannedItem.barcode, + item_name: scannedItem.item_name, + transaction_type: document.getElementById('scan_trans_type').value, + quantity: scannedItem.item_info.uom_quantity, + description: "", + cost: parseFloat(scannedItem.item_info.cost), + vendor: 0, + expires: null, + location_id: scan_transaction_item_location_id + }), + }); + data = await response.json(); + transaction_status = "success" + if (data.error){ + transaction_status = "danger" + } + + UIkit.notification({ + message: data.message, + status: transaction_status, + pos: 'top-right', + timeout: 5000 + }); + + return data.error + +} + +async function replenishScanTable() { + let scanTableBody = document.getElementById("scanTableBody") + scanTableBody.innerHTML = "" + + let reversedScannedItems = scannedItems.slice().reverse() + + for(let i = 0; i < reversedScannedItems.length; i++){ + let tableRow = document.createElement('tr') + + let icon = `` + if(reversedScannedItems[i].error){ + icon = `` + } + + let statusCell = document.createElement('td') + statusCell.innerHTML = icon + let barcodeCell = document.createElement('td') + barcodeCell.innerHTML = reversedScannedItems[i].item.barcode + let nameCell = document.createElement('td') + nameCell.innerHTML = reversedScannedItems[i].item.item_name + let typeCell = document.createElement('td') + typeCell.innerHTML = reversedScannedItems[i].type + let locationCell = document.createElement('td') + if (reversedScannedItems[i].type === "Adjust In"){ + locationCell.innerHTML = reversedScannedItems[i].item.logistics_info.primary_location.uuid + } else { + locationCell.innerHTML = reversedScannedItems[i].item.logistics_info.auto_issue_location.uuid + } + + tableRow.append(statusCell, barcodeCell, nameCell, typeCell, locationCell) + scanTableBody.append(tableRow) + } +} + +async function submitScanReceipt(items) { + const response = await fetch(`/external/postReceipt`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + items: items + }), + }); + data = await response.json(); + transaction_status = "success" + if (data.error){ + transaction_status = "danger" + } + + UIkit.notification({ + message: data.message, + status: transaction_status, + pos: 'top-right', + timeout: 5000 + }); + + return data.error +} + +var openedReceipt = false +async function startReceipt() { + openedReceipt = true + document.getElementById('barcode-input').classList.remove('uk-disabled') + document.getElementById('barcode-table').classList.remove('uk-disabled') + + document.getElementById('receiptStart').classList.add('uk-disabled') + document.getElementById('receiptComplete').classList.remove('uk-disabled') + document.getElementById('receiptClose').classList.remove('uk-disabled') + +} + +async function completeReceipt() { + openedReceipt = false + document.getElementById('barcode-input').classList.add('uk-disabled') + document.getElementById('barcode-table').classList.add('uk-disabled') + + document.getElementById('receiptStart').classList.remove('uk-disabled') + document.getElementById('receiptComplete').classList.add('uk-disabled') + document.getElementById('receiptClose').classList.add('uk-disabled') + + await submitScanReceipt(scannedReceiptItems) + let scanReceiptTableBody = document.getElementById("scanReceiptTableBody") + scanReceiptTableBody.innerHTML = "" + + scannedReceiptItems = Array() + +} + +async function closeReceipt(){ + openedReceipt = false + document.getElementById('barcode-input').classList.add('uk-disabled') + document.getElementById('barcode-table').classList.add('uk-disabled') + + document.getElementById('receiptStart').classList.remove('uk-disabled') + document.getElementById('receiptComplete').classList.add('uk-disabled') + document.getElementById('receiptClose').classList.add('uk-disabled') + + let scanReceiptTableBody = document.getElementById("scanReceiptTableBody") + scanReceiptTableBody.innerHTML = "" + + scannedReceiptItems = Array() +} + +var scannedReceiptItems = Array(); +async function addToReceipt(event) { + if (event.key == "Enter"){ + let barcode = document.getElementById('barcode-scan-receipt').value + let data = await getItemBarcode(barcode) + let scannedItem = data.item + if(scannedItem){ + let expires = scannedItem.food_info.expires + console.log(expires) + if(scannedItem.food_info.expires){ + let today = new Date(); + today.setDate(today.getDate() + Number(scannedItem.food_info.default_expiration)) + expires = today.toISOString().split('T')[0]; + } + scannedReceiptItems.push({item: { + barcode: scannedItem.barcode, + item_name: scannedItem.item_name, + qty: scannedItem.item_info.uom_quantity, + uom: scannedItem.item_info.uom.id, + data: {cost: scannedItem.item_info.cost, expires: expires} + }, type: 'sku'}) + document.getElementById('barcode-scan-receipt').value = "" + } else { + scannedReceiptItems.push({item: { + barcode: `%${barcode}%`, + item_name: "unknown", + qty: 1, + uom: 1, + data: {'cost': 0.00, 'expires': false} + }, type: 'new sku'}) + document.getElementById('barcode-scan-receipt').value = "" + } + } + await replenishScannedReceiptTable(scannedReceiptItems) +} + +async function replenishScannedReceiptTable(items) { + let scanReceiptTableBody = document.getElementById("scanReceiptTableBody") + scanReceiptTableBody.innerHTML = "" + + for(let i = 0; i < items.length; i++){ + let tableRow = document.createElement('tr') + + let typeCell = document.createElement('td') + typeCell.innerHTML = items[i].type + let barcodeCell = document.createElement('td') + barcodeCell.innerHTML = items[i].item.barcode + let nameCell = document.createElement('td') + nameCell.innerHTML = items[i].item.item_name + + let operationsCell = document.createElement('td') + + let editOp = document.createElement('a') + editOp.style = "margin-right: 5px;" + editOp.setAttribute('class', 'uk-button uk-button-small uk-button-default') + editOp.setAttribute('uk-icon', 'icon: pencil') + editOp.onclick = async function () { + await openLineEditModal(i, items[i]) + } + + let deleteOp = document.createElement('a') + deleteOp.setAttribute('class', 'uk-button uk-button-small uk-button-default') + deleteOp.setAttribute('uk-icon', 'icon: trash') + deleteOp.onclick = async function() { + scannedReceiptItems.splice(i, 1) + await replenishScannedReceiptTable(scannedReceiptItems) + } + + operationsCell.append(editOp, deleteOp) + + operationsCell.classList.add("uk-flex") + operationsCell.classList.add("uk-flex-right") + + tableRow.append(typeCell, barcodeCell, nameCell, operationsCell) + scanReceiptTableBody.append(tableRow) + } +} + +async function openLineEditModal(ind, line_data) { + console.log(line_data) + document.getElementById('lineName').value = line_data.item.item_name + document.getElementById('lineQty').value = line_data.item.qty + document.getElementById('lineUOM').value = line_data.item.uom + document.getElementById('lineCost').value = line_data.item.data.cost + document.getElementById('lineExpires').value = line_data.item.data.expires + if(line_data.type === 'sku'){ + document.getElementById('lineUOM').classList.add('uk-disabled') + } else { + document.getElementById('lineUOM').classList.remove('uk-disabled') + } + + if(!line_data.item.data.expires){ + document.getElementById('lineExpires').classList.add('uk-disabled') + } else { + document.getElementById('lineExpires').classList.remove('uk-disabled') + } + + document.getElementById('saveLineButton').onclick = async function() { + line_data.item.item_name = document.getElementById('lineName').value + line_data.item.qty = document.getElementById('lineQty').value + line_data.item.uom = document.getElementById('lineUOM').value + line_data.item.data.cost = document.getElementById('lineCost').value + if(line_data.item.data.expires){ + line_data.item.data.expires = document.getElementById('lineExpires').value + } + + scannedReceiptItems[ind] = line_data + UIkit.modal(document.getElementById("lineEditModal")).hide(); + await replenishScannedReceiptTable(scannedReceiptItems) + } + + UIkit.modal(document.getElementById("lineEditModal")).show(); +} + +var mode = false +async function toggleDarkMode() { + let darkMode = document.getElementById("dark-mode"); + darkMode.disabled = !darkMode.disabled; + mode = !mode; + if(mode){ + document.getElementById('modeToggle').innerHTML = "light_mode" + document.getElementById('main_html').classList.add('uk-light') + } else { + document.getElementById('modeToggle').innerHTML = "dark_mode" + document.getElementById('main_html').classList.remove('uk-light') + + } + +} \ No newline at end of file diff --git a/application/poe/static/js/transactionHandler.js b/application/poe/static/js/transactionHandler.js index 472a337..59162ad 100644 --- a/application/poe/static/js/transactionHandler.js +++ b/application/poe/static/js/transactionHandler.js @@ -78,7 +78,6 @@ async function submitScanTransaction(scannedItem) { scan_transaction_item_location_id = scannedItem.item_locations[i].id } } - const response = await fetch(`/poe/postTransaction`, { method: 'POST', headers: { diff --git a/application/poe/templates/receipts.html b/application/poe/templates/receipts.html new file mode 100644 index 0000000..5eed667 --- /dev/null +++ b/application/poe/templates/receipts.html @@ -0,0 +1,154 @@ + + + + + + + + + + + + + + + + + + + + + + +
    + + +
    +
    +
    +
    +

    Using this method of entering receipts does so by adding each barcode to a list and once the receipt has been built the + the system will then add the receipt to the system. Its important that you have the Barcode input focused and use a scanner that places the + characters into the field before it finishes up with a press of the ENTER key. +

    +
    +
    +
    + +
    +
    + +
    +
    + +
    +
    +
    +
    +
    +
    +
    + + +
    +
    +
    + + + + + + + + + + +
    TypeBarcodeName
    +
    +
    +
    + + {% assets "js_all" %} + + {% endassets %} + + \ No newline at end of file diff --git a/database.log b/database.log index abc5896..fca24ac 100644 --- a/database.log +++ b/database.log @@ -1934,4 +1934,7 @@ sql='UPDATE test_items SET brand = %s, item_type = %s WHERE id=%s RETURNING *;') 2025-04-28 06:46:35.145654 --- ERROR --- DatabaseError(message='can't adapt type 'builtin_function_or_method'', payload={'id': , 'update': {'conv_factor': 3}}, - sql='UPDATE test_itemlinks SET conv_factor = %s WHERE id=%s RETURNING *;') \ No newline at end of file + sql='UPDATE test_itemlinks SET conv_factor = %s WHERE id=%s RETURNING *;') +2025-07-02 18:04:47.600077 --- ERROR --- DatabaseError(message='not all arguments converted during string formatting', + payload=(1, 2), + sql='SELECT * FROM main_locations WHERE id=%s;') \ No newline at end of file