Files
ACORE/apps/power_assets/models.py
T
lokses 16bf2cf1c7 эксплуатация: показания счётчиков, энергоактивы и РРО
Конфиг счётчиков и день/ночь-тарифы, учёт энергосистем (генераторы,
инверторы), доступ к разделу РРО/ПРРО.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-09 18:03:44 +03:00

174 lines
7.5 KiB
Python

"""Учёт инверторов и генераторов с привязкой к объектам (аптека / офис / склад)."""
from django.conf import settings
from django.db import models
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from core.models import BaseModel
from core.models.directories import directory_default
class PowerAsset(BaseModel):
"""Единица энергооборудования коммунального отдела."""
asset_type = models.ForeignKey(
'core.Directory', on_delete=models.PROTECT,
limit_choices_to={'directory_type__code': 'power_asset_type', 'is_active': True},
related_name='power_assets', verbose_name=_('Тип'),
)
brand = models.CharField(_('Бренд'), max_length=100, blank=True)
model_name = models.CharField(_('Модель'), max_length=120)
serial_number = models.CharField(
_('Серийный номер'), max_length=100, blank=True,
help_text=_('Если известен — заполните для учёта.'),
)
inventory_number = models.CharField(
_('Инвентарный номер'), max_length=50, blank=True,
)
location = models.ForeignKey(
'locations.Location', on_delete=models.PROTECT,
related_name='power_assets', verbose_name=_('Объект'),
help_text=_('Аптека, офис или склад, где установлено оборудование.'),
)
status = models.ForeignKey(
'core.Directory', on_delete=models.PROTECT,
limit_choices_to={'directory_type__code': 'power_asset_status', 'is_active': True},
related_name='power_assets_by_status', verbose_name=_('Статус'),
default=directory_default('power_asset_status', 'active'),
)
capacity_kw = models.DecimalField(
_('Мощность, кВт'), max_digits=8, decimal_places=2,
null=True, blank=True,
)
fuel_type = models.CharField(
_('Тип топлива'), max_length=50, blank=True,
help_text=_('Для генераторов: бензин, дизель, газ…'),
)
battery_capacity_kwh = models.DecimalField(
_('Ёмкость АКБ, кВт·ч'), max_digits=8, decimal_places=2,
null=True, blank=True,
help_text=_('Для инверторных систем с накопителем.'),
)
installed_at = models.DateField(_('Дата установки'), null=True, blank=True)
warranty_until = models.DateField(_('Гарантия до'), null=True, blank=True)
last_serviced_at = models.DateField(_('Последнее обслуживание'), null=True, blank=True)
next_service_at = models.DateField(
_('Следующее обслуживание'), null=True, blank=True,
help_text=_('Плановая дата; обновляется из журнала обслуживания.'),
)
notes = models.TextField(_('Примечания'), blank=True)
created_by = models.ForeignKey(
settings.AUTH_USER_MODEL, on_delete=models.SET_NULL,
null=True, blank=True,
related_name='power_assets_created', verbose_name=_('Кто добавил'),
)
class Meta:
verbose_name = _('Энергооборудование')
verbose_name_plural = _('Энергооборудование')
ordering = ['location', 'asset_type__name', 'model_name']
indexes = [
models.Index(fields=['location', 'asset_type']),
models.Index(fields=['status']),
models.Index(fields=['next_service_at']),
]
def __str__(self):
brand = f'{self.brand} ' if self.brand else ''
return f'{self.asset_type.name}: {brand}{self.model_name}'.strip()
@property
def label(self) -> str:
parts = [self.asset_type.name]
if self.brand:
parts.append(self.brand)
parts.append(self.model_name)
if self.serial_number:
parts.append(f'[{self.serial_number}]')
return ' '.join(parts)
@property
def service_overdue(self) -> bool:
if not self.next_service_at:
return False
return self.next_service_at < timezone.localdate()
def refresh_service_dates(self):
"""Пересчитать last/next из журнала обслуживания."""
latest = self.service_logs.order_by('-service_date', '-id').first()
self.last_serviced_at = latest.service_date if latest else None
upcoming = (
self.service_logs.exclude(next_service_at__isnull=True)
.order_by('-service_date', '-id')
.values_list('next_service_at', flat=True)
.first()
)
self.next_service_at = upcoming
self.save(update_fields=['last_serviced_at', 'next_service_at', 'updated_at'])
class PowerAssetService(BaseModel):
"""Запись обслуживания / ТО энергооборудования."""
class ServiceKind(models.TextChoices):
INSPECTION = 'inspection', _('Осмотр / проверка')
MAINTENANCE = 'maintenance', _('Плановое ТО')
OIL_CHANGE = 'oil_change', _('Замена масла / фильтров')
BATTERY = 'battery', _('АКБ / инвертор')
TEST_RUN = 'test_run', _('Пробный запуск')
REPAIR = 'repair', _('Ремонт')
OTHER = 'other', _('Прочее')
asset = models.ForeignKey(
PowerAsset, on_delete=models.CASCADE,
related_name='service_logs', verbose_name=_('Оборудование'),
)
service_date = models.DateField(_('Дата'), default=timezone.localdate)
kind = models.CharField(
_('Вид работ'), max_length=32,
choices=ServiceKind.choices, default=ServiceKind.MAINTENANCE,
)
description = models.TextField(_('Что сделано'))
performed_by_name = models.CharField(
_('Исполнитель'), max_length=120, blank=True,
help_text=_('ФИО или подрядчик, если не сотрудник портала.'),
)
performed_by = models.ForeignKey(
settings.AUTH_USER_MODEL, on_delete=models.SET_NULL,
null=True, blank=True,
related_name='power_services_done', verbose_name=_('Сотрудник'),
)
engine_hours = models.DecimalField(
_('Моточасы'), max_digits=10, decimal_places=1,
null=True, blank=True,
help_text=_('Для генераторов — показания на момент ТО.'),
)
cost = models.DecimalField(
_('Стоимость'), max_digits=12, decimal_places=2,
null=True, blank=True,
)
next_service_at = models.DateField(
_('Следующее ТО'), null=True, blank=True,
)
notes = models.CharField(_('Комментарий'), max_length=255, blank=True)
created_by = models.ForeignKey(
settings.AUTH_USER_MODEL, on_delete=models.SET_NULL,
null=True, blank=True,
related_name='power_services_created', verbose_name=_('Кто внёс'),
)
class Meta:
verbose_name = _('Обслуживание энергооборудования')
verbose_name_plural = _('Журнал обслуживания')
ordering = ['-service_date', '-id']
indexes = [
models.Index(fields=['asset', 'service_date']),
]
def __str__(self):
return f'{self.asset} — {self.get_kind_display()} ({self.service_date:%d.%m.%Y})'