Import latest version from label_printer repository as starting point
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1 +1,4 @@
|
|||||||
label/
|
label/
|
||||||
|
build/
|
||||||
|
logs/
|
||||||
|
pdf_backup/
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@@ -39,8 +39,8 @@ echo.
|
|||||||
|
|
||||||
REM Install dependencies
|
REM Install dependencies
|
||||||
echo [3/5] Installing dependencies...
|
echo [3/5] Installing dependencies...
|
||||||
echo Installing: python-barcode, pillow, reportlab, kivy, pyinstaller...
|
echo Installing: python-barcode, pillow, reportlab, kivy, pyinstaller, pywin32, wmi...
|
||||||
pip install python-barcode pillow reportlab kivy==2.2.1 pyinstaller==6.1.0
|
pip install python-barcode pillow reportlab kivy==2.2.1 pyinstaller==6.1.0 pywin32 wmi
|
||||||
if errorlevel 1 (
|
if errorlevel 1 (
|
||||||
echo ERROR: Failed to install dependencies
|
echo ERROR: Failed to install dependencies
|
||||||
pause
|
pause
|
||||||
|
|||||||
@@ -38,8 +38,8 @@ if ($LASTEXITCODE -ne 0) {
|
|||||||
Write-Host ""
|
Write-Host ""
|
||||||
|
|
||||||
Write-Host "[3/5] Installing dependencies..." -ForegroundColor Cyan
|
Write-Host "[3/5] Installing dependencies..." -ForegroundColor Cyan
|
||||||
Write-Host "Installing: python-barcode, pillow, reportlab, kivy, pyinstaller..."
|
Write-Host "Installing: python-barcode, pillow, reportlab, kivy, pyinstaller, pywin32, wmi..."
|
||||||
pip install python-barcode pillow reportlab kivy==2.2.1 pyinstaller==6.1.0
|
pip install python-barcode pillow reportlab kivy pyinstaller pywin32 wmi
|
||||||
if ($LASTEXITCODE -ne 0) {
|
if ($LASTEXITCODE -ne 0) {
|
||||||
Write-Host "ERROR: Failed to install dependencies" -ForegroundColor Red
|
Write-Host "ERROR: Failed to install dependencies" -ForegroundColor Red
|
||||||
Read-Host "Press Enter to exit"
|
Read-Host "Press Enter to exit"
|
||||||
|
|||||||
@@ -19,6 +19,9 @@ from kivy.graphics import Color, Rectangle
|
|||||||
import os
|
import os
|
||||||
import threading
|
import threading
|
||||||
import platform
|
import platform
|
||||||
|
import time
|
||||||
|
import datetime
|
||||||
|
import glob
|
||||||
from print_label import print_label_standalone, get_available_printers
|
from print_label import print_label_standalone, get_available_printers
|
||||||
from kivy.clock import Clock
|
from kivy.clock import Clock
|
||||||
|
|
||||||
@@ -35,11 +38,175 @@ class LabelPrinterApp(App):
|
|||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
super().__init__(**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):
|
def _shorten_printer_name(self, name, max_len=20):
|
||||||
"""Get list of available printers (cross-platform)"""
|
"""Shorten printer name for display (max 20 chars).
|
||||||
return get_available_printers()
|
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):
|
def build(self):
|
||||||
"""Build the simplified single-column UI"""
|
"""Build the simplified single-column UI"""
|
||||||
@@ -138,7 +305,8 @@ class LabelPrinterApp(App):
|
|||||||
values=self.available_printers,
|
values=self.available_printers,
|
||||||
size_hint_y=None,
|
size_hint_y=None,
|
||||||
height=45,
|
height=45,
|
||||||
font_size='12sp'
|
font_size='12sp',
|
||||||
|
sync_height=True,
|
||||||
)
|
)
|
||||||
self.printer_spinner = printer_spinner
|
self.printer_spinner = printer_spinner
|
||||||
form_layout.add_widget(printer_spinner)
|
form_layout.add_widget(printer_spinner)
|
||||||
@@ -180,7 +348,8 @@ class LabelPrinterApp(App):
|
|||||||
sap_nr = self.sap_input.text.strip()
|
sap_nr = self.sap_input.text.strip()
|
||||||
quantity = self.qty_input.text.strip()
|
quantity = self.qty_input.text.strip()
|
||||||
cable_id = self.cable_id_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
|
# Validate input
|
||||||
if not sap_nr and not quantity and not cable_id:
|
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)
|
# Print in background thread (using PDF by default)
|
||||||
def print_thread():
|
def print_thread():
|
||||||
|
pdf_filename = None
|
||||||
|
success = False
|
||||||
try:
|
try:
|
||||||
success = print_label_standalone(label_text, printer, preview=0, use_pdf=True)
|
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:
|
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
|
# Use Clock.schedule_once to update UI from main thread
|
||||||
Clock.schedule_once(lambda dt: popup.dismiss(), 0)
|
Clock.schedule_once(lambda dt: popup.dismiss(), 0)
|
||||||
Clock.schedule_once(lambda dt: self.show_popup("Success", "Label printed successfully!"), 0.1)
|
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)
|
Clock.schedule_once(lambda dt: self.clear_inputs(), 0.2)
|
||||||
else:
|
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: popup.dismiss(), 0)
|
||||||
Clock.schedule_once(lambda dt: self.show_popup("Error", "Failed to print label"), 0.1)
|
Clock.schedule_once(lambda dt: self.show_popup("Error", "Failed to print label"), 0.1)
|
||||||
except Exception as e:
|
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: popup.dismiss(), 0)
|
||||||
Clock.schedule_once(lambda dt: self.show_popup("Error", f"Print error: {str(e)}"), 0.1)
|
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()
|
thread.start()
|
||||||
|
|
||||||
def clear_inputs(self):
|
def clear_inputs(self):
|
||||||
"""Clear all input fields"""
|
"""Clear only the input fields, preserving printer selection"""
|
||||||
self.sap_input.text = ''
|
self.sap_input.text = ''
|
||||||
self.qty_input.text = ''
|
self.qty_input.text = ''
|
||||||
self.cable_id_input.text = ''
|
self.cable_id_input.text = ''
|
||||||
|
# Printer selection is NOT cleared - it persists until user changes it
|
||||||
|
|
||||||
def show_popup(self, title, message):
|
def show_popup(self, title, message):
|
||||||
"""Show a popup message"""
|
"""Show a popup message"""
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ SYSTEM = platform.system() # 'Linux', 'Windows', 'Darwin'
|
|||||||
def get_available_printers():
|
def get_available_printers():
|
||||||
"""
|
"""
|
||||||
Get list of available printers (cross-platform).
|
Get list of available printers (cross-platform).
|
||||||
|
Includes both local and network printers on Windows.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
list: List of available printer names, with "PDF" as fallback
|
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"]
|
return list(printers.keys()) if printers else ["PDF"]
|
||||||
|
|
||||||
elif SYSTEM == "Windows":
|
elif SYSTEM == "Windows":
|
||||||
# Windows: Try win32print first
|
# Windows: Get local + connected printers (includes print server connections)
|
||||||
try:
|
try:
|
||||||
printers = []
|
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"]
|
return printers if printers else ["PDF"]
|
||||||
except:
|
except Exception as e:
|
||||||
# Fallback for Windows if win32print fails
|
print(f"Error getting Windows printers: {e}")
|
||||||
return ["PDF"]
|
return ["PDF"]
|
||||||
|
|
||||||
elif SYSTEM == "Darwin":
|
elif SYSTEM == "Darwin":
|
||||||
@@ -233,23 +249,58 @@ def print_to_printer(printer_name, file_path):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
elif SYSTEM == "Windows":
|
elif SYSTEM == "Windows":
|
||||||
# Windows: Use win32print or open with default printer
|
# Windows: Print directly without opening PDF viewer
|
||||||
try:
|
try:
|
||||||
if WIN32_AVAILABLE:
|
if WIN32_AVAILABLE:
|
||||||
import win32print
|
import win32print
|
||||||
import win32api
|
import win32api
|
||||||
# Print using the Windows API
|
|
||||||
win32api.ShellExecute(0, "print", file_path, f'/d:"{printer_name}"', ".", 0)
|
if file_path.endswith('.pdf'):
|
||||||
|
# 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:
|
||||||
|
# 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}")
|
print(f"Label sent to printer: {printer_name}")
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
# Fallback: Open with default printer
|
print("win32print not available, PDF saved as backup only")
|
||||||
if file_path.endswith('.pdf'):
|
|
||||||
os.startfile(file_path, "print")
|
|
||||||
else:
|
|
||||||
# For images, use default print application
|
|
||||||
subprocess.run([f'notepad', '/p', file_path], check=False)
|
|
||||||
print(f"Label sent to default printer")
|
|
||||||
return True
|
return True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Windows print error: {e}")
|
print(f"Windows print error: {e}")
|
||||||
|
|||||||
@@ -3,3 +3,5 @@ pillow
|
|||||||
kivy>=2.1.0
|
kivy>=2.1.0
|
||||||
reportlab
|
reportlab
|
||||||
pyinstaller>=6.0.0
|
pyinstaller>=6.0.0
|
||||||
|
pywin32
|
||||||
|
wmi
|
||||||
|
|||||||
Reference in New Issue
Block a user