Files
octopus/QUICK_REFERENCE.md
Andrey Smakotin 2341cf57c1 feat: Реализовать систему наличия товаров и цены вариантов
Добавлена система управления наличием товаров на трёх уровнях:

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>
2025-10-29 23:22:01 +03:00

10 KiB
Raw Blame History

Быстрая справка: Наличие товаров и цены вариантов

В 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 (все проданы или зарезервированы)

Как это обновляется автоматически?

  1. При создании приходного документа (Incoming)
  2. При продаже товара (Sale)
  3. При списании товара (WriteOff)
  4. При изменении резервирования (Reservation)

Вы не должны вручную обновлять Product.in_stock!


Цена варианта - логика

Порядок определения цены ProductVariantGroup:

  1. Есть товары в наличии?

    • Да → берём цену товара с наименьшим приоритетом среди доступных
    • Пример: приоритет 1 доступен → его цена
  2. Нет товаров в наличии?

    • Все недоступны → берём максимальную цену из всех товаров
    • Пример: цены 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)
)

Помощь и контакты

Если что-то не работает:

  1. Проверьте что миграция 0003_add_product_in_stock применена
  2. Убедитесь что сигналы зарегистрированы в inventory/apps.py
  3. Проверьте логи: есть ли ошибки в сигналах при обновлении Stock
  4. Запустите тестовый скрипт: python test_variant_stock.py