From c7f5203aa7e16872510051ca0049a35042cd70c7 Mon Sep 17 00:00:00 2001 From: ske087 Date: Sun, 7 Dec 2025 00:31:41 +0200 Subject: [PATCH] Add boxes/crates management system to warehouse module - Created boxes_crates table with 8-digit auto-generated box numbers - Added manage_boxes route and full CRUD operations - Implemented box status tracking (open/closed) - Added location assignment functionality - Integrated QZ Tray printing with barcode labels - Created manage_boxes.html with table, preview, and print features - Added 'Manage Boxes/Crates' card to warehouse main page - Boxes can be created without location and assigned later - Includes delete functionality for admin/management roles - Added box statistics display --- .../setup_complete_database.py | 33 + py_app/app/routes.py | 7 + py_app/app/templates/main_page_warehouse.html | 9 +- py_app/app/templates/manage_boxes.html | 605 ++++++++++++++++++ py_app/app/warehouse.py | 175 ++++- 5 files changed, 827 insertions(+), 2 deletions(-) create mode 100644 py_app/app/templates/manage_boxes.html 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 - + +
+

Manage Boxes/Crates

+

Track and manage boxes and crates in the warehouse.

+ Go to Boxes +
+ +

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 number will be auto-generated (8 digits)
+ Location can be assigned later after filling the box. +
+ +
+
+ + +
+

All Boxes

+
+ + + + + + + + + + + + + + {% for box in boxes %} + + + + + + + + + + {% endfor %} + +
IDBox NumberStatusLocationCreated AtCreated ByActions
{{ box[0] }}{{ box[1] }} + {{ box[2]|upper }} + {{ box[3] or 'Not assigned' }}{{ box[4].strftime('%Y-%m-%d %H:%M') if box[4] else '' }}{{ box[6] or '' }} +
+ +
+ + + + +
+ + + + +
+
+
+
+ + +
+ +
+

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 +
+ +
+
+
+
Select a box
+
+ +
No box selected
+
+
+ +
+
+ + +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + +{% 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: