# 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. 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' 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() 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).""" 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_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(anzahl): 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