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:
567
documentation/BOXES_IMPLEMENTATION_DETAILS.md
Normal file
567
documentation/BOXES_IMPLEMENTATION_DETAILS.md
Normal file
@@ -0,0 +1,567 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user