[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
@@ -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',
@@ -46,6 +46,16 @@
<field name="country_id" ref="base.at"/>
<field name="is_leave" eval="True"/>
</record>
<!-- AP7: Mutterschutz-Fristen (§§ 3/5 MSchG) — Wochengeldbezug, -->
<!-- Entgelt gestoppt; Wochengeld zahlt die ÖGK. -->
<record id="wett_atp_msch" model="hr.work.entry.type">
<field name="name">Mutterschutz-Frist (MSchG §§ 3/5)</field>
<field name="color">8</field>
<field name="display_code">MSF</field>
<field name="code">ATP_MSCH</field>
<field name="country_id" ref="base.at"/>
<field name="is_leave" eval="True"/>
</record>
<!-- Urlaubsarten (hr.leave.type) mit WET-Verknüpfung -->
<record id="leave_type_at_urlaub" model="hr.leave.type">
@@ -82,6 +92,17 @@
<field name="work_entry_type_id"
ref="l10n_at_hr_payroll_private.wett_atp_au"/>
</record>
<record id="leave_type_at_mutterschutz" model="hr.leave.type">
<field name="name">Mutterschutz-Frist (§§ 3/5 MSchG)</field>
<field name="requires_allocation" eval="False"/>
<field name="leave_validation_type">hr</field>
<field name="request_unit">day</field>
<field name="support_document" eval="True"/>
<field name="company_id" eval="False"/>
<field name="country_id" ref="base.at"/>
<field name="work_entry_type_id"
ref="l10n_at_hr_payroll_private.wett_atp_msch"/>
</record>
<record id="leave_type_at_karenz" model="hr.leave.type">
<field name="name">Unbezahlte Karenz</field>
<field name="requires_allocation" eval="False"/>
@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- AP7: Satzungsmäßiger Sonderzahlungs-Prozentsatz des -->
<!-- Wochengeldes (§ 162 Abs 3 ASVG; lb-msf-03, Stand 2026-07: -->
<!-- 14 % / 17 % / 21 % je nach Ausmaß der Sonderzahlungen — -->
<!-- ⚠ satzungsabhängig, vor Erstverwendung mandantenseitig gegen -->
<!-- die Satzung des KV-Trägers verifizieren; hier 17 % als -->
<!-- mittlerer Wert, kein verifizierter Mandantenwert). -->
<record id="hr_rule_parameter_at_msch_sz_prozentsatz" model="hr.rule.parameter">
<field name="name">Wochengeld-Sonderzahlungsprozentsatz (§ 162 Abs 3 ASVG)</field>
<field name="code">at_msch_sz_prozentsatz</field>
<field name="country_id" ref="base.at"/>
</record>
<record id="hr_rule_parameter_value_at_msch_sz_prozentsatz"
model="hr.rule.parameter.value">
<field name="rule_parameter_id"
ref="hr_rule_parameter_at_msch_sz_prozentsatz"/>
<field name="date_from">2026-01-01</field>
<field name="parameter_value">17.0</field>
</record>
</odoo>
@@ -0,0 +1,34 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- ============================================================ -->
<!-- AP7: Mutterschutz — Entgelt-Stopp während der Beschäftigungs- -->
<!-- verbote (MSchG §§ 3/5): Das Wochengeld zahlt die ÖGK (§ 162 -->
<!-- Abs 1 ASVG), der Dienstgeber schuldet für die Verbots-Tage -->
<!-- kein Entgelt (lb-msf-03). Negative Brutto-Zeile je Arbeitstag -->
<!-- aus dem Arbeitseintragssatz (Muster KRANKENTGELT_PHASE); die -->
<!-- fiktive BV-Grundlage läuft separat (§ 7 Abs 4 BMSVG). -->
<!-- ============================================================ -->
<record id="rule_msch_entgelt" model="hr.salary.rule">
<field name="name">Mutterschutz-Frist (kein Dienstgeber-Entgelt; Wochengeld zahlt ÖGK)</field>
<field name="code">MSCH_ENTGELT</field>
<field name="sequence" eval="47"/>
<field name="category_id" ref="l10n_at_hr_payroll_private.category_atp_brutto"/>
<field name="struct_id" ref="l10n_at_hr_payroll_private.structure_at_privat"/>
<field name="condition_select">python</field>
<field name="condition_python"><![CDATA[
msch = payslip._at_msch_status()
result = bool(msch and msch['arbeitstage'])
]]></field>
<field name="amount_select">code</field>
<field name="l10n_at_bemessung">laufend</field>
<field name="amount_python_compute"><![CDATA[
# Beschäftigungsverbot je Arbeitstag: volle Tages-Entgelt-
# Reduktion (Tagesatz ohne Teilentgelt-Reduktionen — Helper
# teilt die Ausschlüsse mit EFZG und MSCH).
msch = payslip._at_msch_status()
result = -payslip._at_efz_tagesatz(result_rules) \
* len(msch['arbeitstage'])
]]></field>
</record>
</odoo>
@@ -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 13, 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 AugOkt
(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 13 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.
@@ -13,3 +13,4 @@ from . import fristen_private
from . import buchungsbeleg
from . import wien_dag
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
@@ -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
@@ -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)