import os import json from datetime import datetime from reportlab.lib.pagesizes import A4 from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle from reportlab.lib.units import cm from reportlab.lib import colors from reportlab.lib.enums import TA_CENTER, TA_LEFT, TA_RIGHT from reportlab.platypus import ( SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle, HRFlowable ) def _styles(): base = getSampleStyleSheet() custom = { 'title': ParagraphStyle( 'DocTitle', parent=base['Title'], fontSize=16, spaceAfter=6, alignment=TA_CENTER, textColor=colors.HexColor('#1a3a5c'), ), 'subtitle': ParagraphStyle( 'Subtitle', parent=base['Normal'], fontSize=10, spaceAfter=4, alignment=TA_CENTER, textColor=colors.HexColor('#555555'), ), 'section': ParagraphStyle( 'Section', parent=base['Heading2'], fontSize=11, spaceBefore=12, spaceAfter=4, textColor=colors.HexColor('#1a3a5c'), borderPad=2, ), 'normal': base['Normal'], 'small': ParagraphStyle( 'Small', parent=base['Normal'], fontSize=8, textColor=colors.grey, ), 'footer': ParagraphStyle( 'Footer', parent=base['Normal'], fontSize=8, alignment=TA_CENTER, textColor=colors.grey, ), 'signature_label': ParagraphStyle( 'SigLabel', parent=base['Normal'], fontSize=9, alignment=TA_CENTER, ), 'right': ParagraphStyle( 'Right', parent=base['Normal'], alignment=TA_RIGHT, ), } return custom def _header_table(company_name, company_address, doc_type_label, doc_id, created_at, styles): left_data = [ [Paragraph(f'{company_name}', styles['normal'])], [Paragraph(company_address or '', styles['small'])], ] right_data = [ [Paragraph(f'{doc_type_label}', styles['right'])], [Paragraph(f'Doc #: {doc_id}', styles['right'])], [Paragraph(f'Date: {created_at.strftime("%d/%m/%Y") if created_at else ""}', styles['right'])], ] table = Table( [[Table(left_data, colWidths=[9 * cm]), Table(right_data, colWidths=[8 * cm])]], colWidths=[9 * cm, 8 * cm], ) table.setStyle(TableStyle([('VALIGN', (0, 0), (-1, -1), 'TOP')])) return table def _field_table(rows, styles): """rows: list of (label, value) tuples.""" data = [[Paragraph(f'{label}', styles['normal']), Paragraph(str(value or '—'), styles['normal'])] for label, value in rows] t = Table(data, colWidths=[5 * cm, 12 * cm]) t.setStyle(TableStyle([ ('BACKGROUND', (0, 0), (0, -1), colors.HexColor('#eaf0f8')), ('GRID', (0, 0), (-1, -1), 0.4, colors.HexColor('#cccccc')), ('VALIGN', (0, 0), (-1, -1), 'TOP'), ('TOPPADDING', (0, 0), (-1, -1), 4), ('BOTTOMPADDING', (0, 0), (-1, -1), 4), ('LEFTPADDING', (0, 0), (-1, -1), 6), ])) return t def _signature_block(styles): data = [ [Paragraph('Issued by (IT Dept.)', styles['signature_label']), Paragraph('Received by (Employee)', styles['signature_label'])], [Spacer(1, 1.5 * cm), Spacer(1, 1.5 * cm)], [HRFlowable(width='95%'), HRFlowable(width='95%')], [Paragraph('Name / Signature / Date', styles['signature_label']), Paragraph('Name / Signature / Date', styles['signature_label'])], ] t = Table(data, colWidths=[8.5 * cm, 8.5 * cm]) t.setStyle(TableStyle([ ('VALIGN', (0, 0), (-1, -1), 'MIDDLE'), ('ALIGN', (0, 0), (-1, -1), 'CENTER'), ])) return t def generate_paperwork_pdf(doc, app): """ Generate a PDF for a Paperwork document and save it to PDF_FOLDER. Returns the filename (not the full path). """ pdf_dir = os.path.join(app.root_path, '..', app.config['PDF_FOLDER']) os.makedirs(pdf_dir, exist_ok=True) filename = f'doc_{doc.id}_{datetime.utcnow().strftime("%Y%m%d_%H%M%S")}.pdf' filepath = os.path.join(pdf_dir, filename) page_doc = SimpleDocTemplate( filepath, pagesize=A4, rightMargin=2 * cm, leftMargin=2 * cm, topMargin=2 * cm, bottomMargin=2.5 * cm, title=doc.title, ) styles = _styles() company_name = app.config.get('COMPANY_NAME', '') company_address = app.config.get('COMPANY_ADDRESS', '') user = doc.user asset = doc.asset assignment = doc.assignment # Load extra template fields extra_fields = {} if doc.template_data: try: raw = json.loads(doc.template_data) extra_fields = {k.replace('td_', '').replace('_', ' ').title(): v for k, v in raw.items()} except (json.JSONDecodeError, TypeError): pass story = [] # ── Header ────────────────────────────────────────────────────────────── story.append(_header_table(company_name, company_address, doc.doc_type_label, doc.id, doc.created_at, styles)) story.append(Spacer(1, 0.3 * cm)) story.append(HRFlowable(width='100%', thickness=1.5, color=colors.HexColor('#1a3a5c'))) story.append(Spacer(1, 0.4 * cm)) # ── Title ──────────────────────────────────────────────────────────────── story.append(Paragraph(doc.title, styles['title'])) story.append(Spacer(1, 0.5 * cm)) # ── User section ───────────────────────────────────────────────────────── story.append(Paragraph('Employee Information', styles['section'])) user_rows = [ ('Windows ID', user.windows_id), ('Full Name', user.display_name), ('Email', user.display_email), ('Department', user.department or '—'), ('Job Title', user.job_title or '—'), ('Location', user.location or '—'), ] story.append(_field_table(user_rows, styles)) story.append(Spacer(1, 0.4 * cm)) # ── Asset section ───────────────────────────────────────────────────────── if asset: story.append(Paragraph('Asset Information', styles['section'])) asset_rows = [ ('Asset Type', asset.asset_type), ('Brand / Model', f'{asset.brand or ""} {asset.model or ""}'.strip() or '—'), ('Serial Number', asset.serial_number), ('Service Tag', asset.service_tag or '—'), ('Asset Tag', asset.asset_tag or '—'), ('Operating System', asset.operating_system or '—'), ] if asset.ram_gb: asset_rows.append(('RAM', f'{asset.ram_gb} GB')) if asset.storage_gb: asset_rows.append(('Storage', f'{asset.storage_gb} GB')) story.append(_field_table(asset_rows, styles)) story.append(Spacer(1, 0.4 * cm)) # ── Assignment section ──────────────────────────────────────────────────── if assignment: story.append(Paragraph('Assignment Details', styles['section'])) assign_rows = [ ('Assigned Date', str(assignment.assigned_date) if assignment.assigned_date else '—'), ('Returned Date', str(assignment.returned_date) if assignment.returned_date else 'Currently assigned'), ] story.append(_field_table(assign_rows, styles)) story.append(Spacer(1, 0.4 * cm)) # ── Extra / custom fields ───────────────────────────────────────────────── if extra_fields: story.append(Paragraph('Additional Information', styles['section'])) story.append(_field_table(list(extra_fields.items()), styles)) story.append(Spacer(1, 0.4 * cm)) # ── Notes ───────────────────────────────────────────────────────────────── if doc.notes: story.append(Paragraph('Notes', styles['section'])) story.append(Paragraph(doc.notes, styles['normal'])) story.append(Spacer(1, 0.4 * cm)) # Type-specific clauses if doc.document_type == 'assignment': story.append(Paragraph('Terms & Conditions', styles['section'])) clause = ( 'By signing below the employee acknowledges receipt of the above equipment in good ' 'working condition and agrees to: (1) use it solely for company business, ' '(2) report any damage or loss immediately to the IT department, and ' '(3) return it upon request or at the end of employment.' ) story.append(Paragraph(clause, styles['normal'])) story.append(Spacer(1, 0.4 * cm)) if doc.document_type == 'return': story.append(Paragraph('Return Confirmation', styles['section'])) clause = ( 'By signing below both parties confirm that the above equipment has been returned ' 'to the IT department and has been inspected for completeness and condition.' ) story.append(Paragraph(clause, styles['normal'])) story.append(Spacer(1, 0.4 * cm)) # ── Signatures ──────────────────────────────────────────────────────────── story.append(Paragraph('Signatures', styles['section'])) story.append(Spacer(1, 0.3 * cm)) story.append(_signature_block(styles)) story.append(Spacer(1, 0.5 * cm)) # ── Footer ──────────────────────────────────────────────────────────────── story.append(HRFlowable(width='100%', thickness=0.5, color=colors.grey)) story.append(Spacer(1, 0.2 * cm)) story.append(Paragraph( f'Generated by IT Asset Management System · {datetime.utcnow().strftime("%d/%m/%Y %H:%M")} UTC', styles['footer'], )) page_doc.build(story) return filename