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,452 @@
"""
PDF Label Generator for Print Module
Generates 80x110mm labels with sequential numbering based on quantity
"""
from reportlab.lib.pagesizes import letter, A4
from reportlab.lib.units import mm
from reportlab.lib import colors
from reportlab.pdfgen import canvas
from reportlab.lib.styles import getSampleStyleSheet
from reportlab.platypus import Paragraph
from reportlab.lib.enums import TA_CENTER, TA_LEFT
from reportlab.graphics.barcode import code128
from reportlab.graphics import renderPDF
from reportlab.graphics.shapes import Drawing
import os
from flask import current_app
import io
def mm_to_points(mm_value):
"""Convert millimeters to points (ReportLab uses points)"""
return mm_value * mm
class LabelPDFGenerator:
def __init__(self, paper_saving_mode=True):
# Label dimensions: 80mm x 105mm (reduced from 110mm by cutting 5mm from bottom)
self.label_width = mm_to_points(80)
self.label_height = mm_to_points(105)
# Paper-saving mode: positions content at top of label to minimize waste
self.paper_saving_mode = paper_saving_mode
# Match the HTML preview dimensions exactly
# Preview: 227.4px width x 321.3px height
# Convert to proportional dimensions for 80x110mm
self.content_width = mm_to_points(60) # ~227px scaled to 80mm
self.content_height = mm_to_points(68) # Reduced by 20% from 85mm to 68mm
# Position content in label - rectangle positioned 15mm from top
# Label height: 105mm, Rectangle height: 68mm
# content_y = 105mm - 15mm - 68mm = 22mm from bottom
if self.paper_saving_mode:
# Start content from top of label with 15mm top margin
self.content_x = mm_to_points(4) # 4mm from left edge
self.content_y = mm_to_points(22) # 22mm from bottom (15mm from top)
self.top_margin = mm_to_points(15) # 15mm top margin
else:
# Original positioning
self.content_x = mm_to_points(5) # 5mm from left edge
self.content_y = mm_to_points(22) # 22mm from bottom (15mm from top)
self.top_margin = mm_to_points(15) # 15mm top margin
# Row dimensions (9 rows total, row 6 is double height)
self.row_height = self.content_height / 10 # 8.5mm per standard row
self.double_row_height = self.row_height * 2
# Column split at 40% (90.96px / 227.4px = 40%)
self.left_column_width = self.content_width * 0.4
self.right_column_width = self.content_width * 0.6
# Vertical divider starts from row 3
self.vertical_divider_start_y = self.content_y + self.content_height - (2 * self.row_height)
def generate_labels_pdf(self, order_data, quantity, printer_optimized=True):
"""
Generate PDF with multiple labels based on quantity
Creates sequential labels: CP00000711-001 to CP00000711-XXX
Optimized for thermal label printers (Epson TM-T20, Citizen CTS-310)
"""
buffer = io.BytesIO()
# Create canvas with label dimensions
c = canvas.Canvas(buffer, pagesize=(self.label_width, self.label_height))
# Optimize PDF for label printers
if printer_optimized:
self._optimize_for_label_printer(c)
# Extract base production order number for sequential numbering
prod_order = order_data.get('comanda_productie', 'CP00000000')
# Generate labels for each quantity
for i in range(1, quantity + 1):
if i > 1: # Add new page for each label except first
c.showPage()
if printer_optimized:
self._optimize_for_label_printer(c)
# Create sequential label number: CP00000711/001, CP00000711/002, etc.
sequential_number = f"{prod_order}/{i:03d}"
# Draw single label
self._draw_label(c, order_data, sequential_number, i, quantity)
c.save()
buffer.seek(0)
return buffer
def generate_single_label_pdf(self, order_data, piece_number, total_pieces, printer_optimized=True):
"""
Generate PDF with single label for specific piece number
Creates sequential label: CP00000711-001, CP00000711-002, etc.
Optimized for thermal label printers via QZ Tray
"""
buffer = io.BytesIO()
# Create canvas with label dimensions
c = canvas.Canvas(buffer, pagesize=(self.label_width, self.label_height))
# Optimize PDF for label printers
if printer_optimized:
self._optimize_for_label_printer(c)
# Extract base production order number for sequential numbering
prod_order = order_data.get('comanda_productie', 'CP00000000')
# Create sequential label number with specific piece number
sequential_number = f"{prod_order}/{piece_number:03d}"
print(f"DEBUG: Generating label {sequential_number} (piece {piece_number} of {total_pieces})")
# Draw single label with specific piece number
self._draw_label(c, order_data, sequential_number, piece_number, total_pieces)
c.save()
buffer.seek(0)
return buffer
def _optimize_for_label_printer(self, canvas):
"""
Optimize PDF settings for thermal label printers
- Sets high resolution for crisp text
- Minimizes margins to save paper
- Optimizes for monochrome printing
"""
# Set high resolution for thermal printers (300 DPI)
canvas.setPageCompression(1) # Enable compression
# Add PDF metadata for printer optimization
canvas.setCreator("Recticel Label System")
canvas.setTitle("Thermal Label - Optimized for Label Printers")
canvas.setSubject("Production Label")
# Set print scaling to none (100%) to maintain exact dimensions
canvas.setPageRotation(0)
# Add custom PDF properties for label printers
canvas._doc.info.producer = "Optimized for Epson TM-T20 / Citizen CTS-310"
def _draw_label(self, canvas, order_data, sequential_number, current_num, total_qty):
"""Draw a single label matching the HTML preview layout exactly"""
# Draw main content border (like the HTML preview rectangle)
canvas.setStrokeColor(colors.black)
canvas.setLineWidth(2)
canvas.rect(self.content_x, self.content_y, self.content_width, self.content_height)
# Calculate row positions from top
current_y = self.content_y + self.content_height
# Row 1: Company Header - (removed)
row_y = current_y - self.row_height
canvas.setFont("Helvetica-Bold", 10)
text = ""
text_width = canvas.stringWidth(text, "Helvetica-Bold", 10)
x_centered = self.content_x + (self.content_width - text_width) / 2
canvas.drawString(x_centered, row_y + self.row_height/3, text)
current_y = row_y
# Row 2: Customer Name
row_y = current_y - self.row_height
canvas.setFont("Helvetica-Bold", 9)
customer_name = str(order_data.get('customer_name', ''))[:30]
text_width = canvas.stringWidth(customer_name, "Helvetica-Bold", 9)
x_centered = self.content_x + (self.content_width - text_width) / 2
canvas.drawString(x_centered, row_y + self.row_height/3, customer_name)
current_y = row_y
# Draw horizontal lines after rows 1 and 2
canvas.setLineWidth(1)
canvas.line(self.content_x, current_y, self.content_x + self.content_width, current_y)
canvas.line(self.content_x, current_y + self.row_height, self.content_x + self.content_width, current_y + self.row_height)
# Draw vertical divider line (starts from row 3, goes to bottom)
vertical_x = self.content_x + self.left_column_width
canvas.line(vertical_x, current_y, vertical_x, self.content_y)
# Row 3: Quantity ordered
row_y = current_y - self.row_height
canvas.setFont("Helvetica", 8)
canvas.drawString(self.content_x + mm_to_points(2), row_y + self.row_height/3, "Quantity ordered")
canvas.setFont("Helvetica-Bold", 11)
quantity = str(order_data.get('cantitate', '0'))
q_text_width = canvas.stringWidth(quantity, "Helvetica-Bold", 11)
q_x_centered = vertical_x + (self.right_column_width - q_text_width) / 2
canvas.drawString(q_x_centered, row_y + self.row_height/3, quantity)
current_y = row_y
# Row 4: Customer order - CORRECTED to match HTML preview
row_y = current_y - self.row_height
canvas.setFont("Helvetica", 8)
canvas.drawString(self.content_x + mm_to_points(2), row_y + self.row_height/3, "Customer order")
canvas.setFont("Helvetica-Bold", 10)
# Match HTML: com_achiz_client + "-" + nr_linie_com_client
com_achiz_client = str(order_data.get('com_achiz_client', ''))
nr_linie = str(order_data.get('nr_linie_com_client', ''))
if com_achiz_client and nr_linie:
customer_order = f"{com_achiz_client}-{nr_linie}"[:18]
else:
customer_order = "N/A"
co_text_width = canvas.stringWidth(customer_order, "Helvetica-Bold", 10)
co_x_centered = vertical_x + (self.right_column_width - co_text_width) / 2
canvas.drawString(co_x_centered, row_y + self.row_height/3, customer_order)
current_y = row_y
# Row 5: Delivery date
row_y = current_y - self.row_height
canvas.setFont("Helvetica", 8)
canvas.drawString(self.content_x + mm_to_points(2), row_y + self.row_height/3, "Delivery date")
canvas.setFont("Helvetica-Bold", 10)
delivery_date = str(order_data.get('data_livrare', 'N/A'))
if delivery_date != 'N/A' and delivery_date:
try:
# Format date if it's a valid date
from datetime import datetime
if isinstance(delivery_date, str) and len(delivery_date) > 8:
delivery_date = delivery_date[:10] # Take first 10 chars for date
except:
pass
dd_text_width = canvas.stringWidth(delivery_date, "Helvetica-Bold", 10)
dd_x_centered = vertical_x + (self.right_column_width - dd_text_width) / 2
canvas.drawString(dd_x_centered, row_y + self.row_height/3, delivery_date)
current_y = row_y
# Row 6: Description (double height)
row_y = current_y - self.double_row_height
canvas.setFont("Helvetica", 8)
canvas.drawString(self.content_x + mm_to_points(2), row_y + self.double_row_height/2, "Description")
# Handle description text wrapping for double height area
canvas.setFont("Helvetica-Bold", 9)
description = str(order_data.get('descr_com_prod', 'N/A'))
max_chars_per_line = 18
lines = []
words = description.split()
current_line = ""
for word in words:
if len(current_line + word + " ") <= max_chars_per_line:
current_line += word + " "
else:
if current_line:
lines.append(current_line.strip())
current_line = word + " "
if current_line:
lines.append(current_line.strip())
# Draw up to 3 lines in the double height area
line_spacing = self.double_row_height / 4
start_y = row_y + 3 * line_spacing
for i, line in enumerate(lines[:3]):
line_y = start_y - (i * line_spacing)
l_text_width = canvas.stringWidth(line, "Helvetica-Bold", 9)
l_x_centered = vertical_x + (self.right_column_width - l_text_width) / 2
canvas.drawString(l_x_centered, line_y, line)
current_y = row_y
# Row 7: Size
row_y = current_y - self.row_height
canvas.setFont("Helvetica", 8)
canvas.drawString(self.content_x + mm_to_points(2), row_y + self.row_height/3, "Size")
canvas.setFont("Helvetica-Bold", 10)
size = str(order_data.get('dimensiune', 'N/A'))[:12]
s_text_width = canvas.stringWidth(size, "Helvetica-Bold", 10)
s_x_centered = vertical_x + (self.right_column_width - s_text_width) / 2
canvas.drawString(s_x_centered, row_y + self.row_height/3, size)
current_y = row_y
# Row 8: Article Code - CORRECTED to use customer_article_number like HTML preview
row_y = current_y - self.row_height
canvas.setFont("Helvetica", 8)
canvas.drawString(self.content_x + mm_to_points(2), row_y + self.row_height/3, "Article Code")
canvas.setFont("Helvetica-Bold", 10)
# Match HTML: uses customer_article_number, not cod_articol
article_code = str(order_data.get('customer_article_number', 'N/A'))[:12]
ac_text_width = canvas.stringWidth(article_code, "Helvetica-Bold", 10)
ac_x_centered = vertical_x + (self.right_column_width - ac_text_width) / 2
canvas.drawString(ac_x_centered, row_y + self.row_height/3, article_code)
current_y = row_y
# Draw horizontal line between Article Code and Prod Order
canvas.setLineWidth(1)
canvas.line(self.content_x, current_y, self.content_x + self.content_width, current_y)
# Row 9: Prod Order - CORRECTED to match HTML preview with sequential numbering
row_y = current_y - self.row_height
canvas.setFont("Helvetica", 8)
canvas.drawString(self.content_x + mm_to_points(2), row_y + self.row_height/3, "Prod Order")
canvas.setFont("Helvetica-Bold", 10)
# Match HTML: comanda_productie + "-" + sequential number (not quantity!)
comanda_productie = str(order_data.get('comanda_productie', 'N/A'))
prod_order = f"{comanda_productie}-{current_num:03d}" # Sequential number for this specific label
po_text_width = canvas.stringWidth(prod_order, "Helvetica-Bold", 10)
po_x_centered = vertical_x + (self.right_column_width - po_text_width) / 2
canvas.drawString(po_x_centered, row_y + self.row_height/3, prod_order)
# Draw all horizontal lines between rows (from row 3 onwards)
for i in range(6): # 6 lines between 7 rows (3-9)
line_y = self.content_y + self.content_height - (2 + i + 1) * self.row_height
if i == 3: # Account for double height description row
line_y -= self.row_height
canvas.line(self.content_x, line_y, self.content_x + self.content_width, line_y)
# Bottom horizontal barcode - positioned 1.5mm below the main rectangle
barcode_area_height = mm_to_points(12) # Reserve space for barcode
barcode_y = self.content_y - mm_to_points(1.5) - barcode_area_height # 1.5mm gap below rectangle
barcode_width = self.content_width # Use full content width
barcode_x = self.content_x
try:
# Create barcode for sequential number
barcode = code128.Code128(sequential_number,
barWidth=0.25*mm, # Adjust bar width for better fit
barHeight=mm_to_points(10)) # Increase height to 10mm
# Always scale to fit the full allocated width
scale_factor = barcode_width / barcode.width
canvas.saveState()
canvas.translate(barcode_x, barcode_y)
canvas.scale(scale_factor, 1)
barcode.drawOn(canvas, 0, 0)
canvas.restoreState()
# NO TEXT BELOW BARCODE - Remove all text rendering for horizontal barcode
except Exception as e:
# Fallback: Simple barcode pattern that fills the width
canvas.setStrokeColor(colors.black)
canvas.setFillColor(colors.black)
bar_width = barcode_width / 50 # 50 bars across width
for i in range(50):
if i % 3 < 2: # Create barcode-like pattern
x_pos = barcode_x + (i * bar_width)
canvas.rect(x_pos, barcode_y, bar_width * 0.8, mm_to_points(8), fill=1)
# Right side vertical barcode - positioned 3mm further right and fill frame
vertical_barcode_x = self.content_x + self.content_width + mm_to_points(4) # Moved 3mm right (1mm + 3mm = 4mm)
vertical_barcode_y = self.content_y
vertical_barcode_height = self.content_height
vertical_barcode_width = mm_to_points(12) # Increased width for better fill
try:
# Create vertical barcode code - CORRECTED to match HTML preview
# Use same format as customer order: com_achiz_client + "/" + nr_linie_com_client
com_achiz_client = str(order_data.get('com_achiz_client', ''))
nr_linie = str(order_data.get('nr_linie_com_client', ''))
if com_achiz_client and nr_linie:
vertical_code = f"{com_achiz_client}/{nr_linie}"
else:
vertical_code = "000000/00"
# Create a vertical barcode using Code128
v_barcode = code128.Code128(vertical_code,
barWidth=0.15*mm, # Thinner bars for better fit
barHeight=mm_to_points(8)) # Increased bar height
# Draw rotated barcode - fill the entire frame height
canvas.saveState()
canvas.translate(vertical_barcode_x + mm_to_points(6), vertical_barcode_y)
canvas.rotate(90)
# Always scale to fill the frame height
scale_factor = vertical_barcode_height / v_barcode.width
canvas.scale(scale_factor, 1)
v_barcode.drawOn(canvas, 0, 0)
canvas.restoreState()
# NO TEXT FOR VERTICAL BARCODE - Remove all text rendering
except Exception as e:
# Fallback: Vertical barcode pattern that fills the frame
canvas.setStrokeColor(colors.black)
canvas.setFillColor(colors.black)
bar_height = vertical_barcode_height / 60 # 60 bars across height
for i in range(60):
if i % 3 < 2: # Create barcode pattern
y_pos = vertical_barcode_y + (i * bar_height)
canvas.rect(vertical_barcode_x, y_pos, mm_to_points(8), bar_height * 0.8, fill=1)
def generate_order_labels_pdf(order_id, order_data, paper_saving_mode=True):
"""
Main function to generate PDF for an order with multiple labels
Optimized for thermal label printers (Epson TM-T20, Citizen CTS-310)
Args:
order_id: Order identifier
order_data: Order information dictionary
paper_saving_mode: If True, positions content at top to save paper
"""
try:
generator = LabelPDFGenerator(paper_saving_mode=paper_saving_mode)
# Get quantity from order data
quantity = int(order_data.get('cantitate', 1))
# Generate PDF with printer optimization
pdf_buffer = generator.generate_labels_pdf(order_data, quantity, printer_optimized=True)
return pdf_buffer
except Exception as e:
print(f"Error generating PDF labels: {e}")
raise e
def update_order_printed_status(order_id):
"""
Update the order status to printed in the database
"""
try:
from .print_module import get_db_connection
conn = get_db_connection()
cursor = conn.cursor()
# Check if printed_labels column exists
cursor.execute("SHOW COLUMNS FROM order_for_labels LIKE 'printed_labels'")
column_exists = cursor.fetchone()
if column_exists:
# Update printed status
cursor.execute("""
UPDATE order_for_labels
SET printed_labels = 1, updated_at = NOW()
WHERE id = %s
""", (order_id,))
else:
# If column doesn't exist, we could add it or use another method
print(f"Warning: printed_labels column doesn't exist for order {order_id}")
conn.commit()
conn.close()
return True
except Exception as e:
print(f"Error updating printed status for order {order_id}: {e}")
return False

View File

@@ -5,6 +5,7 @@ Handles label printing pages and API endpoints
from flask import Blueprint, render_template, session, redirect, url_for, jsonify, request
import logging
from app.database import get_db
from .print_module import (
get_unprinted_orders_data,
get_printed_orders_data,
@@ -35,6 +36,15 @@ def print_module():
return render_template('modules/labels/print_module.html')
@labels_bp.route('/print-labels', methods=['GET'])
def print_labels():
"""Original print labels interface - complete copy from quality app"""
if 'user_id' not in session:
return redirect(url_for('main.login'))
return render_template('modules/labels/print_labels.html')
@labels_bp.route('/print-lost-labels', methods=['GET'])
def print_lost_labels():
"""Print lost/missing labels interface"""
@@ -74,6 +84,28 @@ def help(page='index'):
</ol>
'''
},
'print_labels': {
'title': 'Print Labels Help',
'content': '''
<h3>Print Labels - Thermal Printer Guide</h3>
<p>This module helps you print labels directly to thermal printers.</p>
<h4>Features:</h4>
<ul>
<li>Live label preview in thermal format</li>
<li>Real-time printer selection</li>
<li>Barcode generation</li>
<li>PDF export fallback</li>
<li>Batch printing support</li>
</ul>
<h4>How to use:</h4>
<ol>
<li>Select orders from the list</li>
<li>Preview labels in the preview pane</li>
<li>Select your printer</li>
<li>Click "Print Labels" to send to printer</li>
</ol>
'''
},
'print_lost_labels': {
'title': 'Print Lost Labels Help',
'content': '''
@@ -198,3 +230,110 @@ def api_update_printed_status(order_id):
except Exception as e:
logger.error(f"Error updating order status: {e}")
return jsonify({'success': False, 'error': str(e)}), 500
@labels_bp.route('/api/generate-pdf', methods=['POST'], endpoint='api_generate_pdf')
def api_generate_pdf():
"""Generate single label PDF for thermal printing via QZ Tray"""
if 'user_id' not in session:
return jsonify({'error': 'Unauthorized'}), 401
try:
from .pdf_generator import LabelPDFGenerator
# Get order data from request
order_data = request.get_json()
if not order_data:
return jsonify({'error': 'No order data provided'}), 400
# Extract piece number and total pieces for sequential numbering
piece_number = order_data.get('piece_number', 1)
total_pieces = order_data.get('total_pieces', 1)
logger.info(f"Generating single label PDF for piece {piece_number} of {total_pieces}")
# Initialize PDF generator in thermal printer optimized mode
pdf_generator = LabelPDFGenerator(paper_saving_mode=True)
# Generate single label PDF with specific piece number for sequential CP numbering
pdf_buffer = pdf_generator.generate_single_label_pdf(order_data, piece_number, total_pieces, printer_optimized=True)
# Create response with PDF data
from flask import make_response
response = make_response(pdf_buffer.getvalue())
response.headers['Content-Type'] = 'application/pdf'
response.headers['Content-Disposition'] = f'inline; filename="label_{piece_number:03d}.pdf"'
return response
except Exception as e:
logger.error(f"Error generating PDF: {e}")
return jsonify({'error': str(e)}), 500
@labels_bp.route('/api/generate-pdf/<int:order_id>/true', methods=['POST'], endpoint='api_generate_batch_pdf')
def api_generate_batch_pdf(order_id):
"""Generate all label PDFs for an order and mark as printed"""
if 'user_id' not in session:
return jsonify({'error': 'Unauthorized'}), 401
try:
from .pdf_generator import LabelPDFGenerator
# Get order data from database
conn = get_db()
cursor = conn.cursor()
cursor.execute("""
SELECT id, 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,
created_at, updated_at, printed_labels, data_livrare, dimensiune
FROM order_for_labels
WHERE id = %s
""", (order_id,))
row = cursor.fetchone()
if not row:
cursor.close()
return jsonify({'error': 'Order not found'}), 404
# Create order data dictionary
columns = [col[0] for col in cursor.description]
order_data = {columns[i]: row[i] for i in range(len(columns))}
# Ensure date fields are strings
if order_data.get('data_livrare'):
order_data['data_livrare'] = str(order_data['data_livrare'])
cursor.close()
logger.info(f"Generating batch PDF for order {order_id} with {order_data.get('cantitate', 0)} labels")
# Initialize PDF generator
pdf_generator = LabelPDFGenerator(paper_saving_mode=True)
# Get quantity from order data
quantity = int(order_data.get('cantitate', 1))
# Generate PDF with all labels
pdf_buffer = pdf_generator.generate_labels_pdf(order_data, quantity, printer_optimized=True)
# Mark order as printed
success = update_order_printed_status(order_id, True)
if not success:
logger.warning(f"Failed to mark order {order_id} as printed, but PDF was generated")
# Create response with PDF data
from flask import make_response
response = make_response(pdf_buffer.getvalue())
response.headers['Content-Type'] = 'application/pdf'
response.headers['Content-Disposition'] = f'attachment; filename="labels_{order_data.get("comanda_productie", "unknown")}.pdf"'
return response
except Exception as e:
logger.error(f"Error generating batch PDF: {e}")
return jsonify({'error': str(e)}), 500

View File

@@ -0,0 +1,807 @@
/* ==========================================================================
PRINT MODULE CSS - Dedicated styles for Labels/Printing Module
==========================================================================
This file contains all CSS for the printing module pages:
- print_module.html (main printing interface)
- print_lost_labels.html (lost labels printing)
- main_page_etichete.html (labels main page)
- upload_data.html (upload orders)
- view_orders.html (view orders)
========================================================================== */
/* ==========================================================================
LABEL PREVIEW STYLES
========================================================================== */
#label-preview {
background: #fafafa;
position: relative;
overflow: visible;
}
/* Label content rectangle styling */
#label-content {
position: absolute;
top: 65.7px;
left: 11.34px;
width: 227.4px;
height: 321.3px;
border: 1px solid #ddd;
background: white;
}
/* Barcode frame styling */
#barcode-frame {
position: absolute;
top: 387px;
left: 50%;
transform: translateX(calc(-50% - 20px));
width: 220px;
max-width: 220px;
height: 50px;
background: white;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
overflow: hidden;
border: none;
}
#barcode-display {
width: 100%;
height: 40px;
max-width: 220px;
}
#barcode-text {
font-size: 8px;
font-family: 'Courier New', monospace;
margin-top: 2px;
text-align: center;
font-weight: bold;
display: none;
}
/* Vertical barcode frame styling */
#vertical-barcode-frame {
position: absolute;
top: 50px;
left: 270px;
width: 321.3px;
height: 40px;
background: white;
display: flex;
align-items: center;
justify-content: center;
transform: rotate(90deg);
transform-origin: left center;
border: none;
}
#vertical-barcode-display {
width: 100%;
height: 35px;
}
#vertical-barcode-text {
position: absolute;
bottom: -15px;
font-size: 7px;
font-family: 'Courier New', monospace;
text-align: center;
font-weight: bold;
width: 100%;
display: none;
}
/* Allow JsBarcode to control SVG colors naturally - removed forced black styling */
/* ==========================================================================
PRINT MODULE TABLE STYLES
========================================================================== */
/* Enhanced table styling for print module tables */
.card.scan-table-card table.print-module-table.scan-table thead th {
border-bottom: 2px solid var(--print-table-border) !important;
background-color: var(--print-table-header-bg) !important;
color: var(--print-table-header-text) !important;
padding: 0.25rem 0.4rem !important;
text-align: left !important;
font-weight: 600 !important;
font-size: 10px !important;
line-height: 1.2 !important;
}
.card.scan-table-card table.print-module-table.scan-table {
width: 100% !important;
border-collapse: collapse !important;
background-color: var(--print-table-body-bg) !important;
}
.card.scan-table-card table.print-module-table.scan-table tbody tr:hover td {
background-color: var(--print-table-hover) !important;
cursor: pointer !important;
}
.card.scan-table-card table.print-module-table.scan-table tbody td {
background-color: var(--print-table-body-bg) !important;
color: var(--print-table-body-text) !important;
border: 1px solid var(--print-table-border) !important;
padding: 0.25rem 0.4rem !important;
}
.card.scan-table-card table.print-module-table.scan-table tbody tr.selected td {
background-color: var(--print-table-selected) !important;
color: white !important;
}
/* ==========================================================================
VIEW ORDERS TABLE STYLES (for print_lost_labels.html)
========================================================================== */
table.view-orders-table.scan-table {
margin: 0 !important;
border-spacing: 0 !important;
border-collapse: collapse !important;
width: 100% !important;
table-layout: fixed !important;
font-size: 11px !important;
}
table.view-orders-table.scan-table thead th {
height: 85px !important;
min-height: 85px !important;
max-height: 85px !important;
vertical-align: middle !important;
text-align: center !important;
white-space: normal !important;
word-wrap: break-word !important;
line-height: 1.3 !important;
padding: 6px 3px !important;
font-size: 11px !important;
background-color: var(--print-table-header-bg) !important;
color: var(--print-table-header-text) !important;
font-weight: bold !important;
text-transform: none !important;
letter-spacing: 0 !important;
overflow: visible !important;
box-sizing: border-box !important;
border: 1px solid var(--print-table-border) !important;
text-overflow: clip !important;
position: relative !important;
}
table.view-orders-table.scan-table tbody td {
padding: 4px 2px !important;
font-size: 10px !important;
text-align: center !important;
border: 1px solid var(--print-table-border) !important;
background-color: var(--print-table-body-bg) !important;
color: var(--print-table-body-text) !important;
white-space: nowrap !important;
overflow: hidden !important;
text-overflow: ellipsis !important;
vertical-align: middle !important;
}
/* Column width definitions for view orders table */
table.view-orders-table.scan-table td:nth-child(1) { width: 50px !important; }
table.view-orders-table.scan-table td:nth-child(2) { width: 80px !important; }
table.view-orders-table.scan-table td:nth-child(3) { width: 80px !important; }
table.view-orders-table.scan-table td:nth-child(4) { width: 150px !important; }
table.view-orders-table.scan-table td:nth-child(5) { width: 70px !important; }
table.view-orders-table.scan-table td:nth-child(6) { width: 80px !important; }
table.view-orders-table.scan-table td:nth-child(7) { width: 75px !important; }
table.view-orders-table.scan-table td:nth-child(8) { width: 90px !important; }
table.view-orders-table.scan-table td:nth-child(9) { width: 70px !important; }
table.view-orders-table.scan-table td:nth-child(10) { width: 100px !important; }
table.view-orders-table.scan-table td:nth-child(11) { width: 90px !important; }
table.view-orders-table.scan-table td:nth-child(12) { width: 70px !important; }
table.view-orders-table.scan-table td:nth-child(13) { width: 50px !important; }
table.view-orders-table.scan-table td:nth-child(14) { width: 70px !important; }
table.view-orders-table.scan-table td:nth-child(15) { width: 100px !important; }
table.view-orders-table.scan-table tbody tr:hover td {
background-color: var(--print-table-hover) !important;
}
table.view-orders-table.scan-table tbody tr.selected td {
background-color: var(--print-table-selected) !important;
color: white !important;
}
/* Remove unwanted spacing */
.report-table-card > * {
margin-top: 0 !important;
}
.report-table-container {
margin-top: 0 !important;
}
/* ==========================================================================
PRINT MODULE LAYOUT STYLES
========================================================================== */
/* Scan container layout */
.scan-container {
display: flex;
flex-direction: row;
gap: 20px;
width: 100%;
align-items: flex-start;
}
/* Label preview card styling */
.card.scan-form-card {
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: center;
min-height: 700px;
width: 330px;
flex-shrink: 0;
position: relative;
padding: 15px;
}
/* Data preview card styling */
.card.scan-table-card {
min-height: 700px;
width: calc(100% - 350px);
margin: 0;
}
/* View Orders and Upload Orders page specific layout - 25/75 split */
.card.report-form-card,
.card.scan-form-card {
min-height: 700px;
width: 25%;
flex-shrink: 0;
padding: 15px;
margin-bottom: 0; /* Remove bottom margin for horizontal layout */
}
.card.report-table-card,
.card.scan-table-card {
min-height: 700px;
width: 75%;
margin: 0;
padding: 15px;
}
/* Upload Orders specific table styling */
.card.scan-table-card table {
margin-bottom: 0;
}
/* Ensure proper scroll behavior for upload preview */
.card.scan-table-card[style*="overflow-y: auto"] {
/* Maintain scroll functionality while keeping consistent height */
max-height: 700px;
}
/* Label view title */
.label-view-title {
width: 100%;
text-align: center;
padding: 0 0 15px 0;
font-size: 18px;
font-weight: bold;
letter-spacing: 0.5px;
}
/* ==========================================================================
SEARCH AND FORM STYLES
========================================================================== */
/* Search card styling */
.search-card {
margin-bottom: 20px;
padding: 20px;
}
.search-field {
width: 100%;
max-width: 400px;
padding: 8px;
font-size: 14px;
border: 1px solid #ddd;
border-radius: 4px;
}
.quantity-field {
width: 100px;
padding: 8px;
font-size: 14px;
border: 1px solid #ddd;
border-radius: 4px;
}
.search-result-table {
margin-top: 15px;
margin-bottom: 15px;
}
/* ==========================================================================
BUTTON STYLES
========================================================================== */
.print-btn {
background-color: #28a745;
color: white;
padding: 10px 20px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
}
.print-btn:hover {
background-color: #218838;
}
.print-btn:disabled {
background-color: #6c757d;
cursor: not-allowed;
}
/* ==========================================================================
REPORT TABLE CONTAINER STYLES
========================================================================== */
.report-table-card h3 {
margin: 0 0 15px 0 !important;
padding: 0 !important;
}
.report-table-card {
padding: 15px !important;
}
/* ==========================================================================
PRINT MODULE SPECIFIC LAYOUT ADJUSTMENTS
========================================================================== */
/* For print_lost_labels.html - Two-column layout */
.scan-container.lost-labels {
display: flex;
flex-direction: column;
gap: 0;
width: 100%;
}
.scan-container.lost-labels .search-card {
width: 100%;
max-height: 100px;
min-height: 70px;
display: flex;
align-items: center;
flex-wrap: wrap;
margin-bottom: 24px;
}
.scan-container.lost-labels .row-container {
display: flex;
flex-direction: row;
gap: 24px;
width: 100%;
align-items: flex-start;
}
/* ==========================================================================
PRINT OPTIONS STYLES
========================================================================== */
/* Print method selection */
.print-method-container {
margin-bottom: 15px;
}
.print-method-label {
font-size: 12px;
font-weight: 600;
color: #495057;
margin-bottom: 8px;
}
.form-check {
margin-bottom: 6px;
}
.form-check-label {
font-size: 11px;
line-height: 1.2;
}
/* Printer selection styling */
#qztray-printer-selection {
margin-bottom: 10px;
}
#qztray-printer-selection label {
font-size: 11px;
font-weight: 600;
color: #495057;
margin-bottom: 3px;
display: block;
}
#qztray-printer-select {
font-size: 11px;
padding: 3px 6px;
}
/* Print button styling */
#print-label-btn {
font-size: 13px;
padding: 8px 24px;
border-radius: 5px;
font-weight: 600;
}
/* QZ Tray info section */
#qztray-info {
width: 100%;
margin-top: 15px;
padding-top: 15px;
border-top: 1px solid #e9ecef;
}
#qztray-info .info-box {
background: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 6px;
padding: 10px;
text-align: center;
}
#qztray-info .info-text {
font-size: 10px;
color: #495057;
margin-bottom: 8px;
}
#qztray-info .download-link {
font-size: 10px;
padding: 4px 16px;
}
/* ==========================================================================
BADGE AND STATUS STYLES
========================================================================== */
.badge {
font-size: 9px;
padding: 2px 6px;
}
.badge-success {
background-color: #28a745;
color: white;
}
.badge-danger {
background-color: #dc3545;
color: white;
}
.badge-warning {
background-color: #ffc107;
color: #212529;
}
/* Status indicators */
#qztray-status {
font-size: 9px;
padding: 2px 6px;
}
/* ==========================================================================
RESPONSIVE DESIGN
========================================================================== */
@media (max-width: 1200px) {
.scan-container {
flex-direction: column;
gap: 15px;
}
.card.scan-form-card {
width: 100%;
}
.card.scan-table-card {
width: 100%;
}
/* View Orders and Upload Orders page responsive */
.card.report-form-card,
.card.scan-form-card {
width: 100%;
margin-bottom: 24px; /* Restore bottom margin for stacked layout */
}
.card.report-table-card,
.card.scan-table-card {
width: 100%;
}
}
@media (max-width: 992px) and (min-width: 769px) {
/* Tablet view - adjust proportions for better fit */
.card.report-form-card,
.card.scan-form-card {
width: 30%;
}
.card.report-table-card,
.card.scan-table-card {
width: 70%;
}
}
@media (max-width: 768px) {
.label-view-title {
font-size: 16px;
}
#label-preview {
width: 280px;
height: 400px;
}
#label-content {
width: 200px;
height: 290px;
}
.search-field {
max-width: 300px;
}
}
/* ==========================================================================
THEME SUPPORT (Light/Dark Mode)
========================================================================== */
/* CSS Custom Properties for Theme Support */
:root {
/* Light mode colors (default) */
--print-table-header-bg: #e9ecef;
--print-table-header-text: #000;
--print-table-body-bg: #fff;
--print-table-body-text: #000;
--print-table-border: #ddd;
--print-table-hover: #f8f9fa;
--print-table-selected: #007bff;
--print-card-bg: #fff;
--print-card-border: #ddd;
--print-search-field-bg: #fff;
--print-search-field-text: #000;
--print-search-field-border: #ddd;
}
/* Light mode theme variables */
body.light-mode {
--print-table-header-bg: #e9ecef;
--print-table-header-text: #000;
--print-table-body-bg: #fff;
--print-table-body-text: #000;
--print-table-border: #ddd;
--print-table-hover: #f8f9fa;
--print-table-selected: #007bff;
--print-card-bg: #fff;
--print-card-border: #ddd;
--print-search-field-bg: #fff;
--print-search-field-text: #000;
--print-search-field-border: #ddd;
}
/* Dark mode theme variables */
body.dark-mode {
--print-table-header-bg: #2a3441;
--print-table-header-text: #ffffff;
--print-table-body-bg: #2a3441;
--print-table-body-text: #ffffff;
--print-table-border: #495057;
--print-table-hover: #3a4451;
--print-table-selected: #007bff;
--print-card-bg: #2a2a2a;
--print-card-border: #555;
--print-search-field-bg: #333;
--print-search-field-text: #fff;
--print-search-field-border: #555;
}
/* Label Preview Theme Support */
body.light-mode #label-preview {
background: #fafafa;
border: none;
}
body.light-mode #label-content {
background: white;
border: 1px solid #ddd;
}
body.light-mode #barcode-frame,
body.light-mode #vertical-barcode-frame {
background: #ffffff;
border: 1px solid var(--print-card-border);
}
body.dark-mode #label-preview {
background: #2a2a2a;
border: none;
}
body.dark-mode #label-content {
background: #f8f9fa;
border: 1px solid #555;
}
body.dark-mode #barcode-frame,
body.dark-mode #vertical-barcode-frame {
background: #ffffff;
border: 1px solid var(--print-card-border);
}
/* Card Theme Support */
body.dark-mode .search-card,
body.dark-mode .card {
background-color: var(--print-card-bg);
border: 1px solid var(--print-card-border);
color: var(--print-table-body-text);
}
/* Search Field Theme Support */
body.dark-mode .search-field,
body.dark-mode .quantity-field {
background-color: var(--print-search-field-bg);
border: 1px solid var(--print-search-field-border);
color: var(--print-search-field-text);
}
/* Button Theme Support */
body.dark-mode .print-btn {
background-color: #28a745;
}
body.dark-mode .print-btn:hover {
background-color: #218838;
}
/* ==========================================================================
UTILITY CLASSES
========================================================================== */
.text-center {
text-align: center;
}
.font-weight-bold {
font-weight: bold;
}
.margin-bottom-15 {
margin-bottom: 15px;
}
.padding-10 {
padding: 10px;
}
.full-width {
width: 100%;
}
.flex-center {
display: flex;
align-items: center;
justify-content: center;
}
/* ==========================================================================
DEBUG STYLES (can be removed in production)
========================================================================== */
.debug-border {
border: 2px solid red !important;
}
.debug-bg {
background-color: rgba(255, 0, 0, 0.1) !important;
}
/* ==========================================================================
PRINT MODULE SPECIFIC STYLES
========================================================================== */
/* Label preview container styling for print_module page */
.scan-form-card {
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: center;
min-height: 700px;
position: relative;
padding: 15px;
}
/* Label preview section */
#label-preview {
border: 1px solid #ddd;
padding: 10px;
position: relative;
background: #fafafa;
width: 100%;
max-width: 301px;
height: 434.7px;
margin: 0 auto;
}
/* Ensure label content scales properly in responsive layout */
@media (max-width: 1024px) {
#label-preview {
max-width: 280px;
height: 404px;
}
.scan-form-card {
padding: 10px;
}
}
@media (max-width: 768px) {
#label-preview {
max-width: 100%;
height: 350px;
}
.scan-form-card {
padding: 8px;
}
}
/* ==========================================================================
FORM CONTROLS FIX
========================================================================== */
/* Fix radio button styling to prevent oval display issues */
.form-check-input[type="radio"] {
width: 1rem !important;
height: 1rem !important;
margin-top: 0.25rem !important;
border: 1px solid #dee2e6 !important;
border-radius: 50% !important;
background-color: #fff !important;
appearance: none !important;
-webkit-appearance: none !important;
-moz-appearance: none !important;
}
.form-check-input[type="radio"]:checked {
background-color: #007bff !important;
border-color: #007bff !important;
background-image: radial-gradient(circle, #fff 30%, transparent 32%) !important;
}
.form-check-input[type="radio"]:focus {
box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25) !important;
outline: none !important;
}
.form-check {
display: flex !important;
align-items: flex-start !important;
margin-bottom: 0.5rem !important;
}
.form-check-label {
margin-left: 0.5rem !important;
cursor: pointer !important;
}

1236
app/static/style.css Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,35 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}Print Module - Quality App{% endblock %}</title>
<!-- Minimal styling - only essential Bootstrap for structure -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
<style>
/* Minimal reset and base styles */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
background-color: #f5f5f5;
color: #333;
}
</style>
{% block head %}{% endblock %}
</head>
<body>
{% block content %}{% endblock %}
<!-- Bootstrap JS - minimal, only for form handling -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
{% block scripts %}{% endblock %}
</body>
</html>

File diff suppressed because it is too large Load Diff

View File

@@ -1,134 +1,70 @@
{% extends "base.html" %}
{% block head %}
<!-- Print Lost Labels CSS with theme support -->
<style>
/* Print Module Theme Support */
.scan-container {
background-color: var(--bg-primary);
color: var(--text-primary);
transition: background-color 0.3s ease, color 0.3s ease;
}
.card.search-card,
.card.scan-form-card,
.card.scan-table-card {
background-color: var(--bg-primary);
border-color: var(--border-color);
color: var(--text-primary);
transition: background-color 0.3s ease, color 0.3s ease, border-color 0.3s ease;
}
.scan-table {
background-color: var(--bg-primary);
color: var(--text-primary);
}
.scan-table thead th {
background-color: var(--bg-secondary);
color: var(--text-primary);
border-color: var(--border-color);
}
.scan-table tbody td {
background-color: var(--bg-primary);
color: var(--text-primary);
border-color: var(--border-color);
}
.scan-table tbody tr:hover {
background-color: rgba(13, 110, 253, 0.08);
}
.form-control,
.form-control-sm,
select {
background-color: var(--input-bg);
color: var(--text-primary);
border-color: var(--input-border);
}
.form-control:focus,
.form-control-sm:focus,
select:focus {
background-color: var(--input-bg);
color: var(--text-primary);
border-color: var(--input-focus-border);
box-shadow: 0 0 0 3px var(--input-focus-shadow);
}
{# .floating-help-btn {
position: fixed;
bottom: 20px;
right: 20px;
z-index: 1000;
}
.floating-help-btn a {
display: inline-block;
width: 50px;
height: 50px;
background-color: var(--bg-secondary);
color: var(--text-primary);
border-radius: 50%;
text-align: center;
line-height: 50px;
font-size: 24px;
transition: all 0.3s ease;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
}
.floating-help-btn a:hover {
transform: scale(1.1);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
} #}
/* Compact table styling for print_lost_labels page */
.print-lost-labels-compact .scan-table.print-module-table {
font-size: 10px;
}
.print-lost-labels-compact .scan-table.print-module-table thead th {
font-size: 10px;
padding: 6px 8px;
line-height: 1.2;
}
.print-lost-labels-compact .scan-table.print-module-table tbody td {
font-size: 9px;
padding: 4px 6px;
line-height: 1.3;
}
/* Keep important data slightly larger and bold */
.print-lost-labels-compact .scan-table.print-module-table tbody td:nth-child(2) {
font-size: 10px;
font-weight: 600;
}
/* Make numbers more compact */
.print-lost-labels-compact .scan-table.print-module-table tbody td:nth-child(5),
.print-lost-labels-compact .scan-table.print-module-table tbody td:nth-child(9),
.print-lost-labels-compact .scan-table.print-module-table tbody td:nth-child(13) {
font-size: 9px;
}
/* Reduce row height */
.print-lost-labels-compact .scan-table.print-module-table tbody tr {
height: auto;
min-height: 24px;
}
{% endblock %}
{% block content %}
<!-- Floating Help Button -->
{# <div class="floating-help-btn">
<a href="{{ url_for('labels.help', page='print_lost_labels') }}" target="_blank" title="Print Lost Labels Help">
📖
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Print Lost Labels - Quality App</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
<!-- Base CSS from new app for header and theme styling -->
<link rel="stylesheet" href="/static/css/base.css">
<link rel="stylesheet" href="/static/css/theme.css">
<!-- Print Module CSS from original app -->
<link rel="stylesheet" href="/static/css/print_module.css">
<!-- Libraries for barcode and PDF generation -->
<script src="https://cdn.jsdelivr.net/npm/jsbarcode@3.11.5/dist/JsBarcode.all.min.js"></script>
</head>
<body>
<!-- Navigation Header from App -->
<nav class="navbar navbar-expand-lg navbar-dark bg-dark sticky-top">
<div class="container-fluid">
<a class="navbar-brand" href="/">
<i class="fas fa-chart-bar"></i> Quality App v2
</a>
</div> #}
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav ms-auto">
<li class="nav-item">
<a class="nav-link" href="/labels/">
<i class="fas fa-arrow-left"></i> Back to Labels
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/">
<i class="fas fa-home"></i> Dashboard
</a>
</li>
<li class="nav-item">
<button id="themeToggleBtn" class="theme-toggle nav-link" type="button" title="Switch Theme">
<i class="fas fa-moon"></i>
</button>
</li>
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" id="userDropdown" role="button" data-bs-toggle="dropdown">
<i class="fas fa-user"></i> User Menu
</a>
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="userDropdown">
<li>
<a class="dropdown-item" href="/profile/">
<i class="fas fa-user-circle"></i> Profile
</a>
</li>
<li><hr class="dropdown-divider"></li>
<li>
<a class="dropdown-item" href="/logout/">
<i class="fas fa-sign-out-alt"></i> Logout
</a>
</li>
</ul>
</li>
</ul>
</div>
</div>
</nav>
<!-- Print Lost Labels Module Content -->
<!-- ROW 1: Search Card (full width) -->
<div class="scan-container lost-labels print-lost-labels-compact">
<div class="card search-card">
@@ -150,10 +86,8 @@
<label for="client-select" style="font-size: 11px; font-weight: 600; display: block; margin-bottom: 4px;">Select Printer/Client:</label>
<select id="client-select" class="form-control form-control-sm" style="width: 85%; margin: 0 auto; font-size: 11px;"></select>
</div>
<!-- Manage Keys Button - Only visible for superadmin -->
{% if session.role == 'superadmin' %}
<a href="{{ url_for('main.download_extension') }}" class="btn btn-info btn-sm" target="_blank" style="font-size: 11px; padding: 4px 12px;">🔑 Manage Keys</a>
{% endif %}
<!-- Manage Keys Button - Only visible for admin/superadmin -->
<a href="/download-extension" class="btn btn-info btn-sm" target="_blank" style="font-size: 11px; padding: 4px 12px; display: none;" id="manage-keys-btn">🔑 Manage Keys</a>
</div>
<!-- Label Preview Section -->
<div id="label-preview" style="padding: 10px; position: relative; background: #fafafa; width: 301px; height: 434.7px;">
@@ -251,9 +185,9 @@
<small>(e.g., CP00000711-001, 002, ...)</small>
</div>
<!-- QZ Tray Installation Info - Simplified -->
<div id="qztray-info" style="width: 100%; margin-top: 15px; padding-top: 15px; border-top: 1px solid #e9ecef;">
<div style="background: #f8f9fa; border: 1px solid #dee2e6; border-radius: 6px; padding: 10px; text-align: center;">
<div style="font-size: 10px; color: #495057; margin-bottom: 8px;">
<div id="qztray-info" style="width: 100%; margin-top: 15px; padding-top: 15px; border-top: 1px solid var(--border-color);">
<div style="background: var(--bg-tertiary); border: 1px solid var(--border-color); border-radius: 6px; padding: 10px; text-align: center;">
<div style="font-size: 10px; color: var(--text-secondary); margin-bottom: 8px;">
QZ Tray is required for direct printing
</div>
<a href="https://filebrowser.moto-adv.com/filebrowser/api/public/dl/Fk0ZaiEY/QP_Tray/qz-tray-2.2.6-SNAPSHOT-x86_64.exe?token=TJ7gSu3CRcWWQuyFLoZv5I8j4diDjP47DDqWRtM0oKAx-2_orj1stfKPJsuuqKR9mE2GQNm1jlZ0BPR7lfZ3gHmu56SkY9fC5AJlC9n_80oX643ojlGc-U7XVb1SDd0w" class="btn btn-outline-secondary btn-sm" style="font-size: 10px; padding: 4px 16px;">
@@ -299,15 +233,15 @@
<!-- JavaScript Libraries -->
<!-- JsBarcode library for real barcode generation -->
<script src="{{ url_for('static', filename='JsBarcode.all.min.js') }}"></script>
<script src="https://cdn.jsdelivr.net/npm/jsbarcode@3.11.5/dist/JsBarcode.all.min.js"></script>
<!-- Add html2canvas library for capturing preview as image -->
<script src="{{ url_for('static', filename='html2canvas.min.js') }}"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js"></script>
<!-- PATCHED QZ Tray library - works with custom server using pairing key authentication -->
<script src="{{ url_for('static', filename='qz-tray.js') }}"></script>
<script src="https://qz.glyphtree.com/api/latest/qz-tray.js"></script>
<script>
// Store all orders data for searching
const allOrders = {{ orders|tojson|safe }};
// Store all orders data for searching (will be populated by API fetch on page load)
let allOrders = [];
let selectedOrderData = null;
// QZ Tray Integration
@@ -536,8 +470,18 @@ async function loadQZTrayPrinters() {
// Print Button Handler
document.addEventListener('DOMContentLoaded', function() {
// Fetch printed orders from API
fetch('/labels/api/printed-orders')
.then(response => response.json())
.then(data => {
allOrders = data.orders || [];
// Display last 20 printed orders on page load
displayRecentOrders(20);
})
.catch(error => {
console.error('Error fetching orders:', error);
alert('Error loading orders. Please refresh the page.');
});
setTimeout(initializeQZTray, 1000);
document.getElementById('print-label-btn').addEventListener('click', async function(e) {
@@ -666,7 +610,7 @@ async function generatePDFAndPrint(selectedPrinter, orderData, pieceNumber, tota
piece_number: pieceNumber,
total_pieces: totalPieces
};
const response = await fetch('/generate_label_pdf', {
const response = await fetch('/labels/api/generate-pdf', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(pdfData)
@@ -710,7 +654,7 @@ function handlePDFGeneration(selectedRow) {
const originalText = button.textContent;
button.textContent = 'Generating PDF...';
button.disabled = true;
fetch(`/generate_labels_pdf/${orderId}/true`, {
fetch(`/labels/api/generate-pdf/${orderId}/true`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' }
})
@@ -982,4 +926,50 @@ function printLabels() {
window.location.href = url;
}
</script>
{% endblock %}
<!-- Bootstrap JS for navbar functionality -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
<!-- Theme toggle functionality -->
<script>
// Initialize theme from localStorage or default to light
function initializeTheme() {
const savedTheme = localStorage.getItem('theme') || 'light';
document.documentElement.setAttribute('data-theme', savedTheme);
document.body.className = savedTheme + '-mode';
updateThemeButton();
}
// Update theme button icon
function updateThemeButton() {
const themeToggleBtn = document.getElementById('themeToggleBtn');
if (themeToggleBtn) {
const currentTheme = document.documentElement.getAttribute('data-theme');
if (currentTheme === 'dark') {
themeToggleBtn.innerHTML = '<i class="fas fa-sun"></i>';
themeToggleBtn.title = 'Switch to Light Mode';
} else {
themeToggleBtn.innerHTML = '<i class="fas fa-moon"></i>';
themeToggleBtn.title = 'Switch to Dark Mode';
}
}
}
// Initialize theme on page load
initializeTheme();
// Theme toggle button handler (if it exists)
const themeToggleBtn = document.getElementById('themeToggleBtn');
if (themeToggleBtn) {
themeToggleBtn.addEventListener('click', function() {
const currentTheme = document.documentElement.getAttribute('data-theme');
const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
document.documentElement.setAttribute('data-theme', newTheme);
document.body.className = newTheme + '-mode';
localStorage.setItem('theme', newTheme);
updateThemeButton();
});
}
</script>
</body>
</html>

View File

@@ -0,0 +1,912 @@
{% extends "base.html" %}
{% block head %}
<!-- Print Module CSS is now loaded via base.html for all printing pages -->
<style>
/* Compact table styling for print_lost_labels page */
.print-lost-labels-compact .scan-table.print-module-table {
font-size: 10px;
}
.print-lost-labels-compact .scan-table.print-module-table thead th {
font-size: 10px;
padding: 6px 8px;
line-height: 1.2;
}
.print-lost-labels-compact .scan-table.print-module-table tbody td {
font-size: 9px;
padding: 4px 6px;
line-height: 1.3;
}
/* Keep important data slightly larger and bold */
.print-lost-labels-compact .scan-table.print-module-table tbody td:nth-child(2) {
font-size: 10px;
font-weight: 600;
}
/* Make numbers more compact */
.print-lost-labels-compact .scan-table.print-module-table tbody td:nth-child(5),
.print-lost-labels-compact .scan-table.print-module-table tbody td:nth-child(9),
.print-lost-labels-compact .scan-table.print-module-table tbody td:nth-child(13) {
font-size: 9px;
}
/* Reduce row height */
.print-lost-labels-compact .scan-table.print-module-table tbody tr {
height: auto;
min-height: 24px;
}
/* Adjust header title */
.print-lost-labels-compact .card.scan-table-card h3 {
font-size: 16px;
padding: 8px 0;
margin-bottom: 8px;
}
</style>
{% endblock %}
{% block content %}
<!-- Floating Help Button -->
<div class="floating-help-btn">
<a href="{{ url_for('main.help', page='print_lost_labels') }}" target="_blank" title="Print Lost Labels Help">
📖
</a>
</div>
<!-- ROW 1: Search Card (full width) -->
<div class="scan-container lost-labels print-lost-labels-compact">
<div class="card search-card">
<div style="display: flex; align-items: center; gap: 15px; flex-wrap: wrap;">
<label for="search-input" style="font-weight: bold; white-space: nowrap;">Search Order (CP...):</label>
<input type="text" id="search-input" class="search-field" placeholder="Type to search..." oninput="searchOrder()" style="flex: 1; min-width: 200px; max-width: 300px;">
<button id="fetch-matching-btn" class="btn btn-secondary" style="padding: 7px 16px; font-size: 14px; white-space: nowrap;" onclick="fetchMatchingOrders()">Find All</button>
</div>
</div>
<!-- ROW 2: Two cards side by side (25% / 75% layout) -->
<div class="row-container">
<!-- Print Preview Card (left, 25% width) -->
<div class="card scan-form-card" style="display: flex; flex-direction: column; justify-content: flex-start; align-items: center; min-height: 700px; flex: 0 0 25%; position: relative; padding: 15px;">
<div class="label-view-title" style="width: 100%; text-align: center; padding: 0 0 15px 0; font-size: 18px; font-weight: bold; letter-spacing: 0.5px;">Label View</div>
<!-- Pairing Keys Section -->
<div style="width: 100%; text-align: center; margin-bottom: 15px;">
<div id="client-select-container" style="display: none; margin-bottom: 8px;">
<label for="client-select" style="font-size: 11px; font-weight: 600; display: block; margin-bottom: 4px;">Select Printer/Client:</label>
<select id="client-select" class="form-control form-control-sm" style="width: 85%; margin: 0 auto; font-size: 11px;"></select>
</div>
<!-- Manage Keys Button - Only visible for superadmin -->
{% if session.role == 'superadmin' %}
<a href="{{ url_for('main.download_extension') }}" class="btn btn-info btn-sm" target="_blank" style="font-size: 11px; padding: 4px 12px;">🔑 Manage Keys</a>
{% endif %}
</div>
<!-- Label Preview Section -->
<div id="label-preview" style="padding: 10px; position: relative; background: #fafafa; width: 301px; height: 434.7px;">
<!-- ...label content rectangle and barcode frames as in print_module.html... -->
<div id="label-content" style="position: absolute; top: 65.7px; left: 11.34px; width: 227.4px; height: 321.3px; background: white;">
<div style="position: absolute; top: 0; left: 0; right: 0; height: 32.13px; display: flex; align-items: center; justify-content: center; font-weight: bold; font-size: 12px; color: #000; z-index: 10;"></div>
<div id="customer-name-row" style="position: absolute; top: 32.13px; left: 0; right: 0; height: 32.13px; display: flex; align-items: center; justify-content: center; font-size: 11px; color: #000;"></div>
<div style="position: absolute; top: 32.13px; left: 0; right: 0; height: 1px; background: #999;"></div>
<div style="position: absolute; top: 64.26px; left: 0; right: 0; height: 1px; background: #999;"></div>
<div style="position: absolute; top: 96.39px; left: 0; right: 0; height: 1px; background: #999;"></div>
<div style="position: absolute; top: 128.52px; left: 0; right: 0; height: 1px; background: #999;"></div>
<div style="position: absolute; top: 160.65px; left: 0; right: 0; height: 1px; background: #999;"></div>
<div style="position: absolute; top: 224.91px; left: 0; right: 0; height: 1px; background: #999;"></div>
<div style="position: absolute; top: 257.04px; left: 0; right: 0; height: 1px; background: #999;"></div>
<div style="position: absolute; top: 289.17px; left: 0; right: 0; height: 1px; background: #999;"></div>
<div style="position: absolute; left: 90.96px; top: 64.26px; width: 1px; height: 257.04px; background: #999;"></div>
<div style="position: absolute; top: 64.26px; left: 0; width: 90.96px; height: 32.13px; display: flex; align-items: center; padding-left: 5px; font-size: 10px; color: #000;">Quantity ordered</div>
<div id="quantity-ordered-value" style="position: absolute; top: 64.26px; left: 90.96px; width: 136.44px; height: 32.13px; display: flex; align-items: center; justify-content: center; font-size: 13px; font-weight: bold; color: #000;"></div>
<div style="position: absolute; top: 96.39px; left: 0; width: 90.96px; height: 32.13px; display: flex; align-items: center; padding-left: 5px; font-size: 10px; color: #000;">Customer order</div>
<div id="client-order-info" style="position: absolute; top: 96.39px; left: 90.96px; width: 136.44px; height: 32.13px; display: flex; align-items: center; justify-content: center; font-size: 12px; font-weight: bold; color: #000;"></div>
<div style="position: absolute; top: 128.52px; left: 0; width: 90.96px; height: 32.13px; display: flex; align-items: center; padding-left: 5px; font-size: 10px; color: #000;">Delivery date</div>
<div id="delivery-date-value" style="position: absolute; top: 128.52px; left: 90.96px; width: 136.44px; height: 32.13px; display: flex; align-items: center; justify-content: center; font-size: 12px; font-weight: bold; color: #000;"></div>
<div style="position: absolute; top: 160.65px; left: 0; width: 90.96px; height: 64.26px; display: flex; align-items: center; padding-left: 5px; font-size: 10px; color: #000;">Product description</div>
<div id="description-value" style="position: absolute; top: 160.65px; left: 90.96px; width: 136.44px; height: 64.26px; display: flex; align-items: center; justify-content: center; font-size: 8px; color: #000; text-align: center; padding: 2px; overflow: hidden;"></div>
<div style="position: absolute; top: 224.91px; left: 0; width: 90.96px; height: 32.13px; display: flex; align-items: center; padding-left: 5px; font-size: 10px; color: #000;">Size</div>
<div id="size-value" style="position: absolute; top: 224.91px; left: 90.96px; width: 136.44px; height: 32.13px; display: flex; align-items: center; justify-content: center; font-size: 10px; font-weight: bold; color: #000;"></div>
<div style="position: absolute; top: 257.04px; left: 0; width: 90.96px; height: 32.13px; display: flex; align-items: center; padding-left: 5px; font-size: 10px; color: #000;">Article code</div>
<div id="article-code-value" style="position: absolute; top: 257.04px; left: 90.96px; width: 136.44px; height: 32.13px; display: flex; align-items: center; justify-content: center; font-size: 9px; font-weight: bold; color: #000;"></div>
<div style="position: absolute; top: 289.17px; left: 0; width: 90.96px; height: 32.13px; display: flex; align-items: center; padding-left: 5px; font-size: 10px; color: #000;">Prod. order</div>
<div id="prod-order-value" style="position: absolute; top: 289.17px; left: 90.96px; width: 136.44px; height: 32.13px; display: flex; align-items: center; justify-content: center; font-size: 10px; font-weight: bold; color: #000;"></div>
</div>
<div id="barcode-frame">
<svg id="barcode-display"></svg>
<div id="barcode-text"></div>
</div>
<div id="vertical-barcode-frame">
<svg id="vertical-barcode-display"></svg>
<div id="vertical-barcode-text"></div>
</div>
</div>
<!-- Print Options (copied from print_module.html) -->
<div style="width: 100%; margin-top: 20px;">
<!-- Print Method Selection -->
<div style="margin-bottom: 15px;" role="group" aria-labelledby="print-method-label">
<div id="print-method-label" style="font-size: 12px; font-weight: 600; color: #495057; margin-bottom: 8px;">
📄 Print Method:
</div>
<!-- Print method options in horizontal layout -->
<div style="display: flex; gap: 15px; flex-wrap: wrap;">
<div class="form-check" style="margin-bottom: 6px;">
<input class="form-check-input" type="radio" name="printMethod" id="qzTrayPrint" value="qztray" checked>
<label class="form-check-label" for="qzTrayPrint" style="font-size: 11px; line-height: 1.2;">
<strong>🖨️ Direct Print</strong> <span id="qztray-status" class="badge badge-success" style="font-size: 9px; padding: 2px 6px;">Ready</span>
</label>
</div>
<div class="form-check" id="pdf-option-container" style="display: none; margin-bottom: 6px;">
<input class="form-check-input" type="radio" name="printMethod" id="pdfGenerate" value="pdf">
<label class="form-check-label" for="pdfGenerate" style="font-size: 11px; line-height: 1.2;">
<strong>📄 PDF Export</strong> <span class="text-muted" style="font-size: 10px;">(fallback)</span>
</label>
</div>
</div>
</div>
<!-- Printer Selection for QZ Tray (Compact) -->
<div id="qztray-printer-selection" style="margin-bottom: 10px;">
<label for="qztray-printer-select" style="font-size: 11px; font-weight: 600; color: #495057; margin-bottom: 3px; display: block;">
Printer:
</label>
<select id="qztray-printer-select" class="form-control form-control-sm" style="font-size: 11px; padding: 3px 6px;">
<option value="">Loading...</option>
</select>
</div>
<!-- Labels Range Selection -->
<div style="margin-bottom: 15px;">
<label for="labels-range-input" style="font-size: 11px; font-weight: 600; color: #495057; margin-bottom: 3px; display: block;">
Select Labels Range:
</label>
<input type="text" id="labels-range-input" class="form-control form-control-sm"
placeholder="e.g., 003 or 003-007"
style="font-size: 11px; padding: 3px 6px; text-align: center;">
<div style="font-size: 9px; color: #6c757d; margin-top: 2px; text-align: center;">
Single: "005" | Range: "003-007" | Leave empty for all
</div>
</div>
<!-- Print Button -->
<div style="width: 100%; text-align: center; margin-bottom: 10px;">
<button id="print-label-btn" class="btn btn-success" style="font-size: 13px; padding: 8px 24px; border-radius: 5px; font-weight: 600;">
🖨️ Print Labels
</button>
</div>
<!-- Print Information -->
<div style="width: 100%; text-align: center; color: #6c757d; font-size: 10px; line-height: 1.3;">
<small>(e.g., CP00000711-001, 002, ...)</small>
</div>
<!-- QZ Tray Installation Info - Simplified -->
<div id="qztray-info" style="width: 100%; margin-top: 15px; padding-top: 15px; border-top: 1px solid #e9ecef;">
<div style="background: #f8f9fa; border: 1px solid #dee2e6; border-radius: 6px; padding: 10px; text-align: center;">
<div style="font-size: 10px; color: #495057; margin-bottom: 8px;">
QZ Tray is required for direct printing
</div>
<a href="https://filebrowser.moto-adv.com/filebrowser/api/public/dl/Fk0ZaiEY/QP_Tray/qz-tray-2.2.6-SNAPSHOT-x86_64.exe?token=TJ7gSu3CRcWWQuyFLoZv5I8j4diDjP47DDqWRtM0oKAx-2_orj1stfKPJsuuqKR9mE2GQNm1jlZ0BPR7lfZ3gHmu56SkY9fC5AJlC9n_80oX643ojlGc-U7XVb1SDd0w" class="btn btn-outline-secondary btn-sm" style="font-size: 10px; padding: 4px 16px;">
📥 Download QZ Tray
</a>
</div>
</div>
</div>
</div>
<!-- Orders Table Card (right, 75% width) -->
<div class="card scan-table-card" style="min-height: 700px; flex: 0 0 75%; margin: 0;">
<h3>Data Preview</h3>
<div class="report-table-container">
<table class="scan-table print-module-table">
<thead>
<tr>
<th>ID</th>
<th>Comanda Productie</th>
<th>Cod Articol</th>
<th>Descr. Com. Prod</th>
<th>Cantitate</th>
<th>Data Livrare</th>
<th>Dimensiune</th>
<th>Com. Achiz. Client</th>
<th>Nr. Linie</th>
<th>Customer Name</th>
<th>Customer Art. Nr.</th>
<th>Open Order</th>
<th>Line</th>
<th>Printed</th>
<th>Created</th>
<th>Qty to Print</th>
</tr>
</thead>
<tbody id="unprinted-orders-table">
<!-- Data will be dynamically loaded here -->
</tbody>
</table>
</div>
</div>
</div>
</div>
<!-- JavaScript Libraries -->
<!-- JsBarcode library for real barcode generation -->
<script src="{{ url_for('static', filename='JsBarcode.all.min.js') }}"></script>
<!-- Add html2canvas library for capturing preview as image -->
<script src="{{ url_for('static', filename='html2canvas.min.js') }}"></script>
<!-- PATCHED QZ Tray library - works with custom server using pairing key authentication -->
<script src="{{ url_for('static', filename='qz-tray.js') }}"></script>
<script>
// Store all orders data for searching
const allOrders = {{ orders|tojson|safe }};
let selectedOrderData = null;
// QZ Tray Integration
let qzTray = null;
let availablePrinters = [];
// Function to display the last N orders in the table
function displayRecentOrders(limit = 20) {
const tbody = document.getElementById('unprinted-orders-table');
tbody.innerHTML = '';
if (allOrders.length === 0) {
tbody.innerHTML = '<tr><td colspan="16" style="text-align:center; color:#6c757d;">No printed orders found.</td></tr>';
return;
}
// Get the last N orders (they are already sorted by updated_at DESC from backend)
const recentOrders = allOrders.slice(0, limit);
recentOrders.forEach((order, idx) => {
const tr = document.createElement('tr');
// Format data_livrare as DD/MM/YYYY if possible
let dataLivrareFormatted = '-';
if (order.data_livrare) {
const d = new Date(order.data_livrare);
if (!isNaN(d)) {
const day = String(d.getDate()).padStart(2, '0');
const month = String(d.getMonth() + 1).padStart(2, '0');
const year = d.getFullYear();
dataLivrareFormatted = `${day}/${month}/${year}`;
} else {
dataLivrareFormatted = order.data_livrare;
}
}
tr.innerHTML = `
<td>${order.id}</td>
<td><strong>${order.comanda_productie}</strong></td>
<td>${order.cod_articol || '-'}</td>
<td>${order.descr_com_prod}</td>
<td style="text-align: right; font-weight: 600;">${order.cantitate}</td>
<td style="text-align: center;">${dataLivrareFormatted}</td>
<td style="text-align: center;">${order.dimensiune || '-'}</td>
<td>${order.com_achiz_client || '-'}</td>
<td style="text-align: right;">${order.nr_linie_com_client || '-'}</td>
<td>${order.customer_name || '-'}</td>
<td>${order.customer_article_number || '-'}</td>
<td>${order.open_for_order || '-'}</td>
<td style="text-align: right;">${order.line_number || '-'}</td>
<td style="text-align: center;">${order.printed_labels == 1 ? '<span style="color: #28a745; font-weight: bold;">✓ Yes</span>' : '<span style="color: #dc3545;">✗ No</span>'}</td>
<td style="font-size: 11px; color: #6c757d;">${order.created_at || '-'}</td>
<td>1</td>
`;
tr.addEventListener('click', function() {
document.querySelectorAll('.print-module-table tbody tr').forEach(row => {
row.classList.remove('selected');
const cells = row.querySelectorAll('td');
cells.forEach(cell => {
cell.style.backgroundColor = '';
cell.style.color = '';
});
});
this.classList.add('selected');
const cells = this.querySelectorAll('td');
cells.forEach(cell => {
cell.style.backgroundColor = '#007bff';
cell.style.color = 'white';
});
updatePreviewCard(order);
});
tbody.appendChild(tr);
});
}
function searchOrder() {
const searchValue = document.getElementById('search-input').value.trim().toLowerCase();
if (!searchValue) {
selectedOrderData = null;
return;
}
// Search for matching order
const matchedOrder = allOrders.find(order =>
order.comanda_productie && order.comanda_productie.toLowerCase().includes(searchValue)
);
if (matchedOrder) {
selectedOrderData = matchedOrder;
} else {
selectedOrderData = null;
}
}
// Fetch all matching orders and populate preview table
function fetchMatchingOrders() {
const searchValue = document.getElementById('search-input').value.trim().toLowerCase();
if (!searchValue) {
alert('Please enter an order code to search.');
return;
}
// Find all matching orders
const matchingOrders = allOrders.filter(order =>
order.comanda_productie && order.comanda_productie.toLowerCase().includes(searchValue)
);
const tbody = document.getElementById('unprinted-orders-table');
tbody.innerHTML = '';
if (matchingOrders.length === 0) {
tbody.innerHTML = '<tr><td colspan="16" style="text-align:center; color:#dc3545;">No matching orders found.</td></tr>';
// Clear preview card
updatePreviewCard(null);
return;
}
matchingOrders.forEach((order, idx) => {
const tr = document.createElement('tr');
// Format data_livrare as DD/MM/YYYY if possible
let dataLivrareFormatted = '-';
if (order.data_livrare) {
const d = new Date(order.data_livrare);
if (!isNaN(d)) {
const day = String(d.getDate()).padStart(2, '0');
const month = String(d.getMonth() + 1).padStart(2, '0');
const year = d.getFullYear();
dataLivrareFormatted = `${day}/${month}/${year}`;
} else {
dataLivrareFormatted = order.data_livrare;
}
}
tr.innerHTML = `
<td>${order.id}</td>
<td><strong>${order.comanda_productie}</strong></td>
<td>${order.cod_articol || '-'}</td>
<td>${order.descr_com_prod}</td>
<td style="text-align: right; font-weight: 600;">${order.cantitate}</td>
<td style="text-align: center;">${dataLivrareFormatted}</td>
<td style="text-align: center;">${order.dimensiune || '-'}</td>
<td>${order.com_achiz_client || '-'}</td>
<td style="text-align: right;">${order.nr_linie_com_client || '-'}</td>
<td>${order.customer_name || '-'}</td>
<td>${order.customer_article_number || '-'}</td>
<td>${order.open_for_order || '-'}</td>
<td style="text-align: right;">${order.line_number || '-'}</td>
<td style="text-align: center;">${order.printed_labels == 1 ? '<span style=\"color: #28a745; font-weight: bold;\">✓ Yes</span>' : '<span style=\"color: #dc3545;\">✗ No</span>'}</td>
<td style="font-size: 11px; color: #6c757d;">${order.created_at || '-'}</td>
<td>1</td>
`;
tr.addEventListener('click', function() {
document.querySelectorAll('.print-module-table tbody tr').forEach(row => {
row.classList.remove('selected');
const cells = row.querySelectorAll('td');
cells.forEach(cell => {
cell.style.backgroundColor = '';
cell.style.color = '';
});
});
this.classList.add('selected');
const cells = this.querySelectorAll('td');
cells.forEach(cell => {
cell.style.backgroundColor = '#007bff';
cell.style.color = 'white';
});
updatePreviewCard(order);
});
tbody.appendChild(tr);
// Update preview card with the first matching order
if (idx === 0) updatePreviewCard(order);
});
}
// QZ Tray and Print Button Logic (copied/adapted from print_module.html)
async function initializeQZTray() {
try {
if (typeof qz === 'undefined') {
document.getElementById('qztray-status').textContent = 'Library Error';
document.getElementById('qztray-status').className = 'badge badge-danger';
return false;
}
qz.websocket.setClosedCallbacks(function() {
document.getElementById('qztray-status').textContent = 'Disconnected';
document.getElementById('qztray-status').className = 'badge badge-warning';
});
await qz.websocket.connect();
qzTray = qz;
const version = await qz.api.getVersion();
document.getElementById('qztray-status').textContent = 'Ready';
document.getElementById('qztray-status').className = 'badge badge-success';
document.getElementById('qztray-printer-selection').style.display = 'block';
document.getElementById('pdf-option-container').style.display = 'none';
await loadQZTrayPrinters();
return true;
} catch (error) {
document.getElementById('qztray-status').textContent = 'Not Connected';
document.getElementById('qztray-status').className = 'badge badge-danger';
document.getElementById('qztray-printer-selection').style.display = 'none';
document.getElementById('pdf-option-container').style.display = 'block';
document.getElementById('pdfGenerate').checked = true;
document.getElementById('qzTrayPrint').disabled = true;
return false;
}
}
async function loadQZTrayPrinters() {
try {
if (!qzTray) return;
const printers = await qzTray.printers.find();
availablePrinters = printers;
const printerSelect = document.getElementById('qztray-printer-select');
printerSelect.innerHTML = '<option value="">Select a printer...</option>';
printers.forEach(printer => {
const option = document.createElement('option');
option.value = printer;
option.textContent = printer;
printerSelect.appendChild(option);
});
const thermalPrinter = printers.find(p =>
p.toLowerCase().includes('thermal') ||
p.toLowerCase().includes('label') ||
p.toLowerCase().includes('zebra') ||
p.toLowerCase().includes('epson')
);
if (thermalPrinter) {
printerSelect.value = thermalPrinter;
}
} catch (error) {}
}
// Print Button Handler
document.addEventListener('DOMContentLoaded', function() {
// Display last 20 printed orders on page load
displayRecentOrders(20);
setTimeout(initializeQZTray, 1000);
document.getElementById('print-label-btn').addEventListener('click', async function(e) {
e.preventDefault();
const selectedRow = document.querySelector('.print-module-table tbody tr.selected');
if (!selectedRow) {
alert('Please select an order first from the table below.');
return;
}
const printMethod = document.querySelector('input[name="printMethod"]:checked').value;
if (printMethod === 'qztray') {
await handleQZTrayPrint(selectedRow);
} else {
handlePDFGeneration(selectedRow);
}
});
document.querySelectorAll('input[name="printMethod"]').forEach(radio => {
radio.addEventListener('change', updatePrintMethodUI);
});
updatePrintMethodUI();
});
function updatePrintMethodUI() {
const printMethod = document.querySelector('input[name="printMethod"]:checked').value;
const printerSelection = document.getElementById('qztray-printer-selection');
const printButton = document.getElementById('print-label-btn');
if (printMethod === 'qztray') {
printButton.textContent = '🖨️ Print Labels';
printButton.className = 'btn btn-primary';
} else {
printerSelection.style.display = 'none';
printButton.textContent = '📄 Generate PDF';
printButton.className = 'btn btn-success';
}
}
async function handleQZTrayPrint(selectedRow) {
try {
if (!qzTray) {
await initializeQZTray();
if (!qzTray) throw new Error('QZ Tray not available');
}
const selectedPrinter = document.getElementById('qztray-printer-select').value;
if (!selectedPrinter) {
alert('Please select a printer first');
return;
}
const cells = selectedRow.querySelectorAll('td');
const orderData = {
id: cells[0].textContent,
comanda_productie: cells[1].textContent.trim(),
cod_articol: cells[2].textContent.trim(),
descr_com_prod: cells[3].textContent.trim(),
cantitate: parseInt(cells[4].textContent.trim()),
data_livrare: cells[5].textContent.trim(),
dimensiune: cells[6].textContent.trim(),
com_achiz_client: cells[7].textContent.trim(),
nr_linie_com_client: cells[8].textContent.trim(),
customer_name: cells[9].textContent.trim()
};
// Parse labels range input
const labelsRangeInput = document.getElementById('labels-range-input').value.trim();
let labelNumbers = [];
if (labelsRangeInput) {
if (labelsRangeInput.includes('-')) {
// Range format: "003-007"
const rangeParts = labelsRangeInput.split('-');
if (rangeParts.length === 2) {
const start = parseInt(rangeParts[0]);
const end = parseInt(rangeParts[1]);
if (!isNaN(start) && !isNaN(end) && start > 0 && end >= start && end <= orderData.cantitate) {
for (let i = start; i <= end; i++) {
labelNumbers.push(i);
}
} else {
alert(`Invalid range. Please use format "001-${String(orderData.cantitate).padStart(3, '0')}" or single number.`);
return;
}
} else {
alert('Invalid range format. Use "003-007" format.');
return;
}
} else {
// Single number format: "005"
const singleNumber = parseInt(labelsRangeInput);
if (!isNaN(singleNumber) && singleNumber > 0 && singleNumber <= orderData.cantitate) {
labelNumbers.push(singleNumber);
} else {
alert(`Invalid label number. Please use 1-${orderData.cantitate}.`);
return;
}
}
} else {
// No range specified, print all labels (original behavior)
for (let i = 1; i <= orderData.cantitate; i++) {
labelNumbers.push(i);
}
}
// Print the specified labels
for (let i = 0; i < labelNumbers.length; i++) {
const labelNumber = labelNumbers[i];
await generatePDFAndPrint(selectedPrinter, orderData, labelNumber, orderData.cantitate);
if (i < labelNumbers.length - 1) await new Promise(resolve => setTimeout(resolve, 500));
}
// Show success message
const rangeText = labelsRangeInput ?
(labelNumbers.length === 1 ? `label ${String(labelNumbers[0]).padStart(3, '0')}` :
`labels ${String(labelNumbers[0]).padStart(3, '0')}-${String(labelNumbers[labelNumbers.length-1]).padStart(3, '0')}`) :
`all ${orderData.cantitate} labels`;
alert(`Successfully printed ${rangeText} for order ${orderData.comanda_productie}`);
} catch (error) {
alert('QZ Tray print error: ' + error.message);
}
}
async function generatePDFAndPrint(selectedPrinter, orderData, pieceNumber, totalPieces) {
try {
const pdfData = {
...orderData,
quantity: 1,
piece_number: pieceNumber,
total_pieces: totalPieces
};
const response = await fetch('/generate_label_pdf', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(pdfData)
});
if (!response.ok) throw new Error('Failed to generate PDF');
const pdfBlob = await response.blob();
const pdfBase64 = await new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onloadend = () => resolve(reader.result.split(',')[1]);
reader.onerror = reject;
reader.readAsDataURL(pdfBlob);
});
const config = qz.configs.create(selectedPrinter, {
scaleContent: false,
rasterize: false,
size: { width: 80, height: 100 },
units: 'mm',
margins: { top: 0, right: 0, bottom: 0, left: 0 }
});
const data = [{ type: 'pdf', format: 'base64', data: pdfBase64 }];
await qz.print(config, data);
} catch (error) {
throw error;
}
}
function handlePDFGeneration(selectedRow) {
// Check if labels range is specified
const labelsRangeInput = document.getElementById('labels-range-input').value.trim();
if (labelsRangeInput) {
alert('PDF generation currently supports printing all labels only. Please use QZ Tray for custom label ranges, or leave the range field empty for PDF generation.');
return;
}
const orderId = selectedRow.querySelector('td').textContent;
const quantityCell = selectedRow.querySelector('td:nth-child(5)');
const quantity = quantityCell ? parseInt(quantityCell.textContent) : 1;
const prodOrderCell = selectedRow.querySelector('td:nth-child(2)');
const prodOrder = prodOrderCell ? prodOrderCell.textContent.trim() : 'N/A';
const button = document.getElementById('print-label-btn');
const originalText = button.textContent;
button.textContent = 'Generating PDF...';
button.disabled = true;
fetch(`/generate_labels_pdf/${orderId}/true`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' }
})
.then(response => {
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
return response.blob();
})
.then(blob => {
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `labels_${prodOrder}_${quantity}pcs.pdf`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
const printWindow = window.open(url, '_blank');
if (printWindow) {
printWindow.focus();
setTimeout(() => {
printWindow.print();
setTimeout(() => { window.URL.revokeObjectURL(url); }, 2000);
}, 1500);
} else {
setTimeout(() => { window.URL.revokeObjectURL(url); }, 1000);
}
setTimeout(() => {}, 1000);
})
.catch(error => {
alert('Failed to generate PDF labels. Error: ' + error.message);
})
.finally(() => {
button.textContent = originalText;
button.disabled = false;
});
}
// Update the preview card with order data (as in print_module.html)
function updatePreviewCard(order) {
function set(id, value) {
const el = document.getElementById(id);
if (el) el.textContent = value || '';
}
// Always clear barcode SVGs before updating
const barcode = document.getElementById('barcode-display');
if (barcode) barcode.innerHTML = '';
const vbarcode = document.getElementById('vertical-barcode-display');
if (vbarcode) vbarcode.innerHTML = '';
if (!order) {
set('customer-name-row', '');
set('quantity-ordered-value', '');
set('client-order-info', '');
set('delivery-date-value', '');
set('description-value', '');
set('size-value', '');
set('article-code-value', '');
set('prod-order-value', '');
set('barcode-text', '');
set('vertical-barcode-text', '');
return;
}
set('customer-name-row', order.customer_name || '');
set('quantity-ordered-value', order.cantitate || '');
set('client-order-info', (order.com_achiz_client && order.nr_linie_com_client) ? `${order.com_achiz_client}-${order.nr_linie_com_client}` : '');
// Format delivery date as DD/MM/YYYY
let deliveryDateFormatted = '';
if (order.data_livrare) {
const d = new Date(order.data_livrare);
if (!isNaN(d)) {
const day = String(d.getDate()).padStart(2, '0');
const month = String(d.getMonth() + 1).padStart(2, '0');
const year = d.getFullYear();
deliveryDateFormatted = `${day}/${month}/${year}`;
} else {
deliveryDateFormatted = order.data_livrare;
}
}
set('delivery-date-value', deliveryDateFormatted);
set('description-value', order.descr_com_prod || '');
set('size-value', order.dimensiune || '');
set('article-code-value', order.cod_articol || '');
set('prod-order-value', (order.comanda_productie && order.cantitate) ? `${order.comanda_productie}-${order.cantitate}` : '');
set('barcode-text', order.comanda_productie ? `${order.comanda_productie}/001` : '');
set('vertical-barcode-text', (order.com_achiz_client && order.nr_linie_com_client) ? `${order.com_achiz_client}/${order.nr_linie_com_client}` : '');
// Generate barcodes if JsBarcode is available (with debugging like print_module.html)
const horizontalBarcodeData = order.comanda_productie ? `${order.comanda_productie}/001` : 'N/A';
const verticalBarcodeData = (order.com_achiz_client && order.nr_linie_com_client) ? `${order.com_achiz_client}/${order.nr_linie_com_client}` : '000000/00';
console.log('🔍 BARCODE DEBUG - Order data:', order);
console.log('🔍 Attempting to generate horizontal barcode:', horizontalBarcodeData);
console.log('🔍 JsBarcode available?', typeof JsBarcode !== 'undefined');
console.log('🔍 JsBarcode object:', typeof JsBarcode !== 'undefined' ? JsBarcode : 'undefined');
// Function to generate barcodes (can be called after library loads)
const generateBarcodes = () => {
if (horizontalBarcodeData !== 'N/A' && typeof JsBarcode !== 'undefined') {
try {
const barcodeElement = document.querySelector("#barcode-display");
console.log('🔍 Horizontal barcode element:', barcodeElement);
console.log('🔍 Element innerHTML before:', barcodeElement ? barcodeElement.innerHTML : 'null');
JsBarcode("#barcode-display", horizontalBarcodeData, {
format: "CODE128",
width: 2,
height: 40,
displayValue: false,
margin: 2,
lineColor: "#000000",
background: "#ffffff"
});
console.log('🔍 Element innerHTML after:', barcodeElement ? barcodeElement.innerHTML : 'null');
console.log('✅ Horizontal barcode generated successfully');
// Force black color on all barcode elements
const barcodeSvg = document.getElementById('barcode-display');
if (barcodeSvg) {
console.log('🔍 SVG elements found:', barcodeSvg.querySelectorAll('rect, path').length);
barcodeSvg.querySelectorAll('rect').forEach((r, i) => {
console.log(`🔍 Setting rect ${i} to black`);
r.setAttribute('fill', '#000000');
r.setAttribute('stroke', '#000000');
});
barcodeSvg.querySelectorAll('path').forEach((p, i) => {
console.log(`🔍 Setting path ${i} to black`);
p.setAttribute('fill', '#000000');
p.setAttribute('stroke', '#000000');
});
}
} catch (e) {
console.error('❌ Failed to generate horizontal barcode:', e);
}
} else {
console.warn('⚠️ Skipping horizontal barcode generation:',
horizontalBarcodeData === 'N/A' ? 'No data' : 'JsBarcode not loaded');
}
// Generate vertical barcode visual using JsBarcode (will be rotated by CSS)
console.log('🔍 Attempting to generate vertical barcode:', verticalBarcodeData);
if (verticalBarcodeData !== '000000/00' && typeof JsBarcode !== 'undefined') {
try {
const verticalElement = document.querySelector("#vertical-barcode-display");
console.log('🔍 Vertical barcode element:', verticalElement);
JsBarcode("#vertical-barcode-display", verticalBarcodeData, {
format: "CODE128",
width: 1.5,
height: 35,
displayValue: false,
margin: 2,
lineColor: "#000000",
background: "#ffffff"
});
console.log('✅ Vertical barcode generated successfully');
// Force black color on all vertical barcode elements
const vbarcodeSvg = document.getElementById('vertical-barcode-display');
if (vbarcodeSvg) {
vbarcodeSvg.querySelectorAll('rect').forEach(r => {
r.setAttribute('fill', '#000000');
r.setAttribute('stroke', '#000000');
});
vbarcodeSvg.querySelectorAll('path').forEach(p => {
p.setAttribute('fill', '#000000');
p.setAttribute('stroke', '#000000');
});
}
} catch (e) {
console.error('❌ Failed to generate vertical barcode:', e);
}
} else {
console.warn('⚠️ Skipping vertical barcode generation:',
verticalBarcodeData === '000000/00' ? 'Default value' : 'JsBarcode not loaded');
}
};
// Try to generate immediately
generateBarcodes();
// If JsBarcode is not loaded, wait a bit and try again (for CDN fallback)
if (typeof JsBarcode === 'undefined') {
setTimeout(() => {
console.log('🔍 Retry after 1s - JsBarcode available?', typeof JsBarcode !== 'undefined');
generateBarcodes();
}, 1000);
}
}
function selectOrder(orderId) {
// Find order by ID
const order = allOrders.find(o => o.id === orderId);
if (order) {
// Populate search field
document.getElementById('search-input').value = order.comanda_productie;
// Display in search result
displaySelectedOrder(order);
// Highlight selected row
document.querySelectorAll('#orders-table tr').forEach(row => {
row.classList.remove('selected');
});
event.currentTarget.classList.add('selected');
// Scroll to top
window.scrollTo({ top: 0, behavior: 'smooth' });
}
}
function displaySelectedOrder(order) {
selectedOrderData = order;
const resultDiv = document.getElementById('search-result');
const tbody = document.getElementById('selected-order-row');
// Format date
let dateStr = '-';
if (order.data_livrare) {
dateStr = order.data_livrare;
}
// Format created_at
let createdStr = '-';
if (order.created_at) {
createdStr = order.created_at;
}
tbody.innerHTML = `
<tr>
<td>${order.id}</td>
<td><strong>${order.comanda_productie}</strong></td>
<td>${order.cod_articol || '-'}</td>
<td>${order.descr_com_prod}</td>
<td style="text-align: right; font-weight: 600;">${order.cantitate}</td>
<td style="text-align: center;">${dateStr}</td>
<td style="text-align: center;">${order.dimensiune || '-'}</td>
<td>${order.com_achiz_client || '-'}</td>
<td style="text-align: right;">${order.nr_linie_com_client || '-'}</td>
<td>${order.customer_name || '-'}</td>
<td>${order.customer_article_number || '-'}</td>
<td>${order.open_for_order || '-'}</td>
<td style="text-align: right;">${order.line_number || '-'}</td>
<td style="text-align: center;">
${order.printed_labels == 1 ? '<span style="color: #28a745; font-weight: bold;">✓ Yes</span>' : '<span style="color: #dc3545;">✗ No</span>'}
</td>
<td style="font-size: 11px; color: #6c757d;">${createdStr}</td>
</tr>
`;
resultDiv.style.display = 'block';
document.getElementById('print-button').disabled = false;
}
function printLabels() {
if (!selectedOrderData) {
alert('Please select an order first');
return;
}
const quantity = parseInt(document.getElementById('quantity-input').value);
if (!quantity || quantity < 1) {
alert('Please enter a valid quantity');
return;
}
// Redirect to print module with order data
const orderIds = [selectedOrderData.id];
const url = `/print_module?order_ids=${orderIds.join(',')}&quantity=${quantity}`;
window.location.href = url;
}
</script>
{% endblock %}

File diff suppressed because it is too large Load Diff

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)

162
test_pdf_generation.py Normal file
View File

@@ -0,0 +1,162 @@
#!/usr/bin/env python3
"""
Test script for PDF generation functionality
Tests both single label and batch PDF generation
"""
import json
import requests
import sys
# Configuration
BASE_URL = "http://localhost:8080"
LOGIN_URL = f"{BASE_URL}/login"
SINGLE_PDF_URL = f"{BASE_URL}/labels/api/generate-pdf"
BATCH_PDF_URL = f"{BASE_URL}/labels/api/generate-pdf/1/true"
# Test credentials
TEST_USERNAME = "admin"
TEST_PASSWORD = "admin123"
def test_pdf_generation():
"""Test PDF generation endpoints"""
session = requests.Session()
# Step 1: Login
print("=" * 70)
print("Step 1: Logging in...")
print("=" * 70)
login_data = {
'username': TEST_USERNAME,
'password': TEST_PASSWORD
}
response = session.post(LOGIN_URL, data=login_data)
if response.status_code not in [200, 302]: # Accept both OK and redirect
print(f"❌ Login failed with status {response.status_code}")
print(f"Response: {response.text[:200]}")
return False
print(f"✓ Login successful (status: {response.status_code})")
# Step 2: Test Single Label PDF Generation
print("\n" + "=" * 70)
print("Step 2: Testing Single Label PDF Generation...")
print("=" * 70)
order_data = {
"id": 1,
"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",
"data_livrara": "2026-02-11",
"dimensiune": "Standard (50x70cm)",
"piece_number": 1,
"total_pieces": 5
}
response = session.post(
SINGLE_PDF_URL,
json=order_data,
headers={'Content-Type': 'application/json'}
)
if response.status_code == 200:
if response.headers.get('Content-Type') == 'application/pdf':
print(f"✓ Single label PDF generated successfully")
print(f" Content-Type: {response.headers.get('Content-Type')}")
print(f" PDF Size: {len(response.content)} bytes")
# Save to file for manual inspection
with open('/tmp/test_label_single.pdf', 'wb') as f:
f.write(response.content)
print(f" Saved to: /tmp/test_label_single.pdf")
else:
print(f"❌ Invalid content type: {response.headers.get('Content-Type')}")
print(f"Response: {response.text[:200]}")
return False
else:
print(f"❌ Single PDF generation failed with status {response.status_code}")
print(f"Response: {response.text[:200]}")
return False
# Step 3: Test Batch PDF Generation
print("\n" + "=" * 70)
print("Step 3: Testing Batch PDF Generation...")
print("=" * 70)
response = session.post(BATCH_PDF_URL)
if response.status_code == 200:
if response.headers.get('Content-Type') == 'application/pdf':
print(f"✓ Batch PDF generated successfully")
print(f" Content-Type: {response.headers.get('Content-Type')}")
print(f" PDF Size: {len(response.content)} bytes")
# Save to file for manual inspection
with open('/tmp/test_label_batch.pdf', 'wb') as f:
f.write(response.content)
print(f" Saved to: /tmp/test_label_batch.pdf")
else:
print(f"❌ Invalid content type: {response.headers.get('Content-Type')}")
print(f"Response: {response.text[:200]}")
return False
else:
print(f"❌ Batch PDF generation failed with status {response.status_code}")
print(f"Response: {response.text[:200]}")
return False
# Step 4: Verify order marked as printed
print("\n" + "=" * 70)
print("Step 4: Verifying order status...")
print("=" * 70)
# Check if order is marked as printed
import pymysql
try:
conn = pymysql.connect(
host='127.0.0.1',
port=3306,
user='quality_user',
password='quality_secure_password_2026',
database='quality_db'
)
cursor = conn.cursor()
cursor.execute("SELECT id, comanda_productie, printed_labels FROM order_for_labels WHERE id = 1")
row = cursor.fetchone()
cursor.close()
conn.close()
if row:
order_id, cp_code, printed_status = row
print(f"Order ID: {order_id}, CP Code: {cp_code}")
if printed_status == 1:
print(f"✓ Order marked as printed (printed_labels = {printed_status})")
else:
print(f"⚠ Order NOT marked as printed (printed_labels = {printed_status})")
else:
print("⚠ Order not found in database")
except Exception as e:
print(f"⚠ Could not verify order status: {e}")
print("\n" + "=" * 70)
print("✓ All PDF generation tests completed successfully!")
print("=" * 70)
return True
if __name__ == '__main__':
try:
success = test_pdf_generation()
sys.exit(0 if success else 1)
except Exception as e:
print(f"\n❌ Unexpected error: {e}")
import traceback
traceback.print_exc()
sys.exit(1)