From ff0756498c66ba80d5dc80a49b6f9406f02b53d5 Mon Sep 17 00:00:00 2001 From: Andrey Smakotin Date: Thu, 20 Nov 2025 23:03:47 +0300 Subject: [PATCH] Fix Product filtering and add kit disassembly functionality MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixed: - Replace is_active with status='active' for Product filtering in IncomingModelForm - Product model uses status field instead of is_active Added: - Showcase field to ProductKit for tracking showcase placement - product_kit field to Reservation for tracking kit-specific reservations - Disassemble button in POS terminal for showcase kits - API endpoint for kit disassembly (release reservations, mark discontinued) - Improved reservation filtering when dismantling specific kits Changes: - ShowcaseManager now links reservations to specific kit instances - POS terminal modal shows disassemble button in edit mode - Kit disassembly properly updates stock aggregates πŸ€– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- myproject/inventory/forms.py | 2 +- .../0005_reservation_product_kit_and_more.py | 25 +++++++ myproject/inventory/models.py | 7 ++ .../inventory/services/showcase_manager.py | 17 +++-- myproject/pos/static/pos/js/terminal.js | 62 +++++++++++++++- myproject/pos/templates/pos/terminal.html | 6 ++ myproject/pos/urls.py | 2 + myproject/pos/views.py | 73 ++++++++++++++++++- myproject/products/admin.py | 3 - .../0008_productkit_showcase_and_more.py | 27 +++++++ myproject/products/models/kits.py | 11 +++ 11 files changed, 221 insertions(+), 14 deletions(-) create mode 100644 myproject/inventory/migrations/0005_reservation_product_kit_and_more.py create mode 100644 myproject/products/migrations/0008_productkit_showcase_and_more.py 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 @@