# modules/serial_writer.py import tkinter as tk from tkinter import ttk, messagebox from pathlib import Path import sys import os from datetime import datetime import json # Добавляем родительскую директорию в путь для импорта sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) try: # Пытаемся импортировать из config.py import config SEGMENT_LENGTHS = config.SEGMENT_LENGTHS DOCUMENTS_PATH = config.DOCUMENTS_PATH SERIAL_FILE = config.SERIAL_FILE APP_STYLE = config.APP_STYLE except ImportError: # Локальные значения по умолчанию SEGMENT_LENGTHS = [4, 9, 4, 4] DOCUMENTS_PATH = Path.home() / "Documents" / "C4" SERIAL_FILE = DOCUMENTS_PATH / "serialdata.dat" APP_STYLE = { 'title_font': ("Segoe UI", 12, "bold"), 'entry_font': ("Consolas", 14), 'label_font': ("Segoe UI", 10), 'padding': {'padx': 20, 'pady': 10} } class SerialWriterTab: """ Класс для создания вкладки ввода серийного номера. Теперь это не отдельное приложение, а компонент для конфигуратора. """ def __init__(self, parent_frame): """ Инициализация компонента вкладки. Args: parent_frame: Родительский фрейм, куда будет помещен интерфейс """ self.parent = parent_frame self.segment_lengths = SEGMENT_LENGTHS self.prefixes = ['A', 'D', 'V', 'N'] # Буквенные префиксы для каждого сегмента self.vars = [] self.entries = [] self._updating = False # защита от рекурсии в trace # Определяем путь для файла serial.temp (рядом с исполняемым файлом) self.executable_dir = Path(sys.executable).parent if hasattr(sys, 'frozen') else Path(__file__).parent.parent self.serial_temp_file = self.executable_dir / "serial.temp" # Создаем основной контейнер для вкладки self.container = ttk.Frame(parent_frame, padding=20) self.container.pack(fill=tk.BOTH, expand=True) self._build_ui() # Загружаем данные о последней записи self.load_last_serial_info() # Проверяем существующий серийный номер self._check_existing_serial() def _build_ui(self): """Построение интерфейса вкладки""" # Заголовок вкладки title_frame = ttk.Frame(self.container) title_frame.pack(fill=tk.X, pady=(0, 20)) title_label = ttk.Label( title_frame, text='Серийный номер аппарата "ФИШКА"', font=APP_STYLE['title_font'] ) title_label.pack() # Разделитель ttk.Separator(self.container, orient="horizontal").pack( fill=tk.X, pady=(0, 20) ) # Фрейм для полей ввода entry_frame = ttk.Frame(self.container) entry_frame.pack(pady=(0, 20)) # Поля ввода серийного номера self._create_entry_fields(entry_frame) # Кнопка загрузки последнего серийника self._create_load_last_button(entry_frame) # Фрейм для кнопок button_frame = ttk.Frame(self.container) button_frame.pack(pady=(20, 0)) self._create_buttons(button_frame) # Панель статуса self._create_status_panel() # Устанавливаем фокус на первое поле self.entries[0].focus_set() def _create_entry_fields(self, parent): """Создание полей ввода для серийного номера""" col = 0 for i, (length, prefix) in enumerate(zip(self.segment_lengths, self.prefixes)): # Создаем Label с префиксом prefix_label = ttk.Label( parent, text=prefix, font=APP_STYLE['entry_font'], foreground="black" ) prefix_label.grid(row=0, column=col, padx=(0, 2), pady=(0, 10)) col += 1 # Поле для цифр (длина на 1 меньше, так как первый символ - буква) var = tk.StringVar() var.trace_add("write", lambda *args, i=i: self._on_segment_change(i)) entry = ttk.Entry( parent, textvariable=var, width=length, # Один символ уже занят буквой justify="center", font=APP_STYLE['entry_font'] ) entry.grid(row=0, column=col, padx=2, pady=(0, 10)) self.vars.append(var) self.entries.append(entry) # Навигация Backspace влево entry.bind("", lambda e, i=i: self._on_key(e, i)) col += 1 if i != len(self.segment_lengths) - 1: slash = ttk.Label(parent, text="/", font=APP_STYLE['entry_font']) slash.grid(row=0, column=col, padx=2, pady=(0, 10)) col += 1 # Enter на последнем поле = Сохранить self.entries[-1].bind("", lambda e: self.save_serial()) def _create_load_last_button(self, parent): """Создание кнопки загрузки последнего серийного номера""" self.load_last_btn = ttk.Button( parent, text="Загрузить последний", command=self.load_last_serial, width=30 ) self.load_last_btn.grid(row=0, column=20, padx=(20, 0), pady=(0, 10)) # Обновляем состояние кнопки self.update_load_last_button() def _create_buttons(self, parent): """Создание кнопок управления""" # Кнопка сохранения save_btn = ttk.Button( parent, text="Сохранить серийный номер", command=self.save_serial, width=30 ) save_btn.grid(row=0, column=0, padx=(0, 10)) # Кнопка очистки clear_btn = ttk.Button( parent, text="Очистить поля", command=self.clear_fields, width=30 ) clear_btn.grid(row=0, column=1, padx=(0, 10)) # Кнопка проверки check_btn = ttk.Button( parent, text="Проверить формат", command=self.check_format, width=30 ) check_btn.grid(row=0, column=2) def _create_status_panel(self): """Создание панели статуса""" status_frame = ttk.LabelFrame( self.container, text="Информация", padding=10 ) status_frame.pack(fill=tk.X, pady=(20, 0)) # Формат номера format_label = ttk.Label( status_frame, text="Формат: A999/D15112025/V999/N999", font=APP_STYLE['label_font'] ) format_label.pack(anchor=tk.W, pady=(0, 5)) # Информация о последней записи self.last_record_label = ttk.Label( status_frame, text="Последняя запись: не найдена", font=APP_STYLE['label_font'], foreground="gray" ) self.last_record_label.pack(anchor=tk.W, pady=(0, 5)) # Текущий серийный номер self.current_serial_label = ttk.Label( status_frame, text="Текущий серийный номер: не указан", font=APP_STYLE['label_font'], foreground="gray" ) self.current_serial_label.pack(anchor=tk.W, pady=(0, 5)) # Статус заполнения self.status_label = ttk.Label( status_frame, text="Статус: Не заполнено", font=APP_STYLE['label_font'] ) self.status_label.pack(anchor=tk.W) def load_last_serial_info(self): """Загрузить информацию о последней записи из файла serial.temp""" try: if self.serial_temp_file.exists(): with self.serial_temp_file.open("r", encoding="utf-8") as f: data = json.load(f) last_date = data.get("last_write_date", "неизвестно") last_serial = data.get("last_serial", "неизвестно") self.last_record_label.config( text=f"Последняя запись: {last_date} - {last_serial}", foreground="blue" ) # Обновляем состояние кнопки загрузки self.update_load_last_button() return { "date": last_date, "serial": last_serial } else: self.last_record_label.config( text="Последняя запись: не найдена", foreground="gray" ) except Exception as e: print(f"Ошибка при чтении serial.temp: {e}") self.last_record_label.config( text="Последняя запись: ошибка чтения", foreground="red" ) return None def save_last_serial_info(self, serial_number): """Сохранить информацию о последней записи в файл serial.temp""" try: data = { "last_write_date": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), "last_serial": serial_number } with self.serial_temp_file.open("w", encoding="utf-8") as f: json.dump(data, f, ensure_ascii=False, indent=2) # Обновляем информацию в интерфейсе self.load_last_serial_info() except Exception as e: print(f"Ошибка при сохранении serial.temp: {e}") def load_last_serial(self): """Загрузить последний серийный номер из serial.temp в поля ввода""" info = self.load_last_serial_info() if info and info.get("serial"): serial = info["serial"] # Разбиваем серийный номер на сегменты segments = serial.split("/") if len(segments) == len(self.segment_lengths): for i, seg in enumerate(segments): if i < len(self.vars): # Убираем буквенный префикс и устанавливаем только цифры if len(seg) >= 2: # Буква + хотя бы одна цифра digits = seg[1:] # Пропускаем первую букву self.vars[i].set(digits) self._update_status() messagebox.showinfo( "Загрузка", f"Загружен последний серийный номер:\n{serial}" ) return True else: messagebox.showwarning( "Ошибка формата", f"Формат последнего серийного номера не соответствует ожидаемому" ) else: messagebox.showinfo( "Информация", "Нет данных о последнем серийном номере" ) return False def update_load_last_button(self): """Обновить состояние кнопки загрузки последнего серийника""" if self.serial_temp_file.exists(): self.load_last_btn.config(state=tk.NORMAL) else: self.load_last_btn.config(state=tk.DISABLED) def _check_existing_serial(self): """Проверить существующий серийный номер при запуске""" try: if SERIAL_FILE.exists(): with SERIAL_FILE.open("r", encoding="utf-8") as f: serial = f.readline().strip() if serial: self.current_serial_label.config( text=f"Текущий серийный номер: {serial}", foreground="green" ) except Exception: pass def _on_segment_change(self, index: int): """Автоматически ограничивает длину сегмента и переносит фокус далее.""" if self._updating: return try: self._updating = True value = self.vars[index].get() max_len = self.segment_lengths[index] - 1 # Один символ уже занят буквой # Разрешаем только цифры digits_only = ''.join(filter(str.isdigit, value)) # Обрезаем лишние цифры if len(digits_only) > max_len: digits_only = digits_only[:max_len] # Устанавливаем очищенное значение if digits_only != value: self.vars[index].set(digits_only) # Если сегмент заполнен, переходим к следующему if len(digits_only) == max_len and index + 1 < len(self.entries): next_entry = self.entries[index + 1] next_entry.focus_set() next_entry.icursor("end") # Обновляем статус self._update_status() finally: self._updating = False def _on_key(self, event, index: int): """Backspace на пустом поле переносит фокус на предыдущий сегмент.""" if event.keysym == "BackSpace": entry = self.entries[index] try: cursor_pos = entry.index(tk.INSERT) except tk.TclError: cursor_pos = 0 if cursor_pos == 0 and index > 0 and entry.get() == "": prev_entry = self.entries[index - 1] prev_entry.focus_set() prev_entry.icursor("end") def _get_segments(self): """Получить значения всех сегментов""" segments = [] for i, (var, prefix) in enumerate(zip(self.vars, self.prefixes)): digits = var.get().strip() segments.append(f"{prefix}{digits}") return segments def _get_full_serial(self): """Получить полный серийный номер""" segments = self._get_segments() return "/".join(segments) def _check_complete(self): """Проверить, полностью ли заполнены все сегменты""" segments = self._get_segments() for seg, length in zip(segments, self.segment_lengths): if len(seg) != length: return False return True def _update_status(self): """Обновить статус заполнения""" if self._check_complete(): serial = self._get_full_serial() self.status_label.config( text=f"Статус: ✓ Готово к сохранению: {serial}", foreground="green" ) else: segments = self._get_segments() partial_serial = "/".join(segments) if any(segments) else "Не заполнено" self.status_label.config( text=f"Статус: {partial_serial}", foreground="orange" if any(segments) else "red" ) def clear_fields(self): """Очистить все поля ввода""" for var in self.vars: var.set("") self.entries[0].focus_set() self.status_label.config( text="Статус: Не заполнено", foreground="red" ) def check_format(self): """Проверить формат введённых данных""" segments = [v.get().strip() for v in self.vars] if not segments or all(seg == "" for seg in segments): messagebox.showinfo( "Проверка формата", "Поля не заполнены." ) return errors = [] for i, (seg, length, prefix) in enumerate(zip(segments, self.segment_lengths, self.prefixes)): expected_digits = length - 1 # Один символ - буква if len(seg) != expected_digits: errors.append(f"Сегмент {prefix}: ожидается {expected_digits} цифр, получено {len(seg)}") elif not seg.isdigit(): errors.append(f"Сегмент {prefix}: должны быть только цифры") if errors: messagebox.showwarning( "Проверка формата", "Найдены ошибки:\n\n" + "\n".join(errors) ) else: serial = self._get_full_serial() messagebox.showinfo( "Проверка формата", f"✓ Формат корректен!\n\nСерийный номер:\n{serial}" ) def save_serial(self): """Сохранить серийный номер в файл.""" if not self._check_complete(): messagebox.showerror( "Ошибка", "Заполните все части серийного номера полностью." ) return serial = self._get_full_serial() try: # Создаем директорию, если её нет DOCUMENTS_PATH.mkdir(parents=True, exist_ok=True) # Добавляем новую строку в конец файла with SERIAL_FILE.open("w", encoding="utf-8") as f: f.write(serial + "\n") # Сохраняем информацию о последней записи self.save_last_serial_info(serial) # Обновляем информацию о текущем серийном номере self.current_serial_label.config( text=f"Текущий серийный номер: {serial}", foreground="green" ) messagebox.showinfo( "Готово", f"Серийный номер успешно сохранён!\n\n" f"Номер: {serial}\n\n" f"Информация сохранена в:\n{self.serial_temp_file}" ) # Обновляем статус self.status_label.config( text=f"Статус: ✓ Сохранено: {serial}", foreground="green" ) except Exception as e: messagebox.showerror( "Ошибка сохранения", f"Не удалось сохранить серийный номер:\n{e}" ) def get_serial_number(self): """Получить текущий серийный номер (для использования другими модулями)""" if self._check_complete(): return self._get_full_serial() return None def load_serial_from_file(self): """Загрузить серийный номер из файла (если существует)""" try: if SERIAL_FILE.exists(): with SERIAL_FILE.open("r", encoding="utf-8") as f: serial = f.readline().strip() segments = serial.split("/") if len(segments) == len(self.segment_lengths): for i, seg in enumerate(segments): if i < len(self.vars): # Убираем буквенный префикс и устанавливаем только цифры if len(seg) >= 2: # Буква + хотя бы одна цифра digits = seg[1:] # Пропускаем первую букву self.vars[i].set(digits) self._update_status() # Обновляем информацию о текущем серийном номере self.current_serial_label.config( text=f"Текущий серийный номер: {serial}", foreground="green" ) return True except Exception: pass return False