Files
octopus/DYNAMIC_COST_PRICE_IMPLEMENTATION.md
Andrey Smakotin 6c8af5ab2c 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>
2025-11-02 19:04:03 +03:00

305 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Реализация динамической себестоимости товаров (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!