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
|
# 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)
|
app = FastAPI(title="Checklist Inteligente API", version=BACKEND_VERSION)
|
||||||
|
|
||||||
# S3/MinIO configuration
|
# S3/MinIO configuration
|
||||||
@@ -1043,31 +1043,11 @@ def update_inspection(
|
|||||||
return db_inspection
|
return db_inspection
|
||||||
|
|
||||||
|
|
||||||
@app.post("/api/inspections/{inspection_id}/complete", response_model=schemas.Inspection)
|
def generate_inspection_pdf(inspection_id: int, db: Session) -> str:
|
||||||
def complete_inspection(
|
"""
|
||||||
inspection_id: int,
|
Genera el PDF de una inspección y lo sube a S3.
|
||||||
db: Session = Depends(get_db),
|
Retorna la URL del PDF generado.
|
||||||
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
|
|
||||||
from reportlab.lib.pagesizes import A4
|
from reportlab.lib.pagesizes import A4
|
||||||
from reportlab.lib import colors
|
from reportlab.lib import colors
|
||||||
from reportlab.lib.units import inch, mm
|
from reportlab.lib.units import inch, mm
|
||||||
@@ -1077,6 +1057,10 @@ def complete_inspection(
|
|||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
import requests
|
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()
|
buffer = BytesIO()
|
||||||
doc = SimpleDocTemplate(
|
doc = SimpleDocTemplate(
|
||||||
buffer,
|
buffer,
|
||||||
@@ -1354,7 +1338,9 @@ def complete_inspection(
|
|||||||
print(f"❌ Error al generar PDF: {e}")
|
print(f"❌ Error al generar PDF: {e}")
|
||||||
import traceback
|
import traceback
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
|
raise HTTPException(status_code=500, detail=f"Error al generar PDF: {str(e)}")
|
||||||
|
|
||||||
|
# Subir a S3
|
||||||
buffer.seek(0)
|
buffer.seek(0)
|
||||||
now = datetime.now()
|
now = datetime.now()
|
||||||
folder = f"{now.year}/{now.month:02d}"
|
folder = f"{now.year}/{now.month:02d}"
|
||||||
@@ -1363,6 +1349,37 @@ def complete_inspection(
|
|||||||
buffer.seek(0)
|
buffer.seek(0)
|
||||||
s3_client.upload_fileobj(buffer, S3_PDF_BUCKET, s3_key, ExtraArgs={"ContentType": "application/pdf"})
|
s3_client.upload_fileobj(buffer, S3_PDF_BUCKET, s3_key, ExtraArgs={"ContentType": "application/pdf"})
|
||||||
pdf_url = f"{S3_ENDPOINT}/{S3_PDF_BUCKET}/{s3_key}"
|
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
|
inspection.pdf_url = pdf_url
|
||||||
db.commit()
|
db.commit()
|
||||||
db.refresh(inspection)
|
db.refresh(inspection)
|
||||||
@@ -1492,6 +1509,14 @@ def update_answer(
|
|||||||
if not db_answer:
|
if not db_answer:
|
||||||
raise HTTPException(status_code=404, detail="Respuesta no encontrada")
|
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
|
# Recalcular puntos si cambió el status
|
||||||
if answer.status and answer.status != db_answer.status:
|
if answer.status and answer.status != db_answer.status:
|
||||||
question = db.query(models.Question).filter(
|
question = db.query(models.Question).filter(
|
||||||
@@ -1510,6 +1535,32 @@ def update_answer(
|
|||||||
|
|
||||||
db.commit()
|
db.commit()
|
||||||
db.refresh(db_answer)
|
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
|
return db_answer
|
||||||
|
|
||||||
|
|
||||||
@@ -1646,6 +1697,35 @@ def admin_edit_answer(
|
|||||||
db.commit()
|
db.commit()
|
||||||
db.refresh(db_answer)
|
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
|
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