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

- Убран черновик как отдельная сущность с процессом финализации
- Черновик теперь просто обычный 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
@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
@transaction.atomic
def adjust_balance(customer_id, amount, description, user):

View File

@@ -45,18 +45,12 @@
<div class="row mb-2">
<div class="col-md-4"><strong>Статус:</strong></div>
<div class="col-md-8">
{% if order.status == 'new' %}
<span class="badge bg-primary">Новый</span>
{% elif order.status == 'confirmed' %}
<span class="badge bg-success">Подтвержден</span>
{% elif order.status == 'in_assembly' %}
<span class="badge bg-warning">В сборке</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>
{% 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>
@@ -126,7 +120,7 @@
</div>
<div class="row mb-2">
<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>
{% else %}
<div class="row mb-2">
@@ -207,25 +201,25 @@
</a>
{% endif %}
</td>
<td>{{ item.quantity }}</td>
<td>{{ item.quantity }} шт.</td>
<td>
{{ item.price }} руб.
{{ 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 }} руб.
Оригинальная: {{ item.original_price|floatformat:2 }} руб.
{% if item.price_difference %}
{% if item.price_difference > 0 %}
<span class="text-success">(+{{ item.price_difference }} руб.)</span>
<span class="text-success">(+{{ item.price_difference|floatformat:2 }} руб.)</span>
{% else %}
<span class="text-danger">({{ item.price_difference }} руб.)</span>
<span class="text-danger">({{ item.price_difference|floatformat:2 }} руб.)</span>
{% endif %}
{% endif %}
</small>
{% endif %}
</td>
<td><strong>{{ item.get_total_price }} руб.</strong></td>
<td><strong>{{ item.get_total_price|floatformat:2 }} руб.</strong></td>
</tr>
{% endfor %}
</tbody>
@@ -315,27 +309,27 @@
{% 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 }} руб.</div>
<div class="col-6 text-end">{{ order.delivery_cost|floatformat:2 }} руб.</div>
</div>
{% endif %}
{% if order.discount_amount > 0 %}
<div class="row mb-2">
<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>
{% endif %}
<hr>
<div class="row mb-3">
<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 class="row mb-2">
<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 class="row mb-2">
<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 class="row mb-2">
<div class="col-12">
@@ -349,12 +343,6 @@
{% endif %}
</div>
</div>
<div class="row mb-2">
<div class="col-12">
<strong>Способ оплаты:</strong><br>
{{ order.get_payment_method_display }}
</div>
</div>
</div>
</div>
@@ -368,10 +356,13 @@
<ul class="list-group list-group-flush">
{% for payment in order.payments.all %}
<li class="list-group-item">
<div><strong>{{ payment.amount }} руб.</strong></div>
<div><strong>{{ payment.amount|floatformat:2 }} руб.</strong></div>
<small class="text-muted">
{{ 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 %}
<br>Принял: {{ payment.created_by.get_full_name }}
{% endif %}

View File

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

View File

@@ -80,14 +80,7 @@ def order_create(request):
address.save()
order.delivery_address = address
# Проверяем какая кнопка нажата
if 'save_as_draft' in request.POST:
# Кнопка "Сохранить как черновик"
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)
@@ -107,7 +100,12 @@ def order_create(request):
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()
# Пересчитываем стоимость доставки если она не установлена вручную
@@ -115,17 +113,14 @@ def order_create(request):
if not delivery_cost or delivery_cost <= 0:
order.reset_delivery_cost()
# Пересчитываем итоговую сумму и обновляем статус оплаты
# Пересчитываем сумму оплачено и итоговую стоимость
order.amount_paid = sum(p.amount for p in order.payments.all())
order.calculate_total()
order.update_payment_status()
# Обрабатываем переплату (если amount_paid > total_amount)
from customers.services.wallet_service import WalletService
WalletService.add_overpayment(order, request.user)
if order.is_draft():
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)
else:
@@ -172,47 +167,6 @@ def order_update(request, order_number):
if form.is_valid() and formset.is_valid() and payment_formset.is_valid():
order = form.save(commit=False)
# Если черновик финализируется
if 'finalize_draft' in request.POST and order.is_draft():
from .services.order_status_service import OrderStatusService
# Переводим в статус "Новый"
order.status = OrderStatusService.get_new_status()
order.modified_by = request.user
# Обрабатываем адрес доставки
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)
@@ -252,20 +206,22 @@ def order_update(request, order_number):
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.update_payment_status()
# Обрабатываем переплату (если amount_paid > total_amount)
from customers.services.wallet_service import WalletService
WalletService.add_overpayment(order, request.user)
if order.is_draft():
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)
else:
@@ -294,9 +250,8 @@ def order_update(request, order_number):
'formset': formset,
'payment_formset': payment_formset,
'order': order,
'title': f'Редактирование {"черновика" if order.is_draft() else "заказа"} #{order.order_number}',
'title': f'Редактирование заказа #{order.order_number}',
'button_text': 'Сохранить изменения',
'is_draft': order.is_draft(),
}
return render(request, 'orders/order_form.html', context)