545 lines
22 KiB
Python
545 lines
22 KiB
Python
# 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("<KeyPress>", 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("<Return>", 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 |