From 0743c440514f4de87a5f2cdaa29ba2c3f176188a Mon Sep 17 00:00:00 2001 From: NAME Date: Thu, 12 Feb 2026 19:25:05 +0200 Subject: [PATCH] Import latest version from label_printer repository as starting point --- .gitignore | 3 + __pycache__/print_label.cpython-313.pyc | Bin 12465 -> 13581 bytes __pycache__/print_label_pdf.cpython-313.pyc | Bin 10070 -> 10069 bytes build_windows.bat | 4 +- build_windows.ps1 | 4 +- label_printer_gui.py | 205 +++++++++++++++++++- print_label.py | 83 ++++++-- requirements_windows.txt | 2 + 8 files changed, 273 insertions(+), 28 deletions(-) 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 20db110c8617d69be816874a0b8623ddac181255..cacd18de38062e7deffef9c7242389a73de1eb2d 100644 GIT binary patch delta 2998 zcmai0YfM|$9Y5#3{5Cc=#s=G5Fks9hkN_jeAjqM%p(7^Y#%d2KT(Y{r39;>SHW#{;jLb_?k z_W%6P|NX!Be}C8Six*BApBM~UfYG3R?Wp?W1LmG!x;b`E5BX?6e1ow53j8EdaMPiW^ZyRMTxl0}be1+Ci(Z-MX!z zTQFAo{QrIU(ckd3b1~|ZGq~~cQUMY3GCR{)l?ZHvHG=+XKe+KGAaFA4XTuxiJr3hw zgl+51^sz;rYajvR_k zPL25|3-%qdv8ix4Fm`leDl8QA0i5%2rLF=eE-Xm$Y(Wz^8)T+z}?mOEzo%`-O z_x{B=ez2JUz4+3XnI=otNw{iGv!S)cdzFZ+MBbXotP(R+FL#^lG{<9TQ6 zwW(iD-I)Bux$lt*_(HH~0#?U$^+%SDyw&;k)X%1_2ku!r|LN(vcy`^ice!o39&b;z z=q1&U!cT{Qz4-}%xB#nT-MM$gyh5)x3~yRT?pjB-iZoViJ;m`{PXJ|Tx^Jpmx0h{f z!}m-hdAsZ4;w9r&k;Tfbq8ojo8d$o8JAK{n2)V#bCq-zJme5Wr*adF(_?aLFZVl3e z4%cD2YHq@c)!jtBIzUWUNB#Pc1>COb!!!pdLTM5y$7zEdbk1Z9dgz>+#SS?SOC%jM z;k&dU6P*hTnnF5yjfMEI#xdA!O~(@6L=)b^h8pNKJ0yHVN#fFuQ}mkG6dIw|h9Kcb z*zxSU>Y@s^qqj_FA)i&5v(Vj#`zqAN6iLiT(ehnR%!_l9@(NbWWb?HP9M_LoWdMa8 zN9x)MJ4whPLNJ&DOl_aovpPI_E~T%Ns5DEcUy%0+IA`pgmI4|2PB zWOSKz?rP1`JW1&k41zHNdugFYFzuswFoPe9@)FGGt;YH~YYE=H__99dsu$`6+cDV7 z2((~Azj5`j^)vXbgRXn6-b;&$kEJ|6CCJOj|Vt z4tLdSLl@mT)a2En&Q@JdLj-=~z41Tt_!_gHyDd;dk`>@C<#j~!pVJnl1wG`)mdqh3 z^MVu?F^U~7gJr=id_$xcPD;sS;H(rolaUH)jHKzsMCPP&oW#_mGoq4-Cz6tauLMw9 z327!8I-OB`Dy6$*MGT?~BK9JKp%4&ME|G_#pi3oZqqF5AlpZWelP82PFQxRXnMxoU z&7`Vms$9h;{~)#weA6+I+73eWr)F`n)*LCIF`!?GTZnPi# zsQrb@x}sWJ*Os@pY}yAl>;uck@7WLL?fj;_XT#oeLtL(3@qKvoPXGGhBlqmn_wBBA zerQF%{OYH=xA{QLQ+uYJG!l+4Z7Ms@09u3AD5FhPdSQ7o3{R8&w$=QJhWs1EL-bM>{sPA2DrgvX%0OO@;9_L9RM znm7z1J zjZEg;f>g#h0-Q<&lY%ypPRMwq-^;A|@~O67-jQDb-7I{uU?~ zh^jQg>f!m+?3tu=K=}if5tvI~L0|L=o?%%3_*FQE2jlTkx)BQS@$exCt^azN!rP1R MtKx(Po#>nT53upjzW@LL delta 1974 zcma)7TWlLe6rI`iuHR1LY!W|XCvoDqq>j_Ls+w1owj27$tKOJ4NgLNVo7B|Vn6*RO z2>L<@2?;gL2o?HM5JCzcpcD&KA%yyJ)j!-)wP9OI#XnLAMFR1|%(_-f1rlTLnY(B1 z+_`&a&+NOeT#Q?OGn)+nw;u~{CToHhEhizQ(HVG4KL|iAXay2PS4>`N+=2!juI`PH z_ySM~cGLhMQBfGFDl*SvloB)yCQ9C3FklD3h$iT!8m40(0P(j-@bT)>q=@b8GS|`y z1a=P;wD_mnOiMIbJW59#C9kJ_#EpUuR~OD=3(i6l)kHi`2TPV>-Lu7Ye5{xl9=&^5+j(OH420 z1A^L5!70#9d%zS7SmuwT3(N+@vQy|Yc7h%ZSdm#f95C_H+|00?l(M3nQLMv}or5EM zB*qU$2V(t0+j{#IHl9w4(j@vq+cK=^`J)LjolVY43X>+Y0<5A*%u14?Q*TKS(-SKu z#b-o?mBq|lDywKxNl8?om|=0oUbQI}v|Z=71~K8nS0;ux_#{}hG@>_j{b8-yv*L6w z=pOij_k5kVeVyOzy4N*&t84VGZ)~A|g>#)7Iy>}X|8m7gLwC9GuQX^JhiH@j{jya* zW%fL>xfhNsTK-yfAh#iWVjjmo-D~IF;EIbP=rIsn*Q@9C;A)s6xQS5Ls9qCa39eg1 z81pnmkR>zndWJ8j^A)2*d~+L~pzL%E%fre%*`kW$uAX1F1BPes=%Rx}d7S zga_4DdQIaS01$M7ez#Vl)n2E*;#RB?E50{Gp^;)ExP12Gxl*D3%%OR`KBxCz<6yjFaTIExP(|RPYx0Mlw6cS={ozK zs#2O>-~oc32xyuv_D==$T&#}gVmZd^N-N>0v(0tTiB7GnZizw*Xs7>+s+PgC@n zY+TMxB~zm8#rHDFNp2(~bS5hYSUEs!J#mPo<9L>zI1ug0h(HU5XY?trN8ZM2>j#+q zo{W5qYlrn&-HvuP#(B={`l+&ham#(K@jln^h;!ZJ0=K!qQr*(t<>ck?P1EJ$h3HF# z-o1s<{e_9x9q!-*&R%e}FZVC^6dYUcaNAa#9u#P*t9i@FI*FVkiZQxw55#yBx1HoVy55d(JdMj{utviIN>p?5VJT&k@ zI&ZY|0Xpx~5GKG7CP)*z&Cs`z&Uc3TLiCLg)3-6lHGj>*aPGU{i_lN%uV}Kz(qd6L zM2>QVkcMO?Dd7rB2~j?Wxu)X+(`USf&2~`QV$$m*UaqXTuSVbzrq~ePmno}c7%oc m7Fe@30jD9XS{tFAP=IS)J&^l-D>VS&F8IglZXNnIJoGoN54INo diff --git a/__pycache__/print_label_pdf.cpython-313.pyc b/__pycache__/print_label_pdf.cpython-313.pyc index 9237ef132562c0cd99d8c7ad064f4e96a13719c5..b0ee2abb2ffae38d1a18fcaf12d7c8f510d2291c 100644 GIT binary patch delta 47 zcmccSch!&YGcPX}0}w3fZ_RAb-pJ?4BBbnW6_b;gl$sM?P?VWhl3EnAIg90u5&(0K B5J3O{ delta 48 zcmccWcg>IQGcPX}0}!k~)0`QpwUN)0MOa0@xTs9uCow5CM>i$4I5Ry@e{&|w8zlgP CGY~fb 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