Compare commits
4
Commits
1e44aaab9d
...
b05849e673
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b05849e673 | ||
|
|
0d390f79b4 | ||
|
|
f4afd4b2e5 | ||
|
|
31cdc7747f |
No files matched your search
@@ -1,5 +1,7 @@
|
|||||||
"""Tätigkeitsnachweis-Generator – Streamlit App."""
|
"""Tätigkeitsnachweis-Generator – Streamlit App."""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
@@ -54,6 +56,15 @@ if "input_validated" not in st.session_state:
|
|||||||
st.session_state.input_validated = False
|
st.session_state.input_validated = False
|
||||||
if "template_validated" not in st.session_state:
|
if "template_validated" not in st.session_state:
|
||||||
st.session_state.template_validated = False
|
st.session_state.template_validated = False
|
||||||
|
# Caches für Validierung/Parsing – nur bei Dateiwechsel neu ausführen
|
||||||
|
if "last_input_file" not in st.session_state:
|
||||||
|
st.session_state.last_input_file = None
|
||||||
|
if "last_template_file" not in st.session_state:
|
||||||
|
st.session_state.last_template_file = None
|
||||||
|
if "cached_val_result" not in st.session_state:
|
||||||
|
st.session_state.cached_val_result = None
|
||||||
|
if "cached_tpl_result" not in st.session_state:
|
||||||
|
st.session_state.cached_tpl_result = None
|
||||||
|
|
||||||
# --- Layout: zwei Spalten ---
|
# --- Layout: zwei Spalten ---
|
||||||
col1, col2 = st.columns(2)
|
col1, col2 = st.columns(2)
|
||||||
@@ -73,39 +84,67 @@ with col1:
|
|||||||
|
|
||||||
if selected_input:
|
if selected_input:
|
||||||
input_path = Path(app_config.input_dir) / selected_input
|
input_path = Path(app_config.input_dir) / selected_input
|
||||||
# Validiere und parse sofort
|
|
||||||
with st.spinner("Validiere Input-Datei…"):
|
# Nur validieren/parsen wenn sich die Datei geändert hat
|
||||||
val_result = validate_input_file(input_path, app_config.input_schema)
|
if selected_input != st.session_state.last_input_file:
|
||||||
|
with st.spinner("Validiere Input-Datei…"):
|
||||||
|
val_result = validate_input_file(input_path, app_config.input_schema, app_config.month_sheet_pattern)
|
||||||
|
|
||||||
|
if val_result.is_valid:
|
||||||
|
records = parse_input_file(
|
||||||
|
input_path,
|
||||||
|
app_config.input_schema,
|
||||||
|
app_config.month_sheet_pattern,
|
||||||
|
app_config.skip_empty_name,
|
||||||
|
app_config.skip_empty_antragsnummer,
|
||||||
|
)
|
||||||
|
st.session_state.parsed_records = records
|
||||||
|
st.session_state.input_validated = True
|
||||||
|
else:
|
||||||
|
st.session_state.parsed_records = []
|
||||||
|
st.session_state.input_validated = False
|
||||||
|
|
||||||
|
st.session_state.cached_val_result = val_result
|
||||||
|
st.session_state.last_input_file = selected_input
|
||||||
|
else:
|
||||||
|
val_result = st.session_state.cached_val_result
|
||||||
|
|
||||||
if not val_result.is_valid:
|
if not val_result.is_valid:
|
||||||
|
st.markdown(
|
||||||
|
'<div style="border:2px solid #e74c3c; border-radius:8px; padding:12px; '
|
||||||
|
'background:#fdedec; margin-bottom:12px">'
|
||||||
|
'<span style="color:#b03a2e; font-weight:bold; font-size:1.1em">'
|
||||||
|
'❌ Validierung fehlgeschlagen</span></div>',
|
||||||
|
unsafe_allow_html=True,
|
||||||
|
)
|
||||||
for err in val_result.errors:
|
for err in val_result.errors:
|
||||||
st.error(err)
|
st.error(f"🔴 {err}")
|
||||||
st.session_state.input_validated = False
|
st.session_state.input_validated = False
|
||||||
else:
|
else:
|
||||||
for warn in val_result.warnings:
|
if val_result.warnings:
|
||||||
st.warning(warn)
|
st.markdown(
|
||||||
|
'<div style="border:2px solid #f39c12; border-radius:8px; padding:8px 12px; '
|
||||||
|
'background:#fef9e7; margin-bottom:8px">'
|
||||||
|
'<span style="color:#7d6608; font-weight:bold">'
|
||||||
|
'⚠️ Hinweise</span></div>',
|
||||||
|
unsafe_allow_html=True,
|
||||||
|
)
|
||||||
|
for warn in val_result.warnings:
|
||||||
|
st.warning(warn)
|
||||||
|
|
||||||
# Parse die Datei
|
# Zeige Zusammenfassung aus gecachten Records
|
||||||
records = parse_input_file(
|
records = st.session_state.parsed_records
|
||||||
input_path,
|
|
||||||
app_config.input_schema,
|
|
||||||
app_config.month_sheet_pattern,
|
|
||||||
app_config.skip_empty_name,
|
|
||||||
app_config.skip_empty_antragsnummer,
|
|
||||||
)
|
|
||||||
st.session_state.parsed_records = records
|
|
||||||
st.session_state.input_validated = True
|
|
||||||
|
|
||||||
# Zeige Zusammenfassung
|
|
||||||
employees = get_unique_employees(records)
|
employees = get_unique_employees(records)
|
||||||
antragsnummern = get_unique_antragsnummern(records)
|
antragsnummern = get_unique_antragsnummern(records)
|
||||||
|
|
||||||
st.success(f"✅ {len(records)} Einträge in {val_result.info.get('sheets_with_data', 0)} Monats-Sheets")
|
st.success(f"✅ {len(records)} Einträge in {val_result.info.get('sheets_with_data', 0)} Monats-Sheets")
|
||||||
st.metric("Mitarbeiter", len(employees))
|
pills = " ".join(
|
||||||
st.metric("Antragsnummern", len(antragsnummern))
|
f'<span style="display:inline-block; background:#e8f0fe; color:#1a73e8; '
|
||||||
with st.expander("Antragsnummern anzeigen"):
|
f'border-radius:16px; padding:4px 12px; margin:3px; font-size:0.9em; '
|
||||||
for an in antragsnummern:
|
f'white-space:nowrap">{an}</span>'
|
||||||
st.text(f"• {an}")
|
for an in antragsnummern
|
||||||
|
)
|
||||||
|
st.markdown(f"**Antragsnummern:**<br>{pills}", unsafe_allow_html=True)
|
||||||
|
|
||||||
with col2:
|
with col2:
|
||||||
st.subheader("2. Template")
|
st.subheader("2. Template")
|
||||||
@@ -125,13 +164,27 @@ with col2:
|
|||||||
)
|
)
|
||||||
|
|
||||||
if selected_template:
|
if selected_template:
|
||||||
template_path = Path(app_config.template_dir) / selected_template
|
# Nur validieren wenn sich das Template geändert hat
|
||||||
with st.spinner("Validiere Template…"):
|
if selected_template != st.session_state.last_template_file:
|
||||||
tpl_result = validate_template(template_path, app_config.output_mapping)
|
template_path = Path(app_config.template_dir) / selected_template
|
||||||
|
with st.spinner("Validiere Template…"):
|
||||||
|
tpl_result = validate_template(template_path, app_config.output_mapping)
|
||||||
|
st.session_state.cached_tpl_result = tpl_result
|
||||||
|
st.session_state.last_template_file = selected_template
|
||||||
|
st.session_state.template_validated = tpl_result.is_valid
|
||||||
|
else:
|
||||||
|
tpl_result = st.session_state.cached_tpl_result
|
||||||
|
|
||||||
if not tpl_result.is_valid:
|
if not tpl_result.is_valid:
|
||||||
|
st.markdown(
|
||||||
|
'<div style="border:2px solid #e74c3c; border-radius:8px; padding:12px; '
|
||||||
|
'background:#fdedec; margin-bottom:12px">'
|
||||||
|
'<span style="color:#b03a2e; font-weight:bold; font-size:1.1em">'
|
||||||
|
'❌ Validierung fehlgeschlagen</span></div>',
|
||||||
|
unsafe_allow_html=True,
|
||||||
|
)
|
||||||
for err in tpl_result.errors:
|
for err in tpl_result.errors:
|
||||||
st.error(err)
|
st.error(f"🔴 {err}")
|
||||||
st.session_state.template_validated = False
|
st.session_state.template_validated = False
|
||||||
else:
|
else:
|
||||||
st.session_state.template_validated = True
|
st.session_state.template_validated = True
|
||||||
@@ -139,6 +192,13 @@ with col2:
|
|||||||
total = tpl_result.info.get("total_target_cells", 0)
|
total = tpl_result.info.get("total_target_cells", 0)
|
||||||
st.success(f"✅ {found}/{total} Zielfelder gefunden")
|
st.success(f"✅ {found}/{total} Zielfelder gefunden")
|
||||||
if tpl_result.warnings:
|
if tpl_result.warnings:
|
||||||
|
st.markdown(
|
||||||
|
'<div style="border:2px solid #f39c12; border-radius:8px; padding:8px 12px; '
|
||||||
|
'background:#fef9e7; margin-bottom:8px">'
|
||||||
|
'<span style="color:#7d6608; font-weight:bold">'
|
||||||
|
'⚠️ Hinweise</span></div>',
|
||||||
|
unsafe_allow_html=True,
|
||||||
|
)
|
||||||
for warn in tpl_result.warnings:
|
for warn in tpl_result.warnings:
|
||||||
st.warning(warn)
|
st.warning(warn)
|
||||||
|
|
||||||
@@ -147,17 +207,50 @@ st.subheader("3. Mitarbeiter auswählen")
|
|||||||
|
|
||||||
if st.session_state.input_validated and st.session_state.parsed_records:
|
if st.session_state.input_validated and st.session_state.parsed_records:
|
||||||
employees = get_unique_employees(st.session_state.parsed_records)
|
employees = get_unique_employees(st.session_state.parsed_records)
|
||||||
selected_employees = st.multiselect(
|
st.caption(f"{len(employees)} Mitarbeiter gefunden – wähle aus, für wen Tätigkeitsnachweise erstellt werden sollen:")
|
||||||
f"{len(employees)} Mitarbeiter gefunden – wähle aus, für wen Tätigkeitsnachweise erstellt werden sollen:",
|
|
||||||
options=employees,
|
# "Alle" / "Keine" Buttons
|
||||||
default=employees,
|
btn_col1, btn_col2 = st.columns(2)
|
||||||
|
if btn_col1.button("☑ Alle auswählen", use_container_width=True):
|
||||||
|
for emp in employees:
|
||||||
|
st.session_state[f"sel_{emp}"] = True
|
||||||
|
if btn_col2.button("☐ Keine auswählen", use_container_width=True):
|
||||||
|
for emp in employees:
|
||||||
|
st.session_state[f"sel_{emp}"] = False
|
||||||
|
|
||||||
|
# Mitarbeiter-Liste mit Rahmen
|
||||||
|
st.markdown(
|
||||||
|
'<div style="border:1px solid #d5d5d5; border-radius:6px; padding:8px 12px; '
|
||||||
|
'background:#fafafa; max-height:300px; overflow-y:auto">',
|
||||||
|
unsafe_allow_html=True,
|
||||||
)
|
)
|
||||||
|
selected_employees = []
|
||||||
|
for emp in employees:
|
||||||
|
key = f"sel_{emp}"
|
||||||
|
if key not in st.session_state:
|
||||||
|
st.session_state[key] = True
|
||||||
|
if st.checkbox(emp, value=st.session_state[key], key=key):
|
||||||
|
selected_employees.append(emp)
|
||||||
|
st.markdown("</div>", unsafe_allow_html=True)
|
||||||
|
|
||||||
st.caption(f"{len(selected_employees)} von {len(employees)} Mitarbeitern ausgewählt")
|
st.caption(f"{len(selected_employees)} von {len(employees)} Mitarbeitern ausgewählt")
|
||||||
else:
|
else:
|
||||||
selected_employees = []
|
selected_employees = []
|
||||||
if not st.session_state.input_validated:
|
if not st.session_state.input_validated:
|
||||||
st.info("Bitte zuerst eine gültige Input-Datei auswählen.")
|
st.info("Bitte zuerst eine gültige Input-Datei auswählen.")
|
||||||
|
|
||||||
|
# --- Hilfsfunktion zum Öffnen von Dateien/Ordnern ---
|
||||||
|
def open_path(path_str: str):
|
||||||
|
"""Öffnet eine Datei mit dem Standardprogramm oder einen Ordner im Dateimanager.
|
||||||
|
Funktioniert plattformübergreifend: macOS, Windows, Linux."""
|
||||||
|
if sys.platform == "win32":
|
||||||
|
os.startfile(path_str)
|
||||||
|
elif sys.platform == "darwin":
|
||||||
|
subprocess.run(["open", path_str])
|
||||||
|
else:
|
||||||
|
subprocess.run(["xdg-open", path_str])
|
||||||
|
|
||||||
|
|
||||||
# --- Generate Button ---
|
# --- Generate Button ---
|
||||||
st.divider()
|
st.divider()
|
||||||
gen_col1, gen_col2, gen_col3 = st.columns([1, 1, 2])
|
gen_col1, gen_col2, gen_col3 = st.columns([1, 1, 2])
|
||||||
@@ -188,8 +281,33 @@ if generate_clicked:
|
|||||||
output_dir=output_dir,
|
output_dir=output_dir,
|
||||||
filename_pattern=app_config.filename_pattern,
|
filename_pattern=app_config.filename_pattern,
|
||||||
)
|
)
|
||||||
st.success(f"✅ {len(generated)} Tätigkeitsnachweise erstellt:")
|
st.session_state.generated_files = generated
|
||||||
for fpath in generated:
|
|
||||||
st.text(f"📄 {fpath.name}")
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
st.error(f"Fehler bei der Generierung: {e}")
|
st.error(f"Fehler bei der Generierung: {e}")
|
||||||
|
|
||||||
|
# --- Generierte Dateien anzeigen (aus Session State, übersteht Reruns) ---
|
||||||
|
if "generated_files" in st.session_state and st.session_state.generated_files:
|
||||||
|
generated = st.session_state.generated_files
|
||||||
|
st.success(f"✅ {len(generated)} Tätigkeitsnachweise erstellt:")
|
||||||
|
for i, fpath in enumerate(generated):
|
||||||
|
col_file, col_open, col_folder = st.columns([0.78, 0.11, 0.11])
|
||||||
|
with col_file:
|
||||||
|
st.markdown(f"📄 `{fpath.name}`")
|
||||||
|
with col_open:
|
||||||
|
st.button(
|
||||||
|
"🔗 Öffnen",
|
||||||
|
key=f"open_file_{i}",
|
||||||
|
on_click=open_path,
|
||||||
|
args=(str(fpath),),
|
||||||
|
help="Datei mit Standardprogramm öffnen",
|
||||||
|
use_container_width=True,
|
||||||
|
)
|
||||||
|
with col_folder:
|
||||||
|
st.button(
|
||||||
|
"📂",
|
||||||
|
key=f"open_folder_{i}",
|
||||||
|
on_click=open_path,
|
||||||
|
args=(str(fpath.parent),),
|
||||||
|
help="Ordner im Finder öffnen",
|
||||||
|
use_container_width=True,
|
||||||
|
)
|
||||||
@@ -1,11 +1,12 @@
|
|||||||
[
|
[
|
||||||
{"source_field": "Antragsnummer", "target_cell": "B3", "format": null},
|
{"source_field": "Antragsnummer", "target_cell": "B3", "format": null},
|
||||||
{"source_field": "Name", "target_cell": "B17", "format": null},
|
{"source_field": "Name", "target_cell": "B17", "format": null},
|
||||||
{"source_field": "Stellenanteil in %", "target_cell": "R17", "format": "%.2f"},
|
{"source_field": "Stellenanteil in %", "target_cell": "R17", "format": null},
|
||||||
{"source_field": "Qualifikation", "target_cell": "B21", "format": null},
|
{"source_field": "Qualifikation", "target_cell": "B21", "format": null},
|
||||||
{"source_field": "Monat (MM.JJ)", "target_cell": "R21", "format": null},
|
{"source_field": "Monat (MM.JJ)", "target_cell": "R21", "format": null},
|
||||||
{"source_field": "Funktion", "target_cell": "B25", "format": null},
|
{"source_field": "Funktion", "target_cell": "B25", "format": null},
|
||||||
{"source_field": "Tätigkeit", "target_cell": "B31", "format": null},
|
{"source_field": "Tätigkeit", "target_cell": "B31", "format": null},
|
||||||
{"source_field": "Ort", "target_cell": "B95", "format": null},
|
{"source_field": "Ort", "target_cell": "B95", "format": null},
|
||||||
|
{"source_field": "Ort 2", "target_cell": "B103", "format": null},
|
||||||
{"source_field": "Datum", "target_cell": "J95", "format": null}
|
{"source_field": "Datum", "target_cell": "J95", "format": null}
|
||||||
]
|
]
|
||||||
+1
-1
@@ -11,7 +11,7 @@ class EmployeeRecord:
|
|||||||
antragsnummer: str
|
antragsnummer: str
|
||||||
qualifikation: str
|
qualifikation: str
|
||||||
funktion: str
|
funktion: str
|
||||||
stellenanteil: float
|
stellenanteil: str
|
||||||
monat: str # z. B. "01.2025"
|
monat: str # z. B. "01.2025"
|
||||||
taetigkeit: str
|
taetigkeit: str
|
||||||
id: Optional[int] = None
|
id: Optional[int] = None
|
||||||
|
|||||||
+28
-12
@@ -17,16 +17,6 @@ def _to_text(value: Any) -> str:
|
|||||||
return str(value).strip()
|
return str(value).strip()
|
||||||
|
|
||||||
|
|
||||||
def _to_float(value: Any, field_name: str) -> float:
|
|
||||||
if value is None or str(value).strip() == "":
|
|
||||||
return 0.0
|
|
||||||
text = str(value).strip().replace(",", ".")
|
|
||||||
try:
|
|
||||||
return float(text)
|
|
||||||
except ValueError:
|
|
||||||
return 0.0
|
|
||||||
|
|
||||||
|
|
||||||
def _to_int_or_none(value: Any) -> Optional[int]:
|
def _to_int_or_none(value: Any) -> Optional[int]:
|
||||||
if value is None or str(value).strip() == "":
|
if value is None or str(value).strip() == "":
|
||||||
return None
|
return None
|
||||||
@@ -36,6 +26,29 @@ def _to_int_or_none(value: Any) -> Optional[int]:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _to_percent_string(value: Any) -> str:
|
||||||
|
"""Wandelt einen Prozentwert in einen String ohne %-Zeichen um.
|
||||||
|
|
||||||
|
Excel speichert Prozentwerte als Dezimalbruch (50 % = 0.5).
|
||||||
|
Falls der Rohwert eine Zahl ≤ 1 ist, wird er als Excel-Prozentwert
|
||||||
|
interpretiert und mit 100 multipliziert.
|
||||||
|
"""
|
||||||
|
if value is None or str(value).strip() == "":
|
||||||
|
return ""
|
||||||
|
# Falls es bereits ein String mit %-Zeichen ist
|
||||||
|
if isinstance(value, str):
|
||||||
|
return value.strip().rstrip("% ")
|
||||||
|
# Numerischer Wert: Excel-Prozentwert (0.5 = 50 %) oder Ganzzahl (50 = 50 %)
|
||||||
|
if isinstance(value, (int, float)):
|
||||||
|
if 0 < value <= 1:
|
||||||
|
return str(int(value * 100))
|
||||||
|
if value == 0:
|
||||||
|
return "0"
|
||||||
|
# Wert > 1: bereits als Ganzzahl gemeint (z. B. 50)
|
||||||
|
return str(int(value))
|
||||||
|
return str(value).strip()
|
||||||
|
|
||||||
|
|
||||||
def _build_column_map(schema: list[FieldSchema]) -> dict[str, int]:
|
def _build_column_map(schema: list[FieldSchema]) -> dict[str, int]:
|
||||||
"""Baut ein Mapping von field_name → 0-basierter Spaltenindex."""
|
"""Baut ein Mapping von field_name → 0-basierter Spaltenindex."""
|
||||||
col_map: dict[str, int] = {}
|
col_map: dict[str, int] = {}
|
||||||
@@ -65,7 +78,9 @@ def parse_input_file(
|
|||||||
|
|
||||||
ws = wb[sheet_name]
|
ws = wb[sheet_name]
|
||||||
# Header ist Zeile 1, Daten ab Zeile 2
|
# Header ist Zeile 1, Daten ab Zeile 2
|
||||||
for row in ws.iter_rows(min_row=2, max_row=ws.max_row, values_only=True):
|
# min_col=1 explizit setzen für korrekte Spaltenzuordnung
|
||||||
|
schema_col_count = len(schema)
|
||||||
|
for row in ws.iter_rows(min_row=2, max_row=ws.max_row, min_col=1, max_col=schema_col_count, values_only=True):
|
||||||
name = _to_text(row[col_map.get("Name", 0)]) if len(row) > col_map.get("Name", 0) else ""
|
name = _to_text(row[col_map.get("Name", 0)]) if len(row) > col_map.get("Name", 0) else ""
|
||||||
antragsnummer = _to_text(row[col_map.get("Antragsnummer", 0)]) if len(row) > col_map.get("Antragsnummer", 0) else ""
|
antragsnummer = _to_text(row[col_map.get("Antragsnummer", 0)]) if len(row) > col_map.get("Antragsnummer", 0) else ""
|
||||||
|
|
||||||
@@ -76,7 +91,8 @@ def parse_input_file(
|
|||||||
|
|
||||||
qualifikation = _to_text(row[col_map.get("Qualifikation", 0)]) if len(row) > col_map.get("Qualifikation", 0) else ""
|
qualifikation = _to_text(row[col_map.get("Qualifikation", 0)]) if len(row) > col_map.get("Qualifikation", 0) else ""
|
||||||
funktion = _to_text(row[col_map.get("Funktion", 0)]) if len(row) > col_map.get("Funktion", 0) else ""
|
funktion = _to_text(row[col_map.get("Funktion", 0)]) if len(row) > col_map.get("Funktion", 0) else ""
|
||||||
stellenanteil = _to_float(row[col_map.get("Stellenanteil in %", 0)], "Stellenanteil in %") if len(row) > col_map.get("Stellenanteil in %", 0) else 0.0
|
stellenanteil_raw = row[col_map.get("Stellenanteil in %", 0)] if len(row) > col_map.get("Stellenanteil in %", 0) else None
|
||||||
|
stellenanteil = _to_percent_string(stellenanteil_raw)
|
||||||
monat = _to_text(row[col_map.get("Monat (MM.JJ)", 0)]) if len(row) > col_map.get("Monat (MM.JJ)", 0) else ""
|
monat = _to_text(row[col_map.get("Monat (MM.JJ)", 0)]) if len(row) > col_map.get("Monat (MM.JJ)", 0) else ""
|
||||||
taetigkeit = _to_text(row[col_map.get("Tätigkeit", 0)]) if len(row) > col_map.get("Tätigkeit", 0) else ""
|
taetigkeit = _to_text(row[col_map.get("Tätigkeit", 0)]) if len(row) > col_map.get("Tätigkeit", 0) else ""
|
||||||
record_id = _to_int_or_none(row[col_map.get("ID", 0)]) if len(row) > col_map.get("ID", 0) else None
|
record_id = _to_int_or_none(row[col_map.get("ID", 0)]) if len(row) > col_map.get("ID", 0) else None
|
||||||
|
|||||||
+15
-3
@@ -6,18 +6,21 @@ from typing import Optional
|
|||||||
from openpyxl import load_workbook
|
from openpyxl import load_workbook
|
||||||
from openpyxl.utils import column_index_from_string
|
from openpyxl.utils import column_index_from_string
|
||||||
|
|
||||||
|
from .config import is_month_sheet
|
||||||
from .models import FieldSchema, OutputMapping, ValidationResult
|
from .models import FieldSchema, OutputMapping, ValidationResult
|
||||||
|
|
||||||
|
|
||||||
def validate_input_file(
|
def validate_input_file(
|
||||||
filepath: Path,
|
filepath: Path,
|
||||||
schema: list[FieldSchema],
|
schema: list[FieldSchema],
|
||||||
|
month_sheet_pattern: str = "* 20??",
|
||||||
) -> ValidationResult:
|
) -> ValidationResult:
|
||||||
"""
|
"""
|
||||||
Validiert eine Input-Datei:
|
Validiert eine Input-Datei:
|
||||||
- Datei existiert
|
- Datei existiert
|
||||||
- Erwartete Spalten sind vorhanden (Header in Zeile 1)
|
- Erwartete Spalten sind vorhanden (Header in Zeile 1)
|
||||||
- Mindestens ein Daten-Sheet enthält Daten
|
- Mindestens ein Monats-Daten-Sheet enthält Daten
|
||||||
|
- Nicht-Monats-Sheets (z. B. "Alle Rollen") werden übersprungen
|
||||||
"""
|
"""
|
||||||
errors: list[str] = []
|
errors: list[str] = []
|
||||||
warnings: list[str] = []
|
warnings: list[str] = []
|
||||||
@@ -37,14 +40,23 @@ def validate_input_file(
|
|||||||
info["sheet_count"] = sheet_count
|
info["sheet_count"] = sheet_count
|
||||||
info["sheet_names"] = wb.sheetnames
|
info["sheet_names"] = wb.sheetnames
|
||||||
|
|
||||||
# Prüfe, ob mindestens ein Sheet die erwarteten Spalten hat
|
# Prüfe nur Monats-Sheets (überspringe z. B. "Alle Rollen")
|
||||||
|
schema_col_count = len(schema)
|
||||||
sheets_with_data = 0
|
sheets_with_data = 0
|
||||||
for sheet_name in wb.sheetnames:
|
for sheet_name in wb.sheetnames:
|
||||||
|
if not is_month_sheet(sheet_name, month_sheet_pattern):
|
||||||
|
continue
|
||||||
|
|
||||||
ws = wb[sheet_name]
|
ws = wb[sheet_name]
|
||||||
if ws.max_row < 2:
|
if ws.max_row < 2:
|
||||||
continue # Kein Header + Daten
|
continue # Kein Header + Daten
|
||||||
|
|
||||||
header_row = list(ws.iter_rows(min_row=1, max_row=1, values_only=True))[0]
|
# min_col=1 explizit setzen, um Verschiebungen bei
|
||||||
|
# ws.min_column > 1 zu vermeiden
|
||||||
|
header_row = list(ws.iter_rows(
|
||||||
|
min_row=1, max_row=1, min_col=1, max_col=schema_col_count,
|
||||||
|
values_only=True,
|
||||||
|
))[0]
|
||||||
missing_cols = []
|
missing_cols = []
|
||||||
for field in schema:
|
for field in schema:
|
||||||
col_idx = column_index_from_string(field.column) - 1
|
col_idx = column_index_from_string(field.column) - 1
|
||||||
|
|||||||
+10
-6
@@ -5,7 +5,7 @@ Es werden KEINE Sheets programmatisch erzeugt oder kopiert –
|
|||||||
ausschließlich die vorhandenen Sheets mit Daten befüllt.
|
ausschließlich die vorhandenen Sheets mit Daten befüllt.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from datetime import date
|
import calendar
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from openpyxl import load_workbook
|
from openpyxl import load_workbook
|
||||||
@@ -95,18 +95,22 @@ def _fill_sheet(ws, record: EmployeeRecord, employee_name: str, field_map: dict)
|
|||||||
value = record.funktion
|
value = record.funktion
|
||||||
elif source_field == "Tätigkeit":
|
elif source_field == "Tätigkeit":
|
||||||
value = record.taetigkeit
|
value = record.taetigkeit
|
||||||
elif source_field == "Ort":
|
elif source_field in ("Ort", "Ort 2"):
|
||||||
value = "Dresden"
|
value = "Dresden"
|
||||||
elif source_field == "Datum":
|
elif source_field == "Datum":
|
||||||
value = date.today().strftime("%d.%m.%Y")
|
parts = record.monat.split(".")
|
||||||
|
if len(parts) == 2:
|
||||||
|
month = int(parts[0])
|
||||||
|
year = int(parts[1])
|
||||||
|
last_day = calendar.monthrange(year, month)[1]
|
||||||
|
value = f"{last_day:02d}.{month:02d}.{year}"
|
||||||
|
else:
|
||||||
|
value = ""
|
||||||
else:
|
else:
|
||||||
continue
|
continue
|
||||||
formatted = _format_value(value, m.format)
|
formatted = _format_value(value, m.format)
|
||||||
_set_cell_value(ws, m.target_cell, formatted)
|
_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]],
|
||||||
|
|||||||
Reference in new issue
Block a user