from django.conf import settings from django.contrib.contenttypes.fields import GenericForeignKey from django.contrib.contenttypes.models import ContentType from django.db import models from django.utils import timezone from employees.models import Department class TaskPriority(models.TextChoices): LOW = "low", "Низкий" MEDIUM = "medium", "Средний" HIGH = "high", "Высокий" CRITICAL = "critical", "Критический" class TaskBoardAccessScope(models.TextChoices): ALL = "all", "Для всех" PRIVATE = "private", "Для себя" RESTRICTED = "restricted", "Выборочно" class TaskBoard(models.Model): name = models.CharField("Название", max_length=255) description = models.TextField("Описание", blank=True) created_by = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.PROTECT, related_name="created_task_boards", verbose_name="Создал", ) members = models.ManyToManyField( settings.AUTH_USER_MODEL, blank=True, related_name="task_boards", verbose_name="Участники", ) departments = models.ManyToManyField( Department, blank=True, related_name="task_boards", verbose_name="Отделы", ) access_scope = models.CharField( "Доступ", max_length=16, choices=TaskBoardAccessScope.choices, default=TaskBoardAccessScope.ALL, ) is_archived = models.BooleanField("Архивная", default=False) created_at = models.DateTimeField("Создано", auto_now_add=True) updated_at = models.DateTimeField("Обновлено", auto_now=True) class Meta: verbose_name = "Доска задач" verbose_name_plural = "Доски задач" ordering = ["name", "id"] def __str__(self): return self.name class TaskUserSettings(models.Model): user = models.OneToOneField( settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="task_settings", verbose_name="Пользователь", ) default_board = models.ForeignKey( TaskBoard, on_delete=models.SET_NULL, null=True, blank=True, related_name="default_for_user_settings", 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 __str__(self): return f"{self.user} - задачи" class TaskColumn(models.Model): board = models.ForeignKey( TaskBoard, on_delete=models.CASCADE, related_name="columns", verbose_name="Доска", ) name = models.CharField("Название", max_length=120) position = models.PositiveIntegerField("Позиция", default=0) color = models.CharField("Цвет", max_length=32, blank=True) is_done = models.BooleanField("Финальная колонка", default=False) is_archived = models.BooleanField("Архивная", default=False) created_at = models.DateTimeField("Создано", auto_now_add=True) updated_at = models.DateTimeField("Обновлено", auto_now=True) class Meta: verbose_name = "Колонка задач" verbose_name_plural = "Колонки задач" ordering = ["position", "id"] constraints = [ models.UniqueConstraint( fields=["board", "name"], name="uniq_task_column_board_name", ) ] def __str__(self): return f"{self.board}: {self.name}" class TaskLabel(models.Model): board = models.ForeignKey( TaskBoard, on_delete=models.CASCADE, related_name="labels", verbose_name="Доска", ) name = models.CharField("Название", max_length=80) color = models.CharField("Цвет", max_length=32, default="#38bdf8") class Meta: verbose_name = "Метка задачи" verbose_name_plural = "Метки задач" ordering = ["name", "id"] constraints = [ models.UniqueConstraint( fields=["board", "name"], name="uniq_task_label_board_name", ) ] def __str__(self): return self.name class Task(models.Model): board = models.ForeignKey( TaskBoard, on_delete=models.CASCADE, related_name="tasks", verbose_name="Доска", ) column = models.ForeignKey( TaskColumn, on_delete=models.PROTECT, related_name="tasks", verbose_name="Колонка", ) title = models.CharField("Название", max_length=255) description = models.TextField("Описание", blank=True) created_by = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.PROTECT, related_name="created_tasks", verbose_name="Создал", ) assignee = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True, related_name="assigned_tasks", verbose_name="Исполнитель", ) labels = models.ManyToManyField( TaskLabel, blank=True, related_name="tasks", verbose_name="Метки", ) priority = models.CharField( "Приоритет", max_length=20, choices=TaskPriority.choices, default=TaskPriority.MEDIUM, ) due_date = models.DateField("Срок", null=True, blank=True) position = models.PositiveIntegerField("Позиция", default=0) completed_at = models.DateTimeField("Завершено", null=True, blank=True) created_at = models.DateTimeField("Создано", auto_now_add=True) updated_at = models.DateTimeField("Обновлено", auto_now=True) class Meta: verbose_name = "Задача" verbose_name_plural = "Задачи" ordering = ["position", "-created_at", "id"] indexes = [ models.Index(fields=["board", "column", "position"]), models.Index(fields=["assignee", "due_date"]), ] def __str__(self): return self.title def save(self, *args, **kwargs): if self.column_id and self.board_id != self.column.board_id: self.board_id = self.column.board_id if self.column_id and self.column.is_done and not self.completed_at: self.completed_at = timezone.now() elif self.column_id and not self.column.is_done: self.completed_at = None super().save(*args, **kwargs) class TaskAttachment(models.Model): task = models.ForeignKey( Task, on_delete=models.CASCADE, related_name="attachments", verbose_name="Задача", ) file = models.FileField( "Файл", upload_to="task_attachments/%Y/%m/%d/", ) file_name = models.CharField("Название файла", max_length=255) file_size = models.PositiveBigIntegerField("Размер файла (байты)") mime_type = models.CharField("MIME-тип", max_length=255, blank=True) uploaded_by = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.PROTECT, related_name="uploaded_task_attachments", verbose_name="Загрузил", ) created_at = models.DateTimeField("Загружено", auto_now_add=True) class Meta: verbose_name = "Вложение задачи" verbose_name_plural = "Вложения задач" ordering = ["created_at", "id"] indexes = [models.Index(fields=["task", "created_at"])] def __str__(self): return f"{self.task}: {self.file_name}" class TaskChecklistItem(models.Model): task = models.ForeignKey( Task, on_delete=models.CASCADE, related_name="checklist_items", verbose_name="Задача", ) title = models.CharField("Пункт", max_length=500) position = models.PositiveIntegerField("Позиция", default=0) is_completed = models.BooleanField("Выполнен", default=False) created_by = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.PROTECT, related_name="created_task_checklist_items", verbose_name="Создал", ) completed_by = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True, related_name="completed_task_checklist_items", verbose_name="Отметил выполненным", ) completed_at = models.DateTimeField("Выполнен", null=True, blank=True) created_at = models.DateTimeField("Создано", auto_now_add=True) updated_at = models.DateTimeField("Обновлено", auto_now=True) class Meta: verbose_name = "Пункт чек-листа задачи" verbose_name_plural = "Пункты чек-листа задач" ordering = ["position", "id"] indexes = [ models.Index(fields=["task", "position"]), models.Index(fields=["task", "is_completed"]), ] def __str__(self): return f"{self.task}: {self.title}" class TaskLinkedObjectKind(models.TextChoices): POST = "post", "Новость" MESSAGE = "message", "Сообщение" CALENDAR_EVENT = "calendar_event", "Календарное событие" DOCUMENT = "document", "Документ" REQUEST = "request", "Заявление" PROCUREMENT_REQUEST = "procurement_request", "Заявка на закупку" EMPLOYEE = "employee", "Сотрудник" GUEST = "guest", "Гость" GUEST_VISIT = "guest_visit", "Заявка на гостевой визит" ATTENDANCE_RECORD = "attendance_record", "Запись посещаемости" class TaskActivityObjectKind(models.TextChoices): POST = "post", "Новость" MESSAGE = "message", "Сообщение" CALENDAR_EVENT = "calendar_event", "Календарное событие" DOCUMENT = "document", "Документ" REQUEST = "request", "Заявление" PROCUREMENT_REQUEST = "procurement_request", "Заявка на закупку" EMPLOYEE = "employee", "Сотрудник" GUEST = "guest", "Гость" GUEST_VISIT = "guest_visit", "Заявка на гостевой визит" ATTENDANCE_RECORD = "attendance_record", "Запись посещаемости" EXTERNAL_LINK = "external_link", "Внешняя ссылка" CHECKLIST_ITEM = "checklist_item", "Пункт чек-листа" COMMENT = "comment", "Комментарий" class TaskActivityAction(models.TextChoices): CREATED = "created", "Создал задачу" UPDATED = "updated", "Обновил задачу" CLAIMED = "claimed", "Взял задачу в работу" MOVED = "moved", "Переместил задачу" LINKED = "linked", "Связал объект" UNLINKED = "unlinked", "Убрал связь" ATTACHMENT_ADDED = "attachment_added", "Добавил файл" ATTACHMENT_REMOVED = "attachment_removed", "Удалил файл" CHECKLIST_ITEM_ADDED = "checklist_item_added", "Добавил пункт чек-листа" CHECKLIST_ITEM_UPDATED = "checklist_item_updated", "Изменил пункт чек-листа" CHECKLIST_ITEM_COMPLETED = ( "checklist_item_completed", "Выполнил пункт чек-листа", ) CHECKLIST_ITEM_REOPENED = ( "checklist_item_reopened", "Вернул пункт чек-листа в работу", ) CHECKLIST_ITEM_REMOVED = "checklist_item_removed", "Удалил пункт чек-листа" COMMENT_ADDED = "comment_added", "Добавил комментарий" COMMENT_EDITED = "comment_edited", "Изменил комментарий" COMMENT_REMOVED = "comment_removed", "Удалил комментарий" class TaskActivity(models.Model): task = models.ForeignKey( Task, on_delete=models.CASCADE, related_name="activities", verbose_name="Задача", ) actor = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True, related_name="task_activities", verbose_name="Исполнитель", ) action = models.CharField( "Действие", max_length=32, choices=TaskActivityAction.choices, ) object_kind = models.CharField( "Тип объекта", max_length=32, choices=TaskActivityObjectKind.choices, blank=True, ) object_id = models.PositiveBigIntegerField("ID объекта", null=True, blank=True) metadata = models.JSONField("Метаданные", default=dict, blank=True) created_at = models.DateTimeField("Создано", auto_now_add=True) class Meta: verbose_name = "История задачи" verbose_name_plural = "История задач" ordering = ["-created_at", "-id"] indexes = [ models.Index(fields=["task", "-created_at"]), ] def __str__(self): return f"{self.task} - {self.action}" class TaskLinkedObject(models.Model): task = models.ForeignKey( Task, on_delete=models.CASCADE, related_name="linked_objects", verbose_name="Задача", ) kind = models.CharField( "Тип связи", max_length=32, choices=TaskLinkedObjectKind.choices, ) content_type = models.ForeignKey( ContentType, on_delete=models.CASCADE, verbose_name="Тип объекта", ) object_id = models.PositiveBigIntegerField("ID объекта") content_object = GenericForeignKey("content_type", "object_id") created_by = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.PROTECT, related_name="created_task_links", verbose_name="Связал", ) created_at = models.DateTimeField("Создано", auto_now_add=True) metadata = models.JSONField("Метаданные", default=dict, blank=True) class Meta: verbose_name = "Связанный объект задачи" verbose_name_plural = "Связанные объекты задач" ordering = ["-created_at", "-id"] constraints = [ models.UniqueConstraint( fields=["task", "content_type", "object_id"], name="uniq_task_linked_object", ) ] indexes = [ models.Index(fields=["task", "kind"]), models.Index(fields=["content_type", "object_id"]), ] def __str__(self): return f"{self.task} -> {self.kind}:{self.object_id}" class TaskExternalLink(models.Model): task = models.ForeignKey( Task, on_delete=models.CASCADE, related_name="external_links", verbose_name="Задача", ) url = models.URLField("Ссылка", max_length=2048) title = models.CharField("Название", max_length=255, blank=True) created_by = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.PROTECT, related_name="created_task_external_links", verbose_name="Добавил", ) created_at = models.DateTimeField("Создано", auto_now_add=True) class Meta: verbose_name = "Внешняя ссылка задачи" verbose_name_plural = "Внешние ссылки задач" ordering = ["-created_at", "-id"] constraints = [ models.UniqueConstraint( fields=["task", "url"], name="uniq_task_external_link", ) ] indexes = [models.Index(fields=["task", "created_at"])] def __str__(self): return f"{self.task} -> {self.title or self.url}"