feat: настройки импорта вынесены в текстовый файл-словарь
- import_settings.json рядом с программой (создаётся автоматически): что импортировать (var), откуда (cell: 'Общие сведения'!B2 или keywords: регэксп поиска по колонке A) - importer.py читает словарь: сначала конкретная ячейка, потом ключевые слова - листы общих сведений/сотрудников/ИС тоже настраиваются (ключевые слова) - UI: меню «Генерация → Файл настроек импорта…» — выбор пути к словарю - при сборке .exe файл остаётся редактируемым без пересборки
This commit is contained in:
@@ -0,0 +1,209 @@
|
|||||||
|
{
|
||||||
|
"common_sheet_keyword": "общие",
|
||||||
|
"employees_sheet_keyword": "сотруд",
|
||||||
|
"is_sheet_exclude_keywords": [
|
||||||
|
"общие сведения",
|
||||||
|
"сотрудники"
|
||||||
|
],
|
||||||
|
"value_column": 1,
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"name": "Полное наименование организации",
|
||||||
|
"var": "fullName",
|
||||||
|
"cell": "",
|
||||||
|
"keywords": "полное наименование организации"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Сокращённое наименование организации",
|
||||||
|
"var": "shortName",
|
||||||
|
"cell": "",
|
||||||
|
"keywords": "сокращенное наименование организации"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Юридический адрес",
|
||||||
|
"var": "addressLegal",
|
||||||
|
"cell": "",
|
||||||
|
"keywords": "адрес организации \\(юридический\\)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Фактический адрес",
|
||||||
|
"var": "addressActual",
|
||||||
|
"cell": "",
|
||||||
|
"keywords": "адрес организации \\(фактический\\)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "ИНН",
|
||||||
|
"var": "inn",
|
||||||
|
"cell": "",
|
||||||
|
"keywords": "инн"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Дата ОГРН",
|
||||||
|
"var": "ogrnDate",
|
||||||
|
"cell": "",
|
||||||
|
"keywords": "дата.*огрн"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "ОГРН",
|
||||||
|
"var": "ogrn",
|
||||||
|
"cell": "",
|
||||||
|
"keywords": "огрн"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "КПП",
|
||||||
|
"var": "kpp",
|
||||||
|
"cell": "",
|
||||||
|
"keywords": "кпп"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Основной ОКВЭД",
|
||||||
|
"var": "okved",
|
||||||
|
"cell": "",
|
||||||
|
"keywords": "основной оквэд"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Должность руководителя",
|
||||||
|
"var": "chiefPosition",
|
||||||
|
"cell": "",
|
||||||
|
"keywords": "должность руководителя"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "ФИО руководителя",
|
||||||
|
"var": "chiefFio",
|
||||||
|
"cell": "",
|
||||||
|
"keywords": "фио руководителя"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Должность ответственного за ПДн",
|
||||||
|
"var": "ispdnPosition",
|
||||||
|
"cell": "",
|
||||||
|
"keywords": "должность.*защит.*информ|должность.*администрат"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "ФИО ответственного за ПДн",
|
||||||
|
"var": "ispdnFio",
|
||||||
|
"cell": "",
|
||||||
|
"keywords": "фио.*защит.*информ|фио.*администрат"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Email ответственного за ПДн",
|
||||||
|
"var": "ispdnEmail",
|
||||||
|
"cell": "",
|
||||||
|
"keywords": "электронная почта.*защит|электронная почта.*ответств|электронная почта.*специалист"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Телефон ответственного за ПДн",
|
||||||
|
"var": "ispdnPhone",
|
||||||
|
"cell": "",
|
||||||
|
"keywords": "телефон.*защит|телефон.*ответств|телефон.*специалист"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Структурное подразделение по безопасности",
|
||||||
|
"var": "ispdnDepartment",
|
||||||
|
"cell": "",
|
||||||
|
"keywords": "структур.*подраздел"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Номер договора",
|
||||||
|
"var": "contractNumber",
|
||||||
|
"cell": "",
|
||||||
|
"keywords": "номер.*договора|номер.*контракта"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Дата договора",
|
||||||
|
"var": "contractDate",
|
||||||
|
"cell": "",
|
||||||
|
"keywords": "дата.*договора|дата.*контракта"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Перечень бумажных документов",
|
||||||
|
"var": "paperDocuments",
|
||||||
|
"cell": "",
|
||||||
|
"keywords": "названи.*документ"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Место хранения документов",
|
||||||
|
"var": "paperStorage",
|
||||||
|
"cell": "",
|
||||||
|
"keywords": "место.*хранен"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Должность ответственного за обработку ПДн",
|
||||||
|
"var": "responsiblePosition",
|
||||||
|
"cell": "",
|
||||||
|
"keywords": "должность.*ответственн"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "ФИО ответственного за обработку ПДн",
|
||||||
|
"var": "responsibleFio",
|
||||||
|
"cell": "",
|
||||||
|
"keywords": "фио.*ответственн"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Должность администратора безопасности",
|
||||||
|
"var": "administratorPosition",
|
||||||
|
"cell": "",
|
||||||
|
"keywords": "должность.*администрат.*(?:безопас|защит|пд)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "ФИО администратора безопасности",
|
||||||
|
"var": "administratorFio",
|
||||||
|
"cell": "",
|
||||||
|
"keywords": "фио.*администрат.*(?:безопас|защит|пд)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Сайт организации",
|
||||||
|
"var": "site_name",
|
||||||
|
"cell": "",
|
||||||
|
"keywords": "сайт организации"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Должность ответственного за сайт",
|
||||||
|
"var": "site_responsible_position",
|
||||||
|
"cell": "",
|
||||||
|
"keywords": "должность ответственного за сайт|должность.*отв.*сайт"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "ФИО ответственного за сайт",
|
||||||
|
"var": "site_responsible_fio",
|
||||||
|
"cell": "",
|
||||||
|
"keywords": "фио.*отв.*сайт|фио.*сайт"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Разработчик сайта",
|
||||||
|
"var": "site_service_provider",
|
||||||
|
"cell": "",
|
||||||
|
"keywords": "наименование организации.*разрабат.*сайт|наименование.*размещала.*сайт|разрабат.*сайт.*организац"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "ИНН разработчика сайта",
|
||||||
|
"var": "site_service_provider_inn",
|
||||||
|
"cell": "",
|
||||||
|
"keywords": "инн.*разрабат.*сайт|инн.*размещала.*сайт"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Размещение сайта (хостинг)",
|
||||||
|
"var": "site_hosting",
|
||||||
|
"cell": "",
|
||||||
|
"keywords": "размещение сайта|адрес расположения серверов|адрес.*сервер.*сайт"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Адрес хостинга",
|
||||||
|
"var": "site_hosting_address",
|
||||||
|
"cell": "",
|
||||||
|
"keywords": "адрес.*хостинг|адрес.*располож.*сервер"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Электронная почта",
|
||||||
|
"var": "email",
|
||||||
|
"cell": "",
|
||||||
|
"keywords": "электронная почта"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Телефон",
|
||||||
|
"var": "phone",
|
||||||
|
"cell": "",
|
||||||
|
"keywords": "телефон"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,174 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""DokoGen — настройки импорта из опросника (редактируемый словарь).
|
||||||
|
|
||||||
|
Идея: «что импортировать и откуда» хранится НЕ в коде, а в текстовом
|
||||||
|
JSON-файле рядом с программой (import_settings.json). При сборке .exe
|
||||||
|
файл остаётся редактируемым — можно править пути к ячейкам и ключевые
|
||||||
|
слова без пересборки.
|
||||||
|
|
||||||
|
Формат файла:
|
||||||
|
{
|
||||||
|
"common_sheet_keyword": "общие", // ключевое слово листа «Общие сведения»
|
||||||
|
"employees_sheet_keyword": "сотруд", // ключевое слово листа «Сотрудники»
|
||||||
|
"is_sheet_exclude_keywords": ["общие сведения", "сотрудники"], // листы, которые НЕ ИС
|
||||||
|
"value_column": 1, // колонка значения (0=A, 1=B, ...)
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"name": "Полное наименование организации", // человекочитаемое название
|
||||||
|
"var": "fullName", // ключ в данных программы
|
||||||
|
"cell": "'Общие сведения'!B2", // (необязательно) конкретная ячейка
|
||||||
|
"keywords": "полное наименование организации" // (необязательно) регэксп поиска по колонке A
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
Порядок применения для каждого поля:
|
||||||
|
1) если задана cell и ячейка заполнена — берём значение оттуда;
|
||||||
|
2) иначе ищем строку, где колонка A совпадает с keywords (регэксп),
|
||||||
|
и берём значение из колонки value_column.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
import json
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# ПУТЬ К ФАЙЛУ НАСТРОЕК ПО УМОЛЧАНИЮ
|
||||||
|
# ============================================================
|
||||||
|
def default_import_settings_path():
|
||||||
|
"""Путь к файлу настроек: рядом с exe (frozen) или рядом с модулем."""
|
||||||
|
if getattr(sys, 'frozen', False):
|
||||||
|
base = os.path.dirname(sys.executable)
|
||||||
|
else:
|
||||||
|
base = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
return os.path.join(base, 'import_settings.json')
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# ДЕФОЛТНЫЙ СЛОВАРЬ (используется, если файл не найден)
|
||||||
|
# ============================================================
|
||||||
|
def _f(name, var, cell='', keywords=''):
|
||||||
|
return {'name': name, 'var': var, 'cell': cell, 'keywords': keywords}
|
||||||
|
|
||||||
|
|
||||||
|
DEFAULT_SETTINGS = {
|
||||||
|
'common_sheet_keyword': 'общие',
|
||||||
|
'employees_sheet_keyword': 'сотруд',
|
||||||
|
'is_sheet_exclude_keywords': ['общие сведения', 'сотрудники'],
|
||||||
|
'value_column': 1,
|
||||||
|
'fields': [
|
||||||
|
_f('Полное наименование организации', 'fullName', keywords=r'полное наименование организации'),
|
||||||
|
_f('Сокращённое наименование организации', 'shortName', keywords=r'сокращенное наименование организации'),
|
||||||
|
_f('Юридический адрес', 'addressLegal', keywords=r'адрес организации \(юридический\)'),
|
||||||
|
_f('Фактический адрес', 'addressActual', keywords=r'адрес организации \(фактический\)'),
|
||||||
|
_f('ИНН', 'inn', keywords=r'инн'),
|
||||||
|
_f('Дата ОГРН', 'ogrnDate', keywords=r'дата.*огрн'),
|
||||||
|
_f('ОГРН', 'ogrn', keywords=r'огрн'),
|
||||||
|
_f('КПП', 'kpp', keywords=r'кпп'),
|
||||||
|
_f('Основной ОКВЭД', 'okved', keywords=r'основной оквэд'),
|
||||||
|
_f('Должность руководителя', 'chiefPosition', keywords=r'должность руководителя'),
|
||||||
|
_f('ФИО руководителя', 'chiefFio', keywords=r'фио руководителя'),
|
||||||
|
_f('Должность ответственного за ПДн', 'ispdnPosition', keywords=r'должность.*защит.*информ|должность.*администрат'),
|
||||||
|
_f('ФИО ответственного за ПДн', 'ispdnFio', keywords=r'фио.*защит.*информ|фио.*администрат'),
|
||||||
|
_f('Email ответственного за ПДн', 'ispdnEmail', keywords=r'электронная почта.*защит|электронная почта.*ответств|электронная почта.*специалист'),
|
||||||
|
_f('Телефон ответственного за ПДн', 'ispdnPhone', keywords=r'телефон.*защит|телефон.*ответств|телефон.*специалист'),
|
||||||
|
_f('Структурное подразделение по безопасности', 'ispdnDepartment', keywords=r'структур.*подраздел'),
|
||||||
|
_f('Номер договора', 'contractNumber', keywords=r'номер.*договора|номер.*контракта'),
|
||||||
|
_f('Дата договора', 'contractDate', keywords=r'дата.*договора|дата.*контракта'),
|
||||||
|
_f('Перечень бумажных документов', 'paperDocuments', keywords=r'названи.*документ'),
|
||||||
|
_f('Место хранения документов', 'paperStorage', keywords=r'место.*хранен'),
|
||||||
|
_f('Должность ответственного за обработку ПДн', 'responsiblePosition', keywords=r'должность.*ответственн'),
|
||||||
|
_f('ФИО ответственного за обработку ПДн', 'responsibleFio', keywords=r'фио.*ответственн'),
|
||||||
|
_f('Должность администратора безопасности', 'administratorPosition', keywords=r'должность.*администрат.*(?:безопас|защит|пд)'),
|
||||||
|
_f('ФИО администратора безопасности', 'administratorFio', keywords=r'фио.*администрат.*(?:безопас|защит|пд)'),
|
||||||
|
_f('Сайт организации', 'site_name', keywords=r'сайт организации'),
|
||||||
|
_f('Должность ответственного за сайт', 'site_responsible_position', keywords=r'должность ответственного за сайт|должность.*отв.*сайт'),
|
||||||
|
_f('ФИО ответственного за сайт', 'site_responsible_fio', keywords=r'фио.*отв.*сайт|фио.*сайт'),
|
||||||
|
_f('Разработчик сайта', 'site_service_provider', keywords=r'наименование организации.*разрабат.*сайт|наименование.*размещала.*сайт|разрабат.*сайт.*организац'),
|
||||||
|
_f('ИНН разработчика сайта', 'site_service_provider_inn', keywords=r'инн.*разрабат.*сайт|инн.*размещала.*сайт'),
|
||||||
|
_f('Размещение сайта (хостинг)', 'site_hosting', keywords=r'размещение сайта|адрес расположения серверов|адрес.*сервер.*сайт'),
|
||||||
|
_f('Адрес хостинга', 'site_hosting_address', keywords=r'адрес.*хостинг|адрес.*располож.*сервер'),
|
||||||
|
_f('Электронная почта', 'email', keywords=r'электронная почта'),
|
||||||
|
_f('Телефон', 'phone', keywords=r'телефон'),
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# ЗАГРУЗКА НАСТРОЕК
|
||||||
|
# ============================================================
|
||||||
|
def load_settings(path=None):
|
||||||
|
"""Загрузка словаря импорта из JSON-файла.
|
||||||
|
|
||||||
|
path=None -> файл по умолчанию рядом с программой.
|
||||||
|
Если файла нет или он битый — возвращается встроенный дефолт.
|
||||||
|
"""
|
||||||
|
if path is None:
|
||||||
|
path = default_import_settings_path()
|
||||||
|
|
||||||
|
if not path or not os.path.exists(path):
|
||||||
|
return DEFAULT_SETTINGS
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(path, encoding='utf-8-sig') as f:
|
||||||
|
loaded = json.load(f)
|
||||||
|
# Дополняем недостающие ключи из дефолта
|
||||||
|
merged = dict(DEFAULT_SETTINGS)
|
||||||
|
for k, v in loaded.items():
|
||||||
|
if k == 'fields' and isinstance(v, list):
|
||||||
|
merged['fields'] = v
|
||||||
|
else:
|
||||||
|
merged[k] = v
|
||||||
|
return merged
|
||||||
|
except Exception:
|
||||||
|
return DEFAULT_SETTINGS
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_default_file(path=None):
|
||||||
|
"""Создаёт файл настроек import_settings.json, если его ещё нет.
|
||||||
|
|
||||||
|
Возвращает путь к файлу. Нужно, чтобы пользователь видел словарь
|
||||||
|
импорта и мог править его в блокноте без пересборки программы.
|
||||||
|
"""
|
||||||
|
if path is None:
|
||||||
|
path = default_import_settings_path()
|
||||||
|
if os.path.exists(path):
|
||||||
|
return path
|
||||||
|
try:
|
||||||
|
os.makedirs(os.path.dirname(path), exist_ok=True)
|
||||||
|
with open(path, 'w', encoding='utf-8') as f:
|
||||||
|
json.dump(DEFAULT_SETTINGS, f, ensure_ascii=False, indent=2)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return path
|
||||||
|
|
||||||
|
|
||||||
|
def parse_cell_ref(cell_ref):
|
||||||
|
"""Разбор ссылки на ячейку: 'Общие сведения'!B2 / Общие сведения!B2 / B2.
|
||||||
|
|
||||||
|
Возвращает (sheet_name или None, координаты 'B2').
|
||||||
|
"""
|
||||||
|
if not cell_ref:
|
||||||
|
return None, ''
|
||||||
|
ref = cell_ref.strip()
|
||||||
|
if ref.startswith('='):
|
||||||
|
ref = ref[1:].strip()
|
||||||
|
if '!' in ref:
|
||||||
|
sheet_part, coord = ref.split('!', 1)
|
||||||
|
sheet_part = sheet_part.strip()
|
||||||
|
if sheet_part.startswith("'") and sheet_part.endswith("'"):
|
||||||
|
sheet_part = sheet_part[1:-1]
|
||||||
|
return sheet_part or None, coord.strip().upper()
|
||||||
|
return None, ref.upper()
|
||||||
|
|
||||||
|
|
||||||
|
def cell_value(ws, coord):
|
||||||
|
"""Значение ячейки по координате 'B2' (с нормализацией)."""
|
||||||
|
if not coord:
|
||||||
|
return ''
|
||||||
|
try:
|
||||||
|
val = ws[coord]
|
||||||
|
return val.value if val is not None else ''
|
||||||
|
except Exception:
|
||||||
|
return ''
|
||||||
+81
-59
@@ -12,6 +12,9 @@ except ImportError:
|
|||||||
openpyxl = None
|
openpyxl = None
|
||||||
|
|
||||||
|
|
||||||
|
from .import_settings import load_settings, parse_cell_ref, cell_value
|
||||||
|
|
||||||
|
|
||||||
def clean_value(val):
|
def clean_value(val):
|
||||||
"""Очистка и нормализация значения ячейки Excel."""
|
"""Очистка и нормализация значения ячейки Excel."""
|
||||||
if val is None:
|
if val is None:
|
||||||
@@ -42,14 +45,20 @@ def clean_value(val):
|
|||||||
return text
|
return text
|
||||||
|
|
||||||
|
|
||||||
def import_152fz(filepath: str, log_fn: Optional[Callable] = None) -> dict:
|
def import_152fz(filepath: str, log_fn: Optional[Callable] = None, settings_path: Optional[str] = None) -> dict:
|
||||||
"""Импорт данных из опросного листа Excel (152-ФЗ)."""
|
"""Импорт данных из опросного листа Excel (152-ФЗ).
|
||||||
|
|
||||||
|
settings_path — путь к JSON-файлу настроек импорта (что и откуда брать).
|
||||||
|
Если None — файл import_settings.json рядом с программой; если его нет —
|
||||||
|
используется встроенный словарь по умолчанию.
|
||||||
|
"""
|
||||||
if openpyxl is None:
|
if openpyxl is None:
|
||||||
raise ImportError("openpyxl не установлен. Установите: pip install openpyxl")
|
raise ImportError("openpyxl не установлен. Установите: pip install openpyxl")
|
||||||
|
|
||||||
if log_fn is None:
|
if log_fn is None:
|
||||||
log_fn = lambda msg: None
|
log_fn = lambda msg: None
|
||||||
|
|
||||||
|
settings = load_settings(settings_path)
|
||||||
log_fn(f"Загрузка опросного листа: {filepath}")
|
log_fn(f"Загрузка опросного листа: {filepath}")
|
||||||
|
|
||||||
data = {
|
data = {
|
||||||
@@ -68,9 +77,9 @@ def import_152fz(filepath: str, log_fn: Optional[Callable] = None) -> dict:
|
|||||||
return data
|
return data
|
||||||
|
|
||||||
try:
|
try:
|
||||||
_import_152_common(wb, data, log_fn)
|
_import_152_common(wb, data, settings, log_fn)
|
||||||
_import_152_is_list(wb, data, log_fn)
|
_import_152_is_list(wb, data, settings, log_fn)
|
||||||
_import_152_employees(wb, data, log_fn)
|
_import_152_employees(wb, data, settings, log_fn)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log_fn(f"❌ Ошибка при импорте: {e}")
|
log_fn(f"❌ Ошибка при импорте: {e}")
|
||||||
log_fn(traceback.format_exc())
|
log_fn(traceback.format_exc())
|
||||||
@@ -88,71 +97,83 @@ def import_152fz(filepath: str, log_fn: Optional[Callable] = None) -> dict:
|
|||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
def _import_152_common(wb, data, log_fn):
|
def _import_152_common(wb, data, settings, log_fn):
|
||||||
"""Импорт общих сведений из листа 'Общие сведения'."""
|
"""Импорт общих сведений по словарю настроек.
|
||||||
|
|
||||||
|
Для каждого поля: сначала пробуем конкретную ячейку (cell),
|
||||||
|
если она задана и заполнена; иначе ищем строку по ключевым
|
||||||
|
словам (keywords) в колонке A листа «Общие сведения».
|
||||||
|
"""
|
||||||
sheet_name = None
|
sheet_name = None
|
||||||
|
sheet_kw = settings.get('common_sheet_keyword', 'общие')
|
||||||
for sn in wb.sheetnames:
|
for sn in wb.sheetnames:
|
||||||
if 'общие' in sn.lower():
|
if sheet_kw in sn.lower():
|
||||||
sheet_name = sn
|
sheet_name = sn
|
||||||
break
|
break
|
||||||
if not sheet_name:
|
if not sheet_name:
|
||||||
log_fn("⚠ Лист 'Общие сведения' не найден")
|
log_fn(f"⚠ Лист по ключу '{sheet_kw}' не найден")
|
||||||
return
|
return
|
||||||
|
|
||||||
ws = wb[sheet_name]
|
ws = wb[sheet_name]
|
||||||
field_map = [
|
value_col = int(settings.get('value_column', 1))
|
||||||
('полное наименование организации', 'fullName'),
|
|
||||||
('сокращенное наименование организации', 'shortName'),
|
|
||||||
('адрес организации \\(юридический\\)', 'addressLegal'),
|
|
||||||
('адрес организации \\(фактический\\)', 'addressActual'),
|
|
||||||
('инн', 'inn'),
|
|
||||||
('дата.*огрн', 'ogrnDate'),
|
|
||||||
('огрн', 'ogrn'),
|
|
||||||
('кпп', 'kpp'),
|
|
||||||
('основной оквэд', 'okved'),
|
|
||||||
('должность руководителя', 'chiefPosition'),
|
|
||||||
('фио руководителя', 'chiefFio'),
|
|
||||||
('должность.*защит.*информ|должность.*администрат', 'ispdnPosition'),
|
|
||||||
('фио.*защит.*информ|фио.*администрат', 'ispdnFio'),
|
|
||||||
('электронная почта.*защит|электронная почта.*ответств|электронная почта.*специалист', 'ispdnEmail'),
|
|
||||||
('телефон.*защит|телефон.*ответств|телефон.*специалист', 'ispdnPhone'),
|
|
||||||
('структур.*подраздел', 'ispdnDepartment'),
|
|
||||||
('номер.*договора|номер.*контракта', 'contractNumber'),
|
|
||||||
('дата.*договора|дата.*контракта', 'contractDate'),
|
|
||||||
('названи.*документ', 'paperDocuments'),
|
|
||||||
('место.*хранен', 'paperStorage'),
|
|
||||||
('должность.*ответственн', 'responsiblePosition'),
|
|
||||||
('фио.*ответственн', 'responsibleFio'),
|
|
||||||
('должность.*администрат.*(?:безопас|защит|пд)', 'administratorPosition'),
|
|
||||||
('фио.*администрат.*(?:безопас|защит|пд)', 'administratorFio'),
|
|
||||||
('сайт организации', 'site_name'),
|
|
||||||
('должность ответственного за сайт|должность.*отв.*сайт', 'site_responsible_position'),
|
|
||||||
('фио.*отв.*сайт|фио.*сайт', 'site_responsible_fio'),
|
|
||||||
('наименование организации.*разрабат.*сайт|наименование.*размещала.*сайт|разрабат.*сайт.*организац', 'site_service_provider'),
|
|
||||||
('инн.*разрабат.*сайт|инн.*размещала.*сайт', 'site_service_provider_inn'),
|
|
||||||
('размещение сайта|адрес расположения серверов|адрес.*сервер.*сайт', 'site_hosting'),
|
|
||||||
('адрес.*хостинг|адрес.*располож.*сервер', 'site_hosting_address'),
|
|
||||||
('электронная почта', 'email'),
|
|
||||||
('телефон', 'phone'),
|
|
||||||
]
|
|
||||||
|
|
||||||
|
# Кэш значений: для каждого поля ищем по строке один раз
|
||||||
|
row_cache = {}
|
||||||
for row in ws.iter_rows(min_row=1, values_only=True):
|
for row in ws.iter_rows(min_row=1, values_only=True):
|
||||||
field_raw = clean_value(row[0]) if row[0] is not None else ''
|
field_raw = clean_value(row[0]) if row[0] is not None else ''
|
||||||
field = field_raw.lower()
|
if not field_raw:
|
||||||
value = clean_value(row[1]) if len(row) > 1 else ''
|
|
||||||
if not field:
|
|
||||||
continue
|
continue
|
||||||
for keyword, key in field_map:
|
row_cache[field_raw.lower()] = (
|
||||||
if re.search(keyword, field):
|
clean_value(row[value_col]) if len(row) > value_col else ''
|
||||||
if value:
|
)
|
||||||
data[key] = value
|
|
||||||
log_fn(f" {keyword}: {value}")
|
for rule in settings.get('fields', []):
|
||||||
break
|
key = rule.get('var', '')
|
||||||
|
if not key:
|
||||||
|
continue
|
||||||
|
name = rule.get('name', key)
|
||||||
|
cell_ref = rule.get('cell', '')
|
||||||
|
keywords = rule.get('keywords', '')
|
||||||
|
|
||||||
|
# 1) Конкретная ячейка
|
||||||
|
if cell_ref:
|
||||||
|
sheet_part, coord = parse_cell_ref(cell_ref)
|
||||||
|
target_ws = ws
|
||||||
|
if sheet_part:
|
||||||
|
for sn in wb.sheetnames:
|
||||||
|
if sn.lower() == sheet_part.lower():
|
||||||
|
target_ws = wb[sn]
|
||||||
|
break
|
||||||
|
val = clean_value(cell_value(target_ws, coord))
|
||||||
|
if val:
|
||||||
|
data[key] = val
|
||||||
|
log_fn(f" {name} [ячейка {coord}]: {val}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 2) Поиск по ключевым словам
|
||||||
|
if keywords:
|
||||||
|
try:
|
||||||
|
rx = re.compile(keywords, re.IGNORECASE)
|
||||||
|
except re.error:
|
||||||
|
rx = None
|
||||||
|
if rx:
|
||||||
|
for field_lower, val in row_cache.items():
|
||||||
|
if rx.search(field_lower):
|
||||||
|
if val:
|
||||||
|
data[key] = val
|
||||||
|
log_fn(f" {name} [по ключу]: {val}")
|
||||||
|
break
|
||||||
|
|
||||||
|
|
||||||
def _import_152_is_list(wb, data, log_fn):
|
def _import_152_is_list(wb, data, settings, log_fn):
|
||||||
"""Импорт информационных систем из остальных листов."""
|
"""Импорт информационных систем из остальных листов."""
|
||||||
is_sheets = [sn for sn in wb.sheetnames if sn not in ('Общие сведения', 'Сотрудники')]
|
exclude_kws = settings.get('is_sheet_exclude_keywords', ['общие сведения', 'сотрудники'])
|
||||||
|
is_sheets = []
|
||||||
|
for sn in wb.sheetnames:
|
||||||
|
sn_lower = sn.lower()
|
||||||
|
if any(kw in sn_lower for kw in exclude_kws):
|
||||||
|
continue
|
||||||
|
is_sheets.append(sn)
|
||||||
if not is_sheets:
|
if not is_sheets:
|
||||||
log_fn("⚠ Листы ИС не найдены")
|
log_fn("⚠ Листы ИС не найдены")
|
||||||
return
|
return
|
||||||
@@ -326,15 +347,16 @@ def _import_152_is_list(wb, data, log_fn):
|
|||||||
log_fn(f"\n✅ Всего импортировано ИС: {len(is_list)}")
|
log_fn(f"\n✅ Всего импортировано ИС: {len(is_list)}")
|
||||||
|
|
||||||
|
|
||||||
def _import_152_employees(wb, data, log_fn):
|
def _import_152_employees(wb, data, settings, log_fn):
|
||||||
"""Импорт комиссии и сотрудников из листа 'Сотрудники'."""
|
"""Импорт комиссии и сотрудников из листа «Сотрудники»."""
|
||||||
sheet_name = None
|
sheet_name = None
|
||||||
|
sheet_kw = settings.get('employees_sheet_keyword', 'сотруд')
|
||||||
for sn in wb.sheetnames:
|
for sn in wb.sheetnames:
|
||||||
if 'сотруд' in sn.lower():
|
if sheet_kw in sn.lower():
|
||||||
sheet_name = sn
|
sheet_name = sn
|
||||||
break
|
break
|
||||||
if not sheet_name:
|
if not sheet_name:
|
||||||
log_fn("⚠ Лист «Сотрудники» не найден")
|
log_fn(f"⚠ Лист по ключу '{sheet_kw}' не найден")
|
||||||
return
|
return
|
||||||
|
|
||||||
ws = wb[sheet_name]
|
ws = wb[sheet_name]
|
||||||
|
|||||||
+20
-1
@@ -38,6 +38,10 @@ class DokoGenApp:
|
|||||||
'.autosave', 'autosave_152.json'
|
'.autosave', 'autosave_152.json'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Путь к файлу настроек импорта (словарь «что и откуда брать»)
|
||||||
|
# None = import_settings.json рядом с программой (создаётся автоматически)
|
||||||
|
self.import_settings_path: Optional[str] = None
|
||||||
|
|
||||||
# Переменные
|
# Переменные
|
||||||
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="")
|
||||||
@@ -83,6 +87,7 @@ class DokoGenApp:
|
|||||||
gen_menu = tk.Menu(menubar, tearoff=0)
|
gen_menu = tk.Menu(menubar, tearoff=0)
|
||||||
menubar.add_cascade(label="Генерация", menu=gen_menu)
|
menubar.add_cascade(label="Генерация", menu=gen_menu)
|
||||||
gen_menu.add_command(label="Импорт из опросника", command=self.import_from_survey)
|
gen_menu.add_command(label="Импорт из опросника", command=self.import_from_survey)
|
||||||
|
gen_menu.add_command(label="Файл настроек импорта…", command=self.select_import_settings)
|
||||||
gen_menu.add_separator()
|
gen_menu.add_separator()
|
||||||
gen_menu.add_command(label="Сгенерировать всё", command=self.generate_all)
|
gen_menu.add_command(label="Сгенерировать всё", command=self.generate_all)
|
||||||
|
|
||||||
@@ -1267,6 +1272,20 @@ class DokoGenApp:
|
|||||||
self.log("✅ Склонения краткого названия обновлены")
|
self.log("✅ Склонения краткого названия обновлены")
|
||||||
|
|
||||||
# ==================== ИМПОРТ ====================
|
# ==================== ИМПОРТ ====================
|
||||||
|
def select_import_settings(self):
|
||||||
|
"""Выбор файла настроек импорта (JSON-словарь)."""
|
||||||
|
from .import_settings import default_import_settings_path, ensure_default_file
|
||||||
|
path = filedialog.askopenfilename(
|
||||||
|
title="Файл настроек импорта (что и откуда брать)",
|
||||||
|
filetypes=[("Настройки импорта", "*.json")]
|
||||||
|
)
|
||||||
|
if not path:
|
||||||
|
self.log(f"Настройки импорта: по умолчанию ({default_import_settings_path()})")
|
||||||
|
self.import_settings_path = None
|
||||||
|
return
|
||||||
|
self.import_settings_path = path
|
||||||
|
self.log(f"Настройки импорта: {path}")
|
||||||
|
|
||||||
def import_from_survey(self):
|
def import_from_survey(self):
|
||||||
path = filedialog.askopenfilename(
|
path = filedialog.askopenfilename(
|
||||||
title="Выберите опросный лист (152-ФЗ)",
|
title="Выберите опросный лист (152-ФЗ)",
|
||||||
@@ -1280,7 +1299,7 @@ class DokoGenApp:
|
|||||||
def _run():
|
def _run():
|
||||||
try:
|
try:
|
||||||
from .importer import import_152fz
|
from .importer import import_152fz
|
||||||
data = import_152fz(path, self.log)
|
data = import_152fz(path, self.log, settings_path=self.import_settings_path)
|
||||||
if data:
|
if data:
|
||||||
self._apply_imported_data(data)
|
self._apply_imported_data(data)
|
||||||
except ImportError:
|
except ImportError:
|
||||||
|
|||||||
Reference in New Issue
Block a user