# -*- coding: utf-8 -*- """ Transfer (Перемещение товара между складами) views GROUP 2: MEDIUM PRIORITY """ import json from decimal import Decimal from django.views.generic import ListView, CreateView, UpdateView, DeleteView, DetailView from django.views import View from django.urls import reverse_lazy from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib import messages from django.http import JsonResponse from django.db import transaction from django.shortcuts import redirect, render from ..models import TransferBatch, TransferItem, Stock from ..forms import TransferBulkForm from inventory.utils.document_generator import generate_transfer_document_number from inventory.services.batch_manager import StockBatchManager from products.models import Product class TransferListView(LoginRequiredMixin, ListView): """ View для просмотра списка документов перемещений товаров. """ model = TransferBatch template_name = 'inventory/transfer/transfer_list.html' context_object_name = 'transfers' paginate_by = 20 def get_queryset(self): return TransferBatch.objects.select_related( 'from_warehouse', 'to_warehouse' ).order_by('-created_at') # ============================================================================ # VIEWS ДЛЯ ПЕРЕМЕЩЕНИЯ ТОВАРОВ (TransferBatch + TransferItem) # ============================================================================ class TransferBulkCreateView(LoginRequiredMixin, View): """ View для создания документа перемещения товаров между складами с FIFO логикой. Один документ может содержать несколько товаров. """ template_name = 'inventory/transfer/transfer_bulk_form.html' def get(self, request): from products.models import Product form = TransferBulkForm() products = Product.objects.filter(status='active').values('id', 'name', 'sku').order_by('name') return render(request, self.template_name, { 'form': form, 'products': products }) def post(self, request): form = TransferBulkForm(request.POST) if not form.is_valid(): messages.error(request, 'Ошибка при заполнении формы') return render(request, self.template_name, {'form': form}, status=400) # Получаем данные из формы from_warehouse = form.cleaned_data.get('from_warehouse') to_warehouse = form.cleaned_data.get('to_warehouse') notes = form.cleaned_data.get('notes', '') # Парсим JSON с товарами products_json = request.POST.get('products_json', '[]') try: products_data = json.loads(products_json) except json.JSONDecodeError: messages.error(request, 'Ошибка при парсинге товаров. Пожалуйста, проверьте данные.') return render(request, self.template_name, {'form': form}, status=400) # Проверяем что товары есть if not products_data: messages.error(request, 'Необходимо добавить хотя бы один товар для перемещения.') return render(request, self.template_name, {'form': form}, status=400) # Валидируем товары products = [] for item in products_data: product_id = item.get('product_id') quantity = Decimal(str(item.get('quantity', 0))) if quantity <= 0: messages.error(request, f'Количество товара должно быть больше нуля') return render(request, self.template_name, {'form': form}, status=400) try: product = Product.objects.get(id=product_id, status='active') products.append((product, quantity)) except Product.DoesNotExist: messages.error(request, f'Товар с ID {product_id} не найден или неактивен') return render(request, self.template_name, {'form': form}, status=400) # Начинаем транзакцию try: with transaction.atomic(): # 1. Создаем документ TransferBatch transfer_batch = TransferBatch.objects.create( from_warehouse=from_warehouse, to_warehouse=to_warehouse, document_number=generate_transfer_document_number(), notes=notes ) # 2. Для каждого товара выполняем FIFO перемещение for product, quantity in products: try: # Получаем список распределений по FIFO transfers = StockBatchManager.transfer_product_by_fifo( product=product, from_warehouse=from_warehouse, to_warehouse=to_warehouse, quantity=quantity ) # Создаем TransferItem для каждого использованного batch for source_batch, qty_transferred, new_batch in transfers: TransferItem.objects.create( transfer_batch=transfer_batch, product=product, batch=source_batch, quantity=qty_transferred, new_batch=new_batch ) except ValueError as e: messages.error(request, f'Ошибка при перемещении товара "{product.name}": {str(e)}') raise # Откатываем транзакцию # 3. Успешно создали документ messages.success( request, f'Документ перемещения {transfer_batch.document_number} успешно создан. ' f'Перемещено {len(products)} видов товаров.' ) return redirect('inventory:transfer-detail', pk=transfer_batch.id) except Exception as e: messages.error(request, f'Ошибка при создании документа перемещения: {str(e)}') return render(request, self.template_name, {'form': form}, status=400) class TransferDetailView(LoginRequiredMixin, DetailView): """ View для просмотра деталей документа перемещения. """ model = TransferBatch template_name = 'inventory/transfer/transfer_detail.html' context_object_name = 'transfer_batch' def get_queryset(self): return TransferBatch.objects.select_related( 'from_warehouse', 'to_warehouse' ).prefetch_related( 'items__product', 'items__batch', 'items__new_batch' ) def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) transfer_batch = self.object # Собираем статистику по документу items = transfer_batch.items.all() total_items = items.count() total_qty = sum(Decimal(str(item.quantity)) for item in items) context['total_items'] = total_items context['total_qty'] = total_qty context['items'] = items return context class TransferDeleteView(LoginRequiredMixin, DeleteView): """ View для удаления документа перемещения. """ model = TransferBatch template_name = 'inventory/transfer/transfer_confirm_delete.html' success_url = reverse_lazy('inventory:transfer-list') def form_valid(self, form): transfer_batch = self.get_object() messages.success(self.request, f'Документ перемещения {transfer_batch.document_number} удалён.') return super().form_valid(form) class GetProductStockView(LoginRequiredMixin, View): """ API endpoint для получения доступного количества товара на конкретном складе. GET параметры: product_id, warehouse_id Возвращает JSON: {"quantity": "100.000", "warehouse_name": "Основной склад"} """ def get(self, request): product_id = request.GET.get('product_id') warehouse_id = request.GET.get('warehouse_id') if not product_id or not warehouse_id: return JsonResponse({ 'error': 'Missing required parameters: product_id, warehouse_id' }, status=400) try: product_id = int(product_id) warehouse_id = int(warehouse_id) except ValueError: return JsonResponse({ 'error': 'Invalid parameter values' }, status=400) try: stock = Stock.objects.get(product_id=product_id, warehouse_id=warehouse_id) return JsonResponse({ 'quantity': str(stock.quantity_available), 'warehouse_name': stock.warehouse.name, 'success': True }) except Stock.DoesNotExist: return JsonResponse({ 'quantity': '0.000', 'warehouse_name': '', 'success': True }) except Exception as e: import traceback traceback.print_exc() return JsonResponse({ 'error': str(e), 'success': False }, status=500)