239 lines
10 KiB
Python
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
|