diff --git a/myproject/inventory/signals.py b/myproject/inventory/signals.py index 890e406..4fcb005 100644 --- a/myproject/inventory/signals.py +++ b/myproject/inventory/signals.py @@ -547,7 +547,8 @@ def create_sale_on_order_completion(sender, instance, created, **kwargs): ) except Exception as e: logger.error( - f"❌ Ошибка финализации ShowcaseItem #{showcase_item.id}: {e}" + f"❌ Ошибка финализации ShowcaseItem #{showcase_item.id}: {e}", + exc_info=True ) if finalized_count > 0: diff --git a/myproject/orders/views.py b/myproject/orders/views.py index 5ef9bb6..503d055 100644 --- a/myproject/orders/views.py +++ b/myproject/orders/views.py @@ -12,6 +12,7 @@ from .models import Order, OrderItem, Address, OrderStatus, Transaction, Payment from .forms import OrderForm, OrderItemFormSet, OrderItemForm, OrderStatusForm, TransactionForm from .filters import OrderFilter from .services.address_service import AddressService +from inventory.models import Reservation import json @@ -1067,6 +1068,7 @@ def create_order_from_pos(request): showcase_item_ids = item_data.get('showcase_item_ids', []) if not showcase_item_ids: + logger.warning(f"⚠️ Пустой список showcase_item_ids для комплекта {kit.name}") continue # Создаём OrderItem с флагом is_from_showcase @@ -1086,8 +1088,29 @@ def create_order_from_pos(request): locked_by_user=request.user ) + reserved_count = 0 + component_count = 0 + for showcase_item in showcase_items: showcase_item.reserve_for_order(order_item) + reserved_count += 1 + + # КРИТИЧНО: Привязываем существующие резервы компонентов к OrderItem + # Эти резервы были созданы при добавлении букета на витрину + component_reservations = Reservation.objects.filter( + showcase_item=showcase_item, + status='reserved' + ) + + for reservation in component_reservations: + reservation.order_item = order_item + reservation.save(update_fields=['order_item']) + component_count += 1 + + logger.info( + f"✓ Витринный комплект '{kit.name}': зарезервировано {reserved_count} экз., " + f"привязано {component_count} резервов компонентов к OrderItem #{order_item.id}" + ) # 5. Пересчитываем стоимость заказа order.calculate_total() diff --git a/myproject/pos/static/pos/js/terminal.js b/myproject/pos/static/pos/js/terminal.js index 0d2fc59..43fd0c6 100644 --- a/myproject/pos/static/pos/js/terminal.js +++ b/myproject/pos/static/pos/js/terminal.js @@ -902,10 +902,22 @@ function renderProducts() { stock.style.color = '#28a745'; // Зелёный (достаточно) } } else { - // Fallback для старых данных или комплектов - stock.textContent = item.in_stock ? 'В наличии' : 'Под заказ'; - if (!item.in_stock) { - stock.style.color = '#dc3545'; + // Комплекты: показываем доступное количество + if (item.type === 'kit' && item.free_qty !== undefined) { + const availableKits = parseFloat(item.free_qty) || 0; + if (availableKits > 0) { + stock.textContent = `В наличии: ${Math.floor(availableKits)} компл.`; + stock.style.color = '#28a745'; // Зелёный + } else { + stock.textContent = 'Под заказ'; + stock.style.color = '#dc3545'; // Красный + } + } else { + // Fallback для старых данных + stock.textContent = item.in_stock ? 'В наличии' : 'Под заказ'; + if (!item.in_stock) { + stock.style.color = '#dc3545'; + } } } diff --git a/myproject/pos/views.py b/myproject/pos/views.py index 431c885..183c433 100644 --- a/myproject/pos/views.py +++ b/myproject/pos/views.py @@ -854,17 +854,20 @@ def get_items_api(request): if not image_url: image_url = None + # Рассчитываем доступное количество комплектов на текущем складе + available_kits = k.calculate_available_quantity(warehouse=current_warehouse) + kits.append({ 'id': k.id, 'name': k.name, 'price': str(k.actual_price), 'category_ids': [c.id for c in k.categories.all()], - 'in_stock': False, # Комплекты всегда "Под заказ" + 'in_stock': available_kits > 0, # Доступен если можно собрать хоть один комплект 'sku': k.sku or '', 'image': image_url, 'type': 'kit', - 'free_qty': '0', # Строка для консистентности с товарами - 'free_qty_sort': 0 # Комплекты всегда внизу при сортировке + 'free_qty': str(available_kits), # Количество комплектов которые можно собрать + 'free_qty_sort': float(available_kits) # Для сортировки }) # Объединяем и сортируем по free_qty_sort DESC diff --git a/myproject/products/models/kits.py b/myproject/products/models/kits.py index d4bbb5a..7ee940d 100644 --- a/myproject/products/models/kits.py +++ b/myproject/products/models/kits.py @@ -13,7 +13,6 @@ from .categories import ProductCategory, ProductTag from .variants import ProductVariantGroup from .products import Product from ..utils.sku_generator import generate_kit_sku -from ..services.kit_availability import KitAvailabilityChecker class ProductKit(BaseProductEntity): @@ -225,10 +224,69 @@ class ProductKit(BaseProductEntity): def check_availability(self, stock_manager=None): """ - Проверяет доступность всего комплекта. - Делегирует проверку в сервис. + Проверяет доступность всего комплекта (возвращает True/False). + Для обратной совместимости. Использует calculate_available_quantity(). """ - return KitAvailabilityChecker.check_availability(self, stock_manager) + return self.calculate_available_quantity() > 0 + + def calculate_available_quantity(self, warehouse=None): + """ + Рассчитывает максимальное количество комплектов, которое можно собрать + на основе свободных остатков компонентов на складе. + + Args: + warehouse: Склад для проверки остатков. Если None, суммируются остатки по всем складам. + + Returns: + Decimal: Максимальное количество комплектов (0 если хоть один компонент недоступен) + """ + from inventory.models import Stock + + if not self.kit_items.exists(): + return Decimal('0') + + min_available = None + + for kit_item in self.kit_items.select_related('product', 'variant_group'): + # Определяем товар для проверки + product = None + if kit_item.product: + product = kit_item.product + elif kit_item.variant_group: + # Берём первый активный товар из группы вариантов + available_products = kit_item.get_available_products() + product = available_products[0] if available_products else None + + if not product: + # Если товар не найден - комплект недоступен + return Decimal('0') + + # Получаем остатки на складе + stock_filter = {'product': product} + if warehouse: + stock_filter['warehouse'] = warehouse + + stocks = Stock.objects.filter(**stock_filter) + + # Суммируем свободное количество (available - reserved) + total_free = Decimal('0') + for stock in stocks: + free_qty = stock.quantity_available - stock.quantity_reserved + total_free += free_qty + + # Вычисляем сколько комплектов можно собрать из этого компонента + component_quantity = kit_item.quantity or Decimal('1') + if component_quantity <= 0: + return Decimal('0') + + kits_from_this_component = total_free / component_quantity + + # Ищем минимум (узкое место) + if min_available is None or kits_from_this_component < min_available: + min_available = kits_from_this_component + + # Возвращаем целую часть (нельзя собрать половину комплекта) + return Decimal(int(min_available)) if min_available is not None else Decimal('0') def make_permanent(self): """ diff --git a/myproject/products/services/kit_availability.py b/myproject/products/services/kit_availability.py deleted file mode 100644 index a09d3ae..0000000 --- a/myproject/products/services/kit_availability.py +++ /dev/null @@ -1,36 +0,0 @@ -""" -Сервис для проверки доступности комплектов. -""" - - -class KitAvailabilityChecker: - """ - Проверяет доступность комплектов на основе остатков товаров. - """ - - @staticmethod - def check_availability(kit, stock_manager=None): - """ - Проверяет доступность всего комплекта. - - Комплект доступен, если для каждой позиции в комплекте - есть хотя бы один доступный вариант товара. - - Args: - kit (ProductKit): Комплект для проверки - stock_manager: Объект управления складом (если не указан, используется стандартный) - - Returns: - bool: True, если комплект полностью доступен, иначе False - """ - from ..utils.stock_manager import StockManager - - if stock_manager is None: - stock_manager = StockManager() - - for kit_item in kit.kit_items.all(): - best_product = kit_item.get_best_available_product(stock_manager) - if not best_product: - return False - - return True diff --git a/myproject/products/templates/products/catalog.html b/myproject/products/templates/products/catalog.html index 55a06d3..8e0d84d 100644 --- a/myproject/products/templates/products/catalog.html +++ b/myproject/products/templates/products/catalog.html @@ -290,6 +290,14 @@ / {{ item.total_available|floatformat:"-3" }} всего + {% elif item.item_type == 'kit' %} + {# Информация об остатках для комплектов #} +