Исправлены 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>
305 lines
14 KiB
Markdown
305 lines
14 KiB
Markdown
# Реализация динамической себестоимости товаров (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!
|