Исправлена работа с formset товаров в форме заказа
Проблемы: - Отображались лишние пустые формы (extra=1) - Новые формы не инициализировали Select2 корректно - Неудобное удаление через checkbox Исправления: 1. forms.py: - Изменен OrderItemFormSet: extra=0, min_num=0 - Формы добавляются только через JavaScript 2. order_form.html: - Добавлен шаблон пустой формы (#empty-form-template) - Заменен checkbox "Удалить" на кнопку с иконкой - Полностью переписана логика добавления/удаления форм - Добавлена валидация: нельзя удалить единственную позицию - Скрытые формы (DELETE=true) визуально помечаются - Автодобавление первой формы при пустом formset - Валидация перед отправкой: минимум 1 товар Результат: - При создании заказа отображается только 1 пустая форма - Кнопка "Добавить товар" корректно создает новые формы - Select2 работает в каждой новой форме - Кнопка "Удалить" работает корректно - Единственную позицию удалить нельзя 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -149,10 +149,10 @@ OrderItemFormSet = inlineformset_factory(
|
||||
Order,
|
||||
OrderItem,
|
||||
form=OrderItemForm,
|
||||
extra=1, # Одна пустая форма для добавления
|
||||
extra=0, # Без пустых форм (будем добавлять через JavaScript)
|
||||
can_delete=True,
|
||||
min_num=1, # Минимум 1 товар в заказе
|
||||
validate_min=True,
|
||||
min_num=0, # Минимум 0 товаров (валидация на уровне бизнес-логики)
|
||||
validate_min=False,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -3,6 +3,21 @@
|
||||
|
||||
{% block title %}{{ title }}{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
<style>
|
||||
/* Скрываем DELETE checkbox */
|
||||
input[name$="-DELETE"] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Визуально помечаем удаленные формы */
|
||||
.order-item-form.deleted {
|
||||
opacity: 0.5;
|
||||
pointer-events: none;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid">
|
||||
<div class="row mb-4">
|
||||
@@ -222,16 +237,15 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-2 text-end">
|
||||
{% if formset.can_delete %}
|
||||
<div class="mb-2">
|
||||
<div class="form-check">
|
||||
{{ item_form.DELETE }}
|
||||
<label class="form-check-label" for="{{ item_form.DELETE.id_for_label }}">
|
||||
Удалить
|
||||
</label>
|
||||
</div>
|
||||
<label class="form-label d-block"> </label>
|
||||
{% if formset.can_delete %}
|
||||
{{ item_form.DELETE }}
|
||||
{% endif %}
|
||||
<button type="button" class="btn btn-danger btn-sm remove-item-btn">
|
||||
<i class="bi bi-trash"></i> Удалить
|
||||
</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% if item_form.errors %}
|
||||
@@ -240,7 +254,58 @@
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<button type="button" class="btn btn-secondary" id="add-item-btn">
|
||||
|
||||
<!-- Скрытый шаблон для новых форм -->
|
||||
<div id="empty-form-template" style="display: none;">
|
||||
<div class="order-item-form border rounded p-3 mb-3" data-form-index="__prefix__">
|
||||
<input type="hidden" name="items-__prefix__-id" id="id_items-__prefix__-id">
|
||||
<input type="hidden" name="items-__prefix__-product" id="id_items-__prefix__-product">
|
||||
<input type="hidden" name="items-__prefix__-product_kit" id="id_items-__prefix__-product_kit">
|
||||
<input type="hidden" name="items-__prefix__-is_custom_price" id="id_items-__prefix__-is_custom_price" value="false">
|
||||
|
||||
<div class="row align-items-end">
|
||||
<div class="col-md-5">
|
||||
<div class="mb-2">
|
||||
<label class="form-label">Товар или комплект</label>
|
||||
<select class="form-select select2-order-item" data-form-index="__prefix__">
|
||||
<option value=""></option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<div class="mb-2">
|
||||
<label class="form-label">Количество</label>
|
||||
<input type="number" name="items-__prefix__-quantity" step="1" min="1" value="1" class="form-control" id="id_items-__prefix__-quantity">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="mb-2">
|
||||
<label class="form-label">Цена</label>
|
||||
<div class="position-relative">
|
||||
<input type="number" name="items-__prefix__-price" step="0.01" min="0" class="form-control" id="id_items-__prefix__-price">
|
||||
<span class="custom-price-badge badge bg-warning position-absolute top-0 end-0 mt-1 me-1" style="display: none;">
|
||||
Изменена
|
||||
</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> руб.
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-2 text-end">
|
||||
<div class="mb-2">
|
||||
<label class="form-label d-block"> </label>
|
||||
<input type="checkbox" name="items-__prefix__-DELETE" id="id_items-__prefix__-DELETE">
|
||||
<button type="button" class="btn btn-danger btn-sm remove-item-btn">
|
||||
<i class="bi bi-trash"></i> Удалить
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="button" class="btn btn-success" id="add-item-btn">
|
||||
<i class="bi bi-plus-circle"></i> Добавить товар
|
||||
</button>
|
||||
<button type="button" class="btn btn-primary ms-2" id="create-temp-kit-btn" data-bs-toggle="modal" data-bs-target="#tempKitModal">
|
||||
@@ -459,80 +524,107 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
const container = document.getElementById('order-items-container');
|
||||
const addButton = document.getElementById('add-item-btn');
|
||||
const totalFormsInput = document.querySelector('#id_items-TOTAL_FORMS');
|
||||
const emptyFormTemplate = document.getElementById('empty-form-template');
|
||||
|
||||
addButton.addEventListener('click', function() {
|
||||
// Функция для добавления новой формы
|
||||
function addNewForm() {
|
||||
const formCount = parseInt(totalFormsInput.value);
|
||||
const lastForm = container.querySelector('.order-item-form:last-child');
|
||||
const newForm = lastForm.cloneNode(true);
|
||||
|
||||
// Обновляем индексы в новой форме
|
||||
const regex = new RegExp('items-(\\d+)-', 'g');
|
||||
newForm.innerHTML = newForm.innerHTML.replace(regex, `items-${formCount}-`);
|
||||
// Клонируем шаблон
|
||||
const templateContent = emptyFormTemplate.querySelector('.order-item-form');
|
||||
const newForm = templateContent.cloneNode(true);
|
||||
|
||||
// Заменяем __prefix__ на реальный индекс
|
||||
newForm.innerHTML = newForm.innerHTML.replace(/__prefix__/g, formCount);
|
||||
newForm.dataset.formIndex = formCount;
|
||||
|
||||
// Очищаем значения
|
||||
newForm.querySelectorAll('input[type="hidden"]').forEach(input => {
|
||||
if (!input.name.includes('-id')) {
|
||||
input.value = '';
|
||||
}
|
||||
});
|
||||
newForm.querySelectorAll('input[type="number"]').forEach(input => {
|
||||
input.value = '';
|
||||
});
|
||||
// Очищаем поле цены
|
||||
const priceField = newForm.querySelector('[name$="-price"]');
|
||||
if (priceField) {
|
||||
priceField.value = '';
|
||||
delete priceField.dataset.originalPrice;
|
||||
}
|
||||
newForm.querySelectorAll('input[type="checkbox"]').forEach(input => {
|
||||
if (input.name.includes('DELETE')) {
|
||||
input.checked = false;
|
||||
}
|
||||
});
|
||||
|
||||
// Скрываем индикаторы кастомной цены
|
||||
const badge = newForm.querySelector('.custom-price-badge');
|
||||
const priceInfo = newForm.querySelector('.original-price-info');
|
||||
if (badge) badge.style.display = 'none';
|
||||
if (priceInfo) priceInfo.style.display = 'none';
|
||||
|
||||
// Удаляем и пересоздаем Select2
|
||||
const select2Element = newForm.querySelector('.select2-order-item');
|
||||
const $select2Element = $(select2Element);
|
||||
|
||||
// Если Select2 уже был инициализирован, уничтожаем его
|
||||
if ($select2Element.data('select2')) {
|
||||
$select2Element.select2('destroy');
|
||||
}
|
||||
|
||||
// Очищаем все опции, кроме первой пустой
|
||||
select2Element.innerHTML = '<option value=""></option>';
|
||||
select2Element.dataset.formIndex = formCount;
|
||||
|
||||
// Добавляем форму в контейнер
|
||||
container.appendChild(newForm);
|
||||
|
||||
// Обновляем счетчик форм
|
||||
totalFormsInput.value = formCount + 1;
|
||||
|
||||
// Инициализируем Select2 для новой формы
|
||||
const select2Element = newForm.querySelector('.select2-order-item');
|
||||
select2Element.dataset.formIndex = formCount;
|
||||
initOrderItemSelect2(select2Element);
|
||||
|
||||
// Инициализируем отслеживание цены для новой формы
|
||||
// Инициализируем отслеживание цены
|
||||
initPriceTracking(newForm);
|
||||
|
||||
// Добавляем обработчик удаления
|
||||
const removeBtn = newForm.querySelector('.remove-item-btn');
|
||||
removeBtn.addEventListener('click', function() {
|
||||
removeForm(newForm);
|
||||
});
|
||||
|
||||
console.log(`Added new form with index ${formCount}`);
|
||||
|
||||
return newForm;
|
||||
}
|
||||
|
||||
// Функция для удаления формы
|
||||
function removeForm(form) {
|
||||
const deleteCheckbox = form.querySelector('input[name$="-DELETE"]');
|
||||
const idField = form.querySelector('input[name$="-id"]');
|
||||
|
||||
// Проверяем, не последняя ли это форма
|
||||
const visibleForms = Array.from(container.querySelectorAll('.order-item-form'))
|
||||
.filter(f => !f.classList.contains('deleted'));
|
||||
|
||||
if (visibleForms.length <= 1) {
|
||||
alert('Нельзя удалить единственную позицию. Добавьте новую позицию перед удалением этой.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Если форма уже сохранена (есть ID), помечаем на удаление
|
||||
if (idField && idField.value) {
|
||||
deleteCheckbox.checked = true;
|
||||
form.classList.add('deleted');
|
||||
console.log('Form marked for deletion');
|
||||
} else {
|
||||
// Если форма новая, просто удаляем из DOM
|
||||
form.remove();
|
||||
console.log('Form removed from DOM');
|
||||
}
|
||||
}
|
||||
|
||||
// Обработчик кнопки добавления
|
||||
addButton.addEventListener('click', addNewForm);
|
||||
|
||||
// Добавляем обработчики удаления для существующих форм
|
||||
container.querySelectorAll('.remove-item-btn').forEach(btn => {
|
||||
btn.addEventListener('click', function() {
|
||||
const form = btn.closest('.order-item-form');
|
||||
removeForm(form);
|
||||
});
|
||||
});
|
||||
|
||||
// Перед отправкой: удалить пустые формы
|
||||
// Если нет ни одной формы, добавляем одну автоматически
|
||||
if (container.querySelectorAll('.order-item-form').length === 0) {
|
||||
addNewForm();
|
||||
}
|
||||
|
||||
// Валидация перед отправкой
|
||||
document.getElementById('order-form').addEventListener('submit', function(e) {
|
||||
const forms = container.querySelectorAll('.order-item-form');
|
||||
forms.forEach(form => {
|
||||
const visibleForms = Array.from(container.querySelectorAll('.order-item-form'))
|
||||
.filter(f => !f.classList.contains('deleted'));
|
||||
|
||||
// Проверяем, что есть хотя бы одна позиция с товаром
|
||||
let hasItems = false;
|
||||
visibleForms.forEach(form => {
|
||||
const productField = form.querySelector('[name$="-product"]');
|
||||
const kitField = form.querySelector('[name$="-product_kit"]');
|
||||
const deleteCheckbox = form.querySelector('[name$="-DELETE"]');
|
||||
|
||||
// Если оба поля пусты - пометить на удаление
|
||||
if (!productField.value && !kitField.value && deleteCheckbox) {
|
||||
deleteCheckbox.checked = true;
|
||||
if (productField.value || kitField.value) {
|
||||
hasItems = true;
|
||||
}
|
||||
});
|
||||
|
||||
if (!hasItems && visibleForms.length > 0) {
|
||||
e.preventDefault();
|
||||
alert('Добавьте хотя бы один товар или комплект в заказ');
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
// === ВРЕМЕННЫЕ КОМПЛЕКТЫ ===
|
||||
|
||||
Reference in New Issue
Block a user