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:
Quality App Developer
2026-01-30 10:50:06 +02:00
parent ac24e20fe1
commit b15cc93b9d
48 changed files with 16452 additions and 607 deletions

View File

@@ -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}")

View File

@@ -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