Реализована полноценная система оплаты для 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:
2025-12-03 15:38:35 +03:00
parent 9dab280def
commit 1cda9086d0
7 changed files with 865 additions and 193 deletions

View File

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