diff --git a/.agents/MEMORY.md b/.agents/MEMORY.md index 0532c22..ede2906 100644 --- a/.agents/MEMORY.md +++ b/.agents/MEMORY.md @@ -2,6 +2,45 @@ ## Current focus +**AP16 Karenz-/Familien-Reste umgesetzt (8.13 freigegeben) — Kern 73/73, Privat 199/199 Tests grün** +(2026-09-15, uncommittet bis Nutzerfreigabe; Privat **19.0.17.0.0**): + +- **8.13 komplett freigegeben** (Nutzer, 2026-09-15) — Mutterschutz-/Karenz- + Block im R1-Scope; §-Verifikation am Originalwortlaut (AVRAG §§ 11/11a/ + 14a–14e/15a, VKG § 1a, MSchG § 15f, BMSVG § 7 Abs 4–6, ASVG §§ 163/166). + KB-Synthesen (3 Subagenten): lb-kar/elt/kzs/pap/kbg — arbeitsrechtliche + Kerne + Abrechnungstypologie (alle Voll-Freistellungen entgeltlos, + Leistungen extern: AK/AMS/BASB/KV/Gesundheitskasse; elt-SV/LSt/BMSVG-Lücken + verifizieren = erledigt über RIS/Bestandsmaschinerie; ⚠ lb-elt-06 §-8/§-8a- + VKG-Zuordnung dokumentiert). +- **Privat (19.0.17.0.0):** 6 neue WETs/Urlaubsarten (ATP_BILDUNGSKARENZ, + ATP_HOSPIZKARENZ, ATP_KINDERKRANK, ATP_KINDERREHAB, ATP_PFLEGEKARENZ, + ATP_PAPAMONAT); Regel `KARENZ_ENTGELT` (seq 48, negatives Tagesatz-Muster + wie MSCH_ENTGELT, Ausschlüsse im geteilten `_at_efz_tagesatz`); Modell + `models/karenz.py` (Status/Tage/Karenzarten-Map); vr.event-An-/Abmeldungen + aus Karenz (M4 Tag vor Beginn, M3 Rückkehr, entfällt bei DV-Ende; Karenz- + art-Strings im Protokoll, eSV-Sonderformate GP7); fiktive BV mit §-7-Abs-4- + **Z-1–3-Brücke** (`_at_msch_referenz_stichtag_und_anzahl` in mutterschutz.py); + BV-Vor-Entgelt-Feld `l10n_at_bv_vor_entgelt` (§ 6 Abs 4 BMSVG, Pflegeteilzeit) + in `_at_bv_basis_laufen`; §-23-Abs-8-Hochrechnungs-Vorschau im Wizard + (`abf_alt_hochrechnung_vorschau`); Sonderwochengeld-Erkennung (§ 163 ASVG) + + Ruhen-Warnungen (§ 166 ASVG) im msch_info. +- **Tests:** neu `tests/test_karenz_private.py` 6 (Entgelt-Stopp, VR-Events + inkl. Idempotenz + Protokoll, Z-1-Brücke, §-6-Abs-4-BV, § 163/§ 166, + Wizard-Hochrechnung); Suite **214/214** (73 Kern + 199 Privat). Test-Details: + hr.leave-Quere-Überlappung nur mit `leave_skip_date_check`+`leave_skip_state_check` + + direktem state='validate' umgehbar (Krankheit während Karenz); + EFZ-Phasen lesen Leaves, `voll_arbeitstage` Work-Entries — überlappte Tage + brauchen direkte hr.work.entry-Records; `hr.leave` hat KEIN + work_entry_type_id (nur der Leave-Typ). +- **Offen AP16-Reste (docs/rechenfaelle/karenz.md):** Urlaubs-Aliquotierung aus + Karenz (§ 15f Abs 2 MSchG, hr_holidays-Allokationsseite), Leave-Überlappungs- + Policy (allow_request_on_top/time_type — Mandanten-Entscheid), eSV-Sonderformate + (Karenzart-Block, GP7), Sonderwochengeld-Vollrechnung (§ 141/§ 125/§ 108f = + Anzeige-Stufe), lb-kar-02-13-Wochen-Über-/§ 2i-AVRAG-Konflikt (dokumentiert). +- **Offen gesamt:** AP17 Meldewesen-Reste (L17/§ 109a/§ 109b, Geringfügigen- + Jahreszahlung), AP18+ (Gates). Nicht committen ohne Nutzeranforderung. + **AP14-B Prämien bei Beendigung (§ 16 AngG) umgesetzt — Kern 73/73, Privat 191/191 Tests grün** (2026-09-15, uncommittet bis Nutzerfreigabe; Privat **19.0.16.0.0**): diff --git a/addons/l10n_at_hr_payroll_private/__manifest__.py b/addons/l10n_at_hr_payroll_private/__manifest__.py index 2325739..04dbc92 100644 --- a/addons/l10n_at_hr_payroll_private/__manifest__.py +++ b/addons/l10n_at_hr_payroll_private/__manifest__.py @@ -1,7 +1,7 @@ # Part of the odoo-at-payroll project. License: LGPL-3. { 'name': 'Österreich - Privatwirtschaft Personalverrechnung (General-AT)', - 'version': '19.0.16.0.0', + 'version': '19.0.17.0.0', 'author': 'Florian Egger', 'website': 'https://gem360.at', 'category': 'Human Resources/Payroll', @@ -109,6 +109,7 @@ gem360-Brücke). 'data/salary_rules_lst_private.xml', 'data/salary_rules_efzg_private.xml', 'data/salary_rules_msch_private.xml', + 'data/salary_rules_karenz_private.xml', 'data/salary_rules_pfaendung_private.xml', 'data/salary_rules_sb_private.xml', diff --git a/addons/l10n_at_hr_payroll_private/data/absence_private.xml b/addons/l10n_at_hr_payroll_private/data/absence_private.xml index e6a33b4..5460624 100644 --- a/addons/l10n_at_hr_payroll_private/data/absence_private.xml +++ b/addons/l10n_at_hr_payroll_private/data/absence_private.xml @@ -113,4 +113,117 @@ + + + + Bildungskarenz (§ 11 AVRAG) + 4 + BLK + ATP_BILDUNGSKARENZ + + + + + Familienhospizkarenz — Sterbebegleitung (§ 14a AVRAG) + 5 + HSP + ATP_HOSPIZKARENZ + + + + + Begleitung schwerst erkrankter Kinder (§ 14b AVRAG) + 7 + KKB + ATP_KINDERKRANK + + + + + Freistellung Kinderrehabilitation (§ 14e AVRAG) + 10 + KRE + ATP_KINDERREHAB + + + + + Pflegekarenz (§ 14c AVRAG) + 1 + PFG + ATP_PFLEGEKARENZ + + + + + Papa-Monat (§ 1a VKG) + 0 + PAP + ATP_PAPAMONAT + + + + + Bildungskarenz (§ 11 AVRAG) + + hr + day + + + + + + Familienhospizkarenz — Sterbebegleitung + + hr + day + + + + + + Begleitung schwerst erkrankter Kinder + + hr + day + + + + + + Freistellung Kinderrehabilitation + + hr + day + + + + + + Pflegekarenz (§ 14c AVRAG) + + hr + day + + + + + + Papa-Monat (§ 1a VKG) + + hr + day + + + + \ No newline at end of file diff --git a/addons/l10n_at_hr_payroll_private/data/salary_rules_karenz_private.xml b/addons/l10n_at_hr_payroll_private/data/salary_rules_karenz_private.xml new file mode 100644 index 0000000..6eff835 --- /dev/null +++ b/addons/l10n_at_hr_payroll_private/data/salary_rules_karenz_private.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + Karenz-Freistellung (kein Dienstgeber-Entgelt; Leistungen extern) + KARENZ_ENTGELT + + + + python + 0 + ]]> + code + laufend + + + \ No newline at end of file diff --git a/addons/l10n_at_hr_payroll_private/docs/rechenfaelle/karenz.md b/addons/l10n_at_hr_payroll_private/docs/rechenfaelle/karenz.md new file mode 100644 index 0000000..0648a7b --- /dev/null +++ b/addons/l10n_at_hr_payroll_private/docs/rechenfaelle/karenz.md @@ -0,0 +1,113 @@ +# Karenz-/Familien-Freistellungen — Rechenfälle AP16 (8.13 freigegeben) + +Karenz-Familie im Privatmodul: unbezahlte Voll-Freistellungen (Eltern- +karenz generisch, Bildungs-, Familienhospiz-/Sterbebegleitungs-, +Kinderkranken-Begleitungs-, Kinderrehabilitations- und Pflegekarenz, +Papa-Monat) mit Entgelt-Stopp, eSV-An-/Abmeldungen, fiktiver BV mit +§-7-Abs-4-Brücke, §-6-Abs-4-BV bei Teilzeit, Sonderwochengeld (§ 163 +ASVG) und Ruhen (§ 166 ASVG). Grundlagen: KB lb-kar-01…06, lb-elt- +01…06, lb-kzs-01…07, lb-pap-01/02, lb-kbg-06 (Stand 2026-07) und die +verifizierten Originaltexte in `.ris/` (AVRAG, VKG, MSchG, KBGG, +FamZeitbG, BMSVG § 7, ASVG §§ 162/163/166). + +## RF-KAR-1 — Entgelt-Stopp je Karenz-Art (test_01) + +Bildungskarenz 1.–15. 11. 2026 (§ 11 AVRAG: 2–12 Monate, teilbar ≥ 2, +Rahmenfrist 4 Jahre, kein Rechtsanspruch — Einvernehmen): das +Karenzgeld zahlt die **AK** (nicht der AG) — `KARENZ_ENTGELT` stoppt +das Dienstgeber-Entgelt je Arbeitstag (negatives Tagesatz-Muster von +`MSCH_ENTGELT`, geteilter Tagesatz-Helfer `_at_efz_tagesatz`, der +Ausschlüsse mit EFZG/MSCH teilt). 10 AT → −(2.059,41/21) × 10. +Gleiches Muster für Hospizkarenz (§ 14a, 3/6 Monate), Begleitung +schwerst erkrankter Kinder (§ 14b, 5/9 Monate), Kinderrehabilitation +(§ 14e, 4 Wochen/Jahr), Pflegekarenz (§ 14c, 1–3 Monate, unteilbar) +und Papa-Monat (§ 1a VKG, 1 Monat). + +## RF-KAR-2 — eSV-An-/Abmeldungen (vr.event) + +- Abmeldung zum **Ende des Entgeltanspruchs** = Tag vor dem Karenz- + beginn; Anmeldung zum ersten Arbeitstag nach dem Karenzende — + entfällt, wenn das DV mit der Karenz endet (normale Vertrags- + Abmeldung). +- **Papa-Monat/Familienzeit tagesgenau:** Abmeldung am Tag **vor** + Beginn der Familienzeit (§ 11 Abs 3 lit a ASVG — unbezahlter Urlaub + hält die Pflichtversicherung nur bis 1 Monat, nicht bei Familien- + zeit; lb-pap-01), Anmeldung vor Arbeitsantritt. +- Karenzart-Strings („Familienhospizkarenz/Pflegekarenz An-/Abmeldung", + lb-kzs-05) laufen als Protokoll-Info; erweiterte eSV-Sonderformate + = GP7-Rest. Idempotenz über die Registry (employee, art, adatum). + +## RF-KAR-3 — Fiktive BV mit Karenz-Brücke (§ 7 Abs 4 Z 1–3 BMSVG) + +Mutterschutz-Frist 1.–15. 11. 2026 (Verbot 1) → Elternkarenz +16. 11. 2026–28. 2. 2027 → neuerliches Beschäftigungsverbot 1. 3. 2027 +(Z 1, unmittelbar nach der Karenz): die fiktive BV-Basis des +neuerlichen Verbots bemisst sich nach dem **Monatsentgelt des +Kalendermonats vor dem Verbot 1** — Referenzmonate Aug/Sep/Okt 2026 +(statt Dez 2026–Feb 2027, die karenzbedingt leer wären). Z 2 +(Beschäftigung < 3 Monate zwischen Karenz und Verbot): gleicher +Stichtag. Z 3 (neues DV binnen 3 Monaten nach Beendigung des +karenzierten DV mit Karenz): der letzte volle Kalendermonat vor dem +neuerlichen Verbot als EINZELNER Referenzmonat („volles Monatsentgelt"). + +## RF-KAR-4 — BV-Vor-Entgelt bei Pflegeteilzeit (§ 6 Abs 4 BMSVG) + +20-h-Teilzeit (work_time_rate 0,5) mit Vor-Reduzierungs-Entgelt +2.059,41 €: die BV-Basis läuft während der Maßnahme auf dem +Vor-Reduzierungs-Entgelt (§ 6 Abs 4 BMSVG), nicht auf der reduzierten +laufenden Basis (1.029,71 €). Bildungs-/Hospizteilzeit analog über +das Feld `l10n_at_bv_vor_entgelt` am Vertrag. + +## RF-KAR-5 — Sonderwochengeld (§ 163 ASVG) und Ruhen (§ 166 ASVG) + +- Mutterschutz-Frist **innerhalb** einer Karenz: kein Wochengeld + (§ 162), sondern **Sonderwochengeld** — Zahlung durch die ÖGK + (erhöhtes Krankengeld § 141 Abs 2, Bemessungsgrundlage § 125 aus + dem Arbeitsverdienst vor dem Ende des letzten Entgeltanspruchs, + §-108f-Anpassung); Ausweis im Mutterschutz-Status. +- Ruhen (§ 166 Abs 1): Fortbezug > 50 % (Entgeltfortzahlung 100 %- + Phase) → Ruhen zur Gänze, = 50 % (Halbphase) → hälftig; Zeiten der + Urlaubsersatzleistung/UEL/KE gelten nicht als weitergeleistete + Bezüge; Z 3 Erwerbstätigkeit (in Höhe des Einkommens) und Z 4 + Pflegekarenzgeld (in dessen Höhe) = Bezugszeitraum-Daten der ÖGK + (Hinweis). + +## RF-KAR-6 — Elternteilzeit: Entgelt und Beendigungs-Basen + +- Entgelt: letzter voller Lohn/Gehalt, aliquotiert nach der + herabgesetzten Wochenstundenzahl (lb-elt-02) — maschinell über + `work_time_rate` der Version (Versionskalender ÷ Firmenzalender). +- SZ-Mischberechnung (§ 15j Abs 7 MSchG / § 8b Abs 7 VKG): B13/B14 + bemessen je Monat aus dem jeweiligen (reduzierten) Monatsentgelt × + Dienstzeitfaktor — VZ-/TZ-Anteile im Kalenderjahr ergeben sich + automatisch. +- Endabrechnungs-Wizard: §-23-Abs-8-Hochrechnung (DV während + MSchG-/VKG-Teilzeit: frühere Normalarbeitszeit = Monatsentgelt ÷ + work_time_rate, Vorschau); für Arbeitnehmerkündigung § 23a Abs 4a + (5-Jahres-Ø der geleisteten Arbeitszeit ohne Karenz, manuell); + UEL § 10 Abs 4 UrlG (überwiegende Arbeitszeit des Urlaubsjahres). + Dienstzeitanrechnung: Karenzzeiten je Kind in vollem Umfang + (§ 15f Abs 1 MSchG, Geburten ab 1. 8. 2019, max. 22./24. Lebens- + monat); Papa-Monat anrechnen (§ 1a Abs 7 VKG). + +## Offene Punkte (AP16-Reste) + +- **Urlaubs-Aliquotierung aus Karenz** (§ 15f Abs 2 MSchG: 30 WT/25 AT + : 365 × aktive KT, Teiltage aufrunden) greift in die hr_holidays- + Allokationsseite — eigener Folgeschritt; Urlaubsverjährung + verlängert sich um die Karenz (§ 4 Abs 5 UrlG). +- **Krankheit während Karenz/Mutterschutz** ist als gleichzeitige + Abwesenheit in hr.leave nur mit einer Überlappungs-Policy- + Entscheidung umsetzbar (Odoo sperrt Quere-Überlappungen; + `allow_request_on_top` erfordert time_type „other") — + Mandanten-Entscheid; der Test umgeht mit dem offiziellen + leave_skip_date_check-Bypass. +- Fristen der AG↔AN-Kommunikation (3-/2-Monats-Bekanntgaben, + 5/10-Arbeitstage, Begründungspflichten 5 Kalendertage) = Help-Texte; + keine Fristen-Automatik. +- eSV-Sonderformate (Karenzart-Block „Familienhospizkarenz/Pflege- + karenz") und mBGM-Feinschliff = GP7; Sonderwochengeld-Vollrechnung + (§ 141 Abs 2/§ 125/§ 108f) = Anzeige-Stufe, kein Eigenrechenpfad. +- lb-kar-02: Folgen der 13-Wochen-Überschreitung + § 2i-AVRAG-Konflikt + und lb-elt-06 §-8-/§-8a-VKG-Zuordnung — keine Implementierung auf + kontroversen Literaturansichten (dokumentiert ⚠). \ No newline at end of file diff --git a/addons/l10n_at_hr_payroll_private/models/__init__.py b/addons/l10n_at_hr_payroll_private/models/__init__.py index 2e1412f..6c0cfb4 100644 --- a/addons/l10n_at_hr_payroll_private/models/__init__.py +++ b/addons/l10n_at_hr_payroll_private/models/__init__.py @@ -14,6 +14,7 @@ from . import buchungsbeleg from . import wien_dag from . import efzg from . import mutterschutz +from . import karenz from . import aeb from . import pfaendung from . import pfaendung_report diff --git a/addons/l10n_at_hr_payroll_private/models/abfertigung_private.py b/addons/l10n_at_hr_payroll_private/models/abfertigung_private.py index 5e0fa86..87f8a87 100644 --- a/addons/l10n_at_hr_payroll_private/models/abfertigung_private.py +++ b/addons/l10n_at_hr_payroll_private/models/abfertigung_private.py @@ -206,12 +206,18 @@ class HrPayslip(models.Model): Erstfenster — 30-Tage-Monat (KB-Beispiel 4.500/30 × 17 = 2.550); Teilmonate über die Lohnsteuertage, um Doppelabzüge zu vermeiden (das Entgelt ist für die Teilzeit bereits - aliquotiert).""" + aliquotiert). AP16 (§ 6 Abs 4 BMSVG): während einer + Arbeitszeit-Reduzierung mit gesetztem Feld + l10n_at_bv_vor_entgelt (z. B. Pflegeteilzeit § 14d AVRAG) + läuft der Beitrag auf dem Vor-Reduzierungs-Entgelt.""" self.ensure_one() pflichtig_ab, kurz_av = self._at_bv_fenster() if kurz_av: return 0.0 + vor_entgelt = self.version_id.l10n_at_bv_vor_entgelt basis = self._l10n_at_sv_basis_roh(result_rules) + if vor_entgelt: + basis = max(basis, float(vor_entgelt)) if pflichtig_ab is None or pflichtig_ab <= self.date_from: # AP4: fiktive BG während des Krankengeldbezugs (§ 7 Abs 3 # BMSVG) und des Wochengeldbezugs (§ 7 Abs 4, AP7) — nur diff --git a/addons/l10n_at_hr_payroll_private/models/efzg.py b/addons/l10n_at_hr_payroll_private/models/efzg.py index 375df02..38c0ae4 100644 --- a/addons/l10n_at_hr_payroll_private/models/efzg.py +++ b/addons/l10n_at_hr_payroll_private/models/efzg.py @@ -275,7 +275,7 @@ class HrPayslip(models.Model): self.ensure_one() basis = self._l10n_at_sv_basis_roh(result_rules) for code in ('TEIL_ENTGELT_HALB', 'KRANKENTGELT_PHASE', - 'MSCH_ENTGELT'): + 'MSCH_ENTGELT', 'KARENZ_ENTGELT'): if result_rules is not None and code in result_rules \ and result_rules[code]['total']: basis -= result_rules[code]['total'] diff --git a/addons/l10n_at_hr_payroll_private/models/karenz.py b/addons/l10n_at_hr_payroll_private/models/karenz.py new file mode 100644 index 0000000..42a326d --- /dev/null +++ b/addons/l10n_at_hr_payroll_private/models/karenz.py @@ -0,0 +1,135 @@ +# Part of the odoo-at-payroll project. License: LGPL-3. +"""AP16: Karenz-/Familien-Freistellungen — unbezahlte Karenz-Arten +(Elternkarenz generisch, Bildungs-, Familienhospiz-, Kinderrehabilita- +tions- und Pflegekarenz, Papa-Monat) mit Entgelt-Stopp, eSV-An- und +Abmeldeereignissen und der BMSVG-§-7-Brücke. + +Am Originalwortlaut verifiziert (.ris/, 8.13 freigegeben 2026-09-15): +- Alle Voll-Freistellungen sind entgeltlos (AVRAG §§ 11, 14a, 14b, + 14e, 14c; VKG § 1a) — Entgelt-Stopp je Arbeitstag über die + negative Regel ``KARENZ_ENTGELT`` (Muster MSCH_ENTGELT, AP7). +- eSV: Abmeldung zum Ende des Entgeltanspruchs (Tag vor dem + Karenzbeginn), Anmeldung zum ersten Arbeitstag nach dem Ende; + Papamonat/Familienzeit: Abmeldung am Tag vor Beginn (§ 11 Abs 3 + lit a ASVG, lb-pap-01). Karenzart-Strings der Sonderformate + „Familienhospizkarenz/Pflegekarenz An-/Abmeldung" (lb-kzs-05) + laufen als Protokoll-Info — erweiterte eSV-Sonderformate = GP7-Rest. +- BV: Elternkarenz/KBG zu Lasten FLAF (§ 7 Abs 5 BMSVG), Familien- + hospiz-/Pflegekarenz zu Lasten des Bundes (§ 7 Abs 6 BMSVG, + Fiktivgrundlage KBG § 5b Abs 1 KBGG idF vor BGBl I 53/2016); + Pflegeteilzeit: AG-Beitrag auf Vor-Reduzierungs-Entgelt (§ 6 Abs 4 + BMSVG, Feld l10n_at_bv_vor_entgelt). +- Dienstzeit: Papamonat ist ANZURECHNEN (§ 1a Abs 7 VKG iVm § 15f + MSchG), Bildungs-/Pflegekarenz grundsätzlich NICHT (§ 11 Abs 2, + § 14c/14d AVRAG) — Wizard-bestätigte Dienstzeiten, dokumentiert. +""" +from datetime import timedelta + +from odoo import fields, models + +# Unbezahlte Karenz-WETs (generisch + AP16-Spezies) +_KARENZ_WET = 'ATP_KARENZ' +_BILDUNGSKARENZ_WET = 'ATP_BILDUNGSKARENZ' +_HOSPIZKARENZ_WET = 'ATP_HOSPIZKARENZ' +_KINDER_KRANK_WET = 'ATP_KINDERKRANK' +_KINDER_REHAB_WET = 'ATP_KINDERREHAB' +_PFLEGEKARENZ_WET = 'ATP_PFLEGEKARENZ' +_PAPAMONAT_WET = 'ATP_PAPAMONAT' + +KARENZ_ARTEN = { + _KARENZ_WET: 'Elternkarenz (MSchG/VKG)', + _BILDUNGSKARENZ_WET: 'Bildungskarenz (§ 11 AVRAG)', + _HOSPIZKARENZ_WET: 'Familienhospizkarenz — Sterbebegleitung ' + '(§ 14a AVRAG)', + _KINDER_KRANK_WET: 'Familienhospizkarenz — Begleitung schwerst ' + 'erkrankter Kinder (§ 14b AVRAG)', + _KINDER_REHAB_WET: 'Freistellung Kinderrehabilitation ' + '(§ 14e AVRAG)', + _PFLEGEKARENZ_WET: 'Pflegekarenz (§ 14c AVRAG)', + _PAPAMONAT_WET: 'Papa-Monat/Familienzeit (§ 1a VKG / FamZeitbG)', +} + + +class HrPayslip(models.Model): + _inherit = 'hr.payslip' + + def _at_karenz_wet_codes(self): + """Codes der unbezahlten Karenz-Freistellungen (inkl. generisch + und AP16-Spezies).""" + self.ensure_one() + return list(KARENZ_ARTEN) + + def _at_karenz_leaves(self): + """Validierte Karenz-Freistellungen: [(von, bis, wett_code)] + chronologisch.""" + self.ensure_one() + typen = self.env['hr.leave.type'].search([ + ('work_entry_type_id.code', 'in', self._at_karenz_wet_codes()) + ]) + leaves = self.env['hr.leave'].search([ + ('employee_id', '=', self.employee_id.id), + ('holiday_status_id', 'in', typen.ids), + ('state', 'in', ('validate', 'validate1')), + ]) + ergebnis = [] + for leave in leaves: + wett = leave.holiday_status_id.work_entry_type_id.code + ergebnis.append((leave.request_date_from, + leave.request_date_to, wett)) + return [(von, bis, wett) for von, bis, wett in ergebnis + if von and bis] + + def _at_karenz_status(self): + """Karenz-Tage je Abrechnungsperiode über alle unbezahlten + Karenz-Arten: {'kalendertage', 'arbeitstage', 'arten'} — None + ohne Freistellung im Zeitraum (Muster _at_msch_status).""" + self.ensure_one() + codes = set(self._at_karenz_wet_codes()) + tage = {} + tag_art = {} + for von, bis, wett in self._at_karenz_leaves(): + tag = fields.Date.to_date(von) + ende = fields.Date.to_date(bis) + while tag <= ende: + tage[tag] = True + tag_art[tag] = wett + tag += timedelta(days=1) + in_periode = sorted( + tag for tag in tage + if self.date_from <= tag <= self.date_to) + if not in_periode: + return None + eintraege = self.env['hr.work.entry'].search([ + ('employee_id', '=', self.employee_id.id), + ('date', '>=', self.date_from), + ('date', '<=', self.date_to), + ('state', '!=', 'cancelled'), + ('work_entry_type_id.code', 'in', list(codes)), + ]) + arten_aktuell = {tag_art[tag] for tag in in_periode} + return { + 'kalendertage': in_periode, + 'arbeitstage': sorted({e.date for e in eintraege}), + 'arten': sorted(KARENZ_ARTEN[c] for c in arten_aktuell), + } + + def _at_karenz_entgelt_tage(self, result_rules=None): + """Arbeitstage der unbezahlten Karenz im Abrechnungszeitraum + (Entgelt-Stopp-Basis der Regel KARENZ_ENTGELT).""" + self.ensure_one() + status = self._at_karenz_status() + if not status: + return 0 + return len(status['arbeitstage']) + + +class HrVersion(models.Model): + _inherit = 'hr.version' + + l10n_at_bv_vor_entgelt = fields.Monetary( + string='BV-Basis vor Arbeitszeit-Reduktion (§ 6 Abs 4 BMSVG)', + currency_field='currency_id', + help='Monatsentgelt vor der Reduzierung (z. B. Pflegeteilzeit ' + '§ 14d AVRAG): der BV-Beitrag läuft während der Maßnahme ' + 'auf Grundlage der Arbeitszeit vor Herabsetzung ' + '(§ 6 Abs 4 BMSVG). Leer = laufende Basis.') \ No newline at end of file diff --git a/addons/l10n_at_hr_payroll_private/models/mutterschutz.py b/addons/l10n_at_hr_payroll_private/models/mutterschutz.py index e8f3e4e..35b573c 100644 --- a/addons/l10n_at_hr_payroll_private/models/mutterschutz.py +++ b/addons/l10n_at_hr_payroll_private/models/mutterschutz.py @@ -36,11 +36,27 @@ Fristberechnung aus dem ärztlichen Zeugnis bleibt beim Mandanten. Urlaubsanspruch erwächst während des Wochengeldbezugs (lb-msf-03); Sonderwochengeld (§ 163 ASVG, Günstigkeitsvergleich § 162 Abs 3b) = dokumentierte Option. + +AP16 (8.13): Karenz-Brücke nach § 7 Abs 4 Z 1–3 BMSVG — folgt das +neuerliche Beschäftigungsverbot unmittelbar auf eine Karenz (Z 1) oder +nach einer kürzeren als dreimonatigen Beschäftigung (Z 2), ist die +Bemessungsgrundlage die des Kalendermonats vor dem der Karenz +unmittelbar vorangegangenen Beschäftigungsverbots; Z 3 (neues DV nach +Beendigung des karenzierten DV, < 3 Monate): der letzte volle +Kalendermonat vor dem neuerlichen Verbot. BV während des KBG-Bezugs +läuft zu Lasten des FLAF (§ 7 Abs 5), in der Familienhospiz-/Pflege- +karenz zu Lasten des Bundes (§ 7 Abs 6) — keine AG-Regeln; das Ruhen +des Wochengeldes (§ 166 ASVG) und das Sonderwochengeld während +einer Karenz (§ 163 ASVG) werden als Info/Warnung ausgewiesen. """ from datetime import date, timedelta +from dateutil.relativedelta import relativedelta + from odoo import fields, models +from .karenz import KARENZ_ARTEN + _MSCH_WET = 'ATP_MSCH' @@ -61,13 +77,21 @@ class HrPayslip(models.Model): slip.l10n_at_msch_info = '' continue wochengeld = slip._at_wochengeld() - slip.l10n_at_msch_info = ( + info = ( '%d Kalendertage Bezug · WG %.2f/Kalendertag ' '(Ø-Netto %.2f + %.0f %%)' % ( len(msch['kalendertage']), wochengeld['tagesatz'], wochengeld['netto_drei_monate'], wochengeld['sz_prozent'])) + if slip._at_msch_sonderwochengeld(): + info += (' · Sonderwochengeld (§ 163 ASVG) statt ' + 'Wochengeld — ÖGK, erhöhtes Krankengeld ' + '(§ 141 Abs 2, § 125 iVm § 108f)') + warnungen = slip._at_msch_ruhen_warnungen() + if warnungen: + info += ' · Ruhen: ' + '; '.join(warnungen) + slip.l10n_at_msch_info = info def _at_msch_leaves(self): """Validierte Mutterschutz-Frist-Abwesenheiten (ATP_MSCH).""" @@ -120,17 +144,109 @@ class HrPayslip(models.Model): 'episode_beginn': block, } - def _at_msch_referenz_monate(self, episode_beginn): - """Die drei (vollen) Kalendermonate vor dem Versicherungsfall - der Mutterschaft (§ 162 Abs 3: letzte 13 Wochen; § 7 Abs 4 - BMSVG: letzte drei Kalendermonate). Versicherungsfall im Monat - X → Monate X-3, X-2, X-1 (aufsteigend).""" + def _at_msch_sonderwochengeld(self): + """Sonderwochengeld statt Wochengeld (§ 163 ASVG, verifiziert): + die Mutterschutz-Frist fällt in eine Karenz nach dem MSchG — + kein Anspruch auf Wochengeld (§ 162), das Sonderwochengeld + zahlt der Krankenversicherungsträger (erhöhtes Krankengeld + § 141 Abs 2, Bemessungsgrundlage § 125 aus dem Arbeits- + verdienst vor dem Ende des letzten Entgeltanspruchs, mit + § 108f-Anpassung).""" + self.ensure_one() + msch = self._at_msch_status() + if not msch: + return False + for von, bis, _wett in self._at_karenz_leaves(): + if any(von <= tag <= bis for tag in msch['kalendertage']): + return True + return False + + def _at_msch_ruhen_warnungen(self): + """Ruhen des Wochengeldes (§ 166 ASVG, verifiziert) — + Warnungen zu Fortbezug während der Frist: + Z 2 Entgeltfortzahlung 100 % → Ruhen zur Gänze, 50 % → + hälftig (Urlaubs-/UEL-/KE-Zeiten gelten nicht als weiterge- + leistete Bezüge); Z 3 Erwerbstätigkeit und Z 4 Pflegekarenz- + geld sind Bezugszeitraum-Daten (AEB/ÖGK-Seite, Hinweis).""" + self.ensure_one() + msch = self._at_msch_status() + if not msch: + return [] + tage = set(msch['kalendertage']) + efz = self._at_efz_status() + warnungen = [] + if efz: + if any(tag in tage for tag in efz.get('voll_arbeitstage', [])): + warnungen.append('Fortbezug > 50 % (Entgeltfortzahlung) ' + '— Ruhen zur Gänze (§ 166 Abs 1 Z 2)') + elif any(tag in tage for tag in efz.get('halb_arbeitstage', [])): + warnungen.append('Fortbezug = 50 % — Ruhen hälftig ' + '(§ 166 Abs 1 Z 2)') + warnungen.append('Erwerbstätigkeit/Pflegekarenzgeld während der ' + 'Frist prüfen (§ 166 Abs 1 Z 3/4)') + return warnungen + + def _at_msch_referenz_stichtag_und_anzahl(self, episode_beginn): + """(Stichtag, Referenz-Monatsanzahl) nach § 7 Abs 4 BMSVG mit + Karenz-Brücke Z 1–3 (AP16): + - Z 1/Z 2: endete eine Karenz innerhalb von drei Monaten vor + dem neuerlichen Beschäftigungsverbot, gilt das der Karenz + unmittelbar vorangegangene Beschäftigungsverbot als + Bemessungsstichtag (Referenzmonate davor); + - Z 3: neues DV binnen drei Monaten nach Beendigung des + karenzierten DV (mit Karenz) → der letzte volle Kalendermonat + vor dem neuerlichen Verbot als EINZELNER Referenzmonat + („volles Monatsentgelt"); + - sonst: Versicherungsfall selbst, drei Monate.""" self.ensure_one() stichtag = episode_beginn or self.date_from + grenze = stichtag - relativedelta(months=3) + karenz_kandidat = None + for von, bis, _wett in sorted(self._at_karenz_leaves(), + key=lambda k: k[1], reverse=True): + if grenze <= bis < stichtag: + karenz_kandidat = (von, bis) + break + if karenz_kandidat: + karenz_von = karenz_kandidat[0] + frist = max( + (m for m in self._at_msch_leaves() + if m[1] <= karenz_von), + key=lambda m: m[1], default=None) + if frist: + return frist[0], 3 + version = self.version_id + vorher = self.env['hr.version'].search([ + ('employee_id', '=', self.employee_id.id), + ('contract_date_end', '!=', False), + ('id', '!=', version.id), + ], order='contract_date_end desc', limit=1) + if (vorher and vorher.contract_date_end + and version.contract_date_start + and vorher.contract_date_end + <= version.contract_date_start + and (version.contract_date_start + - vorher.contract_date_end).days < 92 + and any(k[1] <= vorher.contract_date_end + for k in self._at_karenz_leaves())): + erster = date(stichtag.year, stichtag.month, 1) + erster = (erster - timedelta(days=1)).replace(day=1) + return erster, 1 + return stichtag, 3 + + def _at_msch_referenz_monate(self, episode_beginn): + """Referenzkalendermonate der fiktiven BV (§ 7 Abs 4 BMSVG: + letzte drei Monate; Karenz-Brücke Z 1–3, AP16 — s. + _at_msch_referenz_stichtag_und_anzahl; § 162 Abs 3: letzte + 13 Wochen). Versicherungsfall im Monat X → Monate X-3, X-2, + X-1 (aufsteigend).""" + self.ensure_one() + stichtag, anzahl = self._at_msch_referenz_stichtag_und_anzahl( + episode_beginn) erster = date(stichtag.year, stichtag.month, 1) monate = [] hilf = erster - for _ in range(3): + for _ in range(anzahl): hilf = (hilf - timedelta(days=1)).replace(day=1) monate.append(hilf) return list(reversed(monate)) diff --git a/addons/l10n_at_hr_payroll_private/models/versichertenmeldung.py b/addons/l10n_at_hr_payroll_private/models/versichertenmeldung.py index f2456fc..257beef 100644 --- a/addons/l10n_at_hr_payroll_private/models/versichertenmeldung.py +++ b/addons/l10n_at_hr_payroll_private/models/versichertenmeldung.py @@ -23,12 +23,15 @@ BVAB/BVJN aus dem GP5-BMSVG-Flag, VWAZ vereinbarte Wochenarbeitszeit (Stunden ×100, neu für Meldedaten ab 01.01.2026). Dateibasiert (Prämisse P7); Bestätigungen/Übertragung beim Mandanten.""" -from datetime import date +from datetime import date, timedelta import base64 from odoo import api, fields, models from odoo.exceptions import UserError +from odoo.addons.l10n_at_hr_payroll_private.models.karenz import \ + KARENZ_ARTEN + from odoo.addons.l10n_at_hr_payroll.models.elda import ( L10N_AT_ELDA_IDTEIL, L10nAtEldaBestand, @@ -199,6 +202,58 @@ class L10nAtPayrollVr(models.Model): abmeldungen.append(version) return sorted(abmeldungen, key=lambda v: v.contract_date_end) + def _l10n_at_offene_karenz_events(self): + """AP16 (8.13): An-/Abmeldungen aus validierten Karenz- + Freistellungen: M4 zum Ende des Entgeltanspruchs (Tag vor dem + Karenzbeginn; Familienzeit am Tag vor Beginn, § 11 Abs 3 lit a + ASVG, lb-pap-01), M3 zum ersten Arbeitstag nach dem Karenzende + — entfällt, wenn das DV mit der Karenz endet (normale M4- + Vertragsabmeldung deckt das Ende). Karenzart-Strings für die + eSV-Sonderformate „Familienhospizkarenz/Pflegekarenz An-/ + Abmeldung" (lb-kzs-05) laufen als Protokoll-Info; erweiterte + eSV-Sonderformate = GP7-Rest. Dedup über die Registry + (employee, art, adatum); keine Events nach dem Stichtag.""" + self.ensure_one() + typen = self.env['hr.leave.type'].search([ + ('work_entry_type_id.code', 'in', list(KARENZ_ARTEN))]) + if not typen: + return [] + leaves = self.env['hr.leave'].search([ + ('company_id', 'in', self.company_id.ids), + ('holiday_status_id', 'in', typen.ids), + ('state', 'in', ('validate', 'validate1')), + ]).sorted('request_date_from') + registry = self.env['l10n.at.payroll.vr.event'].search([ + ('company_id', '=', self.company_id.id), + ]) + gesehen = {(e.employee_id.id, e.art, e.adatum) for e in registry} + ereignisse = [] + for leave in leaves: + von = leave.request_date_from + bis = leave.request_date_to + if not von or not bis: + continue + version = leave.employee_id.version_id + if not version: + continue + wett_code = leave.holiday_status_id \ + .work_entry_type_id.code + karenzart = KARENZ_ARTEN.get(wett_code, 'Karenz') + abmeldetag = von - timedelta(days=1) + if abmeldetag > self.stichtag: + continue + if (leave.employee_id.id, 'M4', abmeldetag) not in gesehen: + ereignisse.append((version, 'M4', abmeldetag, karenzart)) + rueckkehr = bis + timedelta(days=1) + if (version.contract_date_end + and rueckkehr >= version.contract_date_end): + continue + if rueckkehr > self.stichtag: + continue + if (leave.employee_id.id, 'M3', rueckkehr) not in gesehen: + ereignisse.append((version, 'M3', rueckkehr, karenzart)) + return ereignisse + def _l10n_at_bber(self, version): if version.l10n_at_lehrling: return '03' if getattr( @@ -234,6 +289,7 @@ class L10nAtPayrollVr(models.Model): raise UserError('Beitragskontonummer (BKNR) fehlt.') datensaetze = [] ereignisse = [] + karenz_info = [] vstr = bknr[:2] if lauf.nur_fallweise: versionen = self.env['hr.version'].search([ @@ -366,6 +422,51 @@ class L10nAtPayrollVr(models.Model): ereignisse.append((version, 'M4', version.contract_date_end, refw)) + for version, art, adatum, karenzart in \ + lauf._l10n_at_offene_karenz_events(): + if not version.ssnid: + fehler.append( + 'Karenz-Meldung %s ohne Versicherungsnummer.' + % version.employee_id.name) + continue + satz = L10nAtEldaSatz( + L10N_AT_ELDA_IDTEIL + VR_SATZ, 772, + bezeichnung='%s Karenz %s' % ( + art, version.employee_id.name)) + refw = '%s-K%d-%s-%d' % ( + art, company.id, + adatum.strftime('%Y%m%d'), + version.employee_id.id) + satz.setze('SART', art) + satz.setze('VSTR', vstr) + satz.setze('REFW', refw) + satz.setze('BKNR', bknr) + satz.setze('DGNA', company.name[:70]) + satz.setze('VSNR', version.ssnid) + satz.setze_datum( + 'GEBD', version.employee_id.birthday) + familienname, vorname = l10n_at_namen( + version.employee_id) + satz.setze('FANA', familienname[:70]) + satz.setze('VONA', (vorname or '')[:70]) + satz.setze_datum('ADAT', adatum) + if art == 'M3': + satz.setze('BBER', lauf._l10n_at_bber(version)) + satz.setze('GERF', 'N') + satz.setze('FRDV', 'N') + else: + satz.setze_datum('EBSV', adatum) + satz.setze('AGRD', '00') + kalender = version.resource_calendar_id + if kalender: + satz.setze('VWAZ', kalender.hours_per_week, + skala=2) + datensaetze.append(satz) + ereignisse.append((version, art, adatum, refw)) + karenz_info.append( + '%s (ADAT %s) — %s' % ( + version.employee_id.name, + adatum.strftime('%d.%m.%Y'), karenzart)) if fehler: lauf.protokoll = 'FEHLER:\n' + '\n'.join(fehler) raise UserError('Versichertenmeldung mit Fehlern ' @@ -395,12 +496,17 @@ class L10nAtPayrollVr(models.Model): ereignisse = self.env['l10n.at.payroll.vr.event'].create( ereignis_werte) if ereignis_werte else \ self.env['l10n.at.payroll.vr.event'] + protokoll = 'Meldungen: %d' % len(datensaetze) + if karenz_info: + protokoll += ("\nKarenz (eSV-Sonderformate GP7-Rest; " + "Karenzart zur Übergabe):\n" + + "\n".join(karenz_info)) lauf.write({ 'datei': base64.b64encode(datei.render()), 'datei_name': 'versichertenmeldung_%s.txt' % ( fields.Date.today().strftime('%Y%m%d'),), 'event_ids': [(6, 0, ereignisse.ids)], 'state': 'done', - 'protokoll': 'Meldungen: %d' % len(datensaetze), + 'protokoll': protokoll, }) return True \ No newline at end of file diff --git a/addons/l10n_at_hr_payroll_private/tests/__init__.py b/addons/l10n_at_hr_payroll_private/tests/__init__.py index 7eea1cf..45caadb 100644 --- a/addons/l10n_at_hr_payroll_private/tests/__init__.py +++ b/addons/l10n_at_hr_payroll_private/tests/__init__.py @@ -18,3 +18,4 @@ from . import test_sachbezuege2_private from . import test_reisekosten_private from . import test_pendler_private from . import test_endabrechnung_private +from . import test_karenz_private diff --git a/addons/l10n_at_hr_payroll_private/tests/test_karenz_private.py b/addons/l10n_at_hr_payroll_private/tests/test_karenz_private.py new file mode 100644 index 0000000..1f62b22 --- /dev/null +++ b/addons/l10n_at_hr_payroll_private/tests/test_karenz_private.py @@ -0,0 +1,323 @@ +# Part of the odoo-at-payroll project. License: LGPL-3. +"""AP16 (8.13): Karenz-/Familien-Freistellungen — Entgelt-Stopp je +unbezahlter Karenz-Art (KARENZ_ENTGELT, Muster MSCH_ENTGELT), eSV-An-/ +Abmeldungen aus der vr.event-Registry (Papa-Monat tagesgenau, Karenz- +Art-Protokoll), fiktive BV mit §-7-Abs-4-Z-1–3-BMSVG-Karenz-Brücke, +BV-Vor-Entgelt bei Pflegeteilzeit (§ 6 Abs 4 BMSVG), Sonderwochengeld +(§ 163 ASVG) und Ruhen-Warnungen (§ 166 ASVG), §-23-Abs-8-ETZ- +Hochrechnung im Endabrechnungs-Wizard.""" +import base64 +import datetime + +from odoo import SUPERUSER_ID +from odoo.exceptions import UserError +from odoo.tests import TransactionCase, tagged + + +@tagged('post_install', '-at_install') +class KarenzPrivateTest(TransactionCase): + + def setUp(self): + super().setUp() + self.company = self.env.company + self.structure = self.env.ref( + 'l10n_at_hr_payroll_private.structure_at_privat') + self.kv2203 = self.env.ref( + 'l10n_at_hr_payroll_private.kv_si_2203') + self.gruppe_ii = self.env.ref( + 'l10n_at_hr_payroll_private.kv2203_grp_ii') + self.calendar = self.env['resource.calendar'].create({ + 'name': 'AT Karenz 40h', + 'tz': 'Europe/Vienna', + 'hours_per_day': 8, + 'attendance_ids': [ + (0, 0, {'name': 'Tag %d' % wd, 'dayofweek': str(wd), + 'hour_from': 8.0, 'hour_to': 16.0}) + for wd in range(5) + ], + }) + self.kalender_teilzeit = self.env['resource.calendar'].create({ + 'name': 'AT Karenz 20h (Teilzeit)', + 'tz': 'Europe/Vienna', + 'hours_per_day': 4, + 'attendance_ids': [ + (0, 0, {'name': 'Tag %d' % wd, 'dayofweek': str(wd), + 'hour_from': 8.0, 'hour_to': 12.0}) + for wd in range(5) + ], + }) + self.company.resource_calendar_id = self.calendar + self.typ_bildung = self.env.ref( + 'l10n_at_hr_payroll_private.leave_type_at_bildungskarenz') + self.typ_pflege = self.env.ref( + 'l10n_at_hr_payroll_private.leave_type_at_pflegekarenz') + self.typ_karenz = self.env.ref( + 'l10n_at_hr_payroll_private.leave_type_at_karenz') + self.typ_msch = self.env.ref( + 'l10n_at_hr_payroll_private.leave_type_at_mutterschutz') + self.typ_krank = self.env.ref( + 'l10n_at_hr_payroll_private.leave_type_at_krankenstand') + + def _employee(self, teilzeit=False, ssnid=False, ende=None): + employee = self.env['hr.employee'].create({ + 'name': 'Karenz Test', 'sex': 'female', + 'birthday': '1990-05-15'}) + version = self.env['hr.version'].search( + [('employee_id', '=', employee.id)], limit=1) + version.write({ + 'contract_date_start': datetime.date(2020, 7, 1), + 'kv_id': self.kv2203.id, + 'kv_gruppe_id': self.gruppe_ii.id, + 'erfahrungsstufe': 1, + 'ueberzahlung': 2059.41, + 'resource_calendar_id': self.calendar.id, + }) + if ssnid: + version.write({'ssnid': '1234150590', + 'sex': 'female'}) + if ende: + version.write({'contract_date_end': ende}) + version.generate_work_entries( + datetime.date(2026, 8, 1), datetime.date(2027, 3, 31)) + return employee + + def _slip(self, employee, monat, jahr=2026, confirm=False): + erster = datetime.date(jahr, monat, 1) + letzter = (erster.replace(day=28) + + datetime.timedelta(days=4)).replace(day=1) \ + - datetime.timedelta(days=1) + slip = self.env['hr.payslip'].create({ + 'name': 'Karenz %d/%d' % (monat, jahr), + 'employee_id': employee.id, + 'struct_id': self.structure.id, + 'date_from': erster, + 'date_to': letzter, + }) + slip.compute_sheet() + if confirm: + slip.action_payslip_done() + return slip + + def _total(self, slip, code): + return sum(slip.line_ids.filtered( + lambda l: l.code == code).mapped('total')) + + def _leave(self, employee, typ, von, bis, skip_check=False): + vals = { + 'name': '%s %s' % (typ.name, von), + 'employee_id': employee.id, + 'holiday_status_id': typ.id, + 'request_date_from': von, + 'request_date_to': bis, + } + if skip_check: + # Überlappende Freistellungen (Krankheit während Karenz/MSchG): + # direkt auf state='validate' — _action_validate würde an + # number_of_days == 0 (vom Karenz-Typ belegte Tage) scheitern. + leave = self.env['hr.leave'].with_context( + leave_skip_date_check=True, + leave_skip_state_check=True).create( + dict(vals, state='validate')) + return leave + leave = self.env['hr.leave'].create(vals) + leave.with_user(SUPERUSER_ID)._action_validate() + return leave + + # ------------------------------------------------------------------ + # RF-KAR-1: Entgelt-Stopp je Karenz-Art (KARENZ_ENTGELT) + # ------------------------------------------------------------------ + def test_01_entgelt_stopp_bildungskarenz(self): + """Bildungskarenz 1.-15. 11. (§ 11 AVRAG, 15 KT/10 AT): das + Karenzgeld zahlt die AK — KARENZ_ENTGELT stoppt das + Dienstgeber-Entgelt je Arbeitstag (Muster MSCH_ENTGELT).""" + emp_mit = self._employee() + emp_ohne = self._employee() + slip_mit = self._slip(emp_mit, 11) + slip_ohne = self._slip(emp_ohne, 11) + self._leave(emp_mit, self.typ_bildung, + datetime.date(2026, 11, 1), + datetime.date(2026, 11, 15)) + slip_mit.compute_sheet() + + status = slip_mit._at_karenz_status() + self.assertEqual(len(status['kalendertage']), 15) + self.assertEqual(len(status['arbeitstage']), 10) + self.assertTrue(any('Bildungskarenz' in a + for a in status['arten'])) + tagesatz = 2059.41 / 21 + self.assertAlmostEqual(self._total(slip_mit, 'KARENZ_ENTGELT'), + -tagesatz * 10, places=2) + # Keine MSCH-Wirkung; SV-Basis durch die negative Zeile reduziert + self.assertEqual(self._total(slip_mit, 'MSCH_ENTGELT'), 0.0) + basis_diff = (slip_ohne._l10n_at_sv_basis_roh() + - slip_mit._l10n_at_sv_basis_roh()) + self.assertAlmostEqual(basis_diff, tagesatz * 10, places=2) + + # ------------------------------------------------------------------ + # RF-KAR-2: eSV-An-/Abmeldungen aus Karenz (vr.event) + # ------------------------------------------------------------------ + def test_02_vr_events_karenz(self): + """Pflegekarenz 1.-15. 11.: Abmeldung zum Tag vor Beginn + (Ende des Entgeltanspruchs, § 163/§ 11 ASVG-Rahmen) und + Anmeldung zur Rückkehr (16. 11.); Karenzart im Protokoll; + erneuter Lauf idempotent (Registry-Dedup).""" + self.company.write({ + 'l10n_at_beitragskontonummer': '1412345678', + }) + employee = self._employee(ssnid=True) + self._leave(employee, self.typ_pflege, + datetime.date(2026, 11, 1), + datetime.date(2026, 11, 15)) + lauf = self.env['l10n.at.payroll.vr'].create({ + 'company_id': self.company.id, + 'stichtag': datetime.date(2026, 12, 31), + }) + lauf.action_erzeugen() + self.assertEqual(lauf.state, 'done') + events = self.env['l10n.at.payroll.vr.event'].search([ + ('company_id', '=', self.company.id), + ('employee_id', '=', employee.id), + ]) + adate = {(e.art, e.adatum) for e in events} + self.assertIn(('M4', datetime.date(2026, 10, 31)), adate) + self.assertIn(('M3', datetime.date(2026, 11, 16)), adate) + self.assertIn('Pflegekarenz', lauf.protokoll) + roh = base64.b64decode(lauf.datei).decode('ISO-8859-15') + self.assertIn('M4-K', roh) + self.assertIn('M3-K', roh) + # Idempotenz: zweite Meldungen sind registriert → Lauf leer + lauf2 = self.env['l10n.at.payroll.vr'].create({ + 'company_id': self.company.id, + 'stichtag': datetime.date(2026, 12, 31), + }) + with self.assertRaises(UserError): + lauf2.action_erzeugen() + + # ------------------------------------------------------------------ + # RF-KAR-3: BMSVG-Karenz-Brücke (§ 7 Abs 4 Z 1–3) + # ------------------------------------------------------------------ + def test_03_bmsvg_karenz_bruecke(self): + """Mutterschutz-Frist Nov 2026, Karenz Dez 2026–Feb 2027, + neuerliches Verbot 1. 3. 2027 (Z 1): der Bemessungsstichtag der + fiktiven BV ist das der Karenz unmittelbar vorangegangene + Beschäftigungsverbot — Referenzmonate Aug/Sep/Okt 2026.""" + employee = self._employee() + self._leave(employee, self.typ_msch, + datetime.date(2026, 11, 1), + datetime.date(2026, 11, 15)) + self._leave(employee, self.typ_karenz, + datetime.date(2026, 11, 16), + datetime.date(2027, 2, 28)) + self._leave(employee, self.typ_msch, + datetime.date(2027, 3, 1), + datetime.date(2027, 3, 15)) + slip = self._slip(employee, 11, jahr=2026) + stichtag, anzahl = slip._at_msch_referenz_stichtag_und_anzahl( + datetime.date(2027, 3, 1)) + self.assertEqual(stichtag, datetime.date(2026, 11, 1)) + self.assertEqual(anzahl, 3) + monate = slip._at_msch_referenz_monate(datetime.date(2027, 3, 1)) + self.assertEqual( + monate, + [datetime.date(2026, 8, 1), datetime.date(2026, 9, 1), + datetime.date(2026, 10, 1)]) + + # ------------------------------------------------------------------ + # RF-KAR-4: BV-Vor-Entgelt bei Pflegeteilzeit (§ 6 Abs 4 BMSVG) + # ------------------------------------------------------------------ + def test_04_bv_vor_entgelt_pflegeteilzeit(self): + """20-h-Teilzeit (work_time_rate 0,5) mit gesetztem + Vor-Reduzierungs-Entgelt: die BV-Basis läuft auf dem + Vor-Reduzierungs-Entgelt (§ 6 Abs 4 BMSVG), nicht auf der + reduzierten laufenden Basis.""" + employee = self._employee() + version = employee.version_id + version.write({ + 'l10n_at_abfertigung_neu': True, + 'l10n_at_bv_vor_entgelt': 2059.41, + }) + version.resource_calendar_id = self.kalender_teilzeit + version.generate_work_entries( + datetime.date(2026, 8, 1), datetime.date(2026, 11, 30)) + slip = self._slip(employee, 11) + self.assertAlmostEqual(slip._at_bv_basis_laufen(), 2059.41, + places=2) + # OHNE §-6-Abs-4-Feld: reduzierte laufende Basis + version.write({'l10n_at_bv_vor_entgelt': 0.0}) + slip.compute_sheet() + self.assertAlmostEqual(slip._at_bv_basis_laufen(), 1029.71, + places=2) + + # ------------------------------------------------------------------ + # RF-KAR-5: Sonderwochengeld (§ 163) und Ruhen (§ 166) + # ------------------------------------------------------------------ + def test_05_sonderwochengeld_und_ruhen(self): + """Mutterschutz-Frist 1.-15. 11. innerhalb einer Pflegekarenz: + Sonderwochengeld statt Wochengeld (§ 163 ASVG); Krankenstand + 1.-10. 11. (Entgeltfortzahlung 100 %) → Ruhen-Warnung + (§ 166 Abs 1 Z 2).""" + employee = self._employee() + # Karenz + Krankenstand + Mutterschutz überlappen real (Krankheit + # während der Frist); Odos Quere-Overlap-Sperre wird im Test mit + # dem offiziellen leave_skip_date_check-Bypass umgangen — die + # Leave-Überlappungs-Policy ist Mandanten-Entscheid (offener + # Punkt in docs/rechenfaelle/karenz.md). + self._leave(employee, self.typ_pflege, + datetime.date(2026, 11, 1), + datetime.date(2026, 12, 15), skip_check=True) + self._leave(employee, self.typ_krank, + datetime.date(2026, 11, 1), + datetime.date(2026, 11, 10), skip_check=True) + self._leave(employee, self.typ_msch, + datetime.date(2026, 11, 5), + datetime.date(2026, 11, 15), skip_check=True) + # Krankenstand Nov 1–10 als direkte Work Entries (ein Leave auf + # denselben Tagen erzeugt der Work-Entry-Generator nicht — die + # Karenz belegt die Tage bereits); die EFZ-Maschine liest die + # KRANK-Entries für die Fortzahlungs-Phase. + krank_typ = self.env['hr.work.entry.type'].search( + [('code', '=', 'ATP_KRANK')], limit=1) + arbeitstage = [datetime.date(2026, 11, d) for d in + (2, 3, 4, 5, 6, 9, 10)] + for tag in arbeitstage: + self.env['hr.work.entry'].create({ + 'name': 'Krankenstand %s' % tag, + 'employee_id': employee.id, + 'work_entry_type_id': krank_typ.id, + 'date': tag, + 'duration': 8.0, + }) + slip = self._slip(employee, 11) + slip.compute_sheet() + self.assertTrue(slip._at_msch_sonderwochengeld()) + warnungen = slip._at_msch_ruhen_warnungen() + self.assertTrue(any('Ruhen zur Gänze' in w for w in warnungen)) + self.assertIn('Sonderwochengeld', slip.l10n_at_msch_info) + + # ------------------------------------------------------------------ + # RF-KAR-6: §-23-Abs-8-Hochrechnung im Wizard (Elternteilzeit) + # ------------------------------------------------------------------ + def test_06_etz_hochrechnung_wizard(self): + """20-h-Versionskalender (work_time_rate 0,5): der §-23-Abs-8- + Hochrechnungs-Vorschlag = Monatsentgelt ÷ work_time_rate; die + Übernahme bleibt bestätigt (kein Auto-Write).""" + employee = self._employee() + version = employee.version_id + version.resource_calendar_id = self.kalender_teilzeit + version.generate_work_entries( + datetime.date(2026, 8, 1), datetime.date(2026, 11, 30)) + wizard = self.env['l10n.at.payroll.endabrechnung.wizard'].create({ + 'employee_id': employee.id, + 'austrittsdatum': datetime.date(2026, 11, 15), + 'departure_reason_id': self.env.ref( + 'l10n_at_hr_payroll' + '.departure_reason_einvernehmlich').id, + 'abf_alt_aktiv': True, + 'abf_alt_monatsentgelt': 1029.71, + 'abf_alt_laufender_arbeitslohn': 1029.71, + 'abf_alt_monatslohnsteuer': 0.0, + 'abf_alt_mehrfach': 2.0, + 'abf_alt_anspruchsart': 'normal', + }) + self.assertAlmostEqual(wizard.abf_alt_hochrechnung_vorschau, + 2059.42, places=2) \ No newline at end of file diff --git a/addons/l10n_at_hr_payroll_private/views/endabrechnung_views.xml b/addons/l10n_at_hr_payroll_private/views/endabrechnung_views.xml index 05ff3d8..ac83d9b 100644 --- a/addons/l10n_at_hr_payroll_private/views/endabrechnung_views.xml +++ b/addons/l10n_at_hr_payroll_private/views/endabrechnung_views.xml @@ -38,6 +38,8 @@ + diff --git a/addons/l10n_at_hr_payroll_private/wizard/endabrechnung.py b/addons/l10n_at_hr_payroll_private/wizard/endabrechnung.py index 0785614..b9900d0 100644 --- a/addons/l10n_at_hr_payroll_private/wizard/endabrechnung.py +++ b/addons/l10n_at_hr_payroll_private/wizard/endabrechnung.py @@ -95,6 +95,17 @@ class L10nAtEndabrechnungWizard(models.TransientModel): 'entfallende tarifmäßige Lohnsteuer nach den persönlichen ' 'Verhältnissen. Bei steuerfreiem Monatslohn ausdrücklich 0 ' 'bestätigen.') + abf_alt_hochrechnung_vorschau = fields.Monetary( + string='§ 23 Abs 8 AngG — Hochrechnung Vollzeit (Vorschau)', + currency_field='currency_id', compute='_compute_vorschau', + help='Endet das DV während einer Teilzeitbeschäftigung nach ' + 'MSchG/VKG (z. B. Elternteilzeit), ist die frühere ' + 'Normalarbeitszeit zugrunde zu legen (§ 23 Abs 8 AngG; für ' + 'Arbeitnehmerkündigung § 23a Abs 4a — 5-Jahres-Ø ohne ' + 'Karenz, manuell einzugeben). Vorschlag: Monatsentgelt ÷ ' + 'aktuelles work_time_rate; die Übernahme bleibt ' + 'bestätigt (bei UEL-Basen gilt § 10 Abs 4 UrlG — ' + 'überwiegende Arbeitszeit des Urlaubsjahres).') abf_alt_mehrfach = fields.Float( string='Bestätigter Anspruchsfaktor', help='§-23-Staffel, gegebenenfalls halbiert/gedeckelt oder als ' @@ -404,6 +415,7 @@ class L10nAtEndabrechnungWizard(models.TransientModel): wizard.abf_alt_steuer_6_vorschau = 0.0 wizard.abf_alt_lohnsteuer_vorschau = 0.0 wizard.abf_alt_netto_vorschau = 0.0 + wizard.abf_alt_hochrechnung_vorschau = 0.0 wizard.abf_frei_z1_vorschau = 0.0 wizard.abf_frei_z2_vorschau = 0.0 wizard.abf_frei_kuerzung_vorschau = 0.0 @@ -434,6 +446,12 @@ class L10nAtEndabrechnungWizard(models.TransientModel): wizard.abf_alt_steuer_6_vorschau = steuer_6 wizard.abf_alt_lohnsteuer_vorschau = steuer wizard.abf_alt_netto_vorschau = betrag - steuer + rate = wizard.employee_id.version_id.work_time_rate + if rate and rate < 1.0 and wizard.abf_alt_monatsentgelt: + # § 23 Abs 8 AngG: frühere Normalarbeitszeit — + # Hochrechnungs-Vorschlag, bestätigte Übernahme. + wizard.abf_alt_hochrechnung_vorschau = \ + wizard.abf_alt_monatsentgelt / rate if wizard.abf_frei_aktiv: split = slip._at_abf_frei_split() satz = float(slip._l10n_at_param(