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 %}
|