253 lines
8.3 KiB
Python

"""
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 .models import Request
from .enums import RequestStatus
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,
)
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