- Автоматическое проведение документов списания и оприходования после завершения инвентаризации
- Оптимизация SQL-запросов: устранение N+1, bulk-операции для Stock, агрегация для StockBatch и Reservation
- Изменение формулы расчета разницы: (quantity_fact + quantity_reserved) - quantity_available
- Переименование поля 'По факту' в 'Подсчитано (факт, свободные)'
- Добавлены столбцы 'В резервах' и 'Всего на складе' в таблицу инвентаризации
- Перемещение столбца 'В системе (свободно)' после 'В резервах' с визуальным выделением
- Центральное выравнивание значений в столбцах таблицы
- Автоматическое выделение текста при фокусе на поле ввода количества
- Исправление форматирования разницы (убраны лишние нули)
- Изменение статуса 'Не обработана' на 'Не проведено'
- Добавление номера документа для инвентаризаций (INV-XXXXXX)
- Отображение всех типов списаний в debug-странице (WriteOff, WriteOffDocument, WriteOffDocumentItem)
- Улучшение отображения документов в детальном просмотре инвентаризации с возможностью перехода к ним
- Исправлен POS для использования миниатюр вместо оригиналов для быстрой загрузки
- Убран fallback на оригиналы - показываем миниатюру или ничего (лучше видно ошибки)
- Исправлен ImageService - возвращает пустую строку если миниатюра обработанного файла не найдена
- Исправлена ошибка JavaScript при массовом удалении фото (insertAdjacentElement на null)
- Добавлен контейнер photos-messages-container для надежного отображения сообщений
- Улучшено логирование ImageService для отладки путей к файлам
- Добавлена проверка exists() с детальным логированием в TenantAwareFileSystemStorage
- Fix MEDIA_ROOT path to match Docker volume mount (/app/myproject/media)
- Update docker-compose.yml volume mounts to match MEDIA_ROOT
- Add setup_directories() function in entrypoint.sh to create media directories with proper permissions
- Add logging to TenantAwareFileSystemStorage for debugging
- Fix is_returned flag logic improvements (from previous work)
- Унифицирован формат номеров документов: IN-XXXXXX (6 цифр), как WO-XXXXXX и MOVE-XXXXXX
- Убрано дублирование функции _extract_number_from_document_number
- Оптимизирована инициализация счетчика incoming: быстрая проверка перед полной инициализацией
- Удален неиспользуемый файл utils.py (функциональность перенесена в document_generator.py)
- Все функции генерации номеров используют единый подход через DocumentCounter.get_next_value()
- Добавлено поле receipt_type в модель IncomingBatch с типами: supplier, inventory, adjustment
- Исправлен баг в InventoryProcessor: теперь корректно создается IncomingBatch при инвентаризации
- Создан IncomingAdjustmentCreateView для оприходования без инвентаризации
- Обновлены формы, шаблоны и админка для поддержки разных типов поступлений
- Добавлена навигация и URL для оприходования
- Тип поступления отображается в списках приходов и партий
- Добавлены системные статусы partially_assembled и fully_assembled в order_status_service.py
- Создана management команда update_order_statuses для обновления статусов у всех тенантов
- Новые статусы интегрируются в существующую логику резервирования и списания товара
- Статусы располагаются между 'В сборке' и 'В доставке' в естественном порядке процесса
- Добавлен Prefetch для активных товаров и комплектов в категориях
- Фильтрация и сортировка вынесены в Prefetch (избегаем повторных запросов)
- Изменен метод build_category_tree для использования предзагруженных данных
Результаты:
- Список категорий: 12→7 запросов, 26.76→~10мс
- Устранены 4 похожих N+1 запроса (products и kits для каждой категории)
- Реализован универсальный поиск по имени, 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