#!/usr/bin/env python3 # -*- coding: utf-8 -*- """Десктоп-интерфейс для внесения принятых документов в реестр. Запуск: python3 gui.py Окно позволяет выбрать реестр, журнал и папку «прием», задать дату внесения и нажать «Внести данные». Обработка идёт в фоновом потоке, поэтому окно не зависает; ход работы и аномалии видны в логе. Результат пишется рядом с исходными файлами с суффиксом «_ЗАПОЛНЕНО» — оригиналы не изменяются. """ from __future__ import annotations import datetime import logging import queue import threading import tkinter as tk from pathlib import Path from tkinter import filedialog, messagebox, ttk from priem_register import Pipeline, config log = logging.getLogger("priem") PAD = 8 def derive_output(path: str) -> Path: """Путь результата: рядом с исходным, с суффиксом «_ЗАПОЛНЕНО».""" p = Path(path) return p.with_name(p.stem + "_ЗАПОЛНЕНО" + p.suffix) class QueueHandler(logging.Handler): """Перекладывает записи лога в очередь — читает их главный поток GUI.""" def __init__(self, q: "queue.Queue"): super().__init__() self.q = q def emit(self, record: logging.LogRecord) -> None: self.q.put(("log", record.levelno, self.format(record))) class App(tk.Tk): def __init__(self): super().__init__() self.title("Приём документов → реестр") self.geometry("780x580") self.minsize(680, 500) self.msg_queue: "queue.Queue" = queue.Queue() self.worker: threading.Thread | None = None self.register_var = tk.StringVar(value=str(config.REGISTER_SRC)) self.journal_var = tk.StringVar(value=str(config.JOURNAL_SRC)) self.priem_var = tk.StringVar(value=str(config.PRIEM_DIR)) self.date_var = tk.StringVar(value=config.ENTRY_DATE.strftime("%d.%m.%Y")) self._build_form() self._build_log() self._attach_logging() self.after(120, self._poll) # --- построение интерфейса ------------------------------------------ def _build_form(self) -> None: frm = ttk.Frame(self, padding=PAD) frm.pack(fill="x") frm.columnconfigure(1, weight=1) self._file_row(frm, 0, "Реестр (.ods):", self.register_var, [("Таблицы ODF", "*.ods"), ("Все файлы", "*.*")]) self._file_row(frm, 1, "Журнал (.xlsx):", self.journal_var, [("Книги Excel", "*.xlsx"), ("Все файлы", "*.*")]) self._dir_row(frm, 2, "Папка «прием»:", self.priem_var) ttk.Label(frm, text="Дата внесения:").grid( row=3, column=0, sticky="w", pady=4) ttk.Entry(frm, textvariable=self.date_var, width=14).grid( row=3, column=1, sticky="w", pady=4) ttk.Label(frm, text="дд.мм.гггг", foreground="#888").grid( row=3, column=2, sticky="w") bar = ttk.Frame(self, padding=(PAD, 0)) bar.pack(fill="x") self.run_btn = ttk.Button(bar, text="Внести данные", command=self._start) self.run_btn.pack(side="left", pady=PAD) self.progress = ttk.Progressbar(bar, mode="indeterminate", length=180) self.status = ttk.Label(bar, text="Готово к работе", foreground="#444") self.status.pack(side="right") def _file_row(self, frm, row, label, var, patterns) -> None: ttk.Label(frm, text=label).grid(row=row, column=0, sticky="w", pady=4) ttk.Entry(frm, textvariable=var).grid(row=row, column=1, sticky="ew", pady=4) ttk.Button(frm, text="Обзор…", command=lambda: self._pick_file(var, patterns)).grid( row=row, column=2, padx=(PAD, 0)) def _dir_row(self, frm, row, label, var) -> None: ttk.Label(frm, text=label).grid(row=row, column=0, sticky="w", pady=4) ttk.Entry(frm, textvariable=var).grid(row=row, column=1, sticky="ew", pady=4) ttk.Button(frm, text="Обзор…", command=lambda: self._pick_dir(var)).grid( row=row, column=2, padx=(PAD, 0)) def _build_log(self) -> None: frm = ttk.Frame(self, padding=PAD) frm.pack(fill="both", expand=True) ttk.Label(frm, text="Ход работы:").pack(anchor="w") self.log_text = tk.Text(frm, height=16, wrap="word", state="disabled", font=("monospace", 10)) scroll = ttk.Scrollbar(frm, command=self.log_text.yview) self.log_text.configure(yscrollcommand=scroll.set) self.log_text.pack(side="left", fill="both", expand=True) scroll.pack(side="right", fill="y") self.log_text.tag_configure("warn", foreground="#b35900") self.log_text.tag_configure("err", foreground="#c0392b") self.log_text.tag_configure("ok", foreground="#1e7d34") def _attach_logging(self) -> None: handler = QueueHandler(self.msg_queue) handler.setFormatter(logging.Formatter("%(message)s")) log.setLevel(logging.INFO) log.addHandler(handler) # --- выбор файлов --------------------------------------------------- def _pick_file(self, var, patterns) -> None: start = Path(var.get()).parent if var.get() else Path.cwd() path = filedialog.askopenfilename(initialdir=str(start), filetypes=patterns) if path: var.set(path) def _pick_dir(self, var) -> None: start = var.get() or str(Path.cwd()) path = filedialog.askdirectory(initialdir=start) if path: var.set(path) # --- запуск обработки ----------------------------------------------- def _start(self) -> None: if self.worker and self.worker.is_alive(): return try: params = self._validate() except ValueError as exc: messagebox.showerror("Проверьте поля", str(exc)) return self._clear_log() self.run_btn.configure(state="disabled") self.progress.pack(side="left", padx=PAD) self.progress.start(12) self.status.configure(text="Обработка…", foreground="#444") self.worker = threading.Thread(target=self._work, args=(params,), daemon=True) self.worker.start() def _validate(self) -> dict: register = self.register_var.get().strip() journal = self.journal_var.get().strip() priem = self.priem_var.get().strip() if not (register and journal and priem): raise ValueError("Заполните все три пути: реестр, журнал и папку «прием».") if not Path(register).is_file(): raise ValueError("Файл реестра не найден:\n%s" % register) if not Path(journal).is_file(): raise ValueError("Файл журнала не найден:\n%s" % journal) if not Path(priem).is_dir(): raise ValueError("Папка «прием» не найдена:\n%s" % priem) raw = self.date_var.get().strip() try: date = datetime.datetime.strptime(raw, "%d.%m.%Y").date() except ValueError: raise ValueError("Дата должна быть в формате дд.мм.гггг, например 08.06.2026.") return { "register_src": register, "journal_src": journal, "priem_dir": priem, "entry_date": date, "register_out": derive_output(register), "journal_out": derive_output(journal), } def _work(self, params: dict) -> None: """Выполняется в фоновом потоке. Связь с GUI — только через очередь.""" try: pipeline = Pipeline( register_src=params["register_src"], register_out=params["register_out"], journal_src=params["journal_src"], journal_out=params["journal_out"], priem_dir=params["priem_dir"], entry_date=params["entry_date"], ) result = pipeline.run() self.msg_queue.put(("done", { "total": result.total, "files": len(result.entries), "notes": list(result.notes), "warnings": list(result.warnings), "register_out": str(params["register_out"]), "journal_out": str(params["journal_out"]), })) except Exception as exc: # noqa: BLE001 — показываем пользователю log.exception("Сбой обработки") self.msg_queue.put(("error", str(exc))) # --- обновление GUI из очереди -------------------------------------- def _poll(self) -> None: try: while True: kind, *payload = self.msg_queue.get_nowait() if kind == "log": levelno, text = payload tag = "warn" if levelno >= logging.WARNING else None self._append(text, tag) elif kind == "done": self._on_done(payload[0]) elif kind == "error": self._on_error(payload[0]) except queue.Empty: pass self.after(120, self._poll) def _on_done(self, info: dict) -> None: self._finish() n_warn = len(info["warnings"]) n_note = len(info["notes"]) if n_warn: self.status.configure(text="Готово, требуют внимания: %d" % n_warn, foreground="#b35900") self._append("\nВыполнено. %d строк не привязаны — проверьте выше." % n_warn, "warn") else: self.status.configure(text="Готово", foreground="#1e7d34") self._append("\nВыполнено. Все данные внесены.", "ok") if n_note: self._append("Присоединено заметок к записям: %d (см. «•» выше)." % n_note) self._append( "\nРеестр: %s\nЖурнал: %s" % (info["register_out"], info["journal_out"])) summary = ("Внесено записей: %d\nОбработано файлов: %d\n" "Присоединено заметок: %d\nНе привязано строк: %d\n\n" "Реестр: %s\nЖурнал: %s") % ( info["total"], info["files"], n_note, n_warn, info["register_out"], info["journal_out"]) if n_warn: summary += ("\n\n%d строк не удалось привязать к записи " "(см. лог) — проверьте вручную." % n_warn) messagebox.showwarning("Готово (есть непривязанные строки)", summary) else: messagebox.showinfo("Готово", summary) def _on_error(self, message: str) -> None: self._finish() self.status.configure(text="Ошибка", foreground="#c0392b") self._append("\nОШИБКА: %s" % message, "err") messagebox.showerror("Ошибка обработки", message) def _finish(self) -> None: self.progress.stop() self.progress.pack_forget() self.run_btn.configure(state="normal") # --- вывод в лог ---------------------------------------------------- def _append(self, text: str, tag: str | None = None) -> None: self.log_text.configure(state="normal") self.log_text.insert("end", text + "\n", (tag,) if tag else ()) self.log_text.see("end") self.log_text.configure(state="disabled") def _clear_log(self) -> None: self.log_text.configure(state="normal") self.log_text.delete("1.0", "end") self.log_text.configure(state="disabled") def main() -> None: App().mainloop() if __name__ == "__main__": main()