Finished migration of poe/scanner

This commit is contained in:
Jadowyne Ulve 2025-07-02 18:20:52 -05:00
parent d81515d4c5
commit 61661db807
11 changed files with 1339 additions and 53 deletions

View File

@ -115,7 +115,6 @@
<ul uk-tab>
<li><a href="#">Manual Transaction</a></li>
<li><a href="#">Scan To Receipt</a></li>
</ul>
<div class="uk-switcher">
@ -201,47 +200,6 @@
<button onclick="submitTransaction()" class="uk-button uk-button-primary">Submit</button>
</div>
</div>
<div class="uk-grid-small" uk-grid>
<div class="uk-width-1-1">
<p class="uk-text-meta">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.
</p>
</div>
<div class="uk-width-1-1" uk-grid>
<div>
<button id="receiptStart" onclick="startReceipt()" class="uk-button uk-button-default">Start Receipt</button>
</div>
<div>
<button id="receiptComplete" onclick="completeReceipt()" class="uk-button uk-button-default uk-disabled">Complete Receipt</button>
</div>
<div>
<button id="receiptClose" onclick="closeReceipt()" class="uk-button uk-button-default uk-disabled">Cancel Receipt</button>
</div>
</div>
<div class="uk-width-1-1">
<hr class="uk-divider-icon">
</div>
<div id="barcode-input" class="uk-width-1-1 uk-flex uk-flex-left uk-disabled" uk-grid>
<div class="uk-width-1-3@m">
<label class="uk-form-label" for="barcode-scan-receipt">Barcode</label>
<input onkeydown="addToReceipt(event)" id="barcode-scan-receipt" class="uk-input uk-flex uk-flex-bottom" type="text">
</div>
</div>
<div id="barcode-table" class="uk-width-1-1 uk-disabled">
<table class="uk-table uk-table-striped uk-table-hover">
<thead>
<tr>
<th class="uk-table-shrink">Type</th>
<th class="uk-table-shrink">Barcode</th>
<th>Name</th>
</tr>
</thead>
<tbody id="scanReceiptTableBody">
</tbody>
</table>
</div>
</div>
</div>
</div>
<!-- Modals -->

View File

@ -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,
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"})

View File

@ -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)

View File

@ -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!"}

View File

@ -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 *;

View File

@ -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 *;

View File

@ -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 *;

View File

@ -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 = `<a><span uk-pagination-previous></span></a>`;
previousElement.classList.add('uk-disabled');
}else {
previousElement.innerHTML = `<a onclick="setPage(${pagination_current-1}, '${elementID}')"><span uk-pagination-previous></span></a>`;
}
paginationElement.append(previousElement)
//first
let firstElement = document.createElement('li')
if(pagination_current<=1){
firstElement.innerHTML = `<a><strong>1</strong></a>`;
firstElement.classList.add('uk-disabled');
}else {
firstElement.innerHTML = `<a onclick="setPage(1, '${elementID}')">1</a>`;
}
paginationElement.append(firstElement)
// ...
if(pagination_current-2>1){
let firstDotElement = document.createElement('li')
firstDotElement.classList.add('uk-disabled')
firstDotElement.innerHTML = `<span>…</span>`;
paginationElement.append(firstDotElement)
}
// last
if(pagination_current-2>0){
let lastElement = document.createElement('li')
lastElement.innerHTML = `<a onclick="setPage(${pagination_current-1}, '${elementID}')">${pagination_current-1}</a>`
paginationElement.append(lastElement)
}
// current
if(pagination_current!=1 && pagination_current != pagination_end){
let currentElement = document.createElement('li')
currentElement.innerHTML = `<li class="uk-active"><span aria-current="page"><strong>${pagination_current}</strong></span></li>`
paginationElement.append(currentElement)
}
// next
if(pagination_current+2<pagination_end+1){
let nextElement = document.createElement('li')
nextElement.innerHTML = `<a onclick="setPage(${pagination_current+1}, '${elementID}')">${pagination_current+1}</a>`
paginationElement.append(nextElement)
}
// ...
if(pagination_current+2<=pagination_end){
let secondDotElement = document.createElement('li')
secondDotElement.classList.add('uk-disabled')
secondDotElement.innerHTML = `<span>…</span>`;
paginationElement.append(secondDotElement)
}
//end
let endElement = document.createElement('li')
if(pagination_current>=pagination_end){
endElement.innerHTML = `<a><strong>${pagination_end}</strong></a>`;
endElement.classList.add('uk-disabled');
}else {
endElement.innerHTML = `<a onclick="setPage(${pagination_end}, '${elementID}')">${pagination_end}</a>`;
}
paginationElement.append(endElement)
//next button
let nextElement = document.createElement('li')
if(pagination_current>=pagination_end){
nextElement.innerHTML = `<a><span uk-pagination-next></span></a>`;
nextElement.classList.add('uk-disabled');
}else {
nextElement.innerHTML = `<a onclick="setPage(${pagination_current+1}, '${elementID}')"><span uk-pagination-next></span></a>`;
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 = `<span uk-icon="check"></span>`
if(reversedScannedItems[i].error){
icon = `<span uk-icon="warning"></span>`
}
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')
}
}

View File

@ -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: {

View File

@ -0,0 +1,154 @@
<!DOCTYPE html>
<html lang="en" dir="ltr" id="main_html">
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0" charset="utf-8" />
<title id="title"></title>
<!-- Material Icons -->
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet" />
<!-- Material Symbols - Outlined Set -->
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined" rel="stylesheet" />
<!-- Material Symbols - Rounded Set -->
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Rounded" rel="stylesheet" />
<!-- Material Symbols - Sharp Set -->
<link rel="stylesheet" href="{{ url_for('static', filename='css/uikit.min.css') }}"/>
<link rel="stylesheet" href="{{ url_for('static', filename='css/pantry.css') }}"/>
<link id="dark-mode" rel="stylesheet" href="{{ url_for('static', filename='css/dark-mode.css') }}" disabled/>
</head>
<style>
.custom_row:hover{
background-color: rgb(230, 230, 230) !important;
cursor: pointer;
}
</style>
<body>
<div uk-sticky="sel-target: .uk-navbar-container; cls-active: uk-navbar-sticky">
<!-- to color the navbar i have to stlye this element the nav element -->
<nav id="navbar" class="uk-navbar-container">
<div class="uk-container uk-container-expand">
<div uk-navbar="dropbar: true">
<div id="offcanvas-slide" uk-offcanvas="mode: slide; overlay: true">
<div class="uk-offcanvas-bar uk-flex uk-flex-column">
<ul class="uk-nav uk-nav-secondary">
<img class="uk-align-center uk-border-circle" data-src="{{ url_for('static', filename='pictures/logo.jpg') }}" style="width: 150px; height: auto;" uk-img />
<li class="uk-nav-header">Apps</li>
<li><a href="/shopping-lists">Shopping Lists</a></li>
<li><a href="/recipes">Recipes</a></li>
<li class="uk-nav-header">Logistics</li>
<li><a href="/items">Items</a></li>
<li>
<a href="/transaction">
<div class="uk-active">Add Transaction<div class="uk-nav-subtitle" disabled>You are adding transactions...</div>
</div></a>
</li>
<li><a href="/workshop">Workshop</a></li>
<li><a href="/receipts">Receipts</a></li>
<li class="uk-nav-header">System Management</li>
<li><a><div>{{current_site}}<div class="uk-nav-subtitle">This is the current site you are viewing...</div></div></a>
<div uk-dropdown="mode: click">
<ul class="uk-nav uk-dropdown-nav">
{% for site in sites %}
{% if site == current_site %}
<li><a class="uk-disabled" href="#">{{site}}</a></li>
{% else %}
<li><a onclick="changeSite('{{site}}')">{{site}}</a></li>
{% endif %}
{% endfor %}
</ul>
</div>
</li>
{% if system_admin %}
<li><a href="/admin">Administration</a></li>
{% endif %}
<li><a href="" class="">{{username}}</a></li>
</ul>
<button class="uk-button uk-margin-small uk-position-top-right" uk-icon="icon: close" href=""></button>
</div>
</div>
<div class="uk-navbar-left uk-margin-small">
<a href="#offcanvas-slide" class="uk-button uk-button-default uk-button-small" uk-icon="icon: menu" uk-toggle> Menu</a>
</div>
<div class="uk-navbar-center uk-margin-small uk-visible@s">
<ul class="uk-breadcrumb">
<li style="cursor: default;"><span><strong>{{current_site}}</strong></span>
<div uk-dropdown="mode: hover">
<ul class="uk-nav uk-dropdown-nav">
{% for site in sites %}
{% if site == current_site %}
<li><a class="uk-disabled" href="#">{{site}}</a></li>
{% else %}
<li><a onclick="changeSite('{{site}}')">{{site}}</a></li>
{% endif %}
{% endfor %}
</ul>
</div>
</li>
<li style="cursor: default; user-select: none;"><span>Logistics</span></li>
<li><a href="/items">Items</a></li>
<li class="uk-disabled"><span>Add Transaction</span></li>
</ul>
</div>
<div class="uk-navbar-right">
<div>
<a onclick="toggleDarkMode()" class="uk-button uk-button-small"><span id="modeToggle" class="uk-flex material-symbols-outlined">dark_mode</span></a>
</div>
<div>
<a href="" class="" uk-icon="icon: user" uk-toggle>{{username}}</a>
</div>
</div>
</div>
</div>
</nav>
</div>
<div class="uk-container uk-section">
<div class="uk-grid-small" uk-grid>
<div class="uk-width-1-1">
<p class="uk-text-meta">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.
</p>
</div>
<div class="uk-width-1-1" uk-grid>
<div>
<button id="receiptStart" onclick="startReceipt()" class="uk-button uk-button-default">Start Receipt</button>
</div>
<div>
<button id="receiptComplete" onclick="completeReceipt()" class="uk-button uk-button-default uk-disabled">Complete Receipt</button>
</div>
<div>
<button id="receiptClose" onclick="closeReceipt()" class="uk-button uk-button-default uk-disabled">Cancel Receipt</button>
</div>
</div>
<div class="uk-width-1-1">
<hr class="uk-divider-icon">
</div>
<div id="barcode-input" class="uk-width-1-1 uk-flex uk-flex-left uk-disabled" uk-grid>
<div class="uk-width-1-3@m">
<label class="uk-form-label" for="barcode-scan-receipt">Barcode</label>
<input onkeydown="addToReceipt(event)" id="barcode-scan-receipt" class="uk-input uk-flex uk-flex-bottom" type="text">
</div>
</div>
<div id="barcode-table" class="uk-width-1-1 uk-disabled">
<table class="uk-table uk-table-striped uk-table-hover">
<thead>
<tr>
<th class="uk-table-shrink">Type</th>
<th class="uk-table-shrink">Barcode</th>
<th>Name</th>
</tr>
</thead>
<tbody id="scanReceiptTableBody">
</tbody>
</table>
</div>
</div>
</div>
</body>
{% assets "js_all" %}
<script type="text/javascript" src="{{ ASSET_URL }}"></script>
{% endassets %}
<script src="{{ url_for('poe.static', filename='js/receiptsHandler.js') }}"></script>
</html>

View File

@ -1935,3 +1935,6 @@
2025-04-28 06:46:35.145654 --- ERROR --- DatabaseError(message='can't adapt type 'builtin_function_or_method'',
payload={'id': <built-in function id>, 'update': {'conv_factor': 3}},
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;')