БОЛЬШОЕ ИЗМЕНЕНИЕ

This commit is contained in:
2025-11-15 15:55:01 +03:00
parent 1f561ac429
commit 53fbb6d3c1
23 changed files with 0 additions and 2969 deletions

View File

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

View File

@@ -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 <noreply@anthropic.com>
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")
Все логи и диагностическая информация выводится в консоль браузера для удобства отладки.

View File

@@ -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 = $('<div class="select2-result-item">');
$container.text(item.text);
// Отображаем actual_price (цену со скидкой если она есть), иначе обычную цену
var displayPrice = item.actual_price || item.price;
if (displayPrice) {
$container.append($('<div class="text-muted small">').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 ✓

View File

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

View File

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

View File

@@ -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 четкий и понятный
То система готова к использованию!

View File

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

View File

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

View File

@@ -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()

View File

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

View File

@@ -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 "=== Тесты завершены ==="

View File

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

View File

@@ -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()

View File

@@ -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")

View File

@@ -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}")

View File

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

View File

@@ -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()

View File

@@ -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()

View File

@@ -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}")

View File

@@ -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")

View File

@@ -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()

View File

@@ -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()

View File

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