feat: логи, фикс табов, справка с инструкциями
1) Логи: все действия пишутся в dokogen.log (импорт, генерация, обращения к Морфер/DaData/ИИ с ответами), кнопка «Экспорт логов» на вкладке «Лог» 2) Табы: при пересборке параграфа табуляция сохраняется как <w:tab/> (раньше Word «съедал» табы между «_____» и датой) 3) Справка: инструкция пользователя (циклы, программа), инструкция администратора (словари, API-ключи, лимиты), справочник переменных, пустой опросник Excel — всё скачивается в Word/Excel
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+26
-10
@@ -461,10 +461,31 @@ def _apply_highlight_color(run_el, color: str):
|
|||||||
rpr.append(hl)
|
rpr.append(hl)
|
||||||
|
|
||||||
|
|
||||||
|
def _add_run_with_tabs(para, text, style_source=None):
|
||||||
|
"""Добавляет run с текстом, сохраняя табуляцию как <w:tab/>.
|
||||||
|
|
||||||
|
python-docx add_run('\t') вставляет символ таба в w:t, который Word
|
||||||
|
может «съесть». Поэтому разбиваем текст по '\t' и вставляем
|
||||||
|
настоящие элементы табуляции.
|
||||||
|
"""
|
||||||
|
parts = str(text).split('\t')
|
||||||
|
for i, part in enumerate(parts):
|
||||||
|
if part:
|
||||||
|
run = para.add_run(part)
|
||||||
|
if style_source is not None:
|
||||||
|
_copy_run_style(style_source, run)
|
||||||
|
if i < len(parts) - 1:
|
||||||
|
run = para.add_run()
|
||||||
|
if style_source is not None:
|
||||||
|
_copy_run_style(style_source, run)
|
||||||
|
run.add_tab()
|
||||||
|
|
||||||
|
|
||||||
def _replace_text_in_para(para, replacements: Dict[str, str]) -> bool:
|
def _replace_text_in_para(para, replacements: Dict[str, str]) -> bool:
|
||||||
"""Заменяет {{var}} в параграфе. Возвращает True, если были замены.
|
"""Заменяет {{var}} в параграфе. Возвращает True, если были замены.
|
||||||
|
|
||||||
Значения «—» (незаполненная информация) подсвечиваются жёлтым маркером.
|
Значения «—» (незаполненная информация) подсвечиваются жёлтым маркером.
|
||||||
|
Табуляция в параграфе сохраняется (<w:tab/>).
|
||||||
"""
|
"""
|
||||||
full = para.text
|
full = para.text
|
||||||
matches = list(VAR_PATTERN.finditer(full))
|
matches = list(VAR_PATTERN.finditer(full))
|
||||||
@@ -483,9 +504,7 @@ def _replace_text_in_para(para, replacements: Dict[str, str]) -> bool:
|
|||||||
continue
|
continue
|
||||||
# текст до переменной
|
# текст до переменной
|
||||||
if m.start() > pos:
|
if m.start() > pos:
|
||||||
run = para.add_run(full[pos:m.start()])
|
_add_run_with_tabs(para, full[pos:m.start()], first)
|
||||||
if first is not None:
|
|
||||||
_copy_run_style(first, run)
|
|
||||||
# значение переменной
|
# значение переменной
|
||||||
val_str = str(val)
|
val_str = str(val)
|
||||||
run = para.add_run(val_str)
|
run = para.add_run(val_str)
|
||||||
@@ -497,9 +516,7 @@ def _replace_text_in_para(para, replacements: Dict[str, str]) -> bool:
|
|||||||
pos = m.end()
|
pos = m.end()
|
||||||
# хвост после последней переменной
|
# хвост после последней переменной
|
||||||
if pos < len(full):
|
if pos < len(full):
|
||||||
run = para.add_run(full[pos:])
|
_add_run_with_tabs(para, full[pos:], first)
|
||||||
if first is not None:
|
|
||||||
_copy_run_style(first, run)
|
|
||||||
return changed
|
return changed
|
||||||
|
|
||||||
|
|
||||||
@@ -566,21 +583,20 @@ def _highlight_unmatched(doc):
|
|||||||
return
|
return
|
||||||
|
|
||||||
# Пересобираем параграф: обычный текст и переменные отдельными run'ами,
|
# Пересобираем параграф: обычный текст и переменные отдельными run'ами,
|
||||||
# переменные — с красным маркером. Сохраняем стиль первого run.
|
# переменные — с красным маркером. Сохраняем стиль первого run и табы.
|
||||||
first = para.runs[0] if para.runs else None
|
first = para.runs[0] if para.runs else None
|
||||||
para.clear()
|
para.clear()
|
||||||
pos = 0
|
pos = 0
|
||||||
for m in matches:
|
for m in matches:
|
||||||
if m.start() > pos:
|
if m.start() > pos:
|
||||||
run = para.add_run(full[pos:m.start()])
|
_add_run_with_tabs(para, full[pos:m.start()], first)
|
||||||
if first is not None:
|
|
||||||
_copy_run_style(first, run)
|
|
||||||
run = para.add_run(m.group(0))
|
run = para.add_run(m.group(0))
|
||||||
if first is not None:
|
if first is not None:
|
||||||
_copy_run_style(first, run)
|
_copy_run_style(first, run)
|
||||||
_apply_highlight(run._element)
|
_apply_highlight(run._element)
|
||||||
pos = m.end()
|
pos = m.end()
|
||||||
if pos < len(full):
|
if pos < len(full):
|
||||||
|
_add_run_with_tabs(para, full[pos:], first)
|
||||||
run = para.add_run(full[pos:])
|
run = para.add_run(full[pos:])
|
||||||
if first is not None:
|
if first is not None:
|
||||||
_copy_run_style(first, run)
|
_copy_run_style(first, run)
|
||||||
|
|||||||
@@ -0,0 +1,281 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""DokoGen — справка и инструкции (Word) + пустой опросник (Excel).
|
||||||
|
|
||||||
|
Файлы генерируются при первом обращении и кладутся в папку docs/
|
||||||
|
рядом с программой (при сборке .exe — рядом с exe):
|
||||||
|
- Инструкция_пользователя.docx
|
||||||
|
- Инструкция_администратора.docx
|
||||||
|
- Опросный_лист_152_ФЗ.xlsx (пустой шаблон)
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import shutil
|
||||||
|
|
||||||
|
try:
|
||||||
|
import docx
|
||||||
|
from docx.shared import Pt, Cm
|
||||||
|
from docx.enum.text import WD_ALIGN_PARAGRAPH
|
||||||
|
DOCX_AVAILABLE = True
|
||||||
|
except ImportError:
|
||||||
|
DOCX_AVAILABLE = False
|
||||||
|
|
||||||
|
|
||||||
|
def docs_dir():
|
||||||
|
"""Папка docs рядом с exe (frozen) или рядом с модулем."""
|
||||||
|
if getattr(sys, 'frozen', False):
|
||||||
|
base = os.path.dirname(sys.executable)
|
||||||
|
else:
|
||||||
|
base = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
d = os.path.join(base, 'docs')
|
||||||
|
os.makedirs(d, exist_ok=True)
|
||||||
|
return d
|
||||||
|
|
||||||
|
|
||||||
|
def user_manual_path():
|
||||||
|
return os.path.join(docs_dir(), 'Инструкция_пользователя.docx')
|
||||||
|
|
||||||
|
|
||||||
|
def admin_manual_path():
|
||||||
|
return os.path.join(docs_dir(), 'Инструкция_администратора.docx')
|
||||||
|
|
||||||
|
|
||||||
|
def variables_reference_path():
|
||||||
|
return os.path.join(docs_dir(), 'Переменные_шаблонов.docx')
|
||||||
|
|
||||||
|
|
||||||
|
def survey_path():
|
||||||
|
return os.path.join(docs_dir(), 'Опросный_лист_152_ФЗ.xlsx')
|
||||||
|
|
||||||
|
|
||||||
|
def _h(doc, text, size=14):
|
||||||
|
p = doc.add_paragraph()
|
||||||
|
run = p.add_run(text)
|
||||||
|
run.bold = True
|
||||||
|
run.font.size = Pt(size)
|
||||||
|
return p
|
||||||
|
|
||||||
|
|
||||||
|
def _p(doc, text, bold=False):
|
||||||
|
p = doc.add_paragraph()
|
||||||
|
run = p.add_run(text)
|
||||||
|
run.bold = bold
|
||||||
|
run.font.size = Pt(11)
|
||||||
|
return p
|
||||||
|
|
||||||
|
|
||||||
|
def _bullet(doc, text):
|
||||||
|
p = doc.add_paragraph(style='List Bullet')
|
||||||
|
run = p.add_run(text)
|
||||||
|
run.font.size = Pt(11)
|
||||||
|
return p
|
||||||
|
|
||||||
|
|
||||||
|
def _generate_variables_reference(path):
|
||||||
|
"""Справочник переменных шаблонов (Word)."""
|
||||||
|
try:
|
||||||
|
from .variables import VARIABLE_DEFS
|
||||||
|
except Exception:
|
||||||
|
VARIABLE_DEFS = []
|
||||||
|
|
||||||
|
doc = docx.Document()
|
||||||
|
_h(doc, 'Переменные шаблонов')
|
||||||
|
_p(doc, 'Список переменных для шаблонов Word. Переменная подставляется в текст '
|
||||||
|
'как есть: {{имя_переменной}}. Циклы: <!-- loop:имя --> ... <!-- loop_end -->.')
|
||||||
|
|
||||||
|
groups = {'A': 'Общие переменные', 'B': '152-ФЗ (персональные данные)'}
|
||||||
|
by_group = {'A': [], 'B': []}
|
||||||
|
for var, group, desc, _ in VARIABLE_DEFS:
|
||||||
|
by_group.setdefault(group, []).append((var, desc))
|
||||||
|
|
||||||
|
for gkey, title in groups.items():
|
||||||
|
items = by_group.get(gkey, [])
|
||||||
|
if not items:
|
||||||
|
continue
|
||||||
|
_h(doc, title, 12)
|
||||||
|
for var, desc in items:
|
||||||
|
_bullet(doc, f'{var} — {desc}')
|
||||||
|
|
||||||
|
_h(doc, 'Циклы', 12)
|
||||||
|
_bullet(doc, 'information_systems — ИС ({{item.name}}, {{item.description}}, {{item.structure}}, {{item.pd_actions}}, {{item.defense_tools_list}}, {{item.users}}, {{item.category}}, {{item.pd_count}} и др.).')
|
||||||
|
_bullet(doc, 'commission — комиссия ({{item.role}}, {{item.position}}, {{item.fio}}, {{item.fio_initials}}, {{item.fio_short}}, {{item.signature}}).')
|
||||||
|
_bullet(doc, 'employees_access — сотрудники с доступом ({{item.position}}, {{item.fio}}).')
|
||||||
|
_bullet(doc, 'paper_documents — бумажные документы ({{item.document}}, {{item.storage}}).')
|
||||||
|
|
||||||
|
doc.save(path)
|
||||||
|
|
||||||
|
|
||||||
|
def _generate_user_manual(path):
|
||||||
|
"""Инструкция пользователя: программа, циклы, переменные, проверки."""
|
||||||
|
doc = docx.Document()
|
||||||
|
_h(doc, 'ДоКоГеНеРаТоР — 152-ФЗ')
|
||||||
|
_h(doc, 'Инструкция пользователя', 13)
|
||||||
|
|
||||||
|
_h(doc, '1. Что это за программа', 12)
|
||||||
|
_p(doc, 'Программа автоматически заполняет шаблоны документов по 152-ФЗ '
|
||||||
|
'(акты обследования, политики, инструкции, приказы и т.д.) данными '
|
||||||
|
'об организации, информационных системах, комиссии и сотрудниках. '
|
||||||
|
'Данные можно ввести вручную на вкладках или импортировать из '
|
||||||
|
'опросного листа Excel.')
|
||||||
|
|
||||||
|
_h(doc, '2. Основные вкладки', 12)
|
||||||
|
_bullet(doc, 'Организация — реквизиты, адреса, руководитель, склонения названий, проверка данных (Морфер / DaData / ИИ).')
|
||||||
|
_bullet(doc, 'Информационные системы — ИС, категории ПДн, действия с ПДн, средства защиты, пользователи.')
|
||||||
|
_bullet(doc, 'Комиссия — состав комиссии по классификации ИСПДн.')
|
||||||
|
_bullet(doc, 'Пользователи — сотрудники с доступом к ПДн.')
|
||||||
|
_bullet(doc, 'Бумажные документы — бумажные носители ПДн и места хранения.')
|
||||||
|
_bullet(doc, 'Генерация — выбор шаблонов и запуск генерации.')
|
||||||
|
_bullet(doc, 'Лог — журнал действий, экспорт в файл.')
|
||||||
|
|
||||||
|
_h(doc, '3. Переменные в шаблонах', 12)
|
||||||
|
_p(doc, 'В шаблонах Word используются переменные в двойных фигурных скобках, '
|
||||||
|
'например {{company_full_name}}, {{inn}}, {{chief_fio_initials}}. '
|
||||||
|
'Полный список — в пункте меню «Справка → Переменные шаблонов».')
|
||||||
|
_p(doc, 'Переменные, которые не удалось заменить (нет данных), подсвечиваются '
|
||||||
|
'КРАСНЫМ маркером. Незаполненная информация заменяется прочерком «—» '
|
||||||
|
'и подсвечивается ЖЁЛТЫМ.')
|
||||||
|
|
||||||
|
_h(doc, '4. Циклы (повторяющиеся блоки)', 12)
|
||||||
|
_p(doc, 'Если в документе нужно повторить блок для каждой ИС, комиссии, '
|
||||||
|
'сотрудника или бумажного документа — используются маркеры цикла:')
|
||||||
|
_p(doc, '<!-- loop:information_systems --> ... {{item.name}} ... <!-- loop_end -->', bold=True)
|
||||||
|
_p(doc, 'Доступные циклы:')
|
||||||
|
_bullet(doc, 'information_systems — ИС ({{item.name}}, {{item.description}}, {{item.structure}}, {{item.pd_actions}}, {{item.defense_tools_list}}, {{item.users}}, {{item.category}}, {{item.pd_count}} и др.).')
|
||||||
|
_bullet(doc, 'commission — комиссия ({{item.role}}, {{item.position}}, {{item.fio}}, {{item.fio_initials}}, {{item.fio_short}}, {{item.signature}}).')
|
||||||
|
_bullet(doc, 'employees_access — сотрудники с доступом ({{item.position}}, {{item.fio}}).')
|
||||||
|
_bullet(doc, 'paper_documents — бумажные документы ({{item.document}}, {{item.storage}}).')
|
||||||
|
_p(doc, 'Внутри цикла переменные пишутся через точку: {{item.поле}}. '
|
||||||
|
'Допускается и синтаксис без точки: {{item_поле}}.')
|
||||||
|
|
||||||
|
_h(doc, '5. Импорт из опросника', 12)
|
||||||
|
_p(doc, 'Меню «Генерация → Импорт из опросника» (или кнопка «📥 Импорт»). '
|
||||||
|
'Выберите заполненный опросный лист Excel — данные подтянутся '
|
||||||
|
'автоматически. Что именно импортировать и откуда — настраивается '
|
||||||
|
'в файле import_settings.json (см. инструкцию администратора).')
|
||||||
|
|
||||||
|
_h(doc, '6. Проверка данных', 12)
|
||||||
|
_bullet(doc, 'Склонение — Морфер 3.0 (бесплатно, без ключа) или ИИ.')
|
||||||
|
_bullet(doc, 'Реквизиты — DaData (по ИНН) или ИИ.')
|
||||||
|
_bullet(doc, 'ИИ-результаты помечаются дисклеймером «сгенерировано ИИ — перепроверьте».')
|
||||||
|
_bullet(doc, 'Ключи API задаются в меню «Файл → Настройки».')
|
||||||
|
|
||||||
|
_h(doc, '7. Генерация', 12)
|
||||||
|
_p(doc, '1) Укажите папку с шаблонами (кнопка «📁 Шаблоны»).\n'
|
||||||
|
'2) Выберите нужные шаблоны в списке.\n'
|
||||||
|
'3) Укажите папку для результатов.\n'
|
||||||
|
'4) Нажмите «⚡ Генерация».')
|
||||||
|
_p(doc, 'Результаты сохраняются в выбранную папку. Если какой-то документ '
|
||||||
|
'не создался — смотрите вкладку «Лог»: там будет причина.')
|
||||||
|
|
||||||
|
doc.save(path)
|
||||||
|
|
||||||
|
|
||||||
|
def _generate_admin_manual(path):
|
||||||
|
"""Инструкция администратора: словари, API-ключи, лимиты."""
|
||||||
|
doc = docx.Document()
|
||||||
|
_h(doc, 'ДоКоГеНеРаТоР — 152-ФЗ')
|
||||||
|
_h(doc, 'Инструкция администратора', 13)
|
||||||
|
|
||||||
|
_h(doc, '1. Файлы настроек рядом с программой', 12)
|
||||||
|
_bullet(doc, 'api_settings.json — API-ключи (DaData, ИИ).')
|
||||||
|
_bullet(doc, 'import_settings.json — словарь импорта: что и откуда брать из опросника.')
|
||||||
|
_bullet(doc, 'dictionaries/ — справочники: defense_tools.json, pd_actions.json, pd_items.json, pd_categories.json.')
|
||||||
|
_bullet(doc, 'dokogen.log — журнал работы (пишется автоматически).')
|
||||||
|
_bullet(doc, 'docs/ — инструкции и пустой опросник (создаются автоматически).')
|
||||||
|
|
||||||
|
_h(doc, '2. Словари (папка dictionaries)', 12)
|
||||||
|
_p(doc, 'JSON-файлы со списками значений для выпадающих списков. '
|
||||||
|
'Формат — простой массив строк:')
|
||||||
|
_p(doc, '[\n "Антивирусное ПО",\n "СКЗИ (криптография)",\n "Межсетевой экран"\n]', bold=True)
|
||||||
|
_p(doc, 'Чтобы добавить значение — откройте файл в Блокноте, добавьте строку '
|
||||||
|
'через запятую и сохраните. Программа перечитает файл при следующем '
|
||||||
|
'открытии окна выбора.')
|
||||||
|
|
||||||
|
_h(doc, '3. Словарь импорта (import_settings.json)', 12)
|
||||||
|
_p(doc, 'Определяет, какие поля опросника импортируются и откуда. Каждое правило:')
|
||||||
|
_p(doc, '{"name": "Полное наименование организации", "var": "fullName", '
|
||||||
|
'"cell": "\'Общие сведения\'!B2", "keywords": "полное наименование организации"}', bold=True)
|
||||||
|
_bullet(doc, 'cell — конкретная ячейка (если заполнена — берём оттуда);')
|
||||||
|
_bullet(doc, 'keywords — регулярное выражение для поиска строки-вопроса в колонке A;')
|
||||||
|
_bullet(doc, 'var — ключ в данных программы (fullName, inn, kpp, ogrn и т.д.).')
|
||||||
|
_p(doc, 'Также настраиваются: common_sheet_keyword (лист «Общие сведения»), '
|
||||||
|
'employees_sheet_keyword (лист «Сотрудники»), is_sheet_exclude_keywords '
|
||||||
|
'(какие листы НЕ ИС), value_column (колонка значения, 1 = B).')
|
||||||
|
|
||||||
|
_h(doc, '4. API-ключи (меню «Файл → Настройки»)', 12)
|
||||||
|
_p(doc, 'DaData — проверка организации по ИНН/ОГРН (наименование, адрес, КПП, руководитель).')
|
||||||
|
_bullet(doc, 'Где взять: https://dadata.ru → регистрация → раздел API. Бесплатный тариф ~10 000 запросов в день.')
|
||||||
|
_bullet(doc, 'Ключ: секция «Статический API-ключ» (формат: буквы и цифры).')
|
||||||
|
_p(doc, 'ИИ (OpenAI-совместимый: DeepSeek, ChatGPT и др.) — проверка склонений и реквизитов.')
|
||||||
|
_bullet(doc, 'DeepSeek: https://platform.deepseek.com → API Keys. Модель deepseek-chat. Дешёвая, ключ пополняется деньгами.')
|
||||||
|
_bullet(doc, 'ChatGPT/OpenAI: https://platform.openai.com → API Keys. Модели gpt-4o-mini, gpt-4o.')
|
||||||
|
_bullet(doc, 'Базовый URL: для DeepSeek можно оставить пустым; для OpenAI — https://api.openai.com.')
|
||||||
|
_p(doc, 'Лимиты запросов: зависят от тарифа. DaData — ~10 000/сутки на бесплатном тарифе. '
|
||||||
|
'DeepSeek/OpenAI — оплата за токены, лимиты настраиваются в личном кабинете. '
|
||||||
|
'Рекомендуется не превышать несколько сотен проверок в день, чтобы не упереться в лимиты.')
|
||||||
|
|
||||||
|
_h(doc, '5. Морфер 3.0', 12)
|
||||||
|
_p(doc, 'Бесплатный веб-сервис склонения слов и ФИО. Ключ не требуется — '
|
||||||
|
'программа обращается к https://ws3.morpher.ru автоматически.')
|
||||||
|
|
||||||
|
_h(doc, '6. Логи', 12)
|
||||||
|
_p(doc, 'Все действия (импорт, генерация, обращения к Морфер/DaData/ИИ с ответами) '
|
||||||
|
'записываются в файл dokogen.log рядом с программой. Экспорт лога — '
|
||||||
|
'кнопка «💾 Экспорт логов» на вкладке «Лог». При проблемах присылайте '
|
||||||
|
'этот файл разработчику.')
|
||||||
|
|
||||||
|
_h(doc, '7. Сборка .exe', 12)
|
||||||
|
_p(doc, 'Программа собирается с PyInstaller: pyinstaller --onefile --windowed main.py. '
|
||||||
|
'Файлы настроек (api_settings.json, import_settings.json, dictionaries/, docs/) '
|
||||||
|
'создаются автоматически рядом с .exe при первом запуске и остаются '
|
||||||
|
'редактируемыми.')
|
||||||
|
|
||||||
|
doc.save(path)
|
||||||
|
|
||||||
|
|
||||||
|
def _ensure_survey():
|
||||||
|
"""Копирует пустой опросник в docs/, если его ещё нет."""
|
||||||
|
target = survey_path()
|
||||||
|
if os.path.exists(target):
|
||||||
|
return target
|
||||||
|
# Ищем шаблон опросника: рядом с программой или встроенный
|
||||||
|
candidates = [
|
||||||
|
os.path.join(os.path.dirname(os.path.abspath(__file__)), 'data', 'Опросный_лист_152_ФЗ.xlsx'),
|
||||||
|
os.path.join(os.path.dirname(os.path.abspath(__file__)), 'Опросный_лист_152_ФЗ.xlsx'),
|
||||||
|
]
|
||||||
|
for c in candidates:
|
||||||
|
if os.path.exists(c):
|
||||||
|
shutil.copy(c, target)
|
||||||
|
return target
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_docs():
|
||||||
|
"""Создаёт инструкции и опросник при первом обращении."""
|
||||||
|
created = []
|
||||||
|
if DOCX_AVAILABLE:
|
||||||
|
up = user_manual_path()
|
||||||
|
if not os.path.exists(up):
|
||||||
|
try:
|
||||||
|
_generate_user_manual(up)
|
||||||
|
created.append(up)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
ap = admin_manual_path()
|
||||||
|
if not os.path.exists(ap):
|
||||||
|
try:
|
||||||
|
_generate_admin_manual(ap)
|
||||||
|
created.append(ap)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
vp = variables_reference_path()
|
||||||
|
if not os.path.exists(vp):
|
||||||
|
try:
|
||||||
|
_generate_variables_reference(vp)
|
||||||
|
created.append(vp)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
sp = _ensure_survey()
|
||||||
|
if sp and os.path.exists(sp):
|
||||||
|
created.append(sp)
|
||||||
|
return created
|
||||||
+106
-5
@@ -49,6 +49,13 @@ class DokoGenApp:
|
|||||||
# None = import_settings.json рядом с программой (создаётся автоматически)
|
# None = import_settings.json рядом с программой (создаётся автоматически)
|
||||||
self.import_settings_path: Optional[str] = None
|
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_number_var = tk.StringVar(value="")
|
||||||
self.doc_date_var = tk.StringVar(value="")
|
self.doc_date_var = tk.StringVar(value="")
|
||||||
@@ -103,7 +110,10 @@ class DokoGenApp:
|
|||||||
help_menu = tk.Menu(menubar, tearoff=0)
|
help_menu = tk.Menu(menubar, tearoff=0)
|
||||||
menubar.add_cascade(label="Справка", menu=help_menu)
|
menubar.add_cascade(label="Справка", menu=help_menu)
|
||||||
help_menu.add_command(label="О программе", command=self.show_about)
|
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):
|
def _build_ui(self):
|
||||||
@@ -364,6 +374,7 @@ class DokoGenApp:
|
|||||||
if not name and not short:
|
if not name and not short:
|
||||||
messagebox.showwarning("Проверка", "Сначала введите полное или краткое название организации")
|
messagebox.showwarning("Проверка", "Сначала введите полное или краткое название организации")
|
||||||
return
|
return
|
||||||
|
self.log(f"🌐 Запрос к Морфер 3.0: «{name or short}»")
|
||||||
self._verify_log("Запрос к Морфер 3.0…")
|
self._verify_log("Запрос к Морфер 3.0…")
|
||||||
|
|
||||||
def _run():
|
def _run():
|
||||||
@@ -395,6 +406,7 @@ class DokoGenApp:
|
|||||||
if not settings.get('ai_api_key'):
|
if not settings.get('ai_api_key'):
|
||||||
messagebox.showwarning("Настройки", "Не задан API-ключ ИИ.\nМеню «Файл → Настройки»")
|
messagebox.showwarning("Настройки", "Не задан API-ключ ИИ.\nМеню «Файл → Настройки»")
|
||||||
return
|
return
|
||||||
|
self.log(f"🤖 Запрос к ИИ ({settings.get('ai_model') or 'deepseek-chat'}): склонение «{name or short}»")
|
||||||
self._verify_log("Запрос к ИИ…")
|
self._verify_log("Запрос к ИИ…")
|
||||||
|
|
||||||
def _run():
|
def _run():
|
||||||
@@ -420,6 +432,7 @@ class DokoGenApp:
|
|||||||
def _verify_declension_done(self, results, err):
|
def _verify_declension_done(self, results, err):
|
||||||
if err:
|
if err:
|
||||||
self._verify_log(f"❌ {err}")
|
self._verify_log(f"❌ {err}")
|
||||||
|
self.log(f"❌ Проверка склонений: {err}")
|
||||||
return
|
return
|
||||||
# Собираем единый результат для применения
|
# Собираем единый результат для применения
|
||||||
combined = {
|
combined = {
|
||||||
@@ -446,6 +459,13 @@ class DokoGenApp:
|
|||||||
lines.append("\nНажмите «✓ Применить», чтобы подставить склонения")
|
lines.append("\nНажмите «✓ Применить», чтобы подставить склонения")
|
||||||
self._verify_result = combined
|
self._verify_result = combined
|
||||||
self._verify_log('\n'.join(lines))
|
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):
|
def _check_company_web(self):
|
||||||
inn_entry = self._company_entries.get('inn')
|
inn_entry = self._company_entries.get('inn')
|
||||||
@@ -457,6 +477,7 @@ class DokoGenApp:
|
|||||||
if not settings.get('dadata_token'):
|
if not settings.get('dadata_token'):
|
||||||
messagebox.showwarning("Настройки", "Не задан API-ключ DaData.\nМеню «Файл → Настройки»")
|
messagebox.showwarning("Настройки", "Не задан API-ключ DaData.\nМеню «Файл → Настройки»")
|
||||||
return
|
return
|
||||||
|
self.log(f"🌐 Запрос к DaData: ИНН/ОГРН {inn}")
|
||||||
self._verify_log("Запрос к DaData…")
|
self._verify_log("Запрос к DaData…")
|
||||||
|
|
||||||
def _run():
|
def _run():
|
||||||
@@ -478,6 +499,7 @@ class DokoGenApp:
|
|||||||
'ogrn_date': c.ogrn_date,
|
'ogrn_date': c.ogrn_date,
|
||||||
'chief_position': c.chief_position, 'chief_fio': c.chief_fio,
|
'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("Запрос к ИИ…")
|
self._verify_log("Запрос к ИИ…")
|
||||||
|
|
||||||
def _run():
|
def _run():
|
||||||
@@ -490,6 +512,7 @@ class DokoGenApp:
|
|||||||
def _verify_company_done(self, result, err):
|
def _verify_company_done(self, result, err):
|
||||||
if err:
|
if err:
|
||||||
self._verify_log(f"❌ {err}")
|
self._verify_log(f"❌ {err}")
|
||||||
|
self.log(f"❌ Проверка реквизитов: {err}")
|
||||||
return
|
return
|
||||||
self._verify_result = result
|
self._verify_result = result
|
||||||
lines = [f"Источник: {result['source']} ({result['source_url']})"]
|
lines = [f"Источник: {result['source']} ({result['source_url']})"]
|
||||||
@@ -527,6 +550,13 @@ class DokoGenApp:
|
|||||||
lines.append(f" {label}: {v}")
|
lines.append(f" {label}: {v}")
|
||||||
lines.append("\nНажмите «✓ Применить», чтобы подставить данные")
|
lines.append("\nНажмите «✓ Применить», чтобы подставить данные")
|
||||||
self._verify_log('\n'.join(lines))
|
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):
|
def _verify_apply(self):
|
||||||
result = self._verify_result
|
result = self._verify_result
|
||||||
@@ -1000,6 +1030,13 @@ class DokoGenApp:
|
|||||||
def _build_log_tab(self):
|
def _build_log_tab(self):
|
||||||
tab = ttk.Frame(self.notebook)
|
tab = ttk.Frame(self.notebook)
|
||||||
self.notebook.add(tab, text="Лог")
|
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 = scrolledtext.ScrolledText(tab, height=10)
|
||||||
self.log_text.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
|
self.log_text.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
|
||||||
self.log_text.insert(tk.END, "Лог готов\n")
|
self.log_text.insert(tk.END, "Лог готов\n")
|
||||||
@@ -2057,8 +2094,32 @@ class DokoGenApp:
|
|||||||
self.log_text.configure(state='disabled')
|
self.log_text.configure(state='disabled')
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
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)
|
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):
|
def show_about(self):
|
||||||
messagebox.showinfo("О программе",
|
messagebox.showinfo("О программе",
|
||||||
"ДоКоГеНеРаТоР — 152-ФЗ\n"
|
"ДоКоГеНеРаТоР — 152-ФЗ\n"
|
||||||
@@ -2066,10 +2127,50 @@ class DokoGenApp:
|
|||||||
"Генератор документов из шаблонов")
|
"Генератор документов из шаблонов")
|
||||||
|
|
||||||
def show_variables(self):
|
def show_variables(self):
|
||||||
text = "Переменные шаблонов:\n\n"
|
"""Показ переменных шаблонов — теперь через скачивание Word-документа."""
|
||||||
for var, group, desc, _ in VARIABLE_DEFS:
|
self.download_variables_doc()
|
||||||
text += f"{var} — {desc} ({'общ.' if group == 'A' else '152-ФЗ'})\n"
|
|
||||||
messagebox.showinfo("Переменные шаблонов", text)
|
# ---------- СПРАВКА: СКАЧИВАНИЕ ИНСТРУКЦИЙ / ОПРОСНИКА ----------
|
||||||
|
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():
|
def main():
|
||||||
|
|||||||
Reference in New Issue
Block a user