Migrating Shoppinglist Module to new schema

This commit is contained in:
Jadowyne Ulve 2025-07-12 08:54:27 -05:00
parent 1018414200
commit ce8e63b596
16 changed files with 244 additions and 165 deletions

View File

@ -3,14 +3,6 @@ import config
from application import postsqldb from application import postsqldb
def request_receipt_id(conn, site_name): def request_receipt_id(conn, site_name):
"""gets the next id for receipts_id, currently returns a 8 digit number
Args:
site (str): site to get the next id for
Returns:
json: receipt_id, message, error keys
"""
next_receipt_id = None next_receipt_id = None
sql = f"SELECT receipt_id FROM {site_name}_receipts ORDER BY id DESC LIMIT 1;" sql = f"SELECT receipt_id FROM {site_name}_receipts ORDER BY id DESC LIMIT 1;"
try: try:
@ -34,17 +26,6 @@ def request_receipt_id(conn, site_name):
return next_receipt_id return next_receipt_id
def selectItemLocationsTuple(site_name, payload, convert=True): 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 = () item_locations = ()
database_config = config.config() database_config = config.config()
select_item_location_sql = f"SELECT * FROM {site_name}_item_locations WHERE part_id = %s AND location_id = %s;" select_item_location_sql = f"SELECT * FROM {site_name}_item_locations WHERE part_id = %s AND location_id = %s;"
@ -62,17 +43,6 @@ def selectItemLocationsTuple(site_name, payload, convert=True):
return error return error
def selectCostLayersTuple(site_name, payload, convert=True): 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 = () cost_layers = ()
database_config = config.config() 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;" 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;"
@ -118,17 +88,6 @@ def selectLocationsTuple(site, payload, convert=True, conn=None):
raise postsqldb.DatabaseError(error, payload, sql) raise postsqldb.DatabaseError(error, payload, sql)
def selectItemLocationsTuple(site_name, payload, convert=True, conn=None): 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 = () item_locations = ()
self_conn = False self_conn = False
select_item_location_sql = f"SELECT * FROM {site_name}_item_locations WHERE part_id = %s AND location_id = %s;" select_item_location_sql = f"SELECT * FROM {site_name}_item_locations WHERE part_id = %s AND location_id = %s;"
@ -276,21 +235,7 @@ def insertCostLayersTuple(site, payload, convert=True, conn=None):
raise postsqldb.DatabaseError(error, payload, sql) raise postsqldb.DatabaseError(error, payload, sql)
def insertTransactionsTuple(site, payload, convert=True, conn=None): def insertTransactionsTuple(site, payload, convert=True, conn=None):
"""insert payload into transactions table for site # payload (tuple): (timestamp[timestamp], logistics_info_id[int], barcode[str], name[str],
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 = () transaction = ()
self_conn = False self_conn = False
with open(f"application/poe/sql/insertTransactionsTuple.sql", "r+") as file: with open(f"application/poe/sql/insertTransactionsTuple.sql", "r+") as file:
@ -319,20 +264,6 @@ def insertTransactionsTuple(site, payload, convert=True, conn=None):
return transaction return transaction
def insertReceiptsTuple(site, payload, convert=True, conn=None): def insertReceiptsTuple(site, payload, convert=True, conn=None):
"""insert payload into receipt table of site
Args:
conn (_T_connector@connect): Postgresql Connector
site (str):
payload (tuple):
convert (bool, optional): Determines if to return tuple as dictionary. Defaults to False.
Raises:
DatabaseError:
Returns:
tuple or dict: inserted tuple
"""
receipt = () receipt = ()
self_conn = False self_conn = False
with open(f"application/poe/sql/insertReceiptsTuple.sql", "r+") as file: with open(f"application/poe/sql/insertReceiptsTuple.sql", "r+") as file:
@ -362,21 +293,6 @@ def insertReceiptsTuple(site, payload, convert=True, conn=None):
raise postsqldb.DatabaseError(error, payload, sql) raise postsqldb.DatabaseError(error, payload, sql)
def insertReceiptItemsTuple(site, payload, convert=True, conn=None): def insertReceiptItemsTuple(site, payload, convert=True, conn=None):
"""insert payload into receipt_items table of site
Args:
conn (_T_connector@connect): Postgresql Connector
site (str):
payload (tuple): (type[str], receipt_id[int], barcode[str], name[str],
qty[float], data[jsonb], status[str])
convert (bool, optional): Determines if to return tuple as dictionary. Defaults to False.
Raises:
DatabaseError:
Returns:
tuple or dict: inserted tuple
"""
receipt_item = () receipt_item = ()
self_conn = False self_conn = False
@ -407,16 +323,6 @@ def insertReceiptItemsTuple(site, payload, convert=True, conn=None):
raise postsqldb.DatabaseError(error, payload, sql) raise postsqldb.DatabaseError(error, payload, sql)
def updateCostLayersTuple(site, payload, convert=True, conn=None): 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 = () cost_layer = ()
self_conn = False self_conn = False
@ -478,21 +384,6 @@ def updateItemLocation(site, payload, convert=True, conn=None):
def deleteCostLayersTuple(site, payload, convert=True, conn=None): 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 = () deleted = ()
self_conn = False 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;" sql = f"WITH deleted_rows AS (DELETE FROM {site}_cost_layers WHERE id IN ({','.join(['%s'] * len(payload))}) RETURNING *) SELECT * FROM deleted_rows;"

View File

View File

@ -4,26 +4,32 @@ from config import config, sites_config
from main import unfoldCostLayers from main import unfoldCostLayers
from user_api import login_required from user_api import login_required
import postsqldb import postsqldb
from application.shoppinglists import shoplist_database
from application import database_payloads
shopping_list_api = Blueprint('shopping_list_API', __name__) shopping_list_api = Blueprint('shopping_list_API', __name__, template_folder="templates", static_folder="static")
@shopping_list_api.route("/shopping-lists")
# ROOT TEMPLATE CALLS
@shopping_list_api.route("/")
@login_required @login_required
def shopping_lists(): def shopping_lists():
sites = [site[1] for site in main.get_sites(session['user']['sites'])] sites = [site[1] for site in main.get_sites(session['user']['sites'])]
return render_template("shopping-lists/index.html", current_site=session['selected_site'], sites=sites) return render_template("lists.html", current_site=session['selected_site'], sites=sites)
@shopping_list_api.route("/shopping-list/<mode>/<id>") @shopping_list_api.route("/<mode>/<id>")
@login_required @login_required
def shopping_list(mode, id): def shopping_list(mode, id):
sites = [site[1] for site in main.get_sites(session['user']['sites'])] sites = [site[1] for site in main.get_sites(session['user']['sites'])]
if mode == "view": if mode == "view":
return render_template("shopping-lists/view.html", id=id, current_site=session['selected_site'], sites=sites) return render_template("view.html", id=id, current_site=session['selected_site'], sites=sites)
if mode == "edit": if mode == "edit":
return render_template("shopping-lists/edit.html", id=id, current_site=session['selected_site'], sites=sites) return render_template("edit.html", id=id, current_site=session['selected_site'], sites=sites)
return redirect("/") return redirect("/")
@shopping_list_api.route('/shopping-lists/addList', methods=["POST"])
# API CALLS
@shopping_list_api.route('/api/addList', methods=["POST"])
def addList(): def addList():
if request.method == "POST": if request.method == "POST":
list_name = request.get_json()['list_name'] list_name = request.get_json()['list_name']
@ -43,7 +49,7 @@ def addList():
return jsonify({'error': False, 'message': 'List added!!'}) return jsonify({'error': False, 'message': 'List added!!'})
return jsonify({'error': True, 'message': 'These was an error with adding the list!'}) return jsonify({'error': True, 'message': 'These was an error with adding the list!'})
@shopping_list_api.route('/shopping-lists/getLists', methods=["GET"]) @shopping_list_api.route('/api/getLists', methods=["GET"])
def getShoppingLists(): def getShoppingLists():
lists = [] lists = []
if request.method == "GET": if request.method == "GET":
@ -77,7 +83,7 @@ def getShoppingLists():
return jsonify({'shopping_lists': lists, 'end':math.ceil(count/limit), 'error': False, 'message': 'Lists queried successfully!'}) return jsonify({'shopping_lists': lists, 'end':math.ceil(count/limit), 'error': False, 'message': 'Lists queried successfully!'})
@shopping_list_api.route('/shopping-lists/getList', methods=["GET"]) @shopping_list_api.route('/api/getList', methods=["GET"])
def getShoppingList(): def getShoppingList():
if request.method == "GET": if request.method == "GET":
sl_id = int(request.args.get('id', 1)) sl_id = int(request.args.get('id', 1))
@ -87,19 +93,19 @@ def getShoppingList():
lists = database.getShoppingList(conn, site_name, (sl_id, ), convert=True) lists = database.getShoppingList(conn, site_name, (sl_id, ), convert=True)
return jsonify({'shopping_list': lists, 'error': False, 'message': 'Lists queried successfully!'}) return jsonify({'shopping_list': lists, 'error': False, 'message': 'Lists queried successfully!'})
@shopping_list_api.route('/shopping-lists/getListItem', methods=["GET"]) # Added to Database
@shopping_list_api.route('/api/getListItem', methods=["GET"])
def getShoppingListItem(): def getShoppingListItem():
list_item = {} list_item = {}
if request.method == "GET": if request.method == "GET":
sli_id = int(request.args.get('sli_id', 1)) sli_id = int(request.args.get('sli_id', 1))
database_config = config()
site_name = session['selected_site'] site_name = session['selected_site']
with psycopg2.connect(**database_config) as conn: list_item = shoplist_database.getShoppingListItem(site_name, (sli_id, ))
list_item = postsqldb.ShoppingListsTable.getItem(conn, site_name, (sli_id, ))
return jsonify({'list_item': list_item, 'error': False, 'message': 'Lists Items queried successfully!'}) return jsonify({'list_item': list_item, 'error': False, 'message': 'Lists Items queried successfully!'})
return jsonify({'list_item': list_item, 'error': True, 'message': 'List Items queried unsuccessfully!'}) return jsonify({'list_item': list_item, 'error': True, 'message': 'List Items queried unsuccessfully!'})
@shopping_list_api.route('/shopping-lists/getItems', methods=["GET"]) # Added to database
@shopping_list_api.route('/api/getItems', methods=["GET"])
def getItems(): def getItems():
recordset = [] recordset = []
count = {'count': 0} count = {'count': 0}
@ -109,46 +115,43 @@ def getItems():
search_string = request.args.get('search_string', 10) search_string = request.args.get('search_string', 10)
site_name = session['selected_site'] site_name = session['selected_site']
offset = (page - 1) * limit offset = (page - 1) * limit
database_config = config() sort_order = "ID ASC"
with psycopg2.connect(**database_config) as conn: payload = (search_string, limit, offset, sort_order)
payload = (search_string, limit, offset) recordset, count = shoplist_database.getItemsWithQOH(site_name, payload, convert=True)
recordset, count = database.getItemsWithQOH(conn, site_name, payload, convert=True)
return jsonify({"items":recordset, "end":math.ceil(count['count']/limit), "error":False, "message":"items fetched succesfully!"}) return jsonify({"items":recordset, "end":math.ceil(count['count']/limit), "error":False, "message":"items fetched succesfully!"})
return jsonify({"items":recordset, "end":math.ceil(count['count']/limit), "error":True, "message":"There was an error with this GET statement"}) return jsonify({"items":recordset, "end":math.ceil(count['count']/limit), "error":True, "message":"There was an error with this GET statement"})
@shopping_list_api.route('/shopping-lists/postListItem', methods=["POST"]) # Added to database
@shopping_list_api.route('/api/postListItem', methods=["POST"])
def postListItem(): def postListItem():
if request.method == "POST": if request.method == "POST":
data = request.get_json()['data'] data = request.get_json()['data']
site_name = session['selected_site'] site_name = session['selected_site']
database_config = config() sl_item = database_payloads.ShoppingListItemPayload(
with psycopg2.connect(**database_config) as conn: uuid = data['uuid'],
sl_item = MyDataclasses.ShoppingListItemPayload( sl_id = data['sl_id'],
uuid = data['uuid'], item_type=data['item_type'],
sl_id = data['sl_id'], item_name=data['item_name'],
item_type=data['item_type'], uom=data['uom'],
item_name=data['item_name'], qty=data['qty'],
uom=data['uom'], item_id=data['item_id'],
qty=data['qty'], links=data['links']
item_id=data['item_id'], )
links=data['links'] shoplist_database.insertShoppingListItemsTuple(site_name, sl_item.payload())
)
database.insertShoppingListItemsTuple(conn, site_name, sl_item.payload())
return jsonify({"error":False, "message":"items fetched succesfully!"}) return jsonify({"error":False, "message":"items fetched succesfully!"})
return jsonify({"error":True, "message":"There was an error with this GET statement"}) return jsonify({"error":True, "message":"There was an error with this GET statement"})
@shopping_list_api.route('/shopping-lists/deleteListItem', methods=["POST"]) # Added to Database
@shopping_list_api.route('/api/deleteListItem', methods=["POST"])
def deleteListItem(): def deleteListItem():
if request.method == "POST": if request.method == "POST":
sli_id = request.get_json()['sli_id'] sli_id = request.get_json()['sli_id']
site_name = session['selected_site'] site_name = session['selected_site']
database_config = config() shoplist_database.deleteShoppingListItemsTuple(site_name, (sli_id, ))
with psycopg2.connect(**database_config) as conn:
database.deleteShoppingListItemsTuple(conn, site_name, (sli_id, ))
return jsonify({"error":False, "message":"item deleted succesfully!"}) return jsonify({"error":False, "message":"item deleted succesfully!"})
return jsonify({"error":True, "message":"There was an error with this POST statement"}) return jsonify({"error":True, "message":"There was an error with this POST statement"})
@shopping_list_api.route('/shopping-lists/saveListItem', methods=["POST"]) @shopping_list_api.route('/api/saveListItem', methods=["POST"])
def saveListItem(): def saveListItem():
if request.method == "POST": if request.method == "POST":
sli_id = request.get_json()['sli_id'] sli_id = request.get_json()['sli_id']
@ -160,7 +163,7 @@ def saveListItem():
return jsonify({"error":False, "message":"items fetched succesfully!"}) return jsonify({"error":False, "message":"items fetched succesfully!"})
return jsonify({"error":True, "message":"There was an error with this GET statement"}) return jsonify({"error":True, "message":"There was an error with this GET statement"})
@shopping_list_api.route('/shopping-lists/getSKUItemsFull', methods=["GET"]) @shopping_list_api.route('/api/getSKUItemsFull', methods=["GET"])
def getSKUItemsFull(): def getSKUItemsFull():
items = [] items = []
count = {'count': 0} count = {'count': 0}

View File

@ -0,0 +1,146 @@
import psycopg2
import config
from application import postsqldb
def getShoppingListItem(site, payload, convert=True, conn=None):
"""_summary_
Args:
conn (_type_): _description_
site (_type_): _description_
payload (_type_): (id, )
convert (bool, optional): _description_. Defaults to True.
Raises:
DatabaseError: _description_
Returns:
_type_: _description_
"""
record = ()
self_conn = False
with open('application/shoppinglists/sql/selectShoppingListItem.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:
record = postsqldb.tupleDictionaryFactory(cur.description, rows)
elif rows and not convert:
record = rows
if self_conn:
conn.close()
return record
except Exception as error:
raise postsqldb.DatabaseError(error, payload, sql)
def getItemsWithQOH(site, payload, convert=True, conn=None):
recordset = []
count = 0
self_conn = False
with open(f"application/shoppinglists/sql/getItemsWithQOH.sql", "r+") as file:
sql = file.read().replace("%%site_name%%", site).replace("%%sort_order%%", payload[3])
payload = list(payload)
payload.pop(3)
try:
if not conn:
database_config = config.config()
conn = psycopg2.connect(**database_config)
conn.autocommit = True
self_conn = True
if convert:
with conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) as cur:
cur.execute(sql, payload)
recordset = cur.fetchall()
recordset = [dict(record) for record in recordset]
cur.execute(f"SELECT COUNT(*) FROM {site}_items WHERE search_string LIKE '%%' || %s || '%%';", (payload[0], ))
count = cur.fetchone()
else:
with conn.cursor() as cur:
cur.execute(sql, payload)
recordset = cur.fetchall()
cur.execute(f"SELECT COUNT(*) FROM {site}_items WHERE search_string LIKE '%%' || %s || '%%';", (payload[0], ))
count = cur.fetchone()
if self_conn:
conn.close()
return recordset, count
except Exception as error:
raise postsqldb.DatabaseError(error, payload, sql)
def deleteShoppingListItemsTuple(site_name, payload, convert=True, conn=None):
deleted = ()
self_conn = False
sql = f"WITH deleted_rows AS (DELETE FROM {site_name}_shopping_list_items WHERE id IN ({','.join(['%s'] * len(payload))}) RETURNING *) SELECT * FROM deleted_rows;"
try:
if not conn:
database_config = config.config()
conn = psycopg2.connect(**database_config)
conn.autocommit = True
self_conn = True
with conn.cursor() as cur:
cur.execute(sql, payload)
rows = cur.fetchall()
if rows and convert:
deleted = [postsqldb.tupleDictionaryFactory(cur.description, r) for r in rows]
elif rows and not convert:
deleted = rows
if self_conn:
conn.commit()
conn.close()
return deleted
except Exception as error:
raise postsqldb.DatabaseError(error, payload, sql)
def insertShoppingListItemsTuple(site, payload, convert=True, conn=None):
shopping_list_item = ()
self_conn = False
with open(f"application/shoppinglists/sql/insertShoppingListItemsTuple.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:
shopping_list_item = postsqldb.tupleDictionaryFactory(cur.description, rows)
elif rows and not convert:
shopping_list_item = rows
if self_conn:
conn.commit()
conn.close()
return shopping_list_item
except Exception as error:
raise postsqldb.DatabaseError(error, payload, sql)

View File

@ -0,0 +1,18 @@
WITH sum_cte AS (
SELECT mi.id, SUM(mil.quantity_on_hand)::FLOAT8 AS total_sum
FROM %%site_name%%_item_locations mil
JOIN %%site_name%%_items mi ON mil.part_id = mi.id
GROUP BY mi.id
)
SELECT %%site_name%%_items.*,
row_to_json(%%site_name%%_item_info.*) as item_info,
sum_cte.total_sum as total_qoh,
(SELECT COALESCE(row_to_json(u), '{}') FROM units as u WHERE u.id=%%site_name%%_item_info.uom) as uom
FROM %%site_name%%_items
LEFT JOIN sum_cte ON %%site_name%%_items.id = sum_cte.id
LEFT JOIN %%site_name%%_item_info ON %%site_name%%_items.item_info_id = %%site_name%%_item_info.id
WHERE %%site_name%%_items.search_string LIKE '%%' || %s || '%%'
ORDER BY %%sort_order%%
LIMIT %s OFFSET %s;

View File

@ -0,0 +1,4 @@
INSERT INTO %%site_name%%_shopping_list_items
(uuid, sl_id, item_type, item_name, uom, qty, item_id, links)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s)
RETURNING *;

View File

@ -0,0 +1,4 @@
SELECT items.*,
(SELECT COALESCE(row_to_json(un), '{}') FROM units un WHERE un.id = items.uom LIMIT 1) AS uom
FROM %%site_name%%_shopping_list_items items
WHERE items.id = %s;

View File

@ -88,7 +88,7 @@ async function replenishLineTable(sl_items){
} }
async function fetchShoppingList() { async function fetchShoppingList() {
const url = new URL('/shopping-lists/getList', window.location.origin); const url = new URL('/shopping-lists/api/getList', window.location.origin);
url.searchParams.append('id', sl_id); url.searchParams.append('id', sl_id);
const response = await fetch(url); const response = await fetch(url);
data = await response.json(); data = await response.json();
@ -277,7 +277,7 @@ async function updateItemsPaginationElement() {
let items_limit = 25; let items_limit = 25;
async function fetchItems() { async function fetchItems() {
const url = new URL('/shopping-lists/getItems', window.location.origin); const url = new URL('/shopping-lists/api/getItems', window.location.origin);
url.searchParams.append('page', pagination_current); url.searchParams.append('page', pagination_current);
url.searchParams.append('limit', items_limit); url.searchParams.append('limit', items_limit);
url.searchParams.append('search_string', search_string); url.searchParams.append('search_string', search_string);
@ -288,7 +288,7 @@ async function fetchItems() {
} }
async function fetchSLItem(sli_id) { async function fetchSLItem(sli_id) {
const url = new URL('/shopping-lists/getListItem', window.location.origin); const url = new URL('/shopping-lists/api/getListItem', window.location.origin);
url.searchParams.append('sli_id', sli_id); url.searchParams.append('sli_id', sli_id);
const response = await fetch(url); const response = await fetch(url);
data = await response.json(); data = await response.json();
@ -320,7 +320,7 @@ async function addCustomItem() {
} }
async function submitItemToList(newItem) { async function submitItemToList(newItem) {
const response = await fetch(`/shopping-lists/postListItem`, { const response = await fetch(`/shopping-lists/api/postListItem`, {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
@ -345,7 +345,7 @@ async function submitItemToList(newItem) {
} }
async function deleteLineItem(sli_id) { async function deleteLineItem(sli_id) {
const response = await fetch(`/shopping-lists/deleteListItem`, { const response = await fetch(`/shopping-lists/api/deleteListItem`, {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
@ -374,7 +374,7 @@ async function deleteLineItem(sli_id) {
} }
async function saveLineItem(sli_id, update) { async function saveLineItem(sli_id, update) {
const response = await fetch(`/shopping-lists/saveListItem`, { const response = await fetch(`/shopping-lists/api/saveListItem`, {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',

View File

@ -58,12 +58,12 @@ async function replenishShoppingListCards(lists) {
editOp.setAttribute('class', 'uk-button uk-button-small uk-button-default') editOp.setAttribute('class', 'uk-button uk-button-small uk-button-default')
editOp.innerHTML = '<span uk-icon="icon: pencil"></span> Edit' editOp.innerHTML = '<span uk-icon="icon: pencil"></span> Edit'
editOp.style = "margin-right: 10px;" editOp.style = "margin-right: 10px;"
editOp.href = `/shopping-list/edit/${lists[i].id}` editOp.href = `/shopping-lists/edit/${lists[i].id}`
let viewOp = document.createElement('a') let viewOp = document.createElement('a')
viewOp.setAttribute('class', 'uk-button uk-button-small uk-button-default') viewOp.setAttribute('class', 'uk-button uk-button-small uk-button-default')
viewOp.innerHTML = '<span uk-icon="icon: eye"></span> View' viewOp.innerHTML = '<span uk-icon="icon: eye"></span> View'
viewOp.href = `/shopping-list/view/${lists[i].id}` viewOp.href = `/shopping-lists/view/${lists[i].id}`
//viewOp.style = "margin-right: 20px;" //viewOp.style = "margin-right: 20px;"
@ -83,7 +83,7 @@ async function openAddListModal() {
var listLimit = 5; var listLimit = 5;
async function getShoppingLists(){ async function getShoppingLists(){
console.log(pagination_current) console.log(pagination_current)
const url = new URL('/shopping-lists/getLists', window.location.origin); const url = new URL('/shopping-lists/api/getLists', window.location.origin);
url.searchParams.append('page', pagination_current); url.searchParams.append('page', pagination_current);
url.searchParams.append('limit', listLimit); url.searchParams.append('limit', listLimit);
response = await fetch(url) response = await fetch(url)
@ -98,7 +98,7 @@ async function addList() {
let list_description = document.getElementById('addListDescription').value let list_description = document.getElementById('addListDescription').value
let list_type = document.getElementById('list_type').value let list_type = document.getElementById('list_type').value
const response = await fetch(`/shopping-lists/addList`, { const response = await fetch(`/shopping-lists/api/addList`, {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',

View File

@ -304,6 +304,6 @@
</div> </div>
</div> </div>
</body> </body>
<script src="{{ url_for('static', filename='handlers/shoppingListEditHandler.js') }}"></script> <script src="{{ url_for('shopping_list_API.static', filename='js/shoppingListEditHandler.js') }}"></script>
<script>const sl_id = {{id|tojson}}</script> <script>const sl_id = {{id|tojson}}</script>
</html> </html>

View File

@ -156,5 +156,5 @@
</div> </div>
</div> </div>
</body> </body>
<script src="{{ url_for('static', filename='handlers/shoppingListsHandler.js') }}"></script> <script src="{{ url_for('shopping_list_API.static', filename='js/shoppingListsHandler.js') }}"></script>
</html> </html>

View File

@ -116,6 +116,6 @@
</div> </div>
</div> </div>
</body> </body>
<script src="{{ url_for('static', filename='handlers/shoppingListViewHandler.js') }}"></script> <script src="{{ url_for('shopping_list_API.static', filename='js/shoppingListViewHandler.js') }}"></script>
<script>const sl_id = {{id|tojson}}</script> <script>const sl_id = {{id|tojson}}</script>
</html> </html>

View File

@ -1944,3 +1944,15 @@
2025-07-04 08:19:34.889657 --- ERROR --- DatabaseError(message='invalid input syntax for type integer: "{"cost": 1.99, "expires": false}"LINE 3: ...41789001314%', 'Chicken Ramen Noodle Soup', 1, 5, '{"cost": ... ^', 2025-07-04 08:19:34.889657 --- ERROR --- DatabaseError(message='invalid input syntax for type integer: "{"cost": 1.99, "expires": false}"LINE 3: ...41789001314%', 'Chicken Ramen Noodle Soup', 1, 5, '{"cost": ... ^',
payload=('sku', 23, '%041789001314%', 'Chicken Ramen Noodle Soup', 1, 5, '{"cost": 1.99, "expires": false}', 'Unresolved'), payload=('sku', 23, '%041789001314%', 'Chicken Ramen Noodle Soup', 1, 5, '{"cost": 1.99, "expires": false}', 'Unresolved'),
sql='INSERT INTO test_recipe_items(uuid, rp_id, item_type, item_name, uom, qty, item_id, links) VALUES (%s, %s, %s, %s, %s, %s, %s, %s) RETURNING *;') sql='INSERT INTO test_recipe_items(uuid, rp_id, item_type, item_name, uom, qty, item_id, links) VALUES (%s, %s, %s, %s, %s, %s, %s, %s) RETURNING *;')
2025-07-12 07:48:13.460875 --- ERROR --- DatabaseError(message='invalid input syntax for type integer: "each"LINE 3: VALUES ('5g89bj2', '5', 'custom', 'test', 'each', 1, NULL, '... ^',
payload=('5g89bj2', '5', 'custom', 'test', 'each', 1, None, '{"main": ""}'),
sql='INSERT INTO test_shopping_list_items(uuid, sl_id, item_type, item_name, uom, qty, item_id, links) VALUES (%s, %s, %s, %s, %s, %s, %s, %s) RETURNING *;')
2025-07-12 07:57:23.925776 --- ERROR --- DatabaseError(message='syntax error at or near "ASC"LINE 16: ORDER BY ASC ^',
payload=['', 25, 0],
sql='WITH sum_cte AS ( SELECT mi.id, SUM(mil.quantity_on_hand)::FLOAT8 AS total_sum FROM test_item_locations mil JOIN test_items mi ON mil.part_id = mi.id GROUP BY mi.id )SELECT test_items.*, row_to_json(test_item_info.*) as item_info, sum_cte.total_sum as total_qoh, (SELECT COALESCE(row_to_json(u), '{}') FROM units as u WHERE u.id=test_item_info.uom) as uomFROM test_itemsLEFT JOIN sum_cte ON test_items.id = sum_cte.idLEFT JOIN test_item_info ON test_items.item_info_id = test_item_info.idWHERE test_items.search_string LIKE '%%' || %s || '%%'ORDER BY ASCLIMIT %s OFFSET %s;')
2025-07-12 07:58:21.551161 --- ERROR --- DatabaseError(message='syntax error at or near "ASC"LINE 16: ORDER BY ASC ^',
payload=['', 25, 0],
sql='WITH sum_cte AS ( SELECT mi.id, SUM(mil.quantity_on_hand)::FLOAT8 AS total_sum FROM test_item_locations mil JOIN test_items mi ON mil.part_id = mi.id GROUP BY mi.id )SELECT test_items.*, row_to_json(test_item_info.*) as item_info, sum_cte.total_sum as total_qoh, (SELECT COALESCE(row_to_json(u), '{}') FROM units as u WHERE u.id=test_item_info.uom) as uomFROM test_itemsLEFT JOIN sum_cte ON test_items.id = sum_cte.idLEFT JOIN test_item_info ON test_items.item_info_id = test_item_info.idWHERE test_items.search_string LIKE '%%' || %s || '%%' ORDER BY ASC LIMIT %s OFFSET %s;')
2025-07-12 08:43:43.017720 --- ERROR --- DatabaseError(message='invalid input syntax for type integer: " Pinch"LINE 1: ... = 'Acai-Blueberry-Pomegranate', qty = '1', uom = ' Pinch', ... ^',
payload={'id': 12, 'update': {'item_name': 'Acai-Blueberry-Pomegranate', 'qty': '1', 'uom': ' Pinch', 'links': {'main': 'test'}}},
sql='UPDATE test_shopping_list_items SET item_name = %s, qty = %s, uom = %s, links = %s WHERE id=%s RETURNING *;')

View File

@ -1,7 +1,7 @@
import celery.schedules import celery.schedules
from flask import Flask, render_template, session, request, redirect, jsonify from flask import Flask, render_template, session, request, redirect, jsonify
from flask_assets import Environment, Bundle from flask_assets import Environment, Bundle
import api, config, user_api, psycopg2, main, api_admin, receipts_API, shopping_list_API, group_api import api, config, user_api, psycopg2, main, api_admin, receipts_API, group_api
from user_api import login_required, update_session_user from user_api import login_required, update_session_user
from workshop_api import workshop_api from workshop_api import workshop_api
import database import database
@ -10,6 +10,7 @@ from webpush import trigger_push_notifications_for_subscriptions
from application.recipes import recipes_api from application.recipes import recipes_api
from application.items import items_API from application.items import items_API
from application.poe import poe_api from application.poe import poe_api
from application.shoppinglists import shoplist_api
from flasgger import Swagger from flasgger import Swagger
@ -31,7 +32,7 @@ app.register_blueprint(items_API.items_api, url_prefix='/items')
app.register_blueprint(poe_api.point_of_ease, url_prefix='/poe') app.register_blueprint(poe_api.point_of_ease, url_prefix='/poe')
app.register_blueprint(workshop_api) app.register_blueprint(workshop_api)
app.register_blueprint(receipts_API.receipt_api) app.register_blueprint(receipts_API.receipt_api)
app.register_blueprint(shopping_list_API.shopping_list_api) app.register_blueprint(shoplist_api.shopping_list_api, url_prefix="/shopping-lists")
app.register_blueprint(group_api.groups_api) app.register_blueprint(group_api.groups_api)
app.register_blueprint(recipes_api.recipes_api, url_prefix='/recipes') app.register_blueprint(recipes_api.recipes_api, url_prefix='/recipes')