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

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

189 lines
7.8 KiB
Python

from django.conf import settings
from django.core.exceptions import ValidationError
from django.db import models
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from core.models import BaseModel
class UtilityMeterConfig(BaseModel):
"""Як знімаються показання лічильника в конкретній аптеці."""
READING_MODE_SINGLE = 'single'
READING_MODE_DAY_NIGHT = 'day_night'
READING_MODE_CHOICES = [
(READING_MODE_SINGLE, _('Звичайний (одне показання)')),
(READING_MODE_DAY_NIGHT, _('День / ніч')),
]
pharmacy = models.ForeignKey(
'locations.Pharmacy', on_delete=models.CASCADE,
related_name='utility_meter_configs', verbose_name=_('Аптека'),
)
utility_type = models.ForeignKey(
'core.Directory', on_delete=models.PROTECT,
limit_choices_to={'directory_type__code': 'utility_type', 'is_active': True},
related_name='utility_meter_configs', verbose_name=_('Тип лічильника'),
)
reading_mode = models.CharField(
_('Режим показань'), max_length=20,
choices=READING_MODE_CHOICES, default=READING_MODE_SINGLE,
)
is_active = models.BooleanField(_('Активний'), default=True)
class Meta:
verbose_name = _('Налаштування лічильника')
verbose_name_plural = _('Налаштування лічильників')
ordering = ['pharmacy', 'utility_type__name']
constraints = [
models.UniqueConstraint(
fields=['pharmacy', 'utility_type'],
name='unique_utility_meter_config_per_pharmacy',
),
]
def __str__(self):
return f'{self.pharmacy} — {self.utility_type.name} ({self.get_reading_mode_display()})'
class UtilityReading(BaseModel):
"""Показання лічильника (світло, вода...), подані аптекою для комунального відділу."""
pharmacy = models.ForeignKey(
'locations.Pharmacy', on_delete=models.CASCADE,
related_name='utility_readings', verbose_name=_('Аптека'),
)
utility_type = models.ForeignKey(
'core.Directory', on_delete=models.PROTECT,
limit_choices_to={'directory_type__code': 'utility_type', 'is_active': True},
related_name='utility_readings', verbose_name=_('Тип лічильника'),
)
reading_date = models.DateField(_('Дата зняття показань'), default=timezone.localdate)
value = models.DecimalField(
_('Показання'), max_digits=12, decimal_places=2,
null=True, blank=True,
)
value_day = models.DecimalField(
_('Показання (день)'), max_digits=12, decimal_places=2,
null=True, blank=True,
)
value_night = models.DecimalField(
_('Показання (ніч)'), max_digits=12, decimal_places=2,
null=True, blank=True,
)
comment = models.CharField(_('Коментар'), max_length=255, blank=True)
submitted_by = models.ForeignKey(
settings.AUTH_USER_MODEL, on_delete=models.SET_NULL,
null=True, blank=True,
related_name='utility_readings_submitted', verbose_name=_('Хто подав'),
)
class Meta:
verbose_name = _('Показання лічильника')
verbose_name_plural = _('Показання лічильників')
ordering = ['-reading_date', '-created_at']
constraints = [
models.UniqueConstraint(
fields=['pharmacy', 'utility_type', 'reading_date'],
name='unique_reading_per_pharmacy_type_date',
),
]
indexes = [
models.Index(fields=['pharmacy', 'utility_type', '-reading_date']),
]
def __str__(self):
if self.is_day_night:
return f'{self.pharmacy} — {self.utility_type.name}: день {self.value_day} / ніч {self.value_night} ({self.reading_date})'
return f'{self.pharmacy} — {self.utility_type.name}: {self.value} ({self.reading_date})'
@property
def is_day_night(self) -> bool:
return self.value_day is not None or self.value_night is not None
@property
def value_display(self) -> str:
if self.is_day_night:
day = self.value_day if self.value_day is not None else '—'
night = self.value_night if self.value_night is not None else '—'
return f'день {day} / ніч {night}'
return str(self.value if self.value is not None else '—')
def clean(self):
super().clean()
from apps.utilities.helpers import get_reading_mode
mode = get_reading_mode(self.pharmacy_id, self.utility_type_id)
if mode == UtilityMeterConfig.READING_MODE_DAY_NIGHT:
if self.value is not None:
raise ValidationError({'value': _('Для лічильника «день/ніч» використовуйте окремі поля.')})
if self.value_day is None or self.value_night is None:
raise ValidationError({
'__all__': [_('Вкажіть показання для денної та нічної зон.')],
})
for field in ('value_day', 'value_night'):
if getattr(self, field) is not None and getattr(self, field) < 0:
raise ValidationError({field: _("Показання не може бути від'ємним")})
else:
if self.value is None:
raise ValidationError({'value': _('Вкажіть показання лічильника.')})
if self.value < 0:
raise ValidationError({'value': _("Показання не може бути від'ємним")})
if self.value_day is not None or self.value_night is not None:
raise ValidationError({
'__all__': [_('Для звичайного лічильника використовуйте одне поле показань.')],
})
if self.reading_date and self.reading_date > timezone.localdate():
raise ValidationError({'reading_date': _('Дата не може бути в майбутньому')})
def _previous_reading(self):
qs = UtilityReading.objects.filter(
pharmacy=self.pharmacy, utility_type=self.utility_type,
reading_date__lt=self.reading_date,
)
if self.pk:
qs = qs.exclude(pk=self.pk)
return qs.order_by('-reading_date').first()
@property
def previous_reading(self):
return self._previous_reading()
def _delta(self, field: str):
previous = self._previous_reading()
if previous is None:
return None
current = getattr(self, field)
prev_val = getattr(previous, field)
if current is None or prev_val is None:
return None
return current - prev_val
@property
def consumption(self):
"""Сумарна витрата (звичайний лічильник або день+ніч)."""
if self.is_day_night:
total = self.consumption_total
return total if total is not None else None
return self._delta('value')
@property
def consumption_day(self):
return self._delta('value_day') if self.is_day_night else None
@property
def consumption_night(self):
return self._delta('value_night') if self.is_day_night else None
@property
def consumption_total(self):
if not self.is_day_night:
return self.consumption
day = self.consumption_day
night = self.consumption_night
if day is None and night is None:
return None
return (day or 0) + (night or 0)