Files
ACORE/apps/it_manager/models/equipment.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

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