From 8f6acfb36499073cd237416bd14d431c0c9cedd9 Mon Sep 17 00:00:00 2001 From: Andrey Smakotin Date: Sun, 16 Nov 2025 21:12:22 +0300 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D0=B0=20=D1=84=D1=83=D0=BD=D0=BA=D1=86=D0=B8=D0=BE=D0=BD?= =?UTF-8?q?=D0=B0=D0=BB=D1=8C=D0=BD=D0=BE=D1=81=D1=82=D1=8C=20=D0=B2=D0=B8?= =?UTF-8?q?=D1=82=D1=80=D0=B8=D0=BD=20=D0=B4=D0=BB=D1=8F=20POS:=20=D0=BC?= =?UTF-8?q?=D0=BE=D0=B4=D0=B5=D0=BB=D0=B8,=20=D1=81=D0=B5=D1=80=D0=B2?= =?UTF-8?q?=D0=B8=D1=81=D1=8B,=20UI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Создана модель Showcase (витрина) привязанная к складу - Расширена Reservation для поддержки витринных резервов - Добавлены поля в OrderItem для маркировки витринных продаж - Реализован ShowcaseManager с методами резервирования, продажи и разбора - Обновлён админ-интерфейс для управления витринами - Добавлена кнопка Витрина в POS (категории) и API для просмотра - Добавлена кнопка На витрину в панели действий POS - Миграции готовы к применению --- myproject/inventory/admin.py | 43 ++- ..._showcase_reservation_showcase_and_more.py | 50 +++ myproject/inventory/models.py | 44 ++- myproject/inventory/services/__init__.py | 2 + .../inventory/services/sale_processor.py | 34 ++ .../inventory/services/showcase_manager.py | 334 ++++++++++++++++++ ...icalorderitem_is_from_showcase_and_more.py | 44 +++ myproject/orders/models.py | 19 + myproject/pos/static/pos/js/terminal.js | 47 +++ myproject/pos/templates/pos/terminal.html | 3 +- myproject/pos/urls.py | 1 + myproject/pos/views.py | 45 +++ 12 files changed, 653 insertions(+), 13 deletions(-) create mode 100644 myproject/inventory/migrations/0003_showcase_reservation_showcase_and_more.py create mode 100644 myproject/inventory/services/showcase_manager.py create mode 100644 myproject/orders/migrations/0003_historicalorderitem_is_from_showcase_and_more.py diff --git a/myproject/inventory/admin.py b/myproject/inventory/admin.py index 51e2688..035ac91 100644 --- a/myproject/inventory/admin.py +++ b/myproject/inventory/admin.py @@ -7,10 +7,29 @@ from decimal import Decimal from inventory.models import ( Warehouse, StockBatch, Incoming, IncomingBatch, Sale, WriteOff, Transfer, Inventory, InventoryLine, Reservation, Stock, StockMovement, - SaleBatchAllocation + SaleBatchAllocation, Showcase ) +# ===== SHOWCASE ===== +@admin.register(Showcase) +class ShowcaseAdmin(admin.ModelAdmin): + list_display = ('name', 'warehouse', 'is_active', 'created_at') + list_filter = ('is_active', 'warehouse', 'created_at') + search_fields = ('name', 'warehouse__name') + date_hierarchy = 'created_at' + fieldsets = ( + ('Основная информация', { + 'fields': ('name', 'warehouse', 'description', 'is_active') + }), + ('Даты', { + 'fields': ('created_at', 'updated_at'), + 'classes': ('collapse',) + }), + ) + readonly_fields = ('created_at', 'updated_at') + + # ===== WAREHOUSE ===== @admin.register(Warehouse) class WarehouseAdmin(admin.ModelAdmin): @@ -269,13 +288,13 @@ class InventoryAdmin(admin.ModelAdmin): # ===== RESERVATION ===== @admin.register(Reservation) class ReservationAdmin(admin.ModelAdmin): - list_display = ('product', 'warehouse', 'quantity', 'status_display', 'order_info', 'reserved_at') - list_filter = ('status', 'reserved_at', 'warehouse') - search_fields = ('product__name', 'order_item__order__order_number') + list_display = ('product', 'warehouse', 'quantity', 'status_display', 'context_info', 'reserved_at') + list_filter = ('status', 'reserved_at', 'warehouse', 'showcase') + search_fields = ('product__name', 'order_item__order__order_number', 'showcase__name') date_hierarchy = 'reserved_at' fieldsets = ( ('Резерв', { - 'fields': ('product', 'warehouse', 'quantity', 'status', 'order_item') + 'fields': ('product', 'warehouse', 'quantity', 'status', 'order_item', 'showcase') }), ('Даты', { 'fields': ('reserved_at', 'released_at', 'converted_at') @@ -296,11 +315,19 @@ class ReservationAdmin(admin.ModelAdmin): ) status_display.short_description = 'Статус' - def order_info(self, obj): + def context_info(self, obj): if obj.order_item: - return f"ORD-{obj.order_item.order.order_number}" + return format_html( + '📎 Заказ ORD-{}', + obj.order_item.order.order_number + ) + elif obj.showcase: + return format_html( + '🌺 Витрина: {}', + obj.showcase.name + ) return "-" - order_info.short_description = 'Заказ' + context_info.short_description = 'Контекст' # ===== STOCK ===== diff --git a/myproject/inventory/migrations/0003_showcase_reservation_showcase_and_more.py b/myproject/inventory/migrations/0003_showcase_reservation_showcase_and_more.py new file mode 100644 index 0000000..6e9fb73 --- /dev/null +++ b/myproject/inventory/migrations/0003_showcase_reservation_showcase_and_more.py @@ -0,0 +1,50 @@ +# Generated by Django 5.0.10 on 2025-11-16 18:07 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('inventory', '0002_initial'), + ('orders', '0002_initial'), + ('products', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='Showcase', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=200, verbose_name='Название')), + ('description', models.TextField(blank=True, null=True, verbose_name='Описание')), + ('is_active', models.BooleanField(default=True, verbose_name='Активна')), + ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Дата создания')), + ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Дата обновления')), + ('warehouse', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='showcases', to='inventory.warehouse', verbose_name='Склад')), + ], + options={ + 'verbose_name': 'Витрина', + 'verbose_name_plural': 'Витрины', + 'ordering': ['warehouse', 'name'], + }, + ), + migrations.AddField( + model_name='reservation', + name='showcase', + field=models.ForeignKey(blank=True, help_text='Витрина, на которой выложен букет', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='reservations', to='inventory.showcase', verbose_name='Витрина'), + ), + migrations.AddIndex( + model_name='reservation', + index=models.Index(fields=['showcase'], name='inventory_r_showcas_bd3508_idx'), + ), + migrations.AddIndex( + model_name='showcase', + index=models.Index(fields=['warehouse'], name='inventory_s_warehou_1e4a8a_idx'), + ), + migrations.AddIndex( + model_name='showcase', + index=models.Index(fields=['is_active'], name='inventory_s_is_acti_387bfb_idx'), + ), + ] diff --git a/myproject/inventory/models.py b/myproject/inventory/models.py index 5ef1d0b..a9c03db 100644 --- a/myproject/inventory/models.py +++ b/myproject/inventory/models.py @@ -359,10 +359,36 @@ class InventoryLine(models.Model): super().save(*args, **kwargs) +class Showcase(models.Model): + """ + Витрина - место выкладки собранных букетов/комплектов. + Привязана к конкретному складу для учёта резервов. + """ + name = models.CharField(max_length=200, verbose_name="Название") + warehouse = models.ForeignKey(Warehouse, on_delete=models.CASCADE, + related_name='showcases', verbose_name="Склад") + description = models.TextField(blank=True, null=True, verbose_name="Описание") + is_active = models.BooleanField(default=True, verbose_name="Активна") + created_at = models.DateTimeField(auto_now_add=True, verbose_name="Дата создания") + updated_at = models.DateTimeField(auto_now=True, verbose_name="Дата обновления") + + class Meta: + verbose_name = "Витрина" + verbose_name_plural = "Витрины" + ordering = ['warehouse', 'name'] + indexes = [ + models.Index(fields=['warehouse']), + models.Index(fields=['is_active']), + ] + + def __str__(self): + return f"{self.name} ({self.warehouse.name})" + + class Reservation(models.Model): """ - Резервирование товара для заказа. - Отслеживает, какой товар зарезервирован за каким заказом. + Резервирование товара для заказа или витрины. + Отслеживает, какой товар зарезервирован за каким заказом или витриной. """ STATUS_CHOICES = [ ('reserved', 'Зарезервирован'), @@ -373,6 +399,10 @@ class Reservation(models.Model): order_item = models.ForeignKey('orders.OrderItem', on_delete=models.CASCADE, related_name='reservations', verbose_name="Позиция заказа", null=True, blank=True) + showcase = models.ForeignKey(Showcase, 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, @@ -393,11 +423,17 @@ class Reservation(models.Model): models.Index(fields=['product', 'warehouse']), models.Index(fields=['status']), models.Index(fields=['order_item']), + models.Index(fields=['showcase']), ] def __str__(self): - order_info = f" (заказ {self.order_item.order.order_number})" if self.order_item else "" - return f"Резерв {self.product.name}: {self.quantity} шт{order_info} [{self.get_status_display()}]" + if self.order_item: + context = f" (заказ {self.order_item.order.order_number})" + elif self.showcase: + context = f" (витрина {self.showcase.name})" + else: + context = "" + return f"Резерв {self.product.name}: {self.quantity} шт{context} [{self.get_status_display()}]" class Stock(models.Model): diff --git a/myproject/inventory/services/__init__.py b/myproject/inventory/services/__init__.py index 2b65052..8b45a5a 100644 --- a/myproject/inventory/services/__init__.py +++ b/myproject/inventory/services/__init__.py @@ -5,9 +5,11 @@ from .batch_manager import StockBatchManager from .sale_processor import SaleProcessor from .inventory_processor import InventoryProcessor +from .showcase_manager import ShowcaseManager __all__ = [ 'StockBatchManager', 'SaleProcessor', 'InventoryProcessor', + 'ShowcaseManager', ] diff --git a/myproject/inventory/services/sale_processor.py b/myproject/inventory/services/sale_processor.py index 4f0e9f9..7ef4c21 100644 --- a/myproject/inventory/services/sale_processor.py +++ b/myproject/inventory/services/sale_processor.py @@ -19,6 +19,40 @@ class SaleProcessor: Обработчик продаж с автоматическим FIFO-списанием. """ + @staticmethod + @transaction.atomic + def create_sale_from_reservation(reservation, order=None): + """ + Создать продажу на основе резерва. + Используется для продажи с витрины. + + Args: + reservation: объект Reservation + order: (опционально) объект Order + + Returns: + Объект Sale + """ + # Определяем цену продажи из заказа или из товара + if order and reservation.order_item: + # Цена из OrderItem + sale_price = reservation.order_item.price + else: + # Цена из товара + sale_price = reservation.product.actual_price or Decimal('0') + + # Создаём продажу с FIFO-списанием + sale = SaleProcessor.create_sale( + product=reservation.product, + warehouse=reservation.warehouse, + quantity=reservation.quantity, + sale_price=sale_price, + order=order, + document_number=None + ) + + return sale + @staticmethod @transaction.atomic def create_sale(product, warehouse, quantity, sale_price, order=None, document_number=None): diff --git a/myproject/inventory/services/showcase_manager.py b/myproject/inventory/services/showcase_manager.py new file mode 100644 index 0000000..bab2fce --- /dev/null +++ b/myproject/inventory/services/showcase_manager.py @@ -0,0 +1,334 @@ +""" +Сервис управления витринами - резервирование, продажа и разбор витринных букетов. +""" +from decimal import Decimal +from django.db import transaction +from django.utils import timezone +from django.core.exceptions import ValidationError + +from inventory.models import Showcase, Reservation, Warehouse +from products.models import ProductKit +from orders.models import Order, OrderItem, OrderStatus +from customers.models import Customer + + +class ShowcaseManager: + """ + Менеджер для работы с витринами и витринными букетами. + """ + + @staticmethod + def reserve_kit_to_showcase(product_kit, showcase, quantity=1): + """ + Резервирует комплект на витрину. + Раскладывает комплект на компоненты и создаёт резервы по каждому товару. + + Args: + product_kit: ProductKit - комплект для резервирования + showcase: Showcase - витрина + quantity: int - количество комплектов (по умолчанию 1) + + Returns: + dict: { + 'success': bool, + 'reservations': list[Reservation], + 'message': str + } + """ + if not showcase.is_active: + return { + 'success': False, + 'reservations': [], + 'message': f'Витрина "{showcase.name}" не активна' + } + + warehouse = showcase.warehouse + reservations = [] + + try: + with transaction.atomic(): + # Раскладываем комплект на компоненты + kit_items = product_kit.kit_items.all() + + if not kit_items.exists(): + return { + 'success': False, + 'reservations': [], + 'message': f'Комплект "{product_kit.name}" не содержит компонентов' + } + + # Создаём резервы по каждому компоненту + for kit_item in kit_items: + if kit_item.product: + # Обычный товар + component_quantity = kit_item.quantity * quantity + + reservation = Reservation.objects.create( + product=kit_item.product, + warehouse=warehouse, + showcase=showcase, + quantity=component_quantity, + status='reserved' + ) + reservations.append(reservation) + + elif kit_item.variant_group: + # Группа вариантов - резервируем первый доступный вариант + # В будущем можно добавить выбор конкретного варианта + variant_items = kit_item.variant_group.items.all() + if variant_items.exists(): + first_variant = variant_items.first() + component_quantity = kit_item.quantity * quantity + + reservation = Reservation.objects.create( + product=first_variant.product, + warehouse=warehouse, + showcase=showcase, + quantity=component_quantity, + status='reserved' + ) + reservations.append(reservation) + + # Обновляем агрегаты Stock для всех затронутых товаров + from inventory.models import Stock + for reservation in reservations: + stock, _ = Stock.objects.get_or_create( + product=reservation.product, + warehouse=warehouse + ) + stock.refresh_from_batches() + + return { + 'success': True, + 'reservations': reservations, + 'message': f'Комплект "{product_kit.name}" зарезервирован на витрине "{showcase.name}"' + } + + except Exception as e: + return { + 'success': False, + 'reservations': [], + 'message': f'Ошибка резервирования: {str(e)}' + } + + @staticmethod + def sell_from_showcase(product_kit, showcase, customer, payment_method='cash_to_courier', + custom_price=None, user=None): + """ + Продаёт комплект с витрины. + Создаёт Order, OrderItem, конвертирует резервы в Sale. + + Args: + product_kit: ProductKit - комплект для продажи + showcase: Showcase - витрина + customer: Customer - покупатель + payment_method: str - способ оплаты + custom_price: Decimal - кастомная цена (опционально) + user: CustomUser - пользователь, выполняющий операцию + + Returns: + dict: { + 'success': bool, + 'order': Order or None, + 'message': str + } + """ + warehouse = showcase.warehouse + + try: + with transaction.atomic(): + # Находим резервы для этого комплекта на витрине + # Группируем по product для подсчёта + reservations = Reservation.objects.filter( + showcase=showcase, + status='reserved' + ).select_related('product') + + if not reservations.exists(): + return { + 'success': False, + 'order': None, + 'message': f'На витрине "{showcase.name}" нет зарезервированных товаров' + } + + # Получаем статус "Завершён" для POS-продаж + completed_status = OrderStatus.objects.filter( + code='completed', + is_positive_end=True + ).first() + + if not completed_status: + # Если нет статуса completed, берём любой положительный + completed_status = OrderStatus.objects.filter( + is_positive_end=True + ).first() + + # Создаём заказ (самовывоз с витринного склада) + order = Order.objects.create( + customer=customer, + is_delivery=False, + pickup_warehouse=warehouse, + status=completed_status, + payment_method=payment_method, + is_paid=True, + modified_by=user + ) + + # Определяем цену + price = custom_price if custom_price else product_kit.actual_price + is_custom = custom_price is not None + + # Создаём позицию заказа + order_item = OrderItem.objects.create( + order=order, + product_kit=product_kit, + quantity=1, + price=price, + is_custom_price=is_custom, + is_from_showcase=True, + showcase=showcase + ) + + # Привязываем резервы к OrderItem + reservations.update(order_item=order_item) + + # Конвертируем резервы в продажи + from inventory.services.sale_processor import SaleProcessor + for reservation in reservations: + # Создаём Sale + sale = SaleProcessor.create_sale_from_reservation( + reservation=reservation, + order=order + ) + + # Обновляем статус резерва + reservation.status = 'converted_to_sale' + reservation.converted_at = timezone.now() + reservation.save() + + # Пересчитываем итоговую сумму заказа + order.calculate_total() + order.amount_paid = order.total_amount + order.update_payment_status() + order.save() + + return { + 'success': True, + 'order': order, + 'message': f'Заказ #{order.order_number} создан. Продан комплект с витрины "{showcase.name}"' + } + + except Exception as e: + return { + 'success': False, + 'order': None, + 'message': f'Ошибка продажи: {str(e)}' + } + + @staticmethod + def dismantle_from_showcase(showcase, product_kit=None): + """ + Разбирает букет на витрине - освобождает резервы. + + Args: + showcase: Showcase - витрина + product_kit: ProductKit - конкретный комплект (опционально) + + Returns: + dict: { + 'success': bool, + 'released_count': int, + 'message': str + } + """ + try: + with transaction.atomic(): + # Находим активные резервы + reservations = Reservation.objects.filter( + showcase=showcase, + status='reserved' + ) + + if product_kit: + # Если указан конкретный комплект, фильтруем резервы + # TODO: добавить связь резерва с конкретным экземпляром комплекта + # Пока освобождаем все резервы витрины + pass + + released_count = reservations.count() + + if released_count == 0: + return { + 'success': False, + 'released_count': 0, + 'message': f'На витрине "{showcase.name}" нет активных резервов' + } + + # Освобождаем резервы + reservations.update( + status='released', + released_at=timezone.now(), + showcase=None + ) + + # Обновляем агрегаты 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 + ) + stock.refresh_from_batches() + except Stock.DoesNotExist: + pass + + return { + 'success': True, + 'released_count': released_count, + 'message': f'Разобрано {released_count} резервов с витрины "{showcase.name}"' + } + + except Exception as e: + return { + 'success': False, + 'released_count': 0, + 'message': f'Ошибка разбора: {str(e)}' + } + + @staticmethod + def get_showcase_kits(showcase): + """ + Возвращает список комплектов, зарезервированных на витрине. + + Args: + showcase: Showcase + + Returns: + list: список словарей с информацией о комплектах + """ + reservations = Reservation.objects.filter( + showcase=showcase, + status='reserved' + ).select_related('product').values('product__id', 'product__name', 'quantity') + + # Группируем по товарам + products_dict = {} + for res in reservations: + product_id = res['product__id'] + if product_id not in products_dict: + products_dict[product_id] = { + 'product_name': res['product__name'], + 'quantity': Decimal('0') + } + products_dict[product_id]['quantity'] += res['quantity'] + + return [ + { + 'product_id': pid, + 'product_name': data['product_name'], + 'quantity': data['quantity'] + } + for pid, data in products_dict.items() + ] diff --git a/myproject/orders/migrations/0003_historicalorderitem_is_from_showcase_and_more.py b/myproject/orders/migrations/0003_historicalorderitem_is_from_showcase_and_more.py new file mode 100644 index 0000000..32145ed --- /dev/null +++ b/myproject/orders/migrations/0003_historicalorderitem_is_from_showcase_and_more.py @@ -0,0 +1,44 @@ +# Generated by Django 5.0.10 on 2025-11-16 18:07 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('inventory', '0003_showcase_reservation_showcase_and_more'), + ('orders', '0002_initial'), + ('products', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='historicalorderitem', + name='is_from_showcase', + field=models.BooleanField(default=False, help_text='True если товар продан с витрины', verbose_name='С витрины'), + ), + migrations.AddField( + model_name='historicalorderitem', + name='showcase', + field=models.ForeignKey(blank=True, db_constraint=False, help_text='Витрина, с которой был продан товар', null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='inventory.showcase', verbose_name='Витрина'), + ), + migrations.AddField( + model_name='orderitem', + name='is_from_showcase', + field=models.BooleanField(default=False, help_text='True если товар продан с витрины', verbose_name='С витрины'), + ), + migrations.AddField( + model_name='orderitem', + name='showcase', + field=models.ForeignKey(blank=True, help_text='Витрина, с которой был продан товар', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='order_items', to='inventory.showcase', verbose_name='Витрина'), + ), + migrations.AddIndex( + model_name='orderitem', + index=models.Index(fields=['is_from_showcase'], name='orders_orde_is_from_32d8f7_idx'), + ), + migrations.AddIndex( + model_name='orderitem', + index=models.Index(fields=['showcase'], name='orders_orde_showcas_aa97bd_idx'), + ), + ] diff --git a/myproject/orders/models.py b/myproject/orders/models.py index af52d17..dd5b02b 100644 --- a/myproject/orders/models.py +++ b/myproject/orders/models.py @@ -683,6 +683,23 @@ class OrderItem(models.Model): help_text="True если цена была изменена вручную при создании заказа" ) + # Витринные продажи + is_from_showcase = models.BooleanField( + default=False, + verbose_name="С витрины", + help_text="True если товар продан с витрины" + ) + + showcase = models.ForeignKey( + 'inventory.Showcase', + on_delete=models.SET_NULL, + null=True, + blank=True, + related_name='order_items', + verbose_name="Витрина", + help_text="Витрина, с которой был продан товар" + ) + # Временные метки created_at = models.DateTimeField( auto_now_add=True, @@ -699,6 +716,8 @@ class OrderItem(models.Model): models.Index(fields=['order']), models.Index(fields=['product']), models.Index(fields=['product_kit']), + models.Index(fields=['is_from_showcase']), + models.Index(fields=['showcase']), ] def __str__(self): diff --git a/myproject/pos/static/pos/js/terminal.js b/myproject/pos/static/pos/js/terminal.js index 5632dc1..2842a66 100644 --- a/myproject/pos/static/pos/js/terminal.js +++ b/myproject/pos/static/pos/js/terminal.js @@ -14,6 +14,48 @@ function renderCategories() { const grid = document.getElementById('categoryGrid'); grid.innerHTML = ''; + // Кнопка "Витрина" - первая в ряду + const showcaseCol = document.createElement('div'); + showcaseCol.className = 'col-6 col-sm-4 col-md-3 col-lg-2'; + const showcaseCard = document.createElement('div'); + showcaseCard.className = 'card category-card showcase-card'; + showcaseCard.style.backgroundColor = '#fff3cd'; + showcaseCard.style.borderColor = '#ffc107'; + showcaseCard.onclick = async () => { + try { + const response = await fetch('/pos/api/showcase-items/'); + const data = await response.json(); + + if (data.success && data.showcases.length > 0) { + let message = '🌺 ВИТРИННЫЕ БУКЕТЫ\n\n'; + + data.showcases.forEach(showcase => { + message += `● ${showcase.name} (Склад: ${showcase.warehouse})\n`; + showcase.items.forEach(item => { + message += ` - ${item.product_name}: ${item.quantity} шт\n`; + }); + message += '\n'; + }); + + alert(message); + } else { + alert('Витрины пусты'); + } + } catch (error) { + console.error('Error fetching showcase items:', error); + alert('Ошибка загрузки витринных букетов'); + } + }; + const showcaseBody = document.createElement('div'); + showcaseBody.className = 'card-body'; + const showcaseName = document.createElement('div'); + showcaseName.className = 'category-name'; + showcaseName.innerHTML = ' ВИТРИНА'; + showcaseBody.appendChild(showcaseName); + showcaseCard.appendChild(showcaseBody); + showcaseCol.appendChild(showcaseCard); + grid.appendChild(showcaseCol); + // Кнопка "Все" const allCol = document.createElement('div'); allCol.className = 'col-6 col-sm-4 col-md-3 col-lg-2'; @@ -241,6 +283,11 @@ function clearCart() { document.getElementById('clearCart').onclick = clearCart; +// Кнопка "На витрину" - функционал будет добавлен позже +document.getElementById('addToShowcaseBtn').onclick = () => { + alert('Функционал "На витрину" будет реализован позже'); +}; + // Заглушки для функционала (будет реализовано позже) document.getElementById('checkoutNow').onclick = async () => { alert('Функционал будет подключен позже: создание заказа и списание со склада.'); diff --git a/myproject/pos/templates/pos/terminal.html b/myproject/pos/templates/pos/terminal.html index 0312200..9fc8b32 100644 --- a/myproject/pos/templates/pos/terminal.html +++ b/myproject/pos/templates/pos/terminal.html @@ -57,7 +57,8 @@
-
diff --git a/myproject/pos/urls.py b/myproject/pos/urls.py index 4a7a84c..20e098a 100644 --- a/myproject/pos/urls.py +++ b/myproject/pos/urls.py @@ -6,4 +6,5 @@ app_name = 'pos' urlpatterns = [ path('', views.pos_terminal, name='terminal'), + path('api/showcase-items/', views.showcase_items_api, name='showcase-items-api'), ] diff --git a/myproject/pos/views.py b/myproject/pos/views.py index 6b10cea..2834d49 100644 --- a/myproject/pos/views.py +++ b/myproject/pos/views.py @@ -1,7 +1,10 @@ # -*- coding: utf-8 -*- from django.shortcuts import render from django.contrib.auth.decorators import login_required +from django.http import JsonResponse +from django.views.decorators.http import require_http_methods from products.models import Product, ProductCategory, ProductKit +from inventory.models import Showcase, Reservation import json @@ -52,3 +55,45 @@ def pos_terminal(request): 'title': 'POS Terminal', } return render(request, 'pos/terminal.html', context) + + +@login_required +@require_http_methods(["GET"]) +def showcase_items_api(request): + """ + API endpoint для получения витринных букетов. + Возвращает комплекты, зарезервированные на активных витринах. + """ + # Получаем все активные резервы на витринах + showcase_reservations = Reservation.objects.filter( + showcase__isnull=False, + showcase__is_active=True, + status='reserved' + ).select_related('showcase', 'product').prefetch_related('product__photos') + + # Группируем по витринам + showcases_dict = {} + for res in showcase_reservations: + showcase_id = res.showcase.id + if showcase_id not in showcases_dict: + showcases_dict[showcase_id] = { + 'id': showcase_id, + 'name': res.showcase.name, + 'warehouse': res.showcase.warehouse.name, + 'items': [] + } + + # Добавляем товар в список + showcases_dict[showcase_id]['items'].append({ + 'product_id': res.product.id, + 'product_name': res.product.name, + 'quantity': str(res.quantity), + 'image': res.product.photos.first().get_thumbnail_url() if res.product.photos.exists() else None, + }) + + showcases_list = list(showcases_dict.values()) + + return JsonResponse({ + 'success': True, + 'showcases': showcases_list + })