391 lines
12 KiB
Python
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)
|