from datetime import time from django.conf import settings from django.db import models class AttendanceAnalysisRun(models.Model): STATUS_SUCCESS = "success" STATUS_FAILED = "failed" STATUS_CHOICES = ( (STATUS_SUCCESS, "Успешно"), (STATUS_FAILED, "Ошибка"), ) employee = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="attendance_analysis_runs", verbose_name="Сотрудник", ) period_start = models.DateField("Начало периода") period_end = models.DateField("Конец периода") status = models.CharField( "Статус", max_length=16, choices=STATUS_CHOICES, default=STATUS_SUCCESS, ) schedule_payload = models.JSONField("График", null=True, blank=True) request_payload = models.JSONField("Запрос", default=dict, blank=True) response_payload = models.JSONField("Ответ", default=dict, blank=True) error = models.TextField("Ошибка", blank=True) triggered_by = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True, related_name="triggered_attendance_analysis_runs", verbose_name="Запустил", ) created_at = models.DateTimeField("Создано", auto_now_add=True) class Meta: ordering = ["-created_at", "-id"] verbose_name = "Запуск анализа посещаемости" verbose_name_plural = "Запуски анализа посещаемости" indexes = [ models.Index(fields=["employee", "period_start", "period_end"]), models.Index(fields=["created_at"]), ] def __str__(self) -> str: return f"{self.employee_id}: {self.period_start} - {self.period_end}" class EmployeeWorkSchedule(models.Model): DEFAULT_WORKDAYS = [ "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", ] employee = models.OneToOneField( settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="work_schedule", verbose_name="Сотрудник", ) start_time = models.TimeField("Начало рабочего дня", default=time(8, 0)) end_time = models.TimeField("Конец рабочего дня", default=time(17, 0)) expected_hours = models.FloatField("Норма часов", default=9) workdays = models.JSONField("Рабочие дни", default=list, blank=True) date_overrides = models.JSONField( "Календарные исключения", default=list, blank=True, ) is_active = models.BooleanField("Использовать в анализе", default=True) updated_by = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True, related_name="updated_work_schedules", verbose_name="Изменил", ) created_at = models.DateTimeField("Создано", auto_now_add=True) updated_at = models.DateTimeField("Обновлено", auto_now=True) class Meta: verbose_name = "График работы сотрудника" verbose_name_plural = "Графики работы сотрудников" def save(self, *args, **kwargs): if not self.workdays: self.workdays = list(self.DEFAULT_WORKDAYS) super().save(*args, **kwargs) def __str__(self) -> str: return f"{self.employee_id}: {self.start_time} - {self.end_time}" def to_logstorm_payload(self) -> dict: return { "start_time": self.start_time.strftime("%H:%M"), "end_time": self.end_time.strftime("%H:%M"), "expected_hours": self.expected_hours, "workdays": list(self.workdays or self.DEFAULT_WORKDAYS), "date_overrides": list(self.date_overrides or []), } class StandardWorkSchedule(models.Model): DEFAULT_WORKDAYS = EmployeeWorkSchedule.DEFAULT_WORKDAYS singleton = models.BooleanField(default=True, unique=True, editable=False) start_time = models.TimeField("Начало рабочего дня", default=time(8, 0)) end_time = models.TimeField("Конец рабочего дня", default=time(17, 0)) expected_hours = models.FloatField("Норма часов", default=9) workdays = models.JSONField("Рабочие дни", default=list, blank=True) date_overrides = models.JSONField( "Календарные исключения", default=list, blank=True, ) updated_by = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True, related_name="updated_standard_work_schedules", verbose_name="Изменил", ) created_at = models.DateTimeField("Создано", auto_now_add=True) updated_at = models.DateTimeField("Обновлено", auto_now=True) class Meta: verbose_name = "Стандартный график работы" verbose_name_plural = "Стандартный график работы" def save(self, *args, **kwargs): self.singleton = True if not self.workdays: self.workdays = list(self.DEFAULT_WORKDAYS) super().save(*args, **kwargs) def __str__(self) -> str: return f"{self.start_time} - {self.end_time}" def to_logstorm_payload(self) -> dict: return { "start_time": self.start_time.strftime("%H:%M"), "end_time": self.end_time.strftime("%H:%M"), "expected_hours": self.expected_hours, "workdays": list(self.workdays or self.DEFAULT_WORKDAYS), "date_overrides": list(self.date_overrides or []), } class AttendanceAutoSyncSettings(models.Model): STATUS_IDLE = "idle" STATUS_RUNNING = "running" STATUS_SUCCESS = "success" STATUS_PARTIAL = "partial" STATUS_FAILED = "failed" STATUS_CHOICES = ( (STATUS_IDLE, "Ожидание"), (STATUS_RUNNING, "Выполняется"), (STATUS_SUCCESS, "Успешно"), (STATUS_PARTIAL, "Частично"), (STATUS_FAILED, "Ошибка"), ) FREQUENCY_CHOICES = ( (5, "5 минут"), (15, "15 минут"), (30, "30 минут"), (60, "1 час"), (1440, "1 сутки"), ) LOOKBACK_CHOICES = ( (1, "1 день"), (3, "3 дня"), (7, "7 дней"), ) singleton = models.BooleanField(default=True, unique=True, editable=False) enabled = models.BooleanField("Включено", default=False) frequency_minutes = models.PositiveIntegerField( "Периодичность", choices=FREQUENCY_CHOICES, default=1440, ) lookback_days = models.PositiveIntegerField( "Глубина обновления", choices=LOOKBACK_CHOICES, default=3, ) next_run_at = models.DateTimeField( "Следующий запуск", null=True, blank=True, ) last_started_at = models.DateTimeField( "Последний старт", null=True, blank=True, ) last_finished_at = models.DateTimeField( "Последнее завершение", null=True, blank=True, ) last_status = models.CharField( "Последний статус", max_length=16, choices=STATUS_CHOICES, default=STATUS_IDLE, ) last_error = models.TextField("Последняя ошибка", blank=True) last_success_count = models.PositiveIntegerField("Успешно", default=0) last_error_count = models.PositiveIntegerField("Ошибок", default=0) updated_by = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True, related_name="updated_attendance_auto_sync_settings", verbose_name="Изменил", ) updated_at = models.DateTimeField("Обновлено", auto_now=True) class Meta: verbose_name = "Автообновление посещаемости" verbose_name_plural = "Автообновление посещаемости" def save(self, *args, **kwargs): self.singleton = True super().save(*args, **kwargs) def __str__(self) -> str: return "Автообновление посещаемости" class AttendanceRecord(models.Model): analysis_run = models.ForeignKey( AttendanceAnalysisRun, on_delete=models.CASCADE, related_name="records", verbose_name="Запуск анализа", ) employee = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="attendance_records", verbose_name="Сотрудник", ) date = models.DateField("Дата") display_name = models.CharField("Имя из анализа", max_length=255, blank=True) arrival_time = models.CharField( "Время прихода", max_length=32, blank=True, null=True, ) departure_time = models.CharField( "Время ухода", max_length=32, blank=True, null=True, ) work_hours = models.FloatField("Отработано часов", null=True, blank=True) expected_hours = models.FloatField("Ожидаемо часов", null=True, blank=True) is_workday = models.BooleanField("Рабочий день", default=True) is_late = models.BooleanField("Опоздание", default=False) late_minutes = models.IntegerField("Минут опоздания", null=True, blank=True) is_early_leave = models.BooleanField("Ранний уход", default=False) early_leave_minutes = models.IntegerField( "Минут раннего ухода", null=True, blank=True, ) is_underwork = models.BooleanField("Недоработка", default=False) underwork_hours = models.FloatField("Часов недоработки", null=True, blank=True) is_overtime = models.BooleanField("Переработка", default=False) overtime_hours = models.FloatField("Часов переработки", null=True, blank=True) is_absent = models.BooleanField("Отсутствие", default=False) statuses = models.JSONField("Статусы", default=list, blank=True) employee_issues = models.JSONField("Проблемы сотрудника", default=list, blank=True) technical_issues = models.JSONField( "Технические проблемы", default=list, blank=True ) personnel_status = models.CharField( "Кадровое состояние", max_length=64, default="normal", blank=True, ) personnel_status_label = models.CharField( "Кадровое состояние для отображения", max_length=128, blank=True, ) personnel_action = models.ForeignKey( "employees.EmployeeAction", on_delete=models.SET_NULL, null=True, blank=True, related_name="attendance_records", verbose_name="Кадровое событие", ) effective_is_workday = models.BooleanField( "Рабочий день с учетом кадрового состояния", default=True, ) is_manually_edited = models.BooleanField( "Изменено вручную", default=False, ) manual_edit_payload = models.JSONField( "Ручная корректировка", default=dict, blank=True, ) manual_edited_by = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True, related_name="manual_attendance_edits", verbose_name="Изменил вручную", ) manual_edited_at = models.DateTimeField( "Время ручного изменения", null=True, blank=True, ) raw_data = models.JSONField("Исходная запись анализа", default=dict, blank=True) updated_at = models.DateTimeField("Обновлено", auto_now=True) class Meta: ordering = ["-date", "-id"] verbose_name = "Запись посещаемости" verbose_name_plural = "Записи посещаемости" constraints = [ models.UniqueConstraint( fields=["employee", "date"], name="attendance_record_employee_date_unique", ), ] indexes = [ models.Index(fields=["employee", "date"]), models.Index(fields=["analysis_run"]), models.Index(fields=["personnel_status"]), models.Index( fields=["is_manually_edited"], name="attendance__is_manu_07f532_idx", ), ] def __str__(self) -> str: return f"{self.employee_id}: {self.date}"