diff --git a/myproject/pos/static/pos/js/terminal.js b/myproject/pos/static/pos/js/terminal.js
index 6c95ff7..8c67a24 100644
--- a/myproject/pos/static/pos/js/terminal.js
+++ b/myproject/pos/static/pos/js/terminal.js
@@ -8,6 +8,13 @@ let currentCategoryId = null;
let isShowcaseView = false; // Флаг режима просмотра витринных букетов
const cart = new Map(); // "type-id" -> {id, name, price, qty, type}
+// Переменные для режима редактирования
+let isEditMode = false;
+let editingKitId = null;
+
+// Временная корзина для модального окна создания/редактирования комплекта
+const tempCart = new Map(); // Изолированное состояние для модалки
+
function formatMoney(v) {
return (Number(v)).toFixed(2);
}
@@ -116,8 +123,25 @@ function renderProducts() {
const card = document.createElement('div');
card.className = 'card product-card';
+ card.style.position = 'relative';
card.onclick = () => addToCart(item);
+ // Если это витринный комплект - добавляем кнопку редактирования
+ if (item.type === 'showcase_kit') {
+ const editBtn = document.createElement('button');
+ editBtn.className = 'btn btn-sm btn-outline-primary';
+ editBtn.style.position = 'absolute';
+ editBtn.style.top = '5px';
+ editBtn.style.right = '5px';
+ editBtn.style.zIndex = '10';
+ editBtn.innerHTML = '';
+ editBtn.onclick = (e) => {
+ e.stopPropagation();
+ openEditKitModal(item.id);
+ };
+ card.appendChild(editBtn);
+ }
+
const body = document.createElement('div');
body.className = 'card-body';
@@ -319,6 +343,12 @@ async function openCreateTempKitModal() {
return;
}
+ // Копируем содержимое cart в tempCart (изолированное состояние модалки)
+ tempCart.clear();
+ cart.forEach((item, key) => {
+ tempCart.set(key, {...item}); // Глубокая копия объекта
+ });
+
// Генерируем название по умолчанию
const now = new Date();
const defaultName = `Витрина — ${now.toLocaleDateString('ru-RU')} ${now.toLocaleTimeString('ru-RU', {hour: '2-digit', minute: '2-digit'})}`;
@@ -327,7 +357,7 @@ async function openCreateTempKitModal() {
// Загружаем список витрин
await loadShowcases();
- // Заполняем список товаров из корзины
+ // Заполняем список товаров из tempCart
renderTempKitItems();
// Открываем модальное окно
@@ -335,6 +365,87 @@ async function openCreateTempKitModal() {
modal.show();
}
+// Открытие модального окна для редактирования комплекта
+async function openEditKitModal(kitId) {
+ try {
+ // Загружаем данные комплекта
+ const response = await fetch(`/pos/api/product-kits/${kitId}/`);
+ const data = await response.json();
+
+ if (!data.success) {
+ alert(`Ошибка: ${data.error}`);
+ return;
+ }
+
+ const kit = data.kit;
+
+ // Устанавливаем режим редактирования
+ isEditMode = true;
+ editingKitId = kitId;
+
+ // Загружаем список витрин
+ await loadShowcases();
+
+ // Очищаем tempCart и заполняем составом комплекта
+ tempCart.clear();
+ kit.items.forEach(item => {
+ const cartKey = `product-${item.product_id}`;
+ tempCart.set(cartKey, {
+ id: item.product_id,
+ name: item.name,
+ price: Number(item.price),
+ qty: Number(item.qty),
+ type: 'product'
+ });
+ });
+ renderTempKitItems(); // Отображаем товары в модальном окне
+
+ // Заполняем поля формы
+ document.getElementById('tempKitName').value = kit.name;
+ document.getElementById('tempKitDescription').value = kit.description;
+ document.getElementById('priceAdjustmentType').value = kit.price_adjustment_type;
+ document.getElementById('priceAdjustmentValue').value = kit.price_adjustment_value;
+
+ if (kit.sale_price) {
+ document.getElementById('useSalePrice').checked = true;
+ document.getElementById('salePrice').value = kit.sale_price;
+ document.getElementById('salePriceBlock').style.display = 'block';
+ } else {
+ document.getElementById('useSalePrice').checked = false;
+ document.getElementById('salePrice').value = '';
+ document.getElementById('salePriceBlock').style.display = 'none';
+ }
+
+ // Выбираем витрину
+ if (kit.showcase_id) {
+ document.getElementById('showcaseSelect').value = kit.showcase_id;
+ }
+
+ // Отображаем фото, если есть
+ if (kit.photo_url) {
+ document.getElementById('photoPreviewImg').src = kit.photo_url;
+ document.getElementById('photoPreview').style.display = 'block';
+ } else {
+ document.getElementById('photoPreview').style.display = 'none';
+ }
+
+ // Обновляем цены
+ updatePriceCalculations();
+
+ // Меняем заголовок и кнопку
+ document.getElementById('createTempKitModalLabel').textContent = 'Редактирование витринного букета';
+ document.getElementById('confirmCreateTempKit').textContent = 'Сохранить изменения';
+
+ // Открываем модальное окно
+ const modal = new bootstrap.Modal(document.getElementById('createTempKitModal'));
+ modal.show();
+
+ } catch (error) {
+ console.error('Error loading kit for edit:', error);
+ alert('Ошибка при загрузке комплекта');
+ }
+}
+
// Обновление списка витринных комплектов
async function refreshShowcaseKits() {
try {
@@ -388,14 +499,14 @@ async function loadShowcases() {
}
}
-// Отображение товаров из корзины в модальном окне
+// Отображение товаров из tempCart в модальном окне
function renderTempKitItems() {
const container = document.getElementById('tempKitItemsList');
container.innerHTML = '';
let estimatedTotal = 0;
- cart.forEach((item, cartKey) => {
+ tempCart.forEach((item, cartKey) => {
// Только товары (не комплекты)
if (item.type !== 'product') return;
@@ -422,10 +533,10 @@ function renderTempKitItems() {
// Расчет и обновление всех цен
function updatePriceCalculations(basePrice = null) {
- // Если basePrice не передан, пересчитываем из корзины
+ // Если basePrice не передан, пересчитываем из tempCart
if (basePrice === null) {
basePrice = 0;
- cart.forEach((item, cartKey) => {
+ tempCart.forEach((item, cartKey) => {
if (item.type === 'product') {
basePrice += item.qty * item.price;
}
@@ -524,7 +635,7 @@ document.getElementById('removePhoto').addEventListener('click', function() {
document.getElementById('photoPreviewImg').src = '';
});
-// Подтверждение создания временного комплекта
+// Подтверждение создания/редактирования временного комплекта
document.getElementById('confirmCreateTempKit').onclick = async () => {
const kitName = document.getElementById('tempKitName').value.trim();
const showcaseId = document.getElementById('showcaseSelect').value;
@@ -537,14 +648,14 @@ document.getElementById('confirmCreateTempKit').onclick = async () => {
return;
}
- if (!showcaseId) {
+ if (!showcaseId && !isEditMode) {
alert('Выберите витрину');
return;
}
- // Собираем товары из корзины
+ // Собираем товары из tempCart (изолированное состояние модалки)
const items = [];
- cart.forEach((item, cartKey) => {
+ tempCart.forEach((item, cartKey) => {
if (item.type === 'product') {
items.push({
product_id: item.id,
@@ -567,7 +678,9 @@ document.getElementById('confirmCreateTempKit').onclick = async () => {
// Формируем FormData для отправки с файлом
const formData = new FormData();
formData.append('kit_name', kitName);
- formData.append('showcase_id', showcaseId);
+ if (showcaseId) {
+ formData.append('showcase_id', showcaseId);
+ }
formData.append('description', description);
formData.append('items', JSON.stringify(items));
formData.append('price_adjustment_type', priceAdjustmentType);
@@ -575,17 +688,28 @@ document.getElementById('confirmCreateTempKit').onclick = async () => {
if (useSalePrice && salePrice > 0) {
formData.append('sale_price', salePrice);
}
+
+ // Фото: для редактирования проверяем, удалено ли оно
if (photoFile) {
formData.append('photo', photoFile);
+ } else if (isEditMode && document.getElementById('photoPreview').style.display === 'none') {
+ // Если фото было удалено
+ formData.append('remove_photo', '1');
}
// Отправляем запрос на сервер
const confirmBtn = document.getElementById('confirmCreateTempKit');
confirmBtn.disabled = true;
- confirmBtn.innerHTML = 'Создание...';
+
+ const url = isEditMode
+ ? `/pos/api/product-kits/${editingKitId}/update/`
+ : '/pos/api/create-temp-kit/';
+
+ const actionText = isEditMode ? 'Сохранение...' : 'Создание...';
+ confirmBtn.innerHTML = `${actionText}`;
try {
- const response = await fetch('/pos/api/create-temp-kit/', {
+ const response = await fetch(url, {
method: 'POST',
headers: {
'X-CSRFToken': getCookie('csrftoken')
@@ -598,14 +722,18 @@ document.getElementById('confirmCreateTempKit').onclick = async () => {
if (data.success) {
// Успех!
- alert(`✅ ${data.message}
+ const successMessage = isEditMode
+ ? `✅ ${data.message}\n\nКомплект: ${data.kit_name}\nЦена: ${data.kit_price} руб.`
+ : `✅ ${data.message}
Комплект: ${data.kit_name}
Цена: ${data.kit_price} руб.
-Зарезервировано компонентов: ${data.reservations_count}`);
+Зарезервировано компонентов: ${data.reservations_count}`;
- // Очищаем корзину
- clearCart();
+ alert(successMessage);
+
+ // Очищаем tempCart (изолированное состояние модалки)
+ tempCart.clear();
// Сбрасываем поля формы
document.getElementById('tempKitDescription').value = '';
@@ -618,6 +746,10 @@ document.getElementById('confirmCreateTempKit').onclick = async () => {
document.getElementById('salePrice').value = '';
document.getElementById('salePriceBlock').style.display = 'none';
+ // Сбрасываем режим редактирования
+ isEditMode = false;
+ editingKitId = null;
+
// Закрываем модальное окно
const modal = bootstrap.Modal.getInstance(document.getElementById('createTempKitModal'));
modal.hide();
@@ -632,11 +764,14 @@ document.getElementById('confirmCreateTempKit').onclick = async () => {
alert(`Ошибка: ${data.error}`);
}
} catch (error) {
- console.error('Error creating temp kit:', error);
- alert('Ошибка при создании комплекта');
+ console.error('Error saving kit:', error);
+ alert('Ошибка при сохранении комплекта');
} finally {
confirmBtn.disabled = false;
- confirmBtn.innerHTML = ' Создать и зарезервировать';
+ const btnText = isEditMode
+ ? ' Сохранить изменения'
+ : ' Создать и зарезервировать';
+ confirmBtn.innerHTML = btnText;
}
};
@@ -656,6 +791,22 @@ function getCookie(name) {
return cookieValue;
}
+// Сброс режима редактирования при закрытии модального окна
+document.getElementById('createTempKitModal').addEventListener('hidden.bs.modal', function() {
+ // Очищаем tempCart (изолированное состояние модалки)
+ tempCart.clear();
+
+ if (isEditMode) {
+ // Сбрасываем режим редактирования
+ isEditMode = false;
+ editingKitId = null;
+
+ // Восстанавливаем заголовок и текст кнопки
+ document.getElementById('createTempKitModalLabel').textContent = 'Создать витринный букет из корзины';
+ document.getElementById('confirmCreateTempKit').innerHTML = ' Создать и зарезервировать';
+ }
+});
+
// Заглушки для функционала (будет реализовано позже)
document.getElementById('checkoutNow').onclick = async () => {
alert('Функционал будет подключен позже: создание заказа и списание со склада.');
diff --git a/myproject/pos/urls.py b/myproject/pos/urls.py
index 47ac5b2..fa027bb 100644
--- a/myproject/pos/urls.py
+++ b/myproject/pos/urls.py
@@ -9,5 +9,7 @@ urlpatterns = [
path('api/showcase-items/', views.showcase_items_api, name='showcase-items-api'),
path('api/get-showcases/', views.get_showcases_api, name='get-showcases-api'),
path('api/showcase-kits/', views.get_showcase_kits_api, name='showcase-kits-api'),
+ path('api/product-kits//', views.get_product_kit_details, name='get-product-kit-details'),
+ path('api/product-kits//update/', views.update_product_kit, name='update-product-kit'),
path('api/create-temp-kit/', views.create_temp_kit_to_showcase, name='create-temp-kit-api'),
]
diff --git a/myproject/pos/views.py b/myproject/pos/views.py
index 804737f..5bb15d8 100644
--- a/myproject/pos/views.py
+++ b/myproject/pos/views.py
@@ -209,6 +209,56 @@ def get_showcase_kits_api(request):
})
+@login_required
+@require_http_methods(["GET"])
+def get_product_kit_details(request, kit_id):
+ """
+ API endpoint для получения полных данных комплекта для редактирования.
+ """
+ try:
+ kit = ProductKit.objects.prefetch_related('kit_items__product', 'photos').get(id=kit_id)
+
+ # Получаем витрину, на которой размещен комплект
+ showcase_reservation = Reservation.objects.filter(
+ product__in=kit.kit_items.values_list('product_id', flat=True),
+ showcase__isnull=False,
+ showcase__is_active=True,
+ status='reserved'
+ ).select_related('showcase').first()
+
+ showcase_id = showcase_reservation.showcase.id if showcase_reservation else None
+
+ # Собираем данные о составе
+ items = [{
+ 'product_id': ki.product.id,
+ 'name': ki.product.name,
+ 'qty': str(ki.quantity),
+ 'price': str(ki.product.actual_price)
+ } for ki in kit.kit_items.all()]
+
+ # Фото
+ photo_url = kit.photos.first().image.url if kit.photos.exists() else None
+
+ return JsonResponse({
+ 'success': True,
+ 'kit': {
+ 'id': kit.id,
+ 'name': kit.name,
+ 'description': kit.description or '',
+ 'price_adjustment_type': kit.price_adjustment_type,
+ 'price_adjustment_value': str(kit.price_adjustment_value),
+ 'sale_price': str(kit.sale_price) if kit.sale_price else '',
+ 'base_price': str(kit.base_price),
+ 'final_price': str(kit.actual_price),
+ 'showcase_id': showcase_id,
+ 'items': items,
+ 'photo_url': photo_url
+ }
+ })
+ except ProductKit.DoesNotExist:
+ return JsonResponse({'success': False, 'error': 'Комплект не найден'}, status=404)
+
+
@login_required
@require_http_methods(["POST"])
def create_temp_kit_to_showcase(request):
@@ -362,3 +412,145 @@ def create_temp_kit_to_showcase(request):
'success': False,
'error': f'Ошибка при создании комплекта: {str(e)}'
}, status=500)
+
+
+@login_required
+@require_http_methods(["POST"])
+def update_product_kit(request, kit_id):
+ """
+ API endpoint для обновления временного комплекта.
+
+ Payload (multipart/form-data):
+ - kit_name: Новое название
+ - description: Описание
+ - items: JSON список [{product_id, quantity}, ...]
+ - price_adjustment_type, price_adjustment_value, sale_price
+ - photo: Новое фото (опционально)
+ - remove_photo: '1' для удаления фото
+ """
+ try:
+ kit = ProductKit.objects.prefetch_related('kit_items__product', 'photos').get(id=kit_id, is_temporary=True)
+
+ # Получаем данные
+ kit_name = request.POST.get('kit_name', '').strip()
+ description = request.POST.get('description', '').strip()
+ items_json = request.POST.get('items', '[]')
+ price_adjustment_type = request.POST.get('price_adjustment_type', 'none')
+ price_adjustment_value = Decimal(str(request.POST.get('price_adjustment_value', 0)))
+ sale_price_str = request.POST.get('sale_price', '')
+ photo_file = request.FILES.get('photo')
+ remove_photo = request.POST.get('remove_photo', '') == '1'
+
+ items = json.loads(items_json)
+
+ sale_price = None
+ if sale_price_str:
+ try:
+ sale_price = Decimal(str(sale_price_str))
+ if sale_price <= 0:
+ sale_price = None
+ except (ValueError, InvalidOperation):
+ sale_price = None
+
+ # Валидация
+ if not kit_name:
+ return JsonResponse({'success': False, 'error': 'Необходимо указать название'}, status=400)
+
+ if not items:
+ return JsonResponse({'success': False, 'error': 'Состав не может быть пустым'}, status=400)
+
+ # Проверяем товары
+ product_ids = [item['product_id'] for item in items]
+ products = Product.objects.in_bulk(product_ids)
+
+ if len(products) != len(product_ids):
+ return JsonResponse({'success': False, 'error': 'Некоторые товары не найдены'}, status=400)
+
+ # Агрегируем количества
+ aggregated_items = {}
+ for item in items:
+ product_id = item['product_id']
+ quantity = Decimal(str(item['quantity']))
+ aggregated_items[product_id] = aggregated_items.get(product_id, Decimal('0')) + quantity
+
+ with transaction.atomic():
+ # Получаем старый состав для сравнения
+ old_items = {ki.product_id: ki.quantity for ki in kit.kit_items.all()}
+
+ # Получаем витрину для резервов
+ showcase_reservation = Reservation.objects.filter(
+ product__in=old_items.keys(),
+ showcase__isnull=False,
+ status='reserved'
+ ).select_related('showcase').first()
+
+ showcase = showcase_reservation.showcase if showcase_reservation else None
+
+ # Вычисляем разницу в составе
+ all_product_ids = set(old_items.keys()) | set(aggregated_items.keys())
+
+ for product_id in all_product_ids:
+ old_qty = old_items.get(product_id, Decimal('0'))
+ new_qty = aggregated_items.get(product_id, Decimal('0'))
+ diff = new_qty - old_qty
+
+ if diff > 0 and showcase:
+ # Нужно дозарезервировать
+ result = ShowcaseManager.reserve_product_to_showcase(
+ product=products[product_id],
+ showcase=showcase,
+ quantity=diff
+ )
+ if not result['success']:
+ raise Exception(f"Недостаточно запасов: {result['message']}")
+
+ elif diff < 0 and showcase:
+ # Нужно освободить резерв
+ ShowcaseManager.release_showcase_reservation(
+ product=products[product_id],
+ showcase=showcase,
+ quantity=abs(diff)
+ )
+
+ # Обновляем комплект
+ kit.name = kit_name
+ kit.description = description
+ kit.price_adjustment_type = price_adjustment_type
+ kit.price_adjustment_value = price_adjustment_value
+ kit.sale_price = sale_price
+ kit.save()
+
+ # Обновляем состав
+ kit.kit_items.all().delete()
+ for product_id, quantity in aggregated_items.items():
+ KitItem.objects.create(
+ kit=kit,
+ product=products[product_id],
+ quantity=quantity
+ )
+
+ kit.recalculate_base_price()
+
+ # Обновляем фото
+ if remove_photo:
+ kit.photos.all().delete()
+
+ if photo_file:
+ from products.models import ProductKitPhoto
+ kit.photos.all().delete() # Удаляем старое
+ ProductKitPhoto.objects.create(kit=kit, image=photo_file, order=0)
+
+ return JsonResponse({
+ 'success': True,
+ 'message': f'Комплект "{kit.name}" обновлён',
+ 'kit_id': kit.id,
+ 'kit_name': kit.name,
+ 'kit_price': str(kit.actual_price)
+ })
+
+ except ProductKit.DoesNotExist:
+ return JsonResponse({'success': False, 'error': 'Комплект не найден'}, status=404)
+ except json.JSONDecodeError:
+ return JsonResponse({'success': False, 'error': 'Неверный формат данных'}, status=400)
+ except Exception as e:
+ return JsonResponse({'success': False, 'error': f'Ошибка: {str(e)}'}, status=500)