Initial commit

This commit is contained in:
2025-12-05 11:27:16 -03:00
commit 804bacfbe3
87 changed files with 7260 additions and 0 deletions

View File

84
gestion_pedidos/admin.py Normal file
View File

@@ -0,0 +1,84 @@
from django.contrib import admin
from .models import (
Cliente, PedidoCliente, ReferenciaPedidoCliente,
Proveedor, PedidoProveedor, ReferenciaPedidoProveedor,
Albaran, ReferenciaAlbaran, Devolucion, StockReferencia
)
@admin.register(Cliente)
class ClienteAdmin(admin.ModelAdmin):
list_display = ('id', 'nombre', 'matricula_vehiculo', 'telefono', 'email')
search_fields = ('nombre', 'matricula_vehiculo', 'email')
list_filter = ('nombre',)
@admin.register(PedidoCliente)
class PedidoClienteAdmin(admin.ModelAdmin):
list_display = ('id', 'numero_pedido', 'cliente', 'fecha_pedido', 'fecha_cita', 'estado')
search_fields = ('numero_pedido', 'cliente__nombre', 'cliente__matricula_vehiculo')
list_filter = ('estado', 'fecha_pedido', 'fecha_cita')
date_hierarchy = 'fecha_pedido'
@admin.register(ReferenciaPedidoCliente)
class ReferenciaPedidoClienteAdmin(admin.ModelAdmin):
list_display = ('id', 'pedido_cliente', 'referencia', 'denominacion', 'unidades_solicitadas',
'unidades_en_stock', 'unidades_pendientes', 'estado')
search_fields = ('referencia', 'denominacion', 'pedido_cliente__numero_pedido')
list_filter = ('estado',)
@admin.register(Proveedor)
class ProveedorAdmin(admin.ModelAdmin):
list_display = ('id', 'nombre', 'email', 'tiene_web', 'activo')
search_fields = ('nombre', 'email')
list_filter = ('tiene_web', 'activo')
@admin.register(PedidoProveedor)
class PedidoProveedorAdmin(admin.ModelAdmin):
list_display = ('id', 'proveedor', 'numero_pedido', 'fecha_pedido', 'estado', 'tipo')
search_fields = ('numero_pedido', 'proveedor__nombre')
list_filter = ('estado', 'tipo', 'fecha_pedido')
@admin.register(ReferenciaPedidoProveedor)
class ReferenciaPedidoProveedorAdmin(admin.ModelAdmin):
list_display = ('id', 'pedido_proveedor', 'referencia', 'unidades_pedidas',
'unidades_recibidas', 'estado')
search_fields = ('referencia', 'pedido_proveedor__numero_pedido')
list_filter = ('estado',)
@admin.register(Albaran)
class AlbaranAdmin(admin.ModelAdmin):
list_display = ('id', 'proveedor', 'numero_albaran', 'fecha_albaran',
'estado_procesado', 'fecha_procesado')
search_fields = ('numero_albaran', 'proveedor__nombre')
list_filter = ('estado_procesado', 'fecha_albaran')
date_hierarchy = 'fecha_albaran'
@admin.register(ReferenciaAlbaran)
class ReferenciaAlbaranAdmin(admin.ModelAdmin):
list_display = ('id', 'albaran', 'referencia', 'denominacion', 'unidades',
'precio_unitario', 'impuesto_tipo')
search_fields = ('referencia', 'denominacion', 'albaran__numero_albaran')
list_filter = ('impuesto_tipo',)
@admin.register(Devolucion)
class DevolucionAdmin(admin.ModelAdmin):
list_display = ('id', 'proveedor', 'referencia', 'unidades', 'fecha_devolucion',
'estado_abono')
search_fields = ('referencia', 'proveedor__nombre')
list_filter = ('estado_abono', 'fecha_devolucion')
@admin.register(StockReferencia)
class StockReferenciaAdmin(admin.ModelAdmin):
list_display = ('id', 'referencia', 'unidades_disponibles', 'ultima_actualizacion')
search_fields = ('referencia',)
list_filter = ('ultima_actualizacion',)

7
gestion_pedidos/apps.py Normal file
View File

@@ -0,0 +1,7 @@
from django.apps import AppConfig
class GestionPedidosConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'gestion_pedidos'

View File

View File

@@ -0,0 +1,28 @@
"""
Comando para iniciar el file watcher que monitorea carpetas
"""
from django.core.management.base import BaseCommand
from gestion_pedidos.services.file_watcher import FileWatcherService
import time
import logging
logger = logging.getLogger(__name__)
class Command(BaseCommand):
help = 'Inicia el servicio de monitoreo de carpetas para procesar PDFs y albaranes automáticamente'
def handle(self, *args, **options):
self.stdout.write(self.style.SUCCESS('Iniciando file watcher...'))
watcher = FileWatcherService()
watcher.start()
try:
while True:
time.sleep(1)
except KeyboardInterrupt:
self.stdout.write(self.style.WARNING('\nDeteniendo file watcher...'))
watcher.stop()
self.stdout.write(self.style.SUCCESS('File watcher detenido.'))

View File

354
gestion_pedidos/models.py Normal file
View File

@@ -0,0 +1,354 @@
from django.db import models
from django.core.validators import MinValueValidator
from django.utils import timezone
class Cliente(models.Model):
"""Cliente con información del vehículo"""
nombre = models.CharField(max_length=200)
matricula_vehiculo = models.CharField(max_length=20, unique=True)
telefono = models.CharField(max_length=20, blank=True, null=True)
email = models.EmailField(blank=True, null=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
db_table = 'clientes'
indexes = [
models.Index(fields=['matricula_vehiculo']),
models.Index(fields=['nombre']),
]
def __str__(self):
return f"{self.nombre} - {self.matricula_vehiculo}"
class PedidoCliente(models.Model):
"""Pedido de recambios de un cliente"""
ESTADOS = [
('pendiente_revision', 'Pendiente Revisión'),
('en_revision', 'En Revisión'),
('pendiente_materiales', 'Pendiente Materiales'),
('completado', 'Completado'),
]
numero_pedido = models.CharField(max_length=50, unique=True, db_index=True)
cliente = models.ForeignKey(Cliente, on_delete=models.CASCADE, related_name='pedidos')
fecha_pedido = models.DateTimeField(default=timezone.now)
fecha_cita = models.DateTimeField(blank=True, null=True)
estado = models.CharField(max_length=30, choices=ESTADOS, default='pendiente_revision')
presupuesto_id = models.CharField(max_length=50, blank=True, null=True)
archivo_pdf_path = models.CharField(max_length=500, blank=True, null=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
db_table = 'pedidos_cliente'
indexes = [
models.Index(fields=['numero_pedido']),
models.Index(fields=['estado']),
models.Index(fields=['fecha_cita']),
models.Index(fields=['fecha_pedido']),
]
ordering = ['-fecha_pedido']
def __str__(self):
return f"Pedido {self.numero_pedido} - {self.cliente.matricula_vehiculo}"
@property
def es_urgente(self):
"""Verifica si el pedido es urgente (menos de 12 horas para la cita)"""
if not self.fecha_cita:
return False
tiempo_restante = self.fecha_cita - timezone.now()
return tiempo_restante.total_seconds() < 12 * 3600 and tiempo_restante.total_seconds() > 0
class ReferenciaPedidoCliente(models.Model):
"""Referencias (piezas) de un pedido de cliente"""
ESTADOS = [
('pendiente', 'Pendiente'),
('parcial', 'Parcial'),
('completo', 'Completo'),
]
pedido_cliente = models.ForeignKey(
PedidoCliente,
on_delete=models.CASCADE,
related_name='referencias'
)
referencia = models.CharField(max_length=100, db_index=True)
denominacion = models.CharField(max_length=500)
unidades_solicitadas = models.IntegerField(validators=[MinValueValidator(1)])
unidades_en_stock = models.IntegerField(default=0, validators=[MinValueValidator(0)])
unidades_pendientes = models.IntegerField(default=0, validators=[MinValueValidator(0)])
estado = models.CharField(max_length=20, choices=ESTADOS, default='pendiente')
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
db_table = 'referencias_pedido_cliente'
indexes = [
models.Index(fields=['referencia']),
models.Index(fields=['estado']),
models.Index(fields=['pedido_cliente', 'estado']),
]
def __str__(self):
return f"{self.referencia} - {self.denominacion} ({self.unidades_solicitadas} unidades)"
def calcular_estado(self):
"""Calcula el estado basado en unidades"""
if self.unidades_pendientes <= 0:
return 'completo'
elif self.unidades_pendientes < self.unidades_solicitadas:
return 'parcial'
return 'pendiente'
def save(self, *args, **kwargs):
self.unidades_pendientes = max(0, self.unidades_solicitadas - self.unidades_en_stock)
self.estado = self.calcular_estado()
super().save(*args, **kwargs)
class Proveedor(models.Model):
"""Proveedor de recambios"""
nombre = models.CharField(max_length=200, db_index=True)
email = models.EmailField(blank=True, null=True)
tiene_web = models.BooleanField(default=True)
activo = models.BooleanField(default=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
db_table = 'proveedores'
indexes = [
models.Index(fields=['nombre']),
models.Index(fields=['activo']),
]
def __str__(self):
return self.nombre
class PedidoProveedor(models.Model):
"""Pedido realizado a un proveedor"""
ESTADOS = [
('pendiente_recepcion', 'Pendiente Recepción'),
('parcial', 'Parcial'),
('completado', 'Completado'),
('cancelado', 'Cancelado'),
]
TIPOS = [
('web', 'Web'),
('manual', 'Manual'),
]
proveedor = models.ForeignKey(
Proveedor,
on_delete=models.CASCADE,
related_name='pedidos'
)
numero_pedido = models.CharField(max_length=100, blank=True, null=True)
fecha_pedido = models.DateTimeField(default=timezone.now)
email_confirmacion_path = models.CharField(max_length=500, blank=True, null=True)
estado = models.CharField(max_length=30, choices=ESTADOS, default='pendiente_recepcion')
tipo = models.CharField(max_length=20, choices=TIPOS, default='web')
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
db_table = 'pedidos_proveedor'
indexes = [
models.Index(fields=['proveedor', 'estado']),
models.Index(fields=['fecha_pedido']),
models.Index(fields=['estado']),
]
ordering = ['-fecha_pedido']
def __str__(self):
return f"Pedido {self.numero_pedido or self.id} - {self.proveedor.nombre}"
class ReferenciaPedidoProveedor(models.Model):
"""Referencias pedidas a un proveedor"""
ESTADOS = [
('pendiente', 'Pendiente'),
('parcial', 'Parcial'),
('recibido', 'Recibido'),
]
pedido_proveedor = models.ForeignKey(
PedidoProveedor,
on_delete=models.CASCADE,
related_name='referencias'
)
referencia_pedido_cliente = models.ForeignKey(
ReferenciaPedidoCliente,
on_delete=models.CASCADE,
related_name='pedidos_proveedor',
blank=True,
null=True
)
referencia = models.CharField(max_length=100, db_index=True)
denominacion = models.CharField(max_length=500)
unidades_pedidas = models.IntegerField(validators=[MinValueValidator(1)])
unidades_recibidas = models.IntegerField(default=0, validators=[MinValueValidator(0)])
estado = models.CharField(max_length=20, choices=ESTADOS, default='pendiente')
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
db_table = 'referencias_pedido_proveedor'
indexes = [
models.Index(fields=['referencia']),
models.Index(fields=['estado']),
models.Index(fields=['pedido_proveedor', 'estado']),
]
def __str__(self):
return f"{self.referencia} - {self.denominacion} ({self.unidades_pedidas} unidades)"
class Albaran(models.Model):
"""Albarán recibido de un proveedor"""
ESTADOS_PROCESADO = [
('pendiente', 'Pendiente'),
('procesado', 'Procesado'),
('clasificacion', 'Pendiente Clasificación'),
('error', 'Error'),
]
proveedor = models.ForeignKey(
Proveedor,
on_delete=models.CASCADE,
related_name='albaranes',
blank=True,
null=True
)
numero_albaran = models.CharField(max_length=100, blank=True, null=True)
fecha_albaran = models.DateField(blank=True, null=True)
archivo_path = models.CharField(max_length=500)
estado_procesado = models.CharField(
max_length=30,
choices=ESTADOS_PROCESADO,
default='pendiente'
)
fecha_procesado = models.DateTimeField(blank=True, null=True)
datos_ocr = models.JSONField(default=dict, blank=True) # Datos extraídos por OCR
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
db_table = 'albaranes'
indexes = [
models.Index(fields=['proveedor', 'estado_procesado']),
models.Index(fields=['numero_albaran']),
models.Index(fields=['fecha_albaran']),
models.Index(fields=['estado_procesado']),
]
ordering = ['-created_at']
def __str__(self):
return f"Albarán {self.numero_albaran or self.id} - {self.proveedor.nombre if self.proveedor else 'Sin proveedor'}"
class ReferenciaAlbaran(models.Model):
"""Referencias contenidas en un albarán"""
TIPOS_IMPUESTO = [
('21', '21%'),
('10', '10%'),
('7', '7%'),
('4', '4%'),
('3', '3%'),
('0', '0%'),
]
albaran = models.ForeignKey(
Albaran,
on_delete=models.CASCADE,
related_name='referencias'
)
referencia = models.CharField(max_length=100, db_index=True)
denominacion = models.CharField(max_length=500)
unidades = models.IntegerField(validators=[MinValueValidator(1)])
precio_unitario = models.DecimalField(max_digits=10, decimal_places=2, default=0)
impuesto_tipo = models.CharField(max_length=5, choices=TIPOS_IMPUESTO, default='21')
impuesto_valor = models.DecimalField(max_digits=10, decimal_places=2, default=0)
referencia_pedido_proveedor = models.ForeignKey(
ReferenciaPedidoProveedor,
on_delete=models.SET_NULL,
related_name='referencias_albaran',
blank=True,
null=True
)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
db_table = 'referencias_albaran'
indexes = [
models.Index(fields=['referencia']),
models.Index(fields=['albaran']),
]
def __str__(self):
return f"{self.referencia} - {self.denominacion} ({self.unidades} unidades)"
class Devolucion(models.Model):
"""Devolución de material a proveedor"""
ESTADOS_ABONO = [
('pendiente', 'Pendiente Abono'),
('abonado', 'Abonado'),
]
proveedor = models.ForeignKey(
Proveedor,
on_delete=models.CASCADE,
related_name='devoluciones'
)
referencia = models.CharField(max_length=100, db_index=True)
denominacion = models.CharField(max_length=500, blank=True, null=True)
unidades = models.IntegerField(validators=[MinValueValidator(1)])
fecha_devolucion = models.DateTimeField(default=timezone.now)
estado_abono = models.CharField(max_length=20, choices=ESTADOS_ABONO, default='pendiente')
albaran_abono = models.ForeignKey(
Albaran,
on_delete=models.SET_NULL,
related_name='devoluciones',
blank=True,
null=True
)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
db_table = 'devoluciones'
indexes = [
models.Index(fields=['proveedor', 'estado_abono']),
models.Index(fields=['referencia']),
models.Index(fields=['estado_abono']),
]
ordering = ['-fecha_devolucion']
def __str__(self):
return f"Devolución {self.referencia} - {self.proveedor.nombre} ({self.unidades} unidades)"
class StockReferencia(models.Model):
"""Stock disponible de una referencia"""
referencia = models.CharField(max_length=100, unique=True, db_index=True)
unidades_disponibles = models.IntegerField(default=0, validators=[MinValueValidator(0)])
ultima_actualizacion = models.DateTimeField(auto_now=True)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
db_table = 'stock_referencias'
indexes = [
models.Index(fields=['referencia']),
]
def __str__(self):
return f"{self.referencia} - {self.unidades_disponibles} unidades"

View File

@@ -0,0 +1,124 @@
from rest_framework import serializers
from .models import (
Cliente, PedidoCliente, ReferenciaPedidoCliente,
Proveedor, PedidoProveedor, ReferenciaPedidoProveedor,
Albaran, ReferenciaAlbaran, Devolucion, StockReferencia
)
class ClienteSerializer(serializers.ModelSerializer):
class Meta:
model = Cliente
fields = '__all__'
class ReferenciaPedidoClienteSerializer(serializers.ModelSerializer):
class Meta:
model = ReferenciaPedidoCliente
fields = '__all__'
class PedidoClienteSerializer(serializers.ModelSerializer):
cliente = ClienteSerializer(read_only=True)
cliente_id = serializers.PrimaryKeyRelatedField(
queryset=Cliente.objects.all(),
source='cliente',
write_only=True,
required=False
)
referencias = ReferenciaPedidoClienteSerializer(many=True, read_only=True)
es_urgente = serializers.ReadOnlyField()
class Meta:
model = PedidoCliente
fields = '__all__'
class ProveedorSerializer(serializers.ModelSerializer):
class Meta:
model = Proveedor
fields = '__all__'
class ReferenciaPedidoProveedorSerializer(serializers.ModelSerializer):
class Meta:
model = ReferenciaPedidoProveedor
fields = '__all__'
class PedidoProveedorSerializer(serializers.ModelSerializer):
proveedor = ProveedorSerializer(read_only=True)
proveedor_id = serializers.PrimaryKeyRelatedField(
queryset=Proveedor.objects.all(),
source='proveedor',
write_only=True
)
referencias = ReferenciaPedidoProveedorSerializer(many=True, read_only=True)
class Meta:
model = PedidoProveedor
fields = '__all__'
class ReferenciaAlbaranSerializer(serializers.ModelSerializer):
class Meta:
model = ReferenciaAlbaran
fields = '__all__'
class AlbaranSerializer(serializers.ModelSerializer):
proveedor = ProveedorSerializer(read_only=True)
proveedor_id = serializers.PrimaryKeyRelatedField(
queryset=Proveedor.objects.all(),
source='proveedor',
write_only=True,
required=False,
allow_null=True
)
referencias = ReferenciaAlbaranSerializer(many=True, read_only=True)
class Meta:
model = Albaran
fields = '__all__'
class DevolucionSerializer(serializers.ModelSerializer):
proveedor = ProveedorSerializer(read_only=True)
proveedor_id = serializers.PrimaryKeyRelatedField(
queryset=Proveedor.objects.all(),
source='proveedor',
write_only=True
)
class Meta:
model = Devolucion
fields = '__all__'
class StockReferenciaSerializer(serializers.ModelSerializer):
class Meta:
model = StockReferencia
fields = '__all__'
# Serializers para actualización de stock
class UpdateStockSerializer(serializers.Serializer):
referencia_id = serializers.IntegerField()
unidades_en_stock = serializers.IntegerField(min_value=0)
class BulkUpdateStockSerializer(serializers.Serializer):
updates = UpdateStockSerializer(many=True)
# Serializer para crear pedido a proveedor manualmente
class CrearPedidoProveedorSerializer(serializers.Serializer):
proveedor_id = serializers.IntegerField()
numero_pedido = serializers.CharField(required=False, allow_blank=True)
tipo = serializers.ChoiceField(choices=['web', 'manual'], default='manual')
referencias = serializers.ListField(
child=serializers.DictField(
child=serializers.CharField()
)
)

View File

View File

@@ -0,0 +1,188 @@
"""
Procesador de albaranes con OCR y vinculación automática
"""
from pathlib import Path
from typing import Dict, Optional, List
from django.utils import timezone
from datetime import datetime
from .ocr_service import OCRService
from ..models import (
Proveedor, Albaran, ReferenciaAlbaran,
ReferenciaPedidoProveedor, PedidoProveedor
)
class AlbaranProcessor:
"""Procesa albaranes y los vincula con pedidos pendientes"""
def __init__(self):
self.ocr_service = OCRService()
def _find_proveedor(self, datos: Dict) -> Optional[Proveedor]:
"""Busca el proveedor basándose en los datos del albarán"""
nombre = datos.get('proveedor', {}).get('nombre', '').strip()
numero = datos.get('proveedor', {}).get('numero', '').strip()
# Buscar por nombre
if nombre:
proveedor = Proveedor.objects.filter(
nombre__icontains=nombre
).first()
if proveedor:
return proveedor
# Buscar por número (si se implementa un campo número_proveedor)
# Por ahora, retornamos None si no se encuentra
return None
def _parse_fecha(self, fecha_str: str) -> Optional[datetime]:
"""Parsea una fecha desde string"""
if not fecha_str:
return None
formatos = [
'%Y-%m-%d',
'%d/%m/%Y',
'%d-%m-%Y',
'%Y/%m/%d',
]
for fmt in formatos:
try:
return datetime.strptime(fecha_str, fmt).date()
except ValueError:
continue
return None
def _match_referencias(
self,
referencias_albaran: List[Dict],
proveedor: Proveedor
) -> Dict[str, ReferenciaPedidoProveedor]:
"""
Busca referencias del albarán en pedidos pendientes del proveedor
Returns:
Dict mapping referencia -> ReferenciaPedidoProveedor
"""
matches = {}
# Obtener todas las referencias pendientes del proveedor
pedidos_pendientes = PedidoProveedor.objects.filter(
proveedor=proveedor,
estado__in=['pendiente_recepcion', 'parcial']
).prefetch_related('referencias')
for pedido in pedidos_pendientes:
for ref_pedido in pedido.referencias.filter(estado__in=['pendiente', 'parcial']):
# Buscar coincidencia en el albarán
for ref_albaran in referencias_albaran:
if ref_albaran['referencia'].strip().upper() == ref_pedido.referencia.strip().upper():
if ref_pedido.referencia not in matches:
matches[ref_pedido.referencia] = ref_pedido
break
return matches
def _match_and_update_referencias(self, albaran: Albaran):
"""Vincula y actualiza referencias del albarán con pedidos pendientes"""
if not albaran.proveedor:
return
referencias_albaran = albaran.referencias.all()
matches = self._match_referencias(
[{'referencia': ref.referencia} for ref in referencias_albaran],
albaran.proveedor
)
for ref_albaran in referencias_albaran:
ref_pedido_proveedor = matches.get(ref_albaran.referencia.strip().upper())
if ref_pedido_proveedor:
ref_albaran.referencia_pedido_proveedor = ref_pedido_proveedor
ref_albaran.save()
# Actualizar pedido proveedor
ref_pedido_proveedor.unidades_recibidas += ref_albaran.unidades
if ref_pedido_proveedor.unidades_recibidas >= ref_pedido_proveedor.unidades_pedidas:
ref_pedido_proveedor.estado = 'recibido'
elif ref_pedido_proveedor.unidades_recibidas > 0:
ref_pedido_proveedor.estado = 'parcial'
ref_pedido_proveedor.save()
# Actualizar referencia pedido cliente
if ref_pedido_proveedor.referencia_pedido_cliente:
ref_cliente = ref_pedido_proveedor.referencia_pedido_cliente
ref_cliente.unidades_en_stock += ref_albaran.unidades
ref_cliente.save()
def process_albaran_file(self, file_path: Path) -> Albaran:
"""
Procesa un archivo de albarán (imagen o PDF)
Returns:
Albaran creado o actualizado
"""
# Procesar con OCR
datos = self.ocr_service.process_albaran(file_path)
# Buscar proveedor
proveedor = self._find_proveedor(datos)
# Parsear fecha
fecha_albaran = self._parse_fecha(datos.get('fecha_albaran', ''))
# Crear albarán
albaran = Albaran.objects.create(
proveedor=proveedor,
numero_albaran=datos.get('numero_albaran', '').strip() or None,
fecha_albaran=fecha_albaran,
archivo_path=str(file_path),
estado_procesado='procesado' if proveedor else 'clasificacion',
fecha_procesado=timezone.now(),
datos_ocr=datos,
)
# Crear referencias del albarán
referencias_data = datos.get('referencias', [])
matches = {}
if proveedor:
matches = self._match_referencias(referencias_data, proveedor)
for ref_data in referencias_data:
ref_pedido_proveedor = matches.get(ref_data['referencia'].strip().upper())
referencia = ReferenciaAlbaran.objects.create(
albaran=albaran,
referencia=ref_data.get('referencia', '').strip(),
denominacion=ref_data.get('denominacion', '').strip(),
unidades=int(ref_data.get('unidades', 1)),
precio_unitario=float(ref_data.get('precio_unitario', 0)),
impuesto_tipo=ref_data.get('impuesto_tipo', '21'),
impuesto_valor=float(ref_data.get('impuesto_valor', 0)),
referencia_pedido_proveedor=ref_pedido_proveedor,
)
# Actualizar pedido proveedor si hay match
if ref_pedido_proveedor:
ref_pedido_proveedor.unidades_recibidas += referencia.unidades
# Actualizar estado
if ref_pedido_proveedor.unidades_recibidas >= ref_pedido_proveedor.unidades_pedidas:
ref_pedido_proveedor.estado = 'recibido'
elif ref_pedido_proveedor.unidades_recibidas > 0:
ref_pedido_proveedor.estado = 'parcial'
ref_pedido_proveedor.save()
# Actualizar referencia pedido cliente
if ref_pedido_proveedor.referencia_pedido_cliente:
ref_cliente = ref_pedido_proveedor.referencia_pedido_cliente
ref_cliente.unidades_en_stock += referencia.unidades
ref_cliente.save()
return albaran

View File

@@ -0,0 +1,104 @@
"""
Parser para emails de confirmación de pedidos a proveedores
"""
import email
from pathlib import Path
from typing import Dict, List, Optional
import re
class EmailPedidoParser:
"""Parser para extraer información de emails de confirmación de pedidos"""
def parse_email_file(self, email_path: Path) -> Dict:
"""
Parsea un archivo de email (.eml)
Returns:
Dict con información del pedido
"""
with open(email_path, 'rb') as f:
msg = email.message_from_bytes(f.read())
# Extraer información básica
subject = msg.get('Subject', '')
from_addr = msg.get('From', '')
date = msg.get('Date', '')
# Extraer cuerpo del email
body = self._get_email_body(msg)
# Buscar número de pedido
numero_pedido = self._extract_numero_pedido(subject, body)
# Buscar referencias en el cuerpo
referencias = self._extract_referencias(body)
return {
'numero_pedido': numero_pedido,
'proveedor_email': from_addr,
'fecha': date,
'asunto': subject,
'cuerpo': body,
'referencias': referencias,
}
def _get_email_body(self, msg: email.message.Message) -> str:
"""Extrae el cuerpo del email"""
body = ""
if msg.is_multipart():
for part in msg.walk():
content_type = part.get_content_type()
if content_type == "text/plain":
try:
body = part.get_payload(decode=True).decode('utf-8', errors='ignore')
break
except:
pass
else:
try:
body = msg.get_payload(decode=True).decode('utf-8', errors='ignore')
except:
pass
return body
def _extract_numero_pedido(self, subject: str, body: str) -> Optional[str]:
"""Extrae el número de pedido del asunto o cuerpo"""
# Patrones comunes
patterns = [
r'pedido[:\s]+([A-Z0-9\-]+)',
r'pedido[:\s]+#?(\d+)',
r'ref[:\s]+([A-Z0-9\-]+)',
r'order[:\s]+([A-Z0-9\-]+)',
]
text = f"{subject} {body}".lower()
for pattern in patterns:
match = re.search(pattern, text, re.IGNORECASE)
if match:
return match.group(1).strip()
return None
def _extract_referencias(self, body: str) -> List[Dict]:
"""Extrae referencias del cuerpo del email"""
referencias = []
# Buscar líneas que parezcan referencias
# Formato común: REF123 - Descripción - Cantidad
pattern = r'([A-Z0-9\-]+)\s*[-]\s*([^-\n]+?)\s*[-]\s*(\d+)'
matches = re.finditer(pattern, body, re.IGNORECASE | re.MULTILINE)
for match in matches:
referencias.append({
'referencia': match.group(1).strip(),
'denominacion': match.group(2).strip(),
'unidades': int(match.group(3)),
})
return referencias

View File

@@ -0,0 +1,113 @@
"""
Servicio para monitorear carpetas y procesar nuevos archivos
"""
import time
from pathlib import Path
from watchdog.observers import Observer
from watchdog.events import FileSystemEventHandler
from django.conf import settings
from .pdf_parser import PDFPedidoParser
from .albaran_processor import AlbaranProcessor
import logging
logger = logging.getLogger(__name__)
class PDFPedidoHandler(FileSystemEventHandler):
"""Handler para procesar nuevos PDFs de pedidos de cliente"""
def __init__(self):
self.parser = PDFPedidoParser()
self.procesados = set()
def on_created(self, event):
if event.is_directory:
return
file_path = Path(event.src_path)
# Solo procesar PDFs
if file_path.suffix.lower() != '.pdf':
return
# Evitar procesar el mismo archivo múltiples veces
if str(file_path) in self.procesados:
return
# Esperar un poco para asegurar que el archivo esté completamente escrito
time.sleep(1)
try:
logger.info(f"Procesando nuevo PDF: {file_path}")
pedido = self.parser.create_pedido_from_pdf(file_path)
logger.info(f"Pedido creado exitosamente: {pedido.numero_pedido}")
self.procesados.add(str(file_path))
except Exception as e:
logger.error(f"Error al procesar PDF {file_path}: {e}")
class AlbaranHandler(FileSystemEventHandler):
"""Handler para procesar nuevos albaranes"""
def __init__(self):
self.processor = AlbaranProcessor()
self.procesados = set()
def on_created(self, event):
if event.is_directory:
return
file_path = Path(event.src_path)
# Procesar imágenes y PDFs
if file_path.suffix.lower() not in ['.pdf', '.jpg', '.jpeg', '.png']:
return
if str(file_path) in self.procesados:
return
time.sleep(1)
try:
logger.info(f"Procesando nuevo albarán: {file_path}")
albaran = self.processor.process_albaran_file(file_path)
logger.info(f"Albarán procesado: {albaran.id}")
self.procesados.add(str(file_path))
except Exception as e:
logger.error(f"Error al procesar albarán {file_path}: {e}")
class FileWatcherService:
"""Servicio para monitorear carpetas"""
def __init__(self):
self.observer = Observer()
self.running = False
def start(self):
"""Inicia el monitoreo de carpetas"""
if self.running:
return
# Monitorear carpeta de pedidos de cliente
pedidos_dir = settings.PEDIDOS_CLIENTES_PDF_DIR
pedidos_handler = PDFPedidoHandler()
self.observer.schedule(pedidos_handler, str(pedidos_dir), recursive=False)
# Monitorear carpeta de albaranes
albaranes_dir = settings.ALBARANES_ESCANEADOS_DIR
albaranes_handler = AlbaranHandler()
self.observer.schedule(albaranes_handler, str(albaranes_dir), recursive=False)
self.observer.start()
self.running = True
logger.info("File watcher iniciado")
def stop(self):
"""Detiene el monitoreo"""
if self.running:
self.observer.stop()
self.observer.join()
self.running = False
logger.info("File watcher detenido")

View File

@@ -0,0 +1,196 @@
"""
Servicio de OCR usando GPT-4 Vision API
"""
import base64
import json
from pathlib import Path
from typing import Dict, List, Optional
from openai import OpenAI
from django.conf import settings
from PIL import Image
import io
class OCRService:
"""Servicio para procesar imágenes y PDFs con GPT-4 Vision"""
def __init__(self):
self.client = OpenAI(api_key=settings.OPENAI_API_KEY)
def _encode_image(self, image_path: Path) -> str:
"""Codifica una imagen en base64"""
with open(image_path, "rb") as image_file:
return base64.b64encode(image_file.read()).decode('utf-8')
def _encode_pil_image(self, image: Image.Image) -> str:
"""Codifica una imagen PIL en base64"""
buffered = io.BytesIO()
image.save(buffered, format="PNG")
return base64.b64encode(buffered.getvalue()).decode('utf-8')
def process_pdf_pedido_cliente(self, pdf_path: Path) -> Dict:
"""
Procesa un PDF de pedido de cliente y extrae la información
Returns:
Dict con: numero_pedido, matricula, fecha_cita, referencias (lista)
"""
from pdf2image import convert_from_path
# Convertir PDF a imágenes
images = convert_from_path(pdf_path, dpi=200)
if not images:
raise ValueError("No se pudieron extraer imágenes del PDF")
# Procesar la primera página (o todas si es necesario)
image = images[0]
base64_image = self._encode_pil_image(image)
prompt = """
Analiza este documento de pedido de cliente de recambios. Extrae la siguiente información en formato JSON:
{
"numero_pedido": "número único del pedido",
"matricula": "matrícula del vehículo",
"fecha_cita": "fecha de la cita en formato YYYY-MM-DD o YYYY-MM-DD HH:MM",
"referencias": [
{
"referencia": "código de la referencia",
"denominacion": "descripción/nombre de la pieza",
"unidades": número de unidades
}
]
}
Si algún campo no está disponible, usa null. La fecha debe estar en formato ISO si es posible.
"""
response = self.client.chat.completions.create(
model="gpt-4o",
messages=[
{
"role": "user",
"content": [
{"type": "text", "text": prompt},
{
"type": "image_url",
"image_url": {
"url": f"data:image/png;base64,{base64_image}"
}
}
]
}
],
max_tokens=2000
)
content = response.choices[0].message.content
# Extraer JSON de la respuesta
try:
# Buscar JSON en la respuesta
json_start = content.find('{')
json_end = content.rfind('}') + 1
if json_start != -1 and json_end > json_start:
json_str = content[json_start:json_end]
return json.loads(json_str)
else:
raise ValueError("No se encontró JSON en la respuesta")
except json.JSONDecodeError as e:
raise ValueError(f"Error al parsear JSON: {e}. Respuesta: {content}")
def process_albaran(self, image_path: Path) -> Dict:
"""
Procesa un albarán y extrae la información
Returns:
Dict con: proveedor (nombre o número), numero_albaran, fecha_albaran,
referencias (lista con precios e impuestos)
"""
base64_image = self._encode_image(image_path)
prompt = """
Analiza este albarán de proveedor. Extrae la siguiente información en formato JSON:
{
"proveedor": {
"nombre": "nombre del proveedor",
"numero": "número de proveedor si está visible"
},
"numero_albaran": "número del albarán",
"fecha_albaran": "fecha del albarán en formato YYYY-MM-DD",
"referencias": [
{
"referencia": "código de la referencia",
"denominacion": "descripción de la pieza",
"unidades": número de unidades,
"precio_unitario": precio por unidad (número decimal),
"impuesto_tipo": "21", "10", "7", "4", "3" o "0" según el porcentaje de IVA,
"impuesto_valor": valor del impuesto (número decimal)
}
],
"totales": {
"base_imponible": total base imponible,
"iva_21": total IVA al 21%,
"iva_10": total IVA al 10%,
"iva_7": total IVA al 7%,
"iva_4": total IVA al 4%,
"iva_3": total IVA al 3%,
"total": total general
}
}
IMPORTANTE:
- Si hay múltiples tipos de impuestos, identifica qué referencias tienen cada tipo
- Si el impuesto no está claro por referencia, intenta calcularlo basándote en los totales
- Si algún campo no está disponible, usa null
"""
response = self.client.chat.completions.create(
model="gpt-4o",
messages=[
{
"role": "user",
"content": [
{"type": "text", "text": prompt},
{
"type": "image_url",
"image_url": {
"url": f"data:image/png;base64,{base64_image}"
}
}
]
}
],
max_tokens=3000
)
content = response.choices[0].message.content
try:
json_start = content.find('{')
json_end = content.rfind('}') + 1
if json_start != -1 and json_end > json_start:
json_str = content[json_start:json_end]
return json.loads(json_str)
else:
raise ValueError("No se encontró JSON en la respuesta")
except json.JSONDecodeError as e:
raise ValueError(f"Error al parsear JSON: {e}. Respuesta: {content}")
def process_image(self, image_path: Path, tipo: str = 'albaran') -> Dict:
"""
Procesa una imagen (albarán o pedido)
Args:
image_path: Ruta a la imagen
tipo: 'albaran' o 'pedido_cliente'
"""
if tipo == 'albaran':
return self.process_albaran(image_path)
elif tipo == 'pedido_cliente':
return self.process_pdf_pedido_cliente(image_path)
else:
raise ValueError(f"Tipo no soportado: {tipo}")

View File

@@ -0,0 +1,95 @@
"""
Servicio para procesar PDFs de pedidos de cliente
"""
import json
from pathlib import Path
from typing import Dict, Optional
from django.utils import timezone
from datetime import datetime
from .ocr_service import OCRService
from ..models import Cliente, PedidoCliente, ReferenciaPedidoCliente
class PDFPedidoParser:
"""Parser para procesar PDFs de pedidos de cliente"""
def __init__(self):
self.ocr_service = OCRService()
def parse_pdf(self, pdf_path: Path) -> Dict:
"""
Parsea un PDF de pedido de cliente
Returns:
Dict con los datos extraídos
"""
return self.ocr_service.process_pdf_pedido_cliente(pdf_path)
def create_pedido_from_pdf(self, pdf_path: Path) -> PedidoCliente:
"""
Crea un PedidoCliente y sus referencias desde un PDF
Returns:
PedidoCliente creado
"""
# Parsear PDF
datos = self.parse_pdf(pdf_path)
# Obtener o crear cliente
matricula = datos.get('matricula', '').strip().upper()
if not matricula:
raise ValueError("No se pudo extraer la matrícula del PDF")
cliente, _ = Cliente.objects.get_or_create(
matricula_vehiculo=matricula,
defaults={
'nombre': datos.get('nombre_cliente', f'Cliente {matricula}'),
}
)
# Parsear fecha de cita
fecha_cita = None
fecha_cita_str = datos.get('fecha_cita')
if fecha_cita_str:
try:
# Intentar diferentes formatos
for fmt in ['%Y-%m-%d %H:%M', '%Y-%m-%d', '%d/%m/%Y %H:%M', '%d/%m/%Y']:
try:
fecha_cita = datetime.strptime(fecha_cita_str, fmt)
fecha_cita = timezone.make_aware(fecha_cita)
break
except ValueError:
continue
except Exception:
pass
# Crear pedido
numero_pedido = datos.get('numero_pedido', '').strip()
if not numero_pedido:
raise ValueError("No se pudo extraer el número de pedido del PDF")
# Verificar si ya existe
if PedidoCliente.objects.filter(numero_pedido=numero_pedido).exists():
raise ValueError(f"El pedido {numero_pedido} ya existe")
pedido = PedidoCliente.objects.create(
numero_pedido=numero_pedido,
cliente=cliente,
fecha_cita=fecha_cita,
estado='pendiente_revision',
archivo_pdf_path=str(pdf_path),
)
# Crear referencias
referencias_data = datos.get('referencias', [])
for ref_data in referencias_data:
ReferenciaPedidoCliente.objects.create(
pedido_cliente=pedido,
referencia=ref_data.get('referencia', '').strip(),
denominacion=ref_data.get('denominacion', '').strip(),
unidades_solicitadas=int(ref_data.get('unidades', 1)),
unidades_en_stock=0,
)
return pedido

21
gestion_pedidos/urls.py Normal file
View File

@@ -0,0 +1,21 @@
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from . import views
router = DefaultRouter()
router.register(r'clientes', views.ClienteViewSet)
router.register(r'pedidos-cliente', views.PedidoClienteViewSet, basename='pedido-cliente')
router.register(r'referencias-pedido-cliente', views.ReferenciaPedidoClienteViewSet, basename='referencia-pedido-cliente')
router.register(r'proveedores', views.ProveedorViewSet)
router.register(r'pedidos-proveedor', views.PedidoProveedorViewSet, basename='pedido-proveedor')
router.register(r'albaranes', views.AlbaranViewSet, basename='albaran')
router.register(r'devoluciones', views.DevolucionViewSet)
router.register(r'stock', views.StockReferenciaViewSet)
urlpatterns = [
path('', include(router.urls)),
path('kanban/', views.KanbanView.as_view(), name='kanban'),
path('referencias-proveedor/', views.ReferenciasProveedorView.as_view(), name='referencias-proveedor'),
path('alertas/', views.AlertasView.as_view(), name='alertas'),
path('mobile/upload/', views.AlbaranViewSet.as_view({'post': 'upload'}), name='mobile-upload'),
]

367
gestion_pedidos/views.py Normal file
View File

@@ -0,0 +1,367 @@
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)