ShowcaseItem: защита от двойной продажи витринных букетов

Новая архитектура:
- 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>
This commit is contained in:
2025-12-09 23:51:37 +03:00
parent 936d2275e4
commit cfc6ce451e
8 changed files with 1076 additions and 318 deletions

View File

@@ -0,0 +1,65 @@
# Generated manually - Fix ShowcaseItem status for already sold kits
from django.db import migrations
def fix_showcase_items_status(apps, schema_editor):
"""
Исправляем статус ShowcaseItem для уже проданных комплектов.
Логика:
- Если у ShowcaseItem нет активных резервов (status='reserved') →
это уже проданный/разобранный букет → удаляем ShowcaseItem
"""
ShowcaseItem = apps.get_model('inventory', 'ShowcaseItem')
Reservation = apps.get_model('inventory', 'Reservation')
# Находим все ShowcaseItem в статусе 'available'
available_items = ShowcaseItem.objects.filter(status='available')
items_to_delete = []
for item in available_items:
# Проверяем есть ли активные резервы для этого экземпляра
has_active_reservations = Reservation.objects.filter(
showcase_item=item,
status='reserved'
).exists()
# Если резервы не привязаны к showcase_item, проверяем старым способом
if not has_active_reservations:
has_active_reservations = Reservation.objects.filter(
product_kit=item.product_kit,
showcase=item.showcase,
status='reserved'
).exists()
if not has_active_reservations:
# Нет активных резервов - этот букет уже продан/разобран
items_to_delete.append(item.id)
# Удаляем ShowcaseItem без активных резервов
if items_to_delete:
ShowcaseItem.objects.filter(id__in=items_to_delete).delete()
def reverse_migration(apps, schema_editor):
"""
Откат невозможен - удалённые ShowcaseItem не восстановить.
Но это безопасно - они относились к уже проданным букетам.
"""
pass
class Migration(migrations.Migration):
dependencies = [
('inventory', '0008_migrate_showcase_kits_to_items'),
]
operations = [
migrations.RunPython(
fix_showcase_items_status,
reverse_code=reverse_migration
),
]