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>
This commit is contained in:
2026-07-22 17:31:52 +03:00
parent f342d67f97
commit 38747a93fc
23 changed files with 3051 additions and 229 deletions
+22 -37
View File
@@ -1,6 +1,8 @@
from django.db import models
from django.utils import timezone
from core.models.directories import directory_default
class InternetProvider(models.Model):
name = models.CharField('Название', max_length=200, unique=True)
@@ -23,35 +25,10 @@ class InternetProvider(models.Model):
@property
def active_contracts_count(self):
return self.contracts.filter(status='active').count()
return self.contracts.filter(status__code='active').count()
class InternetContract(models.Model):
SERVICE_TYPES = [
('internet', 'Интернет'),
('telephony', 'Телефония'),
('vpn', 'VPN'),
('dedicated', 'Выделенная линия'),
('other', 'Другое'),
]
CONNECTION_TYPES = [
('fiber', 'Оптоволокно (FTTH/FTTB)'),
('ethernet', 'Ethernet (FTTB)'),
('docsis', 'Кабель (DOCSIS)'),
('adsl', 'xDSL (ADSL/VDSL)'),
('4g', '4G/LTE'),
('5g', '5G'),
('radio', 'Радиоканал'),
('other', 'Другое'),
]
STATUS_CHOICES = [
('active', 'Активен'),
('suspended', 'Приостановлен'),
('terminated', 'Расторгнут'),
]
provider = models.ForeignKey(
InternetProvider, on_delete=models.PROTECT,
related_name='contracts', verbose_name='Провайдер',
@@ -62,12 +39,24 @@ class InternetContract(models.Model):
)
contract_number = models.CharField('Номер договора', max_length=100)
service_type = models.CharField('Тип услуги', max_length=20,
choices=SERVICE_TYPES, default='internet')
connection_type = models.CharField('Технология подключения', max_length=20,
choices=CONNECTION_TYPES, blank=True)
status = models.CharField('Статус', max_length=20,
choices=STATUS_CHOICES, default='active', db_index=True)
service_type = models.ForeignKey(
'core.Directory', on_delete=models.PROTECT,
limit_choices_to={'directory_type__code': 'contract_service_type', 'is_active': True},
related_name='contracts_by_service_type', verbose_name='Тип услуги',
default=directory_default('contract_service_type', 'internet'),
)
connection_type = models.ForeignKey(
'core.Directory', on_delete=models.SET_NULL,
limit_choices_to={'directory_type__code': 'contract_connection_type', 'is_active': True},
related_name='contracts_by_connection_type', verbose_name='Технология подключения',
null=True, blank=True,
)
status = models.ForeignKey(
'core.Directory', on_delete=models.PROTECT,
limit_choices_to={'directory_type__code': 'contract_status', 'is_active': True},
related_name='contracts_by_status', verbose_name='Статус',
default=directory_default('contract_status', 'active'),
)
start_date = models.DateField('Дата начала')
end_date = models.DateField('Дата окончания', null=True, blank=True)
@@ -90,17 +79,13 @@ class InternetContract(models.Model):
verbose_name_plural = 'Договоры с провайдерами'
ordering = ['location', 'provider']
unique_together = [('provider', 'contract_number')]
indexes = [
models.Index(fields=['status']),
models.Index(fields=['location', 'status']),
]
def __str__(self):
return f'{self.provider} — {self.location} (№{self.contract_number})'
@property
def is_expiring_soon(self):
if not self.end_date or self.status != 'active':
if not self.end_date or self.status.code != 'active':
return False
return (self.end_date - timezone.now().date()).days <= 30