HR-ATS-Portal/backend/job/job_post/export.py

163 lines
5.4 KiB
Python

"""Styled .xlsx export of job requisitions — openpyxl only.
Pure module: no FastAPI imports and no HTTPException.
Takes the already-serialized rows from JobPost.fetch_jobs (serialize_job_row
dicts) so the export always matches what the Jobs screen shows, filters
included. Returns the finished workbook as bytes for a Response body.
"""
from __future__ import annotations
from datetime import datetime
from io import BytesIO
from openpyxl import Workbook
from openpyxl.styles import Alignment, Border, Font, PatternFill, Side
from openpyxl.utils import get_column_letter
BRAND_DARK = "0B3D2E" # header/banner green, matches the app chrome
BRAND_STRIPE = "EFF7F2" # zebra row tint
BORDER_TINT = "CBDCD2"
STATUS_LABELS = {"open": "Open", "closed": "Closed", "on_hold": "On Hold", "completed": "Completed"}
STATUS_COLORS = {"open": "1B7F4B", "closed": "B3261E", "on_hold": "9A6700", "completed": "0F6E56"}
# (header, column width)
COLUMNS = [
("Title", 34),
("Department", 16),
("Location", 20),
("Type", 12),
("Platform", 12),
("Vacancies", 11),
("Experience", 13),
("Salary", 16),
("Status", 10),
("Publishing", 12),
("Recruiter", 18),
("Hiring Manager", 18),
("Created By", 18),
("Created", 13),
("Requirements", 46),
("Nice to Have", 34),
("Description", 60),
]
_THIN = Side(style="thin", color=BORDER_TINT)
_BORDER = Border(left=_THIN, right=_THIN, top=_THIN, bottom=_THIN)
def _experience(row) -> str:
lo, hi = row.get("experience_min"), row.get("experience_max")
if lo is None and hi is None:
return ""
if lo is not None and hi is not None:
return f"{lo}-{hi} years"
return f"{lo if lo is not None else hi}+ years"
def _bullets(items) -> str:
return "\n".join(f"{str(i).strip()}" for i in (items or []) if str(i).strip())
def _created(row):
raw = row.get("created_at")
if not raw:
return None
try:
return datetime.fromisoformat(raw).replace(tzinfo=None)
except ValueError:
return None
def build_jobs_workbook(rows) -> bytes:
wb = Workbook()
ws = wb.active
ws.title = "Jobs"
ws.sheet_properties.tabColor = BRAND_DARK
ws.sheet_view.showGridLines = False
last_col = get_column_letter(len(COLUMNS))
for idx, (_, width) in enumerate(COLUMNS, start=1):
ws.column_dimensions[get_column_letter(idx)].width = width
# Banner
ws.merge_cells(f"A1:{last_col}1")
banner = ws["A1"]
banner.value = "Jobs Export"
banner.font = Font(size=16, bold=True, color=BRAND_DARK)
banner.alignment = Alignment(vertical="center")
ws.row_dimensions[1].height = 30
ws.merge_cells(f"A2:{last_col}2")
sub = ws["A2"]
sub.value = (
f"TalentFlow · generated {datetime.now().strftime('%d %b %Y, %H:%M')} · "
f"{len(rows)} requisition{'s' if len(rows) != 1 else ''}"
)
sub.font = Font(size=10, color="6B7A72")
ws.row_dimensions[3].height = 6
# Header
header_row = 4
for idx, (label, _) in enumerate(COLUMNS, start=1):
cell = ws.cell(row=header_row, column=idx, value=label)
cell.font = Font(bold=True, color="FFFFFF", size=11)
cell.fill = PatternFill("solid", fgColor=BRAND_DARK)
cell.alignment = Alignment(horizontal="center", vertical="center")
cell.border = _BORDER
ws.row_dimensions[header_row].height = 22
# Data
top = Alignment(vertical="top", wrap_text=False)
wrap = Alignment(vertical="top", wrap_text=True)
center = Alignment(horizontal="center", vertical="top")
for r, row in enumerate(rows, start=header_row + 1):
status_key = row.get("requisition_status") or ""
values = [
row.get("title") or "",
row.get("department") or "",
row.get("location") or "",
row.get("employment_type") or "",
row.get("platform") or "",
row.get("vacancies"),
_experience(row),
row.get("salary") or "",
STATUS_LABELS.get(status_key, status_key),
row.get("status") or "",
row.get("recruiter_name") or "",
row.get("hiring_manager_name") or "",
row.get("created_by_name") or "",
_created(row),
_bullets(row.get("requirements")),
_bullets(row.get("optional_skills")),
(row.get("description") or "").strip(),
]
stripe = r % 2 == 0
for c, value in enumerate(values, start=1):
cell = ws.cell(row=r, column=c, value=value)
cell.border = _BORDER
cell.alignment = top
if stripe:
cell.fill = PatternFill("solid", fgColor=BRAND_STRIPE)
ws.cell(row=r, column=1).font = Font(bold=True)
ws.cell(row=r, column=6).alignment = center
status_cell = ws.cell(row=r, column=9)
status_cell.alignment = center
if status_key in STATUS_COLORS:
status_cell.font = Font(bold=True, color=STATUS_COLORS[status_key])
created_cell = ws.cell(row=r, column=14)
if created_cell.value is not None:
created_cell.number_format = "dd mmm yyyy"
for c in (14, 15, 16):
ws.cell(row=r, column=c).alignment = wrap
last_row = header_row + max(len(rows), 1)
ws.auto_filter.ref = f"A{header_row}:{last_col}{last_row}"
ws.freeze_panes = f"A{header_row + 1}"
buf = BytesIO()
wb.save(buf)
return buf.getvalue()