- Отделена модель Delivery от Order (OneToOne связь) - Добавлены обязательные поля delivery_date, time_from, time_to в Delivery - Delivery обязательна при создании заказа (кроме черновиков) - Добавлены методы calculate_total() и reset_delivery_cost() в Order - Добавлена валидация полей доставки в OrderForm - Исправлено создание доменов - убран порт из домена в БД - Исправлен редирект после установки пароля (правильный формат URL) - Исправлена ошибка NoReverseMatch в navbar для public схемы - Удалены все старые миграции (база создается с нуля) - Обновлены views для работы с новой моделью Delivery
260 lines
9.3 KiB
Python
260 lines
9.3 KiB
Python
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'])
|