06/12 Add export PDF function for Issues filter result
This commit is contained in:
@@ -1096,6 +1096,161 @@ def generate_scheduled_report_pdf(report_name, frequency, start, end,
|
||||
return buf.getvalue()
|
||||
|
||||
|
||||
# ══════════════════════════════════════════════════════════════════════════════
|
||||
# ISSUES LIST PDF
|
||||
# ══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
def generate_issues_list_pdf(issues, filter_summary: str = '') -> bytes:
|
||||
"""Return a PDF byte-string for a filtered list of issues.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
issues : list of Issue 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 = 'Issues 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 = []
|
||||
|
||||
if filter_summary:
|
||||
story.append(Paragraph(f'Filters: {filter_summary}', STYLES['ReportSub']))
|
||||
story.append(Paragraph(f'Total records: {len(issues)}', STYLES['ReportSub']))
|
||||
story.append(Spacer(1, 10))
|
||||
|
||||
if not issues:
|
||||
story.append(Paragraph('No issues match the selected filters.', STYLES['FieldValue']))
|
||||
doc.build(story, onFirstPage=_page_cb, onLaterPages=_page_cb)
|
||||
return buf.getvalue()
|
||||
|
||||
# ── Column widths ─────────────────────────────────────────────────────────
|
||||
# #, Reported, Severity, Contract, Facility / Area, Description, Status, SLA, Assigned
|
||||
col_w = [
|
||||
pw * 0.05, # #
|
||||
pw * 0.10, # Reported
|
||||
pw * 0.08, # Severity
|
||||
pw * 0.13, # Contract
|
||||
pw * 0.14, # Facility / Area
|
||||
pw * 0.28, # Description
|
||||
pw * 0.10, # Status
|
||||
pw * 0.07, # SLA
|
||||
pw * 0.05, # Assigned (truncated)
|
||||
]
|
||||
|
||||
hdr_style = ParagraphStyle('ILH', fontName='Helvetica-Bold', fontSize=7.5,
|
||||
textColor=C_WHITE, leading=9)
|
||||
val_style = ParagraphStyle('ILV', 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('#'), _h('Reported'), _h('Severity'), _h('Contract'),
|
||||
_h('Facility / Area'), _h('Description'), _h('Status'), _h('SLA'), _h('Assigned'),
|
||||
]]
|
||||
|
||||
for iss in issues:
|
||||
reported_str = iss.reported_at.strftime('%Y-%m-%d %H:%M') if iss.reported_at else '—'
|
||||
|
||||
sev = iss.severity or 'low'
|
||||
sev_color = SEVERITY_COLORS.get(sev, C_SLATE)
|
||||
|
||||
contract_str = '—'
|
||||
if iss.resolved_facility and iss.resolved_facility.project:
|
||||
contract_str = iss.resolved_facility.project.name
|
||||
facility_str = iss.resolved_facility.name if iss.resolved_facility else '—'
|
||||
area_str = iss.area.name if iss.area else '—'
|
||||
fac_area_str = f'{facility_str}\n{area_str}'
|
||||
|
||||
desc_str = iss.description or ''
|
||||
if len(desc_str) > 90:
|
||||
desc_str = desc_str[:90] + '...'
|
||||
|
||||
status_raw = iss.status or ''
|
||||
if status_raw == 'resolved':
|
||||
status_str = 'Resolved'
|
||||
status_color = C_GREEN
|
||||
elif status_raw == 'pending_verification':
|
||||
status_str = 'Pending Verif.'
|
||||
status_color = colors.HexColor('#0ea5e9')
|
||||
elif status_raw == 'in_progress':
|
||||
status_str = 'In Progress'
|
||||
status_color = C_YELLOW
|
||||
else:
|
||||
status_str = 'Open'
|
||||
status_color = C_RED
|
||||
|
||||
# Compute SLA inline (avoids circular import — same logic as sla.sla_status)
|
||||
from app.utils.sla import sla_status as _sla_status
|
||||
sla = _sla_status(iss)
|
||||
if sla == 'breached':
|
||||
sla_str, sla_color = 'Breached', C_RED
|
||||
elif sla == 'at_risk':
|
||||
sla_str, sla_color = 'At Risk', C_YELLOW
|
||||
elif sla == 'ok':
|
||||
sla_str, sla_color = 'OK', C_GREEN
|
||||
else:
|
||||
sla_str, sla_color = '—', C_SLATE
|
||||
|
||||
assigned_str = '—'
|
||||
if iss.assigned_user:
|
||||
n = iss.assigned_user.display_name
|
||||
assigned_str = n[:18] + ('...' if len(n) > 18 else '')
|
||||
|
||||
tbl_data.append([
|
||||
_v(f'#{iss.id}'),
|
||||
_v(reported_str),
|
||||
_v(sev.title(), color=sev_color),
|
||||
_v(contract_str[:28] + ('...' if len(contract_str) > 28 else '')),
|
||||
Paragraph(f'{facility_str[:28]}\n<font size="6.5" color="{C_SLATE.hexval()}">{area_str[:24]}</font>', val_style),
|
||||
_v(desc_str),
|
||||
_v(status_str, color=status_color),
|
||||
_v(sla_str, color=sla_color),
|
||||
_v(assigned_str),
|
||||
])
|
||||
|
||||
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()
|
||||
|
||||
|
||||
# ══════════════════════════════════════════════════════════════════════════════
|
||||
# INSPECTIONS LIST PDF
|
||||
# ══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
Reference in New Issue
Block a user