diff --git a/dokogen/__init__.py b/dokogen/__init__.py index 1244144..e4be17e 100644 --- a/dokogen/__init__.py +++ b/dokogen/__init__.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- """DokoGen — Пакет генератора документов 187-ФЗ""" -from .models import Company, CommissionMember, InformationSystem, PaperDocument +from .models import Company, CommissionMember, InformationSystem from .declension import inflect_name, decline, company_name_decline, get_short_fio, get_initials from .variables import build_replacements, VARIABLE_DEFS from .generator import process_template diff --git a/dokogen/dictionaries/okved.json b/dokogen/dictionaries/okved.json new file mode 100644 index 0000000..545b4ee --- /dev/null +++ b/dokogen/dictionaries/okved.json @@ -0,0 +1,268 @@ +[ + "01.1 Выращивание однолетних культур", + "01.2 Выращивание многолетних культур", + "01.3 Выращивание рассады", + "01.4 Животноводство", + "01.5 Смешанное сельское хозяйство", + "01.6 Деятельность вспомогательная в области производства сельскохозяйственных культур и послеуборочной обработки сельхозпродукции", + "01.7 Охота, отлов и отстрел диких животных", + "01.8 Лесоводство и прочая деятельность в области лесозаготовок", + "02.1 Лесоводство и прочая деятельность в области лесозаготовок", + "02.2 Предоставление услуг в области лесоводства и лесозаготовок", + "02.3 Сбор и заготовка пищевых лесных ресурсов, недревесных лесных ресурсов и лекарственных растений", + "03.1 Рыболовство", + "03.2 Рыбоводство", + "05.1 Добыча угля", + "05.2 Добыча и обогащение угля", + "06.1 Добыча сырой нефти и природного газа", + "06.2 Добыча природного газа и газового конденсата", + "07.1 Добыча железных руд", + "07.2 Добыча руд цветных металлов", + "08.1 Добыча камня, песка и глины", + "08.9 Добыча полезных ископаемых, не включенных в другие группировки", + "09.1 Деятельность вспомогательная в сфере добычи нефти и природного газа", + "10.1 Переработка и консервирование мяса и мясной пищевой продукции", + "10.2 Переработка и консервирование рыбы, ракообразных и моллюсков", + "10.3 Переработка и консервирование фруктов и овощей", + "10.4 Производство растительных и животных масел и жиров", + "10.5 Производство молочной продукции", + "10.6 Производство мукомольной и крупяной промышленности, крахмала и крахмалосодержащих продуктов", + "10.7 Производство хлебобулочных и мучных кондитерских изделий", + "10.8 Производство прочих пищевых продуктов", + "10.9 Производство готовых кормов для животных", + "11.0 Производство напитков", + "12.0 Производство табачных изделий", + "13.1 Прядение текстильных волокон", + "13.2 Ткацкое производство", + "13.3 Отделка тканей и текстильных изделий", + "13.9 Производство прочих текстильных изделий", + "14.1 Производство одежды, кроме одежды из меха", + "14.2 Производство меховых изделий", + "14.3 Производство вязаных и трикотажных изделий одежды", + "15.1 Дубление и отделка кожи, производство чемоданов, сумок и т.п.", + "15.2 Производство обуви", + "16.1 Распиловка и строгание древесины", + "16.2 Производство изделий из дерева, пробки, соломки", + "17.1 Производство целлюлозы, древесной массы, бумаги и картона", + "17.2 Производство изделий из бумаги и картона", + "18.1 Деятельность полиграфическая и предоставление услуг в этой области", + "18.2 Копирование записанных носителей информации", + "19.1 Производство кокса", + "19.2 Производство нефтепродуктов", + "20.1 Производство основных химических веществ, удобрений, пластмасс и синтетического каучука", + "20.2 Производство пестицидов и прочей агрохимической продукции", + "20.3 Производство красок, лаков и аналогичных покрытий", + "20.4 Производство мыла и моющих, чистящих и полирующих средств", + "20.5 Производство прочих химических продуктов", + "21.1 Производство фармацевтических субстанций", + "21.2 Производство лекарственных препаратов и материалов", + "22.1 Производство резиновых изделий", + "22.2 Производство пластмассовых изделий", + "23.1 Производство стекла и изделий из стекла", + "23.2 Производство огнеупорных изделий", + "23.3 Производство строительных керамических материалов", + "23.4 Производство прочих фарфоровых и керамических изделий", + "23.5 Производство цемента, извести и гипса", + "23.6 Производство изделий из бетона, цемента и гипса", + "23.7 Резка, обработка и отделка камня", + "23.9 Производство абразивных и неметаллических минеральных изделий", + "24.1 Производство чугуна, стали и ферросплавов", + "24.2 Производство стальных труб, полых профилей и фитингов", + "24.3 Производство прочих стальных изделий первичной обработки", + "24.4 Производство основных драгоценных металлов и цветных металлов", + "24.5 Литье металлов", + "25.1 Производство строительных металлических конструкций и изделий", + "25.2 Производство металлических цистерн, резервуаров и емкостей", + "25.3 Производство паровых котлов", + "25.4 Производство оружия и боеприпасов", + "25.5 Ковка, прессование, штамповка и профилирование; порошковая металлургия", + "25.6 Обработка металлических изделий механическая", + "25.7 Производство ножевых изделий и столовых приборов", + "25.9 Производство прочих готовых металлических изделий", + "26.1 Производство элементов электронной аппаратуры и печатных схем", + "26.2 Производство компьютеров и периферийного оборудования", + "26.3 Производство коммуникационного оборудования", + "26.4 Производство бытовой электроники", + "26.5 Производство контрольно-измерительных приборов и навигационных приборов", + "26.6 Производство облучающего и электротерапевтического оборудования", + "26.7 Производство оптических приборов, фото- и кинооборудования", + "26.8 Производство незаписанных магнитных и оптических носителей", + "27.1 Производство электродвигателей, генераторов, трансформаторов", + "27.2 Производство электрических аккумуляторов и аккумуляторных батарей", + "27.3 Производство кабелей и кабельной арматуры", + "27.4 Производство электрического осветительного оборудования", + "27.5 Производство бытовых приборов", + "27.9 Производство прочего электрического оборудования", + "28.1 Производство машин и оборудования общего назначения", + "28.2 Производство прочих машин и оборудования общего назначения", + "28.3 Производство машин и оборудования для сельского и лесного хозяйства", + "28.4 Производство станков, машин и оборудования для обработки металлов", + "28.9 Производство прочих машин и оборудования специального назначения", + "29.1 Производство автотранспортных средств", + "29.2 Производство кузовов, прицепов и полуприцепов", + "29.3 Производство комплектующих и принадлежностей для автотранспортных средств", + "30.1 Строительство кораблей, судов и лодок", + "30.2 Производство железнодорожных локомотивов и подвижного состава", + "30.3 Производство летательных аппаратов и космических аппаратов", + "30.4 Производство военных боевых машин", + "30.9 Производство прочих транспортных средств и оборудования", + "31.0 Производство мебели", + "32.1 Производство ювелирных изделий, бижутерии", + "32.2 Производство музыкальных инструментов", + "32.3 Производство спортивных товаров", + "32.4 Производство игр и игрушек", + "32.5 Производство медицинских инструментов и оборудования", + "32.9 Производство изделий, не включенных в другие группировки", + "33.1 Ремонт и монтаж металлических изделий и машин", + "33.2 Монтаж промышленных машин и оборудования", + "35.1 Производство, передача и распределение электроэнергии", + "35.2 Производство и распределение газообразного топлива", + "35.3 Производство, передача и распределение пара и горячей воды", + "36.0 Забор, очистка и распределение воды", + "37.0 Сбор и обработка сточных вод", + "38.1 Сбор отходов", + "38.2 Обработка и утилизация отходов", + "38.3 Деятельность по обработке вторичного сырья", + "39.0 Предоставление услуг в области ликвидации последствий загрязнений", + "41.1 Разработка строительных проектов", + "41.2 Строительство жилых и нежилых зданий", + "42.1 Строительство автомобильных и железных дорог", + "42.2 Строительство инженерных коммуникаций", + "42.9 Строительство прочих инженерных сооружений", + "43.1 Разборка и снос зданий, подготовка строительного участка", + "43.2 Производство электромонтажных, санитарно-технических и прочих строительно-монтажных работ", + "43.3 Работы строительные отделочные", + "43.9 Работы строительные специализированные прочие", + "45.1 Торговля автотранспортными средствами", + "45.2 Техническое обслуживание и ремонт автотранспортных средств", + "45.3 Торговля автомобильными деталями и принадлежностями", + "45.4 Торговля мотоциклами и их обслуживание", + "46.1 Торговля оптовая за вознаграждение или на договорной основе", + "46.2 Торговля оптовая сельскохозяйственным сырьем и живыми животными", + "46.3 Торговля оптовая пищевыми продуктами, напитками и табачными изделиями", + "46.4 Торговля оптовая непродовольственными потребительскими товарами", + "46.5 Торговля оптовая информационным и коммуникационным оборудованием", + "46.6 Торговля оптовая прочими машинами и оборудованием", + "46.7 Торговля оптовая специализированная прочая", + "46.9 Торговля оптовая неспециализированная", + "47.1 Торговля розничная в неспециализированных магазинах", + "47.2 Торговля розничная пищевыми продуктами в специализированных магазинах", + "47.3 Торговля розничная моторным топливом", + "47.4 Торговля розничная информационным и коммуникационным оборудованием", + "47.5 Торговля розничная прочими бытовыми изделиями", + "47.6 Торговля розничная товарами культурно-развлекательного назначения", + "47.7 Торговля розничная прочими товарами в специализированных магазинах", + "47.8 Торговля розничная в палатках и на рынках", + "47.9 Торговля розничная вне магазинов, палаток, рынков", + "49.1 Деятельность железнодорожного пассажирского транспорта", + "49.2 Деятельность железнодорожного грузового транспорта", + "49.3 Деятельность прочего сухопутного пассажирского транспорта", + "49.4 Деятельность автомобильного грузового транспорта", + "49.5 Деятельность трубопроводного транспорта", + "50.1 Деятельность морского пассажирского транспорта", + "50.2 Деятельность морского грузового транспорта", + "50.3 Деятельность внутреннего водного пассажирского транспорта", + "50.4 Деятельность внутреннего водного грузового транспорта", + "51.1 Деятельность пассажирского воздушного транспорта", + "51.2 Деятельность грузового воздушного транспорта", + "52.1 Деятельность по складированию и хранению", + "52.2 Деятельность транспортная вспомогательная", + "53.1 Деятельность почтовой связи общего пользования", + "53.2 Деятельность почтовой связи прочая и курьерская", + "55.1 Деятельность гостиниц и прочих мест временного проживания", + "55.2 Деятельность по предоставлению мест для краткосрочного проживания", + "55.3 Деятельность по предоставлению мест для кемпингов и стоянок", + "56.1 Деятельность ресторанов и услуги по доставке продуктов питания", + "56.2 Деятельность предприятий общественного питания", + "56.3 Подача напитков", + "58.1 Издание книг, периодических изданий и другие виды издательской деятельности", + "58.2 Издание программного обеспечения", + "59.1 Производство кинофильмов, видеофильмов и телевизионных программ", + "59.2 Деятельность в области звукозаписи и издания музыкальных произведений", + "60.1 Деятельность в области радиовещания", + "60.2 Деятельность в области телевизионного вещания", + "61.1 Деятельность в области проводной связи", + "61.2 Деятельность в области беспроводной связи", + "61.3 Деятельность в области спутниковой связи", + "61.9 Деятельность в области телекоммуникаций прочая", + "62.0 Разработка компьютерного программного обеспечения, консультационные услуги в данной области", + "63.1 Деятельность по обработке данных, предоставление услуг по размещению информации", + "63.9 Деятельность в области информационных услуг прочая", + "64.1 Денежное посредничество", + "64.2 Деятельность холдинговых компаний", + "64.3 Деятельность инвестиционных фондов", + "64.9 Деятельность по финансовому посредничеству прочая", + "65.1 Страхование жизни", + "65.2 Перестрахование", + "65.3 Страхование и перестрахование, негосударственное пенсионное обеспечение", + "66.1 Деятельность вспомогательная в сфере финансовых услуг", + "66.2 Деятельность вспомогательная в сфере страхования", + "66.3 Деятельность по управлению фондами", + "68.1 Покупка и продажа собственного недвижимого имущества", + "68.2 Аренда и управление собственным или арендованным недвижимым имуществом", + "68.3 Операции с недвижимым имуществом за вознаграждение или на договорной основе", + "69.1 Деятельность в области права", + "69.2 Деятельность по оказанию услуг в области бухгалтерского учета и аудита", + "70.1 Деятельность головных офисов", + "70.2 Консультирование по вопросам управления", + "70.3 Деятельность по связям с общественностью", + "71.1 Деятельность в области архитектуры, инженерных изысканий", + "71.2 Технические испытания, исследования, анализ и сертификация", + "72.1 Научные исследования и разработки в области естественных и технических наук", + "72.2 Научные исследования и разработки в области общественных и гуманитарных наук", + "73.1 Деятельность рекламная", + "73.2 Исследование конъюнктуры рынка и опросы общественного мнения", + "74.1 Деятельность специализированная в области дизайна", + "74.2 Деятельность в области фотографии", + "74.3 Деятельность по письменному и устному переводу", + "74.9 Деятельность профессиональная, научная и техническая прочая", + "75.0 Деятельность ветеринарная", + "77.1 Аренда и лизинг автотранспортных средств", + "77.2 Прокат и аренда предметов личного пользования и хозяйственно-бытового назначения", + "77.3 Аренда и лизинг машин и оборудования", + "77.4 Аренда и лизинг интеллектуальной собственности", + "78.1 Деятельность агентств по подбору персонала", + "78.2 Деятельность агентств по временному трудоустройству", + "78.3 Деятельность по обеспечению безопасности в чрезвычайных ситуациях; деятельность по обеспечению безопасности в области информатизации", + "79.1 Деятельность туристических агентств и туроператоров", + "79.9 Услуги по бронированию прочие", + "80.1 Деятельность частных охранных служб", + "80.2 Деятельность систем обеспечения безопасности", + "80.3 Деятельность по расследованию", + "81.1 Деятельность по комплексному обслуживанию помещений", + "81.2 Деятельность по чистке и уборке", + "81.3 Деятельность по благоустройству ландшафта", + "82.1 Деятельность административно-хозяйственная комплексная", + "82.2 Деятельность центров обработки телефонных вызовов", + "82.3 Деятельность по организации конференций и выставок", + "82.9 Деятельность по предоставлению вспомогательных услуг для бизнеса", + "84.1 Деятельность органов государственного управления", + "84.2 Предоставление государственных услуг обществу", + "84.3 Деятельность в области обязательного социального обеспечения", + "85.1 Образование общее", + "85.2 Образование профессиональное", + "85.3 Обучение профессиональное", + "85.4 Образование дополнительное", + "86.1 Деятельность больничных организаций", + "86.2 Медицинская и стоматологическая практика", + "86.9 Деятельность в области медицины прочая", + "87.1 Деятельность по уходу с обеспечением проживания", + "87.2 Деятельность по уходу с обеспечением проживания для умственно отсталых и психически больных", + "87.9 Деятельность по уходу с обеспечением проживания прочая", + "88.1 Предоставление социальных услуг без обеспечения проживания престарелым и инвалидам", + "88.9 Предоставление прочих социальных услуг без обеспечения проживания", + "90.0 Деятельность творческая, деятельность в области искусства и организации развлечений", + "91.0 Деятельность библиотек, архивов, музеев и прочих объектов культуры", + "92.0 Деятельность по организации и проведению азартных игр и пари", + "93.1 Деятельность в области спорта", + "93.2 Деятельность в области отдыха и развлечений", + "94.1 Деятельность профессиональных союзов", + "94.2 Деятельность религиозных организаций", + "94.9 Деятельность общественных организаций прочая", + "95.1 Ремонт компьютеров и коммуникационного оборудования", + "95.2 Ремонт предметов личного потребления и хозяйственно-бытового назначения", + "96.0 Деятельность по предоставлению прочих персональных услуг", + "97.0 Деятельность домашних хозяйств с наемными работниками", + "98.0 Деятельность домашних хозяйств по производству товаров для собственного использования", + "99.0 Деятельность экстерриториальных организаций и органов" +] diff --git a/dokogen/dictionaries/pd_actions.json b/dokogen/dictionaries/pd_actions.json deleted file mode 100644 index 20be6c3..0000000 --- a/dokogen/dictionaries/pd_actions.json +++ /dev/null @@ -1,17 +0,0 @@ -[ - "сбор", - "систематизация", - "хранение", - "извлечение", - "передача (предоставление доступа)", - "блокирование", - "уничтожение", - "запись", - "накопление", - "уточнение (обновление, изменение)", - "использование", - "обезличивание", - "удаление", - "распространение", - "иные действия" -] \ No newline at end of file diff --git a/dokogen/dictionaries/pd_categories.json b/dokogen/dictionaries/pd_categories.json deleted file mode 100644 index a851b52..0000000 --- a/dokogen/dictionaries/pd_categories.json +++ /dev/null @@ -1,16 +0,0 @@ -[ - "Сотрудникам организации", - "Соискателям", - "Родственникам сотрудников", - "Уволенным сотрудникам", - "Контрагентам", - "Представителям контрагентов", - "Клиентам", - "Посетителям сайта", - "Выгодоприобретателям по договорам", - "Субъект персональных данных, предоставивший согласие на трансграничную передачу персональных данных", - "Учащимя", - "Студентам", - "Законным представителям", - "Иным категориям субъектов персональных данных, персональные данные которых обрабатываются" -] \ No newline at end of file diff --git a/dokogen/dictionaries/pd_items.json b/dokogen/dictionaries/pd_items.json deleted file mode 100644 index b514eea..0000000 --- a/dokogen/dictionaries/pd_items.json +++ /dev/null @@ -1,44 +0,0 @@ -[ - "Фамилия, Имя, Отчество", - "Год рождения", - "Месяц рождения", - "Дата рождения", - "Место рождения", - "Семейное положение", - "Социальное положение", - "Имущественное положение", - "Сведения о доходах", - "Пол", - "Адрес электронной почты", - "Адрес места жительства", - "Адрес регистрации", - "Номер телефона", - "Страховой номер индивидуального лицевого счета (СНИЛС)", - "Идентификационный номер налогоплательщика (ИНН);", - "Сведения о гражданстве", - "Данные документа, удостоверяющего личность", - "Данные водительского удостоверения", - "Данные документа, удостоверяющего личность за пределами Российской Федерации", - "Данные документа, содержащиеся в свидетельстве о рождении", - "Реквизиты банковских карт", - "Номер расчетного счета", - "Номер лицевого счета", - "Профессия", - "Должность", - "Сведения о трудовой деятельности (в том числе стаж работы, данные о трудовой занятости на текущее время с указанием наименования и расчетного счета организации)", - "Сведения об отношении к воинской обязанности, о воинском учете", - "Иные персональные данные", - "Сведения, собираемые посредством метрических программ (например, Яндекс.Метрика)", - "Сведения об образовании", - "Фото-видео изображение лица", - "Данные голоса человека", - "Сведения о состоянии здоровья", - "Национальная принадлежность", - "Политические взгляды", - "Религиозные или философские убеждения", - "Сведения о судимости", - "Данные изображения лица, полученные с помощью фото- видео устройств, на основании которых можно установить его личность и которые используются оператором для установления личности субъекта персональных данных (биометрия)", - "Данные голоса человека, на основании которых можно установить его личность и которые используются оператором для установления личности субъекта персональных данных (биометрия)", - "Изображение папиллярных узоров пальцев рук", - "Иные биометрические персональные данные" -] \ No newline at end of file diff --git a/dokogen/help_docs.py b/dokogen/help_docs.py index ec11dbe..4fb930f 100644 --- a/dokogen/help_docs.py +++ b/dokogen/help_docs.py @@ -97,10 +97,10 @@ def _generate_variables_reference(path): _bullet(doc, f'{var} — {desc}') _h(doc, 'Циклы', 12) - _bullet(doc, 'information_systems — ИС ({{item.name}}, {{item.description}}, {{item.structure}}, {{item.pd_actions}}, {{item.defense_tools_list}}, {{item.users}}, {{item.category}}, {{item.pd_count}} и др.).') + _bullet(doc, 'information_systems — объекты КИИ ({{item.name}}, {{item.description}}, {{item.category}}, {{item.defense_tools_list}}, {{item.users}}, {{item.software}} и др.).') _bullet(doc, 'commission — комиссия ({{item.role}}, {{item.position}}, {{item.fio}}, {{item.fio_initials}}, {{item.fio_short}}, {{item.signature}}).') _bullet(doc, 'employees_access — сотрудники с доступом ({{item.position}}, {{item.fio}}).') - _bullet(doc, 'paper_documents — бумажные документы ({{item.document}}, {{item.storage}}).') + doc.save(path) @@ -120,10 +120,10 @@ def _generate_user_manual(path): _h(doc, '2. Основные вкладки', 12) _bullet(doc, 'Организация — реквизиты, адреса, руководитель, склонения названий, проверка данных (Морфер / DaData / ИИ).') - _bullet(doc, 'Информационные системы — ИС, категории ПДн, действия с ПДн, средства защиты, пользователи.') - _bullet(doc, 'Комиссия — состав комиссии по классификации ИСПДн.') - _bullet(doc, 'Пользователи — сотрудники с доступом к ПДн.') - _bullet(doc, 'Бумажные документы — бумажные носители ПДн и места хранения.') + _bullet(doc, 'Объекты КИИ — наименование, описание, категория значимости, средства защиты, пользователи ОКИИ.') + _bullet(doc, 'Комиссия — состав комиссии по категорированию объектов КИИ.') + _bullet(doc, 'Пользователи — сотрудники с доступом к объектам КИИ.') + _bullet(doc, 'Генерация — выбор шаблонов и запуск генерации.') _bullet(doc, 'Лог — журнал действий, экспорт в файл.') @@ -137,13 +137,13 @@ def _generate_user_manual(path): _h(doc, '4. Циклы (повторяющиеся блоки)', 12) _p(doc, 'Если в документе нужно повторить блок для каждой ИС, комиссии, ' - 'сотрудника или бумажного документа — используются маркеры цикла:') + 'сотрудника или объекта КИИ — используются маркеры цикла:') _p(doc, ' ... {{item.name}} ... ', bold=True) _p(doc, 'Доступные циклы:') - _bullet(doc, 'information_systems — ИС ({{item.name}}, {{item.description}}, {{item.structure}}, {{item.pd_actions}}, {{item.defense_tools_list}}, {{item.users}}, {{item.category}}, {{item.pd_count}} и др.).') + _bullet(doc, 'information_systems — объекты КИИ ({{item.name}}, {{item.description}}, {{item.category}}, {{item.defense_tools_list}}, {{item.users}}, {{item.software}} и др.).') _bullet(doc, 'commission — комиссия ({{item.role}}, {{item.position}}, {{item.fio}}, {{item.fio_initials}}, {{item.fio_short}}, {{item.signature}}).') _bullet(doc, 'employees_access — сотрудники с доступом ({{item.position}}, {{item.fio}}).') - _bullet(doc, 'paper_documents — бумажные документы ({{item.document}}, {{item.storage}}).') + _p(doc, 'Внутри цикла переменные пишутся через точку: {{item.поле}}. ' 'Допускается и синтаксис без точки: {{item_поле}}.') @@ -179,7 +179,7 @@ def _generate_admin_manual(path): _h(doc, '1. Файлы настроек рядом с программой', 12) _bullet(doc, 'api_settings.json — API-ключи (DaData, ИИ).') _bullet(doc, 'import_settings.json — словарь импорта: что и откуда брать из опросника.') - _bullet(doc, 'dictionaries/ — справочники: defense_tools.json, pd_actions.json, pd_items.json, pd_categories.json.') + _bullet(doc, 'dictionaries/ — справочники: defense_tools.json, okved.json.') _bullet(doc, 'dokogen.log — журнал работы (пишется автоматически).') _bullet(doc, 'docs/ — инструкции и пустой опросник (создаются автоматически).') diff --git a/dokogen/importer.py b/dokogen/importer.py index 3bff54d..89fa9d7 100644 --- a/dokogen/importer.py +++ b/dokogen/importer.py @@ -141,8 +141,8 @@ def import_187fz(filepath: str, log_fn: Optional[Callable] = None, settings_path data = { 'fullName': '', 'shortName': '', 'addressLegal': '', 'addressActual': '', 'email': '', 'phone': '', 'inn': '', 'ogrn': '', 'kpp': '', - 'okved': '', 'chiefPosition': '', 'chiefFio': '', - 'ispdnFio': '', 'ispdnPosition': '', 'paperDocuments': '', 'paperStorage': '', + 'okved': '', 'okvedExtra': '', 'chiefPosition': '', 'chiefFio': '', + 'ispdnFio': '', 'ispdnPosition': '', 'ispdnDepartment': '', 'ispdnEmail': '', 'ispdnPhone': '', 'informationSystems': [], 'commission': [], 'employeesAccess': [], } @@ -248,7 +248,7 @@ def _import_187_common(wb, data, settings, log_fn): def _import_187_is_list(wb, data, settings, log_fn): - """Импорт информационных систем из остальных листов.""" + """Импорт объектов КИИ из остальных листов (ОБЪЕКТ №1, №2, ...).""" exclude_kws = settings.get('is_sheet_exclude_keywords', ['общие сведения', 'сотрудники']) is_sheets = [] for sn in wb.sheetnames: @@ -257,7 +257,7 @@ def _import_187_is_list(wb, data, settings, log_fn): continue is_sheets.append(sn) if not is_sheets: - log_fn("⚠ Листы ИС не найдены") + log_fn("⚠ Листы объектов КИИ не найдены") return is_list = [] @@ -275,16 +275,20 @@ def _import_187_is_list(wb, data, settings, log_fn): continue is_obj = { - 'name': '', 'description': '', 'room': '', 'employee': '', 'position': '', - 'personalDataList': [], 'pd_subjects_list': [], 'pd_actions': [], - 'usersList': [], 'isInternet': False, 'personalDataCount': 'менее 100 000', - 'defense_tools': [], 'processing_mode': '', + 'name': '', 'description': '', 'usersList': [], + 'isInternet': False, 'defense_tools': [], 'software': '', } - current_section = 'header' - pd_items_raw = [] - subjects_raw = [] - actions_raw = [] + defense_keywords = { + 'антивирус': 'Антивирус', + 'крипто': 'СКЗИ', + 'межсетев': 'Межсетевой экран', + 'сзи.*от несанкционирован': 'СЗИ от НСД', + 'обнаружен.*вторжен': 'СОВ', + 'программные.*средств.*модул': 'Доверенная загрузка', + 'доверен.*загрузк': 'Доверенная загрузка', + 'гарантирован': 'Доверенная загрузка', + } for vals in rows: field = (vals[0] or '').strip() @@ -292,159 +296,58 @@ def _import_187_is_list(wb, data, settings, log_fn): val_b = clean_value(vals[1]) if len(vals) > 1 else '' is_checked = val_b.upper() in ('ДА', 'YES', '✓', '✔', '1', 'TRUE', 'ЕСТЬ') - if 'цель обработки пд' in field_lower or ('цель обработки' in field_lower and ':' in field): - current_section = 'purpose' + if 'наименование объекта' in field_lower or 'название объекта' in field_lower: + is_obj['name'] = val_b + log_fn(f" Объект КИИ: {val_b}") continue - if 'перечисленные пдн принадлежат' in field_lower or 'принадлеж' in field_lower: - current_section = 'subjects' - continue - if 'структура ис' in field_lower or 'структура информационной' in field_lower: + if 'описание объекта' in field_lower or 'описание окки' in field_lower: if val_b: - is_obj['structure'] = val_b - elif ':' in field: - # Значение может быть записано в той же ячейке после двоеточия - tail = field.split(':', 1)[1].strip() - if tail and 'выбрать' not in tail.lower(): - is_obj['structure'] = tail + is_obj['description'] = val_b continue - if 'названия программ' in field_lower or ('программ' in field_lower and 'ос взаимодейств' in field_lower): + if 'операционная' in field_lower or 'прикладные программы' in field_lower or 'наименования программно' in field_lower: if val_b: - is_obj['software'] = val_b - continue - if 'локальную сеть' in field_lower: - is_obj['is_has_lan'] = is_checked - continue - if 'количество записей' in field_lower or 'количество субъект' in field_lower: - if val_b: - is_obj['personalDataCount'] = val_b - continue - if 'перечень действий с пд' in field_lower or ('действи' in field_lower and 'пд' in field_lower): - current_section = 'actions' - continue - if 'способ обработки пд' in field_lower: - if val_b: - is_obj['processing_mode'] = val_b + is_obj['software'] = (is_obj['software'] + '; ' + val_b) if is_obj['software'] else val_b continue if 'интернет' in field_lower: is_obj['isInternet'] = is_checked continue - - if current_section == 'header': - if 'перечень пд' in field_lower: - current_section = 'pd_items' - elif 'название ис' in field_lower or 'наименование ис' in field_lower or 'наименование информационной' in field_lower or 'наименование объекта' in field_lower or 'название объекта' in field_lower: - is_obj['name'] = val_b - log_fn(f" ИС: {val_b}") - elif 'описание ис' in field_lower or 'описание информационной' in field_lower: - if val_b: - is_obj['description'] = val_b - log_fn(f" Описание: {val_b[:60]}") - continue - elif 'сотрудник' in field_lower and 'должност' not in field_lower: - is_obj['employee'] = val_b - if val_b: - parts = [p.strip() for p in val_b.replace('\n', ';').split(';') if p.strip()] - for part in parts: - cleaned = part.strip() - if ':' in cleaned and len(cleaned.split(':')[0]) < 20: - cleaned = cleaned.split(':', 1)[1].strip() - if cleaned and cleaned not in is_obj['usersList']: - is_obj['usersList'].append(cleaned) - elif 'должность сотрудника' in field_lower: - is_obj['position'] = val_b - elif 'наименование отдела' in field_lower: - pass - elif 'номер кабинета' in field_lower: - is_obj['room'] = val_b - elif 'значение' in field_lower or 'примечание' in field_lower: - continue - elif val_b and 'выбрать' not in field_lower: - if not is_obj['description']: - is_obj['description'] = f"{field}: {val_b}" - else: - is_obj['description'] += f"\n{field}: {val_b}" - elif current_section == 'pd_items': - if is_checked and 'выбрать' not in field_lower and field_lower != 'значение': - pd_items_raw.append(field) - if 'цель обработки' in field_lower: - current_section = 'purpose' - elif current_section == 'purpose': - purpose_val = val_b or field or '' - if purpose_val and 'выбрать' not in purpose_val.lower() and ':' not in purpose_val[:40]: - is_obj['purpose'] = purpose_val - log_fn(f" Цель: {purpose_val[:80]}") - current_section = 'header' - elif current_section == 'subjects': - if is_checked and 'выбрать' not in field_lower: - subjects_raw.append(field) - if 'количество записей' in field_lower or 'действи' in field_lower: - continue - elif current_section == 'actions': - if is_checked and 'выбрать' not in field_lower and 'защита' not in field_lower[:20]: - actions_raw.append(field) - if 'защита' in field_lower and 'действи' not in field_lower: - current_section = 'defense' - elif current_section == 'defense': - defense_keywords = { - 'антивирус': 'Антивирус', - 'крипто': 'СКЗИ', - 'межсетев': 'Межсетевой экран', - 'сзи.*от несанкционирован': 'СЗИ от НСД', - 'обнаружен.*вторжен': 'СОВ', - 'программные.*средств.*модул': 'Доверенная загрузка', - 'доверен.*загрузк': 'Доверенная загрузка', - 'гарантирован': 'Доверенная загрузка', - } - matched_label = None - for kw, label in defense_keywords.items(): - if re.search(kw, field_lower): - matched_label = label - break - if matched_label and val_b and 'выбрать' not in field_lower: - # Без префикса категории: просто название средства (Kaspersky, КриптоПро) - is_obj['defense_tools'].append(val_b) - elif matched_label: - pass - elif 'куда' in field_lower or 'передаются' in field_lower: - current_section = 'header' - elif 'способ обработки' in field_lower or 'локальную сеть' in field_lower or 'интернет' in field_lower: - current_section = 'header' - - if pd_items_raw: - is_obj['personalDataList'] = pd_items_raw - log_fn(f" ПДн: {len(pd_items_raw)}") - if subjects_raw: - is_obj['pd_subjects_list'] = subjects_raw - log_fn(f" Субъектов: {len(subjects_raw)}") - if actions_raw: - is_obj['pd_actions'] = actions_raw - log_fn(f" Действий: {len(actions_raw)}") + if 'сотрудник' in field_lower and 'должност' not in field_lower: + if val_b: + parts = [p.strip() for p in val_b.replace('\n', ';').split(';') if p.strip()] + for part in parts: + cleaned = part.strip() + if ':' in cleaned and len(cleaned.split(':')[0]) < 20: + cleaned = cleaned.split(':', 1)[1].strip() + if cleaned and cleaned not in is_obj['usersList']: + is_obj['usersList'].append(cleaned) + continue + # Средства защиты + matched = False + for kw, label in defense_keywords.items(): + if re.search(kw, field_lower): + if val_b and 'выбрать' not in field_lower: + is_obj['defense_tools'].append(val_b) + matched = True + break + if matched: + continue + if val_b and 'выбрать' not in field_lower and field_lower not in ('значение', 'примечание'): + # Остальные заполненные поля — в описание (адрес, сфера, тип и т.д.) + if not is_obj['description']: + is_obj['description'] = f"{field}: {val_b}" + else: + is_obj['description'] += f"\n{field}: {val_b}" if not is_obj['name']: is_obj['name'] = sheet_name.strip() - log_fn(f" ⚠ ИС без названия — использовано имя листа: {is_obj['name']}") - - # Автоопределение категорий ПДн - pd_cats = [] - all_pd_text = ' '.join(p.lower() for p in pd_items_raw) - special_kw = ['состоян.*здоров', 'национальн', 'политическ', 'религиозн', 'философ', 'судимост', 'интимн'] - if any(re.search(kw, all_pd_text) for kw in special_kw): - pd_cats.append('специальные') - bio_kw = ['биометрическ', 'изображен.*лиц', 'голос.*человек', 'папилляр', 'дактилоскоп', 'фото.*изображен'] - if any(re.search(kw, all_pd_text) for kw in bio_kw): - pd_cats.append('биометрические') - if pd_items_raw: - pd_cats.append('иные') - if pd_cats: - is_obj['personalDataCategory'] = pd_cats - log_fn(f" Категории ПДн: {', '.join(pd_cats)}") + log_fn(f" ⚠ Объект без названия — использовано имя листа: {is_obj['name']}") is_list.append(is_obj) - log_fn(f" ✅ {is_obj['name']} — импортирована") + log_fn(f" ✅ {is_obj['name']} — импортирован") if is_list: data['informationSystems'] = is_list - log_fn(f"\n✅ Всего импортировано ИС: {len(is_list)}") + log_fn(f"\n✅ Всего импортировано объектов КИИ: {len(is_list)}") def _import_187_employees(wb, data, settings, log_fn): @@ -497,7 +400,7 @@ def _import_187_employees(wb, data, settings, log_fn): if 'перечень сотрудников' in a_lower: in_commission = False in_employees = True - log_fn(" Секция: сотрудники с доступом к ПДн") + log_fn(" Секция: сотрудники с доступом к объектам КИИ") continue if a_lower in ('№ п/п', '№', 'номер', ''): continue diff --git a/dokogen/models.py b/dokogen/models.py index 88bed71..36ebfcb 100644 --- a/dokogen/models.py +++ b/dokogen/models.py @@ -7,7 +7,7 @@ from typing import List, Dict, Any @dataclass class CommissionMember: - """Член комиссии по ПДн""" + """Член комиссии по категорированию объектов КИИ""" role: str = "член комиссии" position: str = "" fio: str = "" @@ -15,38 +15,19 @@ class CommissionMember: @dataclass class InformationSystem: - """Информационная система (обработка ПДн)""" + """Объект КИИ (вместо информационной системы 152-ФЗ)""" name: str = "" description: str = "" is_local_network: bool = False is_internet: bool = False - defence_level: str = "" - threat_type: str = "3" - category: str = "" - pd_list: str = "" - pd_count: str = "менее 100 000" - users: str = "" - defense_tools_list: List[str] = field(default_factory=list) - processing_modes: str = "" - subject_type: str = "работники организации" - pd_actions_list: List[str] = field(default_factory=list) - pd_subjects_list: List[str] = field(default_factory=list) - personal_data_list: List[str] = field(default_factory=list) - users_list: List[str] = field(default_factory=list) - personal_data_category: List[str] = field(default_factory=list) + category: str = "" # Категория значимости объекта КИИ: 1 / 2 / 3 / без категории + users_list: List[str] = field(default_factory=list) # Пользователи ОКИИ + defense_tools_list: List[str] = field(default_factory=list) # Средства защиты purpose: str = "" - room: str = "" - structure: str = "" # Структура информационной системы + structure: str = "" # Структура объекта КИИ software: str = "" # Программное обеспечение -@dataclass -class PaperDocument: - """Бумажный документ с ПДн""" - name: str = "" - storage: str = "" - - @dataclass class Company: """Организация — основной объект (187-ФЗ)""" @@ -75,6 +56,8 @@ class Company: ogrn: str = "" ogrn_date: str = "" kpp: str = "" + okved: str = "" # Основной ОКВЭД + okved_extra: List[str] = field(default_factory=list) # Дополнительные ОКВЭД # Должностные лица chief_position: str = "" @@ -91,7 +74,6 @@ class Company: # Списки commission: List[CommissionMember] = field(default_factory=list) information_systems: List[InformationSystem] = field(default_factory=list) - paper_documents_list: List[PaperDocument] = field(default_factory=list) employees_access: List[Dict[str, str]] = field(default_factory=list) # Наименование структурного подразделения по безопасности @@ -135,13 +117,10 @@ class Company: InformationSystem(**isys) if isinstance(isys, dict) else isys for isys in d["information_systems"] ] - if "paper_documents_list" in d and isinstance(d["paper_documents_list"], list): - d["paper_documents_list"] = [ - PaperDocument(**p) if isinstance(p, dict) else p - for p in d["paper_documents_list"] - ] if "employees_access" in d and not isinstance(d["employees_access"], list): d["employees_access"] = [] + if "okved_extra" in d and not isinstance(d.get("okved_extra"), list): + d["okved_extra"] = [] valid = {f.name for f in cls.__dataclass_fields__.values()} return cls(**{k: v for k, v in d.items() if k in valid}) diff --git a/dokogen/ui.py b/dokogen/ui.py index a87746d..56881b7 100644 --- a/dokogen/ui.py +++ b/dokogen/ui.py @@ -11,7 +11,7 @@ from datetime import datetime from typing import List, Optional from . import ( - Company, CommissionMember, InformationSystem, PaperDocument, + Company, CommissionMember, InformationSystem, inflect_name, build_replacements, VARIABLE_DEFS, process_template, @@ -157,7 +157,6 @@ class DokoGenApp: self._build_is_tab() self._build_commission_tab() self._build_users_tab() - self._build_paper_tab() self._build_generation_tab() self._build_log_tab() @@ -270,6 +269,33 @@ class DokoGenApp: f2.columnconfigure(1, weight=1) + # --- ОКВЭД --- + f_okved = ttk.LabelFrame(left, text="ОКВЭД", padding=10) + f_okved.pack(fill=tk.X, pady=5) + + # Основной ОКВЭД — один, из словаря или вручную + ttk.Label(f_okved, text="Основной ОКВЭД:").grid(row=0, column=0, sticky=tk.W, pady=2) + okved_entry = ttk.Entry(f_okved, width=50) + okved_entry.grid(row=0, column=1, padx=10, sticky=tk.EW, pady=1) + self._company_entries['okved'] = okved_entry + ttk.Button(f_okved, text="Выбрать из словаря", + command=self._select_main_okved).grid(row=0, column=2, padx=5) + + # Дополнительные ОКВЭД — сколько угодно + ttk.Label(f_okved, text="Дополнительные ОКВЭД:").grid(row=1, column=0, sticky=tk.NW, pady=2) + self.okved_extra_listbox = tk.Listbox(f_okved, height=4, width=50) + self.okved_extra_listbox.grid(row=1, column=1, padx=10, sticky=tk.EW, pady=1) + okved_btn = ttk.Frame(f_okved) + okved_btn.grid(row=1, column=2, padx=5, sticky=tk.N) + ttk.Button(okved_btn, text="Выбрать из словаря", + command=self._select_extra_okved).pack(fill=tk.X, pady=1) + ttk.Button(okved_btn, text="+ Добавить", + command=self._add_okved_manual).pack(fill=tk.X, pady=1) + ttk.Button(okved_btn, text="- Удалить", + command=self._delete_okved_extra).pack(fill=tk.X, pady=1) + + f_okved.columnconfigure(1, weight=1) + # --- Должностные лица --- f3 = ttk.LabelFrame(left, text="Должностные лица", padding=10) f3.pack(fill=tk.X, pady=5) @@ -686,12 +712,12 @@ class DokoGenApp: # ---------- ВКЛАДКА "ИНФОРМАЦИОННЫЕ СИСТЕМЫ" ---------- def _build_is_tab(self): tab = ttk.Frame(self.notebook) - self.notebook.add(tab, text="Информационные системы") + self.notebook.add(tab, text="Объекты КИИ") - # Левая панель — список ИС + # Левая панель — список объектов КИИ left = ttk.Frame(tab) left.pack(side=tk.LEFT, fill=tk.Y, padx=5, pady=5) - ttk.Label(left, text="Информационные системы:").pack() + ttk.Label(left, text="Объекты КИИ:").pack() self.is_listbox = tk.Listbox(left, width=35, height=18, exportselection=False) self.is_listbox.pack(fill=tk.BOTH, expand=True, pady=5) self.is_listbox.bind('<>', self._on_is_select) @@ -703,7 +729,7 @@ class DokoGenApp: ttk.Button(btn_frame, text="- Удалить", command=self._delete_is).pack(side=tk.LEFT, padx=2) # Правая панель — редактирование - self._is_edit_panel = ttk.LabelFrame(tab, text="Редактирование ИС") + self._is_edit_panel = ttk.LabelFrame(tab, text="Редактирование объекта КИИ") self._is_edit_panel.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=5, pady=5) fields_frame = ttk.Frame(self._is_edit_panel, padding=10) @@ -726,51 +752,19 @@ class DokoGenApp: self.is_internet_var = tk.BooleanVar() ttk.Checkbutton(chk_frame, text="Интернет", variable=self.is_internet_var).pack(side=tk.LEFT, padx=5) - # Тип угроз - self.is_threat_var = tk.StringVar(value="3") - self.is_threat_var.trace_add("write", lambda *_: self._on_categories_or_count_changed()) - ttk.Label(fields_frame, text="Тип угроз:").grid(row=3, column=0, sticky=tk.W, pady=2) - threat_frame = ttk.Frame(fields_frame) - threat_frame.grid(row=3, column=1, sticky=tk.W, padx=10) - for val, text in [("1", "1 (НДВ в СПО)"), ("2", "2 (НДВ в ППО)"), ("3", "3 (НДВ нет)")]: - ttk.Radiobutton(threat_frame, text=text, variable=self.is_threat_var, + # Категория значимости объекта КИИ + self.is_category_var = tk.StringVar(value="без категории") + ttk.Label(fields_frame, text="Категория значимости:").grid(row=3, column=0, sticky=tk.W, pady=2) + cat_frame = ttk.Frame(fields_frame) + cat_frame.grid(row=3, column=1, sticky=tk.W, padx=10) + for val in ["1", "2", "3", "без категории"]: + ttk.Radiobutton(cat_frame, text=val, variable=self.is_category_var, value=val).pack(side=tk.LEFT, padx=3) - # Уровень защищённости - self.is_defence_var = tk.StringVar(value="4") - ttk.Label(fields_frame, text="Ур. защищённости:").grid(row=4, column=0, sticky=tk.W, pady=2) - def_frame = ttk.Frame(fields_frame) - def_frame.grid(row=4, column=1, sticky=tk.W, padx=10) - for lvl in ["1", "2", "3", "4"]: - ttk.Radiobutton(def_frame, text=f"{lvl} ур.", - variable=self.is_defence_var, value=lvl).pack(side=tk.LEFT, padx=3) - - - # Категории ПДн - ttk.Label(fields_frame, text="Категории ПДн:").grid(row=5, column=0, sticky=tk.NW, pady=2) - cat_frame = ttk.Frame(fields_frame) - cat_frame.grid(row=5, column=1, sticky=tk.W, padx=10) - self.category_vars = [] - for cat in ["специальные", "биометрические", "общедоступные", "иные"]: - var = tk.BooleanVar() - var.trace_add('write', lambda *_: self._on_categories_or_count_changed()) - cb = ttk.Checkbutton(cat_frame, text=cat, variable=var) - cb.pack(anchor=tk.W) - self.category_vars.append((cat, var)) - - # Количество записей ПДн (выпадающий список) - ttk.Label(fields_frame, text="Кол-во записей:").grid(row=6, column=0, sticky=tk.W, pady=2) - self.is_pd_count_var = tk.StringVar(value="менее 100 000") - self.is_pd_count_combo = ttk.Combobox(fields_frame, textvariable=self.is_pd_count_var, - values=["менее 100 000", "более 100 000"], - state='readonly', width=28) - self.is_pd_count_combo.grid(row=6, column=1, padx=10, sticky=tk.W) - self.is_pd_count_combo.bind('<>', lambda e: self._on_categories_or_count_changed()) - - # Структура ИС - ttk.Label(fields_frame, text="Структура ИС:").grid(row=7, column=0, sticky=tk.W, pady=2) - self.is_structure_entry = ttk.Entry(fields_frame, width=55) - self.is_structure_entry.grid(row=7, column=1, padx=10, sticky=tk.EW) + # Программное обеспечение + ttk.Label(fields_frame, text="Программное обеспечение:").grid(row=4, column=0, sticky=tk.W, pady=2) + self.is_software_entry = ttk.Entry(fields_frame, width=55) + self.is_software_entry.grid(row=4, column=1, padx=10, sticky=tk.EW) fields_frame.columnconfigure(1, weight=1) @@ -778,39 +772,6 @@ class DokoGenApp: nb2 = ttk.Notebook(self._is_edit_panel) nb2.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) - # Список ПДн - pd_tab = ttk.Frame(nb2) - nb2.add(pd_tab, text="Перечень ПДн") - pd_btn_frame = ttk.Frame(pd_tab) - pd_btn_frame.pack(fill=tk.X) - ttk.Button(pd_btn_frame, text="+ Добавить", command=self._add_pd_item).pack(side=tk.LEFT, padx=2) - ttk.Button(pd_btn_frame, text="- Удалить", command=lambda: self._del_from_listbox(self.pd_listbox)).pack(side=tk.LEFT, padx=2) - ttk.Button(pd_btn_frame, text="Выбрать из списка", command=self._select_from_pd_list).pack(side=tk.LEFT, padx=2) - self.pd_listbox = tk.Listbox(pd_tab, height=6) - self.pd_listbox.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) - - # Субъекты ПДн - subj_tab = ttk.Frame(nb2) - nb2.add(subj_tab, text="Субъекты ПДн") - subj_btn_frame = ttk.Frame(subj_tab) - subj_btn_frame.pack(fill=tk.X) - ttk.Button(subj_btn_frame, text="+ Добавить", command=self._add_pd_subjects_item).pack(side=tk.LEFT, padx=2) - ttk.Button(subj_btn_frame, text="- Удалить", command=lambda: self._del_from_listbox(self.pd_subjects_listbox)).pack(side=tk.LEFT, padx=2) - ttk.Button(subj_btn_frame, text="Выбрать из списка", command=self._select_from_subjects_list).pack(side=tk.LEFT, padx=2) - self.pd_subjects_listbox = tk.Listbox(subj_tab, height=6) - self.pd_subjects_listbox.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) - - # Действия с ПДн - act_tab = ttk.Frame(nb2) - nb2.add(act_tab, text="Действия с ПДн") - act_btn_frame = ttk.Frame(act_tab) - act_btn_frame.pack(fill=tk.X) - ttk.Button(act_btn_frame, text="+ Добавить", command=self._add_pd_action_item).pack(side=tk.LEFT, padx=2) - ttk.Button(act_btn_frame, text="- Удалить", command=lambda: self._del_from_listbox(self.pd_actions_listbox)).pack(side=tk.LEFT, padx=2) - ttk.Button(act_btn_frame, text="Выбрать из списка", command=self._select_from_actions_list).pack(side=tk.LEFT, padx=2) - self.pd_actions_listbox = tk.Listbox(act_tab, height=6) - self.pd_actions_listbox.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) - # СЗИ def_tab = ttk.Frame(nb2) nb2.add(def_tab, text="Средства защиты") @@ -822,9 +783,9 @@ class DokoGenApp: self.defense_tools_listbox = tk.Listbox(def_tab, height=6) self.defense_tools_listbox.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) - # Пользователи + # Пользователи ОКИИ user_tab = ttk.Frame(nb2) - nb2.add(user_tab, text="Пользователи ИС") + nb2.add(user_tab, text="Пользователи ОКИИ") self.users_listbox = tk.Listbox(user_tab, height=6) self.users_listbox.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) btnf = ttk.Frame(user_tab) @@ -886,7 +847,7 @@ class DokoGenApp: frame = ttk.Frame(scrollable, padding=10) frame.pack(fill=tk.BOTH, expand=True) - ttk.Label(frame, text="Сотрудники, имеющие доступ к ПДн", + ttk.Label(frame, text="Сотрудники, имеющие доступ к объектам КИИ", font=('Arial', 11, 'bold')).pack(anchor=tk.W, pady=(0, 10)) # Заголовки @@ -971,102 +932,6 @@ class DokoGenApp: if emp['position'] or emp['fio']: self.model.employees_access.append(emp) - # ---------- ВКЛАДКА "БУМАЖНЫЕ ДОКУМЕНТЫ" ---------- - def _build_paper_tab(self): - tab = ttk.Frame(self.notebook) - self.notebook.add(tab, text="Бумажные документы") - - canvas = tk.Canvas(tab, highlightthickness=0) - scrollbar = ttk.Scrollbar(tab, orient=tk.VERTICAL, command=canvas.yview) - scrollable = ttk.Frame(canvas) - scrollable.bind("", - lambda e: canvas.configure(scrollregion=canvas.bbox("all"))) - canvas.create_window((0, 0), window=scrollable, anchor="nw") - canvas.configure(yscrollcommand=scrollbar.set) - canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) - scrollbar.pack(side=tk.RIGHT, fill=tk.Y) - - frame = ttk.Frame(scrollable, padding=10) - frame.pack(fill=tk.BOTH, expand=True) - - ttk.Label(frame, text="Бумажные носители ПДн (документы)", - font=('Arial', 11, 'bold')).pack(anchor=tk.W, pady=(0, 10)) - - # Заголовки - header = ttk.Frame(frame) - header.pack(fill=tk.X) - for w, col in [(45, "Название документа"), (45, "Место хранения")]: - ttk.Label(header, text=col, font=('', 9, 'bold'), - width=w, anchor=tk.W).pack(side=tk.LEFT, padx=2) - - self._paper_frame = ttk.Frame(frame) - self._paper_frame.pack(fill=tk.X, pady=5) - - btn_frame = ttk.Frame(frame) - btn_frame.pack(fill=tk.X, pady=5) - ttk.Button(btn_frame, text="+ Добавить документ", - command=self._add_paper_row_ui).pack(side=tk.LEFT, padx=2) - ttk.Button(btn_frame, text="- Удалить выбранный", - command=self._delete_paper_selected).pack(side=tk.LEFT, padx=2) - - self._paper_count_label = ttk.Label(frame, text="Добавлено: 0") - self._paper_count_label.pack(anchor=tk.W, pady=5) - - self._paper_entries = [] - # Загрузка существующих - for p in self.model.paper_documents_list: - self._add_paper_row(p.name, p.storage) - self._update_paper_count() - - def _add_paper_row(self, name="", storage=""): - """Добавляет одну строку документа.""" - row = ttk.Frame(self._paper_frame) - row.pack(fill=tk.X, pady=2) - - name_entry = ttk.Entry(row, width=45) - name_entry.insert(0, name) - name_entry.pack(side=tk.LEFT, padx=2) - - storage_entry = ttk.Entry(row, width=45) - storage_entry.insert(0, storage) - storage_entry.pack(side=tk.LEFT, padx=2) - - del_btn = ttk.Button(row, text="✖", width=3, - command=lambda: self._delete_paper_row(row)) - del_btn.pack(side=tk.LEFT, padx=5) - - self._paper_entries.append((row, name_entry, storage_entry)) - self._update_paper_count() - - def _add_paper_row_ui(self): - self._add_paper_row() - - def _delete_paper_row(self, row_frame): - self._paper_entries = [e for e in self._paper_entries if e[0] != row_frame] - row_frame.destroy() - self._update_paper_count() - - def _delete_paper_selected(self): - if self._paper_entries: - row_frame = self._paper_entries[-1][0] - self._delete_paper_row(row_frame) - - def _update_paper_count(self): - count = len(self._paper_entries) - if hasattr(self, '_paper_count_label'): - self._paper_count_label.config(text=f"Добавлено: {count}") - - def _collect_paper_documents(self): - """Собирает данные бумажных документов из GUI в модель.""" - self.model.paper_documents_list.clear() - for row, name_e, storage_e in self._paper_entries: - name = name_e.get().strip() - storage = storage_e.get().strip() - if name or storage: - self.model.paper_documents_list.append( - PaperDocument(name=name, storage=storage) - ) - # ---------- ВКЛАДКА "ГЕНЕРАЦИЯ" ---------- def _build_generation_tab(self): tab = ttk.Frame(self.notebook) @@ -1142,29 +1007,21 @@ class DokoGenApp: setattr(c, key, entry.get().strip()) c.contract_number = self.doc_number_var.get() c.contract_date = self.doc_date_var.get() + if hasattr(self, 'okved_extra_listbox'): + c.okved_extra = list(self.okved_extra_listbox.get(0, tk.END)) def _collect_is(self): - """Собирает данные ИС из GUI в модель.""" + """Собирает данные объекта КИИ из GUI в модель.""" if self._current_is_idx is not None and self._current_is_idx < len(self.model.information_systems): isys = self.model.information_systems[self._current_is_idx] isys.name = self.is_name_entry.get().strip() isys.description = self.is_description_entry.get().strip() - if hasattr(self, 'is_structure_entry'): - isys.structure = self.is_structure_entry.get().strip() if hasattr(self, 'is_software_entry'): isys.software = self.is_software_entry.get().strip() isys.is_local_network = self.is_lan_var.get() isys.is_internet = self.is_internet_var.get() - isys.threat_type = self.is_threat_var.get() - isys.defence_level = self.is_defence_var.get() - cats = [c for c, v in self.category_vars if v.get()] - isys.personal_data_category = cats - if hasattr(self, 'is_pd_count_entry'): - isys.pd_count = self.is_pd_count_entry.get().strip() or 'менее 100 000' + isys.category = self.is_category_var.get() # Списки - isys.personal_data_list = list(self.pd_listbox.get(0, tk.END)) - isys.pd_subjects_list = list(self.pd_subjects_listbox.get(0, tk.END)) - isys.pd_actions_list = list(self.pd_actions_listbox.get(0, tk.END)) isys.defense_tools_list = list(self.defense_tools_listbox.get(0, tk.END)) isys.users_list = list(self.users_listbox.get(0, tk.END)) @@ -1199,6 +1056,12 @@ class DokoGenApp: self.doc_number_var.set(c.contract_number or "") self.doc_date_var.set(c.contract_date or "") + # Дополнительные ОКВЭД + if hasattr(self, 'okved_extra_listbox'): + self.okved_extra_listbox.delete(0, tk.END) + for item in (c.okved_extra or []): + self.okved_extra_listbox.insert(tk.END, item) + # Склонения idx_map = {'nomn': 1, 'gent': 2, 'datv': 3, 'accs': 4, 'ablt': 5, 'loct': 6} for code, entry in self.decl_entries.items(): @@ -1236,15 +1099,6 @@ class DokoGenApp: ) self._update_emp_count() - # Бумажные документы - if hasattr(self, '_paper_entries'): - for row_frame, _, _ in self._paper_entries: - row_frame.destroy() - self._paper_entries.clear() - for p in c.paper_documents_list: - self._add_paper_row(p.name, p.storage) - self._update_paper_count() - # Шаблоны self._on_templates_changed() @@ -1296,64 +1150,6 @@ class DokoGenApp: self.log(f"🏙 Город определён: {city}") # ==================== АВТОРАСЧЁТ УРОВНЯ ЗАЩИЩЁННОСТИ ==================== - def _auto_calc_defence_level(self): - """Автоматический расчёт УЗ по ПП №1119: категории ПДн, тип субъектов, - кол-во записей, тип угроз.""" - if self._current_is_idx is None: - return - - categories = [cat for cat, var in self.category_vars if var.get()] - pd_count = self.is_pd_count_var.get() if hasattr(self, 'is_pd_count_var') else 'менее 100 000' - threat_type = self.is_threat_var.get() if hasattr(self, 'is_threat_var') else '3' - is_large = 'более' in pd_count.lower() - # Автоопределение типа субъектов: если в Субъектах ПДн выбраны только "Сотрудники" → работники, иначе → иные - subj_items = self.pd_subjects_listbox.get(0, tk.END) if hasattr(self, 'pd_subjects_listbox') else [] - has_only_employees = len(subj_items) > 0 and all('сотрудник' in s.lower() for s in subj_items) - is_employees = has_only_employees or len(subj_items) == 0 - - # Таблица ПП №1119: (категория, тип_субъектов, объём, тип_угроз) → УЗ - def lookup(cat_group, employees, large, threat): - # Общедоступные → всегда 4 - if cat_group == 'public': - return 4 - # Специальные/биометрические - if cat_group == 'special_bio': - tbl = { - ('emp', False, '1'): 2, ('emp', True, '1'): 1, - ('other', False, '1'): 1, ('other', True, '1'): 1, - ('emp', False, '2'): 2, ('emp', True, '2'): 1, - ('other', False, '2'): 2, ('other', True, '2'): 1, - ('emp', False, '3'): 3, ('emp', True, '3'): 2, - ('other', False, '3'): 3, ('other', True, '3'): 2, - } - else: # иные - tbl = { - ('emp', False, '1'): 3, ('emp', True, '1'): 2, - ('other', False, '1'): 2, ('other', True, '1'): 1, - ('emp', False, '2'): 4, ('emp', True, '2'): 3, - ('other', False, '2'): 3, ('other', True, '2'): 2, - ('emp', False, '3'): 4, ('emp', True, '3'): 4, - ('other', False, '3'): 4, ('other', True, '3'): 3, - } - key = ('emp' if employees else 'other', large, threat) - return tbl.get(key, 4) - - # Определяем группу категорий - if 'общедоступные' in categories and not any(c in categories for c in ['специальные', 'биометрические', 'иные']): - cat_group = 'public' - elif any(c in categories for c in ['специальные', 'биометрические']): - cat_group = 'special_bio' - else: - cat_group = 'other' - - level = lookup(cat_group, is_employees, is_large, threat_type) - self.is_defence_var.set(str(level)) - - def _on_categories_or_count_changed(self): - """Обработчик изменения категорий ПДн или количества записей.""" - if self._current_is_idx is not None: - self._auto_calc_defence_level() - # ==================== КОМИССИЯ ==================== def _add_commission_row(self, role="Член комиссии", position="", fio=""): row = ttk.Frame(self._comm_members_frame) @@ -1421,28 +1217,15 @@ class DokoGenApp: self.is_name_entry.insert(0, isys.name or "") self.is_description_entry.delete(0, tk.END) self.is_description_entry.insert(0, isys.description or "") - if hasattr(self, 'is_structure_entry'): - self.is_structure_entry.delete(0, tk.END) - self.is_structure_entry.insert(0, isys.structure or "") if hasattr(self, 'is_software_entry'): self.is_software_entry.delete(0, tk.END) self.is_software_entry.insert(0, isys.software or "") self.is_lan_var.set(isys.is_local_network) self.is_internet_var.set(isys.is_internet) - self.is_threat_var.set(isys.threat_type or "3") - self.is_defence_var.set(isys.defence_level or "4") - for cat, var in self.category_vars: - var.set(cat in isys.personal_data_category) - # Заполняем кол-во записей ПДн - if hasattr(self, 'is_pd_count_entry'): - self.is_pd_count_entry.delete(0, tk.END) - self.is_pd_count_entry.insert(0, isys.pd_count or 'менее 100 000') + self.is_category_var.set(isys.category or "без категории") # Списки for lb, items in [ - (self.pd_listbox, isys.personal_data_list or []), - (self.pd_subjects_listbox, isys.pd_subjects_list or []), - (self.pd_actions_listbox, isys.pd_actions_list or []), (self.defense_tools_listbox, isys.defense_tools_list or []), (self.users_listbox, isys.users_list or []), ]: @@ -1471,13 +1254,7 @@ class DokoGenApp: description=src.description, is_local_network=src.is_local_network, is_internet=src.is_internet, - defence_level=src.defence_level, - subject_type=src.subject_type, - threat_type=src.threat_type, - personal_data_category=list(src.personal_data_category), - personal_data_list=list(src.personal_data_list), - pd_actions_list=list(src.pd_actions_list), - pd_subjects_list=list(src.pd_subjects_list), + category=src.category, defense_tools_list=list(src.defense_tools_list), users_list=list(src.users_list), ) @@ -1497,16 +1274,6 @@ class DokoGenApp: self.is_listbox.selection_set(0) self._on_is_select() - def _add_pd_item(self): - item = simpledialog.askstring("Добавить ПДн", "Введите персональные данные:") - if item: - self.pd_listbox.insert(tk.END, item) - - def _add_pd_subjects_item(self): - item = simpledialog.askstring("Добавить категорию", "Введите категорию субъектов:") - if item: - self.pd_subjects_listbox.insert(tk.END, item) - def _add_defense_item(self): item = simpledialog.askstring("Добавить СЗИ", "Введите средство защиты:") if item: @@ -1533,7 +1300,7 @@ class DokoGenApp: if not names: messagebox.showinfo( "Выбор пользователей", - "Список пуст.\nЗаполните вкладку «Пользователи» (сотрудники с доступом к ПДн) " + "Список пуст.\nЗаполните вкладку «Пользователи» (сотрудники с доступом к объектам КИИ) " "или добавьте пользователей в другой ИС." ) return @@ -1607,44 +1374,6 @@ class DokoGenApp: dialog.wait_window() - def _add_pd_action_item(self): - item = simpledialog.askstring("Добавить действие", "Введите действие с ПДн:") - if item: - self.pd_actions_listbox.insert(tk.END, item) - - def _select_from_actions_list(self): - """Выбор действий с ПДн из JSON словаря.""" - import json - dict_path = os.path.join(os.path.dirname(__file__), 'dictionaries', 'pd_actions.json') - try: - with open(dict_path, 'r', encoding='utf-8') as f: - items = json.load(f) - self._select_from_dialog("Выбор действий с персональными данными", items, self.pd_actions_listbox) - except Exception as e: - messagebox.showerror("Ошибка", f"Не удалось загрузить словарь: {e}") - - def _select_from_pd_list(self): - """Выбор ПДн из JSON словаря.""" - import json - dict_path = os.path.join(os.path.dirname(__file__), 'dictionaries', 'pd_items.json') - try: - with open(dict_path, 'r', encoding='utf-8') as f: - items = json.load(f) - self._select_from_dialog("Выбор персональных данных", items, self.pd_listbox) - except Exception as e: - messagebox.showerror("Ошибка", f"Не удалось загрузить словарь: {e}") - - def _select_from_subjects_list(self): - """Выбор категорий субъектов из JSON словаря.""" - import json - dict_path = os.path.join(os.path.dirname(__file__), 'dictionaries', 'pd_categories.json') - try: - with open(dict_path, 'r', encoding='utf-8') as f: - items = json.load(f) - self._select_from_dialog("Выбор категорий субъектов ПДн", items, self.pd_subjects_listbox) - except Exception as e: - messagebox.showerror("Ошибка", f"Не удалось загрузить словарь: {e}") - def _select_from_defense_list(self): """Выбор СЗИ из JSON словаря.""" import json @@ -1656,6 +1385,84 @@ class DokoGenApp: except Exception as e: messagebox.showerror("Ошибка", f"Не удалось загрузить словарь: {e}") + # ==================== ОКВЭД ==================== + def _load_okved_dict(self): + """Загружает словарь ОКВЭД (dictionaries/okved.json).""" + import json + dict_path = os.path.join(os.path.dirname(__file__), 'dictionaries', 'okved.json') + try: + with open(dict_path, 'r', encoding='utf-8') as f: + return json.load(f) + except Exception as e: + messagebox.showerror("Ошибка", f"Не удалось загрузить словарь ОКВЭД:\n{e}") + return [] + + def _select_main_okved(self): + """Выбор основного ОКВЭД из словаря (всегда один).""" + items = self._load_okved_dict() + if not items: + return + current = self._company_entries.get('okved').get().strip() if 'okved' in self._company_entries else '' + dialog = tk.Toplevel(self.root) + dialog.title("Основной ОКВЭД") + dialog.geometry("500x500") + dialog.transient(self.root) + dialog.grab_set() + + frame = ttk.Frame(dialog, padding=10) + frame.pack(fill=tk.BOTH, expand=True) + ttk.Label(frame, text="Выберите основной ОКВЭД (один):", + font=('Arial', 10, 'bold')).pack(anchor=tk.W, pady=(0, 5)) + + canvas = tk.Canvas(frame, highlightthickness=0) + scrollbar = ttk.Scrollbar(frame, orient=tk.VERTICAL, command=canvas.yview) + check_frame = ttk.Frame(canvas) + check_frame.bind("", lambda e: canvas.configure(scrollregion=canvas.bbox("all"))) + canvas.create_window((0, 0), window=check_frame, anchor="nw") + canvas.configure(yscrollcommand=scrollbar.set) + canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) + scrollbar.pack(side=tk.RIGHT, fill=tk.Y) + + # Радио-кнопки: основной всегда один + var = tk.StringVar(value=current) + for item in items: + ttk.Radiobutton(check_frame, text=item, variable=var, + value=item).pack(anchor=tk.W, pady=1) + + btn_frame = ttk.Frame(dialog, padding=10) + btn_frame.pack(fill=tk.X) + + def _apply(): + val = var.get() + if val: + entry = self._company_entries.get('okved') + if entry: + entry.delete(0, tk.END) + entry.insert(0, val) + dialog.destroy() + + ttk.Button(btn_frame, text="✅ Применить", command=_apply).pack(side=tk.RIGHT, padx=2) + ttk.Button(btn_frame, text="Отмена", command=dialog.destroy).pack(side=tk.RIGHT, padx=2) + dialog.wait_window() + + def _select_extra_okved(self): + """Выбор дополнительных ОКВЭД из словаря (можно несколько).""" + items = self._load_okved_dict() + if not items: + return + self._select_from_dialog("Выбор дополнительных ОКВЭД", items, self.okved_extra_listbox) + + def _add_okved_manual(self): + """Ручное добавление дополнительного ОКВЭД.""" + item = simpledialog.askstring("Добавить ОКВЭД", "Введите код ОКВЭД:") + if item: + self.okved_extra_listbox.insert(tk.END, item.strip()) + + def _delete_okved_extra(self): + sel = self.okved_extra_listbox.curselection() + if sel: + self.okved_extra_listbox.delete(sel[0]) + # ==================== ШАБЛОНЫ ==================== def select_templates(self): folder = filedialog.askdirectory(title="Папка шаблонов") @@ -1720,7 +1527,6 @@ class DokoGenApp: self._collect_commission() self._collect_is() self._collect_employees() - self._collect_paper_documents() # Проверка if not self._validate_before_generate(): @@ -1939,7 +1745,24 @@ class DokoGenApp: if data.get('ispdnDepartment'): self.model.security_department = data['ispdnDepartment'] - # Информационные системы + # ОКВЭД (основной и дополнительные) + if data.get('okved'): + entry = self._company_entries.get('okved') + if entry: + entry.delete(0, tk.END) + entry.insert(0, data['okved']) + if data.get('okvedExtra'): + if hasattr(self, 'okved_extra_listbox'): + self.okved_extra_listbox.delete(0, tk.END) + extra = data['okvedExtra'] + if isinstance(extra, str): + parts = [p.strip() for p in extra.replace(';', '\n').split('\n') if p.strip()] + else: + parts = [str(p).strip() for p in extra if str(p).strip()] + for p in parts: + self.okved_extra_listbox.insert(tk.END, p) + + # Объекты КИИ if data.get('informationSystems'): self.model.information_systems.clear() for is_data in data['informationSystems']: @@ -1948,17 +1771,12 @@ class DokoGenApp: description=is_data.get('description', ''), is_local_network=is_data.get('is_has_lan', is_data.get('is_local_network', False)), is_internet=is_data.get('isInternet', False), - pd_count=is_data.get('personalDataCount', 'менее 100 000'), - personal_data_list=is_data.get('personalDataList', []), - pd_subjects_list=is_data.get('pd_subjects_list', []), + category=is_data.get('category', ''), defense_tools_list=is_data.get('defense_tools', []), users_list=is_data.get('usersList', []), - personal_data_category=is_data.get('personalDataCategory', []), - processing_modes=is_data.get('processing_mode', ''), - purpose=is_data.get('description', ''), - room=is_data.get('room', ''), + purpose=is_data.get('purpose', is_data.get('description', '')), structure=is_data.get('structure', ''), - pd_actions_list=is_data.get('pd_actions', []), + software=is_data.get('software', ''), ) self.model.information_systems.append(isys) self._refresh_is_listbox() @@ -2005,26 +1823,6 @@ class DokoGenApp: ) self._update_emp_count() - # Бумажные документы (из «Названия документов» / «Место хранения») - if data.get('paperDocuments'): - self.model.paper_documents_list.clear() - names = [n.strip() for n in data['paperDocuments'].replace('\n', ';').split(';') if n.strip()] - storages = [s.strip() for s in (data.get('paperStorage') or '').replace('\n', ';').split(';') if s.strip()] - for i, name in enumerate(names): - storage = storages[i] if i < len(storages) else (storages[0] if storages else '') - self.model.paper_documents_list.append( - PaperDocument(name=name, storage=storage) - ) - # Обновляем GUI - if hasattr(self, '_paper_entries'): - for row_frame, _, _ in self._paper_entries: - row_frame.destroy() - self._paper_entries.clear() - for p in self.model.paper_documents_list: - self._add_paper_row(p.name, p.storage) - self._update_paper_count() - self.log(f" Бумажные документы: {len(self.model.paper_documents_list)} шт.") - self.log("✅ Импорт применён") self.root.after(0, _apply) @@ -2069,7 +1867,6 @@ class DokoGenApp: self._collect_commission() self._collect_is() self._collect_employees() - self._collect_paper_documents() try: with open(filename, 'w', encoding='utf-8') as f: json.dump(self.model.to_dict(), f, ensure_ascii=False, indent=2) @@ -2095,7 +1892,6 @@ class DokoGenApp: self._collect_commission() self._collect_is() self._collect_employees() - self._collect_paper_documents() try: os.makedirs(os.path.dirname(self._autosave_path), exist_ok=True) with open(self._autosave_path, 'w', encoding='utf-8') as f: diff --git a/dokogen/variables.py b/dokogen/variables.py index 140199e..d44914b 100644 --- a/dokogen/variables.py +++ b/dokogen/variables.py @@ -57,7 +57,8 @@ VARIABLE_DEFS = [ ('{{company_ogrn_date}}', 'A', 'Дата ОГРН', 'company:ogrn_date'), ('{{company_kpp}}', 'A', 'КПП', 'company:kpp'), ('{{security_department}}', 'A', 'Наименование структурного подразделения по безопасности', 'company:security_department'), - ('{{item.pd_actions}}', 'B', 'Действия с ПДн (в цикле ИС)', 'item:pd_actions'), + ('{{company_okved}}', 'A', 'Основной ОКВЭД', 'company:okved'), + ('{{company_okved_extra}}', 'A', 'Дополнительные ОКВЭД', 'company:okved_extra'), # === Комиссия === ('{{commission_list}}', 'A', 'Список комиссии', 'commission_list'), @@ -69,22 +70,15 @@ VARIABLE_DEFS = [ ('{{document_number}}', 'A', 'Номер документа', 'doc_number'), # === 187-ФЗ === - ('{{is_security_requirements}}', 'B', 'Требования уровня защищённости', 'security_reqs'), - ('{{is_list}}', 'B', 'Список всех ИС (нумерованный)', 'is_list'), - ('{{is_name}}', 'B', 'Наименование первой ИС', 'is:name'), - ('{{name}}', 'B', 'Название ИС (алиас)', 'is:name'), - ('{{description}}', 'B', 'Описание ИС (алиас)', 'is:description'), - ('{{category}}', 'B', 'Категория ИС (алиас)', 'is:category'), + ('{{is_security_requirements}}', 'B', 'Требования к защите объекта КИИ', 'security_reqs'), + ('{{is_list}}', 'B', 'Список всех объектов КИИ (нумерованный)', 'is_list'), + ('{{is_name}}', 'B', 'Наименование первого объекта КИИ', 'is:name'), + ('{{name}}', 'B', 'Название объекта КИИ (алиас)', 'is:name'), + ('{{description}}', 'B', 'Описание объекта КИИ (алиас)', 'is:description'), + ('{{category}}', 'B', 'Категория значимости объекта КИИ (алиас)', 'is:category'), ('{{number}}', 'B', 'Номер (алиас)', 'doc_number'), - ('{{pd_count}}', 'B', 'Количество записей ПДн (алиас)', 'is:pd_count'), - ('{{pd_list}}', 'B', 'Список ПДн (алиас)', 'is:pd_list'), - ('{{pd_subjects}}', 'B', 'Субъекты ПДн (алиас)', 'is:pd_subjects'), - ('{{threat_type}}', 'B', 'Тип угроз (алиас)', 'is:threat_type'), - ('{{users}}', 'B', 'Пользователи ИС (алиас)', 'is:users'), - ('{{defence_level}}', 'B', 'Уровень защищённости (алиас)', 'is:defence_level'), + ('{{users}}', 'B', 'Пользователи ОКИИ (алиас)', 'is:users'), ('{{full_name_genitive}}', 'A', 'Полное наименование (род.п., алиас)', 'decl:full_name:gent'), - ('{{document}}', 'A', 'Название бумажного документа (алиас)', 'company:paper_documents'), - ('{{storage}}', 'A', 'Место хранения (алиас)', 'company:paper_storage'), ('{{responsible_fio_accs}}', 'A', 'ФИО ответственного (вин.п.)', 'fio_acl:responsible'), ('{{commission1}}', 'A', 'Должность члена комиссии 1', 'commission_member:0:position'), ('{{commission1_fio}}', 'A', 'ФИО члена комиссии 1', 'commission_member:0:fio'), @@ -221,13 +215,11 @@ def _add_is_indexed_vars(replacements: Dict[str, str], isys, idx: int): 'is_address': getattr(isys, 'address', '') or '—', 'is_description': isys.description or '—', 'is_has_internet': 'да' if isys.is_internet else 'нет', - 'is_defence_level': str(isys.defence_level or '—'), - 'is_category': ', '.join(isys.personal_data_category) or isys.category or '—', - 'is_pd_list': ', '.join(isys.personal_data_list or []) or '—', + 'is_category': isys.category or '—', 'is_users': ', '.join(isys.users_list or []) or '—', - 'is_pd_count': str(isys.pd_count or '—'), 'is_purpose': isys.purpose or '—', 'is_structure': getattr(isys, 'structure', '') or '—', + 'is_software': getattr(isys, 'software', '') or '—', } for f, v in fields.items(): replacements[f'{{{{{f}_{idx}}}}}'] = str(v) @@ -295,68 +287,16 @@ def _build_loop_data(company: Company, is_list: List) -> Dict[str, List[Dict]]: 'address': getattr(isys, 'address', '') or '—', 'is_has_lan': 'Да' if isys.is_local_network else 'Нет', 'has_internet': 'да' if isys.is_internet else 'нет', - 'defence_level': str(isys.defence_level or '—'), - 'threat_type': str(isys.threat_type or '3'), - 'category': ', '.join(isys.personal_data_category) or isys.category or '—', - 'pd_list': ', '.join(isys.personal_data_list or []) or '—', - 'pd_count': str(isys.pd_count or '—'), + 'category': isys.category or '—', 'users': ', '.join(isys.users_list or []) or '—', 'users_loop': users_loop, 'defense_tools_list': ', '.join(isys.defense_tools_list or []) or '—', - 'processing_modes': isys.processing_modes or '—', - 'pd_subjects': '; '.join(isys.pd_subjects_list or []) or '—', 'is_purpose': isys.purpose or '—', 'structure': getattr(isys, 'structure', '') or '—', 'software': getattr(isys, 'software', '') or '—', - 'pd_actions': '; '.join(isys.pd_actions_list or []) or '—', }) loops['information_systems'] = items - # Бумажные документы - def _storage_with_prep(storage: str) -> str: - """«сейф» → «в сейфе» (предлог + предложный падеж). - Если уже начинается с предлога — оставить как есть.""" - if not storage: - return '' - s = storage.strip() - low = s.lower() - if low.startswith(('в ', 'на ', 'под ', 'за ', 'у ', 'при ', 'из ', 'со ', 'во ', 'над ', 'перед ')): - return s - words = s.split() - out = [] - # Исключения предложного падежа: «в шкафу», «в углу» (не «в шкафе») - loct_exceptions = {'шкаф': 'шкафу', 'угол': 'углу', 'край': 'краю', 'рот': 'рту', 'мост': 'мосту'} - for w in words: - core = w.strip('.,;') - if not core or core.isdigit() or core.startswith('№'): - out.append(w) - continue - if core.lower() in loct_exceptions: - out.append(loct_exceptions[core.lower()]) - continue - try: - p = dc.morph.parse(core)[0] - inf = p.inflect({'loct'}) - if inf: - out.append(inf.word) - continue - except Exception: - pass - out.append(w) - return 'в ' + ' '.join(out) - - paper_list = getattr(company, 'paper_documents_list', None) or [] - if paper_list: - items = [] - for p in paper_list: - storage = p.storage or '' - items.append({ - 'document': p.name or '', - 'storage': _storage_with_prep(storage), # «в сейфе» - 'storage_raw': storage, # «сейф» (как введено) - }) - loops['paper_documents'] = items - # Сотрудники с доступом employees = getattr(company, 'employees_access', None) or [] if employees: @@ -448,11 +388,6 @@ def _resolve(source: str, ctx: Dict) -> str: 'name': 'name', 'description': 'description', 'category': 'category', - 'defence_level': 'defence_level', - 'threat_type': 'threat_type', - 'pd_list': lambda: ', '.join(first.personal_data_list or []), - 'pd_count': 'pd_count', - 'pd_subjects': lambda: '; '.join(first.pd_subjects_list or []), 'users': lambda: ', '.join(first.users_list or []), } if field in field_map: @@ -501,21 +436,25 @@ def _get_fio(company: Company, who: str) -> str: def _get_security_text(ctx: Dict) -> str: - """Возвращает текст требований для уровня защищённости.""" - defence_level = ctx.get('extended_fields', {}).get('defence_level', '4') + """Возвращает текст требований к защите объекта КИИ по категории значимости.""" is_list = ctx.get('is_list', []) - if not defence_level and is_list: - defence_level = str(getattr(is_list[0], 'defence_level', '4') or '4') - dl = str(defence_level).strip()[0] + category = '' + if is_list: + category = str(getattr(is_list[0], 'category', '') or '').strip() TEXTS = { - '1': """Для обеспечения 1-го уровня защищённости...""", - '2': """Для обеспечения 2-го уровня защищённости...""", - '3': """Для обеспечения 3-го уровня защищённости...""", - '4': """Для обеспечения 4-го уровня защищённости персональных данных необходимо: -— режим безопасности помещений; -— сохранность носителей ПДн; -— утверждение перечня лиц с доступом к ПДн; -— использование сертифицированных СЗИ.""", + '1': """Для значимого объекта КИИ 1-й категории значимости необходимо: +— создание системы безопасности значимого объекта КИИ; +— обеспечение защиты информации от компьютерных атак; +— категорирование объектов КИИ и контроль его результатов.""", + '2': """Для значимого объекта КИИ 2-й категории значимости необходимо: +— создание системы безопасности значимого объекта КИИ; +— обеспечение защиты информации от компьютерных атак.""", + '3': """Для значимого объекта КИИ 3-й категории значимости необходимо: +— обеспечение защиты информации от компьютерных атак.""", } - return TEXTS.get(dl, TEXTS['4']) + if category in TEXTS: + return TEXTS[category] + return """Для объекта КИИ, не являющегося значимым, необходимо: +— принятие мер по защите информации, предусмотренных законодательством РФ о безопасности КИИ.""" + diff --git a/dokogen/verify.py b/dokogen/verify.py index 249bdc7..9f93445 100644 --- a/dokogen/verify.py +++ b/dokogen/verify.py @@ -65,6 +65,17 @@ _ADDRESS_ABBR = { 'СТ': 'ст.', 'Ш': 'ш.', 'Р-Н': 'р-н.', 'Б-Р': 'б-р.', 'МКР': 'мкр.', 'НАБ': 'наб.', 'ТУП': 'туп.', 'ПР-Д': 'пр-д.', 'КРП': 'крп.', 'Г.': 'г.', 'УЛ.': 'ул.', 'Д.': 'д.', 'Р-Н.': 'р-н.', + 'Г.О': 'г.о.', 'Г.О.': 'г.о.', 'ГО': 'г.о.', 'М.О': 'м.о.', 'М.О.': 'м.о.', +} + +# Слова, которые в адресах пишутся со строчной буквы (кроме случаев, +# когда стоят первым словом после точки — «г. Электроугли» и т.п.) +_ADDRESS_LOWERCASE = { + 'область', 'край', 'республика', 'округ', 'район', 'город', 'поселок', + 'посёлок', 'деревня', 'село', 'станица', 'хутор', 'аул', 'улица', + 'проспект', 'переулок', 'бульвар', 'шоссе', 'набережная', 'площадь', + 'проезд', 'тупик', 'аллея', 'линия', 'дом', 'строение', 'корпус', + 'квартира', 'офис', 'помещение', 'этаж', 'комната', 'участок', } @@ -105,7 +116,12 @@ def _normalize_text(text): def normalize_address(text): - """«Г Крымск» → «г. Крымск», «Р-Н» → «р-н.» и т.п.""" + """«Г Крымск» → «г. Крымск», «Р-Н» → «р-н.», «МОСКОВСКАЯ ОБЛАСТЬ» → «Московская область». + + Приводит адрес к нормальному регистру: каждое слово с заглавной буквы, + адресные сокращения (г., ул., д., обл., г.о.) и административные термины + (область, район, город, дом и т.п.) — со строчной. + """ if not text: return '' parts = re.split(r'(\s+|[,\;])', text) @@ -115,7 +131,21 @@ def normalize_address(text): out.append(_ADDRESS_ABBR[p.strip().upper()]) else: out.append(p) - return ''.join(out) + res = ''.join(out) + # КАПС → нормальный регистр по словам + res = _normalize_text(res) + # Адресные термины — со строчной буквы (сохраняя пунктуацию) + words = res.split(' ') + fixed = [] + for w in words: + m = re.match(r'^([^\w]*)([\w\-]+)([^\w]*)$', w, re.UNICODE) + if m: + pre, core, post = m.group(1), m.group(2), m.group(3) + if core.lower() in _ADDRESS_LOWERCASE: + fixed.append(pre + core.lower() + post) + continue + fixed.append(w) + return ' '.join(fixed) def _fmt_date(value):