Files
octopus/myproject/orders/models/order.py

338 lines
13 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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="Обновляется автоматически при добавлении платежей"
)
# Информация о получателе
# Получатель (если None - получатель = покупатель, иначе - отдельный получатель)
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="Комментарии и пожелания к заказу"
)
# Фото
needs_product_photo = models.BooleanField(
default=False,
verbose_name="Необходимо фото товара",
help_text="Требуется фотография товара перед отправкой"
)
needs_delivery_photo = models.BooleanField(
default=False,
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
@property
def is_customer_recipient(self):
"""Является ли покупатель получателем (обратная совместимость)"""
return self.recipient is None