fix: жёлтая подсветка пустых значений в циклах + выборочный Применить

- Циклы (item.*, user_*, алиасы): пустые значения '—'/'' теперь
  подсвечиваются жёлтым маркером (раньше подстановка шла на уровне
  XML простым replace — подсветка терялась)
- Вложенные циклы (users_loop): та же подсветка
- Панель «Проверка данных»: чекбоксы «Подставлять: склонения /
  кр. склон. / адрес / ИНН-КПП-ОГРН / руковод.» — выборочный Применить
- get_short_fio/get_initials: не теряют отчество в сокращённом ФИО
This commit is contained in:
2026-08-03 19:53:08 +04:00
parent 1498e56579
commit 7828684b7d
2 changed files with 72 additions and 21 deletions
+24 -7
View File
@@ -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>')
+48 -14
View File
@@ -303,6 +303,28 @@ 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'],
font=('', 8)).pack(side=tk.LEFT)
ttk.Checkbutton(apply_row, text="кр. склон.", variable=self._apply_flags['short_declension'],
font=('', 8)).pack(side=tk.LEFT)
ttk.Checkbutton(apply_row, text="адрес", variable=self._apply_flags['address'],
font=('', 8)).pack(side=tk.LEFT)
ttk.Checkbutton(apply_row, text="ИНН/КПП/ОГРН", variable=self._apply_flags['requisites'],
font=('', 8)).pack(side=tk.LEFT)
ttk.Checkbutton(apply_row, text="руковод.", variable=self._apply_flags['chief'],
font=('', 8)).pack(side=tk.LEFT)
self._verify_result = None self._verify_result = None
# ---------- ПРОВЕРКА ДАННЫХ (Морфер / DaData / ИИ) ---------- # ---------- ПРОВЕРКА ДАННЫХ (Морфер / DaData / ИИ) ----------
@@ -594,8 +616,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 +632,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,18 +648,23 @@ 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)
if comp.get(data_key) and ui_key in self._company_entries: self._company_entries['address'].insert(0, comp['address'])
self._company_entries[ui_key].delete(0, tk.END) if want('requisites'):
self._company_entries[ui_key].insert(0, comp[data_key]) for data_key, ui_key in {'kpp': 'kpp', 'ogrn': 'ogrn',
if comp.get('chief_position') and 'chief_position' in self._official_entries: 'ogrn_date': 'ogrn_date', 'inn': 'inn'}.items():
self._official_entries['chief_position'].delete(0, tk.END) if comp.get(data_key) and ui_key in self._company_entries:
self._official_entries['chief_position'].insert(0, comp['chief_position']) self._company_entries[ui_key].delete(0, tk.END)
if comp.get('chief_fio') and 'chief_fio' in self._official_entries: self._company_entries[ui_key].insert(0, comp[data_key])
self._official_entries['chief_fio'].delete(0, tk.END) if want('chief'):
self._official_entries['chief_fio'].insert(0, comp['chief_fio']) 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'].insert(0, comp['chief_position'])
if comp.get('chief_fio') and 'chief_fio' in self._official_entries:
self._official_entries['chief_fio'].delete(0, tk.END)
self._official_entries['chief_fio'].insert(0, comp['chief_fio'])
self.log("✅ Реквизиты применены") self.log("✅ Реквизиты применены")
# ---------- ВКЛАДКА "ИНФОРМАЦИОННЫЕ СИСТЕМЫ" ---------- # ---------- ВКЛАДКА "ИНФОРМАЦИОННЫЕ СИСТЕМЫ" ----------