Implement professional PDF-based label printing for boxes and locations

- Replace ZPL/image-based printing with ReportLab PDF generation
- Box labels: 8cm x 5cm landscape with 'BOX Nr:' header and CODE128 barcode
- Location labels: 8cm x 5cm landscape with 'Location nr:' header and CODE128 barcode
- Add /generate_box_label_pdf endpoint using same approach as print_module
- Update FG scan quick box creation to use PDF printing with default printer
- Switch from CDN QZ Tray to local patched version for pairing-key auth
- Improve error handling and logging throughout printing workflow
- Fix import issues (add mm unit to warehouse.py)
- Optimize barcode size and spacing for better readability
This commit is contained in:
Quality App System
2025-12-26 22:28:29 +02:00
parent d4283098a6
commit 9a2e21796e
5 changed files with 375 additions and 63 deletions

View File

@@ -3710,6 +3710,133 @@ def generate_labels_pdf(order_id, paper_saving_mode='true'):
return jsonify({'error': str(e)}), 500 return jsonify({'error': str(e)}), 500
@bp.route('/generate_box_label_pdf', methods=['POST'])
def generate_box_label_pdf():
"""Generate a PDF box label with barcode for printing via QZ Tray"""
if 'role' not in session:
return jsonify({'error': 'Access denied. Please log in.'}), 403
try:
from io import BytesIO
from reportlab.lib.pagesizes import letter
from reportlab.pdfgen import canvas
from reportlab.lib import colors
from reportlab.lib.units import mm
from reportlab.graphics.barcode import code128
from flask import make_response
# Get box number from request
box_number = request.form.get('box_number', 'Unknown')
print(f"DEBUG: Generating box label PDF for box: {box_number}")
# Create PDF buffer
pdf_buffer = BytesIO()
# Create canvas with 8cm x 5cm size in landscape orientation
page_width = 80 * mm # 8 cm
page_height = 50 * mm # 5 cm
c = canvas.Canvas(pdf_buffer, pagesize=(page_width, page_height))
# Optimize for label printer
c.setPageCompression(1)
c.setCreator("Trasabilitate Box Label System")
c.setTitle("Box Label - Optimized for Label Printers")
# Define margins and usable area
margin = 2 * mm
usable_width = page_width - (2 * margin)
usable_height = page_height - (2 * margin)
# Calculate vertical layout
# Top section: "BOX Nr: XXXXXXXX" text
# Bottom section: Barcode
text_height = 12 * mm # Space for text at top (reduced from 15mm)
barcode_height = usable_height - text_height - (1 * mm) # Rest for barcode with minimal spacing (reduced from 3mm)
# === TOP SECTION: BOX Nr Label ===
# Position from top of usable area
text_y = page_height - margin - text_height
# Draw "BOX Nr:" label
c.setFont("Helvetica-Bold", 14)
label_text = "BOX Nr:"
label_width = c.stringWidth(label_text, "Helvetica-Bold", 14)
# Draw box number
c.setFont("Helvetica-Bold", 18)
number_text = box_number
number_width = c.stringWidth(number_text, "Helvetica-Bold", 18)
# Calculate total width and center everything
total_text_width = label_width + 3*mm + number_width # 3mm spacing between label and number
start_x = margin + (usable_width - total_text_width) / 2
# Draw label text
c.setFont("Helvetica-Bold", 14)
c.drawString(start_x, text_y + 5*mm, label_text)
# Draw box number
c.setFont("Helvetica-Bold", 18)
c.drawString(start_x + label_width + 3*mm, text_y + 5*mm, number_text)
# === BOTTOM SECTION: Barcode ===
barcode_y = margin
try:
# Create barcode for box number
barcode = code128.Code128(
box_number,
barWidth=0.4*mm, # Thicker bars for better scanning
barHeight=barcode_height,
humanReadable=True,
fontSize=10
)
# Calculate scaling to fit width
scale_factor = usable_width / barcode.width
# Center the barcode horizontally
barcode_x = margin + (usable_width - (barcode.width * scale_factor)) / 2
# Draw the barcode
c.saveState()
c.translate(barcode_x, barcode_y)
c.scale(scale_factor, 1)
barcode.drawOn(c, 0, 0)
c.restoreState()
print(f"DEBUG: Barcode generated successfully with scale factor: {scale_factor}")
except Exception as e:
print(f"DEBUG: Error generating barcode: {e}")
# Fallback: draw text if barcode fails
c.setFont("Helvetica-Bold", 12)
text_width = c.stringWidth(box_number, "Helvetica-Bold", 12)
c.drawString((page_width - text_width) / 2, barcode_y + barcode_height/2, box_number)
c.save()
# Get PDF data
pdf_buffer.seek(0)
pdf_data = pdf_buffer.getvalue()
# Create response
response = make_response(pdf_data)
response.headers['Content-Type'] = 'application/pdf'
response.headers['Content-Disposition'] = f'inline; filename="box_label_{box_number}.pdf"'
print(f"DEBUG: Box label PDF generated successfully")
return response
except Exception as e:
print(f"DEBUG: Error generating box label PDF: {e}")
import traceback
traceback.print_exc()
return jsonify({'error': str(e)}), 500
@bp.route('/generate_label_pdf', methods=['POST']) @bp.route('/generate_label_pdf', methods=['POST'])
def generate_label_pdf(): def generate_label_pdf():
"""Generate a single label PDF for thermal printing via QZ Tray""" """Generate a single label PDF for thermal printing via QZ Tray"""

View File

@@ -510,11 +510,13 @@ async function printLocationBarcode() {
printStatus.textContent = 'Sending to printer...'; printStatus.textContent = 'Sending to printer...';
// Configure QZ Tray for PDF printing with 4x8cm size // Configure QZ Tray for PDF printing with 8x5cm size in landscape
const config = qzTray.configs.create(selectedPrinter, { const config = qzTray.configs.create(selectedPrinter, {
size: { width: 4, height: 8, units: 'cm' }, scaleContent: false,
margins: { top: 0, right: 0, bottom: 0, left: 0 }, rasterize: false,
orientation: 'portrait' size: { width: 80, height: 50 },
units: 'mm',
margins: { top: 0, right: 0, bottom: 0, left: 0 }
}); });
const data = [{ const data = [{

View File

@@ -3,8 +3,8 @@
{% block title %}Finish Good Scan{% endblock %} {% block title %}Finish Good Scan{% endblock %}
{% block head %} {% block head %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/scan.css') }}"> <link rel="stylesheet" href="{{ url_for('static', filename='css/scan.css') }}">
<!-- QZ Tray for printing --> <!-- QZ Tray for printing - using local patched version for pairing-key authentication -->
<script src="https://cdn.jsdelivr.net/npm/qz-tray@2.2.0/qz-tray.min.js"></script> <script src="{{ url_for('static', filename='qz-tray.js') }}"></script>
<style> <style>
.error-message { .error-message {
color: #ff4444; color: #ff4444;
@@ -714,11 +714,12 @@ document.addEventListener('DOMContentLoaded', function() {
if (result.success && result.box_number) { if (result.success && result.box_number) {
const boxNumber = result.box_number; const boxNumber = result.box_number;
// Step 2: Print the box label using QZ Tray // Step 2: Generate PDF label and print using QZ Tray
try { try {
// Check QZ Tray connection // Check QZ Tray connection
if (!window.qz || !window.qz.websocket.isActive()) { if (!window.qz || !window.qz.websocket.isActive()) {
throw new Error('QZ Tray not connected. Please ensure QZ Tray is running.'); console.log('QZ Tray not connected, attempting to connect...');
await window.qz.websocket.connect();
} }
// Get available printers // Get available printers
@@ -727,17 +728,59 @@ document.addEventListener('DOMContentLoaded', function() {
throw new Error('No printers found'); throw new Error('No printers found');
} }
// Use first available printer // Try to get default printer, fallback to first available
const printer = printers[0]; let printer;
try {
printer = await window.qz.printers.getDefault();
console.log('Using default printer:', printer);
} catch (e) {
printer = printers[0];
console.log('Default printer not found, using first available:', printer);
}
// Create ZPL code for box label // Generate PDF from backend
const zpl = `^XA const formData = new FormData();
^FO50,50^A0N,40,40^FDBox: ${boxNumber}^FS formData.append('box_number', boxNumber);
^FO50,120^BY2,3,80^BCN,80,Y,N,N^FD${boxNumber}^FS
^XZ`;
const config = window.qz.configs.create(printer); const pdfResponse = await fetch('/generate_box_label_pdf', {
await window.qz.print(config, [zpl]); method: 'POST',
body: formData
});
if (!pdfResponse.ok) {
throw new Error('Failed to generate PDF label');
}
// Get PDF as blob and convert to base64
const pdfBlob = await pdfResponse.blob();
const pdfBase64 = await new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onloadend = () => {
const base64 = reader.result.split(',')[1];
resolve(base64);
};
reader.onerror = reject;
reader.readAsDataURL(pdfBlob);
});
console.log('PDF generated, sending to printer...');
// Configure QZ Tray for PDF printing
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 }
});
const printData = [{
type: 'pdf',
format: 'base64',
data: pdfBase64
}];
await window.qz.print(config, printData);
showNotification(`✅ Box ${boxNumber} created and label printed!`, 'success'); showNotification(`✅ Box ${boxNumber} created and label printed!`, 'success');
@@ -748,8 +791,9 @@ document.addEventListener('DOMContentLoaded', function() {
} catch (printError) { } catch (printError) {
console.error('Print error:', printError); console.error('Print error:', printError);
showNotification(`⚠️ Box ${boxNumber} created but print failed: ${printError.message}`, 'warning'); showNotification(`⚠️ Box ${boxNumber} created but print failed: ${printError.message}\n\nPlease ensure QZ Tray is running and a printer is available.`, 'warning');
// Still keep modal open for manual entry // Still keep modal open for manual entry
document.getElementById('scan-box-input').value = boxNumber;
document.getElementById('scan-box-input').focus(); document.getElementById('scan-box-input').focus();
} }
@@ -757,11 +801,12 @@ document.addEventListener('DOMContentLoaded', function() {
throw new Error(result.error || 'Failed to create box'); throw new Error(result.error || 'Failed to create box');
} }
} catch (error) { } catch (error) {
console.error('Error creating box:', error);
showNotification('❌ Error creating box: ' + error.message, 'error'); showNotification('❌ Error creating box: ' + error.message, 'error');
} finally { } finally {
// Re-enable button // Re-enable button
this.disabled = false; this.disabled = false;
this.textContent = 'Quick Box Label Creation'; this.textContent = '📦 Quick Box Label Creation';
} }
}); });

View File

@@ -472,6 +472,7 @@ function updateBoxPreview() {
// QZ Tray functionality // QZ Tray functionality
async function initQZTray() { async function initQZTray() {
console.log('=== Initializing QZ Tray ===');
try { try {
if (!window.qz) { if (!window.qz) {
console.error('QZ Tray library not loaded'); console.error('QZ Tray library not loaded');
@@ -479,23 +480,42 @@ async function initQZTray() {
return; return;
} }
console.log('QZ Tray library loaded');
console.log('Current connection status:', window.qz.websocket.isActive());
if (!window.qz.websocket.isActive()) { if (!window.qz.websocket.isActive()) {
console.log('Connecting to QZ Tray...');
await window.qz.websocket.connect(); await window.qz.websocket.connect();
console.log('Connected to QZ Tray successfully');
} else {
console.log('Already connected to QZ Tray');
} }
await loadPrinters(); await loadPrinters();
document.getElementById('print-status').innerHTML = '<span style="color: green;">✓ QZ Tray connected</span>';
setTimeout(() => {
document.getElementById('print-status').innerHTML = '';
}, 2000);
} catch (e) { } catch (e) {
console.error('QZ Tray initialization failed:', e); console.error('=== QZ Tray initialization failed ===');
document.getElementById('print-status').innerHTML = '<span style="color: red;">QZ Tray not connected</span>'; console.error('Error:', e);
console.error('Error message:', e.message);
document.getElementById('print-status').innerHTML = '<span style="color: red;">QZ Tray not connected - ' + e.message + '</span>';
} }
} }
async function loadPrinters() { async function loadPrinters() {
console.log('Loading printers...');
try { try {
const printers = await window.qz.printers.find(); const printers = await window.qz.printers.find();
console.log('Printers found:', printers);
const printerSelect = document.getElementById('printer-select'); const printerSelect = document.getElementById('printer-select');
printerSelect.innerHTML = ''; printerSelect.innerHTML = '';
if (printers.length === 0) { if (printers.length === 0) {
console.warn('No printers found');
printerSelect.innerHTML = '<option value="">No printers found</option>'; printerSelect.innerHTML = '<option value="">No printers found</option>';
return; return;
} }
@@ -510,6 +530,7 @@ async function loadPrinters() {
// Select first printer by default // Select first printer by default
if (printers.length > 0) { if (printers.length > 0) {
printerSelect.value = printers[0]; printerSelect.value = printers[0];
console.log('Default printer selected:', printers[0]);
} }
} catch (e) { } catch (e) {
console.error('Error loading printers:', e); console.error('Error loading printers:', e);
@@ -563,6 +584,9 @@ function selectAndShowLabel(boxId, boxNumber, buttonElement) {
} }
document.getElementById('print-box-btn').addEventListener('click', async function() { document.getElementById('print-box-btn').addEventListener('click', async function() {
console.log('=== PRINT BUTTON CLICKED ===');
console.log('Selected box number:', selectedBoxNumber);
if (!selectedBoxNumber) { if (!selectedBoxNumber) {
showNotification('Please select a box from the table', 'warning'); showNotification('Please select a box from the table', 'warning');
return; return;
@@ -571,33 +595,97 @@ document.getElementById('print-box-btn').addEventListener('click', async functio
const printerSelect = document.getElementById('printer-select'); const printerSelect = document.getElementById('printer-select');
const selectedPrinter = printerSelect.value; const selectedPrinter = printerSelect.value;
console.log('Selected printer:', selectedPrinter);
if (!selectedPrinter) { if (!selectedPrinter) {
showNotification('Please select a printer', 'warning'); showNotification('Please select a printer', 'warning');
return; return;
} }
try { try {
document.getElementById('print-status').innerHTML = '<span style="color: blue;">Printing...</span>'; // Check QZ Tray connection status
console.log('Checking QZ Tray connection...');
if (!window.qz.websocket.isActive()) {
console.log('QZ Tray not connected, attempting to reconnect...');
document.getElementById('print-status').innerHTML = '<span style="color: blue;">Reconnecting to QZ Tray...</span>';
await window.qz.websocket.connect();
await loadPrinters();
console.log('Reconnected to QZ Tray');
} else {
console.log('QZ Tray already connected');
}
// Create ZPL code for box label document.getElementById('print-status').innerHTML = '<span style="color: blue;">Generating PDF...</span>';
const zpl = `^XA
^FO50,50^A0N,40,40^FDBox: ${selectedBoxNumber}^FS
^FO50,120^BY2,3,80^BCN,80,Y,N,N^FD${selectedBoxNumber}^FS
^XZ`;
const config = window.qz.configs.create(selectedPrinter); // Create FormData to send to backend
await window.qz.print(config, [zpl]); const formData = new FormData();
formData.append('box_number', selectedBoxNumber);
showNotification('Box label printed successfully!', 'success'); // Send to backend to generate PDF
document.getElementById('print-status').innerHTML = '<span style="color: green;">✓ Printed successfully</span>'; const response = await fetch('/generate_box_label_pdf', {
method: 'POST',
body: formData
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || 'Failed to generate PDF');
}
// Get PDF as blob
const pdfBlob = await response.blob();
console.log('PDF generated successfully');
document.getElementById('print-status').innerHTML = '<span style="color: blue;">Sending to printer...</span>';
// Convert blob to base64 for QZ Tray
const pdfBase64 = await new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onloadend = () => {
const base64 = reader.result.split(',')[1];
resolve(base64);
};
reader.onerror = reject;
reader.readAsDataURL(pdfBlob);
});
console.log('PDF converted to base64');
// Configure QZ Tray for PDF printing
const config = window.qz.configs.create(selectedPrinter, {
scaleContent: false,
rasterize: false,
size: { width: 80, height: 50 },
units: 'mm',
margins: { top: 0, right: 0, bottom: 0, left: 0 }
});
console.log('Print config created:', config);
// Prepare PDF data for QZ Tray
const printData = [{
type: 'pdf',
format: 'base64',
data: pdfBase64
}];
console.log('Sending print command...');
const result = await window.qz.print(config, printData);
console.log('Print command result:', result);
showNotification('Box label sent to printer successfully!', 'success');
document.getElementById('print-status').innerHTML = '<span style="color: green;">✓ Sent to printer successfully</span>';
setTimeout(() => { setTimeout(() => {
document.getElementById('print-status').innerHTML = ''; document.getElementById('print-status').innerHTML = '';
}, 3000); }, 3000);
} catch (e) { } catch (e) {
console.error('Print error:', e); console.error('=== PRINT ERROR ===');
console.error('Error details:', e);
console.error('Error message:', e.message);
console.error('Error stack:', e.stack);
showNotification('Print failed: ' + e.message, 'error'); showNotification('Print failed: ' + e.message, 'error');
document.getElementById('print-status').innerHTML = '<span style="color: red;">Print failed</span>'; document.getElementById('print-status').innerHTML = '<span style="color: red;">Print failed: ' + e.message + '</span>';
} }
}); });
</script> </script>

View File

@@ -3,7 +3,7 @@ from flask import current_app, request, render_template, session, redirect, url_
import csv, os, tempfile import csv, os, tempfile
from reportlab.lib.pagesizes import letter from reportlab.lib.pagesizes import letter
from reportlab.pdfgen import canvas from reportlab.pdfgen import canvas
from reportlab.lib.units import cm from reportlab.lib.units import cm, mm
from reportlab.graphics.barcode import code128 from reportlab.graphics.barcode import code128
import io import io
@@ -255,7 +255,7 @@ def import_locations_csv_handler():
return render_template('import_locations_csv.html', report=report, locations=locations) return render_template('import_locations_csv.html', report=report, locations=locations)
def generate_location_label_pdf(): def generate_location_label_pdf():
"""Generate PDF for location barcode label (8x4cm)""" """Generate PDF for location barcode label (8cm x 5cm landscape)"""
try: try:
data = request.get_json() data = request.get_json()
location_code = data.get('location_code', '') location_code = data.get('location_code', '')
@@ -263,46 +263,93 @@ def generate_location_label_pdf():
if not location_code: if not location_code:
return jsonify({'error': 'Location code is required'}), 400 return jsonify({'error': 'Location code is required'}), 400
print(f"DEBUG: Generating location label PDF for: {location_code}")
# Create PDF in memory # Create PDF in memory
buffer = io.BytesIO() buffer = io.BytesIO()
# Create PDF with 8x4cm page size (width x height) # Create PDF with 8cm x 5cm page size in landscape orientation
page_width = 8 * cm page_width = 80 * mm # 8 cm
page_height = 4 * cm page_height = 50 * mm # 5 cm
c = canvas.Canvas(buffer, pagesize=(page_width, page_height)) c = canvas.Canvas(buffer, pagesize=(page_width, page_height))
# Generate Code128 barcode # Optimize for label printer
barcode = code128.Code128(location_code, barWidth=1.0, humanReadable=False) c.setPageCompression(1)
c.setCreator("Trasabilitate Location Label System")
c.setTitle("Location Label - Optimized for Label Printers")
# Calculate the desired barcode dimensions (fill most of the label) # Define margins and usable area
desired_barcode_width = 7 * cm # Almost full width margin = 2 * mm
desired_barcode_height = 2.5 * cm # Most of the height usable_width = page_width - (2 * margin)
usable_height = page_height - (2 * margin)
# Calculate scaling factor to fit the desired width # Calculate vertical layout
scale = desired_barcode_width / barcode.width # Top section: "Location nr: XXXX" text
# Bottom section: Barcode
text_height = 12 * mm # Space for text at top
barcode_height = usable_height - text_height - (1 * mm) # Rest for barcode with minimal spacing
# Calculate actual dimensions after scaling # === TOP SECTION: Location Nr Label ===
actual_width = barcode.width * scale # Position from top of usable area
actual_height = barcode.height * scale text_y = page_height - margin - text_height
# Center the barcode on the label # Draw "Location nr:" label
barcode_x = (page_width - actual_width) / 2 c.setFont("Helvetica-Bold", 14)
barcode_y = (page_height - actual_height) / 2 + 0.3 * cm # Slightly above center for text space label_text = "Location nr:"
label_width = c.stringWidth(label_text, "Helvetica-Bold", 14)
# Draw barcode with scaling # Draw location code
c.saveState() c.setFont("Helvetica-Bold", 18)
c.translate(barcode_x, barcode_y) code_text = location_code
c.scale(scale, scale) code_width = c.stringWidth(code_text, "Helvetica-Bold", 18)
barcode.drawOn(c, 0, 0)
c.restoreState()
# Add location code text below barcode # Calculate total width and center everything
c.setFont("Helvetica-Bold", 10) total_text_width = label_width + 3*mm + code_width # 3mm spacing between label and code
text_width = c.stringWidth(location_code, "Helvetica-Bold", 10) start_x = margin + (usable_width - total_text_width) / 2
text_x = (page_width - text_width) / 2
text_y = barcode_y - 0.5 * cm # Below the barcode # Draw label text
c.drawString(text_x, text_y, location_code) c.setFont("Helvetica-Bold", 14)
c.drawString(start_x, text_y + 5*mm, label_text)
# Draw location code
c.setFont("Helvetica-Bold", 18)
c.drawString(start_x + label_width + 3*mm, text_y + 5*mm, code_text)
# === BOTTOM SECTION: Barcode ===
barcode_y = margin
try:
# Create barcode for location code
barcode = code128.Code128(
location_code,
barWidth=0.4*mm, # Thicker bars for better scanning
barHeight=barcode_height,
humanReadable=True,
fontSize=10
)
# Calculate scaling to fit width
scale_factor = usable_width / barcode.width
# Center the barcode horizontally
barcode_x = margin + (usable_width - (barcode.width * scale_factor)) / 2
# Draw the barcode
c.saveState()
c.translate(barcode_x, barcode_y)
c.scale(scale_factor, 1)
barcode.drawOn(c, 0, 0)
c.restoreState()
print(f"DEBUG: Barcode generated successfully with scale factor: {scale_factor}")
except Exception as e:
print(f"DEBUG: Error generating barcode: {e}")
# Fallback: draw text if barcode fails
c.setFont("Helvetica-Bold", 12)
text_width = c.stringWidth(location_code, "Helvetica-Bold", 12)
c.drawString((page_width - text_width) / 2, barcode_y + barcode_height/2, location_code)
# Finalize PDF # Finalize PDF
c.save() c.save()
@@ -313,10 +360,13 @@ def generate_location_label_pdf():
response.headers['Content-Type'] = 'application/pdf' response.headers['Content-Type'] = 'application/pdf'
response.headers['Content-Disposition'] = f'inline; filename=location_{location_code}_label.pdf' response.headers['Content-Disposition'] = f'inline; filename=location_{location_code}_label.pdf'
print(f"DEBUG: Location label PDF generated successfully")
return response return response
except Exception as e: except Exception as e:
print(f"Error generating location label PDF: {e}") print(f"Error generating location label PDF: {e}")
import traceback
traceback.print_exc()
return jsonify({'error': str(e)}), 500 return jsonify({'error': str(e)}), 500
def update_location(location_id, location_code, size, description): def update_location(location_id, location_code, size, description):