- Реализован универсальный поиск по имени, email и телефону в одной строке
- Добавлен счетчик общего количества клиентов
- Поиск работает по нажатию Enter или кнопке 'Поиск'
- Удалены неиспользуемые фильтры django-filter
- Упрощен интерфейс списка клиентов
- Добавлена кнопка 'Очистить' для сброса поиска
- Создан модуль customers/services/import_export.py согласно best practices
- Класс CustomerExporter: содержит логику экспорта в CSV (ранее была в views)
- Класс CustomerImporter: заглушка для будущей реализации импорта
- Views стали тонкими: customer_export и customer_import делегируют работу сервисам
- Улучшена организация кода: соблюдён принцип Single Responsibility
- Уменьшен размер views.py на 30 строк
- Добавлена подробная документация в docstrings классов и методов
- Логику теперь легко тестировать и переиспользовать (например, в Celery tasks)
Преимущества:
- Чистое разделение ответственности
- Упрощённое тестирование
- Возможность переиспользования в асинхронных задачах
- Соответствие Django best practices
- Исправлена ошибка AttributeError: Customer не имеет полей first_name и last_name
- Модель Customer имеет только поле name (полное имя)
- Удалён экспорт баланса кошелька по требованию пользователя
- Обновлена инструкция в шаблоне импорта: убраны фамилия и баланс
- Добавлены пометки об уникальности email и телефона
- Теперь экспорт работает корректно с полями: ID, Имя, Email, Телефон, Дата создания
- Добавлены кнопки Импорт и Экспорт в header страницы customers/
- Создан URL-маршрут для customer-import и customer-export
- Реализована функция customer_export: экспорт всех клиентов в CSV файл с BOM для Excel
- Экспортируются поля: ID, Имя, Фамилия, Email, Телефон, Баланс кошелька, Дата создания
- Создан шаблон customer_import.html с инструкцией и формой загрузки файла
- Функция customer_import пока заглушка (TODO: реализовать парсинг CSV/Excel)
- Кнопки оформлены в btn-group с иконками Bootstrap Icons
- Системный клиент исключён из экспорта
- Изменён TIME_ZONE с Europe/Moscow на Europe/Minsk в settings.py
- Исправлено создание заказов в POS: теперь используется timezone.localtime() для корректной конвертации UTC → Minsk
- delivery_date и delivery_time_start/end теперь сохраняются в минском времени, а не UTC
- Исправлена разница в 3 часа между временем создания заказа и фактическим временем
- Удалено устаревшее поле payment_method из showcase_manager.py (поле было удалено из модели Order)
Теперь все временные метки заказов соответствуют реальному минскому времени
- Интегрирован готовый компонент ProductSearchPicker в модалку редактирования
- Добавлен collapse-блок с поиском товаров, который отображается только в режиме редактирования
- При выборе товара он автоматически добавляется в tempCart или увеличивается количество если уже есть
- Добавлен CSS и JS для компонента product-search-picker
- В view передаётся categories QuerySet для работы фильтров компонента
- Блок добавления товаров показывается только при редактировании, скрыт при создании нового комплекта
- Добавлены методы reserve_product_to_showcase и release_showcase_reservation в ShowcaseManager
- Методы работают с резервами для всех активных экземпляров витринного комплекта
- НЕ блокируют создание резерва при нехватке товара, возвращают информацию о дефиците (overdraft)
- Обновлён API endpoint update_product_kit для корректировки резервов при изменении состава
- Добавлено визуальное предупреждение на фронте о нехватке товара на складе
- В модалке редактирования комплекта добавлены контролы для изменения количества товаров (+/-, поле ввода, удаление)
- Автоматический пересчёт цен при изменении состава
- Очистка корзины POS после успешного создания витринного комплекта
- Убраны атрибуты data-bs-toggle и role, блокирующие переход по ссылке
- Теперь клик на Заказы открывает список заказов
- Dropdown продолжает работать при наведении благодаря CSS стилям
- Исправлена проблема, когда клик не приводил к переходу на страницу
- Заголовок 'Заказы' теперь ведёт на список заказов при клике
- Сохранена функциональность dropdown-меню при наведении
- Улучшена навигация - теперь можно перейти к заказам напрямую без раскрытия меню
Изменения в navbar.html:
- Объединены ссылки в логические dropdown-группы
- Уменьшено количество пунктов верхнего уровня с 10+ до 6
- Добавлены эмодзи-иконки для визуальной идентификации разделов
Структура меню:
📦 Товары (dropdown)
- Все товары, Каталог, Вариативные товары
- Категории, Теги, Варианты (группы)
📋 Заказы (dropdown)
- Список заказов
- Статусы заказов
👥 Клиенты (одиночная ссылка)
🏭 Склад (dropdown)
- Управление складом
- Витрины
💰 Касса (одиночная ссылка)
⚙️ Настройки (dropdown, только для owner/superuser)
- Роли пользователей
- Debug (только для superuser)
Преимущества:
- Компактная навигация - проще найти нужный раздел
- Логическая группировка связанных функций
- Сохранена подсветка активного раздела
- Улучшена визуальная идентификация с помощью иконок
- Добавлена ссылка 'Категории' в главное меню навигации
- Ссылка размещена логически между 'Варианты' и 'Теги'
- Добавлена подсветка активного пункта меню при работе с категориями
- Теперь доступ к управлению категориями товаров доступен из главного меню
Добавлено:
- Команда clear_tenant_data для полной очистки данных тенанта без удаления схемы
* Очищает все таблицы через TRUNCATE CASCADE
* Сбрасывает ID-последовательности
* Сохраняет схему БД и запись Client
* Поддержка флага --noinput для автоматизации
- Команда init_tenant_data для инициализации системных данных тенанта
* Создаёт системного клиента (АНОНИМНЫЙ ПОКУПАТЕЛЬ для POS)
* Создаёт 8 системных статусов заказов
* Создаёт 5 системных способов оплаты
* Поддержка флага --reset для пересоздания данных
Исправлено:
- Заменены устаревшие фильтры is_active на status='active' для Product и ProductKit
* products/views/category_views.py: исправлены фильтры в build_category_tree и get_context_data
* products/services/kit_pricing.py: исправлены фильтры при получении товаров из variant_group
* products/models/kits.py: исправлен фильтр в get_available_products
* Устранена ошибка FieldError при работе со списком категорий
Улучшено:
- Команда clear_tenant_data теперь предлагает пользователю инициализировать системные данные после очистки
- Добавлена детальная информация о процессе очистки и инициализации данных
Проблема:
На странице списка заказов (order_list) при изменении статуса 'на лету':
- ValidationError показывался через alert() - страшно
- Сообщение содержало служебные элементы
- Статус всё равно менялся визуально (нет отката)
Решение Backend (views.py):
- В set_order_status добавлена обработка ValidationError ПЕРЕД ValueError
- Извлекается чистое сообщение (e.messages[0] или str(e))
- Возвращается JSON: {success: false, error: 'чистое сообщение'}
Решение Frontend (order_list.html):
- Добавлен контейнер для динамических Bootstrap alert
- Создана функция showAlert() для показа красивых alert-danger
- При ошибке:
* Показывается Bootstrap alert с иконкой
* Прокрутка к верху страницы
* Автоскрытие через 5 секунд
* Возврат select к предыдущему значению (откат визуально)
- Больше НЕТ страшных alert()
Теперь пользователь видит:
[красный Bootstrap alert вверху страницы]
⚠️ Заказ 134 был отменён, товары проданы в другом заказе.
Невозможно изменить статус. Для новой продажи создайте новый заказ.
[X]
User-friendly на обеих страницах (форма редактирования + список)!
Проблема:
ValidationError из сигналов отображался как:
'Server error: [\'Заказ 134 был отменён...\']'
со служебными элементами (Server error, квадратные скобки).
Решение:
В order_update добавлена обработка ValidationError перед ValueError:
- Извлекаем чистое сообщение из исключения (e.messages[0] или str(e))
- Показываем через messages.error() — Django автоматически отобразит
красивым Bootstrap alert-danger
- Транзакция откатывается, изменения не сохраняются
Теперь пользователь видит:
[красный Bootstrap alert]
'Заказ 134 был отменён, товары проданы в другом заказе.
Невозможно изменить статус. Для новой продажи создайте новый заказ.'
Без технических префиксов и форматирования - user-friendly.
Проблема:
Сообщение ValidationError с переносами строк \\n отображалось как текст,
а не как реальные переносы, плюс выглядело как 'Server error' - страшно.
Решение:
Сделано короткое однострочное сообщение без \\n:
'Заказ 134 был отменён, товары проданы в другом заказе.
Невозможно изменить статус. Для новой продажи создайте новый заказ.'
Теперь user-friendly, без технических деталей и пугающих форматирований.
Проблема:
Для заказа с is_returned=True без резервов (товар продан в другом заказе)
можно было установить промежуточные статусы (В доставке, Черновик и т.п.),
что не имеет смысла, т.к. физически продавать уже нечего.
Решение:
Валидация теперь проверяет ДО проверки is_positive_end:
- Если is_returned=True И резервов нет И статус НЕ отрицательный →
запрещаем ЛЮБОЕ изменение статуса
- Разрешены только статусы с is_negative_end=True (отменён и т.п.)
Улучшено сообщение об ошибке:
- Убраны длинные объяснения
- Короткая структура с переносами строк
- Чёткое указание: «товары проданы в другом заказе»
- Действие: «создайте новый заказ»
Теперь возвращённый заказ без резервов навсегда остаётся в статусе
отрицательного исхода — как и должно быть в реальности.
Проблема:
1. Флаг is_returned управлялся в разных местах непоследовательно
2. При цепочке completed → cancelled → completed флаг оставался True
3. Можно было установить положительный статус для заказа с is_returned=True
без резервов (товар уже продан в другом заказе)
Решение:
1. ЕДИНАЯ ФУНКЦИЯ update_is_returned_flag():
- Флаг основан на РЕАЛЬНОМ состоянии заказа (наличие Sale)
- Логика: есть Sale → is_returned=False
- Нет Sale + был когда-то в положительном финальном статусе → is_returned=True
- Нет Sale + никогда не был в положительном статусе → is_returned=False
2. ВЫЗОВ update_is_returned_flag() в ключевых точках:
- После создания Sale (create_sale_on_order_completion)
- После отката Sale (rollback_sale_on_status_change)
- После освобождения резервов (release_reservations_on_cancellation)
3. ВАЛИДАЦИЯ в create_sale_on_order_completion:
- Запрещаем переход в положительный финальный статус (is_positive_end=True)
для заказов с is_returned=True, у которых нет резервов
- Даём понятное сообщение: резервы отсутствуют, товары могли быть проданы
в другом заказе, оставьте статус отрицательного исхода или создайте новый заказ
4. АВТОМАТИЧЕСКИЙ СБРОС is_returned:
- При законном переходе в положительный статус с резервами флаг сбрасывается
- Это позволяет исправить ошибочную отмену: cancelled → completed работает,
если резервы на месте (товар не ушёл в другой заказ)
5. УДАЛЕНА ДУБЛИРУЮЩАЯ ЛОГИКА:
- Убрали ручное управление is_returned в rollback_sale_on_status_change
- Убрали ручное управление is_returned в release_reservations_on_cancellation
- Теперь один источник истины через update_is_returned_flag()
Результат:
- Флаг is_returned всегда соответствует реальности (наличию Sale)
- Невозможно установить completed для возвращённого заказа без резервов
- Защита от двойного списания при переиспользовании витринных комплектов
- Понятные сообщения об ошибках для пользователя
- Предсказуемое поведение при любых комбинациях смены статусов
Проблема:
При отмене заказа (completed → cancelled) и последующем возврате в completed
витринные комплекты оставались зарезервированными и не уходили со склада.
Резервы не конвертировались в продажи, Sale не создавались.
Причина:
При откате заказа (уход от completed) мы обнуляли reservation.order_item = None
для витринных комплектов. Это разрывало связь между резервом и позицией заказа.
При повторном переходе в completed сигнал create_sale_on_order_completion
искал резервы по фильтру:
Reservation.objects.filter(order_item=item, product_kit=kit)
Но так как order_item был None, резервы не находились и Sale не создавались.
Решение:
Разделили семантику полей Reservation:
- order_item - принадлежность к позиции заказа (часть жизненного цикла заказа)
- cart_lock_expires_at, locked_by_user, cart_session_id - блокировки корзины
При откате заказа (completed → другой_статус):
- НЕ трогаем order_item - он остаётся привязанным к OrderItem
- Очищаем ТОЛЬКО cart lock поля (expires_at, locked_by_user, session_id)
- Резервы витринных комплектов: status = reserved
При повторном переходе в completed:
- create_sale_on_order_completion находит резервы (order_item сохранён!)
- Создаёт Sale для каждого компонента
- Конвертирует резервы: reserved → converted_to_sale
- Витринный экземпляр помечается как проданный через ShowcaseManager
Изменения в 3 местах:
1. rollback_sale_on_status_change - откат от completed
2. release_reservations_on_cancellation - переход к cancelled
3. release_stock_on_order_delete - удаление заказа
Во всех случаях для витринных комплектов сохраняем order_item, очищаем
только cart lock поля.
Результат:
Теперь витринные комплекты можно продавать/отменять/продавать снова
через смену статуса заказа в админке - как в реальной жизни.
Проблема:
При отмене заказа (completed → cancelled) резервы корректно возвращались
в статус 'reserved', но ShowcaseItem оставались в статусе 'sold'.
Из-за этого витринные букеты не отображались в POS после отмены заказа,
хотя физически должны были вернуться на витрину.
Решение:
В существующий сигнал rollback_sale_on_status_change добавлена логика
возврата витринных экземпляров на витрину:
1. После отката Sale и Reservation находим все ShowcaseItem, проданные
в рамках отменяемого заказа (sold_order_item__order=instance)
2. Для каждого экземпляра:
- Меняем status: sold → available
- Очищаем sold_order_item = None
- Очищаем sold_at = None
- НЕ трогаем showcase и product_kit (букет остаётся на той же витрине)
3. Логируем количество возвращённых экземпляров
Преимущества:
- Элегантно: вся логика отката в одном месте (сигнал)
- Транзакционно: откат Sale, Reservation и ShowcaseItem в одной транзакции
- Универсально: работает для POS и обычных заказов
- Без костылей: используем существующую архитектуру сигналов
Теперь при отмене заказа витринный букет автоматически возвращается
на витрину и снова виден в POS - как в реальной жизни.
Проблема:
При продаже витринного комплекта резервы оставались в статусе 'reserved'
вместо 'converted_to_sale'. Товары из состава комплекта не освобождались.
Причина:
В методе sell_showcase_items порядок операций был неправильный:
1. create_sale_from_reservation вызывался ПЕРВЫМ
2. reservation.order_item устанавливался ПОСЛЕ
В SaleProcessor.create_sale_from_reservation есть логика:
if order and reservation.order_item:
sale_price = reservation.order_item.price
else:
sale_price = reservation.product.actual_price
Так как order_item был None, цена бралась из product.actual_price,
а не из OrderItem, и резерв не конвертировался корректно.
Решение:
Правильный порядок операций:
1. Устанавливаем reservation.order_item = order_item
2. Сохраняем reservation
3. Вызываем create_sale_from_reservation (теперь order_item доступен)
4. Обновляем статус на 'converted_to_sale'
5. Сохраняем финальное состояние
Теперь резервы корректно преобразуются в продажи с правильной ценой
из позиции заказа, и товары освобождаются после продажи.
Проблема:
Валидация showcase_items выполнялась в двух местах:
1. В pos/views.py - проверка status='in_cart' БЕЗ блокировки БД
2. В showcase_manager.py - перезагрузка с select_for_update() и повторная проверка
Это создавало:
- Дублирование кода и логики
- Возможность race condition между двумя запросами
- Избыточные обращения к БД
- Мертвый код (неработающие logger.info)
Решение (best practices):
1. Views только загружают объекты по ID без фильтров по статусу
2. ВСЯ валидация и бизнес-логика в одном месте - ShowcaseManager.sell_showcase_items
3. select_for_update() гарантирует актуальность данных и блокировку на уровне БД
4. Удален мертвый код (logger.info которые не выполнялись)
5. Убрано избыточное логирование ошибок валидации
Результат:
- Единое место ответственности (Single Responsibility)
- Нет дублирования
- Атомарная транзакция с блокировкой
- Чистый, понятный код без костылей
Проблема:
При попытке продажи 2+ экземпляров одного витринного букета возникала ошибка
IntegrityError, так как поле sold_order_item было OneToOneField.
Это означало что к одному OrderItem мог быть привязан только один ShowcaseItem,
что делало невозможной продажу нескольких экземпляров в одной позиции заказа.
Решение:
1. Изменен тип поля sold_order_item с OneToOneField на ForeignKey
- Теперь несколько ShowcaseItem могут относиться к одному OrderItem
- related_name изменен с 'sold_showcase_item' на 'sold_showcase_items'
2. Обновлен метод mark_sold в модели ShowcaseItem
- Добавлена явная проверка статуса 'sold' перед продажей
- Генерируется ValidationError если экземпляр уже продан
- Удален комментарий про OneToOneField защиту
3. Обновлена обработка ошибок в ShowcaseManager.sell_showcase_items
- Убрана обработка IntegrityError
- Добавлена обработка ValidationError от mark_sold
4. Создана миграция 0012_change_sold_order_item_to_fk
Теперь можно успешно продавать 2 и более экземпляров одного витринного букета
в рамках одной позиции заказа.
Для диагностики проблемы с продажей витринных комплектов добавлено
логирование ValidationError с полным traceback. Это поможет определить
в какой именно момент и почему происходит ошибка валидации.
Проблема: Продолжает возникать ошибка 'Один из экземпляров уже был продан'
при попытке продажи витринных букетов.
Изменения для диагностики:
1. Добавлена валидация showcase_item_ids ПЕРЕД передачей в sell_showcase_items
2. Проверка что все экземпляры имеют status='in_cart' и locked_by_user=текущий
3. Фильтр по статусу исключает уже проданные/разобранные экземпляры
4. Добавлено детальное логирование:
- Запрошенные showcase_item_ids
- Количество найденных заблокированных экземпляров
- Недостающие IDs если не все найдены
Улучшенное сообщение об ошибке:
Вместо 'уже был продан' теперь 'уже не заблокированы на вас' с просьбой
обновить страницу - более понятно для пользователя.
Логи помогут выявить:
- Передаются ли дубликаты в showcase_item_ids
- Истекают ли блокировки до момента продажи
- Меняется ли статус экземпляров между добавлением в корзину и checkout
Проблема: При продаже 2+ экземпляров одного витринного комплекта возникала
ошибка 'Один из экземпляров уже был продан'. Это происходило потому что
объекты ShowcaseItem проверялись по старому состоянию из памяти.
Причина:
- При вызове sell_showcase_items() передавался список объектов из запроса
- Первый ShowcaseItem менял статус на 'sold' через mark_sold()
- Второй объект в списке все еще имел старый статус из памяти
- Проверка в цикле срабатывала некорректно
Решение:
- Перезагружаем ВСЕ ShowcaseItem из БД с блокировкой перед обработкой
- Используем select_for_update() для получения актуального статуса
- Теперь каждый экземпляр проверяется по свежим данным из БД
- Защита от race conditions через database-level locking
Результат:
Теперь можно продавать 2+ экземпляра одного букета без ошибок.
Теперь витринные букеты можно увеличивать и уменьшать по экземплярам:
UI изменения:
- Заменен badge на полноценные кнопки +/- как у обычных товаров
- Поле количества readonly с желтым фоном для визуального отличия
- Кнопки используют тот же дизайн что и для обычных товаров
Функционал увеличения (increaseShowcaseKitQty):
- Блокирует еще один доступный экземпляр через API
- Проверяет наличие свободных букетов на витрине
- Показывает сообщение если нет доступных
- Обновляет showcase_item_ids и qty в корзине
Функционал уменьшения (decreaseShowcaseKitQty):
- Снимает блокировку с последнего экземпляра из списка
- При qty=1 полностью удаляет из корзины
- Обновляет список витрины после изменения
Все операции синхронизируются с сервером и Redis.
Проблема: Витринные букеты исчезали из корзины после перезагрузки страницы,
но оставались заблокированными на пользователя через ShowcaseItem.
Решение:
- Изменена валидация корзины при загрузке из Redis
- Теперь проверяется наличие showcase_item_ids в данных корзины
- Блокировки валидируются через ShowcaseItem (не Reservation)
- Проверяется: status='in_cart', locked_by_user, cart_lock_expires_at > now()
- Обновляется qty на актуальное количество действующих блокировок
- Если ни один ShowcaseItem не заблокирован - не восстанавливается в корзину
Теперь витринные букеты корректно восстанавливаются при перезагрузке
и автоматически удаляются из корзины при истечении блокировки.
- Добавлена проверка наличия витринных комплектов (showcase_kit) в корзине
- Кнопка 'НА ВИТРИНУ' блокируется при наличии витринного букета
- Добавлено визуальное оформление: opacity 0.5, disabled state, tooltip
- Показывается предупреждение при попытке создать новый букет
- Функция updateShowcaseButtonState() вызывается при каждом изменении корзины
Проблема:
- Резервы документов списания помечались как 'converted_to_sale'
- Это вводило в заблуждение - списание это не продажа
- В админке резервы списания отображались как 'В продажу'
Решение:
- Добавлен новый статус 'converted_to_writeoff' в Reservation.STATUS_CHOICES
- Увеличен max_length поля status с 20 до 25 символов
- Обновлен WriteOffDocumentService.confirm_document() - теперь использует новый статус
- Обновлено описание поля converted_at (теперь для продажи ИЛИ списания)
- Создана миграция 0011_add_writeoff_status_to_reservation
Изменения:
- inventory/models.py: добавлен статус, увеличен max_length, обновлен help_text
- inventory/services/writeoff_document_service.py: используется converted_to_writeoff
- inventory/migrations/0011_*.py: миграция для изменений модели
Влияние:
- Чистая аналитика: можно отличить продажи от списаний
- Корректный учёт Stock: статус влияет на quantity_reserved
- Защита от ошибок при будущих доработках (откат списания)
- Строка поиска теперь отдельно на всю ширину (input-group)
- Переключатель вида (карточки/список) перенесён к фильтрам
- Переключатель прижат справа (ms-auto) рядом с чекбоксом
- Размер кнопок переключателя - btn-sm для компактности
- Чекбокс больше не прижат к краю (убран ms-auto)
- Более логичная группировка элементов
- Фильтры теперь в одну строку с flexbox
- Убраны обёртки col-auto, используется d-flex gap-2
- Селекты с width: auto для компактности
- Чекбокс 'Только в наличии' прижат к правому краю (ms-auto)
- Более компактный и чистый UI
- Убран класс product-picker-filters и удалён комментарий про счётчик
- Убран заголовок блока
- Строка поиска теперь большая (form-control-lg) и занимает всю ширину
- Добавлена иконка поиска слева от input
- Заголовок перенесен в placeholder строки поиска
- Переключатель вида теперь стандартного размера (не sm)
- Более чистый и современный UI
Проблема: можно было выбрать несколько товаров одновременно
Причина: при смене выделения старый товар не всегда корректно находился в DOM
Решение:
- Добавлен метод _clearAllSelections() для принудительной очистки всех выделений
- Исправлено сравнение ID (добавлен String() в строке 443)
- При выборе нового товара сначала снимаются ВСЕ выделения через querySelectorAll
- Затем выделяется только новый выбранный товар
- Обновлена версия JS (v=3) для сброса кэша
Теперь гарантирован истинный single-select режим
- Добавлено явное приведение к String при сравнении ID товаров
- Исправлена инициализация selected в методе destroy() (null вместо {})
- Добавлена версия к JS файлу (?v=2) для сброса кэша браузера
- Улучшены комментарии о том, что только ОДИН товар может быть выбран
- Гарантирован корректный single-select режим работы компонента