#!/usr/bin/env python3 """ 🖨️ Thermal Printer Bridge PRO v2.2.6 ✅ 2cm DOPO zona (se presente, solo DOMICILIO) ✅ 2cm DOPO orario (se NO zona, per RITIRO/BANCO) ✅ Spazio dimezzato per totale (2 righe prima/dopo) ✅ Separatori 22 caratteri """ from flask import Flask, request, jsonify from functools import wraps from datetime import datetime import logging # ==================== CONFIGURAZIONE BASE ==================== API_KEY = "thermal_kitchen_secret_key_12345LKJ" BRIDGE_PORT = 8090 # ==================== SETUP FLASK ==================== app = Flask(__name__) logging.basicConfig( level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s' ) logger = logging.getLogger(__name__) # ==================== IMPORT ESCPOS ==================== try: from escpos.printer import Network, Dummy ESCPOS_AVAILABLE = True logger.info("✅ python-escpos disponibile") except ImportError: ESCPOS_AVAILABLE = False logger.warning("⚠️ python-escpos non installato") # ==================== AUTENTICAZIONE ==================== def require_api_key(f): @wraps(f) def decorated_function(*args, **kwargs): api_key = request.headers.get('X-API-Key') if not api_key or api_key != API_KEY: logger.warning(f"⚠️ Accesso non autorizzato da {request.remote_addr}") return jsonify({'success': False, 'error': 'API Key non valida'}), 401 return f(*args, **kwargs) return decorated_function # ==================== FUNZIONI HELPER ==================== def get_printer(ip, port=9100): """Connetti a stampante network""" if not ESCPOS_AVAILABLE: from escpos.printer import Dummy return Dummy() try: return Network(ip, port=port) except Exception as e: logger.error(f"❌ Errore connessione {ip}:{port}: {e}") return None def apply_text_formatting(printer, config, field_name): """Applica formattazione testo basata su configurazione""" # RESET COMPLETO printer.set() size = config.get(f'{field_name}_size', 'small') bold = config.get(f'{field_name}_bold', 'no') == 'yes' align = config.get(f'{field_name}_align', 'left') font = config.get(f'{field_name}_font', 'a') params = {} if bold: params['bold'] = True # Allineamento if align == 'center': params['align'] = 'center' elif align == 'right': params['align'] = 'right' else: params['align'] = 'left' # Font if font in ['a', 'b', 'c']: params['font'] = font # Dimensioni if size == 'small': pass # Normale elif size == 'medium': params['double_height'] = True elif size == 'large': params['double_width'] = True params['double_height'] = True if params: printer.set(**params) def reset_formatting(printer): """Reset formattazione""" printer.set() def print_separator(printer, width, char='-'): """Stampa linea separatrice (default: trattino normale)""" printer.text(char * width + "\n") # ==================== STAMPA COMANDA ==================== def print_order_thermal(order_data, printer_config): """Stampa comanda con configurazione personalizzata""" try: printer = get_printer( ip=printer_config['ip'], port=printer_config['port'] ) if not printer: return { 'success': False, 'error': f"Impossibile connettersi a {printer_config['name']}" } config = order_data.get('config', {}) width = 22 # 22 caratteri come richiesto # ==================== ✅ v3.6.0: GIÀ PAGATO (prima riga) ==================== if order_data.get('already_paid', False): # Doppia altezza + doppia larghezza + grassetto per massimo risalto try: printer.set(align='center', bold=True, double_height=True, double_width=True) except TypeError: # Fallback per versioni vecchie di python-escpos printer.set(align='center', bold=True, height=2, width=2) printer.text("*** GIA' PAGATO ***\n") reset_formatting(printer) print_separator(printer, width, '=') print_separator(printer, width, '=') # ==================== NUMERO ORDINE ==================== if config.get('show_order_number', 'yes') == 'yes': order_number = order_data.get('order_number', 'N/A') apply_text_formatting(printer, config, 'order_number') printer.text(f"Comanda n. {order_number}\n") reset_formatting(printer) # ==================== TIPO SERVIZIO ==================== if config.get('show_service_type', 'yes') == 'yes': shipping_method = order_data.get('shipping_method', '') # Determina tipo servizio if 'ritiro' in shipping_method.lower(): service_type = "RITIRO" # ✅ Cambiato da ASPORTO elif 'banco' in shipping_method.lower(): service_type = "BANCO" else: service_type = "DOMICILIO" apply_text_formatting(printer, config, 'service') printer.text(f"{service_type}\n") reset_formatting(printer) # ✅ DUE SEPARATORI dopo tipo servizio print_separator(printer, width) print_separator(printer, width) # ==================== RESOCONTO PRODOTTI (PIZZE SEPARATE) ==================== if config.get('show_product_summary', 'yes') == 'yes': pizze_normali = order_data.get('pizze_normali', 0) pizze_baby = order_data.get('pizze_baby', 0) pizze_maxi = order_data.get('pizze_maxi', 0) menu_count = order_data.get('menu_count', 0) bibite_count = order_data.get('bibite_count', 0) fritture_count = order_data.get('fritture_count', 0) summary_parts = [] # Pizze separate per dimensione pizza_parts = [] if pizze_normali > 0: pizza_parts.append(f"Normali: {pizze_normali}") if pizze_baby > 0: pizza_parts.append(f"Baby: {pizze_baby}") if pizze_maxi > 0: pizza_parts.append(f"Maxi: {pizze_maxi}") if pizza_parts: summary_parts.append(f"PIZZE {', '.join(pizza_parts)}") if menu_count > 0: summary_parts.append(f"Menu: {menu_count}") if bibite_count > 0: summary_parts.append(f"Bibite: {bibite_count}") if fritture_count > 0: summary_parts.append(f"Fritture: {fritture_count}") if summary_parts: apply_text_formatting(printer, config, 'product_summary') for part in summary_parts: printer.text(f"{part}\n") reset_formatting(printer) printer.text("\n") # ==================== ORARIO CONSEGNA ==================== if config.get('show_delivery_time', 'yes') == 'yes': delivery_time = order_data.get('delivery_time', '') if delivery_time: apply_text_formatting(printer, config, 'delivery_time') printer.text(f"ORE {delivery_time}\n") reset_formatting(printer) print_separator(printer, width) printer.text("\n") # 1 riga normale # ==================== ZONA ==================== zona_presente = False if config.get('show_zone', 'yes') == 'yes': zona = order_data.get('zona_selezionata', '') if zona and service_type == "DOMICILIO": apply_text_formatting(printer, config, 'zone') printer.text(f"zona: {zona.upper()}\n") reset_formatting(printer) # ✅ 2cm DOPO zona (8 righe) - solo se zona presente printer.text("\n" * 8) zona_presente = True # Se NON c'è zona, 2cm dopo orario if not zona_presente: printer.text("\n" * 7) # 7 aggiuntive (1+7=8 totali) # ==================== NOTE CLIENTE (PRIMA DEI PRODOTTI) ==================== if config.get('show_customer_note', 'yes') == 'yes': customer_note = order_data.get('customer_note', '') if customer_note: print_separator(printer, width, '=') apply_text_formatting(printer, config, 'customer_note') printer.text(f"NOTE: {customer_note.upper()}\n") reset_formatting(printer) print_separator(printer, width, '=') printer.text("\n") # ==================== PRODOTTI ==================== indent = ' ' * int(config.get('addon_indent', 2)) for item in order_data.get('items', []): name = item.get('name', '') quantity = item.get('quantity', 1) # Nome prodotto apply_text_formatting(printer, config, 'product') printer.text(f"{quantity} {name.upper()}\n") reset_formatting(printer) # Addons (✅ con spazio dopo +) addons = item.get('addons', []) if addons: apply_text_formatting(printer, config, 'addon') for addon in addons: printer.text(f"{indent}+ {addon.upper()}\n") # ✅ Spazio dopo + reset_formatting(printer) print_separator(printer, width) # ✅ Spazio PRIMA del totale (2 righe = dimezzato) printer.text("\n" * 2) # ==================== TOTALE ==================== if config.get('show_total', 'yes') == 'yes': total = order_data.get('total', 0) try: total_num = float(total) except (ValueError, TypeError): total_num = 0.0 apply_text_formatting(printer, config, 'total') printer.text(f"TOTALE {total_num:.2f}\n") reset_formatting(printer) # ✅ Stesso spazio DOPO totale (2 righe) printer.text("\n" * 2) # ==================== CLIENTE ==================== customer = order_data.get('customer', {}) # Nome apply_text_formatting(printer, config, 'customer_name') printer.text(f"{customer.get('name', '').upper()}\n") reset_formatting(printer) # Indirizzo if config.get('show_customer_address', 'yes') == 'yes': address = customer.get('address', '') civic_number = customer.get('civic_number', '') # ✅ v1.4: Numero civico separato if address: apply_text_formatting(printer, config, 'customer_address') # ✅ v1.4: Concatena indirizzo + civico se presente if civic_number: printer.text(f"{address} {civic_number}\n") else: printer.text(f"{address}\n") city = customer.get('city', '') if city: printer.text(f"{city}\n") reset_formatting(printer) # Telefono if config.get('show_customer_phone', 'yes') == 'yes': phone = customer.get('phone', '') if phone: apply_text_formatting(printer, config, 'customer_phone') printer.text(f"{phone}\n") reset_formatting(printer) # ==================== METODO PAGAMENTO ==================== payment_method = order_data.get('payment_method', '') payment_method_title = order_data.get('payment_method_title', '') if payment_method_title: printer.text("\n") apply_text_formatting(printer, config, 'total') # ✅ Verifica se pagamento già effettuato prepaid_methods = ['stripe', 'paypal', 'card', 'carta'] is_prepaid = any(method in payment_method.lower() for method in prepaid_methods) # Traduci metodi comuni payment_map = { 'cash on delivery': 'CONTANTI', 'cod': 'CONTANTI', 'contrassegno': 'CONTANTI', 'bacs': 'BONIFICO', 'cheque': 'ASSEGNO', 'stripe': 'CARTA', 'paypal': 'PAYPAL', 'card': 'CARTA', 'carta di credito': 'CARTA' } payment_lower = payment_method_title.lower() payment_text = payment_map.get(payment_lower, payment_method_title.upper()) printer.text(f"Pagamento: {payment_text}\n") # ✅ Aggiungi "Ordine già pagato" se prepagato if is_prepaid: printer.text("ORDINE GIA PAGATO\n") reset_formatting(printer) # ==================== DATA/ORA ORDINE (PICCOLO) ==================== printer.text("\n") printer.set(font='b') # Font piccolo # Usa data ordine dal database order_date = order_data.get('order_date', '') if order_date: printer.text(f"Ordine del: {order_date}\n") else: # Fallback: usa timestamp corrente now = datetime.now().strftime('%d/%m/%Y %H:%M') printer.text(f"Stampato: {now}\n") reset_formatting(printer) # ==================== SPAZI FINALI ==================== printer.text("\n\n\n") # Taglio carta try: printer.cut(mode='PART') except: try: printer.cut() except: printer.text("\n" * 5) printer.close() logger.info(f"✅ Stampato ordine #{order_data.get('order_number')} su {printer_config['name']}") return { 'success': True, 'message': f"Stampato su {printer_config['name']}" } except Exception as e: logger.error(f"❌ Errore stampa su {printer_config['name']}: {e}") return { 'success': False, 'error': str(e) } # ==================== ENDPOINT ==================== @app.route('/print-order', methods=['POST']) @require_api_key def print_order(): """Endpoint stampa comanda""" try: order_data = request.get_json() if not order_data: return jsonify({'success': False, 'error': 'Nessun dato'}), 400 order_id = order_data.get('order_id', 'N/A') logger.info(f"📥 Ordine #{order_id}") printers = order_data.get('config', {}).get('printers', []) if not printers: return jsonify({'success': False, 'error': 'Nessuna stampante configurata'}), 400 results = [] errors = [] for printer_config in printers: copies = printer_config.get('copies', 1) for copy_num in range(copies): result = print_order_thermal(order_data, printer_config) results.append(result) if not result['success']: errors.append(f"{printer_config['name']}: {result.get('error')}") break if errors: return jsonify({ 'success': False, 'error': ' | '.join(errors), 'results': results }), 500 else: return jsonify({ 'success': True, 'message': f'Stampato {len(results)} copie', 'results': results }) except Exception as e: logger.exception("❌ Errore") return jsonify({'success': False, 'error': str(e)}), 500 @app.route('/test', methods=['GET', 'POST']) @require_api_key def test_connection(): """Test connessione""" return jsonify({ 'success': True, 'message': 'Thermal Bridge PRO operativo', 'version': '2.2.6', 'escpos_available': ESCPOS_AVAILABLE, 'features': [ '2cm DOPO zona (se presente)', '2cm DOPO orario (se NO zona)', 'Spazio totale dimezzato (2+2 righe)', '"Ordine già pagato" per prepagati' ], 'timestamp': datetime.now().isoformat() }) @app.route('/', methods=['GET']) def home(): """Homepage""" html = f""" Thermal Printer Bridge PRO v2.2.6

🖨️ Thermal Printer Bridge PRO v2.2.6

✅ Bridge attivo
ESC/POS: {'✅ Disponibile' if ESCPOS_AVAILABLE else '❌ Non installata'}
Porta: {BRIDGE_PORT}
Versione: 2.2.6

✨ Spazi ottimizzati v2.2.6

""" return html # ==================== RUN ==================== if __name__ == '__main__': logger.info("=" * 60) logger.info("🖨️ THERMAL PRINTER BRIDGE PRO v2.2.6") logger.info("=" * 60) logger.info(f"Porta: {BRIDGE_PORT}") logger.info(f"ESC/POS: {'✅ OK' if ESCPOS_AVAILABLE else '❌ Mancante'}") logger.info("✨ 2cm dopo zona (se presente) o dopo orario") logger.info("=" * 60) if not ESCPOS_AVAILABLE: logger.warning("⚠️ Installa: pip install python-escpos --break-system-packages") app.run(host='0.0.0.0', port=BRIDGE_PORT, debug=False) https://elabonta.store/page-sitemap.xml 2026-03-22T17:19:01+00:00 https://elabonta.store/product-sitemap.xml 2026-02-24T11:19:41+00:00 https://elabonta.store/product_cat-sitemap.xml 2026-02-24T11:19:41+00:00