""" Celery tasks для автоматизации кадровых событий из заявок. """ import logging from datetime import datetime, time from celery import shared_task from django.db.models import Q from django.utils import timezone logger = logging.getLogger(__name__) @shared_task(bind=True, max_retries=3) def create_scheduled_action(self, request_id): """ Создать кадровое событие для заявки по расписанию. Вызывается в date_from для типов кадрового отсутствия. Args: request_id: ID заявки """ from employees.constants import ( ACTION_ON_DAY_OFF, ACTION_ON_LEAVE, ACTION_ON_MATERNITY, ACTION_ON_SICK_LEAVE, ) from employees.services.request_actions import ( build_request_action_comment, create_request_action, ) from .enums import RequestStatus from .models import Request try: request = Request.objects.get(id=request_id) # Проверяем, что заявка все еще одобрена if request.status != RequestStatus.APPROVED: logger.info( "Scheduled employee action creation skipped because request is not approved", extra={ "event": "request.scheduled_action.skipped", "business_request_id": request_id, "request_status": request.status, "reason": "request_not_approved", }, ) return # Определяем тип события action_mapping = { "short_day_off": ACTION_ON_DAY_OFF, "vacation": ACTION_ON_LEAVE, "sick_leave": ACTION_ON_SICK_LEAVE, "day_off": ACTION_ON_DAY_OFF, "maternity": ACTION_ON_MATERNITY, } action_type = action_mapping.get(request.type) if not action_type: logger.warning( "Scheduled employee action creation skipped because request type is unsupported", extra={ "event": "request.scheduled_action.skipped", "business_request_id": request_id, "request_type": request.type, "reason": "request_type_not_mapped", }, ) return is_partial_day_off = request.type == "short_day_off" extra = { "approved_by": (request.approver.id if request.approver else None), "scheduled": True, } if is_partial_day_off: extra.update( { "partial_day": True, "starts_at": request.starts_at.isoformat(), "ends_at": request.ends_at.isoformat(), "duration_minutes": request.duration_minutes, } ) action, created = create_request_action( request=request, action_type=action_type, action_date=request.starts_at if is_partial_day_off else request.date_from, date_to=request.date_to, comment=build_request_action_comment(request), extra=extra, ) if not created: logger.info( "Scheduled employee action creation skipped because action exists", extra={ "event": "request.scheduled_action.skipped", "business_request_id": request_id, "action_type": action_type, "reason": "action_exists", }, ) return logger.info( "Scheduled employee action created", extra={ "event": "request.scheduled_action.created", "business_request_id": request_id, "employee_action_id": action.id, "action_type": action_type, }, ) return f"Created action {action.id}" except Request.DoesNotExist: logger.error( "Scheduled employee action request not found", extra={ "event": "request.scheduled_action.failed", "business_request_id": request_id, "error_type": "Request.DoesNotExist", }, ) raise except Exception as e: logger.exception( "Scheduled employee action creation failed", extra={ "event": "request.scheduled_action.failed", "business_request_id": request_id, "error_type": type(e).__name__, }, ) # Retry через 5 минут raise self.retry(exc=e, countdown=300) @shared_task(bind=True, max_retries=3) def schedule_auto_return(self, request_id): """ Legacy no-op для старых очередей Celery. Возвраты больше не закрывают временные кадровые состояния: период хранится в EmployeeAction.date_to. Старые запланированные задачи должны спокойно завершаться и не создавать дублирующих событий. """ from .models import Request try: request = Request.objects.get(id=request_id) logger.info( "Legacy auto-return task skipped", extra={ "event": "request.auto_return.skipped", "business_request_id": request_id, "interval_ends_at": request.date_to, "reason": "feature_disabled", }, ) return "Auto-return disabled" except Request.DoesNotExist: logger.error( "Legacy auto-return request not found", extra={ "event": "request.auto_return.failed", "business_request_id": request_id, "error_type": "Request.DoesNotExist", }, ) raise except Exception as e: logger.exception( "Legacy auto-return task failed", extra={ "event": "request.auto_return.failed", "business_request_id": request_id, "error_type": type(e).__name__, }, ) raise self.retry(exc=e, countdown=300) @shared_task def cleanup_missed_returns(): """ Legacy no-op: возвратные события больше не создаются автоматически. """ logger.info( "Missed auto-return cleanup skipped", extra={ "event": "request.auto_return_cleanup.skipped", "reason": "feature_disabled", }, ) return 0 @shared_task def process_due_personnel_actions(): """Apply account effects for dismissal actions that are due today. Future dismissal actions are created from approved requests immediately, but account and department deactivation must happen only when their effective date arrives. This task also repairs missed runs by processing all due dismissals. """ from api.v1.employees.views.actions import EmployeeActionViewSet from employees.constants import ACTION_DISMISSED from employees.models import EmployeeAction today = timezone.localdate() end_of_today = timezone.make_aware(datetime.combine(today, time.max)) dismissal_employee_ids = ( EmployeeAction.objects.filter( action=ACTION_DISMISSED, date__lte=end_of_today, ) .filter( Q(employee__is_active=True) | Q(employee__departments_links__is_active=True) | Q(employee__role_assignments__is_active=True) | Q(employee__headed_departments__isnull=False) ) .values_list("employee_id", flat=True) ) employee_ids = sorted(set(dismissal_employee_ids)) viewset = EmployeeActionViewSet() processed = 0 for employee_id in employee_ids: viewset._sync_employee_account_state_by_id(employee_id) processed += 1 logger.info( "Due dismissal actions processed", extra={ "event": "employee.dismissal_actions.processed", "employee_count": processed, "effective_date": today, }, ) return processed