""" Модели единиц измерения. - UnitOfMeasure: справочник единиц измерения - ProductSalesUnit: единицы продажи товара с коэффициентами конверсии """ from decimal import Decimal from django.db import models from django.core.validators import MinValueValidator from django.core.exceptions import ValidationError class UnitOfMeasure(models.Model): """ Справочник единиц измерения. Расширяемый справочник вместо жестко заданных UNIT_CHOICES. """ code = models.CharField( max_length=20, unique=True, verbose_name="Код", help_text="Короткий код: шт, кг, банч, ветка" ) name = models.CharField( max_length=100, verbose_name="Название", help_text="Полное название: Штука, Килограмм, Банч" ) short_name = models.CharField( max_length=10, verbose_name="Сокращение", help_text="Для UI: шт., кг., бч." ) is_active = models.BooleanField( default=True, verbose_name="Активна" ) position = models.PositiveIntegerField( default=0, verbose_name="Порядок сортировки" ) class Meta: verbose_name = "Единица измерения" verbose_name_plural = "Единицы измерения" ordering = ['position', 'name'] def __str__(self): return f"{self.name} ({self.code})" class ProductSalesUnit(models.Model): """ Единица продажи товара с коэффициентом конверсии. Один товар может иметь несколько единиц продажи. Пример: Товар: Пихта Нобилис (базовая единица: банч) Единицы продажи: - Ветка большая: 1 банч = 15 веток, цена 300₽ - Ветка средняя: 1 банч = 30 веток, цена 150₽ - Ветка маленькая: 1 банч = 50 веток, цена 100₽ """ product = models.ForeignKey( 'Product', on_delete=models.CASCADE, related_name='sales_units', verbose_name="Товар" ) name = models.CharField( max_length=100, verbose_name="Название", help_text="Например: 'Ветка большая', 'Ветка средняя'" ) conversion_factor = models.DecimalField( max_digits=15, decimal_places=6, validators=[MinValueValidator(Decimal('0.000001'))], verbose_name="Коэффициент конверсии", help_text="Сколько единиц продажи в 1 базовой единице товара. " "Например: 15 (из 1 банча получается 15 больших веток)" ) price = models.DecimalField( max_digits=10, decimal_places=2, validators=[MinValueValidator(Decimal('0'))], verbose_name="Цена продажи" ) sale_price = models.DecimalField( max_digits=10, decimal_places=2, null=True, blank=True, validators=[MinValueValidator(Decimal('0'))], verbose_name="Цена со скидкой" ) min_quantity = models.DecimalField( max_digits=10, decimal_places=3, default=Decimal('1'), validators=[MinValueValidator(Decimal('0.001'))], verbose_name="Мин. количество", help_text="Минимальное количество для продажи" ) quantity_step = models.DecimalField( max_digits=10, decimal_places=3, default=Decimal('1'), validators=[MinValueValidator(Decimal('0.001'))], verbose_name="Шаг количества", help_text="С каким шагом можно заказывать (0.1, 0.5, 1)" ) is_default = models.BooleanField( default=False, verbose_name="Единица по умолчанию", help_text="Единица, выбираемая по умолчанию при добавлении в заказ" ) is_active = models.BooleanField( default=True, verbose_name="Активна" ) position = models.PositiveIntegerField( default=0, verbose_name="Порядок сортировки" ) class Meta: verbose_name = "Единица продажи товара" verbose_name_plural = "Единицы продажи товаров" ordering = ['position', 'id'] unique_together = [['product', 'name']] def __str__(self): return f"{self.product.name} - {self.name}" def clean(self): super().clean() if self.conversion_factor and self.conversion_factor <= 0: raise ValidationError({ 'conversion_factor': 'Коэффициент конверсии должен быть больше 0' }) if self.sale_price and self.price and self.sale_price >= self.price: raise ValidationError({ 'sale_price': 'Цена со скидкой должна быть меньше основной цены' }) def save(self, *args, **kwargs): # Если это единица по умолчанию, снимаем флаг с других if self.is_default: ProductSalesUnit.objects.filter( product=self.product, is_default=True ).exclude(pk=self.pk).update(is_default=False) super().save(*args, **kwargs) @property def actual_price(self): """Финальная цена (со скидкой или без)""" return self.sale_price if self.sale_price else self.price def convert_to_base(self, quantity): """ Конвертировать количество в базовые единицы товара. Args: quantity: количество в единицах продажи Returns: Decimal: количество в базовых единицах Пример: 10 больших веток → 10 / 15 = 0.667 банча """ if not self.conversion_factor or self.conversion_factor == 0: return Decimal(quantity) return Decimal(quantity) / self.conversion_factor def convert_from_base(self, base_quantity): """ Конвертировать из базовых единиц в единицы продажи. Args: base_quantity: количество в базовых единицах товара Returns: Decimal: количество в единицах продажи Пример: 2.5 банча → 2.5 * 15 = 37.5 больших веток """ if not self.conversion_factor: return Decimal(base_quantity) return Decimal(base_quantity) * self.conversion_factor def validate_quantity(self, quantity): """ Проверить, что количество соответствует ограничениям. Args: quantity: количество для проверки Raises: ValidationError: если количество некорректно """ quantity = Decimal(quantity) if quantity < self.min_quantity: raise ValidationError( f'Минимальное количество для "{self.name}": {self.min_quantity}' ) # Проверяем шаг количества (с учётом погрешности float) if self.quantity_step and self.quantity_step > 0: remainder = quantity % self.quantity_step if remainder > Decimal('0.0001') and (self.quantity_step - remainder) > Decimal('0.0001'): raise ValidationError( f'Количество должно быть кратно {self.quantity_step}' )