template mit 36 Monaten erzeugt. Denn es werden keine sheets selber erstellt.
This commit is contained in:
1 parent
56d605c0cb
commit
39d3ee8baf
2 files changed
+63
-59
No files matched your search
+63
-59
@@ -1,11 +1,15 @@
|
||||
"""Template-basierte Activity-Report-Generierung."""
|
||||
"""Template-basierte Activity-Report-Generierung.
|
||||
|
||||
Verwendet ein Template mit bereits vor-duplizierten Sheets.
|
||||
Es werden KEINE Sheets programmatisch erzeugt oder kopiert –
|
||||
ausschließlich die vorhandenen Sheets mit Daten befüllt.
|
||||
"""
|
||||
|
||||
from datetime import date
|
||||
from pathlib import Path
|
||||
|
||||
from openpyxl import load_workbook
|
||||
from openpyxl.cell.cell import MergedCell
|
||||
from openpyxl.utils import column_index_from_string
|
||||
|
||||
from .models import EmployeeRecord, OutputMapping
|
||||
|
||||
@@ -30,7 +34,6 @@ def _format_value(value, fmt: str | None) -> str:
|
||||
return ""
|
||||
if fmt is None:
|
||||
return str(value)
|
||||
# Numerische Formatierung (z.B. "%.2f")
|
||||
if "%" in fmt:
|
||||
try:
|
||||
return fmt % float(value)
|
||||
@@ -44,7 +47,6 @@ def _set_cell_value(ws, target_cell: str, value) -> None:
|
||||
Behandelt MergedCells, indem in die Top-Left-Zelle des Merge-Bereichs geschrieben wird."""
|
||||
try:
|
||||
cell = ws[target_cell]
|
||||
# Wenn es eine MergedCell ist, finde die Top-Left-Zelle des Merge-Bereichs
|
||||
if isinstance(cell, MergedCell):
|
||||
for merged_range in ws.merged_cells.ranges:
|
||||
if cell.coordinate in merged_range:
|
||||
@@ -54,11 +56,11 @@ def _set_cell_value(ws, target_cell: str, value) -> None:
|
||||
else:
|
||||
cell.value = value
|
||||
except (ValueError, KeyError):
|
||||
pass # Zelle existiert nicht
|
||||
pass
|
||||
|
||||
|
||||
def _format_month_for_template(monat_str: str) -> str:
|
||||
"""Konvertiert 'MM.JJ' zu 'MM.JJJJ' (z.B. '01.2025' → '01.2025', '01.25' → '01.2025')."""
|
||||
"""Konvertiert 'MM.JJ' zu 'MM.JJJJ'."""
|
||||
monat_str = monat_str.strip()
|
||||
parts = monat_str.split(".")
|
||||
if len(parts) == 2:
|
||||
@@ -70,6 +72,42 @@ def _format_month_for_template(monat_str: str) -> str:
|
||||
return monat_str
|
||||
|
||||
|
||||
def _month_name(mi: int) -> str:
|
||||
names = ["", "Januar", "Februar", "März", "April", "Mai", "Juni",
|
||||
"Juli", "August", "September", "Oktober", "November", "Dezember"]
|
||||
return names[mi] if 1 <= mi <= 12 else ""
|
||||
|
||||
|
||||
def _fill_sheet(ws, record: EmployeeRecord, employee_name: str, field_map: dict) -> None:
|
||||
"""Befüllt ein einzelnes Sheet mit den Daten eines EmployeeRecords."""
|
||||
for source_field, m in field_map.items():
|
||||
if source_field == "Monat (MM.JJ)":
|
||||
value = _format_month_for_template(record.monat)
|
||||
elif source_field == "Stellenanteil in %":
|
||||
value = record.stellenanteil
|
||||
elif source_field == "Name":
|
||||
value = employee_name
|
||||
elif source_field == "Antragsnummer":
|
||||
value = record.antragsnummer
|
||||
elif source_field == "Qualifikation":
|
||||
value = record.qualifikation
|
||||
elif source_field == "Funktion":
|
||||
value = record.funktion
|
||||
elif source_field == "Tätigkeit":
|
||||
value = record.taetigkeit
|
||||
elif source_field == "Ort":
|
||||
value = "Dresden"
|
||||
elif source_field == "Datum":
|
||||
value = date.today().strftime("%d.%m.%Y")
|
||||
else:
|
||||
continue
|
||||
formatted = _format_value(value, m.format)
|
||||
_set_cell_value(ws, m.target_cell, formatted)
|
||||
|
||||
# Datum zusätzlich ins Unterschriftsfeld
|
||||
_set_cell_value(ws, "J95", date.today().strftime("%d.%m.%Y"))
|
||||
|
||||
|
||||
def generate_activity_reports(
|
||||
employee_records: dict[str, list[EmployeeRecord]],
|
||||
template_path: Path,
|
||||
@@ -78,13 +116,13 @@ def generate_activity_reports(
|
||||
filename_pattern: str,
|
||||
) -> list[Path]:
|
||||
"""
|
||||
Generiert für jeden Mitarbeiter eine Excel-Datei.
|
||||
Jeder Monat wird als eigenes Sheet im Template-Stil erstellt.
|
||||
Befüllt vor-duplizierte Template-Sheets mit Mitarbeiterdaten.
|
||||
Erzeugt KEINE neuen Sheets – verwendet ausschließlich die im Template
|
||||
bereits vorhandenen Sheet-Kopien.
|
||||
"""
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
generated_files: list[Path] = []
|
||||
|
||||
# Build source_field → mapping lookup
|
||||
field_map: dict[str, OutputMapping] = {m.source_field: m for m in mapping}
|
||||
|
||||
for employee_name, records in employee_records.items():
|
||||
@@ -95,65 +133,31 @@ def generate_activity_reports(
|
||||
safe_first = first_name.replace(" ", "_").replace("/", "_")
|
||||
safe_last = last_name.replace(" ", "_").replace("/", "_")
|
||||
|
||||
filename = filename_pattern.format(
|
||||
first_name=safe_first,
|
||||
last_name=safe_last,
|
||||
)
|
||||
filename = filename_pattern.format(first_name=safe_first, last_name=safe_last)
|
||||
output_path = output_dir / filename
|
||||
|
||||
# Lade Template (bleibt unverändert als Kopiervorlage)
|
||||
wb = load_workbook(template_path)
|
||||
template_sheet = wb.worksheets[0]
|
||||
|
||||
# Sortiere Records nach Monat
|
||||
sorted_records = sorted(records, key=lambda r: r.month_index)
|
||||
total_needed = len(sorted_records)
|
||||
available_sheets = wb.worksheets
|
||||
|
||||
if len(available_sheets) < total_needed:
|
||||
wb.close()
|
||||
raise RuntimeError(
|
||||
f"Template hat nur {len(available_sheets)} Sheets, "
|
||||
f"aber {total_needed} werden benötigt für '{employee_name}'."
|
||||
)
|
||||
|
||||
for idx, record in enumerate(sorted_records):
|
||||
# Immer vom sauberen Template kopieren – nie das Original beschreiben
|
||||
sheet = wb.copy_worksheet(template_sheet)
|
||||
|
||||
# Sheet-Name: Monatsname + Funktionskürzel (erste 3 Buchstaben)
|
||||
month_names = [
|
||||
"", "Januar", "Februar", "März", "April", "Mai", "Juni",
|
||||
"Juli", "August", "September", "Oktober", "November", "Dezember"
|
||||
]
|
||||
sheet = available_sheets[idx]
|
||||
mi = record.month_index
|
||||
func_abbr = record.funktion[:3] if record.funktion else ""
|
||||
sheet_name = f"{mi:02d}_{month_names[mi]}_{func_abbr}" if 1 <= mi <= 12 else f"Monat_{idx + 1}"
|
||||
sheet.title = sheet_name[:31]
|
||||
sheet.title = f"{mi:02d}_{_month_name(mi)}_{func_abbr}"[:31]
|
||||
_fill_sheet(sheet, record, employee_name, field_map)
|
||||
|
||||
# Befülle Zielfelder
|
||||
for source_field, m in field_map.items():
|
||||
if source_field == "Monat (MM.JJ)":
|
||||
value = _format_month_for_template(record.monat)
|
||||
elif source_field == "Stellenanteil in %":
|
||||
value = record.stellenanteil
|
||||
elif source_field == "Name":
|
||||
value = employee_name
|
||||
elif source_field == "Antragsnummer":
|
||||
value = record.antragsnummer
|
||||
elif source_field == "Qualifikation":
|
||||
value = record.qualifikation
|
||||
elif source_field == "Funktion":
|
||||
value = record.funktion
|
||||
elif source_field == "Tätigkeit":
|
||||
value = record.taetigkeit
|
||||
elif source_field == "Ort":
|
||||
value = "Dresden"
|
||||
elif source_field == "Datum":
|
||||
value = date.today().strftime("%d.%m.%Y")
|
||||
else:
|
||||
continue
|
||||
|
||||
formatted = _format_value(value, m.format)
|
||||
_set_cell_value(sheet, m.target_cell, formatted)
|
||||
|
||||
# Setze aktuelles Datum auch ins Unterschriftsfeld
|
||||
today = date.today().strftime("%d.%m.%Y")
|
||||
_set_cell_value(sheet, "J95", today)
|
||||
|
||||
# Entferne das leere Original-Template-Sheet
|
||||
wb.remove(template_sheet)
|
||||
# Lösche überzählige Sheets (von hinten nach vorne)
|
||||
for extra in reversed(available_sheets[total_needed:]):
|
||||
wb.remove(extra)
|
||||
|
||||
wb.save(output_path)
|
||||
wb.close()
|
||||
|
||||
Reference in new issue
Block a user