Merge pull request #11 from Kenearos/claude/fix-calculation-01H2SPCpE7WPnQPcUfTwU2uU

Korrektur: Bonus nur bei WE-Schwelle ≥ 2,0
This commit is contained in:
Kenearos 2025-11-25 13:30:45 +01:00 committed by GitHub
commit 22014614d3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 65 additions and 26 deletions

View file

@ -23,9 +23,12 @@ Hinweise:
### Vergütung ### Vergütung
- **WT** (kein WE-Tag): 250 € pro 1,0 Einheit (Splits anteilig). - **Schwelle**: Gesamter Bonus (WT + WE) wird nur gezahlt, wenn Monats-Summe WE-Einheiten je Person ≥ 2,0.
- **WT** (kein WE-Tag):
- Wenn WE-Schwelle erreicht: 250 € pro 1,0 Einheit (Splits anteilig).
- Wenn WE-Schwelle nicht erreicht: 0 € (kein Bonus).
- **WE** (WE-Tag): - **WE** (WE-Tag):
- Wenn Monats-Summe WE-Einheiten je Person < 2,0 Auszahlung 0 für alle WE-Einheiten. - Wenn Monats-Summe WE-Einheiten < 2,0 Auszahlung 0 für alle WE-Einheiten.
- Wenn Monats-Summe WE-Einheiten ≥ 2,0 → Auszahlung 450 €/WE-Einheit, - Wenn Monats-Summe WE-Einheiten ≥ 2,0 → Auszahlung 450 €/WE-Einheit,
anschließend Abzug genau 1,0 WE-Einheit (max. 1× pro Person/Monat). anschließend Abzug genau 1,0 WE-Einheit (max. 1× pro Person/Monat).
- Abzugs-Priorität: zuerst aus Freitag-WE-Einheiten, Rest aus den übrigen WE-Einheiten (Sa/So/Feiertag/Vortag). Chronologie muss nicht nachgebildet werden; es genügt die Priorität nach Kategorie. - Abzugs-Priorität: zuerst aus Freitag-WE-Einheiten, Rest aus den übrigen WE-Einheiten (Sa/So/Feiertag/Vortag). Chronologie muss nicht nachgebildet werden; es genügt die Priorität nach Kategorie.
@ -39,7 +42,8 @@ Hinweise:
- Schwelle gilt je Person und Kalendermonat. - Schwelle gilt je Person und Kalendermonat.
- Abzug wird nur angewandt, wenn Schwelle erreicht (≥ 2,0). - Abzug wird nur angewandt, wenn Schwelle erreicht (≥ 2,0).
- WE-Dienste unterhalb der Schwelle werden NICHT als Wochentage vergütet. - **Gesamter Bonus (WT + WE) wird nur gezahlt, wenn WE-Schwelle (≥ 2,0) erreicht wird.**
- Unterhalb der Schwelle: Auszahlung = 0 € (weder WT noch WE werden vergütet).
- Rundung: Bei Schwellenprüfung Toleranz 1e-4 (z. B. 1,99995 ≈ 2,0). - Rundung: Bei Schwellenprüfung Toleranz 1e-4 (z. B. 1,99995 ≈ 2,0).
## Parameter (Blatt „Regeln") ## Parameter (Blatt „Regeln")
@ -271,8 +275,8 @@ Beispiel-Formel (als hilfsweise Matrix in Checks):
## Testfälle (sollten „grün" durchlaufen) ## Testfälle (sollten „grün" durchlaufen)
1) **Unter Schwelle**: 1) **Unter Schwelle**:
A hat 1,75 WE und 1,0 WT → Auszahlung_WE = 0 €; Auszahlung_WT = 250 €. A hat 1,75 WE und 1,0 WT → Auszahlung_WE = 0 €; Auszahlung_WT = 0 €; Auszahlung_Gesamt = 0 €.
2) **Genau Schwelle**: 2) **Genau Schwelle**:
A hat 2,0 WE (Fr 1,0 + Sa 1,0) → Abzug 1,0 (zuerst Fr) → WE_bezahlt = 1,0 → 450 €. A hat 2,0 WE (Fr 1,0 + Sa 1,0) → Abzug 1,0 (zuerst Fr) → WE_bezahlt = 1,0 → 450 €.
@ -290,9 +294,9 @@ Beispiel-Formel (als hilfsweise Matrix in Checks):
6) **Unter Schwelle, nur WE-Tage**: 6) **Unter Schwelle, nur WE-Tage**:
A hat 1,0 WE, 0 WT → Auszahlung_WE = 0 €; Auszahlung_Gesamt = 0 €. A hat 1,0 WE, 0 WT → Auszahlung_WE = 0 €; Auszahlung_Gesamt = 0 €.
7) **Vortag-Feiertag**: 7) **Vortag-Feiertag**:
Feiertag Dienstag; Montag ist Vortag (WE). A: Mo(Vortag) 1,0 + Mi (WT) 1,0. Feiertag Dienstag; Montag ist Vortag (WE). A: Mo(Vortag) 1,0 + Mi (WT) 1,0.
WE_Gesamt = 1,0 < 2,0 Auszahlung_WE = 0 ; WT = 250 . WE_Gesamt = 1,0 < 2,0 Auszahlung_WE = 0 ; Auszahlung_WT = 0 ; Auszahlung_Gesamt = 0 .
## Edge-Cases und Präzisierungen ## Edge-Cases und Präzisierungen
@ -317,12 +321,14 @@ Lieferumfang (empfohlen):
## Mini-Changelog ## Mini-Changelog
- 18.11.2025: Korrektur Variante 2: **Gesamter Bonus (WT + WE) wird nur gezahlt, wenn WE_Summe ≥ 2,0**.
Unter Schwelle: Auszahlung_Gesamt = 0 € (weder WT noch WE).
- 14.11.2025: Umstellung auf Variante 2 (streng). WE-Vergütung nur bei WE_Summe ≥ 2,0, - 14.11.2025: Umstellung auf Variante 2 (streng). WE-Vergütung nur bei WE_Summe ≥ 2,0,
anschließend Abzug 1,0 (Freitag zuerst). Unterhalb der Schwelle: WE-Auszahlung = 0 €. anschließend Abzug 1,0 (Freitag zuerst). Unterhalb der Schwelle: WE-Auszahlung = 0 €.
- 13.11.2025: Vorversion (Variante 1) mit WE-Auszahlung ab erstem WE-Dienst und Abzug nach Schwelle (ersetzt). - 13.11.2025: Vorversion (Variante 1) mit WE-Auszahlung ab erstem WE-Dienst und Abzug nach Schwelle (ersetzt).
## Kurztext (für Blatt „Regeln" als Readme-Hinweis) ## Kurztext (für Blatt „Regeln" als Readme-Hinweis)
„WE-Tag = Fr/Sa/So/Feiertag/Vortag (BL-abhängig). Variante 2 (streng): WE werden nur vergütet, wenn im Monat ≥ 2,0 WE-Einheiten erreicht werden; dann 450 €/WE und Abzug 1,0 (Freitag zuerst). WT werden immer mit 250 € vergütet. Splits anteilig. Monat und Bundesland oben wählen." „WE-Tag = Fr/Sa/So/Feiertag/Vortag (BL-abhängig). Variante 2 (streng): Gesamter Bonus (WT + WE) wird nur gezahlt, wenn im Monat ≥ 2,0 WE-Einheiten erreicht werden. Bei Erreichen der Schwelle: WT 250 €/Einheit, WE 450 €/Einheit mit Abzug 1,0 (Freitag zuerst). Unter Schwelle: 0 € Auszahlung. Splits anteilig. Monat und Bundesland oben wählen."
— Ende der README — — Ende der README —

View file

@ -117,18 +117,21 @@ def calculate_verguetung(plan_data, holidays):
# Abzug von 1.0 WE-Einheit (Freitag zuerst) # Abzug von 1.0 WE-Einheit (Freitag zuerst)
abzug_freitag = min(ABZUG, we_fri) abzug_freitag = min(ABZUG, we_fri)
abzug_andere = max(0, ABZUG - abzug_freitag) abzug_andere = max(0, ABZUG - abzug_freitag)
# Bezahlte WE-Einheiten # Bezahlte WE-Einheiten
we_bezahlt = (we_fri - abzug_freitag) + (we_other - abzug_andere) we_bezahlt = (we_fri - abzug_freitag) + (we_other - abzug_andere)
# Auszahlungen - nur wenn Schwelle erreicht
auszahlung_wt = wt * SATZ_WT
auszahlung_we = we_bezahlt * SATZ_WE
else: else:
# Schwelle nicht erreicht - keine WE-Vergütung # Schwelle nicht erreicht - kein Bonus (weder WT noch WE)
abzug_freitag = 0 abzug_freitag = 0
abzug_andere = 0 abzug_andere = 0
we_bezahlt = 0 we_bezahlt = 0
auszahlung_wt = 0
# Auszahlungen auszahlung_we = 0
auszahlung_wt = wt * SATZ_WT
auszahlung_we = we_bezahlt * SATZ_WE
auszahlung_gesamt = auszahlung_wt + auszahlung_we auszahlung_gesamt = auszahlung_wt + auszahlung_we
results.append({ results.append({

View file

@ -66,17 +66,23 @@ class BonusCalculator {
return this.getEmptyResult(); return this.getEmptyResult();
} }
// Separate qualifying and non-qualifying days // Separate qualifying days (Friday separate from others) and non-qualifying days
let qualifyingDays = 0; let qualifyingDaysFriday = 0;
let qualifyingDaysOther = 0;
let normalDays = 0; let normalDays = 0;
const dutyDetails = []; const dutyDetails = [];
duties.forEach(duty => { duties.forEach(duty => {
const isQualifying = this.isQualifyingDay(duty.date); const isQualifying = this.isQualifyingDay(duty.date);
const dayType = this.getDayTypeLabel(duty.date); const dayType = this.getDayTypeLabel(duty.date);
const isFriday = duty.date.getDay() === 5;
if (isQualifying) { if (isQualifying) {
qualifyingDays += duty.share; if (isFriday) {
qualifyingDaysFriday += duty.share;
} else {
qualifyingDaysOther += duty.share;
}
} else { } else {
normalDays += duty.share; normalDays += duty.share;
} }
@ -89,31 +95,51 @@ class BonusCalculator {
}); });
}); });
const qualifyingDaysTotal = qualifyingDaysFriday + qualifyingDaysOther;
// Check if threshold is reached // Check if threshold is reached
const thresholdReached = qualifyingDays >= this.MIN_QUALIFYING_DAYS; const thresholdReached = qualifyingDaysTotal >= this.MIN_QUALIFYING_DAYS;
let bonus = 0; let bonus = 0;
let normalDaysPaid = 0; let normalDaysPaid = 0;
let qualifyingDaysPaid = 0; let qualifyingDaysPaid = 0;
let qualifyingDaysDeducted = 0; let deductionFromFriday = 0;
let deductionFromOther = 0;
let totalDeduction = 0;
if (thresholdReached) { if (thresholdReached) {
// Deduct 1.0 qualifying day // Deduct 1.0 qualifying day with Friday priority
qualifyingDaysDeducted = 1.0; totalDeduction = 1.0;
qualifyingDaysPaid = Math.max(0, qualifyingDays - qualifyingDaysDeducted);
// First deduct from Friday
deductionFromFriday = Math.min(totalDeduction, qualifyingDaysFriday);
// Remaining deduction from other qualifying days
deductionFromOther = Math.max(0, totalDeduction - deductionFromFriday);
// Calculate paid days
const qualifyingDaysFridayPaid = Math.max(0, qualifyingDaysFriday - deductionFromFriday);
const qualifyingDaysOtherPaid = Math.max(0, qualifyingDaysOther - deductionFromOther);
qualifyingDaysPaid = qualifyingDaysFridayPaid + qualifyingDaysOtherPaid;
normalDaysPaid = normalDays; normalDaysPaid = normalDays;
// Calculate bonus // Calculate bonus
bonus = (normalDaysPaid * this.RATE_NORMAL) + (qualifyingDaysPaid * this.RATE_WEEKEND); bonus = (normalDaysPaid * this.RATE_NORMAL) + (qualifyingDaysPaid * this.RATE_WEEKEND);
} }
// If threshold not reached: no bonus paid (neither WT nor WE)
return { return {
totalDuties: duties.length, totalDuties: duties.length,
totalDaysWorked: qualifyingDays + normalDays, totalDaysWorked: qualifyingDaysTotal + normalDays,
normalDays: normalDays, normalDays: normalDays,
qualifyingDays: qualifyingDays, qualifyingDaysFriday: qualifyingDaysFriday,
qualifyingDaysOther: qualifyingDaysOther,
qualifyingDays: qualifyingDaysTotal,
thresholdReached: thresholdReached, thresholdReached: thresholdReached,
qualifyingDaysDeducted: qualifyingDaysDeducted, deductionFromFriday: deductionFromFriday,
deductionFromOther: deductionFromOther,
qualifyingDaysDeducted: totalDeduction,
normalDaysPaid: normalDaysPaid, normalDaysPaid: normalDaysPaid,
qualifyingDaysPaid: qualifyingDaysPaid, qualifyingDaysPaid: qualifyingDaysPaid,
bonusNormalDays: normalDaysPaid * this.RATE_NORMAL, bonusNormalDays: normalDaysPaid * this.RATE_NORMAL,
@ -147,8 +173,12 @@ class BonusCalculator {
totalDuties: 0, totalDuties: 0,
totalDaysWorked: 0, totalDaysWorked: 0,
normalDays: 0, normalDays: 0,
qualifyingDaysFriday: 0,
qualifyingDaysOther: 0,
qualifyingDays: 0, qualifyingDays: 0,
thresholdReached: false, thresholdReached: false,
deductionFromFriday: 0,
deductionFromOther: 0,
qualifyingDaysDeducted: 0, qualifyingDaysDeducted: 0,
normalDaysPaid: 0, normalDaysPaid: 0,
qualifyingDaysPaid: 0, qualifyingDaysPaid: 0,