diff --git a/dokogen/data/Опросный_лист_187_ФЗ.xlsx b/dokogen/data/Опросный_лист_187_ФЗ.xlsx index 076acb0..82df38d 100644 Binary files a/dokogen/data/Опросный_лист_187_ФЗ.xlsx and b/dokogen/data/Опросный_лист_187_ФЗ.xlsx differ diff --git a/dokogen/data/Опросный_лист_187_ФЗ_раздел9.xlsx b/dokogen/data/Опросный_лист_187_ФЗ_раздел9.xlsx new file mode 100644 index 0000000..eab43c4 Binary files /dev/null and b/dokogen/data/Опросный_лист_187_ФЗ_раздел9.xlsx differ diff --git a/dokogen/import_settings.json b/dokogen/import_settings.json index 34886a6..76b3902 100644 --- a/dokogen/import_settings.json +++ b/dokogen/import_settings.json @@ -140,16 +140,16 @@ "keywords": "право владения зданием" }, { - "name": "Сумма прибыли за год", + "name": "Прибыль организации за прошлый год", "var": "profit", "cell": "", - "keywords": "сумма прибыли" + "keywords": "сумма прибыли|прибыль организации|прибыль за прошлый год" }, { - "name": "Сумма налога с прибыли", + "name": "Налог на прибыль за прошлый год", "var": "profitTax", "cell": "", - "keywords": "сумма налога с прибыли" + "keywords": "сумма налога с прибыли|налог на прибыль" }, { "name": "Доменное имя и внешний IP", @@ -318,6 +318,12 @@ "var": "physicalVisitorLog", "cell": "", "keywords": "журнал учета посетителей|журнал учёта посетителей" + }, + { + "name": "Суммарный доход (выручка) за 5 лет", + "var": "income_5y", + "cell": "", + "keywords": "суммарный доход|доход.*5 лет|выручка.*5 лет" } ], "criterion_fields": [ @@ -441,5 +447,155 @@ "keywords": "кооперации головного исполнителя|статусу в кооперации", "unit": "вариант" } - ] + ], + "sphere_fields": { + "Здравоохранение": [ + { + "keywords": "специализированную или скорую", + "key": "health_emergency" + }, + { + "keywords": "аптечная сеть с выручкой от 5 млрд", + "key": "health_pharmacy" + }, + { + "keywords": "штат от 250", + "key": "health_staff" + }, + { + "keywords": "времени нужно на устранение последствий сбоя", + "key": "8_t" + } + ], + "Наука": [ + { + "keywords": "проводите ли нир", + "key": "science_niokr" + }, + { + "keywords": "вредные вещества при исследованиях", + "key": "science_hazard" + }, + { + "keywords": "международным договорам", + "key": "7" + }, + { + "keywords": "рамках гоз", + "key": "science_goz" + } + ], + "Связь": [ + { + "keywords": "абонентам станут недоступны", + "key": "4а" + }, + { + "keywords": "органа власти вы обеспечиваете", + "key": "4б" + } + ], + "Транспорт": [ + { + "keywords": "территории нарушится сообщение", + "key": "3а" + }, + { + "keywords": "без транспортных услуг", + "key": "3б" + }, + { + "keywords": "перевозки выполняете", + "key": "3в" + }, + { + "keywords": "категория у объекта транспортной", + "key": "3г" + }, + { + "keywords": "времени нужно на устранение последствий сбоя", + "key": "8_t" + }, + { + "keywords": "полностью недоступен", + "key": "hours_down" + } + ], + "Банковская сфера и финансовый рынок": [ + { + "keywords": "типу относится организация", + "key": "10_type" + }, + { + "keywords": "объём активов или операций", + "key": "10_value" + } + ], + "Топливно-энергетический комплекс": [ + { + "keywords": "масштабе возможны последствия", + "key": "2а" + }, + { + "keywords": "человек могут пострадать", + "key": "1" + }, + { + "keywords": "вредное воздействие на окружающую среду", + "key": "11б" + }, + { + "keywords": "выполняете ли гоз", + "key": "goz_flag" + } + ], + "Оборонная промышленность": [ + { + "keywords": "компенсации пострадавшим", + "key": "8_a" + }, + { + "keywords": "компьютерные инциденты", + "key": "8_x" + }, + { + "keywords": "может выпустить максимум", + "key": "13a_vmax" + }, + { + "keywords": "успеет выпустить объект, пока устраняют", + "key": "13a_vinc" + }, + { + "keywords": "полный объём продукции требуется", + "key": "13a_vgoz" + }, + { + "keywords": "выпуск единицы продукции в обычном режиме", + "key": "13b_tnorm" + }, + { + "keywords": "выпуск единицы продукции при сбое", + "key": "13b_tnonorm" + }, + { + "keywords": "декларация промышленной безопасности", + "key": "8_decl" + }, + { + "keywords": "перечень стратегических предприятий", + "key": "8_strategic" + } + ] + }, + "sphere_headers": { + "здравоохранен": "Здравоохранение", + "наука": "Наука", + "связи": "Связь", + "транспорта": "Транспорт", + "банковская": "Банковская сфера и финансовый рынок", + "тэк": "Топливно-энергетический комплекс", + "энергетик": "Топливно-энергетический комплекс", + "опк": "Оборонная промышленность" + } } \ No newline at end of file diff --git a/dokogen/importer.py b/dokogen/importer.py index 29db06b..02fa2ad 100644 --- a/dokogen/importer.py +++ b/dokogen/importer.py @@ -168,6 +168,9 @@ def import_187fz(filepath: str, log_fn: Optional[Callable] = None, settings_path _import_187_common(wb, data, settings, log_fn) _import_187_is_list(wb, data, settings, log_fn) _import_187_employees(wb, data, settings, log_fn) + # Раздел 9 «Значения для расчета критериев значимости» — после импорта объектов, + # чтобы сферные значения влились в criterion_input уже созданных объектов. + _import_187_criteria9(wb, data, settings, log_fn) except Exception as e: log_fn(f"❌ Ошибка при импорте: {e}") log_fn(traceback.format_exc()) @@ -290,16 +293,105 @@ def _import_187_common(wb, data, settings, log_fn): if rx: for field_lower, val in row_cache.items(): if rx.search(field_lower): - if val: - if key in ('chiefFio', 'responsibleFio', 'ispdnFio', 'adminFio'): - val = _extract_single_fio(val) - elif 'дата' in name.lower() or 'date' in key.lower(): - val = clean_value(val, as_date=True) - data[key] = val - log_fn(f" {name} [по ключу]: {val}") + if not val: + # Пустое значение в первой строке-совпадении — ищем дальше + # (старый вопрос пуст, а новый раздел 9 заполнен). + continue + if key in ('chiefFio', 'responsibleFio', 'ispdnFio', 'adminFio'): + val = _extract_single_fio(val) + elif 'дата' in name.lower() or 'date' in key.lower(): + val = clean_value(val, as_date=True) + data[key] = val + log_fn(f" {name} [по ключу]: {val}") break +def _import_187_criteria9(wb, data, settings, log_fn): + """Импорт раздела 9 «Значения для расчета критериев значимости» (лист «Общие сведения»). + + Общие для всех сфер поля (суммарный доход за 5 лет, прибыль, налог на прибыль) + подхватываются штатным поиском по keywords (fields). Здесь собираются сферные блоки: + для каждого блока «СФЕРА …» значения вопросов сохраняются в + data['sphereInput'][сфера][ключ] = значение, где ключ — из settings['sphere_fields']. + При импорте объектов значения вливаются в criterion_input объекта с той же сферой. + """ + sphere_fields = settings.get('sphere_fields', {}) or {} + sphere_headers = settings.get('sphere_headers', {}) or {} + if not sphere_fields: + return + + sheet_name = None + sheet_kw = settings.get('common_sheet_keyword', 'общие') + for sn in wb.sheetnames: + if sheet_kw in sn.lower(): + sheet_name = sn + break + if not sheet_name: + return + ws = wb[sheet_name] + value_col = int(settings.get('value_column', 1)) + + sphere_input = {} + current_sphere = None + started = False + 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 '' + if not field_raw: + continue + field_lower = field_raw.strip().lower() + # начало раздела 9 + if '9. значения' in field_lower or 'значения для расчета критериев значимости' in field_lower: + started = True + continue + if not started: + continue + # заголовок «ОБЩИЕ для всех сфер» — общие поля уже через fields, сферу сбрасываем + if 'общие для всех сфер' in field_lower: + current_sphere = None + continue + # заголовок сферы: «СФЕРА ЗДРАВООХРАНЕНИЯ», «БАНКОВСКАЯ СФЕРА» + if 'сфера' in field_lower or 'банковская' in field_lower: + matched = None + for kw, canon in sphere_headers.items(): + if kw in field_lower: + matched = canon + break + if matched: + current_sphere = matched + sphere_input.setdefault(current_sphere, {}) + continue + if current_sphere is None: + continue + val = clean_value(row[value_col]) if len(row) > value_col else '' + if not val: + continue + for rule in sphere_fields.get(current_sphere, []): + kws = rule.get('keywords', '') + if not kws: + continue + try: + rx = re.compile(kws, re.IGNORECASE) + except re.error: + continue + if rx.search(field_lower): + key = rule.get('key', '') + sphere_input[current_sphere][key] = str(val).strip() + log_fn(f" [сфера {current_sphere}] {key}: {str(val)[:60]}") + break + + if sphere_input: + data['sphereInput'] = sphere_input + + # Вливаем сферные значения в criterion_input объектов с той же сферой + for is_obj in data.get('informationSystems', []): + obj_sphere = (is_obj.get('sphere') or '').strip() + if not obj_sphere or obj_sphere not in sphere_input: + continue + ci = is_obj.setdefault('criterion_input', {}) + for k, v in sphere_input[obj_sphere].items(): + ci.setdefault(k, v) + + def _import_187_is_list(wb, data, settings, log_fn): """Импорт объектов КИИ из остальных листов (ОБЪЕКТ №1, №2, ...).