""" Базовые модели для products приложения. Содержит SKUCounter и BaseProductEntity (абстрактный базовый класс). """ from django.db import models, transaction from django.utils import timezone from django.contrib.auth import get_user_model from .managers import ActiveManager, SoftDeleteManager, SoftDeleteQuerySet # Получаем User модель один раз для использования в ForeignKey User = get_user_model() class SKUCounter(models.Model): """ Глобальные счетчики для генерации уникальных номеров артикулов. Используется для товаров (product), комплектов (kit) и категорий (category). """ COUNTER_TYPE_CHOICES = [ ('product', 'Product Counter'), ('kit', 'Kit Counter'), ('category', 'Category Counter'), ] counter_type = models.CharField( max_length=20, unique=True, choices=COUNTER_TYPE_CHOICES, verbose_name="Тип счетчика" ) current_value = models.IntegerField( default=0, verbose_name="Текущее значение" ) class Meta: verbose_name = "Счетчик артикулов" verbose_name_plural = "Счетчики артикулов" def __str__(self): return f"{self.get_counter_type_display()}: {self.current_value}" @classmethod def get_next_value(cls, counter_type): """ Получить следующее значение счетчика (thread-safe). Использует select_for_update для предотвращения race conditions. """ with transaction.atomic(): counter, created = cls.objects.select_for_update().get_or_create( counter_type=counter_type, defaults={'current_value': 0} ) counter.current_value += 1 counter.save() return counter.current_value class BaseProductEntity(models.Model): """ Абстрактный базовый класс для Product и ProductKit. Объединяет общие поля идентификации, описания, статуса и soft delete. Используется как основа для: - Product (простой товар) - ProductKit (комплект товаров) """ # Идентификация name = models.CharField( max_length=200, verbose_name="Название" ) sku = models.CharField( max_length=100, blank=True, null=True, verbose_name="Артикул", db_index=True ) slug = models.SlugField( max_length=200, unique=True, blank=True, verbose_name="URL-идентификатор" ) # Описания description = models.TextField( blank=True, null=True, verbose_name="Описание" ) short_description = models.TextField( blank=True, null=True, verbose_name="Краткое описание", help_text="Используется для карточек товаров, превью и площадок" ) # Статус is_active = models.BooleanField( default=True, verbose_name="Активен" ) # Временные метки created_at = models.DateTimeField( auto_now_add=True, verbose_name="Дата создания" ) updated_at = models.DateTimeField( auto_now=True, verbose_name="Дата обновления" ) # Soft delete is_deleted = models.BooleanField( default=False, verbose_name="Удален", db_index=True ) deleted_at = models.DateTimeField( null=True, blank=True, verbose_name="Время удаления" ) deleted_by = models.ForeignKey( User, on_delete=models.SET_NULL, null=True, blank=True, related_name='deleted_%(class)s_set', verbose_name="Удален пользователем" ) # Managers objects = SoftDeleteManager.from_queryset(SoftDeleteQuerySet)() all_objects = models.Manager() active = ActiveManager() class Meta: abstract = True indexes = [ models.Index(fields=['is_active']), models.Index(fields=['is_deleted']), models.Index(fields=['is_deleted', 'created_at']), ] def __str__(self): return self.name def delete(self, *args, **kwargs): """Мягкое удаление (soft delete)""" user = kwargs.pop('user', None) self.is_deleted = True self.deleted_at = timezone.now() if user: self.deleted_by = user self.save(update_fields=['is_deleted', 'deleted_at', 'deleted_by']) return 1, {self.__class__._meta.label: 1} def hard_delete(self): """Физическое удаление из БД (необратимо!)""" super().delete() def save(self, *args, **kwargs): """Автогенерация slug из name если не задан""" if not self.slug or self.slug.strip() == '': # Используем централизованный сервис для генерации slug from ..services.slug_service import SlugService self.slug = SlugService.generate_unique_slug( self.name, self.__class__, self.pk ) super().save(*args, **kwargs)