- 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
530 lines
18 KiB
Markdown
530 lines
18 KiB
Markdown
# 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';">×</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
|