fix: 6 замечаний по генератору
1) нормализация адреса: «Р-Н Крымский» → «р-н. Крымский», «Г Крымск» → «г. Крымск», «УЛ» → «ул.», «Д.» → «д.» 2) убрана строчка «ПО» на вкладке ИС 3) СЗИ без категорий: «Kaspersky, КриптоПро» (через запятую) 4) кнопка «Выбрать из списка» в подвкладке «Пользователи ИС» 5) бумажные документы: «хранятся в сейфе» (авто-предлог+падеж) 6) незаполненные данные «—» подсвечиваются жёлтым (незаменённые переменные — красным, как было)
This commit is contained in:
+46
-20
@@ -449,32 +449,58 @@ def expand_loops_in_zip(docx_path: str, loops: Dict[str, List[Dict]]) -> str:
|
|||||||
# 3. ЗАМЕНА ПЕРЕМЕННЫХ В ЭЛЕМЕНТАХ DOCX
|
# 3. ЗАМЕНА ПЕРЕМЕННЫХ В ЭЛЕМЕНТАХ DOCX
|
||||||
# ============================================================
|
# ============================================================
|
||||||
|
|
||||||
|
def _apply_highlight_color(run_el, color: str):
|
||||||
|
"""Добавляет маркер highlight указанного цвета к run."""
|
||||||
|
ns = 'http://schemas.openxmlformats.org/wordprocessingml/2006/main'
|
||||||
|
rpr = run_el.find(f'{{{ns}}}rPr')
|
||||||
|
if rpr is None:
|
||||||
|
rpr = OxmlElement('w:rPr')
|
||||||
|
run_el.insert(0, rpr)
|
||||||
|
hl = OxmlElement('w:highlight')
|
||||||
|
hl.set(f'{{{ns}}}val', color)
|
||||||
|
rpr.append(hl)
|
||||||
|
|
||||||
|
|
||||||
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, если были замены.
|
||||||
|
|
||||||
|
Значения «—» (незаполненная информация) подсвечиваются жёлтым маркером.
|
||||||
|
"""
|
||||||
full = para.text
|
full = para.text
|
||||||
matches = VAR_PATTERN.findall(full)
|
matches = list(VAR_PATTERN.finditer(full))
|
||||||
if not matches:
|
if not matches:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
new_text = full
|
# Собираем параграф заново: обычный текст + значения переменных
|
||||||
for var in matches:
|
first = para.runs[0] if para.runs else None
|
||||||
|
para.clear()
|
||||||
|
pos = 0
|
||||||
|
changed = False
|
||||||
|
for m in matches:
|
||||||
|
var = m.group(0)
|
||||||
val = replacements.get(var)
|
val = replacements.get(var)
|
||||||
if val is not None:
|
if val is None:
|
||||||
new_text = new_text.replace(var, str(val))
|
continue
|
||||||
|
# текст до переменной
|
||||||
if new_text == full:
|
if m.start() > pos:
|
||||||
return False
|
run = para.add_run(full[pos:m.start()])
|
||||||
|
if first is not None:
|
||||||
# Сохраняем форматирование первого run
|
_copy_run_style(first, run)
|
||||||
if para.runs:
|
# значение переменной
|
||||||
first = para.runs[0]
|
val_str = str(val)
|
||||||
para.clear()
|
run = para.add_run(val_str)
|
||||||
run = para.add_run(new_text)
|
if first is not None:
|
||||||
_copy_run_style(first, run)
|
_copy_run_style(first, run)
|
||||||
else:
|
if val_str == '—' or val_str == '':
|
||||||
para.clear()
|
_apply_highlight_color(run._element, 'yellow')
|
||||||
para.add_run(new_text)
|
changed = True
|
||||||
return True
|
pos = m.end()
|
||||||
|
# хвост после последней переменной
|
||||||
|
if pos < len(full):
|
||||||
|
run = para.add_run(full[pos:])
|
||||||
|
if first is not None:
|
||||||
|
_copy_run_style(first, run)
|
||||||
|
return changed
|
||||||
|
|
||||||
|
|
||||||
def _copy_run_style(source, target):
|
def _copy_run_style(source, target):
|
||||||
|
|||||||
+32
-1
@@ -71,6 +71,31 @@ def _normalize_caps(text: str) -> str:
|
|||||||
return ' '.join(result)
|
return ' '.join(result)
|
||||||
|
|
||||||
|
|
||||||
|
_ADDRESS_ABBR = {
|
||||||
|
# нормальные сокращения для адресов: «Г» → «г.», «Р-Н» → «р-н.» и т.п.
|
||||||
|
'Г': 'г.', 'УЛ': 'ул.', 'Д': 'д.', 'КВ': 'кв.', 'ПР': 'пр.',
|
||||||
|
'ПЕР': 'пер.', 'ПЛ': 'пл.', 'ОБЛ': 'обл.', 'ПОС': 'пос.',
|
||||||
|
'СТ': 'ст.', 'Ш': 'ш.', 'Р-Н': 'р-н.', 'Б-Р': 'б-р.', 'МКР': 'мкр.',
|
||||||
|
'НАБ': 'наб.', 'ТУП': 'туп.', 'ПР-Д': 'пр-д.', 'КРП': 'крп.',
|
||||||
|
'Г.': 'г.', 'УЛ.': 'ул.', 'Д.': 'д.', 'Р-Н.': 'р-н.',
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_address(text: str) -> str:
|
||||||
|
"""Приводит адрес к нормальному виду: «Г Крымск» → «г. Крымск»,
|
||||||
|
«Р-Н Крымский» → «р-н. Крымский», «УЛ Карла» → «ул. Карла»."""
|
||||||
|
if not text:
|
||||||
|
return ''
|
||||||
|
parts = re.split(r'(\s+|[,\;])', text)
|
||||||
|
out = []
|
||||||
|
for p in parts:
|
||||||
|
if p.strip() and p.strip().upper() in _ADDRESS_ABBR:
|
||||||
|
out.append(_ADDRESS_ABBR[p.strip().upper()])
|
||||||
|
else:
|
||||||
|
out.append(p)
|
||||||
|
return ''.join(out)
|
||||||
|
|
||||||
|
|
||||||
def clean_value(val):
|
def clean_value(val):
|
||||||
"""Очистка и нормализация значения ячейки Excel."""
|
"""Очистка и нормализация значения ячейки Excel."""
|
||||||
if val is None:
|
if val is None:
|
||||||
@@ -145,6 +170,11 @@ def import_152fz(filepath: str, log_fn: Optional[Callable] = None, settings_path
|
|||||||
if data.get(src) and not data.get(dst):
|
if data.get(src) and not data.get(dst):
|
||||||
data[dst] = data[src]
|
data[dst] = data[src]
|
||||||
|
|
||||||
|
# Нормализация адресов: «Г Крымск» → «г. Крымск», «Р-Н» → «р-н.» и т.п.
|
||||||
|
for key in ('addressLegal', 'addressActual'):
|
||||||
|
if data.get(key):
|
||||||
|
data[key] = normalize_address(data[key])
|
||||||
|
|
||||||
_validate_company_data(data, log_fn)
|
_validate_company_data(data, log_fn)
|
||||||
return data
|
return data
|
||||||
|
|
||||||
@@ -366,7 +396,8 @@ def _import_152_is_list(wb, data, settings, log_fn):
|
|||||||
matched_label = label
|
matched_label = label
|
||||||
break
|
break
|
||||||
if matched_label and val_b and 'выбрать' not in field_lower:
|
if matched_label and val_b and 'выбрать' not in field_lower:
|
||||||
is_obj['defense_tools'].append(f"{matched_label}: {val_b}")
|
# Без префикса категории: просто название средства (Kaspersky, КриптоПро)
|
||||||
|
is_obj['defense_tools'].append(val_b)
|
||||||
elif matched_label:
|
elif matched_label:
|
||||||
pass
|
pass
|
||||||
elif 'куда' in field_lower or 'передаются' in field_lower:
|
elif 'куда' in field_lower or 'передаются' in field_lower:
|
||||||
|
|||||||
+23
-7
@@ -595,13 +595,6 @@ class DokoGenApp:
|
|||||||
self.is_structure_entry = ttk.Entry(fields_frame, width=55)
|
self.is_structure_entry = ttk.Entry(fields_frame, width=55)
|
||||||
self.is_structure_entry.grid(row=7, column=1, padx=10, sticky=tk.EW)
|
self.is_structure_entry.grid(row=7, column=1, padx=10, sticky=tk.EW)
|
||||||
|
|
||||||
# Программное обеспечение
|
|
||||||
ttk.Label(fields_frame, text="ПО:").grid(row=8, column=0, sticky=tk.W, pady=2)
|
|
||||||
self.is_software_entry = ttk.Entry(fields_frame, width=55)
|
|
||||||
self.is_software_entry.grid(row=8, column=1, padx=10, sticky=tk.EW)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
fields_frame.columnconfigure(1, weight=1)
|
fields_frame.columnconfigure(1, weight=1)
|
||||||
|
|
||||||
# Вложенные списки
|
# Вложенные списки
|
||||||
@@ -659,6 +652,7 @@ class DokoGenApp:
|
|||||||
self.users_listbox.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
|
self.users_listbox.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
|
||||||
btnf = ttk.Frame(user_tab)
|
btnf = ttk.Frame(user_tab)
|
||||||
btnf.pack(pady=3)
|
btnf.pack(pady=3)
|
||||||
|
ttk.Button(btnf, text="👥 Выбрать из списка", command=self._select_users_from_employees).pack(side=tk.LEFT, padx=2)
|
||||||
ttk.Button(btnf, text="+ Добавить", command=self._add_user_to_is).pack(side=tk.LEFT, padx=2)
|
ttk.Button(btnf, text="+ Добавить", command=self._add_user_to_is).pack(side=tk.LEFT, padx=2)
|
||||||
ttk.Button(btnf, text="- Удалить", command=self._del_user_from_is).pack(side=tk.LEFT, padx=2)
|
ttk.Button(btnf, text="- Удалить", command=self._del_user_from_is).pack(side=tk.LEFT, padx=2)
|
||||||
|
|
||||||
@@ -1339,6 +1333,28 @@ class DokoGenApp:
|
|||||||
if user:
|
if user:
|
||||||
self.users_listbox.insert(tk.END, user)
|
self.users_listbox.insert(tk.END, user)
|
||||||
|
|
||||||
|
def _select_users_from_employees(self):
|
||||||
|
"""Выбор пользователей ИС из списка сотрудников организации (вкладка «Пользователи»)."""
|
||||||
|
names = []
|
||||||
|
for emp in self.model.employees_access:
|
||||||
|
fio = (emp.get('fio') or '').strip()
|
||||||
|
if fio and fio not in names:
|
||||||
|
names.append(fio)
|
||||||
|
# Дополнительно: пользователи из других ИС
|
||||||
|
for isys in self.model.information_systems:
|
||||||
|
for u in (isys.users_list or []):
|
||||||
|
u = u.strip()
|
||||||
|
if u and u not in names:
|
||||||
|
names.append(u)
|
||||||
|
if not names:
|
||||||
|
messagebox.showinfo(
|
||||||
|
"Выбор пользователей",
|
||||||
|
"Список пуст.\nЗаполните вкладку «Пользователи» (сотрудники с доступом к ПДн) "
|
||||||
|
"или добавьте пользователей в другой ИС."
|
||||||
|
)
|
||||||
|
return
|
||||||
|
self._select_from_dialog("Выбор пользователей ИС", names, self.users_listbox)
|
||||||
|
|
||||||
def _del_user_from_is(self):
|
def _del_user_from_is(self):
|
||||||
sel = self.users_listbox.curselection()
|
sel = self.users_listbox.curselection()
|
||||||
if sel:
|
if sel:
|
||||||
|
|||||||
+36
-2
@@ -277,7 +277,7 @@ def _build_loop_data(company: Company, is_list: List) -> Dict[str, List[Dict]]:
|
|||||||
'pd_list': ', '.join(isys.personal_data_list or []) or '—',
|
'pd_list': ', '.join(isys.personal_data_list or []) or '—',
|
||||||
'pd_count': str(isys.pd_count or '—'),
|
'pd_count': str(isys.pd_count or '—'),
|
||||||
'users': ', '.join(isys.users_list or []) or '—',
|
'users': ', '.join(isys.users_list or []) or '—',
|
||||||
'defense_tools_list': '; '.join(isys.defense_tools_list or []) or '—',
|
'defense_tools_list': ', '.join(isys.defense_tools_list or []) or '—',
|
||||||
'processing_modes': isys.processing_modes or '—',
|
'processing_modes': isys.processing_modes or '—',
|
||||||
'pd_subjects': '; '.join(isys.pd_subjects_list or []) or '—',
|
'pd_subjects': '; '.join(isys.pd_subjects_list or []) or '—',
|
||||||
'is_purpose': isys.purpose or '—',
|
'is_purpose': isys.purpose or '—',
|
||||||
@@ -288,13 +288,47 @@ def _build_loop_data(company: Company, is_list: List) -> Dict[str, List[Dict]]:
|
|||||||
loops['information_systems'] = items
|
loops['information_systems'] = items
|
||||||
|
|
||||||
# Бумажные документы
|
# Бумажные документы
|
||||||
|
def _storage_with_prep(storage: str) -> str:
|
||||||
|
"""«сейф» → «в сейфе» (предлог + предложный падеж).
|
||||||
|
Если уже начинается с предлога — оставить как есть."""
|
||||||
|
if not storage:
|
||||||
|
return ''
|
||||||
|
s = storage.strip()
|
||||||
|
low = s.lower()
|
||||||
|
if low.startswith(('в ', 'на ', 'под ', 'за ', 'у ', 'при ', 'из ', 'со ', 'во ', 'над ', 'перед ')):
|
||||||
|
return s
|
||||||
|
words = s.split()
|
||||||
|
out = []
|
||||||
|
# Исключения предложного падежа: «в шкафу», «в углу» (не «в шкафе»)
|
||||||
|
loct_exceptions = {'шкаф': 'шкафу', 'угол': 'углу', 'край': 'краю', 'рот': 'рту', 'мост': 'мосту'}
|
||||||
|
for w in words:
|
||||||
|
core = w.strip('.,;')
|
||||||
|
if not core or core.isdigit() or core.startswith('№'):
|
||||||
|
out.append(w)
|
||||||
|
continue
|
||||||
|
if core.lower() in loct_exceptions:
|
||||||
|
out.append(loct_exceptions[core.lower()])
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
p = dc.morph.parse(core)[0]
|
||||||
|
inf = p.inflect({'loct'})
|
||||||
|
if inf:
|
||||||
|
out.append(inf.word)
|
||||||
|
continue
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
out.append(w)
|
||||||
|
return 'в ' + ' '.join(out)
|
||||||
|
|
||||||
paper_list = getattr(company, 'paper_documents_list', None) or []
|
paper_list = getattr(company, 'paper_documents_list', None) or []
|
||||||
if paper_list:
|
if paper_list:
|
||||||
items = []
|
items = []
|
||||||
for p in paper_list:
|
for p in paper_list:
|
||||||
|
storage = p.storage or ''
|
||||||
items.append({
|
items.append({
|
||||||
'document': p.name or '',
|
'document': p.name or '',
|
||||||
'storage': p.storage or '',
|
'storage': _storage_with_prep(storage), # «в сейфе»
|
||||||
|
'storage_raw': storage, # «сейф» (как введено)
|
||||||
})
|
})
|
||||||
loops['paper_documents'] = items
|
loops['paper_documents'] = items
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user