from django.db import models from accounts.models import CustomUser from customers.models import Customer from simple_history.models import HistoricalRecords from .status import OrderStatus from .recipient import Recipient class Order(models.Model): """ Заказ клиента для доставки цветов. ВАЖНО: Поле payment_method УДАЛЕНО для поддержки смешанной оплаты. Используйте модель Payment (один Order → много Payment) для платежей. """ # Основная информация customer = models.ForeignKey( Customer, on_delete=models.PROTECT, related_name='orders', verbose_name="Клиент" ) order_number = models.PositiveIntegerField( unique=True, editable=False, verbose_name="Номер заказа", help_text="Уникальный номер заказа" ) # Статус заказа status = models.ForeignKey( 'OrderStatus', on_delete=models.PROTECT, related_name='orders', null=True, blank=True, verbose_name="Статус заказа" ) # Флаг для отслеживания возвратов is_returned = models.BooleanField( default=False, verbose_name="Возвращен", help_text="True если заказ был выполнен, но потом отменен или возвращен клиентом" ) # Автосохранение (для черновиков) last_autosave_at = models.DateTimeField( null=True, blank=True, verbose_name="Последнее автосохранение", help_text="Время последнего автоматического сохранения черновика" ) # Оплата # УДАЛЕНО: PAYMENT_METHOD_CHOICES и payment_method поле # Вместо этого используйте модель Payment для смешанной оплаты is_paid = models.BooleanField( default=False, verbose_name="Оплачен" ) total_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 = models.ForeignKey( Recipient, on_delete=models.SET_NULL, null=True, blank=True, related_name='orders', 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=['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 get_absolute_url(self): """Возвращает канонический URL для заказа""" from django.urls import reverse return reverse('orders:order-detail', kwargs={'order_number': self.order_number}) def save(self, *args, **kwargs): # Генерируем уникальный номер заказа при создании (начиная с 100 для 3-значного поиска) if not self.order_number: last_order = Order.objects.order_by('-order_number').first() if last_order: # Если ранее нумерация была ниже 100, начинаем с 100; иначе инкремент self.order_number = max(last_order.order_number + 1, 100) else: self.order_number = 100 super().save(*args, **kwargs) def recalculate_amount_paid(self): """ Пересчитывает оплаченную сумму на основе транзакций. amount_paid = сумма_платежей - сумма_возвратов """ from django.db.models import Sum, Q from decimal import Decimal payments_sum = self.transactions.filter( transaction_type='payment' ).aggregate(total=Sum('amount'))['total'] or Decimal('0') refunds_sum = self.transactions.filter( transaction_type='refund' ).aggregate(total=Sum('amount'))['total'] or Decimal('0') self.amount_paid = payments_sum - refunds_sum self.update_payment_status() 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(update_fields=['payment_status', 'is_paid', 'amount_paid']) def is_draft(self): """Проверяет, является ли заказ черновиком""" return self.status and self.status.code == 'draft' @property def amount_due(self): """Остаток к оплате""" return max(self.total_amount - self.amount_paid, 0) @property def overpayment(self): """Переплата (если amount_paid > total_amount)""" return max(self.amount_paid - self.total_amount, 0) @property def subtotal(self): """Сумма только товаров (без доставки)""" return sum(item.get_total_price() for item in self.items.all()) def calculate_total(self): """ Пересчитывает итоговую сумму заказа. total_amount = subtotal + delivery_cost """ from decimal import Decimal subtotal = self.subtotal delivery_cost = Decimal('0') # Получаем стоимость доставки из связанной модели Delivery if hasattr(self, 'delivery'): delivery_cost = self.delivery.cost self.total_amount = subtotal + delivery_cost self.save(update_fields=['total_amount']) def reset_delivery_cost(self): """ Сбрасывает стоимость доставки. Если есть Delivery, устанавливает cost = 0. """ if hasattr(self, 'delivery'): self.delivery.cost = 0 self.delivery.save(update_fields=['cost']) # === Свойства обратной совместимости для доступа к полям доставки === # Эти свойства обеспечивают доступ к полям Delivery через Order для обратной совместимости # после рефакторинга, когда поля доставки были перенесены в отдельную модель Delivery @property def delivery_date(self): """Дата доставки (обратная совместимость)""" if hasattr(self, 'delivery') and self.delivery: return self.delivery.delivery_date return None @property def delivery_time_start(self): """Время начала доставки (обратная совместимость)""" if hasattr(self, 'delivery') and self.delivery: return self.delivery.time_from return None @property def delivery_time_end(self): """Время окончания доставки (обратная совместимость)""" if hasattr(self, 'delivery') and self.delivery: return self.delivery.time_to return None @property def delivery_time_window(self): """Форматированное окно времени доставки (обратная совместимость)""" if hasattr(self, 'delivery') and self.delivery: if self.delivery.time_from and self.delivery.time_to: return f"{self.delivery.time_from.strftime('%H:%M')} - {self.delivery.time_to.strftime('%H:%M')}" return None @property def delivery_time(self): """Время доставки (обратная совместимость, использует delivery_time_window)""" return self.delivery_time_window @property def is_delivery(self): """Является ли заказ доставкой (обратная совместимость)""" if hasattr(self, 'delivery') and self.delivery: from .delivery import Delivery return self.delivery.delivery_type == Delivery.DELIVERY_TYPE_COURIER return False @property def delivery_address(self): """Адрес доставки (обратная совместимость)""" if hasattr(self, 'delivery') and self.delivery: return self.delivery.address return None @property def delivery_cost(self): """Стоимость доставки (обратная совместимость)""" if hasattr(self, 'delivery') and self.delivery: return self.delivery.cost return 0 @property def pickup_warehouse(self): """Склад самовывоза (обратная совместимость)""" if hasattr(self, 'delivery') and self.delivery: return self.delivery.pickup_warehouse return None