Добавлена система управления наличием товаров на трёх уровнях: 1. Product.in_stock (поле БД) - Булево значение: есть/нет в наличии - Автоматически обновляется при изменении Stock - Используется для быстрого поиска и фильтрации товаров 2. Сигналы для синхронизации (inventory/signals.py) - При изменении Stock → обновляется Product.in_stock - Логика: товар в наличии если есть Stock с quantity_available > 0 3. ProductVariantGroup.in_stock (свойство) - Вариант в наличии если хотя бы один из товаров в наличии - Динамически рассчитывается по Product.in_stock товаров в группе 4. ProductVariantGroup.price (свойство) - Цена по приоритету: берём цену товара с приоритетом 1, если он в наличии - Если никто не в наличии: берём максимальную цену из всех товаров - Возвращает Decimal или None если группа пуста Файлы: - myproject/products/models.py: добавлено поле in_stock и свойства в ProductVariantGroup - myproject/inventory/signals.py: добавлены сигналы для синхронизации - myproject/products/migrations/0003_add_product_in_stock.py: миграция для поля in_stock - VARIANT_STOCK_IMPLEMENTATION.md: полная документация архитектуры - QUICK_REFERENCE.md: быстрая справка по использованию Особенности: ✓ Система простая и элегантная (без костылей) ✓ Обратная совместимость не требуется ✓ Высокая производительность (индексирование, минимум JOIN'ов) ✓ Актуальные данные (сигналы гарантируют синхронизацию) ✓ Легко расширяемая (свойства можно менять без миграций) 🤖 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com>
10 KiB
10 KiB
Быстрая справка: Наличие товаров и цены вариантов
В Python коде
Product (товар)
from products.models import Product
product = Product.objects.get(id=1)
# Проверить есть ли в наличии
if product.in_stock:
print(f"{product.name} - в наличии")
# Получить цену
print(product.sale_price)
# Фильтровать товары в наличии
in_stock = Product.objects.filter(in_stock=True)
out_of_stock = Product.objects.filter(in_stock=False)
ProductVariantGroup (группа вариантов)
from products.models import ProductVariantGroup
group = ProductVariantGroup.objects.prefetch_related('items__product').get(id=1)
# Проверить есть ли в наличии хотя бы один вариант
if group.in_stock:
print(f"Группа '{group.name}' - есть в наличии")
# Получить цену группы
price = group.price # Decimal('50.00')
# Перебрать товары по приоритету
for item in group.items.all().order_by('priority'):
print(f"{item.priority}. {item.product.name}")
if item.product.in_stock:
print(f" -> В наличии ({item.product.sale_price} руб)")
else:
print(f" -> Не в наличии")
В шаблонах (HTML)
Проверка наличия товара
{% if product.in_stock %}
<span class="badge badge-success">В наличии</span>
{% else %}
<span class="badge badge-danger">Нет в наличии</span>
{% endif %}
Отображение цены товара
<div class="price">
{{ product.sale_price }} руб
</div>
Группа вариантов - статус наличия
{% if variant_group.in_stock %}
<p class="text-success">
<strong>Доступно</strong> - есть варианты в наличии
</p>
{% else %}
<p class="text-danger">
<strong>Недоступно</strong> - все варианты закончились
</p>
{% endif %}
Группа вариантов - цена
<div class="variant-price">
Цена: <strong>{{ variant_group.price }} руб</strong>
</div>
Список товаров в группе
<table class="variants">
<thead>
<tr>
<th>Приоритет</th>
<th>Товар</th>
<th>Цена</th>
<th>Статус</th>
</tr>
</thead>
<tbody>
{% for item in variant_group.items.all %}
<tr>
<td>{{ item.priority }}</td>
<td>{{ item.product.name }}</td>
<td>{{ item.product.sale_price }} руб</td>
<td>
{% if item.product.in_stock %}
<span class="badge badge-success">В наличии</span>
{% else %}
<span class="badge badge-secondary">Нет</span>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
Полный пример - карточка варианта
<div class="variant-card">
<h3>{{ variant_group.name }}</h3>
<div class="status">
{% if variant_group.in_stock %}
<span class="badge badge-success badge-lg">✓ В наличии</span>
{% else %}
<span class="badge badge-danger badge-lg">✗ Нет в наличии</span>
{% endif %}
</div>
<div class="price">
<strong>{{ variant_group.price }} руб</strong>
</div>
<div class="variants-list">
<small class="text-muted">Доступные варианты:</small>
<ul>
{% for item in variant_group.items.all|slice:":3" %}
<li>
{{ item.product.name }}
{% if item.product.in_stock %}
✓
{% endif %}
</li>
{% endfor %}
</ul>
</div>
{% if variant_group.in_stock %}
<button class="btn btn-primary">Добавить в корзину</button>
{% else %}
<button class="btn btn-secondary" disabled>Недоступно</button>
{% endif %}
</div>
В View (запросы к БД)
Оптимизация запросов
from django.shortcuts import render
from products.models import ProductVariantGroup
def variant_groups_list(request):
# ПРАВИЛЬНО: используем prefetch_related для оптимизации
groups = ProductVariantGroup.objects.prefetch_related(
'items__product'
)
return render(request, 'variants.html', {
'variant_groups': groups
})
Фильтрация товаров в наличии
from products.models import Product
# Получить только товары в наличии
in_stock = Product.objects.filter(in_stock=True)
# Получить только товары без наличия
out_of_stock = Product.objects.filter(in_stock=False)
# Комбинированный фильтр
available = Product.objects.filter(
is_active=True,
in_stock=True
).order_by('name')
Логика наличия
Когда товар считается "в наличии"?
Товар в наличии (Product.in_stock = True) когда:
- Существует запись в Stock с
quantity_available > 0 - Это может быть на любом из складов
- quantity_available = quantity - reserved (свободный остаток)
Когда он перестаёт быть в наличии?
- Все Stock записи удалены
- Или всем Stock записям quantity_available = 0 (все проданы или зарезервированы)
Как это обновляется автоматически?
- При создании приходного документа (Incoming)
- При продаже товара (Sale)
- При списании товара (WriteOff)
- При изменении резервирования (Reservation)
Вы не должны вручную обновлять Product.in_stock!
Цена варианта - логика
Порядок определения цены ProductVariantGroup:
-
Есть товары в наличии?
- Да → берём цену товара с наименьшим приоритетом среди доступных
- Пример: приоритет 1 доступен → его цена
-
Нет товаров в наличии?
- Все недоступны → берём максимальную цену из всех товаров
- Пример: цены 50, 60, 70 → показываем 70 (самая дорогая)
Пример расчёта:
Группа "Роза красная Freedom"
├─ Приоритет 1: Роза 50см, цена 50 руб, в наличии ✓
├─ Приоритет 2: Роза 60см, цена 60 руб, в наличии ✓
└─ Приоритет 3: Роза 70см, цена 70 руб, нет в наличии ✗
Цена группы = 50 руб (первый в наличии)
Группа "Роза красная Freedom"
├─ Приоритет 1: Роза 50см, цена 50 руб, нет ✗
├─ Приоритет 2: Роза 60см, цена 60 руб, нет ✗
└─ Приоритет 3: Роза 70см, цена 70 руб, нет ✗
Цена группы = 70 руб (максимальная из всех)
Типичные ошибки
❌ Ошибка 1: Попытка обновить Product.in_stock вручную
# НЕПРАВИЛЬНО!
product.in_stock = True
product.save()
# Это будет перезаписано при следующем изменении Stock
✅ Правильно: Система сама обновит Product.in_stock при изменении остатков.
❌ Ошибка 2: Не использовать prefetch_related для вариантов
# НЕПРАВИЛЬНО (N+1 query problem)!
for group in groups:
price = group.price # Это выполнит запрос для каждой группы!
✅ Правильно:
groups = ProductVariantGroup.objects.prefetch_related('items__product')
for group in groups:
price = group.price # Всего 2 запроса вместо N+1
❌ Ошибка 3: Фильтровать по in_stock на ProductVariantGroup
# НЕПРАВИЛЬНО!
groups = ProductVariantGroup.objects.filter(in_stock=True)
# in_stock это свойство, а не поле БД
✅ Правильно:
# Если нужны группы где есть хотя бы один товар в наличии
groups = ProductVariantGroup.objects.filter(
items__product__in_stock=True
).distinct()
# Или отфильтровать в Python
groups = [g for g in groups if g.in_stock]
Дополнительные полезные запросы
Все товары без наличия
from products.models import Product
out_of_stock = Product.objects.filter(in_stock=False)
Группы вариантов где нет ни одного товара в наличии
from django.db.models import Exists, OuterRef
ProductVariantGroup.objects.filter(
~Exists(ProductVariantGroupItem.objects.filter(
variant_group=OuterRef('pk'),
product__in_stock=True
))
)
Товары которые изменили статус наличия за последний час
from django.utils import timezone
from datetime import timedelta
Product.objects.filter(
updated_at__gte=timezone.now() - timedelta(hours=1)
)
Помощь и контакты
Если что-то не работает:
- Проверьте что миграция
0003_add_product_in_stockприменена - Убедитесь что сигналы зарегистрированы в
inventory/apps.py - Проверьте логи: есть ли ошибки в сигналах при обновлении Stock
- Запустите тестовый скрипт:
python test_variant_stock.py