diff --git a/myproject/inventory/forms.py b/myproject/inventory/forms.py index 2eea45e..7fb826c 100644 --- a/myproject/inventory/forms.py +++ b/myproject/inventory/forms.py @@ -328,7 +328,7 @@ class IncomingModelForm(forms.ModelForm): super().__init__(*args, **kwargs) # Фильтруем только активные товары self.fields['product'].queryset = Product.objects.filter( - is_active=True + status='active' ).order_by('name') def clean_quantity(self): diff --git a/myproject/inventory/migrations/0005_reservation_product_kit_and_more.py b/myproject/inventory/migrations/0005_reservation_product_kit_and_more.py new file mode 100644 index 0000000..5a9222e --- /dev/null +++ b/myproject/inventory/migrations/0005_reservation_product_kit_and_more.py @@ -0,0 +1,25 @@ +# Generated by Django 5.0.10 on 2025-11-20 12:09 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('inventory', '0004_showcase_is_default_and_more'), + ('orders', '0003_historicalorderitem_is_from_showcase_and_more'), + ('products', '0008_productkit_showcase_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='reservation', + name='product_kit', + field=models.ForeignKey(blank=True, help_text='Временный комплект, для которого создан резерв', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='reservations', to='products.productkit', verbose_name='Комплект'), + ), + migrations.AddIndex( + model_name='reservation', + index=models.Index(fields=['product_kit'], name='inventory_r_product_70aed5_idx'), + ), + ] diff --git a/myproject/inventory/models.py b/myproject/inventory/models.py index 794f41a..119b35d 100644 --- a/myproject/inventory/models.py +++ b/myproject/inventory/models.py @@ -412,6 +412,10 @@ class Reservation(models.Model): related_name='reservations', verbose_name="Витрина", null=True, blank=True, help_text="Витрина, на которой выложен букет") + product_kit = models.ForeignKey('products.ProductKit', on_delete=models.CASCADE, + related_name='reservations', verbose_name="Комплект", + null=True, blank=True, + help_text="Временный комплект, для которого создан резерв") product = models.ForeignKey(Product, on_delete=models.CASCADE, related_name='reservations', verbose_name="Товар") warehouse = models.ForeignKey(Warehouse, on_delete=models.CASCADE, @@ -433,11 +437,14 @@ class Reservation(models.Model): models.Index(fields=['status']), models.Index(fields=['order_item']), models.Index(fields=['showcase']), + models.Index(fields=['product_kit']), ] def __str__(self): if self.order_item: context = f" (заказ {self.order_item.order.order_number})" + elif self.product_kit: + context = f" (комплект {self.product_kit.name})" elif self.showcase: context = f" (витрина {self.showcase.name})" else: diff --git a/myproject/inventory/services/showcase_manager.py b/myproject/inventory/services/showcase_manager.py index bab2fce..a60bdc5 100644 --- a/myproject/inventory/services/showcase_manager.py +++ b/myproject/inventory/services/showcase_manager.py @@ -67,6 +67,7 @@ class ShowcaseManager: product=kit_item.product, warehouse=warehouse, showcase=showcase, + product_kit=product_kit, quantity=component_quantity, status='reserved' ) @@ -84,6 +85,7 @@ class ShowcaseManager: product=first_variant.product, warehouse=warehouse, showcase=showcase, + product_kit=product_kit, quantity=component_quantity, status='reserved' ) @@ -250,10 +252,8 @@ class ShowcaseManager: ) if product_kit: - # Если указан конкретный комплект, фильтруем резервы - # TODO: добавить связь резерва с конкретным экземпляром комплекта - # Пока освобождаем все резервы витрины - pass + # Если указан конкретный комплект, фильтруем только его резервы + reservations = reservations.filter(product_kit=product_kit) released_count = reservations.count() @@ -264,6 +264,11 @@ class ShowcaseManager: 'message': f'На витрине "{showcase.name}" нет активных резервов' } + # Сохраняем список затронутых товаров и склад ДО обновления резервов + from inventory.models import Stock + affected_products = list(reservations.values_list('product_id', flat=True).distinct()) + warehouse = showcase.warehouse + # Освобождаем резервы reservations.update( status='released', @@ -272,13 +277,11 @@ class ShowcaseManager: ) # Обновляем агрегаты Stock - from inventory.models import Stock - affected_products = reservations.values_list('product_id', flat=True).distinct() for product_id in affected_products: try: stock = Stock.objects.get( product_id=product_id, - warehouse=showcase.warehouse + warehouse=warehouse ) stock.refresh_from_batches() except Stock.DoesNotExist: diff --git a/myproject/pos/static/pos/js/terminal.js b/myproject/pos/static/pos/js/terminal.js index 748240f..84dba53 100644 --- a/myproject/pos/static/pos/js/terminal.js +++ b/myproject/pos/static/pos/js/terminal.js @@ -894,7 +894,10 @@ async function openEditKitModal(kitId) { // Меняем заголовок и кнопку document.getElementById('createTempKitModalLabel').textContent = 'Редактирование витринного букета'; document.getElementById('confirmCreateTempKit').textContent = 'Сохранить изменения'; - + + // Показываем кнопку "Разобрать" в режиме редактирования + document.getElementById('disassembleKitBtn').style.display = 'block'; + // Открываем модальное окно const modal = new bootstrap.Modal(document.getElementById('createTempKitModal')); modal.show(); @@ -1234,6 +1237,58 @@ document.getElementById('confirmCreateTempKit').onclick = async () => { } }; +// Обработчик кнопки "Разобрать букет" +document.getElementById('disassembleKitBtn').addEventListener('click', async () => { + if (!isEditMode || !editingKitId) { + alert('Ошибка: режим редактирования не активен'); + return; + } + + // Запрос подтверждения + const confirmed = confirm( + 'Вы уверены?\n\n' + + 'Букет будет разобран:\n' + + '• Все резервы компонентов будут освобождены\n' + + '• Комплект будет помечен как "Снят"\n\n' + + 'Это действие нельзя отменить!' + ); + + if (!confirmed) { + return; + } + + try { + const response = await fetch(`/pos/api/product-kits/${editingKitId}/disassemble/`, { + method: 'POST', + headers: { + 'X-CSRFToken': getCookie('csrftoken') + } + }); + + const data = await response.json(); + + if (data.success) { + alert(`✅ ${data.message}\n\nОсвобождено резервов: ${data.released_count}`); + + // Закрываем модальное окно + const modal = bootstrap.Modal.getInstance(document.getElementById('createTempKitModal')); + modal.hide(); + + // Обновляем витринные комплекты + isShowcaseView = true; + currentCategoryId = null; + await refreshShowcaseKits(); + renderCategories(); + renderProducts(); + } else { + alert(`❌ Ошибка: ${data.error}`); + } + } catch (error) { + console.error('Error disassembling kit:', error); + alert('Произошла ошибка при разборе букета'); + } +}); + // Вспомогательная функция для получения CSRF токена function getCookie(name) { let cookieValue = null; @@ -1259,10 +1314,13 @@ document.getElementById('createTempKitModal').addEventListener('hidden.bs.modal' // Сбрасываем режим редактирования isEditMode = false; editingKitId = null; - + // Восстанавливаем заголовок и текст кнопки document.getElementById('createTempKitModalLabel').textContent = 'Создать витринный букет из корзины'; document.getElementById('confirmCreateTempKit').innerHTML = ' Создать и зарезервировать'; + + // Скрываем кнопку "Разобрать" + document.getElementById('disassembleKitBtn').style.display = 'none'; } }); diff --git a/myproject/pos/templates/pos/terminal.html b/myproject/pos/templates/pos/terminal.html index 6bfe6b6..7557d45 100644 --- a/myproject/pos/templates/pos/terminal.html +++ b/myproject/pos/templates/pos/terminal.html @@ -252,6 +252,12 @@