Добавлена конвертация строковых значений в Decimal для полей delivery_cost и discount_amount, чтобы избежать ошибки типа при автосохранении. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
320 lines
12 KiB
Python
320 lines
12 KiB
Python
"""
|
||
Сервис для работы с черновиками заказов.
|
||
Содержит бизнес-логику создания, обновления и завершения черновиков.
|
||
"""
|
||
from django.db import transaction
|
||
from django.utils import timezone
|
||
from django.core.exceptions import ValidationError
|
||
from decimal import Decimal
|
||
import decimal
|
||
|
||
from ..models import Order, OrderItem
|
||
from products.models import Product, ProductKit
|
||
|
||
|
||
class DraftOrderService:
|
||
"""
|
||
Сервис для управления черновиками заказов.
|
||
Обеспечивает создание, обновление и финализацию черновиков.
|
||
"""
|
||
|
||
@staticmethod
|
||
def create_draft(user, customer, data=None):
|
||
"""
|
||
Создает новый черновик заказа.
|
||
|
||
Args:
|
||
user: Пользователь, создающий заказ
|
||
customer: Клиент, для которого создается заказ
|
||
data (dict, optional): Дополнительные данные для заказа
|
||
|
||
Returns:
|
||
Order: Созданный черновик заказа
|
||
|
||
Raises:
|
||
ValidationError: Если данные невалидны
|
||
"""
|
||
data = data or {}
|
||
|
||
with transaction.atomic():
|
||
order = Order.objects.create(
|
||
customer=customer,
|
||
status='draft',
|
||
modified_by=user,
|
||
is_delivery=data.get('is_delivery', True),
|
||
delivery_address=data.get('delivery_address'),
|
||
pickup_shop=data.get('pickup_shop'),
|
||
delivery_date=data.get('delivery_date'),
|
||
delivery_time_start=data.get('delivery_time_start'),
|
||
delivery_time_end=data.get('delivery_time_end'),
|
||
delivery_cost=data.get('delivery_cost', Decimal('0')),
|
||
payment_method=data.get('payment_method', 'cash_to_courier'),
|
||
customer_is_recipient=data.get('customer_is_recipient', True),
|
||
recipient_name=data.get('recipient_name'),
|
||
recipient_phone=data.get('recipient_phone'),
|
||
is_anonymous=data.get('is_anonymous', False),
|
||
special_instructions=data.get('special_instructions'),
|
||
last_autosave_at=timezone.now(),
|
||
)
|
||
|
||
return order
|
||
|
||
@staticmethod
|
||
def update_draft(order_id, user, data):
|
||
"""
|
||
Обновляет существующий черновик заказа.
|
||
|
||
Args:
|
||
order_id (int): ID заказа
|
||
user: Пользователь, изменяющий заказ
|
||
data (dict): Данные для обновления
|
||
|
||
Returns:
|
||
Order: Обновленный заказ
|
||
|
||
Raises:
|
||
Order.DoesNotExist: Если заказ не найден
|
||
ValidationError: Если заказ не является черновиком или данные невалидны
|
||
"""
|
||
with transaction.atomic():
|
||
order = Order.objects.select_for_update().get(pk=order_id)
|
||
|
||
if not order.is_draft():
|
||
raise ValidationError("Можно обновлять только черновики заказов")
|
||
|
||
# Обновляем только переданные поля
|
||
# ForeignKey поля требуют специальной обработки
|
||
fk_fields = {
|
||
'customer': 'customers.Customer',
|
||
'delivery_address': 'customers.Address',
|
||
'pickup_shop': 'shops.Shop',
|
||
}
|
||
|
||
simple_fields = [
|
||
'is_delivery', 'delivery_date', 'delivery_time_start', 'delivery_time_end',
|
||
'delivery_cost', 'payment_method', 'customer_is_recipient',
|
||
'recipient_name', 'recipient_phone', 'is_anonymous',
|
||
'special_instructions', 'discount_amount'
|
||
]
|
||
|
||
# Обрабатываем ForeignKey поля
|
||
for field_name, model_path in fk_fields.items():
|
||
if field_name in data and data[field_name]:
|
||
# Получаем модель
|
||
app_label, model_name = model_path.split('.')
|
||
from django.apps import apps
|
||
Model = apps.get_model(app_label, model_name)
|
||
|
||
# Получаем объект по ID
|
||
try:
|
||
instance = Model.objects.get(pk=data[field_name])
|
||
setattr(order, field_name, instance)
|
||
except Model.DoesNotExist:
|
||
pass # Игнорируем несуществующие объекты
|
||
|
||
# Обрабатываем простые поля
|
||
for field in simple_fields:
|
||
if field in data:
|
||
value = data[field]
|
||
|
||
# Конвертируем числовые поля в Decimal
|
||
if field in ['delivery_cost', 'discount_amount']:
|
||
if value == '' or value is None:
|
||
value = None
|
||
else:
|
||
try:
|
||
value = Decimal(str(value))
|
||
except (ValueError, TypeError, decimal.InvalidOperation):
|
||
value = Decimal('0')
|
||
|
||
setattr(order, field, value)
|
||
|
||
order.modified_by = user
|
||
order.last_autosave_at = timezone.now()
|
||
order.save()
|
||
|
||
# Пересчитываем итоговую сумму если изменились товары
|
||
if 'recalculate' in data and data['recalculate']:
|
||
order.calculate_total()
|
||
order.save()
|
||
|
||
return order
|
||
|
||
@staticmethod
|
||
def add_item_to_draft(order_id, product_id=None, product_kit_id=None, quantity=1, price=None):
|
||
"""
|
||
Добавляет товар или комплект в черновик заказа.
|
||
|
||
Args:
|
||
order_id (int): ID заказа
|
||
product_id (int, optional): ID товара
|
||
product_kit_id (int, optional): ID комплекта
|
||
quantity (Decimal): Количество
|
||
price (Decimal, optional): Цена (если None, берется из товара/комплекта)
|
||
|
||
Returns:
|
||
OrderItem: Созданная позиция заказа
|
||
|
||
Raises:
|
||
ValidationError: Если заказ не является черновиком или данные невалидны
|
||
"""
|
||
with transaction.atomic():
|
||
order = Order.objects.get(pk=order_id)
|
||
|
||
if not order.is_draft():
|
||
raise ValidationError("Можно добавлять товары только в черновики заказов")
|
||
|
||
# Определяем товар или комплект
|
||
product = None
|
||
product_kit = None
|
||
|
||
if product_id:
|
||
product = Product.objects.get(pk=product_id)
|
||
if price is None:
|
||
price = product.actual_price
|
||
elif product_kit_id:
|
||
product_kit = ProductKit.objects.get(pk=product_kit_id)
|
||
if price is None:
|
||
price = product_kit.actual_price
|
||
else:
|
||
raise ValidationError("Необходимо указать product_id или product_kit_id")
|
||
|
||
order_item = OrderItem.objects.create(
|
||
order=order,
|
||
product=product,
|
||
product_kit=product_kit,
|
||
quantity=quantity,
|
||
price=price
|
||
)
|
||
|
||
# Обновляем итоговую сумму заказа
|
||
order.calculate_total()
|
||
order.last_autosave_at = timezone.now()
|
||
order.save()
|
||
|
||
return order_item
|
||
|
||
@staticmethod
|
||
def remove_item_from_draft(order_id, order_item_id):
|
||
"""
|
||
Удаляет позицию из черновика заказа.
|
||
|
||
Args:
|
||
order_id (int): ID заказа
|
||
order_item_id (int): ID позиции заказа
|
||
|
||
Raises:
|
||
ValidationError: Если заказ не является черновиком
|
||
"""
|
||
with transaction.atomic():
|
||
order = Order.objects.get(pk=order_id)
|
||
|
||
if not order.is_draft():
|
||
raise ValidationError("Можно удалять товары только из черновиков заказов")
|
||
|
||
OrderItem.objects.filter(pk=order_item_id, order=order).delete()
|
||
|
||
# Обновляем итоговую сумму заказа
|
||
order.calculate_total()
|
||
order.last_autosave_at = timezone.now()
|
||
order.save()
|
||
|
||
@staticmethod
|
||
def finalize_draft(order_id, user):
|
||
"""
|
||
Завершает черновик заказа, переводя его в статус 'new'.
|
||
Выполняет финальную валидацию всех данных.
|
||
|
||
Args:
|
||
order_id (int): ID заказа
|
||
user: Пользователь, завершающий заказ
|
||
|
||
Returns:
|
||
Order: Финализированный заказ
|
||
|
||
Raises:
|
||
ValidationError: Если данные заказа невалидны или заказ не является черновиком
|
||
"""
|
||
with transaction.atomic():
|
||
order = Order.objects.select_for_update().get(pk=order_id)
|
||
|
||
if not order.is_draft():
|
||
raise ValidationError("Можно финализировать только черновики заказов")
|
||
|
||
# Проверяем наличие товаров
|
||
if not order.items.exists():
|
||
raise ValidationError("Заказ должен содержать хотя бы один товар")
|
||
|
||
# Выполняем полную валидацию модели
|
||
order.full_clean()
|
||
|
||
# Изменяем статус на 'new'
|
||
order.status = 'new'
|
||
order.modified_by = user
|
||
order.last_autosave_at = None # Очищаем, т.к. заказ больше не черновик
|
||
order.save()
|
||
|
||
# Привязываем временные комплекты к заказу
|
||
ProductKit.objects.filter(
|
||
is_temporary=True,
|
||
order=order
|
||
).update(order=order)
|
||
|
||
return order
|
||
|
||
@staticmethod
|
||
def get_user_drafts(user, customer=None):
|
||
"""
|
||
Возвращает черновики заказов пользователя.
|
||
|
||
Args:
|
||
user: Пользователь
|
||
customer (Customer, optional): Фильтр по клиенту
|
||
|
||
Returns:
|
||
QuerySet: Черновики заказов
|
||
"""
|
||
drafts = Order.objects.filter(
|
||
status='draft',
|
||
modified_by=user
|
||
).select_related('customer', 'delivery_address', 'pickup_shop')
|
||
|
||
if customer:
|
||
drafts = drafts.filter(customer=customer)
|
||
|
||
return drafts.order_by('-last_autosave_at')
|
||
|
||
@staticmethod
|
||
def delete_old_drafts(days=30):
|
||
"""
|
||
Удаляет старые черновики заказов.
|
||
|
||
Args:
|
||
days (int): Количество дней, после которых черновик считается старым
|
||
|
||
Returns:
|
||
int: Количество удаленных черновиков
|
||
"""
|
||
from datetime import timedelta
|
||
|
||
cutoff_date = timezone.now() - timedelta(days=days)
|
||
|
||
# Находим старые черновики
|
||
old_drafts = Order.objects.filter(
|
||
status='draft',
|
||
last_autosave_at__lt=cutoff_date
|
||
)
|
||
|
||
# Удаляем связанные временные комплекты
|
||
for draft in old_drafts:
|
||
ProductKit.objects.filter(
|
||
is_temporary=True,
|
||
order=draft
|
||
).delete()
|
||
|
||
# Удаляем черновики
|
||
count = old_drafts.count()
|
||
old_drafts.delete()
|
||
|
||
return count
|