Implement print labels module with PDF generation, QZ Tray integration, and theme support

- Migrate print_labels.html and print_lost_labels.html to standalone pages with header and theme toggle
- Implement dark/light theme support using data-theme attribute and CSS variables
- Add PDF generation endpoints for single and batch label printing
- Copy pdf_generator.py from original app with full label formatting (80mm x 105mm)
- Fix data response handling to correctly access data.orders from API endpoints
- Synchronize table text sizes across both print pages
- Remove help buttons from print pages
- Database column rename: data_livrara → data_livrare for consistency
- Update routes to use correct database column names
- Add 7 test orders to database (4 unprinted, 3 printed)
- Implement QZ Tray integration with PDF fallback for label printing
- All CSS uses theme variables for dark/light mode synchronization
This commit is contained in:
Quality App Developer
2026-02-04 23:57:51 +02:00
parent 572b5af570
commit e53e3acc8e
13 changed files with 6138 additions and 1822 deletions

View File

@@ -0,0 +1,182 @@
# Implementation Plan: PDF Generation for Print Functionality
## Current Status
The new Quality App v2 has:
✅ Labels module with routes and templates
✅ print_labels.html and print_lost_labels.html with UI
✅ Database schema (order_for_labels table)
✅ API endpoints for fetching orders
❌ PDF generation functionality (NEEDS IMPLEMENTATION)
`/labels/api/generate-pdf` endpoint
`/labels/api/generate-pdf/{order_id}/true` endpoint
## What Needs to Be Done
### 1. Create PDF Generator Module
**File**: `/srv/quality_app-v2/app/modules/labels/pdf_generator.py`
Copy from old app: `/srv/quality_app/py_app/app/pdf_generator.py`
Key classes:
- `LabelPDFGenerator` - Main PDF generation class
- Methods for single label and batch label generation
### 2. Create/Update Print Module Functions
**File**: `/srv/quality_app-v2/app/modules/labels/print_module.py`
Add to existing functions:
- `update_order_printed_status(order_id, status=1)` - Mark order as printed
- Import PDF generator functions
### 3. Add API Endpoints to Routes
**File**: `/srv/quality_app-v2/app/modules/labels/routes.py`
Add two new routes:
#### Endpoint 1: Single Label PDF (for QZ Tray)
```python
@labels_bp.route('/api/generate-pdf', methods=['POST'])
def api_generate_pdf():
"""Generate single label PDF for QZ Tray thermal printing"""
# Accept JSON with:
# - order_data: Complete order information
# - piece_number: Which label number (1, 2, 3, etc.)
# - total_pieces: Total quantity
# Return: PDF binary data
```
#### Endpoint 2: Batch PDF (for download)
```python
@labels_bp.route('/api/generate-pdf/<int:order_id>/true', methods=['POST'])
def api_generate_batch_pdf(order_id):
"""Generate all label PDFs for an order and mark as printed"""
# Fetch order from database
# Generate all labels in one PDF
# Mark order as printed (printed_labels = 1)
# Return: PDF binary data
```
### 4. Install Required Dependencies
The new app needs these Python packages:
```
reportlab>=3.6.0 (for PDF generation)
```
Check `/srv/quality_app-v2/requirements.txt` and add if needed.
---
## Implementation Details from Old App
### LabelPDFGenerator Class Structure
```python
class LabelPDFGenerator:
def __init__(self, paper_saving_mode=True):
self.label_width = 80mm
self.label_height = 105mm
# ... other dimensions
def generate_labels_pdf(self, order_data, quantity, printer_optimized=True):
# Generate multiple labels (one per page)
# Return BytesIO buffer
def generate_single_label_pdf(self, order_data, piece_number, total_pieces):
# Generate one label for QZ Tray
# Return BytesIO buffer
def _draw_label(self, canvas, order_data, sequential_number, current_num, total_qty):
# Draw all label elements on canvas
def _optimize_for_label_printer(self, canvas):
# Set printer optimization settings
```
### Label Data Structure
```python
order_data = {
'id': int,
'comanda_productie': str, # Production order (CP00000711)
'cod_articol': str, # Article code
'descr_com_prod': str, # Product description
'cantitate': int, # Quantity (total labels)
'com_achiz_client': str, # Customer PO
'nr_linie_com_client': str,# Customer PO line
'customer_name': str,
'customer_article_number': str,
'data_livrare': str/date, # Delivery date
'dimensiune': str, # Product size
'printed_labels': bool # Print status
}
```
### API Request/Response Format
#### Generate Single Label (QZ Tray)
```
POST /labels/api/generate-pdf
Content-Type: application/json
{
"comanda_productie": "CP00000711",
"cod_articol": "ART001",
"descr_com_prod": "Product Description",
"cantitate": 5,
"com_achiz_client": "PO2026001",
"nr_linie_com_client": "001",
"customer_name": "ACME Corp",
"customer_article_number": "ACME-001",
"data_livrare": "2026-02-11",
"dimensiune": "Standard",
"piece_number": 1,
"total_pieces": 5
}
Response: Binary PDF data (Content-Type: application/pdf)
```
#### Generate Batch PDF (Download)
```
POST /labels/api/generate-pdf/711/true
Response: Binary PDF data with all 5 labels
Side effect: Sets order.printed_labels = 1
```
---
## Files to Copy from Old App
1. **pdf_generator.py**
- Source: `/srv/quality_app/py_app/app/pdf_generator.py`
- Destination: `/srv/quality_app-v2/app/modules/labels/pdf_generator.py`
- Status: READY TO COPY
2. **Requirements Update**
- Add `reportlab>=3.6.0` to `/srv/quality_app-v2/requirements.txt`
---
## Next Steps
1. Copy pdf_generator.py from old app
2. Update requirements.txt with reportlab
3. Add two new API routes to routes.py
4. Update print_module.py with update_order_printed_status function
5. Test the endpoints with test data already inserted
---
## Testing Checklist
- [ ] Single label generation works
- [ ] Batch PDF generation works
- [ ] QZ Tray can print generated labels
- [ ] Order marked as printed after successful generation
- [ ] PDF dimensions correct (80mm x 105mm)
- [ ] Barcodes generate correctly
- [ ] All order data displays correctly on label
- [ ] Thermal printer optimization enabled

View File

@@ -0,0 +1,189 @@
# Old Quality App - Printing Module Summary
## Key Files
### 1. `/srv/quality_app/py_app/app/pdf_generator.py`
**Main PDF Generation Engine**
The `LabelPDFGenerator` class handles all label PDF generation:
- **Dimensions**: 80mm x 105mm thermal printer labels
- **Optimized for**: Epson TM-T20, Citizen CTS-310 thermal printers
#### Key Methods:
##### `generate_labels_pdf(order_data, quantity, printer_optimized=True)`
- Generates PDF with multiple labels based on quantity
- Creates sequential labels: CP00000711-001, CP00000711-002, etc.
- Returns BytesIO buffer with complete PDF
##### `generate_single_label_pdf(order_data, piece_number, total_pieces, printer_optimized=True)`
- Generates single label PDF for specific piece number
- Used by QZ Tray for direct thermal printing
- Returns BytesIO buffer with one label
##### `_draw_label(canvas, order_data, sequential_number, current_num, total_qty)`
**Label Layout:**
- **Row 1**: Company header (empty)
- **Row 2**: Customer name
- **Row 3**: Quantity ordered (left) | Right column split 40/60
- **Row 4**: Customer order (com_achiz_client - nr_linie_com_client)
- **Row 5**: Delivery date (data_livrare)
- **Row 6**: Product description (double height)
- **Row 7**: Size (dimensiune)
- **Row 8**: Article code (customer_article_number)
- **Row 9**: Production order (comanda_productie - sequential_number)
**Barcodes:**
- **Bottom**: CODE128 barcode with sequential number (CP00000711/001)
- **Right side**: CODE128 vertical barcode with customer order format
##### `_optimize_for_label_printer(canvas)`
- Sets 300 DPI resolution
- Enables compression
- Sets print scaling to 100% (no scaling)
- Adds PDF metadata for printer optimization
---
### 2. `/srv/quality_app/py_app/app/print_module.py`
**Database Data Retrieval**
#### Key Functions:
##### `get_unprinted_orders_data(limit=100)`
- Returns orders where `printed_labels != 1`
- Retrieves all fields needed for label generation
- Returns list of order dictionaries
##### `get_printed_orders_data(limit=100)`
- Returns orders where `printed_labels = 1`
- Used for "print lost labels" page
- Returns list of order dictionaries
##### `update_order_printed_status(order_id)`
- Updates `printed_labels = 1` for order
- Called after successful printing
---
### 3. API Routes in `/srv/quality_app/py_app/app/routes.py`
#### `/generate_label_pdf` (POST)
**Single Label Generation for QZ Tray**
```python
Accepts JSON:
{
"comanda_productie": "CP00000711",
"cantitate": 5,
"piece_number": 1,
"total_pieces": 5,
... (other order fields)
}
Returns: PDF binary data
Purpose: Generate single label for thermal printer via QZ Tray
```
#### `/generate_labels_pdf/<order_id>/true` (POST)
**Batch Label Generation for PDF Download**
```
Returns: Multi-page PDF with all labels for order
Purpose: Generate all labels for download/preview
Also marks order as printed (printed_labels = 1)
```
---
### 4. `/srv/quality_app/py_app/app/print_config.py`
**Print Service Configuration**
```python
WINDOWS_PRINT_SERVICE_URL = "http://192.168.1.XXX:8765"
PRINT_SERVICE_TIMEOUT = 30
PRINT_SERVICE_ENABLED = True
```
Note: Windows print service is configured but QZ Tray is the primary method.
---
## Label Printing Flow
### For Direct Printing (QZ Tray):
1. User selects order from table
2. Clicks "Print Labels" button
3. Frontend calls `/labels/api/generate-pdf` with order data and piece_number
4. Backend's `LabelPDFGenerator.generate_single_label_pdf()` creates PDF
5. Returns PDF binary to frontend
6. QZ Tray prints directly to thermal printer
7. After successful print, calls `/labels/api/update-printed-status/{id}`
### For PDF Download:
1. User selects order
2. Clicks "Generate PDF" button
3. Frontend calls `/labels/api/generate-pdf/{orderId}/true`
4. Backend's `LabelPDFGenerator.generate_labels_pdf()` creates multi-page PDF
5. Returns PDF for browser download/print
6. Order marked as printed
---
## Key Configuration Classes
### `LabelPDFGenerator` Parameters:
```python
# Label dimensions
label_width = 80mm
label_height = 105mm
# Content area
content_width = 60mm
content_height = 68mm
content_x = 4mm from left
content_y = 22mm from bottom
# Layout
row_height = 6.8mm (content_height / 10)
left_column_width = 40% (24mm)
right_column_width = 60% (36mm)
# Barcode areas
bottom_barcode: 80mm width x 12mm height
vertical_barcode: 12mm width x 68mm height (rotated 90°)
```
---
## Order Data Fields Used
```python
Required fields for label generation:
- comanda_productie: Production order number
- cod_articol: Article code
- descr_com_prod: Product description
- cantitate: Quantity
- com_achiz_client: Customer purchase order
- nr_linie_com_client: Customer order line
- customer_name: Customer name
- customer_article_number: Customer article number
- data_livrare: Delivery date
- dimensiune: Product size
- printed_labels: Status (0 = unprinted, 1 = printed)
```
---
## Thermal Printer Optimization
The PDF generator is specifically optimized for thermal label printers:
1. **No margins**: Fills entire label area
2. **High resolution**: 300 DPI quality
3. **Monochrome**: Black/white only (no colors)
4. **Compression**: Reduces file size
5. **Scaling**: 100% (no scaling in printer)
6. **Bar width**: Optimized for thermal resolution
Tested with:
- Epson TM-T20 (80mm thermal printer)
- Citizen CTS-310 (80mm thermal printer)

View File

@@ -0,0 +1,219 @@
#!/usr/bin/env python3
"""
Insert test data into the order_for_labels table for testing print functionality
"""
import pymysql
from datetime import datetime, timedelta
import sys
# Database connection parameters
DB_HOST = 'mariadb'
DB_PORT = 3306
DB_USER = 'quality_user'
DB_PASSWORD = 'quality_secure_password_2026'
DB_NAME = 'quality_db'
def insert_test_data():
"""Insert test orders into the database"""
try:
# Connect to database
conn = pymysql.connect(
host=DB_HOST,
port=DB_PORT,
user=DB_USER,
password=DB_PASSWORD,
database=DB_NAME,
charset='utf8mb4'
)
cursor = conn.cursor()
# Check if table exists
cursor.execute("SHOW TABLES LIKE 'order_for_labels'")
if not cursor.fetchone():
print("Error: order_for_labels table does not exist")
cursor.close()
conn.close()
return False
# Check if printed_labels column exists
cursor.execute("SHOW COLUMNS FROM order_for_labels LIKE 'printed_labels'")
if not cursor.fetchone():
print("Error: printed_labels column does not exist")
cursor.close()
conn.close()
return False
# Sample test data
today = datetime.now()
delivery_date = today + timedelta(days=7)
test_orders = [
{
'comanda_productie': 'CP00000711',
'cod_articol': 'ART001',
'descr_com_prod': 'Memory Foam Pillow - Premium Quality',
'cantitate': 5,
'com_achiz_client': 'PO2026001',
'nr_linie_com_client': '001',
'customer_name': 'ACME Corporation',
'customer_article_number': 'ACME-MFP-001',
'open_for_order': 1,
'line_number': 1,
'data_livrara': delivery_date.date(),
'dimensiune': 'Standard (50x70cm)',
'printed_labels': 0
},
{
'comanda_productie': 'CP00000712',
'cod_articol': 'ART002',
'descr_com_prod': 'Gel Infused Mattress Topper',
'cantitate': 10,
'com_achiz_client': 'PO2026002',
'nr_linie_com_client': '001',
'customer_name': 'Sleep Solutions Ltd',
'customer_article_number': 'SS-GIM-002',
'open_for_order': 1,
'line_number': 1,
'data_livrara': delivery_date.date(),
'dimensiune': 'Double (140x200cm)',
'printed_labels': 0
},
{
'comanda_productie': 'CP00000713',
'cod_articol': 'ART003',
'descr_com_prod': 'Cooling Gel Pillow - Twin Pack',
'cantitate': 8,
'com_achiz_client': 'PO2026003',
'nr_linie_com_client': '002',
'customer_name': 'Bedding Warehouse',
'customer_article_number': 'BW-CGP-003',
'open_for_order': 1,
'line_number': 2,
'data_livrara': delivery_date.date(),
'dimensiune': 'King (200x200cm)',
'printed_labels': 1
},
{
'comanda_productie': 'CP00000714',
'cod_articol': 'ART004',
'descr_com_prod': 'Hypoallergenic Pillow Insert',
'cantitate': 15,
'com_achiz_client': 'PO2026004',
'nr_linie_com_client': '001',
'customer_name': 'Health Products Inc',
'customer_article_number': 'HPI-HYP-004',
'open_for_order': 0,
'line_number': 1,
'data_livrara': delivery_date.date(),
'dimensiune': 'Standard (50x70cm)',
'printed_labels': 0
},
{
'comanda_productie': 'CP00000715',
'cod_articol': 'ART005',
'descr_com_prod': 'Memory Foam Mattress 10CM',
'cantitate': 3,
'com_achiz_client': 'PO2026005',
'nr_linie_com_client': '001',
'customer_name': 'Premium Comfort Ltd',
'customer_article_number': 'PC-MFM-005',
'open_for_order': 1,
'line_number': 1,
'data_livrara': delivery_date.date(),
'dimensiune': 'Queen (160x200cm)',
'printed_labels': 1
},
{
'comanda_productie': 'CP00000716',
'cod_articol': 'ART006',
'descr_com_prod': 'Latex Pillow - Eco Friendly',
'cantitate': 12,
'com_achiz_client': 'PO2026006',
'nr_linie_com_client': '003',
'customer_name': 'Sustainable Sleep',
'customer_article_number': 'SS-LAT-006',
'open_for_order': 1,
'line_number': 3,
'data_livrara': delivery_date.date(),
'dimensiune': 'Standard (50x70cm)',
'printed_labels': 0
},
{
'comanda_productie': 'CP00000717',
'cod_articol': 'ART007',
'descr_com_prod': 'Body Pillow with Cover',
'cantitate': 6,
'com_achiz_client': 'PO2026007',
'nr_linie_com_client': '001',
'customer_name': 'Comfort Essentials',
'customer_article_number': 'CE-BP-007',
'open_for_order': 1,
'line_number': 1,
'data_livrara': delivery_date.date(),
'dimensiune': 'Long (40x150cm)',
'printed_labels': 1
}
]
# Insert test data
insert_query = """
INSERT INTO order_for_labels
(comanda_productie, cod_articol, descr_com_prod, cantitate,
com_achiz_client, nr_linie_com_client, customer_name, customer_article_number,
open_for_order, line_number, data_livrara, dimensiune, printed_labels,
created_at, updated_at)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, NOW(), NOW())
"""
inserted = 0
for order in test_orders:
try:
cursor.execute(insert_query, (
order['comanda_productie'],
order['cod_articol'],
order['descr_com_prod'],
order['cantitate'],
order['com_achiz_client'],
order['nr_linie_com_client'],
order['customer_name'],
order['customer_article_number'],
order['open_for_order'],
order['line_number'],
order['data_livrara'],
order['dimensiune'],
order['printed_labels']
))
inserted += 1
print(f"✓ Inserted: {order['comanda_productie']} - {order['descr_com_prod'][:40]}")
except Exception as e:
print(f"✗ Failed to insert {order['comanda_productie']}: {e}")
# Commit the transaction
conn.commit()
# Show summary
cursor.execute("SELECT COUNT(*) FROM order_for_labels")
total_orders = cursor.fetchone()[0]
print(f"\n{'='*60}")
print(f"Test data insertion completed!")
print(f"Inserted: {inserted} new orders")
print(f"Total orders in database: {total_orders}")
print(f"{'='*60}")
cursor.close()
conn.close()
return True
except pymysql.Error as e:
print(f"Database error: {e}")
return False
except Exception as e:
print(f"Unexpected error: {e}")
return False
if __name__ == '__main__':
success = insert_test_data()
sys.exit(0 if success else 1)