Files
octopus/myproject/orders/templates/orders/order_detail.html
Andrey Smakotin 3f22677573 Защита от переплаты кошельком и улучшение отображения транзакций
Изменения в UI (order_form.html):
- Добавлен data-code к опциям способов оплаты для идентификации метода кошелька
- ID для селекта способа оплаты (payment-method-select) и поля суммы (payment-amount-input)
- Динамическое ограничение max на поле суммы платежа при выборе кошелька
- Подсказка 'Макс: X руб.' отображается только для оплаты кошельком
- Для внешних методов (карта, наличные) ограничение отсутствует — переплата допустима

Логика JS:
- При выборе метода с code == 'account_balance' устанавливается max = order.amount_due
- Для остальных методов max удаляется — оператор может внести сумму больше остатка
- Переплата по внешним методам корректно зачисляется в кошелёк через WalletService.add_overpayment

Серверная защита (transaction_service.py):
- В TransactionService.create_payment добавлена проверка:
  если payment_method.code == 'account_balance' и amount > order.amount_due — ValidationError
- Сообщение: 'Сумма оплаты из кошелька (X руб.) не может превышать остаток к оплате (Y руб.)'
- Защита от обхода UI через API или прямой вызов

Улучшение отображения (order_form.html, order_detail.html):
- Для возврата в кошелёк (transaction_type == 'refund' и code == 'account_balance') показываем 'на баланс счёта' вместо названия метода
- История становится понятнее: '−750,00 руб. Возврат 29.11.2025 на баланс счёта'

Сценарий:
- Кошелёк клиента 500 руб., заказ 65 руб.
- Оператор выбирает оплату из кошелька — поле суммы ограничено 65 руб.
- Попытка ввести 500 заблокирована UI и серверной валидацией
- Для внешней оплаты (карта онлайн) можно внести 500 руб. — остаток 435 автоматически зачислится в кошелёк как переплата

Цель:
- Исключить путаницу в истории транзакций при оплате кошельком
- Разграничить поведение: кошелёк строго ограничен, внешние методы допускают переплату
- Обеспечить прозрачность движения средств для операторов
2025-11-29 16:54:24 +03:00

383 lines
20 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{% extends 'base.html' %}
{% block title %}Заказ {{ order.order_number }}{% endblock %}
{% block content %}
<div class="container-fluid">
<div class="row mb-4">
<div class="col">
<h1>Заказ {{ order.order_number }}</h1>
</div>
<div class="col-auto">
<a href="{% url 'orders:order-update' order.order_number %}" class="btn btn-primary">
<i class="bi bi-pencil"></i> Редактировать
</a>
<a href="{% url 'orders:order-delete' order.order_number %}" class="btn btn-danger">
<i class="bi bi-trash"></i> Удалить
</a>
<a href="{% url 'orders:order-list' %}" class="btn btn-secondary">
<i class="bi bi-arrow-left"></i> К списку
</a>
</div>
</div>
<div class="row">
<!-- Левая колонка -->
<div class="col-md-8">
<!-- Основная информация -->
<div class="card mb-3">
<div class="card-header">
<h5 class="mb-0">Информация о заказе</h5>
</div>
<div class="card-body">
<div class="row mb-2">
<div class="col-md-4"><strong>Клиент:</strong></div>
<div class="col-md-8">
<a href="{% url 'customers:customer-detail' order.customer.pk %}">
{{ order.customer.name }}
</a>
</div>
</div>
<div class="row mb-2">
<div class="col-md-4"><strong>Телефон:</strong></div>
<div class="col-md-8">{{ order.customer.phone }}</div>
</div>
<div class="row mb-2">
<div class="col-md-4"><strong>Статус:</strong></div>
<div class="col-md-8">
{% if order.status %}
<span class="badge" style="background-color: {{ order.status.color }}; color: #fff;">
{{ order.status.label|default:order.status.name }}
</span>
{% else %}
<span class="badge bg-secondary">Не установлен</span>
{% endif %}
</div>
</div>
<div class="row mb-2">
<div class="col-md-4"><strong>Создан:</strong></div>
<div class="col-md-8">{{ order.created_at|date:"d.m.Y H:i" }}</div>
</div>
<div class="row mb-2">
<div class="col-md-4"><strong>Обновлен:</strong></div>
<div class="col-md-8">{{ order.updated_at|date:"d.m.Y H:i" }}</div>
</div>
</div>
</div>
<!-- Получатель -->
{% if not order.customer_is_recipient %}
<div class="card mb-3">
<div class="card-header">
<h5 class="mb-0">Получатель</h5>
</div>
<div class="card-body">
<div class="row mb-2">
<div class="col-md-4"><strong>Имя получателя:</strong></div>
<div class="col-md-8">{{ order.recipient_name|default:"Не указано" }}</div>
</div>
<div class="row mb-2">
<div class="col-md-4"><strong>Телефон получателя:</strong></div>
<div class="col-md-8">{{ order.recipient_phone|default:"Не указан" }}</div>
</div>
{% if order.is_anonymous %}
<div class="row mb-2">
<div class="col-md-12">
<span class="badge bg-warning">Анонимная доставка</span>
</div>
</div>
{% endif %}
</div>
</div>
{% endif %}
<!-- Доставка -->
<div class="card mb-3">
<div class="card-header">
<h5 class="mb-0">Доставка</h5>
</div>
<div class="card-body">
<div class="row mb-2">
<div class="col-md-4"><strong>Тип:</strong></div>
<div class="col-md-8">
{% if order.is_delivery %}
<span class="badge bg-info">Доставка курьером</span>
{% else %}
<span class="badge bg-secondary">Самовывоз</span>
{% endif %}
</div>
</div>
{% if order.is_delivery %}
<div class="row mb-2">
<div class="col-md-4"><strong>Адрес:</strong></div>
<div class="col-md-8">
{% if order.delivery_address %}
{{ order.delivery_address.full_address }}
{% else %}
<span class="text-danger">Не указан</span>
{% endif %}
</div>
</div>
<div class="row mb-2">
<div class="col-md-4"><strong>Стоимость доставки:</strong></div>
<div class="col-md-8">{{ order.delivery_cost|floatformat:2 }} руб.</div>
</div>
{% else %}
<div class="row mb-2">
<div class="col-md-4"><strong>Склад для самовывоза:</strong></div>
<div class="col-md-8">
{% if order.pickup_warehouse %}
{{ order.pickup_warehouse.name }}<br>
<small class="text-muted">{{ order.pickup_warehouse.full_address }}</small>
{% else %}
<span class="text-danger">Не указан</span>
{% endif %}
</div>
</div>
{% endif %}
<div class="row mb-2">
<div class="col-md-4"><strong>Дата:</strong></div>
<div class="col-md-8">
{% if order.delivery_date %}
{{ order.delivery_date|date:"d.m.Y" }}
{% else %}
<span class="text-muted">Не указана</span>
{% endif %}
</div>
</div>
<div class="row mb-2">
<div class="col-md-4"><strong>Время:</strong></div>
<div class="col-md-8">
{% if order.delivery_time_start and order.delivery_time_end %}
{{ order.delivery_time_window }}
{% else %}
<span class="text-muted">Не указано</span>
{% endif %}
</div>
</div>
{% if order.special_instructions %}
<div class="row mb-2">
<div class="col-md-4"><strong>Особые пожелания:</strong></div>
<div class="col-md-8">{{ order.special_instructions }}</div>
</div>
{% endif %}
{% if order.is_anonymous %}
<div class="row mb-2">
<div class="col-md-12">
<span class="badge bg-warning">Анонимная доставка</span>
</div>
</div>
{% endif %}
</div>
</div>
<!-- Товары -->
<div class="card mb-3">
<div class="card-header">
<h5 class="mb-0">Товары в заказе</h5>
</div>
<div class="card-body">
<table class="table">
<thead>
<tr>
<th>Наименование</th>
<th>Количество</th>
<th>Цена</th>
<th>Сумма</th>
</tr>
</thead>
<tbody>
{% for item in order.items.all %}
<tr>
<td>
{{ item.item_name }}
{% if item.product_kit and item.product_kit.is_temporary %}
<span class="badge bg-info ms-1">Временный</span>
<br>
<small class="text-muted">Создан специально для этого заказа</small>
<br>
<a href="{% url 'products:productkit-make-permanent' item.product_kit.pk %}" class="btn btn-sm btn-outline-success mt-1">
<i class="bi bi-arrow-right-circle"></i> Сделать постоянным
</a>
{% endif %}
</td>
<td>{{ item.quantity }} шт.</td>
<td>
{{ item.price|floatformat:2 }} руб.
{% if item.is_custom_price %}
<span class="badge bg-warning ms-1">Изменена</span>
<br>
<small class="text-muted">
Оригинальная: {{ item.original_price|floatformat:2 }} руб.
{% if item.price_difference %}
{% if item.price_difference > 0 %}
<span class="text-success">(+{{ item.price_difference|floatformat:2 }} руб.)</span>
{% else %}
<span class="text-danger">({{ item.price_difference|floatformat:2 }} руб.)</span>
{% endif %}
{% endif %}
</small>
{% endif %}
</td>
<td><strong>{{ item.get_total_price|floatformat:2 }} руб.</strong></td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
<!-- Правая колонка -->
<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.order_number %}" 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.order_number %}">
{% 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">
<h5 class="mb-0">Оплата</h5>
</div>
<div class="card-body">
<div class="row mb-2">
<div class="col-6"><strong>Товары:</strong></div>
<div class="col-6 text-end">{{ order.subtotal|floatformat:2 }} руб.</div>
</div>
{% if order.is_delivery %}
<div class="row mb-2">
<div class="col-6"><strong>Доставка:</strong></div>
<div class="col-6 text-end">{{ order.delivery_cost|floatformat:2 }} руб.</div>
</div>
{% endif %}
<hr>
<div class="row mb-3">
<div class="col-6"><strong>Итого:</strong></div>
<div class="col-6 text-end"><h5>{{ order.total_amount|floatformat:2 }} руб.</h5></div>
</div>
<div class="row mb-2">
<div class="col-6"><strong>Оплачено:</strong></div>
<div class="col-6 text-end">{{ order.amount_paid|floatformat:2 }} руб.</div>
</div>
<div class="row mb-2">
<div class="col-6"><strong>К оплате:</strong></div>
<div class="col-6 text-end text-danger"><strong>{{ order.amount_due|floatformat:2 }} руб.</strong></div>
</div>
<div class="row mb-2">
<div class="col-12">
<strong>Статус оплаты:</strong>
{% if order.payment_status == 'paid' %}
<span class="badge bg-success w-100">Оплачен полностью</span>
{% elif order.payment_status == 'partial' %}
<span class="badge bg-warning w-100">Частично оплачен</span>
{% else %}
<span class="badge bg-danger w-100">Не оплачен</span>
{% endif %}
</div>
</div>
</div>
</div>
<!-- История транзакций -->
{% if order.transactions.exists %}
<div class="card mb-3">
<div class="card-header">
<h5 class="mb-0">История транзакций</h5>
</div>
<div class="card-body">
<ul class="list-group list-group-flush">
{% for transaction in order.transactions.all|dictsortreversed:"transaction_date" %}
<li class="list-group-item">
<div class="d-flex justify-content-between align-items-start">
<div>
<strong class="{% if transaction.transaction_type == 'payment' %}text-success{% else %}text-danger{% endif %}">
{% if transaction.transaction_type == 'refund' %}{% else %}+{% endif %}{{ transaction.amount|floatformat:2 }} руб.
</strong>
</div>
<div>
{% if transaction.transaction_type == 'payment' %}
<span class="badge bg-success">Платёж</span>
{% else %}
<span class="badge bg-warning text-dark">Возврат</span>
{% endif %}
</div>
</div>
<small class="text-muted">
{{ transaction.transaction_date|date:"d.m.Y H:i" }}<br>
{% if transaction.transaction_type == 'refund' and transaction.payment_method.code == 'account_balance' %}
Возврат на баланс счёта
{% else %}
{{ transaction.payment_method.name }}
{% endif %}
{% if transaction.notes or transaction.reason %}
<br><em>{{ transaction.notes|default:transaction.reason }}</em>
{% endif %}
{% if transaction.created_by %}
<br>Кем: {{ transaction.created_by.get_short_name|default:transaction.created_by.username }}
{% endif %}
</small>
</li>
{% endfor %}
</ul>
</div>
</div>
{% endif %}
</div>
</div>
</div>
{% endblock %}