diff --git a/py_app/app/db_create_scripts/setup_complete_database.py b/py_app/app/db_create_scripts/setup_complete_database.py
index eaaeaf4..c01edb7 100755
--- a/py_app/app/db_create_scripts/setup_complete_database.py
+++ b/py_app/app/db_create_scripts/setup_complete_database.py
@@ -184,6 +184,38 @@ def create_warehouse_locations_table():
print_error(f"Failed to create warehouse_locations table: {e}")
return False
+def create_boxes_crates_table():
+ """Create boxes_crates table for tracking boxes containing scanned orders"""
+ print_step(5, "Creating Boxes/Crates Table")
+
+ try:
+ conn = mariadb.connect(**DB_CONFIG)
+ cursor = conn.cursor()
+
+ boxes_query = """
+ CREATE TABLE IF NOT EXISTS boxes_crates (
+ id BIGINT AUTO_INCREMENT PRIMARY KEY,
+ box_number VARCHAR(8) NOT NULL UNIQUE,
+ status ENUM('open', 'closed') DEFAULT 'open',
+ location_id BIGINT,
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+ created_by VARCHAR(100),
+ FOREIGN KEY (location_id) REFERENCES warehouse_locations(id) ON DELETE SET NULL
+ );
+ """
+ cursor.execute(boxes_query)
+ print_success("Table 'boxes_crates' created successfully")
+
+ conn.commit()
+ cursor.close()
+ conn.close()
+ return True
+
+ except Exception as e:
+ print_error(f"Failed to create boxes_crates table: {e}")
+ return False
+
def create_permissions_tables():
"""Create permission management tables"""
print_step(5, "Creating Permission Management Tables")
@@ -705,6 +737,7 @@ def main():
create_scan_tables,
create_order_for_labels_table,
create_warehouse_locations_table,
+ create_boxes_crates_table,
create_permissions_tables,
create_users_table_mariadb,
# create_sqlite_tables, # Disabled - using MariaDB only
diff --git a/py_app/app/routes.py b/py_app/app/routes.py
index 20102d9..b84ca0d 100755
--- a/py_app/app/routes.py
+++ b/py_app/app/routes.py
@@ -3940,6 +3940,13 @@ def delete_location():
return jsonify({'success': False, 'error': str(e)})
+@warehouse_bp.route('/manage_boxes', methods=['GET', 'POST'])
+@requires_warehouse_module
+def manage_boxes():
+ from app.warehouse import manage_boxes_handler
+ return manage_boxes_handler()
+
+
# Daily Mirror Route Redirects for Backward Compatibility
@bp.route('/daily_mirror_main')
def daily_mirror_main_route():
diff --git a/py_app/app/templates/main_page_warehouse.html b/py_app/app/templates/main_page_warehouse.html
index 7b7cbfa..7ce1732 100755
--- a/py_app/app/templates/main_page_warehouse.html
+++ b/py_app/app/templates/main_page_warehouse.html
@@ -23,7 +23,14 @@
Go to Locations
-
+
+
Warehouse Reports
View and export warehouse activity and inventory reports.
diff --git a/py_app/app/templates/manage_boxes.html b/py_app/app/templates/manage_boxes.html
new file mode 100644
index 0000000..88c8a07
--- /dev/null
+++ b/py_app/app/templates/manage_boxes.html
@@ -0,0 +1,605 @@
+{% extends "base.html" %}
+{% block title %}Manage Boxes{% endblock %}
+
+{% block head %}
+
+
+{% endblock %}
+
+{% block content %}
+
+
+
+
+
Add New Box
+ {% with messages = get_flashed_messages(with_categories=true) %}
+ {% if messages %}
+ {% for category, message in messages %}
+
+ {{ message }}
+
+ {% endfor %}
+ {% endif %}
+ {% endwith %}
+
+
+
+
+
+
+
+
+
+
+
Box Statistics
+
+ Total: {{ boxes|length }}
+ Open: {{ boxes|selectattr('2', 'equalto', 'open')|list|length }}
+ Closed: {{ boxes|selectattr('2', 'equalto', 'closed')|list|length }}
+
+
+ {% if session['role'] in ['administrator', 'management'] %}
+
+
+
+
+ {% endif %}
+
+
+
+
+
Print Box Label
+
+ Click on a box row in the table to select it for printing
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Change location for box:
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+{% endblock %}
diff --git a/py_app/app/warehouse.py b/py_app/app/warehouse.py
index cf5883b..bfb2eb9 100755
--- a/py_app/app/warehouse.py
+++ b/py_app/app/warehouse.py
@@ -44,6 +44,30 @@ def ensure_warehouse_locations_table():
except Exception as e:
print(f"Error ensuring warehouse_locations table: {e}")
+def ensure_boxes_crates_table():
+ try:
+ conn = get_db_connection()
+ cursor = conn.cursor()
+ cursor.execute("SHOW TABLES LIKE 'boxes_crates'")
+ result = cursor.fetchone()
+ if not result:
+ cursor.execute('''
+ CREATE TABLE IF NOT EXISTS boxes_crates (
+ id BIGINT AUTO_INCREMENT PRIMARY KEY,
+ box_number VARCHAR(8) NOT NULL UNIQUE,
+ status ENUM('open', 'closed') DEFAULT 'open',
+ location_id BIGINT,
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+ created_by VARCHAR(100),
+ FOREIGN KEY (location_id) REFERENCES warehouse_locations(id) ON DELETE SET NULL
+ )
+ ''')
+ conn.commit()
+ conn.close()
+ except Exception as e:
+ print(f"Error ensuring boxes_crates table: {e}")
+
# Add warehouse-specific functions below
def add_location(location_code, size, description):
conn = get_db_connection()
@@ -268,9 +292,158 @@ def update_location(location_id, location_code, size, description):
except Exception as e:
print(f"Error updating location: {e}")
return {"success": False, "error": str(e)}
- print(f"Error updating location: {e}")
+
+# ============================================================================
+# Boxes/Crates Functions
+# ============================================================================
+
+def generate_box_number():
+ """Generate next box number with 8 digits (00000001, 00000002, etc.)"""
+ conn = get_db_connection()
+ cursor = conn.cursor()
+ cursor.execute("SELECT MAX(CAST(box_number AS UNSIGNED)) FROM boxes_crates")
+ result = cursor.fetchone()
+ conn.close()
+
+ if result and result[0]:
+ next_number = int(result[0]) + 1
+ else:
+ next_number = 1
+
+ return str(next_number).zfill(8)
+
+def add_box(location_id=None, created_by=None):
+ """Add a new box/crate"""
+ conn = get_db_connection()
+ cursor = conn.cursor()
+
+ box_number = generate_box_number()
+
+ try:
+ cursor.execute(
+ "INSERT INTO boxes_crates (box_number, status, location_id, created_by) VALUES (%s, %s, %s, %s)",
+ (box_number, 'open', location_id if location_id else None, created_by)
+ )
+ conn.commit()
+ conn.close()
+ return f"Box {box_number} created successfully"
+ except Exception as e:
+ conn.close()
+ return f"Error creating box: {e}"
+
+def get_boxes():
+ """Get all boxes with location information"""
+ conn = get_db_connection()
+ cursor = conn.cursor()
+ cursor.execute("""
+ SELECT
+ b.id,
+ b.box_number,
+ b.status,
+ l.location_code,
+ b.created_at,
+ b.updated_at,
+ b.created_by,
+ b.location_id
+ FROM boxes_crates b
+ LEFT JOIN warehouse_locations l ON b.location_id = l.id
+ ORDER BY b.id DESC
+ """)
+ boxes = cursor.fetchall()
+ conn.close()
+ return boxes
+
+def update_box_status(box_id, status):
+ """Update box status (open/closed)"""
+ conn = get_db_connection()
+ cursor = conn.cursor()
+ try:
+ cursor.execute(
+ "UPDATE boxes_crates SET status = %s WHERE id = %s",
+ (status, box_id)
+ )
+ conn.commit()
+ conn.close()
+ return {"success": True, "message": f"Box status updated to {status}"}
+ except Exception as e:
+ conn.close()
return {"success": False, "error": str(e)}
+def update_box_location(box_id, location_id):
+ """Update box location"""
+ conn = get_db_connection()
+ cursor = conn.cursor()
+ try:
+ cursor.execute(
+ "UPDATE boxes_crates SET location_id = %s WHERE id = %s",
+ (location_id if location_id else None, box_id)
+ )
+ conn.commit()
+ conn.close()
+ return {"success": True, "message": "Box location updated"}
+ except Exception as e:
+ conn.close()
+ return {"success": False, "error": str(e)}
+
+def delete_boxes_by_ids(ids_str):
+ """Delete boxes by comma-separated IDs"""
+ ids = [id.strip() for id in ids_str.split(',') if id.strip().isdigit()]
+ if not ids:
+ return "No valid IDs provided."
+ conn = get_db_connection()
+ cursor = conn.cursor()
+ deleted = 0
+ for id in ids:
+ cursor.execute("DELETE FROM boxes_crates WHERE id = %s", (id,))
+ if cursor.rowcount:
+ deleted += 1
+ conn.commit()
+ conn.close()
+ return f"Deleted {deleted} box(es)."
+
+def manage_boxes_handler():
+ """Handler for boxes/crates management page"""
+ try:
+ # Ensure table exists
+ ensure_boxes_crates_table()
+ ensure_warehouse_locations_table()
+
+ if request.method == "POST":
+ action = request.form.get("action")
+
+ if action == "delete_boxes":
+ ids_str = request.form.get("delete_ids", "")
+ message = delete_boxes_by_ids(ids_str)
+ session['flash_message'] = message
+ elif action == "add_box":
+ created_by = session.get('user', 'Unknown')
+ message = add_box(None, created_by) # Create box without location
+ session['flash_message'] = message
+ elif action == "update_status":
+ box_id = request.form.get("box_id")
+ new_status = request.form.get("new_status")
+ message = update_box_status(box_id, new_status)
+ session['flash_message'] = message
+ elif action == "update_location":
+ box_id = request.form.get("box_id")
+ new_location_id = request.form.get("new_location_id")
+ message = update_box_location(box_id, new_location_id)
+ session['flash_message'] = message
+
+ return redirect(url_for('warehouse.manage_boxes'))
+
+ # Get flash message from session if any
+ message = session.pop('flash_message', None)
+ boxes = get_boxes()
+ locations = get_locations()
+ return render_template("manage_boxes.html", boxes=boxes, locations=locations, message=message)
+ except Exception as e:
+ import traceback
+ error_trace = traceback.format_exc()
+ print(f"Error in manage_boxes_handler: {e}")
+ print(error_trace)
+ return f"
Error loading boxes management
{error_trace}", 500
+
def delete_location_by_id(location_id):
"""Delete a warehouse location by ID"""
try: