From 0d882781da60fd7710c73500a488fd06429fa1de Mon Sep 17 00:00:00 2001 From: Andrey Smakotin Date: Sun, 18 Jan 2026 17:16:34 +0300 Subject: [PATCH] =?UTF-8?q?fix(orders):=20=D0=B8=D1=81=D0=BF=D1=80=D0=B0?= =?UTF-8?q?=D0=B2=D0=B8=D1=82=D1=8C=20=D1=83=D0=B4=D0=B0=D0=BB=D0=B5=D0=BD?= =?UTF-8?q?=D0=B8=D0=B5=20=D0=BF=D0=BE=D0=B7=D0=B8=D1=86=D0=B8=D0=B9=20?= =?UTF-8?q?=D0=B7=D0=B0=D0=BA=D0=B0=D0=B7=D0=B0=20=D0=B2=20=D1=84=D0=BE?= =?UTF-8?q?=D1=80=D0=BC=D1=81=D0=B5=D1=82=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Исправлена логика удаления inline-форм для позиций заказа - Добавлена обработка удаления сохранённых и новых форм - Добавлено поле id и DELETE в OrderItemForm для корректной работы формсета - Добавлена проверка на null для created_by на странице отладки - Расширены права доступа к отладочной странице: теперь доступна owner и manager - Добавлено логирование для отладки процесса обновления заказа --- .../templates/inventory/debug_page.html | 8 +- myproject/inventory/views/debug_views.py | 12 +-- myproject/orders/forms.py | 8 +- .../orders/templates/orders/order_form.html | 85 +++++++++++++++++-- myproject/orders/views.py | 58 +++++++++++-- myproject/templates/navbar.html | 4 +- 6 files changed, 150 insertions(+), 25 deletions(-) diff --git a/myproject/inventory/templates/inventory/debug_page.html b/myproject/inventory/templates/inventory/debug_page.html index 8dcbd1b..3b8a27b 100644 --- a/myproject/inventory/templates/inventory/debug_page.html +++ b/myproject/inventory/templates/inventory/debug_page.html @@ -688,7 +688,13 @@ {{ doc.get_receipt_type_display }} {{ doc.date|date:"d.m.Y" }} {{ doc.supplier_name|default:"-" }} - {{ doc.created_by.name|default:doc.created_by.email|default:"-" }} + + {% if doc.created_by %} + {{ doc.created_by.name|default:doc.created_by.email }} + {% else %} + - + {% endif %} + {% if doc.confirmed_by %} {{ doc.confirmed_by.name|default:doc.confirmed_by.email }} ({{ doc.confirmed_at|date:"d.m H:i" }}) diff --git a/myproject/inventory/views/debug_views.py b/myproject/inventory/views/debug_views.py index 3037c85..d0fb93a 100644 --- a/myproject/inventory/views/debug_views.py +++ b/myproject/inventory/views/debug_views.py @@ -1,5 +1,5 @@ """ -Отладочные view для суперюзеров. +Отладочные view для owner и manager. Для мониторинга работы системы инвентаризации. """ 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 -def is_superuser(user): - """Проверка что пользователь - суперюзер.""" - return user.is_superuser +def is_owner_or_manager(user): + """Проверка что пользователь - owner или manager.""" + return user.is_owner or user.is_manager @login_required -@user_passes_test(is_superuser) +@user_passes_test(is_owner_or_manager) def debug_inventory_page(request): """ - Отладочная страница для суперюзеров. + Отладочная страница для owner и manager. Показывает полную картину по инвентаризации: партии, остатки, резервы, продажи. """ # Получаем параметры фильтров diff --git a/myproject/orders/forms.py b/myproject/orders/forms.py index 9e6bc6a..09c9d30 100644 --- a/myproject/orders/forms.py +++ b/myproject/orders/forms.py @@ -461,11 +461,15 @@ class OrderItemForm(forms.ModelForm): widget=forms.TextInput(attrs={'class': 'form-control', 'step': '0.01', 'min': '0'}) ) + # Поле DELETE, которое автоматически добавляется в inline формсете + DELETE = forms.BooleanField(required=False, widget=forms.HiddenInput()) + class Meta: model = OrderItem - fields = ['product', 'product_kit', 'sales_unit', 'quantity', 'price', 'is_custom_price', 'is_from_showcase'] - # ВАЖНО: НЕ включаем 'id' в fields - это предотвращает ошибку валидации + fields = ['id', 'product', 'product_kit', 'sales_unit', 'quantity', 'price', 'is_custom_price', 'is_from_showcase'] + # ВАЖНО: Теперь включаем 'id' в fields для правильной работы inline формсета widgets = { + 'id': forms.HiddenInput(), # Скрываем поле id, но оставляем его для формсета 'quantity': forms.NumberInput(attrs={'min': 1}), # Скрываем поля product и product_kit - они будут заполняться через JS 'product': forms.HiddenInput(), diff --git a/myproject/orders/templates/orders/order_form.html b/myproject/orders/templates/orders/order_form.html index 9f2c3e5..8288b7e 100644 --- a/myproject/orders/templates/orders/order_form.html +++ b/myproject/orders/templates/orders/order_form.html @@ -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(); return newForm; @@ -1560,6 +1576,10 @@ document.addEventListener('DOMContentLoaded', function() { // Сохранённая форма - помечаем на удаление console.log('[removeForm] Помечаем сохранённую форму на удаление (ID:', idField.value, ')'); deleteCheckbox.checked = true; + // Также добавляем скрытое поле, чтобы гарантировать удаление + if (!deleteCheckbox.value) { + deleteCheckbox.value = 'on'; + } form.classList.add('deleted'); form.style.display = 'none'; updateTotalDisplay(); @@ -1569,10 +1589,10 @@ document.addEventListener('DOMContentLoaded', function() { } else { // Новая форма - удаляем из DOM console.log('[removeForm] Удаление новой формы из DOM'); - + // Удаляем форму form.remove(); - + // Обновляем TOTAL_FORMS const totalFormsInput = document.querySelector('[name="items-TOTAL_FORMS"]'); if (totalFormsInput) { @@ -1581,14 +1601,17 @@ document.addEventListener('DOMContentLoaded', function() { console.log(`[removeForm] Обновление TOTAL_FORMS: ${oldTotal} → ${newTotal}`); totalFormsInput.value = newTotal; } - + // Пересчитываем индексы оставшихся форм const remainingForms = Array.from(document.querySelectorAll('.order-item-form:not(.deleted)')); console.log(`[removeForm] Пересчёт индексов для ${remainingForms.length} оставшихся форм...`); - + remainingForms.forEach((currentForm, newIndex) => { - // Находим все поля с name="items-N-..." - const fields = currentForm.querySelectorAll('[name^="items-"]'); + // Обновляем data-атрибут индекса формы + currentForm.setAttribute('data-form-index', newIndex); + + // Находим все поля с name="items-N-..." и select элементы + const fields = currentForm.querySelectorAll('[name^="items-"], select[name^="items-"]'); fields.forEach(field => { const name = field.getAttribute('name'); // Меняем индекс: items-СТАРЫЙ-поле → items-НОВЫЙ-поле @@ -1596,16 +1619,51 @@ document.addEventListener('DOMContentLoaded', function() { if (name !== newName) { console.log(`[removeForm] Переименование: ${name} → ${newName}`); field.setAttribute('name', newName); - + // Обновляем ID тоже (для связи с label) if (field.id) { const newId = field.id.replace(/^id_items-\d+/, `id_items-${newIndex}`); 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); + } + } + } } }); }); - + updateTotalDisplay(); } } @@ -1656,6 +1714,17 @@ document.addEventListener('DOMContentLoaded', function() { // Валидация перед отправкой 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'; + } + }); + // Заказ можно сохранить без товаров }); diff --git a/myproject/orders/views.py b/myproject/orders/views.py index 503d055..36152f9 100644 --- a/myproject/orders/views.py +++ b/myproject/orders/views.py @@ -323,6 +323,30 @@ def order_update(request, order_number): form = OrderForm(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(): try: with transaction.atomic(): @@ -340,11 +364,28 @@ def order_update(request, order_number): order.modified_by = request.user 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() + 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' - + # Получаем данные из формы (уже провалидированы) delivery_type = form.cleaned_data.get('delivery_type') delivery_date = form.cleaned_data.get('delivery_date') @@ -352,7 +393,10 @@ def order_update(request, order_number): time_to = form.cleaned_data.get('time_to') delivery_cost = form.cleaned_data.get('delivery_cost', Decimal('0')) 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 if delivery_type == Delivery.DELIVERY_TYPE_COURIER: @@ -360,13 +404,13 @@ def order_update(request, order_number): address = AddressService.process_address_from_form(order, form.cleaned_data) if address and not address.pk: address.save() - + # Создаем или обновляем Delivery # Для черновиков создаем Delivery без обязательных проверок, чтобы сохранить адрес if is_draft: # Для черновиков создаем Delivery, если есть хотя бы адрес или данные доставки 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, defaults={ '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') } ) + print(f"[DEBUG] Created/Updated Delivery for draft: {delivery_obj.delivery_date}, created: {created}") elif hasattr(order, 'delivery'): # Если заказ стал черновиком и нет данных доставки, удаляем Delivery order.delivery.delete() @@ -385,7 +430,7 @@ def order_update(request, order_number): # Для не-черновиков проверяем обязательные поля if not delivery_type or not delivery_date: raise ValidationError('Необходимо указать способ доставки и дату доставки') - + if delivery_type == Delivery.DELIVERY_TYPE_COURIER: # Для курьерской доставки нужен адрес if not address: @@ -394,7 +439,7 @@ def order_update(request, order_number): # Для самовывоза нужен склад if not pickup_warehouse: raise ValidationError('Для самовывоза необходимо выбрать склад') - + # Создаем или обновляем Delivery delivery, created = Delivery.objects.update_or_create( order=order, @@ -408,6 +453,7 @@ def order_update(request, order_number): '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() diff --git a/myproject/templates/navbar.html b/myproject/templates/navbar.html index ac70a92..6b29699 100644 --- a/myproject/templates/navbar.html +++ b/myproject/templates/navbar.html @@ -94,8 +94,8 @@ {% endif %} - - {% if user.is_superuser %} + + {% if user.is_owner or user.is_manager %} {% url 'inventory:debug_page' as debug_url %} {% if debug_url %}