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:
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user