Added the ability to generate shopping lists

dynamically
This commit is contained in:
Jadowyne Ulve 2025-08-19 16:47:48 -05:00
parent a22faeb7a8
commit faafa75422
13 changed files with 333 additions and 15 deletions

View File

@ -238,6 +238,18 @@ def deleteListItem():
return jsonify({"error":False, "message":"item deleted succesfully!"})
return jsonify({"error":True, "message":"There was an error with this POST statement"})
@shopping_list_api.route('/api/deleteList', methods=["POST"])
@access_api.login_required
def deleteList():
if request.method == "POST":
shopping_list_uuid = request.get_json()['shopping_list_uuid']
site_name = session['selected_site']
user_id = session['user_id']
shoplist_processess.deleteShoppingList(site_name, {'shopping_list_uuid': shopping_list_uuid}, user_id)
return jsonify({"error":False, "message":"List Deleted succesfully!"})
return jsonify({"error":True, "message":"There was an error with this POST statement"})
# Added to Database
@shopping_list_api.route('/api/saveListItem', methods=["POST"])
@access_api.login_required
@ -293,3 +305,15 @@ def setListItemState():
return jsonify({"list_items":items, "error":False, "message":"items fetched succesfully!"})
return jsonify({"list_items":items, "error":True, "message":"There was an error with this GET statement"})
@shopping_list_api.route('/api/postGeneratedList', methods=["POST"])
@access_api.login_required
def postGeneratedList():
if request.method == "POST":
payload: dict = request.get_json()
site_name: str = session['selected_site']
user_id: int = session['user_id']
shoplist_processess.postNewGeneratedList(site_name, payload, user_id)
return jsonify(status=201, message=f"List Generated successfully!")
return jsonify(status=405, message=f"{request.method} is not an accepted method on this endpoint!")

View File

@ -1,7 +1,6 @@
# 3rd Party imports
import psycopg2
# applications imports
import config
from application import postsqldb
@ -150,7 +149,6 @@ def getRecipeItemsByUUID(site, payload, convert=True, conn=None):
except Exception as error:
raise postsqldb.DatabaseError(error, payload, sql)
def getItemsWithQOH(site, payload, convert=True, conn=None):
recordset = []
count = 0
@ -263,7 +261,6 @@ def getListsModal(site, payload, convert=True, conn=None):
except Exception as error:
raise postsqldb.DatabaseError(error, payload, sql)
def getItemsModal(site, payload, convert=True, conn=None):
recordsets = []
count = 0
@ -298,6 +295,63 @@ def getItemsModal(site, payload, convert=True, conn=None):
except Exception as error:
raise postsqldb.DatabaseError(error, payload, sql)
def getItemByUUID(site, payload:dict, convert=True, conn=None):
""" payload: dict = {'item_uuid'}"""
record = ()
self_conn = False
with open('application/shoppinglists/sql/getItemByUUID.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 deleteShoppingListsTuple(site_name, payload, convert=True, conn=None):
deleted = ()
self_conn = False
sql = f"WITH deleted_rows AS (DELETE FROM {site_name}_shopping_lists WHERE {site_name}_shopping_lists.list_uuid 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 deleteShoppingListItemsTuple(site_name, payload, convert=True, conn=None):
deleted = ()
self_conn = False

View File

@ -36,4 +36,152 @@ def addRecipeItemsToList(site:str, data:dict, user_id: int, conn=None):
if self_conn:
conn.commit()
conn.close()
def postNewGeneratedList(site: str, data: dict, user_id: int, conn=None):
"""data={'list_type', 'list_name', 'list_description', 'custom_items', 'uncalculated_items', 'calculated_items', 'recipes', 'full_system_calculated', 'shopping_lists'}"""
list_type: str = data['list_type']
list_name: str = data['list_name']
list_description: str = data['list_description']
custom_items: list = data['custom_items']
uncalculated_items: list = data['uncalculated_items']
calculated_items: list = data['calculated_items']
recipes: list = data['recipes']
full_system_calculated: list = data['full_system_calculated']
shopping_lists: list = data['shopping_lists']
self_conn=False
if not conn:
database_config = config.config()
conn = psycopg2.connect(**database_config)
conn.autocommit = False
self_conn = True
shopping_list = database_payloads.ShoppingListPayload(
name=list_name,
description=list_description,
author=int(user_id),
sub_type="plain",
list_type=list_type
)
shopping_list = shoplist_database.insertShoppingListsTuple(site, shopping_list.payload(), conn=conn)
items_to_add_to_system = []
# start by checcking if i should iterate full sku calc
if full_system_calculated:
safety_stock_items = shoplist_database.getItemsSafetyStock(site, conn=conn)
for item in safety_stock_items:
qty = float(item['item_info']['safety_stock']-float(item['total_sum']))
temp_item = database_payloads.ShoppingListItemPayload(
list_uuid=shopping_list['list_uuid'],
item_type='calculated sku',
item_name=item['item_name'],
uom=item['item_info']['uom'],
qty=qty,
item_uuid=item['item_uuid'],
links=item['links']
)
items_to_add_to_system.append(temp_item)
if calculated_items and not full_system_calculated:
for item_uuid in calculated_items:
item = shoplist_database.getItemByUUID(site, {'item_uuid': item_uuid}, conn=conn)
qty = float(item['item_info']['safety_stock']-float(item['total_sum']))
temp_item = database_payloads.ShoppingListItemPayload(
list_uuid=shopping_list['list_uuid'],
item_type='calculated sku',
item_name=item['item_name'],
uom=item['item_info']['uom'],
qty=qty,
item_uuid=item['item_uuid'],
links=item['links']
)
items_to_add_to_system.append(temp_item)
if custom_items:
for item in custom_items:
temp_item = database_payloads.ShoppingListItemPayload(
list_uuid=shopping_list['list_uuid'],
item_type='custom',
item_name=item['item_name'],
uom=item['uom'],
qty=float(item['qty']),
item_uuid=None,
links={'main': item['link']}
)
items_to_add_to_system.append(temp_item)
if uncalculated_items:
for item in uncalculated_items:
temp_item = database_payloads.ShoppingListItemPayload(
list_uuid=shopping_list['list_uuid'],
item_type='uncalculated sku',
item_name=item['item_name'],
uom=item['uom'],
qty=float(item['qty']),
item_uuid=None,
links={'main': item['link']}
)
items_to_add_to_system.append(temp_item)
if recipes:
for recipe_uuid in recipes:
recipe_items = shoplist_database.getRecipeItemsByUUID(site, (recipe_uuid,), conn=conn)
for item in recipe_items:
temp_item = database_payloads.ShoppingListItemPayload(
list_uuid=shopping_list['list_uuid'],
item_type='recipe',
item_name=item['item_name'],
uom=item['uom'],
qty=float(item['qty']),
item_uuid=item['item_uuid'],
links=item['links']
)
items_to_add_to_system.append(temp_item)
if shopping_lists:
for shopping_list_uuid in shopping_lists:
shopping_list_items = shoplist_database.getShoppingList(site, (shopping_list_uuid,), conn=conn)['sl_items']
for item in shopping_list_items:
temp_item = database_payloads.ShoppingListItemPayload(
list_uuid=shopping_list['list_uuid'],
item_type=item['item_type'],
item_name=item['item_name'],
uom=item['uom']['id'],
qty=float(item['qty']),
item_uuid=item['item_uuid'],
links=item['links']
)
items_to_add_to_system.append(temp_item)
if items_to_add_to_system:
for item in items_to_add_to_system:
shoplist_database.insertShoppingListItemsTuple(site, item.payload(), conn=conn)
if self_conn:
conn.commit()
conn.close()
def deleteShoppingList(site: str, data: dict, user_id: int, conn=None):
shopping_list_uuid = data['shopping_list_uuid']
self_conn=False
if not conn:
database_config = config.config()
conn = psycopg2.connect(**database_config)
conn.autocommit = False
self_conn = True
shopping_list_items = shoplist_database.getShoppingList(site, (shopping_list_uuid, ), conn=conn)['sl_items']
shopping_list_items = [item['list_item_uuid'] for item in shopping_list_items]
shoplist_database.deleteShoppingListsTuple(site, (shopping_list_uuid,), conn=conn)
shoplist_database.deleteShoppingListItemsTuple(site, shopping_list_items, conn=conn)
if self_conn:
conn.commit()
conn.close()

View File

@ -0,0 +1,15 @@
WITH sum_cte AS (
SELECT mi.id, SUM(mil.quantity_on_hand) 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 items.*,
COALESCE(row_to_json(item_info.*), '{}') AS item_info,
COALESCE(sum_cte.total_sum, 0) AS total_sum
FROM %%site_name%%_items items
LEFT JOIN %%site_name%%_item_info item_info ON items.item_info_id = item_info.id
LEFT JOIN units ON units.id = item_info.uom
LEFT JOIN sum_cte ON items.id = sum_cte.id
WHERE items.item_uuid = %(item_uuid)s

View File

@ -1074,4 +1074,28 @@ async function generateListsTable() {
}
}
}
// Generate Functions
async function postGenerateList() {
let data = {
list_type: String(document.getElementById('generated_list_type').value),
list_name: String(document.getElementById('generated_list_name').value),
list_description: String(document.getElementById('generated_list_description').value),
custom_items: Object.values(custom_items),
uncalculated_items: Object.values(uncalculated_items),
calculated_items: Object.keys(calculated_items),
recipes: Object.keys(recipes),
full_system_calculated: full_sku_enabled,
shopping_lists: Object.keys(shopping_lists)
}
const response = await fetch(`/shopping-lists/api/postGeneratedList`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
});
}

View File

@ -54,11 +54,11 @@ async function replenishShoppingListCards(lists) {
footer_div.setAttribute('class', 'uk-card-footer')
footer_div.style = 'height: 40px; border: none;'
let editOp = document.createElement('a')
editOp.setAttribute('class', 'uk-button uk-button-small uk-button-default')
editOp.innerHTML = '<span uk-icon="icon: pencil"></span> Edit'
editOp.style = "margin-right: 10px;"
editOp.href = `/shopping-lists/edit/${lists[i].list_uuid}`
//let editOp = document.createElement('a')
//editOp.setAttribute('class', 'uk-button uk-button-small uk-button-default')
//editOp.innerHTML = '<span uk-icon="icon: pencil"></span> Edit'
//editOp.style = "margin-right: 10px;"
//editOp.href = `/shopping-lists/edit/${lists[i].list_uuid}`
let viewOp = document.createElement('a')
viewOp.setAttribute('class', 'uk-button uk-button-small uk-button-default')
@ -66,8 +66,14 @@ async function replenishShoppingListCards(lists) {
viewOp.href = `/shopping-lists/view/${lists[i].list_uuid}`
//viewOp.style = "margin-right: 20px;"
let deleteOp = document.createElement('a')
deleteOp.setAttribute('class', 'uk-button uk-button-small uk-button-default')
deleteOp.innerHTML = '<span uk-icon="icon: eye"></span> Delete'
deleteOp.onclick = async function(params) { await deleteList(lists[i].list_uuid)}
//viewOp.style = "margin-right: 20px;"
footer_div.append(editOp, viewOp)
footer_div.append(viewOp, deleteOp)
main_div.append(card_header_div, body_div, footer_div)
@ -123,6 +129,35 @@ async function addList() {
});
}
async function deleteList(shopping_list_uuid) {
const response = await fetch(`/shopping-lists/api/deleteList`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
shopping_list_uuid: shopping_list_uuid
}),
});
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
});
let lists = await getShoppingLists()
await replenishShoppingListCards(lists)
await updatePaginationElement()
}
async function changeSite(site){
const response = await fetch(`/changeSite`, {
method: 'POST',

View File

@ -138,7 +138,7 @@
</div>
<!-- basic info section -->
<div class="uk-width-1-1">
<h1 class="uk-heading-xsmall uk-heading-divider">Shopping List Type</h1>
<h1 class="uk-heading-xsmall uk-heading-divider">Shopping List Info</h1>
</div>
<div class="uk-width-1-1">
<p class="uk-text-small">Fill out the basic info asked for here, the description could be helpful to remind yourself and others what the list was generated for.
@ -152,11 +152,14 @@
</div>
</div>
<div class="uk-width-1-1">
<label class="uk-form-label" for="generated_list_name">Shopping List Description</label>
<label class="uk-form-label" for="generated_list_description">Shopping List Description</label>
<div class="uk-form-controls">
<textarea id="generated_list_name" class="uk-textarea" rows="5" placeholder="Enter list description here..." aria-label="Textarea"></textarea>
<textarea id="generated_list_description" class="uk-textarea" rows="5" placeholder="Enter list description here..." aria-label="Textarea"></textarea>
</div>
</div>
<div class="uk-width-1-1">
<button onclick="postGenerateList()" class="uk-button uk-button-primary uk-align-right">Generate List</button>
</div>
<!-- Part section -->
<div class="uk-width-1-1">
<h1 class="uk-heading-xsmall uk-heading-divider">Shopping List Operators</h1>

View File

@ -112,7 +112,7 @@
<div uk-grid>
<div class="uk-width-1-2@m">
<ul class="uk-iconnav uk-flex-center uk-flex-left@m">
<li><a onclick="openAddListModal()" uk-icon="icon: plus">Add List</a></li>
<!--li><a onclick="openAddListModal()" uk-icon="icon: plus">Add List</a></li-->
<li><a href="/shopping-lists/generate" uk-icon="icon: plus">Generate List</a></li>
<li><a href="#" uk-icon="icon: cloud-download">download</a></li>
</ul>

View File

@ -562,4 +562,19 @@
sql='INSERT INTO main_logistics_info(barcode, primary_location, primary_zone, auto_issue_location, auto_issue_zone) VALUES (%s, %s, %s, %s, %s) RETURNING *;')
2025-08-17 19:22:32.824380 --- ERROR --- DatabaseError(message='duplicate key value violates unique constraint "main_item_info_barcode_key"DETAIL: Key (barcode)=(%%) already exists.',
payload=('%%', '', 1.0, 1, 0.0, 0.0, 0.0, False, '{}'),
sql='INSERT INTO main_item_info(barcode, packaging, uom_quantity, uom, cost, safety_stock, lead_time_days, ai_pick, prefixes) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s) RETURNING *;')
sql='INSERT INTO main_item_info(barcode, packaging, uom_quantity, uom, cost, safety_stock, lead_time_days, ai_pick, prefixes) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s) RETURNING *;')
2025-08-19 14:46:59.332054 --- ERROR --- DatabaseError(message='duplicate key value violates unique constraint "main_barcodes_pkey"DETAIL: Key (barcode)=(%181945000062%) already exists.',
payload=('%181945000062%', 'faecba1e-8817-4e19-9ade-d43cb4602aae', '1', '1', ''),
sql='INSERT INTO main_barcodes (barcode, item_uuid, in_exchange, out_exchange, descriptor) VALUES (%s, %s, %s, %s, %s) RETURNING *;')
2025-08-19 15:39:25.102811 --- ERROR --- DatabaseError(message='dict is not a sequence',
payload={'item_uuid': '392f05b5-4ccd-41da-875d-0e593f51b610'},
sql='WITH sum_cte AS ( SELECT mi.id, SUM(mil.quantity_on_hand) AS total_sum FROM main_item_locations mil JOIN main_items mi ON mil.part_id = mi.id GROUP BY mi.id)SELECT * FROM main_items itemsLEFT JOIN main_item_info ON main_items.item_info_id = main_item_info.idLEFT JOIN units ON units.id = main_item_info.uomLEFT JOIN sum_cte ON main_items.id = sum_cte.idWHERE items.item_uuid = %{item_uuid}s')
2025-08-19 15:43:08.370309 --- ERROR --- DatabaseError(message='invalid reference to FROM-clause entry for table "main_items"LINE 10: LEFT JOIN main_item_info ON main_items.item_info_id = main_i... ^HINT: Perhaps you meant to reference the table alias "items".',
payload={'item_uuid': '392f05b5-4ccd-41da-875d-0e593f51b610'},
sql='WITH sum_cte AS ( SELECT mi.id, SUM(mil.quantity_on_hand) AS total_sum FROM main_item_locations mil JOIN main_items mi ON mil.part_id = mi.id GROUP BY mi.id)SELECT * FROM main_items itemsLEFT JOIN main_item_info ON main_items.item_info_id = main_item_info.idLEFT JOIN units ON units.id = main_item_info.uomLEFT JOIN sum_cte ON main_items.id = sum_cte.idWHERE items.item_uuid = %(item_uuid)s')
2025-08-19 15:45:09.890974 --- ERROR --- DatabaseError(message='syntax error at or near "FROM"LINE 11: FROM main_items items ^',
payload={'item_uuid': '392f05b5-4ccd-41da-875d-0e593f51b610'},
sql='WITH sum_cte AS ( SELECT mi.id, SUM(mil.quantity_on_hand) AS total_sum FROM main_item_locations mil JOIN main_items mi ON mil.part_id = mi.id GROUP BY mi.id)SELECT main_items.*, COALESCE(row_to_json(main_item_info.*), '{}') AS item_info, COALESCE(sum_cte.total_sum, 0) AS total_sum,FROM main_items itemsLEFT JOIN main_item_info item_info ON items.item_info_id = item_info.idLEFT JOIN units ON units.id = item_info.uomLEFT JOIN sum_cte ON items.id = sum_cte.idWHERE items.item_uuid = %(item_uuid)s')
2025-08-19 15:45:32.184317 --- ERROR --- DatabaseError(message='syntax error at or near "FROM"LINE 11: FROM main_items items ^',
payload={'item_uuid': '392f05b5-4ccd-41da-875d-0e593f51b610'},
sql='WITH sum_cte AS ( SELECT mi.id, SUM(mil.quantity_on_hand) AS total_sum FROM main_item_locations mil JOIN main_items mi ON mil.part_id = mi.id GROUP BY mi.id)SELECT items.*, COALESCE(row_to_json(item_info.*), '{}') AS item_info, COALESCE(sum_cte.total_sum, 0) AS total_sum,FROM main_items itemsLEFT JOIN main_item_info item_info ON items.item_info_id = item_info.idLEFT JOIN units ON units.id = item_info.uomLEFT JOIN sum_cte ON items.id = sum_cte.idWHERE items.item_uuid = %(item_uuid)s')

BIN
static/pictures/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB