Compare commits
18
Commits
a16f5740cb
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9d57dbe0e5 | ||
|
|
12f94d0927 | ||
|
|
d68eef4bbe | ||
|
|
6ccea72dff | ||
|
|
fb01a14c37 | ||
|
|
5efc3a0e99 | ||
|
|
771bc6987f | ||
|
|
faf675e861 | ||
|
|
7ebcf04f63 | ||
|
|
3134d19089 | ||
|
|
dff3f7b971 | ||
|
|
307f632363 | ||
|
|
9d156bfc0d | ||
|
|
5d7d727ef9 | ||
|
|
2c3d25e790 | ||
|
|
5c75a72b51 | ||
|
|
7828684b7d | ||
|
|
1498e56579 |
@@ -0,0 +1,43 @@
|
|||||||
|
@echo off
|
||||||
|
chcp 65001 >nul
|
||||||
|
title Build DokoGen.exe
|
||||||
|
echo ============================================
|
||||||
|
echo Building DokoGen into a single EXE file
|
||||||
|
echo ============================================
|
||||||
|
echo.
|
||||||
|
|
||||||
|
cd /d "%~dp0"
|
||||||
|
|
||||||
|
echo [1/3] Installing PyInstaller...
|
||||||
|
py -m pip install --upgrade pyinstaller
|
||||||
|
if errorlevel 1 (
|
||||||
|
echo ERROR: failed to install PyInstaller
|
||||||
|
pause
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo [2/3] Installing dependencies...
|
||||||
|
py -m pip install -r requirements.txt
|
||||||
|
if errorlevel 1 (
|
||||||
|
echo ERROR: failed to install dependencies
|
||||||
|
pause
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo [3/3] Building DokoGen.exe (1-3 minutes)...
|
||||||
|
py -m PyInstaller --clean --noconfirm dokogen.spec
|
||||||
|
if errorlevel 1 (
|
||||||
|
echo ERROR: build failed
|
||||||
|
pause
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo ============================================
|
||||||
|
echo DONE! File: dist\DokoGen.exe
|
||||||
|
echo Copy it to any Windows PC and run it.
|
||||||
|
echo Python is NOT required on target PC.
|
||||||
|
echo ============================================
|
||||||
|
pause
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
# -*- mode: python ; coding: utf-8 -*-
|
||||||
|
"""DokoGen — spec для PyInstaller.
|
||||||
|
|
||||||
|
Сборка: py -m PyInstaller --clean --noconfirm dokogen.spec
|
||||||
|
Результат: dist/DokoGen.exe (один файл, запускается на любом Windows без Python).
|
||||||
|
"""
|
||||||
|
import sys
|
||||||
|
from PyInstaller.utils.hooks import collect_data_files, collect_submodules
|
||||||
|
|
||||||
|
# ---------- Данные приложения ----------
|
||||||
|
datas = [
|
||||||
|
# Справочники (defense_tools.json, pd_actions.json, pd_items.json, pd_categories.json)
|
||||||
|
('dokogen/dictionaries', 'dokogen/dictionaries'),
|
||||||
|
# Пустой опросник 152-ФЗ (для «Справка → Пустой опросник»)
|
||||||
|
('dokogen/data', 'dokogen/data'),
|
||||||
|
# Иконка приложения (для окна программы в exe)
|
||||||
|
('dokogen/icon.ico', 'dokogen'),
|
||||||
|
]
|
||||||
|
|
||||||
|
# ---------- Словари pymorphy3 (обязательно!) ----------
|
||||||
|
# Без них склонение организаций/ФИО в exe не будет работать.
|
||||||
|
datas += collect_data_files('pymorphy3_dicts_ru')
|
||||||
|
datas += collect_data_files('pymorphy3')
|
||||||
|
|
||||||
|
# ---------- Скрытые импорты ----------
|
||||||
|
hiddenimports = collect_submodules('pymorphy3') + collect_submodules('pymorphy3_dicts_ru')
|
||||||
|
hiddenimports += [
|
||||||
|
'lxml', 'lxml.etree', 'lxml._elementpath',
|
||||||
|
'openpyxl', 'openpyxl.cell._writer',
|
||||||
|
]
|
||||||
|
|
||||||
|
a = Analysis(
|
||||||
|
['main.py'],
|
||||||
|
pathex=[],
|
||||||
|
binaries=[],
|
||||||
|
datas=datas,
|
||||||
|
hiddenimports=hiddenimports,
|
||||||
|
hookspath=[],
|
||||||
|
hooksconfig={},
|
||||||
|
runtime_hooks=[],
|
||||||
|
excludes=[],
|
||||||
|
noarchive=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
pyz = PYZ(a.pure)
|
||||||
|
|
||||||
|
exe = EXE(
|
||||||
|
pyz,
|
||||||
|
a.scripts,
|
||||||
|
a.binaries,
|
||||||
|
a.datas,
|
||||||
|
[],
|
||||||
|
name='DokoGen',
|
||||||
|
debug=False,
|
||||||
|
bootloader_ignore_signals=False,
|
||||||
|
strip=False,
|
||||||
|
upx=True,
|
||||||
|
upx_exclude=[],
|
||||||
|
runtime_tmpdir=None,
|
||||||
|
console=False, # GUI-приложение, без консоли
|
||||||
|
disable_windowed_traceback=False,
|
||||||
|
icon='dokogen/icon.ico',
|
||||||
|
)
|
||||||
+15
-4
@@ -196,15 +196,26 @@ def _is_quoted(word: str) -> bool:
|
|||||||
|
|
||||||
# ==================== ФОРМАТИРОВАНИЕ ФИО ====================
|
# ==================== ФОРМАТИРОВАНИЕ ФИО ====================
|
||||||
|
|
||||||
|
def _initial(word: str) -> str:
|
||||||
|
"""Инициал из слова: если уже с точкой — вернуть как есть,
|
||||||
|
иначе первая буква + точка. «В.Н.» → «В.Н.», «Владимир» → «В.»"""
|
||||||
|
w = word.strip()
|
||||||
|
if not w:
|
||||||
|
return ''
|
||||||
|
if w.endswith('.'):
|
||||||
|
return w
|
||||||
|
return w[0] + '.'
|
||||||
|
|
||||||
|
|
||||||
def get_short_fio(fio: str) -> str:
|
def get_short_fio(fio: str) -> str:
|
||||||
"""Фамилия И.О."""
|
"""Фамилия И.О."""
|
||||||
if not fio:
|
if not fio:
|
||||||
return ""
|
return ""
|
||||||
parts = fio.split()
|
parts = fio.split()
|
||||||
if len(parts) >= 2:
|
if len(parts) >= 2:
|
||||||
initials = parts[1][0] + "."
|
initials = _initial(parts[1])
|
||||||
if len(parts) >= 3:
|
if len(parts) >= 3:
|
||||||
initials += parts[2][0] + "."
|
initials += _initial(parts[2])
|
||||||
return parts[0] + " " + initials
|
return parts[0] + " " + initials
|
||||||
return fio
|
return fio
|
||||||
|
|
||||||
@@ -215,8 +226,8 @@ def get_initials(fio: str) -> str:
|
|||||||
return ""
|
return ""
|
||||||
parts = fio.split()
|
parts = fio.split()
|
||||||
if len(parts) >= 2:
|
if len(parts) >= 2:
|
||||||
initials = parts[1][0] + "."
|
initials = _initial(parts[1])
|
||||||
if len(parts) >= 3:
|
if len(parts) >= 3:
|
||||||
initials += parts[2][0] + "."
|
initials += _initial(parts[2])
|
||||||
return initials + " " + parts[0]
|
return initials + " " + parts[0]
|
||||||
return fio
|
return fio
|
||||||
|
|||||||
+24
-7
@@ -293,6 +293,23 @@ def _cleanup_marker_paras(doc_xml: str) -> str:
|
|||||||
return etree.tostring(root, xml_declaration=True, encoding='UTF-8', standalone=True).decode('utf-8')
|
return etree.tostring(root, xml_declaration=True, encoding='UTF-8', standalone=True).decode('utf-8')
|
||||||
|
|
||||||
|
|
||||||
|
def _subst_var_xml(xml: str, var: str, val: str) -> str:
|
||||||
|
"""Замена {{var}} в XML-фрагменте.
|
||||||
|
|
||||||
|
Пустые значения («—»/«») подсвечиваются жёлтым маркером: значение
|
||||||
|
выносится в отдельный run с <w:highlight w:val="yellow"/>, чтобы
|
||||||
|
подсветка не затиралась при подстановке на уровне XML (циклы).
|
||||||
|
"""
|
||||||
|
token = '{{%s}}' % var
|
||||||
|
if val not in ('—', ''):
|
||||||
|
return xml.replace(token, val)
|
||||||
|
hl_run = ('<w:rPr><w:highlight w:val="yellow"/></w:rPr>'
|
||||||
|
'<w:t xml:space="preserve">%s</w:t>' % val)
|
||||||
|
return xml.replace(token,
|
||||||
|
'</w:t></w:r><w:r>' + hl_run
|
||||||
|
+ '</w:r><w:r><w:t xml:space="preserve">')
|
||||||
|
|
||||||
|
|
||||||
def _expand_nested_loops(template: str, item: Dict) -> str:
|
def _expand_nested_loops(template: str, item: Dict) -> str:
|
||||||
"""Разворачивает вложенные циклы вида <!-- loop:users_loop --> ... <!-- loop_end -->
|
"""Разворачивает вложенные циклы вида <!-- loop:users_loop --> ... <!-- loop_end -->
|
||||||
данными из item (список словарей). Используется для циклов внутри цикла ИС,
|
данными из item (список словарей). Используется для циклов внутри цикла ИС,
|
||||||
@@ -342,8 +359,8 @@ def _expand_nested_loops(template: str, item: Dict) -> str:
|
|||||||
clone = inner
|
clone = inner
|
||||||
for f, v in sub.items():
|
for f, v in sub.items():
|
||||||
val = str(v) if v else ''
|
val = str(v) if v else ''
|
||||||
clone = clone.replace('{{user_%s}}' % f, val)
|
clone = _subst_var_xml(clone, 'user_%s' % f, val)
|
||||||
clone = clone.replace('{{item.%s}}' % f, val)
|
clone = _subst_var_xml(clone, 'item.%s' % f, val)
|
||||||
parts.append(clone)
|
parts.append(clone)
|
||||||
result = result[:m_start.start()] + ''.join(parts) + result[m_end.end():]
|
result = result[:m_start.start()] + ''.join(parts) + result[m_end.end():]
|
||||||
return result
|
return result
|
||||||
@@ -467,13 +484,13 @@ def expand_loops_in_zip(docx_path: str, loops: Dict[str, List[Dict]]) -> str:
|
|||||||
continue # списки — данные вложенных циклов, не подставляем
|
continue # списки — данные вложенных циклов, не подставляем
|
||||||
val_str = str(value) if value else ('—' if value is None else '')
|
val_str = str(value) if value else ('—' if value is None else '')
|
||||||
# С префиксом item.
|
# С префиксом item.
|
||||||
clone = clone.replace('{{item.%s}}' % field, val_str)
|
clone = _subst_var_xml(clone, 'item.%s' % field, val_str)
|
||||||
# Без префикса (алиасы {{name}}, {{pd_list}}, {{document}} и т.д.)
|
# Без префикса (алиасы {{name}}, {{pd_list}}, {{document}} и т.д.)
|
||||||
clone = clone.replace('{{%s}}' % field, val_str)
|
clone = _subst_var_xml(clone, '%s' % field, val_str)
|
||||||
# Альтернативный синтаксис {{item_поле}} (без точки)
|
# Альтернативный синтаксис {{item_поле}} (без точки)
|
||||||
clone = clone.replace('{{item_%s}}' % field, val_str)
|
clone = _subst_var_xml(clone, 'item_%s' % field, val_str)
|
||||||
clone = clone.replace('{{item.number}}', str(idx))
|
clone = _subst_var_xml(clone, 'item.number', str(idx))
|
||||||
clone = clone.replace('{{number}}', str(idx))
|
clone = _subst_var_xml(clone, 'number', str(idx))
|
||||||
expanded_parts.append(clone)
|
expanded_parts.append(clone)
|
||||||
if br_in_suffix:
|
if br_in_suffix:
|
||||||
expanded_parts.append('<w:p><w:r><w:br w:type="page"/></w:r></w:p>')
|
expanded_parts.append('<w:p><w:r><w:br w:type="page"/></w:r></w:p>')
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 63 KiB |
+55
-5
@@ -34,6 +34,7 @@ class DokoGenApp:
|
|||||||
root.title("ДоКоГеНеРаТоР — 152-ФЗ (Персональные данные)")
|
root.title("ДоКоГеНеРаТоР — 152-ФЗ (Персональные данные)")
|
||||||
root.geometry("1200x800")
|
root.geometry("1200x800")
|
||||||
root.minsize(900, 600)
|
root.minsize(900, 600)
|
||||||
|
self._set_app_icon()
|
||||||
|
|
||||||
self.model = Company()
|
self.model = Company()
|
||||||
self.current_file: Optional[str] = None
|
self.current_file: Optional[str] = None
|
||||||
@@ -81,6 +82,26 @@ class DokoGenApp:
|
|||||||
self._load_autosave()
|
self._load_autosave()
|
||||||
self._start_autosave_timer()
|
self._start_autosave_timer()
|
||||||
|
|
||||||
|
def _set_app_icon(self):
|
||||||
|
"""Устанавливает иконку приложения на окно (заголовок, панель задач).
|
||||||
|
|
||||||
|
Работает и из исходников, и из exe (frozen):
|
||||||
|
- исходники: dokogen/icon.ico рядом с модулем
|
||||||
|
- exe: dokogen/icon.ico из _MEIPASS (ресурсы PyInstaller)
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
if getattr(sys, 'frozen', False):
|
||||||
|
base = getattr(sys, '_MEIPASS', os.path.dirname(sys.executable))
|
||||||
|
ico = os.path.join(base, 'dokogen', 'icon.ico')
|
||||||
|
if not os.path.exists(ico):
|
||||||
|
ico = os.path.join(os.path.dirname(sys.executable), 'icon.ico')
|
||||||
|
else:
|
||||||
|
ico = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'icon.ico')
|
||||||
|
if os.path.exists(ico):
|
||||||
|
self.root.iconbitmap(ico)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
# ==================== МЕНЮ ====================
|
# ==================== МЕНЮ ====================
|
||||||
def _build_menu(self):
|
def _build_menu(self):
|
||||||
menubar = tk.Menu(self.root)
|
menubar = tk.Menu(self.root)
|
||||||
@@ -303,6 +324,23 @@ class DokoGenApp:
|
|||||||
btn_row.pack(fill=tk.X)
|
btn_row.pack(fill=tk.X)
|
||||||
ttk.Button(btn_row, text="✓ Применить", command=self._verify_apply).pack(side=tk.LEFT, padx=2)
|
ttk.Button(btn_row, text="✓ Применить", command=self._verify_apply).pack(side=tk.LEFT, padx=2)
|
||||||
ttk.Button(btn_row, text="Очистить", command=self._verify_clear).pack(side=tk.LEFT, padx=2)
|
ttk.Button(btn_row, text="Очистить", command=self._verify_clear).pack(side=tk.LEFT, padx=2)
|
||||||
|
|
||||||
|
# Выборочное применение: чекбоксы «что подставлять»
|
||||||
|
self._apply_flags = {
|
||||||
|
'declension': tk.BooleanVar(value=True),
|
||||||
|
'short_declension': tk.BooleanVar(value=True),
|
||||||
|
'address': tk.BooleanVar(value=True),
|
||||||
|
'requisites': tk.BooleanVar(value=True),
|
||||||
|
'chief': tk.BooleanVar(value=True),
|
||||||
|
}
|
||||||
|
apply_row = ttk.Frame(right)
|
||||||
|
apply_row.pack(fill=tk.X, pady=(4, 0))
|
||||||
|
ttk.Label(apply_row, text="Подставлять:", font=('', 8)).pack(side=tk.LEFT, padx=(0, 4))
|
||||||
|
ttk.Checkbutton(apply_row, text="склонения", variable=self._apply_flags['declension']).pack(side=tk.LEFT)
|
||||||
|
ttk.Checkbutton(apply_row, text="кр. склон.", variable=self._apply_flags['short_declension']).pack(side=tk.LEFT)
|
||||||
|
ttk.Checkbutton(apply_row, text="адрес", variable=self._apply_flags['address']).pack(side=tk.LEFT)
|
||||||
|
ttk.Checkbutton(apply_row, text="ИНН/КПП/ОГРН", variable=self._apply_flags['requisites']).pack(side=tk.LEFT)
|
||||||
|
ttk.Checkbutton(apply_row, text="руковод.", variable=self._apply_flags['chief']).pack(side=tk.LEFT)
|
||||||
self._verify_result = None
|
self._verify_result = None
|
||||||
|
|
||||||
# ---------- ПРОВЕРКА ДАННЫХ (Морфер / DaData / ИИ) ----------
|
# ---------- ПРОВЕРКА ДАННЫХ (Морфер / DaData / ИИ) ----------
|
||||||
@@ -594,8 +632,15 @@ class DokoGenApp:
|
|||||||
if not result:
|
if not result:
|
||||||
return
|
return
|
||||||
apply_data = result.get('apply', {})
|
apply_data = result.get('apply', {})
|
||||||
|
flags = getattr(self, '_apply_flags', None)
|
||||||
|
|
||||||
|
def want(key):
|
||||||
|
if flags is None:
|
||||||
|
return True
|
||||||
|
return flags[key].get() if key in flags else True
|
||||||
|
|
||||||
idx_map = {'nomn': 1, 'gent': 2, 'datv': 3, 'accs': 4, 'ablt': 5, 'loct': 6}
|
idx_map = {'nomn': 1, 'gent': 2, 'datv': 3, 'accs': 4, 'ablt': 5, 'loct': 6}
|
||||||
if 'declension' in apply_data:
|
if 'declension' in apply_data and want('declension'):
|
||||||
for case, val in apply_data['declension'].items():
|
for case, val in apply_data['declension'].items():
|
||||||
code = self._CASE_TO_CODE.get(case)
|
code = self._CASE_TO_CODE.get(case)
|
||||||
if code and code in self.decl_entries:
|
if code and code in self.decl_entries:
|
||||||
@@ -603,7 +648,7 @@ class DokoGenApp:
|
|||||||
self.decl_entries[code].insert(0, val)
|
self.decl_entries[code].insert(0, val)
|
||||||
setattr(self.model, f'company_name_{idx_map[code]}', val)
|
setattr(self.model, f'company_name_{idx_map[code]}', val)
|
||||||
self.log("✅ Склонения полного названия применены")
|
self.log("✅ Склонения полного названия применены")
|
||||||
if 'short_declension' in apply_data:
|
if 'short_declension' in apply_data and want('short_declension'):
|
||||||
for case, val in apply_data['short_declension'].items():
|
for case, val in apply_data['short_declension'].items():
|
||||||
code = self._CASE_TO_CODE.get(case)
|
code = self._CASE_TO_CODE.get(case)
|
||||||
if code and code in self.short_decl_entries:
|
if code and code in self.short_decl_entries:
|
||||||
@@ -619,12 +664,17 @@ class DokoGenApp:
|
|||||||
if comp.get('short_name'):
|
if comp.get('short_name'):
|
||||||
self.short_name_entry.delete(0, tk.END)
|
self.short_name_entry.delete(0, tk.END)
|
||||||
self.short_name_entry.insert(0, comp['short_name'])
|
self.short_name_entry.insert(0, comp['short_name'])
|
||||||
mapping = {'address': 'address', 'kpp': 'kpp', 'ogrn': 'ogrn',
|
if want('address') and comp.get('address'):
|
||||||
'ogrn_date': 'ogrn_date', 'inn': 'inn'}
|
if 'address' in self._company_entries:
|
||||||
for data_key, ui_key in mapping.items():
|
self._company_entries['address'].delete(0, tk.END)
|
||||||
|
self._company_entries['address'].insert(0, comp['address'])
|
||||||
|
if want('requisites'):
|
||||||
|
for data_key, ui_key in {'kpp': 'kpp', 'ogrn': 'ogrn',
|
||||||
|
'ogrn_date': 'ogrn_date', 'inn': 'inn'}.items():
|
||||||
if comp.get(data_key) and ui_key in self._company_entries:
|
if comp.get(data_key) and ui_key in self._company_entries:
|
||||||
self._company_entries[ui_key].delete(0, tk.END)
|
self._company_entries[ui_key].delete(0, tk.END)
|
||||||
self._company_entries[ui_key].insert(0, comp[data_key])
|
self._company_entries[ui_key].insert(0, comp[data_key])
|
||||||
|
if want('chief'):
|
||||||
if comp.get('chief_position') and 'chief_position' in self._official_entries:
|
if comp.get('chief_position') and 'chief_position' in self._official_entries:
|
||||||
self._official_entries['chief_position'].delete(0, tk.END)
|
self._official_entries['chief_position'].delete(0, tk.END)
|
||||||
self._official_entries['chief_position'].insert(0, comp['chief_position'])
|
self._official_entries['chief_position'].insert(0, comp['chief_position'])
|
||||||
|
|||||||
Reference in New Issue
Block a user