FG Scan form validation improvements with warehouse module updates
- Fixed 3 JavaScript syntax errors in fg_scan.html (lines 951, 840-950, 1175-1215) - Restored form field validation with proper null safety checks - Re-enabled auto-advance between form fields - Re-enabled CP code auto-complete with hyphen detection - Updated validation error messages with clear format specifications and examples - Added autocomplete='off' to all input fields - Removed auto-prefix correction feature - Updated warehouse routes and modules for box assignment workflow - Added/improved database initialization scripts - Updated requirements.txt dependencies Format specifications implemented: - Operator Code: OP + 2 digits (example: OP01, OP99) - CP Code: CP + 8 digits + hyphen + 4 digits (example: CP00000000-0001) - OC1/OC2 Codes: OC + 2 digits (example: OC01, OC99) - Defect Code: 3 digits only
This commit is contained in:
@@ -3,6 +3,7 @@ Quality App v2 - Flask Application Factory
|
||||
Robust, modular application with login, dashboard, and multiple modules
|
||||
"""
|
||||
from flask import Flask
|
||||
from flask_session import Session
|
||||
from datetime import datetime, timedelta
|
||||
import os
|
||||
import logging
|
||||
@@ -35,6 +36,18 @@ def create_app(config=None):
|
||||
app.config['SESSION_COOKIE_SECURE'] = False # Set True in production with HTTPS
|
||||
app.config['SESSION_COOKIE_HTTPONLY'] = True
|
||||
app.config['SESSION_COOKIE_SAMESITE'] = 'Lax'
|
||||
app.config['SESSION_COOKIE_NAME'] = 'quality_app_session'
|
||||
app.config['SESSION_REFRESH_EACH_REQUEST'] = True
|
||||
|
||||
# Use filesystem for session storage (works with multiple gunicorn workers)
|
||||
sessions_dir = os.path.join(app.config.get('LOG_DIR', '/app/data/logs'), '..', 'sessions')
|
||||
os.makedirs(sessions_dir, exist_ok=True)
|
||||
app.config['SESSION_TYPE'] = 'filesystem'
|
||||
app.config['SESSION_FILE_DIR'] = sessions_dir
|
||||
app.config['SESSION_FILE_THRESHOLD'] = 500
|
||||
|
||||
# Initialize Flask-Session
|
||||
Session(app)
|
||||
|
||||
# Initialize database connection
|
||||
logger.info("Initializing database connection...")
|
||||
|
||||
@@ -57,7 +57,7 @@ def save_fg_scan(operator_code, cp_code, oc1_code, oc2_code, defect_code, date,
|
||||
time: Scan time
|
||||
|
||||
Returns:
|
||||
tuple: (success: bool, approved_count: int, rejected_count: int)
|
||||
tuple: (success: bool, scan_id: int, approved_count: int, rejected_count: int)
|
||||
"""
|
||||
try:
|
||||
from datetime import datetime
|
||||
@@ -79,6 +79,9 @@ def save_fg_scan(operator_code, cp_code, oc1_code, oc2_code, defect_code, date,
|
||||
cursor.execute(insert_query, (operator_code, cp_code, oc1_code, oc2_code, defect_code, date, time))
|
||||
db.commit()
|
||||
|
||||
# Get the ID of the inserted record
|
||||
scan_id = cursor.lastrowid
|
||||
|
||||
# Get the quantities from the table for feedback
|
||||
cursor.execute("""
|
||||
SELECT COUNT(*) as total_scans,
|
||||
@@ -91,8 +94,8 @@ def save_fg_scan(operator_code, cp_code, oc1_code, oc2_code, defect_code, date,
|
||||
approved_count = result[1] if result and result[1] else 0
|
||||
rejected_count = result[2] if result and result[2] else 0
|
||||
|
||||
logger.info(f"Scan saved successfully: {cp_code} by {operator_code}")
|
||||
return True, approved_count, rejected_count
|
||||
logger.info(f"Scan saved successfully: {cp_code} by {operator_code} with ID {scan_id}")
|
||||
return True, scan_id, approved_count, rejected_count
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error saving finish goods scan data: {e}")
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
Quality Module Routes
|
||||
"""
|
||||
from flask import Blueprint, render_template, session, redirect, url_for, request, jsonify, flash
|
||||
from app.database import get_db
|
||||
from app.modules.quality.quality import (
|
||||
ensure_scanfg_orders_table,
|
||||
save_fg_scan,
|
||||
@@ -11,6 +12,11 @@ from app.modules.quality.quality import (
|
||||
get_cp_statistics
|
||||
)
|
||||
import logging
|
||||
import base64
|
||||
from io import BytesIO
|
||||
from reportlab.lib.units import mm
|
||||
from reportlab.pdfgen import canvas
|
||||
from reportlab.graphics.barcode import code128
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -59,7 +65,7 @@ def fg_scan():
|
||||
|
||||
try:
|
||||
# Save the scan using business logic function
|
||||
success, approved_count, rejected_count = save_fg_scan(
|
||||
success, scan_id, approved_count, rejected_count = save_fg_scan(
|
||||
operator_code, cp_code, oc1_code, oc2_code, defect_code, date, time
|
||||
)
|
||||
|
||||
@@ -68,11 +74,12 @@ def fg_scan():
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error saving finish goods scan data: {e}")
|
||||
scan_id = None
|
||||
|
||||
# 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 AJAX requests, return JSON response with scan ID
|
||||
return jsonify({'success': True, 'message': 'Scan recorded successfully', 'scan_id': scan_id})
|
||||
|
||||
# For normal form submissions, redirect to prevent form resubmission (POST-Redirect-GET pattern)
|
||||
return redirect(url_for('quality.fg_scan'))
|
||||
@@ -171,3 +178,262 @@ def api_cp_stats(cp_code):
|
||||
'message': f'Error fetching CP statistics: {str(e)}'
|
||||
}), 500
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# QUICK BOX CHECKPOINT ROUTES - For "Scan To Boxes" Feature
|
||||
# ============================================================================
|
||||
|
||||
@quality_bp.route('/api/create-quick-box', methods=['POST'])
|
||||
def create_quick_box():
|
||||
"""Create a new box with auto-incremented number for quick box checkpoint and assign to FG_INCOMING"""
|
||||
if 'user_id' not in session:
|
||||
return jsonify({'error': 'Unauthorized'}), 401
|
||||
|
||||
try:
|
||||
conn = get_db()
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Get FG_INCOMING location ID
|
||||
cursor.execute("""
|
||||
SELECT id FROM warehouse_locations
|
||||
WHERE location_code = 'FG_INCOMING'
|
||||
LIMIT 1
|
||||
""")
|
||||
fg_incoming_result = cursor.fetchone()
|
||||
|
||||
if not fg_incoming_result:
|
||||
logger.error("FG_INCOMING location not found in database")
|
||||
return jsonify({'error': 'FG_INCOMING location not configured'}), 500
|
||||
|
||||
fg_incoming_id = fg_incoming_result[0]
|
||||
|
||||
# Get the next box number by finding max and incrementing
|
||||
cursor.execute("""
|
||||
SELECT MAX(CAST(SUBSTRING(box_number, 4) AS UNSIGNED))
|
||||
FROM boxes_crates
|
||||
WHERE box_number LIKE 'BOX%'
|
||||
""")
|
||||
result = cursor.fetchone()
|
||||
next_num = (result[0] if result[0] else 0) + 1
|
||||
box_number = f"BOX{str(next_num).zfill(8)}"
|
||||
|
||||
# Insert new box with FG_INCOMING location
|
||||
user_id = session.get('user_id')
|
||||
cursor.execute("""
|
||||
INSERT INTO boxes_crates (box_number, status, location_id, created_by, created_at)
|
||||
VALUES (%s, %s, %s, %s, NOW())
|
||||
""", (box_number, 'open', fg_incoming_id, user_id))
|
||||
|
||||
conn.commit()
|
||||
box_id = cursor.lastrowid
|
||||
|
||||
# Create initial location history entry
|
||||
cursor.execute("""
|
||||
INSERT INTO cp_location_history (cp_code, box_id, from_location_id, to_location_id, moved_by, reason)
|
||||
VALUES (%s, %s, %s, %s, %s, %s)
|
||||
""", (box_number, box_id, None, fg_incoming_id, user_id, 'Box created'))
|
||||
|
||||
conn.commit()
|
||||
cursor.close()
|
||||
|
||||
logger.info(f"Quick box created: {box_number} (ID: {box_id}) assigned to FG_INCOMING")
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'box_number': box_number,
|
||||
'box_id': box_id,
|
||||
'location': 'FG_INCOMING',
|
||||
'message': f'Box {box_number} created and assigned to FG_INCOMING location'
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error creating quick box: {e}")
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
|
||||
@quality_bp.route('/api/generate-box-label-pdf', methods=['POST'])
|
||||
def generate_box_label_pdf():
|
||||
"""Generate PDF label with barcode for printing via QZ Tray"""
|
||||
if 'user_id' not in session:
|
||||
return jsonify({'error': 'Unauthorized'}), 401
|
||||
|
||||
try:
|
||||
box_number = request.form.get('box_number', 'Unknown')
|
||||
|
||||
if not box_number or not box_number.startswith('BOX'):
|
||||
return jsonify({'error': 'Invalid box number'}), 400
|
||||
|
||||
# Create PDF with 8cm x 5cm (landscape)
|
||||
pdf_buffer = BytesIO()
|
||||
page_width = 80 * mm # 8 cm
|
||||
page_height = 50 * mm # 5 cm
|
||||
|
||||
c = canvas.Canvas(pdf_buffer, pagesize=(page_width, page_height))
|
||||
c.setPageCompression(1)
|
||||
c.setCreator("Quality App - Box Label System")
|
||||
|
||||
# Margins
|
||||
margin = 2 * mm
|
||||
usable_width = page_width - (2 * margin)
|
||||
usable_height = page_height - (2 * margin)
|
||||
|
||||
# Text section at top
|
||||
text_height = 12 * mm
|
||||
barcode_height = usable_height - text_height - (1 * mm)
|
||||
|
||||
# Draw text label
|
||||
text_y = page_height - margin - 8 * mm
|
||||
c.setFont("Helvetica-Bold", 12)
|
||||
c.drawString(margin, text_y, "BOX Nr:")
|
||||
|
||||
c.setFont("Courier-Bold", 14)
|
||||
c.drawString(margin + 18 * mm, text_y, box_number)
|
||||
|
||||
# Generate and draw barcode
|
||||
try:
|
||||
barcode_obj = code128.Code128(
|
||||
box_number,
|
||||
barWidth=0.5 * mm,
|
||||
barHeight=barcode_height - (2 * mm),
|
||||
humanReadable=False
|
||||
)
|
||||
|
||||
barcode_x = (page_width - barcode_obj.width) / 2
|
||||
barcode_y = margin + 2 * mm
|
||||
barcode_obj.drawOn(c, barcode_x, barcode_y)
|
||||
except Exception as e:
|
||||
logger.warning(f"Barcode generation warning: {e}")
|
||||
# Continue without barcode if generation fails
|
||||
|
||||
c.save()
|
||||
|
||||
# Convert to base64
|
||||
pdf_data = pdf_buffer.getvalue()
|
||||
pdf_base64 = base64.b64encode(pdf_data).decode('utf-8')
|
||||
|
||||
logger.info(f"Generated PDF label for box: {box_number}")
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'pdf_base64': pdf_base64,
|
||||
'box_number': box_number
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error generating box label PDF: {e}")
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
|
||||
@quality_bp.route('/api/assign-cp-to-box', methods=['POST'])
|
||||
def assign_cp_to_box():
|
||||
"""Assign CP code to box and update traceability"""
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
if 'user_id' not in session:
|
||||
logger.warning("Unauthorized assign_cp_to_box request")
|
||||
return jsonify({'error': 'Unauthorized'}), 401
|
||||
|
||||
try:
|
||||
data = request.get_json()
|
||||
if not data:
|
||||
logger.error("No JSON data in request")
|
||||
return jsonify({'error': 'No JSON data provided'}), 400
|
||||
|
||||
box_number = data.get('box_number', '').strip()
|
||||
scan_id = data.get('scan_id')
|
||||
cp_code = data.get('cp_code', '').strip() # Fallback for legacy requests
|
||||
quantity = data.get('quantity', 1)
|
||||
|
||||
logger.info(f"Assigning to box {box_number}, scan_id: {scan_id}, cp_code: {cp_code}, qty: {quantity}")
|
||||
|
||||
conn = get_db()
|
||||
cursor = conn.cursor()
|
||||
|
||||
# If scan_id is provided, fetch the CP code from the scan record
|
||||
if scan_id:
|
||||
cursor.execute("""
|
||||
SELECT CP_full_code FROM scanfg_orders
|
||||
WHERE id = %s
|
||||
""", (scan_id,))
|
||||
scan_result = cursor.fetchone()
|
||||
|
||||
if not scan_result:
|
||||
cursor.close()
|
||||
logger.error(f"Scan {scan_id} not found")
|
||||
return jsonify({'error': f'Scan {scan_id} not found'}), 404
|
||||
|
||||
cp_code = scan_result[0]
|
||||
logger.info(f"Retrieved CP code {cp_code} from scan {scan_id}")
|
||||
|
||||
if not box_number or not cp_code:
|
||||
cursor.close()
|
||||
logger.error(f"Missing required fields: box_number={box_number}, cp_code={cp_code}")
|
||||
return jsonify({'error': 'Missing box_number or cp_code'}), 400
|
||||
|
||||
# Get box ID and location
|
||||
cursor.execute("""
|
||||
SELECT id, location_id FROM boxes_crates
|
||||
WHERE box_number = %s
|
||||
""", (box_number,))
|
||||
box_result = cursor.fetchone()
|
||||
|
||||
if not box_result:
|
||||
cursor.close()
|
||||
logger.error(f"Box {box_number} not found")
|
||||
return jsonify({'error': f'Box {box_number} not found'}), 404
|
||||
|
||||
box_id, location_id = box_result[0], box_result[1]
|
||||
logger.info(f"Found box_id={box_id}, location_id={location_id}")
|
||||
|
||||
# Insert into box_contents
|
||||
cursor.execute("""
|
||||
INSERT INTO box_contents (box_id, cp_code, quantity, added_at)
|
||||
VALUES (%s, %s, %s, NOW())
|
||||
""", (box_id, cp_code, quantity))
|
||||
logger.info(f"Inserted into box_contents")
|
||||
|
||||
# Update scanfg_orders to link CP to box and location
|
||||
if scan_id:
|
||||
# If we have a scan_id, update that specific scan record
|
||||
cursor.execute("""
|
||||
UPDATE scanfg_orders
|
||||
SET box_id = %s, location_id = %s
|
||||
WHERE id = %s
|
||||
""", (box_id, location_id, scan_id))
|
||||
logger.info(f"Updated scanfg_orders scan {scan_id}")
|
||||
else:
|
||||
# Legacy behavior: update by CP code (last one)
|
||||
cursor.execute("""
|
||||
UPDATE scanfg_orders
|
||||
SET box_id = %s, location_id = %s
|
||||
WHERE CP_full_code = %s
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 1
|
||||
""", (box_id, location_id, cp_code))
|
||||
logger.info(f"Updated scanfg_orders for CP {cp_code}")
|
||||
|
||||
# Create location history entry
|
||||
user_id = session.get('user_id')
|
||||
cursor.execute("""
|
||||
INSERT INTO cp_location_history (cp_code, box_id, from_location_id, to_location_id, moved_by, reason)
|
||||
VALUES (%s, %s, %s, %s, %s, %s)
|
||||
""", (cp_code, box_id, None, location_id, user_id, 'Assigned to box'))
|
||||
logger.info(f"Created cp_location_history entry")
|
||||
|
||||
conn.commit()
|
||||
cursor.close()
|
||||
|
||||
logger.info(f"✅ CP {cp_code} successfully assigned to box {box_number} (qty: {quantity}) in location {location_id}")
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': f'CP {cp_code} assigned to box {box_number}',
|
||||
'cp_code': cp_code,
|
||||
'box_id': box_id,
|
||||
'box_number': box_number
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Error assigning CP to box: {str(e)}", exc_info=True)
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
|
||||
@@ -874,7 +874,12 @@ def get_database_tables():
|
||||
|
||||
@settings_bp.route('/api/database/truncate', methods=['POST'])
|
||||
def truncate_table():
|
||||
"""Truncate (clear) a database table"""
|
||||
"""Truncate (clear) a database table
|
||||
|
||||
Special handling for warehouse_locations table:
|
||||
- Preserves the 2 default locations: FG_INCOMING and TRUCK_LOADING
|
||||
- Deletes only user-created locations
|
||||
"""
|
||||
if 'user_id' not in session:
|
||||
return jsonify({'error': 'Unauthorized'}), 401
|
||||
|
||||
@@ -898,12 +903,30 @@ def truncate_table():
|
||||
cursor.close()
|
||||
return jsonify({'error': 'Table not found'}), 404
|
||||
|
||||
# Truncate the table
|
||||
cursor.execute(f'TRUNCATE TABLE {table}')
|
||||
conn.commit()
|
||||
cursor.close()
|
||||
|
||||
return jsonify({'success': True, 'message': f'Table {table} cleared successfully'})
|
||||
# Special handling for warehouse_locations table
|
||||
if table == 'warehouse_locations':
|
||||
# Delete all rows EXCEPT the 2 default locations
|
||||
cursor.execute("""
|
||||
DELETE FROM warehouse_locations
|
||||
WHERE location_code NOT IN ('FG_INCOMING', 'TRUCK_LOADING')
|
||||
""")
|
||||
conn.commit()
|
||||
deleted_count = cursor.rowcount
|
||||
cursor.close()
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': f'Table {table} cleared successfully ({deleted_count} rows deleted)',
|
||||
'preserved_count': 2,
|
||||
'preserved_locations': ['FG_INCOMING', 'TRUCK_LOADING']
|
||||
})
|
||||
else:
|
||||
# For all other tables, perform standard truncate
|
||||
cursor.execute(f'TRUNCATE TABLE {table}')
|
||||
conn.commit()
|
||||
cursor.close()
|
||||
|
||||
return jsonify({'success': True, 'message': f'Table {table} cleared successfully'})
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
@@ -4,7 +4,9 @@ Warehouse Module Routes
|
||||
from flask import Blueprint, render_template, session, redirect, url_for, request, flash, jsonify
|
||||
from app.modules.warehouse.warehouse import (
|
||||
get_all_locations, add_location, update_location, delete_location,
|
||||
delete_multiple_locations, get_location_by_id
|
||||
delete_multiple_locations, get_location_by_id,
|
||||
search_box_by_number, search_location_with_boxes,
|
||||
assign_box_to_location, move_box_to_new_location
|
||||
)
|
||||
import logging
|
||||
|
||||
@@ -123,3 +125,93 @@ def test_barcode():
|
||||
return redirect(url_for('main.login'))
|
||||
|
||||
return render_template('modules/warehouse/test_barcode.html')
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# API Routes for Set Boxes Locations Feature
|
||||
# ============================================================================
|
||||
|
||||
@warehouse_bp.route('/api/search-box', methods=['POST'], endpoint='api_search_box')
|
||||
def api_search_box():
|
||||
"""Search for a box by number"""
|
||||
if 'user_id' not in session:
|
||||
return jsonify({'success': False, 'error': 'Unauthorized'}), 401
|
||||
|
||||
data = request.get_json()
|
||||
box_number = data.get('box_number', '').strip()
|
||||
|
||||
if not box_number:
|
||||
return jsonify({'success': False, 'error': 'Box number is required'}), 400
|
||||
|
||||
success, box_data, status_code = search_box_by_number(box_number)
|
||||
|
||||
if success:
|
||||
return jsonify({'success': True, 'box': box_data}), 200
|
||||
else:
|
||||
return jsonify({'success': False, 'error': f'Box "{box_number}" not found'}), status_code
|
||||
|
||||
|
||||
@warehouse_bp.route('/api/search-location', methods=['POST'], endpoint='api_search_location')
|
||||
def api_search_location():
|
||||
"""Search for a location and get all boxes in it"""
|
||||
if 'user_id' not in session:
|
||||
return jsonify({'success': False, 'error': 'Unauthorized'}), 401
|
||||
|
||||
data = request.get_json()
|
||||
location_code = data.get('location_code', '').strip()
|
||||
|
||||
if not location_code:
|
||||
return jsonify({'success': False, 'error': 'Location code is required'}), 400
|
||||
|
||||
success, response_data, status_code = search_location_with_boxes(location_code)
|
||||
|
||||
if success:
|
||||
return jsonify({'success': True, **response_data}), 200
|
||||
else:
|
||||
return jsonify({'success': False, 'error': response_data.get('error', 'Not found')}), status_code
|
||||
|
||||
|
||||
@warehouse_bp.route('/api/assign-box-to-location', methods=['POST'], endpoint='api_assign_box_to_location')
|
||||
def api_assign_box_to_location():
|
||||
"""Assign a box to a location"""
|
||||
if 'user_id' not in session:
|
||||
return jsonify({'success': False, 'error': 'Unauthorized'}), 401
|
||||
|
||||
data = request.get_json()
|
||||
box_id = data.get('box_id')
|
||||
location_code = data.get('location_code', '').strip()
|
||||
|
||||
if not box_id or not location_code:
|
||||
return jsonify({'success': False, 'error': 'Box ID and location code are required'}), 400
|
||||
|
||||
success, message, status_code = assign_box_to_location(box_id, location_code)
|
||||
|
||||
return jsonify({'success': success, 'message': message}), status_code
|
||||
|
||||
|
||||
@warehouse_bp.route('/api/move-box-to-location', methods=['POST'], endpoint='api_move_box_to_location')
|
||||
def api_move_box_to_location():
|
||||
"""Move a box to a new location"""
|
||||
if 'user_id' not in session:
|
||||
return jsonify({'success': False, 'error': 'Unauthorized'}), 401
|
||||
|
||||
data = request.get_json()
|
||||
box_id = data.get('box_id')
|
||||
new_location_code = data.get('new_location_code', '').strip()
|
||||
|
||||
if not box_id or not new_location_code:
|
||||
return jsonify({'success': False, 'error': 'Box ID and new location code are required'}), 400
|
||||
|
||||
success, message, status_code = move_box_to_new_location(box_id, new_location_code)
|
||||
|
||||
return jsonify({'success': success, 'message': message}), status_code
|
||||
|
||||
|
||||
@warehouse_bp.route('/api/get-locations', methods=['GET'], endpoint='api_get_locations')
|
||||
def api_get_locations():
|
||||
"""Get all warehouse locations for dropdown"""
|
||||
if 'user_id' not in session:
|
||||
return jsonify({'success': False, 'error': 'Unauthorized'}), 401
|
||||
|
||||
locations = get_all_locations()
|
||||
return jsonify({'success': True, 'locations': locations}), 200
|
||||
|
||||
@@ -211,3 +211,227 @@ def delete_multiple_locations(location_ids):
|
||||
except Exception as e:
|
||||
logger.error(f"Error deleting multiple locations: {e}")
|
||||
return False, f"Error deleting locations: {str(e)}"
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Set Boxes Locations - Functions for assigning boxes to locations
|
||||
# ============================================================================
|
||||
|
||||
def search_box_by_number(box_number):
|
||||
"""Search for a box by its number
|
||||
|
||||
Returns:
|
||||
tuple: (success: bool, box_data: dict or None, status_code: int)
|
||||
"""
|
||||
try:
|
||||
if not box_number or not str(box_number).strip():
|
||||
return False, None, 400
|
||||
|
||||
conn = get_db()
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute("""
|
||||
SELECT
|
||||
b.id,
|
||||
b.box_number,
|
||||
b.status,
|
||||
b.location_id,
|
||||
COALESCE(l.location_code, 'Not assigned') as location_code,
|
||||
b.created_at
|
||||
FROM boxes_crates b
|
||||
LEFT JOIN warehouse_locations l ON b.location_id = l.id
|
||||
WHERE b.box_number = %s
|
||||
""", (str(box_number).strip(),))
|
||||
|
||||
result = cursor.fetchone()
|
||||
cursor.close()
|
||||
|
||||
if not result:
|
||||
return False, None, 404
|
||||
|
||||
box_data = {
|
||||
'id': result[0],
|
||||
'box_number': result[1],
|
||||
'status': result[2],
|
||||
'location_id': result[3],
|
||||
'location_code': result[4],
|
||||
'created_at': str(result[5])
|
||||
}
|
||||
|
||||
return True, box_data, 200
|
||||
except Exception as e:
|
||||
logger.error(f"Error searching box: {e}")
|
||||
return False, None, 500
|
||||
|
||||
|
||||
def search_location_with_boxes(location_code):
|
||||
"""Search for a location and get all boxes assigned to it
|
||||
|
||||
Returns:
|
||||
tuple: (success: bool, data: dict, status_code: int)
|
||||
"""
|
||||
try:
|
||||
if not location_code or not str(location_code).strip():
|
||||
return False, {}, 400
|
||||
|
||||
conn = get_db()
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Get location info
|
||||
cursor.execute("""
|
||||
SELECT id, location_code, size, description
|
||||
FROM warehouse_locations
|
||||
WHERE location_code = %s
|
||||
""", (str(location_code).strip(),))
|
||||
|
||||
location = cursor.fetchone()
|
||||
|
||||
if not location:
|
||||
cursor.close()
|
||||
return False, {'error': f'Location "{location_code}" not found'}, 404
|
||||
|
||||
location_id = location[0]
|
||||
|
||||
# Get all boxes in this location
|
||||
cursor.execute("""
|
||||
SELECT
|
||||
id,
|
||||
box_number,
|
||||
status,
|
||||
created_at
|
||||
FROM boxes_crates
|
||||
WHERE location_id = %s
|
||||
ORDER BY id DESC
|
||||
""", (location_id,))
|
||||
|
||||
boxes = cursor.fetchall()
|
||||
cursor.close()
|
||||
|
||||
location_data = {
|
||||
'id': location[0],
|
||||
'location_code': location[1],
|
||||
'size': location[2],
|
||||
'description': location[3]
|
||||
}
|
||||
|
||||
boxes_list = []
|
||||
for box in boxes:
|
||||
boxes_list.append({
|
||||
'id': box[0],
|
||||
'box_number': box[1],
|
||||
'status': box[2],
|
||||
'created_at': str(box[3])
|
||||
})
|
||||
|
||||
return True, {'location': location_data, 'boxes': boxes_list}, 200
|
||||
except Exception as e:
|
||||
logger.error(f"Error searching location: {e}")
|
||||
return False, {'error': str(e)}, 500
|
||||
|
||||
|
||||
def assign_box_to_location(box_id, location_code):
|
||||
"""Assign a box to a warehouse location
|
||||
|
||||
Returns:
|
||||
tuple: (success: bool, message: str, status_code: int)
|
||||
"""
|
||||
try:
|
||||
if not box_id or not location_code:
|
||||
return False, 'Box ID and location code are required', 400
|
||||
|
||||
conn = get_db()
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Check if location exists
|
||||
cursor.execute("""
|
||||
SELECT id FROM warehouse_locations
|
||||
WHERE location_code = %s
|
||||
""", (location_code,))
|
||||
|
||||
location = cursor.fetchone()
|
||||
|
||||
if not location:
|
||||
cursor.close()
|
||||
return False, f'Location "{location_code}" not found', 404
|
||||
|
||||
location_id = location[0]
|
||||
|
||||
# Get box info
|
||||
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
|
||||
|
||||
# Update box location
|
||||
cursor.execute("""
|
||||
UPDATE boxes_crates
|
||||
SET location_id = %s, updated_at = NOW()
|
||||
WHERE id = %s
|
||||
""", (location_id, box_id))
|
||||
|
||||
conn.commit()
|
||||
cursor.close()
|
||||
|
||||
return True, f'Box "{box[0]}" assigned to location "{location_code}"', 200
|
||||
except Exception as e:
|
||||
logger.error(f"Error assigning box to location: {e}")
|
||||
return False, f'Error: {str(e)}', 500
|
||||
|
||||
|
||||
def move_box_to_new_location(box_id, new_location_code):
|
||||
"""Move a box from current location to a new location
|
||||
|
||||
Returns:
|
||||
tuple: (success: bool, message: str, status_code: int)
|
||||
"""
|
||||
try:
|
||||
if not box_id or not new_location_code:
|
||||
return False, 'Box ID and new location code are required', 400
|
||||
|
||||
conn = get_db()
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Check if new location exists
|
||||
cursor.execute("""
|
||||
SELECT id FROM warehouse_locations
|
||||
WHERE location_code = %s
|
||||
""", (new_location_code,))
|
||||
|
||||
location = cursor.fetchone()
|
||||
|
||||
if not location:
|
||||
cursor.close()
|
||||
return False, f'Location "{new_location_code}" not found', 404
|
||||
|
||||
new_location_id = location[0]
|
||||
|
||||
# Get box info
|
||||
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
|
||||
|
||||
# Update box location
|
||||
cursor.execute("""
|
||||
UPDATE boxes_crates
|
||||
SET location_id = %s, updated_at = NOW()
|
||||
WHERE id = %s
|
||||
""", (new_location_id, box_id))
|
||||
|
||||
conn.commit()
|
||||
cursor.close()
|
||||
|
||||
return True, f'Box "{box[0]}" moved to location "{new_location_code}"', 200
|
||||
except Exception as e:
|
||||
logger.error(f"Error moving box: {e}")
|
||||
return False, f'Error: {str(e)}', 500
|
||||
|
||||
@@ -43,8 +43,10 @@ def login():
|
||||
session['email'] = user['email']
|
||||
session['role'] = user['role']
|
||||
session['full_name'] = user['full_name']
|
||||
session.modified = True # Force session to be saved
|
||||
|
||||
logger.info(f"User {username} logged in successfully")
|
||||
logger.debug(f"Session data set: user_id={user['id']}, username={username}")
|
||||
flash(f'Welcome, {user["full_name"]}!', 'success')
|
||||
|
||||
return redirect(url_for('main.dashboard'))
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -348,6 +348,11 @@
|
||||
<p class="mb-1"><strong>Name:</strong> <span id="truncate-table-name"></span></p>
|
||||
<p class="mb-0"><strong>Rows to Delete:</strong> <span id="truncate-row-count" class="badge bg-danger"></span></p>
|
||||
</div>
|
||||
|
||||
<div id="warehouse-locations-warning" style="display: none;" class="alert alert-warning mt-3 mb-0">
|
||||
<i class="fas fa-shield-alt"></i>
|
||||
<strong>Protected Data:</strong> The 2 default warehouse locations (<code>FG_INCOMING</code> and <code>TRUCK_LOADING</code>) will be automatically preserved and not deleted.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -564,6 +569,16 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
confirmTableName.textContent = table;
|
||||
}
|
||||
|
||||
// Show warehouse_locations protection warning
|
||||
const warehouseWarning = document.getElementById('warehouse-locations-warning');
|
||||
if (warehouseWarning) {
|
||||
if (table === 'warehouse_locations') {
|
||||
warehouseWarning.style.display = 'block';
|
||||
} else {
|
||||
warehouseWarning.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
// Enable the button
|
||||
truncateBtn.disabled = false;
|
||||
|
||||
@@ -577,6 +592,12 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
truncateInfo.style.display = 'none';
|
||||
}
|
||||
|
||||
// Hide warehouse warning
|
||||
const warehouseWarning = document.getElementById('warehouse-locations-warning');
|
||||
if (warehouseWarning) {
|
||||
warehouseWarning.style.display = 'none';
|
||||
}
|
||||
|
||||
truncateBtn.disabled = true;
|
||||
}
|
||||
});
|
||||
@@ -663,8 +684,15 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
const modal = bootstrap.Modal.getInstance(document.getElementById('confirmTruncateModal'));
|
||||
if (modal) modal.hide();
|
||||
|
||||
// Build success message
|
||||
let successMsg = 'Table cleared successfully!';
|
||||
if (data.preserved_count > 0) {
|
||||
successMsg += ` (${data.preserved_count} protected locations preserved)`;
|
||||
}
|
||||
successMsg += '\n\nRefreshing page...';
|
||||
|
||||
// Show success message
|
||||
alert('Table cleared successfully! Refreshing page...');
|
||||
alert(successMsg);
|
||||
|
||||
// Refresh the page after a short delay
|
||||
setTimeout(() => {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user