Files
kadastr18/gui.py
jze9 b5f7060a1f Приём документов → реестр + журнал
Внесение данных из файлов приёма в реестр невостребованных документов
и заполнение журнала. Дата внесения — текущая; журнал заполняется чисто
(без строк-примеров шаблона).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 03:11:18 +05:00

287 lines
12 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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()