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

14 KiB
Raw Blame History

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

Пример использования:

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

Добавлено:

@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?

  1. Низкая частота изменений - себестоимость меняется только при поставках/списаниях
  2. Простота - меньше движущихся частей, легче дебажить
  3. Производительность - один SELECT вместо двух обращений (Redis + PostgreSQL)
  4. Транзакционность - гарантируется целостность данных
  5. Не требуется TTL - данные актуальны до изменения партий

Почему Django Signals?

  1. Автоматизация - не нужно помнить вызывать пересчет вручную
  2. Консистентность - гарантируется актуальность данных
  3. Прозрачность - изменения происходят автоматически
  4. Уже используется - в проекте активно применяются signals

Почему средневзвешенная, а не FIFO стоимость следующей партии?

  1. Более точная оценка - учитывает весь остаток на складе
  2. Актуальность для ценообразования - показывает реальную среднюю стоимость товара
  3. Стабильность - не скачет при каждой продаже
  4. Подходит для 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}")

Производительность

Оптимизации

  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!