Revert deduction value to 2.0 across all files as per user feedback

Co-authored-by: Kenearos <86194771+Kenearos@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot] 2025-12-12 12:26:34 +00:00
parent 6ecfd895e1
commit db6e95f45c
13 changed files with 56 additions and 56 deletions

View file

@ -26,7 +26,7 @@ Added native Android mobile app for duty roster management with the same NRW Var
- Same WT-Tag classification
- Same compensation rates (WT: 250€, WE: 450€)
- Same threshold logic (≥ 2.0 WE units)
- Same deduction rules (1.0 unit, Friday priority)
- Same deduction rules (2.0 units, Friday priority)
- Same Variante 2 behavior (no WE compensation below threshold)
**Testing:**

View file

@ -26,7 +26,7 @@ Native Android-App für mobiles Dienstplan-Management. Siehe [android-app/README
- ✅ Automatische Erkennung von Wochenenden (FrSo), Feiertagen und Vortagen
- ✅ Vergütungslogik: WT 250€, WE 450€ (nur ab Schwelle ≥ 2,0 WE-Einheiten)
- ✅ Abzug 1,0 WE-Einheit (Freitag-Priorität) nach Erreichen der Schwelle
- ✅ Abzug 2,0 WE-Einheiten (Freitag-Priorität) nach Erreichen der Schwelle
- ✅ Vorbefüllte Monatsvorlagen mit allen Datumswerten
- ✅ Excel-kompatibel (ohne Office 365 Funktionen)

View file

@ -4,7 +4,7 @@ Stand: 14.11.2025 (Deutschland)
## Ziel
Diese README beschreibt vollständig, wie eine Excel-Arbeitsmappe aufgebaut wird, die Monatsdienste erfasst und automatisch die Vergütung ermittelt inkl. Erkennung von Wochenend-/Feiertagsdiensten (inkl. Vortag), Schwellenlogik und Abzug 1,0 WE-Einheit nach Erreichen der Schwelle. Variante 2 (streng) ist aktiv: WE-Dienste werden nur vergütet, wenn im Monat mindestens 2,0 WE-Einheiten erreicht werden; sonst 0 €. Wochentage (kein WE) werden ebenfalls nur bei Erreichen der WE-Schwelle vergütet.
Diese README beschreibt vollständig, wie eine Excel-Arbeitsmappe aufgebaut wird, die Monatsdienste erfasst und automatisch die Vergütung ermittelt inkl. Erkennung von Wochenend-/Feiertagsdiensten (inkl. Vortag), Schwellenlogik und Abzug 2,0 WE-Einheiten nach Erreichen der Schwelle. Variante 2 (streng) ist aktiv: WE-Dienste werden nur vergütet, wenn im Monat mindestens 2,0 WE-Einheiten erreicht werden; sonst 0 €. Wochentage (kein WE) werden ebenfalls nur bei Erreichen der WE-Schwelle vergütet.
Hinweise:
- Region: Deutschland, Bundesland wählbar (steuert Feiertage).
@ -30,7 +30,7 @@ Hinweise:
- **WE** (WE-Tag):
- 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,
anschließend Abzug genau 1,0 WE-Einheit (max. 1× pro Person/Monat).
anschließend Abzug genau 2,0 WE-Einheiten (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.
### Splits/Anteile
@ -51,7 +51,7 @@ Hinweise:
- Satz_WT = 250
- Satz_WE = 450
- WE_Schwelle = 2,0
- Abzug_nach_WE_Schwelle = 1,0
- Abzug_nach_WE_Schwelle = 2,0
- BL_Auswahl = Dropdown (z. B. BW, BY, BE, …)
- Monat_Auswahl = Datum (erster Tag des Zielmonats, z. B. 01.11.2025)
- Variante = 2 (fix auf „streng")

View file

@ -123,7 +123,7 @@ Edit `PayrollCalculator.kt` and modify the constants:
- `RATE_WT`: Weekday rate (default 250€)
- `RATE_WE`: Weekend rate (default 450€)
- `WE_THRESHOLD`: Threshold for WE compensation (default 2.0)
- `DEDUCTION_AFTER_THRESHOLD`: Deduction amount (default 1.0)
- `DEDUCTION_AFTER_THRESHOLD`: Deduction amount (default 2.0)
### Adding Holidays

View file

@ -24,7 +24,7 @@ class PayrollCalculator {
private const val RATE_WT = 250.0 // Satz_WT
private const val RATE_WE = 450.0 // Satz_WE
private const val WE_THRESHOLD = 2.0 // WE_Schwelle
private const val DEDUCTION_AFTER_THRESHOLD = 1.0 // Abzug_nach_WE_Schwelle
private const val DEDUCTION_AFTER_THRESHOLD = 2.0 // Abzug_nach_WE_Schwelle
private const val TOLERANCE = 0.0001 // For floating-point comparisons
}

View file

@ -52,7 +52,7 @@ class PayrollCalculatorTest {
/**
* Test Case 2: Exactly at threshold (2.0 WE)
* Expected: WE payout = 450 (1.0 unit after deduction), threshold reached
* Expected: WE payout = 0 (0.0 units after 2.0 deduction), threshold reached
*/
@Test
fun testExactlyAtThreshold() {
@ -67,14 +67,14 @@ class PayrollCalculatorTest {
val result = results[0]
assertEquals(2.0, result.weTotal, 0.001)
assertTrue(result.thresholdReached)
assertEquals(1.0, result.deductionTotal, 0.001)
assertEquals(1.0, result.wePaid, 0.001)
assertEquals(450.0, result.payoutWE, 0.001)
assertEquals(2.0, result.deductionTotal, 0.001)
assertEquals(0.0, result.wePaid, 0.001)
assertEquals(0.0, result.payoutWE, 0.001)
}
/**
* Test Case 3: Over threshold (3.5 WE)
* Expected: WE payout = 1125 (2.5 units after 1.0 deduction)
* Expected: WE payout = 675 (1.5 units after 2.0 deduction)
*/
@Test
fun testOverThreshold() {
@ -91,8 +91,8 @@ class PayrollCalculatorTest {
val result = results[0]
assertEquals(3.5, result.weTotal, 0.001)
assertTrue(result.thresholdReached)
assertEquals(2.5, result.wePaid, 0.001)
assertEquals(1125.0, result.payoutWE, 0.001)
assertEquals(1.5, result.wePaid, 0.001)
assertEquals(675.0, result.payoutWE, 0.001)
}
/**
@ -115,9 +115,9 @@ class PayrollCalculatorTest {
assertEquals(0.4, result.weFriday, 0.001)
assertEquals(1.6, result.weOther, 0.001)
assertEquals(0.4, result.deductionFriday, 0.001) // All Friday deducted first
assertEquals(0.6, result.deductionOther, 0.001) // Rest from other (0.6 to reach 1.0 total)
assertEquals(1.0, result.wePaid, 0.001)
assertEquals(450.0, result.payoutWE, 0.001)
assertEquals(1.6, result.deductionOther, 0.001) // Rest from other (1.6 to reach 2.0 total)
assertEquals(0.0, result.wePaid, 0.001)
assertEquals(0.0, result.payoutWE, 0.001)
}
/**
@ -145,11 +145,11 @@ class PayrollCalculatorTest {
assertFalse(resultA.thresholdReached)
assertEquals(0.0, resultA.payoutWE, 0.001)
// B: above threshold
// B: above threshold (2.5 WE - 2.0 deduction = 0.5 paid)
assertTrue(resultB.thresholdReached)
assertEquals(2.5, resultB.weTotal, 0.001)
assertEquals(1.5, resultB.wePaid, 0.001)
assertEquals(675.0, resultB.payoutWE, 0.001)
assertEquals(0.5, resultB.wePaid, 0.001)
assertEquals(225.0, resultB.payoutWE, 0.001)
}
private fun parseDate(dateString: String): Date {

View file

@ -56,7 +56,7 @@ Die ältere Implementierung nutzt eine andere Logik:
- **WT-Tage** werden bei Erreichen der Schwelle mit 250€ vergütet
- **WE-Tage** nur vergütet wenn ≥ 2.0 WE-Einheiten:
- Bei Erreichen: 450€ pro WE-Tag
- Dann Abzug von 1.0 WE-Einheit (Freitag-Priorität)
- Dann Abzug von 2.0 WE-Einheiten (Freitag-Priorität)
- Unter Schwellenwert: Keine Bonuszahlung (weder WE noch WT)
### Wichtiger Unterschied - Beispiel
@ -179,13 +179,13 @@ adb install app/build/outputs/apk/debug/app-debug.apk
- 2 × Montag (2.0)
- 2 × Samstag (2.0)
- Erwartung:
- 2.0 qualifizierende → -1.0 Abzug → 1.0 bezahlt
- Bonus: (2 × 250€) + (1 × 450€) = **950€**
- 2.0 qualifizierende → -2.0 Abzug → 0.0 bezahlt
- Bonus: (2 × 250€) + (0 × 450€) = **500€**
### Testfall 4: Feiertag + Vortag
- 1 × Donnerstag vor Karfreitag (qualifizierend!)
- 1 × Karfreitag (Feiertag, qualifizierend!)
- Erwartung: 2.0 qualifizierende → -1.0 → 1.0 × 450€ = **450€**
- Erwartung: 2.0 qualifizierende → -2.0 → 0.0 × 450€ = **0€**
## Häufige Anpassungen
@ -205,7 +205,7 @@ this.RATE_WEEKEND = 500; // Statt 450
### Abzug ändern (Web-App)
Der Abzug ist als Konstante in `webapp/calculator.js` definiert:
```javascript
this.DEDUCTION_AMOUNT = 1.0; // Im Constructor
this.DEDUCTION_AMOUNT = 2.0; // Im Constructor
```
Um den Abzugswert zu ändern, einfach diesen Wert anpassen.

View file

@ -61,7 +61,7 @@ def _populate_readme(ws):
rules = [
"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 bei Erreichen der WE-Schwelle mit 250 € vergütet.",
"dann 450 €/WE und Abzug 2,0 (Freitag zuerst). WT werden bei Erreichen der WE-Schwelle mit 250 € vergütet.",
"Splits anteilig. Monat und Bundesland in 'Regeln' wählen.",
"",
"Schritte:",
@ -83,7 +83,7 @@ def _populate_rules(ws):
("Satz_WT", 250, "Euro für jeden Werktagsdienst (MoDo, sofern kein WE-Tag)"),
("Satz_WE", 450, "Euro für jeden WE-Tag (FrSo, Feiertag, Vortag Feiertag)"),
("WE_Schwelle", 2.0, "Ab dieser WE-Anzahl wird vergütet (sonst 0 €)"),
("Abzug_nach_WE_Schwelle", 1.0, "Einheiten, die nach Erreichen der Schwelle abgezogen werden"),
("Abzug_nach_WE_Schwelle", 2.0, "Einheiten, die nach Erreichen der Schwelle abgezogen werden"),
("BL_Auswahl", "NRW", "Bundesland (steuert Feiertage)"),
("Monat_Auswahl", date(2025, 11, 1), "Erster Tag des Zielmonats"),
("Variante", 2, "Fix: 2 = streng (WE nur bei Schwelle ≥ 2,0)"),

View file

@ -14,7 +14,7 @@ from collections import defaultdict
SATZ_WT = 250 # Euro für Werktag
SATZ_WE = 450 # Euro für Wochenende
WE_SCHWELLE = 2.0 # Mindestanzahl WE-Dienste für Vergütung
ABZUG = 1.0 # Abzug nach Erreichen der Schwelle
ABZUG = 2.0 # Abzug nach Erreichen der Schwelle
def load_holidays(wb):

View file

@ -21,7 +21,7 @@ Eine Web-Anwendung zur Berechnung von Bonuszahlungen für Wochenend- und Feierta
### Bonusberechnung
1. **Schwellenwert**: Mindestens **2.0 qualifizierende Tage** im Monat erforderlich
2. **Abzug**: Bei Erreichen des Schwellenwerts wird **1.0 qualifizierender Tag** abgezogen (Freitag-Priorität)
2. **Abzug**: Bei Erreichen des Schwellenwerts werden **2.0 qualifizierende Tage** abgezogen (Freitag-Priorität)
3. **Vergütung**:
- Normale Tage: **250€** pro Tag
- Qualifizierende Tage (WE/Feiertag): **450€** pro Tag
@ -34,9 +34,9 @@ Mitarbeiter hat im Monat:
**Berechnung**:
- Qualifizierende Tage: 3.0 (Schwellenwert erreicht ✓)
- Abzug: -1.0 qualifizierender Tag
- Bezahlt: 3 normale Tage + 2 qualifizierende Tage
- **Bonus**: (3 × 250€) + (2 × 450€) = **1.650€**
- Abzug: -2.0 qualifizierende Tage
- Bezahlt: 3 normale Tage + 1 qualifizierender Tag
- **Bonus**: (3 × 250€) + (1 × 450€) = **1.200€**
## Installation & Nutzung

View file

@ -84,8 +84,8 @@ Dienste:
Erwartung:
- Qualifizierende Tage: 2.0
- Schwellenwert: ✅ Erreicht
- Abzug: -1.0
- Bezahlt: 1.0 × 450€ = 450€
- Abzug: -2.0
- Bezahlt: 0.0 × 450€ = 0€
```
### Beispiel 2: Gemischte Dienste
@ -96,8 +96,8 @@ Dienste:
Erwartung:
- Normale Tage: 2.0 × 250€ = 500€
- Qualifizierende Tage: (2.0 - 1.0) × 450€ = 450€
- Gesamt: 950€
- Qualifizierende Tage: (2.0 - 2.0) × 450€ = 0€
- Gesamt: 500€
```
### Beispiel 3: Halbe Dienste
@ -110,8 +110,8 @@ Dienste:
Erwartung:
- Normale Tage: 0.5 × 250€ = 125€
- Qualifizierende Tage: (2.5 - 1.0) × 450€ = 675€
- Gesamt: 800€
- Qualifizierende Tage: (2.5 - 2.0) × 450€ = 225€
- Gesamt: 350€
```
## Tests erweitern

View file

@ -8,7 +8,7 @@ class BonusCalculator {
this.RATE_NORMAL = 250; // Normal day rate (not weekend/holiday)
this.RATE_WEEKEND = 450; // Weekend/holiday rate
this.MIN_QUALIFYING_DAYS = 2.0; // Minimum qualifying days to trigger bonus
this.DEDUCTION_AMOUNT = 1.0; // Deduction after reaching threshold
this.DEDUCTION_AMOUNT = 2.0; // Deduction after reaching threshold
}
/**

View file

@ -191,7 +191,7 @@ runner.test('Berechnung: Unter Schwellenwert (1.0 WE-Tag) = 0€', (t) => {
t.assertEqual(result.totalBonus, 0, 'Bonus sollte 0€ sein');
});
runner.test('Berechnung: Genau 2.0 WE-Tage = 450€', (t) => {
runner.test('Berechnung: Genau 2.0 WE-Tage = 0€', (t) => {
const holidays = new HolidayProvider();
const calculator = new BonusCalculator(holidays);
@ -204,12 +204,12 @@ runner.test('Berechnung: Genau 2.0 WE-Tage = 450€', (t) => {
t.assertEqual(result.qualifyingDays, 2.0, 'Sollte 2.0 qualifizierende Tage haben');
t.assertTrue(result.thresholdReached, 'Schwellenwert sollte erreicht sein');
t.assertEqual(result.qualifyingDaysDeducted, 1.0, 'Sollte 1.0 Tag abziehen');
t.assertEqual(result.qualifyingDaysPaid, 1.0, 'Sollte 1.0 Tag bezahlen');
t.assertEqual(result.totalBonus, 450, 'Bonus sollte 450€ sein');
t.assertEqual(result.qualifyingDaysDeducted, 2.0, 'Sollte 2.0 Tage abziehen');
t.assertEqual(result.qualifyingDaysPaid, 0.0, 'Sollte 0.0 Tage bezahlen');
t.assertEqual(result.totalBonus, 0, 'Bonus sollte 0€ sein');
});
runner.test('Berechnung: 2x halbe WE-Dienste = 450€ (genau Schwelle, nach Abzug 1.0)', (t) => {
runner.test('Berechnung: 2x halbe WE-Dienste = 0€ (genau Schwelle, nach Abzug 2.0)', (t) => {
const holidays = new HolidayProvider();
const calculator = new BonusCalculator(holidays);
@ -224,11 +224,11 @@ runner.test('Berechnung: 2x halbe WE-Dienste = 450€ (genau Schwelle, nach Abzu
t.assertEqual(result.qualifyingDays, 2.0, 'Sollte 2.0 qualifizierende Tage haben (4×0.5)');
t.assertTrue(result.thresholdReached, 'Schwellenwert sollte erreicht sein');
t.assertEqual(result.qualifyingDaysPaid, 1.0, 'Sollte 1.0 Tag bezahlen nach Abzug');
t.assertEqual(result.totalBonus, 450, 'Bonus sollte 450€ sein');
t.assertEqual(result.qualifyingDaysPaid, 0.0, 'Sollte 0.0 Tage bezahlen nach Abzug');
t.assertEqual(result.totalBonus, 0, 'Bonus sollte 0€ sein');
});
runner.test('Berechnung: 3 WE-Tage = 900€', (t) => {
runner.test('Berechnung: 3 WE-Tage = 450€', (t) => {
const holidays = new HolidayProvider();
const calculator = new BonusCalculator(holidays);
@ -241,8 +241,8 @@ runner.test('Berechnung: 3 WE-Tage = 900€', (t) => {
const result = calculator.calculateMonthlyBonus(duties);
t.assertEqual(result.qualifyingDays, 3.0, 'Sollte 3.0 qualifizierende Tage haben');
t.assertEqual(result.qualifyingDaysPaid, 2.0, 'Sollte 2.0 Tage bezahlen (3-1)');
t.assertEqual(result.totalBonus, 900, 'Bonus sollte 900€ sein (2×450€)');
t.assertEqual(result.qualifyingDaysPaid, 1.0, 'Sollte 1.0 Tage bezahlen (3-2)');
t.assertEqual(result.totalBonus, 450, 'Bonus sollte 450€ sein (1×450€)');
});
runner.test('Berechnung: Normale Tage + WE-Tage gemischt', (t) => {
@ -261,10 +261,10 @@ runner.test('Berechnung: Normale Tage + WE-Tage gemischt', (t) => {
t.assertEqual(result.normalDays, 2.0, 'Sollte 2.0 normale Tage haben');
t.assertEqual(result.qualifyingDays, 2.0, 'Sollte 2.0 qualifizierende Tage haben');
t.assertEqual(result.normalDaysPaid, 2.0, 'Sollte 2.0 normale Tage bezahlen');
t.assertEqual(result.qualifyingDaysPaid, 1.0, 'Sollte 1.0 qualifizierenden Tag bezahlen');
t.assertEqual(result.qualifyingDaysPaid, 0.0, 'Sollte 0.0 qualifizierende Tage bezahlen');
t.assertEqual(result.bonusNormalDays, 500, 'Normale Tage: 2×250€ = 500€');
t.assertEqual(result.bonusQualifyingDays, 450, 'WE-Tage: 1×450€ = 450€');
t.assertEqual(result.totalBonus, 950, 'Gesamt: 950€');
t.assertEqual(result.bonusQualifyingDays, 0, 'WE-Tage: 0×450€ = 0€');
t.assertEqual(result.totalBonus, 500, 'Gesamt: 500€');
});
runner.test('Berechnung: Halbe Dienste korrekt berechnet', (t) => {
@ -282,10 +282,10 @@ runner.test('Berechnung: Halbe Dienste korrekt berechnet', (t) => {
t.assertEqual(result.normalDays, 0.5, 'Sollte 0.5 normale Tage haben');
t.assertEqual(result.qualifyingDays, 2.5, 'Sollte 2.5 qualifizierende Tage haben');
t.assertEqual(result.qualifyingDaysPaid, 1.5, 'Sollte 1.5 qualifizierende Tage bezahlen');
t.assertEqual(result.qualifyingDaysPaid, 0.5, 'Sollte 0.5 qualifizierende Tage bezahlen');
t.assertEqual(result.bonusNormalDays, 125, 'Normale Tage: 0.5×250€ = 125€');
t.assertEqual(result.bonusQualifyingDays, 675, 'WE-Tage: 1.5×450€ = 675€');
t.assertEqual(result.totalBonus, 800, 'Gesamt: 800€');
t.assertEqual(result.bonusQualifyingDays, 225, 'WE-Tage: 0.5×450€ = 225€');
t.assertEqual(result.totalBonus, 350, 'Gesamt: 350€');
});
runner.test('Berechnung: Feiertag + Vortag', (t) => {
@ -301,7 +301,7 @@ runner.test('Berechnung: Feiertag + Vortag', (t) => {
t.assertEqual(result.qualifyingDays, 2.0, 'Sollte 2.0 qualifizierende Tage haben');
t.assertTrue(result.thresholdReached, 'Schwellenwert sollte erreicht sein');
t.assertEqual(result.totalBonus, 450, 'Bonus sollte 450€ sein (2.0 - 1.0 = 1.0 × 450€)');
t.assertEqual(result.totalBonus, 0, 'Bonus sollte 0€ sein (2.0 - 2.0 = 0.0 × 450€)');
});
runner.test('Berechnung: Keine Dienste = 0€', (t) => {