From 290b432b733aa8fd933483ba0d50a9ee9437a8d9 Mon Sep 17 00:00:00 2001 From: fegger Date: Sat, 12 Sep 2026 23:27:20 +0200 Subject: [PATCH] =?UTF-8?q?[IMP]=20l10n=5Fat=5Fhr=5Fpayroll=5Fprivate:=20M?= =?UTF-8?q?utterschutz/Wochengeld=20=E2=80=94=20Entgelt-Stopp,=20volle=20?= =?UTF-8?q?=C2=A7-162-Abs-3-Berechnung,=20fiktive=20BV=20(AP7)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rollenklärung (lb-msf-03, ASVG § 162 Abs 1): Wochengeld ist eine KV-Leistung — die ÖGK zahlt; der Dienstgeber stoppt das Entgelt in den Beschäftigungsfristen (MSchG §§ 3/5), liefert die Bemessungsdaten (Arbeits-/Entgeltbestätigung folgt mit AP8) und fuehrt die Abfertigung Neu auf fiktiver Grundlage. - models/mutterschutz.py: _at_msch_status (Kalendertage/Arbeitstage/ Episode aus der neuen Urlaubsart 'Mutterschutz-Frist', WET ATP_MSCH), _at_wochengeld — volle Wochengeld-Rechnung § 162 Abs 3 ASVG: Ø-Netto der drei Referenzmonate (letzte 13 Wochen) x (1 + satzungs- SZ-Prozentsatz 14/17/21 %, Parameter at_msch_sz_prozentsatz, vor Erstverwendung mandantenseitig zu verifizieren) ÷ Kalender- tage; Fallback kurzes DV (lb-msf-03) - Regel MSCH_ENTGELT: Entgelt-Stopp je Arbeitstag der Frist (negativ, Brutto; Tagesatz-Helper mit EFZG geteilt) - Fiktive BV-Grundlage § 7 Abs 4 BMSVG (RIS verifiziert): Monatsentgelt-Ø der letzten drei Kalendermonate vor dem Versicherungs- fall der Mutterschaft inkl. anteiliger Sonderzahlungen x Bezugs-KT/30, im _at_bv_basis_laufen-Hook neben der Krankengeld-Fiktion (AP4) - Rechenfaelle docs/rechenfaelle/mutterschutz.md; 4 Tests (Entgelt-Stopp + fiktive BV, §-162-Abs-3-Arithmetik, Wirkungslosigkeit ohne Frist, kurzes-DV-Fallback); Optionen dokumentiert: Sonderwochengeld § 163, Ruhen § 166, Karenz-Referenzmonat § 7 Abs 4 Z 1-3 --- .../__manifest__.py | 4 +- .../data/absence_private.xml | 21 ++ .../data/rule_parameters_msch_private.xml | 21 ++ .../data/salary_rules_msch_private.xml | 34 +++ .../docs/rechenfaelle/mutterschutz.md | 79 ++++++ .../models/__init__.py | 3 +- .../models/abfertigung_private.py | 10 +- .../l10n_at_hr_payroll_private/models/efzg.py | 7 +- .../models/mutterschutz.py | 229 ++++++++++++++++++ .../tests/__init__.py | 1 + .../tests/test_mutterschutz_private.py | 156 ++++++++++++ 11 files changed, 556 insertions(+), 9 deletions(-) create mode 100644 addons/l10n_at_hr_payroll_private/data/rule_parameters_msch_private.xml create mode 100644 addons/l10n_at_hr_payroll_private/data/salary_rules_msch_private.xml create mode 100644 addons/l10n_at_hr_payroll_private/docs/rechenfaelle/mutterschutz.md create mode 100644 addons/l10n_at_hr_payroll_private/models/mutterschutz.py create mode 100644 addons/l10n_at_hr_payroll_private/tests/test_mutterschutz_private.py diff --git a/addons/l10n_at_hr_payroll_private/__manifest__.py b/addons/l10n_at_hr_payroll_private/__manifest__.py index dcdac1b..2615e82 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.9.2.0', + 'version': '19.0.9.3.0', 'author': 'ixsol / gem360', 'website': 'https://gem360.at', 'category': 'Human Resources/Payroll', @@ -107,12 +107,14 @@ gem360-Brücke). 'data/salary_rules_private.xml', 'data/salary_rules_lst_private.xml', 'data/salary_rules_efzg_private.xml', + 'data/salary_rules_msch_private.xml', 'data/salary_rules_sv_private.xml', 'data/salary_rules_bvg_private.xml', 'data/hr_payroll_account_private.xml', 'data/rule_parameters_mbgm_private.xml', 'data/rule_parameters_wien_private.xml', 'data/rule_parameters_efzg_private.xml', + 'data/rule_parameters_msch_private.xml', 'data/bv_kasse_seed.xml', 'views/kv_views.xml', 'views/bv_views.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 f2f5d0e..e6a33b4 100644 --- a/addons/l10n_at_hr_payroll_private/data/absence_private.xml +++ b/addons/l10n_at_hr_payroll_private/data/absence_private.xml @@ -46,6 +46,16 @@ + + + + Mutterschutz-Frist (MSchG §§ 3/5) + 8 + MSF + ATP_MSCH + + + @@ -82,6 +92,17 @@ + + Mutterschutz-Frist (§§ 3/5 MSchG) + + hr + day + + + + + Unbezahlte Karenz diff --git a/addons/l10n_at_hr_payroll_private/data/rule_parameters_msch_private.xml b/addons/l10n_at_hr_payroll_private/data/rule_parameters_msch_private.xml new file mode 100644 index 0000000..91964fa --- /dev/null +++ b/addons/l10n_at_hr_payroll_private/data/rule_parameters_msch_private.xml @@ -0,0 +1,21 @@ + + + + + + + + + + Wochengeld-Sonderzahlungsprozentsatz (§ 162 Abs 3 ASVG) + at_msch_sz_prozentsatz + + + + + 2026-01-01 + 17.0 + + \ No newline at end of file diff --git a/addons/l10n_at_hr_payroll_private/data/salary_rules_msch_private.xml b/addons/l10n_at_hr_payroll_private/data/salary_rules_msch_private.xml new file mode 100644 index 0000000..8eff35e --- /dev/null +++ b/addons/l10n_at_hr_payroll_private/data/salary_rules_msch_private.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + Mutterschutz-Frist (kein Dienstgeber-Entgelt; Wochengeld zahlt ÖGK) + MSCH_ENTGELT + + + + python + + code + laufend + + + \ No newline at end of file diff --git a/addons/l10n_at_hr_payroll_private/docs/rechenfaelle/mutterschutz.md b/addons/l10n_at_hr_payroll_private/docs/rechenfaelle/mutterschutz.md new file mode 100644 index 0000000..e2d764a --- /dev/null +++ b/addons/l10n_at_hr_payroll_private/docs/rechenfaelle/mutterschutz.md @@ -0,0 +1,79 @@ +# Rechenfälle Mutterschutz/Wochengeld (AP7) + +Modell: `models/mutterschutz.py`, Regel `MSCH_ENTGELT` +(`data/salary_rules_msch_private.xml`), Parameter +`at_msch_sz_prozentsatz` (⚠ satzungsabhängig, vor Erstverwendung +verifizieren). + +## Rechtsquellen (verifiziert 2026-09-12) + +- **MSchG § 3 Abs 1** (Achtwochenfrist vor der voraussichtlichen + Entbindung, ärztliches Zeugnis) und **§ 5 Abs 1** (8 Wochen + Wochenbett; mindestens 12 bei Früh-/Mehrlingsgeburten oder + Kaiserschnitt; Verlängerung um vorzeitig verkürzte Achtwochenfrist, + max. 16 Wochen) — lokal, RIS. +- **ASVG § 162 Abs 1/3/4** (lb-msf-03, Stand 2026-07): Wochengeld = + **Leistung aus der Krankenversicherung**; Bemessung nach dem Ø- + Arbeitsverdienst der letzten 13 Wochen (drei Kalendermonate), + vermindert um Lohnsteuer, SV-Anteil, Kammerumlage, WBF-Beitrag, + Aufwandersätze, SW-Beitrag; erhöht um den satzungsmäßigen + **SZ-Prozentsatz (14/17/21 %)**; je Kalendertag. +- **BMSVG § 7 Abs 4** (RIS, verifiziert): während des Anspruchs auf + (Sonder-)Wochengeld bei aufrechtem Arbeitsverhältnis fiktive + BV-Grundlage = Monatsentgelt aus den letzten **drei + Kalendermonaten vor dem Versicherungsfall der Mutterschaft, + einschließlich anteiliger Sonderzahlungen**; Spezial-Referenzmonat + bei neuerlichem Verbot unmittelbar nach Karenz (Z 1–3, dort Monat + vor dem der Karenz vorangegangenen Verbot) — dokumentierte Option. + +## Rollenverteilung (klärt die frühere Plan-Annahme) + +Die ÖGK zahlt das Wochengeld direkt (§ 162 Abs 1 ASVG) — der +Dienstgeber zahlt für die Verbotstage **kein Entgelt** +(`MSCH_ENTGELT`-Reduktion je Arbeitstag), liefert die +Bemessungsdaten (hier vollständig berechnet; AEB mit AP8) und führt +die BV auf fiktiver Grundlage weiter (§ 7 Abs 4 BMSVG). +Eine „Dienstgeber-Wochengeldzahlung" (deutsches +Mutterschaftsgeld-Modell) existiert in Österreich nicht. + +## RF-MS-1 — Entgelt-Stopp und fiktive BV + +Eintritt 1. 7. 2026, KV-Gruppe II/1 (2.059,41 €), Referenzläufe +Aug/Sep/Okt voll entlohnt; Mutterschutz-Frist 1.–15. 11. 2026 +(15 Kalendertage, 10 Arbeitstage): + +- `MSCH_ENTGELT` = −(2.059,41 ÷ 21 AT) × 10 = −980,67 €. +- Fiktive BV-BG (§ 7 Abs 4) = Ø-Monatsentgelt der Monate Aug–Okt + (je 2.059,41, keine SZ-Zeilen) × 15/30 = 1.029,71 €. +- Wochengeld-Ausweis: Ø-Netto der drei Referenzläufe + 17 % + SZ-Prozentsatz ÷ 92 Kalendertage (31+30+31). + +## RF-MS-2 — Wochengeld-Berechnung (§ 162 Abs 3) + +Je Kalendertag: (Σ Netto der drei Referenzmonate × (1 + SZ-%)) ÷ +Kalendertage der drei Monate. Kurzes Dienstverhältnis: Ø über die +vorhandenen Monate; ohne jede Historie: Netto des laufenden Monats +(lb-msf-03). SZ-Fortzahlung während des Bezugs (vereinbart) wäre +reale Grundlage statt der fiktiven — Option. + +## RF-MS-3 — Fristenmodell + +Schutzfristen als Urlaubsart „Mutterschutz-Frist" (WET `ATP_MSCH`, +Krankmeldungspflicht-Feld zweckentfremdbar); die Berechnung der +Achtwochenfrist aus dem ärztlichen Zeugnis (§ 3 Abs 2 MSchG) und +Verlängerungen (§ 5 Abs 1) bleibt beim Mandanten. + +## Offene Punkte / dokumentierte Reste + +- **Sonderwochengeld** (§ 163 ASVG, Günstigkeitsvergleich § 162 + Abs 3b; Versicherungsfall ≤ 13 Wochen nach Karenzende bzw. + Teilzeit nach KBG) — Option, braucht Verdienthistorie vor dem + letzten Entgeltanspruch inkl. Valorisierung. +- **Ruhen** (§ 166 ASVG): EFZ > 50 %, Erwerbseinkommen, + Pflegekarenzgeld — Abrechnungs-/Melde-Schnittstelle (AP8-Kontext). +- **SZ-Prozentsatz** 14/17/21 % satzungsabhängig — Parameter vor + Erstverwendung gegen die Satzung des KV-Trägers verifizieren. +- **Karenz-Referenzmonat** (§ 7 Abs 4 Z 1–3 BMSVG): Folgeschwangerschaft + unmittelbar nach Karenz — Sonder-Bemessungsmonat, Option. +- **AEB SART 75/76** (Arbeits- und Entgeltbestätigung Wochengeld, + seit 2025 inkl. Prämien-/Provisionsfelder netto) — AP8. \ 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 abe2236..b3b1ea2 100644 --- a/addons/l10n_at_hr_payroll_private/models/__init__.py +++ b/addons/l10n_at_hr_payroll_private/models/__init__.py @@ -12,4 +12,5 @@ from . import versichertenmeldung from . import fristen_private from . import buchungsbeleg from . import wien_dag -from . import efzg \ No newline at end of file +from . import efzg +from . import mutterschutz \ No newline at end of file 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 0c58c1d..5e0fa86 100644 --- a/addons/l10n_at_hr_payroll_private/models/abfertigung_private.py +++ b/addons/l10n_at_hr_payroll_private/models/abfertigung_private.py @@ -214,11 +214,11 @@ class HrPayslip(models.Model): basis = self._l10n_at_sv_basis_roh(result_rules) if pflichtig_ab is None or pflichtig_ab <= self.date_from: # AP4: fiktive BG während des Krankengeldbezugs (§ 7 Abs 3 - # BMSVG) — 50 % des Vormonatsentgelts aliquot nach - # Kalendertagen der Halb-/ÖGK-Phase; nur in beitrags- - # pflichtigen Läufen (Erstfenster-Teilmonate: dokumentierter - # Randfall). - return basis + self._at_bv_fiktiv_krankengeld() + # BMSVG) und des Wochengeldbezugs (§ 7 Abs 4, AP7) — nur + # in beitragspflichtigen Läufen (Erstfenster-Teilmonate: + # dokumentierter Randfall). + return basis + self._at_bv_fiktiv_krankengeld() \ + + self._at_bv_fiktiv_wochengeld() if pflichtig_ab > self.date_to: # LZP liegt zur Gänze im beitragsfreien Erstfenster. return 0.0 diff --git a/addons/l10n_at_hr_payroll_private/models/efzg.py b/addons/l10n_at_hr_payroll_private/models/efzg.py index c642ab6..679ccbd 100644 --- a/addons/l10n_at_hr_payroll_private/models/efzg.py +++ b/addons/l10n_at_hr_payroll_private/models/efzg.py @@ -233,10 +233,13 @@ class HrPayslip(models.Model): def _at_efz_tagesatz(self, result_rules=None): """Voller Tages-Entgeltsatz des Monats (Entgeltausfalls- prinzip): laufende Monatsbasis ohne die eigenen - Teilentgelt-Reduktionen ÷ Arbeitstage des Kalendermonats.""" + Teilentgelt-Reduktionen ÷ Arbeitstage des Kalendermonats. + Geteilter Helper von EFZG (TEIL_ENTGELT_HALB, + KRANKENTGELT_PHASE) und Mutterschutz (MSCH_ENTGELT, AP7).""" self.ensure_one() basis = self._l10n_at_sv_basis_roh(result_rules) - for code in ('TEIL_ENTGELT_HALB', 'KRANKENTGELT_PHASE'): + for code in ('TEIL_ENTGELT_HALB', 'KRANKENTGELT_PHASE', + 'MSCH_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/mutterschutz.py b/addons/l10n_at_hr_payroll_private/models/mutterschutz.py new file mode 100644 index 0000000..e8f3e4e --- /dev/null +++ b/addons/l10n_at_hr_payroll_private/models/mutterschutz.py @@ -0,0 +1,229 @@ +# Part of the odoo-at-payroll project. License: LGPL-3. +"""AP7: Mutterschutz/Wochengeld (MSchG §§ 3/5, ASVG § 162, BMSVG +§ 7 Abs 4 — verifiziert 2026-09-12 gegen RIS bzw. Wissensbasis +lb-msf-03, Stand 2026-07). + +**Klargestellte Rollenverteilung** (lb-msf-03, § 162 Abs 1 ASVG): Das +Wochengeld ist eine **Leistung aus der Krankenversicherung** — die ÖGK +zahlt es direkt an die Arbeitnehmerin. Der Dienstgeber +1. schuldet für die Beschäftigungsverbots-Tage **kein Entgelt** + (Regel ``MSCH_ENTGELT`` stoppt die arbeitsseitigen Bezüge), +2. liefert die **Bemessungsdaten** (13-Wochen-/3-Kalendermonats- + Basis; Arbeits- und Entgeltbestätigung folgt mit AP8) — hier als + vollständige Wochengeld-Berechnung nach § 162 Abs 3 ASVG: + Ø-Arbeitsverdienst der letzten drei Kalendermonate (Geld- und + Sachbezüge, Entstehungsprinzip), vermindert um Lohnsteuer, + SV-Anteil, Kammerumlage, Wohnbauförderungsbeitrag, Aufwandersätze + und Schlechtwetterentschädigungsbeitrag (= Netto der + Referenzläufe), erhöht um den satzungsmäßigen Sonderzahlungs- + Prozentsatz (14/17/21 %, ``at_msch_sz_prozentsatz`` — mandanten- + seitig zu pflegen), je Kalendertag, +3. zahlt die **Abfertigung Neu auf fiktiver Grundlage** weiter + (§ 7 Abs 4 BMSVG): fiktive BG = Monatsentgelt, berechnet nach dem + in den letzten drei Kalendermonaten vor dem Versicherungsfall der + Mutterschaft gebührenden Entgelt **einschließlich anteiliger + Sonderzahlungen** (SZ-Fortzahlung während des Bezugs = vereinbarte + Ausnahme, dokumentierte Option), aliquot nach Kalendertagen + (30-Tage-Konvention). + +Schutzfristen: § 3 MSchG Achtwochenfrist vor der voraussichtlichen +Entbindung; § 5 MSchG acht Wochen Wochenbett (mindestens zwölf bei +Früh-/Mehrlingsgeburten oder Kaiserschnitt; Verlängerung um +vorzeitig verkürzte Achtwochenfrist, max. 16 Wochen) — als +Urlaubsart „Mutterschutz-Frist" (WET ``ATP_MSCH``) zu erfassen; die +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. +""" +from datetime import date, timedelta + +from odoo import fields, models + +_MSCH_WET = 'ATP_MSCH' + + +class HrPayslip(models.Model): + _inherit = 'hr.payslip' + + l10n_at_msch_info = fields.Char( + string='AT Mutterschutz/Wochengeld-Status', + compute='_compute_msch_info', + help='Wochengeld je Kalendertag (§ 162 Abs 3 ASVG), ' + 'Bezugstage und fiktive BV-Grundlage (§ 7 Abs 4 BMSVG) ' + 'im Abrechnungszeitraum.') + + def _compute_msch_info(self): + for slip in self: + msch = slip._at_msch_status() + if not msch: + slip.l10n_at_msch_info = '' + continue + wochengeld = slip._at_wochengeld() + slip.l10n_at_msch_info = ( + '%d Kalendertage Bezug · WG %.2f/Kalendertag ' + '(Ø-Netto %.2f + %.0f %%)' % ( + len(msch['kalendertage']), + wochengeld['tagesatz'], + wochengeld['netto_drei_monate'], + wochengeld['sz_prozent'])) + + def _at_msch_leaves(self): + """Validierte Mutterschutz-Frist-Abwesenheiten (ATP_MSCH).""" + self.ensure_one() + typen = self.env['hr.leave.type'].search([ + ('work_entry_type_id.code', '=', _MSCH_WET)]) + leaves = self.env['hr.leave'].search([ + ('employee_id', '=', self.employee_id.id), + ('holiday_status_id', 'in', typen.ids), + ('state', 'in', ('validate', 'validate1')), + ]) + return [ + (leave.request_date_from, leave.request_date_to) + for leave in leaves + if leave.request_date_from and leave.request_date_to] + + def _at_msch_status(self): + """Mutterschutz-Tage je Abrechnungsperiode: {'kalendertage', + 'arbeitstage', 'episode_beginn'} — None ohne Frist im Zeitraum. + + Episode-Beginn = Anfang des zusammenhängenden Fristenblocks, + der in die Abrechnungsperiode reicht (Versicherungsfall der + Mutterschaft, § 120 Abs 1 Z 3 ASVG) — Bemessungsstichtag für + Wochengeld (§ 162 Abs 3) und fiktive BV (§ 7 Abs 4 BMSVG).""" + self.ensure_one() + tage = {} + for von, bis in self._at_msch_leaves(): + tag = von + while tag <= bis: + tage[tag] = True + 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 + block = in_periode[0] + while (block - timedelta(days=1)) in tage: + block = block - timedelta(days=1) + 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', '=', _MSCH_WET), + ]) + return { + 'kalendertage': in_periode, + 'arbeitstage': sorted({e.date for e in eintraege}), + '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).""" + self.ensure_one() + stichtag = episode_beginn or self.date_from + erster = date(stichtag.year, stichtag.month, 1) + monate = [] + hilf = erster + for _ in range(3): + hilf = (hilf - timedelta(days=1)).replace(day=1) + monate.append(hilf) + return list(reversed(monate)) + + def _at_msch_referenz_slip(self, monats_beginn): + """Vollständiger validierter Referenzlauf des Monats (oder + leer).""" + self.ensure_one() + monats_ende = (monats_beginn.replace(day=28) + + timedelta(days=4)).replace(day=1) \ + - timedelta(days=1) + return self.env['hr.payslip'].search([ + ('employee_id', '=', self.employee_id.id), + ('state', 'in', ('validated', 'paid')), + ('date_from', '<=', monats_beginn), + ('date_to', '>=', monats_ende), + ], order='date_from desc', limit=1) + + def _at_wochengeld(self): + """Wochengeld-Berechnung nach § 162 Abs 3 ASVG (volle + Berechnung, AP7 — Ausweis/AEB-Daten; Zahlung durch die ÖGK): + + Ø-Arbeitsverdienst der drei Referenzmonate, vermindert um + Lohnsteuer, SV-Anteil, Kammerumlage, Wohnbauförderungsbeitrag, + Aufwandersätze und Schlechtwetterentschädigungsbeitrag (= + Netto der Referenzläufe), erhöht um den satzungsmäßigen + SZ-Prozentsatz, je Kalendertag der drei Monate. Kurzes + Dienstverhältnis: Ø über die vorhandenen Monate (lb-msf-03); + ohne jede Historie der laufende Monat.""" + self.ensure_one() + sz_prozent = float(self.env['hr.rule.parameter'] + ._get_parameter_from_code( + 'at_msch_sz_prozentsatz', self.date_from, + raise_if_not_found=False) or 0.0) + ergebnis = { + 'netto_drei_monate': 0.0, 'sz_prozent': sz_prozent, + 'kalendertage_basis': 0, 'tagesatz': 0.0, + 'referenz_monate': [], 'episode_beginn': None, + } + msch = self._at_msch_status() + if not msch: + return ergebnis + ergebnis['episode_beginn'] = msch['episode_beginn'] + netto, kalendertage, gefundene = 0.0, 0, [] + for monat in self._at_msch_referenz_monate( + msch['episode_beginn']): + ergebnis['referenz_monate'].append(monat) + monats_ende = (monat.replace(day=28) + + timedelta(days=4)).replace(day=1) \ + - timedelta(days=1) + kalendertage += monats_ende.day + slip = self._at_msch_referenz_slip(monat) + if slip: + gefundene.append(monat) + netto += sum(slip.line_ids.filtered( + lambda l: l.code == 'NET').mapped('total')) + if not gefundene: + # Ohne jede Historie: Netto des laufenden Monats + # (tatsächlicher Arbeitszeitraum, lb-msf-03). + netto = sum(self.line_ids.filtered( + lambda l: l.code == 'NET').mapped('total')) + kalendertage = 30 + ergebnis['netto_drei_monate'] = netto + ergebnis['kalendertage_basis'] = kalendertage + ergebnis['tagesatz'] = netto * (1.0 + sz_prozent / 100.0) \ + / max(kalendertage, 1) + return ergebnis + + def _at_bv_fiktiv_wochengeld(self): + """Fiktive BV-Grundlage während des Wochengeldbezugs + (§ 7 Abs 4 BMSVG, verifiziert): Monatsentgelt-Ø aus den + letzten drei Kalendermonaten vor dem Versicherungsfall der + Mutterschaft **einschließlich anteiliger Sonderzahlungen** + (hier: laufende + sonstige Bezüge der Referenzläufe; + SZ-Fortzahlung während des Bezugs = vereinbarte Ausnahme, + Option), aliquot nach Kalendertagen des Bezugs + (30-Tage-Konvention).""" + self.ensure_one() + msch = self._at_msch_status() + if not msch or not msch['kalendertage']: + return 0.0 + summe, monate = 0.0, 0 + for monat in self._at_msch_referenz_monate( + msch['episode_beginn']): + slip = self._at_msch_referenz_slip(monat) + if not slip: + continue + summe += (slip._l10n_at_brutto('laufend') + + slip._l10n_at_brutto('sonstig')) + monate += 1 + if not monate: + # Ohne Historie: laufender Monat (kurzes DV, lb-msf-03). + summe = (self._l10n_at_brutto('laufend') + + self._l10n_at_brutto('sonstig')) + monate = 1 + return summe / monate * len(msch['kalendertage']) / 30.0 \ 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 c49d65c..c890b04 100644 --- a/addons/l10n_at_hr_payroll_private/tests/__init__.py +++ b/addons/l10n_at_hr_payroll_private/tests/__init__.py @@ -10,3 +10,4 @@ from . import test_reports_private from . import test_wien_dag_private from . import test_einstufung_private from . import test_efzg_private +from . import test_mutterschutz_private diff --git a/addons/l10n_at_hr_payroll_private/tests/test_mutterschutz_private.py b/addons/l10n_at_hr_payroll_private/tests/test_mutterschutz_private.py new file mode 100644 index 0000000..b6c7468 --- /dev/null +++ b/addons/l10n_at_hr_payroll_private/tests/test_mutterschutz_private.py @@ -0,0 +1,156 @@ +# Part of the odoo-at-payroll project. License: LGPL-3. +"""AP7: Mutterschutz/Wochengeld — Entgelt-Stopp während der +Beschäftigungsverbote, volle Wochengeld-Berechnung (§ 162 Abs 3 +ASVG) als AEB-Datenlieferant, fiktive BV-Grundlage (§ 7 Abs 4 +BMSVG). Rechenfälle: docs/rechenfaelle/mutterschutz.md.""" +import datetime + +from odoo import SUPERUSER_ID +from odoo.tests import TransactionCase, tagged + + +@tagged('post_install', '-at_install') +class MutterschutzPrivateTest(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 MSchG 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.company.resource_calendar_id = self.calendar + self.msch_typ = self.env.ref( + 'l10n_at_hr_payroll_private.leave_type_at_mutterschutz') + + def _employee(self): + employee = self.env['hr.employee'].create({ + 'name': 'Mutterschutz Test', 'sex': 'female'}) + version = self.env['hr.version'].search( + [('employee_id', '=', employee.id)], limit=1) + version.write({ + 'contract_date_start': datetime.date(2026, 7, 1), + 'kv_id': self.kv2203.id, + 'kv_gruppe_id': self.gruppe_ii.id, + 'erfahrungsstufe': 1, + 'resource_calendar_id': self.calendar.id, + }) + version.generate_work_entries( + datetime.date(2026, 8, 1), datetime.date(2026, 12, 31)) + return employee + + def _slip(self, employee, monat): + erster = datetime.date(2026, 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': 'MSchG %d/2026' % monat, + 'employee_id': employee.id, + 'struct_id': self.structure.id, + 'date_from': erster, + 'date_to': letzter, + }) + slip.compute_sheet() + 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 _mutterschutz(self, employee, von, bis): + leave = self.env['hr.leave'].create({ + 'name': 'Mutterschutz %s' % von, + 'employee_id': employee.id, + 'holiday_status_id': self.msch_typ.id, + 'request_date_from': von, + 'request_date_to': bis, + }) + leave.with_user(SUPERUSER_ID)._action_validate() + + def test_01_entgelt_stopp_und_fiktive_bv(self): + """RF-MS-1: Mutterschutz-Frist 1.-15. 11. (15 KT, 10 AT) — + MSCH_ENTGELT = -Tagesatz x 10 AT; fiktive BV-Grundlage = + Ø-Monatsentgelt Aug-Okt x 15/30 (§ 7 Abs 4 BMSVG).""" + employee = self._employee() + referenz_slips = [self._slip(employee, m) for m in (8, 9, 10)] + self._mutterschutz( + employee, datetime.date(2026, 11, 1), + datetime.date(2026, 11, 15)) + slip_nov = self._slip(employee, 11) + msch = slip_nov._at_msch_status() + self.assertEqual(msch['episode_beginn'], + datetime.date(2026, 11, 1)) + self.assertEqual(len(msch['kalendertage']), 15) + self.assertEqual(len(msch['arbeitstage']), 10) + tagesatz = 2059.41 / 21 + self.assertAlmostEqual(self._total(slip_nov, 'MSCH_ENTGELT'), + -tagesatz * 10, places=2) + # Fiktive BV: Referenzmonate Aug/Sep/Okt, je 2.059,41 laufend + fiktiv = slip_nov._at_bv_fiktiv_wochengeld() + self.assertAlmostEqual(fiktiv, 2059.41 * 15 / 30.0, places=2) + basis = slip_nov._at_bv_basis_laufen() + laufend = slip_nov._l10n_at_sv_basis_roh() + self.assertAlmostEqual(basis, laufend + fiktiv, places=2) + + def test_02_wochengeld_berechnung(self): + """RF-MS-2: § 162 Abs 3 ASVG — Tagesatz = (Summe Netto der + drei Referenzmonate x (1 + 17 %)) ÷ Kalendertage (92).""" + employee = self._employee() + referenz_slips = [self._slip(employee, m) for m in (8, 9, 10)] + netto = sum( + sum(s.line_ids.filtered(lambda l: l.code == 'NET') + .mapped('total')) for s in referenz_slips) + self._mutterschutz( + employee, datetime.date(2026, 11, 1), + datetime.date(2026, 11, 15)) + slip_nov = self._slip(employee, 11) + wochengeld = slip_nov._at_wochengeld() + self.assertEqual(wochengeld['referenz_monate'], [ + datetime.date(2026, 8, 1), datetime.date(2026, 9, 1), + datetime.date(2026, 10, 1)]) + self.assertEqual(wochengeld['kalendertage_basis'], 92) + self.assertAlmostEqual(wochengeld['netto_drei_monate'], + netto, places=2) + self.assertAlmostEqual( + wochengeld['tagesatz'], + netto * 1.17 / 92.0, places=4) + + def test_03_ohne_mutterschutz_keine_wirkung(self): + """Ohne Frist: kein MSCH-Status, keine Reduktion, keine + fiktive BV, kein Wochengeld-Ausweis.""" + employee = self._employee() + slip = self._slip(employee, 11) + self.assertFalse(slip._at_msch_status()) + self.assertFalse(self._total(slip, 'MSCH_ENTGELT')) + self.assertEqual(slip._at_bv_fiktiv_wochengeld(), 0.0) + self.assertEqual(slip._at_wochengeld()['tagesatz'], 0.0) + + def test_04_kurzes_dienstverhaeltnis_fallback(self): + """Ohne Referenzläufe: Wochengeld aus dem laufenden Monat + (tatsächlicher Arbeitszeitraum, lb-msf-03).""" + employee = self._employee() + self._mutterschutz( + employee, datetime.date(2026, 11, 1), + datetime.date(2026, 11, 15)) + slip_nov = self._slip(employee, 11) + wochengeld = slip_nov._at_wochengeld() + netto = sum(slip_nov.line_ids.filtered( + lambda l: l.code == 'NET').mapped('total')) + self.assertAlmostEqual(wochengeld['netto_drei_monate'], + netto, places=2) + self.assertAlmostEqual( + wochengeld['tagesatz'], netto * 1.17 / 30.0, places=4) \ No newline at end of file