Добавлен расчёт и отображение доступного количества комплектов
- Добавлен метод calculate_available_quantity() в модель ProductKit для точного расчёта максимального количества комплектов на основе свободных остатков компонентов - Обновлён метод check_availability() для использования нового расчёта (обратная совместимость) - Удалён устаревший сервис kit_availability.py Исправлено отображение остатков комплектов: - products_list.html: вместо прочерка показывается количество комплектов - catalog.html: добавлено отображение доступного количества комплектов с цветовой индикацией - POS terminal.js: в карточке товара показывается конкретное количество вместо общего 'В наличии' Обновлены представления: - ProductsListView: аннотирует комплекты атрибутом total_free - CatalogView: рассчитывает доступное количество для каждого комплекта - POS get_products(): убран хардкод, используется реальный расчёт по складу
This commit is contained in:
@@ -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):
|
||||
"""
|
||||
|
||||
@@ -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
|
||||
@@ -290,6 +290,14 @@
|
||||
<span class="text-muted">/ {{ item.total_available|floatformat:"-3" }} всего</span>
|
||||
</small>
|
||||
</div>
|
||||
{% elif item.item_type == 'kit' %}
|
||||
{# Информация об остатках для комплектов #}
|
||||
<div class="mt-1">
|
||||
<small class="stock-info {% if item.total_free > 0 %}text-success{% else %}text-danger{% endif %}">
|
||||
<i class="bi bi-box-seam"></i>
|
||||
<strong>{{ item.total_free|floatformat:"0" }}</strong> компл. доступно
|
||||
</small>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center mt-1">
|
||||
|
||||
@@ -196,6 +196,8 @@
|
||||
<strong class="{% if item.total_free > 0 %}text-success{% else %}text-danger{% endif %}">{{ item.total_free|floatformat:-2 }}</strong>{% if item.total_reserved > 0 %}<small class="text-muted"> / {{ item.total_available|floatformat:-2 }}</small>
|
||||
<small class="text-warning d-block">{{ item.total_reserved|floatformat:-2 }} в резерве</small>
|
||||
{% endif %}
|
||||
{% elif item.item_type == 'kit' %}
|
||||
<strong class="{% if item.total_free > 0 %}text-success{% else %}text-danger{% endif %}">{{ item.total_free|floatformat:0 }}</strong> <small class="text-muted">компл.</small>
|
||||
{% else %}
|
||||
<span class="text-muted">-</span>
|
||||
{% endif %}
|
||||
|
||||
@@ -78,6 +78,8 @@ class CatalogView(LoginRequiredMixin, TemplateView):
|
||||
if k.id not in kits_dict:
|
||||
k.item_type = 'kit'
|
||||
k.main_photo = k.photos.all()[0] if k.photos.all() else None
|
||||
# Рассчитываем доступное количество комплектов
|
||||
k.total_free = k.calculate_available_quantity()
|
||||
kits_dict[k.id] = k
|
||||
|
||||
# Теперь добавляем все товары, которых еще нет (товары без категорий или не загруженные)
|
||||
@@ -104,6 +106,8 @@ class CatalogView(LoginRequiredMixin, TemplateView):
|
||||
if k.id not in kits_dict:
|
||||
k.item_type = 'kit'
|
||||
k.main_photo = k.photos.all()[0] if k.photos.all() else None
|
||||
# Рассчитываем доступное количество комплектов
|
||||
k.total_free = k.calculate_available_quantity()
|
||||
kits_dict[k.id] = k
|
||||
|
||||
# Объединяем и сортируем
|
||||
|
||||
@@ -347,6 +347,8 @@ class CombinedProductListView(LoginRequiredMixin, ManagerOwnerRequiredMixin, Lis
|
||||
kits_list = list(kits.order_by('-created_at'))
|
||||
for k in kits_list:
|
||||
k.item_type = 'kit'
|
||||
# Рассчитываем доступное количество комплектов (для отображения остатков)
|
||||
k.total_free = k.calculate_available_quantity()
|
||||
|
||||
# Объединяем и сортируем по дате создания
|
||||
combined = sorted(
|
||||
|
||||
Reference in New Issue
Block a user