feat: Add Set Orders on Boxes feature with debouncing and page refresh
- Created warehouse_orders.py module with 8 order management functions - Added /warehouse/set-orders-on-boxes route and 7 API endpoints - Implemented 4-tab interface: assign, find, move, and view orders - Changed assign input from dropdown to text field with BOX validation - Fixed location join issue in warehouse.py (use boxes_crates.location_id) - Added debouncing flag to prevent multiple rapid form submissions - Added page refresh after successful order assignment - Disabled assign button during processing - Added page refresh with 2 second delay for UX feedback - Added CP code validation in inventory page - Improved modal styling with theme support - Fixed set_boxes_locations page to refresh box info after assignments
This commit is contained in:
@@ -9,6 +9,11 @@ from app.modules.warehouse.warehouse import (
|
|||||||
assign_box_to_location, move_box_to_new_location,
|
assign_box_to_location, move_box_to_new_location,
|
||||||
get_cp_inventory_list, search_cp_code, search_by_box_number, get_cp_details
|
get_cp_inventory_list, search_cp_code, search_by_box_number, get_cp_details
|
||||||
)
|
)
|
||||||
|
from app.modules.warehouse.warehouse_orders import (
|
||||||
|
get_unassigned_orders, get_orders_by_box, search_orders_by_cp_code,
|
||||||
|
assign_order_to_box, move_order_to_box, unassign_order_from_box,
|
||||||
|
get_all_boxes_summary
|
||||||
|
)
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -119,6 +124,15 @@ def reports():
|
|||||||
return render_template('modules/warehouse/reports.html')
|
return render_template('modules/warehouse/reports.html')
|
||||||
|
|
||||||
|
|
||||||
|
@warehouse_bp.route('/set-orders-on-boxes', methods=['GET', 'POST'])
|
||||||
|
def set_orders_on_boxes():
|
||||||
|
"""Set orders on boxes - assign or move orders between boxes"""
|
||||||
|
if 'user_id' not in session:
|
||||||
|
return redirect(url_for('main.login'))
|
||||||
|
|
||||||
|
return render_template('modules/warehouse/set_orders_on_boxes.html')
|
||||||
|
|
||||||
|
|
||||||
@warehouse_bp.route('/test-barcode', methods=['GET'])
|
@warehouse_bp.route('/test-barcode', methods=['GET'])
|
||||||
def test_barcode():
|
def test_barcode():
|
||||||
"""Test barcode printing functionality"""
|
"""Test barcode printing functionality"""
|
||||||
@@ -218,6 +232,119 @@ def api_get_locations():
|
|||||||
return jsonify({'success': True, 'locations': locations}), 200
|
return jsonify({'success': True, 'locations': locations}), 200
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# API Routes for Orders Management
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
@warehouse_bp.route('/api/unassigned-orders', methods=['GET'], endpoint='api_unassigned_orders')
|
||||||
|
def api_unassigned_orders():
|
||||||
|
"""Get all unassigned orders"""
|
||||||
|
if 'user_id' not in session:
|
||||||
|
return jsonify({'success': False, 'error': 'Unauthorized'}), 401
|
||||||
|
|
||||||
|
try:
|
||||||
|
limit = request.args.get('limit', 100, type=int)
|
||||||
|
offset = request.args.get('offset', 0, type=int)
|
||||||
|
|
||||||
|
orders = get_unassigned_orders(limit, offset)
|
||||||
|
return jsonify({'success': True, 'orders': orders, 'count': len(orders)}), 200
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error getting unassigned orders: {e}")
|
||||||
|
return jsonify({'success': False, 'error': str(e)}), 500
|
||||||
|
|
||||||
|
|
||||||
|
@warehouse_bp.route('/api/orders-by-box', methods=['POST'], endpoint='api_orders_by_box')
|
||||||
|
def api_orders_by_box():
|
||||||
|
"""Get all orders assigned to a specific box"""
|
||||||
|
if 'user_id' not in session:
|
||||||
|
return jsonify({'success': False, 'error': 'Unauthorized'}), 401
|
||||||
|
|
||||||
|
data = request.get_json()
|
||||||
|
box_id = data.get('box_id')
|
||||||
|
|
||||||
|
if not box_id:
|
||||||
|
return jsonify({'success': False, 'error': 'Box ID is required'}), 400
|
||||||
|
|
||||||
|
success, data_resp, status_code = get_orders_by_box(box_id)
|
||||||
|
return jsonify({'success': success, **data_resp}), status_code
|
||||||
|
|
||||||
|
|
||||||
|
@warehouse_bp.route('/api/search-orders', methods=['POST'], endpoint='api_search_orders')
|
||||||
|
def api_search_orders():
|
||||||
|
"""Search for orders by CP code"""
|
||||||
|
if 'user_id' not in session:
|
||||||
|
return jsonify({'success': False, 'error': 'Unauthorized'}), 401
|
||||||
|
|
||||||
|
data = request.get_json()
|
||||||
|
cp_code = data.get('cp_code', '').strip()
|
||||||
|
|
||||||
|
if not cp_code:
|
||||||
|
return jsonify({'success': False, 'error': 'CP code is required'}), 400
|
||||||
|
|
||||||
|
orders = search_orders_by_cp_code(cp_code)
|
||||||
|
return jsonify({'success': True, 'orders': orders, 'count': len(orders)}), 200
|
||||||
|
|
||||||
|
|
||||||
|
@warehouse_bp.route('/api/assign-order-to-box', methods=['POST'], endpoint='api_assign_order_to_box')
|
||||||
|
def api_assign_order_to_box():
|
||||||
|
"""Assign an order to a box"""
|
||||||
|
if 'user_id' not in session:
|
||||||
|
return jsonify({'success': False, 'error': 'Unauthorized'}), 401
|
||||||
|
|
||||||
|
data = request.get_json()
|
||||||
|
order_id = data.get('order_id')
|
||||||
|
box_id = data.get('box_id')
|
||||||
|
|
||||||
|
if not order_id or not box_id:
|
||||||
|
return jsonify({'success': False, 'error': 'Order ID and box ID are required'}), 400
|
||||||
|
|
||||||
|
success, message, status_code = assign_order_to_box(order_id, box_id)
|
||||||
|
return jsonify({'success': success, 'message': message}), status_code
|
||||||
|
|
||||||
|
|
||||||
|
@warehouse_bp.route('/api/move-order-to-box', methods=['POST'], endpoint='api_move_order_to_box')
|
||||||
|
def api_move_order_to_box():
|
||||||
|
"""Move an order to a different box"""
|
||||||
|
if 'user_id' not in session:
|
||||||
|
return jsonify({'success': False, 'error': 'Unauthorized'}), 401
|
||||||
|
|
||||||
|
data = request.get_json()
|
||||||
|
order_id = data.get('order_id')
|
||||||
|
new_box_id = data.get('new_box_id')
|
||||||
|
|
||||||
|
if not order_id or not new_box_id:
|
||||||
|
return jsonify({'success': False, 'error': 'Order ID and destination box ID are required'}), 400
|
||||||
|
|
||||||
|
success, message, status_code = move_order_to_box(order_id, new_box_id)
|
||||||
|
return jsonify({'success': success, 'message': message}), status_code
|
||||||
|
|
||||||
|
|
||||||
|
@warehouse_bp.route('/api/unassign-order', methods=['POST'], endpoint='api_unassign_order')
|
||||||
|
def api_unassign_order():
|
||||||
|
"""Remove an order from its box"""
|
||||||
|
if 'user_id' not in session:
|
||||||
|
return jsonify({'success': False, 'error': 'Unauthorized'}), 401
|
||||||
|
|
||||||
|
data = request.get_json()
|
||||||
|
order_id = data.get('order_id')
|
||||||
|
|
||||||
|
if not order_id:
|
||||||
|
return jsonify({'success': False, 'error': 'Order ID is required'}), 400
|
||||||
|
|
||||||
|
success, message, status_code = unassign_order_from_box(order_id)
|
||||||
|
return jsonify({'success': success, 'message': message}), status_code
|
||||||
|
|
||||||
|
|
||||||
|
@warehouse_bp.route('/api/boxes-summary', methods=['GET'], endpoint='api_boxes_summary')
|
||||||
|
def api_boxes_summary():
|
||||||
|
"""Get summary of all boxes with order counts"""
|
||||||
|
if 'user_id' not in session:
|
||||||
|
return jsonify({'success': False, 'error': 'Unauthorized'}), 401
|
||||||
|
|
||||||
|
boxes = get_all_boxes_summary()
|
||||||
|
return jsonify({'success': True, 'boxes': boxes}), 200
|
||||||
|
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# API Routes for CP Inventory View
|
# API Routes for CP Inventory View
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|||||||
@@ -452,6 +452,7 @@ def get_cp_inventory_list(limit=100, offset=0):
|
|||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
|
|
||||||
# Get all CP codes with their box and location info (latest entries first)
|
# Get all CP codes with their box and location info (latest entries first)
|
||||||
|
# NOTE: Use box's CURRENT location (from boxes_crates), not the historical scan location
|
||||||
cursor.execute("""
|
cursor.execute("""
|
||||||
SELECT
|
SELECT
|
||||||
s.id,
|
s.id,
|
||||||
@@ -468,7 +469,7 @@ def get_cp_inventory_list(limit=100, offset=0):
|
|||||||
SUM(s.rejected_quantity) as total_rejected
|
SUM(s.rejected_quantity) as total_rejected
|
||||||
FROM scanfg_orders s
|
FROM scanfg_orders s
|
||||||
LEFT JOIN boxes_crates bc ON s.box_id = bc.id
|
LEFT JOIN boxes_crates bc ON s.box_id = bc.id
|
||||||
LEFT JOIN warehouse_locations wl ON s.location_id = wl.id
|
LEFT JOIN warehouse_locations wl ON bc.location_id = wl.id
|
||||||
GROUP BY SUBSTRING(s.CP_full_code, 1, 10), s.box_id
|
GROUP BY SUBSTRING(s.CP_full_code, 1, 10), s.box_id
|
||||||
ORDER BY MAX(s.created_at) DESC
|
ORDER BY MAX(s.created_at) DESC
|
||||||
LIMIT %s OFFSET %s
|
LIMIT %s OFFSET %s
|
||||||
@@ -476,7 +477,15 @@ def get_cp_inventory_list(limit=100, offset=0):
|
|||||||
|
|
||||||
# Convert tuples to dicts using cursor description
|
# Convert tuples to dicts using cursor description
|
||||||
columns = [col[0] for col in cursor.description]
|
columns = [col[0] for col in cursor.description]
|
||||||
results = [{columns[i]: row[i] for i in range(len(columns))} for row in cursor.fetchall()]
|
results = []
|
||||||
|
for row in cursor.fetchall():
|
||||||
|
row_dict = {columns[i]: row[i] for i in range(len(columns))}
|
||||||
|
# Convert time and date fields to strings to avoid JSON serialization issues
|
||||||
|
if row_dict.get('latest_date'):
|
||||||
|
row_dict['latest_date'] = str(row_dict['latest_date'])
|
||||||
|
if row_dict.get('latest_time'):
|
||||||
|
row_dict['latest_time'] = str(row_dict['latest_time'])
|
||||||
|
results.append(row_dict)
|
||||||
cursor.close()
|
cursor.close()
|
||||||
|
|
||||||
return results if results else []
|
return results if results else []
|
||||||
@@ -503,6 +512,7 @@ def search_cp_code(cp_code_search):
|
|||||||
search_term = cp_code_search.replace('-', '').strip().upper()
|
search_term = cp_code_search.replace('-', '').strip().upper()
|
||||||
|
|
||||||
# Search for matching CP codes
|
# Search for matching CP codes
|
||||||
|
# NOTE: Use box's CURRENT location (from boxes_crates), not the historical scan location
|
||||||
cursor.execute("""
|
cursor.execute("""
|
||||||
SELECT
|
SELECT
|
||||||
s.id,
|
s.id,
|
||||||
@@ -519,7 +529,7 @@ def search_cp_code(cp_code_search):
|
|||||||
SUM(s.rejected_quantity) as total_rejected
|
SUM(s.rejected_quantity) as total_rejected
|
||||||
FROM scanfg_orders s
|
FROM scanfg_orders s
|
||||||
LEFT JOIN boxes_crates bc ON s.box_id = bc.id
|
LEFT JOIN boxes_crates bc ON s.box_id = bc.id
|
||||||
LEFT JOIN warehouse_locations wl ON s.location_id = wl.id
|
LEFT JOIN warehouse_locations wl ON bc.location_id = wl.id
|
||||||
WHERE REPLACE(s.CP_full_code, '-', '') LIKE %s
|
WHERE REPLACE(s.CP_full_code, '-', '') LIKE %s
|
||||||
GROUP BY SUBSTRING(s.CP_full_code, 1, 10), s.box_id
|
GROUP BY SUBSTRING(s.CP_full_code, 1, 10), s.box_id
|
||||||
ORDER BY MAX(s.created_at) DESC
|
ORDER BY MAX(s.created_at) DESC
|
||||||
@@ -527,7 +537,15 @@ def search_cp_code(cp_code_search):
|
|||||||
|
|
||||||
# Convert tuples to dicts using cursor description
|
# Convert tuples to dicts using cursor description
|
||||||
columns = [col[0] for col in cursor.description]
|
columns = [col[0] for col in cursor.description]
|
||||||
results = [{columns[i]: row[i] for i in range(len(columns))} for row in cursor.fetchall()]
|
results = []
|
||||||
|
for row in cursor.fetchall():
|
||||||
|
row_dict = {columns[i]: row[i] for i in range(len(columns))}
|
||||||
|
# Convert time and date fields to strings to avoid JSON serialization issues
|
||||||
|
if row_dict.get('latest_date'):
|
||||||
|
row_dict['latest_date'] = str(row_dict['latest_date'])
|
||||||
|
if row_dict.get('latest_time'):
|
||||||
|
row_dict['latest_time'] = str(row_dict['latest_time'])
|
||||||
|
results.append(row_dict)
|
||||||
cursor.close()
|
cursor.close()
|
||||||
|
|
||||||
return results if results else []
|
return results if results else []
|
||||||
@@ -569,7 +587,7 @@ def search_by_box_number(box_number_search):
|
|||||||
s.created_at
|
s.created_at
|
||||||
FROM scanfg_orders s
|
FROM scanfg_orders s
|
||||||
LEFT JOIN boxes_crates bc ON s.box_id = bc.id
|
LEFT JOIN boxes_crates bc ON s.box_id = bc.id
|
||||||
LEFT JOIN warehouse_locations wl ON s.location_id = wl.id
|
LEFT JOIN warehouse_locations wl ON bc.location_id = wl.id
|
||||||
WHERE bc.box_number LIKE %s
|
WHERE bc.box_number LIKE %s
|
||||||
ORDER BY s.created_at DESC
|
ORDER BY s.created_at DESC
|
||||||
LIMIT 500
|
LIMIT 500
|
||||||
@@ -577,7 +595,17 @@ def search_by_box_number(box_number_search):
|
|||||||
|
|
||||||
# Convert tuples to dicts using cursor description
|
# Convert tuples to dicts using cursor description
|
||||||
columns = [col[0] for col in cursor.description]
|
columns = [col[0] for col in cursor.description]
|
||||||
results = [{columns[i]: row[i] for i in range(len(columns))} for row in cursor.fetchall()]
|
results = []
|
||||||
|
for row in cursor.fetchall():
|
||||||
|
row_dict = {columns[i]: row[i] for i in range(len(columns))}
|
||||||
|
# Convert time and date fields to strings to avoid JSON serialization issues
|
||||||
|
if row_dict.get('date'):
|
||||||
|
row_dict['date'] = str(row_dict['date'])
|
||||||
|
if row_dict.get('time'):
|
||||||
|
row_dict['time'] = str(row_dict['time'])
|
||||||
|
if row_dict.get('created_at'):
|
||||||
|
row_dict['created_at'] = str(row_dict['created_at'])
|
||||||
|
results.append(row_dict)
|
||||||
cursor.close()
|
cursor.close()
|
||||||
|
|
||||||
return results if results else []
|
return results if results else []
|
||||||
@@ -602,6 +630,7 @@ def get_cp_details(cp_code):
|
|||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
|
|
||||||
# Search for all entries with this CP base
|
# Search for all entries with this CP base
|
||||||
|
# NOTE: Use box's CURRENT location (from boxes_crates), not the historical scan location
|
||||||
cursor.execute("""
|
cursor.execute("""
|
||||||
SELECT
|
SELECT
|
||||||
s.id,
|
s.id,
|
||||||
@@ -622,7 +651,7 @@ def get_cp_details(cp_code):
|
|||||||
s.created_at
|
s.created_at
|
||||||
FROM scanfg_orders s
|
FROM scanfg_orders s
|
||||||
LEFT JOIN boxes_crates bc ON s.box_id = bc.id
|
LEFT JOIN boxes_crates bc ON s.box_id = bc.id
|
||||||
LEFT JOIN warehouse_locations wl ON s.location_id = wl.id
|
LEFT JOIN warehouse_locations wl ON bc.location_id = wl.id
|
||||||
WHERE SUBSTRING(s.CP_full_code, 1, 10) = %s
|
WHERE SUBSTRING(s.CP_full_code, 1, 10) = %s
|
||||||
ORDER BY s.created_at DESC
|
ORDER BY s.created_at DESC
|
||||||
LIMIT 1000
|
LIMIT 1000
|
||||||
@@ -630,7 +659,17 @@ def get_cp_details(cp_code):
|
|||||||
|
|
||||||
# Convert tuples to dicts using cursor description
|
# Convert tuples to dicts using cursor description
|
||||||
columns = [col[0] for col in cursor.description]
|
columns = [col[0] for col in cursor.description]
|
||||||
results = [{columns[i]: row[i] for i in range(len(columns))} for row in cursor.fetchall()]
|
results = []
|
||||||
|
for row in cursor.fetchall():
|
||||||
|
row_dict = {columns[i]: row[i] for i in range(len(columns))}
|
||||||
|
# Convert time and date fields to strings to avoid JSON serialization issues
|
||||||
|
if row_dict.get('date'):
|
||||||
|
row_dict['date'] = str(row_dict['date'])
|
||||||
|
if row_dict.get('time'):
|
||||||
|
row_dict['time'] = str(row_dict['time'])
|
||||||
|
if row_dict.get('created_at'):
|
||||||
|
row_dict['created_at'] = str(row_dict['created_at'])
|
||||||
|
results.append(row_dict)
|
||||||
cursor.close()
|
cursor.close()
|
||||||
|
|
||||||
return results if results else []
|
return results if results else []
|
||||||
|
|||||||
392
app/modules/warehouse/warehouse_orders.py
Normal file
392
app/modules/warehouse/warehouse_orders.py
Normal file
@@ -0,0 +1,392 @@
|
|||||||
|
"""
|
||||||
|
Warehouse Orders Management - Helper Functions
|
||||||
|
Provides functions for managing orders on boxes
|
||||||
|
"""
|
||||||
|
import logging
|
||||||
|
import pymysql
|
||||||
|
from app.database import get_db
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def get_unassigned_orders(limit=100, offset=0):
|
||||||
|
"""Get all orders not yet assigned to any box
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of unassigned orders
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
conn = get_db()
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT
|
||||||
|
id,
|
||||||
|
CP_full_code,
|
||||||
|
operator_code,
|
||||||
|
quality_code,
|
||||||
|
date,
|
||||||
|
time,
|
||||||
|
approved_quantity,
|
||||||
|
rejected_quantity,
|
||||||
|
created_at
|
||||||
|
FROM scanfg_orders
|
||||||
|
WHERE box_id IS NULL
|
||||||
|
ORDER BY created_at DESC
|
||||||
|
LIMIT %s OFFSET %s
|
||||||
|
""", (limit, offset))
|
||||||
|
|
||||||
|
columns = [col[0] for col in cursor.description]
|
||||||
|
results = []
|
||||||
|
for row in cursor.fetchall():
|
||||||
|
row_dict = {columns[i]: row[i] for i in range(len(columns))}
|
||||||
|
if row_dict.get('date'):
|
||||||
|
row_dict['date'] = str(row_dict['date'])
|
||||||
|
if row_dict.get('time'):
|
||||||
|
row_dict['time'] = str(row_dict['time'])
|
||||||
|
if row_dict.get('created_at'):
|
||||||
|
row_dict['created_at'] = str(row_dict['created_at'])
|
||||||
|
results.append(row_dict)
|
||||||
|
cursor.close()
|
||||||
|
|
||||||
|
return results if results else []
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error getting unassigned orders: {e}")
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
def get_orders_by_box(box_id):
|
||||||
|
"""Get all orders assigned to a specific box
|
||||||
|
|
||||||
|
Args:
|
||||||
|
box_id: ID of the box
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple: (success: bool, data: dict, status_code: int)
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
conn = get_db()
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
# Get box info
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT id, box_number, status, location_id
|
||||||
|
FROM boxes_crates
|
||||||
|
WHERE id = %s
|
||||||
|
""", (box_id,))
|
||||||
|
|
||||||
|
box = cursor.fetchone()
|
||||||
|
|
||||||
|
if not box:
|
||||||
|
cursor.close()
|
||||||
|
return False, {'error': 'Box not found'}, 404
|
||||||
|
|
||||||
|
# Get all orders in this box
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT
|
||||||
|
id,
|
||||||
|
CP_full_code,
|
||||||
|
operator_code,
|
||||||
|
quality_code,
|
||||||
|
date,
|
||||||
|
time,
|
||||||
|
approved_quantity,
|
||||||
|
rejected_quantity,
|
||||||
|
created_at
|
||||||
|
FROM scanfg_orders
|
||||||
|
WHERE box_id = %s
|
||||||
|
ORDER BY created_at DESC
|
||||||
|
""", (box_id,))
|
||||||
|
|
||||||
|
orders = cursor.fetchall()
|
||||||
|
columns = [col[0] for col in cursor.description]
|
||||||
|
|
||||||
|
orders_list = []
|
||||||
|
for order in orders:
|
||||||
|
order_dict = {columns[i]: order[i] for i in range(len(columns))}
|
||||||
|
if order_dict.get('date'):
|
||||||
|
order_dict['date'] = str(order_dict['date'])
|
||||||
|
if order_dict.get('time'):
|
||||||
|
order_dict['time'] = str(order_dict['time'])
|
||||||
|
if order_dict.get('created_at'):
|
||||||
|
order_dict['created_at'] = str(order_dict['created_at'])
|
||||||
|
orders_list.append(order_dict)
|
||||||
|
|
||||||
|
# Get location info if box has a location
|
||||||
|
location_info = None
|
||||||
|
if box[3]: # location_id
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT id, location_code, size, description
|
||||||
|
FROM warehouse_locations
|
||||||
|
WHERE id = %s
|
||||||
|
""", (box[3],))
|
||||||
|
loc = cursor.fetchone()
|
||||||
|
if loc:
|
||||||
|
location_info = {
|
||||||
|
'id': loc[0],
|
||||||
|
'location_code': loc[1],
|
||||||
|
'size': loc[2],
|
||||||
|
'description': loc[3]
|
||||||
|
}
|
||||||
|
|
||||||
|
box_data = {
|
||||||
|
'id': box[0],
|
||||||
|
'box_number': box[1],
|
||||||
|
'status': box[2],
|
||||||
|
'location_id': box[3],
|
||||||
|
'location': location_info,
|
||||||
|
'orders_count': len(orders_list)
|
||||||
|
}
|
||||||
|
|
||||||
|
cursor.close()
|
||||||
|
|
||||||
|
return True, {'box': box_data, 'orders': orders_list}, 200
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error getting orders by box: {e}")
|
||||||
|
return False, {'error': str(e)}, 500
|
||||||
|
|
||||||
|
|
||||||
|
def search_orders_by_cp_code(cp_code):
|
||||||
|
"""Search for orders by CP code
|
||||||
|
|
||||||
|
Args:
|
||||||
|
cp_code: CP code to search for (can be partial)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of matching orders
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
conn = get_db()
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
search_term = cp_code.replace('-', '').strip().upper()
|
||||||
|
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT
|
||||||
|
id,
|
||||||
|
CP_full_code,
|
||||||
|
operator_code,
|
||||||
|
quality_code,
|
||||||
|
date,
|
||||||
|
time,
|
||||||
|
approved_quantity,
|
||||||
|
rejected_quantity,
|
||||||
|
box_id,
|
||||||
|
created_at
|
||||||
|
FROM scanfg_orders
|
||||||
|
WHERE REPLACE(CP_full_code, '-', '') LIKE %s
|
||||||
|
ORDER BY created_at DESC
|
||||||
|
LIMIT 100
|
||||||
|
""", (f"{search_term}%",))
|
||||||
|
|
||||||
|
columns = [col[0] for col in cursor.description]
|
||||||
|
results = []
|
||||||
|
for row in cursor.fetchall():
|
||||||
|
row_dict = {columns[i]: row[i] for i in range(len(columns))}
|
||||||
|
if row_dict.get('date'):
|
||||||
|
row_dict['date'] = str(row_dict['date'])
|
||||||
|
if row_dict.get('time'):
|
||||||
|
row_dict['time'] = str(row_dict['time'])
|
||||||
|
if row_dict.get('created_at'):
|
||||||
|
row_dict['created_at'] = str(row_dict['created_at'])
|
||||||
|
results.append(row_dict)
|
||||||
|
cursor.close()
|
||||||
|
|
||||||
|
return results if results else []
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error searching orders: {e}")
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
def assign_order_to_box(order_id, box_id):
|
||||||
|
"""Assign an order to a box
|
||||||
|
|
||||||
|
Args:
|
||||||
|
order_id: ID of the order (scanfg_orders.id)
|
||||||
|
box_id: ID of the box to assign to
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple: (success: bool, message: str, status_code: int)
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
if not order_id or not box_id:
|
||||||
|
return False, 'Order ID and box ID are required', 400
|
||||||
|
|
||||||
|
conn = get_db()
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
# Verify order exists
|
||||||
|
cursor.execute("SELECT CP_full_code FROM scanfg_orders WHERE id = %s", (order_id,))
|
||||||
|
order = cursor.fetchone()
|
||||||
|
|
||||||
|
if not order:
|
||||||
|
cursor.close()
|
||||||
|
return False, 'Order not found', 404
|
||||||
|
|
||||||
|
# Verify box exists
|
||||||
|
cursor.execute("SELECT box_number FROM boxes_crates WHERE id = %s", (box_id,))
|
||||||
|
box = cursor.fetchone()
|
||||||
|
|
||||||
|
if not box:
|
||||||
|
cursor.close()
|
||||||
|
return False, 'Box not found', 404
|
||||||
|
|
||||||
|
# Assign order to box
|
||||||
|
cursor.execute("""
|
||||||
|
UPDATE scanfg_orders
|
||||||
|
SET box_id = %s
|
||||||
|
WHERE id = %s
|
||||||
|
""", (box_id, order_id))
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
cursor.close()
|
||||||
|
|
||||||
|
return True, f'Order "{order[0]}" assigned to box "{box[0]}"', 200
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error assigning order to box: {e}")
|
||||||
|
return False, f'Error: {str(e)}', 500
|
||||||
|
|
||||||
|
|
||||||
|
def move_order_to_box(order_id, new_box_id):
|
||||||
|
"""Move an order from one box to another
|
||||||
|
|
||||||
|
Args:
|
||||||
|
order_id: ID of the order
|
||||||
|
new_box_id: ID of the destination box
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple: (success: bool, message: str, status_code: int)
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
if not order_id or not new_box_id:
|
||||||
|
return False, 'Order ID and destination box ID are required', 400
|
||||||
|
|
||||||
|
conn = get_db()
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
# Get order info
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT CP_full_code, box_id FROM scanfg_orders WHERE id = %s
|
||||||
|
""", (order_id,))
|
||||||
|
|
||||||
|
order = cursor.fetchone()
|
||||||
|
|
||||||
|
if not order:
|
||||||
|
cursor.close()
|
||||||
|
return False, 'Order not found', 404
|
||||||
|
|
||||||
|
old_box_id = order[1]
|
||||||
|
|
||||||
|
# Verify new box exists
|
||||||
|
cursor.execute("SELECT box_number FROM boxes_crates WHERE id = %s", (new_box_id,))
|
||||||
|
new_box = cursor.fetchone()
|
||||||
|
|
||||||
|
if not new_box:
|
||||||
|
cursor.close()
|
||||||
|
return False, 'Destination box not found', 404
|
||||||
|
|
||||||
|
# Get old box info if it exists
|
||||||
|
old_box_number = None
|
||||||
|
if old_box_id:
|
||||||
|
cursor.execute("SELECT box_number FROM boxes_crates WHERE id = %s", (old_box_id,))
|
||||||
|
old_box = cursor.fetchone()
|
||||||
|
if old_box:
|
||||||
|
old_box_number = old_box[0]
|
||||||
|
|
||||||
|
# Move order to new box
|
||||||
|
cursor.execute("""
|
||||||
|
UPDATE scanfg_orders
|
||||||
|
SET box_id = %s
|
||||||
|
WHERE id = %s
|
||||||
|
""", (new_box_id, order_id))
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
cursor.close()
|
||||||
|
|
||||||
|
if old_box_number:
|
||||||
|
message = f'Order "{order[0]}" moved from "{old_box_number}" to "{new_box[0]}"'
|
||||||
|
else:
|
||||||
|
message = f'Order "{order[0]}" moved to "{new_box[0]}"'
|
||||||
|
|
||||||
|
return True, message, 200
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error moving order to box: {e}")
|
||||||
|
return False, f'Error: {str(e)}', 500
|
||||||
|
|
||||||
|
|
||||||
|
def unassign_order_from_box(order_id):
|
||||||
|
"""Remove order from its assigned box
|
||||||
|
|
||||||
|
Args:
|
||||||
|
order_id: ID of the order
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple: (success: bool, message: str, status_code: int)
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
conn = get_db()
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
# Get order info
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT CP_full_code, box_id FROM scanfg_orders WHERE id = %s
|
||||||
|
""", (order_id,))
|
||||||
|
|
||||||
|
order = cursor.fetchone()
|
||||||
|
|
||||||
|
if not order:
|
||||||
|
cursor.close()
|
||||||
|
return False, 'Order not found', 404
|
||||||
|
|
||||||
|
# Remove from box
|
||||||
|
cursor.execute("""
|
||||||
|
UPDATE scanfg_orders
|
||||||
|
SET box_id = NULL
|
||||||
|
WHERE id = %s
|
||||||
|
""", (order_id,))
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
cursor.close()
|
||||||
|
|
||||||
|
return True, f'Order "{order[0]}" removed from box', 200
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error unassigning order: {e}")
|
||||||
|
return False, f'Error: {str(e)}', 500
|
||||||
|
|
||||||
|
|
||||||
|
def get_all_boxes_summary():
|
||||||
|
"""Get summary of all boxes with order counts
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of boxes with order counts
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
conn = get_db()
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT
|
||||||
|
bc.id,
|
||||||
|
bc.box_number,
|
||||||
|
bc.status,
|
||||||
|
wl.location_code,
|
||||||
|
wl.id as location_id,
|
||||||
|
COUNT(so.id) as order_count
|
||||||
|
FROM boxes_crates bc
|
||||||
|
LEFT JOIN warehouse_locations wl ON bc.location_id = wl.id
|
||||||
|
LEFT JOIN scanfg_orders so ON bc.id = so.box_id
|
||||||
|
GROUP BY bc.id, bc.box_number, bc.status, wl.location_code, wl.id
|
||||||
|
ORDER BY bc.box_number ASC
|
||||||
|
""")
|
||||||
|
|
||||||
|
columns = [col[0] for col in cursor.description]
|
||||||
|
results = []
|
||||||
|
for row in cursor.fetchall():
|
||||||
|
row_dict = {columns[i]: row[i] for i in range(len(columns))}
|
||||||
|
results.append(row_dict)
|
||||||
|
cursor.close()
|
||||||
|
|
||||||
|
return results if results else []
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error getting boxes summary: {e}")
|
||||||
|
return []
|
||||||
@@ -30,6 +30,22 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Set Orders on Boxes Card -->
|
||||||
|
<div class="col-md-6 col-lg-4 mb-4">
|
||||||
|
<div class="card shadow-sm h-100 module-launcher">
|
||||||
|
<div class="card-body text-center">
|
||||||
|
<div class="launcher-icon mb-3">
|
||||||
|
<i class="fas fa-archive text-secondary"></i>
|
||||||
|
</div>
|
||||||
|
<h5 class="card-title">Set Orders on Boxes</h5>
|
||||||
|
<p class="card-text text-muted">Assign, move, or view orders on boxes and manage order-to-box relationships.</p>
|
||||||
|
<a href="{{ url_for('warehouse.set_orders_on_boxes') }}" class="btn btn-secondary btn-sm">
|
||||||
|
<i class="fas fa-arrow-right"></i> Manage Orders
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Create Warehouse Locations Card -->
|
<!-- Create Warehouse Locations Card -->
|
||||||
<div class="col-md-6 col-lg-4 mb-4">
|
<div class="col-md-6 col-lg-4 mb-4">
|
||||||
<div class="card shadow-sm h-100 module-launcher">
|
<div class="card shadow-sm h-100 module-launcher">
|
||||||
|
|||||||
@@ -40,8 +40,11 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<small class="text-muted d-block mt-2">
|
<small class="text-muted d-block mt-2">
|
||||||
Searches for any CP code starting with the entered text. Shows all related entries.
|
<i class="fas fa-info-circle"></i> Must start with "CP" (e.g., CP00000001). Searches for any CP code starting with the entered text.
|
||||||
</small>
|
</small>
|
||||||
|
<div id="cpCodeValidation" class="alert alert-warning d-none mt-2 mb-0 py-2" role="alert">
|
||||||
|
<small><i class="fas fa-exclamation-triangle"></i> <span id="cpCodeValidationText"></span></small>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -133,14 +136,14 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- CP Details Modal -->
|
<!-- CP Details Modal -->
|
||||||
<div class="modal fade" id="cpDetailsModal" tabindex="-1">
|
<div class="modal fade" id="cpDetailsModal" tabindex="-1" aria-labelledby="cpDetailsModalLabel" aria-hidden="true">
|
||||||
<div class="modal-dialog modal-lg">
|
<div class="modal-dialog modal-lg">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<div class="modal-header bg-primary text-white">
|
<div class="modal-header bg-primary text-white">
|
||||||
<h5 class="modal-title">
|
<h5 class="modal-title" id="cpDetailsModalLabel">
|
||||||
<i class="fas fa-details"></i> CP Code Details
|
<i class="fas fa-details"></i> CP Code Details
|
||||||
</h5>
|
</h5>
|
||||||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
|
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<div id="cpDetailsContent">
|
<div id="cpDetailsContent">
|
||||||
@@ -189,6 +192,75 @@
|
|||||||
font-family: 'Courier New', monospace;
|
font-family: 'Courier New', monospace;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Modal Theme Styling */
|
||||||
|
.modal-content {
|
||||||
|
background-color: var(--bg-primary);
|
||||||
|
color: var(--text-primary);
|
||||||
|
border-color: var(--border-color);
|
||||||
|
transition: background-color 0.3s ease, color 0.3s ease, border-color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header {
|
||||||
|
background-color: var(--bg-secondary);
|
||||||
|
border-bottom-color: var(--border-color);
|
||||||
|
color: var(--text-primary);
|
||||||
|
transition: background-color 0.3s ease, border-color 0.3s ease, color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header .modal-title {
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-body {
|
||||||
|
background-color: var(--bg-primary);
|
||||||
|
color: var(--text-primary);
|
||||||
|
transition: background-color 0.3s ease, color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-footer {
|
||||||
|
background-color: var(--bg-secondary);
|
||||||
|
border-top-color: var(--border-color);
|
||||||
|
transition: background-color 0.3s ease, border-color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Modal Button Close - Theme aware */
|
||||||
|
.btn-close {
|
||||||
|
filter: invert(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .btn-close {
|
||||||
|
filter: invert(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Modal Table Styling */
|
||||||
|
.modal-body table {
|
||||||
|
border-color: var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-body th {
|
||||||
|
background-color: var(--bg-secondary);
|
||||||
|
color: var(--text-primary);
|
||||||
|
border-color: var(--border-color);
|
||||||
|
transition: background-color 0.3s ease, color 0.3s ease, border-color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-body td {
|
||||||
|
color: var(--text-primary);
|
||||||
|
border-color: var(--border-color);
|
||||||
|
transition: color 0.3s ease, border-color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Override hardcoded Bootstrap classes in modal */
|
||||||
|
.modal-header.bg-primary {
|
||||||
|
background-color: var(--bg-secondary) !important;
|
||||||
|
color: var(--text-primary) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header.text-white {
|
||||||
|
color: var(--text-primary) !important;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
@@ -197,22 +269,97 @@ let inventoryData = [];
|
|||||||
|
|
||||||
// Initialize on page load
|
// Initialize on page load
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
loadInventory();
|
// Ensure input fields are enabled and ready for input
|
||||||
|
const cpCodeSearchEl = document.getElementById('cpCodeSearch');
|
||||||
|
const boxNumberSearchEl = document.getElementById('boxNumberSearch');
|
||||||
|
|
||||||
// Auto-search on Enter key
|
if (cpCodeSearchEl) {
|
||||||
document.getElementById('cpCodeSearch').addEventListener('keypress', function(e) {
|
cpCodeSearchEl.disabled = false;
|
||||||
|
cpCodeSearchEl.readOnly = false;
|
||||||
|
cpCodeSearchEl.addEventListener('keypress', function(e) {
|
||||||
if (e.key === 'Enter') searchByCpCode();
|
if (e.key === 'Enter') searchByCpCode();
|
||||||
});
|
});
|
||||||
|
// Add real-time validation for CP code
|
||||||
|
cpCodeSearchEl.addEventListener('input', function(e) {
|
||||||
|
validateCpCodeInput(this.value);
|
||||||
|
});
|
||||||
|
cpCodeSearchEl.addEventListener('blur', function(e) {
|
||||||
|
validateCpCodeInput(this.value);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
document.getElementById('boxNumberSearch').addEventListener('keypress', function(e) {
|
if (boxNumberSearchEl) {
|
||||||
|
boxNumberSearchEl.disabled = false;
|
||||||
|
boxNumberSearchEl.readOnly = false;
|
||||||
|
boxNumberSearchEl.addEventListener('keypress', function(e) {
|
||||||
if (e.key === 'Enter') searchByBoxNumber();
|
if (e.key === 'Enter') searchByBoxNumber();
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load inventory after setting up input fields
|
||||||
|
loadInventory();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Validate CP Code input - must start with "CP"
|
||||||
|
function validateCpCodeInput(value) {
|
||||||
|
const validationDiv = document.getElementById('cpCodeValidation');
|
||||||
|
const validationText = document.getElementById('cpCodeValidationText');
|
||||||
|
|
||||||
|
if (!validationDiv || !validationText) return;
|
||||||
|
|
||||||
|
// If field is empty, hide validation message
|
||||||
|
if (!value || value.trim() === '') {
|
||||||
|
validationDiv.classList.add('d-none');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if starts with "CP"
|
||||||
|
if (!value.toUpperCase().startsWith('CP')) {
|
||||||
|
validationText.textContent = 'CP code must start with "CP" (e.g., CP00000001)';
|
||||||
|
validationDiv.classList.remove('d-none');
|
||||||
|
} else {
|
||||||
|
validationDiv.classList.add('d-none');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
function showStatus(message, type = 'info') {
|
function showStatus(message, type = 'info') {
|
||||||
const alert = document.getElementById('statusAlert');
|
// Try to find the alert elements
|
||||||
const messageEl = document.getElementById('statusMessage');
|
let alert = document.getElementById('statusAlert');
|
||||||
|
let messageEl = document.getElementById('statusMessage');
|
||||||
|
|
||||||
|
// If elements don't exist, create them
|
||||||
|
if (!alert) {
|
||||||
|
const container = document.querySelector('.container-fluid') || document.body;
|
||||||
|
alert = document.createElement('div');
|
||||||
|
alert.id = 'statusAlert';
|
||||||
|
alert.className = `alert alert-${type} d-none`;
|
||||||
|
alert.setAttribute('role', 'alert');
|
||||||
|
|
||||||
|
messageEl = document.createElement('span');
|
||||||
|
messageEl.id = 'statusMessage';
|
||||||
|
|
||||||
|
const icon = document.createElement('i');
|
||||||
|
icon.className = 'fas fa-info-circle';
|
||||||
|
|
||||||
|
alert.appendChild(icon);
|
||||||
|
alert.appendChild(document.createTextNode(' '));
|
||||||
|
alert.appendChild(messageEl);
|
||||||
|
|
||||||
|
// Insert after first container-fluid div
|
||||||
|
if (container && container.firstChild) {
|
||||||
|
container.insertBefore(alert, container.firstChild.nextSibling);
|
||||||
|
} else {
|
||||||
|
document.body.insertBefore(alert, document.body.firstChild);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update message element if it was just created or found
|
||||||
|
if (messageEl) {
|
||||||
messageEl.textContent = message;
|
messageEl.textContent = message;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (alert) {
|
||||||
alert.className = `alert alert-${type}`;
|
alert.className = `alert alert-${type}`;
|
||||||
alert.classList.remove('d-none');
|
alert.classList.remove('d-none');
|
||||||
|
|
||||||
@@ -221,20 +368,70 @@ function showStatus(message, type = 'info') {
|
|||||||
alert.classList.add('d-none');
|
alert.classList.add('d-none');
|
||||||
}, 5000);
|
}, 5000);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function showLoading() {
|
function showLoading() {
|
||||||
document.getElementById('loadingSpinner').classList.remove('d-none');
|
let spinner = document.getElementById('loadingSpinner');
|
||||||
|
if (!spinner) {
|
||||||
|
spinner = document.createElement('div');
|
||||||
|
spinner.id = 'loadingSpinner';
|
||||||
|
spinner.className = 'spinner-border d-none';
|
||||||
|
spinner.setAttribute('role', 'status');
|
||||||
|
spinner.style.display = 'none';
|
||||||
|
spinner.innerHTML = '<span class="sr-only">Loading...</span>';
|
||||||
|
document.body.appendChild(spinner);
|
||||||
|
}
|
||||||
|
spinner.classList.remove('d-none');
|
||||||
|
spinner.style.display = 'block';
|
||||||
}
|
}
|
||||||
|
|
||||||
function hideLoading() {
|
function hideLoading() {
|
||||||
document.getElementById('loadingSpinner').classList.add('d-none');
|
const spinner = document.getElementById('loadingSpinner');
|
||||||
|
if (spinner) {
|
||||||
|
spinner.classList.add('d-none');
|
||||||
|
spinner.style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to safely get and update element text content
|
||||||
|
function safeSetElementText(elementId, text) {
|
||||||
|
const el = document.getElementById(elementId);
|
||||||
|
if (el) {
|
||||||
|
el.textContent = text;
|
||||||
|
}
|
||||||
|
return el;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to safely get element value
|
||||||
|
function safeGetElementValue(elementId) {
|
||||||
|
const el = document.getElementById(elementId);
|
||||||
|
return el ? (el.value || '') : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to safely set element value
|
||||||
|
function safeSetElementValue(elementId, value) {
|
||||||
|
const el = document.getElementById(elementId);
|
||||||
|
if (el) {
|
||||||
|
el.value = value;
|
||||||
|
}
|
||||||
|
return el;
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadInventory() {
|
function loadInventory() {
|
||||||
showLoading();
|
showLoading();
|
||||||
currentSearchType = 'all';
|
currentSearchType = 'all';
|
||||||
document.getElementById('cpCodeSearch').value = '';
|
|
||||||
document.getElementById('boxNumberSearch').value = '';
|
// Clear search fields but ensure they're enabled
|
||||||
|
const cpField = safeSetElementValue('cpCodeSearch', '');
|
||||||
|
const boxField = safeSetElementValue('boxNumberSearch', '');
|
||||||
|
|
||||||
|
// Ensure fields are not disabled
|
||||||
|
if (cpField) {
|
||||||
|
cpField.disabled = false;
|
||||||
|
}
|
||||||
|
if (boxField) {
|
||||||
|
boxField.disabled = false;
|
||||||
|
}
|
||||||
|
|
||||||
fetch('/warehouse/api/cp-inventory?limit=500&offset=0')
|
fetch('/warehouse/api/cp-inventory?limit=500&offset=0')
|
||||||
.then(response => {
|
.then(response => {
|
||||||
@@ -245,9 +442,9 @@ function loadInventory() {
|
|||||||
if (data.success) {
|
if (data.success) {
|
||||||
inventoryData = data.inventory;
|
inventoryData = data.inventory;
|
||||||
renderTable(data.inventory);
|
renderTable(data.inventory);
|
||||||
document.getElementById('totalRecords').textContent = data.count;
|
safeSetElementText('totalRecords', data.count);
|
||||||
document.getElementById('showingRecords').textContent = data.count;
|
safeSetElementText('showingRecords', data.count);
|
||||||
document.getElementById('lastUpdated').textContent = new Date().toLocaleTimeString();
|
safeSetElementText('lastUpdated', new Date().toLocaleTimeString());
|
||||||
showStatus(`Loaded ${data.count} inventory items`, 'success');
|
showStatus(`Loaded ${data.count} inventory items`, 'success');
|
||||||
} else {
|
} else {
|
||||||
showStatus(`Error: ${data.error}`, 'danger');
|
showStatus(`Error: ${data.error}`, 'danger');
|
||||||
@@ -265,13 +462,19 @@ function reloadInventory() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function searchByCpCode() {
|
function searchByCpCode() {
|
||||||
const cpCode = document.getElementById('cpCodeSearch').value.trim();
|
const cpCode = safeGetElementValue('cpCodeSearch').trim();
|
||||||
|
|
||||||
if (!cpCode) {
|
if (!cpCode) {
|
||||||
showStatus('Please enter a CP code', 'warning');
|
showStatus('Please enter a CP code', 'warning');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Validate that CP code starts with "CP"
|
||||||
|
if (!cpCode.toUpperCase().startsWith('CP')) {
|
||||||
|
showStatus('CP code must start with "CP" (e.g., CP00000001)', 'danger');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
showLoading();
|
showLoading();
|
||||||
currentSearchType = 'cp';
|
currentSearchType = 'cp';
|
||||||
|
|
||||||
@@ -287,8 +490,8 @@ function searchByCpCode() {
|
|||||||
if (data.success) {
|
if (data.success) {
|
||||||
inventoryData = data.results;
|
inventoryData = data.results;
|
||||||
renderTable(data.results);
|
renderTable(data.results);
|
||||||
document.getElementById('totalRecords').textContent = data.count;
|
safeSetElementText('totalRecords', data.count);
|
||||||
document.getElementById('showingRecords').textContent = data.count;
|
safeSetElementText('showingRecords', data.count);
|
||||||
showStatus(`Found ${data.count} entries for CP code: ${cpCode}`, 'success');
|
showStatus(`Found ${data.count} entries for CP code: ${cpCode}`, 'success');
|
||||||
} else {
|
} else {
|
||||||
showStatus(`Error: ${data.error}`, 'danger');
|
showStatus(`Error: ${data.error}`, 'danger');
|
||||||
@@ -302,12 +505,12 @@ function searchByCpCode() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function clearCpSearch() {
|
function clearCpSearch() {
|
||||||
document.getElementById('cpCodeSearch').value = '';
|
safeSetElementValue('cpCodeSearch', '');
|
||||||
loadInventory();
|
loadInventory();
|
||||||
}
|
}
|
||||||
|
|
||||||
function searchByBoxNumber() {
|
function searchByBoxNumber() {
|
||||||
const boxNumber = document.getElementById('boxNumberSearch').value.trim();
|
const boxNumber = safeGetElementValue('boxNumberSearch').trim();
|
||||||
|
|
||||||
if (!boxNumber) {
|
if (!boxNumber) {
|
||||||
showStatus('Please enter a box number', 'warning');
|
showStatus('Please enter a box number', 'warning');
|
||||||
@@ -329,8 +532,8 @@ function searchByBoxNumber() {
|
|||||||
if (data.success) {
|
if (data.success) {
|
||||||
inventoryData = data.results;
|
inventoryData = data.results;
|
||||||
renderBoxSearchTable(data.results);
|
renderBoxSearchTable(data.results);
|
||||||
document.getElementById('totalRecords').textContent = data.count;
|
safeSetElementText('totalRecords', data.count);
|
||||||
document.getElementById('showingRecords').textContent = data.count;
|
safeSetElementText('showingRecords', data.count);
|
||||||
showStatus(`Found ${data.count} CP entries in box: ${boxNumber}`, 'success');
|
showStatus(`Found ${data.count} CP entries in box: ${boxNumber}`, 'success');
|
||||||
} else {
|
} else {
|
||||||
showStatus(`Error: ${data.error}`, 'danger');
|
showStatus(`Error: ${data.error}`, 'danger');
|
||||||
@@ -344,13 +547,18 @@ function searchByBoxNumber() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function clearBoxSearch() {
|
function clearBoxSearch() {
|
||||||
document.getElementById('boxNumberSearch').value = '';
|
safeSetElementValue('boxNumberSearch', '');
|
||||||
loadInventory();
|
loadInventory();
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderTable(data) {
|
function renderTable(data) {
|
||||||
const tbody = document.getElementById('tableBody');
|
const tbody = document.getElementById('tableBody');
|
||||||
|
|
||||||
|
if (!tbody) {
|
||||||
|
console.warn('tableBody element not found');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!data || data.length === 0) {
|
if (!data || data.length === 0) {
|
||||||
tbody.innerHTML = '<tr><td colspan="10" class="text-center text-muted py-5">No inventory records found</td></tr>';
|
tbody.innerHTML = '<tr><td colspan="10" class="text-center text-muted py-5">No inventory records found</td></tr>';
|
||||||
return;
|
return;
|
||||||
@@ -413,6 +621,11 @@ function renderTable(data) {
|
|||||||
function renderBoxSearchTable(data) {
|
function renderBoxSearchTable(data) {
|
||||||
const tbody = document.getElementById('tableBody');
|
const tbody = document.getElementById('tableBody');
|
||||||
|
|
||||||
|
if (!tbody) {
|
||||||
|
console.warn('tableBody element not found');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!data || data.length === 0) {
|
if (!data || data.length === 0) {
|
||||||
tbody.innerHTML = '<tr><td colspan="10" class="text-center text-muted py-5">No CP entries found in this box</td></tr>';
|
tbody.innerHTML = '<tr><td colspan="10" class="text-center text-muted py-5">No CP entries found in this box</td></tr>';
|
||||||
return;
|
return;
|
||||||
@@ -492,6 +705,12 @@ function viewCpDetails(cpCode) {
|
|||||||
const modal = document.getElementById('cpDetailsModal');
|
const modal = document.getElementById('cpDetailsModal');
|
||||||
const content = document.getElementById('cpDetailsContent');
|
const content = document.getElementById('cpDetailsContent');
|
||||||
|
|
||||||
|
if (!modal || !content) {
|
||||||
|
console.error('Modal or content element not found');
|
||||||
|
showStatus('Error displaying details modal', 'danger');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let html = `
|
let html = `
|
||||||
<h6 class="mb-3">CP Code: <span class="cp-code-mono badge bg-primary">${data.cp_code}</span></h6>
|
<h6 class="mb-3">CP Code: <span class="cp-code-mono badge bg-primary">${data.cp_code}</span></h6>
|
||||||
<p class="text-muted">Total Variations: <strong>${data.count}</strong></p>
|
<p class="text-muted">Total Variations: <strong>${data.count}</strong></p>
|
||||||
@@ -536,8 +755,18 @@ function viewCpDetails(cpCode) {
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
content.innerHTML = html;
|
content.innerHTML = html;
|
||||||
const modalObj = new bootstrap.Modal(modal);
|
|
||||||
modalObj.show();
|
// Get or create Bootstrap Modal instance
|
||||||
|
let modalInstance = bootstrap.Modal.getInstance(modal);
|
||||||
|
if (!modalInstance) {
|
||||||
|
modalInstance = new bootstrap.Modal(modal);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove aria-hidden before showing
|
||||||
|
modal.removeAttribute('aria-hidden');
|
||||||
|
|
||||||
|
// Show the modal
|
||||||
|
modalInstance.show();
|
||||||
} else {
|
} else {
|
||||||
showStatus(`Error: ${data.error}`, 'danger');
|
showStatus(`Error: ${data.error}`, 'danger');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -722,6 +722,7 @@
|
|||||||
|
|
||||||
async function assignBoxToLocationTab0() {
|
async function assignBoxToLocationTab0() {
|
||||||
const locationCode = document.getElementById('location-code-input-tab0').value.trim();
|
const locationCode = document.getElementById('location-code-input-tab0').value.trim();
|
||||||
|
const boxNumber = document.getElementById('box-number-input-tab0').value.trim();
|
||||||
|
|
||||||
if (!currentBoxId_Tab0) {
|
if (!currentBoxId_Tab0) {
|
||||||
showAlert('alert-tab0', 'Please search for a box first', 'error');
|
showAlert('alert-tab0', 'Please search for a box first', 'error');
|
||||||
@@ -751,8 +752,36 @@
|
|||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
showAlert('alert-tab0', data.message, 'success');
|
showAlert('alert-tab0', data.message + ' - Refreshing box info...', 'success');
|
||||||
setTimeout(() => clearTab0(), 1500);
|
// Clear location input but keep box number to refresh details
|
||||||
|
document.getElementById('location-code-input-tab0').value = '';
|
||||||
|
document.getElementById('assign-section-tab0').style.display = 'none';
|
||||||
|
|
||||||
|
// Automatically refresh box details after a brief delay
|
||||||
|
setTimeout(async () => {
|
||||||
|
try {
|
||||||
|
const searchResponse = await fetch('{{ url_for("warehouse.api_search_box") }}', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ box_number: boxNumber })
|
||||||
|
});
|
||||||
|
|
||||||
|
const searchData = await searchResponse.json();
|
||||||
|
|
||||||
|
if (searchData.success) {
|
||||||
|
document.getElementById('display-box-number-tab0').textContent = searchData.box.box_number;
|
||||||
|
document.getElementById('display-box-status-tab0').textContent = searchData.box.status;
|
||||||
|
document.getElementById('display-box-location-tab0').textContent = searchData.box.location_code || 'Unassigned';
|
||||||
|
|
||||||
|
document.getElementById('box-info-tab0').classList.add('show');
|
||||||
|
showAlert('alert-tab0', 'Box updated successfully', 'success');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error refreshing box:', error);
|
||||||
|
}
|
||||||
|
}, 800);
|
||||||
} else {
|
} else {
|
||||||
showAlert('alert-tab0', data.message || 'Error assigning box', 'error');
|
showAlert('alert-tab0', data.message || 'Error assigning box', 'error');
|
||||||
}
|
}
|
||||||
@@ -932,6 +961,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function moveBoxTab2() {
|
async function moveBoxTab2() {
|
||||||
|
const locationCode = document.getElementById('location-code-input-tab2').value.trim();
|
||||||
const newLocationCode = document.getElementById('new-location-input-tab2').value.trim();
|
const newLocationCode = document.getElementById('new-location-input-tab2').value.trim();
|
||||||
|
|
||||||
if (!currentBoxId_Tab2) {
|
if (!currentBoxId_Tab2) {
|
||||||
@@ -962,8 +992,45 @@
|
|||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
showAlert('alert-tab2', data.message, 'success');
|
showAlert('alert-tab2', data.message + ' - Refreshing location...', 'success');
|
||||||
setTimeout(() => clearTab2(), 1500);
|
// Clear the selection but keep the location code to show updated list
|
||||||
|
document.getElementById('new-location-input-tab2').value = '';
|
||||||
|
document.getElementById('move-section-tab2').style.display = 'none';
|
||||||
|
document.getElementById('selected-box-display-tab2').textContent = '-';
|
||||||
|
currentBoxId_Tab2 = null;
|
||||||
|
|
||||||
|
// Automatically refresh and show the updated location list after a brief delay
|
||||||
|
setTimeout(async () => {
|
||||||
|
try {
|
||||||
|
const searchResponse = await fetch('{{ url_for("warehouse.api_search_location") }}', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ location_code: locationCode })
|
||||||
|
});
|
||||||
|
|
||||||
|
const searchData = await searchResponse.json();
|
||||||
|
|
||||||
|
if (searchData.success) {
|
||||||
|
document.getElementById('display-location-code-tab2').textContent = searchData.location.location_code;
|
||||||
|
document.getElementById('display-boxes-count-tab2').textContent = searchData.boxes.length;
|
||||||
|
|
||||||
|
document.getElementById('location-info-tab2').classList.add('show');
|
||||||
|
|
||||||
|
if (searchData.boxes.length > 0) {
|
||||||
|
displayBoxesTab2(searchData.boxes);
|
||||||
|
document.getElementById('boxes-list-tab2').classList.add('show');
|
||||||
|
showAlert('alert-tab2', `Location updated - ${searchData.boxes.length} box(es) found`, 'success');
|
||||||
|
} else {
|
||||||
|
document.getElementById('boxes-list-tab2').classList.remove('show');
|
||||||
|
showAlert('alert-tab2', `Location "${locationCode}" is now empty`, 'warning');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error refreshing location:', error);
|
||||||
|
}
|
||||||
|
}, 800);
|
||||||
} else {
|
} else {
|
||||||
showAlert('alert-tab2', data.message || 'Error moving box', 'error');
|
showAlert('alert-tab2', data.message || 'Error moving box', 'error');
|
||||||
}
|
}
|
||||||
|
|||||||
1064
app/templates/modules/warehouse/set_orders_on_boxes.html
Normal file
1064
app/templates/modules/warehouse/set_orders_on_boxes.html
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user