feat: добавлено редактирование витринных комплектов и изолированное состояние tempCart
- Добавлены API endpoints для получения и обновления витринных комплектов - GET /pos/api/product-kits/<id>/ - получение деталей комплекта - POST /pos/api/product-kits/<id>/update/ - обновление комплекта - Реализовано редактирование комплектов из POS интерфейса - Кнопка редактирования (карандаш) на карточках витринных букетов - Модальное окно предзаполняется данными комплекта - Поддержка изменения состава, цен, описания и фото - Умное управление резервами при изменении состава - Введено изолированное состояние tempCart для модального окна - Основная корзина (cart) больше не затрагивается при редактировании - tempCart используется для создания и редактирования комплектов - Автоочистка tempCart при закрытии модального окна - Устранён побочный эффект загрузки состава комплекта в основную корзину
This commit is contained in:
@@ -8,6 +8,13 @@ let currentCategoryId = null;
|
|||||||
let isShowcaseView = false; // Флаг режима просмотра витринных букетов
|
let isShowcaseView = false; // Флаг режима просмотра витринных букетов
|
||||||
const cart = new Map(); // "type-id" -> {id, name, price, qty, type}
|
const cart = new Map(); // "type-id" -> {id, name, price, qty, type}
|
||||||
|
|
||||||
|
// Переменные для режима редактирования
|
||||||
|
let isEditMode = false;
|
||||||
|
let editingKitId = null;
|
||||||
|
|
||||||
|
// Временная корзина для модального окна создания/редактирования комплекта
|
||||||
|
const tempCart = new Map(); // Изолированное состояние для модалки
|
||||||
|
|
||||||
function formatMoney(v) {
|
function formatMoney(v) {
|
||||||
return (Number(v)).toFixed(2);
|
return (Number(v)).toFixed(2);
|
||||||
}
|
}
|
||||||
@@ -116,8 +123,25 @@ function renderProducts() {
|
|||||||
|
|
||||||
const card = document.createElement('div');
|
const card = document.createElement('div');
|
||||||
card.className = 'card product-card';
|
card.className = 'card product-card';
|
||||||
|
card.style.position = 'relative';
|
||||||
card.onclick = () => addToCart(item);
|
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 = '<i class="bi bi-pencil"></i>';
|
||||||
|
editBtn.onclick = (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
openEditKitModal(item.id);
|
||||||
|
};
|
||||||
|
card.appendChild(editBtn);
|
||||||
|
}
|
||||||
|
|
||||||
const body = document.createElement('div');
|
const body = document.createElement('div');
|
||||||
body.className = 'card-body';
|
body.className = 'card-body';
|
||||||
|
|
||||||
@@ -319,6 +343,12 @@ async function openCreateTempKitModal() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Копируем содержимое cart в tempCart (изолированное состояние модалки)
|
||||||
|
tempCart.clear();
|
||||||
|
cart.forEach((item, key) => {
|
||||||
|
tempCart.set(key, {...item}); // Глубокая копия объекта
|
||||||
|
});
|
||||||
|
|
||||||
// Генерируем название по умолчанию
|
// Генерируем название по умолчанию
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const defaultName = `Витрина — ${now.toLocaleDateString('ru-RU')} ${now.toLocaleTimeString('ru-RU', {hour: '2-digit', minute: '2-digit'})}`;
|
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();
|
await loadShowcases();
|
||||||
|
|
||||||
// Заполняем список товаров из корзины
|
// Заполняем список товаров из tempCart
|
||||||
renderTempKitItems();
|
renderTempKitItems();
|
||||||
|
|
||||||
// Открываем модальное окно
|
// Открываем модальное окно
|
||||||
@@ -335,6 +365,87 @@ async function openCreateTempKitModal() {
|
|||||||
modal.show();
|
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() {
|
async function refreshShowcaseKits() {
|
||||||
try {
|
try {
|
||||||
@@ -388,14 +499,14 @@ async function loadShowcases() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Отображение товаров из корзины в модальном окне
|
// Отображение товаров из tempCart в модальном окне
|
||||||
function renderTempKitItems() {
|
function renderTempKitItems() {
|
||||||
const container = document.getElementById('tempKitItemsList');
|
const container = document.getElementById('tempKitItemsList');
|
||||||
container.innerHTML = '';
|
container.innerHTML = '';
|
||||||
|
|
||||||
let estimatedTotal = 0;
|
let estimatedTotal = 0;
|
||||||
|
|
||||||
cart.forEach((item, cartKey) => {
|
tempCart.forEach((item, cartKey) => {
|
||||||
// Только товары (не комплекты)
|
// Только товары (не комплекты)
|
||||||
if (item.type !== 'product') return;
|
if (item.type !== 'product') return;
|
||||||
|
|
||||||
@@ -422,10 +533,10 @@ function renderTempKitItems() {
|
|||||||
|
|
||||||
// Расчет и обновление всех цен
|
// Расчет и обновление всех цен
|
||||||
function updatePriceCalculations(basePrice = null) {
|
function updatePriceCalculations(basePrice = null) {
|
||||||
// Если basePrice не передан, пересчитываем из корзины
|
// Если basePrice не передан, пересчитываем из tempCart
|
||||||
if (basePrice === null) {
|
if (basePrice === null) {
|
||||||
basePrice = 0;
|
basePrice = 0;
|
||||||
cart.forEach((item, cartKey) => {
|
tempCart.forEach((item, cartKey) => {
|
||||||
if (item.type === 'product') {
|
if (item.type === 'product') {
|
||||||
basePrice += item.qty * item.price;
|
basePrice += item.qty * item.price;
|
||||||
}
|
}
|
||||||
@@ -524,7 +635,7 @@ document.getElementById('removePhoto').addEventListener('click', function() {
|
|||||||
document.getElementById('photoPreviewImg').src = '';
|
document.getElementById('photoPreviewImg').src = '';
|
||||||
});
|
});
|
||||||
|
|
||||||
// Подтверждение создания временного комплекта
|
// Подтверждение создания/редактирования временного комплекта
|
||||||
document.getElementById('confirmCreateTempKit').onclick = async () => {
|
document.getElementById('confirmCreateTempKit').onclick = async () => {
|
||||||
const kitName = document.getElementById('tempKitName').value.trim();
|
const kitName = document.getElementById('tempKitName').value.trim();
|
||||||
const showcaseId = document.getElementById('showcaseSelect').value;
|
const showcaseId = document.getElementById('showcaseSelect').value;
|
||||||
@@ -537,14 +648,14 @@ document.getElementById('confirmCreateTempKit').onclick = async () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!showcaseId) {
|
if (!showcaseId && !isEditMode) {
|
||||||
alert('Выберите витрину');
|
alert('Выберите витрину');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Собираем товары из корзины
|
// Собираем товары из tempCart (изолированное состояние модалки)
|
||||||
const items = [];
|
const items = [];
|
||||||
cart.forEach((item, cartKey) => {
|
tempCart.forEach((item, cartKey) => {
|
||||||
if (item.type === 'product') {
|
if (item.type === 'product') {
|
||||||
items.push({
|
items.push({
|
||||||
product_id: item.id,
|
product_id: item.id,
|
||||||
@@ -567,7 +678,9 @@ document.getElementById('confirmCreateTempKit').onclick = async () => {
|
|||||||
// Формируем FormData для отправки с файлом
|
// Формируем FormData для отправки с файлом
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('kit_name', kitName);
|
formData.append('kit_name', kitName);
|
||||||
formData.append('showcase_id', showcaseId);
|
if (showcaseId) {
|
||||||
|
formData.append('showcase_id', showcaseId);
|
||||||
|
}
|
||||||
formData.append('description', description);
|
formData.append('description', description);
|
||||||
formData.append('items', JSON.stringify(items));
|
formData.append('items', JSON.stringify(items));
|
||||||
formData.append('price_adjustment_type', priceAdjustmentType);
|
formData.append('price_adjustment_type', priceAdjustmentType);
|
||||||
@@ -575,17 +688,28 @@ document.getElementById('confirmCreateTempKit').onclick = async () => {
|
|||||||
if (useSalePrice && salePrice > 0) {
|
if (useSalePrice && salePrice > 0) {
|
||||||
formData.append('sale_price', salePrice);
|
formData.append('sale_price', salePrice);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Фото: для редактирования проверяем, удалено ли оно
|
||||||
if (photoFile) {
|
if (photoFile) {
|
||||||
formData.append('photo', photoFile);
|
formData.append('photo', photoFile);
|
||||||
|
} else if (isEditMode && document.getElementById('photoPreview').style.display === 'none') {
|
||||||
|
// Если фото было удалено
|
||||||
|
formData.append('remove_photo', '1');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Отправляем запрос на сервер
|
// Отправляем запрос на сервер
|
||||||
const confirmBtn = document.getElementById('confirmCreateTempKit');
|
const confirmBtn = document.getElementById('confirmCreateTempKit');
|
||||||
confirmBtn.disabled = true;
|
confirmBtn.disabled = true;
|
||||||
confirmBtn.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>Создание...';
|
|
||||||
|
const url = isEditMode
|
||||||
|
? `/pos/api/product-kits/${editingKitId}/update/`
|
||||||
|
: '/pos/api/create-temp-kit/';
|
||||||
|
|
||||||
|
const actionText = isEditMode ? 'Сохранение...' : 'Создание...';
|
||||||
|
confirmBtn.innerHTML = `<span class="spinner-border spinner-border-sm me-2"></span>${actionText}`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/pos/api/create-temp-kit/', {
|
const response = await fetch(url, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'X-CSRFToken': getCookie('csrftoken')
|
'X-CSRFToken': getCookie('csrftoken')
|
||||||
@@ -598,14 +722,18 @@ document.getElementById('confirmCreateTempKit').onclick = async () => {
|
|||||||
|
|
||||||
if (data.success) {
|
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_name}
|
||||||
Цена: ${data.kit_price} руб.
|
Цена: ${data.kit_price} руб.
|
||||||
Зарезервировано компонентов: ${data.reservations_count}`);
|
Зарезервировано компонентов: ${data.reservations_count}`;
|
||||||
|
|
||||||
// Очищаем корзину
|
alert(successMessage);
|
||||||
clearCart();
|
|
||||||
|
// Очищаем tempCart (изолированное состояние модалки)
|
||||||
|
tempCart.clear();
|
||||||
|
|
||||||
// Сбрасываем поля формы
|
// Сбрасываем поля формы
|
||||||
document.getElementById('tempKitDescription').value = '';
|
document.getElementById('tempKitDescription').value = '';
|
||||||
@@ -618,6 +746,10 @@ document.getElementById('confirmCreateTempKit').onclick = async () => {
|
|||||||
document.getElementById('salePrice').value = '';
|
document.getElementById('salePrice').value = '';
|
||||||
document.getElementById('salePriceBlock').style.display = 'none';
|
document.getElementById('salePriceBlock').style.display = 'none';
|
||||||
|
|
||||||
|
// Сбрасываем режим редактирования
|
||||||
|
isEditMode = false;
|
||||||
|
editingKitId = null;
|
||||||
|
|
||||||
// Закрываем модальное окно
|
// Закрываем модальное окно
|
||||||
const modal = bootstrap.Modal.getInstance(document.getElementById('createTempKitModal'));
|
const modal = bootstrap.Modal.getInstance(document.getElementById('createTempKitModal'));
|
||||||
modal.hide();
|
modal.hide();
|
||||||
@@ -632,11 +764,14 @@ document.getElementById('confirmCreateTempKit').onclick = async () => {
|
|||||||
alert(`Ошибка: ${data.error}`);
|
alert(`Ошибка: ${data.error}`);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error creating temp kit:', error);
|
console.error('Error saving kit:', error);
|
||||||
alert('Ошибка при создании комплекта');
|
alert('Ошибка при сохранении комплекта');
|
||||||
} finally {
|
} finally {
|
||||||
confirmBtn.disabled = false;
|
confirmBtn.disabled = false;
|
||||||
confirmBtn.innerHTML = '<i class="bi bi-check-circle"></i> Создать и зарезервировать';
|
const btnText = isEditMode
|
||||||
|
? '<i class="bi bi-check-circle"></i> Сохранить изменения'
|
||||||
|
: '<i class="bi bi-check-circle"></i> Создать и зарезервировать';
|
||||||
|
confirmBtn.innerHTML = btnText;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -656,6 +791,22 @@ function getCookie(name) {
|
|||||||
return cookieValue;
|
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 = '<i class="bi bi-check-circle"></i> Создать и зарезервировать';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Заглушки для функционала (будет реализовано позже)
|
// Заглушки для функционала (будет реализовано позже)
|
||||||
document.getElementById('checkoutNow').onclick = async () => {
|
document.getElementById('checkoutNow').onclick = async () => {
|
||||||
alert('Функционал будет подключен позже: создание заказа и списание со склада.');
|
alert('Функционал будет подключен позже: создание заказа и списание со склада.');
|
||||||
|
|||||||
@@ -9,5 +9,7 @@ urlpatterns = [
|
|||||||
path('api/showcase-items/', views.showcase_items_api, name='showcase-items-api'),
|
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/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/showcase-kits/', views.get_showcase_kits_api, name='showcase-kits-api'),
|
||||||
|
path('api/product-kits/<int:kit_id>/', views.get_product_kit_details, name='get-product-kit-details'),
|
||||||
|
path('api/product-kits/<int:kit_id>/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'),
|
path('api/create-temp-kit/', views.create_temp_kit_to_showcase, name='create-temp-kit-api'),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -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
|
@login_required
|
||||||
@require_http_methods(["POST"])
|
@require_http_methods(["POST"])
|
||||||
def create_temp_kit_to_showcase(request):
|
def create_temp_kit_to_showcase(request):
|
||||||
@@ -362,3 +412,145 @@ def create_temp_kit_to_showcase(request):
|
|||||||
'success': False,
|
'success': False,
|
||||||
'error': f'Ошибка при создании комплекта: {str(e)}'
|
'error': f'Ошибка при создании комплекта: {str(e)}'
|
||||||
}, status=500)
|
}, 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)
|
||||||
|
|||||||
Reference in New Issue
Block a user