Compare commits
3
Commits
0d5620ae89
..
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0163f21e80 | ||
|
|
2eef2c1052 | ||
|
|
7d82aeb0f5 |
No files matched your search
@@ -95,4 +95,8 @@ Installer-EXE erstellen, die:
|
|||||||
- `{autopf}\AnwesenheitPDF\` (= Program Files, UAC-geschützt)
|
- `{autopf}\AnwesenheitPDF\` (= Program Files, UAC-geschützt)
|
||||||
- Requires admin (PrivilegesRequired=admin)
|
- Requires admin (PrivilegesRequired=admin)
|
||||||
- Startmenü: `{group}\AnwesenheitPDF`
|
- Startmenü: `{group}\AnwesenheitPDF`
|
||||||
- Optional Desktop-Verknüpfung (Checkbox im Wizard)
|
|
||||||
|
## Release Notes
|
||||||
|
- Aktuelle Versionsnummer aus config.json bestaetigen.
|
||||||
|
- Release-Notes-Dokument im Projekt-Root anlegen.
|
||||||
|
- Release-Notes-Datei zusaetzlich zum Installer in `update_check_path` kopieren.
|
||||||
@@ -203,6 +203,19 @@ Die deutschen Monatsabkürzungen entsprechen den Tabellenblatt-Namen in der Exce
|
|||||||
| `Nov` | November |
|
| `Nov` | November |
|
||||||
| `Dez` | Dezember |
|
| `Dez` | Dezember |
|
||||||
|
|
||||||
|
### Sonderfall `P = Praktikum`
|
||||||
|
|
||||||
|
Wenn im Excel-Sheet für einen Tag `P` steht, bleibt der Status im PDF `P`, aber die Stundenfelder werden mit 6 Stunden befüllt:
|
||||||
|
|
||||||
|
| Feld | Wert |
|
||||||
|
|---|---|
|
||||||
|
| `anwesend.N` | `P` |
|
||||||
|
| `ist.N` | `6,00` |
|
||||||
|
| `plan.N` | `6` |
|
||||||
|
| `pauschale_5.N` | `1` |
|
||||||
|
|
||||||
|
`P = Praktikum` wird damit wie ein regulärer Anwesenheitstag mit 6 Stunden abgerechnet und zählt auch für die 5-Euro-Pauschale.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Ausgabe
|
## Ausgabe
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
# Release Notes
|
||||||
|
|
||||||
|
## Version 1.0.17 (2026-05-21)
|
||||||
|
|
||||||
|
### Zusammenfassung
|
||||||
|
- Lokale Excel-Datei waehlbar ueber Dropdown (input/).
|
||||||
|
- Neuer Update-Button neben dem Dropdown (nur bei neuerer Quelldatei sichtbar).
|
||||||
|
- Quelle nutzt nun Netzordner mit mehreren Excel-Dateien.
|
||||||
|
- Dateiauswahl aus Quelle automatisch: aktuelles Jahr bevorzugt, sonst neuestes Jahr.
|
||||||
|
- Pruefung auf Updates beim Start und danach alle 60 Sekunden.
|
||||||
|
- Bei fehlender Verbindung: Warnsymbol in GUI.
|
||||||
|
- Beim Update: Ueberschreib-Hinweis mit Abbruchoption; nach Bestaetigung Download nach input/ und automatische Auswahl.
|
||||||
|
- Bugfix Praktikum: Tage mit Status `P` werden in der PDF jetzt wie ein 6h-Anwesenheitstag behandelt, inklusive `mind. 6 h` und Pauschale `8,60`.
|
||||||
@@ -111,9 +111,9 @@ echo.
|
|||||||
echo [OK] Installer erstellt: dist\AnwesenheitPDF-Setup-%APP_VERSION%.exe
|
echo [OK] Installer erstellt: dist\AnwesenheitPDF-Setup-%APP_VERSION%.exe
|
||||||
|
|
||||||
echo.
|
echo.
|
||||||
echo [5/5] Installer zu update_check_path deployen...
|
echo [5/5] Installer und Release Notes zu update_check_path deployen...
|
||||||
|
|
||||||
powershell -NoProfile -ExecutionPolicy Bypass -Command "$ErrorActionPreference = 'Stop'; $config = Get-Content -Raw -Encoding UTF8 'config.json' | ConvertFrom-Json; $updatePath = $config.update_check_path; if ([string]::IsNullOrWhiteSpace($updatePath)) { Write-Host '[WARNUNG] update_check_path nicht in config.json gefunden. Deployment uebersprungen.'; exit 0 }; $installer = Join-Path (Get-Location) 'dist\AnwesenheitPDF-Setup-%APP_VERSION%.exe'; if (-not (Test-Path -LiteralPath $installer)) { throw ('Installer nicht gefunden: ' + $installer) }; while (-not (Test-Path -LiteralPath $updatePath)) { Write-Host ''; Write-Host ' +----------------------------------------------------------+'; Write-Host ' | [!] Deployment-Pfad nicht erreichbar |'; Write-Host ' +----------------------------------------------------------+'; Write-Host ( ' | Pfad : {0}' -f $updatePath ); Write-Host ' | |'; Write-Host ' | Bitte Netzlaufwerk / Zielpfad verbinden und dann ENTER |'; Write-Host ' | druecken. |'; Write-Host ' +----------------------------------------------------------+'; [void](Read-Host 'Eingabe'); }; Write-Host ('[OK] Deployment-Pfad erreichbar: {0}' -f $updatePath); Copy-Item -LiteralPath $installer -Destination $updatePath -Force; Write-Host ('[OK] Installer deployed zu: {0}' -f (Join-Path $updatePath (Split-Path $installer -Leaf)))"
|
powershell -NoProfile -ExecutionPolicy Bypass -Command "$ErrorActionPreference = 'Stop'; $config = Get-Content -Raw -Encoding UTF8 'config.json' | ConvertFrom-Json; $updatePath = $config.update_check_path; if ([string]::IsNullOrWhiteSpace($updatePath)) { Write-Host '[WARNUNG] update_check_path nicht in config.json gefunden. Deployment uebersprungen.'; exit 0 }; $installer = Join-Path (Get-Location) 'dist\AnwesenheitPDF-Setup-%APP_VERSION%.exe'; $releaseNotes = Join-Path (Get-Location) 'RELEASE_NOTES.md'; if (-not (Test-Path -LiteralPath $installer)) { throw ('Installer nicht gefunden: ' + $installer) }; while (-not (Test-Path -LiteralPath $updatePath)) { Write-Host ''; Write-Host ' +----------------------------------------------------------+'; Write-Host ' | [!] Deployment-Pfad nicht erreichbar |'; Write-Host ' +----------------------------------------------------------+'; Write-Host ( ' | Pfad : {0}' -f $updatePath ); Write-Host ' | |'; Write-Host ' | Bitte Netzlaufwerk / Zielpfad verbinden und dann ENTER |'; Write-Host ' | druecken. |'; Write-Host ' +----------------------------------------------------------+'; [void](Read-Host 'Eingabe'); }; Write-Host ('[OK] Deployment-Pfad erreichbar: {0}' -f $updatePath); Copy-Item -LiteralPath $installer -Destination $updatePath -Force; Write-Host ('[OK] Installer deployed zu: {0}' -f (Join-Path $updatePath (Split-Path $installer -Leaf))); if (Test-Path -LiteralPath $releaseNotes) { Copy-Item -LiteralPath $releaseNotes -Destination $updatePath -Force; Write-Host ('[OK] Release Notes deployed zu: {0}' -f (Join-Path $updatePath (Split-Path $releaseNotes -Leaf))) } else { Write-Host ('[WARNUNG] Release Notes nicht gefunden: {0}' -f $releaseNotes) }"
|
||||||
|
|
||||||
if %ERRORLEVEL% neq 0 (
|
if %ERRORLEVEL% neq 0 (
|
||||||
echo.
|
echo.
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"version": "1.0.15+d07b483",
|
"version": "1.0.17+0d5620a",
|
||||||
"antragsnummer": "100740439",
|
"antragsnummer": "100740439",
|
||||||
"ort": "Dresden",
|
"ort": "Dresden",
|
||||||
"template_pdf": "template/sab.pdf",
|
"template_pdf": "template/sab.pdf",
|
||||||
|
|||||||
@@ -107,6 +107,11 @@ def _count_output_pdfs(config: Dict[str, Any]) -> int:
|
|||||||
|
|
||||||
_UPDATE_CHECK_FILE = _runtime_dir() / "update_check.json"
|
_UPDATE_CHECK_FILE = _runtime_dir() / "update_check.json"
|
||||||
_INSTALLER_PATTERN = re.compile(r"^AnwesenheitPDF-Setup-(\d+\.\d+\.\d+)\.exe$", re.IGNORECASE)
|
_INSTALLER_PATTERN = re.compile(r"^AnwesenheitPDF-Setup-(\d+\.\d+\.\d+)\.exe$", re.IGNORECASE)
|
||||||
|
_SOURCE_EXCEL_DIR = Path(
|
||||||
|
r"j:\Fachbereiche\BO\Produktionsschule\Maßnahmeunterlagen\30_Teilnehmende\01_Anwesenheit\Anwesenheitsliste"
|
||||||
|
)
|
||||||
|
_SOURCE_YEAR_PATTERN = re.compile(r"(20\d{2})")
|
||||||
|
_SOURCE_CHECK_INTERVAL_MS = 60_000
|
||||||
|
|
||||||
|
|
||||||
def _parse_semver(version_str: str) -> Tuple[int, int, int]:
|
def _parse_semver(version_str: str) -> Tuple[int, int, int]:
|
||||||
@@ -173,6 +178,90 @@ def _find_latest_installer(config: Dict[str, Any]) -> Optional[Tuple[str, Path]]
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _extract_year_from_excel_name(filename: str) -> Optional[int]:
|
||||||
|
"""Extract a four-digit year from an Excel filename."""
|
||||||
|
match = _SOURCE_YEAR_PATTERN.search(filename)
|
||||||
|
if not match:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
return int(match.group(1))
|
||||||
|
except ValueError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _choose_source_excel(files: List[Path]) -> Optional[Path]:
|
||||||
|
"""Pick the most relevant source Excel (current year preferred)."""
|
||||||
|
if not files:
|
||||||
|
return None
|
||||||
|
|
||||||
|
current_year = date.today().year
|
||||||
|
with_year: List[Tuple[Path, int]] = []
|
||||||
|
without_year: List[Path] = []
|
||||||
|
|
||||||
|
for file in files:
|
||||||
|
year = _extract_year_from_excel_name(file.name)
|
||||||
|
if year is None:
|
||||||
|
without_year.append(file)
|
||||||
|
else:
|
||||||
|
with_year.append((file, year))
|
||||||
|
|
||||||
|
current_year_files = [f for f, y in with_year if y == current_year]
|
||||||
|
if current_year_files:
|
||||||
|
return max(current_year_files, key=lambda p: p.stat().st_mtime)
|
||||||
|
|
||||||
|
if with_year:
|
||||||
|
newest_year = max(y for _, y in with_year)
|
||||||
|
newest_year_files = [f for f, y in with_year if y == newest_year]
|
||||||
|
return max(newest_year_files, key=lambda p: p.stat().st_mtime)
|
||||||
|
|
||||||
|
return max(without_year, key=lambda p: p.stat().st_mtime)
|
||||||
|
|
||||||
|
|
||||||
|
def _check_source_excel_update() -> Dict[str, Any]:
|
||||||
|
"""Check source folder connectivity and whether a newer Excel is available."""
|
||||||
|
result: Dict[str, Any] = {
|
||||||
|
"connected": False,
|
||||||
|
"update_available": False,
|
||||||
|
"source_file": None,
|
||||||
|
"target_file": None,
|
||||||
|
"status": "",
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
if not _SOURCE_EXCEL_DIR.exists():
|
||||||
|
result["status"] = "Netzlaufwerk nicht erreichbar."
|
||||||
|
return result
|
||||||
|
|
||||||
|
source_files = sorted(_SOURCE_EXCEL_DIR.glob("*.xlsx"), key=lambda p: p.name.lower())
|
||||||
|
result["connected"] = True
|
||||||
|
|
||||||
|
if not source_files:
|
||||||
|
result["status"] = "Keine Excel-Datei in der Quelle gefunden."
|
||||||
|
return result
|
||||||
|
|
||||||
|
source_file = _choose_source_excel(source_files)
|
||||||
|
if source_file is None:
|
||||||
|
result["status"] = "Keine geeignete Excel-Datei in der Quelle gefunden."
|
||||||
|
return result
|
||||||
|
|
||||||
|
target_file = _input_dir() / source_file.name
|
||||||
|
result["source_file"] = source_file
|
||||||
|
result["target_file"] = target_file
|
||||||
|
|
||||||
|
if (not target_file.exists()) or (source_file.stat().st_mtime > target_file.stat().st_mtime):
|
||||||
|
result["update_available"] = True
|
||||||
|
result["status"] = f"Neue Excel verfügbar: {source_file.name}"
|
||||||
|
return result
|
||||||
|
|
||||||
|
result["status"] = f"Excel aktuell: {source_file.name}"
|
||||||
|
return result
|
||||||
|
|
||||||
|
except OSError:
|
||||||
|
result["connected"] = False
|
||||||
|
result["status"] = "Netzlaufwerk nicht erreichbar."
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
def _build_month_keys(config: Dict[str, Any]) -> List[str]:
|
def _build_month_keys(config: Dict[str, Any]) -> List[str]:
|
||||||
"""Return deduplicated month keys from config (preserves insertion order)."""
|
"""Return deduplicated month keys from config (preserves insertion order)."""
|
||||||
seen: set = set()
|
seen: set = set()
|
||||||
@@ -248,6 +337,7 @@ class AnwesenheitApp(ctk.CTk):
|
|||||||
self._out_dir: Optional[Path] = None
|
self._out_dir: Optional[Path] = None
|
||||||
self._input_files: List[Path] = []
|
self._input_files: List[Path] = []
|
||||||
self._excel_name_to_path: Dict[str, Path] = {}
|
self._excel_name_to_path: Dict[str, Path] = {}
|
||||||
|
self._pending_source_excel: Optional[Path] = None
|
||||||
self._tooltip_window: Optional[ctk.CTkToplevel] = None
|
self._tooltip_window: Optional[ctk.CTkToplevel] = None
|
||||||
self._tooltip_label: Optional[tk.Label] = None
|
self._tooltip_label: Optional[tk.Label] = None
|
||||||
|
|
||||||
@@ -258,8 +348,8 @@ class AnwesenheitApp(ctk.CTk):
|
|||||||
self._build_preview_section()
|
self._build_preview_section()
|
||||||
self._build_log_section()
|
self._build_log_section()
|
||||||
|
|
||||||
# Auto-sync after event loop starts
|
# Initial scan + continuous source check
|
||||||
self.after(100, self._start_initial_sync)
|
self.after(100, self._start_source_excel_watch)
|
||||||
# Daily update check
|
# Daily update check
|
||||||
if _should_check_update():
|
if _should_check_update():
|
||||||
self.after(200, self._start_update_check)
|
self.after(200, self._start_update_check)
|
||||||
@@ -348,6 +438,21 @@ class AnwesenheitApp(ctk.CTk):
|
|||||||
)
|
)
|
||||||
self._excel_menu.grid(row=0, column=2, padx=(0, 12), pady=6, sticky="w")
|
self._excel_menu.grid(row=0, column=2, padx=(0, 12), pady=6, sticky="w")
|
||||||
|
|
||||||
|
self._btn_excel_update = ctk.CTkButton(
|
||||||
|
frame,
|
||||||
|
text="Update",
|
||||||
|
width=90,
|
||||||
|
height=28,
|
||||||
|
command=self._on_excel_update_click,
|
||||||
|
)
|
||||||
|
self._btn_excel_update.grid(row=0, column=3, padx=(0, 6), pady=6, sticky="w")
|
||||||
|
self._btn_excel_update.grid_remove()
|
||||||
|
|
||||||
|
self._lbl_source_warning = ctk.CTkLabel(
|
||||||
|
frame, text="", width=24, anchor="w", text_color="orange"
|
||||||
|
)
|
||||||
|
self._lbl_source_warning.grid(row=0, column=4, padx=(0, 12), pady=6, sticky="w")
|
||||||
|
|
||||||
# Row 1: Template
|
# Row 1: Template
|
||||||
ctk.CTkLabel(frame, text="Template:", width=110, anchor="w").grid(
|
ctk.CTkLabel(frame, text="Template:", width=110, anchor="w").grid(
|
||||||
row=1, column=0, padx=(12, 4), pady=6, sticky="w"
|
row=1, column=0, padx=(12, 4), pady=6, sticky="w"
|
||||||
@@ -557,6 +662,32 @@ class AnwesenheitApp(ctk.CTk):
|
|||||||
self._excel_menu.configure(values=[text], state="disabled")
|
self._excel_menu.configure(values=[text], state="disabled")
|
||||||
self._excel_var.set(text)
|
self._excel_var.set(text)
|
||||||
|
|
||||||
|
def _set_source_state(
|
||||||
|
self,
|
||||||
|
connected: bool,
|
||||||
|
update_available: bool,
|
||||||
|
source_file: Optional[Path],
|
||||||
|
status: str,
|
||||||
|
) -> None:
|
||||||
|
self._pending_source_excel = source_file if update_available else None
|
||||||
|
|
||||||
|
if not connected:
|
||||||
|
self._lbl_source_warning.configure(text="⚠")
|
||||||
|
self._btn_excel_update.grid_remove()
|
||||||
|
self._set_download_status(status or "Netzlaufwerk nicht erreichbar.", "orange")
|
||||||
|
return
|
||||||
|
|
||||||
|
self._lbl_source_warning.configure(text="")
|
||||||
|
if update_available and source_file is not None:
|
||||||
|
self._btn_excel_update.grid()
|
||||||
|
self._btn_excel_update.configure(state="normal", text="Update")
|
||||||
|
self._set_download_status(status, "#60A5FA")
|
||||||
|
return
|
||||||
|
|
||||||
|
self._btn_excel_update.grid_remove()
|
||||||
|
if status:
|
||||||
|
self._set_download_status(status, "#22C55E")
|
||||||
|
|
||||||
def _set_download_status(self, text: str, color: str = "#60A5FA") -> None:
|
def _set_download_status(self, text: str, color: str = "#60A5FA") -> None:
|
||||||
self._lbl_download_status.configure(text=text, text_color=color)
|
self._lbl_download_status.configure(text=text, text_color=color)
|
||||||
|
|
||||||
@@ -703,33 +834,78 @@ class AnwesenheitApp(ctk.CTk):
|
|||||||
self._excel_var.set(selected_name)
|
self._excel_var.set(selected_name)
|
||||||
self._excel_path = self._excel_name_to_path[selected_name]
|
self._excel_path = self._excel_name_to_path[selected_name]
|
||||||
|
|
||||||
def _start_initial_sync(self) -> None:
|
def _start_source_excel_watch(self) -> None:
|
||||||
self._set_excel_loading_state("Lade Excel vom Netzlaufwerk ...")
|
self._scan_and_validate()
|
||||||
self._set_download_status("Datei vom Netzlaufwerk wird geprueft ...")
|
self._start_source_excel_check()
|
||||||
threading.Thread(target=self._run_initial_sync, daemon=True).start()
|
|
||||||
|
|
||||||
def _run_initial_sync(self) -> None:
|
def _start_source_excel_check(self) -> None:
|
||||||
|
threading.Thread(target=self._run_source_excel_check, daemon=True).start()
|
||||||
|
|
||||||
|
def _run_source_excel_check(self) -> None:
|
||||||
|
result = _check_source_excel_update()
|
||||||
|
self.after(0, self._on_source_excel_check_done, result)
|
||||||
|
|
||||||
|
def _on_source_excel_check_done(self, result: Dict[str, Any]) -> None:
|
||||||
|
self._set_source_state(
|
||||||
|
connected=bool(result.get("connected")),
|
||||||
|
update_available=bool(result.get("update_available")),
|
||||||
|
source_file=result.get("source_file"),
|
||||||
|
status=str(result.get("status") or ""),
|
||||||
|
)
|
||||||
|
self.after(_SOURCE_CHECK_INTERVAL_MS, self._start_source_excel_check)
|
||||||
|
|
||||||
|
def _on_excel_update_click(self) -> None:
|
||||||
|
source_file = self._pending_source_excel
|
||||||
|
if source_file is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
target_file = _input_dir() / source_file.name
|
||||||
|
if target_file.exists():
|
||||||
|
proceed = messagebox.askokcancel(
|
||||||
|
"Datei überschreiben",
|
||||||
|
f"Die vorhandene Datei\n{target_file.name}\n"
|
||||||
|
"wird durch die neuere Datei ersetzt.\n\nFortfahren?",
|
||||||
|
icon="warning",
|
||||||
|
parent=self,
|
||||||
|
)
|
||||||
|
if not proceed:
|
||||||
|
return
|
||||||
|
|
||||||
|
self._btn_excel_update.configure(state="disabled", text="Lade...")
|
||||||
|
self._set_download_status(f"Lade {source_file.name} ...", "#60A5FA")
|
||||||
|
threading.Thread(
|
||||||
|
target=self._run_source_excel_download,
|
||||||
|
args=(source_file,),
|
||||||
|
daemon=True,
|
||||||
|
).start()
|
||||||
|
|
||||||
|
def _run_source_excel_download(self, source_file: Path) -> None:
|
||||||
try:
|
try:
|
||||||
synced_path, status = _sync_network_excel(_CONFIG)
|
target_file = _copy_excel_to_input(source_file)
|
||||||
self.after(0, self._finish_initial_sync, synced_path, status, None)
|
self.after(0, self._on_source_excel_download_done, target_file, None)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
self.after(0, self._finish_initial_sync, None, "", str(exc))
|
self.after(0, self._on_source_excel_download_done, None, str(exc))
|
||||||
|
|
||||||
def _finish_initial_sync(
|
def _on_source_excel_download_done(
|
||||||
self,
|
self,
|
||||||
synced_path: Optional[Path],
|
target_file: Optional[Path],
|
||||||
status: str,
|
|
||||||
error: Optional[str],
|
error: Optional[str],
|
||||||
) -> None:
|
) -> None:
|
||||||
|
self._btn_excel_update.configure(state="normal", text="Update")
|
||||||
|
|
||||||
if error:
|
if error:
|
||||||
self._set_download_status(
|
self._set_download_status(f"Update fehlgeschlagen: {error}", "red")
|
||||||
f"Netzlaufwerk-Pruefung fehlgeschlagen: {error}",
|
return
|
||||||
"orange",
|
|
||||||
)
|
if target_file is None:
|
||||||
else:
|
self._set_download_status("Update fehlgeschlagen.", "red")
|
||||||
color = "#22C55E" if synced_path else "orange"
|
return
|
||||||
self._set_download_status(status, color)
|
|
||||||
self._scan_and_validate(selected_excel=synced_path)
|
self._set_download_status(f"Excel aktualisiert: {target_file.name}", "#22C55E")
|
||||||
|
self._refresh_excel_dropdown(selected=target_file)
|
||||||
|
self._on_excel_changed()
|
||||||
|
self._pending_source_excel = None
|
||||||
|
self._btn_excel_update.grid_remove()
|
||||||
|
|
||||||
def _on_excel_changed(self) -> None:
|
def _on_excel_changed(self) -> None:
|
||||||
selected = self._excel_name_to_path.get(self._excel_var.get())
|
selected = self._excel_name_to_path.get(self._excel_var.get())
|
||||||
|
|||||||
+11
-1
@@ -59,6 +59,8 @@ def build_field_dict(
|
|||||||
datum_field: creation_date.strftime("%d.%m.%Y"),
|
datum_field: creation_date.strftime("%d.%m.%Y"),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internship_hours = f"{plan_threshold:.2f}".replace(".", ",")
|
||||||
|
qualifying_attendance_count: int = 0
|
||||||
pauschale_5_count: int = 0
|
pauschale_5_count: int = 0
|
||||||
|
|
||||||
for idx, entry in enumerate(attendance.entries):
|
for idx, entry in enumerate(attendance.entries):
|
||||||
@@ -68,12 +70,20 @@ def build_field_dict(
|
|||||||
if entry.is_present:
|
if entry.is_present:
|
||||||
fields[f"ist.{idx}"] = entry.hours_german()
|
fields[f"ist.{idx}"] = entry.hours_german()
|
||||||
fields[f"plan.{idx}"] = plan_value
|
fields[f"plan.{idx}"] = plan_value
|
||||||
|
if entry.hours is not None and entry.hours >= plan_threshold:
|
||||||
|
qualifying_attendance_count += 1
|
||||||
# Pauschale only if actual hours >= planned threshold
|
# Pauschale only if actual hours >= planned threshold
|
||||||
if entry.hours is not None and entry.hours >= plan_threshold:
|
if entry.hours is not None and entry.hours >= plan_threshold:
|
||||||
fields[f"{pauschale_prefix}.{idx}"] = pauschale_present_value
|
fields[f"{pauschale_prefix}.{idx}"] = pauschale_present_value
|
||||||
pauschale_5_count += 1
|
pauschale_5_count += 1
|
||||||
else:
|
else:
|
||||||
fields[f"{pauschale_prefix}.{idx}"] = "0"
|
fields[f"{pauschale_prefix}.{idx}"] = "0"
|
||||||
|
elif entry.status == "P":
|
||||||
|
fields[f"ist.{idx}"] = internship_hours
|
||||||
|
fields[f"plan.{idx}"] = plan_value
|
||||||
|
qualifying_attendance_count += 1
|
||||||
|
fields[f"{pauschale_prefix}.{idx}"] = pauschale_present_value
|
||||||
|
pauschale_5_count += 1
|
||||||
else:
|
else:
|
||||||
fields[f"ist.{idx}"] = ""
|
fields[f"ist.{idx}"] = ""
|
||||||
fields[f"plan.{idx}"] = ""
|
fields[f"plan.{idx}"] = ""
|
||||||
@@ -86,7 +96,7 @@ def build_field_dict(
|
|||||||
fields["pauschale_5_sum"] = f"{pauschale_5_euro:.2f}"
|
fields["pauschale_5_sum"] = f"{pauschale_5_euro:.2f}"
|
||||||
fields["pauschale_25_sum"] = "0.00"
|
fields["pauschale_25_sum"] = "0.00"
|
||||||
fields["pauschale_sum"] = f"{pauschale_5_euro:.2f}"
|
fields["pauschale_sum"] = f"{pauschale_5_euro:.2f}"
|
||||||
fields["anwesend_sum"] = str(pauschale_5_count)
|
fields["anwesend_sum"] = str(qualifying_attendance_count)
|
||||||
|
|
||||||
return fields
|
return fields
|
||||||
|
|
||||||
|
|||||||
@@ -128,21 +128,30 @@ class TestAbsentDay:
|
|||||||
fields = build_field_dict(_make_attendance(_absent(4, status)), cfg, date(2026, 2, 1))
|
fields = build_field_dict(_make_attendance(_absent(4, status)), cfg, date(2026, 2, 1))
|
||||||
assert fields["anwesend.0"] == status
|
assert fields["anwesend.0"] == status
|
||||||
|
|
||||||
@pytest.mark.parametrize("status", ["K", "KK", "E", "F", "P", "U"])
|
@pytest.mark.parametrize("status", ["K", "KK", "E", "F", "U"])
|
||||||
def test_ist_empty(self, cfg, status):
|
def test_ist_empty(self, cfg, status):
|
||||||
fields = build_field_dict(_make_attendance(_absent(4, status)), cfg, date(2026, 2, 1))
|
fields = build_field_dict(_make_attendance(_absent(4, status)), cfg, date(2026, 2, 1))
|
||||||
assert fields["ist.0"] == ""
|
assert fields["ist.0"] == ""
|
||||||
|
|
||||||
@pytest.mark.parametrize("status", ["K", "KK", "E", "F", "P", "U"])
|
@pytest.mark.parametrize("status", ["K", "KK", "E", "F", "U"])
|
||||||
def test_plan_empty(self, cfg, status):
|
def test_plan_empty(self, cfg, status):
|
||||||
fields = build_field_dict(_make_attendance(_absent(4, status)), cfg, date(2026, 2, 1))
|
fields = build_field_dict(_make_attendance(_absent(4, status)), cfg, date(2026, 2, 1))
|
||||||
assert fields["plan.0"] == ""
|
assert fields["plan.0"] == ""
|
||||||
|
|
||||||
@pytest.mark.parametrize("status", ["K", "KK", "E", "F", "P", "U"])
|
@pytest.mark.parametrize("status", ["K", "KK", "E", "F", "U"])
|
||||||
def test_pauschale_empty(self, cfg, status):
|
def test_pauschale_empty(self, cfg, status):
|
||||||
fields = build_field_dict(_make_attendance(_absent(4, status)), cfg, date(2026, 2, 1))
|
fields = build_field_dict(_make_attendance(_absent(4, status)), cfg, date(2026, 2, 1))
|
||||||
assert fields["pauschale_5.0"] == "0"
|
assert fields["pauschale_5.0"] == "0"
|
||||||
|
|
||||||
|
def test_praktikum_sets_fixed_hours(self, cfg):
|
||||||
|
fields = build_field_dict(_make_attendance(_absent(4, "P")), cfg, date(2026, 2, 1))
|
||||||
|
assert fields["ist.0"] == "6,00"
|
||||||
|
assert fields["plan.0"] == "6"
|
||||||
|
|
||||||
|
def test_praktikum_sets_pauschale(self, cfg):
|
||||||
|
fields = build_field_dict(_make_attendance(_absent(4, "P")), cfg, date(2026, 2, 1))
|
||||||
|
assert fields["pauschale_5.0"] == "1"
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Multi-row indexing
|
# Multi-row indexing
|
||||||
@@ -177,6 +186,17 @@ class TestSummaryFields:
|
|||||||
# Day 1 (6h) and day 2 (7h) qualify, day 3 (5h) does not
|
# Day 1 (6h) and day 2 (7h) qualify, day 3 (5h) does not
|
||||||
assert fields["anwesend_sum"] == "2"
|
assert fields["anwesend_sum"] == "2"
|
||||||
|
|
||||||
|
def test_anwesend_sum_counts_praktikum_as_6h(self, cfg):
|
||||||
|
entries = [_present(1, 6.0), _absent(2, "P"), _present(3, 5.0)]
|
||||||
|
fields = build_field_dict(_make_attendance(*entries), cfg, date(2026, 2, 1))
|
||||||
|
assert fields["anwesend_sum"] == "2"
|
||||||
|
|
||||||
|
def test_praktikum_increases_pauschale_sum(self, cfg):
|
||||||
|
entries = [_absent(1, "P")]
|
||||||
|
fields = build_field_dict(_make_attendance(*entries), cfg, date(2026, 2, 1))
|
||||||
|
assert fields["pauschale_5_sum"] == "8.60"
|
||||||
|
assert fields["pauschale_sum"] == "8.60"
|
||||||
|
|
||||||
def test_pauschale_5_sum_euro(self, cfg):
|
def test_pauschale_5_sum_euro(self, cfg):
|
||||||
"""pauschale_5_sum = qualifying days * 8.60 (period decimal for JS)."""
|
"""pauschale_5_sum = qualifying days * 8.60 (period decimal for JS)."""
|
||||||
entries = [_present(1, 6.0), _present(2, 7.0), _present(3, 5.0)]
|
entries = [_present(1, 6.0), _present(2, 7.0), _present(3, 5.0)]
|
||||||
|
|||||||
Reference in new issue
Block a user