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 datetime import date
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from openpyxl import load_workbook
|
from openpyxl import load_workbook
|
||||||
from openpyxl.cell.cell import MergedCell
|
from openpyxl.cell.cell import MergedCell
|
||||||
from openpyxl.utils import column_index_from_string
|
|
||||||
|
|
||||||
from .models import EmployeeRecord, OutputMapping
|
from .models import EmployeeRecord, OutputMapping
|
||||||
|
|
||||||
@@ -30,7 +34,6 @@ def _format_value(value, fmt: str | None) -> str:
|
|||||||
return ""
|
return ""
|
||||||
if fmt is None:
|
if fmt is None:
|
||||||
return str(value)
|
return str(value)
|
||||||
# Numerische Formatierung (z.B. "%.2f")
|
|
||||||
if "%" in fmt:
|
if "%" in fmt:
|
||||||
try:
|
try:
|
||||||
return fmt % float(value)
|
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."""
|
Behandelt MergedCells, indem in die Top-Left-Zelle des Merge-Bereichs geschrieben wird."""
|
||||||
try:
|
try:
|
||||||
cell = ws[target_cell]
|
cell = ws[target_cell]
|
||||||
# Wenn es eine MergedCell ist, finde die Top-Left-Zelle des Merge-Bereichs
|
|
||||||
if isinstance(cell, MergedCell):
|
if isinstance(cell, MergedCell):
|
||||||
for merged_range in ws.merged_cells.ranges:
|
for merged_range in ws.merged_cells.ranges:
|
||||||
if cell.coordinate in merged_range:
|
if cell.coordinate in merged_range:
|
||||||
@@ -54,11 +56,11 @@ def _set_cell_value(ws, target_cell: str, value) -> None:
|
|||||||
else:
|
else:
|
||||||
cell.value = value
|
cell.value = value
|
||||||
except (ValueError, KeyError):
|
except (ValueError, KeyError):
|
||||||
pass # Zelle existiert nicht
|
pass
|
||||||
|
|
||||||
|
|
||||||
def _format_month_for_template(monat_str: str) -> str:
|
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()
|
monat_str = monat_str.strip()
|
||||||
parts = monat_str.split(".")
|
parts = monat_str.split(".")
|
||||||
if len(parts) == 2:
|
if len(parts) == 2:
|
||||||
@@ -70,6 +72,42 @@ def _format_month_for_template(monat_str: str) -> str:
|
|||||||
return monat_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(
|
def generate_activity_reports(
|
||||||
employee_records: dict[str, list[EmployeeRecord]],
|
employee_records: dict[str, list[EmployeeRecord]],
|
||||||
template_path: Path,
|
template_path: Path,
|
||||||
@@ -78,13 +116,13 @@ def generate_activity_reports(
|
|||||||
filename_pattern: str,
|
filename_pattern: str,
|
||||||
) -> list[Path]:
|
) -> list[Path]:
|
||||||
"""
|
"""
|
||||||
Generiert für jeden Mitarbeiter eine Excel-Datei.
|
Befüllt vor-duplizierte Template-Sheets mit Mitarbeiterdaten.
|
||||||
Jeder Monat wird als eigenes Sheet im Template-Stil erstellt.
|
Erzeugt KEINE neuen Sheets – verwendet ausschließlich die im Template
|
||||||
|
bereits vorhandenen Sheet-Kopien.
|
||||||
"""
|
"""
|
||||||
output_dir.mkdir(parents=True, exist_ok=True)
|
output_dir.mkdir(parents=True, exist_ok=True)
|
||||||
generated_files: list[Path] = []
|
generated_files: list[Path] = []
|
||||||
|
|
||||||
# Build source_field → mapping lookup
|
|
||||||
field_map: dict[str, OutputMapping] = {m.source_field: m for m in mapping}
|
field_map: dict[str, OutputMapping] = {m.source_field: m for m in mapping}
|
||||||
|
|
||||||
for employee_name, records in employee_records.items():
|
for employee_name, records in employee_records.items():
|
||||||
@@ -95,65 +133,31 @@ def generate_activity_reports(
|
|||||||
safe_first = first_name.replace(" ", "_").replace("/", "_")
|
safe_first = first_name.replace(" ", "_").replace("/", "_")
|
||||||
safe_last = last_name.replace(" ", "_").replace("/", "_")
|
safe_last = last_name.replace(" ", "_").replace("/", "_")
|
||||||
|
|
||||||
filename = filename_pattern.format(
|
filename = filename_pattern.format(first_name=safe_first, last_name=safe_last)
|
||||||
first_name=safe_first,
|
|
||||||
last_name=safe_last,
|
|
||||||
)
|
|
||||||
output_path = output_dir / filename
|
output_path = output_dir / filename
|
||||||
|
|
||||||
# Lade Template (bleibt unverändert als Kopiervorlage)
|
|
||||||
wb = load_workbook(template_path)
|
wb = load_workbook(template_path)
|
||||||
template_sheet = wb.worksheets[0]
|
|
||||||
|
|
||||||
# Sortiere Records nach Monat
|
|
||||||
sorted_records = sorted(records, key=lambda r: r.month_index)
|
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):
|
for idx, record in enumerate(sorted_records):
|
||||||
# Immer vom sauberen Template kopieren – nie das Original beschreiben
|
sheet = available_sheets[idx]
|
||||||
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"
|
|
||||||
]
|
|
||||||
mi = record.month_index
|
mi = record.month_index
|
||||||
func_abbr = record.funktion[:3] if record.funktion else ""
|
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 = f"{mi:02d}_{_month_name(mi)}_{func_abbr}"[:31]
|
||||||
sheet.title = sheet_name[:31]
|
_fill_sheet(sheet, record, employee_name, field_map)
|
||||||
|
|
||||||
# Befülle Zielfelder
|
# Lösche überzählige Sheets (von hinten nach vorne)
|
||||||
for source_field, m in field_map.items():
|
for extra in reversed(available_sheets[total_needed:]):
|
||||||
if source_field == "Monat (MM.JJ)":
|
wb.remove(extra)
|
||||||
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)
|
|
||||||
|
|
||||||
wb.save(output_path)
|
wb.save(output_path)
|
||||||
wb.close()
|
wb.close()
|
||||||
|
|||||||
Binary file not shown.
Reference in new issue
Block a user