312 lines
8.7 KiB
Markdown
312 lines
8.7 KiB
Markdown
# Anwesenheit PDF Generator
|
|
|
|
Liest Anwesenheitsdaten aus einer Excel-Datei (`.xlsx`) und erzeugt für jeden Teilnehmer eine ausgefüllte PDF-Datei (SAB-Formular).
|
|
|
|
---
|
|
|
|
## Voraussetzungen
|
|
|
|
- Python 3.10 oder höher
|
|
- pip
|
|
|
|
---
|
|
|
|
## Installation
|
|
|
|
```bash
|
|
# Abhängigkeiten installieren
|
|
pip install -r requirements.txt
|
|
```
|
|
|
|
Für Entwicklung inkl. Tests:
|
|
|
|
```bash
|
|
pip install -r requirements-dev.txt
|
|
```
|
|
|
|
---
|
|
|
|
## Konfiguration
|
|
|
|
Vor dem ersten Start müssen zwei Werte in `config.json` eingetragen werden:
|
|
|
|
| Feld | Beschreibung |
|
|
|---|---|
|
|
| `antragsnummer` | SAB-Antragsnummer der Einrichtung |
|
|
| `ort` | Ort für die Unterschriftszeile im PDF |
|
|
|
|
Alle anderen Felder sind vorbelegt und müssen nur bei abweichender Excel-Struktur angepasst werden.
|
|
|
|
### Erweiterte PDF-Optionen
|
|
|
|
Diese Schalter steuern das interne Verhalten beim PDF-Befüllen und müssen in der Regel nicht verändert werden.
|
|
|
|
#### `pdf_use_display_values` (Standard: `true`)
|
|
|
|
Steuert, welche Werte in Dropdown- und Auswahlfelder geschrieben werden.
|
|
|
|
| Wert | Verhalten |
|
|
|---|---|
|
|
| `true` | Lesbare Anzeigewerte (z. B. `"X = anwesend"`, `"mind. 6 h"`, `"8,60"`) werden ins PDF geschrieben. Der PDF-Viewer zeigt den Text direkt an. |
|
|
| `false` | Rohe Export-Werte (z. B. `"X"`, `"6"`, `"1"`) werden verwendet — so wie es das JavaScript im Originalformular intern macht. Sinnvoll, wenn ein externer Viewer (z. B. Adobe Acrobat) die Werte selbst aufzulösen soll. |
|
|
|
|
#### `pdf_need_appearances` (Standard: `false`)
|
|
|
|
Steuert das `NeedAppearances`-Flag in der erzeugten PDF-Datei.
|
|
|
|
| Wert | Verhalten |
|
|
|---|---|
|
|
| `false` | Der PDF-Renderer nutzt die im PDF gespeicherten Appearance-Streams (AP) direkt. Buttons, Footer und Summenfelder werden korrekt angezeigt. **Empfohlene Einstellung.** |
|
|
| `true` | Der PDF-Viewer regeneriert alle Darstellungen aus den Felddefinitionen (`/DA`). Kann helfen, wenn ein externer Viewer einzelne Felder leer anzeigt — kann aber dazu führen, dass Buttons und Footer ausgeblendet werden. |
|
|
|
|
> **Hinweis:** Die Summenfelder erhalten beim Generieren eigene Appearance-Streams (Zwei-Pass-Schreibstrategie), sodass `pdf_need_appearances: false` für alle Felder funktioniert.
|
|
|
|
#### `regression` (Objekt)
|
|
|
|
Einstellungen für den automatisierten Regressions-Bildvergleich (`pytest`). Alle Unterfelder befinden sich in diesem Objekt.
|
|
|
|
```json
|
|
"regression": {
|
|
"enabled": false,
|
|
"mask_footer": true
|
|
}
|
|
```
|
|
|
|
##### `regression.enabled` (Standard: `false`)
|
|
|
|
Aktiviert oder deaktiviert den Bildvergleich.
|
|
|
|
| Wert | Verhalten |
|
|
|---|---|
|
|
| `false` | Regression-Tests werden übersprungen (`pytest.skip`). **Standard.** |
|
|
| `true` | Bildvergleich wird ausgeführt. Erfordert Referenz-PDFs in `tests/fixtures/`. |
|
|
|
|
##### `regression.mask_footer` (Standard: `true`)
|
|
|
|
Steuert, ob der untere Bereich (Footer) beim Pixel-Vergleich ignoriert wird.
|
|
|
|
| Wert | Verhalten |
|
|
|---|---|
|
|
| `true` | Die untere ~20% jeder PDF-Seite wird ignoriert. Sinnvoll, da Template-PDFs je nach Vorlage unterschiedlich viele Footer-Zeilen (2 oder 3) haben können. |
|
|
| `false` | Vollständiger Pixel-by-Pixel-Vergleich inkl. Footer-Bereich. |
|
|
|
|
---
|
|
|
|
## Verzeichnisstruktur
|
|
|
|
```
|
|
generate.py ← Einstiegspunkt
|
|
config.json ← Konfiguration
|
|
input/ ← Excel-Dateien ablegen
|
|
template/
|
|
sab.pdf ← PDF-Vorlage (muss vorhanden sein)
|
|
output/ ← Generierte PDFs (werden hier abgelegt)
|
|
```
|
|
|
|
---
|
|
|
|
## Verwendung
|
|
|
|
### Generierung starten
|
|
|
|
```bash
|
|
python generate.py generate --file "input/Anwesenheitsliste 2026.xlsx" --month Feb
|
|
```
|
|
|
|
Das Programm zeigt vor der Generierung eine Vorschau und fragt zur Bestätigung:
|
|
|
|
```
|
|
=== Vorschau der ermittelten Werte ===
|
|
Excel-Datei : Anwesenheitsliste 2026.xlsx
|
|
Jahr : 2026
|
|
Monat : Feb (02)
|
|
Antragsnummer : 12345
|
|
Ort : Leipzig
|
|
Erstelldatum : 27.03.2026
|
|
Teilnehmer : 49
|
|
Fehler : 0
|
|
|
|
Möchten Sie die PDF-Generierung starten? [j/N]
|
|
```
|
|
|
|
Nur bei Eingabe von `j` werden PDFs geschrieben.
|
|
|
|
### Nur Validierung (keine PDFs erzeugen)
|
|
|
|
```bash
|
|
python generate.py validate --file "input/Anwesenheitsliste 2026.xlsx" --month Feb
|
|
```
|
|
|
|
Führt alle Prüfungen durch und zeigt die Vorschau — ohne Dateien zu schreiben.
|
|
|
|
### PDF als Bild rendern
|
|
|
|
```bash
|
|
python generate.py render-image --pdf "output/(1) Mustermann, Max.pdf" --page 1 --out "preview.png"
|
|
```
|
|
|
|
Dieser Befehl rendert eine PDF-Seite als PNG-Bild. Er nutzt dieselbe Render-Funktion wie der Regressionstest, damit CLI-Vorschau und Testvergleich technisch identisch arbeiten.
|
|
|
|
Alle Seiten auf einmal rendern:
|
|
|
|
```bash
|
|
python generate.py render-image --pdf "output/(1) Mustermann, Max.pdf" --all-pages --out "render_out"
|
|
```
|
|
|
|
---
|
|
|
|
## Optionen
|
|
|
|
### `generate`
|
|
|
|
| Option | Pflicht | Beschreibung |
|
|
|---|---|---|
|
|
| `--file` | ja | Pfad zur Excel-Datei |
|
|
| `--month` | ja | Name des Monats-Sheets (z. B. `Jan`, `Feb`, `Mrz`, `Apr` …) |
|
|
| `--out` | nein | Ausgabeverzeichnis (Standard: `output/` aus `config.json`) |
|
|
|
|
### `validate`
|
|
|
|
| Option | Pflicht | Beschreibung |
|
|
|---|---|---|
|
|
| `--file` | ja | Pfad zur Excel-Datei |
|
|
| `--month` | ja | Name des Monats-Sheets |
|
|
|
|
### `render-image`
|
|
|
|
| Option | Pflicht | Beschreibung |
|
|
|---|---|---|
|
|
| `--pdf` | ja | Pfad zur PDF-Datei |
|
|
| `--out` | nein | Zieldatei (Einzelseite) oder Zielordner (`--all-pages`) |
|
|
| `--dpi` | nein | Render-Auflösung in DPI (Standard: `150`) |
|
|
| `--page` | nein | 1-basierte Seitennummer (Standard: `1`) |
|
|
| `--all-pages` | nein | Rendert alle Seiten statt nur `--page` |
|
|
|
|
---
|
|
|
|
## Sheet-Namen
|
|
|
|
Die deutschen Monatsabkürzungen entsprechen den Tabellenblatt-Namen in der Excel-Datei:
|
|
|
|
| Sheet | Monat |
|
|
|---|---|
|
|
| `Jan` | Januar |
|
|
| `Feb` | Februar |
|
|
| `Mrz` | März |
|
|
| `Apr` | April |
|
|
| `Mai` | Mai |
|
|
| `Jun` | Juni |
|
|
| `Jul` | Juli |
|
|
| `Aug` | August |
|
|
| `Sep` | September |
|
|
| `Okt` | Oktober |
|
|
| `Nov` | November |
|
|
| `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
|
|
|
|
Für jeden Teilnehmer wird eine PDF-Datei erstellt:
|
|
|
|
```
|
|
output/
|
|
(1) Mustermann, Max.pdf
|
|
(2) Beispiel, Anna.pdf
|
|
...
|
|
```
|
|
|
|
Die laufende Nummer entspricht der Nummerierung in Spalte A der Excel-Datei.
|
|
|
|
---
|
|
|
|
## Tests ausführen
|
|
|
|
**Alle Tests starten**
|
|
|
|
```bash
|
|
pytest tests/ -v
|
|
```
|
|
|
|
**Nur den Regression-Test.**
|
|
*Der Test erwartet, dass im Ordner 2026_Feb die Excel-Datei und in reference/ die Ground-Truth-PDFs liegen. Er generiert PDFs in ein Temp-Verzeichnis und vergleicht sie pixelweise. Bei Abweichungen werden Diff-Bilder nach diff geschrieben.*
|
|
|
|
```bash
|
|
python -m pytest tests/test_regression.py -v
|
|
```
|
|
|
|
|
|
---
|
|
|
|
## Validierungsprüfungen
|
|
|
|
Das Programm prüft vor jeder Generierung:
|
|
|
|
1. `config.json` vorhanden und vollständig
|
|
2. Excel-Datei existiert und ist eine `.xlsx`/`.xlsm`-Datei
|
|
3. Jahr aus Dateiname extrahierbar (z. B. `Anwesenheitsliste 2026.xlsx`)
|
|
4. Sheet-Name bekannt (in `month_mapping` der `config.json`)
|
|
5. Sheet existiert in der Arbeitsmappe
|
|
6. Spalte A (lfd. Nummer) enthält Daten
|
|
7. Headerzeile mit `Name, Vorname` gefunden
|
|
8. Tagesspalten ab Spaltenindex 9 vorhanden
|
|
9. PDF-Template unter `template/sab.pdf` erreichbar
|
|
|
|
---
|
|
|
|
## Windows-Installer erstellen
|
|
|
|
Der Build-Prozess erzeugt neben dem Programm auch einen Windows-Installer (`.exe`), der vom SMB-Netzlaufwerk gestartet werden kann.
|
|
|
|
### Voraussetzung: Inno Setup 6
|
|
|
|
Inno Setup 6 muss auf der Build-Maschine installiert sein. Installation über `winget`:
|
|
|
|
```bat
|
|
winget install --id JRSoftware.InnoSetup -e -s winget -i
|
|
```
|
|
|
|
Alternativ manueller Download: <https://jrsoftware.org/isinfo.php>
|
|
|
|
### Installer bauen
|
|
|
|
```bat
|
|
build.bat
|
|
```
|
|
|
|
`build.bat` führt alle vier Schritte automatisch aus:
|
|
|
|
1. Abhängigkeiten installieren (`pip install`)
|
|
2. Version aktualisieren (`update_version.py`)
|
|
3. Programm packen (`PyInstaller` → `dist\AnwesenheitPDF\`)
|
|
4. Installer kompilieren (`ISCC.exe` → `dist\AnwesenheitPDF-Setup-x.y.z.exe`)
|
|
|
|
Ist Inno Setup **nicht** installiert, wird Schritt 4 mit einer Warnung übersprungen — der PyInstaller-Output in `dist\AnwesenheitPDF\` ist trotzdem nutzbar.
|
|
|
|
### Installer verteilen
|
|
|
|
Die fertige Datei `dist\AnwesenheitPDF-Setup-x.y.z.exe` auf das SMB-Netzlaufwerk kopieren. Von dort kann sie direkt gestartet werden — ohne vorherige lokale Kopie.
|
|
|
|
### Was der Installer macht
|
|
|
|
| Aktion | Details |
|
|
|---|---|
|
|
| Installationspfad | `%ProgramFiles%\AnwesenheitPDF\` (Admin erforderlich) |
|
|
| Startmenü-Eintrag | `AnwesenheitPDF` |
|
|
| Desktop-Verknüpfung | Optional (Checkbox im Wizard, Standard: deaktiviert) |
|
|
| Uninstaller | Registriert unter *Programme und Features* |
|
|
| Windows-Version | Windows 10 oder höher |
|