Korrektur: Bonus nur bei WE-Schwelle ≥ 2,0
- Gesamter Bonus (WT + WE) wird nur gezahlt, wenn >= 2,0 WE-Tage erreicht - Unter Schwelle: Auszahlung = 0€ (weder WT noch WE) - Abzug mit Freitag-Priorität implementiert - Testfälle und Dokumentation aktualisiert Betrifft: - webapp/calculator.js: Korrekte Berechnung mit Freitag-Priorität - src/calculate.py: WT-Auszahlung nur bei Schwelle - SPECIFICATION.md: Regeln, Testfälle und Changelog aktualisiert
This commit is contained in:
parent
034b398c2c
commit
736d160586
3 changed files with 65 additions and 26 deletions
|
|
@ -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 —
|
||||||
|
|
|
||||||
|
|
@ -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({
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
Reference in a new issue