# -*- 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('Должность администратора безопасности', 'administratorPosition', keywords=r'должность.*администрат.*(?:безопас|защит|пд)'), _f('ФИО администратора безопасности', 'administratorFio', 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('Сайт организации', '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 ''