- 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
18 KiB
18 KiB
Quick Box Checkpoint - Complete Implementation Guide
Part 1: Backend API Routes
Add these routes to /srv/quality_app-v2/app/modules/quality/routes.py
Route 1: Create Quick Box
@quality_bp.route('/api/create-quick-box', methods=['POST'])
def create_quick_box():
"""Create a new box with auto-incremented number"""
if 'user_id' not in session:
return jsonify({'error': 'Unauthorized'}), 401
try:
conn = get_db()
cursor = conn.cursor()
# Get the next box number
cursor.execute("SELECT MAX(CAST(SUBSTRING(box_number, 4) AS UNSIGNED)) FROM boxes_crates")
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
user_id = session.get('user_id')
cursor.execute("""
INSERT INTO boxes_crates (box_number, status, created_by, created_at)
VALUES (%s, %s, %s, NOW())
""", (box_number, 'open', user_id))
conn.commit()
box_id = cursor.lastrowid
cursor.close()
logger.info(f"Quick box created: {box_number} (ID: {box_id})")
return jsonify({
'success': True,
'box_number': box_number,
'box_id': box_id
})
except Exception as e:
logger.error(f"Error creating quick box: {e}")
return jsonify({'error': str(e)}), 500
Route 2: Generate Box Label PDF
@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:
from io import BytesIO
from reportlab.lib.pagesizes import landscape
from reportlab.lib import colors
from reportlab.lib.units import mm
from reportlab.pdfgen import canvas
from reportlab.graphics.barcode import code128
import base64
box_number = request.form.get('box_number', 'Unknown')
# 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)
# 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
text_y = page_height - margin - 8 * mm
c.setFont("Helvetica-Bold", 14)
c.drawString(margin, text_y, "BOX Nr:")
c.setFont("Courier-Bold", 16)
c.drawString(margin + 20 * mm, text_y, box_number)
# Generate and draw barcode
barcode = code128.Code128(
box_number,
barWidth=0.5 * mm,
barHeight=barcode_height - (2 * mm),
humanReadable=False
)
barcode_x = (page_width - barcode.width) / 2
barcode_y = margin + 2 * mm
barcode.drawOn(c, barcode_x, barcode_y)
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
Route 3: Assign CP to Box
@quality_bp.route('/api/assign-cp-to-box', methods=['POST'])
def assign_cp_to_box():
"""Assign CP code to box"""
if 'user_id' not in session:
return jsonify({'error': 'Unauthorized'}), 401
try:
data = request.get_json()
box_number = data.get('box_number', '').strip()
cp_code = data.get('cp_code', '').strip()
quantity = data.get('quantity', 1)
if not box_number or not cp_code:
return jsonify({'error': 'Missing box_number or cp_code'}), 400
conn = get_db()
cursor = conn.cursor()
# Get box ID
cursor.execute("SELECT id FROM boxes_crates WHERE box_number = %s", (box_number,))
box_result = cursor.fetchone()
if not box_result:
cursor.close()
return jsonify({'error': f'Box {box_number} not found'}), 404
box_id = box_result[0]
# Insert into box_contents
cursor.execute("""
INSERT INTO box_contents (box_id, cp_code, quantity, created_at)
VALUES (%s, %s, %s, NOW())
""", (box_id, cp_code, quantity))
conn.commit()
cursor.close()
logger.info(f"CP {cp_code} assigned to box {box_number} (qty: {quantity})")
return jsonify({
'success': True,
'message': f'CP {cp_code} assigned to box {box_number}',
'box_id': box_id
})
except Exception as e:
logger.error(f"Error assigning CP to box: {e}")
return jsonify({'error': str(e)}), 500
Part 2: Frontend JavaScript Implementation
Replace the incomplete quickBoxLabel section in /srv/quality_app-v2/app/templates/modules/quality/fg_scan.html
Complete Quick Box Creation Function
// Quick box label creation - COMPLETE IMPLEMENTATION
document.getElementById('quickBoxLabel').addEventListener('click', async function() {
// Check if scan-to-boxes is enabled
if (!scanToBoxesEnabled) {
showNotification('⚠️ Please enable "Scan to Boxes" first', 'warning');
return;
}
// Get CP code
const cpCode = document.getElementById('cp_code').value.trim();
if (!cpCode) {
showNotification('⚠️ Please enter a CP code first', 'warning');
return;
}
try {
this.disabled = true;
this.textContent = '⏳ Creating...';
// Step 1: Create box in database
console.log('📦 Step 1: Creating new box...');
const createResponse = await fetch('{{ url_for("quality.create_quick_box") }}', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
},
body: JSON.stringify({})
});
if (!createResponse.ok) {
throw new Error(`Failed to create box: ${createResponse.statusText}`);
}
const createResult = await createResponse.json();
if (!createResult.success || !createResult.box_number) {
throw new Error(createResult.error || 'Failed to create box');
}
const boxNumber = createResult.box_number;
console.log('✅ Box created:', boxNumber);
showNotification(`✅ Box ${boxNumber} created`, 'success');
// Step 2: Generate PDF label
console.log('📄 Step 2: Generating PDF label...');
const pdfFormData = new FormData();
pdfFormData.append('box_number', boxNumber);
const pdfResponse = await fetch('{{ url_for("quality.generate_box_label_pdf") }}', {
method: 'POST',
body: pdfFormData
});
if (!pdfResponse.ok) {
throw new Error('Failed to generate PDF label');
}
const pdfResult = await pdfResponse.json();
if (!pdfResult.success) {
throw new Error(pdfResult.error || 'Failed to generate PDF');
}
console.log('✅ PDF generated');
// Step 3: Print label via QZ Tray
console.log('🖨️ Step 3: Printing label via QZ Tray...');
try {
// Check QZ Tray connection
if (!window.qz || !window.qz.websocket.isActive()) {
console.log('Attempting to connect to QZ Tray...');
await window.qz.websocket.connect();
}
// Get printers
const printers = await window.qz.printers.find();
if (printers.length === 0) {
throw new Error('No printers found');
}
// Get default or first available printer
let printer;
try {
printer = await window.qz.printers.getDefault();
} catch (e) {
printer = printers[0];
}
console.log('Using printer:', printer);
// Configure print job
const config = window.qz.configs.create(printer, {
scaleContent: false,
rasterize: false,
size: { width: 80, height: 50 },
units: 'mm',
margins: { top: 0, right: 0, bottom: 0, left: 0 }
});
// Prepare PDF data
const printData = [{
type: 'pdf',
format: 'base64',
data: pdfResult.pdf_base64
}];
// Print
await window.qz.print(config, printData);
console.log('✅ Label printed');
showNotification(`✅ Box ${boxNumber} created and label printed!`, 'success');
// Step 4: Show modal for CP assignment
console.log('📝 Step 4: Opening modal for CP assignment...');
currentCpCode = cpCode;
document.getElementById('boxNumber').value = boxNumber;
document.getElementById('boxQty').value = '1';
document.getElementById('boxAssignmentModal').style.display = 'flex';
document.getElementById('boxNumber').focus();
} catch (printError) {
console.warn('QZ Tray print failed, attempting browser print:', printError);
showNotification(
`⚠️ Box ${boxNumber} created but QZ Tray print failed.\n` +
`Please check if QZ Tray is running.\n\n` +
`Box will be ready for manual entry.`,
'warning'
);
// Still show modal for manual entry
currentCpCode = cpCode;
document.getElementById('boxNumber').value = boxNumber;
document.getElementById('boxQty').value = '1';
document.getElementById('boxAssignmentModal').style.display = 'flex';
}
} catch (error) {
console.error('❌ Error:', error);
showNotification(`❌ Error: ${error.message}`, 'error');
} finally {
// Re-enable button
this.disabled = false;
this.textContent = '📦 Quick Box Label Creation';
}
});
// Assign CP to Box button
document.getElementById('assignToBox').addEventListener('click', async function() {
const boxNumber = document.getElementById('boxNumber').value.trim();
const boxQty = document.getElementById('boxQty').value.trim();
if (!boxNumber) {
showNotification('⚠️ Please enter a box number', 'warning');
return;
}
if (!boxQty || isNaN(boxQty) || parseInt(boxQty) < 1) {
showNotification('⚠️ Please enter a valid quantity', 'warning');
return;
}
try {
this.disabled = true;
this.textContent = '⏳ Assigning...';
const response = await fetch('{{ url_for("quality.assign_cp_to_box") }}', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
},
body: JSON.stringify({
box_number: boxNumber,
cp_code: currentCpCode,
quantity: boxQty
})
});
if (!response.ok) {
throw new Error(`Server error: ${response.statusText}`);
}
const result = await response.json();
if (result.success) {
showNotification(
`✅ CP ${currentCpCode} assigned to box ${boxNumber}!`,
'success'
);
// Close modal
document.getElementById('boxAssignmentModal').style.display = 'none';
// Reset form
resetForm();
// Clear box inputs
document.getElementById('boxNumber').value = '';
document.getElementById('boxQty').value = '';
// Set focus back to operator code for next scan
document.getElementById('operator_code').focus();
} else {
showNotification(`❌ Error: ${result.error}`, 'error');
}
} catch (error) {
console.error('Error:', error);
showNotification(`❌ Error: ${error.message}`, 'error');
} finally {
this.disabled = false;
this.textContent = 'Assign';
}
});
Update Modal Structure (HTML)
Update the modal in /srv/quality_app-v2/app/templates/modules/quality/fg_scan.html:
<!-- Box Assignment Modal -->
<div id="boxAssignmentModal" class="box-modal" style="display: none;">
<div class="box-modal-content">
<div class="modal-header">
<h2>Assign CP to Box</h2>
<button type="button" class="modal-close" id="closeModal" onclick="document.getElementById('boxAssignmentModal').style.display = 'none';">×</button>
</div>
<div class="modal-body">
<p><strong>CP Code:</strong> <span id="modalCpCode" style="color: #0066cc; font-weight: bold;">-</span></p>
<label for="boxNumber" style="margin-top: 15px;">Box Number:</label>
<input type="text" id="boxNumber" placeholder="BOX00000001" readonly style="background: #f5f5f5;">
<label for="boxQty" style="margin-top: 10px;">Quantity:</label>
<input type="number" id="boxQty" placeholder="Enter quantity" min="1" value="1">
</div>
<div class="modal-footer">
<button type="button" class="btn-secondary" id="cancelModal" onclick="document.getElementById('boxAssignmentModal').style.display = 'none';">Cancel</button>
<button type="button" class="btn-submit" id="assignToBox">✅ Assign to Box</button>
</div>
</div>
</div>
Update QZ Tray Initialization
// Better QZ Tray initialization
function initializeQzTray() {
if (typeof qz === 'undefined') {
console.log('ℹ️ QZ Tray library not loaded');
return false;
}
try {
// Set signature (required for QZ Tray 2.0+)
qz.security.setSignaturePromise(function(toSign) {
return new Promise(function(resolve, reject) {
// For development/local use, allow unsigned
// In production, you would generate a real signature server-side
resolve();
});
});
// Try to connect
qz.websocket.connect()
.then(function() {
qzTrayReady = true;
console.log('✅ QZ Tray connected');
// Get available printers
return qz.printers.find();
})
.then(function(printers) {
console.log('Available printers:', printers);
if (printers.length > 0) {
console.log('✅ Found', printers.length, 'printer(s)');
} else {
console.warn('⚠️ No printers found');
}
})
.catch(function(err) {
console.log('ℹ️ QZ Tray not available:', err);
qzTrayReady = false;
});
return true;
} catch(err) {
console.log('ℹ️ QZ Tray initialization error:', err);
return false;
}
}
Part 3: Database Schema
Ensure these tables exist with proper structure:
CREATE TABLE IF NOT EXISTS boxes_crates (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
box_number VARCHAR(20) 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 INT,
FOREIGN KEY (location_id) REFERENCES warehouse_locations(id) ON DELETE SET NULL,
FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE SET NULL,
INDEX idx_box_number (box_number),
INDEX idx_status (status)
);
CREATE TABLE IF NOT EXISTS box_contents (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
box_id BIGINT NOT NULL,
cp_code VARCHAR(20) NOT NULL,
quantity INT DEFAULT 1,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (box_id) REFERENCES boxes_crates(id) ON DELETE CASCADE,
INDEX idx_box_id (box_id),
INDEX idx_cp_code (cp_code)
);
Testing Instructions
- Enable Feature: Check "Scan To Boxes" in FG Scan page
- Create Box: Click "Quick Box Label Creation"
- Verify:
- Box created with auto-incremented number
- Label printed or shows fallback
- Modal appears for CP assignment
- Box number populated automatically
- Assign CP: Enter quantity and click "Assign to Box"
- Verify Database:
SELECT * FROM boxes_crates WHERE created_at > DATE_SUB(NOW(), INTERVAL 1 HOUR); SELECT * FROM box_contents WHERE created_at > DATE_SUB(NOW(), INTERVAL 1 HOUR);
Error Handling
- QZ Tray not connected → Warning message, fallback to browser print
- Box creation fails → Error message with details
- PDF generation fails → Error message, still show modal
- CP assignment fails → Show error, keep modal open for retry