Рефакторинг моделей заказов и добавление методов оплаты
This commit is contained in:
388
myproject/orders/models/order.py
Normal file
388
myproject/orders/models/order.py
Normal file
@@ -0,0 +1,388 @@
|
||||
from django.db import models
|
||||
from django.core.exceptions import ValidationError
|
||||
from accounts.models import CustomUser
|
||||
from customers.models import Customer
|
||||
from inventory.models import Warehouse
|
||||
from simple_history.models import HistoricalRecords
|
||||
from .status import OrderStatus
|
||||
from .address import Address
|
||||
|
||||
|
||||
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="Уникальный номер заказа"
|
||||
)
|
||||
|
||||
# Тип доставки
|
||||
is_delivery = models.BooleanField(
|
||||
default=True,
|
||||
verbose_name="С доставкой",
|
||||
help_text="True - доставка курьером, False - самовывоз"
|
||||
)
|
||||
|
||||
# Адрес доставки (для курьерской доставки)
|
||||
delivery_address = models.OneToOneField(
|
||||
Address,
|
||||
on_delete=models.CASCADE,
|
||||
null=True,
|
||||
blank=True,
|
||||
related_name='order',
|
||||
verbose_name="Адрес доставки",
|
||||
help_text="Обязательно для курьерской доставки"
|
||||
)
|
||||
|
||||
# Склад для самовывоза
|
||||
pickup_warehouse = models.ForeignKey(
|
||||
Warehouse,
|
||||
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 для самовывоза"
|
||||
)
|
||||
|
||||
is_custom_delivery_cost = models.BooleanField(
|
||||
default=False,
|
||||
verbose_name="Стоимость доставки установлена вручную",
|
||||
help_text="True если стоимость доставки была изменена вручную"
|
||||
)
|
||||
|
||||
# Статус заказа
|
||||
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="Общая сумма заказа включая доставку"
|
||||
)
|
||||
|
||||
# Скидки
|
||||
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']),
|
||||
models.Index(fields=['is_custom_delivery_cost']),
|
||||
]
|
||||
ordering = ['-created_at']
|
||||
|
||||
def __str__(self):
|
||||
return f"Заказ #{self.order_number} - {self.customer}"
|
||||
|
||||
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 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_warehouse:
|
||||
raise ValidationError({
|
||||
'pickup_warehouse': 'Для самовывоза необходимо выбрать склад'
|
||||
})
|
||||
|
||||
# Проверка: время окончания должно быть позже времени начала
|
||||
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 get_delivery_cost(self):
|
||||
"""
|
||||
Возвращает стоимость доставки:
|
||||
- Если установлена вручную - использует ручное значение
|
||||
- Если автоматическая - вычисляет на основе правил
|
||||
|
||||
Returns:
|
||||
Decimal: Стоимость доставки
|
||||
"""
|
||||
if self.is_custom_delivery_cost:
|
||||
return self.delivery_cost
|
||||
else:
|
||||
from orders.services.delivery_cost_calculator import DeliveryCostCalculator
|
||||
return DeliveryCostCalculator.calculate(self)
|
||||
|
||||
def set_delivery_cost(self, cost, is_custom=True):
|
||||
"""
|
||||
Устанавливает стоимость доставки.
|
||||
|
||||
Args:
|
||||
cost: Новая стоимость доставки (Decimal)
|
||||
is_custom: True если устанавливается вручную, False если автоматически
|
||||
"""
|
||||
self.delivery_cost = cost
|
||||
self.is_custom_delivery_cost = is_custom
|
||||
|
||||
def reset_delivery_cost(self):
|
||||
"""
|
||||
Сбрасывает стоимость доставки на автоматический расчет.
|
||||
"""
|
||||
from orders.services.delivery_cost_calculator import DeliveryCostCalculator
|
||||
self.delivery_cost = DeliveryCostCalculator.calculate(self)
|
||||
self.is_custom_delivery_cost = False
|
||||
|
||||
def recalculate_delivery_cost(self):
|
||||
"""
|
||||
Пересчитывает стоимость доставки, если она не установлена вручную.
|
||||
Используется при изменении параметров заказа (товаров, адреса и т.д.)
|
||||
"""
|
||||
if not self.is_custom_delivery_cost:
|
||||
from orders.services.delivery_cost_calculator import DeliveryCostCalculator
|
||||
self.delivery_cost = DeliveryCostCalculator.calculate(self)
|
||||
|
||||
def calculate_total(self):
|
||||
"""Рассчитывает итоговую сумму заказа"""
|
||||
items_total = sum(item.get_total_price() for item in self.items.all())
|
||||
|
||||
# Пересчитываем стоимость доставки если она автоматическая
|
||||
self.recalculate_delivery_cost()
|
||||
|
||||
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()
|
||||
|
||||
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 delivery_cost_display(self):
|
||||
"""
|
||||
Возвращает строку для отображения стоимости доставки с пометкой.
|
||||
Полезно в админке и шаблонах.
|
||||
"""
|
||||
cost = self.get_delivery_cost()
|
||||
suffix = " (ручная)" if self.is_custom_delivery_cost else " (авто)"
|
||||
return f"{cost} руб.{suffix}"
|
||||
|
||||
@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_warehouse:
|
||||
return f"Самовывоз со склада: {self.pickup_warehouse.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 "Время не указано"
|
||||
Reference in New Issue
Block a user