diff --git a/api/routers/invoices.py b/api/routers/invoices.py index cdce728..842a61a 100644 --- a/api/routers/invoices.py +++ b/api/routers/invoices.py @@ -1,35 +1,43 @@ # api/routers/invoices.py -from fastapi import APIRouter, Depends, UploadFile, File, HTTPException -from typing import Dict +from fastapi import APIRouter, Depends, UploadFile, File, HTTPException, status, Form +from typing import Dict, Optional from api.dependencies import get_current_active_user from services import invoice_processor_service -from core.config import settings from db.models import User router = APIRouter() +ALLOWED_CONTENT_TYPES = ["application/pdf", "image/jpeg", "image/png", "image/tiff"] + @router.post("/upload", response_model=Dict[str, str]) async def upload_invoice( + current_user: User = Depends(get_current_active_user), file: UploadFile = File(...), - current_user: User = Depends(get_current_active_user) + # 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) ): """ - Endpoint para subir una factura, procesarla y devolver los datos extraídos. - Requiere autenticación. + Endpoint para subir una factura. Ahora acepta un umbral de confianza opcional. """ - if not file.content_type in ["application/pdf", "image/jpeg", "image/png"]: - raise HTTPException(status_code=400, detail="Tipo de archivo no soportado.") + if file.content_type not in ALLOWED_CONTENT_TYPES: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=f"Tipo de archivo no soportado. Permitidos: {', '.join(ALLOWED_CONTENT_TYPES)}" + ) try: file_bytes = await file.read() + + # Pasamos el umbral recibido al servicio extracted_data = invoice_processor_service.process_invoice_from_bytes( - project_id=settings.GCP_PROJECT_ID, - location=settings.GCP_LOCATION, - processor_id=settings.DOCAI_PROCESSOR_ID, file_bytes=file_bytes, - mime_type=file.content_type + mime_type=file.content_type, + override_threshold=confidence_threshold ) return extracted_data except Exception as e: - raise HTTPException(status_code=500, detail=f"Error al procesar la factura: {e}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Error al procesar la factura: {e}" + ) \ No newline at end of file diff --git a/invoice-processing-google-document-ai-fastAPI.tgz b/invoice-processing-google-document-ai-fastAPI.tgz new file mode 100644 index 0000000..b33511b Binary files /dev/null and b/invoice-processing-google-document-ai-fastAPI.tgz differ diff --git a/requirements.txt b/requirements.txt index a620177..7f506cd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,46 +1,35 @@ # requirements.txt -# Dependencias de la aplicación y del toolkit unificadas -# Framework Web y Servidor +# --- Core Web Framework & Server --- +# El corazón de nuestra API y el servidor para ejecutarla. +# [standard] incluye extras de alto rendimiento como uvloop. fastapi uvicorn[standard] -# Base de datos (ORM) +# --- Database & ORM --- +# Para la interacción con nuestra base de datos SQLite. sqlalchemy -# Para usar SQLite (simple para empezar) -pydantic-settings -# Autenticación y Seguridad -python-jose[cryptography] +# --- Authentication & Security --- +# Hashing de contraseñas y manejo de tokens JWT. +# [bcrypt] y [cryptography] son los backends recomendados. passlib[bcrypt] -python-multipart # Para subida de archivos +python-jose[cryptography] -# Plantillas HTML +# --- Data Validation & Configuration --- +# Validación de datos en la API y carga de configuración desde .env +pydantic-settings +email-validator # Dependencia explícita para Pydantic EmailStr + +# --- Frontend & File Handling --- +# Motor de plantillas para HTML y manejo de subida de archivos. jinja2 +python-multipart -# Dependencias directas -google-cloud-documentai==3.5.0 -python-dotenv -pyinstaller +# --- Google Cloud Services --- +# El cliente oficial para interactuar con Document AI. +google-cloud-documentai + +# --- Utilities --- +# Herramienta robusta para el parsing de fechas. python-dateutil - -# Dependencias transitivas (fijadas para consistencia, tomadas del toolkit) -cachetools==5.5.2 -certifi==2025.8.3 -charset-normalizer==3.4.3 -google-api-core==2.25.1 -google-auth==2.40.3 -googleapis-common-protos==1.70.0 -grpcio==1.74.0 -grpcio-status==1.74.0 -idna==3.10 -proto-plus==1.26.1 -protobuf==6.32.0 -pyasn1==0.6.1 -pyasn1_modules==0.4.2 -requests==2.32.4 -rsa==4.9.1 -urllib3==2.5.0 - -# Validación de email para Pydantic -email-validator diff --git a/requirements.txt.v1 b/requirements.txt.v1 new file mode 100644 index 0000000..a620177 --- /dev/null +++ b/requirements.txt.v1 @@ -0,0 +1,46 @@ +# requirements.txt +# Dependencias de la aplicación y del toolkit unificadas + +# Framework Web y Servidor +fastapi +uvicorn[standard] + +# Base de datos (ORM) +sqlalchemy +# Para usar SQLite (simple para empezar) +pydantic-settings + +# Autenticación y Seguridad +python-jose[cryptography] +passlib[bcrypt] +python-multipart # Para subida de archivos + +# Plantillas HTML +jinja2 + +# Dependencias directas +google-cloud-documentai==3.5.0 +python-dotenv +pyinstaller +python-dateutil + +# Dependencias transitivas (fijadas para consistencia, tomadas del toolkit) +cachetools==5.5.2 +certifi==2025.8.3 +charset-normalizer==3.4.3 +google-api-core==2.25.1 +google-auth==2.40.3 +googleapis-common-protos==1.70.0 +grpcio==1.74.0 +grpcio-status==1.74.0 +idna==3.10 +proto-plus==1.26.1 +protobuf==6.32.0 +pyasn1==0.6.1 +pyasn1_modules==0.4.2 +requests==2.32.4 +rsa==4.9.1 +urllib3==2.5.0 + +# Validación de email para Pydantic +email-validator diff --git a/requirements.txt.v2 b/requirements.txt.v2 new file mode 100644 index 0000000..3931f89 --- /dev/null +++ b/requirements.txt.v2 @@ -0,0 +1,57 @@ +altgraph==0.17.4 +annotated-types==0.7.0 +anyio==4.10.0 +bcrypt==4.3.0 +cachetools==5.5.2 +certifi==2025.8.3 +cffi==1.17.1 +charset-normalizer==3.4.3 +click==8.2.1 +cryptography==45.0.6 +dnspython==2.7.0 +ecdsa==0.19.1 +email_validator==2.2.0 +fastapi==0.116.1 +google-api-core==2.25.1 +google-auth==2.40.3 +google-cloud-documentai==3.5.0 +googleapis-common-protos==1.70.0 +greenlet==3.2.4 +grpcio==1.74.0 +grpcio-status==1.74.0 +h11==0.16.0 +httptools==0.6.4 +idna==3.10 +Jinja2==3.1.6 +MarkupSafe==3.0.2 +packaging==25.0 +passlib==1.7.4 +proto-plus==1.26.1 +protobuf==6.32.0 +pyasn1==0.6.1 +pyasn1_modules==0.4.2 +pycparser==2.22 +pydantic==2.11.7 +pydantic-settings==2.10.1 +pydantic_core==2.33.2 +pyinstaller==6.15.0 +pyinstaller-hooks-contrib==2025.8 +python-dateutil==2.9.0.post0 +python-dotenv==1.1.1 +python-jose==3.5.0 +python-multipart==0.0.20 +PyYAML==6.0.2 +requests==2.32.4 +rsa==4.9.1 +setuptools==80.9.0 +six==1.17.0 +sniffio==1.3.1 +SQLAlchemy==2.0.43 +starlette==0.47.3 +typing-inspection==0.4.1 +typing_extensions==4.15.0 +urllib3==2.5.0 +uvicorn==0.35.0 +uvloop==0.21.0 +watchfiles==1.1.0 +websockets==15.0.1 diff --git a/services/invoice_processor_service.py b/services/invoice_processor_service.py index 6fd3165..f32c6dc 100644 --- a/services/invoice_processor_service.py +++ b/services/invoice_processor_service.py @@ -1,40 +1,59 @@ # services/invoice_processor_service.py import logging -from typing import Dict, List, Any +from typing import Dict, List, Any, Optional -# Importamos nuestro nuevo cliente GCP de forma local y limpia from .gcp_document_ai_client import process_document_gcp - -# (Opcional, si tienes utilidades) from .utils import data_cleaner - -# Importamos la configuración centralizada +# Importamos nuestro limpiador de datos para usarlo +from .utils import data_cleaner from core.config import settings -# --- Lógica de negocio extraída del antiguo processing.py --- -def _get_confidence_threshold_for_field(field_type: str) -> float: - return settings.CONFIDENCE_THRESHOLDS.get(field_type, settings.CONFIDENCE_THRESHOLDS["__default__"]) +# --- Lógica de negocio refactorizada --- -def _extract_specific_fields(entities: List[Any]) -> Dict[str, str]: +def _extract_specific_fields( + entities: List[Any], + override_threshold: 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. + """ extracted_data = {field: "Not found or low confidence" for field in settings.REQUIRED_FIELDS} for entity in entities: entity_type = entity.type_ - threshold = _get_confidence_threshold_for_field(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 and entity.confidence >= threshold: value = entity.mention_text.replace('\n', ' ').strip() - # if entity_type == 'invoice_date': - # value = data_cleaner.normalize_date(value) or f"Unparseable Date: '{value}'" + + # 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 + return extracted_data -# --- Función principal del servicio --- -def process_invoice_from_bytes(file_bytes: bytes, mime_type: str) -> Dict[str, str]: +# --- 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 +) -> Dict[str, str]: """ - Orquesta el proceso completo: llama a Document AI, extrae y limpia los datos. + Orquesta el proceso completo. Ahora pasa el umbral de confianza + opcional a la capa de lógica de negocio. """ try: - # 1. Llamar a la API de Google a través de nuestro cliente dedicado document = process_document_gcp( project_id=settings.GCP_PROJECT_ID, location=settings.GCP_LOCATION, @@ -43,13 +62,12 @@ def process_invoice_from_bytes(file_bytes: bytes, mime_type: str) -> Dict[str, s mime_type=mime_type, ) - # 2. Aplicar la lógica de negocio para extraer y validar los campos - validated_data = _extract_specific_fields(document.entities) + # Pasamos el umbral opcional a la función de extracción + validated_data = _extract_specific_fields(document.entities, override_threshold) - logging.info("Documento procesado con éxito y datos validados.") + logging.info(f"Documento procesado con éxito con un umbral de {override_threshold or 'default'}.") return validated_data except Exception as e: logging.error(f"Error en el flujo de procesamiento de factura: {e}", exc_info=True) - # Re-lanzamos la excepción para que el endpoint de la API la capture y devuelva un 500 - raise + raise \ No newline at end of file diff --git a/templates/dashboard.html b/templates/dashboard.html index eb2e7f3..8f9b136 100644 --- a/templates/dashboard.html +++ b/templates/dashboard.html @@ -5,34 +5,38 @@ Dashboard - ACME Invoice Processor - - -