""" Сервис для работы с черновиками заказов. Содержит бизнес-логику создания, обновления и завершения черновиков. """ 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