diff --git a/DYNAMIC_COST_PRICE_IMPLEMENTATION.md b/DYNAMIC_COST_PRICE_IMPLEMENTATION.md deleted file mode 100644 index 2d97f73..0000000 --- a/DYNAMIC_COST_PRICE_IMPLEMENTATION.md +++ /dev/null @@ -1,304 +0,0 @@ -# Реализация динамической себестоимости товаров (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! diff --git a/FINAL_FIX_SUMMARY.md b/FINAL_FIX_SUMMARY.md deleted file mode 100644 index 60c6b76..0000000 --- a/FINAL_FIX_SUMMARY.md +++ /dev/null @@ -1,248 +0,0 @@ -# Финальное резюме исправления Race Condition при загрузке корректировки цены - -## Дата: 2025-11-02 -## Коммит: c7bf23c -## Статус: ✅ Готово к тестированию - ---- - -## Что было исправлено - -### Критическая проблема -Сохранённые значения корректировки цены не отображались надёжно на странице редактирования комплекта: -- **1/10 раз:** отображалось правильно ✅ -- **9/10 раз:** не отображалось вообще ❌ -- **Пользовательский отзыв:** "Такое ощущение, что оно отображается а потом затирается какой-то переинициализацией" - -### Корневая причина -**Race condition:** при установке значения в input-поле срабатывают события `input` и `change`, которые вызывают `calculateFinalPrice()`, которая перезаписывает скрытые поля со значениями по умолчанию, стирая загруженные значения. - -### Решение -Трёхуровневая защита от race condition: - -1. **Уровень 1: Подавление событий** (`isLoadingAdjustmentValues` флаг) - - Во время загрузки значений: флаг = true - - Event listeners видят флаг и пропускают обработку - - Предотвращает нежелательные вызовы calculateFinalPrice() - -2. **Уровень 2: Защита скрытых полей** (`isInitializing` флаг) - - calculateFinalPrice() проверяет `if (!isInitializing)` перед обновлением скрытых полей - - Даже если событие срабатит, скрытые поля не будут перезаписаны - -3. **Уровень 3: Синхронизация с браузером** (`requestAnimationFrame`) - - `isInitializing = false` устанавливается в конце frame cycle - - Гарантирует правильный порядок выполнения без угадывания timing'а - ---- - -## Файлы изменены - -### `productkit_edit.html` (3 основных изменения) - -**Строка 435:** Добавлен флаг подавления событий -```javascript -let isLoadingAdjustmentValues = false; // Флаг для подавления событий input/change -``` - -**Строки 683-700:** Event listeners защищены флагом -```javascript -input.addEventListener('input', () => { - if (isLoadingAdjustmentValues) { // ← Уровень 1 защиты - console.log('Skipping event during adjustment value loading'); - return; - } - validateSingleAdjustment(); - calculateFinalPrice(); -}); -``` - -**Строки 912-948:** Загрузка сохранённых значений с использованием флагов -```javascript -isLoadingAdjustmentValues = true; // Включаем подавление событий -// Загружаем значения (события подавляются) -increasePercentInput.value = currentAdjustmentValue; -// Вызываем валидацию вручную -validateSingleAdjustment(); -isLoadingAdjustmentValues = false; // Выключаем подавление событий - -// calculateFinalPrice с isInitializing = true (не перезапишет скрытые поля) -await calculateFinalPrice(); - -// requestAnimationFrame для надёжной синхронизации -requestAnimationFrame(() => { - requestAnimationFrame(() => { - isInitializing = false; - console.log('Initialization complete, isInitializing =', isInitializing); - }); -}); -``` - ---- - -## Как проверить исправление - -### Быстрая проверка (1 минута) -1. Открыть http://grach.localhost:8000/products/kits/4/update/ -2. Нажать Ctrl+F5 (очистить кэш) -3. Проверить что в блоке "НА СКОЛЬКО БЫЛА ИЗМЕНЕНА ЦЕНА" отображается "Увеличить на %: 10" -4. Нажать F5 ещё 5-10 раз -5. **Результат:** Должно отображаться каждый раз ✅ - -### Полная проверка (10 минут) -Смотрите документ **ADJUSTMENT_VALUE_FIX_TESTING.md** для полного плана тестирования - -### Проверка логирования в консоли -1. Открыть http://grach.localhost:8000/products/kits/4/update/ -2. Нажать F12 (DevTools) -3. Перейти в Console -4. Нажать Ctrl+F5 -5. **Ожидаемые логи:** - ``` - Loading saved adjustment values: {type: 'increase_percent', value: 10} - isLoadingAdjustmentValues = true, suppressing input/change events - Loaded increase_percent: 10 - isLoadingAdjustmentValues = false, events are enabled again - calculateFinalPrice: calculating... - [... логи расчётов ...] - Initialization complete, isInitializing = false - ``` - ---- - -## Преимущества решения - -| Аспект | Результат | -|--------|-----------| -| **Надёжность** | 99%+ вместо 10% | -| **Логирование** | Можно отследить порядок выполнения в консоли | -| **Поддерживаемость** | Понятный код с комментариями | -| **Производительность** | Нет влияния на производительность | -| **Масштабируемость** | Работает для всех 4 типов корректировки | -| **Отладка** | Логи помогают находить проблемы | - ---- - -## Технические детали - -### Принципы применённые -1. **Explicit Event Suppression** - явное подавление событий вместо угадывания -2. **Defense in Depth** - несколько уровней защиты вместо одного -3. **Browser Synchronization** - использование requestAnimationFrame вместо setTimeout -4. **Logging** - логирование для отладки и понимания потока выполнения - -### Порядок выполнения (с исправлением) -``` -Загрузка страницы (500ms задержка) - ↓ -isLoadingAdjustmentValues = true - ↓ -Загрузка значений в input-поля - ↓ -Events срабатывают но ПОДАВЛЯЮТСЯ флагом - ↓ -validateSingleAdjustment() вызывается вручную - ↓ -isLoadingAdjustmentValues = false - ↓ -calculateFinalPrice() с isInitializing = true - ↓ -requestAnimationFrame × 2 - ↓ -isInitializing = false - ↓ -✅ Готово: значения загружены, события работают, скрытые поля защищены -``` - ---- - -## Что дальше - -### Для пользователя -1. Протестировать исправление согласно ADJUSTMENT_VALUE_FIX_TESTING.md -2. Проверить что значения отображаются 10/10 раз вместо 1/10 -3. Попробовать редактировать комплекты с разными типами корректировки -4. Проверить консоль браузера для понимания порядка выполнения - -### Для разработчика (если потребуется) -Если появятся проблемы: -1. Проверить логи в консоли (F12 → Console) -2. Убедиться что git коммит c7bf23c развернут -3. Очистить браузерный кэш (Ctrl+Shift+Delete) -4. Нажать Ctrl+F5 на странице редактирования -5. Проверить что файл productkit_edit.html содержит все изменения - ---- - -## Файлы документации - -### Основные -- **ADJUSTMENT_VALUE_FIX_TESTING.md** - План тестирования и проверки -- **TECHNICAL_RACE_CONDITION_FIX.md** - Глубокий технический анализ - -### Справочные -- **SESSION_SUMMARY.md** - Резюме всей сессии работ -- **IMPROVEMENTS_SUMMARY.md** - Обзор всех улучшений системы ценообразования -- **KIT_PRICING_SYSTEM_READY.md** - Архитектура всей системы - ---- - -## Интеграция с существующей системой - -### Совместимость с другими компонентами -- ✅ `validateSingleAdjustment()` - работает как прежде -- ✅ `calculateFinalPrice()` - с добавленной защитой скрытых полей -- ✅ Event listeners на продукты - не затронуты -- ✅ Event listeners на количество - не затронуты -- ✅ Select2 интеграция - не затронута - -### Зависимости -- Требуется JavaScript ES6+ (async/await, requestAnimationFrame) -- Браузеры: Chrome, Firefox, Safari, Edge (все современные версии) - ---- - -## Коммит информация - -``` -Commit: c7bf23c -Author: Claude -Date: 2025-11-02 - -fix: Улучшить загрузку сохранённых значений корректировки цены на странице редактирования - -Исправлена критическая проблема, когда сохранённые значения корректировки цены -не отображались надёжно на странице редактирования (отображались только в 1 из 10 случаев). - -Решение: Трёхуровневая защита от race condition -1. Подавление событий input/change флагом isLoadingAdjustmentValues -2. Защита скрытых полей флагом isInitializing в calculateFinalPrice() -3. Синхронизация с браузером через requestAnimationFrame - -Файлы изменены: productkit_edit.html -- Добавлена логика подавления событий -- Расширена обработка загрузки сохранённых значений -- Добавлено логирование для отладки -``` - ---- - -## Заключение - -Исправление использует проверенные техники синхронизации в JavaScript для полного устранения race condition при загрузке сохранённых значений корректировки цены. - -**Результат:** надёжность возросла с 10% до 99%+ - -🎉 **Готово к использованию!** - ---- - -## Контакты для вопросов - -Если что-то не работает как ожидается: -1. Проверьте консоль браузера (F12 → Console) -2. Убедитесь что коммит c7bf23c есть в git -3. Очистите кэш браузера (Ctrl+Shift+Delete) -4. Нажмите Ctrl+F5 на странице редактирования -5. Проверьте что используется правильный тенант ("grach") - -Все логи и диагностическая информация выводится в консоль браузера для удобства отладки. diff --git a/FINAL_REPORT_FIXES.md b/FINAL_REPORT_FIXES.md deleted file mode 100644 index 69a99ab..0000000 --- a/FINAL_REPORT_FIXES.md +++ /dev/null @@ -1,334 +0,0 @@ -# Отчет об исправлениях системы динамического ценообразования комплектов - -## Дата: 2025-11-02 -## Статус: ✅ Готово к тестированию - ---- - -## Проблема 1: Первая строка не считается в цену - -### Описание -При добавлении первого товара в комплект цена не обновлялась. Цена начинала считаться только со второго товара. - -### Решение - -**Файл:** `products/templates/products/productkit_create.html` -**Файл:** `products/templates/products/productkit_edit.html` - -1. **Улучшена функция `getProductPrice()`:** - - Добавлена строгая проверка валидности selectElement - - Добавлена проверка на isNaN и productId <= 0 - - Добавлено консольное логирование для отладки - -2. **Улучшена функция `calculateFinalPrice()`:** - - Добавлена проверка что товар выбран (`if (!productSelect || !productSelect.value) continue`) - - Добавлена валидация количества (если quantity <= 0, использует 1) - - Добавлена проверка что цена > 0 перед добавлением в сумму - -### Результат -✅ Первая строка теперь корректно считается в цену -✅ Цена обновляется в реальном времени при добавлении товара - ---- - -## Проблема 2: Select2 отображает цену без скидки - -### Описание -При поиске товаров в Select2 отображалась обычная цена (`price`), а не цена со скидкой (`actual_price`). - -### Решение - -**Файл:** `products/templates/products/includes/select2-product-init.html` - -Обновлена функция `formatSelectResult()`: -```javascript -function formatSelectResult(item) { - if (item.loading) return item.text; - var $container = $('
'); - $container.text(item.text); - - // Отображаем actual_price (цену со скидкой если она есть), иначе обычную цену - var displayPrice = item.actual_price || item.price; - if (displayPrice) { - $container.append($('
').text(displayPrice + ' руб.')); - } - return $container; -} -``` - -### Результат -✅ Select2 теперь отображает actual_price (цену со скидкой) -✅ Это исправление касается обоих случаев: поиск и список по умолчанию - ---- - -## Архитектура решения - Полный обзор - -### Модель данных (ProductKit) - -```python -class ProductKit(BaseProductEntity): - base_price = DecimalField() # Сумма actual_price компонентов - price = DecimalField() # Итоговая цена (база + корректировка) - price_adjustment_type = CharField() # 'none', 'increase_percent', 'increase_amount', 'decrease_percent', 'decrease_amount' - price_adjustment_value = DecimalField() # Значение корректировки - - def calculate_final_price(self): - """Рассчитывает финальную цену с корректировкой""" - if self.price_adjustment_type == 'none': - return self.base_price - - adjustment_value = self.price_adjustment_value or Decimal('0') - - if 'percent' in self.price_adjustment_type: - adjustment = self.base_price * adjustment_value / Decimal('100') - else: # 'amount' - adjustment = adjustment_value - - if 'increase' in self.price_adjustment_type: - return self.base_price + adjustment - else: # 'decrease' - return max(Decimal('0'), self.base_price - adjustment) -``` - -### Поток данных - -``` -Пользователь выбирает товар - ↓ -Select2 запрашивает API (/api/search-products-variants/?id=X) - ↓ -API возвращает JSON с actual_price - ↓ -getProductPrice() кэширует цену в priceCache - ↓ -calculateFinalPrice() вызывается - ↓ -Суммирует actual_price × quantity для всех компонентов - ↓ -Вычисляет корректировку (автоопределение типа) - ↓ -Обновляет basePriceDisplay и finalPriceDisplay в реальном времени - ↓ -При сохранении отправляет в БД: - - price_adjustment_type - - price_adjustment_value - - calculated price -``` - -### JavaScript логика - -#### getProductPrice(selectElement) -Получает цену товара с приоритизацией: -1. Кэш (самое быстро) -2. data-product-price атрибут на форме -3. Select2 option data attributes -4. AJAX запрос к API - -**Логирование:** -```javascript -console.log('getProductPrice: from cache', productId, cachedPrice); -console.log('getProductPrice: from form data', productId, price); -console.log('getProductPrice: from select2 data', productId, price); -console.log('getProductPrice: fetching from API', productId); -console.log('getProductPrice: from API', productId, price); -console.warn('getProductPrice: returning 0 for product', productId); -``` - -#### calculateFinalPrice() -Асинхронная функция которая: -1. Получает все формы компонентов -2. Для каждой формы: - - Проверяет выбран ли товар - - Получает quantity (или 1 если не задана) - - Ждёт `await getProductPrice()` - - Суммирует actual_price × quantity -3. Автоматически определяет тип корректировки: - - Проверяет какое ОДНО из 4 полей заполнено - - Устанавливает price_adjustment_type - - Устанавливает price_adjustment_value -4. Рассчитывает финальную цену -5. Обновляет display элементы - -### API Endpoint - -**URL:** `/products/api/search-products-variants/` - -**Параметры:** -- `q` - поисковая строка -- `id` - ID товара для получения его данных -- `type` - 'product' или 'variant' -- `page` - номер страницы - -**Response:** -```json -{ - "results": [ - { - "id": 1, - "text": "Роза красная (PROD-000001)", - "sku": "PROD-000001", - "price": "50.00", - "actual_price": "20.00", - "in_stock": true, - "type": "product" - } - ], - "pagination": {"more": false} -} -``` - -### Django Signal для автоматического пересчёта - -**Файл:** `inventory/signals.py` - -```python -@receiver(post_save, sender='products.Product') -def update_kit_prices_on_product_change(sender, instance, created, **kwargs): - """Пересчитывает все комплекты когда меняется цена товара""" - if created: - return - - kit_items = KitItem.objects.filter(product=instance) - kits_to_update = set(item.kit_id for item in kit_items) - - for kit_id in kits_to_update: - kit = ProductKit.objects.get(id=kit_id) - kit.recalculate_base_price() -``` - ---- - -## Файлы изменены - -| Файл | Изменение | Версия | -|------|-----------|--------| -| `products/models/kits.py` | Полная переработка модели ценообразования | ✅ | -| `products/forms.py` | Упрощена, удалены старые поля ценообразования | ✅ | -| `products/views/api_views.py` | Добавлен actual_price во все responses | ✅ | -| `products/views/productkit_views.py` | Добавлен actual_price в context | ✅ | -| `products/templates/productkit_create.html` | Переработан UI + исправлены логика getProductPrice + calculateFinalPrice | ✅ | -| `products/templates/productkit_edit.html` | То же + загрузка сохранённых значений | ✅ | -| `products/templates/includes/kititem_formset.html` | Добавлены data-product-price атрибуты | ✅ | -| `products/templates/includes/select2-product-init.html` | Обновлено отображение actual_price вместо price | ✅ | -| `inventory/signals.py` | Добавлен signal для автоматического пересчёта | ✅ | -| `products/migrations/0004_add_kit_price_adjustment_fields.py` | Migration для новых полей | ✅ | - ---- - -## Тестовые сценарии - -### Сценарий 1: Создание простого комплекта ✅ - -``` -1. Перейти на http://grach.localhost:8000/products/kits/create/ -2. Заполнить название: "Букет из 3 роз" -3. Добавить товар "Роза красная" (qty: 3) - ✓ base_price должна быть 60.00 (20.00 × 3) -4. Увеличить на 10% - ✓ final_price должна быть 66.00 (60 × 1.10) -5. Сохранить - ✓ Комплект должен быть создан с price = 66.00 -``` - -### Сценарий 2: Увеличение суммой ✅ - -``` -1. Создать комплект с товарами на сумму 50 руб -2. В поле "Увеличить на руб" ввести 10 - ✓ final_price должна быть 60.00 - ✓ price_adjustment_type = 'increase_amount' - ✓ price_adjustment_value = 10 -``` - -### Сценарий 3: Уменьшение ✅ - -``` -1. Создать комплект базовой ценой 100 руб -2. Уменьшить на 20% - ✓ final_price = 80.00 - ✓ price_adjustment_type = 'decrease_percent' -3. Или уменьшить на 15 руб - ✓ final_price = 85.00 - ✓ price_adjustment_type = 'decrease_amount' -``` - -### Сценарий 4: Редактирование ✅ - -``` -1. Создать комплект с увеличением на 10% -2. Открыть для редактирования - ✓ Значение 10 должно быть загружено в "Увеличить на %" -3. Изменить на 15% - ✓ final_price пересчитывается в реальном времени -4. Сохранить -``` - -### Сценарий 5: Отображение цены в Select2 ✅ - -``` -1. На форме создания комплекта в поле выбора товара начать вводить "роз" - ✓ В dropdown должны отображаться товары с actual_price (20.00, а не 50.00) -2. При наведении на товар видна цена со скидкой -3. При выборе товара берется actual_price для расчёта -``` - ---- - -## Отладка - -### Просмотр логов в консоли браузера (F12) - -```javascript -// При выборе товара должны видеть: -getProductPrice: fetching from API 1 -getProductPrice: from API 1 20.00 - -// При расчёте цены: -// (логирование каждого товара из calculateFinalPrice) -``` - -### Проверка данных в Network tab - -``` -GET /products/api/search-products-variants/?id=1 -Response: { - "results": [{ - "id": 1, - "actual_price": "20.00", - ... - }] -} -``` - ---- - -## Статус готовности - -| Компонент | Статус | Комментарий | -|-----------|--------|------------| -| Модель ProductKit | ✅ | Применена миграция 0004 | -| API endpoint | ✅ | Возвращает actual_price | -| Select2 форматирование | ✅ | Отображает actual_price | -| Real-time расчёты | ✅ | Все товары считаются корректно | -| Сохранение данных | ✅ | price_adjustment_type и value сохраняются | -| Редактирование | ✅ | Загружаются сохранённые значения | -| Django signal | ✅ | Готов автоматически пересчитывать | -| Документация | ✅ | Полная | - ---- - -## Готово к тестированию! 🎉 - -Система полностью переработана и готова к использованию. - -**URL для тестирования:** -- Создание: http://grach.localhost:8000/products/kits/create/ -- Редактирование: http://grach.localhost:8000/products/kits/ -- API: http://grach.localhost:8000/products/api/search-products-variants/?q=роз - -**Тестовые товары в тенанте "grach":** -1. Роза красная - price: 50.00, sale: 20.00, actual: 20.00 ✓ -2. Белая роза - price: 5.00, actual: 5.00 ✓ -3. Ваниль гибискус - price: 6.00, actual: 6.00 ✓ -4. Хризантема оранжевая - price: 5.00, actual: 5.00 ✓ diff --git a/FINAL_SESSION_REPORT.md b/FINAL_SESSION_REPORT.md deleted file mode 100644 index 9d90de0..0000000 --- a/FINAL_SESSION_REPORT.md +++ /dev/null @@ -1,230 +0,0 @@ -# Финальный отчет - Полная система ценообразования комплектов - -**Дата:** 2025-11-02 -**Статус:** ✅ Полностью готово к использованию -**Коммитов в сессии:** 10 - ---- - -## 📋 Все исправления в одном месте - -### ✅ Исправление 1: Расчёт цены первого товара -**Проблема:** При добавлении первого товара в комплект цена не обновлялась. -**Решение:** Улучшена валидация в `getProductPrice()` и `calculateFinalPrice()`. -**Файлы:** productkit_create.html, productkit_edit.html -**Коммит:** 6c8af5a - -### ✅ Исправление 2: Отображение цены в Select2 -**Проблема:** Select2 показывал обычную цену вместо цены со скидкой. -**Решение:** Обновлена функция `formatSelectResult()` для приоритета `actual_price > price`. -**Файл:** select2-product-init.html -**Коммит:** 6c8af5a - -### ✅ Исправление 3: Количество по умолчанию -**Проблема:** При создании первое поле количества было пусто, второе имело 1. -**Решение:** Добавлен `__init__` в `KitItemForm` с `quantity.initial = 1`. -**Файл:** forms.py -**Коммит:** 6c8af5a - -### ✅ Исправление 4: Auto-select при клике -**Проблема:** Нужно было вручную выделять число перед редактированием. -**Решение:** Добавлен `focus` handler с `this.select()`. -**Файлы:** productkit_create.html, productkit_edit.html -**Коммит:** 6c8af5a - -### ✅ Исправление 5: Отображение цены в списке -**Проблема:** В таблице productkit_list не было красивого отображения цены. -**Решение:** Добавлено отображение со скидкой (зачёркнутая + красная + "Акция"). -**Файл:** productkit_list.html -**Коммит:** 2e305a8 - -### ✅ Исправление 6: Валидация одного поля корректировки -**Проблема:** Можно было заполнить несколько полей одновременно. -**Решение:** Добавлена функция `validateSingleAdjustment()` которая: -- Отключает остальные поля когда одно заполнено -- Помечает как invalid если несколько заполнено -- Очищает лишние и оставляет первое - -**Файлы:** productkit_create.html, productkit_edit.html, forms.py -**Коммит:** 390d547 - -### ✅ Исправление 7: Сохранение комплекта -**Проблема:** Выскакивала ошибка 'ProductKit' object has no attribute 'cost_calculation_info'. -**Решение:** Удален вызов старого валидатора `validate_pricing_method_availability()`. -**Файл:** productkit_views.py -**Коммит:** 045f6a4 - -### ✅ Исправление 8: Отображение detail страницы -**Проблема:** На странице деталей комплекта показывались старые поля (calculated_price, get_pricing_method_display и т.д.). -**Решение:** Обновлен шаблон для новой системы: -- Базовая цена (base_price) -- Итоговая цена (price) -- Скидка (sale_price) -- Корректировка (type + value) - -**Файл:** productkit_detail.html -**Коммит:** 3c62cce - -### ✅ Исправление 9: Пересчёт базовой цены после сохранения -**Проблема:** После сохранения комплекта base_price и price показывали 0.00. -**Решение:** Добавлен вызов `recalculate_base_price()` после сохранения компонентов. -**Файл:** productkit_views.py (CreateView и UpdateView) -**Коммит:** 3c62cce - -### ✅ Исправление 10: Загрузка сохранённых значений при редактировании -**Проблема:** При редактировании комплекта поля корректировки были пусты. -**Решение:** -- Добавлен вызов `validateSingleAdjustment()` после заполнения полей -- Заполнены скрытые поля значениями из БД через `{{ form.FIELD.value }}` -- Добавлено логирование для отладки - -**Файлы:** productkit_edit.html (2 исправления) -**Коммиты:** 3c62cce, c228f80 - ---- - -## 📊 Архитектура решения - -### Поток создания комплекта: - -``` -1. Пользователь вводит название и выбирает товары - ↓ -2. Для каждого товара JavaScript получает actual_price (async) - ↓ -3. calculateFinalPrice() суммирует actual_price × quantity - ↓ -4. Пользователь заполняет ОДНО поле корректировки (%, руб, +/-) - ↓ -5. validateSingleAdjustment() отключает остальные 3 поля - ↓ -6. Финальная цена = base_price +/- корректировка обновляется в реальном времени - ↓ -7. При сохранении: - - Сохраняется комплект (form.save()) - - Сохраняются компоненты (formset.save()) - - Пересчитывается base_price из компонентов - - Рассчитывается price с корректировкой - - Сохраняются: price_adjustment_type, price_adjustment_value -``` - -### Поток редактирования комплекта: - -``` -1. Загружается форма с существующим комплектом - ↓ -2. Скрытые поля заполняются значениями из БД: - - id_price_adjustment_type = 'increase_percent' (например) - - id_price_adjustment_value = 10.00 - ↓ -3. JavaScript загружает эти значения после загрузки страницы (setTimeout) - ↓ -4. Заполняет соответствующее поле (например, increasePercentInput.value = 10) - ↓ -5. Вызывает validateSingleAdjustment(): - - Отключает остальные 3 поля - - Помечает текущее как активное - ↓ -6. calculateFinalPrice() пересчитывает цену - ↓ -7. При сохранении: то же как при создании -``` - ---- - -## 🎯 Результат - -### Что работает: - -✅ **Создание комплектов:** -- Базовая цена вычисляется из actual_price компонентов -- Можно применить корректировку (+/- % или +/- руб) -- Только одно поле корректировки заполняется за раз -- Финальная цена обновляется в реальном времени -- Можно установить sale_price для скидки - -✅ **Редактирование комплектов:** -- Все сохранённые значения загружаются -- Поле корректировки заполняется корректно -- Остальные поля отключены -- Можно изменить корректировку -- Финальная цена пересчитывается - -✅ **Отображение цен:** -- В списке кits: красиво (зачёркнутая + красная + "Акция") -- На detail странице: базовая + итоговая + корректировка -- В Select2: actual_price вместо обычной цены - -✅ **Валидация:** -- Frontend: мгновенная (real-time) -- Backend: при сохранении -- Одно поле корректировки одновременно - ---- - -## 📁 Измененные файлы - -| Файл | Изменения | -|------|-----------| -| `products/models/kits.py` | ✅ Новая модель ценообразования (отдельная сессия) | -| `products/forms.py` | ✅ Добавлен `__init__` для quantity.initial = 1 | -| `products/views/api_views.py` | ✅ Добавлен actual_price в JSON responses | -| `products/views/productkit_views.py` | ✅ Удален старый валидатор + добавлен recalculate_base_price() | -| `products/templates/includes/select2-product-init.html` | ✅ Обновлена formatSelectResult для actual_price | -| `products/templates/products/productkit_list.html` | ✅ Красивое отображение цены | -| `products/templates/products/productkit_detail.html` | ✅ Обновлен для новой системы | -| `products/templates/products/productkit_create.html` | ✅ Функция validateSingleAdjustment + улучшения | -| `products/templates/products/productkit_edit.html` | ✅ То же + загрузка сохранённых значений + заполнение скрытых полей | - ---- - -## 🔗 Git коммиты (в хронологическом порядке) - -``` -6c8af5a - fix: Улучшения системы ценообразования комплектов -2e305a8 - fix: Улучшить отображение цены в списке комплектов -9027cca - docs: Добавить финальное резюме сессии улучшений -390d547 - feat: Добавить валидацию для заполнения одного поля корректировки цены -045f6a4 - fix: Удалить вызов старого валидатора ценообразования -3c62cce - fix: Загружать сохранённые значения корректировки цены при редактировании -c228f80 - fix: Заполнять скрытые поля корректировки значениями из БД при редактировании -``` - ---- - -## 🎓 Ключевые моменты - -### Что сложного было решить: - -1. **Race conditions при загрузке async price** - - Решение: кэширование + await в calculateFinalPrice - -2. **Валидация одного поля** - - Решение: validateSingleAdjustment() с отключением и проверкой - -3. **Загрузка сохранённых значений** - - Решение: setTimeout + проверка скрытых полей формы - -4. **Пересчёт базовой цены** - - Решение: recalculate_base_price() после сохранения компонентов - -### Что хорошо работает: - -1. ✅ Real-time price calculation -2. ✅ Auto-detection adjustment type (какое поле заполнено) -3. ✅ Автоматическое отключение других полей -4. ✅ Загрузка сохранённых значений -5. ✅ Красивое отображение цен везде -6. ✅ Двойная валидация (JS + backend) - ---- - -## 🚀 Готово к использованию! - -Все функции работают корректно. Система полностью функциональна и готова к работе в production. - -**Точки входа для тестирования:** -- Создание: http://grach.localhost:8000/products/kits/create/ -- Список: http://grach.localhost:8000/products/kits/ -- Редактирование: http://grach.localhost:8000/products/kits/4/update/ -- Детали: http://grach.localhost:8000/products/kits/4/ diff --git a/TESTING_REPORT.md b/TESTING_REPORT.md deleted file mode 100644 index 0f8d22d..0000000 --- a/TESTING_REPORT.md +++ /dev/null @@ -1,104 +0,0 @@ -# Тестирование системы инвентаризации - -## Статус: ✅ ВСЕ ТЕСТЫ ПРОЙДЕНЫ - -### 1. FIFO Логика ✅ - -**Тест:** Создание 3 приходов с разными ценами и списание через FIFO - -**Результат:** -- ✅ StockBatches создаются автоматически при Incoming (сигнал) -- ✅ Продажа автоматически применяет FIFO (сигнал) -- ✅ Распределение по партиям корректно: старые партии списываются первыми -- ✅ SaleBatchAllocation корректно отслеживают распределение - -**Примеры:** -``` -Приход 1: 10 шт @ 100 -Приход 2: 15 шт @ 120 -Приход 3: 20 шт @ 150 - -Продажа 1: 18 шт @ 250 - → Allocation: 10 (от batch1) + 8 (от batch2) - -Продажа 2: 20 шт @ 250 - → Allocation: 7 (от batch2) + 13 (от batch3) -``` - -### 2. Сигналы на заказы ✅ - -**Тест:** Создание заказа, изменение статуса на "in_delivery" - -**Результат:** -- ✅ Заказ создан и содержит товар -- ✅ При смене статуса на "in_delivery" автоматически создается Sale -- ✅ Sale автоматически обрабатывается с FIFO -- ✅ SaleBatchAllocation создаются автоматически - -### 3. Инвентаризация (Reconciliation) ✅ - -**Тест:** Физический подсчёт с дефицитом - -**Результат:** -- ✅ Inventory создаётся и принимает InventoryLines -- ✅ При завершении Inventory (status='completed') автоматически обрабатывается (сигнал) -- ✅ Дефицит (разница < 0) автоматически создает WriteOff по FIFO -- ✅ Stock коррегируется для совпадения с физическим подсчётом - -**Пример:** -``` -Система имела: 7 шт -Физический подсчёт: 5 шт -Разница: -2 шт - -Результат: - → WriteOff создан на 2 шт - → Stock уменьшен до 5 шт -``` - -### 4. Django Admin ✅ - -**Проверено:** -- ✅ StockBatchAdmin показывает quantity с цветовой кодировкой: - - Красный (≤0) - - Оранжевый (<10) - - Зелёный (≥10) -- ✅ SaleAdmin показывает inline SaleBatchAllocations -- ✅ Статус обработки Sale отображается с визуальным индикатором -- ✅ WriteOffAdmin показывает причины списания - -## Архитектура - -### Модели -- **StockBatch**: Партия товара с FIFO датой создания -- **Incoming**: Приход → автоматически создает StockBatch (сигнал) -- **Sale**: Продажа → автоматически применяет FIFO (сигнал) -- **SaleBatchAllocation**: Отслеживание какие батчи использованы в продаже -- **Inventory/InventoryLine**: Физический подсчёт -- **WriteOff**: Списание при дефиците (автоматическое по FIFO) -- **Reservation**: Резервирование при создании заказа - -### Сигналы (Автоматизация) -1. **create_stock_batch_on_incoming**: Создает StockBatch при Incoming -2. **process_sale_fifo**: Применяет FIFO при создании Sale -3. **create_sale_on_order_shipment**: Создает Sale при смене статуса заказа на "in_delivery" -4. **reserve_stock_on_order_create**: Резервирует товар при создании заказа -5. **process_inventory_reconciliation**: Обрабатывает инвентаризацию при завершении - -## Исправленные ошибки - -1. **Двойной вызов сигнала на Sale**: Использован `update()` вместо `save()` чтобы избежать повторного срабатывания -2. **WriteOff не создавался при инвентаризации**: Добавлен сигнал на Inventory -3. **StockBatch не создавались при Incoming**: Добавлен сигнал на Incoming - -## Готовность к продакшену - -✅ FIFO логика работает корректно -✅ Все основные операции автоматизированы через сигналы -✅ Admin интерфейс полностью функционален -✅ Данные консистентны между таблицами -✅ Система многотенантная (работает в рамках каждого тенанта) - ---- - -Дата тестирования: 2025-10-27 diff --git a/TESTING_WRITEOFF_IMPROVEMENTS.md b/TESTING_WRITEOFF_IMPROVEMENTS.md deleted file mode 100644 index c54179a..0000000 --- a/TESTING_WRITEOFF_IMPROVEMENTS.md +++ /dev/null @@ -1,228 +0,0 @@ -# Тестирование улучшений WriteOff UI - -## Краткая инструкция тестирования - -### Подготовка -1. Убедитесь что у вас есть актуальные данные: - - Склад (Warehouse) - - Товар (Product) - - Партия товара (StockBatch) с остатком > 0 - -2. Откройте страницу: `http://grach.localhost:8000/inventory/writeoffs/create/` - ---- - -## Тест 1: Информация об остатке партии - -**Шаги:** -1. Выберите партию из dropdown "Партия" -2. **Ожидаемый результат:** - - Под dropdown появляется серый блок: - ``` - 📋 Остаток в партии: X шт - ``` - - Где X это количество в выбранной партии - -**Проверка:** -- ✅ Блок информации появляется при выборе -- ✅ Блок исчезает если выбрать пустое значение -- ✅ Количество обновляется при смене партии - ---- - -## Тест 2: Реал-тайм предупреждение при превышении - -**Шаги:** -1. Выберите партию (например, с остатком 5 шт) -2. Введите в поле "Количество" число МЕНЬШЕ остатка (например, 3) -3. **Ожидаемый результат:** Никаких предупреждений - -**Шаги 2:** -1. Введите в поле "Количество" число БОЛЬШЕ остатка (например, 10) -2. **Ожидаемый результат:** - ``` - ⚠️ Внимание! Вы пытаетесь списать 10 шт, - а в партии только 5 шт. - Недостаток: 5 шт. - ``` - -**Проверка:** -- ✅ Предупреждение НЕ появляется при quantity <= batch.quantity -- ✅ Предупреждение ПОЯВЛЯЕТСЯ при quantity > batch.quantity -- ✅ Предупреждение обновляется в реал-тайме при каждом вводе -- ✅ Правильно рассчитывается недостаток (shortage) - ---- - -## Тест 3: Отправка валидных данных - -**Шаги:** -1. Выберите партию (остаток = 5 шт) -2. Введите количество = 3 (меньше остатка) -3. Выберите причину (например, "damage") -4. Нажмите кнопку "Сохранить" - -**Ожидаемый результат:** -- ✅ Форма отправляется успешно -- ✅ Перенаправляется на список списаний -- ✅ Появляется зеленое сообщение об успехе -- ✅ Остаток в партии уменьшился на 3 (стал 2) - ---- - -## Тест 4: Отправка невалидных данных (количество > остатка) - -**Шаги:** -1. Выберите партию (остаток = 5 шт) -2. Введите количество = 10 (больше остатка) -3. Нажмите кнопку "Сохранить" - -**Ожидаемый результат:** -- ✅ Форма НЕ отправляется -- ✅ На странице появляется красный alert: - ``` - ❌ Ошибка: - Невозможно списать 10 шт из партии, - где только 5 шт. - Недостаток: 5 шт. - ``` -- ✅ Пользователь остается на форме -- ✅ Данные в полях сохраняются (он может отредактировать) -- ✅ Остаток в партии не изменился (остался 5) - ---- - -## Тест 5: Отключенный JavaScript (браузерная консоль) - -Если JavaScript отключен: - -**Шаги:** -1. Откройте DevTools (F12) -2. Отключите JavaScript -3. Обновите страницу -4. Заполните форму с quantity > batch.quantity -5. Нажмите "Сохранить" - -**Ожидаемый результат:** -- ✅ Реал-тайм валидация не работает (это ок - это просто nice-to-have) -- ✅ Форма отправляется на сервер -- ✅ Django form validation ловит ошибку -- ✅ На странице появляется красный alert с ошибкой -- ✅ Остаток в партии не изменился (данные защищены) - ---- - -## Тест 6: Граничные случаи - -### Граничный случай 1: Количество = остатку -**Шаги:** -1. Партия имеет остаток = 5 шт -2. Введите количество = 5 -3. **Ожидаемый результат:** - - ✅ Никаких предупреждений (это валидное значение) - - ✅ Форма отправляется успешно - - ✅ Партия полностью списывается (остаток = 0) - - ✅ Партия деактивируется (is_active = False) - -### Граничный случай 2: Очень малое количество -**Шаги:** -1. Введите количество = 0.001 (если поддерживается) -2. **Ожидаемый результат:** - - ✅ Форма отправляется - - ✅ Остаток уменьшается на 0.001 - - ✅ Все работает корректно - -### Граничный случай 3: Дробные числа -**Шаги:** -1. Партия имеет остаток = 5.5 шт -2. Введите количество = 5.6 -3. **Ожидаемый результат:** - - ✅ Появляется предупреждение о превышении - - ✅ Недостаток = 0.1 шт (правильный расчет) - ---- - -## Проверочный лист - -### Общие проверки -- [ ] JavaScript выполняется без ошибок (проверить консоль F12) -- [ ] Стили Bootstrap применены правильно -- [ ] Все иконки (⚠️, ❌, 📋) отображаются корректно -- [ ] Alert блоки имеют правильные цвета (красный для ошибок, желтый для warning) -- [ ] Кнопка закрытия alert работает - -### Функциональные проверки -- [ ] Информация об остатке появляется при выборе партии -- [ ] Предупреждение появляется при quantity > batch.quantity -- [ ] Ошибка валидации отображается красиво -- [ ] Форма не отправляется при невалидных данных -- [ ] При валидных данных форма отправляется и остаток уменьшается - -### Граничные случаи -- [ ] Количество = остатку работает правильно -- [ ] Дробные количества поддерживаются -- [ ] Партия деактивируется когда количество <= 0 -- [ ] JavaScript отключение не ломает functionality (только UX) - ---- - -## Логирование для отладки - -Если что-то не работает, проверьте: - -1. **Консоль браузера (F12 → Console)** - - Ошибки JavaScript - - Логи из скрипта - -2. **Django логи** - - Ошибки валидации - - Ошибки в сохранении модели - -3. **Network tab (F12 → Network)** - - Проверить POST запросы - - Ответ сервера (200, 400, 500 и т.д.) - ---- - -## Быстрая проверка - -Самый быстрый способ проверить что все работает: - -```bash -# 1. Откройте форму -# http://grach.localhost:8000/inventory/writeoffs/create/ - -# 2. Выберите партию - должна появиться информация об остатке - -# 3. Введите количество больше остатка - должно появиться предупреждение - -# 4. Нажмите сохранить - должна появиться ошибка - -# 5. Поправьте количество на меньшее - предупреждение исчезнет - -# 6. Нажмите сохранить - форма отправится успешно -``` - ---- - -## Известные ограничения - -1. **Форматирование дробных чисел:** JavaScript использует `.toFixed(3)` для недостатка, может быть 0.100 вместо 0.1 - - Это не критично, просто визуально - -2. **Локализация запятой/точки:** Код поддерживает оба варианта в regex но может быть проблемы с разными локалями - - Проверьте что в вашей локали правильно парсятся дробные числа - -3. **Старые браузеры:** Код использует `addEventListener` и `querySelector` - не поддерживается IE11 - - Но IE11 давно не поддерживается, это нормально - ---- - -## Что дальше? - -Если тестирование прошло успешно: -1. ✅ Все остатки партий уменьшаются правильно -2. ✅ Никакие отрицательные значения не создаются -3. ✅ UI четкий и понятный - -То система готова к использованию! diff --git a/test_api.sh b/test_api.sh deleted file mode 100644 index 787e081..0000000 --- a/test_api.sh +++ /dev/null @@ -1,47 +0,0 @@ -#!/bin/bash - -# Тест API endpoints для поиска и создания клиентов -# Использование: bash test_api.sh - -BASE_URL="http://grach.localhost:8000" - -echo "==================================================" -echo "ТЕСТ API ENDPOINTS ДЛЯ ПОИСКА КЛИЕНТОВ" -echo "==================================================" -echo "" - -# Test 1: Поиск по имени -echo "TEST 1: Поиск по имени (q=Иван)" -echo "URL: $BASE_URL/customers/api/search/?q=Иван" -echo "" -curl -s "$BASE_URL/customers/api/search/?q=Иван" | python -m json.tool 2>/dev/null || curl -s "$BASE_URL/customers/api/search/?q=Иван" -echo "" -echo "---" -echo "" - -# Test 2: Поиск по телефону -echo "TEST 2: Поиск по телефону (q=375)" -echo "URL: $BASE_URL/customers/api/search/?q=375" -echo "" -curl -s "$BASE_URL/customers/api/search/?q=375" | python -m json.tool 2>/dev/null || curl -s "$BASE_URL/customers/api/search/?q=375" -echo "" -echo "---" -echo "" - -# Test 3: Пустой поиск (должна вернуться пустая строка results) -echo "TEST 3: Пустой поиск (q=)" -echo "URL: $BASE_URL/customers/api/search/?q=" -echo "" -curl -s "$BASE_URL/customers/api/search/?q=" | python -m json.tool 2>/dev/null || curl -s "$BASE_URL/customers/api/search/?q=" -echo "" -echo "---" -echo "" - -# Test 4: Проверка что endpoint существует -echo "TEST 4: Проверка доступности endpoint'а" -echo "URL: $BASE_URL/customers/api/search/" -echo "" -curl -i "$BASE_URL/customers/api/search/?q=test" 2>&1 | head -15 -echo "" -echo "==================================================" -echo "" diff --git a/test_api_simple.py b/test_api_simple.py deleted file mode 100644 index 70ab4b3..0000000 --- a/test_api_simple.py +++ /dev/null @@ -1,52 +0,0 @@ -#!/usr/bin/env python -""" -Простой скрипт для проверки API endpoints через Django shell -""" -import os -import django - -# Настройка Django -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'myproject.settings') -django.setup() - -from django.test import Client -import json - -client = Client() -BASE_URL = '/customers/api/search/' - -print("=" * 60) -print("ТЕСТ API ENDPOINTS") -print("=" * 60) - -# Test 1: Empty query -print("\nТЕСТ 1: Пустой запрос") -response = client.get(f'{BASE_URL}?q=') -print(f"Статус: {response.status_code}") -print(f"Ответ: {response.content.decode()}") - -# Test 2: Search by single letter -print("\n" + "=" * 60) -print("ТЕСТ 2: Поиск по букве 'И'") -response = client.get(f'{BASE_URL}?q=И') -print(f"Статус: {response.status_code}") -data = json.loads(response.content) -print(f"Результатов: {len(data.get('results', []))}") -if data.get('results'): - for item in data['results'][:3]: - print(f" - {item.get('text', 'No text')}") - -# Test 3: Search by number -print("\n" + "=" * 60) -print("ТЕСТ 3: Поиск по цифрам '29'") -response = client.get(f'{BASE_URL}?q=29') -print(f"Статус: {response.status_code}") -data = json.loads(response.content) -print(f"Результатов: {len(data.get('results', []))}") -if data.get('results'): - for item in data['results'][:3]: - print(f" - {item.get('text', 'No text')}") - -print("\n" + "=" * 60) -print("Готово!") -print("=" * 60) diff --git a/test_approve.py b/test_approve.py deleted file mode 100644 index 663b6f4..0000000 --- a/test_approve.py +++ /dev/null @@ -1,60 +0,0 @@ -import os -import sys -import django - -# Добавляем путь к проекту -sys.path.insert(0, 'C:/Users/team_/Desktop/test_qwen/myproject') - -# Настройка Django -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'myproject.settings') -django.setup() - -from tenants.models import TenantRegistration, Client -from django.contrib.auth import get_user_model -from tenants.admin import TenantRegistrationAdmin - -# Получаем заявку -registration = TenantRegistration.objects.get(schema_name='shop3') - -print(f"Before approval:") -print(f" Status: {registration.status}") -print(f" Tenant: {registration.tenant}") - -# Одобряем заявку -admin = TenantRegistrationAdmin(TenantRegistration, None) -User = get_user_model() - -# Получаем или создаём системного пользователя для логирования -try: - admin_user = User.objects.get(email='admin@localhost') -except User.DoesNotExist: - admin_user = User.objects.create_superuser( - email='admin@localhost', - name='Admin', - password='admin' - ) - -try: - admin._approve_registration(registration, admin_user) - print("\nApproval successful!") - - # Проверяем результат - registration.refresh_from_db() - print(f"\nAfter approval:") - print(f" Status: {registration.status}") - print(f" Tenant: {registration.tenant}") - - # Проверяем, существует ли тенант в БД - if registration.tenant: - client = Client.objects.get(pk=registration.tenant.pk) - print(f" Client created: {client.name} ({client.schema_name})") - - # Проверяем домены - from tenants.models import Domain - domains = Domain.objects.filter(tenant=client) - print(f" Domains: {list(domains.values_list('domain', flat=True))}") - -except Exception as e: - print(f"Approval failed: {str(e)}") - import traceback - traceback.print_exc() diff --git a/test_cost_calculator.py b/test_cost_calculator.py deleted file mode 100644 index 997eaf2..0000000 --- a/test_cost_calculator.py +++ /dev/null @@ -1,127 +0,0 @@ -""" -Тестовый скрипт для проверки логики расчета себестоимости. -Запускается без БД для проверки математической корректности. -""" - -from decimal import Decimal - - -def calculate_weighted_average_cost(batches): - """ - Рассчитать средневзвешенную себестоимость из партий. - - Args: - batches: List of dicts with 'quantity' and 'cost_price' - - Returns: - Decimal: Средневзвешенная себестоимость - """ - if not batches: - return Decimal('0.00') - - total_value = Decimal('0.00') - total_quantity = Decimal('0.00') - - for batch in batches: - quantity = Decimal(str(batch['quantity'])) - cost_price = Decimal(str(batch['cost_price'])) - - total_value += quantity * cost_price - total_quantity += quantity - - if total_quantity == 0: - return Decimal('0.00') - - weighted_cost = total_value / total_quantity - return weighted_cost.quantize(Decimal('0.01')) - - -# Тестовые сценарии -print("="*80) -print("ТЕСТИРОВАНИЕ РАСЧЕТА СЕБЕСТОИМОСТИ") -print("="*80) - -# Тест 1: Товар без партий -print("\nТест 1: Товар без партий") -batches = [] -result = calculate_weighted_average_cost(batches) -print(f" Партии: {batches}") -print(f" Результат: {result} руб.") -print(f" [OK] Ожидаемый результат: 0.00 руб. - {'PASS' if result == Decimal('0.00') else 'FAIL'}") - -# Тест 2: Одна партия -print("\nТест 2: Одна партия") -batches = [ - {'quantity': 10, 'cost_price': 100} -] -result = calculate_weighted_average_cost(batches) -print(f" Партии: {batches}") -print(f" Результат: {result} руб.") -print(f" [OK] Ожидаемый результат: 100.00 руб. - {'PASS' if result == Decimal('100.00') else 'FAIL'}") - -# Тест 3: Две партии с одинаковой стоимостью -print("\nТест 3: Две партии с одинаковой стоимостью") -batches = [ - {'quantity': 10, 'cost_price': 100}, - {'quantity': 5, 'cost_price': 100} -] -result = calculate_weighted_average_cost(batches) -print(f" Партии: {batches}") -print(f" Результат: {result} руб.") -print(f" [OK] Ожидаемый результат: 100.00 руб. - {'PASS' if result == Decimal('100.00') else 'FAIL'}") - -# Тест 4: Две партии с разной стоимостью (реальный FIFO сценарий) -print("\nТест 4: Две партии с разной стоимостью") -batches = [ - {'quantity': 10, 'cost_price': 100}, # Старая партия - {'quantity': 10, 'cost_price': 120} # Новая партия -] -result = calculate_weighted_average_cost(batches) -expected = Decimal('110.00') # (10*100 + 10*120) / 20 = 2200 / 20 = 110 -print(f" Партии: {batches}") -print(f" Расчет: (10*100 + 10*120) / (10+10) = 2200 / 20 = 110.00") -print(f" Результат: {result} руб.") -print(f" [OK] Ожидаемый результат: {expected} руб. - {'PASS' if result == expected else 'FAIL'}") - -# Тест 5: Три партии с разным количеством и ценой -print("\nТест 5: Три партии с разным количеством и ценой") -batches = [ - {'quantity': 5, 'cost_price': 80}, # Самая старая - {'quantity': 15, 'cost_price': 100}, # Средняя - {'quantity': 10, 'cost_price': 120} # Самая новая -] -result = calculate_weighted_average_cost(batches) -# (5*80 + 15*100 + 10*120) / (5+15+10) = (400 + 1500 + 1200) / 30 = 3100 / 30 = 103.33 -expected = Decimal('103.33') -print(f" Партии: {batches}") -print(f" Расчет: (5*80 + 15*100 + 10*120) / (5+15+10) = 3100 / 30 = 103.33") -print(f" Результат: {result} руб.") -print(f" [OK] Ожидаемый результат: {expected} руб. - {'PASS' if result == expected else 'FAIL'}") - -# Тест 6: Реальный сценарий - товар закончился и появился снова -print("\nТест 6: Товар закончился и пришла новая поставка") -print(" Шаг 1: Создан товар, партий нет") -batches = [] -result1 = calculate_weighted_average_cost(batches) -print(f" Себестоимость: {result1} руб.") - -print(" Шаг 2: Пришла первая поставка") -batches = [{'quantity': 20, 'cost_price': 100}] -result2 = calculate_weighted_average_cost(batches) -print(f" Себестоимость: {result2} руб.") - -print(" Шаг 3: Товар продали полностью (партии опустошились)") -batches = [] -result3 = calculate_weighted_average_cost(batches) -print(f" Себестоимость: {result3} руб.") - -print(" Шаг 4: Пришла новая поставка по другой цене") -batches = [{'quantity': 15, 'cost_price': 120}] -result4 = calculate_weighted_average_cost(batches) -print(f" Себестоимость: {result4} руб.") - -print(f"\n [OK] Жизненный цикл: 0.00 -> 100.00 -> 0.00 -> 120.00 - PASS") - -print("\n" + "="*80) -print("ВСЕ ТЕСТЫ ЗАВЕРШЕНЫ") -print("="*80) diff --git a/test_customer_api.sh b/test_customer_api.sh deleted file mode 100644 index b7e5470..0000000 --- a/test_customer_api.sh +++ /dev/null @@ -1,26 +0,0 @@ -#!/bin/bash - -# Тест API эндпоинтов для поиска и создания клиентов - -API_HOST="http://grach.localhost:8000" - -echo "=== Тест API поиска клиентов ===" -echo "" - -# Тест 1: Поиск по имени -echo "1. Поиск по имени 'Иван':" -curl -s -X GET "${API_HOST}/customers/api/search/?q=Иван" \ - -H "Accept: application/json" | python -m json.tool || echo "Ошибка в запросе" - -echo "" -echo "2. Поиск по частичному номеру телефона '2912':" -curl -s -X GET "${API_HOST}/customers/api/search/?q=2912" \ - -H "Accept: application/json" | python -m json.tool || echo "Ошибка в запросе" - -echo "" -echo "3. Поиск по email 'ivan':" -curl -s -X GET "${API_HOST}/customers/api/search/?q=ivan" \ - -H "Accept: application/json" | python -m json.tool || echo "Ошибка в запросе" - -echo "" -echo "=== Тесты завершены ===" diff --git a/test_fifo.py b/test_fifo.py deleted file mode 100644 index a96bb66..0000000 --- a/test_fifo.py +++ /dev/null @@ -1,105 +0,0 @@ -import os -import sys -import django -from decimal import Decimal - -sys.path.insert(0, 'C:/Users/team_/Desktop/test_qwen/myproject') -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'myproject.settings') -django.setup() - -from tenants.models import Client -from django.db import connection -from products.models import Product -from inventory.models import Warehouse, Incoming, Sale, SaleBatchAllocation, StockBatch - -# Переключаемся в grach -grach = Client.objects.get(schema_name='grach') -connection.set_tenant(grach) - -print("=== FIFO Test ===\n") - -# Получаем товар и склад -product = Product.objects.get(sku='FLOWER-001') -warehouse = Warehouse.objects.get(name='Main Warehouse') - -print("Step 1: Creating 3 incoming batches\n") - -# Создаём 3 прихода -incoming1 = Incoming.objects.create( - product=product, - warehouse=warehouse, - quantity=Decimal('10'), - cost_price=Decimal('100.00') -) -print(" Incoming 1: qty=10, cost=100.00") - -incoming2 = Incoming.objects.create( - product=product, - warehouse=warehouse, - quantity=Decimal('15'), - cost_price=Decimal('120.00') -) -print(" Incoming 2: qty=15, cost=120.00") - -incoming3 = Incoming.objects.create( - product=product, - warehouse=warehouse, - quantity=Decimal('20'), - cost_price=Decimal('150.00') -) -print(" Incoming 3: qty=20, cost=150.00\n") - -print("Step 2: Checking StockBatches\n") - -batches = StockBatch.objects.filter(product=product, warehouse=warehouse).order_by('created_at') -print("StockBatches count: {}\n".format(batches.count())) - -for i, batch in enumerate(batches, 1): - print(" Batch {}: qty={}, cost={}, active={}".format(i, batch.quantity, batch.cost_price, batch.is_active)) - -print("\nStep 3: Creating Sale (qty=18)\n") - -try: - sale = Sale.objects.create( - product=product, - warehouse=warehouse, - quantity=Decimal('18'), - sale_price=Decimal('250.00') - ) - print("Sale created: qty=18, price=250.00\n") - - print("Step 4: Checking SaleBatchAllocations\n") - - allocations = SaleBatchAllocation.objects.filter(sale=sale).order_by('batch__created_at') - total = Decimal('0') - - for i, alloc in enumerate(allocations, 1): - print(" Allocation {}: qty={}, batch_cost={}".format(i, alloc.quantity, alloc.batch.cost_price)) - total += alloc.quantity - - print("\nTotal allocated: {}".format(total)) - - if total == Decimal('18'): - print("PASS: Correct total") - else: - print("FAIL: Expected 18, got {}".format(total)) - - # Check FIFO order - expected_order = [Decimal('10'), Decimal('8')] - actual_order = [alloc.quantity for alloc in allocations] - - if actual_order == expected_order: - print("PASS: FIFO order correct (batch1 10 + batch2 8)") - else: - print("FAIL: Expected {}, got {}".format(expected_order, actual_order)) - -except Exception as e: - print("ERROR: {}".format(str(e))) - import traceback - traceback.print_exc() - -print("\nStep 5: Checking remaining stock\n") - -remaining = StockBatch.objects.filter(product=product, warehouse=warehouse).order_by('created_at') -for i, batch in enumerate(remaining, 1): - print(" Batch {}: qty={}, active={}".format(i, batch.quantity, batch.is_active)) diff --git a/test_fresh_superuser.py b/test_fresh_superuser.py deleted file mode 100644 index 5e4338b..0000000 --- a/test_fresh_superuser.py +++ /dev/null @@ -1,67 +0,0 @@ -import os -import sys -import django - -# Добавляем путь к проекту -sys.path.insert(0, 'C:/Users/team_/Desktop/test_qwen/myproject') - -# Настройка Django -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'myproject.settings') -django.setup() - -from tenants.models import TenantRegistration, Client -from django.contrib.auth import get_user_model -from django.db import connection -from tenants.admin import TenantRegistrationAdmin -import uuid - -# Уникальное имя -unique_id = str(uuid.uuid4())[:8] -schema_name = f'test_{unique_id}' - -# Создаём новую заявку -registration = TenantRegistration.objects.create( - shop_name='Fresh Test', - schema_name=schema_name, - owner_email='fresh@example.com', - owner_name='Fresh User', - phone='+375291234567', - status=TenantRegistration.STATUS_PENDING -) - -print(f"\n=== Testing Fresh Superuser Creation ===") -print(f"Schema: {schema_name}\n") - -# Получаем админа из public -public_tenant = Client.objects.get(schema_name='public') -connection.set_tenant(public_tenant) - -User = get_user_model() -admin_user = User.objects.first() - -# Одобряем заявку -admin = TenantRegistrationAdmin(TenantRegistration, None) - -try: - print("Approving registration...") - client = admin._approve_registration(registration, admin_user) - print("Approval completed!\n") - - # Переключаемся в новый тенант - connection.set_tenant(client) - - # Проверим пользователей - all_users = User.objects.all().order_by('email') - print(f"All users in {client.schema_name} schema ({all_users.count()} total):") - - for u in all_users: - status = "SUPERUSER" if u.is_superuser else "regular" - print(f" - {u.email} ({u.name}) [{status}]") - - if not all_users.exists(): - print(" NO USERS FOUND - THIS IS A PROBLEM!") - -except Exception as e: - print(f"ERROR: {str(e)}") - import traceback - traceback.print_exc() diff --git a/test_fresh_tenant.py b/test_fresh_tenant.py deleted file mode 100644 index fb18aeb..0000000 --- a/test_fresh_tenant.py +++ /dev/null @@ -1,61 +0,0 @@ -import os -import sys -import django -import uuid - -sys.path.insert(0, 'C:/Users/team_/Desktop/test_qwen/myproject') -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'myproject.settings') -django.setup() - -# Проверяем, какой пароль в settings -from django.conf import settings -print(f"Settings TENANT_ADMIN_PASSWORD: {settings.TENANT_ADMIN_PASSWORD}") -print(f"Settings TENANT_ADMIN_EMAIL: {settings.TENANT_ADMIN_EMAIL}\n") - -from tenants.models import TenantRegistration, Client, Domain -from django.contrib.auth import get_user_model -from django.db import connection -from tenants.admin import TenantRegistrationAdmin - -# Создаём новую заявку -unique_id = str(uuid.uuid4())[:8] -schema_name = f'test_{unique_id}' - -registration = TenantRegistration.objects.create( - shop_name=f'Fresh Test {unique_id}', - schema_name=schema_name, - owner_email='fresh@example.com', - owner_name='Fresh User', - phone='+375291234567', - status=TenantRegistration.STATUS_PENDING -) - -print(f"Created registration for: {schema_name}\n") - -# Переключаемся в public -public_tenant = Client.objects.get(schema_name='public') -connection.set_tenant(public_tenant) - -User = get_user_model() -admin_user = User.objects.first() - -# Одобряем заявку -admin = TenantRegistrationAdmin(TenantRegistration, None) -admin._approve_registration(registration, admin_user) - -# Переключаемся в новый тенант -registration.refresh_from_db() -tenant = registration.tenant -connection.set_tenant(tenant) - -# Проверяем пароль -admin_in_tenant = User.objects.get(email='admin@localhost') -print(f"Checking password for admin@localhost in {schema_name}:") -print(f" Password 'AdminPassword123': {admin_in_tenant.check_password('AdminPassword123')}") - -if admin_in_tenant.check_password('AdminPassword123'): - print(f"\nSUCCESS! You can login with:") - print(f" Email: admin@localhost") - print(f" Password: AdminPassword123") -else: - print(f"\nFAILED! Password doesn't work") diff --git a/test_inventory_fifo.py b/test_inventory_fifo.py deleted file mode 100644 index 3e76592..0000000 --- a/test_inventory_fifo.py +++ /dev/null @@ -1,113 +0,0 @@ -import os -import sys -import django -from decimal import Decimal - -sys.path.insert(0, 'C:/Users/team_/Desktop/test_qwen/myproject') -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'myproject.settings') -django.setup() - -from tenants.models import Client -from django.db import connection -from products.models import Product -from inventory.models import Warehouse, StockBatch, Incoming, Sale - -# Переключаемся в grach -grach = Client.objects.get(schema_name='grach') -connection.set_tenant(grach) - -print("=== FIFO Inventory Test ===\n") - -# 1. Проверяем, есть ли товары -products = Product.objects.all() -if not products.exists(): - print("ERROR: No products in grach!") - sys.exit(1) - -product = products.first() -print(f"Using product: {product.name} (ID: {product.id})\n") - -# 2. Создаём или получаем склад -warehouse, created = Warehouse.objects.get_or_create( - name='Main Warehouse', - defaults={'location': 'Default Location', 'is_active': True} -) -print(f"Warehouse: {warehouse.name} ({'created' if created else 'exists'})\n") - -# 3. Создаём 3 прихода с разными ценами (FIFO тест) -print("Creating incoming batches...") - -incoming1 = Incoming.objects.create( - product=product, - warehouse=warehouse, - quantity=Decimal('10'), - cost_price=Decimal('100.00'), - notes='Batch 1 - oldest' -) -print(f" Batch 1: qty=10, price=100 (created: {incoming1.created_at})") - -incoming2 = Incoming.objects.create( - product=product, - warehouse=warehouse, - quantity=Decimal('15'), - cost_price=Decimal('120.00'), - notes='Batch 2 - middle' -) -print(f" Batch 2: qty=15, price=120 (created: {incoming2.created_at})") - -incoming3 = Incoming.objects.create( - product=product, - warehouse=warehouse, - quantity=Decimal('20'), - cost_price=Decimal('150.00'), - notes='Batch 3 - newest' -) -print(f" Batch 3: qty=20, price=150 (created: {incoming3.created_at})\n") - -# 4. Проверяем StockBatches -batches = StockBatch.objects.filter(product=product, warehouse=warehouse).order_by('created_at') -print(f"StockBatches created: {batches.count()}") -for batch in batches: - print(f" - qty={batch.quantity}, price={batch.cost_price}, created={batch.created_at}") - -print("\n--- Creating Sale ---\n") - -# 5. Создаём продажу на 18 единиц (должна взять из batch1 (10) + batch2 (8)) -try: - sale = Sale.objects.create( - product=product, - warehouse=warehouse, - quantity=Decimal('18'), - sale_price=Decimal('200.00'), - notes='Test sale for FIFO' - ) - print(f"Sale created: qty=18, price=200\n") - - # 6. Проверяем SaleBatchAllocation (как батчи были распределены) - from inventory.models import SaleBatchAllocation - - allocations = SaleBatchAllocation.objects.filter(sale=sale).order_by('batch__created_at') - print(f"SaleBatchAllocations ({allocations.count()}):") - - total_allocated = Decimal('0') - for alloc in allocations: - print(f" - Batch (created {alloc.batch.created_at.strftime('%H:%M:%S')}): qty={alloc.quantity}, cost={alloc.batch.cost_price}") - total_allocated += alloc.quantity - - print(f"\nTotal allocated: {total_allocated}") - - if total_allocated == Decimal('18'): - print("✓ FIFO allocation correct!") - else: - print(f"✗ ERROR: Expected 18, got {total_allocated}") - -except Exception as e: - print(f"ERROR creating sale: {str(e)}") - import traceback - traceback.print_exc() - -# 7. Проверяем оставшиеся батчи -print("\n--- Remaining Stock ---\n") -remaining_batches = StockBatch.objects.filter(product=product, warehouse=warehouse).order_by('created_at') -for batch in remaining_batches: - print(f" - Batch qty={batch.quantity}, active={batch.is_active}") diff --git a/test_inventory_reconciliation.py b/test_inventory_reconciliation.py deleted file mode 100644 index a5330b2..0000000 --- a/test_inventory_reconciliation.py +++ /dev/null @@ -1,88 +0,0 @@ -import os -import sys -import django -from decimal import Decimal - -sys.path.insert(0, 'C:/Users/team_/Desktop/test_qwen/myproject') -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'myproject.settings') -django.setup() - -from tenants.models import Client -from django.db import connection -from products.models import Product -from inventory.models import Warehouse, Inventory, InventoryLine, WriteOff - -grach = Client.objects.get(schema_name='grach') -connection.set_tenant(grach) - -print("=== Inventory Reconciliation Test ===\n") - -product = Product.objects.get(sku='FLOWER-001') -warehouse = Warehouse.objects.get(name='Main Warehouse') - -# Проверяем текущий остаток -print("Step 1: Current stock status\n") - -current_qty = sum(b.quantity for b in product.stock_batches.filter(warehouse=warehouse)) -print("Current system stock: {} units".format(current_qty)) - -# Создаём инвентаризацию -print("\nStep 2: Creating inventory (physical count)\n") - -inventory = Inventory.objects.create( - warehouse=warehouse, - status='draft' -) -print("Inventory created (status=draft)") - -# Добавляем строку с физическим подсчётом (меньше, чем в системе - ДЕФИЦИТ) -print("\nStep 3: Adding inventory line with deficit\n") - -# Физический подсчёт: 5 единиц (система имеет 7) -inventory_line = InventoryLine.objects.create( - inventory=inventory, - product=product, - quantity_system=current_qty, - quantity_fact=Decimal('5') -) -print("Inventory line: system={}, fact={}, difference={}".format( - inventory_line.quantity_system, - inventory_line.quantity_fact, - inventory_line.difference -)) - -# Завершаем инвентаризацию -print("\nStep 4: Finishing inventory (processing reconciliation)\n") - -inventory.status = 'completed' -inventory.save() -print("Inventory status: completed") - -# Проверяем все WriteOff операции -print("\nStep 5: Checking WriteOff operations\n") - -writeoffs = WriteOff.objects.all().order_by('-date') -print("Total WriteOff records: {}".format(writeoffs.count())) - -if writeoffs.exists(): - print("Recent WriteOffs:") - for wo in writeoffs[:3]: - print(" - qty={}, reason={}".format( - wo.quantity, - wo.reason - )) - print("PASS: WriteOff created for deficit") -else: - print(" No WriteOff found") - -# Проверяем новый остаток -print("\nStep 6: Final stock status\n") - -final_qty = sum(b.quantity for b in product.stock_batches.filter(warehouse=warehouse)) -print("Final system stock: {} units".format(final_qty)) -print("Expected: 5 units") - -if final_qty == Decimal('5'): - print("PASS: Stock reconciled correctly") -else: - print("FAIL: Stock mismatch (expected 5, got {})".format(final_qty)) diff --git a/test_order_signals.py b/test_order_signals.py deleted file mode 100644 index 8d1dd9e..0000000 --- a/test_order_signals.py +++ /dev/null @@ -1,116 +0,0 @@ -import os -import sys -import django -from decimal import Decimal -from datetime import datetime, timedelta - -sys.path.insert(0, 'C:/Users/team_/Desktop/test_qwen/myproject') -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'myproject.settings') -django.setup() - -from tenants.models import Client -from django.db import connection -from products.models import Product -from inventory.models import Warehouse, Incoming, Reservation, Sale -from orders.models import Order, OrderItem -from customers.models import Customer - -grach = Client.objects.get(schema_name='grach') -connection.set_tenant(grach) - -print("=== Order Signals Test ===\n") - -# Получаем товар и склад -product = Product.objects.get(sku='FLOWER-001') -warehouse = Warehouse.objects.get(name='Main Warehouse') - -# Создаем приходы -print("Step 1: Creating incoming batches\n") - -Incoming.objects.create( - product=product, - warehouse=warehouse, - quantity=Decimal('50'), - cost_price=Decimal('100.00') -) -print(" Incoming: qty=50, cost=100.00") - -# Создаем клиента -print("\nStep 2: Creating customer\n") - -customer, created = Customer.objects.get_or_create( - phone='+375291234567', - defaults={ - 'name': 'Test Customer', - 'email': 'test@example.com' - } -) -print(" Customer: {} ({})".format(customer.name, customer.phone)) - -# Создаем заказ -print("\nStep 3: Creating order\n") - -try: - delivery_date = datetime.now() + timedelta(days=1) - - order = Order.objects.create( - order_number='ORD-001', - customer=customer, - status='pending', - delivery_type='courier', - delivery_date=delivery_date - ) - print(" Order created: ORD-001, status=pending") - - # Создаем позицию заказа - order_item = OrderItem.objects.create( - order=order, - product=product, - quantity=Decimal('5'), - price=Decimal('200.00') - ) - print(" OrderItem: 5 units") - - # Проверяем резервирование - print("\nStep 4: Checking reservations\n") - - reservations = Reservation.objects.filter(order_item=order_item, status='reserved') - print("Reservations count: {}".format(reservations.count())) - - if reservations.exists(): - for res in reservations: - print(" PASS: Reservation created - qty={}, status={}".format(res.quantity, res.status)) - else: - print(" FAIL: No reservations found!") - - # Меняем статус на in_delivery - print("\nStep 5: Changing order status to in_delivery\n") - - order.status = 'in_delivery' - order.save() - print(" Order status changed to in_delivery") - - # Проверяем продажу - print("\nStep 6: Checking sales\n") - - sales = Sale.objects.filter(order=order) - print("Sales count: {}".format(sales.count())) - - if sales.exists(): - for sale in sales: - print(" PASS: Sale created - qty={}, price={}".format(sale.quantity, sale.sale_price)) - print(" processed={}".format(sale.processed)) - else: - print(" FAIL: No sales found!") - - # Проверяем обновленное резервирование - print("\nStep 7: Checking updated reservations\n") - - updated_reservations = Reservation.objects.filter(order_item=order_item) - for res in updated_reservations: - print(" Reservation status: {}".format(res.status)) - -except Exception as e: - print("ERROR: {}".format(str(e))) - import traceback - traceback.print_exc() diff --git a/test_order_signals_existing.py b/test_order_signals_existing.py deleted file mode 100644 index 1b0ac62..0000000 --- a/test_order_signals_existing.py +++ /dev/null @@ -1,89 +0,0 @@ -import os -import sys -import django -from decimal import Decimal - -sys.path.insert(0, 'C:/Users/team_/Desktop/test_qwen/myproject') -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'myproject.settings') -django.setup() - -from tenants.models import Client -from django.db import connection -from orders.models import Order -from inventory.models import Reservation, Sale, SaleBatchAllocation - -grach = Client.objects.get(schema_name='grach') -connection.set_tenant(grach) - -print("=== Order Signals Test with Existing Order ===\n") - -# Получаем существующий заказ -try: - order = Order.objects.get(order_number__contains='ORD-20251027') - print("Found order: {}".format(order.order_number)) - print("Customer: {}".format(order.customer.name)) - print("Items in order: {}".format(order.items.count())) - - # Показываем items - print("\nOrder items:") - for item in order.items.all(): - product_name = item.product.name if item.product else item.product_kit.name - print(" - {} qty={} price={}".format(product_name, item.quantity, item.price)) - - # Проверяем резервирования - print("\n--- Checking Reservations ---\n") - - reservations = Reservation.objects.filter(order_item__order=order) - print("Total reservations: {}".format(reservations.count())) - - if reservations.exists(): - for res in reservations: - print(" - qty={}, status={}, warehouse={}".format( - res.quantity, res.status, res.warehouse.name - )) - else: - print(" No reservations found - might not have been created yet") - - # Меняем статус на in_delivery - print("\n--- Changing status to in_delivery ---\n") - - order.status = 'in_delivery' - order.save() - print("Order status changed to: {}".format(order.status)) - - # Проверяем продажи - print("\n--- Checking Sales ---\n") - - sales = Sale.objects.filter(order=order) - print("Total sales: {}".format(sales.count())) - - if sales.exists(): - for sale in sales: - print("\nSale:") - print(" qty={}, price={}".format(sale.quantity, sale.sale_price)) - print(" processed={}".format(sale.processed)) - - # Проверяем allocations - allocations = SaleBatchAllocation.objects.filter(sale=sale) - print(" Allocations: {}".format(allocations.count())) - - for alloc in allocations: - print(" - batch qty={}, cost={}".format( - alloc.quantity, alloc.cost_price - )) - else: - print(" No sales found") - - # Проверяем финальный статус резервирования - print("\n--- Final Reservations Status ---\n") - - final_reservations = Reservation.objects.filter(order_item__order=order) - for res in final_reservations: - print(" Reservation status: {}".format(res.status)) - -except Order.DoesNotExist: - print("ERROR: Order not found!") -except Exception as e: - print("ERROR: {}".format(str(e))) - import traceback - traceback.print_exc() diff --git a/test_registration.py b/test_registration.py deleted file mode 100644 index 31cb061..0000000 --- a/test_registration.py +++ /dev/null @@ -1,28 +0,0 @@ -import os -import sys -import django - -# Добавляем путь к проекту -sys.path.insert(0, 'C:/Users/team_/Desktop/test_qwen/myproject') - -# Настройка Django -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'myproject.settings') -django.setup() - -from tenants.models import TenantRegistration - -# Создаём заявку на регистрацию -registration = TenantRegistration.objects.create( - shop_name='Test Shop 3', - schema_name='shop3', - owner_email='shop3@example.com', - owner_name='John Doe', - phone='+375291234567', - status=TenantRegistration.STATUS_PENDING -) - -print(f"Registration created: {registration.id}") -print(f" Shop: {registration.shop_name}") -print(f" Schema: {registration.schema_name}") -print(f" Email: {registration.owner_email}") -print(f" Status: {registration.status}") diff --git a/test_second_sale.py b/test_second_sale.py deleted file mode 100644 index abf13f5..0000000 --- a/test_second_sale.py +++ /dev/null @@ -1,61 +0,0 @@ -import os -import sys -import django -from decimal import Decimal - -sys.path.insert(0, 'C:/Users/team_/Desktop/test_qwen/myproject') -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'myproject.settings') -django.setup() - -from tenants.models import Client -from django.db import connection -from products.models import Product -from inventory.models import Warehouse, Sale, SaleBatchAllocation - -grach = Client.objects.get(schema_name='grach') -connection.set_tenant(grach) - -print("=== Second Sale Test ===\n") - -product = Product.objects.get(sku='FLOWER-001') -warehouse = Warehouse.objects.get(name='Main Warehouse') - -# Создаем вторую продажу на 20 единиц -print("Creating second sale (qty=20)\n") - -sale2 = Sale.objects.create( - product=product, - warehouse=warehouse, - quantity=Decimal('20'), - sale_price=Decimal('250.00') -) - -print("Sale 2 created: qty=20\n") - -# Проверяем allocations -allocations = SaleBatchAllocation.objects.filter(sale=sale2) -total = sum(a.quantity for a in allocations) - -print("Allocations for Sale 2:") -for alloc in allocations: - print(" - qty={}, cost={}".format(alloc.quantity, alloc.cost_price)) - -print("\nTotal allocated: {}".format(total)) -print("Sale qty: {}".format(sale2.quantity)) - -if total == sale2.quantity: - print("PASS: Allocations match Sale qty") -else: - print("FAIL: Allocations ({}) != Sale qty ({})".format(total, sale2.quantity)) - -# Проверяем порядок FIFO (должны взяться из batch2 (7), batch3 (13)) -expected = [Decimal('7'), Decimal('13')] -actual = [a.quantity for a in allocations] - -print("\nExpected FIFO order: {}".format(expected)) -print("Actual order: {}".format(actual)) - -if actual == expected: - print("PASS: FIFO order correct") -else: - print("FAIL: FIFO order incorrect") diff --git a/test_superuser_creation.py b/test_superuser_creation.py deleted file mode 100644 index 55b2b47..0000000 --- a/test_superuser_creation.py +++ /dev/null @@ -1,86 +0,0 @@ -import os -import sys -import django -import logging - -# Настройка логирования -logging.basicConfig(level=logging.INFO, format='%(levelname)s: %(message)s') - -# Добавляем путь к проекту -sys.path.insert(0, 'C:/Users/team_/Desktop/test_qwen/myproject') - -# Настройка Django -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'myproject.settings') -django.setup() - -from tenants.models import TenantRegistration, Client -from django.contrib.auth import get_user_model -from django.db import connection -from tenants.admin import TenantRegistrationAdmin - -# Создаём новую заявку -registration = TenantRegistration.objects.create( - shop_name='Test Shop 5', - schema_name='shop5', - owner_email='shop5@example.com', - owner_name='Jane Doe', - phone='+375291234567', - status=TenantRegistration.STATUS_PENDING -) - -print(f"\n=== Testing Superuser Creation ===") -print(f"Created registration: {registration.id} for {registration.schema_name}\n") - -# Одобряем заявку -admin = TenantRegistrationAdmin(TenantRegistration, None) -User = get_user_model() - -# Получаем системного пользователя -public_tenant = Client.objects.get(schema_name='public') -connection.set_tenant(public_tenant) - -try: - admin_user = User.objects.get(email='admin@localhost') -except User.DoesNotExist: - admin_user = User.objects.create_superuser( - email='admin@localhost', - name='Admin', - password='admin' - ) - -try: - print("Approving registration...") - admin._approve_registration(registration, admin_user) - print("\nApproval completed!\n") - - # Проверяем, был ли создан суперпользователь в новом тенанте - registration.refresh_from_db() - tenant = registration.tenant - - print(f"Tenant created: {tenant.name} ({tenant.schema_name})") - - # Переключаемся на новый тенант и проверяем пользователей - connection.set_tenant(tenant) - - superusers = User.objects.filter(is_superuser=True) - print(f"\nSuperusers in {tenant.schema_name} schema:") - - if superusers.exists(): - for su in superusers: - print(f" OK: {su.email} ({su.name})") - else: - print(" FAIL: No superusers found!") - - # Проверим ВСЕ пользователей - all_users = User.objects.all() - print(f"\nAll users in {tenant.schema_name} schema:") - if all_users.exists(): - for u in all_users: - print(f" - {u.email} (superuser={u.is_superuser})") - else: - print(" - No users found at all!") - -except Exception as e: - print(f"Error during approval: {str(e)}") - import traceback - traceback.print_exc() diff --git a/test_variant_stock.py b/test_variant_stock.py deleted file mode 100644 index aff5b0f..0000000 --- a/test_variant_stock.py +++ /dev/null @@ -1,279 +0,0 @@ -#!/usr/bin/env python -""" -Скрипт для тестирования системы наличия товаров и цен вариантов. -Проверяет: -1. Обновление Product.in_stock при создании Stock -2. Свойство ProductVariantGroup.in_stock (вариант в наличии если хотя бы один товар в наличии) -3. Свойство ProductVariantGroup.price (берётся цена по приоритету) -""" - -import os -import sys -import django - -# Добавляем путь к myproject -sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'myproject')) -os.chdir(os.path.join(os.path.dirname(__file__), 'myproject')) - -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'myproject.settings') -django.setup() - -from decimal import Decimal -from products.models import Product, ProductVariantGroup, ProductVariantGroupItem -from inventory.models import Stock, StockBatch, Warehouse, Incoming, IncomingBatch -from django.utils import timezone - -def clear_data(): - """Очищаем тестовые данные""" - Product.objects.all().delete() - ProductVariantGroup.objects.all().delete() - Stock.objects.all().delete() - StockBatch.objects.all().delete() - Warehouse.objects.all().delete() - IncomingBatch.objects.all().delete() - print("OK Данные очищены") - -def create_test_data(): - """Создаём тестовые данные""" - - # Создаём склад - warehouse = Warehouse.objects.create( - name="Основной склад", - description="Тестовый склад", - is_active=True, - is_default=True - ) - print(f"OK Создан склад: {warehouse.name}") - - # Создаём товары (розы разной длины) - products = [] - prices = [Decimal('50.00'), Decimal('60.00'), Decimal('70.00')] - - for i, price in enumerate(prices, 1): - product = Product.objects.create( - name=f"Роза красная Freedom {50 + i*10}см", - sku=f"ROSE-RED-{50 + i*10}", - cost_price=Decimal('30.00'), - sale_price=price, - unit='шт', - is_active=True, - in_stock=False # По умолчанию нет в наличии - ) - products.append(product) - print(f"OK Создан товар: {product.name} (цена: {price}, in_stock={product.in_stock})") - - # Создаём группу вариантов - variant_group = ProductVariantGroup.objects.create( - name="Роза красная Freedom", - description="Розы разной высоты" - ) - print(f"OK Создана группа вариантов: {variant_group.name}") - - # Добавляем товары в группу с приоритетами - items = [] - for priority, product in enumerate(products, 1): - item = ProductVariantGroupItem.objects.create( - variant_group=variant_group, - product=product, - priority=priority - ) - items.append(item) - print(f" - {product.name} (приоритет {priority})") - - return warehouse, products, variant_group, items - -def test_scenario_1(): - """ - Тест 1: Создание товара в наличии и проверка автоматического обновления in_stock - """ - print("\n" + "="*80) - print("ТЕСТ 1: Обновление Product.in_stock при создании Stock") - print("="*80) - - warehouse, products, variant_group, items = create_test_data() - - # Получаем первый товар - product = products[0] - print(f"\nПроверяем товар: {product.name}") - print(f"Текущий статус in_stock: {product.in_stock}") - - # Создаём приход товара (это должно создать Stock и обновить in_stock) - incoming_batch = IncomingBatch.objects.create( - warehouse=warehouse, - document_number="IN-0001", - supplier_name="Тестовый поставщик" - ) - print(f"\nOK Создана партия поступления: {incoming_batch.document_number}") - - # Добавляем товар в приход - incoming = Incoming.objects.create( - batch=incoming_batch, - product=product, - quantity=Decimal('100.00'), - cost_price=product.cost_price - ) - print(f"OK Добавлен товар в приход: {incoming.quantity} шт") - - # Проверяем что Stock был создан и in_stock обновлён - stock = Stock.objects.get(product=product, warehouse=warehouse) - print(f"\nOK Stock создан:") - print(f" - quantity_available: {stock.quantity_available}") - print(f" - quantity_reserved: {stock.quantity_reserved}") - print(f" - quantity_free: {stock.quantity_free}") - - # Обновляем товар из БД чтобы получить новое значение - product.refresh_from_db() - print(f"\nOK Product.in_stock обновлён: {product.in_stock}") - - if product.in_stock: - print("PASS: ТЕСТ 1 ПРОЙДЕН: Product.in_stock = True") - else: - print("FAIL: ТЕСТ 1 ПРОВАЛЕН: Product.in_stock должен быть True") - - return warehouse, products, variant_group, items - -def test_scenario_2(warehouse, products, variant_group, items): - """ - Тест 2: Проверка свойства ProductVariantGroup.in_stock - """ - print("\n" + "="*80) - print("ТЕСТ 2: Свойство ProductVariantGroup.in_stock") - print("="*80) - - # Обновляем товары из БД - for product in products: - product.refresh_from_db() - - print(f"\nГруппа вариантов: {variant_group.name}") - print(f"Товары в группе:") - for item in variant_group.items.all(): - print(f" - {item.product.name} (приоритет {item.priority}, in_stock={item.product.in_stock})") - - # Первый товар в наличии, поэтому вариант должен быть в наличии - print(f"\nСвойство variant_group.in_stock: {variant_group.in_stock}") - - if variant_group.in_stock: - print("PASS: ТЕСТ 2 ПРОЙДЕН: Вариант в наличии (хотя бы один товар доступен)") - else: - print("FAIL: ТЕСТ 2 ПРОВАЛЕН: Вариант должен быть в наличии") - -def test_scenario_3(warehouse, products, variant_group, items): - """ - Тест 3: Проверка свойства ProductVariantGroup.price - """ - print("\n" + "="*80) - print("ТЕСТ 3: Свойство ProductVariantGroup.price") - print("="*80) - - print(f"\nГруппа вариантов: {variant_group.name}") - - # Обновляем товары из БД - for product in products: - product.refresh_from_db() - - print(f"Товары в приоритете:") - for item in variant_group.items.all().order_by('priority'): - status = "OK В наличии" if item.product.in_stock else "NO Нет в наличии" - print(f" {item.priority}. {item.product.name} - {item.product.sale_price} руб {status}") - - # Цена должна быть из первого в наличии (приоритет 1, цена 50.00) - price = variant_group.price - expected_price = Decimal('50.00') - - print(f"\nЦена варианта: {price} руб") - print(f"Ожидаемая цена: {expected_price} руб") - - if price == expected_price: - print("PASS: ТЕСТ 3 ПРОЙДЕН: Берётся цена товара с приоритетом 1") - else: - print(f"FAIL: ТЕСТ 3 ПРОВАЛЕН: Цена должна быть {expected_price}, получена {price}") - -def test_scenario_4(): - """ - Тест 4: Проверка цены когда нет товара в наличии (должна быть максимальная) - """ - print("\n" + "="*80) - print("ТЕСТ 4: Цена варианта когда ни один товар не в наличии") - print("="*80) - - # Очищаем данные - clear_data() - - # Создаём новые данные без Stock (товары не в наличии) - warehouse = Warehouse.objects.create( - name="Тестовый склад", - is_active=True, - is_default=True - ) - - # Создаём товары с разными ценами - products = [] - prices = [Decimal('100.00'), Decimal('150.00'), Decimal('200.00')] - - for i, price in enumerate(prices, 1): - product = Product.objects.create( - name=f"Товар {i}", - sku=f"PRODUCT-{i}", - cost_price=Decimal('50.00'), - sale_price=price, - unit='шт', - is_active=True, - in_stock=False # Нет в наличии - ) - products.append(product) - - # Создаём группу вариантов - variant_group = ProductVariantGroup.objects.create( - name="Группа товаров без наличия" - ) - - # Добавляем товары в группу - for priority, product in enumerate(products, 1): - ProductVariantGroupItem.objects.create( - variant_group=variant_group, - product=product, - priority=priority - ) - - print(f"\nГруппа: {variant_group.name}") - print(f"Товары (все без наличия):") - for item in variant_group.items.all().order_by('priority'): - print(f" {item.priority}. {item.product.name} - {item.product.sale_price} руб (in_stock={item.product.in_stock})") - - # Цена должна быть максимальная = 200.00 - price = variant_group.price - expected_price = Decimal('200.00') - - print(f"\nЦена варианта (максимальная): {price} руб") - print(f"Ожидаемая цена (максимальная): {expected_price} руб") - - if price == expected_price: - print("PASS: ТЕСТ 4 ПРОЙДЕН: Берётся максимальная цена из товаров") - else: - print(f"FAIL: ТЕСТ 4 ПРОВАЛЕН: Цена должна быть {expected_price}, получена {price}") - -if __name__ == '__main__': - print("\n" + "="*80) - print("= ТЕСТИРОВАНИЕ СИСТЕМЫ НАЛИЧИЯ ТОВАРОВ И ЦЕН ВАРИАНТОВ") - print("="*80) - - try: - # Очищаем старые данные - clear_data() - - # Тесты 1-3 - warehouse, products, variant_group, items = test_scenario_1() - test_scenario_2(warehouse, products, variant_group, items) - test_scenario_3(warehouse, products, variant_group, items) - - # Тест 4 - test_scenario_4() - - print("\n" + "="*80) - print("= ТЕСТИРОВАНИЕ ЗАВЕРШЕНО") - print("="*80 + "\n") - - except Exception as e: - print(f"\nОШИБКА: {e}") - import traceback - traceback.print_exc() diff --git a/test_writeoff_validation.py b/test_writeoff_validation.py deleted file mode 100644 index 32e774c..0000000 --- a/test_writeoff_validation.py +++ /dev/null @@ -1,116 +0,0 @@ -#!/usr/bin/env python -""" -Test script to verify WriteOff form validation prevents over-listing. -This tests that WriteOffForm.clean() prevents creating WriteOff with quantity > batch.quantity -""" - -import os -import sys -import django - -# Setup Django -sys.path.insert(0, 'myproject') -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'myproject.settings') -django.setup() - -from inventory.forms import WriteOffForm -from inventory.models import WriteOff, StockBatch, Warehouse, Product -from django.core.exceptions import ValidationError - -print("=" * 70) -print("Testing WriteOff Validation") -print("=" * 70) - -# Create test data -warehouse = Warehouse.objects.filter(is_active=True).first() -if not warehouse: - print("ERROR: No active warehouse found. Please create one first.") - sys.exit(1) - -product = Product.objects.filter(tenant=warehouse.tenant).first() -if not product: - print("ERROR: No product found. Please create one first.") - sys.exit(1) - -# Create a batch with 2 items -batch = StockBatch.objects.create( - product=product, - warehouse=warehouse, - quantity=2, - cost_price=100.0, - is_active=True -) - -print(f"\n✓ Created StockBatch with quantity=2") -print(f" Product: {product.name}") -print(f" Warehouse: {warehouse.name}") -print(f" Batch ID: {batch.id}") - -# Test 1: Valid WriteOff (quantity = batch.quantity) -print("\n" + "=" * 70) -print("Test 1: Valid WriteOff with quantity=2 (equal to batch quantity)") -print("=" * 70) - -form_data = { - 'batch': batch.id, - 'quantity': 2, - 'reason': 'damage', - 'document_number': 'DOC001', - 'notes': 'Test valid writeoff' -} - -form = WriteOffForm(data=form_data) -if form.is_valid(): - print("✓ PASS: Form is valid (quantity <= batch.quantity)") -else: - print(f"✗ FAIL: Form should be valid but has errors: {form.errors}") - -# Test 2: Invalid WriteOff (quantity > batch.quantity) -print("\n" + "=" * 70) -print("Test 2: Invalid WriteOff with quantity=3 (exceeds batch quantity=2)") -print("=" * 70) - -form_data = { - 'batch': batch.id, - 'quantity': 3, - 'reason': 'damage', - 'document_number': 'DOC002', - 'notes': 'Test invalid writeoff - should be rejected' -} - -form = WriteOffForm(data=form_data) -if not form.is_valid(): - print("✓ PASS: Form validation correctly rejects over-listing") - for field, errors in form.errors.items(): - for error in errors: - print(f" Error: {error}") -else: - print("✗ FAIL: Form should reject quantity > batch.quantity") - -# Test 3: Invalid WriteOff (quantity = 0) -print("\n" + "=" * 70) -print("Test 3: Invalid WriteOff with quantity=0 (should be > 0)") -print("=" * 70) - -form_data = { - 'batch': batch.id, - 'quantity': 0, - 'reason': 'damage', - 'document_number': 'DOC003', - 'notes': 'Test zero quantity' -} - -form = WriteOffForm(data=form_data) -if not form.is_valid(): - print("✓ PASS: Form validation correctly rejects zero quantity") - for field, errors in form.errors.items(): - for error in errors: - print(f" Error: {error}") -else: - print("✗ FAIL: Form should reject quantity <= 0") - -# Cleanup -batch.delete() -print("\n" + "=" * 70) -print("Test completed successfully!") -print("=" * 70)