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>
This commit is contained in:
358
QUICK_REFERENCE.md
Normal file
358
QUICK_REFERENCE.md
Normal file
@@ -0,0 +1,358 @@
|
||||
# Быстрая справка: Наличие товаров и цены вариантов
|
||||
|
||||
## В Python коде
|
||||
|
||||
### Product (товар)
|
||||
|
||||
```python
|
||||
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 (группа вариантов)
|
||||
|
||||
```python
|
||||
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)
|
||||
|
||||
### Проверка наличия товара
|
||||
|
||||
```html
|
||||
{% if product.in_stock %}
|
||||
<span class="badge badge-success">В наличии</span>
|
||||
{% else %}
|
||||
<span class="badge badge-danger">Нет в наличии</span>
|
||||
{% endif %}
|
||||
```
|
||||
|
||||
### Отображение цены товара
|
||||
|
||||
```html
|
||||
<div class="price">
|
||||
{{ product.sale_price }} руб
|
||||
</div>
|
||||
```
|
||||
|
||||
### Группа вариантов - статус наличия
|
||||
|
||||
```html
|
||||
{% if variant_group.in_stock %}
|
||||
<p class="text-success">
|
||||
<strong>Доступно</strong> - есть варианты в наличии
|
||||
</p>
|
||||
{% else %}
|
||||
<p class="text-danger">
|
||||
<strong>Недоступно</strong> - все варианты закончились
|
||||
</p>
|
||||
{% endif %}
|
||||
```
|
||||
|
||||
### Группа вариантов - цена
|
||||
|
||||
```html
|
||||
<div class="variant-price">
|
||||
Цена: <strong>{{ variant_group.price }} руб</strong>
|
||||
</div>
|
||||
```
|
||||
|
||||
### Список товаров в группе
|
||||
|
||||
```html
|
||||
<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>
|
||||
```
|
||||
|
||||
### Полный пример - карточка варианта
|
||||
|
||||
```html
|
||||
<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 (запросы к БД)
|
||||
|
||||
### Оптимизация запросов
|
||||
|
||||
```python
|
||||
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
|
||||
})
|
||||
```
|
||||
|
||||
### Фильтрация товаров в наличии
|
||||
|
||||
```python
|
||||
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 вручную**
|
||||
|
||||
```python
|
||||
# НЕПРАВИЛЬНО!
|
||||
product.in_stock = True
|
||||
product.save()
|
||||
# Это будет перезаписано при следующем изменении Stock
|
||||
```
|
||||
|
||||
✅ **Правильно:**
|
||||
Система сама обновит Product.in_stock при изменении остатков.
|
||||
|
||||
---
|
||||
|
||||
❌ **Ошибка 2: Не использовать prefetch_related для вариантов**
|
||||
|
||||
```python
|
||||
# НЕПРАВИЛЬНО (N+1 query problem)!
|
||||
for group in groups:
|
||||
price = group.price # Это выполнит запрос для каждой группы!
|
||||
```
|
||||
|
||||
✅ **Правильно:**
|
||||
```python
|
||||
groups = ProductVariantGroup.objects.prefetch_related('items__product')
|
||||
for group in groups:
|
||||
price = group.price # Всего 2 запроса вместо N+1
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
❌ **Ошибка 3: Фильтровать по in_stock на ProductVariantGroup**
|
||||
|
||||
```python
|
||||
# НЕПРАВИЛЬНО!
|
||||
groups = ProductVariantGroup.objects.filter(in_stock=True)
|
||||
# in_stock это свойство, а не поле БД
|
||||
```
|
||||
|
||||
✅ **Правильно:**
|
||||
```python
|
||||
# Если нужны группы где есть хотя бы один товар в наличии
|
||||
groups = ProductVariantGroup.objects.filter(
|
||||
items__product__in_stock=True
|
||||
).distinct()
|
||||
|
||||
# Или отфильтровать в Python
|
||||
groups = [g for g in groups if g.in_stock]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Дополнительные полезные запросы
|
||||
|
||||
### Все товары без наличия
|
||||
|
||||
```python
|
||||
from products.models import Product
|
||||
|
||||
out_of_stock = Product.objects.filter(in_stock=False)
|
||||
```
|
||||
|
||||
### Группы вариантов где нет ни одного товара в наличии
|
||||
|
||||
```python
|
||||
from django.db.models import Exists, OuterRef
|
||||
|
||||
ProductVariantGroup.objects.filter(
|
||||
~Exists(ProductVariantGroupItem.objects.filter(
|
||||
variant_group=OuterRef('pk'),
|
||||
product__in_stock=True
|
||||
))
|
||||
)
|
||||
```
|
||||
|
||||
### Товары которые изменили статус наличия за последний час
|
||||
|
||||
```python
|
||||
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`
|
||||
Reference in New Issue
Block a user