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
+26 -10
View File
@@ -461,10 +461,31 @@ def _apply_highlight_color(run_el, color: str):
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:
"""Заменяет {{var}} в параграфе. Возвращает True, если были замены.
Значения «—» (незаполненная информация) подсвечиваются жёлтым маркером.
Табуляция в параграфе сохраняется (<w:tab/>).
"""
full = para.text
matches = list(VAR_PATTERN.finditer(full))
@@ -483,9 +504,7 @@ def _replace_text_in_para(para, replacements: Dict[str, str]) -> bool:
continue
# текст до переменной
if m.start() > pos:
run = para.add_run(full[pos:m.start()])
if first is not None:
_copy_run_style(first, run)
_add_run_with_tabs(para, full[pos:m.start()], first)
# значение переменной
val_str = str(val)
run = para.add_run(val_str)
@@ -497,9 +516,7 @@ def _replace_text_in_para(para, replacements: Dict[str, str]) -> bool:
pos = m.end()
# хвост после последней переменной
if pos < len(full):
run = para.add_run(full[pos:])
if first is not None:
_copy_run_style(first, run)
_add_run_with_tabs(para, full[pos:], first)
return changed
@@ -566,21 +583,20 @@ def _highlight_unmatched(doc):
return
# Пересобираем параграф: обычный текст и переменные отдельными run'ами,
# переменные — с красным маркером. Сохраняем стиль первого run.
# переменные — с красным маркером. Сохраняем стиль первого run и табы.
first = para.runs[0] if para.runs else None
para.clear()
pos = 0
for m in matches:
if m.start() > pos:
run = para.add_run(full[pos:m.start()])
if first is not None:
_copy_run_style(first, run)
_add_run_with_tabs(para, full[pos:m.start()], first)
run = para.add_run(m.group(0))
if first is not None:
_copy_run_style(first, run)
_apply_highlight(run._element)
pos = m.end()
if pos < len(full):
_add_run_with_tabs(para, full[pos:], first)
run = para.add_run(full[pos:])
if first is not None:
_copy_run_style(first, run)