Implemeted SKU creation in recipe module - edit

This commit is contained in:
Jadowyne Ulve 2025-08-12 15:42:47 -05:00
parent 5bd6d0b552
commit 78d79f9a57
19 changed files with 472 additions and 9 deletions

View File

@ -5,5 +5,6 @@ CREATE TABLE IF NOT EXISTS %%site_name%%_food_info (
ingrediants TEXT [],
nutrients JSONB,
expires BOOLEAN,
default_expiration FLOAT8
default_expiration FLOAT8,
UNIQUE(food_info_uuid)
);

View File

@ -9,6 +9,6 @@ CREATE TABLE IF NOt EXISTS %%site_name%%_item_info (
safety_stock FLOAT8,
lead_time_days FLOAT8,
ai_pick BOOLEAN,
prefixes INTEGER [],
UNIQUE(barcode)
prefixes INTEGER []
UNIQUE(item_info_uuid)
);

View File

@ -6,7 +6,7 @@ CREATE TABLE IF NOT EXISTS %%site_name%%_logistics_info(
primary_zone INTEGER NOT NULL,
auto_issue_location INTEGER NOT NULL,
auto_issue_zone INTEGER NOT NULL,
UNIQUE(barcode),
UNIQUE(logistics_info_uuid),
CONSTRAINT fk_primary_location
FOREIGN KEY(primary_location)
REFERENCES %%site_name%%_locations(id),

View File

@ -2,7 +2,7 @@ CREATE TABLE IF NOT EXISTS %%site_name%%_Transactions (
id SERIAL PRIMARY KEY,
timestamp TIMESTAMP,
logistics_info_id INTEGER NOT NULL,
barcode VARCHAR(255) NOT NULL,
barcode VARCHAR(255),
name VARCHAR(255),
transaction_type VARCHAR(255) NOT NULL,
quantity FLOAT8 NOT NULL,

View File

@ -45,10 +45,6 @@ class ItemInfoPayload:
lead_time_days: float = 0.0
ai_pick: bool = False
prefixes: list = field(default_factory=list)
def __post_init__(self):
if not isinstance(self.barcode, str):
raise TypeError(f"barcode must be of type str; not {type(self.barcode)}")
def payload(self):
return (

View File

@ -126,6 +126,59 @@ def getPicturePath(site:str, payload:tuple):
rows = cur.fetchone()[0]
return rows
def selectSiteTuple(payload, convert=True):
""" payload (tuple): (site_name,) """
site = ()
database_config = config.config()
select_site_sql = f"SELECT * FROM sites WHERE site_name = %s;"
try:
with psycopg2.connect(**database_config) as conn:
with conn.cursor() as cur:
cur.execute(select_site_sql, payload)
rows = cur.fetchone()
if rows and convert:
site = postsqldb.tupleDictionaryFactory(cur.description, rows)
elif rows and not convert:
site = rows
except Exception as error:
raise postsqldb.DatabaseError(error, payload, select_site_sql)
return site
def getZone(site:str, payload:tuple, convert:bool=True):
selected = ()
database_config = config.config()
sql = f"SELECT * FROM {site}_zones WHERE id=%s;"
try:
with psycopg2.connect(**database_config) as conn:
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
return selected
except Exception as error:
raise postsqldb.DatabaseError(error, payload, sql)
def getLocation(site:str, payload:tuple, convert:bool=True):
selected = ()
database_config = config.config()
sql = f"SELECT * FROM {site}_locations WHERE id=%s;"
try:
with psycopg2.connect(**database_config) as conn:
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
return selected
except Exception as error:
raise postsqldb.DatabaseError(error, payload, sql)
def selectItemLocationsTuple(site_name, payload, convert=True, conn=None):
item_locations = ()
self_conn = False
@ -312,6 +365,158 @@ def insertTransactionsTuple(site, payload, convert=True, conn=None):
raise postsqldb.DatabaseError(error, payload, sql)
return transaction
def insertLogisticsInfoTuple(site, payload, convert=True, conn=None):
""" payload (tuple): (barcode[str], primary_location[str], auto_issue_location[str], dynamic_locations[jsonb],
location_data[jsonb], quantity_on_hand[float]) """
logistics_info = ()
self_conn = False
with open(f"application/recipes/sql/insertLogisticsInfoTuple.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:
logistics_info = postsqldb.tupleDictionaryFactory(cur.description, rows)
elif rows and not convert:
logistics_info = rows
if self_conn:
conn.commit()
conn.close()
return logistics_info
except Exception as error:
raise postsqldb.DatabaseError(error, payload, sql)
def insertItemInfoTuple(site, payload, convert=True, conn=None):
""" payload (tuple): (barcode[str], linked_items[lst2pgarr], shopping_lists[lst2pgarr], recipes[lst2pgarr], groups[lst2pgarr],
packaging[str], uom[str], cost[float], safety_stock[float], lead_time_days[float], ai_pick[bool]) """
item_info = ()
self_conn = False
with open(f"application/recipes/sql/insertItemInfoTuple.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_info = postsqldb.tupleDictionaryFactory(cur.description, rows)
elif rows and not convert:
item_info = rows
if self_conn:
conn.commit()
conn.close()
return item_info
except Exception as error:
raise postsqldb.DatabaseError(error, payload, sql)
def insertFoodInfoTuple(site, payload, convert=True, conn=None):
""" payload (_type_): (ingrediants[lst2pgarr], food_groups[lst2pgarr], nutrients[jsonstr], expires[bool]) """
food_info = ()
self_conn = False
with open(f"application/recipes/sql/insertFoodInfoTuple.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:
food_info = postsqldb.tupleDictionaryFactory(cur.description, rows)
elif rows and not convert:
food_info = rows
if self_conn:
conn.commit()
conn.close()
return food_info
except Exception as error:
raise postsqldb.DatabaseError(error, payload, sql)
def insertItemTuple(site, payload, convert=True, conn=None):
""" payload (tuple): (barcode[str], item_name[str], brand[int], description[str],
tags[lst2pgarr], links[jsonb], item_info_id[int], logistics_info_id[int],
food_info_id[int], row_type[str], item_type[str], search_string[str]) """
item = ()
self_conn = False
with open(f"application/recipes/sql/insertItemTuple.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 = postsqldb.tupleDictionaryFactory(cur.description, rows)
elif rows and not convert:
item = rows
if self_conn:
conn.commit()
conn.close()
return item
except Exception as error:
raise postsqldb.DatabaseError(error, payload, sql)
def insertItemLocationsTuple(site, payload, convert=True, conn=None):
""" payload (tuple): (part_id[int], location_id[int], quantity_on_hand[float], cost_layers[lst2pgarr]) """
location = ()
self_conn = False
database_config = config.config()
with open(f"application/recipes/sql/insertItemLocationsTuple.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:
location = postsqldb.tupleDictionaryFactory(cur.description, rows)
elif rows and not convert:
location = rows
if self_conn:
conn.commit()
conn.close()
return location
except Exception as error:
raise postsqldb.DatabaseError(error, payload, sql)
def postAddRecipe(site:str, payload:tuple, convert:bool=True):
database_config = config.config()
record = ()

View File

@ -1,8 +1,10 @@
import psycopg2
import datetime
import json
from application import database_payloads, postsqldb
from application.recipes import database_recipes
from application.items import database_items
import config
def postTransaction(site_name, user_id, data: dict, conn=None):
@ -137,3 +139,95 @@ def process_recipe_receipt(site_name, user_id, data:dict, conn=None):
conn.close()
return True, ""
def postNewSkuFromRecipe(site_name: str, user_id: int, data: dict, conn=None):
""" data = {'name', 'subtype', 'qty', 'uom_id', 'main_link', 'cost'}"""
self_conn = False
if not conn:
database_config = config.config()
conn = psycopg2.connect(**database_config)
conn.autocommit = False
self_conn = True
site = database_recipes.selectSiteTuple((site_name,))
default_zone = database_recipes.getZone(site_name,(site['default_zone'], ))
default_location = database_recipes.getLocation(site_name, (site['default_primary_location'],))
uuid = f"{default_zone['name']}@{default_location['name']}"
# create logistics info
logistics_info = database_payloads.LogisticsInfoPayload(
barcode=None,
primary_location=site['default_primary_location'],
primary_zone=site['default_zone'],
auto_issue_location=site['default_auto_issue_location'],
auto_issue_zone=site['default_zone']
)
# create item info
item_info = database_payloads.ItemInfoPayload(barcode=None)
# create Food Info
food_info = database_payloads.FoodInfoPayload()
logistics_info_id = 0
item_info_id = 0
food_info_id = 0
brand_id = 1
logistics_info = database_recipes.insertLogisticsInfoTuple(site_name, logistics_info.payload(), conn=conn)
item_info = database_recipes.insertItemInfoTuple(site_name, item_info.payload(), conn=conn)
food_info = database_recipes.insertFoodInfoTuple(site_name, food_info.payload(), conn=conn)
name = data['name']
name = name.replace("'", "@&apostraphe&")
links = {'main': data['main_link']}
search_string = f"&&{name}&&"
item = database_payloads.ItemsPayload(
barcode=None,
item_name=data['name'],
item_info_id=item_info['id'],
logistics_info_id=logistics_info['id'],
food_info_id=food_info['id'],
links=links,
brand=brand_id,
row_type="single",
item_type=data['subtype'],
search_string=search_string
)
item = database_recipes.insertItemTuple(site_name, item.payload(), conn=conn)
with conn.cursor() as cur:
cur.execute(f"SELECT id FROM {site_name}_locations WHERE uuid=%s;", (uuid, ))
location_id = cur.fetchone()[0]
database_payloads.ItemLocationPayload
item_location = database_payloads.ItemLocationPayload(item['id'], location_id)
database_recipes.insertItemLocationsTuple(site_name, item_location.payload(), conn=conn)
creation_tuple = database_payloads.TransactionPayload(
datetime.datetime.now(),
logistics_info['id'],
None,
item['item_name'],
"SYSTEM",
0.0,
"Item added to the System!",
user_id,
{'location': uuid}
)
database_recipes.insertTransactionsTuple(site_name, creation_tuple.payload(), conn=conn)
item_uuid = item['item_uuid']
if self_conn:
conn.commit()
conn.close()
return False, item_uuid
return conn, item_uuid

View File

@ -157,6 +157,34 @@ def postSKUItem():
return jsonify({'recipe': recipe, 'error': False, 'message': 'Recipe Item was added successful!'})
return jsonify({'recipe': recipe, 'error': True, 'message': f'method {request.method} is not allowed!'})
@recipes_api.route('/api/postNewSKUItem', methods=["POST"])
@access_api.login_required
def postNewSKUItem():
recipe = {}
if request.method == "POST":
recipe_id = int(request.get_json()['recipe_id'])
site_name = session['selected_site']
user_id = session['user_id']
_, item_uuid= recipe_processes.postNewSkuFromRecipe(site_name, user_id, request.get_json())
item = database_recipes.selectItemTupleByUUID(site_name, (item_uuid,))
recipe_item = db.RecipesTable.ItemPayload(
item_uuid=item_uuid,
rp_id=recipe_id,
item_type='sku',
item_name=request.get_json()['name'],
uom=request.get_json()['uom_id'],
qty=float(request.get_json()['qty']),
item_id=item['item_id'],
links={'main': request.get_json()['main_link']}
)
database_recipes.postAddRecipeItem(site_name, recipe_item.payload())
recipe = database_recipes.getRecipe(site_name, (recipe_id, ))
return jsonify({'recipe': recipe, 'error': False, 'message': 'Recipe Item was added successful!'})
return jsonify({'recipe': recipe, 'error': True, 'message': f'method {request.method} is not allowed!'})
@recipes_api.route('/postImage/<recipe_id>', methods=["POST"])
@access_api.login_required
def uploadImage(recipe_id):

View File

@ -0,0 +1,4 @@
INSERT INTO %%site_name%%_food_info
(ingrediants, food_groups, nutrients, expires, default_expiration)
VALUES (%s, %s, %s, %s, %s)
RETURNING *;

View File

@ -0,0 +1,4 @@
INSERT INTO %%site_name%%_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 *;

View File

@ -0,0 +1,4 @@
INSERT INTO %%site_name%%_item_locations
(part_id, location_id, quantity_on_hand, cost_layers)
VALUES (%s, %s, %s, %s)
RETURNING *;

View File

@ -0,0 +1,5 @@
INSERT INTO %%site_name%%_items
(barcode, item_name, brand, description, tags, links, item_info_id, logistics_info_id,
food_info_id, row_type, item_type, search_string)
VALUES(%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
RETURNING *;

View File

@ -0,0 +1,4 @@
INSERT INTO %%site_name%%_logistics_info
(barcode, primary_location, primary_zone, auto_issue_location, auto_issue_zone)
VALUES (%s, %s, %s, %s, %s)
RETURNING *;

View File

@ -254,6 +254,47 @@ async function addSKUItem(item_id) {
UIkit.modal(document.getElementById('itemsModal')).hide();
}
async function addNewSKUItem() {
let newSKUName = document.getElementById('newSKUName').value
let newSKUSubtype = document.getElementById('newSKUSubtype').value
let newSKUQty = parseFloat(document.getElementById('newSKUQty').value)
let newSKUUOM = parseInt(document.getElementById('newSKUUOM').value)
let newWeblink = document.getElementById('newWeblink').value
let newSKUCost = parseFloat(document.getElementById('newSKUCost').value)
const response = await fetch(`/recipes/api/postNewSKUItem`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
recipe_id: recipe.id,
name: newSKUName,
subtype: newSKUSubtype,
qty: newSKUQty,
uom_id: newSKUUOM,
main_link: newWeblink,
cost: newSKUCost
}),
});
data = await response.json()
message_type = "primary"
if(data.error){
message_type = "danger"
}
UIkit.notification({
message: data.message,
status: message_type,
pos: 'top-right',
timeout: 5000
});
recipe = data.recipe
await replenishRecipe()
UIkit.modal(document.getElementById('addNewSKUItem')).hide();
}
let updated = {}
async function postUpdate() {
let description = document.getElementById('recipeDescription').value
@ -326,6 +367,18 @@ async function deleteInstruction(index){
await replenishInstructions()
}
async function openNewSKUModal() {
let itemsModal = document.getElementById('addNewSKUItem')
document.getElementById('newSKUName').value = ""
document.getElementById('newSKUSubtype').value = "FOOD"
document.getElementById('newSKUQty').value = 1
document.getElementById('newSKUUOM').value = "1"
document.getElementById('newWeblink').value = ""
document.getElementById('newSKUCost').value = 0.00
UIkit.modal(itemsModal).show();
}
let pagination_current = 1;
let pagination_end = 10;
let search_string = "";

View File

@ -174,6 +174,8 @@
<div class="uk-button-group class="uk-width-expand" ">
<button uk-toggle="target: #addCustomItem" type="button" id="addCustom" class="uk-button uk-button-default">Add Custom</button>
<button id="addSku" onclick="openSKUModal()" class="uk-button uk-button-default">Add SKU</button>
<button id="addSku" onclick="openNewSKUModal()" class="uk-button uk-button-default">Create SKU</button>
</div>
</div>
<div class="uk-width-1-1 uk-margin-small-top">
@ -318,6 +320,69 @@
</div>
</div>
</div>
<!-- add New SKU Line Modal-->
<div id="addNewSKUItem" uk-modal>
<div class="uk-modal-dialog">
<button class="uk-modal-close-default" type="button" uk-close></button>
<div class="uk-modal-header">
<h2 id="lineHeader" class="uk-modal-title">Add New Item...</h2>
</div>
<div class="uk-modal-body">
<div class="uk-margin-small">
<p class="uk-text-meta">Adding a new sku with both create a new item in the system with the information you provide and also add it to the recipe.
</p>
</div>
<div class="uk-margin-small">
<label class="uk-form-label" for="newSKUType">Line Type</label>
<div class="uk-form-controls">
<input class="uk-input uk-disabled" id="newSKUType" type="text" placeholder="" value="new sku">
</div>
</div>
<div class="uk-margin-small">
<label class="uk-form-label" for="newSKUName">Item Name</label>
<div class="uk-form-controls">
<input class="uk-input" id="newSKUName" type="text" placeholder="">
</div>
</div>
<div class="uk-margin-small">
<label class="uk-form-label" for="newSKUSubtype">Item Type</label>
<select id="newSKUSubtype" class="uk-select" aria-label="Select">
<option value="FOOD">Food</option>
<option value="FOOD_PLU">Food (PLU)</option>
</select>
</div>
<div class="uk-margin-small">
<label class="uk-form-label" for="newSKUQty">Quantity</label>
<div class="uk-form-controls">
<input class="uk-input" id="newSKUQty" type="number">
</div>
</div>
<div class="uk-margin-small">
<label class="uk-form-label" for="newSKUUOM">Unit of Measure</label>
<select id="newSKUUOM" class="uk-select" aria-label="Select">
{% for unit in units %}
<option value="{{unit['id']}}">{{unit['fullname']}}</option>
{% endfor %}
</select>
</div>
<div class="uk-margin-small">
<label class="uk-form-label" for="newSKUCost">Cost</label>
<div class="uk-form-controls">
<input class="uk-input" id="newSKUCost" type="number">
</div>
</div>
<div class="uk-margin-small">
<label class="uk-form-label" for="newWeblink">Weblink</label>
<div class="uk-form-controls">
<input class="uk-input" id="newWeblink" type="text" placeholder="">
</div>
</div>
</div>
<div class="uk-modal-footer uk-text-right">
<button onclick="addNewSKUItem()" class="uk-button uk-button-primary" type="button">Add</button>
</div>
</div>
</div>
<!-- Items modal lookup -->
<div id="itemsModal" uk-modal>
<div id="itemsModalInner" class="uk-modal-dialog uk-modal-body " uk-overflow-auto>