Исправлены 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>
14 KiB
Реализация динамической себестоимости товаров (FIFO)
Обзор
Реализована система автоматического расчета себестоимости товаров на основе партий товара (StockBatch) с использованием средневзвешенного метода FIFO.
Основные принципы
Логика расчета
- Товар без партий →
cost_price = 0.00 - Товар с партиями →
cost_price = средневзвешенная стоимость - Товар закончился →
cost_price = 0.00 - Новая поставка →
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
Пример использования:
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- срабатывает при создании/изменении StockBatchupdate_product_cost_on_batch_delete- срабатывает при удалении StockBatch
Триггеры автообновления:
- Создание новой партии (поступление товара)
- Изменение количества в партии
- Изменение стоимости партии
- Удаление партии
3. Property в модели Product
Файл: myproject/products/models/products.py
Добавлено:
@property
def cost_price_details(self):
"""
Детали расчета себестоимости для отображения в UI.
Returns:
dict: {
'cached_cost': Decimal, # Кешированная себестоимость (из БД)
'calculated_cost': Decimal, # Рассчитанная себестоимость (из партий)
'is_synced': bool, # Совпадают ли значения
'total_quantity': Decimal, # Общее количество в партиях
'batches': [...] # Список партий с деталями
}
"""
Обновлено поле:
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
Использование:
# Пересчитать все товары
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
- ✅ Одна партия → стоимость партии
- ✅ Две партии одинаковой стоимости → та же стоимость
- ✅ Две партии разной стоимости → средневзвешенная
- ✅ Три партии с разным количеством → корректный расчет
- ✅ Жизненный цикл товара → корректные переходы
Запуск тестов:
python test_cost_calculator.py
Архитектурные решения
Почему кеширование в БД, а не Redis?
- Низкая частота изменений - себестоимость меняется только при поставках/списаниях
- Простота - меньше движущихся частей, легче дебажить
- Производительность - один SELECT вместо двух обращений (Redis + PostgreSQL)
- Транзакционность - гарантируется целостность данных
- Не требуется TTL - данные актуальны до изменения партий
Почему Django Signals?
- Автоматизация - не нужно помнить вызывать пересчет вручную
- Консистентность - гарантируется актуальность данных
- Прозрачность - изменения происходят автоматически
- Уже используется - в проекте активно применяются signals
Почему средневзвешенная, а не FIFO стоимость следующей партии?
- Более точная оценка - учитывает весь остаток на складе
- Актуальность для ценообразования - показывает реальную среднюю стоимость товара
- Стабильность - не скачет при каждой продаже
- Подходит для ProductKit - корректный расчет стоимости комплектов
Влияние на ProductKit
Расчет стоимости комплектов автоматически использует обновленную себестоимость компонентов:
# 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)
- Рассчитанная стоимость - актуальный расчет из партий
- Статус синхронизации - совпадают ли значения
Ручной пересчет
Если возникла рассинхронизация, можно запустить:
python manage.py recalculate_product_costs
Логирование
Все операции логируются в стандартный Django logger:
logger.info(f"Обновлена себестоимость товара {product.sku}: {old_cost} -> {new_cost}")
logger.error(f"Ошибка при расчете себестоимости для товара {product.sku}: {e}")
Производительность
Оптимизации
- Кеширование в БД - один запрос вместо пересчета каждый раз
- update_fields=['cost_price'] - обновляется только одно поле
- Selective signals - обновление только при реальных изменениях
- Bulk operations - management команда для массового пересчета
Нагрузка
- Чтение cost_price - 0 дополнительных запросов (из БД)
- Создание партии - 1 дополнительный UPDATE для товара
- Изменение партии - 1 дополнительный UPDATE для товара
- Удаление партии - 1 дополнительный UPDATE для товара
Дальнейшие улучшения (опционально)
Если появятся проблемы производительности:
- Отложенное обновление - помечать товары для пересчета и обрабатывать фоном
- Celery tasks - асинхронный пересчет в очереди
- Redis кеширование - для часто запрашиваемых деталей расчета
- Database triggers - перенести логику в PostgreSQL
Дополнительная функциональность:
- История изменений - логировать изменения себестоимости
- API endpoint - получение деталей расчета через REST API
- Alerts - уведомления при значительных изменениях стоимости
- Аналитика - графики изменения себестоимости во времени
Файлы изменений
Созданные файлы:
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_detailsmyproject/products/templates/products/product_detail.html- обновлен UI
Заключение
Реализована полнофункциональная система динамического расчета себестоимости товаров:
✅ Автоматическое обновление - через Django signals ✅ Производительность - кеширование в БД ✅ Прозрачность - детальное отображение в UI ✅ Надежность - протестированная математика ✅ Простота - без дополнительных зависимостей (Redis) ✅ Масштабируемость - готова к расширению при необходимости
Система готова к использованию в production!