Files
catops/cntmanage/flightslot/actions/exportweek.py
2025-12-10 11:05:22 +01:00

269 lines
12 KiB
Python

from django.http import HttpRequest, HttpResponse
from django.db.models.query import QuerySet
from openpyxl import Workbook
from openpyxl.styles import Font, PatternFill, Alignment, Border, Side
from openpyxl.utils import get_column_letter
from openpyxl.worksheet.page import PageMargins
from ..models.courses import CourseTypes
from ..models.missions import Training
from ..models.weekpref import WeekPreference
from ..models.hourbuildings import HourBuilding, HourBuildingLegFlight, HourBuildingLegStop, HourBuildingLegBase
from datetime import date, datetime
from typing import List
# Enable cell merging for equal mission
MERGE: bool = False
PALETTE : List[str] = [
"#E6F2FF", # azzurro chiarissimo
"#E5FBF8", # verde acqua molto chiaro
"#ECFBE1", # verde chiarissimo
"#FFFBD1", # giallo molto chiaro
"#FFF1D6", # giallo-arancio molto chiaro
"#FFE3DD", # rosa pesca molto chiaro
"#F3E6FA", # lilla chiarissimo
]
def export_selected(request: HttpRequest, queryset: QuerySet[WeekPreference]) -> HttpResponse:
if not queryset.first():
raise Exception("Empty queryset")
# Init Variables
today = date.today()
year = today.year
month = today.month
day = today.day
week = queryset.first().week if queryset.first() else date.today().isocalendar().week
weeks = queryset.order_by("week").distinct("week").all()
# Prepare export filename and http content
filename = f"{year}{month}{day}_week_{'+'.join([str(w.week) for w in weeks])}_export.xlsx"
response = HttpResponse(content_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')
response['Content-Disposition'] = f'attachment; filename="{filename}"'
# Create workbook and sheet
wb = Workbook()
ws = wb.active
if not ws:
raise Exception("Export: cannot select active workbook")
ws.title = f"Week Preferences"
# Header titles
days = [f"{datetime.strptime(f"{year} {week} {x}", "%G %V %u").strftime("%A")} {datetime.strptime(f"{year} {week} {x}", "%G %V %u").day}" for x in range(1,8)]
headers = ["Week", "Student", "Course", *days, "Notes", "Cell.", "Mail"]
# Header fields positions
week_index: int = headers.index("Week") + 1
student_index: int = headers.index("Student") + 1
course_index: int = headers.index("Course") + 1
cell_index: int = headers.index("Cell.") + 1
mail_index: int = headers.index("Mail") + 1
note_index: int = headers.index("Notes") + 1
# Stile header
header_fill = PatternFill("solid", fgColor="0e005c")
bold_white = Font(color="FFFFFF", bold=True)
bold_black = Font(color="000000", bold=True)
center = Alignment(horizontal="center", vertical="center", wrapText=True)
# Cell styles
border_thick: Side = Side(style='medium', color='000000')
border_thin: Side = Side(style='thin', color='000000', border_style='dashed')
border_bottom: Border = Border(bottom=border_thick)
border_bottom_thin: Border = Border(bottom=border_thin)
border_left: Border = Border(left=border_thick)
border_right: Border = Border(right=border_thick)
border_right_thin: Border = Border(right=border_thin)
border_all: Border = Border(bottom=border_thick, top=border_thick, left=border_thick, right=None)
# Scrittura header
head_size: int = len(headers)
for col, h in enumerate(headers, start=1):
cell = ws.cell(row=1, column=col, value=h)
cell.fill = header_fill
cell.font = bold_white
cell.alignment = center
match col:
case int(1):
cell.border = Border(top=border_thick, bottom=border_thick, left=border_thick)
case int(head_size):
cell.border = Border(top=border_thick, bottom=border_thick, right=border_thick)
case _:
cell.border = Border(top=border_thick, bottom=border_thick)
### Start of Student Loop ###
# Fill worksheet with EVERY training and hb for every student
# Each of this iterations fills the table for a student
row: int = 2
row_offset: int = 0
for i, q in enumerate(queryset.order_by("-week", "student__surname", "student__name", "student__course"), start=1):
student_data: List[str]
student_phone: str = str(q.student.phone) if q.student.phone else ""
student_email: str = q.student.email
student_course_type: str
student_course_number: str
student_course_ac: str = f"({'/'.join(t.type for t in q.student.aircrafts.distinct("type").all())})"
if q.student.course:
student_course_type = q.student.course.ctype
student_course_number = str(q.student.course.cnumber)
student_data = [
"\n".join([f"{q.student.surname} {q.student.name}", student_course_ac]),
f"{student_course_type}-{student_course_number}"
]
else:
student_data = [f"{q.student.surname} {q.student.name}", f"No Course Assigned"]
# Fill Training mission rows
mission_name: List[str]
mission_days: List[str]
mission_notes: str
mission_data: List[List[str]] = []
for t in Training.objects.filter(weekpref = q.id):
if not t.mission:
raise Exception("No Training Mission Assigned")
mission_notes = t.notes.strip().capitalize() if t.notes else ""
mission_name = [f"{t.mission.mtype}-{t.mission.mnum}"]
if q.student.course and q.student.course.ctype == CourseTypes.PPL: # add course aircraft only for PPL students
mission_name.append(student_course_ac)
mission_name_joined: str = "\n".join(mission_name)
mission_days = [
mission_name_joined if t.monday else "",
mission_name_joined if t.tuesday else "",
mission_name_joined if t.wednesday else "",
mission_name_joined if t.thursday else "",
mission_name_joined if t.friday else "",
mission_name_joined if t.saturday else "",
mission_name_joined if t.sunday else ""
]
mission_data.append([str(q.week), *student_data, *mission_days, mission_notes, student_phone, student_email, ])
# Fill HourBuilding rows
hb_name: List[str]
hb_days: List[str]
hb_data: List[List[str]] = []
for h in HourBuilding.objects.filter(weekpref = q.id):
hb_name = ["HB", f"({h.aircraft})"]
hb_legs_all = HourBuildingLegBase.objects.filter(hb_id = h.id)
for hh in hb_legs_all:
time_str: str = ':'.join(str(hh.time).split(':')[:2]) # keep only hours and minutes
if isinstance(hh, HourBuildingLegFlight):
hb_pax: str | None = " ".join(x.capitalize() for x in hh.pax.split()) if hh.pax else None
hb_name.append(f"{hh.departure} -> {hh.destination} [{time_str}]{f' / Pax: {hb_pax}' if hb_pax else ''}")
elif isinstance(hh, HourBuildingLegStop):
hb_name.append(f"STOP [{time_str}] {"Refuel" if hh.refuel else ""}" )
hb_name_joined: str = "\n".join(hb_name)
hb_days = [
hb_name_joined if h.monday else "",
hb_name_joined if h.tuesday else "",
hb_name_joined if h.wednesday else "",
hb_name_joined if h.thursday else "",
hb_name_joined if h.friday else "",
hb_name_joined if h.saturday else "",
hb_name_joined if h.sunday else ""
]
hb_notes: str = h.notes.strip().capitalize() if h.notes else ""
hb_data.append([str(q.week), *student_data, *hb_days, hb_notes, str(q.student.phone), q.student.email])
# Build rows for table
all_data: List[List[str]] = mission_data + hb_data
student_start: int = row + row_offset
for ri, row_content in enumerate(all_data):
for c, cell_content in enumerate(row_content, start=1):
cell = ws.cell(row = row + row_offset, column = c, value = cell_content)
cell.alignment = center
# Format Student Name
if c == student_index:
cell.font = bold_black
# Format Course Column with color
elif c == course_index and q.student.course:
cell.font = bold_black
cell.fill = PatternFill("solid", fgColor=str(q.student.course.color).lstrip('#').lower())
# Add internal borders between mix cells and notes
elif c > course_index and c <= note_index:
cell.border = border_bottom_thin + border_right_thin
# Fill mix cells if the cell is not empty
if c > course_index and c <= note_index:
if len(cell_content):
cell.fill = PatternFill('solid', fgColor=PALETTE[ri % len(PALETTE)].lstrip("#").lower())
if MERGE:
prev_cell_val: str = row_content[0]
merge_start: bool = False
merge_col_start: int = 1
for c, cell_content in enumerate(row_content, start=1):
# Merge cells in the row
if cell_content == prev_cell_val and not merge_start:
merge_start = True
merge_col_start = c-1 # start merge from previous column
elif cell_content != prev_cell_val and merge_start:
merge_start = False
ws.merge_cells(start_row=row+row_offset,
end_row=row+row_offset,
start_column=max(merge_col_start,1),
end_column=max(c-1,1)) # end merge to previous column
prev_cell_val = cell_content
# Incement row counter
row_offset += 1
# End week preferences for this student
student_end: int = row + row_offset - 1
# Add thick border to the last cell row of this student
for c in range(course_index, mail_index + 1):
ws.cell(row=student_end, column=c).border = Border(bottom=border_thick, right=border_thin)
# And for last column also a vertical border all student high
if c == mail_index:
ws.cell(row=student_end, column=c).border = Border(bottom=border_thick, right=border_thick)
# Merge Week, thick border
ws.cell(row=student_start, column=week_index).border = border_all
ws.merge_cells(start_row=student_start, end_row=student_end, start_column=week_index, end_column=week_index)
# Merge Name, thick border
ws.cell(row=student_start, column=student_index).border = border_all
ws.merge_cells(start_row=student_start, end_row=student_end, start_column=student_index, end_column=student_index)
# Merge Course
ws.merge_cells(start_row=student_start, end_row=student_end, start_column=course_index, end_column=course_index)
# Merge Cell
ws.merge_cells(start_row=student_start, end_row=student_end, start_column=cell_index, end_column=cell_index)
# Merge Mail
ws.merge_cells(start_row=student_start, end_row=student_end, start_column=mail_index, end_column=mail_index)
# Keep the largest column
for column_cells in ws.columns:
col_letter: str = "A"
max_len: List[int] = []
for cell in column_cells:
cell_lines = str(cell.value).splitlines()
if len(cell_lines) == 0:
continue
max_len.append(max([len(ll) for ll in cell_lines]))
length: int = max(max_len)
if column_cells[0].column:
col_letter = get_column_letter(column_cells[0].column)
ws.column_dimensions[col_letter].width = min(length + 2, 35)
### End of Student Loop ###
# Set paper size and format
ws.page_setup.orientation = ws.ORIENTATION_LANDSCAPE
ws.page_setup.paperSize = ws.PAPERSIZE_A3
ws.page_setup.fitToHeight = 0
ws.page_setup.fitToWidth = 1
ws.print_options.horizontalCentered = True
ws.page_setup.fitToPage = True
ws.page_margins = PageMargins(
left=0.25, right=0.25,
top=0.75, bottom=0.75,
header=0.3, footer=0.3
)
ws.print_area = ws.calculate_dimension()
# Save document in HttpResponse
wb.save(response)
return response