Исправлена ошибка создания заказов в POS после рефакторинга модели доставки
- Обновлён pos/views.py: метод pos_checkout теперь создаёт Order и связанную модель Delivery - Обновлён showcase_manager.py: метод sell_showcase_item_to_customer использует новую архитектуру - Удалён устаревший скрипт create_demo_orders.py - Исправлена ошибка 'property is_delivery of Order object has no setter'
This commit is contained in:
@@ -34,6 +34,12 @@ let editingKitId = null;
|
||||
// Временная корзина для модального окна создания/редактирования комплекта
|
||||
const tempCart = new Map();
|
||||
|
||||
// ===== ПЕРЕМЕННЫЕ ДЛЯ МОДАЛЬНОГО ОКНА ВЫБОРА ЕДИНИЦЫ ПРОДАЖИ =====
|
||||
let unitModalProduct = null; // Текущий товар для модального окна
|
||||
let unitModalSalesUnits = []; // Список единиц продажи
|
||||
let selectedSalesUnit = null; // Выбранная единица продажи
|
||||
let unitModalInstance = null; // Bootstrap Modal instance
|
||||
|
||||
// ===== СОХРАНЕНИЕ КОРЗИНЫ В REDIS =====
|
||||
|
||||
let saveCartTimeout = null;
|
||||
@@ -390,6 +396,255 @@ async function createNewCustomer() {
|
||||
}
|
||||
}
|
||||
|
||||
// ===== ФУНКЦИИ ДЛЯ МОДАЛЬНОГО ОКНА ВЫБОРА ЕДИНИЦЫ ПРОДАЖИ =====
|
||||
|
||||
/**
|
||||
* Открывает модальное окно выбора единицы продажи
|
||||
* @param {object} product - Объект товара с информацией о единицах продажи
|
||||
*/
|
||||
async function openProductUnitModal(product) {
|
||||
unitModalProduct = product;
|
||||
|
||||
// Устанавливаем название товара
|
||||
document.getElementById('unitModalProductName').textContent =
|
||||
`${product.name}${product.sku ? ' (' + product.sku + ')' : ''}`;
|
||||
|
||||
// Загружаем единицы продажи
|
||||
try {
|
||||
const response = await fetch(
|
||||
`/products/api/products/${product.id}/sales-units/?warehouse=${currentWarehouse.id}`
|
||||
);
|
||||
const data = await response.json();
|
||||
|
||||
if (!data.success || !data.sales_units || data.sales_units.length === 0) {
|
||||
alert('Не удалось загрузить единицы продажи');
|
||||
return;
|
||||
}
|
||||
|
||||
unitModalSalesUnits = data.sales_units;
|
||||
|
||||
// Отрисовываем список единиц
|
||||
renderUnitSelectionList();
|
||||
|
||||
// Выбираем единицу по умолчанию или первую
|
||||
const defaultUnit = unitModalSalesUnits.find(u => u.is_default) || unitModalSalesUnits[0];
|
||||
if (defaultUnit) {
|
||||
selectUnit(defaultUnit);
|
||||
}
|
||||
|
||||
// Открываем модальное окно
|
||||
if (!unitModalInstance) {
|
||||
unitModalInstance = new bootstrap.Modal(document.getElementById('selectProductUnitModal'));
|
||||
}
|
||||
unitModalInstance.show();
|
||||
|
||||
} catch (error) {
|
||||
console.error('Ошибка загрузки единиц продажи:', error);
|
||||
alert('Ошибка загрузки данных. Попробуйте ещё раз.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Отрисовывает список единиц продажи
|
||||
*/
|
||||
function renderUnitSelectionList() {
|
||||
const listContainer = document.getElementById('unitSelectionList');
|
||||
listContainer.innerHTML = '';
|
||||
|
||||
unitModalSalesUnits.forEach(unit => {
|
||||
const card = document.createElement('div');
|
||||
card.className = 'unit-selection-card';
|
||||
card.dataset.unitId = unit.id;
|
||||
card.onclick = () => selectUnit(unit);
|
||||
|
||||
// Доступное количество
|
||||
const availableQty = parseFloat(unit.available_quantity || 0);
|
||||
let stockBadgeClass = 'stock-badge-none';
|
||||
let stockText = 'Нет на складе';
|
||||
|
||||
if (availableQty > 10) {
|
||||
stockBadgeClass = 'stock-badge-good';
|
||||
stockText = `${availableQty} ${unit.unit_short_name} доступно`;
|
||||
} else if (availableQty > 0) {
|
||||
stockBadgeClass = 'stock-badge-low';
|
||||
stockText = `${availableQty} ${unit.unit_short_name} доступно`;
|
||||
}
|
||||
|
||||
// Бейдж "По умолчанию"
|
||||
const defaultBadge = unit.is_default ?
|
||||
'<span class="badge bg-primary ms-2">По умолчанию</span>' : '';
|
||||
|
||||
card.innerHTML = `
|
||||
<div class="d-flex justify-content-between align-items-start">
|
||||
<div class="flex-grow-1">
|
||||
<div class="unit-name">${unit.name}${defaultBadge}</div>
|
||||
<div class="unit-code text-muted">${unit.unit_code} (${unit.unit_short_name})</div>
|
||||
</div>
|
||||
<div class="unit-price">${formatMoney(unit.actual_price)} руб</div>
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<span class="badge ${stockBadgeClass}">${stockText}</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
listContainer.appendChild(card);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Выбирает единицу продажи
|
||||
* @param {object} unit - Объект единицы продажи
|
||||
*/
|
||||
function selectUnit(unit) {
|
||||
selectedSalesUnit = unit;
|
||||
|
||||
// Обновляем визуальное выделение
|
||||
document.querySelectorAll('.unit-selection-card').forEach(card => {
|
||||
if (card.dataset.unitId === String(unit.id)) {
|
||||
card.classList.add('selected');
|
||||
} else {
|
||||
card.classList.remove('selected');
|
||||
}
|
||||
});
|
||||
|
||||
// Обновляем отображение выбранной единицы
|
||||
document.getElementById('selectedUnitDisplay').textContent =
|
||||
`${unit.name} (${unit.unit_short_name})`;
|
||||
|
||||
// Устанавливаем минимальное количество и шаг
|
||||
const qtyInput = document.getElementById('unitModalQuantity');
|
||||
qtyInput.value = roundQuantity(unit.min_quantity, 3);
|
||||
qtyInput.min = unit.min_quantity;
|
||||
qtyInput.step = unit.quantity_step;
|
||||
|
||||
// Устанавливаем цену
|
||||
document.getElementById('unitModalPrice').value = unit.actual_price;
|
||||
|
||||
// Обновляем подсказку
|
||||
const hintEl = document.getElementById('unitQtyHint');
|
||||
hintEl.textContent = `Мин. ${unit.min_quantity}, шаг ${unit.quantity_step}`;
|
||||
|
||||
// Сбрасываем индикатор изменения цены
|
||||
document.getElementById('priceOverrideIndicator').style.display = 'none';
|
||||
|
||||
// Пересчитываем итого
|
||||
calculateUnitModalSubtotal();
|
||||
|
||||
// Валидируем количество
|
||||
validateUnitQuantity();
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверяет количество на соответствие ограничениям
|
||||
* @returns {boolean} - true если валидно
|
||||
*/
|
||||
function validateUnitQuantity() {
|
||||
if (!selectedSalesUnit) return false;
|
||||
|
||||
const qtyInput = document.getElementById('unitModalQuantity');
|
||||
const qty = parseFloat(qtyInput.value);
|
||||
const errorEl = document.getElementById('unitQtyError');
|
||||
const confirmBtn = document.getElementById('confirmAddUnitToCart');
|
||||
|
||||
// Проверка минимального количества
|
||||
if (qty < parseFloat(selectedSalesUnit.min_quantity)) {
|
||||
errorEl.textContent = `Минимальное количество: ${selectedSalesUnit.min_quantity}`;
|
||||
errorEl.style.display = 'block';
|
||||
confirmBtn.disabled = true;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Проверка шага (с учётом погрешности)
|
||||
const step = parseFloat(selectedSalesUnit.quantity_step);
|
||||
const minQty = parseFloat(selectedSalesUnit.min_quantity);
|
||||
const diff = qty - minQty;
|
||||
const remainder = diff % step;
|
||||
const epsilon = 0.0001;
|
||||
|
||||
if (remainder > epsilon && (step - remainder) > epsilon) {
|
||||
errorEl.textContent = `Количество должно быть кратно ${step}`;
|
||||
errorEl.style.display = 'block';
|
||||
confirmBtn.disabled = true;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Всё ок, скрываем ошибку
|
||||
errorEl.style.display = 'none';
|
||||
confirmBtn.disabled = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Рассчитывает итоговую сумму
|
||||
*/
|
||||
function calculateUnitModalSubtotal() {
|
||||
const qtyRaw = parseFloat(document.getElementById('unitModalQuantity').value) || 0;
|
||||
const qty = roundQuantity(qtyRaw, 3); // Округляем количество
|
||||
const price = parseFloat(document.getElementById('unitModalPrice').value) || 0;
|
||||
// Округляем до 2 знаков после запятой для корректного отображения
|
||||
const subtotal = Math.round(qty * price * 100) / 100;
|
||||
|
||||
document.getElementById('unitModalSubtotal').textContent = `${formatMoney(subtotal)} руб`;
|
||||
|
||||
// Проверяем изменение цены
|
||||
if (selectedSalesUnit && Math.abs(price - parseFloat(selectedSalesUnit.actual_price)) > 0.01) {
|
||||
document.getElementById('priceOverrideIndicator').style.display = 'block';
|
||||
} else {
|
||||
document.getElementById('priceOverrideIndicator').style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Добавляет товар с выбранной единицей в корзину
|
||||
*/
|
||||
function addToCartFromModal() {
|
||||
if (!validateUnitQuantity()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const qtyRaw = parseFloat(document.getElementById('unitModalQuantity').value);
|
||||
const qty = roundQuantity(qtyRaw, 3); // Округляем количество
|
||||
const price = parseFloat(document.getElementById('unitModalPrice').value);
|
||||
const priceOverridden = Math.abs(price - parseFloat(selectedSalesUnit.actual_price)) > 0.01;
|
||||
|
||||
// Формируем ключ корзины: product-{id}-{sales_unit_id}
|
||||
const cartKey = `product-${unitModalProduct.id}-${selectedSalesUnit.id}`;
|
||||
|
||||
// Добавляем или обновляем в корзине
|
||||
if (cart.has(cartKey)) {
|
||||
const existing = cart.get(cartKey);
|
||||
existing.qty = roundQuantity(existing.qty + qty, 3); // Округляем сумму
|
||||
existing.price = price; // Обновляем цену
|
||||
existing.quantity_step = parseFloat(selectedSalesUnit.quantity_step) || 1; // Обновляем шаг
|
||||
existing.price_overridden = priceOverridden;
|
||||
} else {
|
||||
cart.set(cartKey, {
|
||||
id: unitModalProduct.id,
|
||||
name: unitModalProduct.name,
|
||||
price: price,
|
||||
qty: qty,
|
||||
type: 'product',
|
||||
sales_unit_id: selectedSalesUnit.id,
|
||||
unit_name: selectedSalesUnit.name,
|
||||
unit_short_name: selectedSalesUnit.unit_short_name,
|
||||
quantity_step: parseFloat(selectedSalesUnit.quantity_step) || 1, // Сохраняем шаг количества
|
||||
price_overridden: priceOverridden
|
||||
});
|
||||
}
|
||||
|
||||
// Обновляем корзину
|
||||
renderCart();
|
||||
saveCartToRedis();
|
||||
|
||||
// Перерисовываем товары для обновления визуального остатка
|
||||
if (!isShowcaseView) {
|
||||
renderProducts();
|
||||
}
|
||||
|
||||
// Закрываем модальное окно
|
||||
unitModalInstance.hide();
|
||||
}
|
||||
|
||||
function renderCategories() {
|
||||
const grid = document.getElementById('categoryGrid');
|
||||
grid.innerHTML = '';
|
||||
@@ -755,6 +1010,13 @@ function setupInfiniteScroll() {
|
||||
}
|
||||
|
||||
async function addToCart(item) {
|
||||
// ПРОВЕРКА НА НАЛИЧИЕ НЕСКОЛЬКИХ ЕДИНИЦ ПРОДАЖИ
|
||||
if (item.type === 'product' && item.sales_units_count > 1) {
|
||||
// Открываем модальное окно выбора единицы
|
||||
await openProductUnitModal(item);
|
||||
return;
|
||||
}
|
||||
|
||||
const cartKey = `${item.type}-${item.id}`; // Уникальный ключ: "product-1" или "kit-1"
|
||||
|
||||
// СПЕЦИАЛЬНАЯ ЛОГИКА ДЛЯ ВИТРИННЫХ КОМПЛЕКТОВ (Soft Lock)
|
||||
@@ -819,7 +1081,8 @@ async function addToCart(item) {
|
||||
if (!cart.has(cartKey)) {
|
||||
cart.set(cartKey, { id: item.id, name: item.name, price: Number(item.price), qty: 1, type: item.type });
|
||||
} else {
|
||||
cart.get(cartKey).qty += 1;
|
||||
const cartItem = cart.get(cartKey);
|
||||
cartItem.qty = roundQuantity(cartItem.qty + 1, 3);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -850,10 +1113,13 @@ async function updateCartItemQty(cartKey, newQty) {
|
||||
const item = cart.get(cartKey);
|
||||
if (!item) return;
|
||||
|
||||
if (newQty <= 0) {
|
||||
// Округляем новое количество
|
||||
const roundedQty = roundQuantity(newQty, 3);
|
||||
|
||||
if (roundedQty <= 0) {
|
||||
await removeFromCart(cartKey);
|
||||
} else {
|
||||
item.qty = newQty;
|
||||
item.qty = roundedQty;
|
||||
renderCart();
|
||||
saveCartToRedis();
|
||||
|
||||
@@ -898,10 +1164,16 @@ function renderCart() {
|
||||
if (item.type === 'kit' || item.type === 'showcase_kit') {
|
||||
typeIcon = '<i class="bi bi-box-seam text-info" title="Комплект"></i> ';
|
||||
}
|
||||
|
||||
// Единица продажи (если есть)
|
||||
let unitInfo = '';
|
||||
if (item.sales_unit_id && item.unit_name) {
|
||||
unitInfo = ` <span class="badge bg-secondary" style="font-size: 0.7rem;">${item.unit_name}</span>`;
|
||||
}
|
||||
|
||||
namePrice.innerHTML = `
|
||||
<div class="fw-semibold small">${typeIcon}${item.name}</div>
|
||||
<div class="text-muted" style="font-size: 0.75rem;">${formatMoney(item.price)} / шт</div>
|
||||
<div class="fw-semibold small">${typeIcon}${item.name}${unitInfo}</div>
|
||||
<div class="text-muted" style="font-size: 0.75rem;">${formatMoney(item.price)} / ${item.unit_short_name || 'шт'}</div>
|
||||
`;
|
||||
|
||||
// Знак умножения
|
||||
@@ -932,7 +1204,7 @@ function renderCart() {
|
||||
qtyInput.style.width = '60px';
|
||||
qtyInput.style.textAlign = 'center';
|
||||
qtyInput.style.padding = '0.375rem 0.25rem';
|
||||
qtyInput.value = item.qty;
|
||||
qtyInput.value = roundQuantity(item.qty, 3);
|
||||
qtyInput.min = 1;
|
||||
qtyInput.readOnly = true; // Только чтение - изменяем только через +/-
|
||||
qtyInput.style.backgroundColor = '#fff3cd'; // Желтый фон как у витринных
|
||||
@@ -960,7 +1232,8 @@ function renderCart() {
|
||||
minusBtn.onclick = async (e) => {
|
||||
e.preventDefault();
|
||||
const currentQty = cart.get(cartKey).qty;
|
||||
await updateCartItemQty(cartKey, currentQty - 1);
|
||||
const step = item.quantity_step || 1; // Используем шаг единицы или 1 по умолчанию
|
||||
await updateCartItemQty(cartKey, roundQuantity(currentQty - step, 3));
|
||||
};
|
||||
|
||||
// Поле ввода количества
|
||||
@@ -970,12 +1243,18 @@ function renderCart() {
|
||||
qtyInput.style.width = '60px';
|
||||
qtyInput.style.textAlign = 'center';
|
||||
qtyInput.style.padding = '0.375rem 0.25rem';
|
||||
qtyInput.value = item.qty;
|
||||
qtyInput.value = roundQuantity(item.qty, 3);
|
||||
qtyInput.min = 1;
|
||||
qtyInput.step = item.quantity_step || 0.001; // Устанавливаем шаг единицы продажи
|
||||
qtyInput.onchange = async (e) => {
|
||||
const newQty = parseInt(e.target.value) || 1;
|
||||
const newQty = parseFloat(e.target.value) || 1;
|
||||
await updateCartItemQty(cartKey, newQty);
|
||||
};
|
||||
// Округление при потере фокуса
|
||||
qtyInput.onblur = (e) => {
|
||||
const rawValue = parseFloat(e.target.value) || 1;
|
||||
e.target.value = roundQuantity(rawValue, 3);
|
||||
};
|
||||
|
||||
// Кнопка плюс
|
||||
const plusBtn = document.createElement('button');
|
||||
@@ -984,7 +1263,8 @@ function renderCart() {
|
||||
plusBtn.onclick = async (e) => {
|
||||
e.preventDefault();
|
||||
const currentQty = cart.get(cartKey).qty;
|
||||
await updateCartItemQty(cartKey, currentQty + 1);
|
||||
const step = item.quantity_step || 1; // Используем шаг единицы или 1 по умолчанию
|
||||
await updateCartItemQty(cartKey, roundQuantity(currentQty + step, 3));
|
||||
};
|
||||
|
||||
// Собираем контейнер
|
||||
@@ -2103,6 +2383,10 @@ async function handleCheckoutSubmit(paymentsData) {
|
||||
if (item.type === 'showcase_kit' && item.showcase_item_ids) {
|
||||
itemData.showcase_item_ids = item.showcase_item_ids;
|
||||
}
|
||||
// Для товаров с единицами продажи
|
||||
if (item.sales_unit_id) {
|
||||
itemData.sales_unit_id = item.sales_unit_id;
|
||||
}
|
||||
return itemData;
|
||||
}),
|
||||
payments: paymentsData,
|
||||
@@ -2222,6 +2506,51 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
});
|
||||
renderCart(); // Отрисовываем восстановленную корзину
|
||||
}
|
||||
|
||||
// ===== ОБРАБОТЧИКИ ДЛЯ МОДАЛКИ ВЫБОРА ЕДИНИЦЫ ПРОДАЖИ =====
|
||||
|
||||
// Кнопки изменения количества
|
||||
document.getElementById('unitQtyDecrement').addEventListener('click', () => {
|
||||
const input = document.getElementById('unitModalQuantity');
|
||||
const step = parseFloat(input.step) || 1;
|
||||
const newValue = Math.max(parseFloat(input.min), parseFloat(input.value) - step);
|
||||
input.value = roundQuantity(newValue, 3);
|
||||
calculateUnitModalSubtotal();
|
||||
validateUnitQuantity();
|
||||
});
|
||||
|
||||
document.getElementById('unitQtyIncrement').addEventListener('click', () => {
|
||||
const input = document.getElementById('unitModalQuantity');
|
||||
const step = parseFloat(input.step) || 1;
|
||||
const newValue = parseFloat(input.value) + step;
|
||||
input.value = roundQuantity(newValue, 3);
|
||||
calculateUnitModalSubtotal();
|
||||
validateUnitQuantity();
|
||||
});
|
||||
|
||||
// Изменение количества вручную
|
||||
document.getElementById('unitModalQuantity').addEventListener('input', () => {
|
||||
calculateUnitModalSubtotal();
|
||||
validateUnitQuantity();
|
||||
});
|
||||
|
||||
// Округление количества при потере фокуса
|
||||
document.getElementById('unitModalQuantity').addEventListener('blur', (e) => {
|
||||
const rawValue = parseFloat(e.target.value) || 0;
|
||||
e.target.value = roundQuantity(rawValue, 3);
|
||||
calculateUnitModalSubtotal();
|
||||
validateUnitQuantity();
|
||||
});
|
||||
|
||||
// Изменение цены
|
||||
document.getElementById('unitModalPrice').addEventListener('input', () => {
|
||||
calculateUnitModalSubtotal();
|
||||
});
|
||||
|
||||
// Кнопка подтверждения добавления в корзину
|
||||
document.getElementById('confirmAddUnitToCart').addEventListener('click', () => {
|
||||
addToCartFromModal();
|
||||
});
|
||||
});
|
||||
|
||||
// Смена склада
|
||||
|
||||
Reference in New Issue
Block a user