Files
quality_app-v2/documentation/BOXES_IMPLEMENTATION_DETAILS.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

568 lines
17 KiB
Markdown

# Quick Box Creation & Printing Implementation Details
## Overview
The "Quick Box Creation" feature allows users to quickly create boxes and assign finish goods (FG) CP codes to them directly from the FG scan page, with automatic box label printing support.
---
## 1. Database Tables
### boxes_crates Table
**Location:** `warehouse.py` (lines 32-45)
```sql
CREATE TABLE IF NOT EXISTS boxes_crates (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
box_number VARCHAR(8) 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 VARCHAR(100),
FOREIGN KEY (location_id) REFERENCES warehouse_locations(id) ON DELETE SET NULL
)
```
**Fields:**
- `box_number`: 8-digit unique identifier (00000001, 00000002, etc.)
- `status`: 'open' (receiving items) or 'closed' (ready for warehouse)
- `location_id`: References warehouse location (nullable)
- `created_by`: Username of operator who created the box
- `created_at/updated_at`: Timestamps
### box_contents Table
**Location:** `warehouse.py` (lines 47-62)
```sql
CREATE TABLE IF NOT EXISTS box_contents (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
box_id BIGINT NOT NULL,
cp_code VARCHAR(15) NOT NULL,
scan_id BIGINT,
scanned_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
scanned_by VARCHAR(100),
FOREIGN KEY (box_id) REFERENCES boxes_crates(id) ON DELETE CASCADE,
INDEX idx_box_id (box_id),
INDEX idx_cp_code (cp_code)
)
```
**Fields:**
- `box_id`: References the box this CP code is in
- `cp_code`: The finish good CP code being scanned
- `scanned_by`: Username of operator who scanned
- `scanned_at`: When the CP code was added to the box
---
## 2. Frontend Implementation
### FG Scan Page (`fg_scan.html`)
**Location:** `/srv/quality_app/py_app/app/templates/fg_scan.html`
#### Key Global Variables (Lines 10-14)
```javascript
let scanToBoxesEnabled = false;
let currentCpCode = null;
// Functions defined at global scope for accessibility
async function submitScanWithBoxAssignment() { ... }
function showBoxModal(cpCode) { ... }
async function assignCpToBox(boxNumber) { ... }
function showNotification(message, type = 'info') { ... }
```
#### Toggle Control
- Checkbox ID: `scan-to-boxes-toggle`
- Persists state in localStorage: `scan_to_boxes_enabled`
- When enabled: Allows QZ Tray connection for direct label printing
#### Complete Workflow (Lines 19-84)
**Step 1: Form Submission with Box Assignment**
```javascript
async function submitScanWithBoxAssignment() {
const form = document.getElementById('fg-scan-form');
const formData = new FormData(form);
// Submit scan to server
const response = await fetch(window.location.href, {
method: 'POST',
headers: {'X-Requested-With': 'XMLHttpRequest'},
body: formData
});
if (response.ok) {
currentCpCode = formData.get('cp_code');
const defectCode = formData.get('defect_code') || '000';
// Only show modal for approved items (defect code 000 or 0)
if (defectCode === '000' || defectCode === '0') {
showBoxModal(currentCpCode);
} else {
// Reload page for defective items
setTimeout(() => window.location.reload(), 1000);
}
// Clear form for next scan
document.getElementById('cp_code').value = '';
// ... clear other fields
}
}
```
**Step 2: Show Box Modal**
```javascript
function showBoxModal(cpCode) {
document.getElementById('modal-cp-code').textContent = cpCode;
document.getElementById('box-assignment-modal').style.display = 'block';
document.getElementById('scan-box-input').value = '';
document.getElementById('scan-box-input').focus();
}
```
**Step 3: Assign CP to Box via API**
```javascript
async function assignCpToBox(boxNumber) {
const response = await fetch('/warehouse/assign_cp_to_box', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
box_number: boxNumber,
cp_code: currentCpCode
})
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.error || 'Failed to assign CP to box');
}
return await response.json();
}
```
---
## 3. Backend Implementation
### Routes
#### FG Scan Route
**Location:** `/srv/quality_app/py_app/app/routes.py` (lines 1020-1090)
```python
@bp.route('/fg_scan', methods=['GET', 'POST'])
@requires_quality_module
def fg_scan():
ensure_scanfg_orders_table()
if request.method == 'POST':
operator_code = request.form.get('operator_code')
cp_code = request.form.get('cp_code')
oc1_code = request.form.get('oc1_code')
oc2_code = request.form.get('oc2_code')
defect_code = request.form.get('defect_code')
date = request.form.get('date')
time = request.form.get('time')
# Insert scan record
with db_connection_context() as conn:
cursor = conn.cursor()
insert_query = """
INSERT INTO scanfg_orders
(operator_code, CP_full_code, OC1_code, OC2_code, quality_code, date, time)
VALUES (%s, %s, %s, %s, %s, %s, %s)
"""
cursor.execute(insert_query,
(operator_code, cp_code, oc1_code, oc2_code, defect_code, date, time))
conn.commit()
# Handle AJAX requests for scan-to-boxes feature
if request.headers.get('X-Requested-With') == 'XMLHttpRequest' or \
request.accept_mimetypes.best == 'application/json':
return jsonify({'success': True, 'message': 'Scan recorded successfully'})
# Standard form submission
return redirect(url_for('main.fg_scan'))
```
**Key Points:**
- Accepts AJAX requests for scan-to-boxes feature
- Returns JSON for AJAX, redirects for normal form submission
- Only quality_module permission required
#### Assign CP to Box Route
**Location:** `/srv/quality_app/py_app/app/warehouse.py` (lines 619-658)
```python
def assign_cp_to_box_handler():
"""Handle assigning CP code to a box"""
from flask import request, jsonify, session
import json
try:
ensure_box_contents_table()
data = json.loads(request.data)
box_number = data.get('box_number')
cp_code = data.get('cp_code')
scanned_by = session.get('user', 'Unknown')
if not box_number or not cp_code:
return jsonify({'success': False, 'error': 'Missing box_number or cp_code'}), 400
conn = get_db_connection()
cursor = conn.cursor()
# Find box by number
cursor.execute("SELECT id FROM boxes_crates WHERE box_number = %s", (box_number,))
box = cursor.fetchone()
if not box:
conn.close()
return jsonify({'success': False, 'error': f'Box {box_number} not found'}), 404
box_id = box[0]
# Insert into box_contents
cursor.execute("""
INSERT INTO box_contents (box_id, cp_code, scanned_by)
VALUES (%s, %s, %s)
""", (box_id, cp_code, scanned_by))
conn.commit()
conn.close()
return jsonify({
'success': True,
'message': f'CP {cp_code} assigned to box {box_number}'
}), 200
```
### Box Management Functions
#### Generate Box Number
**Location:** `warehouse.py` (lines 155-165)
```python
def generate_box_number():
"""Generate next box number with 8 digits (00000001, 00000002, etc.)"""
conn = get_db_connection()
cursor = conn.cursor()
cursor.execute("SELECT MAX(CAST(box_number AS UNSIGNED)) FROM boxes_crates")
result = cursor.fetchone()
conn.close()
if result and result[0]:
next_number = int(result[0]) + 1
else:
next_number = 1
return str(next_number).zfill(8)
```
#### Add Box
**Location:** `warehouse.py` (lines 167-183)
```python
def add_box(location_id=None, created_by=None):
"""Add a new box/crate"""
conn = get_db_connection()
cursor = conn.cursor()
box_number = generate_box_number()
try:
cursor.execute(
"INSERT INTO boxes_crates (box_number, status, location_id, created_by) VALUES (%s, %s, %s, %s)",
(box_number, 'open', location_id if location_id else None, created_by)
)
conn.commit()
conn.close()
return f"Box {box_number} created successfully"
except Exception as e:
conn.close()
return f"Error creating box: {e}"
```
#### Search Box by Number
**Location:** `warehouse.py` (lines 740-785)
```python
def search_box_by_number(box_number):
"""Search for a box by box number and return its details including location"""
try:
if not box_number:
return False, {'message': 'Box number is required'}, 400
conn = get_db_connection()
cursor = conn.cursor()
cursor.execute("""
SELECT
b.id,
b.box_number,
b.status,
b.location_id,
w.location_code
FROM boxes_crates b
LEFT JOIN warehouse_locations w ON b.location_id = w.id
WHERE b.box_number = %s
""", (box_number,))
result = cursor.fetchone()
conn.close()
if result:
return True, {
'box': {
'id': result[0],
'box_number': result[1],
'status': result[2],
'location_id': result[3],
'location_code': result[4]
}
}, 200
else:
return False, {'message': f'Box "{box_number}" not found in the system'}, 404
```
---
## 4. Complete Workflow
### From FG Scan to Box Label Printing
```
1. USER SCANS CP CODE
2. FG SCAN FORM SUBMISSION (AJAX)
- POST /fg_scan with scan data
- Quality code must be 000 (approved)
3. SCAN RECORDED IN DATABASE
- Inserted into scanfg_orders table
- Quantities calculated by trigger
4. BOX MODAL DISPLAYED
- Shows CP code that was just scanned
- Focuses on box number input
5. USER ENTERS BOX NUMBER
- Can scan or type existing box number
- Or create new box (if enabled)
6. CP ASSIGNED TO BOX
- POST /warehouse/assign_cp_to_box
- Data inserted into box_contents table
- Records: box_id, cp_code, scanned_by, timestamp
7. BOX LABEL PRINTED (if enabled)
- QZ Tray connects to printer
- Box label PDF generated
- Sent to printer
8. READY FOR NEXT SCAN
- Modal closes
- Form cleared
- Focus returns to CP code input
```
---
## 5. Data Inserted into Boxes Table
### When Creating a Box
**boxes_crates table:**
```sql
INSERT INTO boxes_crates (box_number, status, location_id, created_by)
VALUES ('00000001', 'open', NULL, 'operator_username');
```
**Data Details:**
- `box_number`: Auto-generated (00000001, 00000002, etc.)
- `status`: Always starts as 'open' (can scan items into it)
- `location_id`: NULL initially (assigned later when moved to warehouse location)
- `created_at`: CURRENT_TIMESTAMP (automatic)
- `updated_at`: CURRENT_TIMESTAMP (automatic)
- `created_by`: Session username
### When Assigning CP Code to Box
**box_contents table:**
```sql
INSERT INTO box_contents (box_id, cp_code, scanned_by)
VALUES (1, 'CP12345678-0001', 'operator_username');
```
**Data Details:**
- `box_id`: Foreign key to boxes_crates.id
- `cp_code`: The CP code from the scan
- `scan_id`: NULL (optional, could link to scanfg_orders.id)
- `scanned_by`: Session username
- `scanned_at`: CURRENT_TIMESTAMP (automatic)
---
## 6. Box Label Printing Solution
### QZ Tray Integration
**Location:** `/srv/quality_app/py_app/app/templates/fg_scan.html` (lines 7-9)
```html
<!-- QZ Tray for printing - using local patched version for pairing-key authentication -->
<script src="{{ url_for('static', filename='qz-tray.js') }}"></script>
```
**Features:**
- Local patched version for pairing-key authentication
- Connects when scan-to-boxes toggle is enabled
- Handles direct printer communication
- Supports page-by-page printing
### Label Generation
**Box Label PDF Structure:**
- Location: `warehouse.py` (lines 220-400)
- Page Size: 8cm x 5cm landscape
- Content: Box number as text + barcode
- Uses ReportLab for PDF generation
```python
from reportlab.lib.pagesizes import landscape
from reportlab.pdfgen import canvas
from reportlab.graphics.barcode import code128
from reportlab.lib.units import mm
# 8cm x 5cm landscape label
page_width = 80 * mm
page_height = 50 * mm
# Barcode generation
barcode = code128.Code128(
box_number,
barWidth=0.4*mm,
barHeight=barcode_height,
humanReadable=True,
fontSize=10
)
```
---
## 7. API Endpoints
### Warehouse Module Routes
**Location:** `/srv/quality_app/py_app/app/routes.py` (lines 5657-5717)
```python
@bp.route('/api/warehouse/box/search', methods=['POST'])
@requires_warehouse_module
def api_search_box():
"""Search for a box by box number"""
data = request.get_json()
box_number = data.get('box_number', '').strip()
success, response_data, status_code = search_box_by_number(box_number)
return jsonify({'success': success, **response_data}), status_code
@bp.route('/api/warehouse/box/assign-location', methods=['POST'])
@requires_warehouse_module
def api_assign_box_to_location():
"""Assign a box to a warehouse location"""
data = request.get_json()
box_id = data.get('box_id')
location_code = data.get('location_code', '').strip()
success, response_data, status_code = assign_box_to_location(box_id, location_code)
return jsonify({'success': success, **response_data}), status_code
@bp.route('/api/warehouse/box/change-status', methods=['POST'])
@requires_warehouse_module
def api_change_box_status():
"""Change the status of a box (open/closed)"""
data = request.get_json()
box_id = data.get('box_id')
new_status = data.get('new_status', '').strip()
success, response_data, status_code = change_box_status(box_id, new_status)
return jsonify({'success': success, **response_data}), status_code
@bp.route('/api/warehouse/location/search', methods=['POST'])
@requires_warehouse_module
def api_search_location():
"""Search for a location and get all boxes in it"""
data = request.get_json()
location_code = data.get('location_code', '').strip()
success, response_data, status_code = search_location_with_boxes(location_code)
return jsonify({'success': success, **response_data}), status_code
@bp.route('/api/warehouse/box/move-location', methods=['POST'])
@requires_warehouse_module
def api_move_box_to_location():
"""Move a box from one location to another"""
data = request.get_json()
box_id = data.get('box_id')
new_location_code = data.get('new_location_code', '').strip()
success, response_data, status_code = move_box_to_new_location(box_id, new_location_code)
return jsonify({'success': success, **response_data}), status_code
```
---
## 8. File Locations Summary
| Component | File Path | Lines |
|-----------|-----------|-------|
| Database Tables | `warehouse.py` | 32-62 |
| Box Functions | `warehouse.py` | 155-183, 185-210, 212-232, 234-264, 266-284 |
| Assign CP to Box | `warehouse.py` | 619-658 |
| Search/Assign/Move Functions | `warehouse.py` | 740-980 |
| FG Scan Route | `routes.py` | 1020-1090 |
| Warehouse API Routes | `routes.py` | 5657-5717 |
| Frontend JS | `fg_scan.html` | 10-200 |
| QZ Tray Script | `fg_scan.html` | 7-9 |
---
## 9. Key Implementation Notes
### Quality Control
- Only **approved items** (quality_code = 000) trigger box modal
- Rejected items reload page instead
- Prevents mixing defective items in boxes
### Auto-increment Box Numbers
- 8-digit zero-padded format (00000001, 00000002)
- Automatic generation on box creation
- Ensures unique, scannable identifiers
### Session Management
- Operator username tracked in `created_by` and `scanned_by` fields
- Enables full audit trail of who created and modified boxes
### Toggle for Feature
- localStorage persistence for scan-to-boxes setting
- Separate from checkbox state on page refresh
- QZ Tray only connects when enabled
### Error Handling
- AJAX error notifications to user
- Graceful fallbacks for printer failures
- Database transaction rollback on errors
---
## 10. Integration with New App
To implement in the new app (/srv/quality_app-v2):
1. **Copy database table schemas** from warehouse.py
2. **Implement warehouse module** in models
3. **Add FG scan route** with AJAX support
4. **Create box assignment API** endpoint
5. **Add QZ Tray integration** to frontend
6. **Implement box label generation** for PDFs
7. **Set up permissions** for quality and warehouse modules
The implementation is modular and can be adapted to the new Flask structure.