06/12 Add export PDF function for Inspection filter result
This commit is contained in:
+138
-1
@@ -23,7 +23,7 @@ try:
|
||||
except ImportError:
|
||||
_PIL_AVAILABLE = False
|
||||
|
||||
from reportlab.lib.pagesizes import letter
|
||||
from reportlab.lib.pagesizes import letter, landscape
|
||||
from reportlab.lib import colors
|
||||
from reportlab.lib.units import inch
|
||||
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
|
||||
@@ -1092,5 +1092,142 @@ def generate_scheduled_report_pdf(report_name, frequency, start, end,
|
||||
STYLES['FooterStyle'],
|
||||
))
|
||||
|
||||
doc.build(story, onFirstPage=_page_cb, onLaterPages=_page_cb)
|
||||
return buf.getvalue()
|
||||
|
||||
|
||||
# ══════════════════════════════════════════════════════════════════════════════
|
||||
# INSPECTIONS LIST PDF
|
||||
# ══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
def generate_inspections_list_pdf(inspections, filter_summary: str = '') -> bytes:
|
||||
"""Return a PDF byte-string for a filtered list of inspections.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
inspections : list of Inspection model instances
|
||||
filter_summary : human-readable string describing active filters (optional)
|
||||
"""
|
||||
buf = io.BytesIO()
|
||||
generated_at = datetime.now().strftime('%B %d, %Y %I:%M %p ET')
|
||||
report_title = 'Inspections List'
|
||||
|
||||
page_size = landscape(letter)
|
||||
doc = SimpleDocTemplate(
|
||||
buf,
|
||||
pagesize=page_size,
|
||||
leftMargin=0.65 * inch,
|
||||
rightMargin=0.65 * inch,
|
||||
topMargin=1.1 * inch,
|
||||
bottomMargin=0.75 * inch,
|
||||
title=report_title,
|
||||
author='Janitorial QC System',
|
||||
)
|
||||
|
||||
def _page_cb(canvas, doc):
|
||||
_on_page(canvas, doc, report_title, generated_at)
|
||||
|
||||
pw = page_size[0] - 1.3 * inch # usable page width (~9.7 in)
|
||||
|
||||
story = []
|
||||
|
||||
# ── Filter summary line ───────────────────────────────────────────────────
|
||||
if filter_summary:
|
||||
story.append(Paragraph(f'Filters: {filter_summary}', STYLES['ReportSub']))
|
||||
story.append(Paragraph(f'Total records: {len(inspections)}', STYLES['ReportSub']))
|
||||
story.append(Spacer(1, 10))
|
||||
|
||||
if not inspections:
|
||||
story.append(Paragraph('No inspections match the selected filters.', STYLES['FieldValue']))
|
||||
doc.build(story, onFirstPage=_page_cb, onLaterPages=_page_cb)
|
||||
return buf.getvalue()
|
||||
|
||||
# ── Column widths (total = pw) ────────────────────────────────────────────
|
||||
# Date, Contract, Facility, Area, Template, Inspector, Score, Status
|
||||
col_w = [
|
||||
pw * 0.11, # Date
|
||||
pw * 0.16, # Contract
|
||||
pw * 0.17, # Facility
|
||||
pw * 0.10, # Area
|
||||
pw * 0.16, # Template
|
||||
pw * 0.12, # Inspector
|
||||
pw * 0.08, # Score
|
||||
pw * 0.10, # Status
|
||||
]
|
||||
|
||||
# ── Table header ──────────────────────────────────────────────────────────
|
||||
hdr_style = ParagraphStyle('LH', fontName='Helvetica-Bold', fontSize=7.5,
|
||||
textColor=C_WHITE, leading=9)
|
||||
val_style = ParagraphStyle('LV', fontName='Helvetica', fontSize=7.5,
|
||||
textColor=C_DARK, leading=9)
|
||||
|
||||
def _h(text):
|
||||
return Paragraph(text, hdr_style)
|
||||
|
||||
def _v(text, color=None):
|
||||
if color:
|
||||
return Paragraph(f'<font color="{color.hexval()}">{text}</font>', val_style)
|
||||
return Paragraph(text, val_style)
|
||||
|
||||
tbl_data = [[
|
||||
_h('Date'), _h('Contract'), _h('Facility'), _h('Area'),
|
||||
_h('Template'), _h('Inspector'), _h('Score'), _h('Status'),
|
||||
]]
|
||||
|
||||
for ins in inspections:
|
||||
date_str = ins.inspection_date.strftime('%Y-%m-%d %H:%M')
|
||||
contract_str = (ins.facility.project.name
|
||||
if ins.facility and ins.facility.project else '—')
|
||||
facility_str = ins.facility.name if ins.facility else '—'
|
||||
area_str = ins.area.name if ins.area else '—'
|
||||
template_str = ins.template.name if ins.template else '—'
|
||||
inspector_str = ins.inspector.display_name if ins.inspector else '—'
|
||||
|
||||
if ins.overall_score is not None:
|
||||
sc = float(ins.overall_score)
|
||||
score_str = f'{sc:.1f}%'
|
||||
score_color = C_GREEN if sc >= 90 else C_YELLOW if sc >= 70 else C_RED
|
||||
else:
|
||||
score_str = '—'
|
||||
score_color = C_SLATE
|
||||
|
||||
status_raw = ins.status or ''
|
||||
if status_raw == 'completed':
|
||||
status_str = 'Submitted'
|
||||
status_color = C_GREEN
|
||||
elif status_raw == 'flagged':
|
||||
status_str = 'Flagged'
|
||||
status_color = C_RED
|
||||
else:
|
||||
status_str = status_raw.replace('_', ' ').title()
|
||||
status_color = C_SLATE
|
||||
|
||||
follow_up_suffix = ' (Follow-up)' if ins.follow_up_required else ''
|
||||
|
||||
tbl_data.append([
|
||||
_v(date_str),
|
||||
_v(contract_str[:30] + ('...' if len(contract_str) > 30 else '')),
|
||||
_v(facility_str[:35] + ('...' if len(facility_str) > 35 else '')),
|
||||
_v(area_str[:20] + ('...' if len(area_str) > 20 else '')),
|
||||
_v(template_str[:30] + ('...' if len(template_str) > 30 else '')),
|
||||
_v(inspector_str[:22] + ('...' if len(inspector_str) > 22 else '')),
|
||||
_v(score_str, color=score_color),
|
||||
_v(status_str + follow_up_suffix, color=status_color),
|
||||
])
|
||||
|
||||
tbl = Table(tbl_data, colWidths=col_w, repeatRows=1)
|
||||
tbl.setStyle(TableStyle([
|
||||
('BACKGROUND', (0, 0), (-1, 0), C_DARK),
|
||||
('ROWBACKGROUNDS', (0, 1), (-1, -1), [C_WHITE, C_LIGHT]),
|
||||
('INNERGRID', (0, 0), (-1, -1), 0.25, C_BORDER),
|
||||
('BOX', (0, 0), (-1, -1), 0.5, C_BORDER),
|
||||
('VALIGN', (0, 0), (-1, -1), 'TOP'),
|
||||
('TOPPADDING', (0, 0), (-1, -1), 4),
|
||||
('BOTTOMPADDING', (0, 0), (-1, -1), 4),
|
||||
('LEFTPADDING', (0, 0), (-1, -1), 4),
|
||||
('RIGHTPADDING', (0, 0), (-1, -1), 4),
|
||||
]))
|
||||
story.append(tbl)
|
||||
|
||||
doc.build(story, onFirstPage=_page_cb, onLaterPages=_page_cb)
|
||||
return buf.getvalue()
|
||||
Reference in New Issue
Block a user