From 39798af448d2e6062303742ff6ebd77d07258ae4 Mon Sep 17 00:00:00 2001 From: Andrey Smakotin Date: Wed, 10 Dec 2025 23:35:46 +0300 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D1=8B=20=D0=BF=D1=80=D0=B5=D0=B4=D1=81=D1=82=D0=B0=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D0=B8=D1=8F=20=D0=B8=20=D0=BC=D0=B0=D1=80?= =?UTF-8?q?=D1=88=D1=80=D1=83=D1=82=D1=8B=20=D0=B4=D0=BB=D1=8F=20=D0=B4?= =?UTF-8?q?=D0=BE=D0=BA=D1=83=D0=BC=D0=B5=D0=BD=D1=82=D0=BE=D0=B2=20=D1=81?= =?UTF-8?q?=D0=BF=D0=B8=D1=81=D0=B0=D0=BD=D0=B8=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - WriteOffDocumentListView - список документов с пагинацией - WriteOffDocumentCreateView - создание нового документа - WriteOffDocumentDetailView - детальный просмотр документа - WriteOffDocumentAddItemView - добавление позиции (AJAX) - WriteOffDocumentUpdateItemView - обновление позиции (AJAX) - WriteOffDocumentRemoveItemView - удаление позиции (AJAX) - WriteOffDocumentConfirmView - проведение документа - WriteOffDocumentCancelView - отмена документа - Добавлены URL-маршруты для всех операций с документами списания - Поддержка AJAX запросов для динамической работы с позициями --- myproject/inventory/urls.py | 18 +- .../inventory/views/writeoff_document.py | 208 ++++++++++++++++++ 2 files changed, 225 insertions(+), 1 deletion(-) create mode 100644 myproject/inventory/views/writeoff_document.py diff --git a/myproject/inventory/urls.py b/myproject/inventory/urls.py index 423cf29..76beddb 100644 --- a/myproject/inventory/urls.py +++ b/myproject/inventory/urls.py @@ -28,6 +28,12 @@ from .views import ( ) # Showcase views from .views.showcase import ShowcaseListView, ShowcaseCreateView, ShowcaseUpdateView, ShowcaseDeleteView, SetDefaultShowcaseView +# WriteOff Document views +from .views.writeoff_document import ( + WriteOffDocumentListView, WriteOffDocumentCreateView, WriteOffDocumentDetailView, + WriteOffDocumentAddItemView, WriteOffDocumentUpdateItemView, WriteOffDocumentRemoveItemView, + WriteOffDocumentConfirmView, WriteOffDocumentCancelView +) # Debug views from .views.debug_views import debug_inventory_page from . import views @@ -68,12 +74,22 @@ urlpatterns = [ path('inventory-ops//', InventoryDetailView.as_view(), name='inventory-detail'), path('inventory-ops//lines/add/', InventoryLineCreateBulkView.as_view(), name='inventory-lines-add'), - # ==================== WRITEOFF ==================== + # ==================== WRITEOFF (одиночные записи) ==================== path('writeoffs/', WriteOffListView.as_view(), name='writeoff-list'), path('writeoffs/create/', WriteOffCreateView.as_view(), name='writeoff-create'), path('writeoffs//edit/', WriteOffUpdateView.as_view(), name='writeoff-update'), path('writeoffs//delete/', WriteOffDeleteView.as_view(), name='writeoff-delete'), + # ==================== WRITEOFF DOCUMENT (документы списания) ==================== + path('writeoff-documents/', WriteOffDocumentListView.as_view(), name='writeoff-document-list'), + path('writeoff-documents/create/', WriteOffDocumentCreateView.as_view(), name='writeoff-document-create'), + path('writeoff-documents//', WriteOffDocumentDetailView.as_view(), name='writeoff-document-detail'), + path('writeoff-documents//add-item/', WriteOffDocumentAddItemView.as_view(), name='writeoff-document-add-item'), + path('writeoff-documents//update-item//', WriteOffDocumentUpdateItemView.as_view(), name='writeoff-document-update-item'), + path('writeoff-documents//remove-item//', WriteOffDocumentRemoveItemView.as_view(), name='writeoff-document-remove-item'), + path('writeoff-documents//confirm/', WriteOffDocumentConfirmView.as_view(), name='writeoff-document-confirm'), + path('writeoff-documents//cancel/', WriteOffDocumentCancelView.as_view(), name='writeoff-document-cancel'), + # ==================== TRANSFER ==================== path('transfers/', TransferListView.as_view(), name='transfer-list'), path('transfers/create/', TransferBulkCreateView.as_view(), name='transfer-create'), # Новая форма массового перемещения diff --git a/myproject/inventory/views/writeoff_document.py b/myproject/inventory/views/writeoff_document.py new file mode 100644 index 0000000..bab5ead --- /dev/null +++ b/myproject/inventory/views/writeoff_document.py @@ -0,0 +1,208 @@ +""" +Views для работы с документами списания (WriteOffDocument). +""" + +from django.views.generic import ListView, CreateView, DetailView, View +from django.contrib.auth.mixins import LoginRequiredMixin +from django.urls import reverse +from django.shortcuts import get_object_or_404, redirect +from django.contrib import messages +from django.http import JsonResponse +from django.db import transaction +from django.core.exceptions import ValidationError + +from inventory.models import WriteOffDocument, WriteOffDocumentItem +from inventory.forms import WriteOffDocumentForm, WriteOffDocumentItemForm +from inventory.services.writeoff_document_service import WriteOffDocumentService + + +class WriteOffDocumentListView(LoginRequiredMixin, ListView): + """Список документов списания""" + model = WriteOffDocument + template_name = 'inventory/writeoff_document/list.html' + context_object_name = 'documents' + paginate_by = 20 + + def get_queryset(self): + return WriteOffDocument.objects.select_related( + 'warehouse', 'created_by', 'confirmed_by' + ).prefetch_related('items').order_by('-date', '-created_at') + + +class WriteOffDocumentCreateView(LoginRequiredMixin, CreateView): + """Создание документа списания""" + model = WriteOffDocument + form_class = WriteOffDocumentForm + template_name = 'inventory/writeoff_document/form.html' + + def form_valid(self, form): + document = WriteOffDocumentService.create_document( + warehouse=form.cleaned_data['warehouse'], + date=form.cleaned_data['date'], + notes=form.cleaned_data.get('notes'), + created_by=self.request.user + ) + messages.success(self.request, f'Документ {document.document_number} создан') + return redirect('inventory:writeoff-document-detail', pk=document.pk) + + +class WriteOffDocumentDetailView(LoginRequiredMixin, DetailView): + """Детальный просмотр документа списания""" + model = WriteOffDocument + template_name = 'inventory/writeoff_document/detail.html' + context_object_name = 'document' + + def get_queryset(self): + return WriteOffDocument.objects.select_related( + 'warehouse', 'created_by', 'confirmed_by' + ).prefetch_related('items__product', 'items__reservation') + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context['item_form'] = WriteOffDocumentItemForm(document=self.object) + return context + + +class WriteOffDocumentAddItemView(LoginRequiredMixin, View): + """Добавление позиции в документ списания""" + + @transaction.atomic + def post(self, request, pk): + document = get_object_or_404(WriteOffDocument, pk=pk) + form = WriteOffDocumentItemForm(request.POST, document=document) + + if form.is_valid(): + try: + item = WriteOffDocumentService.add_item( + document=document, + product=form.cleaned_data['product'], + quantity=form.cleaned_data['quantity'], + reason=form.cleaned_data['reason'], + notes=form.cleaned_data.get('notes') + ) + + if request.headers.get('X-Requested-With') == 'XMLHttpRequest': + return JsonResponse({ + 'success': True, + 'item_id': item.id, + 'message': f'Добавлено: {item.product.name}' + }) + + messages.success(request, f'Добавлено: {item.product.name}') + + except ValidationError as e: + if request.headers.get('X-Requested-With') == 'XMLHttpRequest': + return JsonResponse({'success': False, 'error': str(e)}, status=400) + messages.error(request, str(e)) + else: + if request.headers.get('X-Requested-With') == 'XMLHttpRequest': + errors = '; '.join([f"{k}: {v[0]}" for k, v in form.errors.items()]) + return JsonResponse({'success': False, 'error': errors}, status=400) + for field, errors in form.errors.items(): + for error in errors: + messages.error(request, f'{field}: {error}') + + return redirect('inventory:writeoff-document-detail', pk=pk) + + +class WriteOffDocumentUpdateItemView(LoginRequiredMixin, View): + """Обновление позиции документа списания""" + + @transaction.atomic + def post(self, request, pk, item_pk): + document = get_object_or_404(WriteOffDocument, pk=pk) + item = get_object_or_404(WriteOffDocumentItem, pk=item_pk, document=document) + + try: + quantity = request.POST.get('quantity') + reason = request.POST.get('reason') + notes = request.POST.get('notes') + + WriteOffDocumentService.update_item( + item, + quantity=quantity, + reason=reason, + notes=notes + ) + + if request.headers.get('X-Requested-With') == 'XMLHttpRequest': + return JsonResponse({ + 'success': True, + 'message': f'Обновлено: {item.product.name}' + }) + + messages.success(request, f'Обновлено: {item.product.name}') + + except ValidationError as e: + if request.headers.get('X-Requested-With') == 'XMLHttpRequest': + return JsonResponse({'success': False, 'error': str(e)}, status=400) + messages.error(request, str(e)) + + return redirect('inventory:writeoff-document-detail', pk=pk) + + +class WriteOffDocumentRemoveItemView(LoginRequiredMixin, View): + """Удаление позиции из документа списания""" + + @transaction.atomic + def post(self, request, pk, item_pk): + document = get_object_or_404(WriteOffDocument, pk=pk) + item = get_object_or_404(WriteOffDocumentItem, pk=item_pk, document=document) + + try: + product_name = item.product.name + WriteOffDocumentService.remove_item(item) + + if request.headers.get('X-Requested-With') == 'XMLHttpRequest': + return JsonResponse({ + 'success': True, + 'message': f'Удалено: {product_name}' + }) + + messages.success(request, f'Удалено: {product_name}') + + except ValidationError as e: + if request.headers.get('X-Requested-With') == 'XMLHttpRequest': + return JsonResponse({'success': False, 'error': str(e)}, status=400) + messages.error(request, str(e)) + + return redirect('inventory:writeoff-document-detail', pk=pk) + + +class WriteOffDocumentConfirmView(LoginRequiredMixin, View): + """Проведение документа списания""" + + @transaction.atomic + def post(self, request, pk): + document = get_object_or_404(WriteOffDocument, pk=pk) + + try: + result = WriteOffDocumentService.confirm_document( + document, + confirmed_by=request.user + ) + messages.success( + request, + f'Документ {document.document_number} проведён. ' + f'Списано {result["total_quantity"]} шт на сумму {result["total_cost"]:.2f}' + ) + except ValidationError as e: + messages.error(request, str(e)) + + return redirect('inventory:writeoff-document-detail', pk=pk) + + +class WriteOffDocumentCancelView(LoginRequiredMixin, View): + """Отмена документа списания""" + + @transaction.atomic + def post(self, request, pk): + document = get_object_or_404(WriteOffDocument, pk=pk) + + try: + WriteOffDocumentService.cancel_document(document) + messages.success(request, f'Документ {document.document_number} отменён') + except ValidationError as e: + messages.error(request, str(e)) + + return redirect('inventory:writeoff-document-list')