174 lines
5.7 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 для vacation/sick_leave/day_off.
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(
f"Request #{request_id} is no longer approved "
f"(status={request.status}), skipping action creation"
)
return
# Определяем тип события
action_mapping = {
"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(
f"Request #{request_id} has type '{request.type}' "
f"which doesn't map to scheduled action"
)
return
action, created = create_request_action(
request=request,
action_type=action_type,
action_date=request.date_from,
date_to=request.date_to,
comment=build_request_action_comment(request),
extra={
"approved_by": (request.approver.id if request.approver else None),
"scheduled": True,
},
)
if not created:
logger.info(f"Action already exists for Request #{request_id}, skipping")
return
logger.info(
f"Created scheduled EmployeeAction #{action.id} ({action_type}) "
f"for Request #{request_id}"
)
return f"Created action {action.id}"
except Request.DoesNotExist:
logger.error(f"Request #{request_id} not found")
raise
except Exception as e:
logger.error(
f"Failed to create scheduled action for Request #{request_id}: {e}",
exc_info=True,
)
# 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(
"Skipping legacy auto-return for Request #%s; interval ends at %s",
request_id,
request.date_to,
)
return "Auto-return disabled"
except Request.DoesNotExist:
logger.error(f"Request #{request_id} not found for auto-return")
raise
except Exception as e:
logger.error(
f"Failed to process legacy auto-return for Request #{request_id}: {e}",
exc_info=True,
)
raise self.retry(exc=e, countdown=300)
@shared_task
def cleanup_missed_returns():
"""
Legacy no-op: возвратные события больше не создаются автоматически.
"""
logger.info("cleanup_missed_returns skipped: auto-return is 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("Processed due dismissal actions for %s employees", processed)
return processed