Files
odoo-at-payroll/addons/l10n_at_gemeinde_payroll/tests/test_entgelt.py
T
fegger a7959c1259 [IMP] l10n_at_gemeinde_payroll: implement AP2 pay computation engine (GemBG Bgld.)
Entgelt-Engine nach Implementierungsplan AP2 (freigegeben 2026-09-09):

- Structure Type "Bgld. GemBG 2014" + Structure "Gemeindebedienstete"
  mit 4 Kategorien (GEMBG_BRUTTO, GEMBG_ABZUG, GEMBG_NETTO, GEMBG_DG).
- Work-Entry-Types: Überstunden Tag/Nacht (§ 76), Mehrdienstleistung,
  Journaldienst (§ 79), Bereitschaft (§ 80), Sonn-/Feiertag (§ 78).
- Input-Types: Erschwerniszulage (§ 83), Fahrtkostenzuschuss (§ 88),
  Jubiläumszuwendung (§ 89).
- Entgelt-Regeln (13 Rules):
  * ME — Monatsentgelt: Katalog-Lookup (Gruppe+Stufe+Stichtag),
    Ausbildungsphase 5 % Abzug (§ 133h), Teilzeit via work_time_rate.
  * KIZU — Kinderzulage: kinder_anzahl × 15,60 EUR (§ 61 GemBG),
    bei Teilzeit UNGEKÜRZT (§ 59 Abs 2).
  * FUZU — Funktionszulage: hr.version-Feld × work_time_rate (§ 62).
  * UEZ_TAG/UEZ_NACHT — Überstundenvergütung (§ 76):
    Grund = ME / (4,33 × Wochenstunden); Zuschlag 50 % Tag / 100 % Nacht.
  * JZU — Journaldienstzulage (§ 79): Work-Entry × Parametersatz.
  * SFZU — Sonn-/Feiertagszulage (§ 78): Stunden × Grundvergütung × %.
  * BERB — Bereitschaftsentschädigung (§ 80): Stunden × Parametersatz.
  * ERZU/FKZ/JUB — Input-basierte Bezüge (§§ 83/88/89).
  * SZQ — Sonderzahlung QUARTALSWEISE (§ 133n): in Quartalsendmonaten
    (März, Juni, September, Dezember) 50 % × (ME + KIZU).
  * NETTO — Kategoriensumme (wird mit AP3/AP4 erweitert).
- hr.version-Felder: kinder_anzahl, funktionszulage, ausbildungsphase
  (§ 61/§ 62/§ 133h) mit Employee-Spiegel nach Engine-Muster.
- Rule-Parameter: Kinderzulage 15,60 EUR, Journaldienstzulage,
  Sonn-/Feiertags-%, Bereitschaftsstunde (letzte 3 als AP0-Platzhalter
  mit 0.0 — Werte aus der Bezügeanpassung nach Beschaffung).
- Tests (8 Rechenfälle): Vollzeit av1 Stufe 1, Kinderzulage 2 Kinder
  = 31,20 EUR, Funktionszulage, Ausbildungsphase 5 % Abzug,
  SZ Quartalsende + Nicht-Quartalsende, Netto=Brutto (AP3/AP4 offen),
  Stufe 12 Max.
- Employee-Form um Zulagen-Gruppe erweitert.

Modul-Version 19.0.1.3.0. Statische Validierung grün; Runtime-Testlauf
auf dem Dev-Host ausständig.
2026-09-09 13:01:00 +02:00

117 lines
4.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Part of the gem360 onboarding. See LICENSE / project docs for details.
from datetime import date
from odoo.tests import TransactionCase, tagged
@tagged('post_install', '-at_install')
class EntgeltTest(TransactionCase):
"""AP2 Rechenfälle: Monatsentgelt, Kinderzulage, Funktionszulage,
Sonderzahlung (quartalsweise), Ausbildungsphase."""
def setUp(self):
super().setUp()
self.structure = self.env.ref(
'l10n_at_gemeinde_payroll.structure_gembg_bgld')
self.gruppe_av1 = self.env.ref(
'l10n_at_gemeinde_payroll.bgld_grp_av_av1')
self.parameter = self.env['hr.rule.parameter']
def _create_employee_with_payslip(self, month=None, **kwargs):
employee = self.env['hr.employee'].create({
'name': 'Test GemBG Entgelt',
})
version = self.env['hr.version'].search(
[('employee_id', '=', employee.id)], limit=1)
if not version:
version = self.env['hr.version'].create({
'employee_id': employee.id,
'date_start': date(2020, 1, 1),
})
version_vals = {
'entlohnungsgruppe_id': self.gruppe_av1.id,
'entlohnungsstufe': kwargs.get('stufe', 1),
}
version_vals.update(kwargs.get('version_vals', {}))
version.write(version_vals)
# Ausbildungsphase und Zulagen direkt auf Version
if 'ausbildungsphase' in kwargs:
version.ausbildungsphase = kwargs['ausbildungsphase']
if 'funktionszulage' in kwargs:
version.funktionszulage = kwargs['funktionszulage']
if 'kinder_anzahl' in kwargs:
version.kinder_anzahl = kwargs['kinder_anzahl']
slip_month = month or 7
slip = self.env['hr.payslip'].create({
'employee_id': employee.id,
'struct_id': self.structure.id,
'date_from': date(2026, slip_month, 1),
'date_to': date(2026, slip_month, 28),
})
slip.compute_sheet()
return employee, slip
def _get_rule_total(self, slip, code):
line = slip.line_ids.filtered(
lambda l: l.code == code)
return line.total if line else 0.0
def test_01_monatsentgelt_av1_stufe1(self):
"""Vollzeit av1 Stufe 1 = 7.135,20 € (Wert 1.7.2026)."""
_, slip = self._create_employee_with_payslip(month=7)
self.assertAlmostEqual(
self._get_rule_total(slip, 'ME'), 7135.20, places=2)
def test_02_kinderzulage_ungekuerzt_bei_teilzeit(self):
"""Kinderzulage: 2 Kinder = 31,20 €, auch bei Teilzeit."""
_, slip = self._create_employee_with_payslip(
month=7, kinder_anzahl=2)
# 2 × 15,60 = 31,20
self.assertAlmostEqual(
self._get_rule_total(slip, 'KIZU'), 31.20, places=2)
def test_03_funktionszulage(self):
"""Funktionszulage: 200 € am Vertrag."""
_, slip = self._create_employee_with_payslip(
month=7, funktionszulage=200.0)
self.assertAlmostEqual(
self._get_rule_total(slip, 'FUZU'), 200.0, places=2)
def test_04_ausbildungsphase_5_prozent_abzug(self):
"""§ 133h: 5 % Abzug auf Monatsentgelt."""
_, slip = self._create_employee_with_payslip(
month=7, ausbildungsphase=True)
# 7.135,20 × 0,95 = 6.778,44
self.assertAlmostEqual(
self._get_rule_total(slip, 'ME'), 6778.44, places=2)
def test_05_sonderzahlung_quartalsende(self):
"""SZ in September (Q3-Ende) = 50 % × (ME + KIZU)."""
_, slip = self._create_employee_with_payslip(
month=9, kinder_anzahl=1)
me = self._get_rule_total(slip, 'ME')
kizu = self._get_rule_total(slip, 'KIZU')
sz = self._get_rule_total(slip, 'SZQ')
self.assertAlmostEqual(sz, (me + kizu) * 0.5, places=2)
def test_06_sonderzahlung_kein_quartalsende(self):
"""Kein SZ außerhalb der Quartalsendmonate."""
_, slip = self._create_employee_with_payslip(month=7)
self.assertEqual(self._get_rule_total(slip, 'SZQ'), 0.0)
def test_07_netto_ohne_abzuege(self):
"""R1: Netto = Brutto (AP3/AP4 Abzüge noch nicht implementiert)."""
_, slip = self._create_employee_with_payslip(
month=7, kinder_anzahl=1, funktionszulage=100.0)
brutto = self._get_rule_total(slip, 'ME') \
+ self._get_rule_total(slip, 'KIZU') \
+ self._get_rule_total(slip, 'FUZU')
netto = self._get_rule_total(slip, 'NETTO')
self.assertAlmostEqual(netto, brutto, places=2)
def test_08_stufe_12_max(self):
"""Stufe 12 von av1 = 8.712,50 €."""
_, slip = self._create_employee_with_payslip(month=7, stufe=12)
self.assertAlmostEqual(
self._get_rule_total(slip, 'ME'), 8712.50, places=2)