Реализована полноценная система оплаты для POS-терминала
Добавлена интеграция оплаты в POS с поддержкой одиночной и смешанной оплаты, работой с кошельком клиента и автоматическим созданием заказов. Backend изменения: - TransactionService: добавлены методы get_available_payment_methods() и create_multiple_payments() для фильтрации способов оплаты и атомарного создания нескольких платежей - POS API: новый endpoint pos_checkout() для создания заказов со статусом "Выполнен" с обработкой платежей, освобождением блокировок и очисткой корзины - Template tags: payment_tags.py для получения способов оплаты в шаблонах Frontend изменения: - PaymentWidget: переиспользуемый ES6 класс с поддержкой single/mixed режимов, автоматической валидацией и интеграцией с кошельком клиента - terminal.html: компактное модальное окно (70vw) с оптимизированной компоновкой, удален функционал скидок, добавлен показ баланса кошелька - terminal.js: динамическая загрузка PaymentWidget, интеграция с backend API, обработка успешной оплаты и ошибок Поддерживаемые способы оплаты: наличные, карта, онлайн, баланс счёта. Смешанная оплата позволяет комбинировать несколько способов в одной транзакции. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -235,3 +235,74 @@ class TransactionService:
|
||||
Decimal: Сумма, которую можно вернуть
|
||||
"""
|
||||
return max(order.amount_paid, Decimal('0'))
|
||||
|
||||
@staticmethod
|
||||
def get_available_payment_methods(exclude_codes=None, only_active=True):
|
||||
"""
|
||||
Получить список доступных способов оплаты.
|
||||
|
||||
Args:
|
||||
exclude_codes: list[str] - коды для исключения (например ['legal_entity'])
|
||||
only_active: bool - только активные методы
|
||||
|
||||
Returns:
|
||||
QuerySet[PaymentMethod]
|
||||
"""
|
||||
from orders.models import PaymentMethod
|
||||
|
||||
qs = PaymentMethod.objects.all()
|
||||
|
||||
if only_active:
|
||||
qs = qs.filter(is_active=True)
|
||||
|
||||
if exclude_codes:
|
||||
qs = qs.exclude(code__in=exclude_codes)
|
||||
|
||||
return qs.order_by('order', 'name')
|
||||
|
||||
@staticmethod
|
||||
@transaction.atomic
|
||||
def create_multiple_payments(order, payments_list, user):
|
||||
"""
|
||||
Создать несколько платежей за одну транзакцию (смешанная оплата).
|
||||
|
||||
Args:
|
||||
order: Order
|
||||
payments_list: list[dict] - [{'payment_method': code_or_object, 'amount': Decimal, 'notes': str}, ...]
|
||||
user: CustomUser
|
||||
|
||||
Returns:
|
||||
list[Transaction]
|
||||
|
||||
Raises:
|
||||
ValidationError: если сумма превышает amount_due или недостаточно средств
|
||||
"""
|
||||
from orders.models import Transaction
|
||||
|
||||
transactions = []
|
||||
total_amount = Decimal('0')
|
||||
|
||||
# Валидация общей суммы
|
||||
for payment_data in payments_list:
|
||||
amount = _quantize(payment_data['amount'])
|
||||
if amount <= 0:
|
||||
raise ValueError(f'Сумма платежа должна быть положительной: {amount}')
|
||||
total_amount += amount
|
||||
|
||||
if total_amount > order.amount_due:
|
||||
raise ValidationError(
|
||||
f'Общая сумма платежей ({total_amount}) превышает сумму к оплате ({order.amount_due})'
|
||||
)
|
||||
|
||||
# Создаём транзакции
|
||||
for payment_data in payments_list:
|
||||
txn = TransactionService.create_payment(
|
||||
order=order,
|
||||
amount=payment_data['amount'],
|
||||
payment_method=payment_data['payment_method'],
|
||||
user=user,
|
||||
notes=payment_data.get('notes')
|
||||
)
|
||||
transactions.append(txn)
|
||||
|
||||
return transactions
|
||||
|
||||
370
myproject/orders/static/orders/js/payment_widget.js
Normal file
370
myproject/orders/static/orders/js/payment_widget.js
Normal file
@@ -0,0 +1,370 @@
|
||||
/**
|
||||
* PaymentWidget - переиспользуемый виджет для оплаты заказа.
|
||||
* Поддерживает одиночную и смешанную оплату.
|
||||
*
|
||||
* @example
|
||||
* const widget = new PaymentWidget({
|
||||
* containerId: 'paymentContainer',
|
||||
* mode: 'single', // или 'mixed'
|
||||
* order: { id: 1, total: 5000, amount_due: 5000 },
|
||||
* customer: { id: 1, wallet_balance: 1000 },
|
||||
* paymentMethods: [
|
||||
* { id: 1, code: 'account_balance', name: 'С баланса счёта' },
|
||||
* { id: 2, code: 'cash', name: 'Наличными' },
|
||||
* ...
|
||||
* ],
|
||||
* onSubmit: (payments) => { console.log(payments); }
|
||||
* });
|
||||
*/
|
||||
export class PaymentWidget {
|
||||
constructor(options = {}) {
|
||||
this.containerId = options.containerId;
|
||||
this.mode = options.mode || 'single'; // 'single' | 'mixed'
|
||||
this.order = options.order;
|
||||
this.customer = options.customer;
|
||||
this.paymentMethods = options.paymentMethods || [];
|
||||
this.onSubmit = options.onSubmit || (() => {});
|
||||
|
||||
// Внутреннее состояние
|
||||
this.payments = []; // [{ method_id, method_code, method_name, amount, notes }]
|
||||
this.selectedMethod = null; // Для single mode
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
this.render();
|
||||
this.attachEvents();
|
||||
}
|
||||
|
||||
render() {
|
||||
const container = document.getElementById(this.containerId);
|
||||
if (!container) {
|
||||
console.error(`Container #${this.containerId} not found`);
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = `
|
||||
<!-- Способы оплаты -->
|
||||
<div class="mb-2">
|
||||
<label class="form-label fw-semibold small mb-1">Способ оплаты</label>
|
||||
<div class="d-flex flex-wrap gap-1" id="${this.containerId}-methods">
|
||||
${this.renderPaymentMethods()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
${this.mode === 'mixed' ? this.renderPaymentsList() : ''}
|
||||
|
||||
<!-- Сумма -->
|
||||
<div class="mb-2">
|
||||
<label class="form-label fw-semibold small mb-1">Сумма</label>
|
||||
<div class="input-group input-group-sm">
|
||||
<input type="number"
|
||||
class="form-control form-control-sm"
|
||||
id="${this.containerId}-amount"
|
||||
step="0.01"
|
||||
min="0.01"
|
||||
value="${this.order.amount_due}"
|
||||
placeholder="0.00">
|
||||
<span class="input-group-text">руб.</span>
|
||||
</div>
|
||||
<small class="text-muted" id="${this.containerId}-hint"></small>
|
||||
</div>
|
||||
|
||||
<!-- Примечание -->
|
||||
<div class="mb-2">
|
||||
<label class="form-label small mb-1">Примечание</label>
|
||||
<input type="text"
|
||||
class="form-control form-control-sm"
|
||||
id="${this.containerId}-notes"
|
||||
placeholder="Опционально">
|
||||
</div>
|
||||
|
||||
<!-- Кнопки -->
|
||||
<div class="d-flex gap-2">
|
||||
${this.mode === 'mixed' ? `
|
||||
<button type="button"
|
||||
class="btn btn-sm btn-outline-primary"
|
||||
id="${this.containerId}-add-btn">
|
||||
<i class="bi bi-plus-circle"></i> Добавить
|
||||
</button>
|
||||
` : ''}
|
||||
<button type="button"
|
||||
class="btn btn-sm btn-success flex-grow-1"
|
||||
id="${this.containerId}-submit-btn">
|
||||
<i class="bi bi-check-lg"></i> Оплатить
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
renderPaymentMethods() {
|
||||
return this.paymentMethods.map(method => {
|
||||
const showWallet = method.code === 'account_balance' && this.customer;
|
||||
return `
|
||||
<button type="button"
|
||||
class="btn btn-sm btn-outline-primary payment-method-btn"
|
||||
data-method-id="${method.id}"
|
||||
data-method-code="${method.code}"
|
||||
data-method-name="${method.name}"
|
||||
style="font-size: 0.85rem;">
|
||||
${method.name}
|
||||
${showWallet ? `
|
||||
<span class="badge bg-info ms-1" style="font-size: 0.75rem;">
|
||||
${this.customer.wallet_balance.toFixed(2)}
|
||||
</span>
|
||||
` : ''}
|
||||
</button>
|
||||
`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
renderPaymentsList() {
|
||||
return `
|
||||
<div class="mb-2">
|
||||
<label class="form-label fw-semibold small mb-1">Добавленные платежи</label>
|
||||
<div id="${this.containerId}-payments-list"
|
||||
class="border rounded p-2 bg-light"
|
||||
style="min-height: 50px; max-height: 150px; overflow-y: auto; font-size: 0.9rem;">
|
||||
<small class="text-muted">Платежи не добавлены</small>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
attachEvents() {
|
||||
// Выбор способа оплаты
|
||||
document.querySelectorAll(`#${this.containerId}-methods .payment-method-btn`).forEach(btn => {
|
||||
btn.addEventListener('click', () => this.selectPaymentMethod(btn));
|
||||
});
|
||||
|
||||
// Добавить платеж (mixed mode)
|
||||
if (this.mode === 'mixed') {
|
||||
const addBtn = document.getElementById(`${this.containerId}-add-btn`);
|
||||
if (addBtn) {
|
||||
addBtn.addEventListener('click', () => this.addPayment());
|
||||
}
|
||||
}
|
||||
|
||||
// Отправка
|
||||
const submitBtn = document.getElementById(`${this.containerId}-submit-btn`);
|
||||
if (submitBtn) {
|
||||
submitBtn.addEventListener('click', () => this.submit());
|
||||
}
|
||||
}
|
||||
|
||||
selectPaymentMethod(btnElement) {
|
||||
// Снимаем выделение
|
||||
document.querySelectorAll(`#${this.containerId}-methods .payment-method-btn`).forEach(b => {
|
||||
b.classList.remove('active');
|
||||
});
|
||||
|
||||
// Выделяем выбранный
|
||||
btnElement.classList.add('active');
|
||||
|
||||
this.selectedMethod = {
|
||||
id: btnElement.dataset.methodId,
|
||||
code: btnElement.dataset.methodCode,
|
||||
name: btnElement.dataset.methodName
|
||||
};
|
||||
|
||||
// Валидация для кошелька
|
||||
const amountInput = document.getElementById(`${this.containerId}-amount`);
|
||||
const hint = document.getElementById(`${this.containerId}-hint`);
|
||||
|
||||
if (this.selectedMethod.code === 'account_balance' && this.customer) {
|
||||
const maxAmount = Math.min(this.order.amount_due, this.customer.wallet_balance);
|
||||
amountInput.max = maxAmount;
|
||||
amountInput.value = maxAmount.toFixed(2);
|
||||
hint.textContent = `Макс: ${maxAmount.toFixed(2)} руб.`;
|
||||
} else {
|
||||
amountInput.removeAttribute('max');
|
||||
amountInput.value = this.order.amount_due.toFixed(2);
|
||||
hint.textContent = '';
|
||||
}
|
||||
}
|
||||
|
||||
addPayment() {
|
||||
if (!this.selectedMethod) {
|
||||
alert('Выберите способ оплаты');
|
||||
return;
|
||||
}
|
||||
|
||||
const amountInput = document.getElementById(`${this.containerId}-amount`);
|
||||
const notesInput = document.getElementById(`${this.containerId}-notes`);
|
||||
|
||||
const amount = parseFloat(amountInput.value);
|
||||
|
||||
if (amount <= 0 || isNaN(amount)) {
|
||||
alert('Введите сумму больше 0');
|
||||
return;
|
||||
}
|
||||
|
||||
// Проверка не превышает ли общая сумма amount_due
|
||||
const currentTotal = this.getTotalPayments();
|
||||
if (currentTotal + amount > this.order.amount_due) {
|
||||
alert(`Общая сумма платежей (${(currentTotal + amount).toFixed(2)}) превышает сумму к оплате (${this.order.amount_due.toFixed(2)})`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Добавляем в список
|
||||
this.payments.push({
|
||||
method_id: this.selectedMethod.id,
|
||||
method_code: this.selectedMethod.code,
|
||||
method_name: this.selectedMethod.name,
|
||||
amount: amount,
|
||||
notes: notesInput.value.trim()
|
||||
});
|
||||
|
||||
// Обновляем UI
|
||||
this.updatePaymentsList();
|
||||
|
||||
// Сбрасываем форму
|
||||
const remaining = this.order.amount_due - this.getTotalPayments();
|
||||
amountInput.value = remaining > 0 ? remaining.toFixed(2) : '0.00';
|
||||
notesInput.value = '';
|
||||
|
||||
// Снимаем выделение способа
|
||||
document.querySelectorAll(`#${this.containerId}-methods .payment-method-btn`).forEach(b => {
|
||||
b.classList.remove('active');
|
||||
});
|
||||
this.selectedMethod = null;
|
||||
|
||||
const hint = document.getElementById(`${this.containerId}-hint`);
|
||||
hint.textContent = remaining > 0 ? `Осталось оплатить: ${remaining.toFixed(2)} руб.` : 'Оплачено полностью';
|
||||
}
|
||||
|
||||
removePayment(index) {
|
||||
this.payments.splice(index, 1);
|
||||
this.updatePaymentsList();
|
||||
|
||||
// Обновляем подсказку
|
||||
const amountInput = document.getElementById(`${this.containerId}-amount`);
|
||||
const remaining = this.order.amount_due - this.getTotalPayments();
|
||||
amountInput.value = remaining > 0 ? remaining.toFixed(2) : '0.00';
|
||||
|
||||
const hint = document.getElementById(`${this.containerId}-hint`);
|
||||
hint.textContent = remaining > 0 ? `Осталось оплатить: ${remaining.toFixed(2)} руб.` : '';
|
||||
}
|
||||
|
||||
updatePaymentsList() {
|
||||
const listContainer = document.getElementById(`${this.containerId}-payments-list`);
|
||||
if (!listContainer) return;
|
||||
|
||||
if (this.payments.length === 0) {
|
||||
listContainer.innerHTML = '<small class="text-muted">Платежи не добавлены</small>';
|
||||
return;
|
||||
}
|
||||
|
||||
listContainer.innerHTML = this.payments.map((p, index) => `
|
||||
<div class="d-flex justify-content-between align-items-center mb-1 pb-1 ${index < this.payments.length - 1 ? 'border-bottom' : ''}">
|
||||
<div style="font-size: 0.9rem;">
|
||||
<strong>${p.method_name}</strong>
|
||||
<span class="text-success ms-2">${p.amount.toFixed(2)} руб.</span>
|
||||
${p.notes ? `<br><small class="text-muted">${p.notes}</small>` : ''}
|
||||
</div>
|
||||
<button type="button"
|
||||
class="btn btn-sm btn-outline-danger remove-payment-btn"
|
||||
data-index="${index}"
|
||||
style="font-size: 0.75rem; padding: 0.1rem 0.3rem;">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
`).join('');
|
||||
|
||||
// Прикрепляем обработчики удаления
|
||||
listContainer.querySelectorAll('.remove-payment-btn').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
const index = parseInt(btn.dataset.index);
|
||||
this.removePayment(index);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
getTotalPayments() {
|
||||
return this.payments.reduce((sum, p) => sum + p.amount, 0);
|
||||
}
|
||||
|
||||
validate() {
|
||||
if (this.mode === 'single') {
|
||||
// Одиночная оплата
|
||||
if (!this.selectedMethod) {
|
||||
throw new Error('Выберите способ оплаты');
|
||||
}
|
||||
|
||||
const amountInput = document.getElementById(`${this.containerId}-amount`);
|
||||
const amount = parseFloat(amountInput.value);
|
||||
|
||||
if (amount <= 0 || isNaN(amount)) {
|
||||
throw new Error('Введите сумму больше 0');
|
||||
}
|
||||
|
||||
if (amount > this.order.amount_due) {
|
||||
throw new Error(`Сумма превышает остаток к оплате (${this.order.amount_due.toFixed(2)} руб.)`);
|
||||
}
|
||||
|
||||
// Проверка кошелька
|
||||
if (this.selectedMethod.code === 'account_balance' && this.customer) {
|
||||
if (amount > this.customer.wallet_balance) {
|
||||
throw new Error(`Недостаточно средств в кошельке (${this.customer.wallet_balance.toFixed(2)} руб.)`);
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
// Смешанная оплата
|
||||
if (this.payments.length === 0) {
|
||||
throw new Error('Добавьте хотя бы один платеж');
|
||||
}
|
||||
|
||||
const total = this.getTotalPayments();
|
||||
|
||||
if (total > this.order.amount_due) {
|
||||
throw new Error(`Сумма платежей превышает остаток к оплате (${this.order.amount_due.toFixed(2)} руб.)`);
|
||||
}
|
||||
|
||||
// Проверка кошелька
|
||||
if (this.customer) {
|
||||
const walletPayments = this.payments.filter(p => p.method_code === 'account_balance');
|
||||
const walletTotal = walletPayments.reduce((sum, p) => sum + p.amount, 0);
|
||||
|
||||
if (walletTotal > this.customer.wallet_balance) {
|
||||
throw new Error(`Недостаточно средств в кошельке (${this.customer.wallet_balance.toFixed(2)} руб.)`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
submit() {
|
||||
try {
|
||||
this.validate();
|
||||
|
||||
let paymentsData;
|
||||
|
||||
if (this.mode === 'single') {
|
||||
// Одиночная оплата
|
||||
const amountInput = document.getElementById(`${this.containerId}-amount`);
|
||||
const notesInput = document.getElementById(`${this.containerId}-notes`);
|
||||
|
||||
paymentsData = [{
|
||||
payment_method: this.selectedMethod.code,
|
||||
amount: parseFloat(amountInput.value),
|
||||
notes: notesInput.value.trim()
|
||||
}];
|
||||
} else {
|
||||
// Смешанная оплата
|
||||
paymentsData = this.payments.map(p => ({
|
||||
payment_method: p.method_code,
|
||||
amount: p.amount,
|
||||
notes: p.notes
|
||||
}));
|
||||
}
|
||||
|
||||
this.onSubmit(paymentsData);
|
||||
|
||||
} catch (error) {
|
||||
alert(error.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
30
myproject/orders/templatetags/payment_tags.py
Normal file
30
myproject/orders/templatetags/payment_tags.py
Normal file
@@ -0,0 +1,30 @@
|
||||
"""
|
||||
Template tags для работы с платежами.
|
||||
"""
|
||||
from django import template
|
||||
from orders.services.transaction_service import TransactionService
|
||||
|
||||
register = template.Library()
|
||||
|
||||
|
||||
@register.simple_tag
|
||||
def get_payment_methods_for_pos():
|
||||
"""
|
||||
Получить способы оплаты для POS (исключая legal_entity).
|
||||
Используется в шаблонах POS.
|
||||
"""
|
||||
return TransactionService.get_available_payment_methods(
|
||||
exclude_codes=['legal_entity'],
|
||||
only_active=True
|
||||
)
|
||||
|
||||
|
||||
@register.filter
|
||||
def format_currency(value):
|
||||
"""
|
||||
Форматирование валюты: 1234.50 -> '1 234.50 руб.'
|
||||
"""
|
||||
try:
|
||||
return f"{float(value):,.2f}".replace(',', ' ') + " руб."
|
||||
except (ValueError, TypeError):
|
||||
return "0.00 руб."
|
||||
@@ -1513,123 +1513,183 @@ function renderCheckoutModal() {
|
||||
|
||||
// Обновляем информацию о клиенте
|
||||
updateCustomerDisplay();
|
||||
|
||||
// Обновляем базовую цену и пересчитываем
|
||||
updateCheckoutPricing(total);
|
||||
}
|
||||
|
||||
// Пересчёт цен в модалке продажи
|
||||
function updateCheckoutPricing(basePrice = null) {
|
||||
// Если basePrice не передан, пересчитываем из корзины
|
||||
if (basePrice === null) {
|
||||
basePrice = 0;
|
||||
cart.forEach((item) => {
|
||||
basePrice += item.qty * item.price;
|
||||
});
|
||||
// ===== CHECKOUT: ПОДТВЕРЖДЕНИЕ ПРОДАЖИ =====
|
||||
|
||||
let paymentWidget = null;
|
||||
|
||||
// При открытии модалки checkout
|
||||
document.getElementById('checkoutModal').addEventListener('show.bs.modal', () => {
|
||||
const customer = selectedCustomer || SYSTEM_CUSTOMER;
|
||||
const walletBalance = customer.wallet_balance || 0;
|
||||
|
||||
// Показываем баланс кошелька
|
||||
const walletDiv = document.getElementById('checkoutWalletBalance');
|
||||
if (customer.id !== SYSTEM_CUSTOMER.id) {
|
||||
document.getElementById('checkoutWalletBalanceAmount').textContent = walletBalance.toFixed(2);
|
||||
walletDiv.style.display = 'block';
|
||||
} else {
|
||||
walletDiv.style.display = 'none';
|
||||
}
|
||||
|
||||
// Базовая сумма
|
||||
document.getElementById('checkoutBasePrice').textContent = formatMoney(basePrice) + ' руб.';
|
||||
|
||||
// Скидка
|
||||
const discountType = document.getElementById('discountType').value;
|
||||
const discountValue = parseFloat(document.getElementById('discountValue').value) || 0;
|
||||
|
||||
let discountedPrice = basePrice;
|
||||
if (discountType !== 'none' && discountValue > 0) {
|
||||
if (discountType === 'percent') {
|
||||
discountedPrice = basePrice - (basePrice * discountValue / 100);
|
||||
} else if (discountType === 'amount') {
|
||||
discountedPrice = Math.max(0, basePrice - discountValue);
|
||||
|
||||
// Вычисляем итоговую сумму
|
||||
let totalAmount = 0;
|
||||
cart.forEach((item) => {
|
||||
totalAmount += item.qty * item.price;
|
||||
});
|
||||
|
||||
document.getElementById('checkoutFinalPrice').textContent = formatMoney(totalAmount) + ' руб.';
|
||||
|
||||
// Инициализируем виджет в single mode
|
||||
initPaymentWidget('single', {
|
||||
order: { total: totalAmount, amount_due: totalAmount, amount_paid: 0 },
|
||||
customer: { id: customer.id, name: customer.name, wallet_balance: walletBalance }
|
||||
});
|
||||
});
|
||||
|
||||
// Переключение режима оплаты
|
||||
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').classList.add('active');
|
||||
document.getElementById('singlePaymentMode').classList.remove('active');
|
||||
reinitPaymentWidget('mixed');
|
||||
});
|
||||
|
||||
function reinitPaymentWidget(mode) {
|
||||
const customer = selectedCustomer || SYSTEM_CUSTOMER;
|
||||
const totalAmountText = document.getElementById('checkoutFinalPrice').textContent;
|
||||
const totalAmount = parseFloat(totalAmountText.replace(/[^\d.]/g, ''));
|
||||
|
||||
initPaymentWidget(mode, {
|
||||
order: { total: totalAmount, amount_due: totalAmount, amount_paid: 0 },
|
||||
customer: { id: customer.id, name: customer.name, wallet_balance: customer.wallet_balance || 0 }
|
||||
});
|
||||
}
|
||||
|
||||
async function initPaymentWidget(mode, data) {
|
||||
const paymentMethods = [
|
||||
{ id: 1, code: 'account_balance', name: 'С баланса счёта' },
|
||||
{ id: 2, code: 'cash', name: 'Наличными' },
|
||||
{ id: 3, code: 'card', name: 'Картой' },
|
||||
{ id: 4, code: 'online', name: 'Онлайн' }
|
||||
];
|
||||
|
||||
// Динамически загружаем PaymentWidget если еще не загружен
|
||||
if (!window.PaymentWidget) {
|
||||
try {
|
||||
const module = await import('/static/orders/js/payment_widget.js');
|
||||
window.PaymentWidget = module.PaymentWidget;
|
||||
} catch (error) {
|
||||
console.error('Ошибка загрузки PaymentWidget:', error);
|
||||
alert('Ошибка загрузки модуля оплаты. Перезагрузите страницу.');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById('checkoutDiscountedPrice').textContent = formatMoney(discountedPrice) + ' руб.';
|
||||
|
||||
// Финальная цена (с учётом ручной суммы если задана)
|
||||
const useManualPrice = document.getElementById('useManualPrice').checked;
|
||||
const manualPrice = parseFloat(document.getElementById('manualPrice').value) || 0;
|
||||
|
||||
let finalPrice = discountedPrice;
|
||||
if (useManualPrice && manualPrice > 0) {
|
||||
finalPrice = manualPrice;
|
||||
}
|
||||
|
||||
document.getElementById('checkoutFinalPrice').textContent = formatMoney(finalPrice);
|
||||
|
||||
paymentWidget = new window.PaymentWidget({
|
||||
containerId: 'paymentWidgetContainer',
|
||||
mode: mode,
|
||||
order: data.order,
|
||||
customer: data.customer,
|
||||
paymentMethods: paymentMethods,
|
||||
onSubmit: (paymentsData) => handleCheckoutSubmit(paymentsData)
|
||||
});
|
||||
}
|
||||
|
||||
// Обработчики для полей скидки и цены
|
||||
document.getElementById('discountType').addEventListener('change', function() {
|
||||
const discountBlock = document.getElementById('discountValueBlock');
|
||||
if (this.value === 'none') {
|
||||
discountBlock.style.display = 'none';
|
||||
document.getElementById('discountValue').value = '0';
|
||||
} else {
|
||||
discountBlock.style.display = 'block';
|
||||
}
|
||||
updateCheckoutPricing();
|
||||
});
|
||||
|
||||
document.getElementById('discountValue').addEventListener('input', function() {
|
||||
updateCheckoutPricing();
|
||||
});
|
||||
|
||||
document.getElementById('useManualPrice').addEventListener('change', function() {
|
||||
const manualPriceBlock = document.getElementById('manualPriceBlock');
|
||||
if (this.checked) {
|
||||
manualPriceBlock.style.display = 'block';
|
||||
} else {
|
||||
manualPriceBlock.style.display = 'none';
|
||||
document.getElementById('manualPrice').value = '';
|
||||
}
|
||||
updateCheckoutPricing();
|
||||
});
|
||||
|
||||
document.getElementById('manualPrice').addEventListener('input', function() {
|
||||
updateCheckoutPricing();
|
||||
});
|
||||
|
||||
// Подтверждение продажи (пока заглушка)
|
||||
// Обработчик кнопки "Подтвердить продажу"
|
||||
document.getElementById('confirmCheckoutBtn').onclick = () => {
|
||||
const payment = document.getElementById('paymentMethod').value;
|
||||
const note = document.getElementById('orderNote').value.trim();
|
||||
|
||||
const paymentText = {
|
||||
'cash': 'Наличные',
|
||||
'card': 'Карта',
|
||||
'mixed': 'Смешанная оплата'
|
||||
}[payment] || payment;
|
||||
|
||||
// Получаем данные о ценах и скидке
|
||||
const basePrice = document.getElementById('checkoutBasePrice').textContent;
|
||||
const discountType = document.getElementById('discountType').value;
|
||||
const discountValue = document.getElementById('discountValue').value;
|
||||
const finalPrice = document.getElementById('checkoutFinalPrice').textContent;
|
||||
|
||||
let discountInfo = 'нет скидки';
|
||||
if (discountType === 'percent' && discountValue > 0) {
|
||||
discountInfo = `скидка ${discountValue}%`;
|
||||
} else if (discountType === 'amount' && discountValue > 0) {
|
||||
discountInfo = `скидка ${discountValue} руб.`;
|
||||
if (paymentWidget) {
|
||||
paymentWidget.submit();
|
||||
}
|
||||
|
||||
const useManual = document.getElementById('useManualPrice').checked;
|
||||
if (useManual) {
|
||||
discountInfo += ' (установлена своя сумма)';
|
||||
}
|
||||
|
||||
alert(`Функция проведения продажи будет подключена позже.
|
||||
|
||||
Базовая сумма: ${basePrice}
|
||||
Скидка: ${discountInfo}
|
||||
Итого к оплате: ${finalPrice} руб.
|
||||
Оплата: ${paymentText}
|
||||
Комментарий: ${note || '—'}`);
|
||||
|
||||
const modal = bootstrap.Modal.getInstance(document.getElementById('checkoutModal'));
|
||||
modal.hide();
|
||||
};
|
||||
|
||||
// Отправка заказа на сервер
|
||||
async function handleCheckoutSubmit(paymentsData) {
|
||||
try {
|
||||
// Блокируем кнопку
|
||||
const btn = document.getElementById('confirmCheckoutBtn');
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>Обработка...';
|
||||
|
||||
// Собираем данные
|
||||
const customer = selectedCustomer || SYSTEM_CUSTOMER;
|
||||
const orderData = {
|
||||
customer_id: customer.id,
|
||||
warehouse_id: currentWarehouse.id,
|
||||
items: Array.from(cart.values()).map(item => ({
|
||||
type: item.type,
|
||||
id: item.id,
|
||||
quantity: item.qty,
|
||||
price: item.price
|
||||
})),
|
||||
payments: paymentsData,
|
||||
notes: document.getElementById('orderNote').value.trim()
|
||||
};
|
||||
|
||||
// Отправляем на сервер
|
||||
const response = await fetch('/pos/api/checkout/', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': getCookie('csrftoken')
|
||||
},
|
||||
body: JSON.stringify(orderData)
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
// Успех
|
||||
alert(`Заказ #${result.order_number} успешно создан!\nСумма: ${result.total_amount.toFixed(2)} руб.`);
|
||||
|
||||
// Очищаем корзину
|
||||
cart.clear();
|
||||
updateCartUI();
|
||||
updateCartCount();
|
||||
|
||||
// Закрываем модалку
|
||||
const modal = bootstrap.Modal.getInstance(document.getElementById('checkoutModal'));
|
||||
modal.hide();
|
||||
|
||||
// Перезагружаем витринные комплекты
|
||||
loadShowcaseKits();
|
||||
|
||||
} else {
|
||||
alert('Ошибка: ' + result.error);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Ошибка checkout:', error);
|
||||
alert('Ошибка при проведении продажи: ' + error.message);
|
||||
} finally {
|
||||
// Разблокируем кнопку
|
||||
const btn = document.getElementById('confirmCheckoutBtn');
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = '<i class="bi bi-check2-circle"></i> Подтвердить продажу';
|
||||
}
|
||||
}
|
||||
|
||||
function getCookie(name) {
|
||||
let cookieValue = null;
|
||||
if (document.cookie && document.cookie !== '') {
|
||||
const cookies = document.cookie.split(';');
|
||||
for (let i = 0; i < cookies.length; i++) {
|
||||
const cookie = cookies[i].trim();
|
||||
if (cookie.substring(0, name.length + 1) === (name + '=')) {
|
||||
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return cookieValue;
|
||||
}
|
||||
|
||||
document.getElementById('scheduleLater').onclick = async () => {
|
||||
alert('Функционал будет подключен позже: создание заказа на доставку/самовывоз.');
|
||||
};
|
||||
|
||||
@@ -274,121 +274,96 @@
|
||||
|
||||
<!-- Модалка: Продажа -->
|
||||
<div class="modal fade" id="checkoutModal" tabindex="-1" aria-labelledby="checkoutModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<div class="modal-dialog modal-dialog-centered modal-lg" style="max-width: 70vw;">
|
||||
<div class="modal-content" style="max-height: 90vh; overflow: hidden;">
|
||||
<div class="modal-header py-2">
|
||||
<h5 class="modal-title" id="checkoutModalLabel">
|
||||
<i class="bi bi-cash-stack"></i> Продажа
|
||||
</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="row">
|
||||
<div class="modal-body py-2" style="overflow-y: auto;">
|
||||
<div class="row g-3">
|
||||
<!-- Левая колонка: состав заказа -->
|
||||
<div class="col-md-7">
|
||||
<!-- Информация о клиенте -->
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-semibold">Клиент</label>
|
||||
<div class="d-flex gap-1 align-items-center">
|
||||
<button class="btn btn-outline-primary btn-sm d-flex align-items-center" id="checkoutCustomerSelectBtn">
|
||||
<i class="bi bi-person me-1"></i>
|
||||
<div class="d-flex flex-column align-items-start lh-1">
|
||||
<small class="text-muted" style="font-size: 0.65rem;">Клиент</small>
|
||||
<span id="checkoutCustomerSelectBtnText" class="fw-semibold">Выбрать</span>
|
||||
</div>
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outline-danger" id="checkoutResetCustomerBtn" title="Сброс на системного клиента" style="display: none;">
|
||||
<i class="bi bi-x-lg"></i>
|
||||
</button>
|
||||
<!-- Информация о клиенте и баланс в одной строке -->
|
||||
<div class="row g-2 mb-2">
|
||||
<div class="col-auto">
|
||||
<label class="form-label fw-semibold small mb-1">Клиент</label>
|
||||
<div class="d-flex gap-1">
|
||||
<button class="btn btn-outline-primary btn-sm" id="checkoutCustomerSelectBtn" style="font-size: 0.85rem; padding: 0.25rem 0.5rem;">
|
||||
<i class="bi bi-person me-1"></i>
|
||||
<span id="checkoutCustomerSelectBtnText">Выбрать</span>
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outline-danger" id="checkoutResetCustomerBtn" title="Сброс" style="display: none; font-size: 0.85rem; padding: 0.25rem 0.5rem;">
|
||||
<i class="bi bi-x-lg"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col" id="checkoutWalletBalance" style="display: none;">
|
||||
<label class="form-label fw-semibold small mb-1">Баланс кошелька</label>
|
||||
<div class="alert alert-info py-1 px-2 mb-0" style="font-size: 0.9rem;">
|
||||
<i class="bi bi-wallet2"></i>
|
||||
<span id="checkoutWalletBalanceAmount">0.00</span> руб.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<strong>Состав заказа</strong>
|
||||
<div class="border rounded p-3 mt-2" id="checkoutItems" style="max-height: 240px; overflow-y: auto; background: #f8f9fa;">
|
||||
<!-- Состав заказа -->
|
||||
<div class="mb-2">
|
||||
<strong class="small">Состав заказа</strong>
|
||||
<div class="border rounded p-2 mt-1" id="checkoutItems" style="max-height: 180px; overflow-y: auto; background: #f8f9fa; font-size: 0.9rem;">
|
||||
<!-- Заполняется из JS -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Опции оплаты и комментарий -->
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label for="paymentMethod" class="form-label">Способ оплаты</label>
|
||||
<select class="form-select" id="paymentMethod">
|
||||
<option value="cash">Наличные</option>
|
||||
<option value="card">Карта</option>
|
||||
<option value="mixed">Смешанная оплата</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label for="orderNote" class="form-label">Комментарий</label>
|
||||
<input type="text" class="form-control" id="orderNote" placeholder="Примечание к заказу">
|
||||
</div>
|
||||
<!-- Комментарий к заказу -->
|
||||
<div class="mb-2">
|
||||
<label for="orderNote" class="form-label small mb-1">Комментарий</label>
|
||||
<input type="text" class="form-control form-control-sm" id="orderNote" placeholder="Примечание к заказу">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Правая колонка: ценообразование -->
|
||||
|
||||
<!-- Правая колонка: оплата -->
|
||||
<div class="col-md-5">
|
||||
<div class="card">
|
||||
<div class="card-header bg-light">
|
||||
<strong>Ценообразование</strong>
|
||||
<div class="card mb-0">
|
||||
<div class="card-header bg-light py-2">
|
||||
<strong class="small">Оплата</strong>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<!-- Базовая сумма -->
|
||||
<div class="mb-2">
|
||||
<small class="text-muted">Базовая сумма (корзина):</small>
|
||||
<div class="fw-bold" id="checkoutBasePrice">0.00 руб.</div>
|
||||
</div>
|
||||
|
||||
<!-- Скидка -->
|
||||
<div class="mb-2">
|
||||
<label for="discountType" class="form-label small">Скидка</label>
|
||||
<select class="form-select form-select-sm" id="discountType">
|
||||
<option value="none">Без скидки</option>
|
||||
<option value="percent">Процент (%)</option>
|
||||
<option value="amount">Сумма (руб.)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="mb-2" id="discountValueBlock" style="display: none;">
|
||||
<label for="discountValue" class="form-label small">Значение скидки</label>
|
||||
<input type="number" class="form-control form-control-sm" id="discountValue"
|
||||
min="0" step="0.01" value="0" placeholder="0.00">
|
||||
</div>
|
||||
|
||||
<!-- Расчётная сумма со скидкой -->
|
||||
<div class="card-body p-2">
|
||||
<!-- Итого к оплате -->
|
||||
<div class="mb-2 pb-2 border-bottom">
|
||||
<small class="text-muted">Сумма со скидкой:</small>
|
||||
<div class="fw-bold text-success" id="checkoutDiscountedPrice">0.00 руб.</div>
|
||||
<small class="text-muted">Итого к оплате:</small>
|
||||
<div class="fw-bold text-success fs-5" id="checkoutFinalPrice">0.00 руб.</div>
|
||||
</div>
|
||||
|
||||
<!-- Ручная финальная сумма -->
|
||||
<div class="mb-0">
|
||||
<div class="form-check form-switch mb-2">
|
||||
<input class="form-check-input" type="checkbox" id="useManualPrice">
|
||||
<label class="form-check-label small" for="useManualPrice">
|
||||
Установить свою сумму (приоритет)
|
||||
</label>
|
||||
</div>
|
||||
<div id="manualPriceBlock" style="display: none;">
|
||||
<input type="number" class="form-control form-control-sm" id="manualPrice"
|
||||
min="0" step="0.01" placeholder="Введите сумму">
|
||||
|
||||
<!-- Переключатель режима -->
|
||||
<div class="mb-2">
|
||||
<label class="form-label fw-semibold small mb-1">Режим оплаты</label>
|
||||
<div class="btn-group w-100" role="group">
|
||||
<button type="button"
|
||||
class="btn btn-sm btn-outline-primary active"
|
||||
id="singlePaymentMode">
|
||||
Одним способом
|
||||
</button>
|
||||
<button type="button"
|
||||
class="btn btn-sm btn-outline-primary"
|
||||
id="mixedPaymentMode">
|
||||
Смешанная
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Итоговая сумма продажи -->
|
||||
<div class="alert alert-success mt-3 mb-0">
|
||||
<strong>Итого к оплате:</strong><br>
|
||||
<span class="fs-3" id="checkoutFinalPrice">0.00</span> руб.
|
||||
</div>
|
||||
|
||||
<!-- Контейнер для PaymentWidget -->
|
||||
<div id="paymentWidgetContainer"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<div class="modal-footer py-2">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Отмена</button>
|
||||
<button type="button" class="btn btn-success btn-lg" id="confirmCheckoutBtn">
|
||||
<button type="button" class="btn btn-success" id="confirmCheckoutBtn">
|
||||
<i class="bi bi-check2-circle"></i> Подтвердить продажу
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -31,4 +31,6 @@ urlpatterns = [
|
||||
path('api/product-kits/<int:kit_id>/disassemble/', views.disassemble_product_kit, name='disassemble-product-kit'),
|
||||
# Создать временный комплект и зарезервировать на витрину [POST]
|
||||
path('api/create-temp-kit/', views.create_temp_kit_to_showcase, name='create-temp-kit-api'),
|
||||
# Создать заказ и провести оплату в POS [POST]
|
||||
path('api/checkout/', views.pos_checkout, name='pos-checkout'),
|
||||
]
|
||||
@@ -1240,3 +1240,167 @@ def disassemble_product_kit(request, kit_id):
|
||||
'success': False,
|
||||
'error': f'Ошибка при разборе: {str(e)}'
|
||||
}, status=500)
|
||||
|
||||
|
||||
@login_required
|
||||
@require_http_methods(["POST"])
|
||||
def pos_checkout(request):
|
||||
"""
|
||||
Создать заказ и провести оплату в POS-терминале.
|
||||
|
||||
Payload (JSON):
|
||||
{
|
||||
"customer_id": int,
|
||||
"warehouse_id": int,
|
||||
"items": [
|
||||
{"type": "product"|"kit"|"showcase_kit", "id": int, "quantity": float, "price": float},
|
||||
...
|
||||
],
|
||||
"payments": [
|
||||
{"payment_method": "cash"|"card"|"online"|"account_balance", "amount": float, "notes": str},
|
||||
...
|
||||
],
|
||||
"notes": str (optional)
|
||||
}
|
||||
"""
|
||||
from orders.models import Order, OrderItem, OrderStatus
|
||||
from orders.services.transaction_service import TransactionService
|
||||
from customers.models import Customer
|
||||
from products.models import Product, ProductKit
|
||||
from inventory.models import Warehouse, Reservation
|
||||
from django.db import transaction as db_transaction
|
||||
from decimal import Decimal
|
||||
import json
|
||||
|
||||
try:
|
||||
body = json.loads(request.body)
|
||||
|
||||
# Валидация
|
||||
customer_id = body.get('customer_id')
|
||||
warehouse_id = body.get('warehouse_id')
|
||||
items_data = body.get('items', [])
|
||||
payments_data = body.get('payments', [])
|
||||
order_notes = body.get('notes', '')
|
||||
|
||||
if not customer_id:
|
||||
return JsonResponse({'success': False, 'error': 'Не указан клиент'}, status=400)
|
||||
if not warehouse_id:
|
||||
return JsonResponse({'success': False, 'error': 'Не указан склад'}, status=400)
|
||||
if not items_data:
|
||||
return JsonResponse({'success': False, 'error': 'Корзина пуста'}, status=400)
|
||||
if not payments_data:
|
||||
return JsonResponse({'success': False, 'error': 'Не указаны способы оплаты'}, status=400)
|
||||
|
||||
# Получаем объекты
|
||||
customer = get_object_or_404(Customer, id=customer_id)
|
||||
warehouse = get_object_or_404(Warehouse, id=warehouse_id, is_active=True)
|
||||
|
||||
try:
|
||||
completed_status = OrderStatus.objects.get(code='completed', is_system=True)
|
||||
except OrderStatus.DoesNotExist:
|
||||
return JsonResponse({
|
||||
'success': False,
|
||||
'error': 'Статус "Выполнен" не найден в системе'
|
||||
}, status=500)
|
||||
|
||||
# Атомарная операция
|
||||
with db_transaction.atomic():
|
||||
# 1. Создаём заказ
|
||||
order = Order.objects.create(
|
||||
customer=customer,
|
||||
is_delivery=False, # POS - всегда самовывоз
|
||||
pickup_warehouse=warehouse,
|
||||
status=completed_status, # Сразу "Выполнен"
|
||||
special_instructions=order_notes,
|
||||
modified_by=request.user
|
||||
)
|
||||
|
||||
# 2. Добавляем товары
|
||||
for item_data in items_data:
|
||||
item_type = item_data['type']
|
||||
item_id = item_data['id']
|
||||
quantity = Decimal(str(item_data['quantity']))
|
||||
price = Decimal(str(item_data['price']))
|
||||
|
||||
if item_type == 'product':
|
||||
product = Product.objects.get(id=item_id)
|
||||
OrderItem.objects.create(
|
||||
order=order,
|
||||
product=product,
|
||||
quantity=quantity,
|
||||
price=price,
|
||||
is_custom_price=False
|
||||
)
|
||||
elif item_type in ['kit', 'showcase_kit']:
|
||||
kit = ProductKit.objects.get(id=item_id)
|
||||
OrderItem.objects.create(
|
||||
order=order,
|
||||
product_kit=kit,
|
||||
quantity=quantity,
|
||||
price=price,
|
||||
is_custom_price=False
|
||||
)
|
||||
|
||||
# 3. Пересчитываем итоговую стоимость
|
||||
order.calculate_total()
|
||||
|
||||
# 4. Проводим платежи
|
||||
payments_list = []
|
||||
for payment_data in payments_data:
|
||||
payments_list.append({
|
||||
'payment_method': payment_data['payment_method'],
|
||||
'amount': Decimal(str(payment_data['amount'])),
|
||||
'notes': payment_data.get('notes', f"Оплата POS: {payment_data['payment_method']}")
|
||||
})
|
||||
|
||||
transactions = TransactionService.create_multiple_payments(
|
||||
order=order,
|
||||
payments_list=payments_list,
|
||||
user=request.user
|
||||
)
|
||||
|
||||
# 5. Обновляем статус оплаты
|
||||
order.update_payment_status()
|
||||
|
||||
# 6. Освобождаем блокировки витринных комплектов
|
||||
showcase_kit_ids = [
|
||||
item_data['id'] for item_data in items_data
|
||||
if item_data['type'] == 'showcase_kit'
|
||||
]
|
||||
|
||||
if showcase_kit_ids:
|
||||
Reservation.objects.filter(
|
||||
product_kit_id__in=showcase_kit_ids,
|
||||
locked_by_user=request.user,
|
||||
status='reserved'
|
||||
).update(
|
||||
cart_lock_expires_at=None,
|
||||
locked_by_user=None,
|
||||
cart_session_id=None
|
||||
)
|
||||
|
||||
# 7. Очищаем корзину из Redis
|
||||
from django.core.cache import cache
|
||||
cart_key = f'pos:cart:{request.user.id}:{warehouse_id}'
|
||||
cache.delete(cart_key)
|
||||
|
||||
return JsonResponse({
|
||||
'success': True,
|
||||
'order_number': order.order_number,
|
||||
'order_id': order.id,
|
||||
'total_amount': float(order.total_amount),
|
||||
'amount_paid': float(order.amount_paid),
|
||||
'amount_due': float(order.amount_due),
|
||||
'payments_count': len(transactions),
|
||||
'message': f'Заказ #{order.order_number} успешно создан и оплачен'
|
||||
})
|
||||
|
||||
except (Customer.DoesNotExist, Warehouse.DoesNotExist, Product.DoesNotExist, ProductKit.DoesNotExist) as e:
|
||||
return JsonResponse({'success': False, 'error': 'Объект не найден'}, status=404)
|
||||
except ValidationError as e:
|
||||
return JsonResponse({'success': False, 'error': str(e)}, status=400)
|
||||
except json.JSONDecodeError:
|
||||
return JsonResponse({'success': False, 'error': 'Неверный формат JSON'}, status=400)
|
||||
except Exception as e:
|
||||
logger.error(f'Ошибка при проведении продажи POS: {str(e)}', exc_info=True)
|
||||
return JsonResponse({'success': False, 'error': f'Ошибка: {str(e)}'}, status=500)
|
||||
|
||||
Reference in New Issue
Block a user