368 lines
14 KiB
Python
368 lines
14 KiB
Python
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)
|