[IMP] l10n_at_hr_payroll_private: Mutterschutz/Wochengeld — Entgelt-Stopp, volle §-162-Abs-3-Berechnung, fiktive BV (AP7)

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
This commit is contained in:
2026-09-12 23:27:20 +02:00
parent 0abc1383d1
commit 290b432b73
11 changed files with 556 additions and 9 deletions
@@ -12,4 +12,5 @@ from . import versichertenmeldung
from . import fristen_private
from . import buchungsbeleg
from . import wien_dag
from . import efzg
from . import efzg
from . import mutterschutz
@@ -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
@@ -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']
@@ -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