fix(orders): исправить удаление позиций заказа в формсете
- Исправлена логика удаления inline-форм для позиций заказа - Добавлена обработка удаления сохранённых и новых форм - Добавлено поле id и DELETE в OrderItemForm для корректной работы формсета - Добавлена проверка на null для created_by на странице отладки - Расширены права доступа к отладочной странице: теперь доступна owner и manager - Добавлено логирование для отладки процесса обновления заказа
This commit is contained in:
@@ -688,7 +688,13 @@
|
|||||||
<td><span class="badge bg-info">{{ doc.get_receipt_type_display }}</span></td>
|
<td><span class="badge bg-info">{{ doc.get_receipt_type_display }}</span></td>
|
||||||
<td class="text-muted-small">{{ doc.date|date:"d.m.Y" }}</td>
|
<td class="text-muted-small">{{ doc.date|date:"d.m.Y" }}</td>
|
||||||
<td>{{ doc.supplier_name|default:"-" }}</td>
|
<td>{{ doc.supplier_name|default:"-" }}</td>
|
||||||
<td class="text-muted-small">{{ doc.created_by.name|default:doc.created_by.email|default:"-" }}</td>
|
<td class="text-muted-small">
|
||||||
|
{% if doc.created_by %}
|
||||||
|
{{ doc.created_by.name|default:doc.created_by.email }}
|
||||||
|
{% else %}
|
||||||
|
-
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
<td class="text-muted-small">
|
<td class="text-muted-small">
|
||||||
{% if doc.confirmed_by %}
|
{% if doc.confirmed_by %}
|
||||||
{{ doc.confirmed_by.name|default:doc.confirmed_by.email }} ({{ doc.confirmed_at|date:"d.m H:i" }})
|
{{ doc.confirmed_by.name|default:doc.confirmed_by.email }} ({{ doc.confirmed_at|date:"d.m H:i" }})
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
"""
|
"""
|
||||||
Отладочные view для суперюзеров.
|
Отладочные view для owner и manager.
|
||||||
Для мониторинга работы системы инвентаризации.
|
Для мониторинга работы системы инвентаризации.
|
||||||
"""
|
"""
|
||||||
from django.contrib.auth.decorators import login_required, user_passes_test
|
from django.contrib.auth.decorators import login_required, user_passes_test
|
||||||
@@ -15,16 +15,16 @@ from products.models import Product
|
|||||||
from inventory.models import Warehouse
|
from inventory.models import Warehouse
|
||||||
|
|
||||||
|
|
||||||
def is_superuser(user):
|
def is_owner_or_manager(user):
|
||||||
"""Проверка что пользователь - суперюзер."""
|
"""Проверка что пользователь - owner или manager."""
|
||||||
return user.is_superuser
|
return user.is_owner or user.is_manager
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
@user_passes_test(is_superuser)
|
@user_passes_test(is_owner_or_manager)
|
||||||
def debug_inventory_page(request):
|
def debug_inventory_page(request):
|
||||||
"""
|
"""
|
||||||
Отладочная страница для суперюзеров.
|
Отладочная страница для owner и manager.
|
||||||
Показывает полную картину по инвентаризации: партии, остатки, резервы, продажи.
|
Показывает полную картину по инвентаризации: партии, остатки, резервы, продажи.
|
||||||
"""
|
"""
|
||||||
# Получаем параметры фильтров
|
# Получаем параметры фильтров
|
||||||
|
|||||||
@@ -461,11 +461,15 @@ class OrderItemForm(forms.ModelForm):
|
|||||||
widget=forms.TextInput(attrs={'class': 'form-control', 'step': '0.01', 'min': '0'})
|
widget=forms.TextInput(attrs={'class': 'form-control', 'step': '0.01', 'min': '0'})
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Поле DELETE, которое автоматически добавляется в inline формсете
|
||||||
|
DELETE = forms.BooleanField(required=False, widget=forms.HiddenInput())
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = OrderItem
|
model = OrderItem
|
||||||
fields = ['product', 'product_kit', 'sales_unit', 'quantity', 'price', 'is_custom_price', 'is_from_showcase']
|
fields = ['id', 'product', 'product_kit', 'sales_unit', 'quantity', 'price', 'is_custom_price', 'is_from_showcase']
|
||||||
# ВАЖНО: НЕ включаем 'id' в fields - это предотвращает ошибку валидации
|
# ВАЖНО: Теперь включаем 'id' в fields для правильной работы inline формсета
|
||||||
widgets = {
|
widgets = {
|
||||||
|
'id': forms.HiddenInput(), # Скрываем поле id, но оставляем его для формсета
|
||||||
'quantity': forms.NumberInput(attrs={'min': 1}),
|
'quantity': forms.NumberInput(attrs={'min': 1}),
|
||||||
# Скрываем поля product и product_kit - они будут заполняться через JS
|
# Скрываем поля product и product_kit - они будут заполняться через JS
|
||||||
'product': forms.HiddenInput(),
|
'product': forms.HiddenInput(),
|
||||||
|
|||||||
@@ -1542,6 +1542,22 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Убедимся, что все поля имеют правильные имена и ID
|
||||||
|
const fields = newForm.querySelectorAll('[name]');
|
||||||
|
fields.forEach(field => {
|
||||||
|
const name = field.getAttribute('name');
|
||||||
|
if (name && name.includes('__prefix__')) {
|
||||||
|
const newName = name.replace(/__prefix__/g, formCount);
|
||||||
|
field.setAttribute('name', newName);
|
||||||
|
}
|
||||||
|
|
||||||
|
const id = field.getAttribute('id');
|
||||||
|
if (id && id.includes('__prefix__')) {
|
||||||
|
const newId = id.replace(/__prefix__/g, formCount);
|
||||||
|
field.setAttribute('id', newId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
updateTotalDisplay();
|
updateTotalDisplay();
|
||||||
|
|
||||||
return newForm;
|
return newForm;
|
||||||
@@ -1560,6 +1576,10 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
// Сохранённая форма - помечаем на удаление
|
// Сохранённая форма - помечаем на удаление
|
||||||
console.log('[removeForm] Помечаем сохранённую форму на удаление (ID:', idField.value, ')');
|
console.log('[removeForm] Помечаем сохранённую форму на удаление (ID:', idField.value, ')');
|
||||||
deleteCheckbox.checked = true;
|
deleteCheckbox.checked = true;
|
||||||
|
// Также добавляем скрытое поле, чтобы гарантировать удаление
|
||||||
|
if (!deleteCheckbox.value) {
|
||||||
|
deleteCheckbox.value = 'on';
|
||||||
|
}
|
||||||
form.classList.add('deleted');
|
form.classList.add('deleted');
|
||||||
form.style.display = 'none';
|
form.style.display = 'none';
|
||||||
updateTotalDisplay();
|
updateTotalDisplay();
|
||||||
@@ -1587,8 +1607,11 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
console.log(`[removeForm] Пересчёт индексов для ${remainingForms.length} оставшихся форм...`);
|
console.log(`[removeForm] Пересчёт индексов для ${remainingForms.length} оставшихся форм...`);
|
||||||
|
|
||||||
remainingForms.forEach((currentForm, newIndex) => {
|
remainingForms.forEach((currentForm, newIndex) => {
|
||||||
// Находим все поля с name="items-N-..."
|
// Обновляем data-атрибут индекса формы
|
||||||
const fields = currentForm.querySelectorAll('[name^="items-"]');
|
currentForm.setAttribute('data-form-index', newIndex);
|
||||||
|
|
||||||
|
// Находим все поля с name="items-N-..." и select элементы
|
||||||
|
const fields = currentForm.querySelectorAll('[name^="items-"], select[name^="items-"]');
|
||||||
fields.forEach(field => {
|
fields.forEach(field => {
|
||||||
const name = field.getAttribute('name');
|
const name = field.getAttribute('name');
|
||||||
// Меняем индекс: items-СТАРЫЙ-поле → items-НОВЫЙ-поле
|
// Меняем индекс: items-СТАРЫЙ-поле → items-НОВЫЙ-поле
|
||||||
@@ -1602,6 +1625,41 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
const newId = field.id.replace(/^id_items-\d+/, `id_items-${newIndex}`);
|
const newId = field.id.replace(/^id_items-\d+/, `id_items-${newIndex}`);
|
||||||
field.setAttribute('id', newId);
|
field.setAttribute('id', newId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Обновляем for атрибут у label, если есть
|
||||||
|
const label = document.querySelector(`label[for="${field.id}"]`);
|
||||||
|
if (label) {
|
||||||
|
label.setAttribute('for', newId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Обновляем data-атрибут у select2 элементов
|
||||||
|
if (field.classList.contains('select2-order-item')) {
|
||||||
|
field.setAttribute('data-form-index', newIndex);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Обновляем select элементы, если есть
|
||||||
|
const selects = currentForm.querySelectorAll('select');
|
||||||
|
selects.forEach(select => {
|
||||||
|
const name = select.getAttribute('name');
|
||||||
|
if (name && name.startsWith('items-')) {
|
||||||
|
const newName = name.replace(/^items-\d+/, `items-${newIndex}`);
|
||||||
|
if (name !== newName) {
|
||||||
|
select.setAttribute('name', newName);
|
||||||
|
|
||||||
|
// Обновляем ID тоже (для связи с label)
|
||||||
|
if (select.id) {
|
||||||
|
const newId = select.id.replace(/^id_items-\d+/, `id_items-${newIndex}`);
|
||||||
|
select.setAttribute('id', newId);
|
||||||
|
|
||||||
|
// Обновляем for атрибут у label, если есть
|
||||||
|
const label = document.querySelector(`label[for="${select.id}"]`);
|
||||||
|
if (label) {
|
||||||
|
label.setAttribute('for', newId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -1656,6 +1714,17 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
|
|
||||||
// Валидация перед отправкой
|
// Валидация перед отправкой
|
||||||
document.getElementById('order-form').addEventListener('submit', function(e) {
|
document.getElementById('order-form').addEventListener('submit', function(e) {
|
||||||
|
// Убедимся, что все удаленные формы действительно отмечены для удаления
|
||||||
|
const deletedForms = document.querySelectorAll('.order-item-form.deleted');
|
||||||
|
deletedForms.forEach(form => {
|
||||||
|
const deleteCheckbox = form.querySelector('input[name$="-DELETE"]');
|
||||||
|
if (deleteCheckbox) {
|
||||||
|
deleteCheckbox.checked = true;
|
||||||
|
// Убедимся, что значение установлено
|
||||||
|
deleteCheckbox.value = 'on';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Заказ можно сохранить без товаров
|
// Заказ можно сохранить без товаров
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -323,6 +323,30 @@ def order_update(request, order_number):
|
|||||||
form = OrderForm(request.POST, instance=order)
|
form = OrderForm(request.POST, instance=order)
|
||||||
formset = OrderItemFormSet(request.POST, instance=order)
|
formset = OrderItemFormSet(request.POST, instance=order)
|
||||||
|
|
||||||
|
# Логирование для отладки удаления позиций заказа
|
||||||
|
print("\n=== DEBUG ORDER UPDATE ===")
|
||||||
|
print(f"POST data keys: {list(request.POST.keys())}")
|
||||||
|
print(f"items-TOTAL_FORMS: {request.POST.get('items-TOTAL_FORMS')}")
|
||||||
|
print(f"items-INITIAL_FORMS: {request.POST.get('items-INITIAL_FORMS')}")
|
||||||
|
|
||||||
|
# Проверяем, какие формы были отмечены для удаления
|
||||||
|
total_forms = int(request.POST.get('items-TOTAL_FORMS', 0))
|
||||||
|
for i in range(total_forms):
|
||||||
|
delete_field = request.POST.get(f'items-{i}-DELETE')
|
||||||
|
has_id = bool(request.POST.get(f'items-{i}-id'))
|
||||||
|
print(f"Form {i}: DELETE={delete_field}, has_id={has_id}")
|
||||||
|
print("=========================\n")
|
||||||
|
|
||||||
|
print(f"Form is valid: {form.is_valid()}")
|
||||||
|
print(f"Formset is valid: {formset.is_valid()}")
|
||||||
|
if not form.is_valid():
|
||||||
|
print(f"Form errors: {form.errors}")
|
||||||
|
if not formset.is_valid():
|
||||||
|
print(f"Formset errors: {formset.errors}")
|
||||||
|
for i, form_err in enumerate(formset.errors):
|
||||||
|
if form_err:
|
||||||
|
print(f"Form {i} errors: {form_err}")
|
||||||
|
|
||||||
if form.is_valid() and formset.is_valid():
|
if form.is_valid() and formset.is_valid():
|
||||||
try:
|
try:
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
@@ -340,7 +364,24 @@ def order_update(request, order_number):
|
|||||||
|
|
||||||
order.modified_by = request.user
|
order.modified_by = request.user
|
||||||
order.save()
|
order.save()
|
||||||
|
print(f"Before formset.save(): Order has {order.items.count()} items")
|
||||||
|
|
||||||
|
# Логирование форм, отмеченных для удаления
|
||||||
|
print("Forms in formset:")
|
||||||
|
for i, form in enumerate(formset):
|
||||||
|
if hasattr(form, 'cleaned_data') and form.cleaned_data:
|
||||||
|
delete_flag = form.cleaned_data.get('DELETE', False)
|
||||||
|
form_id = form.cleaned_data.get('id')
|
||||||
|
print(f" Form {i}: id={form_id}, DELETE={delete_flag}, has_changed={form.has_changed()}")
|
||||||
|
print(f" Full cleaned_data: {form.cleaned_data}")
|
||||||
|
if delete_flag:
|
||||||
|
print(f" -> This form should be deleted")
|
||||||
|
else:
|
||||||
|
print(f" Form {i}: No cleaned_data or empty")
|
||||||
|
|
||||||
formset.save()
|
formset.save()
|
||||||
|
print(f"After formset.save(): Order has {order.items.count()} items")
|
||||||
|
print(f"Forms marked for deletion: {[form for form in formset if form.cleaned_data.get('DELETE')] if formset.can_delete else 'N/A'}")
|
||||||
|
|
||||||
# Проверяем, является ли заказ черновиком
|
# Проверяем, является ли заказ черновиком
|
||||||
is_draft = order.status and order.status.code == 'draft'
|
is_draft = order.status and order.status.code == 'draft'
|
||||||
@@ -353,6 +394,9 @@ def order_update(request, order_number):
|
|||||||
delivery_cost = form.cleaned_data.get('delivery_cost', Decimal('0'))
|
delivery_cost = form.cleaned_data.get('delivery_cost', Decimal('0'))
|
||||||
pickup_warehouse = form.cleaned_data.get('pickup_warehouse')
|
pickup_warehouse = form.cleaned_data.get('pickup_warehouse')
|
||||||
|
|
||||||
|
print(f"[DEBUG] Delivery data - type: {delivery_type}, date: {delivery_date}, time_from: {time_from}, time_to: {time_to}")
|
||||||
|
print(f"[DEBUG] Is draft: {is_draft}, address: {form.cleaned_data.get('address_street')}")
|
||||||
|
|
||||||
# Обрабатываем адрес для курьерской доставки (даже для черновиков, если указан)
|
# Обрабатываем адрес для курьерской доставки (даже для черновиков, если указан)
|
||||||
address = None
|
address = None
|
||||||
if delivery_type == Delivery.DELIVERY_TYPE_COURIER:
|
if delivery_type == Delivery.DELIVERY_TYPE_COURIER:
|
||||||
@@ -366,7 +410,7 @@ def order_update(request, order_number):
|
|||||||
if is_draft:
|
if is_draft:
|
||||||
# Для черновиков создаем Delivery, если есть хотя бы адрес или данные доставки
|
# Для черновиков создаем Delivery, если есть хотя бы адрес или данные доставки
|
||||||
if address or delivery_type or pickup_warehouse or delivery_date:
|
if address or delivery_type or pickup_warehouse or delivery_date:
|
||||||
Delivery.objects.update_or_create(
|
delivery_obj, created = Delivery.objects.update_or_create(
|
||||||
order=order,
|
order=order,
|
||||||
defaults={
|
defaults={
|
||||||
'delivery_type': delivery_type or Delivery.DELIVERY_TYPE_COURIER,
|
'delivery_type': delivery_type or Delivery.DELIVERY_TYPE_COURIER,
|
||||||
@@ -378,6 +422,7 @@ def order_update(request, order_number):
|
|||||||
'cost': delivery_cost if delivery_cost else Decimal('0')
|
'cost': delivery_cost if delivery_cost else Decimal('0')
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
print(f"[DEBUG] Created/Updated Delivery for draft: {delivery_obj.delivery_date}, created: {created}")
|
||||||
elif hasattr(order, 'delivery'):
|
elif hasattr(order, 'delivery'):
|
||||||
# Если заказ стал черновиком и нет данных доставки, удаляем Delivery
|
# Если заказ стал черновиком и нет данных доставки, удаляем Delivery
|
||||||
order.delivery.delete()
|
order.delivery.delete()
|
||||||
@@ -408,6 +453,7 @@ def order_update(request, order_number):
|
|||||||
'cost': delivery_cost if delivery_cost else Decimal('0')
|
'cost': delivery_cost if delivery_cost else Decimal('0')
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
print(f"[DEBUG] Created/Updated Delivery for non-draft: {delivery.delivery_date}, created: {created}")
|
||||||
|
|
||||||
# Пересчитываем итоговую стоимость
|
# Пересчитываем итоговую стоимость
|
||||||
order.calculate_total()
|
order.calculate_total()
|
||||||
|
|||||||
@@ -94,8 +94,8 @@
|
|||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<!-- 🔧 Debug (только для superuser) -->
|
<!-- 🔧 Debug (для owner или manager) -->
|
||||||
{% if user.is_superuser %}
|
{% if user.is_owner or user.is_manager %}
|
||||||
{% url 'inventory:debug_page' as debug_url %}
|
{% url 'inventory:debug_page' as debug_url %}
|
||||||
{% if debug_url %}
|
{% if debug_url %}
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
|
|||||||
Reference in New Issue
Block a user