Реализация системы кошелька клиента для переплат
- Добавлено поле wallet_balance в модель Customer - Создана модель WalletTransaction для истории операций - Реализован сервис WalletService с методами: * add_overpayment - автоматическое зачисление переплаты * pay_with_wallet - оплата заказа из кошелька * adjust_balance - ручная корректировка баланса - Интеграция с Payment.save() для автоматической обработки переплат - UI для оплаты из кошелька в деталях заказа - Отображение баланса и долга на странице клиента - Админка с inline транзакций и запретом ручного создания - Добавлен способ оплаты account_balance - Миграция 0004 для customers приложения
This commit is contained in:
@@ -8,6 +8,13 @@ class Command(BaseCommand):
|
||||
|
||||
def handle(self, *args, **options):
|
||||
payment_methods = [
|
||||
{
|
||||
'code': 'account_balance',
|
||||
'name': 'С баланса счёта',
|
||||
'description': 'Оплата из кошелька клиента',
|
||||
'is_system': True,
|
||||
'order': 0
|
||||
},
|
||||
{
|
||||
'code': 'cash',
|
||||
'name': 'Наличными',
|
||||
|
||||
@@ -142,3 +142,11 @@ class Payment(models.Model):
|
||||
# Пересчитываем общую сумму оплаты в заказе
|
||||
self.order.amount_paid = sum(p.amount for p in self.order.payments.all())
|
||||
self.order.update_payment_status()
|
||||
|
||||
# Нормализация переплаты: лишнее в кошелёк, amount_paid = total_amount
|
||||
try:
|
||||
from customers.services.wallet_service import WalletService
|
||||
WalletService.add_overpayment(self.order, self.created_by)
|
||||
except Exception:
|
||||
# Если обработка переплаты не удалась, продолжаем без ошибок
|
||||
pass
|
||||
|
||||
@@ -236,6 +236,64 @@
|
||||
|
||||
<!-- Правая колонка -->
|
||||
<div class="col-md-4">
|
||||
<!-- Кошелёк клиента -->
|
||||
{% if order.customer and order.customer.wallet_balance > 0 and order.amount_due > 0 %}
|
||||
<div class="card mb-3 border-success">
|
||||
<div class="card-header bg-success text-white">
|
||||
<h5 class="mb-0">Кошелёк клиента</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="mb-2">
|
||||
<strong>Баланс:</strong>
|
||||
<span class="h5 text-success">{{ order.customer.wallet_balance|floatformat:2 }} руб.</span>
|
||||
</p>
|
||||
<p class="text-muted small">Можно использовать для оплаты этого заказа</p>
|
||||
|
||||
<!-- Кнопка "Применить максимум" -->
|
||||
<form method="post" action="{% url 'orders:apply-wallet' order.pk %}" class="mb-2">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="wallet_amount" value="{% if order.customer.wallet_balance < order.amount_due %}{{ order.customer.wallet_balance|floatformat:2 }}{% else %}{{ order.amount_due|floatformat:2 }}{% endif %}">
|
||||
<button type="submit" class="btn btn-success w-100">
|
||||
<i class="bi bi-wallet2"></i> Применить максимум
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<!-- Ручной ввод суммы -->
|
||||
<form method="post" action="{% url 'orders:apply-wallet' order.pk %}">
|
||||
{% csrf_token %}
|
||||
<div class="input-group">
|
||||
<input
|
||||
type="number"
|
||||
step="0.01"
|
||||
min="0"
|
||||
max="{% if order.customer.wallet_balance < order.amount_due %}{{ order.customer.wallet_balance|floatformat:2 }}{% else %}{{ order.amount_due|floatformat:2 }}{% endif %}"
|
||||
name="wallet_amount"
|
||||
class="form-control"
|
||||
placeholder="Сумма"
|
||||
>
|
||||
<button type="submit" class="btn btn-outline-success">
|
||||
Оплатить
|
||||
</button>
|
||||
</div>
|
||||
<small class="text-muted">Введите сумму для списания из кошелька</small>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% elif order.customer and order.customer.wallet_balance > 0 %}
|
||||
<div class="card mb-3">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">Кошелёк клиента</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="mb-0">
|
||||
<strong>Баланс:</strong>
|
||||
<span class="h5 text-success">{{ order.customer.wallet_balance|floatformat:2 }} руб.</span>
|
||||
</p>
|
||||
<small class="text-muted">Заказ уже оплачен полностью</small>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Оплата -->
|
||||
<div class="card mb-3">
|
||||
<div class="card-header">
|
||||
|
||||
@@ -16,6 +16,9 @@ urlpatterns = [
|
||||
path('create-draft/', views.create_draft_from_form, name='order-create-draft'),
|
||||
path('api/customer-address-history/', views.get_customer_address_history, name='api-customer-address-history'),
|
||||
|
||||
# Wallet payment
|
||||
path('<int:pk>/apply-wallet/', views.apply_wallet_payment, name='apply-wallet'),
|
||||
|
||||
# Order Status Management URLs
|
||||
path('statuses/', views.order_status_list, name='status_list'),
|
||||
path('statuses/create/', views.order_status_create, name='status_create'),
|
||||
|
||||
@@ -7,6 +7,7 @@ from django.views.decorators.http import require_http_methods
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import models
|
||||
from decimal import Decimal
|
||||
from .models import Order, OrderItem, Address, OrderStatus
|
||||
from .forms import OrderForm, OrderItemFormSet, OrderStatusForm, PaymentFormSet
|
||||
from .filters import OrderFilter
|
||||
@@ -604,3 +605,47 @@ def order_status_delete(request, pk):
|
||||
# === ВРЕМЕННЫЕ КОМПЛЕКТЫ ===
|
||||
# УДАЛЕНО: Логика создания временных комплектов перенесена в products.services.kit_service
|
||||
# Используйте API endpoint: products:api-temporary-kit-create
|
||||
|
||||
|
||||
# === КОШЕЛЁК КЛИЕНТА ===
|
||||
|
||||
@login_required
|
||||
def apply_wallet_payment(request, pk):
|
||||
"""
|
||||
Применение оплаты из кошелька клиента к заказу.
|
||||
Вызывается через POST-запрос с суммой для списания.
|
||||
"""
|
||||
if request.method != 'POST':
|
||||
return redirect('orders:order-detail', pk=pk)
|
||||
|
||||
order = get_object_or_404(Order, pk=pk)
|
||||
|
||||
# Получаем запрашиваемую сумму из формы
|
||||
try:
|
||||
raw_amount = request.POST.get('wallet_amount', '0')
|
||||
amount = Decimal(str(raw_amount).replace(',', '.'))
|
||||
except (ValueError, TypeError, ArithmeticError):
|
||||
messages.error(request, 'Некорректная сумма для списания из кошелька.')
|
||||
return redirect('orders:order-detail', pk=pk)
|
||||
|
||||
# Вызываем сервис для оплаты из кошелька
|
||||
try:
|
||||
from customers.services.wallet_service import WalletService
|
||||
paid_amount = WalletService.pay_with_wallet(order, amount, request.user)
|
||||
|
||||
if paid_amount and paid_amount > 0:
|
||||
messages.success(
|
||||
request,
|
||||
f'Из кошелька клиента списано {paid_amount} руб. для оплаты заказа #{order.order_number}.'
|
||||
)
|
||||
else:
|
||||
messages.warning(
|
||||
request,
|
||||
'Не удалось списать средства из кошелька. Проверьте баланс и сумму заказа.'
|
||||
)
|
||||
except ValueError as e:
|
||||
messages.error(request, str(e))
|
||||
except Exception as e:
|
||||
messages.error(request, f'Ошибка при оплате из кошелька: {str(e)}')
|
||||
|
||||
return redirect('orders:order-detail', pk=pk)
|
||||
|
||||
Reference in New Issue
Block a user