From 2f6bb5d02946e7915b3d660185c6a2d22c6af504 Mon Sep 17 00:00:00 2001 From: Quality App Developer Date: Mon, 2 Feb 2026 01:18:54 +0200 Subject: [PATCH] feat: Migrate label printing module from legacy app - Created labels module with complete structure in app/modules/labels/ - Implemented print_module.py with database functions: * get_unprinted_orders_data() - Retrieve unprinted orders from database * get_printed_orders_data() - Retrieve printed orders from database * update_order_printed_status() - Mark orders as printed * search_orders_by_cp_code() - Search orders by production code - Created routes.py with Flask Blueprint and API endpoints: * GET /labels/ - Module home page with feature launchers * GET /labels/print-module - Main label printing interface * GET /labels/print-lost-labels - Lost label reprinting interface * GET /labels/api/unprinted-orders - API for unprinted orders * GET /labels/api/printed-orders - API for printed orders * POST /labels/api/search-orders - Search orders API * POST /labels/api/update-printed-status/ - Update status API - Migrated HTML templates from legacy app with theme support: * print_module.html - Thermal label printing with QZ Tray integration * print_lost_labels.html - Lost label search and reprint interface * Added comprehensive CSS variables for dark/light mode theming * Preserved all original JavaScript functionality for printing - Created index.html module home page with feature launchers - Registered labels blueprint in app/__init__.py with /labels prefix - Added 'Label Printing' module card to dashboard with print icon All functionality preserved from original implementation: - QZ Tray thermal printer integration - JsBarcode barcode generation (horizontal + vertical) - PDF export fallback - Session-based authentication for all routes - Database integration with proper error handling --- app/__init__.py | 4 +- app/modules/labels/__init__.py | 2 + app/modules/labels/print_module.py | 200 ++ app/modules/labels/routes.py | 128 ++ app/routes.py | 7 + app/templates/modules/labels/index.html | 105 ++ .../modules/labels/print_lost_labels.html | 985 ++++++++++ .../modules/labels/print_module.html | 1665 +++++++++++++++++ 8 files changed, 3095 insertions(+), 1 deletion(-) create mode 100644 app/modules/labels/__init__.py create mode 100644 app/modules/labels/print_module.py create mode 100644 app/modules/labels/routes.py create mode 100644 app/templates/modules/labels/index.html create mode 100644 app/templates/modules/labels/print_lost_labels.html create mode 100644 app/templates/modules/labels/print_module.html diff --git a/app/__init__.py b/app/__init__.py index e9592a9..cfafecf 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -143,14 +143,16 @@ def register_blueprints(app): from app.modules.settings.routes import settings_bp from app.modules.warehouse.routes import warehouse_bp from app.modules.warehouse.boxes_routes import boxes_bp + from app.modules.labels.routes import labels_bp app.register_blueprint(main_bp) app.register_blueprint(quality_bp, url_prefix='/quality') app.register_blueprint(settings_bp, url_prefix='/settings') app.register_blueprint(warehouse_bp, url_prefix='/warehouse') app.register_blueprint(boxes_bp) + app.register_blueprint(labels_bp, url_prefix='/labels') - app.logger.info("Blueprints registered: main, quality, settings, warehouse, boxes") + app.logger.info("Blueprints registered: main, quality, settings, warehouse, boxes, labels") def register_error_handlers(app): diff --git a/app/modules/labels/__init__.py b/app/modules/labels/__init__.py new file mode 100644 index 0000000..fd4200d --- /dev/null +++ b/app/modules/labels/__init__.py @@ -0,0 +1,2 @@ +# Labels Module +# Handles label printing and management for thermal printers diff --git a/app/modules/labels/print_module.py b/app/modules/labels/print_module.py new file mode 100644 index 0000000..761c2fa --- /dev/null +++ b/app/modules/labels/print_module.py @@ -0,0 +1,200 @@ +""" +Labels Module - Print Module Functions +Handles retrieval of orders for label printing +""" +import logging +from app.database import get_db + +logger = logging.getLogger(__name__) + + +def get_unprinted_orders_data(limit=100): + """ + Retrieve unprinted orders from the database for label printing + Returns list of order dictionaries where printed_labels != 1 + """ + try: + conn = get_db() + cursor = conn.cursor() + + # Check if order_for_labels table exists + cursor.execute("SHOW TABLES LIKE 'order_for_labels'") + if not cursor.fetchone(): + logger.warning("order_for_labels table does not exist") + return [] + + # Check if printed_labels column exists + cursor.execute("SHOW COLUMNS FROM order_for_labels LIKE 'printed_labels'") + column_exists = cursor.fetchone() + + if column_exists: + # Use printed_labels column + 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 printed_labels IS NULL OR printed_labels = 0 + ORDER BY created_at DESC + LIMIT %s + """, (limit,)) + else: + # Fallback: get all orders if no printed_labels column + 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, 0 as printed_labels, data_livrare, dimensiune + FROM order_for_labels + ORDER BY created_at DESC + LIMIT %s + """, (limit,)) + + columns = [col[0] for col in cursor.description] + orders = [] + + for row in cursor.fetchall(): + order_dict = {columns[i]: row[i] for i in range(len(columns))} + # Ensure date fields are strings + if order_dict.get('created_at'): + order_dict['created_at'] = str(order_dict['created_at']) + if order_dict.get('updated_at'): + order_dict['updated_at'] = str(order_dict['updated_at']) + if order_dict.get('data_livrare'): + order_dict['data_livrare'] = str(order_dict['data_livrare']) + orders.append(order_dict) + + cursor.close() + logger.info(f"Retrieved {len(orders)} unprinted orders") + return orders + + except Exception as e: + logger.error(f"Error retrieving unprinted orders: {e}") + return [] + + +def get_printed_orders_data(limit=100): + """ + Retrieve printed orders from the database + Returns list of order dictionaries where printed_labels = 1 + """ + try: + conn = get_db() + cursor = conn.cursor() + + # Check if order_for_labels table exists + cursor.execute("SHOW TABLES LIKE 'order_for_labels'") + if not cursor.fetchone(): + logger.warning("order_for_labels table does not exist") + return [] + + # Check if printed_labels column exists + cursor.execute("SHOW COLUMNS FROM order_for_labels LIKE 'printed_labels'") + column_exists = cursor.fetchone() + + if column_exists: + # Get orders where printed_labels = 1 + 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 printed_labels = 1 + ORDER BY updated_at DESC + LIMIT %s + """, (limit,)) + else: + # Fallback: no printed orders if column doesn't exist + logger.info("printed_labels column does not exist - no printed orders available") + cursor.close() + return [] + + columns = [col[0] for col in cursor.description] + orders = [] + + for row in cursor.fetchall(): + order_dict = {columns[i]: row[i] for i in range(len(columns))} + # Ensure date fields are strings + if order_dict.get('created_at'): + order_dict['created_at'] = str(order_dict['created_at']) + if order_dict.get('updated_at'): + order_dict['updated_at'] = str(order_dict['updated_at']) + if order_dict.get('data_livrare'): + order_dict['data_livrare'] = str(order_dict['data_livrare']) + orders.append(order_dict) + + cursor.close() + logger.info(f"Retrieved {len(orders)} printed orders") + return orders + + except Exception as e: + logger.error(f"Error retrieving printed orders: {e}") + return [] + + +def update_order_printed_status(order_id, printed=True): + """ + Update the printed_labels status for an order + """ + try: + conn = get_db() + cursor = conn.cursor() + + cursor.execute(""" + UPDATE order_for_labels + SET printed_labels = %s + WHERE id = %s + """, (1 if printed else 0, order_id)) + + conn.commit() + cursor.close() + logger.info(f"Updated order {order_id} printed status to {printed}") + return True + + except Exception as e: + logger.error(f"Error updating order printed status: {e}") + return False + + +def search_orders_by_cp_code(cp_code_search): + """ + Search for orders by CP code / Production Order + """ + try: + conn = get_db() + cursor = conn.cursor() + + search_term = cp_code_search.strip().upper() + + 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 UPPER(comanda_productie) LIKE %s + ORDER BY created_at DESC + LIMIT 100 + """, (f"{search_term}%",)) + + columns = [col[0] for col in cursor.description] + orders = [] + + for row in cursor.fetchall(): + order_dict = {columns[i]: row[i] for i in range(len(columns))} + if order_dict.get('created_at'): + order_dict['created_at'] = str(order_dict['created_at']) + if order_dict.get('updated_at'): + order_dict['updated_at'] = str(order_dict['updated_at']) + if order_dict.get('data_livrare'): + order_dict['data_livrare'] = str(order_dict['data_livrare']) + orders.append(order_dict) + + cursor.close() + return orders + + except Exception as e: + logger.error(f"Error searching orders by CP code: {e}") + return [] diff --git a/app/modules/labels/routes.py b/app/modules/labels/routes.py new file mode 100644 index 0000000..382e927 --- /dev/null +++ b/app/modules/labels/routes.py @@ -0,0 +1,128 @@ +""" +Labels Module Routes +Handles label printing pages and API endpoints +""" +from flask import Blueprint, render_template, session, redirect, url_for, jsonify, request +import logging + +from .print_module import ( + get_unprinted_orders_data, + get_printed_orders_data, + update_order_printed_status, + search_orders_by_cp_code +) + +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-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') + + +# ============================================================================ +# 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/', 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 diff --git a/app/routes.py b/app/routes.py index 2bd2501..73945c3 100644 --- a/app/routes.py +++ b/app/routes.py @@ -86,6 +86,13 @@ def dashboard(): 'color': 'info', 'url': url_for('warehouse.warehouse_index') }, + { + 'name': 'Label Printing', + 'description': 'Print and manage thermal labels for orders', + 'icon': 'fa-print', + 'color': 'success', + 'url': url_for('labels.labels_index') + }, { 'name': 'Settings', 'description': 'Configure application settings', diff --git a/app/templates/modules/labels/index.html b/app/templates/modules/labels/index.html new file mode 100644 index 0000000..78bcc3e --- /dev/null +++ b/app/templates/modules/labels/index.html @@ -0,0 +1,105 @@ +{% extends "base.html" %} + +{% block title %}Labels Module - Quality App v2{% endblock %} + +{% block content %} +
+
+
+

+ Labels Module +

+

Manage and print labels for thermal printers

+
+
+ +
+ +
+
+
+
+ +
+
Print Labels
+

Print labels directly to thermal printers with live preview.

+ + Open Printing + +
+
+
+ + +
+
+
+
+ +
+
Print Lost Labels
+

Search and reprint labels for orders that need reprinting.

+ + Reprint Labels + +
+
+
+
+ + +
+
+
+
+
Module Overview
+
+
+
+
+
Key Features:
+
    +
  • Real-time label preview
  • +
  • Direct thermal printer integration
  • +
  • PDF export fallback
  • +
  • Batch label printing
  • +
  • QZ Tray support for Windows/Mac/Linux
  • +
+
+
+
Supported Printers:
+
    +
  • Zebra thermal printers
  • +
  • Epson TM series
  • +
  • Brother thermal printers
  • +
  • Generic thermal printers via QZ Tray
  • +
  • PDF export for any printer
  • +
+
+
+
+
+
+
+
+ + +{% endblock %} diff --git a/app/templates/modules/labels/print_lost_labels.html b/app/templates/modules/labels/print_lost_labels.html new file mode 100644 index 0000000..c52026e --- /dev/null +++ b/app/templates/modules/labels/print_lost_labels.html @@ -0,0 +1,985 @@ +{% extends "base.html" %} + +{% block head %} + + +{% endblock %} + +{% block content %} + + + +
+ +
+
Label View
+ + +
+ + + + {% if session.role == 'superadmin' %} + šŸ”‘ Manage Keys + {% endif %} +
+ + +
+ +
+ +
+ +
+ + +
+ +
+ + +
+
+
+
+
+
+
+
+ + +
+ + +
+ Quantity ordered +
+
+ +
+ + +
+ Customer order +
+
+ +
+ + +
+ Delivery date +
+
+ +
+ + +
+ Product description +
+
+ +
+ + +
+ Size +
+
+ +
+ + +
+ Article code +
+
+ +
+ + +
+ Prod. order +
+
+ +
+
+ + +
+ + + + + +
+ + +
+ + + + + +
+
+ + +
+ +
+ + + +
+
+ + +
+ + +
+
+ + +
+ + +
+ + +
+ +
+ + +
+ (e.g., CP00000711-001, 002, ...) +
+ + +
+
+
+ QZ Tray is required for direct printing +
+ + šŸ“„ Download QZ Tray + +
+
+
+
+ + +
+

Data Preview (Unprinted Orders)

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + + + + + + + + + + + + +{% endblock %} \ No newline at end of file