Рефакторинг: отдельные endpoints для управления платежами (Django best practices)
ПРОБЛЕМА: Использование PaymentFormSet для платежей было НЕПРАВИЛЬНЫМ подходом: 1. Платежи = финансовые транзакции (не должны редактироваться inline) 2. Формы валидировали существующие платежи как новые 3. Сложная логика с formset management forms 4. Конфликты валидации кошелька РЕШЕНИЕ (Django Best Practices): Разделили управление платежами на отдельные операции: АРХИТЕКТУРА: ` POST /orders/111/payments/add/ # Добавить платеж POST /orders/111/payments/123/delete/ # Удалить платеж ` ПРЕИМУЩЕСТВА: ✅ Чистая архитектура (separation of concerns) ✅ Платежи = неизменяемые транзакции ✅ Простая валидация (только для новых) ✅ Легко тестировать ✅ API-ready структура ИЗМЕНЕНИЯ: 1. orders/views.py: - Убран PaymentFormSet из order_create и order_update - Добавлен payment_add(request, order_number) - Добавлен payment_delete(request, order_number, payment_id) - Используется простой PaymentForm вместо formset - Payment.save() автоматически обрабатывает: * Списание из кошелька * Обработку переплаты * Обновление amount_paid 2. orders/urls.py: - Добавлены URL patterns для payment-add и payment-delete - Структура: /orders/<number>/payments/add|<id>/delete/ 3. orders/templates/orders/order_form.html: - Убран PaymentFormSet и все его скрипты (~265 строк) - Простая HTML форма для добавления платежа - Существующие платежи: read-only список с кнопками удаления - Каждое удаление = отдельный POST запрос - Для создания: показываем предупреждение вместо формы 4. orders/templatetags/orders_tags.py (NEW): - Template tag get_payment_methods - Загружает активные способы оплаты - Использование: {% get_payment_methods as payment_methods %} РЕЗУЛЬТАТ: - Код: -191 строка - Логика: простая и понятная - Архитектура: правильная (как в учебнике) - Платежи: только add/delete (без edit) - Валидация: работает корректно - UX: чище и понятнее
This commit is contained in:
@@ -571,44 +571,32 @@
|
|||||||
|
|
||||||
<!-- Оплата (смешанная оплата) -->
|
<!-- Оплата (смешанная оплата) -->
|
||||||
<div class="card mb-3">
|
<div class="card mb-3">
|
||||||
<div class="card-header d-flex justify-content-between align-items-center">
|
<div class="card-header">
|
||||||
<h5 class="mb-0"><i class="bi bi-credit-card"></i> Оплата</h5>
|
<h5 class="mb-0"><i class="bi bi-credit-card"></i> Оплата</h5>
|
||||||
<button type="button" class="btn btn-sm btn-success" id="add-payment-btn">
|
|
||||||
<i class="bi bi-plus-circle"></i> Добавить платеж
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
|
|
||||||
<!-- Блок кошелька клиента -->
|
<!-- Блок кошелька клиента -->
|
||||||
{% if order.customer %}
|
{% if order.customer %}
|
||||||
<div class="alert alert-info d-flex justify-content-between align-items-center">
|
<div class="alert alert-info mb-3">
|
||||||
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
<div>
|
<div>
|
||||||
<strong><i class="bi bi-wallet2"></i> Кошелёк клиента:</strong>
|
<strong><i class="bi bi-wallet2"></i> Кошелёк клиента:</strong>
|
||||||
{% if order.customer.wallet_balance > 0 %}
|
{% if order.customer.wallet_balance > 0 %}
|
||||||
<span class="text-success fw-bold">{{ order.customer.wallet_balance|floatformat:2 }} руб.</span>
|
<span class="text-success fw-bold">{{ order.customer.wallet_balance|floatformat:2 }} руб.</span>
|
||||||
{% else %}
|
{% else %}
|
||||||
<span class="text-muted">0.00 руб.</span>
|
<span class="text-muted">0.00 руб.</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<span class="ms-3">Остаток к оплате: <strong>{{ order.amount_due|floatformat:2 }} руб.</strong></span>
|
|
||||||
</div>
|
</div>
|
||||||
{% if order.customer.wallet_balance > 0 and order.amount_due > 0 %}
|
<div>
|
||||||
<div class="d-flex gap-2">
|
<strong>Остаток к оплате:</strong>
|
||||||
<button type="button" class="btn btn-primary btn-sm" id="apply-wallet-max-btn">
|
<span class="text-primary fw-bold">{{ order.amount_due|floatformat:2 }} руб.</span>
|
||||||
Учесть максимум
|
|
||||||
</button>
|
|
||||||
<div class="input-group" style="max-width: 280px;">
|
|
||||||
<input type="number" step="0.01" min="0" class="form-control form-control-sm" id="apply-wallet-amount-input" placeholder="Сумма из кошелька">
|
|
||||||
<button type="button" class="btn btn-outline-primary btn-sm" id="apply-wallet-amount-btn">Учесть сумму</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<!-- Скрытые поля для formset management -->
|
<!-- Уже сохраненные платежи -->
|
||||||
{{ payment_formset.management_form }}
|
|
||||||
|
|
||||||
<!-- Уже сохраненные платежи (информационно) -->
|
|
||||||
{% if order.pk and order.payments.exists %}
|
{% if order.pk and order.payments.exists %}
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<h6 class="text-muted mb-3"><i class="bi bi-check-circle"></i> Проведенные платежи</h6>
|
<h6 class="text-muted mb-3"><i class="bi bi-check-circle"></i> Проведенные платежи</h6>
|
||||||
@@ -628,13 +616,14 @@
|
|||||||
<span class="text-muted">{{ payment.notes|default:"—" }}</span>
|
<span class="text-muted">{{ payment.notes|default:"—" }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-1 text-end">
|
<div class="col-md-1 text-end">
|
||||||
<button type="button" class="btn btn-outline-danger btn-sm delete-existing-payment-btn"
|
<form method="post" action="{% url 'orders:payment-delete' order.order_number payment.id %}" style="display: inline;">
|
||||||
data-payment-id="{{ payment.id }}"
|
{% csrf_token %}
|
||||||
data-payment-name="{{ payment.payment_method.name }}"
|
<button type="submit" class="btn btn-outline-danger btn-sm"
|
||||||
data-payment-amount="{{ payment.amount|floatformat:2 }}"
|
onclick="return confirm('Удалить платеж {{ payment.payment_method.name }} на сумму {{ payment.amount|floatformat:2 }} руб.?');"
|
||||||
title="Удалить платеж">
|
title="Удалить платеж">
|
||||||
<i class="bi bi-trash"></i>
|
<i class="bi bi-trash"></i>
|
||||||
</button>
|
</button>
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -642,250 +631,63 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<!-- Контейнер для НОВЫХ платежей -->
|
<!-- Форма добавления нового платежа -->
|
||||||
<div id="payments-container">
|
{% if order.pk %}
|
||||||
<!-- Здесь будут добавляться новые платежи -->
|
<div class="border rounded p-3 bg-white">
|
||||||
|
<h6 class="mb-3"><i class="bi bi-plus-circle"></i> Добавить новый платеж</h6>
|
||||||
|
<form method="post" action="{% url 'orders:payment-add' order.order_number %}" id="payment-add-form">
|
||||||
|
{% csrf_token %}
|
||||||
|
<div class="row align-items-end">
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label">Способ оплаты</label>
|
||||||
|
<select name="payment_method" class="form-select" required>
|
||||||
|
<option value="">---------</option>
|
||||||
|
{% load orders_tags %}
|
||||||
|
{% get_payment_methods as payment_methods %}
|
||||||
|
{% for pm in payment_methods %}
|
||||||
|
<option value="{{ pm.id }}">{{ pm.name }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<label class="form-label">Сумма</label>
|
||||||
|
<input type="number" name="amount" step="0.01" min="0.01" class="form-control" placeholder="0.00" required>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<label class="form-label">Примечания</label>
|
||||||
|
<input type="text" name="notes" class="form-control" placeholder="Опционально">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-2">
|
||||||
|
<button type="submit" class="btn btn-success w-100">
|
||||||
|
<i class="bi bi-plus-lg"></i> Добавить
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="alert alert-warning mb-0">
|
||||||
|
<i class="bi bi-info-circle"></i> Сначала создайте заказ, затем вы сможете добавлять платежи.
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<!-- Итоговая сумма платежей -->
|
<!-- Итоговая сумма платежей -->
|
||||||
<div id="payments-total-section" class="border-top pt-3 mt-3 mb-3">
|
{% if order.pk %}
|
||||||
|
<div class="border-top pt-3 mt-3">
|
||||||
<div class="row align-items-center">
|
<div class="row align-items-center">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<p class="mb-0 text-muted"><i class="bi bi-cash-stack"></i> Всего внесено:</p>
|
<p class="mb-0 text-muted"><i class="bi bi-cash-stack"></i> Всего внесено:</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-auto">
|
<div class="col-auto">
|
||||||
<h5 class="mb-0 text-success">
|
<h5 class="mb-0 text-success">
|
||||||
<span id="payments-total-value">{{ order.amount_paid|default:"0.00"|floatformat:2 }}</span> руб.
|
{{ order.amount_paid|default:"0.00"|floatformat:2 }} руб.
|
||||||
</h5>
|
</h5>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %}
|
||||||
<!-- Скрытый шаблон для новых платежей -->
|
|
||||||
<template id="empty-payment-form-template">
|
|
||||||
<div class="payment-form border rounded p-3 mb-3" data-form-index="__prefix__">
|
|
||||||
<input type="hidden" name="payments-__prefix__-id" id="id_payments-__prefix__-id" form="order-form">
|
|
||||||
<input type="checkbox" name="payments-__prefix__-DELETE" id="id_payments-__prefix__-DELETE" form="order-form" style="display: none;">
|
|
||||||
|
|
||||||
<div class="row align-items-end">
|
|
||||||
<div class="col-md-4">
|
|
||||||
<div class="mb-2">
|
|
||||||
<label class="form-label">Способ оплаты</label>
|
|
||||||
<select name="payments-__prefix__-payment_method" class="form-select" id="id_payments-__prefix__-payment_method" form="order-form">
|
|
||||||
<option value="">---------</option>
|
|
||||||
{% for pm in payment_formset.forms.0.fields.payment_method.queryset %}
|
|
||||||
<option value="{{ pm.id }}">{{ pm.name }}</option>
|
|
||||||
{% endfor %}
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-3">
|
|
||||||
<div class="mb-2">
|
|
||||||
<label class="form-label">Сумма</label>
|
|
||||||
<input type="number" name="payments-__prefix__-amount" step="0.01" min="0" class="form-control" placeholder="Сумма платежа" id="id_payments-__prefix__-amount" form="order-form">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-4">
|
|
||||||
<div class="mb-2">
|
|
||||||
<label class="form-label">Примечания</label>
|
|
||||||
<textarea name="payments-__prefix__-notes" class="form-control" rows="1" placeholder="Примечания к платежу (опционально)" id="id_payments-__prefix__-notes" form="order-form"></textarea>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-1">
|
|
||||||
<button type="button" class="btn btn-danger btn-sm w-100 remove-payment-btn">
|
|
||||||
<i class="bi bi-trash"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
(function() {
|
|
||||||
const walletBalance = parseFloat("{{ order.customer.wallet_balance|default:'0' }}".replace(',', '.')) || 0;
|
|
||||||
const amountDue = parseFloat("{{ order.amount_due|default:'0' }}".replace(',', '.')) || 0;
|
|
||||||
|
|
||||||
function addPaymentRow() {
|
|
||||||
const totalFormsInput = document.querySelector('input[name="payments-TOTAL_FORMS"]');
|
|
||||||
const idx = parseInt(totalFormsInput.value, 10);
|
|
||||||
const tpl = document.getElementById('empty-payment-form-template').innerHTML.replace(/__prefix__/g, idx);
|
|
||||||
const container = document.getElementById('payments-container');
|
|
||||||
const wrapper = document.createElement('div');
|
|
||||||
wrapper.innerHTML = tpl.trim();
|
|
||||||
container.appendChild(wrapper.firstElementChild);
|
|
||||||
totalFormsInput.value = String(idx + 1);
|
|
||||||
return container.querySelector(`.payment-form[data-form-index="${idx}"]`);
|
|
||||||
}
|
|
||||||
|
|
||||||
function selectAccountBalance(selectEl) {
|
|
||||||
if (!selectEl) return;
|
|
||||||
const options = Array.from(selectEl.options);
|
|
||||||
const target = options.find(o => o.textContent.trim() === 'С баланса счёта');
|
|
||||||
if (target) {
|
|
||||||
selectEl.value = target.value;
|
|
||||||
selectEl.dispatchEvent(new Event('change', { bubbles: true }));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function applyWallet(amount) {
|
|
||||||
if (!amount || amount <= 0) {
|
|
||||||
alert('Введите сумму больше 0.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (amount > walletBalance) {
|
|
||||||
alert(`Недостаточно средств в кошельке. Доступно ${walletBalance.toFixed(2)} руб.`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (amount > amountDue) {
|
|
||||||
alert(`Сумма превышает остаток к оплате (${amountDue.toFixed(2)} руб.).`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Всегда добавляем новую строку платежа
|
|
||||||
const formEl = addPaymentRow();
|
|
||||||
const sel = formEl.querySelector('select[id^="id_payments-"][id$="-payment_method"]');
|
|
||||||
const amt = formEl.querySelector('input[id^="id_payments-"][id$="-amount"]');
|
|
||||||
const notes = formEl.querySelector('textarea[id^="id_payments-"][id$="-notes"]');
|
|
||||||
|
|
||||||
// Загружаем список способов оплаты
|
|
||||||
if (sel) {
|
|
||||||
fetch('/products/api/payment-methods/')
|
|
||||||
.then(response => response.json())
|
|
||||||
.then(data => {
|
|
||||||
sel.innerHTML = '<option value="">---------</option>';
|
|
||||||
data.forEach(method => {
|
|
||||||
const option = document.createElement('option');
|
|
||||||
option.value = method.id;
|
|
||||||
option.textContent = method.name;
|
|
||||||
sel.appendChild(option);
|
|
||||||
});
|
|
||||||
|
|
||||||
// После загрузки устанавливаем "С баланса счёта"
|
|
||||||
for (let i = 0; i < sel.options.length; i++) {
|
|
||||||
if (sel.options[i].textContent.trim() === 'С баланса счёта') {
|
|
||||||
sel.value = sel.options[i].value;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
console.error('Error loading payment methods:', error);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Проставляем сумму
|
|
||||||
if (amt) {
|
|
||||||
const maxUsable = Math.min(walletBalance, amountDue);
|
|
||||||
const finalAmount = Math.min(amount, maxUsable);
|
|
||||||
amt.value = finalAmount.toFixed(2);
|
|
||||||
amt.setAttribute('max', maxUsable.toFixed(2));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Небольшая подсказка в примечания
|
|
||||||
if (notes && !notes.value) {
|
|
||||||
notes.value = 'Оплата из кошелька';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Добавляем обработчик удаления
|
|
||||||
const removeBtn = formEl.querySelector('.remove-payment-btn');
|
|
||||||
if (removeBtn) {
|
|
||||||
removeBtn.addEventListener('click', function() {
|
|
||||||
if (!confirm('Вы действительно хотите удалить этот платеж?')) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const deleteCheckbox = formEl.querySelector('input[name$="-DELETE"]');
|
|
||||||
const idField = formEl.querySelector('input[name$="-id"]');
|
|
||||||
if (idField && idField.value) {
|
|
||||||
deleteCheckbox.checked = true;
|
|
||||||
formEl.classList.add('deleted');
|
|
||||||
formEl.style.display = 'none';
|
|
||||||
} else {
|
|
||||||
formEl.remove();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Обработчики кнопок применения кошелька
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
|
||||||
const applyMaxBtn = document.getElementById('apply-wallet-max-btn');
|
|
||||||
const applyAmountBtn = document.getElementById('apply-wallet-amount-btn');
|
|
||||||
const amountInput = document.getElementById('apply-wallet-amount-input');
|
|
||||||
|
|
||||||
if (applyMaxBtn) {
|
|
||||||
applyMaxBtn.addEventListener('click', function() {
|
|
||||||
const maxUsable = Math.min(walletBalance, amountDue);
|
|
||||||
applyWallet(maxUsable);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (applyAmountBtn && amountInput) {
|
|
||||||
applyAmountBtn.addEventListener('click', function() {
|
|
||||||
const val = parseFloat((amountInput.value || '0').replace(',', '.')) || 0;
|
|
||||||
applyWallet(val);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Автозаполнение при выборе "С баланса счёта"
|
|
||||||
document.getElementById('payments-container').addEventListener('change', function(e) {
|
|
||||||
const sel = e.target;
|
|
||||||
if (sel.tagName === 'SELECT') {
|
|
||||||
const label = sel.options[sel.selectedIndex]?.textContent?.trim() || '';
|
|
||||||
if (label === 'С баланса счёта') {
|
|
||||||
const wrap = sel.closest('.payment-form');
|
|
||||||
const amt = wrap.querySelector('input[id^="id_payments-"][id$="-amount"]');
|
|
||||||
if (amt) {
|
|
||||||
const maxUsable = Math.min(walletBalance, amountDue);
|
|
||||||
amt.value = maxUsable.toFixed(2);
|
|
||||||
amt.setAttribute('max', maxUsable.toFixed(2));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
})();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
// Обработчик удаления существующих платежей
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
|
||||||
const deleteButtons = document.querySelectorAll('.delete-existing-payment-btn');
|
|
||||||
|
|
||||||
deleteButtons.forEach(button => {
|
|
||||||
button.addEventListener('click', function() {
|
|
||||||
const paymentId = this.dataset.paymentId;
|
|
||||||
const paymentName = this.dataset.paymentName;
|
|
||||||
const paymentAmount = this.dataset.paymentAmount;
|
|
||||||
|
|
||||||
if (!confirm(`Удалить платеж "${paymentName}" на сумму ${paymentAmount} руб.?`)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Создаем скрытую форму для отправки
|
|
||||||
const form = document.createElement('form');
|
|
||||||
form.method = 'POST';
|
|
||||||
form.action = window.location.href;
|
|
||||||
|
|
||||||
const csrfToken = document.querySelector('[name=csrfmiddlewaretoken]').value;
|
|
||||||
const csrfInput = document.createElement('input');
|
|
||||||
csrfInput.type = 'hidden';
|
|
||||||
csrfInput.name = 'csrfmiddlewaretoken';
|
|
||||||
csrfInput.value = csrfToken;
|
|
||||||
form.appendChild(csrfInput);
|
|
||||||
|
|
||||||
const paymentIdInput = document.createElement('input');
|
|
||||||
paymentIdInput.type = 'hidden';
|
|
||||||
paymentIdInput.name = 'delete_payment_id';
|
|
||||||
paymentIdInput.value = paymentId;
|
|
||||||
form.appendChild(paymentIdInput);
|
|
||||||
|
|
||||||
document.body.appendChild(form);
|
|
||||||
form.submit();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="card mb-3">
|
<div class="card mb-3">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
# Инициализация пакета templatetags
|
# Template tags package
|
||||||
|
|||||||
14
myproject/orders/templatetags/orders_tags.py
Normal file
14
myproject/orders/templatetags/orders_tags.py
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from django import template
|
||||||
|
from orders.models import PaymentMethod
|
||||||
|
|
||||||
|
register = template.Library()
|
||||||
|
|
||||||
|
|
||||||
|
@register.simple_tag
|
||||||
|
def get_payment_methods():
|
||||||
|
"""
|
||||||
|
Получить список активных способов оплаты.
|
||||||
|
Использование: {% get_payment_methods as payment_methods %}
|
||||||
|
"""
|
||||||
|
return PaymentMethod.objects.filter(is_active=True).order_by('order', 'name')
|
||||||
@@ -11,6 +11,10 @@ urlpatterns = [
|
|||||||
path('<int:order_number>/edit/', views.order_update, name='order-update'),
|
path('<int:order_number>/edit/', views.order_update, name='order-update'),
|
||||||
path('<int:order_number>/delete/', views.order_delete, name='order-delete'),
|
path('<int:order_number>/delete/', views.order_delete, name='order-delete'),
|
||||||
|
|
||||||
|
# Payment Management
|
||||||
|
path('<int:order_number>/payments/add/', views.payment_add, name='payment-add'),
|
||||||
|
path('<int:order_number>/payments/<int:payment_id>/delete/', views.payment_delete, name='payment-delete'),
|
||||||
|
|
||||||
# AJAX endpoints
|
# AJAX endpoints
|
||||||
path('api/customer-address-history/', views.get_customer_address_history, name='api-customer-address-history'),
|
path('api/customer-address-history/', views.get_customer_address_history, name='api-customer-address-history'),
|
||||||
|
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ from django.contrib.auth.decorators import login_required
|
|||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
from .models import Order, OrderItem, Address, OrderStatus
|
from .models import Order, OrderItem, Address, OrderStatus, Payment, PaymentMethod
|
||||||
from .forms import OrderForm, OrderItemFormSet, OrderStatusForm, PaymentFormSet
|
from .forms import OrderForm, OrderItemFormSet, OrderStatusForm, PaymentForm
|
||||||
from .filters import OrderFilter
|
from .filters import OrderFilter
|
||||||
from .services.address_service import AddressService
|
from .services.address_service import AddressService
|
||||||
import json
|
import json
|
||||||
@@ -65,9 +65,8 @@ def order_create(request):
|
|||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
form = OrderForm(request.POST)
|
form = OrderForm(request.POST)
|
||||||
formset = OrderItemFormSet(request.POST)
|
formset = OrderItemFormSet(request.POST)
|
||||||
payment_formset = PaymentFormSet(request.POST, prefix='payments')
|
|
||||||
|
|
||||||
if form.is_valid() and formset.is_valid() and payment_formset.is_valid():
|
if form.is_valid() and formset.is_valid():
|
||||||
# Сохраняем форму БЕЗ commit, чтобы не вызывать reset_delivery_cost() до сохранения items
|
# Сохраняем форму БЕЗ commit, чтобы не вызывать reset_delivery_cost() до сохранения items
|
||||||
order = form.save(commit=False)
|
order = form.save(commit=False)
|
||||||
|
|
||||||
@@ -90,31 +89,12 @@ def order_create(request):
|
|||||||
formset.instance = order
|
formset.instance = order
|
||||||
formset.save()
|
formset.save()
|
||||||
|
|
||||||
# Сохраняем платежи (устанавливаем created_by)
|
|
||||||
payment_formset.instance = order
|
|
||||||
unsaved_payments = payment_formset.save(commit=False)
|
|
||||||
|
|
||||||
for p in unsaved_payments:
|
|
||||||
if p.created_by_id is None:
|
|
||||||
p.created_by = request.user
|
|
||||||
p.order = order
|
|
||||||
p.save()
|
|
||||||
|
|
||||||
# Обрабатываем удалённые платежи
|
|
||||||
from customers.services.wallet_service import WalletService
|
|
||||||
for obj in payment_formset.deleted_objects:
|
|
||||||
# Если удаляем платёж из кошелька - возвращаем сумму обратно
|
|
||||||
if hasattr(obj, 'payment_method') and obj.payment_method and getattr(obj.payment_method, 'code', '') == 'account_balance':
|
|
||||||
WalletService.refund_wallet_payment(order, obj.amount, request.user)
|
|
||||||
obj.delete()
|
|
||||||
|
|
||||||
# Пересчитываем стоимость доставки если она не установлена вручную
|
# Пересчитываем стоимость доставки если она не установлена вручную
|
||||||
delivery_cost = form.cleaned_data.get('delivery_cost')
|
delivery_cost = form.cleaned_data.get('delivery_cost')
|
||||||
if not delivery_cost or delivery_cost <= 0:
|
if not delivery_cost or delivery_cost <= 0:
|
||||||
order.reset_delivery_cost()
|
order.reset_delivery_cost()
|
||||||
|
|
||||||
# Пересчитываем сумму оплачено и итоговую стоимость
|
# Пересчитываем итоговую стоимость
|
||||||
order.amount_paid = sum(p.amount for p in order.payments.all())
|
|
||||||
order.calculate_total()
|
order.calculate_total()
|
||||||
order.update_payment_status()
|
order.update_payment_status()
|
||||||
|
|
||||||
@@ -137,12 +117,10 @@ def order_create(request):
|
|||||||
|
|
||||||
form = OrderForm(initial=initial_data)
|
form = OrderForm(initial=initial_data)
|
||||||
formset = OrderItemFormSet()
|
formset = OrderItemFormSet()
|
||||||
payment_formset = PaymentFormSet(prefix='payments')
|
|
||||||
|
|
||||||
context = {
|
context = {
|
||||||
'form': form,
|
'form': form,
|
||||||
'formset': formset,
|
'formset': formset,
|
||||||
'payment_formset': payment_formset,
|
|
||||||
'preselected_customer': preselected_customer,
|
'preselected_customer': preselected_customer,
|
||||||
'title': 'Создание заказа',
|
'title': 'Создание заказа',
|
||||||
'button_text': 'Создать заказ',
|
'button_text': 'Создать заказ',
|
||||||
@@ -157,36 +135,10 @@ def order_update(request, order_number):
|
|||||||
order = get_object_or_404(Order, order_number=order_number)
|
order = get_object_or_404(Order, order_number=order_number)
|
||||||
|
|
||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
# Обработка удаления существующего платежа
|
|
||||||
delete_payment_id = request.POST.get('delete_payment_id')
|
|
||||||
if delete_payment_id:
|
|
||||||
try:
|
|
||||||
from orders.models import Payment
|
|
||||||
from customers.services.wallet_service import WalletService
|
|
||||||
|
|
||||||
payment = Payment.objects.get(pk=delete_payment_id, order=order)
|
|
||||||
|
|
||||||
# Если это платеж из кошелька - возвращаем средства
|
|
||||||
if payment.payment_method and payment.payment_method.code == 'account_balance':
|
|
||||||
WalletService.refund_wallet_payment(order, payment.amount, request.user)
|
|
||||||
|
|
||||||
payment.delete()
|
|
||||||
|
|
||||||
# Пересчитываем сумму оплаты
|
|
||||||
order.amount_paid = sum(p.amount for p in order.payments.all())
|
|
||||||
order.update_payment_status()
|
|
||||||
|
|
||||||
messages.success(request, 'Платеж успешно удален.')
|
|
||||||
return redirect('orders:order-update', order_number=order.order_number)
|
|
||||||
except Payment.DoesNotExist:
|
|
||||||
messages.error(request, 'Платеж не найден.')
|
|
||||||
return redirect('orders:order-update', order_number=order.order_number)
|
|
||||||
|
|
||||||
form = OrderForm(request.POST, instance=order)
|
form = OrderForm(request.POST, instance=order)
|
||||||
formset = OrderItemFormSet(request.POST, instance=order)
|
formset = OrderItemFormSet(request.POST, instance=order)
|
||||||
payment_formset = PaymentFormSet(request.POST, instance=order, prefix='payments')
|
|
||||||
|
|
||||||
if form.is_valid() and formset.is_valid() and payment_formset.is_valid():
|
if form.is_valid() and formset.is_valid():
|
||||||
order = form.save(commit=False)
|
order = form.save(commit=False)
|
||||||
|
|
||||||
# Обрабатываем адрес доставки
|
# Обрабатываем адрес доставки
|
||||||
@@ -218,26 +170,7 @@ def order_update(request, order_number):
|
|||||||
order.save()
|
order.save()
|
||||||
formset.save()
|
formset.save()
|
||||||
|
|
||||||
# Сохраняем платежи (устанавливаем created_by)
|
# Пересчитываем итоговую стоимость
|
||||||
payment_formset.instance = order
|
|
||||||
unsaved_payments = payment_formset.save(commit=False)
|
|
||||||
|
|
||||||
for p in unsaved_payments:
|
|
||||||
if p.created_by_id is None:
|
|
||||||
p.created_by = request.user
|
|
||||||
p.order = order
|
|
||||||
p.save()
|
|
||||||
|
|
||||||
# Обрабатываем удалённые платежи
|
|
||||||
from customers.services.wallet_service import WalletService
|
|
||||||
for obj in payment_formset.deleted_objects:
|
|
||||||
# Если удаляем платёж из кошелька - возвращаем сумму обратно
|
|
||||||
if hasattr(obj, 'payment_method') and obj.payment_method and getattr(obj.payment_method, 'code', '') == 'account_balance':
|
|
||||||
WalletService.refund_wallet_payment(order, obj.amount, request.user)
|
|
||||||
obj.delete()
|
|
||||||
|
|
||||||
# Пересчитываем сумму оплачено и итоговую стоимость
|
|
||||||
order.amount_paid = sum(p.amount for p in order.payments.all())
|
|
||||||
order.calculate_total()
|
order.calculate_total()
|
||||||
order.update_payment_status()
|
order.update_payment_status()
|
||||||
|
|
||||||
@@ -254,20 +187,15 @@ def order_update(request, order_number):
|
|||||||
for i, item_form in enumerate(formset):
|
for i, item_form in enumerate(formset):
|
||||||
if item_form.errors:
|
if item_form.errors:
|
||||||
print(f" Item form {i} errors: {item_form.errors}")
|
print(f" Item form {i} errors: {item_form.errors}")
|
||||||
if not payment_formset.is_valid():
|
|
||||||
print(f"PaymentFormSet errors: {payment_formset.errors}")
|
|
||||||
print(f"PaymentFormSet non_form_errors: {payment_formset.non_form_errors()}")
|
|
||||||
print("=== КОНЕЦ ОШИБОК ===\n")
|
print("=== КОНЕЦ ОШИБОК ===\n")
|
||||||
messages.error(request, 'Пожалуйста, исправьте ошибки в форме.')
|
messages.error(request, 'Пожалуйста, исправьте ошибки в форме.')
|
||||||
else:
|
else:
|
||||||
form = OrderForm(instance=order)
|
form = OrderForm(instance=order)
|
||||||
formset = OrderItemFormSet(instance=order)
|
formset = OrderItemFormSet(instance=order)
|
||||||
payment_formset = PaymentFormSet(instance=order, prefix='payments')
|
|
||||||
|
|
||||||
context = {
|
context = {
|
||||||
'form': form,
|
'form': form,
|
||||||
'formset': formset,
|
'formset': formset,
|
||||||
'payment_formset': payment_formset,
|
|
||||||
'order': order,
|
'order': order,
|
||||||
'title': f'Редактирование заказа #{order.order_number}',
|
'title': f'Редактирование заказа #{order.order_number}',
|
||||||
'button_text': 'Сохранить изменения',
|
'button_text': 'Сохранить изменения',
|
||||||
@@ -293,6 +221,76 @@ def order_delete(request, order_number):
|
|||||||
return render(request, 'orders/order_confirm_delete.html', context)
|
return render(request, 'orders/order_confirm_delete.html', context)
|
||||||
|
|
||||||
|
|
||||||
|
# === УПРАВЛЕНИЕ ПЛАТЕЖАМИ ===
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
@require_http_methods(["POST"])
|
||||||
|
def payment_add(request, order_number):
|
||||||
|
"""
|
||||||
|
Добавление нового платежа к заказу.
|
||||||
|
Отдельный endpoint для чистоты архитектуры.
|
||||||
|
"""
|
||||||
|
order = get_object_or_404(Order, order_number=order_number)
|
||||||
|
|
||||||
|
form = PaymentForm(request.POST)
|
||||||
|
|
||||||
|
if form.is_valid():
|
||||||
|
payment = form.save(commit=False)
|
||||||
|
payment.order = order
|
||||||
|
payment.created_by = request.user
|
||||||
|
|
||||||
|
try:
|
||||||
|
# save() вызовет Payment.save() который обработает:
|
||||||
|
# - Списание из кошелька (если account_balance)
|
||||||
|
# - Обработку переплаты
|
||||||
|
# - Обновление amount_paid и payment_status
|
||||||
|
payment.save()
|
||||||
|
|
||||||
|
messages.success(
|
||||||
|
request,
|
||||||
|
f'Платеж на сумму {payment.amount} руб. '
|
||||||
|
f'({payment.payment_method.name}) успешно добавлен.'
|
||||||
|
)
|
||||||
|
except ValidationError as e:
|
||||||
|
messages.error(request, f'Ошибка при добавлении платежа: {e}')
|
||||||
|
else:
|
||||||
|
# Показываем ошибки валидации
|
||||||
|
for field, errors in form.errors.items():
|
||||||
|
for error in errors:
|
||||||
|
messages.error(request, f'{field}: {error}')
|
||||||
|
|
||||||
|
# Возвращаемся на страницу редактирования
|
||||||
|
return redirect('orders:order-update', order_number=order.order_number)
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
@require_http_methods(["POST"])
|
||||||
|
def payment_delete(request, order_number, payment_id):
|
||||||
|
"""
|
||||||
|
Удаление платежа.
|
||||||
|
Возвращает средства в кошелек, если платеж был из кошелька.
|
||||||
|
"""
|
||||||
|
order = get_object_or_404(Order, order_number=order_number)
|
||||||
|
payment = get_object_or_404(Payment, pk=payment_id, order=order)
|
||||||
|
|
||||||
|
# Сохраняем данные для сообщения
|
||||||
|
payment_info = f'{payment.payment_method.name} на сумму {payment.amount} руб.'
|
||||||
|
|
||||||
|
# Если это платеж из кошелька - возвращаем средства
|
||||||
|
if payment.payment_method.code == 'account_balance':
|
||||||
|
from customers.services.wallet_service import WalletService
|
||||||
|
WalletService.refund_wallet_payment(order, payment.amount, request.user)
|
||||||
|
|
||||||
|
payment.delete()
|
||||||
|
|
||||||
|
# Пересчитываем сумму оплаты
|
||||||
|
order.amount_paid = sum(p.amount for p in order.payments.all())
|
||||||
|
order.update_payment_status()
|
||||||
|
|
||||||
|
messages.success(request, f'Платеж {payment_info} успешно удален.')
|
||||||
|
return redirect('orders:order-update', order_number=order.order_number)
|
||||||
|
|
||||||
|
|
||||||
# === AJAX ENDPOINTS ===
|
# === AJAX ENDPOINTS ===
|
||||||
|
|
||||||
@require_http_methods(["POST"])
|
@require_http_methods(["POST"])
|
||||||
|
|||||||
Reference in New Issue
Block a user