diff --git a/api/routers/invoices.py b/api/routers/invoices.py index 842a61a..6601f3d 100644 --- a/api/routers/invoices.py +++ b/api/routers/invoices.py @@ -3,7 +3,8 @@ from fastapi import APIRouter, Depends, UploadFile, File, HTTPException, status, from typing import Dict, Optional from api.dependencies import get_current_active_user -from services import invoice_processor_service +# Importamos la función específica para que el código sea más claro +from services.invoice_processor_service import process_invoice_from_bytes from db.models import User router = APIRouter() @@ -14,11 +15,14 @@ ALLOWED_CONTENT_TYPES = ["application/pdf", "image/jpeg", "image/png", "image/ti async def upload_invoice( current_user: User = Depends(get_current_active_user), file: UploadFile = File(...), - # Nuevo parámetro: viene del formulario, es opcional y debe estar entre 0.0 y 1.0 - confidence_threshold: Optional[float] = Form(None, ge=0.0, le=1.0) + # --- PUNTO CRÍTICO --- + # El nombre del parámetro aquí, `default_confidence_override`, DEBE coincidir + # con el nombre usado en el formData.append() del JavaScript. + default_confidence_override: Optional[float] = Form(None, ge=0.0, le=1.0) ): """ - Endpoint para subir una factura. Ahora acepta un umbral de confianza opcional. + Endpoint para subir una factura. Acepta un umbral para sobrescribir + la confianza por defecto. """ if file.content_type not in ALLOWED_CONTENT_TYPES: raise HTTPException( @@ -29,11 +33,11 @@ async def upload_invoice( try: file_bytes = await file.read() - # Pasamos el umbral recibido al servicio - extracted_data = invoice_processor_service.process_invoice_from_bytes( + # Pasamos el parámetro con el nombre correcto al servicio + extracted_data = process_invoice_from_bytes( file_bytes=file_bytes, mime_type=file.content_type, - override_threshold=confidence_threshold + default_confidence_override=default_confidence_override ) return extracted_data except Exception as e: diff --git a/core/config.py b/core/config.py index 364bad9..fb3d742 100644 --- a/core/config.py +++ b/core/config.py @@ -15,20 +15,25 @@ class Settings(BaseSettings): GCP_LOCATION: str DOCAI_PROCESSOR_ID: str - # --- Lógica de Negocio (extraída del antiguo config.py) --- + # --- Lógica de Negocio (pedido por Jaime, datos a extraer) --- REQUIRED_FIELDS: List[str] = [ - "supplier_name", "invoice_id", "invoice_date", - "total_amount" + "total_amount", + "net_amount", + "receiver_name", + "supplier_tax_id" ] - - # Umbrales de confianza por campo. Un valor por defecto y anulaciones específicas. - CONFIDENCE_THRESHOLDS: Dict[str, float] = { - "__default__": 0.85, - "supplier_name": 0.90, - "total_amount": 0.95 - } + # --- CAMBIO PARA DEPURACIÓN --- + # Bajamos los umbrales para asegurarnos de que los datos se extraen. + CONFIDENCE_THRESHOLDS: Dict[str, float] = { + "__default__": 0.82, # <-- Bajamos el por defecto al 10% + "supplier_name": 0.80, + "total_amount": 0.75, # <-- Bajamos todos a un nivel muy bajo + "net_amount": 0.92, + "receiver_name": 0.74, + "supplier_tax_id": 0.46 # <-- Especialmente este que estaba en 0.46 + } # Creamos una única instancia global de la configuración settings = Settings() diff --git a/invoice-processing-google-document-ai-fastAPI.tgz b/invoice-processing-google-document-ai-fastAPI.tgz deleted file mode 100644 index b33511b..0000000 Binary files a/invoice-processing-google-document-ai-fastAPI.tgz and /dev/null differ diff --git a/services/invoice_processor_service.py b/services/invoice_processor_service.py index f32c6dc..8beb716 100644 --- a/services/invoice_processor_service.py +++ b/services/invoice_processor_service.py @@ -3,55 +3,50 @@ import logging from typing import Dict, List, Any, Optional from .gcp_document_ai_client import process_document_gcp -# Importamos nuestro limpiador de datos para usarlo from .utils import data_cleaner from core.config import settings -# --- Lógica de negocio refactorizada --- - def _extract_specific_fields( - entities: List[Any], - override_threshold: Optional[float] = None + entities: List[Any], + # El nombre del parámetro aquí debe coincidir con el que se le pasa desde el router + default_confidence_override: Optional[float] = None ) -> Dict[str, str]: """ - Filtra y normaliza entidades. Si se proporciona `override_threshold`, - se utiliza ese valor para todos los campos. De lo contrario, utiliza - los umbrales definidos en la configuración. + Filtra y normaliza entidades. Si se proporciona `default_confidence_override`, + se utiliza para el umbral por defecto. Los umbrales específicos de la + configuración siempre tienen prioridad. """ extracted_data = {field: "Not found or low confidence" for field in settings.REQUIRED_FIELDS} + # Determina el umbral por defecto para esta ejecución + default_threshold = default_confidence_override if default_confidence_override is not None else settings.CONFIDENCE_THRESHOLDS["__default__"] + for entity in entities: entity_type = entity.type_ - # Lógica de decisión del umbral - if override_threshold is not None: - threshold = override_threshold - else: - # Comportamiento original: usar la configuración por campo - threshold = settings.CONFIDENCE_THRESHOLDS.get(entity_type, settings.CONFIDENCE_THRESHOLDS["__default__"]) + if entity_type in settings.REQUIRED_FIELDS: + # Lógica corregida: Prioriza el umbral específico del campo, si no, usa el por defecto. + threshold = settings.CONFIDENCE_THRESHOLDS.get(entity_type, default_threshold) - if entity_type in settings.REQUIRED_FIELDS and entity.confidence >= threshold: - value = entity.mention_text.replace('\n', ' ').strip() - - # Reactivamos la limpieza de fechas - if entity_type == 'invoice_date': - normalized_date = data_cleaner.normalize_date(value) - value = normalized_date if normalized_date else f"Unparseable Date: '{value}'" - - extracted_data[entity_type] = value + if entity.confidence >= threshold: + value = entity.mention_text.replace('\n', ' ').strip() + + if entity_type == 'invoice_date': + normalized_date = data_cleaner.normalize_date(value) + value = normalized_date if normalized_date else f"Unparseable Date: '{value}'" + + extracted_data[entity_type] = value return extracted_data -# --- Función principal del servicio actualizada --- - def process_invoice_from_bytes( file_bytes: bytes, mime_type: str, - override_threshold: Optional[float] = None # Nuevo parámetro opcional + # El nombre del parámetro aquí debe coincidir con el del router + default_confidence_override: Optional[float] = None ) -> Dict[str, str]: """ - Orquesta el proceso completo. Ahora pasa el umbral de confianza - opcional a la capa de lógica de negocio. + Orquesta el proceso completo. """ try: document = process_document_gcp( @@ -62,10 +57,10 @@ def process_invoice_from_bytes( mime_type=mime_type, ) - # Pasamos el umbral opcional a la función de extracción - validated_data = _extract_specific_fields(document.entities, override_threshold) + validated_data = _extract_specific_fields(document.entities, default_confidence_override) - logging.info(f"Documento procesado con éxito con un umbral de {override_threshold or 'default'}.") + log_threshold = default_confidence_override if default_confidence_override is not None else "config default" + logging.info(f"Documento procesado. Umbral por defecto usado: {log_threshold}.") return validated_data except Exception as e: diff --git a/services/utils/data_cleaner.py b/services/utils/data_cleaner.py index 83a78bb..45294df 100644 --- a/services/utils/data_cleaner.py +++ b/services/utils/data_cleaner.py @@ -1,4 +1,4 @@ -# src/cli_invoice_processor/data_cleaner.py +# services/utils/data_cleaner.py import logging import locale from dateutil import parser diff --git a/templates/dashboard.html b/templates/dashboard.html index 8f9b136..4aedc7e 100644 --- a/templates/dashboard.html +++ b/templates/dashboard.html @@ -20,13 +20,11 @@ .upload-area { margin-top: 1rem; border: 2px dashed #bdc3c7; padding: 2rem; border-radius: 8px; text-align: center; background-color: #ecf0f1; } .upload-form button { background-color: #3498db; color: white; border: none; padding: 12px 24px; border-radius: 5px; cursor: pointer; font-size: 16px; font-weight: bold; margin-top: 1rem; } .upload-form button:disabled { background-color: #95a5a6; cursor: not-allowed; } - /* --- Nuevo: Estilos para la configuración avanzada --- */ .settings { margin-top: 2rem; padding: 1.5rem; background-color: #f8f9fa; border-radius: 8px; } .slider-container { display: flex; align-items: center; gap: 15px; } .slider-container label { font-weight: bold; } .slider-container input[type="range"] { flex-grow: 1; } .slider-container #confidence-value { font-weight: bold; color: #3498db; min-width: 45px; } - /* --- Fin de nuevos estilos --- */ .results { margin-top: 2rem; background-color: #f8f9fa; border: 1px solid #dee2e6; padding: 1.5rem; border-radius: 8px; display: none; } .results-table { width: 100%; border-collapse: collapse; } .results-table td { padding: 12px; border-bottom: 1px solid #dee2e6; } @@ -49,10 +47,13 @@

Configuración Avanzada

- + 85%
+

+ Este valor se usará para campos que no tienen un umbral específico en la configuración. Los umbrales para campos como 'Importe Total' o 'CIF' mantendrán su valor predefinido en el sistema. +

@@ -76,7 +77,6 @@ const confidenceSlider = document.getElementById('confidence-slider'); const confidenceValue = document.getElementById('confidence-value'); - // --- Lógica para actualizar el valor del slider en la UI --- confidenceSlider.addEventListener('input', () => { confidenceValue.textContent = `${confidenceSlider.value}%`; }); @@ -97,19 +97,18 @@ const file = fileInput.files[0]; const token = localStorage.getItem('accessToken'); - // --- Nuevo: Leer el valor del slider --- + if (!file) { + showMessage('Por favor, selecciona un archivo.', 'error'); + submitButton.disabled = false; + return; + } + const thresholdValue = confidenceSlider.value; const thresholdFloat = parseFloat(thresholdValue) / 100.0; - // ------------------------------------ - - if (!file) { /* ... (código sin cambios) ... */ } - if (!token) { /* ... (código sin cambios) ... */ } - + const formData = new FormData(); formData.append('file', file); - // --- Nuevo: Añadir el umbral al FormData --- - formData.append('confidence_threshold', thresholdFloat); - // ---------------------------------------- + formData.append('default_confidence_override', thresholdFloat); try { const response = await fetch('/api/invoices/upload', { @@ -117,6 +116,7 @@ headers: { 'Authorization': `Bearer ${token}` }, body: formData, }); + const data = await response.json(); if (response.ok) { @@ -126,15 +126,14 @@ showMessage(data.detail || 'Ocurrió un error.', 'error'); } } catch (error) { - showMessage('Error de conexión.', 'error'); + console.error('Ha ocurrido un error en el bloque try/catch:', error); + showMessage('Error de conexión o al procesar la respuesta.', 'error'); } finally { submitButton.disabled = false; fileInput.value = ''; } }); - - // --- Funciones de Ayuda para la UI --- function showMessage(text, type = 'info') { messageContainer.textContent = text; messageContainer.className = `message ${type}`; @@ -142,16 +141,13 @@ } function displayResults(data) { - // Limpiar tabla anterior resultsTable.innerHTML = ''; - // Crear una fila por cada dato extraído for (const key in data) { const row = resultsTable.insertRow(); const keyCell = row.insertCell(0); const valueCell = row.insertCell(1); - // Formatear la clave para que sea legible (ej. 'supplier_name' -> 'Supplier Name') keyCell.textContent = key.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase()); valueCell.textContent = data[key]; } @@ -160,4 +156,4 @@ } - + \ No newline at end of file