from rest_framework import viewsets, status from rest_framework.decorators import action from rest_framework.response import Response from rest_framework.views import APIView from django.db.models import Q, Prefetch from django.utils import timezone from django.shortcuts import render from datetime import timedelta from pathlib import Path import json from .models import ( Cliente, PedidoCliente, ReferenciaPedidoCliente, Proveedor, PedidoProveedor, ReferenciaPedidoProveedor, Albaran, ReferenciaAlbaran, Devolucion, StockReferencia ) from .serializers import ( ClienteSerializer, PedidoClienteSerializer, ReferenciaPedidoClienteSerializer, ProveedorSerializer, PedidoProveedorSerializer, ReferenciaPedidoProveedorSerializer, AlbaranSerializer, ReferenciaAlbaranSerializer, DevolucionSerializer, StockReferenciaSerializer, UpdateStockSerializer, BulkUpdateStockSerializer, CrearPedidoProveedorSerializer ) from .services.albaran_processor import AlbaranProcessor from .services.pdf_parser import PDFPedidoParser from django.conf import settings class ClienteViewSet(viewsets.ModelViewSet): queryset = Cliente.objects.all() serializer_class = ClienteSerializer filterset_fields = ['nombre', 'matricula_vehiculo'] class PedidoClienteViewSet(viewsets.ModelViewSet): queryset = PedidoCliente.objects.select_related('cliente').prefetch_related('referencias').all() serializer_class = PedidoClienteSerializer def get_queryset(self): queryset = super().get_queryset() # Filtros estado = self.request.query_params.get('estado') if estado: queryset = queryset.filter(estado=estado) urgente = self.request.query_params.get('urgente') if urgente == 'true': queryset = [p for p in queryset if p.es_urgente] matricula = self.request.query_params.get('matricula') if matricula: queryset = queryset.filter(cliente__matricula_vehiculo__icontains=matricula) return queryset @action(detail=True, methods=['post']) def actualizar_estado(self, request, pk=None): """Actualiza el estado del pedido""" pedido = self.get_object() nuevo_estado = request.data.get('estado') if nuevo_estado in dict(PedidoCliente.ESTADOS): pedido.estado = nuevo_estado pedido.save() return Response({'status': 'Estado actualizado'}) return Response({'error': 'Estado inválido'}, status=400) class ReferenciaPedidoClienteViewSet(viewsets.ModelViewSet): queryset = ReferenciaPedidoCliente.objects.all() serializer_class = ReferenciaPedidoClienteSerializer @action(detail=True, methods=['post']) def marcar_stock(self, request, pk=None): """Marca unidades en stock para una referencia""" referencia = self.get_object() unidades = request.data.get('unidades_en_stock', 0) referencia.unidades_en_stock = max(0, unidades) referencia.save() # Actualizar estado del pedido si es necesario pedido = referencia.pedido_cliente todas_completas = all( ref.unidades_pendientes == 0 for ref in pedido.referencias.all() ) if todas_completas and pedido.estado != 'completado': pedido.estado = 'completado' pedido.save() elif pedido.estado == 'pendiente_revision': pedido.estado = 'en_revision' pedido.save() return Response(ReferenciaPedidoClienteSerializer(referencia).data) class ProveedorViewSet(viewsets.ModelViewSet): queryset = Proveedor.objects.filter(activo=True) serializer_class = ProveedorSerializer class PedidoProveedorViewSet(viewsets.ModelViewSet): queryset = PedidoProveedor.objects.select_related('proveedor').prefetch_related('referencias').all() serializer_class = PedidoProveedorSerializer def get_queryset(self): queryset = super().get_queryset() proveedor_id = self.request.query_params.get('proveedor_id') if proveedor_id: queryset = queryset.filter(proveedor_id=proveedor_id) return queryset @action(detail=False, methods=['post']) def crear_manual(self, request): """Crea un pedido a proveedor manualmente""" serializer = CrearPedidoProveedorSerializer(data=request.data) if not serializer.is_valid(): return Response(serializer.errors, status=400) data = serializer.validated_data proveedor = Proveedor.objects.get(id=data['proveedor_id']) pedido = PedidoProveedor.objects.create( proveedor=proveedor, numero_pedido=data.get('numero_pedido', ''), tipo=data['tipo'], estado='pendiente_recepcion' ) # Crear referencias for ref_data in data['referencias']: ref_pedido_cliente_id = ref_data.get('referencia_pedido_cliente_id') ref_pedido_cliente = None if ref_pedido_cliente_id: try: ref_pedido_cliente = ReferenciaPedidoCliente.objects.get(id=ref_pedido_cliente_id) except ReferenciaPedidoCliente.DoesNotExist: pass ReferenciaPedidoProveedor.objects.create( pedido_proveedor=pedido, referencia_pedido_cliente=ref_pedido_cliente, referencia=ref_data['referencia'], denominacion=ref_data.get('denominacion', ''), unidades_pedidas=int(ref_data['unidades']), ) return Response(PedidoProveedorSerializer(pedido).data, status=201) class AlbaranViewSet(viewsets.ModelViewSet): queryset = Albaran.objects.select_related('proveedor').prefetch_related('referencias').all() serializer_class = AlbaranSerializer def get_queryset(self): queryset = super().get_queryset() estado = self.request.query_params.get('estado_procesado') if estado: queryset = queryset.filter(estado_procesado=estado) return queryset @action(detail=False, methods=['post']) def upload(self, request): """Sube un albarán desde móvil o web""" if 'archivo' not in request.FILES: return Response({'error': 'No se proporcionó archivo'}, status=400) archivo = request.FILES['archivo'] # Guardar archivo upload_dir = settings.ALBARANES_ESCANEADOS_DIR upload_dir.mkdir(exist_ok=True) file_path = upload_dir / archivo.name with open(file_path, 'wb+') as destination: for chunk in archivo.chunks(): destination.write(chunk) # Procesar try: processor = AlbaranProcessor() albaran = processor.process_albaran_file(file_path) return Response(AlbaranSerializer(albaran).data, status=201) except Exception as e: return Response({'error': str(e)}, status=400) @action(detail=True, methods=['post']) def vincular_proveedor(self, request, pk=None): """Vincula un albarán a un proveedor manualmente""" albaran = self.get_object() proveedor_id = request.data.get('proveedor_id') if not proveedor_id: return Response({'error': 'proveedor_id requerido'}, status=400) try: proveedor = Proveedor.objects.get(id=proveedor_id) albaran.proveedor = proveedor albaran.estado_procesado = 'procesado' albaran.save() # Reprocesar para vincular referencias processor = AlbaranProcessor() processor._match_and_update_referencias(albaran) return Response(AlbaranSerializer(albaran).data) except Proveedor.DoesNotExist: return Response({'error': 'Proveedor no encontrado'}, status=404) class DevolucionViewSet(viewsets.ModelViewSet): queryset = Devolucion.objects.select_related('proveedor').all() serializer_class = DevolucionSerializer def get_queryset(self): queryset = super().get_queryset() estado = self.request.query_params.get('estado_abono') if estado: queryset = queryset.filter(estado_abono=estado) return queryset @action(detail=True, methods=['post']) def vincular_abono(self, request, pk=None): """Vincula una devolución con un albarán de abono""" devolucion = self.get_object() albaran_id = request.data.get('albaran_id') if not albaran_id: return Response({'error': 'albaran_id requerido'}, status=400) try: albaran = Albaran.objects.get(id=albaran_id) devolucion.albaran_abono = albaran devolucion.estado_abono = 'abonado' devolucion.save() return Response(DevolucionSerializer(devolucion).data) except Albaran.DoesNotExist: return Response({'error': 'Albarán no encontrado'}, status=404) class StockReferenciaViewSet(viewsets.ModelViewSet): queryset = StockReferencia.objects.all() serializer_class = StockReferenciaSerializer # Vista para Kanban class KanbanView(APIView): """Vista para obtener datos del Kanban""" def get(self, request): pedidos = PedidoCliente.objects.select_related('cliente').prefetch_related( Prefetch('referencias', queryset=ReferenciaPedidoCliente.objects.all()) ).all() # Agrupar por estado kanban_data = { 'pendiente_revision': [], 'en_revision': [], 'pendiente_materiales': [], 'completado': [], } for pedido in pedidos: pedido_data = PedidoClienteSerializer(pedido).data kanban_data[pedido.estado].append(pedido_data) # Si es request HTML, renderizar template if request.accepted_renderer.format == 'html' or 'text/html' in request.META.get('HTTP_ACCEPT', ''): return render(request, 'kanban.html') return Response(kanban_data) # Vista para referencias pendientes por proveedor class ReferenciasProveedorView(APIView): """Vista para ver referencias pendientes por proveedor""" def get(self, request): proveedor_id = request.query_params.get('proveedor_id') # Obtener referencias pendientes de pedidos a proveedor queryset = ReferenciaPedidoProveedor.objects.filter( estado__in=['pendiente', 'parcial'] ).select_related( 'pedido_proveedor__proveedor', 'referencia_pedido_cliente__pedido_cliente__cliente' ) if proveedor_id: queryset = queryset.filter(pedido_proveedor__proveedor_id=proveedor_id) # Agrupar por proveedor proveedores_data = {} for ref in queryset: proveedor = ref.pedido_proveedor.proveedor if proveedor.id not in proveedores_data: proveedores_data[proveedor.id] = { 'proveedor': ProveedorSerializer(proveedor).data, 'referencias_pendientes': [], 'referencias_devolucion': [], } proveedores_data[proveedor.id]['referencias_pendientes'].append( ReferenciaPedidoProveedorSerializer(ref).data ) # Agregar devoluciones pendientes devoluciones = Devolucion.objects.filter(estado_abono='pendiente') if proveedor_id: devoluciones = devoluciones.filter(proveedor_id=proveedor_id) for dev in devoluciones: if dev.proveedor.id not in proveedores_data: proveedores_data[dev.proveedor.id] = { 'proveedor': ProveedorSerializer(dev.proveedor).data, 'referencias_pendientes': [], 'referencias_devolucion': [], } proveedores_data[dev.proveedor.id]['referencias_devolucion'].append( DevolucionSerializer(dev).data ) # Si es request HTML, renderizar template if request.accepted_renderer.format == 'html' or 'text/html' in request.META.get('HTTP_ACCEPT', ''): return render(request, 'proveedores.html', { 'proveedores_data': list(proveedores_data.values()) }) return Response(list(proveedores_data.values())) # Vista para alertas class AlertasView(APIView): """Vista para obtener alertas (pedidos urgentes)""" def get(self, request): ahora = timezone.now() limite = ahora + timedelta(hours=12) pedidos_urgentes = PedidoCliente.objects.filter( fecha_cita__gte=ahora, fecha_cita__lte=limite, estado__in=['pendiente_revision', 'en_revision', 'pendiente_materiales'] ).select_related('cliente').prefetch_related('referencias') alertas = [] for pedido in pedidos_urgentes: referencias_faltantes = [ ref for ref in pedido.referencias.all() if ref.unidades_pendientes > 0 ] if referencias_faltantes: alertas.append({ 'pedido': PedidoClienteSerializer(pedido).data, 'referencias_faltantes': ReferenciaPedidoClienteSerializer( referencias_faltantes, many=True ).data, 'horas_restantes': (pedido.fecha_cita - ahora).total_seconds() / 3600, }) return Response(alertas)