Исправлена ошибка создания заказов в 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:
@@ -356,3 +356,70 @@ body {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Стили для модального окна выбора единицы продажи */
|
||||
.unit-selection-card {
|
||||
border: 2px solid #dee2e6;
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.unit-selection-card:hover {
|
||||
border-color: #0d6efd;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.unit-selection-card.selected {
|
||||
border-color: #0d6efd;
|
||||
background: #e7f3ff;
|
||||
box-shadow: 0 2px 12px rgba(13,110,253,0.3);
|
||||
}
|
||||
|
||||
.unit-selection-card .unit-name {
|
||||
font-weight: 600;
|
||||
font-size: 1rem;
|
||||
color: #212529;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.unit-selection-card .unit-price {
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
color: #0d6efd;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.unit-selection-card .unit-availability {
|
||||
font-size: 0.85rem;
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
.unit-selection-card .unit-code {
|
||||
font-size: 0.8rem;
|
||||
color: #adb5bd;
|
||||
}
|
||||
|
||||
.unit-selection-card .badge {
|
||||
font-size: 0.75rem;
|
||||
padding: 4px 8px;
|
||||
}
|
||||
|
||||
/* Индикаторы наличия */
|
||||
.stock-badge-good {
|
||||
background-color: #d4edda;
|
||||
color: #155724;
|
||||
}
|
||||
|
||||
.stock-badge-low {
|
||||
background-color: #fff3cd;
|
||||
color: #856404;
|
||||
}
|
||||
|
||||
.stock-badge-none {
|
||||
background-color: #f8d7da;
|
||||
color: #721c24;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
// Смена склада
|
||||
|
||||
@@ -482,6 +482,91 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Модалка: Выбор единицы продажи товара -->
|
||||
<div class="modal fade" id="selectProductUnitModal" tabindex="-1" aria-labelledby="selectProductUnitModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="selectProductUnitModalLabel">
|
||||
<i class="bi bi-box-seam"></i> <span id="unitModalProductName"></span>
|
||||
</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="row">
|
||||
<!-- Левая колонка: список единиц продажи -->
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-semibold">Выберите единицу продажи</label>
|
||||
<div id="unitSelectionList" class="d-flex flex-column gap-2" style="max-height: 400px; overflow-y: auto;">
|
||||
<!-- Заполняется через JavaScript -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Правая колонка: параметры -->
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-header bg-light">
|
||||
<strong>Параметры добавления</strong>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<!-- Выбранная единица -->
|
||||
<div class="mb-3">
|
||||
<label class="form-label small">Выбрана единица</label>
|
||||
<div id="selectedUnitDisplay" class="fw-semibold text-primary">—</div>
|
||||
</div>
|
||||
|
||||
<!-- Количество -->
|
||||
<div class="mb-3">
|
||||
<label for="unitModalQuantity" class="form-label">Количество</label>
|
||||
<div class="input-group">
|
||||
<button class="btn btn-outline-secondary" type="button" id="unitQtyDecrement">
|
||||
<i class="bi bi-dash"></i>
|
||||
</button>
|
||||
<input type="number" class="form-control text-center" id="unitModalQuantity"
|
||||
value="1" min="0.001" step="1">
|
||||
<button class="btn btn-outline-secondary" type="button" id="unitQtyIncrement">
|
||||
<i class="bi bi-plus"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div id="unitQtyError" class="text-danger small mt-1" style="display: none;"></div>
|
||||
<div id="unitQtyHint" class="text-muted small mt-1"></div>
|
||||
</div>
|
||||
|
||||
<!-- Цена -->
|
||||
<div class="mb-3">
|
||||
<label for="unitModalPrice" class="form-label">Цена за единицу</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text">₽</span>
|
||||
<input type="number" class="form-control" id="unitModalPrice"
|
||||
value="0" min="0" step="0.01">
|
||||
</div>
|
||||
<div id="priceOverrideIndicator" class="text-warning small mt-1" style="display: none;">
|
||||
<i class="bi bi-exclamation-triangle"></i> Цена изменена
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Итого -->
|
||||
<div class="alert alert-info mb-0">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<strong>Итого:</strong>
|
||||
<span class="fs-4" id="unitModalSubtotal">0.00 ₽</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Отмена</button>
|
||||
<button type="button" class="btn btn-success" id="confirmAddUnitToCart" disabled>
|
||||
<i class="bi bi-cart-plus"></i> Добавить в корзину
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
|
||||
@@ -792,6 +792,10 @@ def get_items_api(request):
|
||||
reserved = p.reserved_qty
|
||||
free_qty = available - reserved
|
||||
|
||||
# Подсчитываем активные единицы продажи
|
||||
sales_units_count = p.sales_units.filter(is_active=True).count()
|
||||
has_sales_units = sales_units_count > 0
|
||||
|
||||
products.append({
|
||||
'id': p.id,
|
||||
'name': p.name,
|
||||
@@ -804,7 +808,9 @@ def get_items_api(request):
|
||||
'available_qty': str(available),
|
||||
'reserved_qty': str(reserved),
|
||||
'free_qty': str(free_qty), # Передаём как строку для сохранения точности
|
||||
'free_qty_sort': float(free_qty) # Для сортировки отдельное поле
|
||||
'free_qty_sort': float(free_qty), # Для сортировки отдельное поле
|
||||
'sales_units_count': sales_units_count,
|
||||
'has_sales_units': has_sales_units
|
||||
})
|
||||
|
||||
# Prefetch для первого фото комплектов
|
||||
@@ -1434,21 +1440,28 @@ def pos_checkout(request):
|
||||
with db_transaction.atomic():
|
||||
# 1. Создаём заказ с текущей датой и временем в локальном часовом поясе (Europe/Minsk)
|
||||
from django.utils import timezone as tz
|
||||
from orders.models import Delivery
|
||||
now_utc = tz.now() # Текущее время в UTC
|
||||
now_local = tz.localtime(now_utc) # Конвертируем в локальный часовой пояс (Europe/Minsk)
|
||||
current_time = now_local.time() # Извлекаем время в минском часовом поясе
|
||||
|
||||
order = Order.objects.create(
|
||||
customer=customer,
|
||||
is_delivery=False, # POS - всегда самовывоз
|
||||
pickup_warehouse=warehouse,
|
||||
status=completed_status, # Сразу "Выполнен"
|
||||
delivery_date=now_local.date(), # Текущая дата в минском часовом поясе
|
||||
delivery_time_start=current_time, # Текущее время (Минск)
|
||||
delivery_time_end=current_time, # То же время (точное время)
|
||||
special_instructions=order_notes,
|
||||
modified_by=request.user
|
||||
)
|
||||
|
||||
# Создаём связанную доставку (самовывоз для POS)
|
||||
Delivery.objects.create(
|
||||
order=order,
|
||||
delivery_type=Delivery.DELIVERY_TYPE_PICKUP, # POS - всегда самовывоз
|
||||
pickup_warehouse=warehouse,
|
||||
delivery_date=now_local.date(), # Текущая дата в минском часовом поясе
|
||||
time_from=current_time, # Текущее время (Минск)
|
||||
time_to=current_time, # То же время (точное время)
|
||||
cost=0 # Самовывоз бесплатный
|
||||
)
|
||||
|
||||
# 2. Добавляем товары
|
||||
from inventory.models import ShowcaseItem
|
||||
|
||||
Reference in New Issue
Block a user