from django.db import models from accounts.models import CustomUser from products.models import Product, ProductKit class Customer(models.Model): """ Модель покупателя. """ user = models.OneToOneField(CustomUser, on_delete=models.CASCADE, null=True, blank=True, related_name='customer', verbose_name="Пользователь") first_name = models.CharField(max_length=100, verbose_name="Имя") last_name = models.CharField(max_length=100, verbose_name="Фамилия") email = models.EmailField(unique=True, verbose_name="Email") phone = models.CharField(max_length=20, blank=True, null=True, verbose_name="Телефон") created_at = models.DateTimeField(auto_now_add=True, verbose_name="Дата регистрации") updated_at = models.DateTimeField(auto_now=True, verbose_name="Дата обновления") class Meta: verbose_name = "Покупатель" verbose_name_plural = "Покупатели" indexes = [ models.Index(fields=['email']), ] def __str__(self): return f"{self.first_name} {self.last_name} ({self.email})" class Order(models.Model): """ Заказ клиента. """ STATUS_CHOICES = [ ('created', 'Создан'), ('confirmed', 'Подтвержден'), ('assembled', 'Собран'), ('delivered', 'Доставлен'), ('cancelled', 'Отменен'), ] customer = models.ForeignKey(Customer, on_delete=models.CASCADE, related_name='orders', verbose_name="Клиент") status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='created', verbose_name="Статус") total_price = models.DecimalField(max_digits=10, decimal_places=2, verbose_name="Общая сумма") created_at = models.DateTimeField(auto_now_add=True, verbose_name="Дата создания") updated_at = models.DateTimeField(auto_now=True, verbose_name="Дата обновления") class Meta: verbose_name = "Заказ" verbose_name_plural = "Заказы" indexes = [ models.Index(fields=['status']), models.Index(fields=['customer']), models.Index(fields=['created_at']), ] def __str__(self): return f"Заказ #{self.id} - {self.customer}" class OrderItem(models.Model): """ Строка заказа — может быть простым товаром или комплектом. """ order = models.ForeignKey(Order, on_delete=models.CASCADE, related_name='items', verbose_name="Заказ") product = models.ForeignKey(Product, on_delete=models.CASCADE, null=True, blank=True, related_name='order_items', verbose_name="Товар") kit = models.ForeignKey(ProductKit, on_delete=models.CASCADE, null=True, blank=True, related_name='order_items', verbose_name="Комплект") quantity = models.DecimalField(max_digits=10, decimal_places=3, default=1, verbose_name="Количество") # Снапшот-поля (для истории и отчётов) snapshot_name = models.CharField(max_length=200, verbose_name="Название (на момент заказа)") snapshot_sku = models.CharField(max_length=100, blank=True, null=True, verbose_name="Артикул (на момент заказа)") sale_price = models.DecimalField(max_digits=10, decimal_places=2, verbose_name="Цена продажи") cost_price = models.DecimalField(max_digits=10, decimal_places=2, verbose_name="Себестоимость") composition_snapshot = models.JSONField(null=True, blank=True, verbose_name="Состав комплекта (снапшот)") class Meta: verbose_name = "Позиция заказа" verbose_name_plural = "Позиции заказов" indexes = [ models.Index(fields=['order']), ] def save(self, *args, **kwargs): # Валидация: либо product, либо kit, но не оба if self.product and self.kit: raise ValueError("Нельзя одновременно указать товар и комплект") if not self.product and not self.kit: raise ValueError("Необходимо указать либо товар, либо комплект") # Заполнение снапшот-полей if self.product: if not self.snapshot_name: self.snapshot_name = self.product.name if not self.snapshot_sku: self.snapshot_sku = self.product.sku if not self.sale_price: self.sale_price = self.product.sale_price if not self.cost_price: self.cost_price = self.product.cost_price elif self.kit: if not self.snapshot_name: self.snapshot_name = self.kit.name if not self.sale_price or not self.cost_price: # Здесь можно реализовать логику подсчета цены комплекта # в зависимости от метода ценообразования if self.kit.pricing_method == 'fixed' and self.kit.fixed_price: self.sale_price = self.kit.fixed_price # В реальном приложении нужно реализовать все методы ценообразования if self.kit.pricing_method != 'fixed' and not self.composition_snapshot: # Формирование снапшота состава комплекта composition = [] for item in self.kit.kit_items.all(): composition.append({ "product_id": item.product.id, "name": item.product.name, "sku": item.product.sku, "quantity": float(item.quantity), "cost_price": float(item.product.cost_price), "sale_price": float(item.product.sale_price) }) self.composition_snapshot = composition super().save(*args, **kwargs) def __str__(self): return f"{self.snapshot_name} x{self.quantity} в заказе #{self.order.id}"