""" Сервис управления витринами - резервирование, продажа и разбор витринных букетов. """ from decimal import Decimal from django.db import transaction from django.utils import timezone from django.core.exceptions import ValidationError from inventory.models import Showcase, Reservation, Warehouse from products.models import ProductKit from orders.models import Order, OrderItem, OrderStatus from customers.models import Customer class ShowcaseManager: """ Менеджер для работы с витринами и витринными букетами. """ @staticmethod def reserve_kit_to_showcase(product_kit, showcase, quantity=1): """ Резервирует комплект на витрину. Раскладывает комплект на компоненты и создаёт резервы по каждому товару. Args: product_kit: ProductKit - комплект для резервирования showcase: Showcase - витрина quantity: int - количество комплектов (по умолчанию 1) Returns: dict: { 'success': bool, 'reservations': list[Reservation], 'message': str } """ if not showcase.is_active: return { 'success': False, 'reservations': [], 'message': f'Витрина "{showcase.name}" не активна' } warehouse = showcase.warehouse reservations = [] try: with transaction.atomic(): # Раскладываем комплект на компоненты kit_items = product_kit.kit_items.all() if not kit_items.exists(): return { 'success': False, 'reservations': [], 'message': f'Комплект "{product_kit.name}" не содержит компонентов' } # Создаём резервы по каждому компоненту for kit_item in kit_items: if kit_item.product: # Обычный товар component_quantity = kit_item.quantity * quantity reservation = Reservation.objects.create( product=kit_item.product, warehouse=warehouse, showcase=showcase, product_kit=product_kit, quantity=component_quantity, status='reserved' ) reservations.append(reservation) elif kit_item.variant_group: # Группа вариантов - резервируем первый доступный вариант # В будущем можно добавить выбор конкретного варианта variant_items = kit_item.variant_group.items.all() if variant_items.exists(): first_variant = variant_items.first() component_quantity = kit_item.quantity * quantity reservation = Reservation.objects.create( product=first_variant.product, warehouse=warehouse, showcase=showcase, product_kit=product_kit, quantity=component_quantity, status='reserved' ) reservations.append(reservation) # Обновляем агрегаты Stock для всех затронутых товаров from inventory.models import Stock for reservation in reservations: stock, _ = Stock.objects.get_or_create( product=reservation.product, warehouse=warehouse ) stock.refresh_from_batches() return { 'success': True, 'reservations': reservations, 'message': f'Комплект "{product_kit.name}" зарезервирован на витрине "{showcase.name}"' } except Exception as e: return { 'success': False, 'reservations': [], 'message': f'Ошибка резервирования: {str(e)}' } @staticmethod def sell_from_showcase(product_kit, showcase, customer, payment_method='cash_to_courier', custom_price=None, user=None): """ Продаёт комплект с витрины. Создаёт Order, OrderItem, конвертирует резервы в Sale. Args: product_kit: ProductKit - комплект для продажи showcase: Showcase - витрина customer: Customer - покупатель payment_method: str - способ оплаты custom_price: Decimal - кастомная цена (опционально) user: CustomUser - пользователь, выполняющий операцию Returns: dict: { 'success': bool, 'order': Order or None, 'message': str } """ warehouse = showcase.warehouse try: with transaction.atomic(): # Находим резервы для этого комплекта на витрине # Группируем по product для подсчёта reservations = Reservation.objects.filter( showcase=showcase, status='reserved' ).select_related('product') if not reservations.exists(): return { 'success': False, 'order': None, 'message': f'На витрине "{showcase.name}" нет зарезервированных товаров' } # Получаем статус "Завершён" для POS-продаж completed_status = OrderStatus.objects.filter( code='completed', is_positive_end=True ).first() if not completed_status: # Если нет статуса completed, берём любой положительный completed_status = OrderStatus.objects.filter( is_positive_end=True ).first() # Создаём заказ (самовывоз с витринного склада) order = Order.objects.create( customer=customer, is_delivery=False, pickup_warehouse=warehouse, status=completed_status, payment_method=payment_method, is_paid=True, modified_by=user ) # Определяем цену price = custom_price if custom_price else product_kit.actual_price is_custom = custom_price is not None # Создаём позицию заказа order_item = OrderItem.objects.create( order=order, product_kit=product_kit, quantity=1, price=price, is_custom_price=is_custom, is_from_showcase=True, showcase=showcase ) # Привязываем резервы к OrderItem reservations.update(order_item=order_item) # Конвертируем резервы в продажи from inventory.services.sale_processor import SaleProcessor for reservation in reservations: # Создаём Sale sale = SaleProcessor.create_sale_from_reservation( reservation=reservation, order=order ) # Обновляем статус резерва reservation.status = 'converted_to_sale' reservation.converted_at = timezone.now() reservation.save() # Пересчитываем итоговую сумму заказа order.calculate_total() order.amount_paid = order.total_amount order.update_payment_status() order.save() return { 'success': True, 'order': order, 'message': f'Заказ #{order.order_number} создан. Продан комплект с витрины "{showcase.name}"' } except Exception as e: return { 'success': False, 'order': None, 'message': f'Ошибка продажи: {str(e)}' } @staticmethod def dismantle_from_showcase(showcase, product_kit=None): """ Разбирает букет на витрине - освобождает резервы. Args: showcase: Showcase - витрина product_kit: ProductKit - конкретный комплект (опционально) Returns: dict: { 'success': bool, 'released_count': int, 'message': str } """ try: with transaction.atomic(): # Находим активные резервы reservations = Reservation.objects.filter( showcase=showcase, status='reserved' ) if product_kit: # Если указан конкретный комплект, фильтруем только его резервы reservations = reservations.filter(product_kit=product_kit) released_count = reservations.count() if released_count == 0: return { 'success': False, 'released_count': 0, 'message': f'На витрине "{showcase.name}" нет активных резервов' } # Сохраняем список затронутых товаров и склад ДО обновления резервов from inventory.models import Stock affected_products = list(reservations.values_list('product_id', flat=True).distinct()) warehouse = showcase.warehouse # Освобождаем резервы reservations.update( status='released', released_at=timezone.now(), showcase=None ) # Обновляем агрегаты Stock for product_id in affected_products: try: stock = Stock.objects.get( product_id=product_id, warehouse=warehouse ) stock.refresh_from_batches() except Stock.DoesNotExist: pass return { 'success': True, 'released_count': released_count, 'message': f'Разобрано {released_count} резервов с витрины "{showcase.name}"' } except Exception as e: return { 'success': False, 'released_count': 0, 'message': f'Ошибка разбора: {str(e)}' } @staticmethod def get_showcase_kits(showcase): """ Возвращает список комплектов, зарезервированных на витрине. Args: showcase: Showcase Returns: list: список словарей с информацией о комплектах """ reservations = Reservation.objects.filter( showcase=showcase, status='reserved' ).select_related('product').values('product__id', 'product__name', 'quantity') # Группируем по товарам products_dict = {} for res in reservations: product_id = res['product__id'] if product_id not in products_dict: products_dict[product_id] = { 'product_name': res['product__name'], 'quantity': Decimal('0') } products_dict[product_id]['quantity'] += res['quantity'] return [ { 'product_id': pid, 'product_name': data['product_name'], 'quantity': data['quantity'] } for pid, data in products_dict.items() ]