Рефакторинг: убрана финализация черновиков и улучшены шаблоны заказов

- Убран черновик как отдельная сущность с процессом финализации
- Черновик теперь просто обычный OrderStatus
- Удалены кнопки 'Сохранить как черновик' и 'Финализировать черновик'
- Унифицирована логика сохранения/обновления заказов для всех статусов

Улучшения шаблонов:
- Стандартизировано форматирование валюты через floatformat:2
- Исправлено отображение статуса (используется OrderStatus.label и color)
- Исправлено отображение способа оплаты (корректное использование ForeignKey)
- Добавлены иконки к заголовкам секций для лучшего UX
- Удалены избыточные console.log (~160 строк)
- Очищены комментарии и улучшена читаемость кода
- Убрано использование переменной is_draft в контексте
- Добавлена визуальная согласованность между шаблонами заказов
This commit is contained in:
2025-11-29 01:49:48 +03:00
parent 9415aca63d
commit a97fc39a2c
4 changed files with 228 additions and 383 deletions

View File

@@ -139,6 +139,46 @@ class WalletService:
return usable_amount return usable_amount
@staticmethod
@transaction.atomic
def refund_wallet_payment(order, amount, user):
"""
Возврат средств в кошелёк при удалении платежа.
Увеличивает баланс кошелька и создаёт транзакцию deposit.
Args:
order: Заказ, по которому был платёж
amount: Сумма возврата
user: Пользователь, инициировавший возврат
Returns:
Decimal: Возвращённая сумма
"""
from customers.models import Customer, WalletTransaction
amount = _quantize(amount)
if amount <= 0:
return None
# Блокируем запись клиента
customer = Customer.objects.select_for_update().get(pk=order.customer_id)
# Увеличиваем баланс
customer.wallet_balance = _quantize(customer.wallet_balance + amount)
customer.save(update_fields=['wallet_balance'])
# Создаём транзакцию возврата
WalletTransaction.objects.create(
customer=customer,
amount=amount,
transaction_type='deposit',
order=order,
description=f'Возврат платежа по заказу #{order.order_number}',
created_by=user
)
return amount
@staticmethod @staticmethod
@transaction.atomic @transaction.atomic
def adjust_balance(customer_id, amount, description, user): def adjust_balance(customer_id, amount, description, user):

View File

@@ -45,18 +45,12 @@
<div class="row mb-2"> <div class="row mb-2">
<div class="col-md-4"><strong>Статус:</strong></div> <div class="col-md-4"><strong>Статус:</strong></div>
<div class="col-md-8"> <div class="col-md-8">
{% if order.status == 'new' %} {% if order.status %}
<span class="badge bg-primary">Новый</span> <span class="badge" style="background-color: {{ order.status.color }}; color: #fff;">
{% elif order.status == 'confirmed' %} {{ order.status.label|default:order.status.name }}
<span class="badge bg-success">Подтвержден</span> </span>
{% elif order.status == 'in_assembly' %} {% else %}
<span class="badge bg-warning">В сборке</span> <span class="badge bg-secondary">Не установлен</span>
{% elif order.status == 'in_delivery' %}
<span class="badge bg-info">В доставке</span>
{% elif order.status == 'delivered' %}
<span class="badge bg-success">Доставлен</span>
{% elif order.status == 'cancelled' %}
<span class="badge bg-danger">Отменен</span>
{% endif %} {% endif %}
</div> </div>
</div> </div>
@@ -126,7 +120,7 @@
</div> </div>
<div class="row mb-2"> <div class="row mb-2">
<div class="col-md-4"><strong>Стоимость доставки:</strong></div> <div class="col-md-4"><strong>Стоимость доставки:</strong></div>
<div class="col-md-8">{{ order.delivery_cost }} руб.</div> <div class="col-md-8">{{ order.delivery_cost|floatformat:2 }} руб.</div>
</div> </div>
{% else %} {% else %}
<div class="row mb-2"> <div class="row mb-2">
@@ -207,25 +201,25 @@
</a> </a>
{% endif %} {% endif %}
</td> </td>
<td>{{ item.quantity }}</td> <td>{{ item.quantity }} шт.</td>
<td> <td>
{{ item.price }} руб. {{ item.price|floatformat:2 }} руб.
{% if item.is_custom_price %} {% if item.is_custom_price %}
<span class="badge bg-warning ms-1">Изменена</span> <span class="badge bg-warning ms-1">Изменена</span>
<br> <br>
<small class="text-muted"> <small class="text-muted">
Оригинальная: {{ item.original_price }} руб. Оригинальная: {{ item.original_price|floatformat:2 }} руб.
{% if item.price_difference %} {% if item.price_difference %}
{% if item.price_difference > 0 %} {% if item.price_difference > 0 %}
<span class="text-success">(+{{ item.price_difference }} руб.)</span> <span class="text-success">(+{{ item.price_difference|floatformat:2 }} руб.)</span>
{% else %} {% else %}
<span class="text-danger">({{ item.price_difference }} руб.)</span> <span class="text-danger">({{ item.price_difference|floatformat:2 }} руб.)</span>
{% endif %} {% endif %}
{% endif %} {% endif %}
</small> </small>
{% endif %} {% endif %}
</td> </td>
<td><strong>{{ item.get_total_price }} руб.</strong></td> <td><strong>{{ item.get_total_price|floatformat:2 }} руб.</strong></td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
@@ -315,27 +309,27 @@
{% if order.is_delivery %} {% if order.is_delivery %}
<div class="row mb-2"> <div class="row mb-2">
<div class="col-6"><strong>Доставка:</strong></div> <div class="col-6"><strong>Доставка:</strong></div>
<div class="col-6 text-end">{{ order.delivery_cost }} руб.</div> <div class="col-6 text-end">{{ order.delivery_cost|floatformat:2 }} руб.</div>
</div> </div>
{% endif %} {% endif %}
{% if order.discount_amount > 0 %} {% if order.discount_amount > 0 %}
<div class="row mb-2"> <div class="row mb-2">
<div class="col-6"><strong>Скидка:</strong></div> <div class="col-6"><strong>Скидка:</strong></div>
<div class="col-6 text-end text-danger">-{{ order.discount_amount }} руб.</div> <div class="col-6 text-end text-danger">-{{ order.discount_amount|floatformat:2 }} руб.</div>
</div> </div>
{% endif %} {% endif %}
<hr> <hr>
<div class="row mb-3"> <div class="row mb-3">
<div class="col-6"><strong>Итого:</strong></div> <div class="col-6"><strong>Итого:</strong></div>
<div class="col-6 text-end"><h5>{{ order.total_amount }} руб.</h5></div> <div class="col-6 text-end"><h5>{{ order.total_amount|floatformat:2 }} руб.</h5></div>
</div> </div>
<div class="row mb-2"> <div class="row mb-2">
<div class="col-6"><strong>Оплачено:</strong></div> <div class="col-6"><strong>Оплачено:</strong></div>
<div class="col-6 text-end">{{ order.amount_paid }} руб.</div> <div class="col-6 text-end">{{ order.amount_paid|floatformat:2 }} руб.</div>
</div> </div>
<div class="row mb-2"> <div class="row mb-2">
<div class="col-6"><strong>К оплате:</strong></div> <div class="col-6"><strong>К оплате:</strong></div>
<div class="col-6 text-end text-danger"><strong>{{ order.amount_due }} руб.</strong></div> <div class="col-6 text-end text-danger"><strong>{{ order.amount_due|floatformat:2 }} руб.</strong></div>
</div> </div>
<div class="row mb-2"> <div class="row mb-2">
<div class="col-12"> <div class="col-12">
@@ -349,12 +343,6 @@
{% endif %} {% endif %}
</div> </div>
</div> </div>
<div class="row mb-2">
<div class="col-12">
<strong>Способ оплаты:</strong><br>
{{ order.get_payment_method_display }}
</div>
</div>
</div> </div>
</div> </div>
@@ -368,10 +356,13 @@
<ul class="list-group list-group-flush"> <ul class="list-group list-group-flush">
{% for payment in order.payments.all %} {% for payment in order.payments.all %}
<li class="list-group-item"> <li class="list-group-item">
<div><strong>{{ payment.amount }} руб.</strong></div> <div><strong>{{ payment.amount|floatformat:2 }} руб.</strong></div>
<small class="text-muted"> <small class="text-muted">
{{ payment.payment_date|date:"d.m.Y H:i" }}<br> {{ payment.payment_date|date:"d.m.Y H:i" }}<br>
{{ payment.get_payment_method_display }} {{ payment.payment_method.name }}
{% if payment.notes %}
<br><em>{{ payment.notes }}</em>
{% endif %}
{% if payment.created_by %} {% if payment.created_by %}
<br>Принял: {{ payment.created_by.get_full_name }} <br>Принял: {{ payment.created_by.get_full_name }}
{% endif %} {% endif %}

View File

@@ -111,7 +111,7 @@
</div> </div>
</div> </div>
<form method="post" id="order-form" {% if is_draft %}data-is-draft="true"{% endif %}> <form method="post" id="order-form">
{% csrf_token %} {% csrf_token %}
<!-- Основная информация --> <!-- Основная информация -->
@@ -247,7 +247,7 @@
Изменена Изменена
</span> </span>
<small class="text-muted original-price-info position-absolute" style="display: none; top: calc(100% + 2px); left: 0; white-space: nowrap;"> <small class="text-muted original-price-info position-absolute" style="display: none; top: calc(100% + 2px); left: 0; white-space: nowrap;">
Оригинальная: <span class="original-price-value"></span> руб. Оригинальная: <span class="original-price-value"></span>&nbsp;руб.
</small> </small>
</div> </div>
</div> </div>
@@ -275,7 +275,7 @@
<div id="order-items-total-section" class="border-top pt-3 mt-3 mb-3"> <div id="order-items-total-section" class="border-top pt-3 mt-3 mb-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">Сумма товаров:</p> <p class="mb-0 text-muted"><i class="bi bi-calculator"></i> Сумма товаров:</p>
</div> </div>
<div class="col-auto"> <div class="col-auto">
<h5 class="mb-0 text-primary"> <h5 class="mb-0 text-primary">
@@ -317,7 +317,7 @@
Изменена Изменена
</span> </span>
<small class="text-muted original-price-info position-absolute" style="display: none; top: calc(100% + 2px); left: 0; white-space: nowrap;"> <small class="text-muted original-price-info position-absolute" style="display: none; top: calc(100% + 2px); left: 0; white-space: nowrap;">
Оригинальная: <span class="original-price-value"></span> руб. Оригинальная: <span class="original-price-value"></span>&nbsp;руб.
</small> </small>
</div> </div>
</div> </div>
@@ -544,7 +544,7 @@
{% endif %} {% endif %}
<small class="d-block text-muted mt-1"> <small class="d-block text-muted mt-1">
<i class="bi bi-info-circle"></i> <i class="bi bi-info-circle"></i>
Оставьте пустым для автоматического расчета (бесплатно от 100 руб., иначе 15 руб.) Оставьте пустым для автоматического расчета стоимости доставки
</small> </small>
</div> </div>
</div> </div>
@@ -572,15 +572,10 @@
<!-- Оплата (смешанная оплата) --> <!-- Оплата (смешанная оплата) -->
<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 d-flex justify-content-between align-items-center">
<h5 class="mb-0">Оплата</h5> <h5 class="mb-0"><i class="bi bi-credit-card"></i> Оплата</h5>
<div> <button type="button" class="btn btn-sm btn-success" id="add-payment-btn">
<span class="badge bg-{% if order.payment_status == 'paid' %}success{% elif order.payment_status == 'partial' %}warning{% else %}danger{% endif %} me-2"> <i class="bi bi-plus-circle"></i> Добавить платеж
{{ order.get_payment_status_display }} </button>
</span>
<button type="button" class="btn btn-sm btn-success" id="add-payment-btn">
<i class="bi bi-plus-circle"></i> Добавить платеж
</button>
</div>
</div> </div>
<div class="card-body"> <div class="card-body">
@@ -588,7 +583,7 @@
{% if order.customer %} {% if order.customer %}
<div class="alert alert-info d-flex justify-content-between align-items-center"> <div class="alert alert-info d-flex justify-content-between align-items-center">
<div> <div>
<strong>Кошелёк клиента:</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 %}
@@ -657,7 +652,7 @@
<div id="payments-total-section" class="border-top pt-3 mt-3 mb-3"> <div id="payments-total-section" class="border-top pt-3 mt-3 mb-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">Внесено платежей:</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">
@@ -759,37 +754,90 @@
return; return;
} }
// Найти существующую форму платежа без метода, иначе добавить новую // Всегда добавляем новую строку платежа
let formEl = document.querySelector('#payments-container .payment-form:last-child'); const formEl = addPaymentRow();
if (!formEl) {
formEl = addPaymentRow();
}
const sel = formEl.querySelector('select[id^="id_payments-"][id$="-payment_method"]'); const sel = formEl.querySelector('select[id^="id_payments-"][id$="-payment_method"]');
const amt = formEl.querySelector('input[id^="id_payments-"][id$="-amount"]'); const amt = formEl.querySelector('input[id^="id_payments-"][id$="-amount"]');
const notes = formEl.querySelector('textarea[id^="id_payments-"][id$="-notes"]');
selectAccountBalance(sel); // Загружаем список способов оплаты
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) { if (amt) {
amt.value = amount.toFixed(2); const maxUsable = Math.min(walletBalance, amountDue);
amt.setAttribute('max', Math.min(walletBalance, amountDue).toFixed(2)); 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();
}
});
} }
} }
const applyMaxBtn = document.getElementById('apply-wallet-max-btn'); // Обработчики кнопок применения кошелька
const applyAmountBtn = document.getElementById('apply-wallet-amount-btn'); document.addEventListener('DOMContentLoaded', function() {
const amountInput = document.getElementById('apply-wallet-amount-input'); 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) { if (applyMaxBtn) {
applyMaxBtn.addEventListener('click', function() { applyMaxBtn.addEventListener('click', function() {
const maxUsable = Math.min(walletBalance, amountDue); const maxUsable = Math.min(walletBalance, amountDue);
applyWallet(maxUsable); applyWallet(maxUsable);
}); });
} }
if (applyAmountBtn && amountInput) { if (applyAmountBtn && amountInput) {
applyAmountBtn.addEventListener('click', function() { applyAmountBtn.addEventListener('click', function() {
const val = parseFloat((amountInput.value || '0').replace(',', '.')) || 0; const val = parseFloat((amountInput.value || '0').replace(',', '.')) || 0;
applyWallet(val); applyWallet(val);
}); });
} }
});
// Автозаполнение при выборе "С баланса счёта" // Автозаполнение при выборе "С баланса счёта"
document.getElementById('payments-container').addEventListener('change', function(e) { document.getElementById('payments-container').addEventListener('change', function(e) {
@@ -812,7 +860,7 @@
<div class="card mb-3"> <div class="card mb-3">
<div class="card-header"> <div class="card-header">
<h5 class="mb-0">Дополнительно</h5> <h5 class="mb-0"><i class="bi bi-three-dots"></i> Дополнительно</h5>
</div> </div>
<div class="card-body"> <div class="card-body">
<div class="mb-3 form-check"> <div class="mb-3 form-check">
@@ -833,23 +881,12 @@
<div class="col"> <div class="col">
<div class="d-flex gap-2"> <div class="d-flex gap-2">
{% if is_create_page %} {% if is_create_page %}
<!-- На странице создания показываем обе кнопки --> <!-- На странице создания показываем одну кнопку -->
<button type="submit" name="create_order" class="btn btn-primary btn-lg"> <button type="submit" class="btn btn-primary btn-lg">
<i class="bi bi-check-circle"></i> Создать заказ <i class="bi bi-check-circle"></i> Создать заказ
</button> </button>
<button type="submit" name="save_as_draft" class="btn btn-secondary btn-lg">
<i class="bi bi-save"></i> Сохранить как черновик
</button>
{% elif is_draft %}
<!-- Для черновиков показываем кнопку финализации и обычного сохранения -->
<button type="submit" name="finalize_draft" class="btn btn-success btn-lg">
<i class="bi bi-check-circle-fill"></i> Финализировать черновик
</button>
<button type="submit" class="btn btn-primary btn-lg">
<i class="bi bi-save"></i> Сохранить изменения
</button>
{% else %} {% else %}
<!-- Для обычных заказов - только сохранение --> <!-- Для любых заказов - только сохранение -->
<button type="submit" class="btn btn-primary btn-lg"> <button type="submit" class="btn btn-primary btn-lg">
<i class="bi bi-save"></i> {{ button_text }} <i class="bi bi-save"></i> {{ button_text }}
</button> </button>
@@ -865,27 +902,11 @@
</div> </div>
<script> <script>
// Глобально определяем initOrderItemSelect2, чтобы она была доступна при вызове ниже // Глобально определяем initOrderItemSelect2
window.initOrderItemSelect2 = function(element) { window.initOrderItemSelect2 = function(element) {
console.log('[initOrderItemSelect2] Вызвана для элемента:', element);
// Проверяем доступность jQuery
if (typeof $ === 'undefined') {
console.error('[initOrderItemSelect2] jQuery не загружен!');
return;
}
const $element = $(element); const $element = $(element);
const formIndex = element.dataset.formIndex; const formIndex = element.dataset.formIndex;
console.log('[initOrderItemSelect2] formIndex:', formIndex);
// Проверяем, что функция initProductSelect2 доступна
if (typeof window.initProductSelect2 !== 'function') {
console.error('[initOrderItemSelect2] window.initProductSelect2 не определена. Убедитесь, что select2-product-search.js загружен.');
return;
}
console.log('[initOrderItemSelect2] Инициализация Select2 через initProductSelect2...');
// Инициализируем Select2 с AJAX поиском // Инициализируем Select2 с AJAX поиском
window.initProductSelect2( window.initProductSelect2(
element, element,
@@ -945,30 +966,21 @@ window.initOrderItemSelect2 = function(element) {
form.querySelector('[name$="-product_kit"]').value = ''; form.querySelector('[name$="-product_kit"]').value = '';
form.querySelector('[name$="-price"]').value = ''; form.querySelector('[name$="-price"]').value = '';
}); });
console.log('[initOrderItemSelect2] Инициализация завершена успешно');
}; };
// Ждем пока jQuery загрузится // Ждем пока jQuery загрузится
function initCustomerSelect2() { function initCustomerSelect2() {
if (typeof $ === 'undefined') { if (typeof $ === 'undefined') {
console.log('jQuery еще не загружен, ждем...');
setTimeout(initCustomerSelect2, 100); setTimeout(initCustomerSelect2, 100);
return; return;
} }
console.log('=== ИНИЦИАЛИЗАЦИЯ SELECT2 ДЛЯ CUSTOMER ===');
const $customerSelect = $('#id_customer'); const $customerSelect = $('#id_customer');
const ajaxUrl = '{% url "customers:api-search-customers" %}'; const ajaxUrl = '{% url "customers:api-search-customers" %}';
console.log('1. Поле customer найдено:', $customerSelect.length); // Сохраняем текущее значение перед очисткой
console.log('2. AJAX URL:', ajaxUrl);
// Сохраняем текущее значение перед очисткой (может быть при редактировании черновика)
const currentValue = $customerSelect.val(); const currentValue = $customerSelect.val();
const currentText = $customerSelect.find(':selected').text(); const currentText = $customerSelect.find(':selected').text();
console.log('Сохраняем текущее значение:', currentValue, currentText);
// НЕ очищаем, если у нас есть текущий выбранный клиент // НЕ очищаем, если у нас есть текущий выбранный клиент
if (!currentValue) { if (!currentValue) {
@@ -989,19 +1001,14 @@ function initCustomerSelect2() {
dataType: 'json', dataType: 'json',
delay: 500, delay: 500,
data: function(params) { data: function(params) {
console.log('3. AJAX запрос с query:', params.term);
return { return {
q: params.term || '' q: params.term || ''
}; };
}, },
processResults: function(data) { processResults: function(data) {
console.log('4. AJAX ответ получен:', data);
return { return {
results: data.results || [] results: data.results || []
}; };
},
error: function(xhr, status, error) {
console.error('5. AJAX ОШИБКА:', status, error, xhr.responseText);
} }
}, },
templateResult: formatCustomerOption, templateResult: formatCustomerOption,
@@ -1009,22 +1016,13 @@ function initCustomerSelect2() {
escapeMarkup: function(markup) { return markup; } escapeMarkup: function(markup) { return markup; }
}); });
console.log('6. Select2 инициализирован'); // Восстанавливаем значение если оно было
// Восстанавливаем значение если оно было (для редактирования черновика)
if (currentValue && currentText) { if (currentValue && currentText) {
console.log('Восстанавливаем значение:', currentValue, 'Текст:', currentText);
// Select2 видит option элемент в DOM (так как мы его не очищали)
// Просто устанавливаем значение через Select2 API
$customerSelect.val(currentValue); $customerSelect.val(currentValue);
console.log('Значение восстановлено:', $customerSelect.val());
} }
// Select2 готов и есть предзаполненное значение // Select2 готов и есть предзаполненное значение
if (currentValue && window.DraftCreator) { if (currentValue && window.DraftCreator) {
console.log('7. Уведомляем DraftCreator о предзаполненном клиенте');
setTimeout(function() { setTimeout(function() {
window.DraftCreator.triggerDraftCreation(); window.DraftCreator.triggerDraftCreation();
}, 100); }, 100);
@@ -1032,68 +1030,46 @@ function initCustomerSelect2() {
// Слушаем события // Слушаем события
$customerSelect.on('select2:open', function(e) { $customerSelect.on('select2:open', function(e) {
console.log('7. Dropdown открыт');
// Устанавливаем фокус на input field
setTimeout(function() { setTimeout(function() {
$('.select2-search__field:visible').first().focus(); $('.select2-search__field:visible').first().focus();
}, 100); }, 100);
}); });
$customerSelect.on('select2:searching', function(e) { // Обработчик для перехвата ПЕРЕД выбором
console.log('8. Поиск с term:', e.params.term);
});
// Обработчик для перехвата ПЕРЕД выбором (используется для фальшивых опций)
$customerSelect.on('select2:selecting', function(e) { $customerSelect.on('select2:selecting', function(e) {
console.log('9. Событие select2:selecting, e.params:', e.params);
// Проверяем наличие e.params и e.params.data // Проверяем наличие e.params и e.params.data
if (!e.params || !e.params.data) { if (!e.params || !e.params.data) {
console.log('9a. Нет данных в e.params на selecting, пропускаем');
return; return;
} }
const data = e.params.data; const data = e.params.data;
console.log('9b. Попытка выбрать элемент (перед выбором):', data);
if (data.is_create_option || data.id === '__create_new__') { if (data.is_create_option || data.id === '__create_new__') {
console.log('9c. Это опция создания клиента - предотвращаем выбор и открываем модаль');
// Предотвращаем выбор этой опции // Предотвращаем выбор этой опции
e.preventDefault(); e.preventDefault();
// Закрываем dropdown
$customerSelect.select2('close'); $customerSelect.select2('close');
// Очищаем значение
$customerSelect.val(null).trigger('change.select2'); $customerSelect.val(null).trigger('change.select2');
// Открываем модаль
window.openCreateCustomerModal(data.search_text); window.openCreateCustomerModal(data.search_text);
return false; return false;
} }
console.log('9d. Обычный клиент, разрешаем выбор');
}); });
$customerSelect.on('select2:select', function(e) { $customerSelect.on('select2:select', function(e) {
console.log('10. Событие select2:select, e.params:', e.params);
// Проверяем наличие e.params и e.params.data // Проверяем наличие e.params и e.params.data
if (!e.params || !e.params.data) { if (!e.params || !e.params.data) {
console.log('10a. Нет данных в e.params, пропускаем обработку');
return; return;
} }
const data = e.params.data; const data = e.params.data;
console.log('10b. Выбран элемент:', data);
if (data.is_create_option || data.id === '__create_new__') { if (data.is_create_option || data.id === '__create_new__') {
console.log('11. Открываем модальное окно для создания клиента');
this.value = ''; this.value = '';
// Триггерим нативное change событие
const changeEvent = new Event('change', { bubbles: true }); const changeEvent = new Event('change', { bubbles: true });
this.dispatchEvent(changeEvent); this.dispatchEvent(changeEvent);
window.openCreateCustomerModal(data.search_text); window.openCreateCustomerModal(data.search_text);
} else { } else {
// Триггерим нативное change событие для других обработчиков
console.log('12. Триггерим нативное change событие для customer ID:', data.id);
const changeEvent = new Event('change', { bubbles: true }); const changeEvent = new Event('change', { bubbles: true });
this.dispatchEvent(changeEvent); this.dispatchEvent(changeEvent);
} }
@@ -1195,58 +1171,43 @@ if (typeof $ !== 'undefined') {
// ВАЖНО: Этот код должен быть ВНЕ jQuery document.ready, // ВАЖНО: Этот код должен быть ВНЕ jQuery document.ready,
// чтобы выполниться после полной загрузки DOM // чтобы выполниться после полной загрузки DOM
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
console.log('[DELIVERY TYPE] DOM loaded, initializing...');
const deliveryTypeRadios = document.querySelectorAll('input[name="delivery-type"]'); const deliveryTypeRadios = document.querySelectorAll('input[name="delivery-type"]');
const isDeliveryCheckbox = document.getElementById('{{ form.is_delivery.id_for_label }}'); const isDeliveryCheckbox = document.getElementById('{{ form.is_delivery.id_for_label }}');
const deliveryModeFields = document.getElementById('delivery-mode-fields'); const deliveryModeFields = document.getElementById('delivery-mode-fields');
const pickupFields = document.getElementById('pickup-fields'); const pickupFields = document.getElementById('pickup-fields');
function syncDeliveryTypeFromRadio() { function syncDeliveryTypeFromRadio() {
// Синхронизирует чекбокс И UI с выбранной радиокнопкой
const selectedType = document.querySelector('input[name="delivery-type"]:checked').value; const selectedType = document.querySelector('input[name="delivery-type"]:checked').value;
if (selectedType === 'delivery') { if (selectedType === 'delivery') {
// Доставка
isDeliveryCheckbox.checked = true; isDeliveryCheckbox.checked = true;
deliveryModeFields.style.display = 'block'; deliveryModeFields.style.display = 'block';
pickupFields.style.display = 'none'; pickupFields.style.display = 'none';
console.log('[DELIVERY TYPE] Delivery selected');
} else { } else {
// Самовывоз
isDeliveryCheckbox.checked = false; isDeliveryCheckbox.checked = false;
deliveryModeFields.style.display = 'none'; deliveryModeFields.style.display = 'none';
pickupFields.style.display = 'block'; pickupFields.style.display = 'block';
console.log('[DELIVERY TYPE] Pickup selected');
} }
} }
function syncUIFromCheckbox() { function syncUIFromCheckbox() {
// Синхронизирует ТОЛЬКО UI (не трогает чекбокс) с текущим значением чекбокса
if (isDeliveryCheckbox.checked) { if (isDeliveryCheckbox.checked) {
// Синхронизируем радиокнопки
document.getElementById('delivery-type-delivery').checked = true; document.getElementById('delivery-type-delivery').checked = true;
// Синхронизируем видимость секций
deliveryModeFields.style.display = 'block'; deliveryModeFields.style.display = 'block';
pickupFields.style.display = 'none'; pickupFields.style.display = 'none';
console.log('[DELIVERY TYPE] UI synced: showing delivery fields');
} else { } else {
// Синхронизируем радиокнопки
document.getElementById('delivery-type-pickup').checked = true; document.getElementById('delivery-type-pickup').checked = true;
// Синхронизируем видимость секций
deliveryModeFields.style.display = 'none'; deliveryModeFields.style.display = 'none';
pickupFields.style.display = 'block'; pickupFields.style.display = 'block';
console.log('[DELIVERY TYPE] UI synced: showing pickup fields');
} }
} }
// Обработчики для кнопок - при клике синхронизируем чекбокс с радиокнопками // Обработчики для кнопок
deliveryTypeRadios.forEach(radio => { deliveryTypeRadios.forEach(radio => {
radio.addEventListener('change', syncDeliveryTypeFromRadio); radio.addEventListener('change', syncDeliveryTypeFromRadio);
}); });
// Инициализация при загрузке - синхронизируем UI с текущим значением чекбокса (из формы) // Инициализация при загрузке
console.log('[DELIVERY TYPE] Initializing delivery type, checkbox value:', isDeliveryCheckbox.checked);
syncUIFromCheckbox(); syncUIFromCheckbox();
// Показ/скрытие полей получателя // Показ/скрытие полей получателя
@@ -1262,44 +1223,27 @@ document.addEventListener('DOMContentLoaded', function() {
} }
customerIsRecipientCheckbox.addEventListener('change', toggleRecipientFields); customerIsRecipientCheckbox.addEventListener('change', toggleRecipientFields);
toggleRecipientFields(); // Инициализация при загрузке toggleRecipientFields();
// === РАСЧЁТ ИТОГОВОЙ СУММЫ ТОВАРОВ === // === РАСЧЁТ ИТОГОВОЙ СУММЫ ТОВАРОВ ===
function calculateOrderItemsTotal() { function calculateOrderItemsTotal() {
// Собираем все видимые (не удалённые) формы товаров
const visibleForms = Array.from(document.querySelectorAll('.order-item-form')) const visibleForms = Array.from(document.querySelectorAll('.order-item-form'))
.filter(form => !form.classList.contains('deleted')); .filter(form => !form.classList.contains('deleted'));
let total = 0; let total = 0;
console.log('[TOTAL] Calculating total for', visibleForms.length, 'forms');
// Для каждого товара: количество × цена
visibleForms.forEach((form, index) => { visibleForms.forEach((form, index) => {
const quantityField = form.querySelector('[name$="-quantity"]'); const quantityField = form.querySelector('[name$="-quantity"]');
const priceField = form.querySelector('[name$="-price"]'); const priceField = form.querySelector('[name$="-price"]');
console.log(`[TOTAL] Form ${index}:`, form);
console.log(`[TOTAL] Form ${index}: quantityField=${quantityField}, priceField=${priceField}`);
const allInputs = form.querySelectorAll('input');
console.log(`[TOTAL] Form ${index}: All inputs:`, allInputs);
allInputs.forEach((input, i) => {
console.log(` Input ${i}: name="${input.name}", id="${input.id}", type="${input.type}"`);
});
if (quantityField && priceField) { if (quantityField && priceField) {
const quantity = parseFloat(quantityField.value) || 0; const quantity = parseFloat(quantityField.value) || 0;
// Заменяем запятую на точку для корректного парсинга
const priceValue = priceField.value.replace(',', '.'); const priceValue = priceField.value.replace(',', '.');
const price = parseFloat(priceValue) || 0; const price = parseFloat(priceValue) || 0;
console.log(`[TOTAL] Form ${index}: quantity=${quantityField.value} (parsed: ${quantity}), price="${priceField.value}" (parsed: ${price}), subtotal=${quantity * price}`);
total += quantity * price; total += quantity * price;
} else {
console.log(`[TOTAL] Form ${index}: SKIPPED - missing fields!`);
} }
}); });
console.log('[TOTAL] Final total:', total);
return total; return total;
} }
@@ -1308,7 +1252,6 @@ document.addEventListener('DOMContentLoaded', function() {
const totalElement = document.getElementById('order-items-total-value'); const totalElement = document.getElementById('order-items-total-value');
if (totalElement) { if (totalElement) {
// Форматируем до 2 знаков после запятой
totalElement.textContent = total.toFixed(2); totalElement.textContent = total.toFixed(2);
} }
} }
@@ -1373,19 +1316,15 @@ document.addEventListener('DOMContentLoaded', function() {
function addNewForm() { function addNewForm() {
const formCount = parseInt(totalFormsInput.value); const formCount = parseInt(totalFormsInput.value);
// Клонируем шаблон
const templateContent = emptyFormTemplate.content || emptyFormTemplate; const templateContent = emptyFormTemplate.content || emptyFormTemplate;
const formTemplate = templateContent.querySelector('.order-item-form'); const formTemplate = templateContent.querySelector('.order-item-form');
const newForm = formTemplate.cloneNode(true); const newForm = formTemplate.cloneNode(true);
// Заменяем __prefix__ на реальный индекс
newForm.innerHTML = newForm.innerHTML.replace(/__prefix__/g, formCount); newForm.innerHTML = newForm.innerHTML.replace(/__prefix__/g, formCount);
newForm.dataset.formIndex = formCount; newForm.dataset.formIndex = formCount;
// Добавляем форму в контейнер
container.appendChild(newForm); container.appendChild(newForm);
// Обновляем счетчик форм
totalFormsInput.value = formCount + 1; totalFormsInput.value = formCount + 1;
// Инициализируем Select2 для новой формы // Инициализируем Select2 для новой формы
@@ -1393,22 +1332,15 @@ document.addEventListener('DOMContentLoaded', function() {
select2Element.dataset.formIndex = formCount; select2Element.dataset.formIndex = formCount;
if (typeof window.initOrderItemSelect2 === 'function') { if (typeof window.initOrderItemSelect2 === 'function') {
window.initOrderItemSelect2(select2Element); window.initOrderItemSelect2(select2Element);
} else {
console.error('window.initOrderItemSelect2 is not available');
} }
// Инициализируем отслеживание цены
initPriceTracking(newForm); initPriceTracking(newForm);
// Добавляем обработчик удаления
const removeBtn = newForm.querySelector('.remove-item-btn'); const removeBtn = newForm.querySelector('.remove-item-btn');
removeBtn.addEventListener('click', function() { removeBtn.addEventListener('click', function() {
removeForm(newForm); removeForm(newForm);
}); });
console.log(`Added new form with index ${formCount}`);
// Обновляем итоговую сумму после добавления формы
updateOrderItemsTotal(); updateOrderItemsTotal();
return newForm; return newForm;
@@ -1416,31 +1348,23 @@ document.addEventListener('DOMContentLoaded', function() {
// Функция для удаления формы // Функция для удаления формы
function removeForm(form) { function removeForm(form) {
// Показываем диалог подтверждения
if (!confirm('Вы действительно хотите удалить этот товар из заказа?')) { if (!confirm('Вы действительно хотите удалить этот товар из заказа?')) {
return; // Пользователь отменил удаление return;
} }
const deleteCheckbox = form.querySelector('input[name$="-DELETE"]'); const deleteCheckbox = form.querySelector('input[name$="-DELETE"]');
const idField = form.querySelector('input[name$="-id"]'); const idField = form.querySelector('input[name$="-id"]');
// Если форма уже сохранена (есть ID), помечаем на удаление
if (idField && idField.value) { if (idField && idField.value) {
deleteCheckbox.checked = true; deleteCheckbox.checked = true;
form.classList.add('deleted'); form.classList.add('deleted');
form.style.display = 'none'; // Скрываем форму визуально form.style.display = 'none';
console.log('Form marked for deletion, id:', idField.value);
// Обновляем итоговую сумму после удаления
updateOrderItemsTotal(); updateOrderItemsTotal();
// Триггерим автосохранение для отправки изменений
if (typeof window.orderAutosave !== 'undefined' && window.orderAutosave.scheduleAutosave) { if (typeof window.orderAutosave !== 'undefined' && window.orderAutosave.scheduleAutosave) {
window.orderAutosave.scheduleAutosave(); window.orderAutosave.scheduleAutosave();
} }
} else { } else {
// Если форма новая, просто удаляем из DOM
form.remove(); form.remove();
console.log('Form removed from DOM');
// Обновляем итоговую сумму после удаления
updateOrderItemsTotal(); updateOrderItemsTotal();
} }
} }
@@ -1461,13 +1385,12 @@ document.addEventListener('DOMContentLoaded', function() {
addNewForm(); addNewForm();
} }
// Инициализируем итоговую сумму при загрузке страницы // Инициализируем итоговую сумму
updateOrderItemsTotal(); updateOrderItemsTotal();
// Валидация перед отправкой (убрана обязательность товаров — можно сохранить пустой заказ) // Валидация перед отправкой
document.getElementById('order-form').addEventListener('submit', function(e) { document.getElementById('order-form').addEventListener('submit', function(e) {
// Валидация отключена — заказ можно сохранить без товаров // Заказ можно сохранить без товаров
// Товары можно добавить позже
}); });
// === ВРЕМЕННЫЕ КОМПЛЕКТЫ === // === ВРЕМЕННЫЕ КОМПЛЕКТЫ ===
@@ -1745,55 +1668,35 @@ document.addEventListener('DOMContentLoaded', function() {
// Функция для добавления нового платежа // Функция для добавления нового платежа
function addNewPayment() { function addNewPayment() {
console.log('[addNewPayment] START, paymentFormCount:', paymentFormCount);
// ВАЖНО: Получаем HTML из template.content и заменяем __prefix__
const tempContainer = document.createElement('div'); const tempContainer = document.createElement('div');
tempContainer.appendChild(paymentFormTemplate.content.cloneNode(true)); tempContainer.appendChild(paymentFormTemplate.content.cloneNode(true));
const templateHtml = tempContainer.innerHTML; const templateHtml = tempContainer.innerHTML;
console.log('[addNewPayment] templateHtml (first 200 chars):', templateHtml.substring(0, 200));
const replacedHtml = templateHtml.replace(/__prefix__/g, paymentFormCount); const replacedHtml = templateHtml.replace(/__prefix__/g, paymentFormCount);
console.log('[addNewPayment] replacedHtml (first 200 chars):', replacedHtml.substring(0, 200));
// Создаем элемент из обработанного HTML
const tempDiv = document.createElement('div'); const tempDiv = document.createElement('div');
tempDiv.innerHTML = replacedHtml; tempDiv.innerHTML = replacedHtml;
const newPaymentDiv = tempDiv.firstElementChild; const newPaymentDiv = tempDiv.firstElementChild;
newPaymentDiv.setAttribute('data-form-index', paymentFormCount); newPaymentDiv.setAttribute('data-form-index', paymentFormCount);
// Добавляем в контейнер
paymentsContainer.appendChild(newPaymentDiv); paymentsContainer.appendChild(newPaymentDiv);
console.log('[addNewPayment] Added payment form, index:', paymentFormCount);
// DEBUG: Проверяем что поля действительно в DOM
const allInputs = newPaymentDiv.querySelectorAll('input, select, textarea');
console.log('[addNewPayment] Total inputs in new form:', allInputs.length);
allInputs.forEach((inp, idx) => {
console.log(` [${idx}] name="${inp.name}", type="${inp.type || inp.tagName}"`);
});
// Обновляем счетчик форм
paymentFormCount++; paymentFormCount++;
document.querySelector('[name="payments-TOTAL_FORMS"]').value = paymentFormCount; document.querySelector('[name="payments-TOTAL_FORMS"]').value = paymentFormCount;
// Добавляем обработчик удаления
const removeBtn = newPaymentDiv.querySelector('.remove-payment-btn'); const removeBtn = newPaymentDiv.querySelector('.remove-payment-btn');
removeBtn.addEventListener('click', function() { removeBtn.addEventListener('click', function() {
removePayment(newPaymentDiv); removePayment(newPaymentDiv);
}); });
// Добавляем обработчики для автоматического пересчета
const amountField = newPaymentDiv.querySelector('[name$="-amount"]'); const amountField = newPaymentDiv.querySelector('[name$="-amount"]');
if (amountField) { if (amountField) {
amountField.addEventListener('input', updatePaymentsTotal); amountField.addEventListener('input', updatePaymentsTotal);
} }
// Загружаем payment methods в select
loadPaymentMethods(newPaymentDiv.querySelector('select[name$="-payment_method"]')); loadPaymentMethods(newPaymentDiv.querySelector('select[name$="-payment_method"]'));
// Обновляем итоговую сумму
updatePaymentsTotal(); updatePaymentsTotal();
return newPaymentDiv; return newPaymentDiv;
@@ -1808,19 +1711,14 @@ document.addEventListener('DOMContentLoaded', function() {
const deleteCheckbox = form.querySelector('input[name$="-DELETE"]'); const deleteCheckbox = form.querySelector('input[name$="-DELETE"]');
const idField = form.querySelector('input[name$="-id"]'); const idField = form.querySelector('input[name$="-id"]');
// Если форма уже сохранена (есть ID), помечаем на удаление
if (idField && idField.value) { if (idField && idField.value) {
deleteCheckbox.checked = true; deleteCheckbox.checked = true;
form.classList.add('deleted'); form.classList.add('deleted');
form.style.display = 'none'; form.style.display = 'none';
console.log('Payment form marked for deletion, id:', idField.value);
} else { } else {
// Если форма новая, просто удаляем из DOM
form.remove(); form.remove();
console.log('Payment form removed from DOM');
} }
// Обновляем итоговую сумму
updatePaymentsTotal(); updatePaymentsTotal();
} }
@@ -1860,10 +1758,10 @@ document.addEventListener('DOMContentLoaded', function() {
field.addEventListener('input', updatePaymentsTotal); field.addEventListener('input', updatePaymentsTotal);
}); });
// Инициализируем итоговую сумму при загрузке страницы // Инициализируем итоговую сумму
updatePaymentsTotal(); updatePaymentsTotal();
// Закрытие обработчика DOMContentLoaded для управления типом доставки и остальных функций // Закрытие DOMContentLoaded
}); });
</script> </script>
@@ -2236,45 +2134,6 @@ if (!document.getElementById('notification-styles')) {
const form = document.getElementById('order-form'); const form = document.getElementById('order-form');
if (form) { if (form) {
form.addEventListener('submit', function(e) { form.addEventListener('submit', function(e) {
console.log('[FORM SUBMIT] Form is being submitted');
console.log('[FORM SUBMIT] TOTAL_FORMS:', document.querySelector('[name="payments-TOTAL_FORMS"]')?.value);
// Проверяем payment поля в DOM ПЕРЕД submit
console.log('[FORM SUBMIT] Checking payments container...');
const paymentsContainer = document.getElementById('payments-container');
console.log('[FORM SUBMIT] payments-container exists:', !!paymentsContainer);
console.log('[FORM SUBMIT] payments-container childElementCount:', paymentsContainer?.childElementCount);
console.log('[FORM SUBMIT] form.contains(paymentsContainer):', form.contains(paymentsContainer));
// Проверяем что внутри контейнера
if (paymentsContainer && paymentsContainer.childElementCount > 0) {
const firstChild = paymentsContainer.firstElementChild;
console.log('[FORM SUBMIT] First child of container:', firstChild);
const inputs = firstChild.querySelectorAll('input, select, textarea');
console.log('[FORM SUBMIT] Inputs in first child:', inputs.length);
inputs.forEach((inp, idx) => {
console.log(` [${idx}] name="${inp.name}", value="${inp.value}", form="${inp.form?.id}"`);
});
}
console.log('[FORM SUBMIT] Payment fields in DOM (via form.querySelectorAll):');
const paymentInputs = form.querySelectorAll('[name^="payments-"]');
console.log('[FORM SUBMIT] Found payment inputs:', paymentInputs.length);
paymentInputs.forEach(inp => {
const isDisabled = inp.disabled ? ' [DISABLED]' : '';
const val = inp.type === 'checkbox' ? inp.checked : inp.value;
console.log(` ${inp.name} = "${val}"${isDisabled}`);
});
// Логируем все payment поля в FormData
const formData = new FormData(form);
console.log('[FORM SUBMIT] Payment fields in FormData:');
for (let [key, value] of formData.entries()) {
if (key.startsWith('payments-')) {
console.log(` ${key} = ${value}`);
}
}
isSubmitting = true; isSubmitting = true;
}); });
} }
@@ -2285,8 +2144,8 @@ if (!document.getElementById('notification-styles')) {
return; // Не показываем предупреждение если форма отправляется return; // Не показываем предупреждение если форма отправляется
} }
{% if is_create_page or is_draft %} {% if is_create_page %}
// Только для создания заказа и редактирования черновика // Только для создания заказа
e.preventDefault(); e.preventDefault();
e.returnValue = 'Несохраненные данные будут потеряны. Вы уверены?'; e.returnValue = 'Несохраненные данные будут потеряны. Вы уверены?';
return e.returnValue; return e.returnValue;

View File

@@ -80,15 +80,8 @@ def order_create(request):
address.save() address.save()
order.delivery_address = address order.delivery_address = address
# Проверяем какая кнопка нажата # Статус берём из формы (в том числе может быть "Черновик")
if 'save_as_draft' in request.POST: order.modified_by = request.user
# Кнопка "Сохранить как черновик"
from .services.order_status_service import OrderStatusService
order.status = OrderStatusService.get_draft_status()
order.modified_by = request.user
else:
# Кнопка "Создать заказ" - статус из формы или NULL
order.modified_by = request.user
# Сохраняем заказ в БД (теперь у него есть pk) # Сохраняем заказ в БД (теперь у него есть pk)
order.save() order.save()
@@ -107,7 +100,12 @@ def order_create(request):
p.order = order p.order = order
p.save() p.save()
# Обрабатываем удалённые платежи
from customers.services.wallet_service import WalletService
for obj in payment_formset.deleted_objects: 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() obj.delete()
# Пересчитываем стоимость доставки если она не установлена вручную # Пересчитываем стоимость доставки если она не установлена вручную
@@ -115,18 +113,15 @@ def order_create(request):
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()
# Обрабатываем переплату (если amount_paid > total_amount) # Обрабатываем переплату (если amount_paid > total_amount)
from customers.services.wallet_service import WalletService
WalletService.add_overpayment(order, request.user) WalletService.add_overpayment(order, request.user)
if order.is_draft(): messages.success(request, f'Заказ #{order.order_number} успешно создан!')
messages.success(request, f'Черновик #{order.order_number} успешно создан!')
else:
messages.success(request, f'Заказ #{order.order_number} успешно создан!')
return redirect('orders:order-detail', order_number=order.order_number) return redirect('orders:order-detail', order_number=order.order_number)
else: else:
messages.error(request, 'Пожалуйста, исправьте ошибки в форме.') messages.error(request, 'Пожалуйста, исправьте ошибки в форме.')
@@ -172,102 +167,63 @@ def order_update(request, order_number):
if form.is_valid() and formset.is_valid() and payment_formset.is_valid(): if form.is_valid() and formset.is_valid() and payment_formset.is_valid():
order = form.save(commit=False) order = form.save(commit=False)
# Если черновик финализируется # Обрабатываем адрес доставки
if 'finalize_draft' in request.POST and order.is_draft(): if order.is_delivery:
from .services.order_status_service import OrderStatusService address = AddressService.process_address_from_form(order, form.cleaned_data)
# Переводим в статус "Новый" if address:
order.status = OrderStatusService.get_new_status() # Если адрес не существует в БД, сохраняем его
order.modified_by = request.user if not address.pk:
address.save()
# Обрабатываем адрес доставки order.delivery_address = address
if order.is_delivery:
address = AddressService.process_address_from_form(order, form.cleaned_data)
if address:
if not address.pk:
address.save()
order.delivery_address = address
order.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()
for obj in payment_formset.deleted_objects:
obj.delete()
# Пересчитываем итоговую сумму и обновляем статус оплаты
order.calculate_total()
order.update_payment_status()
# Обрабатываем переплату (если amount_paid > total_amount)
from customers.services.wallet_service import WalletService
WalletService.add_overpayment(order, request.user)
messages.success(request, f'Черновик #{order.order_number} успешно завершен и переведен в статус "Новый"!')
return redirect('orders:order-detail', order_number=order.order_number)
else:
# Обрабатываем адрес доставки
if order.is_delivery:
address = AddressService.process_address_from_form(order, form.cleaned_data)
if address:
# Если адрес не существует в БД, сохраняем его
if not address.pk:
address.save()
order.delivery_address = address
else:
# Если режим "без адреса", удаляем существующий адрес
if order.delivery_address:
old_address = order.delivery_address
order.delivery_address = None
# Удаляем старый адрес, если он больше не используется
if old_address and not old_address.order:
old_address.delete()
else: else:
# Если не доставка, удаляем адрес если он был # Если режим "без адреса", удаляем существующий адрес
if order.delivery_address: if order.delivery_address:
old_address = order.delivery_address old_address = order.delivery_address
order.delivery_address = None order.delivery_address = None
# Удаляем старый адрес # Удаляем старый адрес, если он больше не используется
if old_address and not old_address.order: if old_address and not old_address.order:
old_address.delete() old_address.delete()
else:
# Если не доставка, удаляем адрес если он был
if order.delivery_address:
old_address = order.delivery_address
order.delivery_address = None
# Удаляем старый адрес
if old_address and not old_address.order:
old_address.delete()
order.modified_by = request.user order.modified_by = request.user
order.save() order.save()
formset.save() formset.save()
# Сохраняем платежи (устанавливаем created_by) # Сохраняем платежи (устанавливаем created_by)
payment_formset.instance = order payment_formset.instance = order
unsaved_payments = payment_formset.save(commit=False) unsaved_payments = payment_formset.save(commit=False)
for p in unsaved_payments: for p in unsaved_payments:
if p.created_by_id is None: if p.created_by_id is None:
p.created_by = request.user p.created_by = request.user
p.order = order p.order = order
p.save() p.save()
for obj in payment_formset.deleted_objects: # Обрабатываем удалённые платежи
obj.delete() 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.calculate_total() order.amount_paid = sum(p.amount for p in order.payments.all())
order.update_payment_status() order.calculate_total()
order.update_payment_status()
# Обрабатываем переплату (если amount_paid > total_amount)
from customers.services.wallet_service import WalletService # Обрабатываем переплату (если amount_paid > total_amount)
WalletService.add_overpayment(order, request.user) WalletService.add_overpayment(order, request.user)
if order.is_draft(): messages.success(request, f'Заказ #{order.order_number} успешно обновлен!')
messages.success(request, f'Черновик #{order.order_number} успешно обновлен!') return redirect('orders:order-detail', order_number=order.order_number)
else:
messages.success(request, f'Заказ #{order.order_number} успешно обновлен!')
return redirect('orders:order-detail', order_number=order.order_number)
else: else:
# Логируем ошибки для отладки # Логируем ошибки для отладки
print("\n=== ОШИБКИ ВАЛИДАЦИИ ФОРМЫ ===") print("\n=== ОШИБКИ ВАЛИДАЦИИ ФОРМЫ ===")
@@ -294,9 +250,8 @@ def order_update(request, order_number):
'formset': formset, 'formset': formset,
'payment_formset': payment_formset, 'payment_formset': payment_formset,
'order': order, 'order': order,
'title': f'Редактирование {"черновика" if order.is_draft() else "заказа"} #{order.order_number}', 'title': f'Редактирование заказа #{order.order_number}',
'button_text': 'Сохранить изменения', 'button_text': 'Сохранить изменения',
'is_draft': order.is_draft(),
} }
return render(request, 'orders/order_form.html', context) return render(request, 'orders/order_form.html', context)