diff --git a/myproject/inventory/services/showcase_manager.py b/myproject/inventory/services/showcase_manager.py
index 6e09493..422596f 100644
--- a/myproject/inventory/services/showcase_manager.py
+++ b/myproject/inventory/services/showcase_manager.py
@@ -209,16 +209,19 @@ class ShowcaseManager:
reservation.order_item = order_item
reservation.save()
- # Теперь создаём продажу с правильной ценой из OrderItem
- SaleProcessor.create_sale_from_reservation(
- reservation=reservation,
- order=order
- )
+ # ВАЖНО: Мы НЕ создаём продажу (Sale) здесь и НЕ меняем статус на 'converted_to_sale'.
+ # Это сделает сигнал create_sale_on_order_completion автоматически.
+ # Таким образом обеспечивается единая точка создания продаж для всех типов товаров.
- # Обновляем статус резерва
- reservation.status = 'converted_to_sale'
- reservation.converted_at = timezone.now()
- reservation.save()
+ # SaleProcessor.create_sale_from_reservation(
+ # reservation=reservation,
+ # order=order
+ # )
+
+ # Статус резерва остается 'reserved', чтобы сигнал его увидел
+ # reservation.status = 'converted_to_sale'
+ # reservation.converted_at = timezone.now()
+ # reservation.save()
sold_count += 1
diff --git a/myproject/inventory/signals.py b/myproject/inventory/signals.py
index ec2f4e4..ff476e7 100644
--- a/myproject/inventory/signals.py
+++ b/myproject/inventory/signals.py
@@ -366,7 +366,7 @@ def create_sale_on_order_completion(sender, instance, created, **kwargs):
# === ЗАЩИТА ОТ ПРЕЖДЕВРЕМЕННОГО СОЗДАНИЯ SALE ===
# Проверяем, есть ли уже Sale для этого заказа
if Sale.objects.filter(order=instance).exists():
- logger.info(f"✓ Заказ {instance.order_number}: Sale уже существуют, пропускаем")
+ logger.info(f"Заказ {instance.order_number}: Sale уже существуют, пропускаем")
update_is_returned_flag(instance)
return
@@ -376,7 +376,7 @@ def create_sale_on_order_completion(sender, instance, created, **kwargs):
previous_status = getattr(instance, '_previous_status', None)
if previous_status and previous_status.is_positive_end:
logger.info(
- f"🔄 Заказ {instance.order_number}: повторный переход в положительный статус "
+ f"Заказ {instance.order_number}: повторный переход в положительный статус "
f"({previous_status.name} → {instance.status.name}). Проверяем Sale..."
)
if Sale.objects.filter(order=instance).exists():
@@ -454,13 +454,66 @@ def create_sale_on_order_completion(sender, instance, created, **kwargs):
)
continue
+ # === РАСЧЕТ ЦЕНЫ ===
+ # Рассчитываем фактическую стоимость продажи всего комплекта с учетом скидок
+ # 1. Базовая стоимость позиции
+ item_subtotal = Decimal(str(item.price)) * Decimal(str(item.quantity))
+
+ # 2. Скидки
+ item_discount = Decimal(str(item.discount_amount)) if item.discount_amount is not None else Decimal('0')
+
+ # Скидка на заказ (распределенная)
+ instance.refresh_from_db()
+ order_total = instance.subtotal if hasattr(instance, 'subtotal') else Decimal('0')
+ delivery_cost = Decimal(str(instance.delivery.cost)) if hasattr(instance, 'delivery') and instance.delivery else Decimal('0')
+ order_discount_amount = (order_total - (Decimal(str(instance.total_amount)) - delivery_cost)) if order_total > 0 else Decimal('0')
+
+ if order_total > 0 and order_discount_amount > 0:
+ item_order_discount = order_discount_amount * (item_subtotal / order_total)
+ else:
+ item_order_discount = Decimal('0')
+
+ kit_net_total = item_subtotal - item_discount - item_order_discount
+ if kit_net_total < 0:
+ kit_net_total = Decimal('0')
+
+ # 3. Суммарная каталожная стоимость всех компонентов (для пропорции)
+ total_catalog_price = Decimal('0')
+ for reservation in kit_reservations:
+ qty = reservation.quantity_base or reservation.quantity
+ price = reservation.product.actual_price or Decimal('0')
+ total_catalog_price += price * qty
+
+ # 4. Коэффициент распределения
+ if total_catalog_price > 0:
+ ratio = kit_net_total / total_catalog_price
+ else:
+ # Если каталожная цена 0, распределяем просто по количеству или 0
+ ratio = Decimal('0')
+
# Создаем Sale для каждого компонента комплекта
for reservation in kit_reservations:
try:
- # Рассчитываем цену продажи компонента пропорционально цене комплекта
- # Используем actual_price компонента как цену продажи
- component_sale_price = reservation.product.actual_price
+ # Рассчитываем цену продажи компонента пропорционально
+ catalog_price = reservation.product.actual_price or Decimal('0')
+ if ratio > 0:
+ # Распределяем реальную выручку
+ component_sale_price = catalog_price * ratio
+ else:
+ # Если выручка 0 или каталожные цены 0
+ if total_catalog_price == 0 and kit_net_total > 0:
+ # Крайний случай: товаров на 0 руб, а продали за деньги (услуга?)
+ # Распределяем равномерно
+ count = kit_reservations.count()
+ component_qty = reservation.quantity_base or reservation.quantity
+ if count > 0 and component_qty > 0:
+ component_sale_price = (kit_net_total / count) / component_qty
+ else:
+ component_sale_price = Decimal('0')
+ else:
+ component_sale_price = Decimal('0')
+
sale = SaleProcessor.create_sale(
product=reservation.product,
warehouse=warehouse,
@@ -472,7 +525,8 @@ def create_sale_on_order_completion(sender, instance, created, **kwargs):
sales_created.append(sale)
logger.info(
f"✓ Sale создан для компонента комплекта '{kit.name}': "
- f"{reservation.product.name} - {reservation.quantity_base or reservation.quantity} шт. (базовых единиц)"
+ f"{reservation.product.name} - {reservation.quantity_base or reservation.quantity} шт. "
+ f"(цена: {component_sale_price})"
)
except ValueError as e:
logger.error(
@@ -548,6 +602,21 @@ def create_sale_on_order_completion(sender, instance, created, **kwargs):
else:
base_price = price_with_discount
+ # LOGGING DEBUG INFO
+ # print(f"DEBUG SALE PRICE CALCULATION for Item #{item.id} ({product.name}):")
+ # print(f" Price: {item.price}, Qty: {item.quantity}, Subtotal: {item_subtotal}")
+ # print(f" Item Discount: {item_discount}, Order Discount Share: {item_order_discount}, Total Discount: {total_discount}")
+ # print(f" Price w/ Discount: {price_with_discount}")
+ # print(f" Sales Unit: {item.sales_unit}, Conversion: {item.conversion_factor_snapshot}")
+ # print(f" FINAL BASE PRICE: {base_price}")
+ # print(f" Sales Unit Object: {item.sales_unit}")
+ # if item.sales_unit:
+ # print(f" Sales Unit Conversion: {item.sales_unit.conversion_factor}")
+
+ logger.info(f"DEBUG SALE PRICE CALCULATION for Item #{item.id} ({product.name}):")
+ logger.info(f" Price: {item.price}, Qty: {item.quantity}, Subtotal: {item_subtotal}")
+ logger.info(f" FINAL BASE PRICE: {base_price}")
+
# Создаем Sale (с автоматическим FIFO-списанием)
sale = SaleProcessor.create_sale(
product=product,
diff --git a/myproject/myproject/settings.py b/myproject/myproject/settings.py
index 22e9ce1..2e13230 100644
--- a/myproject/myproject/settings.py
+++ b/myproject/myproject/settings.py
@@ -631,3 +631,5 @@ if not DEBUG and not ENCRYPTION_KEY:
"ENCRYPTION_KEY not set! Encrypted fields will fail. "
"Generate with: python -c \"from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())\""
)
+
+
diff --git a/myproject/pos/static/pos/js/terminal.js b/myproject/pos/static/pos/js/terminal.js
index 0665b26..f263565 100644
--- a/myproject/pos/static/pos/js/terminal.js
+++ b/myproject/pos/static/pos/js/terminal.js
@@ -72,15 +72,15 @@ function saveCartToRedis() {
},
body: JSON.stringify({ cart: cartObj })
})
- .then(response => response.json())
- .then(data => {
- if (!data.success) {
- console.error('Ошибка сохранения корзины:', data.error);
- }
- })
- .catch(error => {
- console.error('Ошибка при сохранении корзины в Redis:', error);
- });
+ .then(response => response.json())
+ .then(data => {
+ if (!data.success) {
+ console.error('Ошибка сохранения корзины:', data.error);
+ }
+ })
+ .catch(error => {
+ console.error('Ошибка при сохранении корзины в Redis:', error);
+ });
}, 500); // Debounce 500ms
}
@@ -160,7 +160,7 @@ function updateCustomerDisplay() {
// Обновляем видимость кнопок сброса (в корзине и в модалке продажи)
[document.getElementById('resetCustomerBtn'),
- document.getElementById('checkoutResetCustomerBtn')].forEach(resetBtn => {
+ document.getElementById('checkoutResetCustomerBtn')].forEach(resetBtn => {
if (resetBtn) {
resetBtn.style.display = isSystemCustomer ? 'none' : 'block';
}
@@ -242,18 +242,18 @@ function selectCustomer(customerId, customerName, walletBalance = 0) {
'Content-Type': 'application/json'
}
})
- .then(response => response.json())
- .then(data => {
- if (!data.success) {
- console.error('Ошибка сохранения клиента:', data.error);
- } else {
- // Обновляем баланс из ответа сервера
- selectedCustomer.wallet_balance = data.wallet_balance || 0;
- }
- })
- .catch(error => {
- console.error('Ошибка при сохранении клиента в Redis:', error);
- });
+ .then(response => response.json())
+ .then(data => {
+ if (!data.success) {
+ console.error('Ошибка сохранения клиента:', data.error);
+ } else {
+ // Обновляем баланс из ответа сервера
+ selectedCustomer.wallet_balance = data.wallet_balance || 0;
+ }
+ })
+ .catch(error => {
+ console.error('Ошибка при сохранении клиента в Redis:', error);
+ });
}
/**
@@ -272,12 +272,12 @@ function initCustomerSelect2() {
url: '/customers/api/search/',
dataType: 'json',
delay: 300,
- data: function(params) {
+ data: function (params) {
return {
q: params.term
};
},
- processResults: function(data) {
+ processResults: function (data) {
return {
results: data.results
};
@@ -289,7 +289,7 @@ function initCustomerSelect2() {
});
// Обработка выбора клиента из списка
- $searchInput.on('select2:select', function(e) {
+ $searchInput.on('select2:select', function (e) {
const data = e.params.data;
// Проверяем это не опция "Создать нового клиента"
@@ -439,40 +439,40 @@ async function createNewCustomer() {
*/
async function openProductUnitModal(product) {
unitModalProduct = product;
-
+
// Устанавливаем название товара
- document.getElementById('unitModalProductName').textContent =
+ 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('Ошибка загрузки данных. Попробуйте ещё раз.');
@@ -531,7 +531,7 @@ function renderUnitSelectionList() {
*/
function selectUnit(unit) {
selectedSalesUnit = unit;
-
+
// Обновляем визуальное выделение
document.querySelectorAll('.unit-selection-card').forEach(card => {
if (card.dataset.unitId === String(unit.id)) {
@@ -540,7 +540,7 @@ function selectUnit(unit) {
card.classList.remove('selected');
}
});
-
+
// Обновляем отображение выбранной единицы
document.getElementById('selectedUnitDisplay').textContent =
unit.name;
@@ -550,20 +550,20 @@ function selectUnit(unit) {
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();
}
@@ -574,12 +574,12 @@ function selectUnit(unit) {
*/
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}`;
@@ -587,21 +587,21 @@ function validateUnitQuantity() {
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;
@@ -617,9 +617,9 @@ function calculateUnitModalSubtotal() {
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';
@@ -635,15 +635,15 @@ 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);
@@ -664,16 +664,16 @@ function addToCartFromModal() {
price_overridden: priceOverridden
});
}
-
+
// Обновляем корзину
renderCart();
saveCartToRedis();
-
+
// Перерисовываем товары для обновления визуального остатка
if (!isShowcaseView) {
renderProducts();
}
-
+
// Закрываем модальное окно
unitModalInstance.hide();
}
@@ -729,7 +729,7 @@ async function addProductWithUnitToCart(product, salesUnit, qty = 1) {
function renderCategories() {
const grid = document.getElementById('categoryGrid');
grid.innerHTML = '';
-
+
// Кнопка "Витрина" - первая в ряду
const showcaseCol = document.createElement('div');
showcaseCol.className = 'col-6 col-custom-3 col-md-3 col-lg-2';
@@ -753,7 +753,7 @@ function renderCategories() {
showcaseCard.appendChild(showcaseBody);
showcaseCol.appendChild(showcaseCard);
grid.appendChild(showcaseCol);
-
+
// Кнопка "Все"
const allCol = document.createElement('div');
allCol.className = 'col-6 col-custom-3 col-md-3 col-lg-2';
@@ -776,12 +776,12 @@ function renderCategories() {
allCard.appendChild(allBody);
allCol.appendChild(allCard);
grid.appendChild(allCol);
-
+
// Категории
CATEGORIES.forEach(cat => {
const col = document.createElement('div');
col.className = 'col-6 col-custom-3 col-md-3 col-lg-custom-5';
-
+
const card = document.createElement('div');
card.className = 'card category-card' + (currentCategoryId === cat.id && !isShowcaseView ? ' active' : '');
card.onclick = async () => {
@@ -792,14 +792,14 @@ function renderCategories() {
renderCategories();
await loadItems(); // Загрузка через API
};
-
+
const body = document.createElement('div');
body.className = 'card-body';
-
+
const name = document.createElement('div');
name.className = 'category-name';
name.textContent = cat.name;
-
+
body.appendChild(name);
card.appendChild(body);
col.appendChild(card);
@@ -810,13 +810,13 @@ function renderCategories() {
function renderProducts() {
const grid = document.getElementById('productGrid');
grid.innerHTML = '';
-
+
let filtered;
-
+
// Если выбран режим витрины - показываем витринные комплекты
if (isShowcaseView) {
filtered = showcaseKits;
-
+
// Для витрины — клиентская фильтрация по поиску
const searchTerm = document.getElementById('searchInput').value.toLowerCase().trim();
if (searchTerm) {
@@ -836,12 +836,12 @@ function renderProducts() {
filtered.forEach(item => {
const col = document.createElement('div');
col.className = 'col-6 col-custom-3 col-md-3 col-lg-custom-5';
-
+
const card = document.createElement('div');
card.className = 'card product-card';
card.style.position = 'relative';
card.onclick = () => addToCart(item);
-
+
// Если это витринный комплект - добавляем кнопку редактирования
if (item.type === 'showcase_kit') {
// ИНДИКАЦИЯ БЛОКИРОВКИ
@@ -914,10 +914,10 @@ function renderProducts() {
}
}
}
-
+
const body = document.createElement('div');
body.className = 'card-body';
-
+
// Изображение товара/комплекта
const imageDiv = document.createElement('div');
imageDiv.className = 'product-image';
@@ -930,18 +930,18 @@ function renderProducts() {
} else {
imageDiv.innerHTML = '';
}
-
+
// Информация о товаре/комплекте
const info = document.createElement('div');
info.className = 'product-info';
-
+
const name = document.createElement('div');
name.className = 'product-name';
name.textContent = item.name;
-
+
const stock = document.createElement('div');
stock.className = 'product-stock';
-
+
// Для витринных комплектов показываем количество (доступно/всего) и дней на витрине
if (item.type === 'showcase_kit') {
const availableCount = item.available_count || 0;
@@ -1080,26 +1080,26 @@ function renderProducts() {
}
}
}
-
+
const sku = document.createElement('div');
sku.className = 'product-sku';
-
+
const skuText = document.createElement('span');
skuText.textContent = item.sku || 'н/д';
-
+
const priceSpan = document.createElement('span');
priceSpan.className = 'product-price';
// Используем цену из единицы продажи если есть, иначе базовую цену
const itemPrice = item.default_sales_unit ? item.price_in_unit : item.price;
priceSpan.textContent = `${formatMoney(itemPrice)}`;
-
+
sku.appendChild(skuText);
sku.appendChild(priceSpan);
-
+
info.appendChild(name);
info.appendChild(stock);
info.appendChild(sku);
-
+
body.appendChild(imageDiv);
body.appendChild(info);
card.appendChild(body);
@@ -1111,32 +1111,32 @@ function renderProducts() {
// Загрузка товаров через API
async function loadItems(append = false) {
if (isLoadingItems) return;
-
+
isLoadingItems = true;
-
+
if (!append) {
currentPage = 1;
ITEMS = [];
}
-
+
try {
const params = new URLSearchParams({
page: currentPage,
page_size: 60
});
-
+
if (currentCategoryId) {
params.append('category_id', currentCategoryId);
}
-
+
// Добавляем поисковый запрос, если есть
if (currentSearchQuery) {
params.append('query', currentSearchQuery);
}
-
+
const response = await fetch(`/pos/api/items/?${params}`);
const data = await response.json();
-
+
if (data.success) {
if (append) {
ITEMS = ITEMS.concat(data.items);
@@ -1174,7 +1174,7 @@ function setupInfiniteScroll() {
rootMargin: '200px'
}
);
-
+
// Наблюдаем за концом грида
const sentinel = document.createElement('div');
sentinel.id = 'scroll-sentinel';
@@ -1199,7 +1199,7 @@ async function addToCart(item) {
await openProductUnitModal(item);
return;
}
-
+
const cartKey = `${item.type}-${item.id}`; // Уникальный ключ: "product-1" или "kit-1"
// СПЕЦИАЛЬНАЯ ЛОГИКА ДЛЯ ВИТРИННЫХ КОМПЛЕКТОВ (Soft Lock)
@@ -1358,7 +1358,7 @@ function renderCart() {
if (item.type === 'kit' || item.type === 'showcase_kit') {
typeIcon = ' ';
}
-
+
// Единица продажи (если есть)
let unitInfo = '';
if (item.sales_unit_id && item.unit_name) {
@@ -1468,18 +1468,18 @@ function renderCart() {
qtyControl.appendChild(qtyInput);
qtyControl.appendChild(plusBtn);
}
-
+
// Сумма за позицию
const itemTotal = document.createElement('div');
itemTotal.className = 'item-total';
itemTotal.textContent = formatMoney(item.price * item.qty);
-
+
// Кнопка удаления
const deleteBtn = document.createElement('button');
deleteBtn.className = 'btn btn-sm btn-link text-danger p-0';
deleteBtn.innerHTML = '';
deleteBtn.onclick = () => removeFromCart(cartKey);
-
+
row.appendChild(namePrice);
row.appendChild(multiplySign);
row.appendChild(qtyControl);
@@ -1487,7 +1487,7 @@ function renderCart() {
row.appendChild(deleteBtn);
// Обработчик клика для редактирования товара
- row.addEventListener('click', function(e) {
+ row.addEventListener('click', function (e) {
// Игнорируем клики на кнопки управления количеством и удаления
if (e.target.closest('button') || e.target.closest('input')) {
return;
@@ -1501,10 +1501,10 @@ function renderCart() {
});
list.appendChild(row);
-
+
total += item.qty * item.price;
});
-
+
document.getElementById('cartTotal').textContent = formatMoney(total);
// Обновляем состояние кнопки "НА ВИТРИНУ"
@@ -1785,7 +1785,7 @@ async function openCreateTempKitModal() {
alert('Корзина пуста. Добавьте товары перед созданием комплекта.');
return;
}
-
+
// Проверяем что в корзине НЕТ витринных комплектов
let hasShowcaseKit = false;
for (const [cartKey, item] of cart) {
@@ -1794,12 +1794,12 @@ async function openCreateTempKitModal() {
break;
}
}
-
+
if (hasShowcaseKit) {
alert('⚠️ В корзине уже есть витринный комплект!\n\nНельзя создать новый букет на витрину, пока в корзине находится другой витринный букет.\n\nУдалите витринный букет из корзины или завершите текущую продажу.');
return;
}
-
+
// Проверяем что в корзине только товары (не обычные комплекты)
let hasKits = false;
for (const [cartKey, item] of cart) {
@@ -1808,29 +1808,29 @@ async function openCreateTempKitModal() {
break;
}
}
-
+
if (hasKits) {
alert('В корзине есть комплекты. Для витрины добавляйте только отдельные товары.');
return;
}
-
+
// Копируем содержимое cart в tempCart (изолированное состояние модалки)
tempCart.clear();
cart.forEach((item, key) => {
- tempCart.set(key, {...item}); // Глубокая копия объекта
+ tempCart.set(key, { ...item }); // Глубокая копия объекта
});
-
+
// Генерируем название по умолчанию
const randomSuffix = Math.floor(Math.random() * 900) + 100;
const defaultName = `Витринный букет ${randomSuffix}`;
document.getElementById('tempKitName').value = defaultName;
-
+
// Загружаем список витрин
await loadShowcases();
-
+
// Заполняем список товаров из tempCart
renderTempKitItems();
-
+
// Открываем модальное окно
const modal = new bootstrap.Modal(document.getElementById('createTempKitModal'));
modal.show();
@@ -1842,21 +1842,21 @@ async function openEditKitModal(kitId) {
// Загружаем данные комплекта
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 => {
@@ -1871,7 +1871,7 @@ async function openEditKitModal(kitId) {
});
});
renderTempKitItems(); // Отображаем товары в модальном окне
-
+
// Заполняем поля формы
document.getElementById('tempKitName').value = kit.name;
document.getElementById('tempKitDescription').value = kit.description;
@@ -1890,7 +1890,7 @@ async function openEditKitModal(kitId) {
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;
@@ -1900,12 +1900,12 @@ async function openEditKitModal(kitId) {
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;
@@ -1913,10 +1913,10 @@ async function openEditKitModal(kitId) {
} else {
document.getElementById('photoPreview').style.display = 'none';
}
-
+
// Обновляем цены
updatePriceCalculations();
-
+
// Меняем заголовок и кнопку
document.getElementById('createTempKitModalLabel').textContent = 'Редактирование витринного букета';
document.getElementById('confirmCreateTempKit').textContent = 'Сохранить изменения';
@@ -1926,12 +1926,12 @@ async function openEditKitModal(kitId) {
document.getElementById('writeOffKitBtn').style.display = 'block';
document.getElementById('showcaseKitQuantityBlock').style.display = 'none';
document.getElementById('addProductBlock').style.display = 'block';
-
+
// Инициализируем компонент поиска товаров
setTimeout(() => {
if (window.ProductSearchPicker) {
const picker = ProductSearchPicker.init('#temp-kit-product-picker', {
- onAddSelected: function(product, instance) {
+ onAddSelected: function (product, instance) {
if (product) {
// Добавляем товар в tempCart
const cartKey = `product-${product.id}`;
@@ -1949,10 +1949,10 @@ async function openEditKitModal(kitId) {
type: 'product'
});
}
-
+
// Обновляем отображение
renderTempKitItems();
-
+
// Очищаем выбор в пикере
instance.clearSelection();
}
@@ -1967,7 +1967,7 @@ async function openEditKitModal(kitId) {
// Проверяем актуальность цен (сразу после открытия)
checkPricesActual();
-
+
} catch (error) {
console.error('Error loading kit for edit:', error);
alert('Ошибка при загрузке комплекта');
@@ -2084,10 +2084,10 @@ async function loadShowcases() {
try {
const response = await fetch('/pos/api/get-showcases/');
const data = await response.json();
-
+
const select = document.getElementById('showcaseSelect');
select.innerHTML = '';
-
+
if (data.success && data.showcases.length > 0) {
let defaultShowcaseId = null;
@@ -2120,16 +2120,16 @@ async function loadShowcases() {
function renderTempKitItems() {
const container = document.getElementById('tempKitItemsList');
container.innerHTML = '';
-
+
let estimatedTotal = 0;
-
+
tempCart.forEach((item, cartKey) => {
// Только товары (не комплекты)
if (item.type !== 'product') return;
-
+
const itemDiv = document.createElement('div');
itemDiv.className = 'd-flex justify-content-between align-items-center mb-2 pb-2 border-bottom';
-
+
// Левая часть: название и цена
const leftDiv = document.createElement('div');
leftDiv.className = 'flex-grow-1';
@@ -2138,11 +2138,11 @@ function renderTempKitItems() {
${formatMoney(item.price)} руб. / шт.
`;
-
+
// Правая часть: контролы количества и удаление
const rightDiv = document.createElement('div');
rightDiv.className = 'd-flex align-items-center gap-2';
-
+
// Кнопка минус
const minusBtn = document.createElement('button');
minusBtn.className = 'btn btn-sm btn-outline-secondary';
@@ -2156,7 +2156,7 @@ function renderTempKitItems() {
}
renderTempKitItems();
};
-
+
// Поле количества
const qtyInput = document.createElement('input');
qtyInput.type = 'number';
@@ -2169,7 +2169,7 @@ function renderTempKitItems() {
item.qty = Math.max(1, newQty);
renderTempKitItems();
};
-
+
// Кнопка плюс
const plusBtn = document.createElement('button');
plusBtn.className = 'btn btn-sm btn-outline-secondary';
@@ -2179,13 +2179,13 @@ function renderTempKitItems() {
item.qty++;
renderTempKitItems();
};
-
+
// Сумма за товар
const totalDiv = document.createElement('div');
totalDiv.className = 'text-end ms-2';
totalDiv.style.minWidth = '80px';
totalDiv.innerHTML = `${formatMoney(item.qty * item.price)} руб.`;
-
+
// Кнопка удаления
const deleteBtn = document.createElement('button');
deleteBtn.className = 'btn btn-sm btn-outline-danger';
@@ -2195,25 +2195,25 @@ function renderTempKitItems() {
tempCart.delete(cartKey);
renderTempKitItems();
};
-
+
rightDiv.appendChild(minusBtn);
rightDiv.appendChild(qtyInput);
rightDiv.appendChild(plusBtn);
rightDiv.appendChild(totalDiv);
rightDiv.appendChild(deleteBtn);
-
+
itemDiv.appendChild(leftDiv);
itemDiv.appendChild(rightDiv);
container.appendChild(itemDiv);
-
+
estimatedTotal += item.qty * item.price;
});
-
+
// Если корзина пуста
if (tempCart.size === 0) {
container.innerHTML = '
Нет товаров
'; } - + // Обновляем все расчеты цен updatePriceCalculations(estimatedTotal); } @@ -2229,14 +2229,14 @@ function updatePriceCalculations(basePrice = null) { } }); } - + // Базовая цена 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') { @@ -2249,23 +2249,23 @@ function updatePriceCalculations(basePrice = null) { 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() { +document.getElementById('priceAdjustmentType').addEventListener('change', function () { const adjustmentBlock = document.getElementById('adjustmentValueBlock'); if (this.value === 'none') { adjustmentBlock.style.display = 'none'; @@ -2276,11 +2276,11 @@ document.getElementById('priceAdjustmentType').addEventListener('change', functi updatePriceCalculations(); }); -document.getElementById('priceAdjustmentValue').addEventListener('input', function() { +document.getElementById('priceAdjustmentValue').addEventListener('input', function () { updatePriceCalculations(); }); -document.getElementById('useSalePrice').addEventListener('change', function() { +document.getElementById('useSalePrice').addEventListener('change', function () { const salePriceBlock = document.getElementById('salePriceBlock'); if (this.checked) { salePriceBlock.style.display = 'block'; @@ -2291,12 +2291,12 @@ document.getElementById('useSalePrice').addEventListener('change', function() { updatePriceCalculations(); }); -document.getElementById('salePrice').addEventListener('input', function() { +document.getElementById('salePrice').addEventListener('input', function () { updatePriceCalculations(); }); // Обработчик загрузки фото -document.getElementById('tempKitPhoto').addEventListener('change', function(e) { +document.getElementById('tempKitPhoto').addEventListener('change', function (e) { const file = e.target.files[0]; if (file) { if (!file.type.startsWith('image/')) { @@ -2304,10 +2304,10 @@ document.getElementById('tempKitPhoto').addEventListener('change', function(e) { this.value = ''; return; } - + // Превью const reader = new FileReader(); - reader.onload = function(event) { + reader.onload = function (event) { document.getElementById('photoPreviewImg').src = event.target.result; document.getElementById('photoPreview').style.display = 'block'; }; @@ -2316,7 +2316,7 @@ document.getElementById('tempKitPhoto').addEventListener('change', function(e) { }); // Удаление фото -document.getElementById('removePhoto').addEventListener('click', function() { +document.getElementById('removePhoto').addEventListener('click', function () { document.getElementById('tempKitPhoto').value = ''; document.getElementById('photoPreview').style.display = 'none'; document.getElementById('photoPreviewImg').src = ''; @@ -2335,12 +2335,12 @@ document.getElementById('confirmCreateTempKit').onclick = async () => { alert('Введите название комплекта'); return; } - + if (!showcaseId && !isEditMode) { alert('Выберите витрину'); return; } - + // Собираем товары из tempCart (изолированное состояние модалки) const items = []; tempCart.forEach((item, cartKey) => { @@ -2351,18 +2351,18 @@ document.getElementById('confirmCreateTempKit').onclick = async () => { }); } }); - + if (items.length === 0) { alert('Нет товаров для создания комплекта'); 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; - + // Получаем количество букетов для создания const showcaseKitQuantity = parseInt(document.getElementById('showcaseKitQuantity').value, 10) || 1; @@ -2388,12 +2388,11 @@ document.getElementById('confirmCreateTempKit').onclick = async () => { formData.append('items', JSON.stringify(items)); formData.append('price_adjustment_type', priceAdjustmentType); formData.append('price_adjustment_value', priceAdjustmentValue); - // Если пользователь не задал свою цену, используем вычисленную - const finalSalePrice = useSalePrice ? salePrice : calculatedPrice; - if (finalSalePrice > 0) { - formData.append('sale_price', finalSalePrice); + // Если пользователь явно указал свою цену + if (useSalePrice && salePrice > 0) { + formData.append('sale_price', salePrice); } - + // Фото: для редактирования проверяем, удалено ли оно if (photoFile) { formData.append('photo', photoFile); @@ -2401,18 +2400,18 @@ document.getElementById('confirmCreateTempKit').onclick = async () => { // Если фото было удалено formData.append('remove_photo', '1'); } - + // Отправляем запрос на сервер const confirmBtn = document.getElementById('confirmCreateTempKit'); confirmBtn.disabled = true; - - const url = isEditMode + + const url = isEditMode ? `/pos/api/product-kits/${editingKitId}/update/` : '/pos/api/create-temp-kit/'; - + const actionText = isEditMode ? 'Сохранение...' : 'Создание...'; confirmBtn.innerHTML = `${actionText}`; - + try { const response = await fetch(url, { method: 'POST', @@ -2422,14 +2421,14 @@ document.getElementById('confirmCreateTempKit').onclick = async () => { }, body: formData }); - + const data = await response.json(); - + if (data.success) { // Успех! const createdCount = data.available_count || 1; const qtyInfo = createdCount > 1 ? `\nСоздано экземпляров: ${createdCount}` : ''; - + let successMessage = isEditMode ? `✅ ${data.message}\n\nКомплект: ${data.kit_name}\nЦена: ${data.kit_price} руб.` : `✅ ${data.message} @@ -2464,24 +2463,24 @@ document.getElementById('confirmCreateTempKit').onclick = async () => { document.getElementById('salePrice').value = ''; document.getElementById('salePriceBlock').style.display = 'none'; document.getElementById('showcaseKitQuantity').value = '1'; // Сброс количества - + // Запоминаем, был ли режим редактирования до сброса const wasEditMode = isEditMode; - + // Сбрасываем режим редактирования isEditMode = false; editingKitId = null; - + // Закрываем модальное окно const modal = bootstrap.Modal.getInstance(document.getElementById('createTempKitModal')); modal.hide(); - + // Если это было СОЗДАНИЕ витринного комплекта из корзины, // очищаем основную корзину POS if (!wasEditMode) { await clearCart(); } - + // Обновляем витринные комплекты и переключаемся на вид витрины isShowcaseView = true; currentCategoryId = null; @@ -2496,7 +2495,7 @@ document.getElementById('confirmCreateTempKit').onclick = async () => { alert('Ошибка при сохранении комплекта'); } finally { confirmBtn.disabled = false; - const btnText = isEditMode + const btnText = isEditMode ? ' Сохранить изменения' : ' Создать и зарезервировать'; confirmBtn.innerHTML = btnText; @@ -2644,16 +2643,16 @@ const getCsrfToken = () => { if (csrfInput) { return csrfInput.value; } - + // Fallback: пытаемся прочитать из cookie (если CSRF_USE_SESSIONS=False) return getCookie('csrftoken'); }; // Сброс режима редактирования при закрытии модального окна -document.getElementById('createTempKitModal').addEventListener('hidden.bs.modal', function() { +document.getElementById('createTempKitModal').addEventListener('hidden.bs.modal', function () { // Очищаем tempCart (изолированное состояние модалки) tempCart.clear(); - + // Сброс режима редактирования при закрытии модального окна if (isEditMode) { // Сбрасываем режим редактирования @@ -2774,13 +2773,13 @@ document.getElementById('checkoutModal').addEventListener('show.bs.modal', async }); // Переключение режима оплаты -document.getElementById('singlePaymentMode').addEventListener('click', function() { +document.getElementById('singlePaymentMode').addEventListener('click', function () { document.getElementById('singlePaymentMode').classList.add('active'); document.getElementById('mixedPaymentMode').classList.remove('active'); reinitPaymentWidget('single'); }); -document.getElementById('mixedPaymentMode').addEventListener('click', function() { +document.getElementById('mixedPaymentMode').addEventListener('click', function () { document.getElementById('mixedPaymentMode').classList.add('active'); document.getElementById('singlePaymentMode').classList.remove('active'); reinitPaymentWidget('mixed'); @@ -3514,9 +3513,9 @@ document.addEventListener('DOMContentLoaded', () => { }); renderCart(); // Отрисовываем восстановленную корзину } - + // ===== ОБРАБОТЧИКИ ДЛЯ МОДАЛКИ ВЫБОРА ЕДИНИЦЫ ПРОДАЖИ ===== - + // Кнопки изменения количества document.getElementById('unitQtyDecrement').addEventListener('click', () => { const input = document.getElementById('unitModalQuantity'); @@ -3526,7 +3525,7 @@ document.addEventListener('DOMContentLoaded', () => { calculateUnitModalSubtotal(); validateUnitQuantity(); }); - + document.getElementById('unitQtyIncrement').addEventListener('click', () => { const input = document.getElementById('unitModalQuantity'); const step = parseFloat(input.step) || 1; @@ -3535,13 +3534,13 @@ document.addEventListener('DOMContentLoaded', () => { calculateUnitModalSubtotal(); validateUnitQuantity(); }); - + // Изменение количества вручную document.getElementById('unitModalQuantity').addEventListener('input', () => { calculateUnitModalSubtotal(); validateUnitQuantity(); }); - + // Округление количества при потере фокуса document.getElementById('unitModalQuantity').addEventListener('blur', (e) => { const rawValue = parseFloat(e.target.value) || 0; @@ -3549,12 +3548,12 @@ document.addEventListener('DOMContentLoaded', () => { calculateUnitModalSubtotal(); validateUnitQuantity(); }); - + // Изменение цены document.getElementById('unitModalPrice').addEventListener('input', () => { calculateUnitModalSubtotal(); }); - + // Кнопка подтверждения добавления в корзину document.getElementById('confirmAddUnitToCart').addEventListener('click', () => { addToCartFromModal(); @@ -3677,16 +3676,16 @@ if (changeWarehouseBtn) { document.addEventListener('click', async (e) => { const warehouseItem = e.target.closest('.warehouse-item'); if (!warehouseItem) return; - + const warehouseId = warehouseItem.dataset.warehouseId; const warehouseName = warehouseItem.dataset.warehouseName; - + // Проверяем, есть ли товары в корзине if (cart.size > 0) { const confirmed = confirm(`При смене склада корзина будет очищена.\n\nПереключиться на склад "${warehouseName}"?`); if (!confirmed) return; } - + try { // Отправляем запрос на смену склада const response = await fetch(`/pos/api/set-warehouse/${warehouseId}/`, { @@ -3695,9 +3694,9 @@ document.addEventListener('click', async (e) => { 'X-CSRFToken': getCsrfToken() } }); - + const data = await response.json(); - + if (data.success) { // Перезагружаем страницу для обновления данных location.reload(); @@ -3718,19 +3717,19 @@ const clearSearchBtn = document.getElementById('clearSearchBtn'); searchInput.addEventListener('input', (e) => { const query = e.target.value.trim(); - + // Показываем/скрываем кнопку очистки if (e.target.value.length > 0) { clearSearchBtn.style.display = 'block'; } else { clearSearchBtn.style.display = 'none'; } - + // Отменяем предыдущий таймер if (searchDebounceTimer) { clearTimeout(searchDebounceTimer); } - + // Если поле пустое — очищаем экран if (query === '') { currentSearchQuery = ''; @@ -3738,19 +3737,19 @@ searchInput.addEventListener('input', (e) => { renderProducts(); // Пустой экран return; } - + // Минимальная длина поиска — 3 символа if (query.length < 3) { // Не реагируем на ввод менее 3 символов return; } - + // Для витрины — мгновенная клиентская фильтрация if (isShowcaseView) { renderProducts(); return; } - + // Для обычных товаров/комплектов — серверный поиск с debounce 300мс searchDebounceTimer = setTimeout(async () => { currentSearchQuery = query; @@ -3845,18 +3844,18 @@ async function createDeferredOrder() { if (result.success) { console.log(`✅ Заказ #${result.order_number} создан (черновик). ShowcaseItem зарезервированы.`); - + // КРИТИЧНО: Очищаем корзину POS (включая витринные ��укеты) cart.clear(); renderCart(); saveCartToRedis(); // Сохраняем пустую корзину в Redis - + // Перезагружаем витрину (чтобы зарезервированные букеты исчезли) if (isShowcaseView) { await refreshShowcaseKits(); renderProducts(); } - + // Открываем форму редактирования в новой вкладке window.open(`/orders/${result.order_number}/edit/`, '_blank'); } else { diff --git a/myproject/reproduce_issue.py b/myproject/reproduce_issue.py new file mode 100644 index 0000000..867e984 --- /dev/null +++ b/myproject/reproduce_issue.py @@ -0,0 +1,120 @@ + +import os +import sys +import json +import django +from decimal import Decimal + +# Setup Django +sys.path.append(os.getcwd()) +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "myproject.settings") +django.setup() + +from django.test import RequestFactory +from django.contrib.auth import get_user_model +from django.db import connection + +from customers.models import Customer +from inventory.models import Warehouse, Sale +from products.models import Product, UnitOfMeasure +from pos.views import pos_checkout +from orders.models import OrderStatus + +def run(): + # Setup Data + User = get_user_model() + user = User.objects.first() + if not user: + print("No user found") + return + + # Create/Get Customer + customer, _ = Customer.objects.get_or_create( + name="Test Customer", + defaults={'phone': '+375291112233'} + ) + + # Create/Get Warehouse + warehouse, _ = Warehouse.objects.get_or_create( + name="Test Warehouse", + defaults={'is_active': True} + ) + + # Create product + product, _ = Product.objects.get_or_create( + name="Test Product Debug", + defaults={ + 'sku': 'DEBUG001', + 'buying_price': 10, + 'actual_price': 50, + 'warehouse': warehouse + } + ) + product.actual_price = 50 + product.save() + + # Ensure OrderStatus exists + OrderStatus.objects.get_or_create(code='completed', is_system=True, defaults={'name': 'Completed', 'is_positive_end': True}) + OrderStatus.objects.get_or_create(code='draft', is_system=True, defaults={'name': 'Draft'}) + + # Prepare Request + factory = RequestFactory() + + payload = { + "customer_id": customer.id, + "warehouse_id": warehouse.id, + "items": [ + { + "type": "product", + "id": product.id, + "quantity": 1, + "price": 100.00, # Custom price + "quantity_base": 1 + } + ], + "payments": [ + {"payment_method": "cash", "amount": 100.00} + ], + "notes": "Debug Sale" + } + + request = factory.post( + '/pos/api/checkout/', + data=json.dumps(payload), + content_type='application/json' + ) + request.user = user + + print("Executing pos_checkout...") + response = pos_checkout(request) + print(f"Response: {response.content}") + + # Verify Sale + sales = Sale.objects.filter(product=product).order_by('-id')[:1] + if sales: + sale = sales[0] + print(f"Sale created. ID: {sale.id}") + print(f"Sale Quantity: {sale.quantity}") + print(f"Sale Price: {sale.sale_price}") + if sale.sale_price == 0: + print("FAILURE: Sale price is 0!") + else: + print(f"SUCCESS: Sale price is {sale.sale_price}") + else: + print("FAILURE: No Sale created!") + +if __name__ == "__main__": + from django_tenants.utils import schema_context + # Replace with actual schema name if needed, assuming 'public' for now or the default tenant + # Since I don't know the tenant, I'll try to run in the current context. + # But usually need to set schema. + # Let's try to find a tenant. + from tenants.models import Client + tenant = Client.objects.first() + if tenant: + print(f"Running in tenant: {tenant.schema_name}") + with schema_context(tenant.schema_name): + run() + else: + print("No tenant found, running in public?") + run()