38747a93fc
- EquipmentModel (каталог: тип+бренд+модель заводятся раз) отделён от Equipment (экземпляр: серийник+локация); миграция сворачивает данные. Массовый ввод серийников, массовая выдача на локации/сотрудников - Enum-choices (тип/статус/приоритет заявок, вид работ, каналы, расходники, договоры) переведены на core.Directory с quick-add из формы - Фронт-страница /it-manager/: вкладки оборудование/заявки/работы/ расходники/провайдеры/обращения; searchable-select локаций - Тесты (10) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
119 lines
4.6 KiB
Python
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}'
|