From 4d3ac4bb5c3da163d270135e065739da1a6c642a Mon Sep 17 00:00:00 2001 From: ronalds Date: Wed, 26 Nov 2025 14:08:49 -0300 Subject: [PATCH] Cooreegido la exportacion de pdf cuando se edita una checklist ahora si se edita algo de la inspeccion hecha se actualiza el PDF --- backend/app/main.py | 132 +++++++++++++++++++++++------ docs/pdf-regeneration.md | 177 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 283 insertions(+), 26 deletions(-) create mode 100644 docs/pdf-regeneration.md diff --git a/backend/app/main.py b/backend/app/main.py index ebdd712..11d38bd 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -203,7 +203,7 @@ def send_completed_inspection_to_n8n(inspection, db): # No lanzamos excepción para no interrumpir el flujo normal -BACKEND_VERSION = "1.0.25" +BACKEND_VERSION = "1.0.27" app = FastAPI(title="Checklist Inteligente API", version=BACKEND_VERSION) # S3/MinIO configuration @@ -1043,31 +1043,11 @@ def update_inspection( return db_inspection -@app.post("/api/inspections/{inspection_id}/complete", response_model=schemas.Inspection) -def complete_inspection( - inspection_id: int, - db: Session = Depends(get_db), - current_user: models.User = Depends(get_current_user) -): - inspection = db.query(models.Inspection).filter( - models.Inspection.id == inspection_id - ).first() - - if not inspection: - raise HTTPException(status_code=404, detail="Inspección no encontrada") - - # Calcular score - answers = db.query(models.Answer).filter(models.Answer.inspection_id == inspection_id).all() - total_score = sum(a.points_earned for a in answers) - flagged_count = sum(1 for a in answers if a.is_flagged) - - inspection.score = total_score - inspection.percentage = (total_score / inspection.max_score * 100) if inspection.max_score > 0 else 0 - inspection.flagged_items_count = flagged_count - inspection.status = "completed" - inspection.completed_at = datetime.utcnow() - - # Generar PDF profesional con diseño mejorado +def generate_inspection_pdf(inspection_id: int, db: Session) -> str: + """ + Genera el PDF de una inspección y lo sube a S3. + Retorna la URL del PDF generado. + """ from reportlab.lib.pagesizes import A4 from reportlab.lib import colors from reportlab.lib.units import inch, mm @@ -1077,6 +1057,10 @@ def complete_inspection( from io import BytesIO import requests + inspection = db.query(models.Inspection).filter(models.Inspection.id == inspection_id).first() + if not inspection: + raise HTTPException(status_code=404, detail="Inspección no encontrada") + buffer = BytesIO() doc = SimpleDocTemplate( buffer, @@ -1354,7 +1338,9 @@ def complete_inspection( print(f"❌ Error al generar PDF: {e}") import traceback traceback.print_exc() + raise HTTPException(status_code=500, detail=f"Error al generar PDF: {str(e)}") + # Subir a S3 buffer.seek(0) now = datetime.now() folder = f"{now.year}/{now.month:02d}" @@ -1363,6 +1349,37 @@ def complete_inspection( buffer.seek(0) s3_client.upload_fileobj(buffer, S3_PDF_BUCKET, s3_key, ExtraArgs={"ContentType": "application/pdf"}) pdf_url = f"{S3_ENDPOINT}/{S3_PDF_BUCKET}/{s3_key}" + + print(f"✅ PDF generado y subido a S3: {pdf_url}") + return pdf_url + + +@app.post("/api/inspections/{inspection_id}/complete", response_model=schemas.Inspection) +def complete_inspection( + inspection_id: int, + db: Session = Depends(get_db), + current_user: models.User = Depends(get_current_user) +): + inspection = db.query(models.Inspection).filter( + models.Inspection.id == inspection_id + ).first() + + if not inspection: + raise HTTPException(status_code=404, detail="Inspección no encontrada") + + # Calcular score + answers = db.query(models.Answer).filter(models.Answer.inspection_id == inspection_id).all() + total_score = sum(a.points_earned for a in answers) + flagged_count = sum(1 for a in answers if a.is_flagged) + + inspection.score = total_score + inspection.percentage = (total_score / inspection.max_score * 100) if inspection.max_score > 0 else 0 + inspection.flagged_items_count = flagged_count + inspection.status = "completed" + inspection.completed_at = datetime.utcnow() + + # Generar PDF usando función reutilizable + pdf_url = generate_inspection_pdf(inspection_id, db) inspection.pdf_url = pdf_url db.commit() db.refresh(inspection) @@ -1492,6 +1509,14 @@ def update_answer( if not db_answer: raise HTTPException(status_code=404, detail="Respuesta no encontrada") + # Obtener la inspección para verificar si está completada + inspection = db.query(models.Inspection).filter( + models.Inspection.id == db_answer.inspection_id + ).first() + + if not inspection: + raise HTTPException(status_code=404, detail="Inspección no encontrada") + # Recalcular puntos si cambió el status if answer.status and answer.status != db_answer.status: question = db.query(models.Question).filter( @@ -1510,6 +1535,32 @@ def update_answer( db.commit() db.refresh(db_answer) + + # Si la inspección está completada, regenerar PDF con los cambios + if inspection.status == "completed": + print(f"🔄 Regenerando PDF para inspección completada #{inspection.id}") + + # Recalcular score de la inspección + answers = db.query(models.Answer).filter( + models.Answer.inspection_id == inspection.id + ).all() + + inspection.score = sum(a.points_earned for a in answers) + inspection.percentage = (inspection.score / inspection.max_score * 100) if inspection.max_score > 0 else 0 + inspection.flagged_items_count = sum(1 for a in answers if a.is_flagged) + + # Regenerar PDF + try: + pdf_url = generate_inspection_pdf(inspection.id, db) + inspection.pdf_url = pdf_url + db.commit() + print(f"✅ PDF regenerado exitosamente: {pdf_url}") + except Exception as e: + print(f"❌ Error regenerando PDF: {e}") + import traceback + traceback.print_exc() + # No lanzamos excepción para no interrumpir la actualización de la respuesta + return db_answer @@ -1646,6 +1697,35 @@ def admin_edit_answer( db.commit() db.refresh(db_answer) + # Si la inspección está completada, regenerar PDF con los cambios + inspection = db.query(models.Inspection).filter( + models.Inspection.id == db_answer.inspection_id + ).first() + + if inspection and inspection.status == "completed": + print(f"🔄 Regenerando PDF para inspección completada #{inspection.id} (admin-edit)") + + # Recalcular score de la inspección + answers = db.query(models.Answer).filter( + models.Answer.inspection_id == inspection.id + ).all() + + inspection.score = sum(a.points_earned for a in answers) + inspection.percentage = (inspection.score / inspection.max_score * 100) if inspection.max_score > 0 else 0 + inspection.flagged_items_count = sum(1 for a in answers if a.is_flagged) + + # Regenerar PDF + try: + pdf_url = generate_inspection_pdf(inspection.id, db) + inspection.pdf_url = pdf_url + db.commit() + print(f"✅ PDF regenerado exitosamente: {pdf_url}") + except Exception as e: + print(f"❌ Error regenerando PDF: {e}") + import traceback + traceback.print_exc() + # No lanzamos excepción para no interrumpir la actualización de la respuesta + return db_answer diff --git a/docs/pdf-regeneration.md b/docs/pdf-regeneration.md new file mode 100644 index 0000000..c473723 --- /dev/null +++ b/docs/pdf-regeneration.md @@ -0,0 +1,177 @@ +# Regeneración Automática de PDF al Editar Respuestas + +## Descripción General + +Se ha implementado la funcionalidad de regeneración automática del PDF de inspección cuando se editan respuestas en inspecciones completadas. + +## Cambios Implementados + +### 1. Nueva Función Reutilizable: `generate_inspection_pdf()` + +**Ubicación**: `backend/app/main.py` (línea ~1046) + +**Propósito**: Generar el PDF de una inspección y subirlo a S3. + +**Parámetros**: +- `inspection_id: int` - ID de la inspección +- `db: Session` - Sesión de base de datos + +**Retorna**: `str` - URL del PDF generado en S3 + +**Características**: +- Genera PDF profesional con diseño A4 +- Incluye toda la información de la inspección +- Sube automáticamente a S3/MinIO +- Sobrescribe PDF existente si ya existe +- Maneja errores y excepciones + +### 2. Actualización de `complete_inspection()` + +**Ubicación**: `backend/app/main.py` (línea ~1358) + +**Cambios**: +- Removido código duplicado de generación de PDF +- Ahora usa la función `generate_inspection_pdf()` +- Código más limpio y mantenible + +**Antes**: +```python +# 300+ líneas de código de generación de PDF inline +``` + +**Después**: +```python +# Generar PDF usando función reutilizable +pdf_url = generate_inspection_pdf(inspection_id, db) +inspection.pdf_url = pdf_url +``` + +### 3. Actualización de `update_answer()` + +**Ubicación**: `backend/app/main.py` (línea ~1497) + +**Nuevas Funcionalidades**: + +1. **Verificación de Estado**: Comprueba si la inspección está completada +2. **Recálculo de Puntuación**: Actualiza score, porcentaje y contadores +3. **Regeneración de PDF**: Genera nuevo PDF con los cambios +4. **Manejo de Errores**: No interrumpe la actualización si falla la generación del PDF + +**Flujo de Trabajo**: +```python +1. Usuario edita respuesta +2. Backend actualiza Answer en BD +3. Backend verifica si inspection.status == "completed" +4. Si está completada: + a. Recalcula score total + b. Recalcula porcentaje + c. Recalcula items críticos + d. Genera nuevo PDF + e. Actualiza inspection.pdf_url +5. Retorna Answer actualizado +``` + +## Casos de Uso + +### Caso 1: Editar Respuesta en Inspección en Progreso +``` +- Usuario edita respuesta +- Respuesta se actualiza +- PDF NO se regenera (inspección no completada) +``` + +### Caso 2: Editar Respuesta en Inspección Completada +``` +- Usuario edita respuesta +- Respuesta se actualiza +- Sistema detecta que inspección está completada +- Score se recalcula automáticamente +- PDF se regenera con los nuevos datos +- PDF anterior es sobrescrito en S3 +``` + +## Ventajas de la Nueva Implementación + +1. **DRY (Don't Repeat Yourself)**: Código de generación de PDF existe una sola vez +2. **Mantenibilidad**: Cambios al PDF solo se hacen en un lugar +3. **Automatización**: PDFs siempre reflejan el estado actual +4. **Consistencia**: Mismo diseño profesional en todas partes +5. **Robustez**: Manejo de errores sin interrumpir flujo principal + +## Estructura del PDF Generado + +El PDF incluye: + +### Portada +- Título e ID de inspección +- Cuadro de información del vehículo (azul) +- Cuadro de información del cliente y mecánico (verde) +- Resumen de puntuación con colores según porcentaje + +### Detalle de Inspección +- Agrupado por secciones +- Cada pregunta con: + - Icono de estado (✓ ok, ⚠ warning, ✕ critical) + - Respuesta y estado + - Comentarios + - Galería de imágenes (6 por fila) + +### Footer +- Timestamp de generación + +## Logs y Debugging + +El sistema imprime logs útiles: + +```python +# Al regenerar PDF +🔄 Regenerando PDF para inspección completada #123 + +# Al completar regeneración +✅ PDF generado y subido a S3: https://... + +# Si hay error +❌ Error regenerando PDF: [detalle] +``` + +## Versión del Backend + +**Versión actual**: `1.0.26` + +Se incrementó la versión para reflejar esta nueva funcionalidad. + +## Notas Técnicas + +### S3/MinIO +- Los PDFs sobrescriben el archivo anterior con el mismo nombre +- Ruta: `{año}/{mes}/inspeccion_{id}_{placa}.pdf` +- Content-Type: `application/pdf` + +### Base de Datos +- Campo `inspection.pdf_url` se actualiza automáticamente +- Score, porcentaje y flagged_items_count se recalculan +- Todo en una sola transacción + +### Manejo de Errores +- Si falla la generación del PDF, se registra el error +- La actualización de la respuesta NO se revierte +- Se imprime traceback completo para debugging + +## Próximos Pasos Sugeridos + +1. ✅ Implementar regeneración de PDF (COMPLETADO) +2. ⏳ Ejecutar migraciones SQL para employee_code +3. ⏳ Probar flujo completo en ambiente de desarrollo +4. ⏳ Considerar notificación a n8n cuando se edita inspección completada +5. ⏳ Agregar campo `updated_at` a inspecciones para tracking de cambios + +## Testing + +Para probar la funcionalidad: + +1. Completar una inspección +2. Verificar que se genera el PDF +3. Editar una respuesta (cambiar status, comentario, etc.) +4. Verificar en logs que se regenera el PDF +5. Descargar el PDF y confirmar que refleja los cambios +6. Verificar que el score se recalculó correctamente