feat: ImportMapper — универсальный редактор импорта из Excel-опросников
- Профили: 152-ФЗ, 187-ФЗ, 117-ФЗ, МУ, КЗИ, ПЗИ - Сканирование Excel: что импортируется и куда - Галочки: включить/выключить поля, добавить свои привязки - Сохранение настроек по типу опросника - Выгрузка данных в JSON (export/export_<тип>.json) - Тесты: отключение полей, пользовательские привязки, секции, ИС
This commit is contained in:
@@ -0,0 +1,6 @@
|
|||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
settings/
|
||||||
|
export/
|
||||||
|
.venv/
|
||||||
|
venv/
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
# ImportMapper
|
||||||
|
|
||||||
|
Универсальный редактор импорта из Excel-опросников.
|
||||||
|
Показывает, какие данные из файла опросника импортируются и куда,
|
||||||
|
позволяет включить/выключить поля и добавить свои привязки.
|
||||||
|
|
||||||
|
Поддерживаемые типы опросников (профили в `profiles/`):
|
||||||
|
|
||||||
|
- `152` — 152-ФЗ «О персональных данных»
|
||||||
|
- `187` — 187-ФЗ «О безопасности КИИ»
|
||||||
|
- `117` — 117-ФЗ (безопасность объектов КИИ / ГИС)
|
||||||
|
- `mu` — МУ (методические указания)
|
||||||
|
- `kzi` — КЗИ (комплекс средств защиты информации)
|
||||||
|
- `pzi` — ПЗИ (программные средства защиты информации)
|
||||||
|
|
||||||
|
## Запуск
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install -r requirements.txt
|
||||||
|
python3 main.py
|
||||||
|
```
|
||||||
|
|
||||||
|
## Как пользоваться
|
||||||
|
|
||||||
|
1. Выберите тип опросника в выпадающем списке.
|
||||||
|
2. Нажмите «Обзор» и укажите Excel-файл опросника.
|
||||||
|
3. Нажмите «Сканировать» — программа покажет, что импортируется.
|
||||||
|
4. Галочками включите/выключите нужные поля.
|
||||||
|
5. «💾 Сохранить настройки» — сохраняет настройки для этого типа опросника.
|
||||||
|
6. «📤 Выгрузить данные» — собирает данные из Excel в JSON по настройкам
|
||||||
|
(файл `export/export_<тип>.json`), который может использовать генератор.
|
||||||
|
|
||||||
|
## Структура
|
||||||
|
|
||||||
|
- `main.py` — точка входа
|
||||||
|
- `editor.py` — окно редактора
|
||||||
|
- `core.py` — профили, сканирование Excel, извлечение данных
|
||||||
|
- `profiles/*.json` — правила импорта для каждого типа опросника
|
||||||
|
- `settings/settings_<тип>.json` — настройки пользователя (создаются автоматически)
|
||||||
|
- `export/export_<тип>.json` — выгруженные данные (создаются автоматически)
|
||||||
|
|
||||||
|
## Профили
|
||||||
|
|
||||||
|
Каждый профиль — JSON-файл:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "152",
|
||||||
|
"name": "152-ФЗ (персональные данные)",
|
||||||
|
"common_sheet": "общие",
|
||||||
|
"employee_sheet": "сотруд",
|
||||||
|
"is_enabled": true,
|
||||||
|
"rules": [
|
||||||
|
{"pattern": "полное наименование организации", "field": "fullName", "desc": "Полное наименование организации"}
|
||||||
|
],
|
||||||
|
"sections": {
|
||||||
|
"commission": {"header": "состав комиссии"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- `common_sheet` — лист с общими сведениями (подстрока имени листа)
|
||||||
|
- `employee_sheet` — лист с комиссией/сотрудниками (подстрока имени листа)
|
||||||
|
- `is_enabled` — включать ли разбор остальных листов как информационных систем
|
||||||
|
- `rules` — правила: регулярное выражение заголовка → поле программы
|
||||||
|
- `sections` — секции на листе сотрудников (заголовок секции → id)
|
||||||
|
|
||||||
|
Правила для 187/117/МУ/КЗИ/ПЗИ — базовые; их можно дополнить под конкретные
|
||||||
|
опросники через редактор (галочка «Найдено, но не импортируется») или
|
||||||
|
отредактировав JSON профиля.
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""ImportMapper — универсальный редактор импорта из Excel-опросников."""
|
||||||
@@ -0,0 +1,350 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""ImportMapper — ядро: профили опросников, сканирование Excel, извлечение данных."""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import json
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
|
try:
|
||||||
|
import openpyxl
|
||||||
|
except ImportError:
|
||||||
|
openpyxl = None
|
||||||
|
|
||||||
|
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
PROFILES_DIR = os.path.join(BASE_DIR, 'profiles')
|
||||||
|
SETTINGS_DIR = os.path.join(BASE_DIR, 'settings')
|
||||||
|
EXPORT_DIR = os.path.join(BASE_DIR, 'export')
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# НОРМАЛИЗАЦИЯ ЗНАЧЕНИЙ
|
||||||
|
# ============================================================
|
||||||
|
def clean_value(val):
|
||||||
|
"""Очистка и нормализация значения ячейки Excel."""
|
||||||
|
if val is None:
|
||||||
|
return ''
|
||||||
|
if isinstance(val, (int, float)):
|
||||||
|
# Попытка распознать дату (серийный номер Excel)
|
||||||
|
if 10000 < val < 80000:
|
||||||
|
serial = int(val)
|
||||||
|
if serial > 60:
|
||||||
|
serial -= 1
|
||||||
|
dt = datetime(1899, 12, 30) + timedelta(days=serial)
|
||||||
|
return dt.strftime('%d.%m.%Y')
|
||||||
|
if isinstance(val, float) and val == int(val):
|
||||||
|
return str(int(val))
|
||||||
|
return str(val)
|
||||||
|
text = str(val).strip()
|
||||||
|
text = text.replace('\n', ' ').replace('\r', ' ')
|
||||||
|
text = ' '.join(text.split())
|
||||||
|
# Приведение регистра: если текст только из ЗАГЛАВНЫХ
|
||||||
|
if len(text) > 4 and any('А' <= c <= 'Я' or c == 'Ё' for c in text):
|
||||||
|
has_lower = any('а' <= c <= 'я' or c == 'ё' for c in text)
|
||||||
|
if not has_lower:
|
||||||
|
words = text.split()
|
||||||
|
text = text.title() if len(words) >= 3 else text.capitalize()
|
||||||
|
return text
|
||||||
|
|
||||||
|
|
||||||
|
def is_noise_label(val: str) -> bool:
|
||||||
|
"""Служебные строки опросника, которые импортировать не нужно."""
|
||||||
|
noise = {'№ п/п', '№', 'номер', 'п/п', '№пп', '№ пп', 'значение', 'примечание',
|
||||||
|
'вопрос', 'ответ', 'выбрать', 'да', 'нет', 'нет данных', 'поле'}
|
||||||
|
if val.strip() in noise:
|
||||||
|
return True
|
||||||
|
if re.fullmatch(r'\d+', val.strip()):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# ПРОФИЛИ ОПРОСНИКОВ
|
||||||
|
# ============================================================
|
||||||
|
def list_profiles():
|
||||||
|
"""Список доступных профилей опросников."""
|
||||||
|
result = []
|
||||||
|
if not os.path.isdir(PROFILES_DIR):
|
||||||
|
return result
|
||||||
|
for fn in sorted(os.listdir(PROFILES_DIR)):
|
||||||
|
if fn.endswith('.json'):
|
||||||
|
try:
|
||||||
|
with open(os.path.join(PROFILES_DIR, fn), encoding='utf-8') as f:
|
||||||
|
profile = json.load(f)
|
||||||
|
result.append(profile)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def load_profile(profile_id):
|
||||||
|
for p in list_profiles():
|
||||||
|
if p.get('id') == profile_id:
|
||||||
|
return p
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def settings_path(profile_id):
|
||||||
|
os.makedirs(SETTINGS_DIR, exist_ok=True)
|
||||||
|
return os.path.join(SETTINGS_DIR, f'settings_{profile_id}.json')
|
||||||
|
|
||||||
|
|
||||||
|
def load_settings(profile_id):
|
||||||
|
try:
|
||||||
|
with open(settings_path(profile_id), encoding='utf-8') as f:
|
||||||
|
return json.load(f)
|
||||||
|
except Exception:
|
||||||
|
return {'disabled': [], 'custom': []}
|
||||||
|
|
||||||
|
|
||||||
|
def save_settings(profile_id, settings):
|
||||||
|
with open(settings_path(profile_id), 'w', encoding='utf-8') as f:
|
||||||
|
json.dump(settings, f, ensure_ascii=False, indent=2)
|
||||||
|
|
||||||
|
|
||||||
|
def is_disabled(settings, key):
|
||||||
|
return key in settings.get('disabled', [])
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# СКАНИРОВАНИЕ EXCEL
|
||||||
|
# ============================================================
|
||||||
|
def scan_excel(filepath, profile):
|
||||||
|
"""Сканирует Excel по правилам профиля.
|
||||||
|
|
||||||
|
Возвращает (detected, unmapped, error):
|
||||||
|
detected — пары (заголовок → поле), которые импортируются
|
||||||
|
unmapped — найденные заголовки без привязки
|
||||||
|
"""
|
||||||
|
if not openpyxl:
|
||||||
|
return [], [], 'openpyxl не установлен. Установите: pip install openpyxl'
|
||||||
|
|
||||||
|
rules = profile.get('rules', [])
|
||||||
|
common_sheet = profile.get('common_sheet', 'общие')
|
||||||
|
employee_sheet = profile.get('employee_sheet', 'сотруд')
|
||||||
|
|
||||||
|
detected = []
|
||||||
|
unmapped = []
|
||||||
|
|
||||||
|
try:
|
||||||
|
wb = openpyxl.load_workbook(filepath, data_only=True, read_only=False)
|
||||||
|
except Exception as e:
|
||||||
|
return [], [], f'Не удалось открыть файл: {e}'
|
||||||
|
|
||||||
|
try:
|
||||||
|
for sheet_name in wb.sheetnames:
|
||||||
|
sheet_lower = sheet_name.lower()
|
||||||
|
if common_sheet and common_sheet.lower() in sheet_lower:
|
||||||
|
sheet_type = 'common'
|
||||||
|
elif employee_sheet and employee_sheet.lower() in sheet_lower:
|
||||||
|
sheet_type = 'employees'
|
||||||
|
else:
|
||||||
|
sheet_type = 'is'
|
||||||
|
|
||||||
|
ws = wb[sheet_name]
|
||||||
|
for row in ws.iter_rows(min_row=1, values_only=True):
|
||||||
|
label = str(row[0]).strip() if row and row[0] is not None else ''
|
||||||
|
value = str(row[1]).strip() if len(row) > 1 and row[1] is not None else ''
|
||||||
|
if not label:
|
||||||
|
continue
|
||||||
|
|
||||||
|
label_lower = label.lower()
|
||||||
|
mapped = False
|
||||||
|
for rule in rules:
|
||||||
|
pattern = rule.get('pattern', '')
|
||||||
|
if pattern and re.search(pattern, label_lower):
|
||||||
|
detected.append({
|
||||||
|
'field': rule.get('field', ''),
|
||||||
|
'excel_text': label,
|
||||||
|
'value': value[:60],
|
||||||
|
'sheet': sheet_name,
|
||||||
|
'desc': rule.get('desc', rule.get('field', '')),
|
||||||
|
})
|
||||||
|
mapped = True
|
||||||
|
break
|
||||||
|
|
||||||
|
if not mapped:
|
||||||
|
if is_noise_label(label_lower):
|
||||||
|
continue
|
||||||
|
unmapped.append({
|
||||||
|
'excel_text': label,
|
||||||
|
'sheet': sheet_name,
|
||||||
|
'suggested': _suggest_field(label, sheet_type, profile),
|
||||||
|
})
|
||||||
|
finally:
|
||||||
|
try:
|
||||||
|
wb.close()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return detected, unmapped, ''
|
||||||
|
|
||||||
|
|
||||||
|
def _suggest_field(value, sheet_type, profile):
|
||||||
|
"""Пытается предположить, какому полю соответствует заголовок."""
|
||||||
|
val = value.lower().strip()
|
||||||
|
if is_noise_label(val):
|
||||||
|
return ''
|
||||||
|
|
||||||
|
for rule in profile.get('rules', []):
|
||||||
|
pattern = rule.get('pattern', '')
|
||||||
|
if pattern and re.search(pattern, val):
|
||||||
|
return rule.get('field', '')
|
||||||
|
|
||||||
|
keywords = {
|
||||||
|
'fullName': ['полное наименование', 'наименование организации', 'полное название'],
|
||||||
|
'shortName': ['сокращенное наименование', 'краткое наименование', 'сокращ'],
|
||||||
|
'addressLegal': ['адрес', 'место нахождения'],
|
||||||
|
'chiefFio': ['руководител', 'глава', 'директор'],
|
||||||
|
'inn': ['инн', 'идентификационный номер'],
|
||||||
|
'ogrn': ['огрн', 'основной государствен'],
|
||||||
|
'kpp': ['кпп'],
|
||||||
|
'phone': ['телефон', 'контактный'],
|
||||||
|
'email': ['почта', 'email', 'e-mail'],
|
||||||
|
'is_name': ['наименование ис', 'название ис', 'наименование информационной'],
|
||||||
|
'is_software': ['программное обеспечение', 'программный'],
|
||||||
|
}
|
||||||
|
for field, kws in keywords.items():
|
||||||
|
for kw in kws:
|
||||||
|
if kw in val:
|
||||||
|
return field
|
||||||
|
|
||||||
|
return 'unknown_' + value[:20].replace(' ', '_').lower()
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# ИЗВЛЕЧЕНИЕ ДАННЫХ (ПО НАСТРОЙКАМ РЕДАКТОРА)
|
||||||
|
# ============================================================
|
||||||
|
def extract_data(filepath, profile, settings):
|
||||||
|
"""Извлекает данные из опросника с учётом настроек редактора.
|
||||||
|
|
||||||
|
Возвращает dict: {'fields': {...}, 'sections': {...}, 'is_list': [...]}
|
||||||
|
"""
|
||||||
|
if not openpyxl:
|
||||||
|
return {'error': 'openpyxl не установлен'}
|
||||||
|
|
||||||
|
disabled = set(settings.get('disabled', []))
|
||||||
|
custom = settings.get('custom', [])
|
||||||
|
rules = profile.get('rules', [])
|
||||||
|
common_sheet = profile.get('common_sheet', 'общие')
|
||||||
|
employee_sheet = profile.get('employee_sheet', 'сотруд')
|
||||||
|
|
||||||
|
result = {'fields': {}, 'sections': {}, 'is_list': []}
|
||||||
|
|
||||||
|
# Все секции профиля всегда присутствуют в выгрузке (пустые, если отключены)
|
||||||
|
for sec_id in profile.get('sections', {}):
|
||||||
|
result['sections'][sec_id] = []
|
||||||
|
|
||||||
|
try:
|
||||||
|
wb = openpyxl.load_workbook(filepath, data_only=True, read_only=False)
|
||||||
|
except Exception as e:
|
||||||
|
return {'error': str(e)}
|
||||||
|
|
||||||
|
try:
|
||||||
|
# --- Общие сведения ---
|
||||||
|
common_sn = None
|
||||||
|
for sn in wb.sheetnames:
|
||||||
|
if common_sheet and common_sheet.lower() in sn.lower():
|
||||||
|
common_sn = sn
|
||||||
|
break
|
||||||
|
if common_sn:
|
||||||
|
ws = wb[common_sn]
|
||||||
|
for row in ws.iter_rows(min_row=1, values_only=True):
|
||||||
|
label = clean_value(row[0]) if row and row[0] is not None else ''
|
||||||
|
value = clean_value(row[1]) if len(row) > 1 else ''
|
||||||
|
if not label:
|
||||||
|
continue
|
||||||
|
label_lower = label.lower()
|
||||||
|
for rule in rules:
|
||||||
|
pattern = rule.get('pattern', '')
|
||||||
|
field = rule.get('field', '')
|
||||||
|
if pattern and re.search(pattern, label_lower):
|
||||||
|
if not is_disabled(settings, field) and value:
|
||||||
|
result['fields'][field] = value
|
||||||
|
break
|
||||||
|
|
||||||
|
# --- Секции (комиссия, сотрудники) ---
|
||||||
|
sections = profile.get('sections', {})
|
||||||
|
emp_sn = None
|
||||||
|
for sn in wb.sheetnames:
|
||||||
|
if employee_sheet and employee_sheet.lower() in sn.lower():
|
||||||
|
emp_sn = sn
|
||||||
|
break
|
||||||
|
if emp_sn and sections:
|
||||||
|
ws = wb[emp_sn]
|
||||||
|
current = None
|
||||||
|
for row in ws.iter_rows(min_row=1, values_only=True):
|
||||||
|
col_a = clean_value(row[0]) if row and row[0] is not None else ''
|
||||||
|
a_lower = col_a.lower()
|
||||||
|
header_hit = None
|
||||||
|
for sec_id, sec_cfg in sections.items():
|
||||||
|
if is_disabled(settings, sec_id):
|
||||||
|
continue
|
||||||
|
if re.search(sec_cfg.get('header', ''), a_lower):
|
||||||
|
header_hit = sec_id
|
||||||
|
break
|
||||||
|
if header_hit:
|
||||||
|
current = header_hit
|
||||||
|
result['sections'].setdefault(current, [])
|
||||||
|
continue
|
||||||
|
if current:
|
||||||
|
# Заголовок таблицы внутри секции — пропускаем
|
||||||
|
if a_lower in ('№ п/п', '№', 'номер', 'п/п', '№пп', '№ пп'):
|
||||||
|
continue
|
||||||
|
row_data = [clean_value(c) if c is not None else ''
|
||||||
|
for c in row[:6]]
|
||||||
|
if any(row_data):
|
||||||
|
# Номер строки в первой колонке не нужен
|
||||||
|
if re.fullmatch(r'\d+', row_data[0].strip()):
|
||||||
|
row_data[0] = ''
|
||||||
|
result['sections'][current].append(row_data)
|
||||||
|
|
||||||
|
# --- Информационные системы (листы, отличные от общих/сотрудников) ---
|
||||||
|
if profile.get('is_enabled'):
|
||||||
|
for sn in wb.sheetnames:
|
||||||
|
if common_sheet and common_sheet.lower() in sn.lower():
|
||||||
|
continue
|
||||||
|
if employee_sheet and employee_sheet.lower() in sn.lower():
|
||||||
|
continue
|
||||||
|
ws = wb[sn]
|
||||||
|
is_obj = {'name': '', 'rows': []}
|
||||||
|
for row in ws.iter_rows(min_row=1, values_only=True):
|
||||||
|
vals = [clean_value(c) for c in row]
|
||||||
|
label = (vals[0] or '').strip() if vals else ''
|
||||||
|
if not label:
|
||||||
|
continue
|
||||||
|
is_obj['rows'].append(vals)
|
||||||
|
label_lower = label.lower()
|
||||||
|
if re.search(r'название ис|наименование ис|наименование информационной',
|
||||||
|
label_lower) and not is_disabled(settings, 'is_name'):
|
||||||
|
if len(vals) > 1:
|
||||||
|
is_obj['name'] = vals[1]
|
||||||
|
if is_obj['rows']:
|
||||||
|
if not is_obj['name']:
|
||||||
|
is_obj['name'] = sn.strip()
|
||||||
|
result['is_list'].append(is_obj)
|
||||||
|
|
||||||
|
# --- Пользовательские привязки ---
|
||||||
|
for cm in custom:
|
||||||
|
excel_text = (cm.get('excel_text') or '').strip().lower()
|
||||||
|
sheet = (cm.get('sheet') or '').strip()
|
||||||
|
field = (cm.get('field') or '').strip()
|
||||||
|
if not excel_text or not field:
|
||||||
|
continue
|
||||||
|
target_sheets = [sheet] if sheet in wb.sheetnames else wb.sheetnames
|
||||||
|
for sn in target_sheets:
|
||||||
|
ws = wb[sn]
|
||||||
|
for row in ws.iter_rows(min_row=1, values_only=True):
|
||||||
|
label = clean_value(row[0]) if row and row[0] is not None else ''
|
||||||
|
if label.lower().strip() == excel_text:
|
||||||
|
value = clean_value(row[1]) if len(row) > 1 else ''
|
||||||
|
if value:
|
||||||
|
result['fields'][field] = value
|
||||||
|
break
|
||||||
|
finally:
|
||||||
|
try:
|
||||||
|
wb.close()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return result
|
||||||
@@ -0,0 +1,363 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""ImportMapper — окно редактора импорта (простая версия)
|
||||||
|
|
||||||
|
Как пользоваться:
|
||||||
|
1. Выберите тип опросника (152-ФЗ, 187-ФЗ и т.д.).
|
||||||
|
2. Укажите Excel-файл и нажмите «Сканировать».
|
||||||
|
3. Галочками включите/выключите нужные поля.
|
||||||
|
4. «Сохранить настройки» — настройки сохранятся для этого типа опросника.
|
||||||
|
5. «Выгрузить данные» — соберёт данные из Excel в JSON по настройкам.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import tkinter as tk
|
||||||
|
from tkinter import ttk, messagebox, filedialog
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
from typing import Dict, List, Optional
|
||||||
|
|
||||||
|
from .core import (
|
||||||
|
list_profiles, load_profile, load_settings, save_settings,
|
||||||
|
scan_excel, extract_data,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ImportMapperWindow:
|
||||||
|
def __init__(self, parent, excel_path: str = None):
|
||||||
|
self.parent = parent
|
||||||
|
self.excel_path = excel_path
|
||||||
|
|
||||||
|
self.profiles = list_profiles()
|
||||||
|
self.profile = self.profiles[0] if self.profiles else None
|
||||||
|
|
||||||
|
self.detected = [] # найденные по правилам профиля
|
||||||
|
self.unmapped = [] # найденные, но без привязки
|
||||||
|
self.disabled = set() # выключенные поля
|
||||||
|
self.custom = [] # добавленные пользователем привязки
|
||||||
|
|
||||||
|
self._row_map = {}
|
||||||
|
self._vars = {}
|
||||||
|
|
||||||
|
self.window = tk.Toplevel(parent)
|
||||||
|
self.window.title("Редактор импорта из Excel-опросников")
|
||||||
|
self.window.geometry("860x640")
|
||||||
|
self.window.minsize(700, 500)
|
||||||
|
self.window.transient(parent)
|
||||||
|
self.window.grab_set()
|
||||||
|
|
||||||
|
self._build_ui()
|
||||||
|
self._load_current_settings()
|
||||||
|
|
||||||
|
if excel_path:
|
||||||
|
self.file_var.set(excel_path)
|
||||||
|
self._scan()
|
||||||
|
|
||||||
|
# ---------- ПОСТРОЕНИЕ ОКНА ----------
|
||||||
|
def _build_ui(self):
|
||||||
|
top = ttk.Frame(self.window, padding=6)
|
||||||
|
top.pack(fill=tk.X)
|
||||||
|
|
||||||
|
ttk.Label(top, text="Тип опросника:").pack(side=tk.LEFT)
|
||||||
|
self.profile_var = tk.StringVar()
|
||||||
|
self.profile_combo = ttk.Combobox(
|
||||||
|
top, textvariable=self.profile_var, state='readonly', width=34,
|
||||||
|
values=[p['name'] for p in self.profiles])
|
||||||
|
self.profile_combo.pack(side=tk.LEFT, padx=4)
|
||||||
|
if self.profile:
|
||||||
|
self.profile_combo.current(0)
|
||||||
|
self.profile_combo.bind('<<ComboboxSelected>>', self._on_profile_change)
|
||||||
|
|
||||||
|
ttk.Label(top, text="Файл:").pack(side=tk.LEFT, padx=(10, 2))
|
||||||
|
self.file_var = tk.StringVar(value=self.excel_path or '')
|
||||||
|
ttk.Entry(top, textvariable=self.file_var, width=30).pack(side=tk.LEFT)
|
||||||
|
ttk.Button(top, text="Обзор", command=self._browse_file).pack(side=tk.LEFT, padx=2)
|
||||||
|
ttk.Button(top, text="Сканировать", command=self._scan).pack(side=tk.LEFT, padx=2)
|
||||||
|
|
||||||
|
# Список
|
||||||
|
canvas = tk.Canvas(self.window, highlightthickness=0)
|
||||||
|
scrollbar = ttk.Scrollbar(self.window, orient=tk.VERTICAL, command=canvas.yview)
|
||||||
|
self._list_frame = ttk.Frame(canvas)
|
||||||
|
self._list_frame.bind(
|
||||||
|
"<Configure>", lambda e: canvas.configure(scrollregion=canvas.bbox("all")))
|
||||||
|
canvas.create_window((0, 0), window=self._list_frame, anchor="nw")
|
||||||
|
canvas.configure(yscrollcommand=scrollbar.set)
|
||||||
|
canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=(6, 0), pady=4)
|
||||||
|
scrollbar.pack(side=tk.RIGHT, fill=tk.Y, pady=4)
|
||||||
|
|
||||||
|
# Низ
|
||||||
|
bottom = ttk.Frame(self.window, padding=6)
|
||||||
|
bottom.pack(fill=tk.X)
|
||||||
|
self.status_var = tk.StringVar(value="Выберите файл опросника")
|
||||||
|
ttk.Label(bottom, textvariable=self.status_var).pack(side=tk.LEFT)
|
||||||
|
ttk.Button(bottom, text="📤 Выгрузить данные",
|
||||||
|
command=self._export_data).pack(side=tk.RIGHT, padx=2)
|
||||||
|
ttk.Button(bottom, text="💾 Сохранить настройки",
|
||||||
|
command=self._save).pack(side=tk.RIGHT, padx=2)
|
||||||
|
ttk.Button(bottom, text="Закрыть",
|
||||||
|
command=self.window.destroy).pack(side=tk.RIGHT, padx=2)
|
||||||
|
|
||||||
|
# ---------- ПРОФИЛИ ----------
|
||||||
|
def _on_profile_change(self, event=None):
|
||||||
|
idx = self.profile_combo.current()
|
||||||
|
if 0 <= idx < len(self.profiles):
|
||||||
|
self.profile = self.profiles[idx]
|
||||||
|
self._load_current_settings()
|
||||||
|
if self.file_var.get().strip():
|
||||||
|
self._scan()
|
||||||
|
|
||||||
|
def _load_current_settings(self):
|
||||||
|
if not self.profile:
|
||||||
|
return
|
||||||
|
settings = load_settings(self.profile['id'])
|
||||||
|
self.disabled = set(settings.get('disabled', []))
|
||||||
|
self.custom = settings.get('custom', [])
|
||||||
|
|
||||||
|
# ---------- ФАЙЛ ----------
|
||||||
|
def _browse_file(self):
|
||||||
|
path = filedialog.askopenfilename(
|
||||||
|
title="Выберите опросный лист (Excel)",
|
||||||
|
filetypes=[("Excel", "*.xlsx *.xls")]
|
||||||
|
)
|
||||||
|
if path:
|
||||||
|
self.file_var.set(path)
|
||||||
|
self.excel_path = path
|
||||||
|
self._scan()
|
||||||
|
|
||||||
|
def _scan(self):
|
||||||
|
path = self.file_var.get().strip()
|
||||||
|
if not path or not os.path.isfile(path):
|
||||||
|
messagebox.showwarning("Предупреждение", "Укажите существующий файл опросника")
|
||||||
|
return
|
||||||
|
if not self.profile:
|
||||||
|
messagebox.showwarning("Предупреждение", "Не выбран тип опросника")
|
||||||
|
return
|
||||||
|
|
||||||
|
self.status_var.set("Сканирование…")
|
||||||
|
self.window.update()
|
||||||
|
|
||||||
|
detected, unmapped, error = scan_excel(path, self.profile)
|
||||||
|
if error:
|
||||||
|
messagebox.showerror("Ошибка", error)
|
||||||
|
self.status_var.set(f"Ошибка: {error}")
|
||||||
|
return
|
||||||
|
|
||||||
|
self.detected = detected
|
||||||
|
self.unmapped = unmapped
|
||||||
|
self._render()
|
||||||
|
|
||||||
|
# ---------- ОТРИСОВКА ----------
|
||||||
|
def _render(self):
|
||||||
|
for w in self._list_frame.winfo_children():
|
||||||
|
w.destroy()
|
||||||
|
self._row_map.clear()
|
||||||
|
self._vars.clear()
|
||||||
|
|
||||||
|
custom_keys = {(c['excel_text'], c['sheet']) for c in self.custom}
|
||||||
|
|
||||||
|
ttk.Label(self._list_frame,
|
||||||
|
text="Импортируется из опросника (снимите галочку, чтобы отключить)",
|
||||||
|
font=('', 10, 'bold')).pack(anchor=tk.W, pady=(4, 2))
|
||||||
|
|
||||||
|
rows = []
|
||||||
|
for d in self.detected:
|
||||||
|
rows.append(('detected', d))
|
||||||
|
detected_keys = {(d['excel_text'], d['sheet']) for d in self.detected}
|
||||||
|
for c in self.custom:
|
||||||
|
if (c['excel_text'], c['sheet']) not in detected_keys:
|
||||||
|
rows.append(('custom', c))
|
||||||
|
|
||||||
|
if not rows:
|
||||||
|
ttk.Label(self._list_frame, text="— ничего не найдено —",
|
||||||
|
foreground='#888').pack(anchor=tk.W, padx=24)
|
||||||
|
for kind, info in rows:
|
||||||
|
self._add_row(kind, info)
|
||||||
|
|
||||||
|
ttk.Label(self._list_frame,
|
||||||
|
text="Найдено в файле, но не импортируется (отметьте галочкой, чтобы добавить)",
|
||||||
|
font=('', 10, 'bold')).pack(anchor=tk.W, pady=(12, 2))
|
||||||
|
|
||||||
|
addable = [u for u in self.unmapped
|
||||||
|
if (u['excel_text'], u['sheet']) not in custom_keys]
|
||||||
|
if not addable:
|
||||||
|
ttk.Label(self._list_frame, text="— таких строк нет —",
|
||||||
|
foreground='#888').pack(anchor=tk.W, padx=24)
|
||||||
|
for u in addable:
|
||||||
|
self._add_row('unmapped', u)
|
||||||
|
|
||||||
|
imported = sum(1 for kind, info in rows
|
||||||
|
if (kind == 'detected' and info['field'] not in self.disabled)
|
||||||
|
or kind == 'custom')
|
||||||
|
self.status_var.set(
|
||||||
|
f"Импортируется полей: {imported} | Отключено: {len(self.disabled)} | "
|
||||||
|
f"Можно добавить: {len(addable)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
def _add_row(self, kind: str, info: dict):
|
||||||
|
row_id = f"{kind}_{len(self._row_map)}"
|
||||||
|
row = ttk.Frame(self._list_frame)
|
||||||
|
row.pack(fill=tk.X, pady=1)
|
||||||
|
|
||||||
|
if kind == 'detected':
|
||||||
|
checked = info['field'] not in self.disabled
|
||||||
|
label = info['excel_text']
|
||||||
|
desc = info['desc']
|
||||||
|
sheet = info['sheet']
|
||||||
|
elif kind == 'custom':
|
||||||
|
checked = True
|
||||||
|
label = info['excel_text']
|
||||||
|
desc = info.get('desc') or info['field']
|
||||||
|
sheet = info['sheet']
|
||||||
|
else: # unmapped
|
||||||
|
checked = False
|
||||||
|
label = info['excel_text']
|
||||||
|
suggested = info['suggested']
|
||||||
|
desc = suggested if suggested and not suggested.startswith('unknown_') else '—'
|
||||||
|
sheet = info['sheet']
|
||||||
|
|
||||||
|
var = tk.BooleanVar(value=checked)
|
||||||
|
self._vars[row_id] = var
|
||||||
|
self._row_map[row_id] = {'kind': kind, 'info': info}
|
||||||
|
|
||||||
|
ttk.Checkbutton(row, variable=var,
|
||||||
|
command=lambda rid=row_id: self._toggle(rid)).pack(side=tk.LEFT)
|
||||||
|
|
||||||
|
label_text = label[:45] + ('…' if len(label) > 45 else '')
|
||||||
|
ttk.Label(row, text=f"«{label_text}»", width=48, anchor=tk.W).pack(
|
||||||
|
side=tk.LEFT, padx=(2, 6))
|
||||||
|
ttk.Label(row, text=f"→ {desc}", anchor=tk.W,
|
||||||
|
foreground='#1a7a3c' if checked else '#888').pack(
|
||||||
|
side=tk.LEFT, fill=tk.X, expand=True)
|
||||||
|
ttk.Label(row, text=f"(лист: {sheet[:20]})", foreground='#999',
|
||||||
|
font=('', 8)).pack(side=tk.RIGHT, padx=4)
|
||||||
|
|
||||||
|
# ---------- ПЕРЕКЛЮЧЕНИЕ ГАЛОЧЕК ----------
|
||||||
|
def _toggle(self, row_id: str):
|
||||||
|
info = self._row_map.get(row_id)
|
||||||
|
if not info:
|
||||||
|
return
|
||||||
|
kind = info['kind']
|
||||||
|
data = info['info']
|
||||||
|
checked = self._vars[row_id].get()
|
||||||
|
|
||||||
|
if kind == 'detected':
|
||||||
|
field = data['field']
|
||||||
|
if checked:
|
||||||
|
self.disabled.discard(field)
|
||||||
|
else:
|
||||||
|
self.disabled.add(field)
|
||||||
|
|
||||||
|
elif kind == 'custom':
|
||||||
|
if not checked:
|
||||||
|
self.custom = [c for c in self.custom
|
||||||
|
if not (c['excel_text'] == data['excel_text']
|
||||||
|
and c['sheet'] == data['sheet'])]
|
||||||
|
|
||||||
|
elif kind == 'unmapped':
|
||||||
|
if checked:
|
||||||
|
field = data['suggested']
|
||||||
|
if not field or field.startswith('unknown_'):
|
||||||
|
field = self._ask_field()
|
||||||
|
if not field:
|
||||||
|
self._vars[row_id].set(False)
|
||||||
|
return
|
||||||
|
self.custom.append({
|
||||||
|
'excel_text': data['excel_text'],
|
||||||
|
'sheet': data['sheet'],
|
||||||
|
'field': field,
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
self.custom = [c for c in self.custom
|
||||||
|
if not (c['excel_text'] == data['excel_text']
|
||||||
|
and c['sheet'] == data['sheet'])]
|
||||||
|
|
||||||
|
self._render()
|
||||||
|
|
||||||
|
def _ask_field(self) -> Optional[str]:
|
||||||
|
"""Простой выбор: куда импортировать найденную строку."""
|
||||||
|
dialog = tk.Toplevel(self.window)
|
||||||
|
dialog.title("Куда импортировать?")
|
||||||
|
dialog.geometry("480x200")
|
||||||
|
dialog.transient(self.window)
|
||||||
|
dialog.grab_set()
|
||||||
|
|
||||||
|
ttk.Label(dialog, text="Выберите, куда подставить это значение:").pack(pady=(12, 4))
|
||||||
|
|
||||||
|
choices = []
|
||||||
|
field_by_desc = {}
|
||||||
|
for rule in (self.profile or {}).get('rules', []):
|
||||||
|
desc = rule.get('desc') or rule.get('field')
|
||||||
|
if desc not in field_by_desc:
|
||||||
|
field_by_desc[desc] = rule.get('field')
|
||||||
|
choices.append(desc)
|
||||||
|
|
||||||
|
var = tk.StringVar()
|
||||||
|
combo = ttk.Combobox(dialog, textvariable=var, values=choices,
|
||||||
|
state='readonly', width=52)
|
||||||
|
combo.pack(padx=10, pady=4)
|
||||||
|
if choices:
|
||||||
|
combo.current(0)
|
||||||
|
combo.focus()
|
||||||
|
|
||||||
|
result = []
|
||||||
|
|
||||||
|
def _ok():
|
||||||
|
result.append(var.get())
|
||||||
|
dialog.destroy()
|
||||||
|
|
||||||
|
ttk.Button(dialog, text="OK", command=_ok).pack(side=tk.LEFT, padx=20, pady=10)
|
||||||
|
ttk.Button(dialog, text="Отмена", command=dialog.destroy).pack(
|
||||||
|
side=tk.RIGHT, padx=20, pady=10)
|
||||||
|
|
||||||
|
dialog.wait_window()
|
||||||
|
if not result:
|
||||||
|
return None
|
||||||
|
return field_by_desc.get(result[0])
|
||||||
|
|
||||||
|
# ---------- СОХРАНЕНИЕ И ВЫГРУЗКА ----------
|
||||||
|
def _save(self):
|
||||||
|
if not self.profile:
|
||||||
|
return
|
||||||
|
settings = {
|
||||||
|
'disabled': sorted(self.disabled),
|
||||||
|
'custom': self.custom,
|
||||||
|
}
|
||||||
|
save_settings(self.profile['id'], settings)
|
||||||
|
self.status_var.set("✅ Настройки сохранены")
|
||||||
|
messagebox.showinfo(
|
||||||
|
"Готово",
|
||||||
|
f"Настройки импорта для «{self.profile['name']}» сохранены.\n\n"
|
||||||
|
f"Файл: settings/settings_{self.profile['id']}.json"
|
||||||
|
)
|
||||||
|
|
||||||
|
def _export_data(self):
|
||||||
|
path = self.file_var.get().strip()
|
||||||
|
if not path or not os.path.isfile(path):
|
||||||
|
messagebox.showwarning("Предупреждение", "Сначала укажите файл опросника")
|
||||||
|
return
|
||||||
|
if not self.profile:
|
||||||
|
return
|
||||||
|
|
||||||
|
settings = {
|
||||||
|
'disabled': sorted(self.disabled),
|
||||||
|
'custom': self.custom,
|
||||||
|
}
|
||||||
|
data = extract_data(path, self.profile, settings)
|
||||||
|
if 'error' in data:
|
||||||
|
messagebox.showerror("Ошибка", data['error'])
|
||||||
|
return
|
||||||
|
|
||||||
|
from .core import EXPORT_DIR
|
||||||
|
os.makedirs(EXPORT_DIR, exist_ok=True)
|
||||||
|
out_path = os.path.join(EXPORT_DIR, f"export_{self.profile['id']}.json")
|
||||||
|
with open(out_path, 'w', encoding='utf-8') as f:
|
||||||
|
json.dump(data, f, ensure_ascii=False, indent=2)
|
||||||
|
|
||||||
|
n_fields = len(data.get('fields', {}))
|
||||||
|
n_sections = sum(len(v) for v in data.get('sections', {}).values())
|
||||||
|
n_is = len(data.get('is_list', []))
|
||||||
|
self.status_var.set(
|
||||||
|
f"✅ Выгружено: полей {n_fields}, строк секций {n_sections}, ИС {n_is}")
|
||||||
|
messagebox.showinfo(
|
||||||
|
"Готово",
|
||||||
|
f"Данные выгружены в:\n{out_path}\n\n"
|
||||||
|
f"Поля: {n_fields} | Строки секций: {n_sections} | ИС: {n_is}"
|
||||||
|
)
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""ImportMapper — универсальный редактор импорта из Excel-опросников.
|
||||||
|
|
||||||
|
Запуск:
|
||||||
|
python3 main.py
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
|
||||||
|
import tkinter as tk
|
||||||
|
from editor import ImportMapperWindow
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
root = tk.Tk()
|
||||||
|
root.title("ImportMapper — редактор импорта из опросников")
|
||||||
|
root.geometry("300x120")
|
||||||
|
root.withdraw() # главное окно скрыто, работает дочернее
|
||||||
|
|
||||||
|
ImportMapperWindow(root)
|
||||||
|
|
||||||
|
root.mainloop()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"id": "117",
|
||||||
|
"name": "117-ФЗ (безопасность объектов КИИ / ГИС)",
|
||||||
|
"common_sheet": "общие",
|
||||||
|
"employee_sheet": "сотруд",
|
||||||
|
"is_enabled": false,
|
||||||
|
"rules": [
|
||||||
|
{"pattern": "полное наименование организации", "field": "fullName", "desc": "Полное наименование организации"},
|
||||||
|
{"pattern": "сокращенное наименование организации", "field": "shortName", "desc": "Сокращённое наименование"},
|
||||||
|
{"pattern": "инн", "field": "inn", "desc": "ИНН"},
|
||||||
|
{"pattern": "кпп", "field": "kpp", "desc": "КПП"},
|
||||||
|
{"pattern": "огрн", "field": "ogrn", "desc": "ОГРН"},
|
||||||
|
{"pattern": "адрес", "field": "addressLegal", "desc": "Адрес организации"},
|
||||||
|
{"pattern": "должность руководителя", "field": "chiefPosition", "desc": "Должность руководителя"},
|
||||||
|
{"pattern": "фио руководителя", "field": "chiefFio", "desc": "ФИО руководителя"}
|
||||||
|
],
|
||||||
|
"sections": {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
{
|
||||||
|
"id": "152",
|
||||||
|
"name": "152-ФЗ (персональные данные)",
|
||||||
|
"common_sheet": "общие",
|
||||||
|
"employee_sheet": "сотруд",
|
||||||
|
"is_enabled": true,
|
||||||
|
"rules": [
|
||||||
|
{"pattern": "полное наименование организации", "field": "fullName", "desc": "Полное наименование организации"},
|
||||||
|
{"pattern": "сокращенное наименование организации", "field": "shortName", "desc": "Сокращённое наименование"},
|
||||||
|
{"pattern": "адрес организации \\(юридический\\)", "field": "addressLegal", "desc": "Юридический адрес"},
|
||||||
|
{"pattern": "адрес организации \\(фактический\\)", "field": "addressActual", "desc": "Фактический адрес"},
|
||||||
|
{"pattern": "инн", "field": "inn", "desc": "ИНН"},
|
||||||
|
{"pattern": "дата.*огрн", "field": "ogrnDate", "desc": "Дата ОГРН"},
|
||||||
|
{"pattern": "огрн", "field": "ogrn", "desc": "ОГРН"},
|
||||||
|
{"pattern": "кпп", "field": "kpp", "desc": "КПП"},
|
||||||
|
{"pattern": "основной оквэд", "field": "okved", "desc": "Основной ОКВЭД"},
|
||||||
|
{"pattern": "должность руководителя", "field": "chiefPosition", "desc": "Должность руководителя"},
|
||||||
|
{"pattern": "фио руководителя", "field": "chiefFio", "desc": "ФИО руководителя"},
|
||||||
|
{"pattern": "должность.*защит.*информ|должность.*администрат", "field": "ispdnPosition", "desc": "Должность ответственного за ПДн"},
|
||||||
|
{"pattern": "фио.*защит.*информ|фио.*администрат", "field": "ispdnFio", "desc": "ФИО ответственного за ПДн"},
|
||||||
|
{"pattern": "электронная почта.*защит|электронная почта.*ответств|электронная почта.*специалист", "field": "ispdnEmail", "desc": "Email ответственного за ПДн"},
|
||||||
|
{"pattern": "телефон.*защит|телефон.*ответств|телефон.*специалист", "field": "ispdnPhone", "desc": "Телефон ответственного за ПДн"},
|
||||||
|
{"pattern": "структур.*подраздел", "field": "ispdnDepartment", "desc": "Подразделение ответственного за ПДн"},
|
||||||
|
{"pattern": "номер.*договора|номер.*контракта", "field": "contractNumber", "desc": "Номер договора"},
|
||||||
|
{"pattern": "дата.*договора|дата.*контракта", "field": "contractDate", "desc": "Дата договора"},
|
||||||
|
{"pattern": "названи.*документ", "field": "paperDocuments", "desc": "Перечень бумажных документов"},
|
||||||
|
{"pattern": "место.*хранен", "field": "paperStorage", "desc": "Место хранения документов"},
|
||||||
|
{"pattern": "должность.*ответственн", "field": "responsiblePosition", "desc": "Должность ответственного за обработку ПДн"},
|
||||||
|
{"pattern": "фио.*ответственн", "field": "responsibleFio", "desc": "ФИО ответственного за обработку ПДн"},
|
||||||
|
{"pattern": "сайт организации", "field": "site_name", "desc": "Сайт организации"},
|
||||||
|
{"pattern": "электронная почта", "field": "email", "desc": "Электронная почта"},
|
||||||
|
{"pattern": "телефон", "field": "phone", "desc": "Телефон"},
|
||||||
|
{"pattern": "название ис|наименование ис|наименование информационной", "field": "is_name", "desc": "Название информационной системы"},
|
||||||
|
{"pattern": "цель обработки", "field": "is_purpose", "desc": "Цель обработки ПДн"},
|
||||||
|
{"pattern": "сотрудник", "field": "is_employee", "desc": "Сотрудники с доступом к ИС"},
|
||||||
|
{"pattern": "должность сотрудника", "field": "is_position", "desc": "Должность сотрудника"},
|
||||||
|
{"pattern": "номер кабинета", "field": "is_room", "desc": "Номер кабинета"},
|
||||||
|
{"pattern": "количество записей|количество субъект", "field": "is_personalDataCount", "desc": "Количество записей ПДн"},
|
||||||
|
{"pattern": "перечень пд", "field": "is_personalDataList", "desc": "Перечень ПДн"},
|
||||||
|
{"pattern": "принадлеж", "field": "is_pd_subjects_list", "desc": "Категории субъектов ПДн"},
|
||||||
|
{"pattern": "перечень действий", "field": "is_pd_actions", "desc": "Перечень действий с ПДн"},
|
||||||
|
{"pattern": "способ обработки", "field": "is_processing_mode", "desc": "Способ обработки ПДн"},
|
||||||
|
{"pattern": "интернет", "field": "is_isInternet", "desc": "Использование интернета"},
|
||||||
|
{"pattern": "защита", "field": "is_defense_tools", "desc": "Средства защиты информации"},
|
||||||
|
{"pattern": "состав комиссии", "field": "commission", "desc": "Состав комиссии"},
|
||||||
|
{"pattern": "перечень сотрудников", "field": "employeesAccess", "desc": "Сотрудники с доступом к ПДн"}
|
||||||
|
],
|
||||||
|
"sections": {
|
||||||
|
"commission": {"header": "состав комиссии"},
|
||||||
|
"employeesAccess": {"header": "перечень сотрудников"}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"id": "187",
|
||||||
|
"name": "187-ФЗ (КИИ — критическая информационная инфраструктура)",
|
||||||
|
"common_sheet": "общие",
|
||||||
|
"employee_sheet": "сотруд",
|
||||||
|
"is_enabled": false,
|
||||||
|
"rules": [
|
||||||
|
{"pattern": "полное наименование организации", "field": "fullName", "desc": "Полное наименование организации"},
|
||||||
|
{"pattern": "сокращенное наименование организации", "field": "shortName", "desc": "Сокращённое наименование"},
|
||||||
|
{"pattern": "инн", "field": "inn", "desc": "ИНН"},
|
||||||
|
{"pattern": "кпп", "field": "kpp", "desc": "КПП"},
|
||||||
|
{"pattern": "огрн", "field": "ogrn", "desc": "ОГРН"},
|
||||||
|
{"pattern": "адрес", "field": "addressLegal", "desc": "Адрес организации"},
|
||||||
|
{"pattern": "должность руководителя", "field": "chiefPosition", "desc": "Должность руководителя"},
|
||||||
|
{"pattern": "фио руководителя", "field": "chiefFio", "desc": "ФИО руководителя"},
|
||||||
|
{"pattern": "электронная почта", "field": "email", "desc": "Электронная почта"},
|
||||||
|
{"pattern": "телефон", "field": "phone", "desc": "Телефон"}
|
||||||
|
],
|
||||||
|
"sections": {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"id": "kzi",
|
||||||
|
"name": "КЗИ (комплекс средств защиты информации)",
|
||||||
|
"common_sheet": "общие",
|
||||||
|
"employee_sheet": "сотруд",
|
||||||
|
"is_enabled": false,
|
||||||
|
"rules": [
|
||||||
|
{"pattern": "полное наименование организации", "field": "fullName", "desc": "Полное наименование организации"},
|
||||||
|
{"pattern": "сокращенное наименование организации", "field": "shortName", "desc": "Сокращённое наименование"},
|
||||||
|
{"pattern": "инн", "field": "inn", "desc": "ИНН"},
|
||||||
|
{"pattern": "кпп", "field": "kpp", "desc": "КПП"},
|
||||||
|
{"pattern": "огрн", "field": "ogrn", "desc": "ОГРН"},
|
||||||
|
{"pattern": "адрес", "field": "addressLegal", "desc": "Адрес организации"},
|
||||||
|
{"pattern": "должность руководителя", "field": "chiefPosition", "desc": "Должность руководителя"},
|
||||||
|
{"pattern": "фио руководителя", "field": "chiefFio", "desc": "ФИО руководителя"}
|
||||||
|
],
|
||||||
|
"sections": {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"id": "mu",
|
||||||
|
"name": "МУ (методические указания)",
|
||||||
|
"common_sheet": "общие",
|
||||||
|
"employee_sheet": "сотруд",
|
||||||
|
"is_enabled": false,
|
||||||
|
"rules": [
|
||||||
|
{"pattern": "полное наименование организации", "field": "fullName", "desc": "Полное наименование организации"},
|
||||||
|
{"pattern": "сокращенное наименование организации", "field": "shortName", "desc": "Сокращённое наименование"},
|
||||||
|
{"pattern": "инн", "field": "inn", "desc": "ИНН"},
|
||||||
|
{"pattern": "кпп", "field": "kpp", "desc": "КПП"},
|
||||||
|
{"pattern": "огрн", "field": "ogrn", "desc": "ОГРН"},
|
||||||
|
{"pattern": "адрес", "field": "addressLegal", "desc": "Адрес организации"},
|
||||||
|
{"pattern": "должность руководителя", "field": "chiefPosition", "desc": "Должность руководителя"},
|
||||||
|
{"pattern": "фио руководителя", "field": "chiefFio", "desc": "ФИО руководителя"}
|
||||||
|
],
|
||||||
|
"sections": {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"id": "pzi",
|
||||||
|
"name": "ПЗИ (программные средства защиты информации)",
|
||||||
|
"common_sheet": "общие",
|
||||||
|
"employee_sheet": "сотруд",
|
||||||
|
"is_enabled": false,
|
||||||
|
"rules": [
|
||||||
|
{"pattern": "полное наименование организации", "field": "fullName", "desc": "Полное наименование организации"},
|
||||||
|
{"pattern": "сокращенное наименование организации", "field": "shortName", "desc": "Сокращённое наименование"},
|
||||||
|
{"pattern": "инн", "field": "inn", "desc": "ИНН"},
|
||||||
|
{"pattern": "кпп", "field": "kpp", "desc": "КПП"},
|
||||||
|
{"pattern": "огрн", "field": "ogrn", "desc": "ОГРН"},
|
||||||
|
{"pattern": "адрес", "field": "addressLegal", "desc": "Адрес организации"},
|
||||||
|
{"pattern": "должность руководителя", "field": "chiefPosition", "desc": "Должность руководителя"},
|
||||||
|
{"pattern": "фио руководителя", "field": "chiefFio", "desc": "ФИО руководителя"}
|
||||||
|
],
|
||||||
|
"sections": {}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
openpyxl>=3.0
|
||||||
Reference in New Issue
Block a user