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

392 lines
15 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 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 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 clean(self):
"""Валидация модели"""
super().clean()
# Проверка: для самовывоза обязателен склад
if not self.is_delivery and not self.pickup_warehouse:
raise ValidationError({
'pickup_warehouse': 'Для самовывоза необходимо выбрать склад'
})
# Проверка: время окончания должно быть позже или равно времени начала
# Равные времена означают точное время доставки (например, "к 13:00")
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:
# Если времена равны - это точное время доставки
if self.delivery_time_start == self.delivery_time_end:
return f"к {self.delivery_time_start.strftime('%H:%M')}"
return f"{self.delivery_time_start.strftime('%H:%M')} - {self.delivery_time_end.strftime('%H:%M')}"
return "Время не указано"