Add template selection and multiple copy printing features
- Implemented template selection: type 0=OK (green), type 1=NOK (red) - Added multiple copy printing (1-100 copies) - Extended file format to 5 fields: ARTICLE;NR_ART;SERIAL;TYPE;COUNT - Created OK/NOK SVG templates with visual distinction - Fixed PDF landscape orientation issues - Updated SumatraPDF to use noscale for exact dimensions - Auto-generate conf folder with default templates on first run - Made pystray optional for system tray functionality - Updated build scripts for Python 3.13 compatibility (Kivy 2.3+, PyInstaller 6.18) - Added comprehensive build documentation - Improved printer configuration guidance
This commit is contained in:
@@ -28,10 +28,17 @@ from print_label import print_label_standalone, get_available_printers
|
||||
from kivy.clock import Clock
|
||||
from watchdog.observers import Observer
|
||||
from watchdog.events import FileSystemEventHandler
|
||||
import pystray
|
||||
from pystray import MenuItem as item
|
||||
from PIL import Image, ImageDraw
|
||||
|
||||
# Optional system tray support
|
||||
try:
|
||||
import pystray
|
||||
from pystray import MenuItem as item
|
||||
PYSTRAY_AVAILABLE = True
|
||||
except ImportError:
|
||||
PYSTRAY_AVAILABLE = False
|
||||
print("Warning: pystray not available. System tray functionality disabled.")
|
||||
|
||||
# Set window size - portrait/phone dimensions (375x667 like iPhone)
|
||||
# Adjusted to be slightly wider for touch-friendly UI
|
||||
Window.size = (420, 700)
|
||||
@@ -63,6 +70,8 @@ class LabelPrinterApp(App):
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
# Initialize conf folder with default templates
|
||||
self.initialize_conf_folder()
|
||||
# Build printer display names and mapping to full names
|
||||
full_printers = get_available_printers()
|
||||
self.printer_display_map = {} # display_name -> full_name
|
||||
@@ -109,6 +118,107 @@ class LabelPrinterApp(App):
|
||||
"""Resolve display name back to full printer name for printing."""
|
||||
return self.printer_display_map.get(display_name, display_name)
|
||||
|
||||
def initialize_conf_folder(self):
|
||||
"""
|
||||
Initialize conf folder with default templates and configuration.
|
||||
Creates folder and files if they don't exist.
|
||||
"""
|
||||
conf_dir = 'conf'
|
||||
|
||||
# Create conf folder if it doesn't exist
|
||||
if not os.path.exists(conf_dir):
|
||||
os.makedirs(conf_dir, exist_ok=True)
|
||||
print(f"Created conf folder: {conf_dir}")
|
||||
|
||||
# Define default SVG template (minimal 35mm x 25mm with placeholders)
|
||||
default_svg_template = '''<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="35mm" height="25mm" viewBox="0 0 35 25">
|
||||
<defs>
|
||||
<style type="text/css">
|
||||
.text { font-family: Arial; font-weight: bold; font-size: 3.5px; fill: #000000; }
|
||||
.checkmark { fill: #008000; }
|
||||
</style>
|
||||
</defs>
|
||||
<g id="content">
|
||||
<text x="2" y="7" class="text">Nr. Comanda: {Article}</text>
|
||||
<text x="2" y="12" class="text">Nr. Art.: {NrArt}</text>
|
||||
<text x="2" y="17" class="text">Serial No.: {Serial}</text>
|
||||
<circle cx="31" cy="7" r="2.5" class="checkmark"/>
|
||||
<path d="M30,7 L30.8,7.8 L32,6" stroke="white" stroke-width="0.5" fill="none"/>
|
||||
</g>
|
||||
</svg>'''
|
||||
|
||||
# OK template (green checkmark)
|
||||
ok_svg_template = '''<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="35mm" height="25mm" viewBox="0 0 35 25">
|
||||
<defs>
|
||||
<style type="text/css">
|
||||
.text { font-family: Arial; font-weight: bold; font-size: 3.5px; fill: #000000; }
|
||||
.checkmark { fill: #008000; }
|
||||
.ok-text { font-family: Arial; font-weight: bold; font-size: 4px; fill: #008000; }
|
||||
</style>
|
||||
</defs>
|
||||
<g id="content">
|
||||
<text x="2" y="7" class="text">Nr. Comanda: {Article}</text>
|
||||
<text x="2" y="12" class="text">Nr. Art.: {NrArt}</text>
|
||||
<text x="2" y="17" class="text">Serial No.: {Serial}</text>
|
||||
<text x="29" y="5" class="ok-text">OK</text>
|
||||
<circle cx="31" cy="12" r="2.5" class="checkmark"/>
|
||||
<path d="M30,12 L30.8,12.8 L32,11" stroke="white" stroke-width="0.5" fill="none"/>
|
||||
</g>
|
||||
</svg>'''
|
||||
|
||||
# NOK template (red text and checkmark)
|
||||
nok_svg_template = '''<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="35mm" height="25mm" viewBox="0 0 35 25">
|
||||
<defs>
|
||||
<style type="text/css">
|
||||
.text { font-family: Arial; font-weight: bold; font-size: 3.5px; fill: #CC0000; }
|
||||
.checkmark { fill: #CC0000; }
|
||||
.nok-text { font-family: Arial; font-weight: bold; font-size: 4px; fill: #CC0000; }
|
||||
</style>
|
||||
</defs>
|
||||
<g id="content">
|
||||
<text x="2" y="7" class="text">Nr. Comanda: {Article}</text>
|
||||
<text x="2" y="12" class="text">Nr. Art.: {NrArt}</text>
|
||||
<text x="2" y="17" class="text">Serial No.: {Serial}</text>
|
||||
<text x="27" y="5" class="nok-text">NOK</text>
|
||||
<circle cx="31" cy="12" r="2.5" class="checkmark"/>
|
||||
<path d="M30,12 L30.8,12.8 L32,11" stroke="white" stroke-width="0.5" fill="none"/>
|
||||
</g>
|
||||
</svg>'''
|
||||
|
||||
# Create SVG template files if they don't exist
|
||||
templates = {
|
||||
'label_template.svg': default_svg_template,
|
||||
'label_template_ok.svg': ok_svg_template,
|
||||
'label_template_nok.svg': nok_svg_template,
|
||||
}
|
||||
|
||||
for filename, content in templates.items():
|
||||
file_path = os.path.join(conf_dir, filename)
|
||||
if not os.path.exists(file_path):
|
||||
try:
|
||||
with open(file_path, 'w', encoding='utf-8') as f:
|
||||
f.write(content)
|
||||
print(f"Created template: {file_path}")
|
||||
except Exception as e:
|
||||
print(f"Failed to create {file_path}: {e}")
|
||||
|
||||
# Create default app.conf if it doesn't exist
|
||||
config_path = os.path.join(conf_dir, 'app.conf')
|
||||
if not os.path.exists(config_path):
|
||||
default_config = '''[Settings]
|
||||
file_path = C:\\Users\\Public\\Documents\\check.txt
|
||||
printer = PDF
|
||||
'''
|
||||
try:
|
||||
with open(config_path, 'w', encoding='utf-8') as f:
|
||||
f.write(default_config)
|
||||
print(f"Created config: {config_path}")
|
||||
except Exception as e:
|
||||
print(f"Failed to create {config_path}: {e}")
|
||||
|
||||
def cleanup_old_pdfs(self, days=5):
|
||||
"""
|
||||
Delete PDF files older than specified days from pdf_backup folder.
|
||||
@@ -469,6 +579,10 @@ class LabelPrinterApp(App):
|
||||
"""
|
||||
Minimize the application to system tray.
|
||||
"""
|
||||
if not PYSTRAY_AVAILABLE:
|
||||
print("System tray not available - window will minimize normally")
|
||||
return
|
||||
|
||||
if self.is_minimized:
|
||||
return
|
||||
|
||||
@@ -652,9 +766,13 @@ class LabelPrinterApp(App):
|
||||
print(f"Error clearing file: {e}")
|
||||
|
||||
def read_file_variables(self):
|
||||
"""Read variables from monitored file"""
|
||||
"""Read variables from monitored file
|
||||
Expected format: article;nr_art;serial;template_type;count
|
||||
template_type: 0=OK, 1=NOK
|
||||
count: number of labels to print (default=1)
|
||||
"""
|
||||
if not self.monitored_file or not os.path.exists(self.monitored_file):
|
||||
return None, None, None
|
||||
return None, None, None, 0, 1
|
||||
|
||||
try:
|
||||
with open(self.monitored_file, 'r', encoding='utf-8') as f:
|
||||
@@ -662,14 +780,16 @@ class LabelPrinterApp(App):
|
||||
|
||||
# Skip if file is empty or only contains "-" (cleared marker)
|
||||
if not content or content == '-':
|
||||
return None, None, None
|
||||
return None, None, None, 0, 1
|
||||
|
||||
# Parse file content - expecting format: article;nr_art;serial
|
||||
# Parse file content - expecting format: article;nr_art;serial;template_type;count
|
||||
# or key=value pairs on separate lines
|
||||
lines = content.split('\n')
|
||||
article = ""
|
||||
nr_art = ""
|
||||
serial = ""
|
||||
template_type = 0 # Default to OK template
|
||||
count = 1 # Default to 1 label
|
||||
|
||||
# Try semicolon-separated format first
|
||||
if ';' in content:
|
||||
@@ -680,6 +800,20 @@ class LabelPrinterApp(App):
|
||||
nr_art = parts[1].strip()
|
||||
if len(parts) >= 3:
|
||||
serial = parts[2].strip()
|
||||
if len(parts) >= 4:
|
||||
try:
|
||||
template_type = int(parts[3].strip())
|
||||
except ValueError:
|
||||
template_type = 0
|
||||
if len(parts) >= 5:
|
||||
try:
|
||||
count = int(parts[4].strip())
|
||||
if count < 1:
|
||||
count = 1
|
||||
if count > 100: # Safety limit
|
||||
count = 100
|
||||
except ValueError:
|
||||
count = 1
|
||||
else:
|
||||
# Try key=value format
|
||||
for line in lines:
|
||||
@@ -694,16 +828,30 @@ class LabelPrinterApp(App):
|
||||
nr_art = value
|
||||
elif key in ['serial', 'serial_no', 'serial-no', 'serialno']:
|
||||
serial = value
|
||||
elif key in ['template', 'template_type', 'type']:
|
||||
try:
|
||||
template_type = int(value)
|
||||
except ValueError:
|
||||
template_type = 0
|
||||
elif key in ['count', 'quantity', 'copies']:
|
||||
try:
|
||||
count = int(value)
|
||||
if count < 1:
|
||||
count = 1
|
||||
if count > 100:
|
||||
count = 100
|
||||
except ValueError:
|
||||
count = 1
|
||||
|
||||
return article, nr_art, serial
|
||||
return article, nr_art, serial, template_type, count
|
||||
except Exception as e:
|
||||
print(f"Error reading file: {e}")
|
||||
return None, None, None
|
||||
|
||||
def print_label(self, instance):
|
||||
"""Handle print button press - read from file and print"""
|
||||
# Read variables from file
|
||||
article, nr_art, serial = self.read_file_variables()
|
||||
# Read variables from file including template type and count
|
||||
article, nr_art, serial, template_type, count = self.read_file_variables()
|
||||
|
||||
# Resolve display name to full printer name
|
||||
printer = self._get_full_printer_name(self.printer_spinner.text)
|
||||
@@ -713,10 +861,23 @@ class LabelPrinterApp(App):
|
||||
self.show_popup("Error", "No data in file or file not set", auto_dismiss_after=3)
|
||||
return
|
||||
|
||||
# Select template based on template_type
|
||||
if template_type == 1:
|
||||
template_path = os.path.join('conf', 'label_template_nok.svg')
|
||||
template_name = "NOK"
|
||||
else:
|
||||
template_path = os.path.join('conf', 'label_template_ok.svg')
|
||||
template_name = "OK"
|
||||
|
||||
# Verify template exists, fallback to default if not
|
||||
if not os.path.exists(template_path):
|
||||
template_path = os.path.join('conf', 'label_template.svg')
|
||||
template_name = "DEFAULT"
|
||||
|
||||
# Create combined label text using semicolon separator
|
||||
label_text = f"{article};{nr_art};{serial}"
|
||||
|
||||
# Show loading popup
|
||||
# Show loading popup with count info
|
||||
popup = Popup(
|
||||
title='Printing',
|
||||
content=BoxLayout(
|
||||
@@ -727,15 +888,32 @@ class LabelPrinterApp(App):
|
||||
size_hint=(0.8, 0.3)
|
||||
)
|
||||
|
||||
popup.content.add_widget(Label(text='Processing label...\nPlease wait'))
|
||||
popup.content.add_widget(Label(text=f'Processing {count} label(s)...\nTemplate: {template_name}\nPlease wait'))
|
||||
popup.open()
|
||||
|
||||
# Print in background thread (using PDF by default)
|
||||
def print_thread():
|
||||
pdf_filename = None
|
||||
success = False
|
||||
all_success = True
|
||||
try:
|
||||
success = print_label_standalone(label_text, printer, preview=0, use_pdf=True)
|
||||
# Print multiple copies if count > 1
|
||||
for i in range(count):
|
||||
if count > 1:
|
||||
Clock.schedule_once(lambda dt, idx=i: popup.content.children[0].text.replace(
|
||||
f'Processing {count} label(s)...',
|
||||
f'Printing {idx+1} of {count}...'
|
||||
), 0)
|
||||
|
||||
success = print_label_standalone(label_text, printer, preview=0, use_pdf=True, svg_template=template_path)
|
||||
|
||||
if not success:
|
||||
all_success = False
|
||||
break
|
||||
|
||||
# Small delay between prints for multiple copies
|
||||
if i < count - 1:
|
||||
time.sleep(0.5)
|
||||
|
||||
# Get the PDF filename that was created
|
||||
# Files are saved to pdf_backup/ with timestamp
|
||||
@@ -744,7 +922,7 @@ class LabelPrinterApp(App):
|
||||
# Get the most recently created PDF file
|
||||
pdf_filename = max(pdf_files, key=os.path.getctime)
|
||||
|
||||
if success:
|
||||
if all_success:
|
||||
# Log the successful print action
|
||||
self.log_print_action(article, nr_art, serial, printer, pdf_filename or "unknown", True)
|
||||
|
||||
@@ -753,7 +931,7 @@ class LabelPrinterApp(App):
|
||||
|
||||
# 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_after=3), 0.1)
|
||||
Clock.schedule_once(lambda dt: self.show_popup("Success", f"{count} label(s) printed successfully!", auto_dismiss_after=3), 0.1)
|
||||
else:
|
||||
# Log the failed print action
|
||||
self.log_print_action(article, nr_art, serial, printer, pdf_filename or "unknown", False)
|
||||
|
||||
Reference in New Issue
Block a user