from django.db import models from django.core.exceptions import ValidationError from accounts.models import CustomUser from customers.models import Customer, Address from products.models import Product, ProductKit from shops.models import Shop import uuid class Order(models.Model): """ Заказ клиента для доставки цветов. """ # Основная информация customer = models.ForeignKey( Customer, on_delete=models.PROTECT, related_name='orders', verbose_name="Клиент" ) order_number = models.CharField( max_length=50, unique=True, editable=False, verbose_name="Номер заказа", help_text="Уникальный номер заказа для отображения клиенту" ) # Тип доставки DELIVERY_TYPE_CHOICES = [ ('courier', 'Курьерская доставка'), ('pickup', 'Самовывоз'), ] delivery_type = models.CharField( max_length=20, choices=DELIVERY_TYPE_CHOICES, default='courier', verbose_name="Тип доставки" ) # Адрес доставки (для курьерской доставки) delivery_address = models.ForeignKey( Address, on_delete=models.PROTECT, null=True, blank=True, related_name='orders', verbose_name="Адрес доставки", help_text="Обязательно для курьерской доставки" ) # Пункт самовывоза (для самовывоза) pickup_shop = models.ForeignKey( Shop, on_delete=models.PROTECT, null=True, blank=True, related_name='pickup_orders', verbose_name="Точка самовывоза", help_text="Обязательно для самовывоза" ) # Дата и время доставки/самовывоза delivery_date = models.DateField( verbose_name="Дата доставки/самовывоза" ) delivery_time_start = models.TimeField( verbose_name="Время от", help_text="Начало временного интервала" ) delivery_time_end = models.TimeField( verbose_name="Время до", help_text="Конец временного интервала" ) delivery_cost = models.DecimalField( max_digits=10, decimal_places=2, default=0, verbose_name="Стоимость доставки", help_text="0 для самовывоза" ) # Статус заказа STATUS_CHOICES = [ ('new', 'Новый'), ('confirmed', 'Подтвержден'), ('in_assembly', 'В сборке'), ('in_delivery', 'В доставке'), ('delivered', 'Доставлен'), ('cancelled', 'Отменен'), ] status = models.CharField( max_length=20, choices=STATUS_CHOICES, default='new', verbose_name="Статус заказа" ) # Оплата PAYMENT_METHOD_CHOICES = [ ('cash_to_courier', 'Наличные курьеру'), ('card_to_courier', 'Карта курьеру'), ('online', 'Онлайн оплата'), ('bank_transfer', 'Банковский перевод'), ] payment_method = models.CharField( max_length=20, choices=PAYMENT_METHOD_CHOICES, default='cash_to_courier', verbose_name="Способ оплаты" ) is_paid = models.BooleanField( default=False, verbose_name="Оплачен" ) total_amount = models.DecimalField( max_digits=10, decimal_places=2, default=0, verbose_name="Итоговая сумма заказа", help_text="Общая сумма заказа включая доставку" ) # Дополнительная информация is_anonymous = models.BooleanField( default=False, verbose_name="Анонимная доставка", help_text="Не сообщать получателю имя отправителя" ) special_instructions = models.TextField( blank=True, null=True, verbose_name="Особые пожелания", help_text="Комментарии и пожелания к заказу" ) # Временные метки 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=['customer']), models.Index(fields=['status']), models.Index(fields=['delivery_date']), models.Index(fields=['delivery_type']), models.Index(fields=['created_at']), models.Index(fields=['order_number']), ] ordering = ['-created_at'] def __str__(self): return f"Заказ #{self.order_number} - {self.customer}" def save(self, *args, **kwargs): # Генерируем уникальный номер заказа при создании if not self.order_number: self.order_number = self.generate_order_number() super().save(*args, **kwargs) def generate_order_number(self): """Генерирует уникальный номер заказа""" # Формат: ORD-YYYYMMDD-XXXX (например: ORD-20250126-A3F2) from datetime import datetime date_str = datetime.now().strftime('%Y%m%d') unique_id = uuid.uuid4().hex[:4].upper() return f"ORD-{date_str}-{unique_id}" def clean(self): """Валидация модели""" super().clean() # Проверка: для курьерской доставки обязателен адрес if self.delivery_type == 'courier' and not self.delivery_address: raise ValidationError({ 'delivery_address': 'Для курьерской доставки необходимо указать адрес доставки' }) # Проверка: для самовывоза обязателен пункт самовывоза if self.delivery_type == 'pickup' and not self.pickup_shop: raise ValidationError({ 'pickup_shop': 'Для самовывоза необходимо выбрать точку самовывоза' }) # Проверка: время окончания должно быть позже времени начала if self.delivery_time_start and self.delivery_time_end: if self.delivery_time_end <= self.delivery_time_start: raise ValidationError({ 'delivery_time_end': 'Время окончания должно быть позже времени начала' }) def calculate_total(self): """Рассчитывает итоговую сумму заказа""" items_total = sum(item.get_total_price() for item in self.items.all()) self.total_amount = items_total + self.delivery_cost return self.total_amount @property def delivery_info(self): """Информация о доставке для отображения""" if self.delivery_type == 'courier': return f"Доставка по адресу: {self.delivery_address.full_address}" elif self.delivery_type == 'pickup': return f"Самовывоз из: {self.pickup_shop.name}" return "Не указано" @property def delivery_time_window(self): """Временное окно доставки""" return f"{self.delivery_time_start.strftime('%H:%M')} - {self.delivery_time_end.strftime('%H:%M')}" class OrderItem(models.Model): """ Позиция (товар) в заказе. Хранит информацию о товаре или комплекте, количестве и цене на момент заказа. """ order = models.ForeignKey( Order, on_delete=models.CASCADE, related_name='items', verbose_name="Заказ" ) # Товар или комплект (один из двух должен быть заполнен) product = models.ForeignKey( Product, on_delete=models.PROTECT, null=True, blank=True, related_name='order_items', verbose_name="Товар" ) product_kit = models.ForeignKey( ProductKit, on_delete=models.PROTECT, null=True, blank=True, related_name='order_items', verbose_name="Комплект товаров" ) quantity = models.PositiveIntegerField( default=1, verbose_name="Количество" ) price = models.DecimalField( max_digits=10, decimal_places=2, verbose_name="Цена за единицу", help_text="Цена на момент создания заказа (фиксируется)" ) # Временные метки created_at = models.DateTimeField( auto_now_add=True, verbose_name="Дата добавления" ) class Meta: verbose_name = "Позиция заказа" verbose_name_plural = "Позиции заказа" indexes = [ models.Index(fields=['order']), models.Index(fields=['product']), models.Index(fields=['product_kit']), ] def __str__(self): item_name = "" if self.product: item_name = self.product.name elif self.product_kit: item_name = self.product_kit.name return f"{item_name} x{self.quantity} в заказе #{self.order.order_number}" def clean(self): """Валидация модели""" super().clean() # Проверка: должен быть заполнен либо product, либо product_kit if not self.product and not self.product_kit: raise ValidationError( 'Необходимо указать либо товар, либо комплект товаров' ) # Проверка: не должны быть заполнены оба поля одновременно if self.product and self.product_kit: raise ValidationError( 'Нельзя указать одновременно и товар, и комплект' ) def save(self, *args, **kwargs): # Автоматически фиксируем цену при создании, если она не указана if not self.price: if self.product: self.price = self.product.price elif self.product_kit: self.price = self.product_kit.price super().save(*args, **kwargs) def get_total_price(self): """Возвращает общую стоимость позиции""" return self.price * self.quantity @property def item_name(self): """Название товара/комплекта""" if self.product: return self.product.name elif self.product_kit: return self.product_kit.name return "Не указано"