From 2ff0eaa538d79078d76720b20a653f4c0d4de0da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=98=D0=B3=D0=BE=D1=80=D1=8C=20=D0=9D=D0=B0=D0=B4=D0=B5?= =?UTF-8?q?=D0=B8=D0=BD?= Date: Fri, 9 Jan 2026 16:55:52 +0300 Subject: [PATCH] =?UTF-8?q?fix:=20=D0=B8=D1=81=D0=BF=D1=80=D0=B0=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D1=8B=20=D0=BE=D1=88=D0=B8=D0=B1=D0=BA=D0=B8?= =?UTF-8?q?=20=D0=B2=20=D1=81=D0=B8=D1=81=D1=82=D0=B5=D0=BC=D0=B5=20=D1=83?= =?UTF-8?q?=D0=B2=D0=B5=D0=B4=D0=BE=D0=BC=D0=BB=D0=B5=D0=BD=D0=B8=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Добавлен эндпоинт удаления комментариев к заявкам (DELETE /api/v1/requests/{pk}/comments/{comment_id}/) - Исправлены references на переменную в сигналах комментариев (comment -> instance) - Добавлена обработка статуса cancelled для заявок в notification_type_map - Обновлены процессоры уведомлений в task_base.py (правильные поля моделей) - Создан скрипт генерации тестовых пользователей (scripts/create_test_users.py) --- backend/api/v1/requests_app/views.py | 30 ++ backend/notifications/task_base.py | 181 ++++++------ backend/notifications/tasks.py | 2 + backend/requests_app/notification_signals.py | 10 +- backend/scripts/create_test_users.py | 277 +++++++++++++++++++ 5 files changed, 409 insertions(+), 91 deletions(-) create mode 100644 backend/scripts/create_test_users.py diff --git a/backend/api/v1/requests_app/views.py b/backend/api/v1/requests_app/views.py index 4640d280..118161af 100644 --- a/backend/api/v1/requests_app/views.py +++ b/backend/api/v1/requests_app/views.py @@ -367,6 +367,36 @@ class RequestViewSet(viewsets.ModelViewSet): logger.info(f"[RequestViewSet.comments] POST: comment created id={saved.id}") return Response(ser.data, status=status.HTTP_201_CREATED) + @action(detail=True, methods=["delete"], url_path="comments/(?P[^/.]+)") + def delete_comment(self, request: DRFRequest, pk: int | str | None = None, comment_id: int | str | None = None) -> Response: + """Удаление комментария. + + DELETE /api/v1/requests/{pk}/comments/{comment_id}/ + + Права: только автор комментария или staff. + """ + import logging + logger = logging.getLogger(__name__) + + req_obj = self.get_object() + + try: + comment = RequestComment.objects.get(id=comment_id, request=req_obj) + except RequestComment.DoesNotExist: + return Response({"detail": "Comment not found"}, status=status.HTTP_404_NOT_FOUND) + + # Проверка прав: только автор или staff + if not (request.user.is_staff or comment.author == request.user): + return Response( + {"detail": "You don't have permission to delete this comment"}, + status=status.HTTP_403_FORBIDDEN + ) + + logger.info(f"[RequestViewSet.delete_comment] user={request.user.id} deleting comment_id={comment_id}") + comment.delete() + + return Response(status=status.HTTP_204_NO_CONTENT) + # ---------- Экшены статусов (только бизнес-валидация) ---------- @action(detail=True, methods=["post"]) diff --git a/backend/notifications/task_base.py b/backend/notifications/task_base.py index 0f7a7a9f..aebc77f8 100644 --- a/backend/notifications/task_base.py +++ b/backend/notifications/task_base.py @@ -360,31 +360,36 @@ class EventNotificationProcessor(BaseNotificationProcessor): User = get_user_model() - event = CalendarEvent.objects.select_related('creator').get(id=event_id) + event = CalendarEvent.objects.select_related('created_by', 'employee', 'department').get(id=event_id) # Собираем всех получателей recipients = set() - # Участники события - for participant in event.participants.all(): - recipients.add(participant) + # Если событие отдела - уведомляем всех сотрудников отдела + if event.department: + recipients.update(self.get_department_employees([event.department.id])) - # Департаменты (если есть) - if event.departments.exists(): - recipients.update(self.get_department_employees(event.departments.all())) + # Если личное событие сотрудника - уведомляем только его + elif event.employee: + # Проверяем что сотрудник активен + if event.employee.is_active and event.employee.email_verified: + recipients.add(event.employee) - # Исключаем создателя и неактивных - recipients = self.get_active_employees(recipients) - if event.creator: - recipients.discard(event.creator) + # Если глобальное событие (нет department и employee) - уведомляем всех активных + else: + recipients = set(User.objects.filter(is_active=True, email_verified=True)) + + # Исключаем создателя + if event.created_by: + recipients.discard(event.created_by) if not recipients: - return self.log_result(event_id, 0) + return self.log_result('event', event_id, 'skipped', 0) # Отправляем уведомления notifications_sent = self.send_notifications( recipients=recipients, - notification_type='calendar_event_created', + notification_type='event_created', title=f'Новое событие: {event.title}', message=truncate_text(event.description or '', 150), content_object=event, @@ -397,10 +402,10 @@ class EventNotificationProcessor(BaseNotificationProcessor): } ) - return self.log_result(event_id, notifications_sent) + return self.log_result('event', event_id, 'created', notifications_sent) except Exception as exc: - return self.handle_exception(exc, f"event {event_id}") + self.handle_exception(exc) class PostNotificationProcessor(BaseNotificationProcessor): @@ -410,55 +415,47 @@ class PostNotificationProcessor(BaseNotificationProcessor): """Обработка уведомлений для поста""" try: from feed.models import Post + from django.contrib.auth import get_user_model - post = Post.objects.select_related('author').get(id=post_id) - - if post.is_deleted: - return {"status": "skipped", "reason": "deleted"} + User = get_user_model() + post = Post.objects.select_related('author', 'department').get(id=post_id) # Собираем получателей recipients = set() - # Департаменты - if post.target_departments.exists(): - recipients.update(self.get_department_employees(post.target_departments.all())) + # Если пост для конкретного отдела - уведомляем сотрудников отдела + if post.department: + recipients.update(self.get_department_employees([post.department.id])) + else: + # Иначе уведомляем всех активных сотрудников + recipients = set(User.objects.filter(is_active=True, email_verified=True)) - # Конкретные пользователи - for user in post.target_users.all(): - recipients.add(user) - - # Если нет конкретных получателей - всем активным - if not recipients: - from django.contrib.auth import get_user_model - User = get_user_model() - recipients = set(User.objects.filter(is_active=True)) - - # Исключаем автора и неактивных - recipients = self.get_active_employees(recipients) + # Исключаем автора if post.author: recipients.discard(post.author) if not recipients: - return self.log_result(post_id, 0) + return self.log_result('post', post_id, 'skipped', 0) # Отправляем уведомления notifications_sent = self.send_notifications( recipients=recipients, notification_type='feed_new_post', - title=f'Новая публикация от {post.author.get_full_name() if post.author else "Система"}', - message=truncate_text(post.content, 200), + title=f'{post.title}', + message=truncate_text(post.body, 200), content_object=post, action_url=f'/feed/?post={post.id}', metadata={ 'post_id': post.id, 'author_id': post.author.id if post.author else None, + 'post_type': post.type, } ) - return self.log_result(post_id, notifications_sent) + return self.log_result('post', post_id, 'created', notifications_sent) except Exception as exc: - return self.handle_exception(exc, f"post {post_id}") + self.handle_exception(exc) class RequestNotificationProcessor(BaseNotificationProcessor): @@ -475,76 +472,83 @@ class RequestNotificationProcessor(BaseNotificationProcessor): try: from requests_app.models import Request - request_obj = Request.objects.select_related('requester', 'handler').get(id=request_id) + request_obj = Request.objects.select_related('employee', 'approver').prefetch_related('recipients', 'cc_users').get(id=request_id) # Собираем получателей в зависимости от типа уведомления recipients = set() if notification_type == 'created': - # Уведомляем исполнителя и копии - if request_obj.handler: - recipients.add(request_obj.handler) + # Уведомляем согласующего и получателей + if request_obj.approver: + recipients.add(request_obj.approver) for user in request_obj.recipients.all(): - recipients.add(user) + if user.is_active and getattr(user, 'email_verified', True): + recipients.add(user) for user in request_obj.cc_users.all(): - recipients.add(user) + if user.is_active and getattr(user, 'email_verified', True): + recipients.add(user) elif notification_type == 'status_changed': - # Уведомляем создателя - if request_obj.requester: - recipients.add(request_obj.requester) + # Уведомляем создателя заявки + if request_obj.employee: + if request_obj.employee.is_active and getattr(request_obj.employee, 'email_verified', True): + recipients.add(request_obj.employee) elif notification_type == 'comment_added': - # Уведомляем всех участников кроме автора комментария - if request_obj.requester: - recipients.add(request_obj.requester) - if request_obj.handler: - recipients.add(request_obj.handler) + # Уведомляем всех участников + if request_obj.employee: + if request_obj.employee.is_active and getattr(request_obj.employee, 'email_verified', True): + recipients.add(request_obj.employee) + + if request_obj.approver: + if request_obj.approver.is_active and getattr(request_obj.approver, 'email_verified', True): + recipients.add(request_obj.approver) + for user in request_obj.recipients.all(): - recipients.add(user) + if user.is_active and getattr(user, 'email_verified', True): + recipients.add(user) + for user in request_obj.cc_users.all(): - recipients.add(user) - - # Исключаем неактивных - recipients = self.get_active_employees(recipients) + if user.is_active and getattr(user, 'email_verified', True): + recipients.add(user) if not recipients: - return self.log_result(request_id, 0) + return self.log_result('request', request_id, notification_type, 0) # Формируем заголовок title_map = { - 'created': f'Новая заявка #{request_obj.id}: {request_obj.subject}', + 'created': f'Новая заявка #{request_obj.id}: {request_obj.title or request_obj.get_type_display()}', 'status_changed': f'Изменён статус заявки #{request_obj.id}', 'comment_added': f'Новый комментарий к заявке #{request_obj.id}', } type_code_map = { - 'created': 'request_created', + 'created': 'request_new', 'status_changed': 'request_status_changed', - 'comment_added': 'request_comment_added', + 'comment_added': 'request_comment', } # Отправляем уведомления notifications_sent = self.send_notifications( recipients=recipients, - notification_type=type_code_map.get(notification_type, 'request_created'), + notification_type=type_code_map.get(notification_type, 'request_new'), title=title_map.get(notification_type, f'Обновление заявки #{request_obj.id}'), - message=truncate_text(request_obj.description, 150), + message=truncate_text(request_obj.comment or '', 150), content_object=request_obj, action_url=f'/requests/{request_obj.id}/', metadata={ 'request_id': request_obj.id, - 'subject': request_obj.subject, + 'request_type': request_obj.type, 'status': request_obj.status, } ) - return self.log_result(request_id, notifications_sent) + return self.log_result('request', request_id, notification_type, notifications_sent) except Exception as exc: - return self.handle_exception(exc, f"request {request_id}") + self.handle_exception(exc) class DocumentNotificationProcessor(BaseNotificationProcessor): @@ -554,35 +558,39 @@ class DocumentNotificationProcessor(BaseNotificationProcessor): """Обработка уведомлений для документа""" try: from documents.models import Document + from django.contrib.auth import get_user_model - document = Document.objects.select_related('author').get(id=document_id) - - if document.is_deleted: - return {"status": "skipped", "reason": "deleted"} + User = get_user_model() + document = Document.objects.select_related('uploaded_by').prefetch_related('departments', 'recipients').get(id=document_id) # Собираем получателей recipients = set() - # Департаменты - if document.departments.exists(): - recipients.update(self.get_department_employees(document.departments.all())) + # Если разослать всем + if document.sent_to_all: + recipients = set(User.objects.filter(is_active=True, email_verified=True)) + else: + # Департаменты + if document.departments.exists(): + dept_ids = [d.id for d in document.departments.all()] + recipients.update(self.get_department_employees(dept_ids)) + + # Конкретные получатели + for user in document.recipients.all(): + if user.is_active and getattr(user, 'email_verified', True): + recipients.add(user) - # Конкретные пользователи - for user in document.readers.all(): - recipients.add(user) - - # Исключаем автора и неактивных - recipients = self.get_active_employees(recipients) - if document.author: - recipients.discard(document.author) + # Исключаем автора + if document.uploaded_by: + recipients.discard(document.uploaded_by) if not recipients: - return self.log_result(document_id, 0) + return self.log_result('document', document_id, 'skipped', 0) # Отправляем уведомления notifications_sent = self.send_notifications( recipients=recipients, - notification_type='document_created', + notification_type='document_ready', title=f'Новый документ: {document.title}', message=truncate_text(document.description or '', 150), content_object=document, @@ -590,11 +598,10 @@ class DocumentNotificationProcessor(BaseNotificationProcessor): metadata={ 'document_id': document.id, 'document_title': document.title, - 'document_type': document.document_type, } ) - return self.log_result(document_id, notifications_sent) + return self.log_result('document', document_id, 'created', notifications_sent) except Exception as exc: - return self.handle_exception(exc, f"document {document_id}") + self.handle_exception(exc) diff --git a/backend/notifications/tasks.py b/backend/notifications/tasks.py index 87538ffd..7ade3a11 100644 --- a/backend/notifications/tasks.py +++ b/backend/notifications/tasks.py @@ -356,6 +356,8 @@ def process_request_notifications_task( 'created': 'created', 'approved': 'status_changed', 'rejected': 'status_changed', + 'cancelled': 'status_changed', + 'updated': 'status_changed', 'comment': 'comment_added', } return processor.process(request_id, notification_type_map.get(action, 'created')) diff --git a/backend/requests_app/notification_signals.py b/backend/requests_app/notification_signals.py index ef3f0cff..e0b44238 100644 --- a/backend/requests_app/notification_signals.py +++ b/backend/requests_app/notification_signals.py @@ -67,7 +67,8 @@ def create_request_notifications(sender, instance, created, **kwargs): # Определяем action по статусу action = 'approved' if new_status == 'approved' else \ - 'rejected' if new_status == 'rejected' else 'updated' + 'rejected' if new_status == 'rejected' else \ + 'cancelled' if new_status == 'cancelled' else 'updated' def send_task(): """Отложенная отправка задачи после commit транзакции""" @@ -215,8 +216,9 @@ def create_comment_notification(sender, instance, created, **kwargs): notify_request_comment(instance) transaction.on_commit(send_task) - author = comment.author + author = instance.author recipients_set = set() + request_obj = instance.request # Автор заявки if request_obj.employee.id != author.id: @@ -264,14 +266,14 @@ def create_comment_notification(sender, instance, created, **kwargs): title=f"💬 Новый комментарий к заявлению от {employee_name}", message=( f"{author_name} прокомментировал заявление " - f'"{request_type}": {comment.text[:100]}' + f'"{request_type}": {instance.text[:100]}' ), content_object=request_obj, action_url=f"/requests/{request_obj.id}/", metadata={ "request_id": request_obj.id, "request_type": request_obj.type, - "comment_id": comment.id, + "comment_id": instance.id, "author_id": author.id, }, ) diff --git a/backend/scripts/create_test_users.py b/backend/scripts/create_test_users.py new file mode 100644 index 00000000..3c6d8636 --- /dev/null +++ b/backend/scripts/create_test_users.py @@ -0,0 +1,277 @@ +""" +Скрипт для создания тестовых пользователей в корпоративном портале. +Создает 100+ пользователей с разными ролями и отделами. +""" +import os +import sys +from pathlib import Path + +# Добавляем путь к backend в sys.path +backend_dir = Path(__file__).resolve().parent.parent +sys.path.insert(0, str(backend_dir)) + +import django +from datetime import date, timedelta +import random + +# Setup Django +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'eusrr_backend.settings') +django.setup() + +from django.contrib.auth import get_user_model +from employees.models import Department, Position, EmployeeDepartment + +User = get_user_model() + +# Списки для генерации данных +FIRST_NAMES = [ + 'Александр', 'Дмитрий', 'Максим', 'Сергей', 'Андрей', 'Алексей', 'Артем', 'Илья', + 'Кирилл', 'Михаил', 'Иван', 'Даниил', 'Егор', 'Никита', 'Матвей', 'Роман', + 'Владислав', 'Ярослав', 'Тимофей', 'Владимир', 'Павел', 'Николай', 'Денис', 'Олег', + 'Анна', 'Мария', 'Елена', 'Ольга', 'Татьяна', 'Наталья', 'Екатерина', 'Ирина', + 'Юлия', 'Светлана', 'Людмила', 'Галина', 'Анастасия', 'Дарья', 'Виктория', 'Валерия', + 'Ксения', 'Полина', 'Алина', 'Вероника', 'София', 'Марина', 'Оксана', 'Алла' +] + +LAST_NAMES = [ + 'Иванов', 'Петров', 'Сидоров', 'Смирнов', 'Кузнецов', 'Попов', 'Васильев', 'Соколов', + 'Михайлов', 'Новиков', 'Федоров', 'Морозов', 'Волков', 'Алексеев', 'Лебедев', 'Семенов', + 'Егоров', 'Павлов', 'Козлов', 'Степанов', 'Николаев', 'Орлов', 'Андреев', 'Макаров', + 'Никитин', 'Захаров', 'Зайцев', 'Соловьев', 'Борисов', 'Яковлев', 'Григорьев', 'Романов', + 'Воробьев', 'Сергеев', 'Фролов', 'Дмитриев', 'Ковалев', 'Белов', 'Комаров', 'Жуков', + 'Иванова', 'Петрова', 'Сидорова', 'Смирнова', 'Кузнецова', 'Попова', 'Васильева', 'Соколова' +] + +PATRONYMICS = [ + 'Александрович', 'Дмитриевич', 'Максимович', 'Сергеевич', 'Андреевич', 'Алексеевич', + 'Артемович', 'Ильич', 'Кириллович', 'Михайлович', 'Иванович', 'Даниилович', + 'Александровна', 'Дмитриевна', 'Максимовна', 'Сергеевна', 'Андреевна', 'Алексеевна', + 'Артемовна', 'Ильинична', 'Кирилловна', 'Михайловна', 'Ивановна', 'Данииловна' +] + +DEPARTMENTS_DATA = [ + {'name': 'Разработка', 'code': 'DEV'}, + {'name': 'Тестирование', 'code': 'QA'}, + {'name': 'Продажи', 'code': 'SALES'}, + {'name': 'Маркетинг', 'code': 'MKT'}, + {'name': 'HR', 'code': 'HR'}, + {'name': 'Финансы', 'code': 'FIN'}, + {'name': 'Бухгалтерия', 'code': 'ACC'}, + {'name': 'Логистика', 'code': 'LOG'}, + {'name': 'Производство', 'code': 'PROD'}, + {'name': 'Закупки', 'code': 'PURCH'}, +] + +POSITIONS_DATA = [ + 'Младший специалист', + 'Специалист', + 'Старший специалист', + 'Ведущий специалист', + 'Менеджер', + 'Старший менеджер', + 'Руководитель группы', + 'Руководитель отдела', + 'Директор', + 'Стажер', +] + + +def create_departments(): + """Создание отделов""" + print('\n📁 Создание отделов...') + departments = [] + for dept_data in DEPARTMENTS_DATA: + dept, created = Department.objects.get_or_create( + name=dept_data['name'], + defaults={'description': f'Отдел {dept_data["name"]}'} + ) + departments.append(dept) + if created: + print(f' ✅ Создан отдел: {dept.name}') + else: + print(f' ℹ️ Отдел существует: {dept.name}') + return departments + + +def create_positions(): + """Создание должностей""" + print('\n💼 Создание должностей...') + positions = [] + for pos_name in POSITIONS_DATA: + pos, created = Position.objects.get_or_create( + name=pos_name + ) + positions.append(pos) + if created: + print(f' ✅ Создана должность: {pos.name}') + return positions + + +def generate_email(first_name, last_name, counter): + """Генерация email""" + first = first_name.lower() + last = last_name.lower() + + # Транслитерация + translit = { + 'а': 'a', 'б': 'b', 'в': 'v', 'г': 'g', 'д': 'd', 'е': 'e', 'ё': 'e', + 'ж': 'zh', 'з': 'z', 'и': 'i', 'й': 'y', 'к': 'k', 'л': 'l', 'м': 'm', + 'н': 'n', 'о': 'o', 'п': 'p', 'р': 'r', 'с': 's', 'т': 't', 'у': 'u', + 'ф': 'f', 'х': 'h', 'ц': 'ts', 'ч': 'ch', 'ш': 'sh', 'щ': 'sch', 'ъ': '', + 'ы': 'y', 'ь': '', 'э': 'e', 'ю': 'yu', 'я': 'ya' + } + + first_en = ''.join(translit.get(c, c) for c in first) + last_en = ''.join(translit.get(c, c) for c in last) + + return f'{first_en}.{last_en}{counter}@company.local' + + +def generate_phone(): + """Генерация номера телефона""" + return f'+7{random.randint(900, 999)}{random.randint(1000000, 9999999)}' + + +def generate_birthdate(): + """Генерация даты рождения (25-65 лет)""" + years_ago = random.randint(25, 65) + days_offset = random.randint(0, 364) + return date.today() - timedelta(days=years_ago * 365 + days_offset) + + +def create_users(count=120, departments=None, positions=None): + """Создание пользователей""" + print(f'\n👥 Создание {count} пользователей...') + + if not departments: + departments = list(Department.objects.all()) + if not positions: + positions = list(Position.objects.all()) + + created_count = 0 + skipped_count = 0 + + for i in range(1, count + 1): + first_name = random.choice(FIRST_NAMES) + last_name = random.choice(LAST_NAMES) + patronymic = random.choice(PATRONYMICS) + + email = generate_email(first_name, last_name, i) + + # Проверяем, существует ли пользователь + if User.objects.filter(email=email).exists(): + skipped_count += 1 + continue + + try: + # Создаем пользователя + user = User.objects.create_user( + email=email, + password='test123', # Простой пароль для тестирования + first_name=first_name, + last_name=last_name, + patronymic=patronymic, + phone_number=generate_phone(), + birth_date=generate_birthdate(), + is_active=True, + email_verified=True, + is_staff=random.random() < 0.1, # 10% - staff + send_activation_email=False + ) + + # Назначаем должность + position = random.choice(positions) + user.position = position + user.save() + + # Назначаем отдел(ы) + # 70% - один отдел, 25% - два отдела, 5% - три отдела + dept_count = random.choices([1, 2, 3], weights=[70, 25, 5])[0] + user_departments = random.sample(departments, min(dept_count, len(departments))) + + for dept in user_departments: + EmployeeDepartment.objects.create( + employee=user, + department=dept, + is_active=True + ) + + created_count += 1 + + if created_count % 10 == 0: + print(f' ✅ Создано {created_count} пользователей...') + + except Exception as e: + print(f' ❌ Ошибка создания пользователя {email}: {e}') + skipped_count += 1 + + return created_count, skipped_count + + +def print_statistics(): + """Вывод статистики""" + print('\n' + '=' * 60) + print('📊 СТАТИСТИКА БАЗЫ ДАННЫХ') + print('=' * 60) + + total_users = User.objects.count() + active_users = User.objects.filter(is_active=True).count() + staff_users = User.objects.filter(is_staff=True).count() + + print(f'\n👥 Пользователи:') + print(f' Всего: {total_users}') + print(f' Активных: {active_users}') + print(f' Персонал: {staff_users}') + + departments = Department.objects.all() + print(f'\n📁 Отделы: {departments.count()}') + for dept in departments: + emp_count = EmployeeDepartment.objects.filter( + department=dept, + is_active=True + ).count() + print(f' • {dept.name}: {emp_count} сотрудников') + + positions = Position.objects.all() + print(f'\n💼 Должности: {positions.count()}') + for pos in positions: + emp_count = User.objects.filter(position=pos).count() + if emp_count > 0: + print(f' • {pos.name}: {emp_count} сотрудников') + + print('\n' + '=' * 60) + + +def main(): + print('=' * 60) + print('🚀 ГЕНЕРАЦИЯ ТЕСТОВЫХ ПОЛЬЗОВАТЕЛЕЙ') + print('=' * 60) + + # Создаем отделы и должности + departments = create_departments() + positions = create_positions() + + # Создаем пользователей + created, skipped = create_users( + count=120, + departments=departments, + positions=positions + ) + + print(f'\n✅ Создание завершено!') + print(f' Создано: {created} пользователей') + if skipped > 0: + print(f' Пропущено: {skipped} (уже существуют)') + + # Выводим статистику + print_statistics() + + print('\n💡 Данные для входа:') + print(' Username: user001, user002, ... user120') + print(' Password: test123') + print(' Email: имя.фамилия1@company.local, и т.д.') + print('\n' + '=' * 60) + + +if __name__ == '__main__': + main()