Files
IT_asset_management/app/services/pdf_service.py

239 lines
10 KiB
Python

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'<b>{company_name}</b>', styles['normal'])],
[Paragraph(company_address or '', styles['small'])],
]
right_data = [
[Paragraph(f'<b>{doc_type_label}</b>', 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'<b>{label}</b>', 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