Files
label_printer/label_printer_gui.py
scheianuionut f09c365384 Fix printer detection, implement portable deployment with SumatraPDF
- Fixed network printer enumeration (PRINTER_ENUM_LOCAL | PRINTER_ENUM_CONNECTIONS)
- Added printer name truncation to 20 chars with full name mapping
- Implemented silent PDF printing using SumatraPDF with landscape orientation
- Added auto-dismiss for success popup (3 seconds)
- Bundled SumatraPDF inside executable for portable single-file deployment
- Updated build script to embed SumatraPDF
- Added setup_sumatra.ps1 for downloading SumatraPDF portable
- Added DEPLOYMENT.md documentation
2026-02-06 14:00:17 +02:00

457 lines
16 KiB
Python

"""
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()