Приём документов → реестр + журнал
Внесение данных из файлов приёма в реестр невостребованных документов и заполнение журнала. Дата внесения — текущая; журнал заполняется чисто (без строк-примеров шаблона). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
286
gui.py
Normal file
286
gui.py
Normal file
@@ -0,0 +1,286 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user