Files
ACORE/apps/it_manager/models/tickets.py
T
lokses 38747a93fc IT-менеджер: каталог моделей оборудования + перевод choices на справочники
- EquipmentModel (каталог: тип+бренд+модель заводятся раз) отделён от
  Equipment (экземпляр: серийник+локация); миграция сворачивает данные.
  Массовый ввод серийников, массовая выдача на локации/сотрудников
- Enum-choices (тип/статус/приоритет заявок, вид работ, каналы, расходники,
  договоры) переведены на core.Directory с quick-add из формы
- Фронт-страница /it-manager/: вкладки оборудование/заявки/работы/
  расходники/провайдеры/обращения; searchable-select локаций
- Тесты (10)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 17:31:52 +03:00

119 lines
4.6 KiB
Python

from django.db import models
from django.conf import settings
from django.utils import timezone
from core.models.directories import directory_default
class Ticket(models.Model):
number = models.CharField('Номер', max_length=20, unique=True, editable=False)
title = models.CharField('Заголовок', max_length=200)
ticket_type = models.ForeignKey(
'core.Directory', on_delete=models.PROTECT,
limit_choices_to={'directory_type__code': 'ticket_type', 'is_active': True},
related_name='tickets_by_type', verbose_name='Тип',
)
priority = models.ForeignKey(
'core.Directory', on_delete=models.PROTECT,
limit_choices_to={'directory_type__code': 'ticket_priority', 'is_active': True},
related_name='tickets_by_priority', verbose_name='Приоритет',
default=directory_default('ticket_priority', 'medium'),
)
status = models.ForeignKey(
'core.Directory', on_delete=models.PROTECT,
limit_choices_to={'directory_type__code': 'ticket_status', 'is_active': True},
related_name='tickets_by_status', verbose_name='Статус',
default=directory_default('ticket_status', 'new'),
)
requester = models.ForeignKey(
settings.AUTH_USER_MODEL, on_delete=models.PROTECT,
related_name='created_tickets', verbose_name='Заявитель',
)
assignee = models.ForeignKey(
settings.AUTH_USER_MODEL, on_delete=models.SET_NULL,
null=True, blank=True,
related_name='assigned_tickets', verbose_name='Исполнитель',
)
equipment = models.ForeignKey(
'it_manager.Equipment', on_delete=models.SET_NULL,
null=True, blank=True,
related_name='tickets', verbose_name='Оборудование',
)
location = models.ForeignKey(
'locations.Location', on_delete=models.SET_NULL,
null=True, blank=True,
related_name='it_tickets', verbose_name='Локация',
)
description = models.TextField('Описание проблемы')
resolution = models.TextField('Решение', blank=True)
created_at = models.DateTimeField(auto_now_add=True, db_index=True)
updated_at = models.DateTimeField(auto_now=True)
resolved_at = models.DateTimeField('Решена в', null=True, blank=True)
closed_at = models.DateTimeField('Закрыта в', null=True, blank=True)
class Meta:
verbose_name = 'Заявка'
verbose_name_plural = 'Заявки'
ordering = ['-created_at']
def __str__(self):
return f'[{self.number}] {self.title}'
def save(self, *args, **kwargs):
if not self.number:
self.number = self._generate_number()
if self.status.code == 'resolved' and not self.resolved_at:
self.resolved_at = timezone.now()
if self.status.code == 'closed' and not self.closed_at:
self.closed_at = timezone.now()
super().save(*args, **kwargs)
def _generate_number(self):
today = timezone.now()
prefix = f'IT-{today.year}{today.month:02d}'
last = (
Ticket.objects
.filter(number__startswith=prefix)
.order_by('-number')
.values_list('number', flat=True)
.first()
)
seq = int(last.split('-')[-1]) + 1 if last else 1
return f'{prefix}-{seq:04d}'
@property
def is_overdue(self):
if self.status.code in ('resolved', 'closed'):
return False
sla_hours = {'critical': 4, 'high': 8, 'medium': 24, 'low': 72}
limit = sla_hours.get(self.priority.code, 24)
return (timezone.now() - self.created_at).total_seconds() / 3600 > limit
class TicketComment(models.Model):
ticket = models.ForeignKey(
Ticket, on_delete=models.CASCADE,
related_name='comments', verbose_name='Заявка',
)
author = models.ForeignKey(
settings.AUTH_USER_MODEL, on_delete=models.PROTECT,
related_name='ticket_comments', verbose_name='Автор',
)
text = models.TextField('Комментарий')
is_internal = models.BooleanField(
'Внутренний', default=False,
help_text='Виден только сотрудникам IT',
)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
verbose_name = 'Комментарий'
verbose_name_plural = 'Комментарии'
ordering = ['created_at']
def __str__(self):
return f'{self.ticket.number} / {self.author} / {self.created_at:%d.%m.%Y %H:%M}'