Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3e9cd39859 | ||
|
|
9bdd90c073 | ||
|
|
e50f423304 | ||
|
|
ef219e00ba | ||
|
|
30f3ffaded |
No files matched your search
+46
@@ -0,0 +1,46 @@
|
|||||||
|
@echo off
|
||||||
|
title Tätigkeitsnachweis-Generator – Cold Run
|
||||||
|
echo ========================================
|
||||||
|
echo Cold Run – Headless-Generierung
|
||||||
|
echo ========================================
|
||||||
|
echo.
|
||||||
|
|
||||||
|
:: Prüfe ob Python verfügbar ist
|
||||||
|
where python >nul 2>nul
|
||||||
|
if %ERRORLEVEL% neq 0 (
|
||||||
|
echo [FEHLER] Python wurde nicht gefunden.
|
||||||
|
echo Bitte installiere Python 3.9 oder neuer von https://python.org
|
||||||
|
pause
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
echo [INFO] Python gefunden:
|
||||||
|
python --version
|
||||||
|
|
||||||
|
:: Prüfe ob venv existiert, sonst erstelle
|
||||||
|
if not exist ".venv\Scripts\python.exe" (
|
||||||
|
echo [FEHLER] Virtuelle Umgebung nicht gefunden.
|
||||||
|
echo Bitte zuerst start.bat ausführen, um das Projekt einzurichten.
|
||||||
|
pause
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
:: Aktiviere venv und installiere Abhängigkeiten
|
||||||
|
call .venv\Scripts\activate.bat
|
||||||
|
echo [INFO] Stelle sicher, dass Abhängigkeiten installiert sind...
|
||||||
|
pip install -r requirements.txt --quiet
|
||||||
|
if %ERRORLEVEL% neq 0 (
|
||||||
|
echo [FEHLER] Konnte Abhängigkeiten nicht installieren.
|
||||||
|
pause
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
:: Starte Cold Run
|
||||||
|
echo.
|
||||||
|
echo [INFO] Starte Cold Run – generiere Tätigkeitsnachweis für erste Person...
|
||||||
|
echo.
|
||||||
|
python coldrun.py
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo [INFO] Cold Run abgeschlossen.
|
||||||
|
pause
|
||||||
+13
-1
@@ -22,7 +22,7 @@ from src.config import (
|
|||||||
get_available_templates,
|
get_available_templates,
|
||||||
)
|
)
|
||||||
from src.parser import parse_input_file, group_by_employee, get_unique_employees
|
from src.parser import parse_input_file, group_by_employee, get_unique_employees
|
||||||
from src.validator import validate_template
|
from src.validator import validate_input_file, validate_template
|
||||||
from src.writer import generate_activity_reports
|
from src.writer import generate_activity_reports
|
||||||
|
|
||||||
|
|
||||||
@@ -70,6 +70,18 @@ def main():
|
|||||||
total = tpl_result.info.get("total_fields", "?")
|
total = tpl_result.info.get("total_fields", "?")
|
||||||
print(f"[OK] {found}/{total} Platzhalter im Template gefunden.")
|
print(f"[OK] {found}/{total} Platzhalter im Template gefunden.")
|
||||||
|
|
||||||
|
# ── Input validieren ──────────────────────────────────────────────
|
||||||
|
print()
|
||||||
|
print("[INFO] Validiere Input-Datei…")
|
||||||
|
inp_result = validate_input_file(input_path, config.input_schema, config.month_sheet_pattern)
|
||||||
|
if not inp_result.is_valid:
|
||||||
|
for err in inp_result.errors:
|
||||||
|
print(f"[FEHLER] {err}")
|
||||||
|
return 1
|
||||||
|
if inp_result.warnings:
|
||||||
|
for warn in inp_result.warnings:
|
||||||
|
print(f"[WARN] {warn}")
|
||||||
|
|
||||||
# ── Input parsen ──────────────────────────────────────────────────
|
# ── Input parsen ──────────────────────────────────────────────────
|
||||||
print()
|
print()
|
||||||
print("[INFO] Parse Input-Datei…")
|
print("[INFO] Parse Input-Datei…")
|
||||||
|
|||||||
@@ -1,13 +1,10 @@
|
|||||||
[
|
[
|
||||||
{"source_field": "Antragsnummer", "pdf_field": "Auftragsnummer_Field", "format": null},
|
{"source_field": "Antragsnummer", "pdf_field": "Antragsnummer_Field", "format": null},
|
||||||
{"source_field": "Name", "pdf_field": "Name_Field", "format": null},
|
{"source_field": "Name", "pdf_field": "Name_Field", "format": null},
|
||||||
{"source_field": "Stellenanteil in %", "pdf_field": "Stellenanteil_Field", "format": null},
|
{"source_field": "Stellenanteil in %", "pdf_field": "Stellenanteil_Field", "format": null},
|
||||||
{"source_field": "Qualifikation", "pdf_field": "Qualifikation_Field", "format": null},
|
{"source_field": "Qualifikation", "pdf_field": "Qualifikation_Field", "format": null},
|
||||||
{"source_field": "Monat (MM.JJ)", "pdf_field": "Monat_Field", "format": null},
|
{"source_field": "Monat (MM.JJ)", "pdf_field": "Monat_Field", "format": null},
|
||||||
{"source_field": "Funktion", "pdf_field": "Funktion_Field", "format": null},
|
{"source_field": "Funktion", "pdf_field": "Funktion_Field", "format": null},
|
||||||
{"source_field": "Tätigkeit", "pdf_field": "Tätigkeiten_Field", "format": null},
|
{"source_field": "Tätigkeit", "pdf_field": "Tätigkeiten_Field", "format": null},
|
||||||
{"source_field": "Tätigkeit", "pdf_field": "Tätigkeiten-inkl-Reisen_Field","format": null},
|
|
||||||
{"source_field": "Ort", "pdf_field": "Ort_Field", "format": null},
|
|
||||||
{"source_field": "Ort 2", "pdf_field": "Ort2_Field", "format": null},
|
|
||||||
{"source_field": "Datum", "pdf_field": "Datum_Field", "format": null}
|
{"source_field": "Datum", "pdf_field": "Datum_Field", "format": null}
|
||||||
]
|
]
|
||||||
@@ -81,6 +81,49 @@ def validate_input_file(
|
|||||||
info["sheets_with_data"] = sheets_with_data
|
info["sheets_with_data"] = sheets_with_data
|
||||||
wb.close()
|
wb.close()
|
||||||
|
|
||||||
|
# ── Dateninhalts-Prüfung: leere Pflichtfelder ───────────────────
|
||||||
|
required_fields = [f for f in schema if f.required]
|
||||||
|
if required_fields and sheets_with_data > 0:
|
||||||
|
# Mapping field_name → column index
|
||||||
|
field_col_map: dict[str, int] = {}
|
||||||
|
for f in required_fields:
|
||||||
|
field_col_map[f.field_name] = column_index_from_string(f.column) - 1
|
||||||
|
max_col = max(field_col_map.values()) + 1
|
||||||
|
|
||||||
|
# {field_name: {name1, name2, ...}} für leere Pflichtwerte
|
||||||
|
empty_by_field: dict[str, set[str]] = {f.field_name: set() for f in required_fields}
|
||||||
|
|
||||||
|
wb2 = load_workbook(filepath, data_only=True)
|
||||||
|
for sheet_name in wb2.sheetnames:
|
||||||
|
if not is_month_sheet(sheet_name, month_sheet_pattern):
|
||||||
|
continue
|
||||||
|
ws = wb2[sheet_name]
|
||||||
|
if ws.max_row < 2:
|
||||||
|
continue
|
||||||
|
for row in ws.iter_rows(min_row=2, max_row=ws.max_row, min_col=1, max_col=max_col, values_only=True):
|
||||||
|
name_val = str(row[field_col_map.get("Name", 0)] or "").strip() if len(row) > field_col_map.get("Name", 0) else ""
|
||||||
|
if not name_val:
|
||||||
|
continue
|
||||||
|
for f in required_fields:
|
||||||
|
col_idx = field_col_map[f.field_name]
|
||||||
|
if col_idx < len(row):
|
||||||
|
cell_val = str(row[col_idx] or "").strip() if row[col_idx] is not None else ""
|
||||||
|
if not cell_val:
|
||||||
|
empty_by_field[f.field_name].add(name_val)
|
||||||
|
wb2.close()
|
||||||
|
|
||||||
|
for field_name, names in empty_by_field.items():
|
||||||
|
if names:
|
||||||
|
sorted_names = sorted(names)
|
||||||
|
total = len(sorted_names)
|
||||||
|
preview = sorted_names[:5]
|
||||||
|
suffix = " …" if total > 5 else ""
|
||||||
|
warnings.append(
|
||||||
|
f"Feld '{field_name}' ist leer bei {total} Person(en): "
|
||||||
|
f"{', '.join(preview)}{suffix}"
|
||||||
|
)
|
||||||
|
# ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
if sheets_with_data == 0:
|
if sheets_with_data == 0:
|
||||||
errors.append("Kein Sheet mit den erwarteten Spalten gefunden.")
|
errors.append("Kein Sheet mit den erwarteten Spalten gefunden.")
|
||||||
|
|
||||||
|
|||||||
+35
-3
@@ -169,6 +169,12 @@ def _fill_pdf_pages(
|
|||||||
field_info: dict[str, dict],
|
field_info: dict[str, dict],
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Befüllt alle Template-Seiten: redacted _Field-Platzhalter und fügt echte Daten ein."""
|
"""Befüllt alle Template-Seiten: redacted _Field-Platzhalter und fügt echte Daten ein."""
|
||||||
|
# Mehrzeilige Felder, die als Textbox (mit Zeilenumbruch) behandelt werden
|
||||||
|
MULTILINE_FIELDS = {"Tätigkeiten_Field", "Dienstreisen_Field"}
|
||||||
|
TEXTBOX_FONT_SCALE = 1 # Schriftgrößen-Faktor für Textboxen
|
||||||
|
TEXTBOX_WIDTH_PT = 400 # Breite der Textbox in Punkt
|
||||||
|
TEXTBOX_HEIGHT_PT = 120 # Höhe der Textbox in Punkt
|
||||||
|
|
||||||
for m in mapping:
|
for m in mapping:
|
||||||
if m.pdf_field is None:
|
if m.pdf_field is None:
|
||||||
continue
|
continue
|
||||||
@@ -188,12 +194,38 @@ def _fill_pdf_pages(
|
|||||||
value = _get_field_value(record, employee_name, m.source_field)
|
value = _get_field_value(record, employee_name, m.source_field)
|
||||||
formatted = _format_value(value, m.format)
|
formatted = _format_value(value, m.format)
|
||||||
|
|
||||||
# Platzhalter redacten
|
# Platzhalter immer redacten, auch wenn kein Wert zum Einsetzen da ist
|
||||||
page.add_redact_annot(rect)
|
page.add_redact_annot(rect)
|
||||||
page.apply_redactions()
|
page.apply_redactions()
|
||||||
|
|
||||||
# Neuen Text einfügen
|
if not formatted:
|
||||||
if formatted:
|
continue
|
||||||
|
|
||||||
|
# Mehrzeilige Felder: insert_textbox mit Zeilenumbruch
|
||||||
|
if m.pdf_field in MULTILINE_FIELDS:
|
||||||
|
# Doppelte Leerzeilen entfernen
|
||||||
|
clean_text = "\n".join(
|
||||||
|
line for line in formatted.split("\n") if line.strip()
|
||||||
|
)
|
||||||
|
if not clean_text:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Rechteck für die Textbox: ausgehend vom Marker, nach rechts & unten erweitert
|
||||||
|
textbox_rect = fitz.Rect(
|
||||||
|
rect.x0, # links
|
||||||
|
rect.y0, # oben
|
||||||
|
rect.x0 + TEXTBOX_WIDTH_PT, # rechts
|
||||||
|
rect.y0 + TEXTBOX_HEIGHT_PT, # unten
|
||||||
|
)
|
||||||
|
page.insert_textbox(
|
||||||
|
textbox_rect,
|
||||||
|
clean_text,
|
||||||
|
fontname="helv",
|
||||||
|
fontsize=fontsize * TEXTBOX_FONT_SCALE,
|
||||||
|
lineheight=1.5,
|
||||||
|
align=fitz.TEXT_ALIGN_LEFT,
|
||||||
|
)
|
||||||
|
else:
|
||||||
page.insert_text(
|
page.insert_text(
|
||||||
rect.bl,
|
rect.bl,
|
||||||
formatted,
|
formatted,
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Reference in new issue
Block a user