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 %}
+ When enabled, good quality scans (000) will prompt for box assignment +
+Search and view products, boxes, and their warehouse locations.
+ View Inventory +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 %} +| CP Code | +Box Number | +Location | +Scanned At | +Scanned By | +Placed At Location | +Placed By | +Box Status | +Location 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 %} + | +
📦 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 %} +{error_trace}", 500
+
+