feat: Add new inventory and POS components, including a script to reproduce a POS checkout sale price bug.
This commit is contained in:
@@ -209,16 +209,19 @@ class ShowcaseManager:
|
|||||||
reservation.order_item = order_item
|
reservation.order_item = order_item
|
||||||
reservation.save()
|
reservation.save()
|
||||||
|
|
||||||
# Теперь создаём продажу с правильной ценой из OrderItem
|
# ВАЖНО: Мы НЕ создаём продажу (Sale) здесь и НЕ меняем статус на 'converted_to_sale'.
|
||||||
SaleProcessor.create_sale_from_reservation(
|
# Это сделает сигнал create_sale_on_order_completion автоматически.
|
||||||
reservation=reservation,
|
# Таким образом обеспечивается единая точка создания продаж для всех типов товаров.
|
||||||
order=order
|
|
||||||
)
|
|
||||||
|
|
||||||
# Обновляем статус резерва
|
# SaleProcessor.create_sale_from_reservation(
|
||||||
reservation.status = 'converted_to_sale'
|
# reservation=reservation,
|
||||||
reservation.converted_at = timezone.now()
|
# order=order
|
||||||
reservation.save()
|
# )
|
||||||
|
|
||||||
|
# Статус резерва остается 'reserved', чтобы сигнал его увидел
|
||||||
|
# reservation.status = 'converted_to_sale'
|
||||||
|
# reservation.converted_at = timezone.now()
|
||||||
|
# reservation.save()
|
||||||
|
|
||||||
sold_count += 1
|
sold_count += 1
|
||||||
|
|
||||||
|
|||||||
@@ -366,7 +366,7 @@ def create_sale_on_order_completion(sender, instance, created, **kwargs):
|
|||||||
# === ЗАЩИТА ОТ ПРЕЖДЕВРЕМЕННОГО СОЗДАНИЯ SALE ===
|
# === ЗАЩИТА ОТ ПРЕЖДЕВРЕМЕННОГО СОЗДАНИЯ SALE ===
|
||||||
# Проверяем, есть ли уже Sale для этого заказа
|
# Проверяем, есть ли уже Sale для этого заказа
|
||||||
if Sale.objects.filter(order=instance).exists():
|
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)
|
update_is_returned_flag(instance)
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -376,7 +376,7 @@ def create_sale_on_order_completion(sender, instance, created, **kwargs):
|
|||||||
previous_status = getattr(instance, '_previous_status', None)
|
previous_status = getattr(instance, '_previous_status', None)
|
||||||
if previous_status and previous_status.is_positive_end:
|
if previous_status and previous_status.is_positive_end:
|
||||||
logger.info(
|
logger.info(
|
||||||
f"🔄 Заказ {instance.order_number}: повторный переход в положительный статус "
|
f"Заказ {instance.order_number}: повторный переход в положительный статус "
|
||||||
f"({previous_status.name} → {instance.status.name}). Проверяем Sale..."
|
f"({previous_status.name} → {instance.status.name}). Проверяем Sale..."
|
||||||
)
|
)
|
||||||
if Sale.objects.filter(order=instance).exists():
|
if Sale.objects.filter(order=instance).exists():
|
||||||
@@ -454,12 +454,65 @@ def create_sale_on_order_completion(sender, instance, created, **kwargs):
|
|||||||
)
|
)
|
||||||
continue
|
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 для каждого компонента комплекта
|
# Создаем Sale для каждого компонента комплекта
|
||||||
for reservation in kit_reservations:
|
for reservation in kit_reservations:
|
||||||
try:
|
try:
|
||||||
# Рассчитываем цену продажи компонента пропорционально цене комплекта
|
# Рассчитываем цену продажи компонента пропорционально
|
||||||
# Используем actual_price компонента как цену продажи
|
catalog_price = reservation.product.actual_price or Decimal('0')
|
||||||
component_sale_price = reservation.product.actual_price
|
|
||||||
|
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(
|
sale = SaleProcessor.create_sale(
|
||||||
product=reservation.product,
|
product=reservation.product,
|
||||||
@@ -472,7 +525,8 @@ def create_sale_on_order_completion(sender, instance, created, **kwargs):
|
|||||||
sales_created.append(sale)
|
sales_created.append(sale)
|
||||||
logger.info(
|
logger.info(
|
||||||
f"✓ Sale создан для компонента комплекта '{kit.name}': "
|
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:
|
except ValueError as e:
|
||||||
logger.error(
|
logger.error(
|
||||||
@@ -548,6 +602,21 @@ def create_sale_on_order_completion(sender, instance, created, **kwargs):
|
|||||||
else:
|
else:
|
||||||
base_price = price_with_discount
|
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 (с автоматическим FIFO-списанием)
|
||||||
sale = SaleProcessor.create_sale(
|
sale = SaleProcessor.create_sale(
|
||||||
product=product,
|
product=product,
|
||||||
|
|||||||
@@ -631,3 +631,5 @@ if not DEBUG and not ENCRYPTION_KEY:
|
|||||||
"ENCRYPTION_KEY not set! Encrypted fields will fail. "
|
"ENCRYPTION_KEY not set! Encrypted fields will fail. "
|
||||||
"Generate with: python -c \"from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())\""
|
"Generate with: python -c \"from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())\""
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -72,15 +72,15 @@ function saveCartToRedis() {
|
|||||||
},
|
},
|
||||||
body: JSON.stringify({ cart: cartObj })
|
body: JSON.stringify({ cart: cartObj })
|
||||||
})
|
})
|
||||||
.then(response => response.json())
|
.then(response => response.json())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
if (!data.success) {
|
if (!data.success) {
|
||||||
console.error('Ошибка сохранения корзины:', data.error);
|
console.error('Ошибка сохранения корзины:', data.error);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
console.error('Ошибка при сохранении корзины в Redis:', error);
|
console.error('Ошибка при сохранении корзины в Redis:', error);
|
||||||
});
|
});
|
||||||
}, 500); // Debounce 500ms
|
}, 500); // Debounce 500ms
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -160,7 +160,7 @@ function updateCustomerDisplay() {
|
|||||||
// Обновляем видимость кнопок сброса (в корзине и в модалке продажи)
|
// Обновляем видимость кнопок сброса (в корзине и в модалке продажи)
|
||||||
|
|
||||||
[document.getElementById('resetCustomerBtn'),
|
[document.getElementById('resetCustomerBtn'),
|
||||||
document.getElementById('checkoutResetCustomerBtn')].forEach(resetBtn => {
|
document.getElementById('checkoutResetCustomerBtn')].forEach(resetBtn => {
|
||||||
if (resetBtn) {
|
if (resetBtn) {
|
||||||
resetBtn.style.display = isSystemCustomer ? 'none' : 'block';
|
resetBtn.style.display = isSystemCustomer ? 'none' : 'block';
|
||||||
}
|
}
|
||||||
@@ -242,18 +242,18 @@ function selectCustomer(customerId, customerName, walletBalance = 0) {
|
|||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.then(response => response.json())
|
.then(response => response.json())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
if (!data.success) {
|
if (!data.success) {
|
||||||
console.error('Ошибка сохранения клиента:', data.error);
|
console.error('Ошибка сохранения клиента:', data.error);
|
||||||
} else {
|
} else {
|
||||||
// Обновляем баланс из ответа сервера
|
// Обновляем баланс из ответа сервера
|
||||||
selectedCustomer.wallet_balance = data.wallet_balance || 0;
|
selectedCustomer.wallet_balance = data.wallet_balance || 0;
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
console.error('Ошибка при сохранении клиента в Redis:', error);
|
console.error('Ошибка при сохранении клиента в Redis:', error);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -272,12 +272,12 @@ function initCustomerSelect2() {
|
|||||||
url: '/customers/api/search/',
|
url: '/customers/api/search/',
|
||||||
dataType: 'json',
|
dataType: 'json',
|
||||||
delay: 300,
|
delay: 300,
|
||||||
data: function(params) {
|
data: function (params) {
|
||||||
return {
|
return {
|
||||||
q: params.term
|
q: params.term
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
processResults: function(data) {
|
processResults: function (data) {
|
||||||
return {
|
return {
|
||||||
results: data.results
|
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;
|
const data = e.params.data;
|
||||||
|
|
||||||
// Проверяем это не опция "Создать нового клиента"
|
// Проверяем это не опция "Создать нового клиента"
|
||||||
@@ -1487,7 +1487,7 @@ function renderCart() {
|
|||||||
row.appendChild(deleteBtn);
|
row.appendChild(deleteBtn);
|
||||||
|
|
||||||
// Обработчик клика для редактирования товара
|
// Обработчик клика для редактирования товара
|
||||||
row.addEventListener('click', function(e) {
|
row.addEventListener('click', function (e) {
|
||||||
// Игнорируем клики на кнопки управления количеством и удаления
|
// Игнорируем клики на кнопки управления количеством и удаления
|
||||||
if (e.target.closest('button') || e.target.closest('input')) {
|
if (e.target.closest('button') || e.target.closest('input')) {
|
||||||
return;
|
return;
|
||||||
@@ -1817,7 +1817,7 @@ async function openCreateTempKitModal() {
|
|||||||
// Копируем содержимое cart в tempCart (изолированное состояние модалки)
|
// Копируем содержимое cart в tempCart (изолированное состояние модалки)
|
||||||
tempCart.clear();
|
tempCart.clear();
|
||||||
cart.forEach((item, key) => {
|
cart.forEach((item, key) => {
|
||||||
tempCart.set(key, {...item}); // Глубокая копия объекта
|
tempCart.set(key, { ...item }); // Глубокая копия объекта
|
||||||
});
|
});
|
||||||
|
|
||||||
// Генерируем название по умолчанию
|
// Генерируем название по умолчанию
|
||||||
@@ -1931,7 +1931,7 @@ async function openEditKitModal(kitId) {
|
|||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (window.ProductSearchPicker) {
|
if (window.ProductSearchPicker) {
|
||||||
const picker = ProductSearchPicker.init('#temp-kit-product-picker', {
|
const picker = ProductSearchPicker.init('#temp-kit-product-picker', {
|
||||||
onAddSelected: function(product, instance) {
|
onAddSelected: function (product, instance) {
|
||||||
if (product) {
|
if (product) {
|
||||||
// Добавляем товар в tempCart
|
// Добавляем товар в tempCart
|
||||||
const cartKey = `product-${product.id}`;
|
const cartKey = `product-${product.id}`;
|
||||||
@@ -2265,7 +2265,7 @@ function updatePriceCalculations(basePrice = null) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Обработчики для полей цены
|
// Обработчики для полей цены
|
||||||
document.getElementById('priceAdjustmentType').addEventListener('change', function() {
|
document.getElementById('priceAdjustmentType').addEventListener('change', function () {
|
||||||
const adjustmentBlock = document.getElementById('adjustmentValueBlock');
|
const adjustmentBlock = document.getElementById('adjustmentValueBlock');
|
||||||
if (this.value === 'none') {
|
if (this.value === 'none') {
|
||||||
adjustmentBlock.style.display = 'none';
|
adjustmentBlock.style.display = 'none';
|
||||||
@@ -2276,11 +2276,11 @@ document.getElementById('priceAdjustmentType').addEventListener('change', functi
|
|||||||
updatePriceCalculations();
|
updatePriceCalculations();
|
||||||
});
|
});
|
||||||
|
|
||||||
document.getElementById('priceAdjustmentValue').addEventListener('input', function() {
|
document.getElementById('priceAdjustmentValue').addEventListener('input', function () {
|
||||||
updatePriceCalculations();
|
updatePriceCalculations();
|
||||||
});
|
});
|
||||||
|
|
||||||
document.getElementById('useSalePrice').addEventListener('change', function() {
|
document.getElementById('useSalePrice').addEventListener('change', function () {
|
||||||
const salePriceBlock = document.getElementById('salePriceBlock');
|
const salePriceBlock = document.getElementById('salePriceBlock');
|
||||||
if (this.checked) {
|
if (this.checked) {
|
||||||
salePriceBlock.style.display = 'block';
|
salePriceBlock.style.display = 'block';
|
||||||
@@ -2291,12 +2291,12 @@ document.getElementById('useSalePrice').addEventListener('change', function() {
|
|||||||
updatePriceCalculations();
|
updatePriceCalculations();
|
||||||
});
|
});
|
||||||
|
|
||||||
document.getElementById('salePrice').addEventListener('input', function() {
|
document.getElementById('salePrice').addEventListener('input', function () {
|
||||||
updatePriceCalculations();
|
updatePriceCalculations();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Обработчик загрузки фото
|
// Обработчик загрузки фото
|
||||||
document.getElementById('tempKitPhoto').addEventListener('change', function(e) {
|
document.getElementById('tempKitPhoto').addEventListener('change', function (e) {
|
||||||
const file = e.target.files[0];
|
const file = e.target.files[0];
|
||||||
if (file) {
|
if (file) {
|
||||||
if (!file.type.startsWith('image/')) {
|
if (!file.type.startsWith('image/')) {
|
||||||
@@ -2307,7 +2307,7 @@ document.getElementById('tempKitPhoto').addEventListener('change', function(e) {
|
|||||||
|
|
||||||
// Превью
|
// Превью
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
reader.onload = function(event) {
|
reader.onload = function (event) {
|
||||||
document.getElementById('photoPreviewImg').src = event.target.result;
|
document.getElementById('photoPreviewImg').src = event.target.result;
|
||||||
document.getElementById('photoPreview').style.display = 'block';
|
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('tempKitPhoto').value = '';
|
||||||
document.getElementById('photoPreview').style.display = 'none';
|
document.getElementById('photoPreview').style.display = 'none';
|
||||||
document.getElementById('photoPreviewImg').src = '';
|
document.getElementById('photoPreviewImg').src = '';
|
||||||
@@ -2388,10 +2388,9 @@ document.getElementById('confirmCreateTempKit').onclick = async () => {
|
|||||||
formData.append('items', JSON.stringify(items));
|
formData.append('items', JSON.stringify(items));
|
||||||
formData.append('price_adjustment_type', priceAdjustmentType);
|
formData.append('price_adjustment_type', priceAdjustmentType);
|
||||||
formData.append('price_adjustment_value', priceAdjustmentValue);
|
formData.append('price_adjustment_value', priceAdjustmentValue);
|
||||||
// Если пользователь не задал свою цену, используем вычисленную
|
// Если пользователь явно указал свою цену
|
||||||
const finalSalePrice = useSalePrice ? salePrice : calculatedPrice;
|
if (useSalePrice && salePrice > 0) {
|
||||||
if (finalSalePrice > 0) {
|
formData.append('sale_price', salePrice);
|
||||||
formData.append('sale_price', finalSalePrice);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Фото: для редактирования проверяем, удалено ли оно
|
// Фото: для редактирования проверяем, удалено ли оно
|
||||||
@@ -2650,7 +2649,7 @@ const getCsrfToken = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Сброс режима редактирования при закрытии модального окна
|
// Сброс режима редактирования при закрытии модального окна
|
||||||
document.getElementById('createTempKitModal').addEventListener('hidden.bs.modal', function() {
|
document.getElementById('createTempKitModal').addEventListener('hidden.bs.modal', function () {
|
||||||
// Очищаем tempCart (изолированное состояние модалки)
|
// Очищаем tempCart (изолированное состояние модалки)
|
||||||
tempCart.clear();
|
tempCart.clear();
|
||||||
|
|
||||||
@@ -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('singlePaymentMode').classList.add('active');
|
||||||
document.getElementById('mixedPaymentMode').classList.remove('active');
|
document.getElementById('mixedPaymentMode').classList.remove('active');
|
||||||
reinitPaymentWidget('single');
|
reinitPaymentWidget('single');
|
||||||
});
|
});
|
||||||
|
|
||||||
document.getElementById('mixedPaymentMode').addEventListener('click', function() {
|
document.getElementById('mixedPaymentMode').addEventListener('click', function () {
|
||||||
document.getElementById('mixedPaymentMode').classList.add('active');
|
document.getElementById('mixedPaymentMode').classList.add('active');
|
||||||
document.getElementById('singlePaymentMode').classList.remove('active');
|
document.getElementById('singlePaymentMode').classList.remove('active');
|
||||||
reinitPaymentWidget('mixed');
|
reinitPaymentWidget('mixed');
|
||||||
|
|||||||
120
myproject/reproduce_issue.py
Normal file
120
myproject/reproduce_issue.py
Normal file
@@ -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()
|
||||||
Reference in New Issue
Block a user