Corp/backend/api/v1/attendance/serializers.py
2026-04-22 18:26:43 +03:00

391 lines
12 KiB
Python

from rest_framework import serializers
from attendance.models import (
AttendanceAnalysisRun,
AttendanceAutoSyncSettings,
AttendanceRecord,
EmployeeWorkSchedule,
StandardWorkSchedule,
)
from attendance.services import get_standard_work_schedule_payload
from api.v1.employees.serializers import EmployeeBriefSerializer
class DateOverrideSerializer(serializers.Serializer):
date = serializers.DateField()
is_workday = serializers.BooleanField()
reason = serializers.CharField(required=False, allow_blank=True)
start_time = serializers.CharField(required=False, allow_blank=True)
end_time = serializers.CharField(required=False, allow_blank=True)
expected_hours = serializers.FloatField(required=False)
class LogStormScheduleSerializer(serializers.Serializer):
start_time = serializers.CharField()
end_time = serializers.CharField()
expected_hours = serializers.FloatField()
workdays = serializers.ListField(
child=serializers.CharField(),
required=False,
)
date_overrides = DateOverrideSerializer(many=True, required=False)
class LogStormAttendanceAnalyzeSerializer(serializers.Serializer):
employee_id = serializers.IntegerField()
period_start = serializers.DateField()
period_end = serializers.DateField()
schedule = LogStormScheduleSerializer(required=False)
def validate(self, attrs):
if attrs["period_start"] > attrs["period_end"]:
raise serializers.ValidationError(
"period_start must be less than or equal to period_end"
)
return attrs
class EmployeeWorkScheduleSerializer(serializers.ModelSerializer):
employee_id = serializers.IntegerField(source="employee.id", read_only=True)
is_default = serializers.BooleanField(read_only=True, default=False)
class Meta:
model = EmployeeWorkSchedule
fields = (
"id",
"employee_id",
"start_time",
"end_time",
"expected_hours",
"workdays",
"date_overrides",
"is_active",
"is_default",
"updated_by",
"created_at",
"updated_at",
)
read_only_fields = (
"id",
"employee_id",
"is_default",
"updated_by",
"created_at",
"updated_at",
)
def validate_workdays(self, value):
allowed = set(EmployeeWorkSchedule.DEFAULT_WORKDAYS) | {"Saturday", "Sunday"}
if not isinstance(value, list):
raise serializers.ValidationError("workdays must be a list")
normalized = []
for item in value:
if item not in allowed:
raise serializers.ValidationError(f"Unknown weekday: {item}")
if item not in normalized:
normalized.append(item)
return normalized
def validate_expected_hours(self, value):
if value <= 0 or value > 24:
raise serializers.ValidationError("expected_hours must be between 0 and 24")
return value
class StandardWorkScheduleSerializer(serializers.ModelSerializer):
is_default = serializers.BooleanField(read_only=True, default=False)
class Meta:
model = StandardWorkSchedule
fields = (
"id",
"start_time",
"end_time",
"expected_hours",
"workdays",
"date_overrides",
"is_default",
"updated_by",
"created_at",
"updated_at",
)
read_only_fields = (
"id",
"is_default",
"updated_by",
"created_at",
"updated_at",
)
def validate_workdays(self, value):
allowed = set(StandardWorkSchedule.DEFAULT_WORKDAYS) | {"Saturday", "Sunday"}
if not isinstance(value, list):
raise serializers.ValidationError("workdays must be a list")
normalized = []
for item in value:
if item not in allowed:
raise serializers.ValidationError(f"Unknown weekday: {item}")
if item not in normalized:
normalized.append(item)
return normalized
def validate_expected_hours(self, value):
if value <= 0 or value > 24:
raise serializers.ValidationError("expected_hours must be between 0 and 24")
return value
class AttendanceAutoSyncSettingsSerializer(serializers.ModelSerializer):
last_status_label = serializers.CharField(
source="get_last_status_display",
read_only=True,
)
class Meta:
model = AttendanceAutoSyncSettings
fields = (
"id",
"enabled",
"frequency_minutes",
"lookback_days",
"next_run_at",
"last_started_at",
"last_finished_at",
"last_status",
"last_status_label",
"last_error",
"last_success_count",
"last_error_count",
"updated_by",
"updated_at",
)
read_only_fields = (
"id",
"next_run_at",
"last_started_at",
"last_finished_at",
"last_status",
"last_status_label",
"last_error",
"last_success_count",
"last_error_count",
"updated_by",
"updated_at",
)
def validate_frequency_minutes(self, value):
allowed = {item[0] for item in AttendanceAutoSyncSettings.FREQUENCY_CHOICES}
if value not in allowed:
raise serializers.ValidationError("Unsupported frequency")
return value
def validate_lookback_days(self, value):
allowed = {item[0] for item in AttendanceAutoSyncSettings.LOOKBACK_CHOICES}
if value not in allowed:
raise serializers.ValidationError("Unsupported lookback")
return value
def default_work_schedule_response(employee_id: int) -> dict:
payload = get_standard_work_schedule_payload()
return {
"id": None,
"employee_id": employee_id,
"start_time": payload["start_time"],
"end_time": payload["end_time"],
"expected_hours": payload["expected_hours"],
"workdays": payload["workdays"],
"date_overrides": payload["date_overrides"],
"is_active": False,
"is_default": True,
"updated_by": None,
"created_at": None,
"updated_at": None,
}
def default_standard_work_schedule_response() -> dict:
payload = get_standard_work_schedule_payload()
return {
"id": None,
"start_time": payload["start_time"],
"end_time": payload["end_time"],
"expected_hours": payload["expected_hours"],
"workdays": payload["workdays"],
"date_overrides": payload["date_overrides"],
"is_default": True,
"updated_by": None,
"created_at": None,
"updated_at": None,
}
class AttendanceMonthlyMatrixQuerySerializer(serializers.Serializer):
employee_ids = serializers.CharField()
month = serializers.RegexField(r"^\d{4}-\d{2}$")
def validate_employee_ids(self, value):
employee_ids = []
for raw_item in value.split(","):
item = raw_item.strip()
if not item:
continue
try:
employee_id = int(item)
except ValueError as exc:
raise serializers.ValidationError(
"employee_ids must be a comma-separated list of integers"
) from exc
if employee_id not in employee_ids:
employee_ids.append(employee_id)
if not employee_ids:
raise serializers.ValidationError("employee_ids is required")
return employee_ids
def validate_month(self, value):
year, month = [int(part) for part in value.split("-")]
if month < 1 or month > 12:
raise serializers.ValidationError("month must be between 01 and 12")
return value
class AttendanceMonthlyMatrixExportQuerySerializer(serializers.Serializer):
employee_ids = serializers.CharField()
period_start = serializers.DateField()
period_end = serializers.DateField()
def validate_employee_ids(self, value):
employee_ids = []
for raw_item in value.split(","):
item = raw_item.strip()
if not item:
continue
try:
employee_id = int(item)
except ValueError as exc:
raise serializers.ValidationError(
"employee_ids must be a comma-separated list of integers"
) from exc
if employee_id not in employee_ids:
employee_ids.append(employee_id)
if not employee_ids:
raise serializers.ValidationError("employee_ids is required")
return employee_ids
def validate(self, attrs):
if attrs["period_start"] > attrs["period_end"]:
raise serializers.ValidationError(
"period_start must be less than or equal to period_end"
)
return attrs
class AttendanceAnalysisRunSerializer(serializers.ModelSerializer):
class Meta:
model = AttendanceAnalysisRun
fields = (
"id",
"employee",
"period_start",
"period_end",
"status",
"schedule_payload",
"request_payload",
"response_payload",
"error",
"triggered_by",
"created_at",
)
read_only_fields = fields
class AttendanceRecordSerializer(serializers.ModelSerializer):
analysis_run_id = serializers.IntegerField(read_only=True)
comments_count = serializers.IntegerField(read_only=True, default=0)
non_working_reason = serializers.SerializerMethodField()
class Meta:
model = AttendanceRecord
fields = (
"id",
"analysis_run_id",
"employee",
"date",
"display_name",
"arrival_time",
"departure_time",
"work_hours",
"expected_hours",
"is_workday",
"effective_is_workday",
"non_working_reason",
"is_late",
"late_minutes",
"is_early_leave",
"early_leave_minutes",
"is_underwork",
"underwork_hours",
"is_overtime",
"overtime_hours",
"is_absent",
"statuses",
"employee_issues",
"technical_issues",
"personnel_status",
"personnel_status_label",
"personnel_action",
"is_manually_edited",
"manual_edit_payload",
"manual_edited_by",
"manual_edited_at",
"raw_data",
"comments_count",
"updated_at",
)
read_only_fields = fields
def get_non_working_reason(self, obj):
if obj.effective_is_workday:
return ""
if obj.personnel_status_label:
return obj.personnel_status_label
if not obj.is_workday:
return "Выходной по графику/календарю"
return "Нерабочий день"
class AttendanceRecordUpdateSerializer(serializers.ModelSerializer):
class Meta:
model = AttendanceRecord
fields = (
"arrival_time",
"departure_time",
"work_hours",
"expected_hours",
"is_workday",
"effective_is_workday",
"is_late",
"late_minutes",
"is_early_leave",
"early_leave_minutes",
"is_underwork",
"underwork_hours",
"is_overtime",
"overtime_hours",
"is_absent",
)
extra_kwargs = {
"arrival_time": {"required": False, "allow_blank": True, "allow_null": True},
"departure_time": {"required": False, "allow_blank": True, "allow_null": True},
}
class AttendanceRecordCommentSerializer(serializers.Serializer):
id = serializers.IntegerField(read_only=True)
record = serializers.IntegerField(read_only=True)
author = EmployeeBriefSerializer(read_only=True)
text = serializers.CharField()
created_at = serializers.DateTimeField(read_only=True)