Добавлены операции с кошельком клиента: пополнение и возврат
- Добавлены view wallet_deposit и wallet_withdraw с защитой (login_required, is_staff, CSRF) - Новые маршруты: /customers/<pk>/wallet/deposit/ и /customers/<pk>/wallet/withdraw/ - UI на странице клиента: две симметричные формы для пополнения и списания баланса - Пополнение: произвольная сумма с обязательным описанием (подарки, компенсации) - Возврат/списание: с ограничением макс. суммы = текущий баланс, обязательное описание - Все операции логируются в WalletTransaction с типом 'adjustment' - Защита от операций с системным клиентом - Компактный симметричный дизайн форм с фиксированной высотой подсказок
This commit is contained in:
@@ -82,6 +82,82 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Операции с кошельком -->
|
||||||
|
<div class="col-md-12">
|
||||||
|
<div class="card mb-4">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5>Операции с кошельком клиента</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row">
|
||||||
|
<!-- Пополнение -->
|
||||||
|
<div class="col-md-6">
|
||||||
|
<h6 class="text-success mb-3"><i class="bi bi-plus-circle"></i> Пополнение кошелька</h6>
|
||||||
|
<form method="post" action="{% url 'customers:wallet-deposit' customer.pk %}">
|
||||||
|
{% csrf_token %}
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="wallet_deposit_amount" class="form-label">Сумма, руб.</label>
|
||||||
|
<input type="number"
|
||||||
|
step="0.01"
|
||||||
|
min="0.01"
|
||||||
|
class="form-control"
|
||||||
|
id="wallet_deposit_amount"
|
||||||
|
name="amount"
|
||||||
|
placeholder="0.00"
|
||||||
|
required>
|
||||||
|
<div style="height: 1.25rem;"></div>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="wallet_deposit_description" class="form-label">Описание (обязательно)</label>
|
||||||
|
<textarea class="form-control"
|
||||||
|
id="wallet_deposit_description"
|
||||||
|
name="description"
|
||||||
|
rows="2"
|
||||||
|
placeholder="Например: Подарок, компенсация за некачественный товар"
|
||||||
|
required></textarea>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-success w-100"><i class="bi bi-plus-circle"></i> Пополнить кошелёк</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Возврат / списание -->
|
||||||
|
<div class="col-md-6">
|
||||||
|
<h6 class="text-danger mb-3"><i class="bi bi-dash-circle"></i> Возврат / списание с кошелька</h6>
|
||||||
|
<form method="post" action="{% url 'customers:wallet-withdraw' customer.pk %}">
|
||||||
|
{% csrf_token %}
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="wallet_withdraw_amount" class="form-label">Сумма, руб.</label>
|
||||||
|
<input type="number"
|
||||||
|
step="0.01"
|
||||||
|
min="0.01"
|
||||||
|
max="{{ customer.wallet_balance }}"
|
||||||
|
class="form-control"
|
||||||
|
id="wallet_withdraw_amount"
|
||||||
|
name="amount"
|
||||||
|
placeholder="0.00"
|
||||||
|
required>
|
||||||
|
<small class="text-muted d-block" style="height: 1.25rem; line-height: 1.25rem;">Макс: {{ customer.wallet_balance|floatformat:2 }} руб.</small>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="wallet_withdraw_description" class="form-label">Описание (обязательно)</label>
|
||||||
|
<textarea class="form-control"
|
||||||
|
id="wallet_withdraw_description"
|
||||||
|
name="description"
|
||||||
|
rows="2"
|
||||||
|
placeholder="Например: Возврат наличными, перевод на карту клиента"
|
||||||
|
required></textarea>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-danger w-100"><i class="bi bi-dash-circle"></i> Списать с кошелька</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="alert alert-info mb-0 mt-3">
|
||||||
|
<i class="bi bi-info-circle"></i> Все операции с кошельком автоматически логируются в истории транзакций ниже.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- История транзакций кошелька -->
|
<!-- История транзакций кошелька -->
|
||||||
<div class="col-md-12">
|
<div class="col-md-12">
|
||||||
<div class="card mb-4">
|
<div class="card mb-4">
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ urlpatterns = [
|
|||||||
path('<int:pk>/', views.customer_detail, name='customer-detail'),
|
path('<int:pk>/', views.customer_detail, name='customer-detail'),
|
||||||
path('<int:pk>/edit/', views.customer_update, name='customer-update'),
|
path('<int:pk>/edit/', views.customer_update, name='customer-update'),
|
||||||
path('<int:pk>/delete/', views.customer_delete, name='customer-delete'),
|
path('<int:pk>/delete/', views.customer_delete, name='customer-delete'),
|
||||||
|
path('<int:pk>/wallet/deposit/', views.wallet_deposit, name='wallet-deposit'),
|
||||||
|
path('<int:pk>/wallet/withdraw/', views.wallet_withdraw, name='wallet-withdraw'),
|
||||||
|
|
||||||
# AJAX API endpoints
|
# AJAX API endpoints
|
||||||
path('api/search/', views.api_search_customers, name='api-search-customers'),
|
path('api/search/', views.api_search_customers, name='api-search-customers'),
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
from django.shortcuts import render, get_object_or_404, redirect
|
from django.shortcuts import render, get_object_or_404, redirect
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.core.paginator import Paginator
|
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.db.models import Q
|
||||||
from django.http import JsonResponse
|
from django.http import JsonResponse
|
||||||
from django.views.decorators.http import require_http_methods
|
from django.views.decorators.http import require_http_methods
|
||||||
from django.contrib.auth.decorators import login_required
|
from django.contrib.auth.decorators import login_required
|
||||||
import phonenumbers
|
import phonenumbers
|
||||||
import json
|
import json
|
||||||
|
from decimal import Decimal
|
||||||
from .models import Customer
|
from .models import Customer
|
||||||
from .forms import CustomerForm
|
from .forms import CustomerForm
|
||||||
|
|
||||||
@@ -484,3 +485,72 @@ def api_create_customer(request):
|
|||||||
'success': False,
|
'success': False,
|
||||||
'error': f'Ошибка сервера: {str(e)}'
|
'error': f'Ошибка сервера: {str(e)}'
|
||||||
}, status=500)
|
}, 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)
|
||||||
|
|||||||
Reference in New Issue
Block a user