From 1a3e26d86d101ec6033e676c3e6a00b65b4765db Mon Sep 17 00:00:00 2001 From: ske087 Date: Wed, 10 Dec 2025 22:30:52 +0200 Subject: [PATCH] updated to set boxes , and updated to fill boxes --- py_app/app/__init__.py | 2 +- .../setup_complete_database.py | 72 ++++ py_app/app/routes.py | 100 ++++- py_app/app/templates/fg_scan.html | 338 ++++++++++++++++- py_app/app/templates/main_page_warehouse.html | 9 +- py_app/app/templates/warehouse_inventory.html | 348 ++++++++++++++++++ py_app/app/warehouse.py | 193 ++++++++++ 7 files changed, 1053 insertions(+), 9 deletions(-) create mode 100644 py_app/app/templates/warehouse_inventory.html diff --git a/py_app/app/__init__.py b/py_app/app/__init__.py index 6f13c7e..c9802cd 100755 --- a/py_app/app/__init__.py +++ b/py_app/app/__init__.py @@ -14,7 +14,7 @@ def create_app(): from app.routes import bp as main_bp, warehouse_bp from app.daily_mirror import daily_mirror_bp app.register_blueprint(main_bp, url_prefix='/') - app.register_blueprint(warehouse_bp) + app.register_blueprint(warehouse_bp, url_prefix='/warehouse') app.register_blueprint(daily_mirror_bp) # Add 'now' function to Jinja2 globals 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 c01edb7..08dd235 100755 --- a/py_app/app/db_create_scripts/setup_complete_database.py +++ b/py_app/app/db_create_scripts/setup_complete_database.py @@ -216,6 +216,76 @@ def create_boxes_crates_table(): print_error(f"Failed to create boxes_crates table: {e}") return False +def create_box_contents_table(): + """Create box_contents table for tracking CP codes in boxes""" + print_step(6, "Creating Box Contents Tracking Table") + + try: + conn = mariadb.connect(**DB_CONFIG) + cursor = conn.cursor() + + box_contents_query = """ + CREATE TABLE IF NOT EXISTS box_contents ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + box_id BIGINT NOT NULL, + cp_code VARCHAR(15) NOT NULL, + scan_id BIGINT, + scanned_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + scanned_by VARCHAR(100), + FOREIGN KEY (box_id) REFERENCES boxes_crates(id) ON DELETE CASCADE, + INDEX idx_box_id (box_id), + INDEX idx_cp_code (cp_code) + ); + """ + cursor.execute(box_contents_query) + print_success("Table 'box_contents' created successfully") + + conn.commit() + cursor.close() + conn.close() + return True + + except Exception as e: + print_error(f"Failed to create box_contents table: {e}") + return False + +def create_location_contents_table(): + """Create location_contents table for tracking boxes in locations""" + print_step(7, "Creating Location Contents Tracking Table") + + try: + conn = mariadb.connect(**DB_CONFIG) + cursor = conn.cursor() + + location_contents_query = """ + CREATE TABLE IF NOT EXISTS location_contents ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + location_id BIGINT NOT NULL, + box_id BIGINT NOT NULL, + placed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + placed_by VARCHAR(100), + removed_at TIMESTAMP NULL, + removed_by VARCHAR(100), + status ENUM('active', 'removed') DEFAULT 'active', + FOREIGN KEY (location_id) REFERENCES warehouse_locations(id) ON DELETE CASCADE, + FOREIGN KEY (box_id) REFERENCES boxes_crates(id) ON DELETE CASCADE, + INDEX idx_location_id (location_id), + INDEX idx_box_id (box_id), + INDEX idx_status (status) + ); + """ + cursor.execute(location_contents_query) + print_success("Table 'location_contents' created successfully") + + conn.commit() + cursor.close() + conn.close() + return True + + except Exception as e: + print_error(f"Failed to create location_contents table: {e}") + return False + def create_permissions_tables(): """Create permission management tables""" print_step(5, "Creating Permission Management Tables") @@ -738,6 +808,8 @@ def main(): create_order_for_labels_table, create_warehouse_locations_table, create_boxes_crates_table, + create_box_contents_table, + create_location_contents_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 b84ca0d..fc0ddf4 100755 --- a/py_app/app/routes.py +++ b/py_app/app/routes.py @@ -217,6 +217,74 @@ def get_db_connection(): database=settings['database_name'] ) +def ensure_scanfg_orders_table(): + """Ensure scanfg_orders table exists with proper structure and trigger""" + try: + conn = get_db_connection() + cursor = conn.cursor() + + # Check if table exists + cursor.execute("SHOW TABLES LIKE 'scanfg_orders'") + if cursor.fetchone(): + conn.close() + return # Table already exists + + print("Creating scanfg_orders table...") + + # Create table + cursor.execute(""" + CREATE TABLE IF NOT EXISTS scanfg_orders ( + Id BIGINT AUTO_INCREMENT PRIMARY KEY, + operator_code VARCHAR(50), + CP_base_code VARCHAR(10), + CP_full_code VARCHAR(15), + OC1_code VARCHAR(50), + OC2_code VARCHAR(50), + quality_code INT, + date DATE, + time TIME, + approved_quantity INT DEFAULT 0, + rejected_quantity INT DEFAULT 0, + INDEX idx_cp_base (CP_base_code), + INDEX idx_date (date), + INDEX idx_quality (quality_code) + ) + """) + + # Create trigger + cursor.execute("DROP TRIGGER IF EXISTS set_quantities_fg") + cursor.execute(""" + CREATE TRIGGER set_quantities_fg + BEFORE INSERT ON scanfg_orders + FOR EACH ROW + BEGIN + SET @cp_base = SUBSTRING(NEW.CP_full_code, 1, 10); + SET @approved = (SELECT COUNT(*) FROM scanfg_orders + WHERE SUBSTRING(CP_full_code, 1, 10) = @cp_base + AND quality_code = 0); + SET @rejected = (SELECT COUNT(*) FROM scanfg_orders + WHERE SUBSTRING(CP_full_code, 1, 10) = @cp_base + AND quality_code != 0); + + IF NEW.quality_code = 0 THEN + SET NEW.approved_quantity = @approved + 1; + SET NEW.rejected_quantity = @rejected; + ELSE + SET NEW.approved_quantity = @approved; + SET NEW.rejected_quantity = @rejected + 1; + END IF; + + SET NEW.CP_base_code = @cp_base; + END + """) + + conn.commit() + conn.close() + print("✅ scanfg_orders table and trigger created successfully") + + except mariadb.Error as e: + print(f"Error creating scanfg_orders table: {e}") + @bp.route('/dashboard') def dashboard(): print("Session user:", session.get('user'), session.get('role')) @@ -589,6 +657,8 @@ def logout(): @bp.route('/fg_scan', methods=['GET', 'POST']) @requires_quality_module def fg_scan(): + # Ensure scanfg_orders table exists + ensure_scanfg_orders_table() if request.method == 'POST': # Handle form submission @@ -637,6 +707,14 @@ def fg_scan(): except mariadb.Error as e: print(f"Error saving finish goods scan data: {e}") flash(f"Error saving scan data: {e}") + + # Check if this is an AJAX request (for scan-to-boxes feature) + if request.headers.get('X-Requested-With') == 'XMLHttpRequest' or request.accept_mimetypes.best == 'application/json': + # For AJAX requests, return JSON response without redirect + return jsonify({'success': True, 'message': 'Scan recorded successfully'}) + + # For normal form submissions, redirect to prevent form resubmission (POST-Redirect-GET pattern) + return redirect(url_for('main.fg_scan')) # Fetch the latest scan data for display from scanfg_orders scan_data = [] @@ -1336,7 +1414,7 @@ def get_fg_report_data(): cursor.execute(""" SELECT Id, operator_code, CP_base_code, CP_full_code, OC1_code, OC2_code, quality_code, date, time, approved_quantity, rejected_quantity FROM scanfg_orders - WHERE date = ? + WHERE date = %s ORDER BY date DESC, time DESC """, (today,)) rows = cursor.fetchall() @@ -1351,7 +1429,7 @@ def get_fg_report_data(): cursor.execute(""" SELECT Id, operator_code, CP_base_code, CP_full_code, OC1_code, OC2_code, quality_code, date, time, approved_quantity, rejected_quantity FROM scanfg_orders - WHERE date >= ? + WHERE date >= %s ORDER BY date DESC, time DESC """, (start_date,)) rows = cursor.fetchall() @@ -1365,7 +1443,7 @@ def get_fg_report_data(): cursor.execute(""" SELECT Id, operator_code, CP_full_code, OC1_code, OC2_code, quality_code, date, time, approved_quantity, rejected_quantity FROM scanfg_orders - WHERE date = ? AND quality_code != 0 + WHERE date = %s AND quality_code != 0 ORDER BY date DESC, time DESC """, (today,)) rows = cursor.fetchall() @@ -1380,7 +1458,7 @@ def get_fg_report_data(): cursor.execute(""" SELECT Id, operator_code, CP_full_code, OC1_code, OC2_code, quality_code, date, time, approved_quantity, rejected_quantity FROM scanfg_orders - WHERE date >= ? AND quality_code != 0 + WHERE date >= %s AND quality_code != 0 ORDER BY date DESC, time DESC """, (start_date,)) rows = cursor.fetchall() @@ -3947,6 +4025,20 @@ def manage_boxes(): return manage_boxes_handler() +@warehouse_bp.route('/assign_cp_to_box', methods=['POST']) +@requires_warehouse_module +def assign_cp_to_box(): + from app.warehouse import assign_cp_to_box_handler + return assign_cp_to_box_handler() + + +@warehouse_bp.route('/inventory', methods=['GET']) +@requires_warehouse_module +def warehouse_inventory(): + from app.warehouse import view_warehouse_inventory_handler + return view_warehouse_inventory_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/fg_scan.html b/py_app/app/templates/fg_scan.html index 3ffebc8..1d1f4c0 100644 --- a/py_app/app/templates/fg_scan.html +++ b/py_app/app/templates/fg_scan.html @@ -15,7 +15,128 @@ } + + + {% endblock %} {% block content %}
@@ -531,6 +738,17 @@ document.addEventListener('DOMContentLoaded', function() { + + +
+ +

+ When enabled, good quality scans (000) will prompt for box assignment +

+
@@ -573,4 +791,118 @@ document.addEventListener('DOMContentLoaded', function() { + + + + + + {% endblock %} diff --git a/py_app/app/templates/main_page_warehouse.html b/py_app/app/templates/main_page_warehouse.html index 7ce1732..406d56a 100755 --- a/py_app/app/templates/main_page_warehouse.html +++ b/py_app/app/templates/main_page_warehouse.html @@ -30,7 +30,14 @@ Go to Boxes - + +
+

View Products/Boxes/Locations

+

Search and view products, boxes, and their warehouse locations.

+ View Inventory +
+ +

Warehouse Reports

View and export warehouse activity and inventory reports.

diff --git a/py_app/app/templates/warehouse_inventory.html b/py_app/app/templates/warehouse_inventory.html new file mode 100644 index 0000000..e55bd35 --- /dev/null +++ b/py_app/app/templates/warehouse_inventory.html @@ -0,0 +1,348 @@ +{% extends "base.html" %} +{% block title %}Warehouse Inventory{% endblock %} + +{% block head %} + + + +{% endblock %} + +{% block content %} +
+

Warehouse Inventory - Products/Boxes/Locations

+ + +
+

🔍 Search Inventory

+
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + Clear +
+
+
+ + + {% if inventory_data %} +
+
+

Total Records

+
{{ inventory_data|length }}
+
+
+

Unique CP Codes

+
{{ inventory_data|map(attribute=0)|unique|list|length }}
+
+
+

Unique Boxes

+
{{ inventory_data|map(attribute=1)|unique|list|length }}
+
+
+

Locations Used

+
{{ inventory_data|map(attribute=2)|select|unique|list|length }}
+
+
+ {% endif %} + + +
+

Inventory Details

+ + {% if inventory_data %} + + + + + + + + + + + + + + + + {% for row in inventory_data %} + + + + + + + + + + + + {% endfor %} + +
CP CodeBox NumberLocationScanned AtScanned ByPlaced At LocationPlaced ByBox StatusLocation Status
{{ row[0] }}{{ row[1] }}{{ row[2] or 'Not assigned'|safe }}{{ row[3].strftime('%Y-%m-%d %H:%M') if row[3] else '' }}{{ row[4] or '' }}{{ row[5].strftime('%Y-%m-%d %H:%M') if row[5] else '' }}{{ row[6] or '' }} + + {{ row[7]|upper if row[7] else 'N/A' }} + + + {% if row[8] %} + + {{ row[8]|upper }} + + {% else %} + - + {% endif %} +
+ {% else %} +
+

📦 No inventory data found

+ {% if search_cp or search_box or search_location %} +

Try adjusting your search criteria

+ {% else %} +

Start scanning products to boxes to populate inventory

+ {% endif %} +
+ {% endif %} +
+
+{% endblock %} diff --git a/py_app/app/warehouse.py b/py_app/app/warehouse.py index bfb2eb9..16eda41 100755 --- a/py_app/app/warehouse.py +++ b/py_app/app/warehouse.py @@ -68,6 +68,64 @@ def ensure_boxes_crates_table(): except Exception as e: print(f"Error ensuring boxes_crates table: {e}") +def ensure_box_contents_table(): + """Ensure box_contents table exists for tracking CP codes in boxes""" + try: + conn = get_db_connection() + cursor = conn.cursor() + cursor.execute("SHOW TABLES LIKE 'box_contents'") + result = cursor.fetchone() + if not result: + cursor.execute(''' + CREATE TABLE IF NOT EXISTS box_contents ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + box_id BIGINT NOT NULL, + cp_code VARCHAR(15) NOT NULL, + scan_id BIGINT, + scanned_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + scanned_by VARCHAR(100), + FOREIGN KEY (box_id) REFERENCES boxes_crates(id) ON DELETE CASCADE, + INDEX idx_box_id (box_id), + INDEX idx_cp_code (cp_code) + ) + ''') + conn.commit() + print("box_contents table created successfully") + conn.close() + except Exception as e: + print(f"Error ensuring box_contents table: {e}") + +def ensure_location_contents_table(): + """Ensure location_contents table exists for tracking boxes in locations""" + try: + conn = get_db_connection() + cursor = conn.cursor() + cursor.execute("SHOW TABLES LIKE 'location_contents'") + result = cursor.fetchone() + if not result: + cursor.execute(''' + CREATE TABLE IF NOT EXISTS location_contents ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + location_id BIGINT NOT NULL, + box_id BIGINT NOT NULL, + placed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + placed_by VARCHAR(100), + removed_at TIMESTAMP NULL, + removed_by VARCHAR(100), + status ENUM('active', 'removed') DEFAULT 'active', + FOREIGN KEY (location_id) REFERENCES warehouse_locations(id) ON DELETE CASCADE, + FOREIGN KEY (box_id) REFERENCES boxes_crates(id) ON DELETE CASCADE, + INDEX idx_location_id (location_id), + INDEX idx_box_id (box_id), + INDEX idx_status (status) + ) + ''') + conn.commit() + print("location_contents table created successfully") + conn.close() + except Exception as e: + print(f"Error ensuring location_contents table: {e}") + # Add warehouse-specific functions below def add_location(location_code, size, description): conn = get_db_connection() @@ -418,6 +476,17 @@ def manage_boxes_handler(): elif action == "add_box": created_by = session.get('user', 'Unknown') message = add_box(None, created_by) # Create box without location + + # Check if this is an AJAX request + if request.headers.get('X-Requested-With') == 'XMLHttpRequest': + # Extract box number from message (format: "Box 12345678 created successfully") + import re + match = re.search(r'Box (\d{8})', message) + if match: + return jsonify({'success': True, 'box_number': match.group(1), 'message': message}) + else: + return jsonify({'success': False, 'error': message}) + session['flash_message'] = message elif action == "update_status": box_id = request.form.get("box_id") @@ -467,3 +536,127 @@ def delete_location_by_id(location_id): except Exception as e: print(f"Error deleting location: {e}") return {"success": False, "error": str(e)} + +def assign_cp_to_box_handler(): + """Handle assigning CP code to a box""" + from flask import request, jsonify, session + import json + + try: + # Ensure box_contents table exists + ensure_box_contents_table() + + data = json.loads(request.data) + box_number = data.get('box_number') + cp_code = data.get('cp_code') + scanned_by = session.get('user', 'Unknown') + + if not box_number or not cp_code: + return jsonify({'success': False, 'error': 'Missing box_number or cp_code'}), 400 + + conn = get_db_connection() + cursor = conn.cursor() + + # Find the box by number + cursor.execute("SELECT id FROM boxes_crates WHERE box_number = %s", (box_number,)) + box = cursor.fetchone() + + if not box: + conn.close() + return jsonify({'success': False, 'error': f'Box {box_number} not found'}), 404 + + box_id = box[0] + + # Insert into box_contents + cursor.execute(""" + INSERT INTO box_contents (box_id, cp_code, scanned_by) + VALUES (%s, %s, %s) + """, (box_id, cp_code, scanned_by)) + + conn.commit() + conn.close() + + return jsonify({ + 'success': True, + 'message': f'CP {cp_code} assigned to box {box_number}' + }), 200 + + except Exception as e: + import traceback + print(f"Error in assign_cp_to_box_handler: {e}") + print(traceback.format_exc()) + return jsonify({'success': False, 'error': str(e)}), 500 + +def view_warehouse_inventory_handler(): + """Handle warehouse inventory view - shows CP codes, boxes, and locations""" + from flask import render_template, request + + try: + # Ensure tables exist + ensure_box_contents_table() + ensure_location_contents_table() + + # Get search parameters + search_cp = request.args.get('search_cp', '').strip() + search_box = request.args.get('search_box', '').strip() + search_location = request.args.get('search_location', '').strip() + + conn = get_db_connection() + cursor = conn.cursor() + + # Build query with joins and filters + query = """ + SELECT + bc.cp_code, + b.box_number, + wl.location_code, + bc.scanned_at, + bc.scanned_by, + lc.placed_at, + lc.placed_by, + b.status as box_status, + lc.status as location_status + FROM box_contents bc + INNER JOIN boxes_crates b ON bc.box_id = b.id + LEFT JOIN location_contents lc ON b.id = lc.box_id AND lc.status = 'active' + LEFT JOIN warehouse_locations wl ON lc.location_id = wl.id + WHERE 1=1 + """ + + params = [] + + if search_cp: + query += " AND bc.cp_code LIKE %s" + params.append(f"%{search_cp}%") + + if search_box: + query += " AND b.box_number LIKE %s" + params.append(f"%{search_box}%") + + if search_location: + query += " AND wl.location_code LIKE %s" + params.append(f"%{search_location}%") + + query += " ORDER BY bc.scanned_at DESC" + + cursor.execute(query, params) + inventory_data = cursor.fetchall() + + conn.close() + + return render_template( + 'warehouse_inventory.html', + inventory_data=inventory_data, + search_cp=search_cp, + search_box=search_box, + search_location=search_location + ) + + except Exception as e: + import traceback + error_trace = traceback.format_exc() + print(f"Error in view_warehouse_inventory_handler: {e}") + print(error_trace) + return f"

Error loading warehouse inventory

{error_trace}
", 500 + +