feat(pos): add write-off functionality for showcase kits

Add support for writing off showcase kits by creating a write-off document with components, converting reservations, and updating statuses.

- Add `write_off_from_showcase` static method to ShowcaseManager
- Add API endpoint `/pos/api/product-kits/<int:kit_id>/write-off/`
- Add write-off button to POS terminal UI
- Implement confirmation dialog with detailed information
- Add redirect to write-off document detail page after success

The write-off process includes:
1. Creating a write-off document in draft state
2. Converting existing reservations to write-off document items
3. Marking the showcase item as dismantled
4. Setting the product kit status to discontinued (if not already)

Breaking Changes: No
This commit is contained in:
2026-01-24 03:21:56 +03:00
parent 9c91a99189
commit 38fbf36731
5 changed files with 248 additions and 2 deletions

View File

@@ -666,6 +666,113 @@ class ShowcaseManager:
'message': f'Ошибка разбора: {str(e)}'
}
@staticmethod
def write_off_from_showcase(showcase_item, reason='spoilage', notes=None, created_by=None):
"""
Списывает экземпляр витринного комплекта:
1. Создаёт документ списания с компонентами комплекта
2. Преобразует резервы комплекта в позиции документа списания
3. Помечает экземпляр как разобранный
Args:
showcase_item: ShowcaseItem - экземпляр для списания
reason: str - причина списания (spoilage по умолчанию)
notes: str - примечания
created_by: User - пользователь
Returns:
dict: {
'success': bool,
'document_id': int,
'document_number': str,
'items_count': int,
'message': str,
'error': str (при ошибке)
}
"""
from inventory.services.writeoff_document_service import WriteOffDocumentService
# Проверка статуса
if showcase_item.status == 'sold':
return {
'success': False,
'document_id': None,
'message': 'Нельзя списать проданный экземпляр'
}
if showcase_item.status == 'dismantled':
return {
'success': False,
'document_id': None,
'message': 'Экземпляр уже разобран'
}
try:
with transaction.atomic():
warehouse = showcase_item.showcase.warehouse
product_kit = showcase_item.product_kit
# Создаём документ списания (черновик)
document = WriteOffDocumentService.create_document(
warehouse=warehouse,
date=timezone.now().date(),
notes=f'Списание витринного комплекта: {product_kit.name}',
created_by=created_by
)
# Получаем резервы этого экземпляра
reservations = Reservation.objects.filter(
showcase_item=showcase_item,
status='reserved'
).select_related('product')
items_count = 0
for reservation in reservations:
# Добавляем позицию в документ списания
# Используем add_item без создания резерва (меняем статус существующего)
from inventory.models import WriteOffDocumentItem
item = WriteOffDocumentItem.objects.create(
document=document,
product=reservation.product,
quantity=reservation.quantity,
reason=reason,
notes=notes
)
# Привязываем существующий резерв к позиции документа
reservation.writeoff_document_item = item
reservation.status = 'converted_to_writeoff'
reservation.converted_at = timezone.now()
reservation.save(update_fields=['writeoff_document_item', 'status', 'converted_at'])
items_count += 1
# Помечаем экземпляр как разобранный
showcase_item.status = 'dismantled'
showcase_item.save(update_fields=['status'])
# Помечаем шаблон комплекта как снятый
if product_kit.status != 'discontinued':
product_kit.status = 'discontinued'
product_kit.save(update_fields=['status'])
return {
'success': True,
'document_id': document.id,
'document_number': document.document_number,
'items_count': items_count,
'message': f'Создан документ {document.document_number} с {items_count} позициями'
}
except Exception as e:
return {
'success': False,
'document_id': None,
'message': f'Ошибка списания: {str(e)}'
}
@staticmethod
def get_showcase_items_for_pos(showcase=None):
"""