Cooreegido la exportacion de pdf cuando se edita una checklist ahora si se edita algo de la inspeccion hecha se actualiza el PDF
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
|
||||
177
docs/pdf-regeneration.md
Normal file
177
docs/pdf-regeneration.md
Normal file
@@ -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
|
||||
Reference in New Issue
Block a user