Added site planners to list generation

This commit is contained in:
Jadowyne Ulve 2025-08-20 17:30:25 -05:00
parent faafa75422
commit 160c21427d
12 changed files with 333 additions and 25 deletions

View File

@ -314,6 +314,7 @@ def postGeneratedList():
payload: dict = request.get_json() payload: dict = request.get_json()
site_name: str = session['selected_site'] site_name: str = session['selected_site']
user_id: int = session['user_id'] user_id: int = session['user_id']
print(payload)
shoplist_processess.postNewGeneratedList(site_name, payload, user_id) shoplist_processess.postNewGeneratedList(site_name, payload, user_id)
return jsonify(status=201, message=f"List Generated successfully!") 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!") return jsonify(status=405, message=f"{request.method} is not an accepted method on this endpoint!")

View File

@ -324,6 +324,36 @@ def getItemByUUID(site, payload:dict, convert=True, conn=None):
except Exception as error: except Exception as error:
raise postsqldb.DatabaseError(error, payload, sql) raise postsqldb.DatabaseError(error, payload, sql)
def getEventRecipes(site, payload, convert=True, conn=None):
""" payload: dict = {'plan_uuid', 'start_date', 'end_date'}"""
records = ()
self_conn = False
with open('application/shoppinglists/sql/getEventsRecipes.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.fetchall()
if rows and convert:
records = [postsqldb.tupleDictionaryFactory(cur.description, row) for row in rows]
elif rows and not convert:
records = rows
if self_conn:
conn.close()
return records
except Exception as error:
raise postsqldb.DatabaseError(error, payload, sql)
def deleteShoppingListsTuple(site_name, payload, convert=True, conn=None): def deleteShoppingListsTuple(site_name, payload, convert=True, conn=None):
deleted = () deleted = ()
self_conn = False self_conn = False

View File

@ -48,6 +48,7 @@ def postNewGeneratedList(site: str, data: dict, user_id: int, conn=None):
recipes: list = data['recipes'] recipes: list = data['recipes']
full_system_calculated: list = data['full_system_calculated'] full_system_calculated: list = data['full_system_calculated']
shopping_lists: list = data['shopping_lists'] shopping_lists: list = data['shopping_lists']
site_plans: list = data['site_plans']
self_conn=False self_conn=False
@ -158,6 +159,26 @@ def postNewGeneratedList(site: str, data: dict, user_id: int, conn=None):
items_to_add_to_system.append(temp_item) items_to_add_to_system.append(temp_item)
if site_plans:
for site_plan in site_plans:
if site_plan['plan_uuid'] == 'site': site_plan['plan_uuid'] = None
plan_recipes = [event['recipe_uuid'] for event in shoplist_database.getEventRecipes(site, site_plan, conn=conn)]
if plan_recipes:
for recipe_uuid in plan_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 items_to_add_to_system: if items_to_add_to_system:
for item in items_to_add_to_system: for item in items_to_add_to_system:
shoplist_database.insertShoppingListItemsTuple(site, item.payload(), conn=conn) shoplist_database.insertShoppingListItemsTuple(site, item.payload(), conn=conn)
@ -180,8 +201,10 @@ def deleteShoppingList(site: str, data: dict, user_id: int, conn=None):
shopping_list_items = [item['list_item_uuid'] for item in shopping_list_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.deleteShoppingListsTuple(site, (shopping_list_uuid,), conn=conn)
if shopping_list_items:
shoplist_database.deleteShoppingListItemsTuple(site, shopping_list_items, conn=conn) shoplist_database.deleteShoppingListItemsTuple(site, shopping_list_items, conn=conn)
if self_conn: if self_conn:
conn.commit() conn.commit()
conn.close() conn.close()

View File

@ -0,0 +1,7 @@
SELECT events.recipe_uuid
FROM %%site_name%%_plan_events events
WHERE events.plan_uuid IS NULL
AND events.event_type = 'recipe'
AND events.recipe_uuid IS NOT NULL
AND events.event_date_start <= %(end_date)s
AND events.event_date_end >= %(start_date)s;

View File

@ -1076,6 +1076,130 @@ async function generateListsTable() {
} }
// Site Planner Functions
var site_planners = {}
var site_planner_card_active = false;
async function addPlannerCard(){
if(!site_planner_card_active){
document.getElementById('plannerCard').hidden = false
site_planner_card_active = true;
}
}
async function removePlannerCard(){
document.getElementById('plannerCard').hidden = true
site_planner_card_active = false;
site_planners = []
}
var PlannerZoneState = true
async function changePlannerZoneState() {
PlannerZoneState = !PlannerZoneState
document.getElementById('plannerZone').hidden = !PlannerZoneState
}
async function openPlannerModal(){
document.getElementById('planUUID').setAttribute('class', 'uk-input uk-disabled')
document.getElementById('planUUID').value = 'site'
document.getElementById('planStartDate').value = ''
document.getElementById('planEndDate').value = ''
document.getElementById('plannerModalButton').innerHTML = "Save"
document.getElementById('plannerModalButton').onclick = async function () { await addPlanner()}
UIkit.modal(document.getElementById('plannerModal')).show()
}
async function addPlanner() {
var planner_select = document.getElementById('planUUID')
planner_uuid = planner_select.value
plan_name = planner_select.options[planner_select.selectedIndex].text
startDate = document.getElementById('planStartDate').value
endDate = document.getElementById('planEndDate').value
site_planners[planner_uuid] = {
start_date: startDate,
end_date: endDate,
plan_uuid: planner_uuid,
plan_name: plan_name
}
UIkit.modal(document.getElementById('plannerModal')).hide()
console.log(site_planners)
await generatePlannerTable()
}
async function editPlanner(planUUID) {
let data = site_planners[planUUID]
document.getElementById('planUUID').setAttribute('class', 'uk-input uk-disabled')
document.getElementById('planUUID').value = data['plan_uuid']
document.getElementById('planStartDate').value = data['start_date']
document.getElementById('planEndDate').value = data['end_date']
document.getElementById('plannerModalButton').innerHTML = "Save"
document.getElementById('plannerModalButton').onclick = async function () {
var planner_select = document.getElementById('planUUID')
planner_uuid = planner_select.value
plan_name = planner_select.options[planner_select.selectedIndex].text
startDate = document.getElementById('planStartDate').value
endDate = document.getElementById('planEndDate').value
site_planners[planner_uuid] = {
start_date: startDate,
end_date: endDate,
plan_uuid: planner_uuid,
plan_name: plan_name
}
await generatePlannerTable()
UIkit.modal(document.getElementById('plannerModal')).hide()
}
UIkit.modal(document.getElementById('plannerModal')).show()
}
async function deletePlan(plannerUUID) {
delete site_planners[plannerUUID]
await generatePlannerTable()
}
async function generatePlannerTable() {
let plannerTableBody = document.getElementById('plannerTableBody')
plannerTableBody.innerHTML = ""
for(const key in site_planners){
if(site_planners.hasOwnProperty(key)){
let tableRow = document.createElement('tr')
let nameCell = document.createElement('td')
nameCell.innerHTML = `${site_planners[key].plan_name}`
let startCell = document.createElement('td')
startCell.innerHTML = `${site_planners[key].start_date}`
let endCell = document.createElement('td')
endCell.innerHTML = `${site_planners[key].end_date}`
let opCell = document.createElement('td')
let editButton = document.createElement('button')
editButton.setAttribute('class', 'uk-button uk-button-default uk-button-small')
editButton.setAttribute('uk-tooltip', 'Edits this rows plan dates.')
editButton.innerHTML = "Edit"
editButton.onclick = async function() {await editPlanner(site_planners[key].plan_uuid)}
let removeButton = document.createElement('button')
removeButton.setAttribute('class', 'uk-button uk-button-default uk-button-small')
removeButton.setAttribute('uk-tooltip', 'Removes Shopping List from the saved shopping lists')
removeButton.innerHTML = "Remove"
removeButton.onclick = async function() {await deletePlan(site_planners[key].plan_uuid)}
opCell.append(editButton, removeButton)
tableRow.append(nameCell, startCell, endCell, opCell)
plannerTableBody.append(tableRow)
}
}
}
// Generate Functions // Generate Functions
async function postGenerateList() { async function postGenerateList() {
@ -1088,7 +1212,8 @@ async function postGenerateList() {
calculated_items: Object.keys(calculated_items), calculated_items: Object.keys(calculated_items),
recipes: Object.keys(recipes), recipes: Object.keys(recipes),
full_system_calculated: full_sku_enabled, full_system_calculated: full_sku_enabled,
shopping_lists: Object.keys(shopping_lists) shopping_lists: Object.keys(shopping_lists),
site_plans: Object.values(site_planners)
} }
const response = await fetch(`/shopping-lists/api/postGeneratedList`, { const response = await fetch(`/shopping-lists/api/postGeneratedList`, {
@ -1098,4 +1223,5 @@ async function postGenerateList() {
}, },
body: JSON.stringify(data), body: JSON.stringify(data),
}); });
location.href = "/shopping-lists"
} }

View File

@ -23,30 +23,55 @@ async function replenishLineTable(sl_items){
listItemsTableBody.innerHTML = "" listItemsTableBody.innerHTML = ""
console.log(sl_items) console.log(sl_items)
for(let i = 0; i < sl_items.length; i++){ let grouped = sl_items.reduce((accumen, item) => {
let tableRow = document.createElement('tr') if (!accumen[item.item_type]) {
accumen[item.item_type] = [];
}
accumen[item.item_type].push(item);
return accumen;
}, {});
console.log(grouped)
for(let key in grouped){
console.log(key)
let items = grouped[key]
let headerRow = document.createElement('tr')
let headerCell = document.createElement('td')
headerCell.colSpan = 3;
headerCell.textContent = key.toUpperCase();
headerCell.className = 'type-header';
headerCell.style = `font-weight: bold;background: #eee; text-align: left;`
headerRow.appendChild(headerCell);
listItemsTableBody.appendChild(headerRow);
for(let i = 0; i < items.length; i++){
console.log(items)
let tableRow = document.createElement('tr')
let item = items[i]
let checkboxCell = document.createElement('td') let checkboxCell = document.createElement('td')
checkboxCell.innerHTML = `<label><input class="uk-checkbox" type="checkbox" ${sl_items[i].list_item_state ? 'checked' : ''}></label>` checkboxCell.innerHTML = `<label><input class="uk-checkbox" type="checkbox" ${item.list_item_state ? 'checked' : ''}></label>`
checkboxCell.onclick = async function (event) { checkboxCell.onclick = async function (event) {
await updateListItemState(sl_items[i].list_item_uuid, event.target.checked) console.log(item)
await updateListItemState(item.list_item_uuid, event.target.checked)
} }
namefield = sl_items[i].item_name namefield = items[i].item_name
if(sl_items[i].links.hasOwnProperty('main')){ if(items[i].links.hasOwnProperty('main')){
namefield = `<a href=${sl_items[i].links.main} target='_blank'>${sl_items[i].item_name}</a>` namefield = `<a href=${item.links.main} target='_blank'>${item.item_name}</a>`
} }
let nameCell = document.createElement('td') let nameCell = document.createElement('td')
nameCell.innerHTML = namefield nameCell.innerHTML = namefield
let qtyuomCell = document.createElement('td') let qtyuomCell = document.createElement('td')
qtyuomCell.innerHTML = `${sl_items[i].qty} ${sl_items[i].uom.fullname}` qtyuomCell.innerHTML = `${item.qty} ${item.uom.fullname}`
checkboxCell.checked = sl_items[i].list_item_state checkboxCell.checked = item.list_item_state
tableRow.append(checkboxCell, nameCell, qtyuomCell) tableRow.append(checkboxCell, nameCell, qtyuomCell)
listItemsTableBody.append(tableRow) listItemsTableBody.append(tableRow)
} }
}
} }
async function fetchShoppingList() { async function fetchShoppingList() {

View File

@ -176,11 +176,11 @@
<li class="uk-nav-header">Active Operators</li> <li class="uk-nav-header">Active Operators</li>
<li class="uk-nav-divider"></li> <li class="uk-nav-divider"></li>
<li><a onclick="addCustomItemsCard()" uk-tooltip="title: Create Custom items to add into the generated list.; pos: right">Custom Items</a></li> <li><a onclick="addCustomItemsCard()" uk-tooltip="title: Create Custom items to add into the generated list.; pos: right">Custom Items</a></li>
<li><a onclick="addUncalculatedItemsCard()" uk-tooltip="title: Add items from the system that take a static quantity into the generated list.; pos: right">Non-Calculated System Items</a></li> <li><a onclick="addUncalculatedItemsCard()" uk-tooltip="title: Add items from the system that take a static quantity into the generated list.; pos: right">Un-Calculated System Items</a></li>
<li><a onclick="addCalculatedItemsCard()" uk-tooltip="title: Add items from the system that calculate quantity using quantity on hand and a set safety stocks into the generated list.; pos: right">Calculated System Items</a></li> <li><a onclick="addCalculatedItemsCard()" uk-tooltip="title: Add items from the system that calculate quantity using quantity on hand and a set safety stocks into the generated list.; pos: right">Calculated System Items</a></li>
<li><a onclick="addRecipesCard()" uk-tooltip="title: Add Recipes that will take all the ingrediants and add them into the generated list.; pos: right">System Recipes</a></li> <li><a onclick="addRecipesCard()" uk-tooltip="title: Add Recipes that will take all the ingrediants and add them into the generated list.; pos: right">System Recipes</a></li>
<li><a onclick="addFullSKUCard()" uk-tooltip="title: Takes a full safety stock count from the system and adds any quantities below their safety stocks into the generated list.; pos: right">Full System Calculated</a></li> <li><a onclick="addFullSKUCard()" uk-tooltip="title: Takes a full safety stock count from the system and adds any quantities below their safety stocks into the generated list.; pos: right">Full System Calculated</a></li>
<li><a class="uk-disabled" uk-tooltip="title: Uses a date range and selected planners for each to add any planned recipes into the generated list.; pos: right">Site Planners</a></li> <li><a onclick="addPlannerCard()" uk-tooltip="title: Uses a date range and selected planners for each to add any planned recipes into the generated list.; pos: right">Site Planners</a></li>
<li><a onclick="addListsCard()" uk-tooltip="title: Combine already made lists into this one; pos: right">Shopping Lists</a></li> <li><a onclick="addListsCard()" uk-tooltip="title: Combine already made lists into this one; pos: right">Shopping Lists</a></li>
</ul> </ul>
@ -353,6 +353,39 @@
</div> </div>
</div> </div>
</div> </div>
<!-- Full Calculated SKU -->
<div id="plannerCard" hidden>
<div class="uk-card uk-card-default uk-card-small uk-card-body">
<h3>Site Planner Operator
<span class="">
<button onclick="changePlannerZoneState()" class="uk-button uk-button-small" title="Show/Hide the card body." uk-tooltip>Show/Hide</button>
</span>
<span class="uk-align-right">
<button onclick="removePlannerCard()" class="uk-button uk-button-small" title="Will remove the Recipes card and data from the list." uk-tooltip >Remove</button>
</span>
</h3>
<p class="uk-text-meta">Site Planner Operators allow you to select specific plans and a date range on that planner to insert any planned recipes into the list. This is best
utilized without the Recipe Operator, but it has been allowed to have both. Currently you can only have one date range for a plan. If you
do attempt to add from the same plan twice it will overwrite the last date range.
</p>
<div id="plannerZone" uk-grid>
<div class="uk-width-1-1">
<table class="uk-table uk-table-striped">
<thead>
<tr>
<th>Plan Name</th>
<th>Start Date</th>
<th>End Date</th>
<th>Operations</th>
</tr>
</thead>
<tbody id="plannerTableBody"></tbody>
</table>
<button onclick="openPlannerModal()" class="uk-button uk-button-secondary uk-width-1-1">Add Item</button>
</div>
</div>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
@ -655,6 +688,45 @@
</div> </div>
</div> </div>
</div> </div>
<!-- Site Planner Modal -->
<div id="plannerModal" class="uk-modal">
<div class="uk-modal-dialog uk-modal-body">
<h2 class="uk-modal-title">Add Planner Date Range...</h2>
<p class="uk-text-small">Site Planner Operators allow you to select specific plans and a date range on that planner to insert any planned recipes into the list. This is best
utilized without the Recipe Operator, but it has been allowed to have both.
</p>
<table class="uk-table uk-table-responsive uk-table-striped">
<thead>
<tr>
<th></th>
<th></th>
</tr>
</thead>
<tbody>
<tr>
<td>Plan</td>
<td>
<select id="planUUID" class="uk-select" aria-label="Select">
<option value="site">Site Planner</option>
</select>
</td>
</tr>
<tr>
<td>Start Date</td>
<td><input id="planStartDate" class="uk-input" type="date"></td>
</tr>
<tr>
<td>End Date</td>
<td><input id="planEndDate" class="uk-input" type="date"></td>
</tr>
</tbody>
</table>
<p class="uk-text-right">
<button class="uk-button uk-button-default uk-modal-close" type="button">Cancel</button>
<button id="plannerModalButton" onclick="addPlanner()" class="uk-button uk-button-primary" type="button">Add Planner</button>
</p>
</div>
</div>
</div> </div>
</div> </div>
</body> </body>

View File

@ -578,3 +578,27 @@
2025-08-19 15:45:32.184317 --- ERROR --- DatabaseError(message='syntax error at or near "FROM"LINE 11: FROM main_items items ^', 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'}, 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') 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')
2025-08-19 17:38:27.622014 --- ERROR --- DatabaseError(message='syntax error at or near ")"LINE 1: ...WHERE main_shopping_list_items.list_item_uuid IN () RETURNIN... ^',
payload=[],
sql='WITH deleted_rows AS (DELETE FROM main_shopping_list_items WHERE main_shopping_list_items.list_item_uuid IN () RETURNING *) SELECT * FROM deleted_rows;')
2025-08-19 17:39:39.750553 --- ERROR --- DatabaseError(message='syntax error at or near ")"LINE 1: ...WHERE main_shopping_list_items.list_item_uuid IN () RETURNIN... ^',
payload=[],
sql='WITH deleted_rows AS (DELETE FROM main_shopping_list_items WHERE main_shopping_list_items.list_item_uuid IN () RETURNING *) SELECT * FROM deleted_rows;')
2025-08-19 17:55:16.825854 --- ERROR --- DatabaseError(message='duplicate key value violates unique constraint "main_shopping_lists_name_key"DETAIL: Key (name)=() already exists.',
payload=('', '', 1, datetime.datetime(2025, 8, 19, 17, 55, 16, 816288), 'plain', 'temporary'),
sql='INSERT INTO main_shopping_lists(name, description, author, creation_date, sub_type, list_type) VALUES (%s, %s, %s, %s, %s, %s) RETURNING *;')
2025-08-20 05:17:43.764499 --- ERROR --- DatabaseError(message='syntax error at or near ")"LINE 1: ...WHERE main_shopping_list_items.list_item_uuid IN () RETURNIN... ^',
payload=[],
sql='WITH deleted_rows AS (DELETE FROM main_shopping_list_items WHERE main_shopping_list_items.list_item_uuid IN () RETURNING *) SELECT * FROM deleted_rows;')
2025-08-20 05:17:54.498269 --- ERROR --- DatabaseError(message='syntax error at or near ")"LINE 1: ...WHERE main_shopping_list_items.list_item_uuid IN () RETURNIN... ^',
payload=[],
sql='WITH deleted_rows AS (DELETE FROM main_shopping_list_items WHERE main_shopping_list_items.list_item_uuid IN () RETURNING *) SELECT * FROM deleted_rows;')
2025-08-20 05:17:57.313016 --- ERROR --- DatabaseError(message='syntax error at or near ")"LINE 1: ...WHERE main_shopping_list_items.list_item_uuid IN () RETURNIN... ^',
payload=[],
sql='WITH deleted_rows AS (DELETE FROM main_shopping_list_items WHERE main_shopping_list_items.list_item_uuid IN () RETURNING *) SELECT * FROM deleted_rows;')
2025-08-20 15:32:51.822870 --- ERROR --- DatabaseError(message='duplicate key value violates unique constraint "main_shopping_lists_name_key"DETAIL: Key (name)=(test) already exists.',
payload=('test', 'test', 1, datetime.datetime(2025, 8, 20, 15, 32, 51, 814595), 'plain', 'temporary'),
sql='INSERT INTO main_shopping_lists(name, description, author, creation_date, sub_type, list_type) VALUES (%s, %s, %s, %s, %s, %s) RETURNING *;')
2025-08-20 15:38:30.303845 --- ERROR --- DatabaseError(message='column "start_date" does not existLINE 1: ... FROM main_plan_events WHERE plan_uuid = NULL AND start_date... ^',
payload={'start_date': '2025-08-18', 'end_date': '2025-08-24', 'plan_uuid': None, 'plan_name': 'Site Planner'},
sql='SELECT * FROM main_plan_events WHERE plan_uuid = %(plan_uuid)s AND start_date <= %(end_date)s AND end_date >= %(start_date)s;')