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.status = "completed"
inspection.completed_at = datetime.utcnow() 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.pagesizes import A4
from reportlab.lib import colors from reportlab.lib import colors
from reportlab.lib.units import inch from reportlab.lib.units import inch, mm
from reportlab.platypus import SimpleDocTemplate, Table, TableStyle, Paragraph, Spacer, Image as RLImage from reportlab.platypus import SimpleDocTemplate, Table, TableStyle, Paragraph, Spacer, Image as RLImage, PageBreak, KeepTogether
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle 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 from io import BytesIO
import requests import requests
buffer = BytesIO() 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 = [] elements = []
styles = getSampleStyleSheet() styles = getSampleStyleSheet()
title_style = styles['Title']
normal_style = styles['Normal'] # Estilos personalizados
header_style = ParagraphStyle('Header', parent=styles['Heading2'], alignment=TA_CENTER, spaceAfter=12) title_style = ParagraphStyle(
# Portada 'CustomTitle',
elements.append(Paragraph(f"Informe de Inspección #{inspection.id}", title_style)) parent=styles['Heading1'],
elements.append(Spacer(1, 12)) fontSize=24,
elements.append(Paragraph(f"Vehículo: {inspection.vehicle_brand or ''} {inspection.vehicle_model or ''} - Placa: {inspection.vehicle_plate}", normal_style)) textColor=colors.HexColor('#1e3a8a'),
elements.append(Paragraph(f"Cliente: {inspection.client_name or ''}", normal_style)) 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() 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() 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)) # ===== PORTADA =====
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, 10*mm))
elements.append(Spacer(1, 18)) elements.append(Paragraph("📋 INFORME DE INSPECCIÓN VEHICULAR", title_style))
# Tabla de respuestas con miniaturas elements.append(Paragraph(f"{inspection.id}", subtitle_style))
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() elements.append(Spacer(1, 5*mm))
table_data = [["Sección", "Pregunta", "Respuesta", "Estado", "Comentario", "Miniaturas"]]
# 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: for ans in answers:
question = ans.question 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 = [] media_imgs = []
for media in ans.media_files: for media in ans.media_files:
if media.file_type == "image": if media.file_type == "image":
try: try:
img_resp = requests.get(media.file_path) img_resp = requests.get(media.file_path, timeout=10)
if img_resp.status_code == 200: if img_resp.status_code == 200:
img_bytes = BytesIO(img_resp.content) 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) media_imgs.append(rl_img)
except Exception as e: except Exception as e:
print(f"Error cargando imagen {media.file_path}: {e}") print(f"Error cargando imagen {media.file_path}: {e}")
row = [
question.section or "", if media_imgs:
question.text, # Crear tabla de miniaturas (máximo 6 por fila)
ans.answer_value, img_rows = []
ans.status, for i in range(0, len(media_imgs), 6):
ans.comment or "", img_rows.append(media_imgs[i:i+6])
media_imgs if media_imgs else ""
] img_table = Table(img_rows)
table_data.append(row) img_table.setStyle(TableStyle([
table = Table(table_data, colWidths=[1.2*inch, 2.5*inch, 1*inch, 0.8*inch, 2*inch, 1.5*inch]) ('ALIGN', (0, 0), (-1, -1), 'LEFT'),
table.setStyle(TableStyle([ ('VALIGN', (0, 0), (-1, -1), 'MIDDLE'),
('BACKGROUND', (0,0), (-1,0), colors.lightgrey), ('PADDING', (0, 0), (-1, -1), 2),
('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),
])) ]))
elements.append(table) question_data.append([img_table])
elements.append(Spacer(1, 18))
elements.append(Paragraph(f"Generado por Checklist Inteligente - {datetime.now().strftime('%d/%m/%Y %H:%M')}", header_style)) # 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: try:
doc.build(elements) doc.build(elements)
except Exception as e: 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) buffer.seek(0)
now = datetime.now() now = datetime.now()
folder = f"{now.year}/{now.month:02d}" folder = f"{now.year}/{now.month:02d}"