197 lines
7.5 KiB
Python
197 lines
7.5 KiB
Python
"""
|
|
Procesador de albaranes con OCR y vinculación automática
|
|
"""
|
|
from pathlib import Path
|
|
from typing import Dict, Optional, List
|
|
from datetime import datetime
|
|
from prisma import Prisma
|
|
from app.services.ocr_service import OCRService
|
|
|
|
|
|
class AlbaranProcessor:
|
|
"""Procesa albaranes y los vincula con pedidos pendientes"""
|
|
|
|
def __init__(self, db: Prisma):
|
|
self.db = db
|
|
self.ocr_service = OCRService()
|
|
|
|
async def _find_proveedor(self, datos: Dict) -> Optional[int]:
|
|
"""Busca el proveedor basándose en los datos del albarán"""
|
|
nombre = datos.get('proveedor', {}).get('nombre', '').strip()
|
|
|
|
if nombre:
|
|
proveedor = await self.db.proveedor.find_first(
|
|
where={"nombre": {"contains": nombre, "mode": "insensitive"}}
|
|
)
|
|
if proveedor:
|
|
return proveedor.id
|
|
|
|
return None
|
|
|
|
def _parse_fecha(self, fecha_str: str) -> Optional[datetime]:
|
|
"""Parsea una fecha desde string"""
|
|
if not fecha_str:
|
|
return None
|
|
|
|
from datetime import datetime
|
|
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
|
|
|
|
async def _match_referencias(
|
|
self,
|
|
referencias_albaran: List[Dict],
|
|
proveedor_id: int
|
|
) -> Dict[str, int]:
|
|
"""
|
|
Busca referencias del albarán en pedidos pendientes del proveedor
|
|
|
|
Returns:
|
|
Dict mapping referencia -> referencia_pedido_proveedor_id
|
|
"""
|
|
matches = {}
|
|
|
|
# Obtener todas las referencias pendientes del proveedor
|
|
pedidos_pendientes = await self.db.pedidoproveedor.find_many(
|
|
where={
|
|
"proveedorId": proveedor_id,
|
|
"estado": {"in": ["pendiente_recepcion", "parcial"]}
|
|
},
|
|
include={"referencias": True}
|
|
)
|
|
|
|
for pedido in pedidos_pendientes:
|
|
for ref_pedido in pedido.referencias:
|
|
if ref_pedido.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.id
|
|
break
|
|
|
|
return matches
|
|
|
|
async def match_and_update_referencias(self, albaran):
|
|
"""Vincula y actualiza referencias del albarán con pedidos pendientes"""
|
|
if not albaran.proveedorId:
|
|
return
|
|
|
|
referencias_albaran = await self.db.referenciaalbaran.find_many(
|
|
where={"albaranId": albaran.id}
|
|
)
|
|
|
|
matches = await self._match_referencias(
|
|
[{"referencia": ref.referencia} for ref in referencias_albaran],
|
|
albaran.proveedorId
|
|
)
|
|
|
|
for ref_albaran in referencias_albaran:
|
|
ref_pedido_proveedor_id = matches.get(ref_albaran.referencia.strip().upper())
|
|
|
|
if ref_pedido_proveedor_id:
|
|
# Actualizar referencia albarán
|
|
await self.db.referenciaalbaran.update(
|
|
where={"id": ref_albaran.id},
|
|
data={"referenciaPedidoProveedorId": ref_pedido_proveedor_id}
|
|
)
|
|
|
|
# Actualizar pedido proveedor
|
|
ref_pedido = await self.db.referenciapedidoproveedor.find_unique(
|
|
where={"id": ref_pedido_proveedor_id}
|
|
)
|
|
|
|
nuevas_unidades_recibidas = ref_pedido.unidadesRecibidas + ref_albaran.unidades
|
|
|
|
nuevo_estado = "recibido"
|
|
if nuevas_unidades_recibidas < ref_pedido.unidadesPedidas:
|
|
nuevo_estado = "parcial" if nuevas_unidades_recibidas > 0 else "pendiente"
|
|
|
|
await self.db.referenciapedidoproveedor.update(
|
|
where={"id": ref_pedido_proveedor_id},
|
|
data={
|
|
"unidadesRecibidas": nuevas_unidades_recibidas,
|
|
"estado": nuevo_estado
|
|
}
|
|
)
|
|
|
|
# Actualizar referencia pedido cliente
|
|
if ref_pedido.referenciaPedidoClienteId:
|
|
ref_cliente = await self.db.referenciapedidocliente.find_unique(
|
|
where={"id": ref_pedido.referenciaPedidoClienteId}
|
|
)
|
|
|
|
await self.db.referenciapedidocliente.update(
|
|
where={"id": ref_cliente.id},
|
|
data={
|
|
"unidadesEnStock": ref_cliente.unidadesEnStock + ref_albaran.unidades,
|
|
"unidadesPendientes": max(0, ref_cliente.unidadesSolicitadas - (ref_cliente.unidadesEnStock + ref_albaran.unidades))
|
|
}
|
|
)
|
|
|
|
async def process_albaran_file(self, file_path: Path):
|
|
"""
|
|
Procesa un archivo de albarán (imagen o PDF)
|
|
|
|
Returns:
|
|
Albaran creado
|
|
"""
|
|
# Procesar con OCR
|
|
datos = self.ocr_service.process_albaran(file_path)
|
|
|
|
# Buscar proveedor
|
|
proveedor_id = await self._find_proveedor(datos)
|
|
|
|
# Parsear fecha
|
|
fecha_albaran = self._parse_fecha(datos.get('fecha_albaran', ''))
|
|
|
|
# Crear albarán
|
|
albaran = await self.db.albaran.create(
|
|
data={
|
|
"proveedorId": proveedor_id,
|
|
"numeroAlbaran": datos.get('numero_albaran', '').strip() or None,
|
|
"fechaAlbaran": fecha_albaran,
|
|
"archivoPath": str(file_path),
|
|
"estadoProcesado": "procesado" if proveedor_id else "clasificacion",
|
|
"fechaProcesado": datetime.now() if proveedor_id else None,
|
|
"datosOcr": datos,
|
|
"referencias": {
|
|
"create": [
|
|
{
|
|
"referencia": ref_data.get('referencia', '').strip(),
|
|
"denominacion": ref_data.get('denominacion', '').strip(),
|
|
"unidades": int(ref_data.get('unidades', 1)),
|
|
"precioUnitario": float(ref_data.get('precio_unitario', 0)),
|
|
"impuestoTipo": ref_data.get('impuesto_tipo', '21'),
|
|
"impuestoValor": float(ref_data.get('impuesto_valor', 0)),
|
|
}
|
|
for ref_data in datos.get('referencias', [])
|
|
]
|
|
}
|
|
},
|
|
include={"proveedor": True, "referencias": True}
|
|
)
|
|
|
|
# Vincular referencias si hay proveedor
|
|
if proveedor_id:
|
|
await self.match_and_update_referencias(albaran)
|
|
# Recargar albarán con referencias actualizadas
|
|
albaran = await self.db.albaran.find_unique(
|
|
where={"id": albaran.id},
|
|
include={"proveedor": True, "referencias": True}
|
|
)
|
|
|
|
return albaran
|
|
|