from django.conf import settings from django.db import models from django.utils.text import slugify class CategoryType(models.TextChoices): PRODUCT = 'product', 'Товар' SERVICE = 'service', 'Услуга' class ModerationStatus(models.TextChoices): DRAFT = 'draft', 'Черновик' PENDING = 'pending', 'На модерации' PUBLISHED = 'published', 'Опубликовано' REJECTED = 'rejected', 'Отклонено' class VerificationStatus(models.TextChoices): PENDING = 'pending', 'На проверке' APPROVED = 'approved', 'Одобрен' REVISION = 'revision', 'На доработке' REJECTED = 'rejected', 'Отклонён' class LegalStatus(models.TextChoices): SELF_EMPLOYED = 'self_employed', 'Самозанятый' IP = 'ip', 'ИП' LEGAL_ENTITY = 'legal', 'Юрлицо' class Category(models.Model): """Двухуровневый справочник категорий (ТЗ §3.2).""" parent = models.ForeignKey( 'self', null=True, blank=True, on_delete=models.CASCADE, related_name='children', verbose_name='Родитель', ) name = models.CharField('Название', max_length=200) slug = models.SlugField('Slug', max_length=220, unique=True, blank=True) type = models.CharField( 'Тип', max_length=10, choices=CategoryType.choices, default=CategoryType.PRODUCT, ) sort_order = models.PositiveIntegerField('Порядок сортировки', default=0) class Meta: verbose_name = 'Категория' verbose_name_plural = 'Категории' ordering = ('sort_order', 'name') def __str__(self): return f'{self.parent.name} / {self.name}' if self.parent else self.name def save(self, *args, **kwargs): if not self.slug: self.slug = slugify(self.name, allow_unicode=True) super().save(*args, **kwargs) class Brand(models.Model): """Профиль продавца — «Бренд» (ТЗ §4: Brand / SellerProfile).""" user = models.OneToOneField( settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='brand', verbose_name='Владелец', ) name = models.CharField('Название бренда', max_length=200) slug = models.SlugField('Slug', max_length=220, unique=True, blank=True) logo = models.ImageField('Логотип', upload_to='brands/logos/', blank=True, null=True) cover = models.ImageField('Обложка', upload_to='brands/covers/', blank=True, null=True) story = models.TextField('История бренда', max_length=3000, blank=True) legal_status = models.CharField( 'Правовой статус', max_length=20, choices=LegalStatus.choices, default=LegalStatus.SELF_EMPLOYED, ) inn = models.CharField('ИНН', max_length=12, blank=True) region = models.CharField('Регион', max_length=120, blank=True) city = models.CharField('Город', max_length=120, blank=True) socials = models.JSONField('Соцсети', default=dict, blank=True) verification_status = models.CharField( 'Статус верификации', max_length=20, choices=VerificationStatus.choices, default=VerificationStatus.PENDING, ) production_photo = models.ImageField( 'Фото производства', upload_to='brands/verification/', blank=True, null=True, ) statement_doc = models.FileField( 'Выписка/справка', upload_to='brands/verification/', blank=True, null=True, ) badges = models.JSONField('Знаки', default=list, blank=True) rating = models.DecimalField('Рейтинг', max_digits=3, decimal_places=2, default=0) created_at = models.DateTimeField('Дата создания', auto_now_add=True) class Meta: verbose_name = 'Бренд' verbose_name_plural = 'Бренды' ordering = ('name',) def __str__(self): return self.name def save(self, *args, **kwargs): if not self.slug: self.slug = slugify(self.name, allow_unicode=True) super().save(*args, **kwargs) class Product(models.Model): """Карточка товара (ТЗ §4: Product).""" brand = models.ForeignKey( Brand, on_delete=models.CASCADE, related_name='products', verbose_name='Бренд', ) category = models.ForeignKey( Category, on_delete=models.PROTECT, related_name='products', verbose_name='Категория', ) name = models.CharField('Название', max_length=255) slug = models.SlugField('Slug', max_length=280, unique=True, blank=True) description = models.TextField('Описание', blank=True) price = models.DecimalField('Цена', max_digits=12, decimal_places=2) attributes = models.JSONField('Характеристики', default=dict, blank=True) stock = models.PositiveIntegerField('Остаток', default=0) lead_time_days = models.PositiveIntegerField('Срок изготовления, дней', default=0) moderation_status = models.CharField( 'Статус модерации', max_length=20, choices=ModerationStatus.choices, default=ModerationStatus.DRAFT, ) is_active = models.BooleanField('Активен', default=True) created_at = models.DateTimeField('Дата создания', auto_now_add=True) class Meta: verbose_name = 'Товар' verbose_name_plural = 'Товары' ordering = ('-created_at',) def __str__(self): return self.name def save(self, *args, **kwargs): if not self.slug: self.slug = slugify(self.name, allow_unicode=True) super().save(*args, **kwargs) @property def is_available(self): return self.is_active and self.stock > 0 class ProductImage(models.Model): """Изображение товара (галерея до 10 фото, ТЗ §3.2).""" product = models.ForeignKey( Product, on_delete=models.CASCADE, related_name='images', verbose_name='Товар', ) image = models.ImageField('Изображение', upload_to='products/') sort_order = models.PositiveIntegerField('Порядок', default=0) class Meta: verbose_name = 'Изображение товара' verbose_name_plural = 'Изображения товаров' ordering = ('sort_order', 'id') def __str__(self): return f'Фото #{self.pk} для {self.product}' class ServiceFormat(models.TextChoices): ONLINE = 'online', 'Онлайн' OFFLINE = 'offline', 'Офлайн' class Service(models.Model): """Карточка услуги (ТЗ §4: Service).""" brand = models.ForeignKey( Brand, on_delete=models.CASCADE, related_name='services', verbose_name='Бренд', ) category = models.ForeignKey( Category, on_delete=models.PROTECT, related_name='services', verbose_name='Категория', ) name = models.CharField('Название', max_length=255) slug = models.SlugField('Slug', max_length=280, unique=True, blank=True) description = models.TextField('Описание', blank=True) format = models.CharField( 'Формат', max_length=10, choices=ServiceFormat.choices, default=ServiceFormat.OFFLINE, ) duration_minutes = models.PositiveIntegerField('Длительность, мин', default=60) price = models.DecimalField('Цена', max_digits=12, decimal_places=2) moderation_status = models.CharField( 'Статус модерации', max_length=20, choices=ModerationStatus.choices, default=ModerationStatus.DRAFT, ) is_active = models.BooleanField('Активна', default=True) created_at = models.DateTimeField('Дата создания', auto_now_add=True) class Meta: verbose_name = 'Услуга' verbose_name_plural = 'Услуги' ordering = ('-created_at',) def __str__(self): return self.name def save(self, *args, **kwargs): if not self.slug: self.slug = slugify(self.name, allow_unicode=True) super().save(*args, **kwargs) class TimeSlot(models.Model): """Слот записи на услугу (ТЗ §3.4).""" service = models.ForeignKey( Service, on_delete=models.CASCADE, related_name='slots', verbose_name='Услуга', ) start = models.DateTimeField('Начало') end = models.DateTimeField('Окончание') is_booked = models.BooleanField('Занят', default=False) class Meta: verbose_name = 'Слот записи' verbose_name_plural = 'Слоты записи' ordering = ('start',) def __str__(self): return f'{self.service} — {self.start:%d.%m.%Y %H:%M}'