# -*- coding: utf-8 -*- import logging from django.shortcuts import render, redirect from django.views.generic import ListView, CreateView, UpdateView, DeleteView, 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.views.decorators.http import require_http_methods from django.utils.decorators import method_decorator from django.db import IntegrityError, transaction from ..models import Incoming, IncomingBatch, Warehouse from ..forms import IncomingForm, IncomingLineForm from inventory.utils import generate_incoming_document_number from products.models import Product file_logger = logging.getLogger('incoming_sequence_file') class IncomingListView(LoginRequiredMixin, ListView): """ Список всех приходов товара (истории поступлений) """ model = Incoming template_name = 'inventory/incoming/incoming_list.html' context_object_name = 'incomings' paginate_by = 20 def get_queryset(self): queryset = Incoming.objects.select_related('product', 'batch', 'batch__warehouse').order_by('-created_at') # Фильтры (если переданы) product_id = self.request.GET.get('product') warehouse_id = self.request.GET.get('warehouse') if product_id: queryset = queryset.filter(product_id=product_id) if warehouse_id: queryset = queryset.filter(batch__warehouse_id=warehouse_id) return queryset class IncomingUpdateView(LoginRequiredMixin, UpdateView): """ Редактирование поступления (только если ещё не обработано). Обработанные приходы редактировать нельзя. """ model = Incoming form_class = IncomingForm template_name = 'inventory/incoming/incoming_form.html' success_url = reverse_lazy('inventory:incoming-list') def form_valid(self, form): # При редактировании можем оставить номер пустым - модель генерирует при сохранении # Но это только если объект ещё не имеет номера (новый) messages.success(self.request, f'Приход товара обновлён.') return super().form_valid(form) class IncomingDeleteView(LoginRequiredMixin, DeleteView): """ Отмена/удаление поступления товара. """ model = Incoming template_name = 'inventory/incoming/incoming_confirm_delete.html' success_url = reverse_lazy('inventory:incoming-list') def form_valid(self, form): incoming = self.get_object() messages.success( self.request, f'Приход товара "{incoming.product.name}" отменён.' ) return super().form_valid(form) class IncomingCreateView(LoginRequiredMixin, View): """ Создание поступлений товара на склад. Позволяет добавить один или несколько товаров в одной форме с одинаковым номером документа и складом. По умолчанию показывается одна пустая строка товара, но пользователь может добавить неограниченное количество товаров. """ template_name = 'inventory/incoming/incoming_bulk_form.html' def get(self, request): """Отображение формы ввода товаров.""" form = IncomingForm() # Django-tenants автоматически фильтрует по текущей схеме products = Product.objects.filter(is_active=True).order_by('name') # Генерируем номер документа автоматически generated_document_number = generate_incoming_document_number() context = { 'form': form, 'products': products, 'generated_document_number': generated_document_number, } return render(request, self.template_name, context) def post(self, request): """Обработка формы ввода товаров.""" form = IncomingForm(request.POST) if not form.is_valid(): # Django-tenants автоматически фильтрует по текущей схеме products = Product.objects.filter(is_active=True).order_by('name') context = { 'form': form, 'products': products, 'errors': form.errors, } return render(request, self.template_name, context) # Получаем данные header warehouse = form.cleaned_data['warehouse'] # Оставляем пустой document_number как есть - модель будет генерировать при сохранении document_number = form.cleaned_data.get('document_number', '').strip() or None supplier_name = form.cleaned_data.get('supplier_name', '') header_notes = form.cleaned_data.get('notes', '') # Получаем данные товаров из POST products_data = self._parse_products_from_post(request.POST) if not products_data: messages.error(request, 'Пожалуйста, добавьте хотя бы один товар.') # Django-tenants автоматически фильтрует по текущей схеме products = Product.objects.filter(is_active=True).order_by('name') context = { 'form': form, 'products': products, } return render(request, self.template_name, context) # Генерируем номер партии один раз (если не указан) if not document_number: document_number = generate_incoming_document_number() file_logger.info(f"--- POST started | batch_doc_number={document_number} | items_count={len(products_data)}") try: # Используем транзакцию для атомарности: либо все товары, либо ничего with transaction.atomic(): # 1. Создаем партию (содержит номер документа и метаданные) batch = IncomingBatch.objects.create( warehouse=warehouse, document_number=document_number, supplier_name=supplier_name, notes=header_notes ) file_logger.info(f" ✓ Created batch: {document_number}") # 2. Создаем товары в этой партии created_count = 0 for product_data in products_data: incoming = Incoming.objects.create( batch=batch, product_id=product_data['product_id'], quantity=product_data['quantity'], cost_price=product_data['cost_price'], ) created_count += 1 file_logger.info(f" ✓ Item: {incoming.product.name} ({incoming.quantity} шт)") file_logger.info(f"✓ SUCCESS: Batch {document_number} with {created_count} items") messages.success( request, f'✓ Успешно создана партия "{document_number}" с {created_count} товарами.' ) return redirect('inventory:incoming-list') except IntegrityError as e: # Ошибка дублирования номера (обычно при вводе вручную существующего номера) file_logger.error(f"✗ IntegrityError: {str(e)} | doc_number={document_number}") if 'document_number' in str(e): error_msg = ( f'❌ Номер документа "{document_number}" уже существует в системе. ' f'Пожалуйста, оставьте поле пустым для автогенерации свободного номера. ' f'Данные, которые вы вводили, сохранены ниже.' ) messages.error(request, error_msg) else: messages.error(request, f'Ошибка при создании партии: {str(e)}') # Восстанавливаем данные на форме products = Product.objects.filter(is_active=True).order_by('name') context = { 'form': form, 'products': products, 'products_json': request.POST.get('products_json', '[]'), } return render(request, self.template_name, context) except Exception as e: messages.error( request, f'❌ Ошибка при создании приходов: {str(e)}' ) # Django-tenants автоматически фильтрует по текущей схеме products = Product.objects.filter(is_active=True).order_by('name') context = { 'form': form, 'products': products, 'products_json': request.POST.get('products_json', '[]'), # Сохраняем товары } return render(request, self.template_name, context) def _parse_products_from_post(self, post_data): """ Парсит данные товаров из POST данных. Ожидается formato: product_ids: [1, 2, 3] quantities: [100, 50, 30] cost_prices: [50, 30, 20] """ products_data = [] # Получаем JSON данные из hidden input (если используется) import json products_json = post_data.get('products_json', '[]') try: products_list = json.loads(products_json) for item in products_list: if item.get('product_id') and item.get('quantity') and item.get('cost_price'): products_data.append({ 'product_id': int(item['product_id']), 'quantity': float(item['quantity']), 'cost_price': float(item['cost_price']), }) except (json.JSONDecodeError, ValueError): pass return products_data