Files
stundenfuchs/app/services/exporters.py
T
maddin 6fbd1bb3c2
CI / checks (push) Has been cancelled
chore: initialize public repository
2026-03-22 12:57:09 +00:00

238 lines
7.7 KiB
Python

import json
from datetime import date
from io import BytesIO
from openpyxl import Workbook
from reportlab.lib.pagesizes import A4, landscape
from reportlab.pdfgen import canvas
from app.services.calculations import minutes_to_hhmm
from app.services.targets import monday_of
def create_excel_export(rows: list[dict], week_summaries: list[dict], totals: dict, title: str) -> bytes:
workbook = Workbook()
sheet = workbook.active
sheet.title = "Tage"
headers = [
"Datum",
"Wochentag",
"KW",
"Start",
"Ende",
"Pause (min)",
"Brutto",
"Netto",
"Stundenausgleich",
"Sonderstatus",
"Wochen-Soll",
"Wochen-Delta",
"Notiz",
]
sheet.append(headers)
for row in rows:
sheet.append(
[
row["date"].isoformat(),
row["weekday_name"],
row["iso_week"],
row["start_time"] or "",
row["end_time"] or "",
row["break_minutes"],
minutes_to_hhmm(row["gross_minutes"]),
minutes_to_hhmm(row["net_minutes"]),
minutes_to_hhmm(row["overtime_adjustment_minutes"]),
row["special_status_label"] or "",
minutes_to_hhmm(row["weekly_target_minutes"]),
minutes_to_hhmm(row["weekly_delta_minutes"]),
row["notes"] or "",
]
)
for col in ["A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M"]:
sheet.column_dimensions[col].width = 16
summary = workbook.create_sheet("Wochen")
summary_headers = ["KW-Start", "KW-Ende", "Ist", "Soll", "Delta"]
summary.append(summary_headers)
for item in week_summaries:
summary.append(
[
item["week_start"].isoformat(),
item["week_end"].isoformat(),
minutes_to_hhmm(item["ist_minutes"]),
minutes_to_hhmm(item["soll_minutes"]),
minutes_to_hhmm(item["delta_minutes"]),
]
)
summary.append([])
summary.append(["Gesamt", "", minutes_to_hhmm(totals["ist_minutes"]), "", minutes_to_hhmm(totals["delta_minutes"])])
meta = workbook.create_sheet("Meta")
meta.append([title])
meta.append([f"Zeitraum: {totals['from_date'].isoformat()} bis {totals['to_date'].isoformat()}"])
output = BytesIO()
workbook.save(output)
return output.getvalue()
def create_pdf_export(rows: list[dict], week_summaries: list[dict], totals: dict, title: str) -> bytes:
output = BytesIO()
pdf = canvas.Canvas(output, pagesize=landscape(A4))
width, height = landscape(A4)
y = height - 35
pdf.setFont("Helvetica-Bold", 13)
pdf.drawString(24, y, title)
y -= 18
pdf.setFont("Helvetica", 10)
pdf.drawString(24, y, f"Zeitraum: {totals['from_date'].isoformat()} bis {totals['to_date'].isoformat()}")
y -= 24
pdf.setFont("Helvetica-Bold", 8)
pdf.drawString(24, y, "Datum")
pdf.drawString(88, y, "Tag")
pdf.drawString(124, y, "KW")
pdf.drawString(154, y, "Start")
pdf.drawString(198, y, "Ende")
pdf.drawString(242, y, "Pause")
pdf.drawString(286, y, "Brutto")
pdf.drawString(338, y, "Netto")
pdf.drawString(390, y, "Ausgl.")
pdf.drawString(436, y, "Status")
pdf.drawString(490, y, "Soll")
pdf.drawString(542, y, "W-Delta")
pdf.drawString(610, y, "Notiz")
y -= 12
pdf.setFont("Helvetica", 8)
for row in rows:
if y < 40:
pdf.showPage()
y = height - 30
pdf.setFont("Helvetica", 8)
note = (row["notes"] or "").strip()
if len(note) > 18:
note = f"{note[:15]}..."
pdf.drawString(24, y, row["date"].isoformat())
pdf.drawString(88, y, row["weekday_short"])
pdf.drawString(124, y, str(row["iso_week"]))
pdf.drawString(154, y, row["start_time"] or "-")
pdf.drawString(198, y, row["end_time"] or "-")
pdf.drawString(242, y, str(row["break_minutes"]))
pdf.drawString(286, y, minutes_to_hhmm(row["gross_minutes"]))
pdf.drawString(338, y, minutes_to_hhmm(row["net_minutes"]))
pdf.drawString(390, y, minutes_to_hhmm(row["overtime_adjustment_minutes"]))
pdf.drawString(436, y, row["special_status_label"] or "-")
pdf.drawString(490, y, minutes_to_hhmm(row["weekly_target_minutes"]))
pdf.drawString(542, y, minutes_to_hhmm(row["weekly_delta_minutes"]))
pdf.drawString(610, y, note)
y -= 11
y -= 12
pdf.setFont("Helvetica-Bold", 10)
pdf.drawString(24, y, "Wochenzusammenfassung")
y -= 14
pdf.setFont("Helvetica", 9)
for item in week_summaries:
if y < 40:
pdf.showPage()
y = height - 30
pdf.setFont("Helvetica", 9)
line = (
f"{item['week_start'].isoformat()} - {item['week_end'].isoformat()} | "
f"Ist {minutes_to_hhmm(item['ist_minutes'])} | "
f"Soll {minutes_to_hhmm(item['soll_minutes'])} | "
f"Delta {minutes_to_hhmm(item['delta_minutes'])}"
)
pdf.drawString(24, y, line)
y -= 12
y -= 10
pdf.setFont("Helvetica-Bold", 10)
pdf.drawString(
24,
y,
f"Gesamt Ist: {minutes_to_hhmm(totals['ist_minutes'])} | Gesamt Delta: {minutes_to_hhmm(totals['delta_minutes'])}",
)
pdf.save()
return output.getvalue()
def create_backup_export(payload: dict) -> bytes:
return json.dumps(payload, ensure_ascii=False, indent=2).encode("utf-8")
def build_export_rows(
days: list[date],
entries_by_date: dict,
week_target_map: dict[date, int],
week_ist_map: dict[date, int],
week_delta_map: dict[date, int],
special_status_map: dict[date, str] | None = None,
overtime_adjustment_map: dict[date, int] | None = None,
) -> list[dict]:
weekday_names = ["Montag", "Dienstag", "Mittwoch", "Donnerstag", "Freitag", "Samstag", "Sonntag"]
weekday_short = ["Mo", "Di", "Mi", "Do", "Fr", "Sa", "So"]
special_status_map = special_status_map or {}
overtime_adjustment_map = overtime_adjustment_map or {}
special_status_labels = {
"holiday": "Feiertag",
"sick": "Krankheit",
}
rows: list[dict] = []
for day in days:
entry = entries_by_date.get(day)
week_start = monday_of(day)
weekly_target = week_target_map[week_start]
weekly_delta = week_delta_map[week_start]
if entry:
gross = entry.end_minutes - entry.start_minutes
net = gross - entry.break_minutes
start_time = f"{entry.start_minutes // 60:02d}:{entry.start_minutes % 60:02d}"
end_time = f"{entry.end_minutes // 60:02d}:{entry.end_minutes % 60:02d}"
break_minutes = entry.break_minutes
notes = entry.notes
else:
gross = 0
net = 0
start_time = None
end_time = None
break_minutes = 0
notes = None
rows.append(
{
"date": day,
"weekday_name": weekday_names[day.weekday()],
"weekday_short": weekday_short[day.weekday()],
"iso_week": day.isocalendar()[1],
"start_time": start_time,
"end_time": end_time,
"break_minutes": break_minutes,
"gross_minutes": gross,
"net_minutes": net,
"overtime_adjustment_minutes": overtime_adjustment_map.get(day, 0),
"special_status_label": special_status_labels.get(special_status_map.get(day, "")),
"weekly_target_minutes": weekly_target,
"weekly_delta_minutes": weekly_delta,
"notes": notes,
}
)
return rows