158 lines
5.8 KiB
Python
158 lines
5.8 KiB
Python
import calendar
|
|
from datetime import date
|
|
|
|
from django.utils import timezone
|
|
from rest_framework.exceptions import ValidationError
|
|
|
|
from requests_app.enums import DAY_OFF_REQUEST_TYPES, RequestStatus, RequestType
|
|
from requests_app.models import Request as EmployeeRequest
|
|
|
|
|
|
UNPAID_VACATION_MARKERS = (
|
|
"за свой счет",
|
|
"за свой счёт",
|
|
"без сохранения заработной платы",
|
|
"без сохранения зарплаты",
|
|
"без сохранения зп",
|
|
"неоплачиваем",
|
|
)
|
|
|
|
|
|
def can_view_request_statistics(user) -> bool:
|
|
return bool(
|
|
getattr(user, "is_staff", False)
|
|
or getattr(user, "is_superuser", False)
|
|
or user.has_perm("requests_app.view_statistics")
|
|
or user.has_perm("requests_app.can_view_all_requests")
|
|
)
|
|
|
|
|
|
def resolve_stats_window_from_query(period, query_params):
|
|
if period == "month":
|
|
today = timezone.localdate()
|
|
_, last_day = calendar.monthrange(today.year, today.month)
|
|
return today.replace(day=1), today.replace(day=last_day)
|
|
if period == "year":
|
|
today = timezone.localdate()
|
|
return today.replace(month=1, day=1), today.replace(month=12, day=31)
|
|
if period != "custom":
|
|
return None, None
|
|
|
|
date_from_raw = (query_params.get("date_from") or "").strip()
|
|
date_to_raw = (query_params.get("date_to") or "").strip()
|
|
if not date_from_raw or not date_to_raw:
|
|
raise ValidationError(
|
|
{
|
|
"date_from": ["Обязательное поле для custom периода."],
|
|
"date_to": ["Обязательное поле для custom периода."],
|
|
}
|
|
)
|
|
start_date = _parse_iso_date(date_from_raw, "date_from")
|
|
end_date = _parse_iso_date(date_to_raw, "date_to")
|
|
if start_date > end_date:
|
|
raise ValidationError(
|
|
{"date_to": ["Дата окончания не может быть раньше даты начала."]}
|
|
)
|
|
return start_date, end_date
|
|
|
|
|
|
def _parse_iso_date(raw_value, field_name):
|
|
try:
|
|
return date.fromisoformat(raw_value)
|
|
except ValueError as exc:
|
|
raise ValidationError(
|
|
{field_name: ["Ожидается дата в формате YYYY-MM-DD."]}
|
|
) from exc
|
|
|
|
|
|
def _duration_days_in_window(request_obj, start_date, end_date):
|
|
if start_date is None or end_date is None:
|
|
if request_obj.date_from and request_obj.date_to:
|
|
return max((request_obj.date_to - request_obj.date_from).days + 1, 0)
|
|
return 1 if request_obj.date_from else 0
|
|
if request_obj.date_from and request_obj.date_to:
|
|
overlap_start = max(request_obj.date_from, start_date)
|
|
overlap_end = min(request_obj.date_to, end_date)
|
|
return 0 if overlap_start > overlap_end else (overlap_end - overlap_start).days + 1
|
|
return int(
|
|
bool(
|
|
request_obj.date_from
|
|
and start_date <= request_obj.date_from <= end_date
|
|
)
|
|
)
|
|
|
|
|
|
def _is_unpaid_vacation(request_obj):
|
|
text = " ".join(
|
|
[
|
|
str(getattr(request_obj, "title", "") or ""),
|
|
str(getattr(request_obj, "comment", "") or ""),
|
|
]
|
|
).lower()
|
|
return any(marker in text for marker in UNPAID_VACATION_MARKERS)
|
|
|
|
|
|
def build_employee_statistics(
|
|
*,
|
|
employee_id,
|
|
employee_name,
|
|
period,
|
|
start_date=None,
|
|
end_date=None,
|
|
):
|
|
base_queryset = EmployeeRequest.objects.filter(employee_id=employee_id).exclude(
|
|
status=RequestStatus.DRAFT
|
|
)
|
|
if start_date and end_date:
|
|
base_queryset = base_queryset.filter(
|
|
created_at__date__gte=start_date,
|
|
created_at__date__lte=end_date,
|
|
)
|
|
|
|
totals = {
|
|
"sick_leave_days": 0,
|
|
"day_off_days": 0,
|
|
"short_day_off_minutes": 0,
|
|
"maternity_days": 0,
|
|
"paid_vacation_days": 0,
|
|
"unpaid_vacation_days": 0,
|
|
}
|
|
approved_requests = EmployeeRequest.objects.filter(
|
|
employee_id=employee_id,
|
|
status=RequestStatus.APPROVED,
|
|
).only("type", "date_from", "date_to", "time_from", "time_to", "title", "comment")
|
|
for request_obj in approved_requests:
|
|
duration = _duration_days_in_window(request_obj, start_date, end_date)
|
|
if request_obj.type == RequestType.SICK_LEAVE:
|
|
totals["sick_leave_days"] += duration
|
|
elif request_obj.type == RequestType.SHORT_DAY_OFF:
|
|
if request_obj.date_from and (
|
|
start_date is None or start_date <= request_obj.date_from
|
|
) and (end_date is None or request_obj.date_from <= end_date):
|
|
totals["short_day_off_minutes"] += request_obj.duration_minutes or 0
|
|
elif request_obj.type == RequestType.DAY_OFF:
|
|
totals["day_off_days"] += duration
|
|
elif request_obj.type == RequestType.MATERNITY:
|
|
totals["maternity_days"] += duration
|
|
elif request_obj.type == RequestType.VACATION:
|
|
key = (
|
|
"unpaid_vacation_days"
|
|
if _is_unpaid_vacation(request_obj)
|
|
else "paid_vacation_days"
|
|
)
|
|
totals[key] += duration
|
|
|
|
return {
|
|
"employee_id": employee_id,
|
|
"employee_name": employee_name,
|
|
"period": period,
|
|
"date_from": start_date.isoformat() if start_date else None,
|
|
"date_to": end_date.isoformat() if end_date else None,
|
|
"total_submitted_requests": base_queryset.count(),
|
|
"sick_leave_requests_count": base_queryset.filter(type=RequestType.SICK_LEAVE).count(),
|
|
"day_off_requests_count": base_queryset.filter(type__in=DAY_OFF_REQUEST_TYPES).count(),
|
|
"short_day_off_requests_count": base_queryset.filter(type=RequestType.SHORT_DAY_OFF).count(),
|
|
"maternity_requests_count": base_queryset.filter(type=RequestType.MATERNITY).count(),
|
|
**totals,
|
|
}
|