Files
quality_app-v2/documentation/QUICK_BOX_CHECKPOINT_IMPLEMENTATION_COMPLETE.md
Quality App Developer b15cc93b9d 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
2026-01-30 10:50:06 +02:00

530 lines
18 KiB
Markdown
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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
```python
@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
```python
@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
```python
@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
```javascript
// 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`:
```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';">&times;</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
```javascript
// 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:
```sql
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
1. **Enable Feature**: Check "Scan To Boxes" in FG Scan page
2. **Create Box**: Click "Quick Box Label Creation"
3. **Verify**:
- Box created with auto-incremented number
- Label printed or shows fallback
- Modal appears for CP assignment
- Box number populated automatically
4. **Assign CP**: Enter quantity and click "Assign to Box"
5. **Verify Database**:
```sql
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