Initial commit
This commit is contained in:
0
gestion_pedidos/__init__.py
Normal file
0
gestion_pedidos/__init__.py
Normal file
84
gestion_pedidos/admin.py
Normal file
84
gestion_pedidos/admin.py
Normal 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
7
gestion_pedidos/apps.py
Normal file
@@ -0,0 +1,7 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class GestionPedidosConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'gestion_pedidos'
|
||||
|
||||
0
gestion_pedidos/management/__init__.py
Normal file
0
gestion_pedidos/management/__init__.py
Normal file
0
gestion_pedidos/management/commands/__init__.py
Normal file
0
gestion_pedidos/management/commands/__init__.py
Normal file
28
gestion_pedidos/management/commands/start_file_watcher.py
Normal file
28
gestion_pedidos/management/commands/start_file_watcher.py
Normal 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.'))
|
||||
|
||||
0
gestion_pedidos/migrations/__init__.py
Normal file
0
gestion_pedidos/migrations/__init__.py
Normal file
354
gestion_pedidos/models.py
Normal file
354
gestion_pedidos/models.py
Normal 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"
|
||||
|
||||
124
gestion_pedidos/serializers.py
Normal file
124
gestion_pedidos/serializers.py
Normal 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()
|
||||
)
|
||||
)
|
||||
|
||||
0
gestion_pedidos/services/__init__.py
Normal file
0
gestion_pedidos/services/__init__.py
Normal file
188
gestion_pedidos/services/albaran_processor.py
Normal file
188
gestion_pedidos/services/albaran_processor.py
Normal 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
|
||||
104
gestion_pedidos/services/email_parser.py
Normal file
104
gestion_pedidos/services/email_parser.py
Normal 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
|
||||
|
||||
113
gestion_pedidos/services/file_watcher.py
Normal file
113
gestion_pedidos/services/file_watcher.py
Normal 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")
|
||||
|
||||
196
gestion_pedidos/services/ocr_service.py
Normal file
196
gestion_pedidos/services/ocr_service.py
Normal 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}")
|
||||
|
||||
95
gestion_pedidos/services/pdf_parser.py
Normal file
95
gestion_pedidos/services/pdf_parser.py
Normal 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
21
gestion_pedidos/urls.py
Normal 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
367
gestion_pedidos/views.py
Normal 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)
|
||||
Reference in New Issue
Block a user