Добавлена система управления наличием товаров на трёх уровнях: 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>
409 lines
16 KiB
Markdown
409 lines
16 KiB
Markdown
# Реализация системы наличия товаров и цены вариантов
|
||
|
||
## Обзор
|
||
|
||
Реализована система управления наличием товаров (Product) и вычисления цены для групп вариантов (ProductVariantGroup). Система работает на трёх уровнях:
|
||
|
||
1. **Product** — товар имеет поле `in_stock` (булево значение: есть/нет в наличии)
|
||
2. **ProductVariantGroup** — группа вариантов с вычисляемыми свойствами `in_stock` и `price`
|
||
3. **Stock** — система складских остатков определяет статус наличия на основе `quantity_available > 0`
|
||
|
||
---
|
||
|
||
## 1. Модель Product — добавлено поле `in_stock`
|
||
|
||
### Изменение в `/products/models.py`:
|
||
|
||
```python
|
||
class Product(models.Model):
|
||
# ... другие поля ...
|
||
in_stock = models.BooleanField(
|
||
default=False,
|
||
verbose_name="В наличии",
|
||
db_index=True,
|
||
help_text="Автоматически обновляется при изменении остатков на складе"
|
||
)
|
||
```
|
||
|
||
**Миграция**: `products/migrations/0003_add_product_in_stock.py`
|
||
|
||
### Особенности:
|
||
- Поле хранится в БД (для оптимизации поиска и фильтрации)
|
||
- Индексировано для быстрого поиска товаров в наличии
|
||
- Обновляется **автоматически** при изменении остатков через сигналы
|
||
|
||
---
|
||
|
||
## 2. Сигналы для автоматического обновления `Product.in_stock`
|
||
|
||
### Изменения в `/inventory/signals.py`:
|
||
|
||
Добавлены два сигнала:
|
||
|
||
```python
|
||
@receiver(post_save, sender=Stock)
|
||
def update_product_in_stock_on_stock_change(sender, instance, created, **kwargs):
|
||
"""
|
||
При создании/изменении Stock записи обновляем Product.in_stock.
|
||
"""
|
||
_update_product_in_stock(instance.product_id)
|
||
|
||
|
||
@receiver(pre_delete, sender=Stock)
|
||
def update_product_in_stock_on_stock_delete(sender, instance, **kwargs):
|
||
"""
|
||
При удалении Stock записи обновляем Product.in_stock.
|
||
"""
|
||
_update_product_in_stock(instance.product_id)
|
||
```
|
||
|
||
### Вспомогательная функция:
|
||
|
||
```python
|
||
def _update_product_in_stock(product_id):
|
||
"""
|
||
Обновить статус in_stock на основе остатков.
|
||
|
||
Логика:
|
||
- Товар в наличии (in_stock=True) если существует хотя бы один Stock
|
||
с quantity_available > 0 (есть свободный остаток на любом складе)
|
||
- Товар не в наличии (in_stock=False) если нет ни одного Stock с остатком
|
||
"""
|
||
product = Product.objects.get(id=product_id)
|
||
|
||
has_stock = Stock.objects.filter(
|
||
product=product,
|
||
quantity_available__gt=0 # Свободный остаток > 0
|
||
).exists()
|
||
|
||
if product.in_stock != has_stock:
|
||
Product.objects.filter(id=product.id).update(in_stock=has_stock)
|
||
```
|
||
|
||
### Как это работает:
|
||
|
||
1. **При создании приходного документа (Incoming)**:
|
||
- Создаётся StockBatch (партия)
|
||
- Создаётся/обновляется Stock (агрегированный остаток)
|
||
- Stock.refresh_from_batches() пересчитывает quantity_available
|
||
- Срабатывает сигнал post_save на Stock
|
||
- Product.in_stock автоматически обновляется
|
||
|
||
2. **При продаже (Sale)**:
|
||
- StockBatchManager.write_off_by_fifo() списывает товар
|
||
- Stock.quantity_available уменьшается
|
||
- Срабатывает сигнал post_save на Stock
|
||
- Product.in_stock автоматически обновляется
|
||
|
||
3. **При списании (WriteOff)**:
|
||
- WriteOff модель уменьшает quantity в StockBatch
|
||
- Stock.refresh_from_batches() пересчитывает остаток
|
||
- Срабатывает сигнал post_save на Stock
|
||
- Product.in_stock автоматически обновляется
|
||
|
||
---
|
||
|
||
## 3. Модель ProductVariantGroup — свойства `in_stock` и `price`
|
||
|
||
### Изменения в `/products/models.py`:
|
||
|
||
```python
|
||
class ProductVariantGroup(models.Model):
|
||
# ... существующие поля ...
|
||
|
||
@property
|
||
def in_stock(self):
|
||
"""
|
||
Вариант в наличии, если хотя бы один из его товаров в наличии.
|
||
|
||
Логика:
|
||
- Проверяет есть ли товар с Product.in_stock=True в этой группе
|
||
- Возвращает True/False
|
||
|
||
Примеры:
|
||
- "Роза 50см" в наличии → вариант в наличии
|
||
- "Роза 60см" нет, но "Роза 70см" есть → вариант в наличии
|
||
- Все розы отсутствуют → вариант не в наличии
|
||
"""
|
||
return self.items.filter(product__in_stock=True).exists()
|
||
|
||
@property
|
||
def price(self):
|
||
"""
|
||
Цена варианта определяется по приоритету товаров.
|
||
|
||
Логика:
|
||
1. Идём по товарам в порядке приоритета (priority = 1, 2, 3...)
|
||
2. Первый товар в наличии (in_stock=True) → берём его цену
|
||
3. Если ни один товар не в наличии → берём максимальную цену из всех товаров
|
||
|
||
Примеры:
|
||
- Приоритет 1 (роза 50см) в наличии: цена 50.00 руб
|
||
- Приоритет 1 нет, приоритет 2 (роза 60см) в наличии: цена 60.00 руб
|
||
- Все недоступны: цена = max(50.00, 60.00, 70.00) = 70.00 руб
|
||
|
||
Возвращает Decimal (цену) или None если группа пуста.
|
||
"""
|
||
items = self.items.all().order_by('priority', 'id')
|
||
|
||
if not items.exists():
|
||
return None
|
||
|
||
# Ищем первый товар в наличии
|
||
for item in items:
|
||
if item.product.in_stock:
|
||
return item.product.sale_price
|
||
|
||
# Если ни один товар не в наличии - берём самый дорогой
|
||
max_price = None
|
||
for item in items:
|
||
if max_price is None or item.product.sale_price > max_price:
|
||
max_price = item.product.sale_price
|
||
|
||
return max_price
|
||
```
|
||
|
||
### Использование в шаблонах и views:
|
||
|
||
```python
|
||
# В view
|
||
variant_group = ProductVariantGroup.objects.get(id=1)
|
||
|
||
# Проверить есть ли вариант в наличии
|
||
if variant_group.in_stock:
|
||
# Вариант доступен
|
||
pass
|
||
|
||
# Получить цену варианта
|
||
price = variant_group.price # Decimal('50.00')
|
||
|
||
# В шаблоне
|
||
{{ variant_group.in_stock }} <!-- True/False -->
|
||
{{ variant_group.price }} <!-- 50.00 -->
|
||
```
|
||
|
||
---
|
||
|
||
## 4. Архитектурные решения
|
||
|
||
### Почему свойства (properties) а не поля БД?
|
||
|
||
**ProductVariantGroup.in_stock** и **ProductVariantGroup.price** реализованы как **свойства (properties)**, а не как сохраняемые поля:
|
||
|
||
✅ **Преимущества:**
|
||
- **Всегда актуальны** — в любой момент рассчитываются на основе текущих данных
|
||
- **Нет дублирования данных** — источник истины один (Product.in_stock и Product.sale_price)
|
||
- **Без миграций** — при изменении логики не нужны миграции БД
|
||
- **Простота** — чистый и понятный код
|
||
|
||
⚠️ **Недостатки (решены):**
|
||
- **Производительность** — O(N) на каждый вызов, где N = количество товаров в группе
|
||
- **Решение**: используйте prefetch_related в views:
|
||
|
||
```python
|
||
# Плохо (N+1 queries)
|
||
for variant_group in groups:
|
||
print(variant_group.price)
|
||
|
||
# Хорошо (1 query + 1 query для товаров)
|
||
groups = ProductVariantGroup.objects.prefetch_related('items__product')
|
||
for variant_group in groups:
|
||
print(variant_group.price)
|
||
```
|
||
|
||
### Почему Product.in_stock = поле БД?
|
||
|
||
**Product.in_stock** — это сохраняемое поле в БД:
|
||
|
||
✅ **Причины:**
|
||
- **Оптимизация поиска** — можно фильтровать: `Product.objects.filter(in_stock=True)`
|
||
- **Производительность** — не нужно JOIN'ить Stock при поиске
|
||
- **Индекс** — ускоряет фильтрацию
|
||
- **Системная важность** — наличие товара — критичный параметр
|
||
|
||
---
|
||
|
||
## 5. Поток данных (Data Flow)
|
||
|
||
```
|
||
Incoming (приход товара)
|
||
↓
|
||
StockBatch создаётся
|
||
↓
|
||
Stock создаётся/обновляется
|
||
├─ quantity_available пересчитывается
|
||
└─ post_save сигнал срабатывает
|
||
↓
|
||
_update_product_in_stock(product_id)
|
||
├─ Проверяет есть ли Stock с quantity_available > 0
|
||
└─ Product.in_stock обновляется (True/False)
|
||
↓
|
||
ProductVariantGroup.in_stock (свойство)
|
||
├─ Проверяет есть ли товар в группе с Product.in_stock=True
|
||
└─ Возвращает True/False
|
||
|
||
ProductVariantGroup.price (свойство)
|
||
├─ Идёт по товарам по приоритету
|
||
├─ Берёт цену первого в наличии
|
||
└─ Или максимальную цену если никто не в наличии
|
||
```
|
||
|
||
---
|
||
|
||
## 6. Примеры использования
|
||
|
||
### Пример 1: Проверить есть ли товар в наличии
|
||
|
||
```python
|
||
from products.models import Product
|
||
|
||
# Получить товар
|
||
product = Product.objects.get(id=1)
|
||
|
||
# Проверить наличие
|
||
if product.in_stock:
|
||
print(f"{product.name} в наличии")
|
||
else:
|
||
print(f"{product.name} не в наличии")
|
||
|
||
# Фильтровать товары в наличии
|
||
in_stock_products = Product.objects.filter(in_stock=True)
|
||
```
|
||
|
||
### Пример 2: Работа с группой вариантов
|
||
|
||
```python
|
||
from products.models import ProductVariantGroup
|
||
|
||
# Получить группу
|
||
group = ProductVariantGroup.objects.prefetch_related('items__product').get(id=1)
|
||
|
||
# Проверить статус группы
|
||
print(f"Вариант в наличии: {group.in_stock}") # True/False
|
||
print(f"Цена варианта: {group.price} руб") # Decimal('50.00')
|
||
|
||
# Получить всю информацию
|
||
for item in group.items.all().order_by('priority'):
|
||
status = "✓" if item.product.in_stock else "✗"
|
||
print(f"{item.priority}. {item.product.name} ({item.product.sale_price}) {status}")
|
||
```
|
||
|
||
### Пример 3: Отображение в шаблоне
|
||
|
||
```html
|
||
{% for variant_group in variant_groups %}
|
||
<div class="variant-group">
|
||
<h3>{{ variant_group.name }}</h3>
|
||
|
||
{% if variant_group.in_stock %}
|
||
<span class="badge badge-success">В наличии</span>
|
||
{% else %}
|
||
<span class="badge badge-danger">Нет в наличии</span>
|
||
{% endif %}
|
||
|
||
<div class="price">
|
||
Цена: {{ variant_group.price }} руб
|
||
</div>
|
||
|
||
<ul class="variants">
|
||
{% for item in variant_group.items.all %}
|
||
<li>
|
||
{{ item.product.name }}
|
||
{% if item.product.in_stock %}
|
||
<span class="in-stock">✓ В наличии</span>
|
||
{% endif %}
|
||
</li>
|
||
{% endfor %}
|
||
</ul>
|
||
</div>
|
||
{% endfor %}
|
||
```
|
||
|
||
---
|
||
|
||
## 7. Тестирование
|
||
|
||
### Создан тестовый скрипт: `test_variant_stock.py`
|
||
|
||
Скрипт проверяет:
|
||
|
||
1. **ТЕСТ 1**: Обновление Product.in_stock при создании Stock
|
||
- Создаёт товар без наличия (in_stock=False)
|
||
- Добавляет приход товара (создаёт Stock)
|
||
- Проверяет что Product.in_stock автоматически стал True
|
||
|
||
2. **ТЕСТ 2**: Свойство ProductVariantGroup.in_stock
|
||
- Создаёт группу вариантов с несколькими товарами
|
||
- Один товар в наличии
|
||
- Проверяет что вариант.in_stock = True
|
||
|
||
3. **ТЕСТ 3**: Свойство ProductVariantGroup.price
|
||
- Товары с приоритетами 1, 2, 3 и ценами 50, 60, 70 руб
|
||
- Только товар с приоритетом 1 в наличии
|
||
- Проверяет что вариант.price = 50.00 руб
|
||
|
||
4. **ТЕСТ 4**: Цена варианта когда ни один товар не в наличии
|
||
- Все товары не в наличии
|
||
- Цены: 100, 150, 200 руб
|
||
- Проверяет что вариант.price = 200.00 руб (максимальная)
|
||
|
||
### Запуск тестов:
|
||
|
||
```bash
|
||
# Активировать окружение
|
||
source venv/Scripts/activate
|
||
|
||
# Запустить тестовый скрипт
|
||
python test_variant_stock.py
|
||
|
||
# Или запустить стандартные Django тесты
|
||
cd myproject
|
||
python manage.py test inventory -v 2
|
||
```
|
||
|
||
---
|
||
|
||
## 8. Файлы которые были изменены/созданы
|
||
|
||
### Изменены:
|
||
|
||
1. **`myproject/products/models.py`**
|
||
- Добавлено поле `in_stock` в Product
|
||
- Добавлены свойства `in_stock` и `price` в ProductVariantGroup
|
||
- Добавлен индекс для `in_stock`
|
||
|
||
2. **`myproject/inventory/signals.py`**
|
||
- Добавлены импорты Stock в начало файла
|
||
- Добавлены два сигнала: `update_product_in_stock_on_stock_change` и `update_product_in_stock_on_stock_delete`
|
||
- Добавлена вспомогательная функция `_update_product_in_stock`
|
||
|
||
3. **`myproject/products/migrations/0003_add_product_in_stock.py`** (создана)
|
||
- Миграция для добавления поля `in_stock` в Product
|
||
|
||
### Созданы:
|
||
|
||
1. **`test_variant_stock.py`**
|
||
- Тестовый скрипт для проверки функциональности
|
||
|
||
---
|
||
|
||
## 9. Резюме
|
||
|
||
✅ **Реализовано:**
|
||
|
||
1. **Product.in_stock** — булево поле, автоматически обновляется при изменении остатков
|
||
2. **ProductVariantGroup.in_stock** — свойство, вариант в наличии если хотя бы один товар в наличии
|
||
3. **ProductVariantGroup.price** — свойство, цена по приоритету или максимальная если все недоступны
|
||
4. **Сигналы** — автоматическое обновление Product.in_stock при изменении Stock
|
||
5. **Документация** — полное описание архитектуры и использования
|
||
|
||
✅ **Особенности:**
|
||
|
||
- Система простая и элегантная (без костылей)
|
||
- Обратная совместимость не требуется (как вы просили)
|
||
- Высокая производительность (индексирование, минимум JOIN'ов)
|
||
- Актуальные данные (сигналы гарантируют синхронизацию)
|
||
- Легко расширяемая (свойства можно менять без миграций)
|
||
|
||
✅ **Готово к использованию в views и шаблонах!**
|