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 from simple_history.models import HistoricalRecords 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="Уникальный номер заказа для отображения клиенту" ) # Тип доставки is_delivery = models.BooleanField( default=True, verbose_name="С доставкой", help_text="True - доставка курьером, False - самовывоз" ) # Адрес доставки (для курьерской доставки) 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( null=True, blank=True, verbose_name="Дата доставки/самовывоза", help_text="Может быть заполнено позже" ) delivery_time_start = models.TimeField( null=True, blank=True, verbose_name="Время от", help_text="Начало временного интервала" ) delivery_time_end = models.TimeField( null=True, blank=True, 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="Общая сумма заказа включая доставку" ) # Скидки discount_amount = models.DecimalField( max_digits=10, decimal_places=2, default=0, verbose_name="Сумма скидки", help_text="Применяется вручную или через систему скидок" ) # Частичная оплата amount_paid = models.DecimalField( max_digits=10, decimal_places=2, default=0, verbose_name="Оплачено", help_text="Сумма, внесенная клиентом" ) PAYMENT_STATUS_CHOICES = [ ('unpaid', 'Не оплачен'), ('partial', 'Частично оплачен'), ('paid', 'Оплачен полностью'), ] payment_status = models.CharField( max_length=20, choices=PAYMENT_STATUS_CHOICES, default='unpaid', verbose_name="Статус оплаты", help_text="Обновляется автоматически при добавлении платежей" ) # Дополнительная информация customer_is_recipient = models.BooleanField( default=True, verbose_name="Покупатель является получателем", help_text="Если отмечено, данные получателя не требуются отдельно" ) # Данные получателя (если покупатель != получатель) recipient_name = models.CharField( max_length=200, blank=True, null=True, verbose_name="Имя получателя", help_text="Заполняется, если покупатель не является получателем" ) recipient_phone = models.CharField( max_length=20, blank=True, null=True, 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="Дата обновления" ) modified_by = models.ForeignKey( CustomUser, on_delete=models.SET_NULL, null=True, blank=True, related_name='modified_orders', verbose_name="Изменен пользователем", help_text="Последний пользователь, изменивший заказ" ) # История изменений history = HistoricalRecords() class Meta: verbose_name = "Заказ" verbose_name_plural = "Заказы" indexes = [ models.Index(fields=['customer']), models.Index(fields=['status']), models.Index(fields=['delivery_date']), models.Index(fields=['is_delivery']), models.Index(fields=['payment_status']), 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.is_delivery and not self.delivery_address: raise ValidationError({ 'delivery_address': 'Для доставки необходимо указать адрес доставки' }) # Проверка: для самовывоза обязателен пункт самовывоза if not self.is_delivery 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()) subtotal = items_total + self.delivery_cost self.total_amount = subtotal - self.discount_amount return self.total_amount def update_payment_status(self): """Автоматически обновляет статус оплаты на основе amount_paid""" if self.amount_paid >= self.total_amount: self.payment_status = 'paid' self.is_paid = True elif self.amount_paid > 0: self.payment_status = 'partial' self.is_paid = False else: self.payment_status = 'unpaid' self.is_paid = False self.save() @property def amount_due(self): """Остаток к оплате""" return max(self.total_amount - self.amount_paid, 0) @property def delivery_info(self): """Информация о доставке для отображения""" if self.is_delivery: if self.delivery_address: return f"Доставка по адресу: {self.delivery_address.full_address}" return "Доставка (адрес не указан)" else: if self.pickup_shop: return f"Самовывоз из: {self.pickup_shop.name}" return "Самовывоз (точка не указана)" @property def delivery_time_window(self): """Временное окно доставки""" if self.delivery_time_start and self.delivery_time_end: return f"{self.delivery_time_start.strftime('%H:%M')} - {self.delivery_time_end.strftime('%H:%M')}" return "Время не указано" 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="Цена на момент создания заказа (фиксируется)" ) is_custom_price = models.BooleanField( default=False, verbose_name="Цена изменена вручную", help_text="True если цена была изменена вручную при создании заказа" ) # Временные метки 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 "Не указано" @property def original_price(self): """Оригинальная цена товара/комплекта из каталога""" if self.product: return self.product.actual_price elif self.product_kit: return self.product_kit.actual_price return None @property def price_difference(self): """Разница между установленной ценой и оригинальной""" if self.is_custom_price and self.original_price: return self.price - self.original_price return None class Payment(models.Model): """ Платеж по заказу. Хранит историю всех платежей, включая частичные оплаты. """ order = models.ForeignKey( Order, on_delete=models.CASCADE, related_name='payments', verbose_name="Заказ" ) amount = models.DecimalField( max_digits=10, decimal_places=2, verbose_name="Сумма платежа" ) payment_method = models.CharField( max_length=20, choices=Order.PAYMENT_METHOD_CHOICES, verbose_name="Способ оплаты" ) payment_date = models.DateTimeField( auto_now_add=True, verbose_name="Дата и время платежа" ) created_by = models.ForeignKey( CustomUser, on_delete=models.SET_NULL, null=True, blank=True, related_name='payments_created', verbose_name="Принял платеж" ) notes = models.TextField( blank=True, null=True, verbose_name="Примечания", help_text="Дополнительная информация о платеже" ) class Meta: verbose_name = "Платеж" verbose_name_plural = "Платежи" ordering = ['-payment_date'] indexes = [ models.Index(fields=['order']), models.Index(fields=['payment_date']), ] def __str__(self): return f"Платеж {self.amount} руб. по заказу #{self.order.order_number}" def save(self, *args, **kwargs): """При сохранении платежа обновляем сумму оплаты в заказе""" super().save(*args, **kwargs) # Пересчитываем общую сумму оплаты в заказе self.order.amount_paid = sum(p.amount for p in self.order.payments.all()) self.order.update_payment_status()