""" Label Printer GUI Application using Kivy Simplified mobile-friendly interface for printing labels. """ from kivy.app import App from kivy.uix.boxlayout import BoxLayout from kivy.uix.gridlayout import GridLayout from kivy.uix.label import Label from kivy.uix.textinput import TextInput from kivy.uix.button import Button from kivy.uix.spinner import Spinner from kivy.uix.scrollview import ScrollView from kivy.uix.popup import Popup from kivy.core.window import Window from kivy.uix.image import Image as KivyImage from kivy.graphics import Color, Rectangle import os import threading import platform import time import datetime import glob from print_label import print_label_standalone, get_available_printers from kivy.clock import Clock # Set window size - portrait/phone dimensions (375x667 like iPhone) # Adjusted to be slightly wider for touch-friendly UI Window.size = (420, 700) class LabelPrinterApp(App): """Simplified Kivy application for label printing""" def __init__(self, **kwargs): super().__init__(**kwargs) # Build printer display names and mapping to full names full_printers = get_available_printers() self.printer_display_map = {} # display_name -> full_name self.available_printers = [] for full_name in full_printers: display_name = self._shorten_printer_name(full_name) # Ensure unique display names if display_name in self.printer_display_map: display_name = full_name[:20] self.printer_display_map[display_name] = full_name self.available_printers.append(display_name) # Clean old PDF backup files on startup self.cleanup_old_pdfs() # Clean old log files on startup self.cleanup_old_logs() def _shorten_printer_name(self, name, max_len=20): """Shorten printer name for display (max 20 chars). For network printers like \\\\server\\printer, show just the printer part.""" if name.startswith('\\\\'): # Network printer: \\server\printer -> extract printer name parts = name.strip('\\').split('\\') if len(parts) >= 2: short = parts[-1] # Just the printer name else: short = name else: short = name # Truncate to max_len if len(short) > max_len: short = short[:max_len] return short def _get_full_printer_name(self, display_name): """Resolve display name back to full printer name for printing.""" return self.printer_display_map.get(display_name, display_name) def cleanup_old_pdfs(self, days=5): """ Delete PDF files older than specified days from pdf_backup folder. Args: days (int): Delete files older than this many days (default: 5) """ pdf_backup_dir = 'pdf_backup' # Create folder if it doesn't exist if not os.path.exists(pdf_backup_dir): os.makedirs(pdf_backup_dir, exist_ok=True) return try: current_time = time.time() cutoff_time = current_time - (days * 24 * 3600) # Convert days to seconds for filename in os.listdir(pdf_backup_dir): file_path = os.path.join(pdf_backup_dir, filename) # Only process PDF files if not filename.endswith('.pdf'): continue # Check if file is older than cutoff if os.path.isfile(file_path): file_mtime = os.path.getmtime(file_path) if file_mtime < cutoff_time: try: os.remove(file_path) print(f"Deleted old PDF: {filename}") except Exception as e: print(f"Failed to delete {filename}: {e}") except Exception as e: print(f"Error during PDF cleanup: {e}") def cleanup_old_logs(self, days=5): """ Delete log files older than specified days from logs folder. Args: days (int): Delete files older than this many days (default: 5) """ logs_dir = 'logs' # Create folder if it doesn't exist if not os.path.exists(logs_dir): os.makedirs(logs_dir, exist_ok=True) return try: current_time = time.time() cutoff_time = current_time - (days * 24 * 3600) # Convert days to seconds for filename in os.listdir(logs_dir): file_path = os.path.join(logs_dir, filename) # Process both .log and .csv files if not (filename.endswith('.log') or filename.endswith('.csv')): continue # Check if file is older than cutoff if os.path.isfile(file_path): file_mtime = os.path.getmtime(file_path) if file_mtime < cutoff_time: try: os.remove(file_path) print(f"Deleted old log: {filename}") except Exception as e: print(f"Failed to delete {filename}: {e}") except Exception as e: print(f"Error during log cleanup: {e}") def log_print_action(self, sap_nr, quantity, cable_id, printer, pdf_filename, success): """ Log the print action to a CSV log file (table format). Args: sap_nr (str): SAP article number quantity (str): Quantity value cable_id (str): Cable ID printer (str): Printer name pdf_filename (str): Path to the generated PDF file success (bool): Whether the print was successful """ logs_dir = 'logs' # Create logs folder if it doesn't exist os.makedirs(logs_dir, exist_ok=True) try: # Create log filename with date log_date = datetime.datetime.now().strftime("%Y%m%d") log_filename = os.path.join(logs_dir, f"print_log_{log_date}.csv") # Create log entry values timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") status = "SUCCESS" if success else "FAILED" # Escape CSV values (handle commas and quotes) def escape_csv(value): """Escape CSV special characters""" value = str(value) if ',' in value or '"' in value or '\n' in value: value = '"' + value.replace('"', '""') + '"' return value # Create CSV line log_line = ( f"{timestamp},{status}," f"{escape_csv(sap_nr)}," f"{escape_csv(quantity)}," f"{escape_csv(cable_id)}," f"{escape_csv(printer)}," f"{escape_csv(pdf_filename)}\n" ) # Check if file exists to add header on first entry file_exists = os.path.isfile(log_filename) # Write to log file with open(log_filename, 'a', encoding='utf-8') as f: # Add header if file is new if not file_exists: f.write("Timestamp,Status,SAP-Nr,Quantity,Cable ID,Printer,PDF File\n") # Append log entry f.write(log_line) print(f"Log entry saved to: {log_filename}") except Exception as e: print(f"Error saving log entry: {e}") def build(self): """Build the simplified single-column UI""" self.title = "Label Printing" # Main container - single column layout main_layout = BoxLayout(orientation='vertical', spacing=8, padding=12) # Title title = Label( text='[b]Label Printing[/b]', markup=True, size_hint_y=0.08, font_size='18sp', color=(1, 1, 1, 1) ) main_layout.add_widget(title) # Scroll view for form fields scroll = ScrollView(size_hint_y=0.75) form_layout = GridLayout(cols=1, spacing=8, size_hint_y=None, padding=8) form_layout.bind(minimum_height=form_layout.setter('height')) # SAP-Nr. Articol sap_label = Label( text='SAP-Nr. Articol:', size_hint_y=None, height=30, font_size='12sp' ) form_layout.add_widget(sap_label) self.sap_input = TextInput( multiline=False, size_hint_y=None, height=45, font_size='14sp', background_color=(0.95, 0.95, 0.95, 1), padding=(10, 10) ) self.sap_input.bind(text=self.on_sap_text_change) form_layout.add_widget(self.sap_input) # Cantitate qty_label = Label( text='Cantitate:', size_hint_y=None, height=30, font_size='12sp' ) form_layout.add_widget(qty_label) self.qty_input = TextInput( multiline=False, size_hint_y=None, height=45, font_size='14sp', background_color=(0.95, 0.95, 0.95, 1), padding=(10, 10), input_filter='int' # Only allow numbers ) self.qty_input.bind(text=self.on_qty_text_change) form_layout.add_widget(self.qty_input) # ID rola cablu cable_id_label = Label( text='ID rola cablu:', size_hint_y=None, height=30, font_size='12sp' ) form_layout.add_widget(cable_id_label) self.cable_id_input = TextInput( multiline=False, size_hint_y=None, height=45, font_size='14sp', background_color=(0.95, 0.95, 0.95, 1), padding=(10, 10) ) self.cable_id_input.bind(text=self.on_cable_id_text_change) form_layout.add_widget(self.cable_id_input) # Printer selection printer_label = Label( text='Select Printer:', size_hint_y=None, height=30, font_size='12sp' ) form_layout.add_widget(printer_label) printer_spinner = Spinner( text=self.available_printers[0] if self.available_printers else "No Printers", values=self.available_printers, size_hint_y=None, height=45, font_size='12sp', sync_height=True, ) self.printer_spinner = printer_spinner form_layout.add_widget(printer_spinner) scroll.add_widget(form_layout) main_layout.add_widget(scroll) # Print button print_button = Button( text='PRINT LABEL', size_hint_y=0.15, font_size='14sp', background_color=(0.2, 0.6, 0.2, 1), background_normal='', bold=True ) print_button.bind(on_press=self.print_label) main_layout.add_widget(print_button) return main_layout def on_sap_text_change(self, instance, value): """Limit SAP input to 25 characters""" if len(value) > 25: self.sap_input.text = value[:25] def on_qty_text_change(self, instance, value): """Limit Quantity input to 25 characters""" if len(value) > 25: self.qty_input.text = value[:25] def on_cable_id_text_change(self, instance, value): """Limit Cable ID input to 25 characters""" if len(value) > 25: self.cable_id_input.text = value[:25] def print_label(self, instance): """Handle print button press""" sap_nr = self.sap_input.text.strip() quantity = self.qty_input.text.strip() cable_id = self.cable_id_input.text.strip() # Resolve display name to full printer name printer = self._get_full_printer_name(self.printer_spinner.text) # Validate input if not sap_nr and not quantity and not cable_id: self.show_popup("Error", "Please enter at least one field") return # Create combined label text label_text = f"{sap_nr}|{quantity}|{cable_id}" # Show loading popup popup = Popup( title='Printing', content=BoxLayout( orientation='vertical', padding=10, spacing=10 ), size_hint=(0.8, 0.3) ) popup.content.add_widget(Label(text='Processing label...\nPlease wait')) popup.open() # Print in background thread (using PDF by default) def print_thread(): pdf_filename = None success = False try: success = print_label_standalone(label_text, printer, preview=0, use_pdf=True) # Get the PDF filename that was created # Files are saved to pdf_backup/ with timestamp pdf_files = glob.glob('pdf_backup/final_label_*.pdf') if pdf_files: # Get the most recently created PDF file pdf_filename = max(pdf_files, key=os.path.getctime) if success: # Log the successful print action self.log_print_action(sap_nr, quantity, cable_id, printer, pdf_filename or "unknown", True) # Use Clock.schedule_once to update UI from main thread Clock.schedule_once(lambda dt: popup.dismiss(), 0) Clock.schedule_once(lambda dt: self.show_popup("Success", "Label printed successfully!", auto_dismiss=True), 0.1) # Clear inputs after successful print (but keep printer selection) Clock.schedule_once(lambda dt: self.clear_inputs(), 0.2) else: # Log the failed print action self.log_print_action(sap_nr, quantity, cable_id, printer, pdf_filename or "unknown", False) Clock.schedule_once(lambda dt: popup.dismiss(), 0) Clock.schedule_once(lambda dt: self.show_popup("Error", "Failed to print label"), 0.1) except Exception as e: # Log the error self.log_print_action(sap_nr, quantity, cable_id, printer, pdf_filename or "unknown", False) Clock.schedule_once(lambda dt: popup.dismiss(), 0) Clock.schedule_once(lambda dt: self.show_popup("Error", f"Print error: {str(e)}"), 0.1) thread = threading.Thread(target=print_thread) thread.daemon = True thread.start() def clear_inputs(self): """Clear only the input fields, preserving printer selection""" self.sap_input.text = '' self.qty_input.text = '' self.cable_id_input.text = '' # Printer selection is NOT cleared - it persists until user changes it def show_popup(self, title, message, auto_dismiss=False): """Show a popup message Args: title (str): Popup title message (str): Popup message auto_dismiss (bool): If True, popup will auto-dismiss after 3 seconds """ popup = Popup( title=title, content=BoxLayout( orientation='vertical', padding=10, spacing=10 ), size_hint=(0.8, 0.4) ) popup.content.add_widget(Label(text=message)) close_button = Button(text='OK', size_hint_y=0.3) close_button.bind(on_press=popup.dismiss) popup.content.add_widget(close_button) popup.open() # Auto-dismiss after 3 seconds if requested if auto_dismiss: Clock.schedule_once(lambda dt: popup.dismiss(), 3) if __name__ == '__main__': app = LabelPrinterApp() app.run()