Реализована полноценная система оплаты для 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:
@@ -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