""" Сервис для работы с черновиками заказов. Содержит бизнес-логику создания, обновления и завершения черновиков. """ from django.db import transaction from django.utils import timezone from django.core.exceptions import ValidationError from decimal import Decimal import decimal from datetime import datetime, date, time from ..models import Order, OrderItem, Address from products.models import Product, ProductKit from .address_service import AddressService 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) # Обновляем только переданные поля # ForeignKey поля требуют специальной обработки fk_fields = { 'customer': 'customers.Customer', '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 # Игнорируем несуществующие объекты # === Обработка адреса доставки === # Новая логика с выбором режима адреса if 'address_mode' in data: address = AddressService.process_address_from_form(order, data) if address: # Если адрес не существует в БД, сохраняем его if not address.pk: address.save() order.delivery_address = address else: # Если режим "без адреса", удаляем существующий адрес if order.delivery_address: old_address = order.delivery_address order.delivery_address = None # Удаляем старый адрес если он больше не используется if old_address and not old_address.order: old_address.delete() elif 'delivery_address' in data and data['delivery_address']: # Старая логика для совместимости (если передается delivery_address напрямую) try: address = Address.objects.get(pk=data['delivery_address']) order.delivery_address = address except Address.DoesNotExist: pass # Обрабатываем простые поля for field in simple_fields: if field in data: value = data[field] # Конвертируем boolean поля if field in ['is_delivery', 'customer_is_recipient', 'is_anonymous']: # Явно конвертируем в bool, обрабатывая различные типы данных original_value = value if isinstance(value, bool): value = value elif isinstance(value, str): value = value.lower() in ('true', '1', 'yes', 'on') elif value is None: value = False else: value = bool(value) # Логируем для отладки if field == 'is_delivery': import logging logger = logging.getLogger(__name__) logger.info(f"[AUTOSAVE] is_delivery: original={original_value} (type={type(original_value)}), converted={value}") # Конвертируем числовые поля в Decimal elif 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') # Конвертируем дату elif field == 'delivery_date': if value == '' or value is None: value = None elif isinstance(value, str): try: value = datetime.strptime(value, '%Y-%m-%d').date() except ValueError: value = None # Конвертируем время elif field in ['delivery_time_start', 'delivery_time_end']: if value == '' or value is None: value = None elif isinstance(value, str): try: # Формат времени может быть HH:MM или HH:MM:SS if len(value.split(':')) == 2: value = datetime.strptime(value, '%H:%M').time() else: value = datetime.strptime(value, '%H:%M:%S').time() except ValueError: value = None 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