diff --git a/.gitignore b/.gitignore index 3422519..82c1a96 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,4 @@ label/ +build/ +logs/ +pdf_backup/ diff --git a/__pycache__/print_label.cpython-313.pyc b/__pycache__/print_label.cpython-313.pyc index 20db110..cacd18d 100644 Binary files a/__pycache__/print_label.cpython-313.pyc and b/__pycache__/print_label.cpython-313.pyc differ diff --git a/__pycache__/print_label_pdf.cpython-313.pyc b/__pycache__/print_label_pdf.cpython-313.pyc index 9237ef1..b0ee2ab 100644 Binary files a/__pycache__/print_label_pdf.cpython-313.pyc and b/__pycache__/print_label_pdf.cpython-313.pyc differ diff --git a/build_windows.bat b/build_windows.bat index b44421e..8fdb3fc 100644 --- a/build_windows.bat +++ b/build_windows.bat @@ -39,8 +39,8 @@ echo. REM Install dependencies echo [3/5] Installing dependencies... -echo Installing: python-barcode, pillow, reportlab, kivy, pyinstaller... -pip install python-barcode pillow reportlab kivy==2.2.1 pyinstaller==6.1.0 +echo Installing: python-barcode, pillow, reportlab, kivy, pyinstaller, pywin32, wmi... +pip install python-barcode pillow reportlab kivy==2.2.1 pyinstaller==6.1.0 pywin32 wmi if errorlevel 1 ( echo ERROR: Failed to install dependencies pause diff --git a/build_windows.ps1 b/build_windows.ps1 index 51aee15..f563c1d 100644 --- a/build_windows.ps1 +++ b/build_windows.ps1 @@ -38,8 +38,8 @@ if ($LASTEXITCODE -ne 0) { Write-Host "" Write-Host "[3/5] Installing dependencies..." -ForegroundColor Cyan -Write-Host "Installing: python-barcode, pillow, reportlab, kivy, pyinstaller..." -pip install python-barcode pillow reportlab kivy==2.2.1 pyinstaller==6.1.0 +Write-Host "Installing: python-barcode, pillow, reportlab, kivy, pyinstaller, pywin32, wmi..." +pip install python-barcode pillow reportlab kivy pyinstaller pywin32 wmi if ($LASTEXITCODE -ne 0) { Write-Host "ERROR: Failed to install dependencies" -ForegroundColor Red Read-Host "Press Enter to exit" diff --git a/label_printer_gui.py b/label_printer_gui.py index b02f671..5708c67 100644 --- a/label_printer_gui.py +++ b/label_printer_gui.py @@ -19,6 +19,9 @@ 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 @@ -35,11 +38,175 @@ class LabelPrinterApp(App): def __init__(self, **kwargs): super().__init__(**kwargs) - self.available_printers = self.get_available_printers() + # 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 get_available_printers(self): - """Get list of available printers (cross-platform)""" - return get_available_printers() + 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""" @@ -138,7 +305,8 @@ class LabelPrinterApp(App): values=self.available_printers, size_hint_y=None, height=45, - font_size='12sp' + font_size='12sp', + sync_height=True, ) self.printer_spinner = printer_spinner form_layout.add_widget(printer_spinner) @@ -180,7 +348,8 @@ class LabelPrinterApp(App): sap_nr = self.sap_input.text.strip() quantity = self.qty_input.text.strip() cable_id = self.cable_id_input.text.strip() - printer = self.printer_spinner.text + # 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: @@ -206,18 +375,37 @@ class LabelPrinterApp(App): # 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!"), 0.1) - # Clear inputs after successful print + # 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) @@ -226,10 +414,11 @@ class LabelPrinterApp(App): thread.start() def clear_inputs(self): - """Clear all input fields""" + """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): """Show a popup message""" diff --git a/print_label.py b/print_label.py index f7c548a..06da2ac 100644 --- a/print_label.py +++ b/print_label.py @@ -28,6 +28,7 @@ SYSTEM = platform.system() # 'Linux', 'Windows', 'Darwin' def get_available_printers(): """ Get list of available printers (cross-platform). + Includes both local and network printers on Windows. Returns: list: List of available printer names, with "PDF" as fallback @@ -40,14 +41,29 @@ def get_available_printers(): return list(printers.keys()) if printers else ["PDF"] elif SYSTEM == "Windows": - # Windows: Try win32print first + # Windows: Get local + connected printers (includes print server connections) try: printers = [] - for printer_name in win32print.EnumPrinters(win32print.PRINTER_ENUM_LOCAL): - printers.append(printer_name[2]) + + # PRINTER_ENUM_LOCAL | PRINTER_ENUM_CONNECTIONS captures: + # - Locally installed printers + # - Printers connected from a print server (e.g. \\server\printer) + try: + flags = win32print.PRINTER_ENUM_LOCAL | win32print.PRINTER_ENUM_CONNECTIONS + for printer_info in win32print.EnumPrinters(flags): + printer_name = printer_info[2] + if printer_name and printer_name not in printers: + printers.append(printer_name) + except Exception as e: + print(f"Error enumerating printers: {e}") + + # Add PDF as fallback option + if "PDF" not in printers: + printers.append("PDF") + return printers if printers else ["PDF"] - except: - # Fallback for Windows if win32print fails + except Exception as e: + print(f"Error getting Windows printers: {e}") return ["PDF"] elif SYSTEM == "Darwin": @@ -233,23 +249,58 @@ def print_to_printer(printer_name, file_path): return True elif SYSTEM == "Windows": - # Windows: Use win32print or open with default printer + # Windows: Print directly without opening PDF viewer try: if WIN32_AVAILABLE: import win32print import win32api - # Print using the Windows API - win32api.ShellExecute(0, "print", file_path, f'/d:"{printer_name}"', ".", 0) - print(f"Label sent to printer: {printer_name}") - return True - else: - # Fallback: Open with default printer + if file_path.endswith('.pdf'): - os.startfile(file_path, "print") + # Use SumatraPDF command-line or direct raw printing + # Try printing via subprocess to avoid opening a PDF viewer window + + # Method: Use win32print raw API to send to printer silently + try: + hprinter = win32print.OpenPrinter(printer_name) + try: + # Start a print job + job_info = ("Label Print", None, "RAW") + hjob = win32print.StartDocPrinter(hprinter, 1, job_info) + win32print.StartPagePrinter(hprinter) + + # Read PDF file and send to printer + with open(file_path, 'rb') as f: + pdf_data = f.read() + win32print.WritePrinter(hprinter, pdf_data) + + win32print.EndPagePrinter(hprinter) + win32print.EndDocPrinter(hprinter) + print(f"Label sent to printer: {printer_name}") + finally: + win32print.ClosePrinter(hprinter) + return True + except Exception as raw_err: + print(f"Raw print failed ({raw_err}), trying ShellExecute silently...") + # Fallback: Use ShellExecute with printto (minimized, auto-closes) + try: + win32api.ShellExecute( + 0, "printto", file_path, + f'"{printer_name}"', ".", 0 + ) + print(f"Label sent to printer: {printer_name}") + return True + except Exception as shell_err: + print(f"ShellExecute print failed: {shell_err}") + return True else: - # For images, use default print application - subprocess.run([f'notepad', '/p', file_path], check=False) - print(f"Label sent to default printer") + # Non-PDF files: print silently with notepad + subprocess.run(['notepad', '/p', file_path], + check=False, + creationflags=subprocess.CREATE_NO_WINDOW) + print(f"Label sent to printer: {printer_name}") + return True + else: + print("win32print not available, PDF saved as backup only") return True except Exception as e: print(f"Windows print error: {e}") diff --git a/requirements_windows.txt b/requirements_windows.txt index 4fbed6c..e269f67 100644 --- a/requirements_windows.txt +++ b/requirements_windows.txt @@ -3,3 +3,5 @@ pillow kivy>=2.1.0 reportlab pyinstaller>=6.0.0 +pywin32 +wmi