Commit Graph

401 Commits

Author SHA1 Message Date
f7b62b45f3 Исправлен метод delete(): теперь делает статус СНЯТ (discontinued), а не АРХИВНЫЙ (archived) 2025-12-01 11:09:18 +03:00
4cb5a605f8 Simplified product/kit deletion message - available for recovery for some time 2025-12-01 11:07:59 +03:00
c670406ae0 Исправлено извлечение ID товаров из API в форме поступления
Проблема:
- Поиск товаров возвращал пустые результаты
- API /products/api/search-products-variants/ возвращает ID в формате 'product_123'
- Форма incoming ожидает числовой ID (123)
- Select2 не мог сохранить значение из-за несовпадения формата

Решение:
- Добавлена функция processResults в AJAX настройках Select2
- Извлекаем числовой ID из строки 'product_123' -> '123'
- Обрабатываем как группированные результаты, так и плоские
- Сохраняем остальные поля (text, sku, price, actual_price)

Логика обработки:
1. Проверяем наличие children (группа)
2. Если группа - обрабатываем каждый item в children
3. Если плоский список - обрабатываем напрямую
4. Используем .replace('product_', '') для извлечения ID

Теперь Select2 корректно:
- Показывает список товаров
- Сохраняет выбранное значение
- Отправляет числовой ID в форму
2025-12-01 10:31:25 +03:00
dc39f56b9a Исправлен поиск товаров в форме массового поступления
Проблема:
- На странице /inventory/incoming/create/ не работал поиск товаров
- Использовался обычный <select> с предзагруженным списком всех товаров
- При большом количестве товаров список был неудобным
- Невозможно было искать товары по названию в реальном времени

Решение:
- Заменён обычный <select> на Select2 с AJAX автокомплитом
- Подключен API endpoint /products/api/search-products-variants/
- Поиск товаров работает в реальном времени (с задержкой 250ms)
- Минимальная длина поиска: 0 символов (можно открыть весь список)
- Поддержка русского языка
- Theme: bootstrap-5 для визуального соответствия

Изменения:
- Удалён предзагруженный список товаров из контекста шаблона
- Добавлена инициализация Select2 для каждой новой строки товара
- При удалении строки вызывается select2('destroy') для очистки
- Исправлена логика восстановления товаров при ошибке валидации
- Используется Option API для программной установки значений

Технические детали:
- jQuery и Select2 уже подключены в base.html
- Не дублируем подключение библиотек
- Используем событие 'change' для Select2 вместо 'input'
- Кэширование AJAX запросов включено

Теперь поиск товаров работает корректно! 🎉
2025-12-01 10:29:57 +03:00
4597ddbd87 🔥 КРИТИЧЕСКОЕ ИСПРАВЛЕНИЕ: Дублирование резервов при изменении количества
Проблема:
- При изменении количества в OrderItem для заказа в статусе 'completed'
- Создавался ДУБЛИКАТ резерва (старый converted_to_sale + новый reserved)
- Это приводило к двойному списанию товара со склада (-10 лишних единиц)
- Фильтр status='reserved' пропускал существующие резервы в других статусах

Сценарий бага:
1. Заказ выполнен: 20 шт → резерв 20 шт (converted_to_sale)
2. Увеличить на 10 шт (до 30) → создаётся НОВЫЙ резерв 30 шт (reserved)
3. Итого: 20 + 30 = 50 шт зарезервировано вместо 30!
4. При переводе обратно в 'completed' → двойное списание (50 вместо 30)

Решение:
- Убран фильтр status='reserved' из update_reservation_on_item_change
- Теперь резерв ищется по order_item независимо от статуса
- Обновляется ТОЛЬКО quantity, статус НЕ меняется
- Добавлен @transaction.atomic для атомарности операции
- Добавлено логирование всех операций с резервами
- Используется save(update_fields=['quantity']) для оптимизации

Безопасность решения:
- Резервы разных заказов НЕ конфликтуют (разные order_item)
- Один товар в разных заказах = разные OrderItem = разные Reservation
- Каждый OrderItem имеет уникальный резерв
- Дубликаты больше НЕ создаются

Изменённые файлы:
- inventory/signals.py (функция update_reservation_on_item_change)
- FIX_RESERVATION_DUPLICATE_BUG.md (полная документация бага и решения)

Покрытие всех сценариев:
 Создание заказа с товарами
 Добавление товара при редактировании
 Изменение количества (черновик)
 Изменение количества (выполнен) - ИСПРАВЛЕНО
 Повторное сохранение заказа

КРИТИЧНО: Это исправление влияет на учёт товара и требует тестирования!
2025-12-01 10:22:28 +03:00
5300b83565 Исправлен фильтр по номеру заказа на отладочной странице
Проблема:
- Фильтр по заказу не работал
- order_number в форме передается как строка (ORD-103 или 103)
- В модели Order поле order_number имеет тип PositiveIntegerField
- Не было преобразования строки в число

Исправление:
- Добавлен парсинг номера заказа:
  * Если формат 'ORD-103' -> извлекается число 103
  * Если просто '103' -> преобразуется в число 103
- Добавлена обработка ошибок (try-except)
- Добавлена фильтрация таблицы заказов (показывать только выбранный)

Теперь фильтр работает с любым форматом ввода:
- ORD-103 ✓
- ord-103 ✓
- 103 ✓
2025-12-01 10:10:49 +03:00
d712da1816 Добавлены столбцы статуса оплаты и суммы оплаты в таблицу заказов
Добавлено:
- Столбец 'Статус оплаты' с цветовой индикацией:
  * Зеленый бейдж - Оплачен полностью
  * Желтый бейдж - Частично оплачен
  * Красный бейдж - Не оплачен
- Столбец 'Оплачено' показывает: amount_paid / total_amount
  * Например: 500.00 / 1000.00 (оплачено 500 из 1000)

Использованные поля:
- payment_status (paid/partial/unpaid)
- amount_paid (сумма внесенная клиентом)
- total_amount (итоговая сумма заказа)
2025-12-01 10:08:47 +03:00
a8dc3897c5 Исправлено отображение суммы заказа в таблице Order
Проблема:
- В шаблоне использовалось несуществующее поле total_price
- Фактическое поле в модели Order называется total_amount

Исправление:
- Заменено order.total_price на order.total_amount
- Теперь итоговая сумма заказа отображается корректно
2025-12-01 10:07:58 +03:00
992db6bd69 Исправлено отображение себестоимости в таблице StockBatch
Проблема:
- В шаблоне использовалось несуществующее поле cost_per_unit
- Фактическое поле в модели StockBatch называется cost_price

Исправление:
- Заменено batch.cost_per_unit на batch.cost_price
- Теперь закупочная цена партии отображается корректно
2025-12-01 10:06:51 +03:00
5c61789b71 Исправлено отображение себестоимости в таблице SaleBatchAllocation
Проблема:
- В шаблоне использовалось несуществующее поле cost_per_unit
- Фактическое поле в модели называется cost_price

Исправление:
- Заменено alloc.cost_per_unit на alloc.cost_price
- Теперь себестоимость за единицу отображается корректно
2025-12-01 10:05:32 +03:00
337335ec58 Добавлена отладочная страница для суперюзеров + исправлены ошибки полей
Реализация:
- Создан view debug_inventory_page (только для суперюзеров)
- URL: /inventory/debug/
- Компактный дизайн с минимальными отступами (10-11px)
- Ссылка 🔧 Debug в navbar (видна только суперюзерам)

Функционал:
1. Показывает полную картину инвентаризации на одной странице:
   - Заказы (Order) - номер, статус, покупатель, is_returned
   - Остатки (Stock) - доступно, зарезервировано, свободно
   - Партии (StockBatch) - количество, активность, дата поступления
   - Резервы (Reservation) - статус, заказ, все даты
   - Продажи (Sale) - количество, цена продажи, заказ
   - Списания (SaleBatchAllocation) - откуда списано, сколько

2. Фильтры:
   - По товару (dropdown)
   - По номеру заказа (текстовое поле)
   - По складу (dropdown)

3. UI:
   - Цветовая индикация статусов резервов
   - Бейджи для ключевых данных
   - Компактные таблицы Bootstrap
   - Неактивные партии выделены красным

Исправления:
- Reservation.created_at → reserved_at (у модели нет created_at)
- Sale.created_at → date (дата операции хранится в поле date)
- Product.is_active → archived_at__isnull=True (используется soft delete)
- Удалена колонка себестоимости из Sale (это поле не хранится в модели)

Файлы:
- inventory/views/debug_views.py - новый view
- inventory/templates/inventory/debug_page.html - шаблон
- inventory/urls.py - добавлен роут
- templates/navbar.html - добавлена ссылка

Юзкейс:
Суперюзер принимает товар → оформляет заказ → меняет статусы →
переходит на /inventory/debug/ → видит полную картину изменений
2025-12-01 10:04:00 +03:00
8e036ba5e1 Исправлено поле created_at на reserved_at для Reservation
Проблема:
- У модели Reservation нет поля created_at
- Есть поле reserved_at для даты создания резерва

Исправление:
- В view изменена сортировка order_by('-reserved_at')
- В шаблоне изменено отображение даты res.reserved_at
2025-12-01 09:59:45 +03:00
6bb15db5a0 Добавлена отладочная страница для суперюзеров (Inventory Debug)
Реализация:
- Создан view debug_inventory_page (только для суперюзеров)
- URL: /inventory/debug/
- Компактный дизайн с минимальными отступами и маленьким шрифтом (10-11px)

Функционал:
1. Показывает полную картину инвентаризации на одной странице:
   - Заказы (Order) - номер, статус, покупатель, is_returned
   - Остатки (Stock) - доступно, зарезервировано, свободно
   - Партии (StockBatch) - количество, активность, дата поступления
   - Резервы (Reservation) - статус (reserved/converted_to_sale/released), заказ, даты
   - Продажи (Sale) - количество, цены, заказ
   - Списания (SaleBatchAllocation) - откуда списано, сколько

2. Фильтры:
   - По товару (dropdown с названием и SKU)
   - По номеру заказа (текстовое поле)
   - По складу (dropdown)
   - Кнопка 'Применить' и 'Сбросить'

3. UI:
   - Цветовая индикация статусов резервов
   - Бейджи для ключевых данных
   - Компактные таблицы Bootstrap
   - Неактивные партии выделены красным
   - Ограничение в 100 записей на таблицу для производительности

4. Навигация:
   - Ссылка 🔧 Debug в navbar (видна только суперюзерам)
   - Красный цвет для видимости

Юзкейс:
Суперюзер принимает товар на склад → оформляет заказ → меняет статусы →
переходит на /inventory/debug/ → видит полную картину всех изменений

Файлы:
- inventory/views/debug_views.py - новый view
- inventory/templates/inventory/debug_page.html - шаблон
- inventory/urls.py - добавлен роут
- templates/navbar.html - добавлена ссылка для суперюзеров
2025-12-01 09:57:06 +03:00
7b1922c186 Исправлена обработка резервов при переходе ОТМЕНЁН → ВЫПОЛНЕН
Проблема:
- При смене статуса заказа ОТМЕНЁН → ВЫПОЛНЕН
- Sale создавался и товар списывался корректно ✓
- НО резервы оставались в статусе 'released' вместо 'converted_to_sale'
- Это приводило к некорректной истории и возможным проблемам при откате

Причина:
- Сигнал искал только резервы в статусе 'reserved'
- После отмены резервы были в статусе 'released'
- При повторном выполнении они не обновлялись

Решение:
- Изменён фильтр резервов: берём ВСЕ кроме 'converted_to_sale'
- Теперь обрабатываются резервы в любом статусе (reserved, released, и др.)
- Элегантное решение без хардкода конкретных статусов

Дополнительно:
- Добавлен @transaction.atomic к сигналам обновления Stock
- Защита от race conditions при одновременном изменении резервов
- Минимальные издержки, максимальная надёжность

Результат:
- Корректная работа при ЛЮБЫХ переходах статусов:
  * reserved → converted_to_sale ✓
  * released → converted_to_sale ✓
  * повторный вызов → пропуск ✓
- Целостность данных гарантирована транзакциями
- Элегантный код без костылей
2025-12-01 02:57:44 +03:00
a5a983b198 Исправлена проблема с резервами при откате из статуса 'Выполнен'
Проблема:
- При откате заказа из статуса 'completed' в 'возврат' или другой статус
- Резервы правильно обновлялись на 'reserved' или 'released'
- НО Stock.quantity_reserved не обновлялся
- В результате товар показывался как полностью свободный, хотя был резерв

Причина:
- В сигнале rollback_sale_on_status_change использовался .update()
- Это не вызывало сигнал update_stock_on_reservation_change
- Stock не пересчитывался автоматически

Решение:
- Заменен .update() на .save(update_fields=[...]) в сигнале отката
- Теперь при изменении резервов автоматически срабатывает сигнал
- Stock корректно обновляется в обоих направлениях:
  * completed → резервы converted_to_sale → Stock обновляется
  * откат → резервы reserved/released → Stock обновляется
- Убран костыль с ручным вызовом refresh_from_batches()

Результат:
- Элегантное единообразное решение для всех сценариев
- Stock автоматически синхронизируется с резервами
- Работает корректно при любых изменениях статуса заказа
2025-12-01 02:40:40 +03:00
e4cb175db2 Исправлена критическая проблема с резервами при смене статуса заказа
Проблема:
- При смене статуса заказа на 'Выполнен' товар списывался со склада
- Резервы обновлялись на статус 'converted_to_sale'
- НО Stock.quantity_reserved не обновлялся автоматически
- В результате резервы продолжали 'держать' товар, хотя он уже продан

Решение:
1. Изменен сигнал create_sale_on_order_completion:
   - Используется .save(update_fields=[...]) вместо .update()
   - Это вызывает сигнал update_stock_on_reservation_change
   - Убран костыль с ручным вызовом refresh_from_batches()

2. Оптимизирован сигнал update_stock_on_reservation_change:
   - Stock обновляется ТОЛЬКО при изменении status или quantity
   - При изменении других полей (даты и т.д.) Stock НЕ пересчитывается
   - Предотвращены лишние пересчёты и улучшена производительность

3. Добавлены диагностические инструменты:
   - check_stock_103.py - для диагностики проблем с Stock
   - fix_stock_after_sale.py - команда для исправления старых заказов
   - diagnose_reservation_issue.py - универсальная диагностика

Результат:
- Элегантное решение без дублирования логики
- Stock автоматически обновляется при изменении резервов
- Работает везде, не только в заказах
- Оптимизировано для производительности
2025-12-01 02:34:54 +03:00
490e5d5401 Добавлены интеграционные тесты создания тенантов
Создан файл tenants/tests/test_tenant_creation.py с 7 E2E тестами:

1. test_new_tenant_gets_all_5_payment_methods (КРИТИЧЕСКИЙ)
   - Проверяет что новый тенант получает все 5 способов оплаты
   - Включая account_balance (основной баг который исправили)
   - Проверяет правильность порядка, флагов, названий

2. test_new_tenant_gets_order_statuses
   - Проверяет создание системных статусов заказов
   - Минимум 3 статуса (draft, completed и другие)

3. test_new_tenant_gets_system_customer
   - Проверяет создание системного клиента
   - Для анонимных продаж

4. test_new_tenant_gets_superuser
   - Проверяет создание суперпользователя
   - С email из настроек TENANT_ADMIN_EMAIL

5. test_new_tenant_gets_domain
   - Проверяет создание домена
   - Формат: {schema_name}.localhost

6. test_registration_status_changes_to_approved
   - Проверяет изменение статуса заявки
   - PENDING → APPROVED

7. test_complete_tenant_onboarding (КОМПЛЕКСНЫЙ E2E)
   - Проверяет весь процесс онбординга
   - Все предыдущие проверки в одном тесте
   - Красивый вывод результата в консоль

Особенности:
- TransactionTestCase для работы с реальными схемами
- Создание администратора в setUp для каждого теста
- Автоматическая очистка схем в tearDown
- Вызов реального метода _approve_registration из админки
- Полное тестирование процесса как в продакшене

Результат: 2 главных теста прошли успешно ✓
test_new_tenant_gets_all_5_payment_methods: OK (10s)
test_complete_tenant_onboarding: OK (9.5s)
2025-12-01 01:41:01 +03:00
8a64b569bd Добавлены тесты для способов оплаты
Создан файл orders/tests/test_payment_methods.py с комплексными тестами:

1. PaymentMethodCreationTest (6 тестов)
   - Проверка создания всех 5 способов оплаты через команду
   - Проверка системных флагов и активности
   - Проверка правильности порядка сортировки
   - Проверка идемпотентности команды
   - Критический тест наличия account_balance

2. PaymentMethodMultiTenantTest (2 теста)
   - Проверка изоляции данных между тенантами
   - Проверка кастомных способов оплаты в разных тенантах

3. PaymentMethodTransactionTest (7 тестов)
   - Проверка связи PaymentMethod.transactions
   - Проверка создания транзакций
   - Проверка изоляции транзакций по способам оплаты
   - Проверка защиты от удаления (PROTECT)
   - Критический тест использования account_balance
   - Исправление бага obj.payments → obj.transactions

4. PaymentMethodOrderingTest (2 теста)
   - Проверка сортировки по полю order
   - Проверка что account_balance первый (order=0)

Особенности тестирования:
- Использование TenantTestCase для изоляции тенантов
- Использование TransactionTestCase для мультитенантных тестов
- Ручное создание/удаление схем для безопасности
- Проверка изоляции данных между схемами

Результат: 15 тестов, все прошли успешно ✓
2025-12-01 01:30:23 +03:00
7188b11f65 Единый источник истины для способов оплаты
Проблема #1: Дублирование кода способов оплаты
- В tenants/admin.py был полный список способов оплаты (45 строк)
- В orders/management/commands/create_payment_methods.py был другой список
- При создании тенанта отсутствовал способ 'account_balance'
- Нарушение DRY принципа

Решение: Single Source of Truth
- Единственный источник истины: команда create_payment_methods
- В tenants/admin.py заменено дублирование на call_command()
- Удалено 45 строк дублирующего кода
- Теперь все тенанты получают одинаковый полный список

Проблема #2: AttributeError в админке PaymentMethod
- obj.payments.count() вызывал ошибку
- В модели Transaction связь называется 'transactions', а не 'payments'

Решение: Исправлено в orders/admin.py
- obj.payments → obj.transactions (2 места)
- Админка PaymentMethod теперь работает корректно

Для тенанта buba:
- Создан скрипт add_payment_methods_to_buba.py
- Добавлен недостающий способ оплаты 'С баланса счёта'

Изменённые файлы:
- myproject/tenants/admin.py - вызов команды вместо дублирования
- myproject/orders/admin.py - исправлено на transactions
- add_payment_methods_to_buba.py - скрипт для существующих тенантов
2025-12-01 01:22:40 +03:00
293e8640ef Исправлено создание резервов при сохранении заказов
Проблема #1: Резервы не создавались при создании заказа
- Order сохранялся БЕЗ items → сигнал reserve_stock_on_order_create
  не мог создать резервы (items.all() был пустой)
- OrderItem создавались ПОСЛЕ, но сигнал уже отработал

Решение #1: Создание резервов через сигнал OrderItem
- Доработан сигнал update_reservation_on_item_change
- Убрано раннее возвращение для created=True
- Теперь резервы создаются при добавлении OrderItem (любым способом)
- Работает для всех сценариев:
  * Создание заказа с товарами
  * Добавление товаров при редактировании
  * Изменение количества

Проблема #2: Риск расхождений при удалении заказа
- Сигнал pre_delete освобождал резервы ДО удаления
- Если удаление падало с ошибкой → резервы освобождены, Order не удалён
- Возникало расхождение данных

Решение #2: transaction.on_commit для освобождения резервов
- Добавлен @transaction.atomic к сигналу release_stock_on_order_delete
- Резервы получаются ДО удаления через list()
- Освобождение происходит через transaction.on_commit()
- Резервы освобождаются ТОЛЬКО если удаление успешно
- Гарантия целостности данных

Изменённые файлы:
- myproject/inventory/signals.py - оба сигнала исправлены
- RESERVATION_FIX.md - полная документация
2025-12-01 01:10:58 +03:00
e0437cdb5a Исправлено двойное списание товаров при смене статуса заказа
Проблема:
- При изменении статуса заказа на 'Выполнен' товар списывался дважды
- Заказ на 10 шт создавал Sale на 10 шт, но со склада уходило 20 шт

Найдено ДВЕ причины:

1. Повторное обновление резервов через .save() (inventory/signals.py)
   - Резервы обновлялись через res.save() каждый раз при сохранении заказа
   - Это вызывало сигнал update_stock_on_reservation_change
   - При повторном сохранении заказа происходило двойное срабатывание

   Решение:
   - Проверка дубликатов ПЕРЕД обновлением резервов
   - Замена .save() на .update() для массового обновления без вызова сигналов
   - Ручное обновление Stock после .update()

2. Двойное FIFO-списание (inventory/services/sale_processor.py)
   - Sale создавалась с processed=False
   - Сигнал process_sale_fifo срабатывал и списывал товар (1-й раз)
   - Затем SaleProcessor.create_sale() тоже списывал товар (2-й раз)

   Решение:
   - Sale создаётся сразу с processed=True
   - Сигнал не срабатывает, списание только в сервисе

Дополнительно:
- Ограничен выбор статусов при создании заказа только промежуточными
- Статус 'Черновик' установлен по умолчанию
- Убран пустой выбор '-------' из поля статуса

Изменённые файлы:
- myproject/orders/forms.py - настройки статусов для формы заказа
- myproject/inventory/signals.py - исправление сигнала create_sale_on_order_completion
- myproject/inventory/services/sale_processor.py - исправление create_sale
- myproject/test_order_status_default.py - обновлён тест
- DOUBLE_SALE_FIX.md - документация по исправлению
2025-12-01 00:56:26 +03:00
4e66f03957 Исправлено обновление резервов при завершении заказа
Проблема: Резервы не обновлялись в статус 'converted_to_sale' если Sale уже существовали,
что приводило к отображению завершенных заказов на странице активных резервов.

Решение: Переместил обновление резервов ПЕРЕД проверкой существования Sale.
Теперь резервы всегда обновляются при переходе заказа в статус 'completed',
независимо от того, были ли уже созданы записи Sale.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-30 22:36:39 +03:00
6a2e180b29 Исправлена ошибка NoneType при создании заказа
Проблема: При создании заказа instance.status может быть None,
что вызывало AttributeError при попытке доступа к .code

Решение: Добавлена проверка 'not instance.status' перед
обращением к instance.status.code

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-30 22:16:48 +03:00
920dbf4273 Добавлена защита от повторного списания + команда исправления дубликатов
Проблема: Сигнал post_save срабатывает несколько раз,
создавая дубликаты Sale для одного заказа.

Решения:
1. Добавлена проверка Sale.objects.filter(order=instance).exists()
   перед созданием продаж (inventory/signals.py:74-75)
2. Создана management команда fix_duplicate_sales для исправления
   существующих дубликатов

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-30 22:15:33 +03:00
24292b2e47 Исправлено сравнение статуса заказа в сигнале списания
Проблема: Order.status - это объект OrderStatus, а не строка.
Сравнение instance.status != 'completed' всегда возвращало True.

Решение: Сравниваем instance.status.code != 'completed'

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-30 22:05:46 +03:00
d502a37583 Перенесено списание товаров со склада на статус 'completed'
Изменения:
- Сигнал create_sale_on_order_shipment переименован в create_sale_on_order_completion
- Списание товаров (создание Sale) теперь происходит при статусе 'completed' вместо 'in_delivery'
- Исправлен выбор склада: используется Order.pickup_warehouse, если задан
- Та же логика применена к резервированию товаров

Обоснование для цветочного бизнеса:
- Букет может вернуться в магазин (клиента нет дома, перенос доставки)
- Товар физически находится в магазине до момента доставки
- Резерв показывает что товар занят - этого достаточно для промежуточных статусов
- Простота: списываем только когда ТОЧНО продали

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-30 21:51:28 +03:00
e723c26e6c Удаление фото по расписанию из папки temp 2025-11-30 21:20:45 +03:00
e5b37dcd81 Удалена дублирующая форма загрузки фото в форме создания товара 2025-11-30 19:01:12 +03:00
f483b04488 Исправлен импорт pillow-heif: убрана несуществующая функция register_avif_opener (AVIF регистрируется автоматически через register_heif_opener) 2025-11-30 18:41:49 +03:00
213fedcad5 Исправлена регистрация HEIF/AVIF декодеров в Celery worker для поддержки HEIC фото 2025-11-30 18:37:30 +03:00
1cea3661e0 Добавлена поддержка HEIC/HEIF фото с iPhone: подключен pillow-heif, расширен валидатор форматов, увеличен лимит размера до 20MB 2025-11-30 18:23:44 +03:00
03048b6345 Очистка временных файлов документации 2025-11-30 14:51:24 +03:00
4343b2eb5b Улучшен UI формы оплаты: красивый бейдж НЕ ОПЛАЧЕНО с пульсирующей анимацией, убран устаревший код смешанной оплаты, исправлены критичные ошибки JavaScript 2025-11-29 22:47:56 +03:00
9c6092262c Добавлена автоподстановка суммы для оплаты с кошелька и подсказка при нулевом балансе 2025-11-29 22:29:28 +03:00
4d197790fc Упрощена форма оплаты заказа: единая форма платежа/возврата с переключателем режимов 2025-11-29 22:23:42 +03:00
e10faf697f Удален метод add_overpayment - больше не используется 2025-11-29 20:49:05 +03:00
7d45106411 Убрана автоматическая обработка переплаты - только ручное управление 2025-11-29 20:42:58 +03:00
8cd2cfdb84 Уточнен текст предупреждения о переплате - объяснена автоматика 2025-11-29 20:40:37 +03:00
7c1780697a Добавлено свойство overpayment для корректного отображения переплаты 2025-11-29 20:38:15 +03:00
42eddc0fd1 Упрощена логика обработки переплаты - убрана автоматика из calculate_total
Проблема:
- calculate_total() пытался автоматически обрабатывать переплату
- Это приводило к дублированию логики и сложной отладке
- Нарушался принцип единственной ответственности

Решение:
- Удалена автоматическая обработка переплаты из Order.calculate_total()
- Теперь calculate_total() ТОЛЬКО считает сумму - всё
- Переплата обрабатывается ТОЛЬКО в TransactionService при создании платежей/возвратов
- Добавлено предупреждение в UI о переплате с инструкцией

Как работает теперь:
1. При оплате - TransactionService автоматически вызывает add_overpayment()
2. При изменении товаров - calculate_total() только пересчитывает сумму
3. Если появилась переплата - оператор видит предупреждение
4. Оператор вручную создаёт возврат в кошелёк через форму

Преимущества:
- Одно место ответственности за переплаты
- Прозрачность для оператора
- Нет скрытых автоматизмов
- Легко обслуживать и отлаживать
- Стандартный подход для e-commerce
2025-11-29 20:34:20 +03:00
575c5d0c2f Исправлена двойная обработка переплаты при изменении суммы заказа
Проблема:
- add_overpayment вызывался дважды:
  1. При оплате 300 руб (заказ 150) → +150 в кошелёк
  2. При изменении суммы до 100 → +200 в кошелёк
- Итого: 350 руб вместо правильных 200 руб

Причина:
- calculate_total() вызывал add_overpayment при любой переплате
- Не учитывалось, что переплата уже была обработана при оплате

Решение:
- Сохраняем old_total перед пересчётом
- Вызываем add_overpayment ТОЛЬКО если:
  - old_total > 0 (заказ уже существовал)
  - total_amount < old_total (сумма УМЕНЬШИЛАСЬ)
  - amount_paid > total_amount (есть переплата)
- Это предотвращает двойную обработку при первичной оплате

Теперь переплата обрабатывается корректно только при изменении суммы заказа
2025-11-29 20:28:00 +03:00
a7ccbbec48 Исправлен баг с переплатой при изменении суммы заказа
Проблема:
- Создан заказ на 150 руб, оплачено 300 руб → 150 руб в кошелёк
- Изменены товары, сумма стала 100 руб
- amount_paid остался 300, total_amount стал 100
- Новая переплата 200 руб НЕ переносилась в кошелёк автоматически

Решение:
- В Order.calculate_total() добавлена проверка переплаты после пересчёта суммы
- Если amount_paid > total_amount, вызывается WalletService.add_overpayment()
- Излишек автоматически переносится в кошелёк, amount_paid нормализуется до total_amount
- Создаётся WalletTransaction для аудита

Теперь при уменьшении суммы заказа переплата корректно возвращается клиенту
2025-11-29 20:23:25 +03:00
29e47e7248 Оптимизация запросов на странице клиента
- Устранён N+1 для статусов заказов: добавлен select_related('status')
- Расчёт total_debt перенесён на сторону БД через aggregate с Greatest/Coalesce
- Избежана загрузка всех активных заказов в память для подсчёта долга
- Количество активных заказов теперь считается через count() без загрузки данных
- Ожидаемый эффект: минус 10+ запросов на страницу, быстрее рендер при большом количестве заказов
2025-11-29 19:31:44 +03:00
e7ac4bd8a8 Исправлено отображение статуса заказов и пагинация истории
- Исправлен баг отображения статуса: теперь сравнение с order.status.code вместо order.status
- Добавлена обработка отсутствующего статуса (показывает 'Без статуса')
- Пагинация истории заказов: добавлен якорь #ordersHistoryCollapse ко всем ссылкам
- Автооткрытие collapse при переходе по пагинации через JavaScript
- Плавная прокрутка к секции истории после раскрытия collapse (событие shown.bs.collapse)
- Пользователь остаётся в секции истории заказов при переходе между страницами
2025-11-29 19:27:08 +03:00
22fad84545 Сворачиваемые секции истории на странице клиента
- История кошелька и история заказов теперь под collapse (свёрнуты по умолчанию)
- Кликабельные заголовки с иконкой chevron для раскрытия
- Badge с количеством элементов в заголовке
- Кнопка 'Новый заказ' доступна в свёрнутом состоянии (event.stopPropagation)
- Компактный и удобный интерфейс для работы с большими списками
2025-11-29 18:53:14 +03:00
915efa16dc Компактный layout операций с кошельком: две колонки
- Пополнение и Списание размещены рядом (col-md-6 каждая)
- Упрощены тексты заголовков, лейблов и кнопок для компактности
- Фиксированная высота для подсказок — обеспечена симметрия полей
- Убран разделитель между формами
- Короткие placeholder'ы в полях ввода
2025-11-29 18:52:09 +03:00
c4e83fd535 Улучшен layout страницы клиента: две колонки вверху
- Информация о клиенте — левая колонка (col-md-6)
- Операции с кошельком — правая колонка (col-md-6)
- Баланс кошелька перенесён из таблицы в заголовок блока операций
- История кошелька и история заказов остаются в полную ширину
- Компактный вертикальный layout форм в правой колонке
2025-11-29 18:48:43 +03:00
4b7241bcfc Добавлены операции с кошельком клиента: пополнение и возврат
- Добавлены view wallet_deposit и wallet_withdraw с защитой (login_required, is_staff, CSRF)
- Новые маршруты: /customers/<pk>/wallet/deposit/ и /customers/<pk>/wallet/withdraw/
- UI на странице клиента: две симметричные формы для пополнения и списания баланса
- Пополнение: произвольная сумма с обязательным описанием (подарки, компенсации)
- Возврат/списание: с ограничением макс. суммы = текущий баланс, обязательное описание
- Все операции логируются в WalletTransaction с типом 'adjustment'
- Защита от операций с системным клиентом
- Компактный симметричный дизайн форм с фиксированной высотой подсказок
2025-11-29 18:09:40 +03:00
3f22677573 Защита от переплаты кошельком и улучшение отображения транзакций
Изменения в UI (order_form.html):
- Добавлен data-code к опциям способов оплаты для идентификации метода кошелька
- ID для селекта способа оплаты (payment-method-select) и поля суммы (payment-amount-input)
- Динамическое ограничение max на поле суммы платежа при выборе кошелька
- Подсказка 'Макс: X руб.' отображается только для оплаты кошельком
- Для внешних методов (карта, наличные) ограничение отсутствует — переплата допустима

Логика JS:
- При выборе метода с code == 'account_balance' устанавливается max = order.amount_due
- Для остальных методов max удаляется — оператор может внести сумму больше остатка
- Переплата по внешним методам корректно зачисляется в кошелёк через WalletService.add_overpayment

Серверная защита (transaction_service.py):
- В TransactionService.create_payment добавлена проверка:
  если payment_method.code == 'account_balance' и amount > order.amount_due — ValidationError
- Сообщение: 'Сумма оплаты из кошелька (X руб.) не может превышать остаток к оплате (Y руб.)'
- Защита от обхода UI через API или прямой вызов

Улучшение отображения (order_form.html, order_detail.html):
- Для возврата в кошелёк (transaction_type == 'refund' и code == 'account_balance') показываем 'на баланс счёта' вместо названия метода
- История становится понятнее: '−750,00 руб. Возврат 29.11.2025 на баланс счёта'

Сценарий:
- Кошелёк клиента 500 руб., заказ 65 руб.
- Оператор выбирает оплату из кошелька — поле суммы ограничено 65 руб.
- Попытка ввести 500 заблокирована UI и серверной валидацией
- Для внешней оплаты (карта онлайн) можно внести 500 руб. — остаток 435 автоматически зачислится в кошелёк как переплата

Цель:
- Исключить путаницу в истории транзакций при оплате кошельком
- Разграничить поведение: кошелёк строго ограничен, внешние методы допускают переплату
- Обеспечить прозрачность движения средств для операторов
2025-11-29 16:54:24 +03:00
312cd808e6 Уточнение UI возвратов: пометка метода кошелька и корректный текст предупреждения
Изменения:
- Добавлена пометка '(кошелёк клиента)' к методу оплаты с кодом account_balance в селектах платежа и возврата
- Обновлён текст предупреждения о возврате: теперь явно указано, что зачисление в кошелёк происходит только при выборе метода 'кошелёк клиента'
- Для всех остальных методов (наличные, карта и т.п.) возврат — это информационная метка для истории, без фактического движения средств

Цель:
- Устранить путаницу операторов относительно поведения возвратов
- Чётко разделить возврат клиенту (внешними способами) и зачисление в кошелёк (только для account_balance)
- UI теперь соответствует фактической серверной логике в Transaction.save()

Защита от переплаты:
- Серверная валидация в TransactionService.create_refund проверяет amount <= order.amount_paid
- UI ограничение max на поле ввода суммы возврата
- ValidationError с понятным сообщением при попытке превысить лимит
2025-11-29 15:47:47 +03:00