feat: логи, фикс табов, справка с инструкциями

1) Логи: все действия пишутся в dokogen.log (импорт, генерация,
   обращения к Морфер/DaData/ИИ с ответами), кнопка «Экспорт логов»
   на вкладке «Лог»
2) Табы: при пересборке параграфа табуляция сохраняется как <w:tab/>
   (раньше Word «съедал» табы между «_____» и датой)
3) Справка: инструкция пользователя (циклы, программа), инструкция
   администратора (словари, API-ключи, лимиты), справочник переменных,
   пустой опросник Excel — всё скачивается в Word/Excel
This commit is contained in:
2026-08-03 14:32:59 +04:00
parent 01d317eab1
commit 50d3afdbf2
8 changed files with 413 additions and 15 deletions
+106 -5
View File
@@ -49,6 +49,13 @@ class DokoGenApp:
# None = import_settings.json рядом с программой (создаётся автоматически)
self.import_settings_path: Optional[str] = None
# Файловый лог: рядом с exe (frozen) или рядом с модулем
if getattr(sys, 'frozen', False):
_base = os.path.dirname(sys.executable)
else:
_base = os.path.dirname(os.path.abspath(__file__))
self._log_path = os.path.join(_base, 'dokogen.log')
# Переменные
self.doc_number_var = tk.StringVar(value="")
self.doc_date_var = tk.StringVar(value="")
@@ -103,7 +110,10 @@ class DokoGenApp:
help_menu = tk.Menu(menubar, tearoff=0)
menubar.add_cascade(label="Справка", menu=help_menu)
help_menu.add_command(label="О программе", command=self.show_about)
help_menu.add_command(label="Переменные шаблонов", command=self.show_variables)
help_menu.add_command(label="Инструкция пользователя (Word)…", command=self.download_user_manual)
help_menu.add_command(label="Инструкция администратора (Word)…", command=self.download_admin_manual)
help_menu.add_command(label="Переменные шаблонов (Word)…", command=self.download_variables_doc)
help_menu.add_command(label="Пустой опросник (Excel)…", command=self.download_survey)
# ==================== ИНТЕРФЕЙС ====================
def _build_ui(self):
@@ -364,6 +374,7 @@ class DokoGenApp:
if not name and not short:
messagebox.showwarning("Проверка", "Сначала введите полное или краткое название организации")
return
self.log(f"🌐 Запрос к Морфер 3.0: «{name or short}»")
self._verify_log("Запрос к Морфер 3.0…")
def _run():
@@ -395,6 +406,7 @@ class DokoGenApp:
if not settings.get('ai_api_key'):
messagebox.showwarning("Настройки", "Не задан API-ключ ИИ.\nМеню «Файл → Настройки»")
return
self.log(f"🤖 Запрос к ИИ ({settings.get('ai_model') or 'deepseek-chat'}): склонение «{name or short}»")
self._verify_log("Запрос к ИИ…")
def _run():
@@ -420,6 +432,7 @@ class DokoGenApp:
def _verify_declension_done(self, results, err):
if err:
self._verify_log(f"{err}")
self.log(f"❌ Проверка склонений: {err}")
return
# Собираем единый результат для применения
combined = {
@@ -446,6 +459,13 @@ class DokoGenApp:
lines.append("\nНажмите «✓ Применить», чтобы подставить склонения")
self._verify_result = combined
self._verify_log('\n'.join(lines))
# Общий лог с ответом сервиса
sample = ''
for key, res in results:
sample = res['data'].get('П', '')
if sample:
break
self.log(f"{combined['source']}: склонение проверено → «{sample}»")
def _check_company_web(self):
inn_entry = self._company_entries.get('inn')
@@ -457,6 +477,7 @@ class DokoGenApp:
if not settings.get('dadata_token'):
messagebox.showwarning("Настройки", "Не задан API-ключ DaData.\nМеню «Файл → Настройки»")
return
self.log(f"🌐 Запрос к DaData: ИНН/ОГРН {inn}")
self._verify_log("Запрос к DaData…")
def _run():
@@ -478,6 +499,7 @@ class DokoGenApp:
'ogrn_date': c.ogrn_date,
'chief_position': c.chief_position, 'chief_fio': c.chief_fio,
}
self.log(f"🤖 Запрос к ИИ ({settings.get('ai_model') or 'deepseek-chat'}): реквизиты (ИНН {company_data.get('inn') or ''})")
self._verify_log("Запрос к ИИ…")
def _run():
@@ -490,6 +512,7 @@ class DokoGenApp:
def _verify_company_done(self, result, err):
if err:
self._verify_log(f"{err}")
self.log(f"❌ Проверка реквизитов: {err}")
return
self._verify_result = result
lines = [f"Источник: {result['source']} ({result['source_url']})"]
@@ -527,6 +550,13 @@ class DokoGenApp:
lines.append(f" {label}: {v}")
lines.append("\nНажмите «✓ Применить», чтобы подставить данные")
self._verify_log('\n'.join(lines))
# Общий лог с ответом сервиса
summary = ''
if 'full_name' in d:
summary = f"{d.get('full_name', '')[:60]} (ИНН {d.get('inn', '')})"
else:
summary = f"оценка {d.get('rating', '')}, замечаний: {len(d.get('notes') or [])}"
self.log(f"{result['source']}: проверка реквизитов → {summary}")
def _verify_apply(self):
result = self._verify_result
@@ -1000,6 +1030,13 @@ class DokoGenApp:
def _build_log_tab(self):
tab = ttk.Frame(self.notebook)
self.notebook.add(tab, text="Лог")
top = ttk.Frame(tab)
top.pack(fill=tk.X, padx=5, pady=3)
ttk.Button(top, text="💾 Экспорт логов…", command=self._export_logs).pack(side=tk.LEFT)
ttk.Label(top, text="Журнал пишется в dokogen.log рядом с программой",
foreground='gray').pack(side=tk.LEFT, padx=8)
self.log_text = scrolledtext.ScrolledText(tab, height=10)
self.log_text.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
self.log_text.insert(tk.END, "Лог готов\n")
@@ -2057,8 +2094,32 @@ class DokoGenApp:
self.log_text.configure(state='disabled')
except Exception:
pass
# Дублируем в файловый журнал
try:
with open(self._log_path, 'a', encoding='utf-8') as f:
f.write(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] {message}\n")
except Exception:
pass
self.status_var.set(message)
def _export_logs(self):
"""Экспорт лога в файл."""
filename = filedialog.asksaveasfilename(
title="Экспорт логов",
defaultextension=".log",
initialfile=f"dokogen_log_{datetime.now().strftime('%Y%m%d_%H%M')}.log",
filetypes=[("Лог-файл", "*.log"), ("Текстовый файл", "*.txt"), ("Все файлы", "*.*")]
)
if not filename:
return
try:
content = self.log_text.get('1.0', tk.END)
with open(filename, 'w', encoding='utf-8') as f:
f.write(content)
self.log(f"💾 Лог экспортирован: {filename}")
except Exception as e:
messagebox.showerror("Ошибка", f"Не удалось экспортировать лог: {e}")
def show_about(self):
messagebox.showinfo("О программе",
"ДоКоГеНеРаТоР — 152-ФЗ\n"
@@ -2066,10 +2127,50 @@ class DokoGenApp:
"Генератор документов из шаблонов")
def show_variables(self):
text = "Переменные шаблонов:\n\n"
for var, group, desc, _ in VARIABLE_DEFS:
text += f"{var}{desc} ({'общ.' if group == 'A' else '152-ФЗ'})\n"
messagebox.showinfo("Переменные шаблонов", text)
"""Показ переменных шаблонов — теперь через скачивание Word-документа."""
self.download_variables_doc()
# ---------- СПРАВКА: СКАЧИВАНИЕ ИНСТРУКЦИЙ / ОПРОСНИКА ----------
def _save_resource(self, source_path, default_name, title):
"""Копирует ресурс (docx/xlsx) в выбранное пользователем место."""
if not source_path or not os.path.exists(source_path):
messagebox.showerror("Справка", f"Файл не найден:\n{source_path}")
return
filename = filedialog.asksaveasfilename(
title=title,
defaultextension=os.path.splitext(default_name)[1],
initialfile=default_name,
filetypes=[("Документ", "*.docx"), ("Все файлы", "*.*")]
)
if not filename:
return
try:
import shutil
shutil.copy(source_path, filename)
self.log(f"💾 Сохранено: {filename}")
messagebox.showinfo("Справка", f"Файл сохранён:\n{filename}")
except Exception as e:
messagebox.showerror("Ошибка", f"Не удалось сохранить: {e}")
def download_user_manual(self):
from .help_docs import ensure_docs, user_manual_path
ensure_docs()
self._save_resource(user_manual_path(), "Инструкция_пользователя.docx", "Сохранить инструкцию пользователя")
def download_admin_manual(self):
from .help_docs import ensure_docs, admin_manual_path
ensure_docs()
self._save_resource(admin_manual_path(), "Инструкция_администратора.docx", "Сохранить инструкцию администратора")
def download_variables_doc(self):
from .help_docs import ensure_docs, variables_reference_path
ensure_docs()
self._save_resource(variables_reference_path(), "Переменные_шаблонов.docx", "Сохранить справочник переменных")
def download_survey(self):
from .help_docs import ensure_docs, survey_path
ensure_docs()
self._save_resource(survey_path(), "Опросный_лист_152_ФЗ.xlsx", "Сохранить пустой опросник")
def main():