Compare commits

...

4 Commits

Author SHA1 Message Date
5700314b10 feat(ui): replace alert notifications with toast messages
Add toast notification functionality using Bootstrap Toasts and update
checkout success/error handling to use toast messages instead of alert boxes.

**Changes:**
- Add `showToast` function to `terminal.js`
- Add toast container and templates to `terminal.html`
- Replace alert() calls in handleCheckoutSubmit with showToast()
2026-01-26 17:44:03 +03:00
b24a0d9f21 feat: Add UI for inventory transfer list and detail views. 2026-01-25 16:44:54 +03:00
034be20a5a feat: add showcase manager service 2026-01-25 15:28:41 +03:00
f75e861bb8 feat: Add new inventory and POS components, including a script to reproduce a POS checkout sale price bug. 2026-01-25 15:26:57 +03:00
8 changed files with 475 additions and 235 deletions

View File

@@ -162,8 +162,6 @@ class ShowcaseManager:
Raises: Raises:
IntegrityError: если экземпляр уже был продан (защита на уровне БД) IntegrityError: если экземпляр уже был продан (защита на уровне БД)
""" """
from inventory.services.sale_processor import SaleProcessor
sold_count = 0 sold_count = 0
order = order_item.order order = order_item.order
@@ -207,17 +205,9 @@ class ShowcaseManager:
# Сначала устанавливаем order_item для правильного определения цены # Сначала устанавливаем order_item для правильного определения цены
reservation.order_item = order_item reservation.order_item = order_item
reservation.save() # ВАЖНО: Мы НЕ создаём продажу (Sale) здесь и НЕ меняем статус на 'converted_to_sale'.
# Это сделает сигнал create_sale_on_order_completion автоматически.
# Теперь создаём продажу с правильной ценой из OrderItem # Таким образом обеспечивается единая точка создания продаж для всех типов товаров.
SaleProcessor.create_sale_from_reservation(
reservation=reservation,
order=order
)
# Обновляем статус резерва
reservation.status = 'converted_to_sale'
reservation.converted_at = timezone.now()
reservation.save() reservation.save()
sold_count += 1 sold_count += 1

View File

@@ -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,

View File

@@ -74,10 +74,12 @@
{% for item in items %} {% for item in items %}
<tr> <tr>
<td class="px-3 py-2"> <td class="px-3 py-2">
<a href="{% url 'products:product-detail' item.product.id %}">{{ item.product.name }}</a> <a href="{% url 'products:product-detail' item.product.id %}">{{
item.product.name }}</a>
</td> </td>
<td class="px-3 py-2" style="text-align: right;">{{ item.quantity }}</td> <td class="px-3 py-2" style="text-align: right;">{{ item.quantity }}</td>
<td class="px-3 py-2" style="text-align: right;">{{ item.batch.cost_price }} ₽/ед.</td> <td class="px-3 py-2" style="text-align: right;">{{ item.batch.cost_price }} ₽/ед.
</td>
<td class="px-3 py-2"> <td class="px-3 py-2">
<span class="badge bg-secondary">{{ item.batch.id }}</span> <span class="badge bg-secondary">{{ item.batch.id }}</span>
</td> </td>
@@ -132,9 +134,11 @@
<a href="{% url 'inventory:transfer-list' %}" class="btn btn-outline-secondary btn-sm"> <a href="{% url 'inventory:transfer-list' %}" class="btn btn-outline-secondary btn-sm">
<i class="bi bi-arrow-left me-1"></i>Вернуться к списку <i class="bi bi-arrow-left me-1"></i>Вернуться к списку
</a> </a>
<!--
<a href="{% url 'inventory:transfer-delete' transfer_document.id %}" class="btn btn-outline-danger btn-sm"> <a href="{% url 'inventory:transfer-delete' transfer_document.id %}" class="btn btn-outline-danger btn-sm">
<i class="bi bi-trash me-1"></i>Удалить <i class="bi bi-trash me-1"></i>Удалить
</a> </a>
-->
</div> </div>
</div> </div>
</div> </div>
@@ -143,13 +147,13 @@
</div> </div>
<style> <style>
.breadcrumb-sm { .breadcrumb-sm {
font-size: 0.875rem; font-size: 0.875rem;
padding: 0.5rem 0; padding: 0.5rem 0;
} }
.table-hover tbody tr:hover { .table-hover tbody tr:hover {
background-color: #f8f9fa; background-color: #f8f9fa;
} }
</style> </style>
{% endblock %} {% endblock %}

View File

@@ -39,9 +39,11 @@
<a href="{% url 'inventory:transfer-detail' t.pk %}" class="btn btn-sm btn-outline-info" title="Просмотр"> <a href="{% url 'inventory:transfer-detail' t.pk %}" class="btn btn-sm btn-outline-info" title="Просмотр">
<i class="bi bi-eye"></i> <i class="bi bi-eye"></i>
</a> </a>
<!--
<a href="{% url 'inventory:transfer-delete' t.pk %}" class="btn btn-sm btn-outline-danger" title="Удалить"> <a href="{% url 'inventory:transfer-delete' t.pk %}" class="btn btn-sm btn-outline-danger" title="Удалить">
<i class="bi bi-trash"></i> <i class="bi bi-trash"></i>
</a> </a>
-->
</td> </td>
</tr> </tr>
{% endfor %} {% endfor %}

View File

@@ -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())\""
) )

View File

@@ -12,6 +12,38 @@ function roundQuantity(value, decimals = 3) {
return Math.round(value * Math.pow(10, decimals)) / Math.pow(10, decimals); return Math.round(value * Math.pow(10, decimals)) / Math.pow(10, decimals);
} }
/**
* Показывает toast уведомление в правом верхнем углу
* @param {string} type - 'success' или 'error'
* @param {string} message - Текст сообщения
*/
function showToast(type, message) {
const toastId = type === 'success' ? 'orderSuccessToast' : 'orderErrorToast';
const messageId = type === 'success' ? 'toastMessage' : 'errorMessage';
const bgClass = type === 'success' ? 'bg-success' : 'bg-danger';
const toastElement = document.getElementById(toastId);
const messageElement = document.getElementById(messageId);
// Устанавливаем сообщение
messageElement.textContent = message;
// Добавляем цвет фона
toastElement.classList.add(bgClass, 'text-white');
// Создаём и показываем toast (автоматически скроется через 3 секунды)
const toast = new bootstrap.Toast(toastElement, {
delay: 3000,
autohide: true
});
toast.show();
// Убираем класс цвета после скрытия
toastElement.addEventListener('hidden.bs.toast', () => {
toastElement.classList.remove(bgClass, 'text-white');
}, { once: true });
}
const CATEGORIES = JSON.parse(document.getElementById('categoriesData').textContent); const CATEGORIES = JSON.parse(document.getElementById('categoriesData').textContent);
let ITEMS = []; // Будем загружать через API let ITEMS = []; // Будем загружать через API
let showcaseKits = JSON.parse(document.getElementById('showcaseKitsData').textContent); let showcaseKits = JSON.parse(document.getElementById('showcaseKitsData').textContent);
@@ -272,12 +304,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 +321,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 +1519,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 +1849,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 +1963,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 +2297,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 +2308,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 +2323,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 +2339,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 +2348,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 +2420,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 +2681,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 +2805,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');
@@ -3416,8 +3447,8 @@ async function handleCheckoutSubmit(paymentsData) {
if (result.success) { if (result.success) {
console.log('✅ Заказ успешно создан:', result); console.log('✅ Заказ успешно создан:', result);
// Успех // Показываем toast уведомление
alert(`Заказ #${result.order_number} успешно создан!\nСумма: ${result.total_amount.toFixed(2)} руб.`); showToast('success', `Заказ #${result.order_number} успешно создан! Сумма: ${result.total_amount.toFixed(2)} руб.`);
// Очищаем корзину // Очищаем корзину
cart.clear(); cart.clear();
@@ -3438,12 +3469,12 @@ async function handleCheckoutSubmit(paymentsData) {
}, 500); }, 500);
} else { } else {
alert('Ошибка: ' + result.error); showToast('error', 'Ошибка: ' + result.error);
} }
} catch (error) { } catch (error) {
console.error('Ошибка checkout:', error); console.error('Ошибка checkout:', error);
alert('Ошибка при проведении продажи: ' + error.message); showToast('error', 'Ошибка при проведении продажи: ' + error.message);
} finally { } finally {
// Разблокируем кнопку // Разблокируем кнопку
const btn = document.getElementById('confirmCheckoutBtn'); const btn = document.getElementById('confirmCheckoutBtn');

View File

@@ -729,6 +729,28 @@
<!-- Модалка редактирования товара в корзине --> <!-- Модалка редактирования товара в корзине -->
{% include 'pos/components/edit_cart_item_modal.html' %} {% include 'pos/components/edit_cart_item_modal.html' %}
<!-- Toast Container для уведомлений -->
<div class="toast-container position-fixed top-0 end-0 p-3" style="z-index: 1060;">
<div id="orderSuccessToast" class="toast align-items-center border-0" role="alert" aria-live="assertive" aria-atomic="true">
<div class="d-flex">
<div class="toast-body">
<i class="bi bi-check-circle-fill text-success me-2 fs-5"></i>
<span id="toastMessage"></span>
</div>
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast" aria-label="Close" style="display: none;"></button>
</div>
</div>
<div id="orderErrorToast" class="toast align-items-center border-0" role="alert" aria-live="assertive" aria-atomic="true">
<div class="d-flex">
<div class="toast-body">
<i class="bi bi-exclamation-circle-fill text-danger me-2 fs-5"></i>
<span id="errorMessage"></span>
</div>
</div>
</div>
</div>
{% endblock %} {% endblock %}
{% block extra_js %} {% block extra_js %}

View 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()