# -*- 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, Http404 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, IncomingModelForm 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 = IncomingModelForm template_name = 'inventory/incoming/incoming_form.html' success_url = reverse_lazy('inventory:incoming-list') def dispatch(self, request, *args, **kwargs): """Проверяем, можно ли редактировать приход""" obj = self.get_object() if not obj.can_edit: messages.error( request, f'Нельзя редактировать приход товара "{obj.product.name}", ' f'так как для него уже создана складская партия. ' f'Редактирование возможно только до обработки прихода.' ) return redirect('inventory:incoming-list') return super().dispatch(request, *args, **kwargs) 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(status='active').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(status='active').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 receipt_type = form.cleaned_data.get('receipt_type', 'supplier') 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(status='active').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, receipt_type=receipt_type, supplier_name=supplier_name if receipt_type == 'supplier' else '', 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(status='active').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(status='active').order_by('name') context = { 'form': form, 'products': products, 'products_json': request.POST.get('products_json', '[]'), # Сохраняем товары } return render(request, self.template_name, context) @staticmethod def _parse_products_from_post(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 class IncomingAdjustmentCreateView(LoginRequiredMixin, View): """ Создание оприходования товара на склад (без инвентаризации). Аналогично IncomingCreateView, но с типом 'adjustment' и без поля поставщика. """ template_name = 'inventory/incoming/incoming_bulk_form.html' def get(self, request): """Отображение формы ввода товаров.""" form = IncomingForm(initial={'receipt_type': 'adjustment'}) # Django-tenants автоматически фильтрует по текущей схеме products = Product.objects.filter(status='active').order_by('name') # Генерируем номер документа автоматически generated_document_number = generate_incoming_document_number() context = { 'form': form, 'products': products, 'generated_document_number': generated_document_number, 'is_adjustment': True, # Флаг для шаблона, чтобы скрыть supplier_name } return render(request, self.template_name, context) def post(self, request): """Обработка формы ввода товаров.""" # Устанавливаем receipt_type в 'adjustment' post_data = request.POST.copy() post_data['receipt_type'] = 'adjustment' form = IncomingForm(post_data) if not form.is_valid(): # Django-tenants автоматически фильтрует по текущей схеме products = Product.objects.filter(status='active').order_by('name') context = { 'form': form, 'products': products, 'errors': form.errors, 'is_adjustment': True, } return render(request, self.template_name, context) # Получаем данные header warehouse = form.cleaned_data['warehouse'] document_number = form.cleaned_data.get('document_number', '').strip() or None receipt_type = 'adjustment' # Всегда adjustment для этого view header_notes = form.cleaned_data.get('notes', '') # Получаем данные товаров из POST products_data = IncomingCreateView._parse_products_from_post(request.POST) if not products_data: messages.error(request, 'Пожалуйста, добавьте хотя бы один товар.') products = Product.objects.filter(status='active').order_by('name') context = { 'form': form, 'products': products, 'is_adjustment': True, } return render(request, self.template_name, context) # Генерируем номер партии один раз (если не указан) if not document_number: document_number = generate_incoming_document_number() file_logger.info(f"--- POST started (adjustment) | 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, receipt_type=receipt_type, supplier_name='', # Не заполняем для adjustment 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(status='active').order_by('name') context = { 'form': form, 'products': products, 'products_json': request.POST.get('products_json', '[]'), 'is_adjustment': True, } return render(request, self.template_name, context) except Exception as e: messages.error( request, f'❌ Ошибка при создании оприходования: {str(e)}' ) products = Product.objects.filter(status='active').order_by('name') context = { 'form': form, 'products': products, 'products_json': request.POST.get('products_json', '[]'), 'is_adjustment': True, } return render(request, self.template_name, context)