38747a93fc
- EquipmentModel (каталог: тип+бренд+модель заводятся раз) отделён от Equipment (экземпляр: серийник+локация); миграция сворачивает данные. Массовый ввод серийников, массовая выдача на локации/сотрудников - Enum-choices (тип/статус/приоритет заявок, вид работ, каналы, расходники, договоры) переведены на core.Directory с quick-add из формы - Фронт-страница /it-manager/: вкладки оборудование/заявки/работы/ расходники/провайдеры/обращения; searchable-select локаций - Тесты (10) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
175 lines
7.1 KiB
Python
175 lines
7.1 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 EquipmentModel(models.Model):
|
|
"""Каталог моделей: тип + бренд + название заводятся один раз
|
|
(например «Mikrotik RB952Ui-5ac2nD»). Конкретные экземпляры (Equipment)
|
|
ссылаются на модель и несут только серийник/инвентарник/локацию —
|
|
больше не нужно каждый раз переписывать тип и модель."""
|
|
|
|
equipment_type = models.ForeignKey(
|
|
'core.Directory', on_delete=models.PROTECT,
|
|
limit_choices_to={'directory_type__code': 'equipment_type', 'is_active': True},
|
|
related_name='models_by_type', verbose_name='Тип',
|
|
)
|
|
brand = models.ForeignKey(
|
|
'core.Directory', on_delete=models.SET_NULL,
|
|
limit_choices_to={'directory_type__code': 'equipment_brand', 'is_active': True},
|
|
related_name='models_by_brand', verbose_name='Бренд',
|
|
null=True, blank=True,
|
|
)
|
|
name = models.CharField('Модель', max_length=100)
|
|
# Гарантия по умолчанию (месяцев) — при массовом вводе экземпляров
|
|
# warranty_until можно посчитать от даты покупки, не вводя вручную
|
|
warranty_months = models.PositiveIntegerField(
|
|
'Гарантия (мес.)', null=True, blank=True,
|
|
help_text='Стандартный срок гарантии для этой модели',
|
|
)
|
|
notes = models.TextField('Примечания', 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 = ['equipment_type__name', 'brand__name', 'name']
|
|
constraints = [
|
|
models.UniqueConstraint(
|
|
fields=['equipment_type', 'brand', 'name'],
|
|
name='unique_equipment_model',
|
|
),
|
|
]
|
|
|
|
def __str__(self):
|
|
brand = f'{self.brand.name} ' if self.brand_id else ''
|
|
return f'{brand}{self.name}'.strip()
|
|
|
|
@property
|
|
def full_label(self):
|
|
return f'{self.equipment_type.name} · {self}'
|
|
|
|
|
|
class Equipment(models.Model):
|
|
equipment_model = models.ForeignKey(
|
|
EquipmentModel, on_delete=models.PROTECT,
|
|
related_name='units', verbose_name='Модель',
|
|
)
|
|
serial_number = models.CharField('Серийный номер', max_length=100, unique=True)
|
|
inventory_number = models.CharField('Инвентарный номер', max_length=50,
|
|
blank=True, unique=True, null=True)
|
|
|
|
status = models.ForeignKey(
|
|
'core.Directory', on_delete=models.PROTECT,
|
|
limit_choices_to={'directory_type__code': 'equipment_status', 'is_active': True},
|
|
related_name='equipment_by_status', verbose_name='Статус',
|
|
default=directory_default('equipment_status', 'stock'),
|
|
)
|
|
assigned_to = models.ForeignKey(
|
|
settings.AUTH_USER_MODEL, on_delete=models.SET_NULL,
|
|
null=True, blank=True,
|
|
related_name='assigned_equipment', verbose_name='Выдано сотруднику',
|
|
)
|
|
location = models.ForeignKey(
|
|
'locations.Location', on_delete=models.SET_NULL,
|
|
null=True, blank=True,
|
|
related_name='location_equipment', verbose_name='Локация',
|
|
)
|
|
|
|
purchase_date = models.DateField('Дата покупки', null=True, blank=True)
|
|
warranty_until = models.DateField('Гарантия до', null=True, blank=True)
|
|
|
|
notes = models.TextField('Примечания', 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 = ['equipment_model__equipment_type__name', 'equipment_model__name']
|
|
indexes = [
|
|
models.Index(fields=['assigned_to']),
|
|
models.Index(fields=['equipment_model']),
|
|
]
|
|
|
|
def __str__(self):
|
|
return f'{self.equipment_model} [{self.serial_number}]'
|
|
|
|
# --- Удобные проксі до моделі каталогу (щоб шаблони/код не лізли
|
|
# кожен раз через equipment_model) ---
|
|
@property
|
|
def equipment_type(self):
|
|
return self.equipment_model.equipment_type
|
|
|
|
@property
|
|
def brand(self):
|
|
return self.equipment_model.brand
|
|
|
|
@property
|
|
def model(self):
|
|
return self.equipment_model.name
|
|
|
|
@property
|
|
def is_under_warranty(self):
|
|
if not self.warranty_until:
|
|
return None
|
|
return self.warranty_until >= timezone.now().date()
|
|
|
|
@property
|
|
def warranty_status(self):
|
|
if not self.warranty_until:
|
|
return 'Нет данных'
|
|
if self.is_under_warranty:
|
|
days = (self.warranty_until - timezone.now().date()).days
|
|
if days <= 30:
|
|
return f'Истекает через {days} дн.'
|
|
return 'Действительна'
|
|
return 'Истекла'
|
|
|
|
|
|
class EquipmentAssignment(models.Model):
|
|
"""История выдачи и перемещений оборудования."""
|
|
|
|
equipment = models.ForeignKey(
|
|
Equipment, on_delete=models.CASCADE,
|
|
related_name='assignments', verbose_name='Оборудование',
|
|
)
|
|
assigned_to = models.ForeignKey(
|
|
settings.AUTH_USER_MODEL, on_delete=models.SET_NULL,
|
|
null=True, blank=True,
|
|
related_name='equipment_assignments', verbose_name='Сотрудник',
|
|
)
|
|
location = models.ForeignKey(
|
|
'locations.Location', on_delete=models.SET_NULL,
|
|
null=True, blank=True,
|
|
related_name='equipment_assignments', verbose_name='Локация',
|
|
)
|
|
assigned_by = models.ForeignKey(
|
|
settings.AUTH_USER_MODEL, on_delete=models.SET_NULL,
|
|
null=True, blank=True,
|
|
related_name='issued_equipment', verbose_name='Выдал',
|
|
)
|
|
assigned_at = models.DateTimeField('Дата выдачи', default=timezone.now)
|
|
returned_at = models.DateTimeField('Дата возврата', null=True, blank=True)
|
|
notes = models.TextField('Примечания', blank=True)
|
|
|
|
class Meta:
|
|
verbose_name = 'Перемещение оборудования'
|
|
verbose_name_plural = 'История перемещений'
|
|
ordering = ['-assigned_at']
|
|
indexes = [
|
|
models.Index(fields=['equipment', 'assigned_at']),
|
|
]
|
|
|
|
def __str__(self):
|
|
target = self.assigned_to or self.location or 'Склад'
|
|
return f'{self.equipment} → {target} ({self.assigned_at:%d.%m.%Y})'
|
|
|
|
@property
|
|
def is_active(self):
|
|
return self.returned_at is None
|