From 5f1d897b7fb04b7395c3933fbb2522f268786f0a Mon Sep 17 00:00:00 2001 From: lizerk Date: Wed, 18 Mar 2026 10:16:21 +0300 Subject: [PATCH] =?UTF-8?q?feat:=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B0=20=D0=BF=D0=BE=D0=B4=D0=B4=D0=B5=D1=80=D0=B6?= =?UTF-8?q?=D0=BA=D0=B0=20=D1=81=D0=BE=D0=B7=D0=B4=D0=B0=D0=BD=D0=B8=D1=8F?= =?UTF-8?q?=20=D1=81=D0=BE=D0=B1=D1=8B=D1=82=D0=B8=D0=B9=20=D0=B2=D0=BE?= =?UTF-8?q?=D0=B7=D0=B2=D1=80=D0=B0=D1=82=D0=B0=20=D0=B8=D0=B7=20=D0=BE?= =?UTF-8?q?=D1=82=D0=BF=D1=83=D1=81=D0=BA=D0=B0/=D0=B1=D0=BE=D0=BB=D1=8C?= =?UTF-8?q?=D0=BD=D0=B8=D1=87=D0=BD=D0=BE=D0=B3=D0=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Добавлен RETURN_ACTION_MAPPING для vacation/sick_leave → returned_from_leave - Migration команда теперь создаёт события возврата на date_to + 1 день - Исправлена конвертация date в datetime с timezone - Обработка случаев когда основное событие уже существует, но нужно создать возврат - Статистика показывает количество созданных событий возврата --- .../commands/migrate_requests_to_actions.py | 150 ++++++++++++++---- 1 file changed, 122 insertions(+), 28 deletions(-) diff --git a/backend/requests_app/management/commands/migrate_requests_to_actions.py b/backend/requests_app/management/commands/migrate_requests_to_actions.py index 6dcc0897..7682c82d 100644 --- a/backend/requests_app/management/commands/migrate_requests_to_actions.py +++ b/backend/requests_app/management/commands/migrate_requests_to_actions.py @@ -4,6 +4,7 @@ Management команда для миграции существующих за Использование: python manage.py migrate_requests_to_actions [--dry-run] """ +from datetime import datetime, timedelta from django.core.management.base import BaseCommand from django.db import transaction from django.utils import timezone @@ -14,6 +15,13 @@ from employees.models import EmployeeAction from employees.signals import IMMEDIATE_ACTION_MAPPING, SCHEDULED_ACTION_MAPPING +# Маппинг для событий возврата из отпуска/больничного +RETURN_ACTION_MAPPING = { + 'vacation': 'returned_from_leave', + 'sick_leave': 'returned_from_leave', +} + + class Command(BaseCommand): help = 'Создает EmployeeAction для всех одобренных заявок, у которых их еще нет' @@ -46,6 +54,7 @@ class Command(BaseCommand): created_count = 0 skipped_count = 0 + return_count = 0 # Счётчик событий возврата errors = [] for request in approved_requests: @@ -59,6 +68,9 @@ class Command(BaseCommand): action=action_type ).exists() + # Флаг - нужно ли создавать основное событие + should_create_main = not existing + if existing: skipped_count += 1 self.stdout.write( @@ -67,10 +79,35 @@ class Command(BaseCommand): f'EmployeeAction уже существует' ) ) + + # Проверяем нужно ли создать событие возврата + should_create_return = False + return_action = None + return_date = None + + if request.type in RETURN_ACTION_MAPPING and request.date_to: + return_action = RETURN_ACTION_MAPPING[request.type] + return_date = request.date_to + timedelta(days=1) + + # Проверяем что возврат ещё не создан + existing_return = EmployeeAction.objects.filter( + extra__request_id=request.id, + action=return_action + ).exists() + + should_create_return = not existing_return + + # Если нечего создавать - пропускаем + if not should_create_main and not should_create_return: continue - # Определяем дату события - action_date = request.date_from or request.decided_at or request.created_at or timezone.now() + # Определяем дату события и нормализуем к datetime с timezone + raw_date = request.date_from or request.decided_at or request.created_at or timezone.now() + if isinstance(raw_date, datetime): + action_date = raw_date if timezone.is_aware(raw_date) else timezone.make_aware(raw_date) + else: + # Конвертируем date в datetime + action_date = timezone.make_aware(datetime.combine(raw_date, datetime.min.time())) # Формируем комментарий action_comment = f"Заявление #{request.id}" @@ -86,37 +123,86 @@ class Command(BaseCommand): } if dry_run: - # action_date может быть date или datetime - date_str = action_date.date() if hasattr(action_date, 'date') else action_date - self.stdout.write( - self.style.SUCCESS( - f' WOULD CREATE: Request #{request.id} ({request.type}) → ' - f'EmployeeAction ({action_type}) for {request.employee} ' - f'on {date_str}' + # Создаём основное событие + if should_create_main: + self.stdout.write( + self.style.SUCCESS( + f' WOULD CREATE: Request #{request.id} ({request.type}) → ' + f'EmployeeAction ({action_type}) for {request.employee} ' + f'on {action_date.date()}' + ) ) - ) - created_count += 1 + created_count += 1 + + # Создаём событие возврата + if should_create_return: + self.stdout.write( + self.style.SUCCESS( + f' WOULD CREATE RETURN: Request #{request.id} → ' + f'EmployeeAction ({return_action}) on {return_date}' + ) + ) + return_count += 1 else: try: with transaction.atomic(): - action = EmployeeAction.objects.create( - employee=request.employee, - action=action_type, - date=action_date, - comment=action_comment, - extra=extra_data - ) - - # Применяем эффекты (деактивация, LDAP sync) - self._apply_effects(action) - - created_count += 1 - self.stdout.write( - self.style.SUCCESS( - f' ✓ Created: EmployeeAction #{action.id} ({action_type}) ' - f'from Request #{request.id} for {request.employee}' + # Создаём основное событие + if should_create_main: + action = EmployeeAction.objects.create( + employee=request.employee, + action=action_type, + date=action_date, + comment=action_comment, + extra=extra_data + ) + + # Применяем эффекты (деактивация, LDAP sync) + self._apply_effects(action) + + created_count += 1 + self.stdout.write( + self.style.SUCCESS( + f' ✓ Created: EmployeeAction #{action.id} ({action_type}) ' + f'from Request #{request.id} for {request.employee}' + ) + ) + + # Создаём событие возврата + if should_create_return: + return_comment = ( + f"Автоматически: окончание " + f"{request.get_type_display().lower()} " + f"(заявка #{request.id})" + ) + + # Конвертируем date в datetime с timezone + return_datetime = timezone.make_aware( + datetime.combine(return_date, datetime.min.time()) + ) + + return_action_obj = EmployeeAction.objects.create( + employee=request.employee, + action=return_action, + date=return_datetime, + comment=return_comment, + extra={ + 'request_id': request.id, + 'auto_return': True, + 'migrated': True, + 'migration_date': timezone.now().isoformat() + } + ) + + # Применяем эффекты для возврата + self._apply_effects(return_action_obj) + + return_count += 1 + self.stdout.write( + self.style.SUCCESS( + f' ✓ Created RETURN: EmployeeAction #{return_action_obj.id} ' + f'({return_action}) on {return_date}' + ) ) - ) except Exception as e: errors.append((request.id, str(e))) @@ -145,6 +231,14 @@ class Command(BaseCommand): ) ) + if return_count: + verb = 'Будет создано' if dry_run else 'Создано' + self.stdout.write( + self.style.SUCCESS( + f'{verb} событий возврата: {return_count}' + ) + ) + if skipped_count: self.stdout.write( self.style.WARNING(