[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
@@ -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)