fix: Улучшения системы ценообразования комплектов
Исправлены 4 проблемы: 1. Расчёт цены первого товара - улучшена валидация в getProductPrice и calculateFinalPrice 2. Отображение actual_price в Select2 вместо обычной цены 3. Количество по умолчанию = 1 для новых форм компонентов 4. Auto-select текста при клике на поле количества для удобства редактирования Изменённые файлы: - products/forms.py: добавлен __init__ в KitItemForm для quantity.initial = 1 - products/templates/includes/select2-product-init.html: обновлена formatSelectResult - products/templates/productkit_create.html: добавлен focus handler для auto-select - products/templates/productkit_edit.html: добавлен focus handler для auto-select 🤖 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
304
DYNAMIC_COST_PRICE_IMPLEMENTATION.md
Normal file
304
DYNAMIC_COST_PRICE_IMPLEMENTATION.md
Normal file
@@ -0,0 +1,304 @@
|
||||
# Реализация динамической себестоимости товаров (FIFO)
|
||||
|
||||
## Обзор
|
||||
|
||||
Реализована система автоматического расчета себестоимости товаров на основе партий товара (StockBatch) с использованием средневзвешенного метода FIFO.
|
||||
|
||||
## Основные принципы
|
||||
|
||||
### Логика расчета
|
||||
|
||||
1. **Товар без партий** → `cost_price = 0.00`
|
||||
2. **Товар с партиями** → `cost_price = средневзвешенная стоимость`
|
||||
3. **Товар закончился** → `cost_price = 0.00`
|
||||
4. **Новая поставка** → `cost_price = пересчитывается автоматически`
|
||||
|
||||
### Формула расчета
|
||||
|
||||
```
|
||||
cost_price = Σ(quantity × cost_price) / Σ(quantity)
|
||||
```
|
||||
|
||||
Где суммируются все активные партии товара с `quantity > 0`.
|
||||
|
||||
## Реализованные компоненты
|
||||
|
||||
### 1. Сервис расчета себестоимости
|
||||
|
||||
**Файл:** `myproject/products/services/cost_calculator.py`
|
||||
|
||||
**Класс:** `ProductCostCalculator`
|
||||
|
||||
**Методы:**
|
||||
- `calculate_weighted_average_cost(product)` - рассчитывает средневзвешенную стоимость
|
||||
- `update_product_cost(product, save=True)` - обновляет кешированную стоимость
|
||||
- `get_cost_details(product)` - возвращает детальную информацию для UI
|
||||
|
||||
**Пример использования:**
|
||||
```python
|
||||
from products.services.cost_calculator import ProductCostCalculator
|
||||
|
||||
# Рассчитать стоимость
|
||||
cost = ProductCostCalculator.calculate_weighted_average_cost(product)
|
||||
|
||||
# Обновить кешированное значение
|
||||
old_cost, new_cost, was_updated = ProductCostCalculator.update_product_cost(product)
|
||||
|
||||
# Получить детали для отображения
|
||||
details = ProductCostCalculator.get_cost_details(product)
|
||||
```
|
||||
|
||||
### 2. Django Signals для автообновления
|
||||
|
||||
**Файл:** `myproject/inventory/signals.py`
|
||||
|
||||
**Сигналы:**
|
||||
- `update_product_cost_on_batch_change` - срабатывает при создании/изменении StockBatch
|
||||
- `update_product_cost_on_batch_delete` - срабатывает при удалении StockBatch
|
||||
|
||||
**Триггеры автообновления:**
|
||||
- Создание новой партии (поступление товара)
|
||||
- Изменение количества в партии
|
||||
- Изменение стоимости партии
|
||||
- Удаление партии
|
||||
|
||||
### 3. Property в модели Product
|
||||
|
||||
**Файл:** `myproject/products/models/products.py`
|
||||
|
||||
**Добавлено:**
|
||||
```python
|
||||
@property
|
||||
def cost_price_details(self):
|
||||
"""
|
||||
Детали расчета себестоимости для отображения в UI.
|
||||
|
||||
Returns:
|
||||
dict: {
|
||||
'cached_cost': Decimal, # Кешированная себестоимость (из БД)
|
||||
'calculated_cost': Decimal, # Рассчитанная себестоимость (из партий)
|
||||
'is_synced': bool, # Совпадают ли значения
|
||||
'total_quantity': Decimal, # Общее количество в партиях
|
||||
'batches': [...] # Список партий с деталями
|
||||
}
|
||||
"""
|
||||
```
|
||||
|
||||
**Обновлено поле:**
|
||||
```python
|
||||
cost_price = models.DecimalField(
|
||||
max_digits=10,
|
||||
decimal_places=2,
|
||||
verbose_name="Себестоимость",
|
||||
help_text="Автоматически вычисляется из партий (средневзвешенная стоимость по FIFO)"
|
||||
)
|
||||
```
|
||||
|
||||
### 4. Обновленная страница товара
|
||||
|
||||
**Файл:** `myproject/products/templates/products/product_detail.html`
|
||||
|
||||
**Добавлено:**
|
||||
- Отображение текущей себестоимости
|
||||
- Кнопка "Детали расчета" (раскрывающаяся секция)
|
||||
- Таблица с разбивкой по партиям:
|
||||
- Склад
|
||||
- Количество
|
||||
- Себестоимость за единицу
|
||||
- Общая стоимость партии
|
||||
- Дата создания партии
|
||||
- Сравнение кешированной и рассчитанной стоимости
|
||||
- Предупреждение при рассинхронизации
|
||||
|
||||
### 5. Management команда для пересчета
|
||||
|
||||
**Файл:** `myproject/products/management/commands/recalculate_product_costs.py`
|
||||
|
||||
**Использование:**
|
||||
```bash
|
||||
# Пересчитать все товары
|
||||
python manage.py recalculate_product_costs
|
||||
|
||||
# Показать детальную информацию
|
||||
python manage.py recalculate_product_costs --verbose
|
||||
|
||||
# Предварительный просмотр без сохранения
|
||||
python manage.py recalculate_product_costs --dry-run
|
||||
|
||||
# Показать только товары с изменениями
|
||||
python manage.py recalculate_product_costs --only-changed
|
||||
```
|
||||
|
||||
## Примеры работы
|
||||
|
||||
### Сценарий 1: Создание товара
|
||||
```
|
||||
1. Создается товар → cost_price = 0.00 (нет партий)
|
||||
```
|
||||
|
||||
### Сценарий 2: Первая поставка
|
||||
```
|
||||
1. Товар: cost_price = 0.00
|
||||
2. Приход: 10 шт по 100 руб → создается StockBatch
|
||||
3. Signal срабатывает → cost_price = 100.00
|
||||
```
|
||||
|
||||
### Сценарий 3: Вторая поставка по другой цене
|
||||
```
|
||||
1. Товар: cost_price = 100.00 (партия: 10 шт × 100 руб)
|
||||
2. Приход: 10 шт по 120 руб → создается новая StockBatch
|
||||
3. Signal срабатывает → cost_price = 110.00
|
||||
Расчет: (10×100 + 10×120) / 20 = 2200 / 20 = 110.00
|
||||
```
|
||||
|
||||
### Сценарий 4: Товар закончился
|
||||
```
|
||||
1. Товар: cost_price = 110.00 (партии: 10+10 шт)
|
||||
2. Продажа: 20 шт → партии опустошаются (quantity = 0)
|
||||
3. Signal срабатывает → cost_price = 0.00
|
||||
```
|
||||
|
||||
### Сценарий 5: Новая поставка после опустошения
|
||||
```
|
||||
1. Товар: cost_price = 0.00
|
||||
2. Приход: 15 шт по 130 руб → создается StockBatch
|
||||
3. Signal срабатывает → cost_price = 130.00
|
||||
```
|
||||
|
||||
## Тестирование
|
||||
|
||||
### Математическая корректность
|
||||
|
||||
Создан тестовый скрипт: `test_cost_calculator.py`
|
||||
|
||||
**Результаты тестов:**
|
||||
- ✅ Товар без партий → 0.00
|
||||
- ✅ Одна партия → стоимость партии
|
||||
- ✅ Две партии одинаковой стоимости → та же стоимость
|
||||
- ✅ Две партии разной стоимости → средневзвешенная
|
||||
- ✅ Три партии с разным количеством → корректный расчет
|
||||
- ✅ Жизненный цикл товара → корректные переходы
|
||||
|
||||
**Запуск тестов:**
|
||||
```bash
|
||||
python test_cost_calculator.py
|
||||
```
|
||||
|
||||
## Архитектурные решения
|
||||
|
||||
### Почему кеширование в БД, а не Redis?
|
||||
|
||||
1. **Низкая частота изменений** - себестоимость меняется только при поставках/списаниях
|
||||
2. **Простота** - меньше движущихся частей, легче дебажить
|
||||
3. **Производительность** - один SELECT вместо двух обращений (Redis + PostgreSQL)
|
||||
4. **Транзакционность** - гарантируется целостность данных
|
||||
5. **Не требуется TTL** - данные актуальны до изменения партий
|
||||
|
||||
### Почему Django Signals?
|
||||
|
||||
1. **Автоматизация** - не нужно помнить вызывать пересчет вручную
|
||||
2. **Консистентность** - гарантируется актуальность данных
|
||||
3. **Прозрачность** - изменения происходят автоматически
|
||||
4. **Уже используется** - в проекте активно применяются signals
|
||||
|
||||
### Почему средневзвешенная, а не FIFO стоимость следующей партии?
|
||||
|
||||
1. **Более точная оценка** - учитывает весь остаток на складе
|
||||
2. **Актуальность для ценообразования** - показывает реальную среднюю стоимость товара
|
||||
3. **Стабильность** - не скачет при каждой продаже
|
||||
4. **Подходит для ProductKit** - корректный расчет стоимости комплектов
|
||||
|
||||
## Влияние на ProductKit
|
||||
|
||||
Расчет стоимости комплектов автоматически использует обновленную себестоимость компонентов:
|
||||
|
||||
```python
|
||||
# myproject/products/services/kit_pricing.py
|
||||
class KitCostCalculator:
|
||||
def calculate_cost(kit):
|
||||
for kit_item in kit.kit_items:
|
||||
item_cost = product.cost_price # ← Теперь динамическая!
|
||||
total_cost += item_cost * item_quantity
|
||||
```
|
||||
|
||||
## Мониторинг и отладка
|
||||
|
||||
### Проверка синхронизации
|
||||
|
||||
На странице товара отображается:
|
||||
- **Кешированная стоимость** - значение из БД (cost_price)
|
||||
- **Рассчитанная стоимость** - актуальный расчет из партий
|
||||
- **Статус синхронизации** - совпадают ли значения
|
||||
|
||||
### Ручной пересчет
|
||||
|
||||
Если возникла рассинхронизация, можно запустить:
|
||||
```bash
|
||||
python manage.py recalculate_product_costs
|
||||
```
|
||||
|
||||
### Логирование
|
||||
|
||||
Все операции логируются в стандартный Django logger:
|
||||
```python
|
||||
logger.info(f"Обновлена себестоимость товара {product.sku}: {old_cost} -> {new_cost}")
|
||||
logger.error(f"Ошибка при расчете себестоимости для товара {product.sku}: {e}")
|
||||
```
|
||||
|
||||
## Производительность
|
||||
|
||||
### Оптимизации
|
||||
|
||||
1. **Кеширование в БД** - один запрос вместо пересчета каждый раз
|
||||
2. **update_fields=['cost_price']** - обновляется только одно поле
|
||||
3. **Selective signals** - обновление только при реальных изменениях
|
||||
4. **Bulk operations** - management команда для массового пересчета
|
||||
|
||||
### Нагрузка
|
||||
|
||||
- **Чтение cost_price** - 0 дополнительных запросов (из БД)
|
||||
- **Создание партии** - 1 дополнительный UPDATE для товара
|
||||
- **Изменение партии** - 1 дополнительный UPDATE для товара
|
||||
- **Удаление партии** - 1 дополнительный UPDATE для товара
|
||||
|
||||
## Дальнейшие улучшения (опционально)
|
||||
|
||||
### Если появятся проблемы производительности:
|
||||
|
||||
1. **Отложенное обновление** - помечать товары для пересчета и обрабатывать фоном
|
||||
2. **Celery tasks** - асинхронный пересчет в очереди
|
||||
3. **Redis кеширование** - для часто запрашиваемых деталей расчета
|
||||
4. **Database triggers** - перенести логику в PostgreSQL
|
||||
|
||||
### Дополнительная функциональность:
|
||||
|
||||
1. **История изменений** - логировать изменения себестоимости
|
||||
2. **API endpoint** - получение деталей расчета через REST API
|
||||
3. **Alerts** - уведомления при значительных изменениях стоимости
|
||||
4. **Аналитика** - графики изменения себестоимости во времени
|
||||
|
||||
## Файлы изменений
|
||||
|
||||
### Созданные файлы:
|
||||
- `myproject/products/services/cost_calculator.py` - сервис расчета
|
||||
- `myproject/products/management/commands/recalculate_product_costs.py` - команда пересчета
|
||||
- `test_cost_calculator.py` - тесты математической корректности
|
||||
- `DYNAMIC_COST_PRICE_IMPLEMENTATION.md` - данная документация
|
||||
|
||||
### Измененные файлы:
|
||||
- `myproject/inventory/signals.py` - добавлены signals для автообновления
|
||||
- `myproject/products/models/products.py` - добавлен property cost_price_details
|
||||
- `myproject/products/templates/products/product_detail.html` - обновлен UI
|
||||
|
||||
## Заключение
|
||||
|
||||
Реализована полнофункциональная система динамического расчета себестоимости товаров:
|
||||
|
||||
✅ **Автоматическое обновление** - через Django signals
|
||||
✅ **Производительность** - кеширование в БД
|
||||
✅ **Прозрачность** - детальное отображение в UI
|
||||
✅ **Надежность** - протестированная математика
|
||||
✅ **Простота** - без дополнительных зависимостей (Redis)
|
||||
✅ **Масштабируемость** - готова к расширению при необходимости
|
||||
|
||||
Система готова к использованию в production!
|
||||
Reference in New Issue
Block a user