Actualziar modelo de PDF

This commit is contained in:
2025-11-26 09:23:35 -03:00
parent d35f0343e1
commit 70f984bfdf

View File

@@ -1067,76 +1067,294 @@ def complete_inspection(
inspection.status = "completed"
inspection.completed_at = datetime.utcnow()
# Generar PDF con miniaturas de imágenes y subir a MinIO
# Generar PDF profesional con diseño mejorado
from reportlab.lib.pagesizes import A4
from reportlab.lib import colors
from reportlab.lib.units import inch
from reportlab.platypus import SimpleDocTemplate, Table, TableStyle, Paragraph, Spacer, Image as RLImage
from reportlab.lib.units import inch, mm
from reportlab.platypus import SimpleDocTemplate, Table, TableStyle, Paragraph, Spacer, Image as RLImage, PageBreak, KeepTogether
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.lib.enums import TA_CENTER
from reportlab.lib.enums import TA_CENTER, TA_LEFT, TA_RIGHT, TA_JUSTIFY
from io import BytesIO
import requests
buffer = BytesIO()
doc = SimpleDocTemplate(buffer, pagesize=A4, rightMargin=30, leftMargin=30, topMargin=30, bottomMargin=30)
doc = SimpleDocTemplate(
buffer,
pagesize=A4,
rightMargin=15*mm,
leftMargin=15*mm,
topMargin=20*mm,
bottomMargin=20*mm
)
elements = []
styles = getSampleStyleSheet()
title_style = styles['Title']
normal_style = styles['Normal']
header_style = ParagraphStyle('Header', parent=styles['Heading2'], alignment=TA_CENTER, spaceAfter=12)
# Portada
elements.append(Paragraph(f"Informe de Inspección #{inspection.id}", title_style))
elements.append(Spacer(1, 12))
elements.append(Paragraph(f"Vehículo: {inspection.vehicle_brand or ''} {inspection.vehicle_model or ''} - Placa: {inspection.vehicle_plate}", normal_style))
elements.append(Paragraph(f"Cliente: {inspection.client_name or ''}", normal_style))
# Estilos personalizados
title_style = ParagraphStyle(
'CustomTitle',
parent=styles['Heading1'],
fontSize=24,
textColor=colors.HexColor('#1e3a8a'),
spaceAfter=6,
alignment=TA_CENTER,
fontName='Helvetica-Bold'
)
subtitle_style = ParagraphStyle(
'CustomSubtitle',
parent=styles['Normal'],
fontSize=11,
textColor=colors.HexColor('#475569'),
spaceAfter=20,
alignment=TA_CENTER
)
section_header_style = ParagraphStyle(
'SectionHeader',
parent=styles['Heading2'],
fontSize=14,
textColor=colors.HexColor('#1e40af'),
spaceBefore=16,
spaceAfter=10,
fontName='Helvetica-Bold',
borderWidth=0,
borderColor=colors.HexColor('#3b82f6'),
borderPadding=6,
backColor=colors.HexColor('#eff6ff')
)
info_style = ParagraphStyle(
'InfoStyle',
parent=styles['Normal'],
fontSize=10,
textColor=colors.HexColor('#334155'),
spaceAfter=4
)
small_style = ParagraphStyle(
'SmallStyle',
parent=styles['Normal'],
fontSize=8,
textColor=colors.HexColor('#64748b')
)
# Obtener datos
mechanic = db.query(models.User).filter(models.User.id == inspection.mechanic_id).first()
checklist = db.query(models.Checklist).filter(models.Checklist.id == inspection.checklist_id).first()
elements.append(Paragraph(f"Mecánico: {mechanic.full_name if mechanic else ''}", normal_style))
elements.append(Paragraph(f"Checklist: {checklist.name if checklist else ''}", normal_style))
elements.append(Paragraph(f"Fecha: {inspection.started_at.strftime('%d/%m/%Y %H:%M') if inspection.started_at else ''}", normal_style))
elements.append(Spacer(1, 18))
# Tabla de respuestas con miniaturas
answers = db.query(models.Answer).options(joinedload(models.Answer.media_files)).join(models.Question).filter(models.Answer.inspection_id == inspection_id).order_by(models.Question.section, models.Question.order).all()
table_data = [["Sección", "Pregunta", "Respuesta", "Estado", "Comentario", "Miniaturas"]]
# ===== PORTADA =====
elements.append(Spacer(1, 10*mm))
elements.append(Paragraph("📋 INFORME DE INSPECCIÓN VEHICULAR", title_style))
elements.append(Paragraph(f"{inspection.id}", subtitle_style))
elements.append(Spacer(1, 5*mm))
# Cuadro de información del vehículo
vehicle_data = [
[Paragraph("<b>🚗 INFORMACIÓN DEL VEHÍCULO</b>", info_style)],
[Table([
[Paragraph("<b>Placa:</b>", small_style), Paragraph(f"{inspection.vehicle_plate}", info_style)],
[Paragraph("<b>Marca:</b>", small_style), Paragraph(f"{inspection.vehicle_brand or 'N/A'}", info_style)],
[Paragraph("<b>Modelo:</b>", small_style), Paragraph(f"{inspection.vehicle_model or 'N/A'}", info_style)],
[Paragraph("<b>Kilometraje:</b>", small_style), Paragraph(f"{inspection.vehicle_km or 'N/A'} km", info_style)]
], colWidths=[30*mm, 50*mm])]
]
vehicle_table = Table(vehicle_data, colWidths=[85*mm])
vehicle_table.setStyle(TableStyle([
('BACKGROUND', (0, 0), (-1, 0), colors.HexColor('#dbeafe')),
('TEXTCOLOR', (0, 0), (-1, 0), colors.HexColor('#1e40af')),
('ALIGN', (0, 0), (-1, -1), 'LEFT'),
('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
('FONTSIZE', (0, 0), (-1, 0), 11),
('PADDING', (0, 0), (-1, -1), 8),
('BACKGROUND', (0, 1), (-1, -1), colors.white),
('BOX', (0, 0), (-1, -1), 1, colors.HexColor('#93c5fd')),
('VALIGN', (0, 0), (-1, -1), 'TOP'),
]))
# Cuadro de información del cliente e inspección
inspection_data = [
[Paragraph("<b>👤 INFORMACIÓN DEL CLIENTE</b>", info_style)],
[Table([
[Paragraph("<b>Cliente:</b>", small_style), Paragraph(f"{inspection.client_name or 'N/A'}", info_style)],
[Paragraph("<b>OR N°:</b>", small_style), Paragraph(f"{inspection.or_number or 'N/A'}", info_style)],
[Paragraph("<b>Mecánico:</b>", small_style), Paragraph(f"{mechanic.full_name if mechanic else 'N/A'}", info_style)],
[Paragraph("<b>Cód. Operario:</b>", small_style), Paragraph(f"{inspection.mechanic_employee_code or 'N/A'}", info_style)],
[Paragraph("<b>Fecha:</b>", small_style), Paragraph(f"{inspection.started_at.strftime('%d/%m/%Y %H:%M') if inspection.started_at else 'N/A'}", info_style)]
], colWidths=[30*mm, 50*mm])]
]
inspection_info_table = Table(inspection_data, colWidths=[85*mm])
inspection_info_table.setStyle(TableStyle([
('BACKGROUND', (0, 0), (-1, 0), colors.HexColor('#dcfce7')),
('TEXTCOLOR', (0, 0), (-1, 0), colors.HexColor('#15803d')),
('ALIGN', (0, 0), (-1, -1), 'LEFT'),
('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
('FONTSIZE', (0, 0), (-1, 0), 11),
('PADDING', (0, 0), (-1, -1), 8),
('BACKGROUND', (0, 1), (-1, -1), colors.white),
('BOX', (0, 0), (-1, -1), 1, colors.HexColor('#86efac')),
('VALIGN', (0, 0), (-1, -1), 'TOP'),
]))
# Tabla con ambos cuadros lado a lado
info_table = Table([[vehicle_table, inspection_info_table]], colWidths=[90*mm, 90*mm])
info_table.setStyle(TableStyle([
('ALIGN', (0, 0), (-1, -1), 'CENTER'),
('VALIGN', (0, 0), (-1, -1), 'TOP'),
]))
elements.append(info_table)
elements.append(Spacer(1, 8*mm))
# Resumen de puntuación
percentage = inspection.percentage
score_color = colors.HexColor('#22c55e') if percentage >= 80 else colors.HexColor('#eab308') if percentage >= 60 else colors.HexColor('#ef4444')
score_data = [
[
Paragraph("<b>Puntuación Total</b>", info_style),
Paragraph(f"<b>{inspection.score} / {inspection.max_score}</b>", info_style),
Paragraph(f"<b>{percentage:.1f}%</b>", info_style),
Paragraph(f"<b>⚠️ {inspection.flagged_items_count} Críticos</b>", info_style)
]
]
score_table = Table(score_data, colWidths=[45*mm, 45*mm, 45*mm, 45*mm])
score_table.setStyle(TableStyle([
('BACKGROUND', (0, 0), (-1, -1), score_color),
('TEXTCOLOR', (0, 0), (-1, -1), colors.white),
('ALIGN', (0, 0), (-1, -1), 'CENTER'),
('FONTNAME', (0, 0), (-1, -1), 'Helvetica-Bold'),
('FONTSIZE', (0, 0), (-1, -1), 12),
('PADDING', (0, 0), (-1, -1), 10),
('BOX', (0, 0), (-1, -1), 2, colors.white),
]))
elements.append(score_table)
elements.append(PageBreak())
# ===== DETALLE DE RESPUESTAS =====
elements.append(Paragraph("📝 DETALLE DE LA INSPECCIÓN", section_header_style))
elements.append(Spacer(1, 5*mm))
# Obtener respuestas agrupadas por sección
answers = db.query(models.Answer).options(
joinedload(models.Answer.media_files),
joinedload(models.Answer.question)
).join(models.Question).filter(
models.Answer.inspection_id == inspection_id
).order_by(
models.Question.section,
models.Question.order
).all()
current_section = None
for ans in answers:
question = ans.question
# Nueva sección
if question.section != current_section:
if current_section is not None:
elements.append(Spacer(1, 5*mm))
current_section = question.section
elements.append(Paragraph(f"{question.section or 'General'}", section_header_style))
elements.append(Spacer(1, 3*mm))
# Estado visual
status_colors = {
'ok': colors.HexColor('#22c55e'),
'warning': colors.HexColor('#eab308'),
'critical': colors.HexColor('#ef4444')
}
status_icons = {
'ok': '',
'warning': '',
'critical': ''
}
status_color = status_colors.get(ans.status, colors.HexColor('#64748b'))
status_icon = status_icons.get(ans.status, '')
# Tabla de pregunta/respuesta
question_data = []
# Fila 1: Pregunta
question_data.append([
Paragraph(f"<b>{status_icon} {question.text}</b>", info_style),
])
# Fila 2: Respuesta y estado
answer_text = ans.answer_value or 'Sin respuesta'
question_data.append([
Table([
[
Paragraph(f"<b>Respuesta:</b> {answer_text}", small_style),
Paragraph(f"<b>Estado:</b> {ans.status.upper()}", ParagraphStyle('status', parent=small_style, textColor=status_color, fontName='Helvetica-Bold'))
]
], colWidths=[120*mm, 50*mm])
])
# Fila 3: Comentario (si existe)
if ans.comment:
question_data.append([
Paragraph(f"<b>Comentario:</b> {ans.comment}", small_style)
])
# Fila 4: Imágenes (si existen)
if ans.media_files:
media_imgs = []
for media in ans.media_files:
if media.file_type == "image":
try:
img_resp = requests.get(media.file_path)
img_resp = requests.get(media.file_path, timeout=10)
if img_resp.status_code == 200:
img_bytes = BytesIO(img_resp.content)
rl_img = RLImage(img_bytes, width=0.7*inch, height=0.7*inch)
rl_img = RLImage(img_bytes, width=25*mm, height=25*mm)
media_imgs.append(rl_img)
except Exception as e:
print(f"Error cargando imagen {media.file_path}: {e}")
row = [
question.section or "",
question.text,
ans.answer_value,
ans.status,
ans.comment or "",
media_imgs if media_imgs else ""
]
table_data.append(row)
table = Table(table_data, colWidths=[1.2*inch, 2.5*inch, 1*inch, 0.8*inch, 2*inch, 1.5*inch])
table.setStyle(TableStyle([
('BACKGROUND', (0,0), (-1,0), colors.lightgrey),
('TEXTCOLOR', (0,0), (-1,0), colors.black),
('ALIGN', (0,0), (-1,-1), 'LEFT'),
('VALIGN', (0,0), (-1,-1), 'TOP'),
('FONTNAME', (0,0), (-1,0), 'Helvetica-Bold'),
('FONTSIZE', (0,0), (-1,0), 10),
('BOTTOMPADDING', (0,0), (-1,0), 8),
('GRID', (0,0), (-1,-1), 0.5, colors.grey),
if media_imgs:
# Crear tabla de miniaturas (máximo 6 por fila)
img_rows = []
for i in range(0, len(media_imgs), 6):
img_rows.append(media_imgs[i:i+6])
img_table = Table(img_rows)
img_table.setStyle(TableStyle([
('ALIGN', (0, 0), (-1, -1), 'LEFT'),
('VALIGN', (0, 0), (-1, -1), 'MIDDLE'),
('PADDING', (0, 0), (-1, -1), 2),
]))
elements.append(table)
elements.append(Spacer(1, 18))
elements.append(Paragraph(f"Generado por Checklist Inteligente - {datetime.now().strftime('%d/%m/%Y %H:%M')}", header_style))
question_data.append([img_table])
# Tabla de la pregunta completa
q_table = Table(question_data, colWidths=[180*mm])
q_table.setStyle(TableStyle([
('BACKGROUND', (0, 0), (-1, 0), colors.HexColor('#f1f5f9')),
('PADDING', (0, 0), (-1, -1), 6),
('BOX', (0, 0), (-1, -1), 0.5, colors.HexColor('#cbd5e1')),
('LEFTPADDING', (0, 0), (-1, -1), 8),
('VALIGN', (0, 0), (-1, -1), 'TOP'),
]))
elements.append(KeepTogether(q_table))
elements.append(Spacer(1, 3*mm))
# ===== FOOTER =====
elements.append(Spacer(1, 10*mm))
elements.append(Paragraph(
f"Documento generado automáticamente por Checklist Inteligente el {datetime.now().strftime('%d/%m/%Y a las %H:%M')}",
ParagraphStyle('footer', parent=small_style, alignment=TA_CENTER, textColor=colors.HexColor('#94a3b8'))
))
# Generar PDF
try:
doc.build(elements)
except Exception as e:
print(f"Error al generar PDF: {e}")
print(f"Error al generar PDF: {e}")
import traceback
traceback.print_exc()
buffer.seek(0)
now = datetime.now()
folder = f"{now.year}/{now.month:02d}"