From 4b7241bcfcbc9f7f5ab59102ebe33e79584bec32 Mon Sep 17 00:00:00 2001 From: Andrey Smakotin Date: Sat, 29 Nov 2025 18:09:40 +0300 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D1=8B=20=D0=BE=D0=BF=D0=B5=D1=80=D0=B0=D1=86=D0=B8=D0=B8?= =?UTF-8?q?=20=D1=81=20=D0=BA=D0=BE=D1=88=D0=B5=D0=BB=D1=8C=D0=BA=D0=BE?= =?UTF-8?q?=D0=BC=20=D0=BA=D0=BB=D0=B8=D0=B5=D0=BD=D1=82=D0=B0:=20=D0=BF?= =?UTF-8?q?=D0=BE=D0=BF=D0=BE=D0=BB=D0=BD=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=B8?= =?UTF-8?q?=20=D0=B2=D0=BE=D0=B7=D0=B2=D1=80=D0=B0=D1=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Добавлены view wallet_deposit и wallet_withdraw с защитой (login_required, is_staff, CSRF) - Новые маршруты: /customers//wallet/deposit/ и /customers//wallet/withdraw/ - UI на странице клиента: две симметричные формы для пополнения и списания баланса - Пополнение: произвольная сумма с обязательным описанием (подарки, компенсации) - Возврат/списание: с ограничением макс. суммы = текущий баланс, обязательное описание - Все операции логируются в WalletTransaction с типом 'adjustment' - Защита от операций с системным клиентом - Компактный симметричный дизайн форм с фиксированной высотой подсказок --- .../templates/customers/customer_detail.html | 76 +++++++++++++++++++ myproject/customers/urls.py | 2 + myproject/customers/views.py | 72 +++++++++++++++++- 3 files changed, 149 insertions(+), 1 deletion(-) diff --git a/myproject/customers/templates/customers/customer_detail.html b/myproject/customers/templates/customers/customer_detail.html index aa7ad78..41e3e1e 100644 --- a/myproject/customers/templates/customers/customer_detail.html +++ b/myproject/customers/templates/customers/customer_detail.html @@ -82,6 +82,82 @@ + +
+
+
+
Операции с кошельком клиента
+
+
+
+ +
+
Пополнение кошелька
+
+ {% csrf_token %} +
+ + +
+
+
+ + +
+ +
+
+ + +
+
Возврат / списание с кошелька
+
+ {% csrf_token %} +
+ + + Макс: {{ customer.wallet_balance|floatformat:2 }} руб. +
+
+ + +
+ +
+
+
+
+ Все операции с кошельком автоматически логируются в истории транзакций ниже. +
+
+
+
+
diff --git a/myproject/customers/urls.py b/myproject/customers/urls.py index 08b2b30..2fffff9 100644 --- a/myproject/customers/urls.py +++ b/myproject/customers/urls.py @@ -9,6 +9,8 @@ urlpatterns = [ path('/', views.customer_detail, name='customer-detail'), path('/edit/', views.customer_update, name='customer-update'), path('/delete/', views.customer_delete, name='customer-delete'), + path('/wallet/deposit/', views.wallet_deposit, name='wallet-deposit'), + path('/wallet/withdraw/', views.wallet_withdraw, name='wallet-withdraw'), # AJAX API endpoints path('api/search/', views.api_search_customers, name='api-search-customers'), diff --git a/myproject/customers/views.py b/myproject/customers/views.py index b271983..f241d2b 100644 --- a/myproject/customers/views.py +++ b/myproject/customers/views.py @@ -1,13 +1,14 @@ from django.shortcuts import render, get_object_or_404, redirect from django.contrib import messages from django.core.paginator import Paginator -from django.core.exceptions import ValidationError +from django.core.exceptions import ValidationError, PermissionDenied from django.db.models import Q from django.http import JsonResponse from django.views.decorators.http import require_http_methods from django.contrib.auth.decorators import login_required import phonenumbers import json +from decimal import Decimal from .models import Customer from .forms import CustomerForm @@ -484,3 +485,72 @@ def api_create_customer(request): 'success': False, 'error': f'Ошибка сервера: {str(e)}' }, status=500) + + +@login_required +@require_http_methods(["POST"]) +def wallet_deposit(request, pk): + """Пополнение кошелька клиента""" + if not request.user.is_staff: + raise PermissionDenied("У вас нет прав для изменения баланса кошелька клиента.") + + customer = get_object_or_404(Customer, pk=pk) + + if customer.is_system_customer: + messages.error(request, 'Операции с кошельком недоступны для системного клиента.') + return redirect('customers:customer-detail', pk=pk) + + amount_str = request.POST.get('amount') or '' + description = (request.POST.get('description') or '').strip() + + try: + amount = Decimal(amount_str.replace(',', '.')) + except Exception: + messages.error(request, 'Некорректное значение суммы для пополнения.') + return redirect('customers:customer-detail', pk=pk) + + try: + customer.adjust_wallet(amount, description, request.user) + messages.success(request, f'Кошелёк клиента пополнен на {amount:.2f} руб.') + except ValueError as e: + messages.error(request, str(e)) + except ValidationError as e: + messages.error(request, '; '.join(e.messages) if hasattr(e, 'messages') else str(e)) + + return redirect('customers:customer-detail', pk=pk) + + +@login_required +@require_http_methods(["POST"]) +def wallet_withdraw(request, pk): + """Возврат / списание с кошелька клиента""" + if not request.user.is_staff: + raise PermissionDenied("У вас нет прав для изменения баланса кошелька клиента.") + + customer = get_object_or_404(Customer, pk=pk) + + if customer.is_system_customer: + messages.error(request, 'Операции с кошельком недоступны для системного клиента.') + return redirect('customers:customer-detail', pk=pk) + + amount_str = request.POST.get('amount') or '' + description = (request.POST.get('description') or '').strip() + + try: + amount = Decimal(amount_str.replace(',', '.')) + except Exception: + messages.error(request, 'Некорректное значение суммы для списания.') + return redirect('customers:customer-detail', pk=pk) + + # Для списания делаем сумму отрицательной + withdraw_amount = -amount + + try: + customer.adjust_wallet(withdraw_amount, description, request.user) + messages.success(request, f'С кошелька клиента списано {amount:.2f} руб.') + except ValueError as e: + messages.error(request, str(e)) + except ValidationError as e: + messages.error(request, '; '.join(e.messages) if hasattr(e, 'messages') else str(e)) + + return redirect('customers:customer-detail', pk=pk)