feat: динамическая загрузка витринных комплектов в POS
- Добавлен API endpoint GET /pos/api/showcase-kits/ для получения актуальных витринных букетов - Изменена переменная SHOWCASE_KITS на изменяемую showcaseKits - Добавлена функция refreshShowcaseKits() для обновления данных с сервера - Кнопка ВИТРИНА теперь загружает свежие данные перед отображением - После создания временного букета автоматически обновляется список и переключается вид на витрину - Исправлена проблема с отображением только что созданных витринных букетов
This commit is contained in:
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
const CATEGORIES = JSON.parse(document.getElementById('categoriesData').textContent);
|
const CATEGORIES = JSON.parse(document.getElementById('categoriesData').textContent);
|
||||||
const ITEMS = JSON.parse(document.getElementById('itemsData').textContent); // Единый массив товаров и комплектов
|
const ITEMS = JSON.parse(document.getElementById('itemsData').textContent); // Единый массив товаров и комплектов
|
||||||
const SHOWCASE_KITS = JSON.parse(document.getElementById('showcaseKitsData').textContent); // Витринные комплекты
|
let showcaseKits = JSON.parse(document.getElementById('showcaseKitsData').textContent); // Витринные комплекты (изменяемый)
|
||||||
|
|
||||||
let currentCategoryId = null;
|
let currentCategoryId = null;
|
||||||
let isShowcaseView = false; // Флаг режима просмотра витринных букетов
|
let isShowcaseView = false; // Флаг режима просмотра витринных букетов
|
||||||
@@ -23,9 +23,10 @@ function renderCategories() {
|
|||||||
showcaseCard.className = 'card category-card showcase-card' + (isShowcaseView ? ' active' : '');
|
showcaseCard.className = 'card category-card showcase-card' + (isShowcaseView ? ' active' : '');
|
||||||
showcaseCard.style.backgroundColor = '#fff3cd';
|
showcaseCard.style.backgroundColor = '#fff3cd';
|
||||||
showcaseCard.style.borderColor = '#ffc107';
|
showcaseCard.style.borderColor = '#ffc107';
|
||||||
showcaseCard.onclick = () => {
|
showcaseCard.onclick = async () => {
|
||||||
isShowcaseView = true;
|
isShowcaseView = true;
|
||||||
currentCategoryId = null;
|
currentCategoryId = null;
|
||||||
|
await refreshShowcaseKits(); // Загружаем свежие данные
|
||||||
renderCategories();
|
renderCategories();
|
||||||
renderProducts();
|
renderProducts();
|
||||||
};
|
};
|
||||||
@@ -97,7 +98,7 @@ function renderProducts() {
|
|||||||
|
|
||||||
// Если выбран режим витрины - показываем витринные комплекты
|
// Если выбран режим витрины - показываем витринные комплекты
|
||||||
if (isShowcaseView) {
|
if (isShowcaseView) {
|
||||||
filtered = SHOWCASE_KITS;
|
filtered = showcaseKits; // Используем изменяемую переменную
|
||||||
} else {
|
} else {
|
||||||
// Обычный режим - показываем товары и комплекты
|
// Обычный режим - показываем товары и комплекты
|
||||||
filtered = currentCategoryId
|
filtered = currentCategoryId
|
||||||
@@ -334,6 +335,22 @@ async function openCreateTempKitModal() {
|
|||||||
modal.show();
|
modal.show();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Обновление списка витринных комплектов
|
||||||
|
async function refreshShowcaseKits() {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/pos/api/showcase-kits/');
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
showcaseKits = data.items;
|
||||||
|
} else {
|
||||||
|
console.error('Failed to refresh showcase kits:', data);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error refreshing showcase kits:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Загрузка списка витрин
|
// Загрузка списка витрин
|
||||||
async function loadShowcases() {
|
async function loadShowcases() {
|
||||||
try {
|
try {
|
||||||
@@ -344,12 +361,24 @@ async function loadShowcases() {
|
|||||||
select.innerHTML = '<option value="">Выберите витрину...</option>';
|
select.innerHTML = '<option value="">Выберите витрину...</option>';
|
||||||
|
|
||||||
if (data.success && data.showcases.length > 0) {
|
if (data.success && data.showcases.length > 0) {
|
||||||
|
let defaultShowcaseId = null;
|
||||||
|
|
||||||
data.showcases.forEach(showcase => {
|
data.showcases.forEach(showcase => {
|
||||||
const option = document.createElement('option');
|
const option = document.createElement('option');
|
||||||
option.value = showcase.id;
|
option.value = showcase.id;
|
||||||
option.textContent = `${showcase.name} (${showcase.warehouse_name})`;
|
option.textContent = `${showcase.name} (${showcase.warehouse_name})`;
|
||||||
select.appendChild(option);
|
select.appendChild(option);
|
||||||
|
|
||||||
|
// Запоминаем витрину склада по умолчанию
|
||||||
|
if (showcase.is_default_warehouse) {
|
||||||
|
defaultShowcaseId = showcase.id;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Автовыбор витрины склада по умолчанию
|
||||||
|
if (defaultShowcaseId) {
|
||||||
|
select.value = defaultShowcaseId;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
select.innerHTML = '<option value="">Нет доступных витрин</option>';
|
select.innerHTML = '<option value="">Нет доступных витрин</option>';
|
||||||
}
|
}
|
||||||
@@ -371,15 +400,15 @@ function renderTempKitItems() {
|
|||||||
if (item.type !== 'product') return;
|
if (item.type !== 'product') return;
|
||||||
|
|
||||||
const itemDiv = document.createElement('div');
|
const itemDiv = document.createElement('div');
|
||||||
itemDiv.className = 'd-flex justify-content-between align-items-center mb-2 pb-2 border-bottom';
|
itemDiv.className = 'd-flex justify-content-between align-items-center mb-1 pb-1 border-bottom';
|
||||||
itemDiv.innerHTML = `
|
itemDiv.innerHTML = `
|
||||||
<div>
|
<div>
|
||||||
<strong>${item.name}</strong>
|
<strong class="small">${item.name}</strong>
|
||||||
<br>
|
<br>
|
||||||
<small class="text-muted">${item.qty} шт × ${formatMoney(item.price)} руб.</small>
|
<small class="text-muted">${item.qty} шт × ${formatMoney(item.price)} руб.</small>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-end">
|
<div class="text-end">
|
||||||
<strong>${formatMoney(item.qty * item.price)} руб.</strong>
|
<strong class="small">${formatMoney(item.qty * item.price)} руб.</strong>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
container.appendChild(itemDiv);
|
container.appendChild(itemDiv);
|
||||||
@@ -387,13 +416,120 @@ function renderTempKitItems() {
|
|||||||
estimatedTotal += item.qty * item.price;
|
estimatedTotal += item.qty * item.price;
|
||||||
});
|
});
|
||||||
|
|
||||||
document.getElementById('tempKitEstimatedPrice').textContent = formatMoney(estimatedTotal);
|
// Обновляем все расчеты цен
|
||||||
|
updatePriceCalculations(estimatedTotal);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Расчет и обновление всех цен
|
||||||
|
function updatePriceCalculations(basePrice = null) {
|
||||||
|
// Если basePrice не передан, пересчитываем из корзины
|
||||||
|
if (basePrice === null) {
|
||||||
|
basePrice = 0;
|
||||||
|
cart.forEach((item, cartKey) => {
|
||||||
|
if (item.type === 'product') {
|
||||||
|
basePrice += item.qty * item.price;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Базовая цена
|
||||||
|
document.getElementById('tempKitBasePrice').textContent = formatMoney(basePrice) + ' руб.';
|
||||||
|
|
||||||
|
// Корректировка
|
||||||
|
const adjustmentType = document.getElementById('priceAdjustmentType').value;
|
||||||
|
const adjustmentValue = parseFloat(document.getElementById('priceAdjustmentValue').value) || 0;
|
||||||
|
|
||||||
|
let calculatedPrice = basePrice;
|
||||||
|
if (adjustmentType !== 'none' && adjustmentValue > 0) {
|
||||||
|
if (adjustmentType === 'increase_percent') {
|
||||||
|
calculatedPrice = basePrice + (basePrice * adjustmentValue / 100);
|
||||||
|
} else if (adjustmentType === 'increase_amount') {
|
||||||
|
calculatedPrice = basePrice + adjustmentValue;
|
||||||
|
} else if (adjustmentType === 'decrease_percent') {
|
||||||
|
calculatedPrice = Math.max(0, basePrice - (basePrice * adjustmentValue / 100));
|
||||||
|
} else if (adjustmentType === 'decrease_amount') {
|
||||||
|
calculatedPrice = Math.max(0, basePrice - adjustmentValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('tempKitCalculatedPrice').textContent = formatMoney(calculatedPrice) + ' руб.';
|
||||||
|
|
||||||
|
// Финальная цена (с учетом sale_price если задана)
|
||||||
|
const useSalePrice = document.getElementById('useSalePrice').checked;
|
||||||
|
const salePrice = parseFloat(document.getElementById('salePrice').value) || 0;
|
||||||
|
|
||||||
|
let finalPrice = calculatedPrice;
|
||||||
|
if (useSalePrice && salePrice > 0) {
|
||||||
|
finalPrice = salePrice;
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('tempKitFinalPrice').textContent = formatMoney(finalPrice);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Обработчики для полей цены
|
||||||
|
document.getElementById('priceAdjustmentType').addEventListener('change', function() {
|
||||||
|
const adjustmentBlock = document.getElementById('adjustmentValueBlock');
|
||||||
|
if (this.value === 'none') {
|
||||||
|
adjustmentBlock.style.display = 'none';
|
||||||
|
document.getElementById('priceAdjustmentValue').value = '0';
|
||||||
|
} else {
|
||||||
|
adjustmentBlock.style.display = 'block';
|
||||||
|
}
|
||||||
|
updatePriceCalculations();
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('priceAdjustmentValue').addEventListener('input', function() {
|
||||||
|
updatePriceCalculations();
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('useSalePrice').addEventListener('change', function() {
|
||||||
|
const salePriceBlock = document.getElementById('salePriceBlock');
|
||||||
|
if (this.checked) {
|
||||||
|
salePriceBlock.style.display = 'block';
|
||||||
|
} else {
|
||||||
|
salePriceBlock.style.display = 'none';
|
||||||
|
document.getElementById('salePrice').value = '';
|
||||||
|
}
|
||||||
|
updatePriceCalculations();
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('salePrice').addEventListener('input', function() {
|
||||||
|
updatePriceCalculations();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Обработчик загрузки фото
|
||||||
|
document.getElementById('tempKitPhoto').addEventListener('change', function(e) {
|
||||||
|
const file = e.target.files[0];
|
||||||
|
if (file) {
|
||||||
|
if (!file.type.startsWith('image/')) {
|
||||||
|
alert('Пожалуйста, выберите файл изображения');
|
||||||
|
this.value = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Превью
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = function(event) {
|
||||||
|
document.getElementById('photoPreviewImg').src = event.target.result;
|
||||||
|
document.getElementById('photoPreview').style.display = 'block';
|
||||||
|
};
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Удаление фото
|
||||||
|
document.getElementById('removePhoto').addEventListener('click', function() {
|
||||||
|
document.getElementById('tempKitPhoto').value = '';
|
||||||
|
document.getElementById('photoPreview').style.display = 'none';
|
||||||
|
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;
|
||||||
|
const description = document.getElementById('tempKitDescription').value.trim();
|
||||||
|
const photoFile = document.getElementById('tempKitPhoto').files[0];
|
||||||
|
|
||||||
// Валидация
|
// Валидация
|
||||||
if (!kitName) {
|
if (!kitName) {
|
||||||
@@ -422,6 +558,27 @@ document.getElementById('confirmCreateTempKit').onclick = async () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Получаем данные о ценах
|
||||||
|
const priceAdjustmentType = document.getElementById('priceAdjustmentType').value;
|
||||||
|
const priceAdjustmentValue = parseFloat(document.getElementById('priceAdjustmentValue').value) || 0;
|
||||||
|
const useSalePrice = document.getElementById('useSalePrice').checked;
|
||||||
|
const salePrice = useSalePrice ? (parseFloat(document.getElementById('salePrice').value) || 0) : 0;
|
||||||
|
|
||||||
|
// Формируем FormData для отправки с файлом
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('kit_name', kitName);
|
||||||
|
formData.append('showcase_id', showcaseId);
|
||||||
|
formData.append('description', description);
|
||||||
|
formData.append('items', JSON.stringify(items));
|
||||||
|
formData.append('price_adjustment_type', priceAdjustmentType);
|
||||||
|
formData.append('price_adjustment_value', priceAdjustmentValue);
|
||||||
|
if (useSalePrice && salePrice > 0) {
|
||||||
|
formData.append('sale_price', salePrice);
|
||||||
|
}
|
||||||
|
if (photoFile) {
|
||||||
|
formData.append('photo', photoFile);
|
||||||
|
}
|
||||||
|
|
||||||
// Отправляем запрос на сервер
|
// Отправляем запрос на сервер
|
||||||
const confirmBtn = document.getElementById('confirmCreateTempKit');
|
const confirmBtn = document.getElementById('confirmCreateTempKit');
|
||||||
confirmBtn.disabled = true;
|
confirmBtn.disabled = true;
|
||||||
@@ -431,16 +588,10 @@ document.getElementById('confirmCreateTempKit').onclick = async () => {
|
|||||||
const response = await fetch('/pos/api/create-temp-kit/', {
|
const response = await fetch('/pos/api/create-temp-kit/', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'X-CSRFToken': getCookie('csrftoken')
|
'X-CSRFToken': getCookie('csrftoken')
|
||||||
|
// Не указываем Content-Type - браузер сам установит multipart/form-data
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: formData
|
||||||
kit_name: kitName,
|
|
||||||
showcase_id: parseInt(showcaseId),
|
|
||||||
items: items,
|
|
||||||
price_adjustment_type: 'none',
|
|
||||||
price_adjustment_value: 0
|
|
||||||
})
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
@@ -456,9 +607,27 @@ document.getElementById('confirmCreateTempKit').onclick = async () => {
|
|||||||
// Очищаем корзину
|
// Очищаем корзину
|
||||||
clearCart();
|
clearCart();
|
||||||
|
|
||||||
|
// Сбрасываем поля формы
|
||||||
|
document.getElementById('tempKitDescription').value = '';
|
||||||
|
document.getElementById('tempKitPhoto').value = '';
|
||||||
|
document.getElementById('photoPreview').style.display = 'none';
|
||||||
|
document.getElementById('priceAdjustmentType').value = 'none';
|
||||||
|
document.getElementById('priceAdjustmentValue').value = '0';
|
||||||
|
document.getElementById('adjustmentValueBlock').style.display = 'none';
|
||||||
|
document.getElementById('useSalePrice').checked = false;
|
||||||
|
document.getElementById('salePrice').value = '';
|
||||||
|
document.getElementById('salePriceBlock').style.display = 'none';
|
||||||
|
|
||||||
// Закрываем модальное окно
|
// Закрываем модальное окно
|
||||||
const modal = bootstrap.Modal.getInstance(document.getElementById('createTempKitModal'));
|
const modal = bootstrap.Modal.getInstance(document.getElementById('createTempKitModal'));
|
||||||
modal.hide();
|
modal.hide();
|
||||||
|
|
||||||
|
// Обновляем витринные комплекты и переключаемся на вид витрины
|
||||||
|
isShowcaseView = true;
|
||||||
|
currentCategoryId = null;
|
||||||
|
await refreshShowcaseKits();
|
||||||
|
renderCategories();
|
||||||
|
renderProducts();
|
||||||
} else {
|
} else {
|
||||||
alert(`Ошибка: ${data.error}`);
|
alert(`Ошибка: ${data.error}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -107,7 +107,7 @@
|
|||||||
|
|
||||||
<!-- Modal: Создание временного комплекта на витрину -->
|
<!-- Modal: Создание временного комплекта на витрину -->
|
||||||
<div class="modal fade" id="createTempKitModal" tabindex="-1" aria-labelledby="createTempKitModalLabel" aria-hidden="true">
|
<div class="modal fade" id="createTempKitModal" tabindex="-1" aria-labelledby="createTempKitModalLabel" aria-hidden="true">
|
||||||
<div class="modal-dialog modal-lg">
|
<div class="modal-dialog modal-xl">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h5 class="modal-title" id="createTempKitModalLabel">
|
<h5 class="modal-title" id="createTempKitModalLabel">
|
||||||
@@ -116,6 +116,9 @@
|
|||||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
|
<div class="row">
|
||||||
|
<!-- Левая колонка: основные поля -->
|
||||||
|
<div class="col-md-6">
|
||||||
<!-- Название комплекта -->
|
<!-- Название комплекта -->
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="tempKitName" class="form-label">Название комплекта *</label>
|
<label for="tempKitName" class="form-label">Название комплекта *</label>
|
||||||
@@ -130,18 +133,93 @@
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Описание -->
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="tempKitDescription" class="form-label">Описание (опционально)</label>
|
||||||
|
<textarea class="form-control" id="tempKitDescription" rows="3" placeholder="Краткое описание комплекта"></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Загрузка фото -->
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="tempKitPhoto" class="form-label">Фото комплекта (опционально)</label>
|
||||||
|
<input type="file" class="form-control" id="tempKitPhoto" accept="image/*">
|
||||||
|
<div id="photoPreview" class="mt-2" style="display: none;">
|
||||||
|
<img id="photoPreviewImg" src="" alt="Preview" class="img-thumbnail" style="max-width: 200px; max-height: 200px;">
|
||||||
|
<button type="button" class="btn btn-sm btn-danger ms-2" id="removePhoto">
|
||||||
|
<i class="bi bi-x"></i> Удалить
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Правая колонка: цены и состав -->
|
||||||
|
<div class="col-md-6">
|
||||||
<!-- Список товаров в корзине -->
|
<!-- Список товаров в корзине -->
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label">Товары в комплекте</label>
|
<label class="form-label">Товары в комплекте</label>
|
||||||
<div class="border rounded p-3" id="tempKitItemsList" style="max-height: 300px; overflow-y: auto;">
|
<div class="border rounded p-2" id="tempKitItemsList" style="max-height: 200px; overflow-y: auto; background: #f8f9fa;">
|
||||||
<!-- Динамически заполняется через JS -->
|
<!-- Динамически заполняется через JS -->
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Итоговая цена (расчётная) -->
|
<!-- Блок ценообразования -->
|
||||||
<div class="alert alert-info mb-0">
|
<div class="card">
|
||||||
<strong>Расчётная цена:</strong> <span id="tempKitEstimatedPrice">0.00</span> руб.
|
<div class="card-header bg-light">
|
||||||
<small class="d-block text-muted">Цена будет пересчитана автоматически после создания</small>
|
<strong>Ценообразование</strong>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<!-- Базовая цена -->
|
||||||
|
<div class="mb-2">
|
||||||
|
<small class="text-muted">Базовая цена (сумма компонентов):</small>
|
||||||
|
<div class="fw-bold" id="tempKitBasePrice">0.00 руб.</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Корректировка цены -->
|
||||||
|
<div class="mb-2">
|
||||||
|
<label for="priceAdjustmentType" class="form-label small">Корректировка цены</label>
|
||||||
|
<select class="form-select form-select-sm" id="priceAdjustmentType">
|
||||||
|
<option value="none">Без изменения</option>
|
||||||
|
<option value="increase_percent">Увеличить на %</option>
|
||||||
|
<option value="increase_amount">Увеличить на сумму</option>
|
||||||
|
<option value="decrease_percent">Уменьшить на %</option>
|
||||||
|
<option value="decrease_amount">Уменьшить на сумму</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-2" id="adjustmentValueBlock" style="display: none;">
|
||||||
|
<label for="priceAdjustmentValue" class="form-label small">Значение</label>
|
||||||
|
<input type="number" class="form-control form-control-sm" id="priceAdjustmentValue"
|
||||||
|
min="0" step="0.01" value="0" placeholder="0.00">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Расчётная цена с корректировкой -->
|
||||||
|
<div class="mb-2 pb-2 border-bottom">
|
||||||
|
<small class="text-muted">Расчётная цена:</small>
|
||||||
|
<div class="fw-bold text-primary" id="tempKitCalculatedPrice">0.00 руб.</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Ручная финальная цена (sale_price) -->
|
||||||
|
<div class="mb-0">
|
||||||
|
<div class="form-check form-switch mb-2">
|
||||||
|
<input class="form-check-input" type="checkbox" id="useSalePrice">
|
||||||
|
<label class="form-check-label small" for="useSalePrice">
|
||||||
|
Установить свою цену (приоритет)
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div id="salePriceBlock" style="display: none;">
|
||||||
|
<input type="number" class="form-control form-control-sm" id="salePrice"
|
||||||
|
min="0" step="0.01" placeholder="Введите цену">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Итоговая цена продажи -->
|
||||||
|
<div class="alert alert-success mt-3 mb-0">
|
||||||
|
<strong>Итоговая цена продажи:</strong><br>
|
||||||
|
<span class="fs-4" id="tempKitFinalPrice">0.00</span> руб.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
|
|||||||
@@ -8,5 +8,6 @@ urlpatterns = [
|
|||||||
path('', views.pos_terminal, name='terminal'),
|
path('', views.pos_terminal, name='terminal'),
|
||||||
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/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'),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ from django.http import JsonResponse
|
|||||||
from django.views.decorators.http import require_http_methods
|
from django.views.decorators.http import require_http_methods
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from decimal import Decimal
|
from decimal import Decimal, InvalidOperation
|
||||||
import json
|
import json
|
||||||
|
|
||||||
from products.models import Product, ProductCategory, ProductKit, KitItem
|
from products.models import Product, ProductCategory, ProductKit, KitItem
|
||||||
@@ -184,7 +184,8 @@ def get_showcases_api(request):
|
|||||||
'id': s.id,
|
'id': s.id,
|
||||||
'name': s.name,
|
'name': s.name,
|
||||||
'warehouse_name': s.warehouse.name,
|
'warehouse_name': s.warehouse.name,
|
||||||
'warehouse_id': s.warehouse.id
|
'warehouse_id': s.warehouse.id,
|
||||||
|
'is_default_warehouse': s.warehouse.is_default # Для автовыбора
|
||||||
} for s in showcases]
|
} for s in showcases]
|
||||||
|
|
||||||
return JsonResponse({
|
return JsonResponse({
|
||||||
@@ -193,6 +194,21 @@ def get_showcases_api(request):
|
|||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
@require_http_methods(["GET"])
|
||||||
|
def get_showcase_kits_api(request):
|
||||||
|
"""
|
||||||
|
API endpoint для получения актуального списка витринных комплектов.
|
||||||
|
Используется для динамического обновления после создания нового букета.
|
||||||
|
"""
|
||||||
|
showcase_kits_data = get_showcase_kits_for_pos()
|
||||||
|
|
||||||
|
return JsonResponse({
|
||||||
|
'success': True,
|
||||||
|
'items': showcase_kits_data
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
@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):
|
||||||
@@ -200,26 +216,39 @@ def create_temp_kit_to_showcase(request):
|
|||||||
API endpoint для создания временного комплекта из корзины POS
|
API endpoint для создания временного комплекта из корзины POS
|
||||||
и резервирования его на витрину.
|
и резервирования его на витрину.
|
||||||
|
|
||||||
Ожидаемый payload:
|
Ожидаемый payload (multipart/form-data):
|
||||||
{
|
- kit_name: Название комплекта
|
||||||
"kit_name": "Название комплекта",
|
- showcase_id: ID витрины
|
||||||
"showcase_id": 1,
|
- items: JSON список [{product_id, quantity}, ...]
|
||||||
"items": [
|
- description: Описание (опционально)
|
||||||
{"product_id": 1, "quantity": 2.0},
|
- price_adjustment_type: Тип корректировки (опционально)
|
||||||
{"product_id": 3, "quantity": 1.0}
|
- price_adjustment_value: Значение корректировки (опционально)
|
||||||
],
|
- sale_price: Ручная финальная цена (опционально)
|
||||||
"price_adjustment_type": "none", // optional
|
- photo: Файл изображения (опционально)
|
||||||
"price_adjustment_value": 0 // optional
|
|
||||||
}
|
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
data = json.loads(request.body)
|
# Получаем данные из FormData
|
||||||
|
kit_name = request.POST.get('kit_name', '').strip()
|
||||||
|
showcase_id = request.POST.get('showcase_id')
|
||||||
|
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')
|
||||||
|
|
||||||
kit_name = data.get('kit_name', '').strip()
|
# Парсим items из JSON
|
||||||
showcase_id = data.get('showcase_id')
|
items = json.loads(items_json)
|
||||||
items = data.get('items', [])
|
|
||||||
price_adjustment_type = data.get('price_adjustment_type', 'none')
|
# Sale price (опционально)
|
||||||
price_adjustment_value = Decimal(str(data.get('price_adjustment_value', 0)))
|
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:
|
if not kit_name:
|
||||||
@@ -275,10 +304,12 @@ def create_temp_kit_to_showcase(request):
|
|||||||
# 1. Создаём ProductKit (is_temporary=True)
|
# 1. Создаём ProductKit (is_temporary=True)
|
||||||
kit = ProductKit.objects.create(
|
kit = ProductKit.objects.create(
|
||||||
name=kit_name,
|
name=kit_name,
|
||||||
|
description=description,
|
||||||
is_temporary=True,
|
is_temporary=True,
|
||||||
status='active',
|
status='active',
|
||||||
price_adjustment_type=price_adjustment_type,
|
price_adjustment_type=price_adjustment_type,
|
||||||
price_adjustment_value=price_adjustment_value
|
price_adjustment_value=price_adjustment_value,
|
||||||
|
sale_price=sale_price
|
||||||
)
|
)
|
||||||
|
|
||||||
# 2. Создаём KitItem для каждого товара из корзины
|
# 2. Создаём KitItem для каждого товара из корзины
|
||||||
@@ -292,7 +323,16 @@ def create_temp_kit_to_showcase(request):
|
|||||||
# 3. Пересчитываем цену комплекта
|
# 3. Пересчитываем цену комплекта
|
||||||
kit.recalculate_base_price()
|
kit.recalculate_base_price()
|
||||||
|
|
||||||
# 4. Резервируем комплект на витрину
|
# 4. Загружаем фото, если есть
|
||||||
|
if photo_file:
|
||||||
|
from products.models import ProductKitPhoto
|
||||||
|
ProductKitPhoto.objects.create(
|
||||||
|
kit=kit,
|
||||||
|
image=photo_file,
|
||||||
|
order=0
|
||||||
|
)
|
||||||
|
|
||||||
|
# 5. Резервируем комплект на витрину
|
||||||
result = ShowcaseManager.reserve_kit_to_showcase(
|
result = ShowcaseManager.reserve_kit_to_showcase(
|
||||||
product_kit=kit,
|
product_kit=kit,
|
||||||
showcase=showcase,
|
showcase=showcase,
|
||||||
|
|||||||
Reference in New Issue
Block a user