- 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 для оприходования
- Тип поступления отображается в списках приходов и партий
- Изменён 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)
Теперь все временные метки заказов соответствуют реальному минскому времени
- Добавлены методы reserve_product_to_showcase и release_showcase_reservation в ShowcaseManager
- Методы работают с резервами для всех активных экземпляров витринного комплекта
- НЕ блокируют создание резерва при нехватке товара, возвращают информацию о дефиците (overdraft)
- Обновлён API endpoint update_product_kit для корректировки резервов при изменении состава
- Добавлено визуальное предупреждение на фронте о нехватке товара на складе
- В модалке редактирования комплекта добавлены контролы для изменения количества товаров (+/-, поле ввода, удаление)
- Автоматический пересчёт цен при изменении состава
- Очистка корзины POS после успешного создания витринного комплекта
Проблема:
Сообщение 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. Сохраняем финальное состояние
Теперь резервы корректно преобразуются в продажи с правильной ценой
из позиции заказа, и товары освобождаются после продажи.
Проблема:
При попытке продажи 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 и более экземпляров одного витринного букета
в рамках одной позиции заказа.
Проблема: При продаже 2+ экземпляров одного витринного комплекта возникала
ошибка 'Один из экземпляров уже был продан'. Это происходило потому что
объекты ShowcaseItem проверялись по старому состоянию из памяти.
Причина:
- При вызове sell_showcase_items() передавался список объектов из запроса
- Первый ShowcaseItem менял статус на 'sold' через mark_sold()
- Второй объект в списке все еще имел старый статус из памяти
- Проверка в цикле срабатывала некорректно
Решение:
- Перезагружаем ВСЕ ShowcaseItem из БД с блокировкой перед обработкой
- Используем select_for_update() для получения актуального статуса
- Теперь каждый экземпляр проверяется по свежим данным из БД
- Защита от race conditions через database-level locking
Результат:
Теперь можно продавать 2+ экземпляра одного букета без ошибок.
Проблема:
- Резервы документов списания помечались как '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
- Защита от ошибок при будущих доработках (откат списания)
Проблема: можно было выбрать несколько товаров одновременно
Причина: при смене выделения старый товар не всегда корректно находился в DOM
Решение:
- Добавлен метод _clearAllSelections() для принудительной очистки всех выделений
- Исправлено сравнение ID (добавлен String() в строке 443)
- При выборе нового товара сначала снимаются ВСЕ выделения через querySelectorAll
- Затем выделяется только новый выбранный товар
- Обновлена версия JS (v=3) для сброса кэша
Теперь гарантирован истинный single-select режим
- Добавлено явное приведение к String при сравнении ID товаров
- Исправлена инициализация selected в методе destroy() (null вместо {})
- Добавлена версия к JS файлу (?v=2) для сброса кэша браузера
- Улучшены комментарии о том, что только ОДИН товар может быть выбран
- Гарантирован корректный single-select режим работы компонента
- Удалён весь функционал множественного выбора
- Удалены кнопки 'Выбрать все' и 'Сбросить'
- Удалён счётчик выбранных товаров
- state.selected теперь содержит один объект вместо словаря
- Убраны параметры multi_select, max_selection, show_select_all
- onAddSelected теперь возвращает объект вместо массива
- Удалены методы getSelectedIds() и setSelection()
- Упрощена логика _toggleProduct для single-select
- Обновлены все callback'и для работы с одним товаром
- Компонент стал значительно проще и понятнее
- Колонка 'Количество' фиксирована на 120px
- Колонка 'Причина' фиксирована на 150px
- Колонка 'Действия' фиксирована на 100px
- Input поле количества с margin-left: auto для выравнивания справа
- Таблица больше не 'прыгает' при переключении в режим редактирования
- Улучшена визуальная стабильность интерфейса
- Используется фильтр smart_quantity вместо floatformat
- Целые числа отображаются без дробной части: 2 вместо 2,000
- Дробные числа без лишних нулей: 2,5 вместо 2,500
- Запятая используется вместо точки (русский формат)
- JavaScript также форматирует количество после сохранения
- Улучшена читаемость для работы с цветами и штучными товарами
- Реализовано редактирование количества, причины и примечаний прямо в таблице
- Кнопка редактирования (карандаш) включает режим редактирования
- Кнопка сохранения (галочка) отправляет изменения на сервер через AJAX
- Кнопка отмены восстанавливает исходные значения
- Автофокус на поле количества при входе в режим редактирования
- Spinner при сохранении для визуальной обратной связи
- Не нужно удалять и заново добавлять позицию при ошибке в количестве
- Убрана правая боковая панель с формой
- Перенесён поиск товаров и форма в центральную карточку 'Добавить позицию в документ'
- Форма теперь располагается горизонтально под поиском
- Кнопка изменена на явную: 'Добавить в документ' с иконкой check-circle
- Добавлена подсказка об использовании поиска
- Улучшена визуальная иерархия: информация о документе → добавление позиции → таблица позиций
- Более простой и понятный UX для пользователей
- Добавлен параметр warehouse в API search_products_and_variants
- API фильтрует товары по наличию на указанном складе через Stock
- Обновлен _apply_product_filters для поддержки warehouse_id
- ProductSearchPicker теперь поддерживает data-warehouse-id
- Warehouse автоматически передается в AJAX запросы
- В WriteOffDocumentDetailView добавлены categories и tags в контекст
- Компонент поиска встроен в detail.html с жестким фильтром по складу документа
- Single-select режим для выбора одного товара
- JS автоматически заполняет select формы при выборе товара
- Отображение выбранного товара с фото и артикулом
- Автофокус на поле количества после выбора товара
- Пользователь видит только товары доступные на складе документа
- Добавлен пункт 'Документы списания' в выпадающее меню 'Операции' в base_inventory_minimal.html
- Добавлена карточка 'Документы списания' на главную страницу склада (home.html)
- Новая функциональность теперь доступна из двух мест для удобства пользователей
- Старый функционал одиночных списаний остается доступным
- Зарегистрированы модели WriteOffDocument и WriteOffDocumentItem в админке
- Настроен inline для позиций документа в админке
- Добавлены цветовые индикаторы статусов документа
- Настроены фильтры, поиск и сортировка для удобной работы
- Добавлен сигнал release_reservation_on_writeoff_item_delete
- Автоматическое освобождение резервов при удалении позиций через админку
- Защита от утечки резервов при прямом удалении через ORM
- В WriteOffCreateView добавлена передача категорий и тегов в контекст
- Шаблон writeoff_form.html обновлен с использованием product_search_picker
- Автоматическая фильтрация партий по выбранному товару
- Отображение информации о выбранном товаре с фото
- Улучшенный UX при выборе товара для списания
- Подключены CSS и JS компонента поиска товаров
- list.html - список документов с фильтрацией по статусу и складу
- form.html - форма создания документа
- detail.html - детальный просмотр документа с возможностью добавления/редактирования позиций
- Интерактивное управление позициями через AJAX (добавление, редактирование, удаление)
- Отображение статистики документа (количество позиций, общее количество, себестоимость)
- Кнопки проведения и отмены документа с подтверждением
- Адаптивный дизайн с использованием Bootstrap 5
- WriteOffDocumentListView - список документов с пагинацией
- WriteOffDocumentCreateView - создание нового документа
- WriteOffDocumentDetailView - детальный просмотр документа
- WriteOffDocumentAddItemView - добавление позиции (AJAX)
- WriteOffDocumentUpdateItemView - обновление позиции (AJAX)
- WriteOffDocumentRemoveItemView - удаление позиции (AJAX)
- WriteOffDocumentConfirmView - проведение документа
- WriteOffDocumentCancelView - отмена документа
- Добавлены URL-маршруты для всех операций с документами списания
- Поддержка AJAX запросов для динамической работы с позициями
- WriteOffDocumentForm - создание/редактирование документа списания
- WriteOffDocumentItemForm - добавление/редактирование позиций документа
- Автоматическая установка текущей даты и склада по умолчанию
- Фильтрация товаров по наличию на выбранном складе
- Валидация количества с проверкой доступных остатков
- Учет текущего резерва при редактировании позиций
- Создан WriteOffDocumentService с методами работы с документами списания
- create_document() - создание документа с автогенерацией номера (WO-XXXXXX)
- add_item() - добавление позиции с автоматическим созданием резерва
- update_item() - обновление позиции с пересчетом резерва
- remove_item() - удаление позиции с освобождением резерва
- confirm_document() - проведение документа (создание WriteOff записей по FIFO)
- cancel_document() - отмена с освобождением всех резервов
- Добавлена валидация доступного количества товара при создании/обновлении позиций
- Добавлена функция generate_writeoff_document_number() для генерации номеров документов
- Создана модель WriteOffDocument для коллективного списания с поддержкой статусов (черновик/проведен/отменен)
- Добавлена модель WriteOffDocumentItem для позиций документа
- Расширена модель Reservation связью с WriteOffDocumentItem для резервирования товара в черновике
- Добавлен тип счетчика 'writeoff' в DocumentCounter для автонумерации
- Реализована логика резервирования товара в черновике документа (уменьшает quantity_free)
- При проведении документа создаются WriteOff записи по методу FIFO
Новая архитектура:
- ShowcaseItem модель - физический экземпляр букета на витрине
- OneToOneField(sold_order_item) - БД-уровневая защита от двойной продажи
- Поддержка создания нескольких экземпляров одного букета
- Возможность продавать N из M доступных (например 2 из 5)
Изменения:
- inventory/models.py: добавлена модель ShowcaseItem с методами lock/unlock/mark_sold
- inventory/services/showcase_manager.py: переработан для работы с ShowcaseItem
- pos/views.py: API поддерживает quantity и showcase_item_ids
- pos/templates/pos/terminal.html: поле "Сколько букетов создать"
- pos/static/pos/js/terminal.js: выбор количества, передача showcase_item_ids
Миграции:
- 0007: создание модели ShowcaseItem
- 0008: data migration существующих букетов
- 0009: очистка ShowcaseItem для уже проданных букетов
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Проблема: При переводе заказа в статус 'completed' возникала ошибка
"Не удалось создать Sale для заказа", т.к. резервы этого же заказа
блокировали списание товара.
Причина: write_off_by_fifo() считал все резервы со статусом 'reserved'
как занятые, включая резервы текущего заказа, которые ещё не были
переведены в 'converted_to_sale'.
Решение:
- Добавлен параметр exclude_order в write_off_by_fifo() для исключения
резервов конкретного заказа из расчёта занятого товара
- SaleProcessor.create_sale() теперь передаёт order в write_off_by_fifo()
- Добавлены транзакции в views для атомарности операций с заказами:
при ошибке в сигналах статус заказа откатывается вместе со всеми
связанными изменениями
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Проблема: при отмене заказа с витринным временным комплектом резервы освобождались
(status='released'), и букет исчезал с витрины. Это неправильное поведение для временных
комплектов на витрине - они должны оставаться доступными для продажи.
Решение:
- В сигнале rollback_sale_on_status_change добавлено разделение резервов на:
* Обычные резервы - работают как раньше (released при отмене, reserved при возврате)
* Витринные временные комплекты (is_temporary=True, showcase!=null) - ВСЕГДА возвращаются
в статус reserved, независимо от типа отката заказа
- Для витринных комплектов сохраняются привязки к showcase и product_kit
- Букеты остаются видимыми на витрине и доступны для повторной продажи
Бизнес-логика:
- При ВОЗВРАТЕ (completed → draft/in_delivery): букет возвращается на витрину
- При ОТМЕНЕ (completed → cancelled): букет ТАКЖЕ возвращается на витрину
- Букет можно убрать только вручную через функцию разбора комплекта
- Добавлена специальная обработка витринных комплектов в сигнале update_reservation_on_item_change:
* При создании OrderItem с витринным комплектом привязываются существующие витринные резервы компонентов
* Не создаются новые резервы на уровне комплекта
- Исправлена логика создания Sale для комплектов в сигнале create_sale_on_order_completion:
* Для комплектов (витринных и обычных) создаются Sale для каждого компонента через резервы
* Используется FIFO-списание для компонентов
* Предотвращена ошибка передачи ProductKit в поле Reservation.product
Fixes: Cannot assign ProductKit to Reservation.product field
Fixes: Не удалось создать Sale для заказа с витринным комплектом
- Исправлен метод write_off_by_fifo() для учета зарезервированных партий
- Добавлено автоматическое проставление даты и времени при создании заказов в POS
- Исправлена ошибка фильтрации Product (is_active -> status='active') в transfers
- Предотвращает списание из зарезервированных партий, устраняя отрицательные остатки
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Удалены из git:
- Скрипты активации и диагностики тенантов
- Тестовые файлы (test_*.py, test_*.txt)
- SQL скрипты для отладки
- Backup файлы (*.backup, *.old)
- Дубликат .gitignore из myproject/
Файлы остались на диске, но теперь игнорируются git.
В репозитории остались только:
- myproject/ (основной код проекта)
- requirements.txt
- .gitignore
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
- Удалены все файлы .md (30 файлов)
- Добавлена маска *.md в .gitignore для защиты от будущих коммитов
- Причина: .md файлы содержали примеры паролей и внутреннюю документацию
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
- Added all real order statuses: new, confirmed, in_assembly, in_delivery, return
- Test #2: expanded to test cancellation from 4 different statuses (new, confirmed, in_assembly, in_delivery)
- Test #3: updated to use new → cancelled → in_assembly flow
- Test #4: updated to use in_delivery status instead of packing
- All 5 tests passing successfully
Проблема:
- Поле OrderStatus.color имеет max_length=7 (для hex типа #FF5733)
- В тестах использовались названия: 'secondary', 'warning', 'success' (9 символов)
- PostgreSQL выдавал ошибку: value too long for type character varying(7)
Решение:
- Заменены названия цветов на hex коды:
* 'secondary' → '#9E9E9E' (серый)
* 'warning' → '#FF9800' (оранжевый)
* 'success' → '#4CAF50' (зелёный)
* 'danger' → '#F44336' (красный)
* 'info' → '#17A2B8' (голубой)
Tests cover:
1. Multiple status transitions (draft->completed->cancelled->completed)
2. Cancellation from draft (reservation release)
3. Un-cancellation to pending (reservation restore)
4. Creating order with intermediate status
5. Rollback from completed to draft
Each test verifies:
- Stock state (available, reserved, free)
- Reservation status transitions
- Sale creation/deletion without duplicates
- StockBatch quantity changes
Files:
- inventory/tests/test_order_status_transitions.py (570 lines)
- inventory/tests/README.md (138 lines)
- inventory/tests/__init__.py
- run_status_tests.bat (launch script)