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.db.models.functions import Lower from django.utils import timezone from employees.models import Department class TaskPriority(models.TextChoices): LOW = "low", "Низкий" MEDIUM = "medium", "Средний" HIGH = "high", "Высокий" CRITICAL = "critical", "Критический" class TaskAutomationKind(models.TextChoices): EVENT = "event", "Событие" SCHEDULE = "schedule", "Срок" BUTTON = "button", "Кнопка" class TaskAutomationTrigger(models.TextChoices): MANUAL = "manual", "Ручной запуск" TASK_CREATED = "task_created", "Задача создана" TASK_UPDATED = "task_updated", "Задача изменена" TASK_MOVED = "task_moved", "Задача перемещена" LABEL_ADDED = "label_added", "Метка добавлена" LABEL_REMOVED = "label_removed", "Метка удалена" ASSIGNEE_CHANGED = "assignee_changed", "Исполнитель изменён" DUE_DATE_CHANGED = "due_date_changed", "Срок изменён" PRIORITY_CHANGED = "priority_changed", "Срочность изменена" ATTACHMENT_ADDED = "attachment_added", "Файл добавлен" COMMENT_ADDED = "comment_added", "Комментарий добавлен" CHECKLIST_COMPLETED = "checklist_completed", "Пункт чек-листа выполнен" LINKED_OBJECT_ADDED = "linked_object_added", "Связанный объект добавлен" DATE_REACHED = "date_reached", "Наступила дата" class TaskAutomationRunStatus(models.TextChoices): RUNNING = "running", "Выполняется" SUCCESS = "success", "Выполнено" PARTIAL = "partial", "Выполнено частично" FAILED = "failed", "Ошибка" SKIPPED = "skipped", "Пропущено" 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) avatar = models.ImageField( "Аватар доски", upload_to="task_board_avatars/%Y/%m/", null=True, 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 TaskBoardGroup(models.Model): name = models.CharField("Название", max_length=120) user = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="task_board_groups", verbose_name="Пользователь", ) position = models.PositiveIntegerField("Позиция", default=0) created_at = models.DateTimeField("Создано", auto_now_add=True) updated_at = models.DateTimeField("Обновлено", auto_now=True) class Meta: verbose_name = "Группа досок задач" verbose_name_plural = "Группы досок задач" ordering = ["position", "name", "id"] constraints = [ models.UniqueConstraint( fields=["user", "name"], name="uniq_task_board_group_user_name", ) ] def __str__(self): return f"{self.user}: {self.name}" class TaskBoardPlacement(models.Model): user = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="task_board_placements", verbose_name="Пользователь", ) board = models.ForeignKey( TaskBoard, on_delete=models.CASCADE, related_name="user_placements", verbose_name="Доска", ) group = models.ForeignKey( TaskBoardGroup, on_delete=models.SET_NULL, null=True, blank=True, related_name="board_placements", verbose_name="Группа", ) is_pinned = models.BooleanField("Закреплена", default=False) pinned_at = models.DateTimeField("Закреплена в", null=True, blank=True) notifications_enabled = models.BooleanField("Уведомления", default=True) view_state = models.JSONField( "Состояние представления", default=dict, blank=True, ) created_at = models.DateTimeField("Создано", auto_now_add=True) updated_at = models.DateTimeField("Обновлено", auto_now=True) class Meta: verbose_name = "Размещение доски задач" verbose_name_plural = "Размещения досок задач" constraints = [ models.UniqueConstraint( fields=["user", "board"], name="uniq_task_board_placement_user_board", ) ] indexes = [ models.Index(fields=["user", "is_pinned"]), models.Index(fields=["board", "user"]), ] def __str__(self): return f"{self.user}: {self.board} -> {self.group}" 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="Доска по умолчанию", ) default_group = models.ForeignKey( TaskBoardGroup, 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="Доска", ) parent = models.ForeignKey( "self", on_delete=models.CASCADE, related_name="subcolumns", null=True, blank=True, 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"], condition=models.Q(parent__isnull=True), name="uniq_task_top_column_board_name", ), models.UniqueConstraint( fields=["parent", "name"], condition=models.Q(parent__isnull=False), name="uniq_task_subcolumn_parent_name", ), models.UniqueConstraint( fields=["board"], condition=models.Q(parent__isnull=True, is_done=True), name="uniq_task_done_column_per_board", ), ] def __str__(self): return f"{self.board}: {self.name}" @property def is_subcolumn(self): return self.parent_id is not None class TaskColumnRow(models.Model): column = models.ForeignKey( TaskColumn, on_delete=models.CASCADE, related_name="rows", verbose_name="Основная колонка", ) name = models.CharField("Название", max_length=120) position = models.PositiveIntegerField("Позиция", default=0) color = models.CharField("Цвет", max_length=32, 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"] constraints = [ models.UniqueConstraint( fields=["column", "name"], name="uniq_task_column_row_name", ) ] def __str__(self): return f"{self.column}: {self.name}" class TaskLabel(models.Model): 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( Lower("name"), name="uniq_task_label_name_ci", ) ] 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="Колонка", ) row = models.ForeignKey( TaskColumnRow, on_delete=models.SET_NULL, related_name="tasks", null=True, blank=True, 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="Исполнитель", ) participants = models.ManyToManyField( settings.AUTH_USER_MODEL, blank=True, related_name="participating_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", "row", "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 TaskAutomation(models.Model): name = models.CharField("Название", max_length=160) description = models.TextField("Описание", blank=True) kind = models.CharField( "Тип", max_length=16, choices=TaskAutomationKind.choices, default=TaskAutomationKind.EVENT, ) trigger = models.CharField( "Событие", max_length=32, choices=TaskAutomationTrigger.choices, default=TaskAutomationTrigger.TASK_UPDATED, ) conditions = models.JSONField( "Условия", default=dict, blank=True, ) actions = models.JSONField( "Действия", default=list, blank=True, ) schedule_config = models.JSONField( "Настройки срока", default=dict, blank=True, ) applies_to_all_boards = models.BooleanField( "Для всех досок", default=True, ) boards = models.ManyToManyField( TaskBoard, blank=True, related_name="automations", verbose_name="Доски", ) is_active = models.BooleanField("Активна", default=True) stop_on_error = models.BooleanField("Остановить при ошибке", default=True) position = models.PositiveIntegerField("Позиция", default=0) created_by = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.PROTECT, related_name="created_task_automations", verbose_name="Создал", ) last_run_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 = ["kind", "position", "name", "id"] indexes = [ models.Index(fields=["kind", "is_active", "position"]), models.Index(fields=["trigger", "is_active"]), ] def __str__(self): return self.name def applies_to_board(self, board_id): return self.applies_to_all_boards or self.boards.filter(id=board_id).exists() class TaskAutomationRun(models.Model): automation = models.ForeignKey( TaskAutomation, on_delete=models.CASCADE, related_name="runs", verbose_name="Автоматизация", ) task = models.ForeignKey( Task, on_delete=models.CASCADE, related_name="automation_runs", verbose_name="Задача", ) trigger_activity = models.ForeignKey( "TaskActivity", on_delete=models.SET_NULL, null=True, blank=True, related_name="automation_runs", verbose_name="Событие", ) actor = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True, related_name="task_automation_runs", verbose_name="Запустил", ) source = models.CharField("Источник", max_length=16) status = models.CharField( "Статус", max_length=16, choices=TaskAutomationRunStatus.choices, default=TaskAutomationRunStatus.RUNNING, ) idempotency_key = models.CharField( "Ключ идемпотентности", max_length=255, unique=True, ) context = models.JSONField("Контекст", default=dict, blank=True) actions_log = models.JSONField("Журнал действий", default=list, blank=True) error = models.TextField("Ошибка", blank=True) started_at = models.DateTimeField("Начато", auto_now_add=True) finished_at = models.DateTimeField("Завершено", null=True, blank=True) class Meta: verbose_name = "Запуск автоматизации задач" verbose_name_plural = "Запуски автоматизаций задач" ordering = ["-started_at", "-id"] indexes = [ models.Index(fields=["automation", "-started_at"]), models.Index(fields=["task", "-started_at"]), models.Index(fields=["status", "-started_at"]), ] def __str__(self): return f"{self.automation}: {self.task} ({self.status})" class TaskAttachment(models.Model): task = models.ForeignKey( Task, on_delete=models.CASCADE, related_name="attachments", verbose_name="Задача", ) comment = models.ForeignKey( "communications.Message", on_delete=models.SET_NULL, null=True, blank=True, related_name="task_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) is_cover = models.BooleanField("Обложка задачи", default=False) 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"])] constraints = [ models.UniqueConstraint( fields=["task"], condition=models.Q(is_cover=True), name="uniq_task_cover_attachment", ) ] 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}"