# Реализация динамической себестоимости товаров (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!