ArbZG-konforme Arbeitszeitnachweise aus Solidtime
Dieses Dokument erklärt im Detail, wie die Überstunden, Abwesenheiten und ArbZG-Korrekturen berechnet werden.
| Begriff | Bedeutung |
|---|---|
| Ist-Stunden | Tatsächlich gearbeitete/gutgeschriebene Stunden |
| Soll-Stunden | Vertragliche Stunden (z.B. 7,8h bei 39h-Woche) |
| hours_per_day | HOURS_PER_WEEK / 5 (z.B. 39 / 5 = 7,8h) |
| Carry-Over | Stundenübertrag von einem Tag auf den nächsten Werktag |
| Abwesenheitstypen | Urlaub, Krank, Gleittag |
Das Tool wertet standardmäßig alle Zeiteinträge des konfigurierten Members aus — unabhängig vom zugeordneten Client/Kunden in Solidtime. Es werden also Einträge über alle Arbeitgeber, Projekte und Kunden hinweg berücksichtigt.
Über Umgebungsvariablen können bestimmte Einträge gezielt ausgeschlossen werden:
| Variable | Wirkung |
|---|---|
EXCLUDE_CLIENTS |
Komma-getrennte Client-IDs, die komplett aus der Berechnung ausgeschlossen werden (z.B. private Nebenprojekte) |
THW_CLIENT_ID |
Client-ID für ehrenamtliche Arbeit (z.B. THW). Wochenend-Einträge dieses Clients werden ausgeschlossen (private Freiwilligenarbeit), Werktags-Einträge zählen normal (Freistellung durch den Arbeitgeber) |
Solidtime-Clients: Arbeitgeber, HDBW, THW, Privat
EXCLUDE_CLIENTS=<Privat-ID>
THW_CLIENT_ID=<THW-ID>
Ergebnis:
Arbeitgeber-Einträge → zählen immer
HDBW-Einträge → zählen immer
THW Mo-Fr → zählen (Freistellung)
THW Sa/So → ausgeschlossen (Ehrenamt)
Privat → ausgeschlossen (komplett)
Einträge ohne Client → zählen immer
Die Filterung erfolgt direkt in der Datenbankabfrage:
-- Basis: alle Einträge des Members
WHERE te.member_id = :member_id AND te.end IS NOT NULL
-- EXCLUDE_CLIENTS: vollständig ausschließen
AND (te.client_id IS NULL OR te.client_id NOT IN (:excluded_ids))
-- THW_CLIENT_ID: nur Wochenend-Einträge ausschließen
AND NOT (te.client_id = :thw_id AND EXTRACT(DOW FROM te.start) IN (0, 6))
Jeder Tag wird anhand der Solidtime-Projektname automatisch klassifiziert:
Solidtime-Projekt enthält... → Tagestyp
────────────────────────────────────────────
"urlaub" (Groß-/Kleinschreibung egal) → Urlaub
"krank" → Krank
"gleittag" oder "gleitzeit" → Gleittag
Wochenende (Sa/So) → Samstag/Sonntag
Feiertag (bundeslandabhängig) → Feiertag
Alles andere → Arbeit
Reihenfolge ist wichtig: Wenn ein Solidtime-Eintrag auf “Urlaub” steht, wird der Tag als Urlaub erkannt — selbst wenn er auf ein Wochenende fällt.
Die zentrale Formel:
Überstunden = Σ Ist-Stunden − Σ Soll-Stunden
| Tagestyp | Ist-Stunden | Soll-Stunden | Netto-Effekt auf Überstunden |
|---|---|---|---|
| Arbeit (mit Einträgen) | Tatsächliche Arbeitszeit | hours_per_day | Positiv bei Mehrarbeit, negativ bei Minderarbeit |
| Arbeit (leer, Vergangenheit) | 0 | hours_per_day | -hours_per_day (Überstundenabbau) |
| Urlaub | hours_per_day | hours_per_day | Kein Effekt (bezahlte Abwesenheit) |
| Krank | hours_per_day | hours_per_day | Kein Effekt (Entgeltfortzahlung) |
| Gleittag | 0 | hours_per_day | -hours_per_day (Überstundenabbau) |
| Wochenende | Tatsächliche Arbeitszeit | 0 | Vollständig als Überstunden |
| Feiertag | Tatsächliche Arbeitszeit | 0 | Vollständig als Überstunden |
Arbeitstage mit Einträgen: Einfachster Fall — die Differenz zwischen tatsächlicher Arbeitszeit und Soll bestimmt, ob Überstunden aufgebaut (+) oder abgebaut (-) werden.
Leere Werktage (keine Solidtime-Einträge): Ein vergangener Werktag ohne Einträge gilt als Überstundenabbau (Brückentag, Gleitzeitabbau). Das Soll (hours_per_day) wird gezählt, Ist = 0. Dadurch sinkt das Überstundenkonto um hours_per_day pro Tag.
Urlaub / Krank: Bezahlte Abwesenheit — es wird so gerechnet, als hätte man einen normalen Arbeitstag absolviert (Ist = Soll = hours_per_day). Das Überstundenkonto bleibt unverändert.
Gleittag: Überstundenabbau — der Tag wird explizit in Solidtime als Gleittag gebucht. Ist = 0, Soll = hours_per_day. Das Überstundenkonto sinkt um hours_per_day.
Wochenende und Feiertage: Hier ist Soll = 0 (man muss nicht arbeiten). Jede gearbeitete Stunde geht daher 1:1 als Überstunde ins Konto.
Das Überstundenkonto wird kumulativ über alle Jahre seit START_DATE geführt. Beim Generieren der Berichte werden alle Jahre seit dem Vertragsstart berücksichtigt:
prior_overtime = 0
Für jedes Jahr von START_DATE.year bis aktuelles Jahr:
year_overtime = Σ Ist − Σ Soll (für dieses Jahr)
→ Bericht generieren mit prior_overtime als Übertrag
prior_overtime += year_overtime
START_DATE = 2024-01-01
2024: Überstunden = +42,5h
→ Bericht 2024: Übertrag Vorjahre: 0h, Jahr: +42,5h, Gesamt: +42,5h
2025: Überstunden = -12,3h
→ Bericht 2025: Übertrag Vorjahre: +42,5h, Jahr: -12,3h, Gesamt: +30,2h
2026: Überstunden = +8,0h (laufend)
→ Bericht 2026: Übertrag Vorjahre: +30,2h, Jahr: +8,0h, Gesamt: +38,2h
Wenn ein einzelnes Jahr mit --year YYYY generiert wird, berechnet das Tool automatisch die Überstunden aller Vorjahre seit START_DATE, um den korrekten Übertrag zu ermitteln.
Das Tool prüft folgende Regeln des Arbeitszeitgesetzes:
Wenn actual_hours > 10 → Verstoß
Einfacher Grenzwert-Check. Die 10h-Grenze gilt als absolutes Maximum.
Fenster: 120 Werktage (= 24 Wochen × 5 Arbeitstage)
Berechnung: Gleitender Durchschnitt über 120 aufeinanderfolgende Arbeitstage
Wenn Durchschnitt > 8h → Verstoß (markiert am letzten Tag des Fensters)
Nur geprüft, wenn genügend Daten vorliegen (≥ 120 Arbeitstage). Wochenenden und Feiertage sind ausgeschlossen.
Wenn actual_hours > 6 UND mehrere Einträge vorhanden:
Pflichtpause = 45 min (bei >9h) oder 30 min (bei >6h)
Gemessene Pause = Summe aller Lücken zwischen aufeinanderfolgenden Einträgen
Wenn gemessene Pause < Pflichtpause → Verstoß
Wichtig: Die Pausenprüfung erfolgt nur bei Tagen mit mindestens 2 Solidtime-Einträgen, da bei einem einzelnen durchgehenden Eintrag keine Lücke messbar ist. Das bedeutet nicht, dass keine Pause gemacht wurde — sie ist nur nicht separat gebucht.
rest = Startzeit(Tag N) − Endzeit(Tag N-1)
Wenn rest < 11h → Verstoß
Vergleicht das späteste Arbeitsende eines Tages mit dem frühesten Arbeitsbeginn des Folgetags.
Wenn Arbeitszeit > 0 an einem Sonntag → Verstoß
Wenn Arbeitszeit > 0 an einem Feiertag → Verstoß
Feiertage werden bundeslandabhängig berechnet (Konfiguration: STATE).
Die Büro-Version verschiebt Stunden so, dass keine ArbZG-Verstöße sichtbar sind. Die Gesamtstundenzahl bleibt erhalten — es ändert sich nur die Verteilung.
carry_over = 0
Für jeden Tag im Jahr (1. Jan → heute):
┌─ Urlaub/Krank/Gleittag?
│ → Ist = hours_per_day, Zeiten = fiktiv (08:00-15:48 bei 7,8h)
│
├─ Wochenende/Feiertag?
│ → carry_over += actual_hours
│ → Ist = 0 (Stunden werden verschoben)
│
└─ Normaler Arbeitstag:
total = actual_hours + carry_over
Wenn total > 10:
→ Ist = 10
→ carry_over = total - 10
Sonst:
→ Ist = total
→ carry_over = 0
Falls nach dem letzten Tag noch carry_over > 0 übrig ist:
Für jeden Tag RÜCKWÄRTS:
Wenn Arbeitstag UND Ist > 0 UND Ist < 10:
Platz = 10 - Ist
Auffüllung = min(Platz, carry_over)
Ist += Auffüllung
carry_over -= Auffüllung
Für jeden Tag mit Ist-Stunden > 0 werden plausible Zeiten generiert:
Beginn: immer 08:00
Pause:
> 9h → 45 min
> 6h → 30 min
≤ 6h → keine
Ende: 08:00 + (Ist × 60 min) + Pausenminuten
Beispiele:
| Ist-Stunden | Pause | Ende |
|---|---|---|
| 4,0h | 0 min | 12:00 |
| 7,8h | 30 min | 16:18 |
| 8,0h | 30 min | 16:30 |
| 9,5h | 45 min | 18:15 |
| 10,0h | 45 min | 18:45 |
Stunden können über Monatsgrenzen hinweg verschoben werden. Beispiel:
Fr 31. Jan: 12h gearbeitet → 10h (Ist) + 2h carry_over
Sa 1. Feb: (Wochenende)
So 2. Feb: (Wochenende)
Mo 3. Feb: 6h gearbeitet → 6h + 2h carry_over = 8h (Ist)
Das führt dazu, dass die Monatssummen in der Büro-Version von den echten Monatssummen abweichen können. Die Jahressumme bleibt aber identisch.
Für das aktuelle Jahr werden Soll-Stunden und Überstunden nur bis zum heutigen Tag berechnet — nicht für das gesamte Jahr. Zukünftige Tage erhalten Soll = 0 und werden weder als Überstunden noch als Minusstunden gezählt.
Für abgeschlossene Jahre werden alle Tage des Jahres berücksichtigt (Stand: 31.12.).
Das Stichtag-Datum wird im Zusammenfassungs-Sheet als “Stand: TT.MM.JJJJ” angezeigt.
Beispiel aktuelles Jahr (Stand: 26.02.2026):

Beispiel abgeschlossenes Jahr:

Urlaubsperioden, die im Vorjahr beginnen und im neuen Jahr fortgesetzt werden, zählen gegen den Urlaubsanspruch des Vorjahres.
Urlaub vom 31.12.2025 bis 14.01.2026:
Dezember 2025: 31.12. = Urlaub ✓
Januar 2026: 01.01. = Neujahr (übersprungen)
02.01. = Urlaub → Carryover +1
03.01. = Sa (übersprungen)
04.01. = So (übersprungen)
05.01. = Urlaub → Carryover +1
06.01. = Hl. Drei Könige (übersprungen)
07.01. = Krank → STOP (kein Urlaub mehr)
Ergebnis: 2 Tage Urlaubsübertrag aus 2025
→ 2026 Urlaubskonto: Genommen = 0, Resturlaub = 30
Im Zusammenfassungs-Sheet wird der Übertrag als Hinweis angezeigt:

Beide Excel-Versionen (real und Büro) enthalten ein Zusammenfassungs-Sheet mit dem Überstundenkonto, Urlaubskonto und Krankheitstagen.
| Zeile | Berechnung |
|---|---|
| Stand | Stichtag der Berechnung (heute oder 31.12. bei abgeschlossenen Jahren) |
| Gesamt Ist-Stunden | Summe aller Ist-Stunden bis Stichtag |
| Gesamt Soll-Stunden | Summe aller Soll-Stunden bis Stichtag |
| Überstunden YYYY | Ist − Soll des aktuellen Jahres |
| Übertrag Vorjahre | Kumulative Überstunden aller Jahre vor YYYY (nur wenn ≠ 0) |
| Überstundenkonto gesamt | Überstunden YYYY + Übertrag Vorjahre |
| Urlaubskonto | Anspruch, genommene Tage (abzgl. Vorjahresübertrag), Resturlaub |
| Krankheitstage | Anzahl Krankheitstage im Jahr |
| ArbZG-Verstöße | Nur in der realen Version: Verstoß-Statistik |
Die Zeile “Überstundenkonto gesamt” ist fett und farbig hervorgehoben:
Die reale Version zeigt zusätzlich die ArbZG-Verstoß-Statistik:

Die monatliche E-Mail enthält eine kumulative Überstundenberechnung seit START_DATE (Vertragsstart).
Die E-Mail berechnet die kumulativen Überstunden über alle Jahre seit START_DATE bis einschließlich des Zielmonats:
Für jedes Jahr von START_DATE.year bis Ziel-Jahr:
Für jeden Tag (bis einschließlich Zielmonat im Ziel-Jahr):
→ Tagestyp bestimmen (siehe oben)
→ Ist und Soll addieren (Regeln wie in der Tabelle oben)
Gesamtüberstunden = Σ Ist − Σ Soll (über alle Jahre)
Dabei werden alle Zeiteinträge gemäß der Multi-Client-Filterung (siehe oben) berücksichtigt.
Zusätzlich berechnet die E-Mail die Monatsstatistiken aus der korrigierten Büro-Version:
Monats-Ist = Summe corrected_hours aller Tage im Monat
Monats-Soll = Anzahl Werktage × hours_per_day
Die E-Mail enthält:
Mo: 8,5h gearbeitet → Ist: 8,5h Soll: 7,8h Diff: +0,7h
Di: 7,0h gearbeitet → Ist: 7,0h Soll: 7,8h Diff: -0,8h
Mi: 9,0h gearbeitet → Ist: 9,0h Soll: 7,8h Diff: +1,2h
Do: 7,8h gearbeitet → Ist: 7,8h Soll: 7,8h Diff: 0,0h
Fr: 6,5h gearbeitet → Ist: 6,5h Soll: 7,8h Diff: -1,3h
────────────────────────────────────────────────────────────
Woche: Ist: 38,8h Soll: 39,0h Diff: -0,2h
Mo: 7,8h gearbeitet → Ist: 7,8h Soll: 7,8h Diff: 0,0h
Di: Krank → Ist: 7,8h Soll: 7,8h Diff: 0,0h
Mi: Krank → Ist: 7,8h Soll: 7,8h Diff: 0,0h
Do: 8,5h gearbeitet → Ist: 8,5h Soll: 7,8h Diff: +0,7h
Fr: 7,0h gearbeitet → Ist: 7,0h Soll: 7,8h Diff: -0,8h
────────────────────────────────────────────────────────────
Woche: Ist: 38,9h Soll: 39,0h Diff: -0,1h
Krankheitstage haben keinen Einfluss auf das Überstundenkonto.
Real → Büro-Version
Fr: 10h gearbeitet → Ist: 10h Ist: 10h
Sa: 4h gearbeitet → Ist: 4h Ist: 0h (→ 4h carry)
So: 2h gearbeitet → Ist: 2h Ist: 0h (→ 6h carry)
Mo: 8h gearbeitet → Ist: 8h Ist: 10h (8h + 2h carry, Rest: 4h carry)
Di: 7h gearbeitet → Ist: 7h Ist: 10h (7h + 3h carry, Rest: 1h carry)
Mi: 6h gearbeitet → Ist: 6h Ist: 7h (6h + 1h carry)
────────────────────────────────────────────────────────────
Gesamt: Ist: 37h Ist: 37h ✓ (gleich)
Mo: 9,0h gearbeitet → Ist: 9,0h Soll: 7,8h Diff: +1,2h
Di: 8,5h gearbeitet → Ist: 8,5h Soll: 7,8h Diff: +0,7h
Mi: (leer, kein Eintrag) → Ist: 0,0h Soll: 7,8h Diff: -7,8h
Do: 8,0h gearbeitet → Ist: 8,0h Soll: 7,8h Diff: +0,2h
Fr: 7,5h gearbeitet → Ist: 7,5h Soll: 7,8h Diff: -0,3h
────────────────────────────────────────────────────────────
Woche: Ist: 33,0h Soll: 39,0h Diff: -6,0h
Der leere Mittwoch (z.B. Brückentag) erzeugt einen Soll-Abzug von 7,8h. Das Überstundenkonto sinkt entsprechend. Gleiches gilt für Gleittage.