- Fix QZ Tray library loading errors (net::ERR_NAME_NOT_RESOLVED) by switching from CDN to local static file - Add /labels/api/pairing-keys endpoint to fetch valid printer pairing keys - Update print_labels.html to use local js/qz-tray.js - Update print_lost_labels.html to use local js/qz-tray.js - Update print_lost_labels_new.html to use correct path js/qz-tray.js - Update fg_scan.html to use local js/qz-tray.js - API returns active pairing keys from qz_pairing_keys table for printer selection
525 lines
20 KiB
Python
Executable File
525 lines
20 KiB
Python
Executable File
"""
|
|
Labels Module Routes
|
|
Handles label printing pages and API endpoints
|
|
"""
|
|
from flask import Blueprint, render_template, session, redirect, url_for, jsonify, request, flash
|
|
import logging
|
|
import json
|
|
import uuid
|
|
import os
|
|
import tempfile
|
|
|
|
from app.database import get_db
|
|
from .print_module import (
|
|
get_unprinted_orders_data,
|
|
get_printed_orders_data,
|
|
update_order_printed_status,
|
|
search_orders_by_cp_code
|
|
)
|
|
from .import_labels import (
|
|
process_csv_file,
|
|
process_excel_file,
|
|
save_orders_to_database
|
|
)
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
labels_bp = Blueprint('labels', __name__, url_prefix='/labels')
|
|
|
|
|
|
@labels_bp.route('/', methods=['GET'])
|
|
def labels_index():
|
|
"""Labels module home page"""
|
|
if 'user_id' not in session:
|
|
return redirect(url_for('main.login'))
|
|
|
|
return render_template('modules/labels/index.html')
|
|
|
|
|
|
@labels_bp.route('/print-module', methods=['GET'])
|
|
def print_module():
|
|
"""Label printing interface with thermal printer support"""
|
|
if 'user_id' not in session:
|
|
return redirect(url_for('main.login'))
|
|
|
|
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"""
|
|
if 'user_id' not in session:
|
|
return redirect(url_for('main.login'))
|
|
|
|
return render_template('modules/labels/print_lost_labels.html')
|
|
|
|
|
|
@labels_bp.route('/import-labels', methods=['GET', 'POST'])
|
|
def import_labels():
|
|
"""Import labels data from CSV or Excel file"""
|
|
if 'user_id' not in session:
|
|
return redirect(url_for('main.login'))
|
|
|
|
if request.method == 'POST':
|
|
action = request.form.get('action', 'preview')
|
|
|
|
if action == 'preview':
|
|
# Handle file upload and show preview
|
|
if 'file' not in request.files:
|
|
flash('No file selected', 'error')
|
|
return redirect(request.url)
|
|
|
|
file = request.files['file']
|
|
|
|
if file.filename == '':
|
|
flash('No file selected', 'error')
|
|
return redirect(request.url)
|
|
|
|
filename_lower = file.filename.lower()
|
|
|
|
# Check file type
|
|
if not (filename_lower.endswith('.csv') or filename_lower.endswith('.xlsx') or filename_lower.endswith('.xls')):
|
|
flash('Please upload a CSV or Excel file (.csv, .xlsx, .xls)', 'error')
|
|
return redirect(request.url)
|
|
|
|
try:
|
|
# Save file temporarily
|
|
upload_id = str(uuid.uuid4())
|
|
temp_file = tempfile.NamedTemporaryFile(delete=False, suffix=os.path.splitext(file.filename)[1])
|
|
file.save(temp_file.name)
|
|
temp_file.close()
|
|
|
|
# Process file
|
|
if filename_lower.endswith('.csv'):
|
|
orders_data, errors, warnings = process_csv_file(temp_file.name)
|
|
else:
|
|
orders_data, errors, warnings = process_excel_file(temp_file.name)
|
|
|
|
# Clean up temp file
|
|
try:
|
|
os.unlink(temp_file.name)
|
|
except:
|
|
pass
|
|
|
|
# Save orders data to temp file
|
|
temp_data_file = f'/tmp/upload_{upload_id}.json'
|
|
with open(temp_data_file, 'w') as f:
|
|
json.dump(orders_data, f)
|
|
|
|
# Store in session
|
|
session['upload_id'] = upload_id
|
|
session['import_filename'] = file.filename
|
|
session.modified = True
|
|
|
|
# Get headers for preview
|
|
database_fields = [
|
|
'comanda_productie', 'cod_articol', 'descr_com_prod', 'cantitate',
|
|
'data_livrare', 'dimensiune', 'com_achiz_client', 'nr_linie_com_client',
|
|
'customer_name', 'customer_article_number', 'open_for_order', 'line_number'
|
|
]
|
|
|
|
headers = [field for field in database_fields if field in orders_data[0].keys()] if orders_data else database_fields
|
|
preview_data = orders_data[:10]
|
|
|
|
# Flash any warnings/errors
|
|
for warning in warnings[:5]:
|
|
flash(warning, 'warning')
|
|
if len(warnings) > 5:
|
|
flash(f'... and {len(warnings) - 5} more warnings', 'warning')
|
|
|
|
for error in errors[:10]:
|
|
flash(error, 'error')
|
|
if len(errors) > 10:
|
|
flash(f'... and {len(errors) - 10} more errors', 'error')
|
|
|
|
if not orders_data:
|
|
flash('No valid data found in file', 'error')
|
|
return redirect(request.url)
|
|
|
|
return render_template('modules/labels/import_labels.html',
|
|
preview_data=preview_data,
|
|
headers=headers,
|
|
show_preview=True,
|
|
filename=file.filename,
|
|
total_orders=len(orders_data))
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error processing import file: {e}")
|
|
flash(f'Error processing file: {str(e)}', 'error')
|
|
return redirect(request.url)
|
|
|
|
elif action == 'save':
|
|
# Save data to database
|
|
upload_id = session.get('upload_id')
|
|
|
|
if not upload_id:
|
|
flash('No data to save. Please upload a file first.', 'error')
|
|
return redirect(url_for('labels.import_labels'))
|
|
|
|
try:
|
|
# Load orders data from temp file
|
|
temp_data_file = f'/tmp/upload_{upload_id}.json'
|
|
with open(temp_data_file, 'r') as f:
|
|
orders_data = json.load(f)
|
|
|
|
# Save to database
|
|
inserted_count, errors = save_orders_to_database(orders_data)
|
|
|
|
# Clean up
|
|
try:
|
|
os.unlink(temp_data_file)
|
|
except:
|
|
pass
|
|
|
|
session.pop('upload_id', None)
|
|
session.pop('import_filename', None)
|
|
session.modified = True
|
|
|
|
# Flash results
|
|
if errors:
|
|
for error in errors[:5]:
|
|
flash(error, 'error')
|
|
if len(errors) > 5:
|
|
flash(f'... and {len(errors) - 5} more errors', 'error')
|
|
flash(f'Imported {inserted_count} orders with {len(errors)} errors', 'warning')
|
|
else:
|
|
flash(f'Successfully imported {inserted_count} orders for labels', 'success')
|
|
|
|
return redirect(url_for('labels.import_labels'))
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error saving import data: {e}")
|
|
flash(f'Error saving data: {str(e)}', 'error')
|
|
return redirect(url_for('labels.import_labels'))
|
|
|
|
# GET request - show the import form
|
|
return render_template('modules/labels/import_labels.html')
|
|
|
|
|
|
@labels_bp.route('/help/<page>', methods=['GET'])
|
|
def help(page='index'):
|
|
"""Help page for labels module"""
|
|
if 'user_id' not in session:
|
|
return redirect(url_for('main.login'))
|
|
|
|
# Map page names to help content
|
|
help_pages = {
|
|
'print_module': {
|
|
'title': 'Print Module 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_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': '''
|
|
<h3>Print Lost Labels - Reprint Guide</h3>
|
|
<p>Use this page to search and reprint labels for orders that need reprinting.</p>
|
|
<h4>Features:</h4>
|
|
<ul>
|
|
<li>Search orders by production code</li>
|
|
<li>Filter previously printed orders</li>
|
|
<li>Reprint with updated information</li>
|
|
</ul>
|
|
<h4>How to use:</h4>
|
|
<ol>
|
|
<li>Enter the production order code</li>
|
|
<li>Click "Search" to find the order</li>
|
|
<li>Select the order and preview</li>
|
|
<li>Click "Reprint Labels" to print again</li>
|
|
</ol>
|
|
'''
|
|
}
|
|
}
|
|
|
|
help_data = help_pages.get(page, help_pages.get('index', {'title': 'Help', 'content': 'No help available'}))
|
|
|
|
return f'''
|
|
<html>
|
|
<head>
|
|
<title>{help_data['title']}</title>
|
|
<style>
|
|
body {{ font-family: Arial, sans-serif; padding: 20px; }}
|
|
h3 {{ color: #333; }}
|
|
ul, ol {{ margin: 10px 0; padding-left: 20px; }}
|
|
li {{ margin: 5px 0; }}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
{help_data['content']}
|
|
</body>
|
|
</html>
|
|
'''
|
|
|
|
|
|
# ============================================================================
|
|
# API Endpoints for Labels Module
|
|
# ============================================================================
|
|
|
|
@labels_bp.route('/api/unprinted-orders', methods=['GET'], endpoint='api_unprinted_orders')
|
|
def api_unprinted_orders():
|
|
"""Get all unprinted orders for label printing"""
|
|
if 'user_id' not in session:
|
|
return jsonify({'success': False, 'error': 'Unauthorized'}), 401
|
|
|
|
try:
|
|
limit = request.args.get('limit', 100, type=int)
|
|
if limit > 500:
|
|
limit = 500
|
|
if limit < 1:
|
|
limit = 1
|
|
|
|
orders = get_unprinted_orders_data(limit)
|
|
return jsonify({'success': True, 'orders': orders, 'count': len(orders)}), 200
|
|
except Exception as e:
|
|
logger.error(f"Error getting unprinted orders: {e}")
|
|
return jsonify({'success': False, 'error': str(e)}), 500
|
|
|
|
|
|
@labels_bp.route('/api/printed-orders', methods=['GET'], endpoint='api_printed_orders')
|
|
def api_printed_orders():
|
|
"""Get all printed orders"""
|
|
if 'user_id' not in session:
|
|
return jsonify({'success': False, 'error': 'Unauthorized'}), 401
|
|
|
|
try:
|
|
limit = request.args.get('limit', 100, type=int)
|
|
if limit > 500:
|
|
limit = 500
|
|
if limit < 1:
|
|
limit = 1
|
|
|
|
orders = get_printed_orders_data(limit)
|
|
return jsonify({'success': True, 'orders': orders, 'count': len(orders)}), 200
|
|
except Exception as e:
|
|
logger.error(f"Error getting printed orders: {e}")
|
|
return jsonify({'success': False, 'error': str(e)}), 500
|
|
|
|
|
|
@labels_bp.route('/api/search-orders', methods=['POST'], endpoint='api_search_orders')
|
|
def api_search_orders():
|
|
"""Search for orders by CP code"""
|
|
if 'user_id' not in session:
|
|
return jsonify({'success': False, 'error': 'Unauthorized'}), 401
|
|
|
|
try:
|
|
data = request.get_json()
|
|
cp_code = data.get('cp_code', '').strip()
|
|
|
|
if not cp_code or len(cp_code) < 1:
|
|
return jsonify({'success': False, 'error': 'CP code is required'}), 400
|
|
|
|
results = search_orders_by_cp_code(cp_code)
|
|
return jsonify({'success': True, 'orders': results, 'count': len(results)}), 200
|
|
except Exception as e:
|
|
logger.error(f"Error searching orders: {e}")
|
|
return jsonify({'success': False, 'error': str(e)}), 500
|
|
|
|
|
|
@labels_bp.route('/api/update-printed-status/<int:order_id>', methods=['POST'], endpoint='api_update_printed_status')
|
|
def api_update_printed_status(order_id):
|
|
"""Mark an order as printed"""
|
|
if 'user_id' not in session:
|
|
return jsonify({'success': False, 'error': 'Unauthorized'}), 401
|
|
|
|
try:
|
|
data = request.get_json() or {}
|
|
printed = data.get('printed', True)
|
|
|
|
success = update_order_printed_status(order_id, printed)
|
|
if success:
|
|
return jsonify({'success': True, 'message': 'Order status updated'}), 200
|
|
else:
|
|
return jsonify({'success': False, 'error': 'Failed to update order'}), 500
|
|
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
|
|
|
|
|
|
@labels_bp.route('/api/pairing-keys', methods=['GET'], endpoint='api_pairing_keys')
|
|
def api_pairing_keys():
|
|
"""Get QZ Tray pairing keys for printer selection"""
|
|
if 'user_id' not in session:
|
|
return jsonify({'error': 'Unauthorized'}), 401
|
|
|
|
try:
|
|
conn = get_db()
|
|
cursor = conn.cursor()
|
|
|
|
cursor.execute("""
|
|
SELECT id, printer_name, pairing_key, valid_until
|
|
FROM qz_pairing_keys
|
|
WHERE valid_until >= CURDATE()
|
|
ORDER BY printer_name ASC
|
|
""")
|
|
|
|
pairing_keys = []
|
|
for row in cursor.fetchall():
|
|
pairing_keys.append({
|
|
'id': row[0],
|
|
'printer_name': row[1],
|
|
'pairing_key': row[2]
|
|
})
|
|
|
|
cursor.close()
|
|
|
|
logger.info(f"Retrieved {len(pairing_keys)} valid pairing keys")
|
|
return jsonify(pairing_keys), 200
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error fetching pairing keys: {e}")
|
|
return jsonify({'error': str(e)}), 500 |