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