[IMP] l10n_at_hr_payroll_private: add premiums at termination under § 16 AngG (AP14-B)

Close the premium-at-termination gap of the end-of-employment cycle: an aliquote periodic premium (annual, sales, bonus, 15th payment) whose entitlement period outlasts the termination is covered by § 16 AngG — the pro-rata amount follows the ratio of the elapsed service to the entitlement period, is due for employees regardless of the termination ground (loss clauses void, 9 ObA 82/13v), while performance-linked premiums for workers may validly hinge on non-fault grounds and premiums tied to a specific service (e.g. Bilanzgeld) are not aliquote without the service (lb-end-18). Two routes: PRAM_END (bemessung sonstig) covers premiums paid with the end-of-employment run — a special payment inside the annual sixth at the flat 6 % rate with the 620 EUR free amount and 2 615 EUR free threshold checked by the core, SV as special-payment contributions (KV/PV/AV without AK/WF) against the monthly HBG and the annual SZ cap, fully payroll-levy liable and BV-contributory at the uncapped 1.53 % rate (§ 6 Abs 1 iVm Abs 5 BMSVG). PRAM_END_TARIF (bemessung laufend plus the core l10n_at_s67_abs6_ueberhang flag) covers premiums due at a pre-agreed maturity because their amount only becomes determinable after the entitlement period ends (e.g. balance-sheet premiums): taxed like running pay with the payment-month tariff without touching the annual sixth (§ 67 Abs 10, not a § 67 Abs 8 lit c case per 98/14/0009 = ARD 5483/14/2004) — the SV contribution month rolls back into the termination year via a new payslip field consumed by an extended branch of the existing SV-basis-by-Beitragsmonat override (historical caps and rates, E-MVB 044-01-00-006), while the LSt stays in the payment month. The wizard section previews the § 16 pro-rata amount from the full premium, entitlement period and elapsed service, keeps the payable amount confirmable (KB agreement), requires the service/claim-basis confirmation, enforces the contribution month for the deferred case, and lands the memo on the payslip for the separate Lohnzettel workflow (LStR Rz 911a); the existing manual PRAM rule is untouched. The account matrix posts both rules to 6230. Verified against the original statute text in .ris/ (AngG § 16, BMSVG § 6 Abs 5) and the knowledge-base source lb-end-18; the full SZ roll-forward (storno plus recomputation against the termination year's SZ annual cap) and the mBGM storno/re-transmission remain open in the GP7 payroll-reporting track, and the BV attribution of the deferred premium remains in the payment month as a documented approximation.

Version 19.0.16.0.0 (private)
This commit is contained in:
2026-09-15 13:33:49 +02:00
parent 472b19a4b2
commit 850e99df20
10 changed files with 499 additions and 9 deletions
+38
View File
@@ -2,6 +2,44 @@
## Current focus
**AP14-B Prämien bei Beendigung (§ 16 AngG) umgesetzt — Kern 73/73, Privat 191/191 Tests grün**
(2026-09-15, uncommittet bis Nutzerfreigabe; Privat **19.0.16.0.0**):
- **Recht (RIS-Wortlaut § 16 AngG + § 6 Abs 5 BMSVG verifiziert, lb-end-18):**
§-16-Aliquot = Betrag × zurückgelegte Dienstzeit / Dienstperiode;
Angestellte: unabhängig vom Beendigungsgrund gebührend (Verlust-/Kündigungs-
Klauseln unwirksam, 9 ObA 82/13v); Arbeiter: leistungsabhängige Prämien
dürfen wirksam an „kein Verschulden“ gebunden sein; spezifische Leistungen
(z. B. Bilanzgeld) ohne Tätigkeit nicht aliquot. LSt: mit Endabrechnung
sonstiger Bezug im Sechstel (6 %); später zum VORHINREIN vereinbarten
Fälligkeitszeitpunkt (Höhe erst nach Periodenablauf ermittelbar, z. B.
Bilanzprämie) → § 67 Abs 10: Tarif im Zuflussmonat OHNE Sechstel, kein
lit-c-Fall (98/14/0009), gesonderter Lohnzettel (Rz 911a). SV: SZ-artig
→ SZ-HBG-Mechanik; spätere Prämie → Rollung ins Austrittsjahr (E-MVB
044-01-00-006); LNK grundsätzlich pflichtig; BV 1,53 % ungecappt (§ 6 Abs 5).
- **Privat (19.0.16.0.0):** Regeln `PRAM_END` (seq 35, bemessung sonstig —
Sechstel/SZ-SV/LNK/BV) + `PRAM_END_TARIF` (seq 36, laufend + Kern-Flag
`l10n_at_s67_abs6_ueberhang` = §-67-Abs-10-Pfad ohne Sechstel); Eingaben
ATP_PRAM_END/ATP_PRAM_END_TARIF; Konten 6230 (LSt über LST_SZ/LST_LFD,
keine eigene LSt-Regel); Wizard-Abschnitt (§-16-Aliquot-Vorschau, Zahlungs-
art, Rollungs-Beitragsmonat, Bestätigungspflichten, Nachweis + Payslip-Tab
`l10n_at_pram_nachweis`); Lauf-Feld `l10n_at_pram_beitragsmonat` + Roll-
Branch in der bestehenden `_l10n_at_sv_basis_laufen_monate`-Überschreibung
(Pin erweitert — **Achtung: keine zweite Methode mit gleichem Namen anlegen,
die letzte Definition gewinnt**); LSt bleibt im Zuflussmonat.
- **Tests:** endabrechnung 24 (RF-PRM-1..3: sonstig-Fall mit SZ-SV/LNK/BV,
§-67-Abs-10 mit historischer Rollung 2025-06 aus 2026er-Auszahlung,
Wizard-E2E + 3 UserError-Fälle); Suite **208/208** (73 Kern + 191 Privat)
auf Wegwerf-DB. `compute_sheet()` löscht Linien neu — Linien-Overrides
überleben nicht; Lauf-Felder (wie der Rollungs-Beitragsmonat) sind der
richtige Weg.
- **Verifikationsreste/GP7:** vollständige SZ-Aufrollung (Storno + Neurechnung
gegen SZ-JahreshBG des Austrittsjahres) und mBGM-Storno/Neuübermittlung
offen; BV-Basis der späteren Prämie verbleibt im Zuflussmonat
(Approximation dokumentiert); gesonderter Lohnzettel Rz 911a (GP7).
- **Offen:** AP16 Karenz-Reste (Gate 8.13). Nicht committen ohne
Nutzeranforderung.
**AP15-C Vergleichssummen (§ 67 Abs 8 lit a) umgesetzt — Kern 73/73, Privat 188/188 Tests grün**
(2026-09-15, uncommittet bis Nutzerfreigabe; Privat **19.0.15.0.0**):
@@ -1,7 +1,7 @@
# Part of the odoo-at-payroll project. License: LGPL-3.
{
'name': 'Österreich - Privatwirtschaft Personalverrechnung (General-AT)',
'version': '19.0.15.0.0',
'version': '19.0.16.0.0',
'author': 'Florian Egger',
'website': 'https://gem360.at',
'category': 'Human Resources/Payroll',
@@ -47,4 +47,12 @@
<field name="name">Vergleichssumme — BV-Anwartschafts-Teil (Brutto)</field>
<field name="code">ATP_VERGLEICH_BV</field>
</record>
<record id="input_type_atp_pram_end" model="hr.payslip.input.type">
<field name="name">Aliquote Prämie bei Beendigung (Brutto, § 16 AngG)</field>
<field name="code">ATP_PRAM_END</field>
</record>
<record id="input_type_atp_pram_end_tarif" model="hr.payslip.input.type">
<field name="name">Später fällige anteilige Prämie (Brutto, § 67 Abs 10)</field>
<field name="code">ATP_PRAM_END_TARIF</field>
</record>
</odoo>
@@ -15,6 +15,11 @@
<!-- Override, BV-Anwartschafts-Teil bis 7.500 € mit 6 %; beide -->
<!-- SV-pflichtig (Zuflussmonat; Aufrollung je Beitragsmonat bzw. -->
<!-- PV-Verlängerung siehe lb-end-22) und LNK-pflichtig. -->
<!-- Prämie: § 16 AngG-Aliquot bei Beendigung (AP14-B, lb-end-18) — -->
<!-- mit Endabrechnung sonstiger Bezug (Sechstel, SZ-SV); später -->
<!-- fällig über § 67 Abs 10 (tarifisch im Zuflussmonat ohne -->
<!-- Sechstel, Kern-Flag) mit Rollung des SV-Beitragsmonats ins -->
<!-- Austrittsjahr (Wizard-Write auf die Linie; mBGM-Storno GP7). -->
<!-- ============================================================ -->
<!-- Urlaubsersatzleistung — laufender Anteil (§ 67 Abs 8 lit d). -->
@@ -173,6 +178,48 @@ result = inputs['ATP_VERGLEICH_BV'].amount
]]></field>
</record>
<!-- Aliquote Prämie/Sonderzahlung bei Beendigung (§ 16 AngG):
mit der Endabrechnung ausgezahlt — sonstiger Bezug im
Jahressechstel (6 %, Freibetrag/Freigrenze), SZ-SV, voll
LNK- und BV-pflichtig (lb-end-18). -->
<record id="rule_pram_end" model="hr.salary.rule">
<field name="name">Aliquote Prämie bei Beendigung (§ 16 AngG)</field>
<field name="code">PRAM_END</field>
<field name="sequence" eval="35"/>
<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">result = 'ATP_PRAM_END' in inputs and inputs['ATP_PRAM_END'].amount</field>
<field name="l10n_at_bemessung">sonstig</field>
<field name="amount_select">code</field>
<field name="amount_python_compute"><![CDATA[
result = inputs['ATP_PRAM_END'].amount
]]></field>
</record>
<!-- Später fällige anteilige Prämie (§ 67 Abs 10): die Höhe ist
erst nach Ablauf der Anspruchsperiode ermittelbar und wird zum
vereinbarten Fälligkeitszeitpunkt ausgezahlt — wie ein laufender
Bezug zum Tarif des Zuflussmonats, OHNE Sechstel-Erhöhung
(Kern-Flag), gesonderter Lohnzettel (LStR Rz 911a); der
SV-Beitragsmonat wird per Rollung ins Austrittsjahr auf die
Linie geschrieben (E-MVB 044-01-00-006). -->
<record id="rule_pram_end_tarif" model="hr.salary.rule">
<field name="name">Später fällige anteilige Prämie (§ 67 Abs 10)</field>
<field name="code">PRAM_END_TARIF</field>
<field name="sequence" eval="36"/>
<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">result = 'ATP_PRAM_END_TARIF' in inputs and inputs['ATP_PRAM_END_TARIF'].amount</field>
<field name="l10n_at_bemessung">laufend</field>
<field name="l10n_at_s67_abs6_ueberhang" eval="True"/>
<field name="amount_select">code</field>
<field name="amount_python_compute"><![CDATA[
result = inputs['ATP_PRAM_END_TARIF'].amount
]]></field>
</record>
<!-- Zeitguthaben an Normalarbeitszeit (§ 19e Abs 2 AZG: + 50 %). -->
<record id="rule_zeit_guthaben" model="hr.salary.rule">
<field name="name">Zeitguthaben Normalarbeitszeit + 50 % (§ 19e Abs 2 AZG)</field>
@@ -1,4 +1,4 @@
# Endabrechnungs-Zyklus — Rechenfälle AP14-A / AP15-A / AP15-C
# Endabrechnungs-Zyklus — Rechenfälle AP14-A / AP14-B / AP15-A / AP15-C
Stand: 2026-09. AP14-A bildet die Beendigungs-Abrechnung auf der
privaten Structure ab: Urlaubsersatzleistung (UEL), Abgangsentschädigung,
@@ -236,6 +236,53 @@ BV-Kassen-Anwartschaft (lb-end-21 Beispiel 2):
Fünftel-Tarifteil ist über die laufende Bemessung pfändbar, seine
Lohnsteuer steckt in `LST_LFD`.
## RF-PRM-1 — Aliquote Prämie, mit Endabrechnung (§ 16 AngG)
3.000 € aliquote Jahres-/Leistungsprämie für 3 der 12 Monate
Anspruchszeitraum (voller Betrag 12.000 € × 3/12, lb-end-18):
- `PRAM_END` (bemessung „sonstig“): sonstiger Bezug innerhalb des
Jahressechstels mit 6 % (`LST_SZ`-Pfad, Freibetrag 620/Freigrenze
2.615 vom Kern geprüft), ohne Sechstel-Erhöhung;
- SZ-SV: KV/PV/AV ohne AK/WF (§ 62 Z 3/Z 5), monatliche HBG und
SZ-Jahreshöchstbetrag (Kern `_l10n_at_svdn_basis_sz`);
- voll LNK-pflichtig (DB/DZ/KommSt, lb-end-18), BV 1,53 % ungecappt
(§ 6 Abs 1 iVm Abs 5 BMSVG);
- § 16: für Angestellte unabhängig vom Beendigungsgrund gebührend;
leistungsabhängige Prämien bei Arbeitern dürfen wirksam an „kein
Verschulden“ gebunden sein; Prämien mit spezifischer Leistungs-
verknüpfung (z. B. Bilanzgeld) sind ohne Tätigkeit nicht aliquot
zu leisten. Wizard bestätigt die Leistungs-/Anspruchsgrundlage.
## RF-PRM-2 — Später fällige Prämie (§ 67 Abs 10) mit SV-Rollung
3.000 € anteilige Prämie für Leistungen im Austrittsjahr 2025, Höhe
erst nach Ablauf der Periode ermittelbar, Fälligkeit und Auszahlung
März 2026 (lb-end-18):
- `PRAM_END_TARIF` (bemessung „laufend“ + Kern-Flag
`l10n_at_s67_abs6_ueberhang`): wie ein laufender Bezug zum Tarif des
Zuflussmonats (LST_LFD), OHNE Sechstel-Erhöhung (§ 67 Abs 10);
- keine Nachzahlung iSd § 67 Abs 8 lit c, weil die Zahlung nicht
später als zum vereinbarten Fälligkeitszeitpunkt erfolgt
(98/14/0009 = ARD 5483/14/2004);
- SV-Rollung: Lauf-Feld `l10n_at_pram_beitragsmonat` ordnet die
Zeile dem Beitragsmonat des Austrittsjahres zu (E-MVB
044-01-00-006) — historische HBG (2025: 6.450 €) und Sätze des
Rollmonats, private Überschreibung von
`_l10n_at_sv_basis_laufen_monate` (Pin); LSt und mBGM-Schnittstelle
(Storno/Neuübermittlung) bleiben im GP7-Track;
- gesonderter Lohnzettel für den Kalendermonat der Zahlung
(LStR Rz 911a); LNK und BV wie RF-PRM-1.
## RF-PRM-3 — Wizard-Aliquot und Validierungen
Der Wizard-Abschnitt schlägt den §-16-Aliquot-Betrag vor (voller
Betrag × zurückgelegte Dienstzeit / Anspruchsperiode), der zahlende
Betrag bleibt bestätigbar (KV-Vereinbarung). Validierungen:
Leistungs-/Anspruchsgrundlage bestätigen, Dienstzeit ≤ Anspruchszeit-
raum, bei späterer Fälligkeit Beitragsmonat der Rollung erforderlich.
## Offene Punkte (Verifikationsreste)
- KZ-Positionierung der Abs-6-Bezüge (außerhalb KZ 210/220, LSt in
@@ -248,8 +295,12 @@ BV-Kassen-Anwartschaft (lb-end-21 Beispiel 2):
praxis (zwölf Monate voller Bezüge) widerspricht dem VwGH
(Krankengeld 6/7 einbeziehen) — bewusst ohne Automatik, Basis ist
bestätigbar (lb-end-13 ⚠).
- Prämien-Beendigung (§ 16 AngG-Aliquotierung, Rollung) — PRAM/SZ-Regeln
vorhanden; Feinschliff AP14-B.
- Prämien-Beendigung (§ 16 AngG-Aliquotierung, Rollung) — mit AP14-B
umgesetzt (`PRAM_END`/`PRAM_END_TARIF`, Wizard-Abschnitt,
Beitragsmonat-Rollung auf dem Lauf); offen bleibt die vollständige
SZ-Aufrollung (Storno + Neurechnung gegen die SZ-Jahreshöchst-
betrag des Austrittsjahres) und das mBGM-Storno/Neuübermitteln
(GP7/Meldewesen-Track).
- Meldewesen-Schnittstellen: „Ende des Entgeltanspruches" in mBGM/
Abmeldung (GP7), gesonderter Lohnzettel bei Zahlung außerhalb des
Beendigungsmonats (LStR 2002 Rz 911a).
@@ -83,6 +83,10 @@ L10N_AT_BUCHUNGSMATRIX = {
# Teil — gleiche vorläufige Aufwandskonten wie die Abfertigungen.
'rule_vergleich': {'debit': ('6200',), 'debit_arbeiter': ('6000',)},
'rule_vergleich_bv': {'debit': ('6200',), 'debit_arbeiter': ('6000',)},
# Prämie bei Beendigung (§ 16 AngG): mit Endabrechnung und später
# fällig — Prämien-Aufwandskonto wie rule_pram.
'rule_pram_end': {'debit': ('6230',)},
'rule_pram_end_tarif': {'debit': ('6230',)},
# Nettoauszahlung (reconcilable, s. o.)
'rule_netto': {'credit': ('3850',)},
# DG-Beiträge (650655 SV Arbeiter · 656659 SV Angestellte)
@@ -145,6 +145,19 @@ class HrPayslip(models.Model):
'BV-Anwartschafts-Zeiträume; beim Pauschalvergleich der '
'Verhältnisfaktor Forderungsbetrag : Vergleichssumme '
'(E-MVB 011-02-00-001).')
l10n_at_pram_nachweis = fields.Text(
string='AT Prämie bei Beendigung — Nachweis/Begründung', copy=False,
help='Anspruchsgrundlage (KV-/Vertragsvereinbarung, '
'Anspruchszeitraum), §-16-Aliquotierung, '
'Leistungsverknüpfung und Fälligkeitsvereinbarung '
'dokumentieren (lb-end-18).')
l10n_at_pram_beitragsmonat = fields.Date(
string='AT Prämie (§ 67 Abs 10) — SV-Beitragsmonat der Rollung',
copy=False,
help='Beitragsmonat, dem die später fällige anteilige Prämie '
'sv-rechtlich zuzuordnen ist — der Zeitraum, in dem die '
'Leistungen erbracht wurden (E-MVB 044-01-00-006, lb-end-18). '
'Leer = Zuflussmonat.')
# ------------------------------------------------------------------
# SV-Tage-Umrechnung und Verlängerungsmonate (§ 11 Abs 2 ASVG)
@@ -590,7 +603,11 @@ class HrPayslip(models.Model):
gelegt. Der Standardanteil jedes Monats bleibt auf die volle
monatliche HBG gedeckelt, der Verlängerungsanteil auf den
Tageswert (monatliche HBG × Verlängerungstage/30, lb-url-15);
die Fenster laufen sequenziell (Abgang zuerst, UEL danach)."""
die Fenster laufen sequenziell (Abgang zuerst, UEL danach).
AP14-B: die später fällige Prämie (PRAM_END_TARIF) rollt über
l10n_at_pram_beitragsmonat in den Beitragsmonat des
Austrittsjahres (E-MVB 044, lb-end-18); die LSt bleibt im
Zuflussmonat (§ 67 Abs 10)."""
self.ensure_one()
gruppen = {}
verlaengerung = {}
@@ -617,8 +634,12 @@ class HrPayslip(models.Model):
tage_pro_monat[monat] = \
tage_pro_monat.get(monat, 0) + tage
continue
monat = (rule.l10n_at_beitragsmonat or self.date_from) \
.replace(day=1)
if rule.code == 'PRAM_END_TARIF' \
and self.l10n_at_pram_beitragsmonat:
monat = self.l10n_at_pram_beitragsmonat.replace(day=1)
else:
monat = (rule.l10n_at_beitragsmonat or self.date_from) \
.replace(day=1)
gruppen[monat] = gruppen.get(monat, 0.0) + werte['total']
standard = self.date_from.replace(day=1)
if standard in gruppen:
@@ -4,7 +4,9 @@ lit d), SV-Verlängerung (§ 11 Abs 2 ASVG, sequentielle Fenster),
Abgangsentschädigung (§ 67 Abs 8 lit b Fünftel), Aufrechnung offener
Vorschüsse (§ 293 Abs 3 EO) und Zeitguthaben (§ 19e Abs 2 AZG).
AP15-C: Vergleichssummen (§ 67 Abs 8 lit a) — Rest-Fünftel,
BV-Anwartschafts-Teil mit 6 %/7.500-€-Deckel, Wizard."""
BV-Anwartschafts-Teil mit 6 %/7.500-€-Deckel, Wizard.
AP14-B: Prämien bei Beendigung (§ 16 AngG-Aliquot, lb-end-18) —
sonstiger Bezug mit Endabrechnung, § 67 Abs 10 mit Rollung."""
from datetime import date, timedelta
from odoo.exceptions import UserError
@@ -798,3 +800,153 @@ class EndabrechnungPrivateTest(TransactionCase):
self.assertAlmostEqual(
self._total(slip, 'LST_VERGLEICH_BV'),
7500.0 * (1.0 - satz / 100.0) * steuersatz / 100.0, places=2)
# ------------------------------------------------------------------
# RF-PRM-1: Prämie mit Endabrechnung (§ 16 AngG, sonstiger Bezug)
# ------------------------------------------------------------------
def test_22_pram_end_mit_endabrechnung(self):
"""3.000 € aliquote Prämie, mit der Endabrechnung ausgezahlt
(lb-end-18): sonstiger Bezug im Jahressechstel (6 %-Band,
Sechstel unverändert), SZ-SV (ohne AK/WF), voll LNK-pflichtig,
BV-Beitrag 1,53 % ungecappt (§ 6 Abs 5 BMSVG)."""
emp_mit = self._employee(ueberzahlung=3000.0)
emp_ohne = self._employee(ueberzahlung=3000.0)
for emp in (emp_mit, emp_ohne):
emp.version_id.write({'l10n_at_abfertigung_neu': True})
slip_mit = self._slip(emp_mit)
slip_ohne = self._slip(emp_ohne)
self._input(slip_mit, 'ATP_PRAM_END', 3000.0)
self.assertAlmostEqual(self._total(slip_mit, 'PRAM_END'), 3000.0,
places=2)
# Sonstiger Bezug erhöht das Jahressechstel nicht
self.assertAlmostEqual(
slip_mit._l10n_at_sechstel() - slip_ohne._l10n_at_sechstel(),
0.0, places=2)
# SZ-Bemessung enthält die Prämie (SV-DN nach § 67 Abs 12
# abziehen, um den Brutto-Anteil zu isolieren)
svdn_sz_diff = (slip_mit._l10n_at_svdn_sz()
- slip_ohne._l10n_at_svdn_sz())
self.assertAlmostEqual(
slip_mit._l10n_at_sz_bemessung()
- slip_ohne._l10n_at_sz_bemessung() + svdn_sz_diff,
3000.0, places=2)
# SZ-SV: KV/PV/AV, ohne AK/WF (§ 62 Z 3/Z 5)
satz_sz = sum(slip_mit._l10n_at_sv_satz(
'dn', k, staffel_basis=slip_mit._l10n_at_sv_basis_roh())
for k in ('kv', 'pv', 'av'))
self.assertAlmostEqual(svdn_sz_diff, 3000.0 * satz_sz / 100.0,
places=2)
# LNK über die volle Prämie
self.assertAlmostEqual(
slip_mit._at_lnk_basis('flaf')
- slip_ohne._at_lnk_basis('flaf'), 3000.0, places=2)
# BV 1,53 % ungecappt (§ 6 Abs 1 iVm Abs 5 BMSVG)
satz_bv = float(slip_mit._l10n_at_param('at_sv_bv_dg'))
self.assertAlmostEqual(
slip_mit._at_bv_beitrag() - slip_ohne._at_bv_beitrag(),
3000.0 * satz_bv / 100.0, places=2)
# ------------------------------------------------------------------
# RF-PRM-2: Später fällige Prämie (§ 67 Abs 10) mit SV-Rollung
# ------------------------------------------------------------------
def test_23_pram_end_tarif_und_rollung(self):
"""3.000 € anteilige Prämie für Leistungen im Austrittsjahr 2025,
fällig und ausgezahlt im März 2026 (lb-end-18): tarifisch im
Zuflussmonat OHNE Sechstel-Erhöhung (§ 67 Abs 10); SV-Rollung
auf den Beitragsmonat im Austrittsjahr mit historischen Sätzen
und HBG (E-MVB 044)."""
emp_mit = self._employee(ueberzahlung=3000.0,
ende=date(2025, 12, 31))
emp_ohne = self._employee(ueberzahlung=3000.0,
ende=date(2025, 12, 31))
slip_mit = self._slip(emp_mit, monat=3, jahr=2026)
slip_ohne = self._slip(emp_ohne, monat=3, jahr=2026)
self._input(slip_mit, 'ATP_PRAM_END_TARIF', 3000.0)
self.assertAlmostEqual(self._total(slip_mit, 'PRAM_END_TARIF'),
3000.0, places=2)
# Sechstel unverändert (§ 67 Abs 10: kein Jahressechstel-Effekt)
self.assertAlmostEqual(
slip_mit._l10n_at_sechstel() - slip_ohne._l10n_at_sechstel(),
0.0, places=2)
# Tarifisch in der laufenden Bemessung des Zuflussmonats
svdn_diff = (slip_mit._l10n_at_svdn_gesamt()
- slip_ohne._l10n_at_svdn_gesamt())
self.assertAlmostEqual(
slip_mit._l10n_at_bemessung_laufen()
- slip_ohne._l10n_at_bemessung_laufen() + svdn_diff,
3000.0, places=2)
# LNK über die volle Prämie
self.assertAlmostEqual(
slip_mit._at_lnk_basis('flaf')
- slip_ohne._at_lnk_basis('flaf'), 3000.0, places=2)
# Rollung: SV-Beitragsmonat auf dem Lauf (Austrittsjahr 2025);
# historische HBG (2025: 6.450 €) und Sätze des Rollmonats
slip_mit.write({'l10n_at_pram_beitragsmonat': date(2025, 6, 1)})
slip_mit.compute_sheet()
basis = self._basis_monate(slip_mit)
self.assertAlmostEqual(basis[date(2025, 6, 1)], 3000.0, places=2)
# Im Zuflussmonat bleibt die Prämie außer Ansatz (§ 54 ASVG:
# Zuordnung zum Leistungszeitraum) — die Basis entspricht der
# eines Laufs ohne Prämie.
basis_ohne = self._basis_monate(slip_ohne)
self.assertAlmostEqual(basis[date(2026, 3, 1)],
basis_ohne[date(2026, 3, 1)], places=2)
# ------------------------------------------------------------------
# RF-PRM-3: Wizard-E2E und Validierungen (§-16-Aliquot, Rollung)
# ------------------------------------------------------------------
def test_24_pram_wizard(self):
emp = self._employee()
emp.version_id.write({
'structure_type_id': self.env.ref(
'l10n_at_hr_payroll_private.structure_type_at_privat').id,
'l10n_at_abfertigung_neu': False,
})
basis = {
'employee_id': emp.id,
'austrittsdatum': date(2026, 3, 15),
'departure_reason_id': self.env.ref(
'l10n_at_hr_payroll.departure_reason_einvernehmlich').id,
'pram_aktiv': True,
'pram_voller_betrag': 6000.0,
'pram_anspruchszeitraum_monate': 12,
'pram_dienstzeit_monate': 3,
'pram_nachweis': 'KV-Prämienvereinbarung, Leistungszeitraum',
}
wizard = self.env['l10n.at.payroll.endabrechnung.wizard'].create(
dict(basis))
self.assertAlmostEqual(wizard.pram_aliquot_vorschau, 1500.0,
places=2)
with self.assertRaises(UserError):
wizard.action_payslip() # Leistungsgrundlage nicht bestätigt
wizard.write({'pram_leistung_bestaetigt': True,
'pram_betrag': 1000.0})
ergebnis = wizard.action_payslip()
slip = self.env['hr.payslip'].browse(ergebnis['res_id'])
self.assertAlmostEqual(self._total(slip, 'PRAM_END'), 1000.0,
places=2)
self.assertEqual(
slip.l10n_at_pram_nachweis,
'KV-Prämienvereinbarung, Leistungszeitraum')
# Später fällig: Beitragsmonat der Rollung erforderlich
wizard2 = self.env[
'l10n.at.payroll.endabrechnung.wizard'].create({
**basis,
'pram_leistung_bestaetigt': True,
'pram_betrag': 1000.0,
'pram_zahlungsart': 'spaeter_faellig',
})
with self.assertRaises(UserError):
wizard2.action_payslip() # Rollungs-Beitragsmonat fehlt
wizard2.write({'pram_beitragsmonat': date(2026, 1, 1)})
ergebnis = wizard2.action_payslip()
slip2 = self.env['hr.payslip'].browse(ergebnis['res_id'])
self.assertAlmostEqual(self._total(slip2, 'PRAM_END_TARIF'),
1000.0, places=2)
self.assertEqual(slip2.l10n_at_pram_beitragsmonat,
date(2026, 1, 1))
basis = self._basis_monate(slip2)
self.assertIn(date(2026, 1, 1), basis)
@@ -81,6 +81,21 @@
<field name="vergleich_bv_lst_vorschau"/>
</group>
</group>
<group string="Prämie bei Beendigung (§ 16 AngG)">
<field name="pram_aktiv"/>
<group invisible="not pram_aktiv">
<field name="pram_voller_betrag"/>
<field name="pram_anspruchszeitraum_monate"/>
<field name="pram_dienstzeit_monate"/>
<field name="pram_aliquot_vorschau"/>
<field name="pram_betrag"/>
<field name="pram_zahlungsart"/>
<field name="pram_beitragsmonat"
invisible="pram_zahlungsart != 'spaeter_faellig'"/>
<field name="pram_nachweis" placeholder="KV-/Vertragsvereinbarung, Aliquotierung, Fälligkeitszeitpunkt, Leistungsverknüpfung"/>
<field name="pram_leistung_bestaetigt"/>
</group>
</group>
<group string="Vorschau (§ 10 UrlG, § 11 Abs 2 ASVG)">
<group>
<field name="uel_lfd_vorschau"/>
@@ -169,6 +184,24 @@
</field>
</record>
<record id="hr_payslip_view_form_pram_end" model="ir.ui.view">
<field name="name">hr.payslip.form.pram.end</field>
<field name="model">hr.payslip</field>
<field name="inherit_id" ref="hr_payroll.view_hr_payslip_form"/>
<field name="arch" type="xml">
<xpath expr="//notebook" position="inside">
<page string="Prämie bei Beendigung (§ 16 AngG)"
name="l10n_at_pram_end"
invisible="not l10n_at_pram_nachweis">
<group>
<field name="l10n_at_pram_nachweis"
readonly="state != 'draft'"/>
</group>
</page>
</xpath>
</field>
</record>
<record id="action_endabrechnung_wizard" model="ir.actions.act_window">
<field name="name">Endabrechnung (Beendigung)</field>
<field name="res_model">l10n.at.payroll.endabrechnung.wizard</field>
@@ -183,6 +183,62 @@ class L10nAtEndabrechnungWizard(models.TransientModel):
help='Vergleichstext, strittige Ansprüche, erkennbare '
'Abfertigungsanteile, BV-Zeiträume und allfälliger '
'Verhältnisfaktor dokumentieren.')
pram_aktiv = fields.Boolean(
string='Prämie bei Beendigung (§ 16 AngG)',
help='Aliquote periodische Prämie/Sonderzahlung (Jahres-, '
'Umsatzprämie, Bonifikation, 15. Bezug), deren Anspruchs-'
'zeitraum bei der Beendigung noch nicht abgelaufen ist '
'(lb-end-18).')
pram_zahlungsart = fields.Selection(
[('mit_endabrechnung',
'Mit der Endabrechnung ausgezahlt (sonstiger Bezug, '
'Jahressechstel)'),
('spaeter_faellig',
'Später fällig (§ 67 Abs 10 — Tarif im Zahlungsmonat, '
'ohne Sechstel; gesonderter Lohnzettel)')],
string='Zahlungs-/Fälligkeitsart', default='mit_endabrechnung',
help='Wird die Prämie erst zum im Vorhinein vereinbarten '
'Fälligkeitszeitpunkt fällig, weil ihre Höhe erst nach '
'Ablauf der Anspruchsperiode ermittelbar ist (z. B. '
'Bilanzprämie), liegt keine Nachzahlung vor und läuft die '
'Besteuerung über § 67 Abs 10 (lb-end-18).')
pram_anspruchszeitraum_monate = fields.Integer(
string='Anspruchszeitraum (Monate)', default=12,
help='Dienstperiode, für die die Prämie gewährt wird — z. B. '
'Kalenderjahr = 12 Monate (§ 16 AngG).')
pram_dienstzeit_monate = fields.Integer(
string='Zurückgelegte Dienstzeit im Anspruchszeitraum (Monate)',
help='Grundlage der §-16-Aliquot-Vorschau; der tatsächliche '
'Betrag folgt der KV-/Vertragsvereinbarung und bleibt '
'bestätigbar.')
pram_voller_betrag = fields.Monetary(
string='Voller Prämien-Betrag (vor Aliquotierung)',
currency_field='currency_id',
help='Betrag für die gesamte Anspruchsperiode — Basis der '
'§-16-Aliquot-Vorschau.')
pram_betrag = fields.Monetary(
string='Zu zahlender (aliquote) Brutto-Betrag',
currency_field='currency_id')
pram_beitragsmonat = fields.Date(
string='SV-Beitragsmonat (Rollung ins Austrittsjahr)',
help='Nur bei späterer Fälligkeit: Beitragsmonat, dem die '
'Prämie sv-rechtlich zuzuordnen ist — der Zeitraum, in dem '
'die Leistungen erbracht wurden (E-MVB 044-01-00-006). '
'Die mBGM-Storno-/Neuübermittlung erfolgt im GP7-Track.')
pram_leistung_bestaetigt = fields.Boolean(
string='Leistungs-/Anspruchsgrundlage bestätigt',
help='Bestätigung, dass die Prämie nicht an eine noch nicht '
'erbrachte spezifische Leistung gebunden ist (z. B. '
'Bilanzgeld — kein aliquoter Anspruch ohne Tätigkeit) und '
'allfällige leistungsabhängige Verlustklauseln (nur '
'Arbeiter, wirksam nur bei schuldhaftem Beendigungsgrund) '
'geprüft sind. Für Angestellte gebührt die aliquote Prämie '
'unabhängig von der Art der Beendigung (§ 16 AngG).')
pram_nachweis = fields.Text(
string='Nachweis/Begründung',
help='KV-/Vertragsvereinbarung, Anspruchszeitraum, '
'Aliquotierung, Fälligkeitszeitpunkt und '
'Leistungsverknüpfung dokumentieren.')
currency_id = fields.Many2one(
'res.currency', default=lambda self: self.env.company.currency_id)
@@ -240,6 +296,9 @@ class L10nAtEndabrechnungWizard(models.TransientModel):
vergleich_bv_lst_vorschau = fields.Monetary(
string='LSt 6 % BV-Teil (Vorschau)', currency_field='currency_id',
compute='_compute_vorschau')
pram_aliquot_vorschau = fields.Monetary(
string='§-16-Aliquote Prämie (Vorschau)',
currency_field='currency_id', compute='_compute_vorschau')
@api.onchange('employee_id')
def _onchange_employee(self):
@@ -253,6 +312,31 @@ class L10nAtEndabrechnungWizard(models.TransientModel):
wizard._at_abf_alt_vorschlag_setzen()
wizard._at_abf_frei_vorschlag_setzen()
@api.onchange('pram_aktiv', 'pram_voller_betrag',
'pram_dienstzeit_monate', 'pram_anspruchszeitraum_monate')
def _onchange_pram(self):
for wizard in self:
if not wizard.pram_aktiv:
continue
aliquote = wizard._at_pram_aliquot()
if aliquote and not wizard.pram_betrag:
wizard.pram_betrag = aliquote
def _at_pram_aliquot(self):
"""§ 16 AngG (Originalwortlaut): Betrag, der dem Verhältnis
zwischen der Dienstperiode des Anspruchs und der zurückgelegten
Dienstzeit entspricht."""
self.ensure_one()
if (not self.pram_anspruchszeitraum_monate
or self.pram_anspruchszeitraum_monate < 0
or self.pram_dienstzeit_monate < 0
or not self.pram_voller_betrag):
return 0.0
return (self.pram_voller_betrag
* min(self.pram_dienstzeit_monate,
self.pram_anspruchszeitraum_monate)
/ self.pram_anspruchszeitraum_monate)
@api.onchange('austrittsdatum', 'abf_alt_anspruchsart',
'abf_alt_dienstzeit_monate', 'abf_alt_aktiv',
'abf_alt_monatsentgelt', 'abf_alt_mehrfach',
@@ -306,7 +390,10 @@ class L10nAtEndabrechnungWizard(models.TransientModel):
'abf_frei_erhaltene_abfertigungen',
'abf_frei_gesetzlicher_anspruch',
'vergleich_aktiv', 'vergleich_rest_betrag',
'vergleich_bv_betrag')
'vergleich_bv_betrag',
'pram_aktiv', 'pram_voller_betrag',
'pram_dienstzeit_monate',
'pram_anspruchszeitraum_monate')
def _compute_vorschau(self):
for wizard in self:
wizard.uel_lfd_vorschau = 0.0
@@ -327,6 +414,7 @@ class L10nAtEndabrechnungWizard(models.TransientModel):
wizard.vergleich_fuenftel_frei_vorschau = 0.0
wizard.vergleich_tarif_basis_vorschau = 0.0
wizard.vergleich_bv_lst_vorschau = 0.0
wizard.pram_aliquot_vorschau = 0.0
if not wizard.employee_id or not wizard.austrittsdatum:
continue
slip = wizard._vorschau_payslip()
@@ -369,6 +457,7 @@ class L10nAtEndabrechnungWizard(models.TransientModel):
slip._at_fuenftel_tarif_basis()
wizard.vergleich_bv_lst_vorschau = \
slip._at_vergleich_bv_lst_6()
wizard.pram_aliquot_vorschau = wizard._at_pram_aliquot()
def _vorschau_payslip(self):
"""Flüchtiger Payslip-Kontext für die Vorschau-Berechnung (kein
@@ -405,6 +494,7 @@ class L10nAtEndabrechnungWizard(models.TransientModel):
self.abf_frei_gesetzlicher_anspruch,
'l10n_at_abf_frei_nachweis': self.abf_frei_nachweis,
'l10n_at_vergleich_nachweis': self.vergleich_nachweis,
'l10n_at_pram_nachweis': self.pram_nachweis,
})
eingaben = [
('ATP_UEL_TAGE', self.uel_tage),
@@ -416,6 +506,12 @@ class L10nAtEndabrechnungWizard(models.TransientModel):
if self.vergleich_aktiv else 0.0),
('ATP_VERGLEICH_BV', self.vergleich_bv_betrag
if self.vergleich_aktiv else 0.0),
('ATP_PRAM_END', self.pram_betrag
if self.pram_aktiv
and self.pram_zahlungsart == 'mit_endabrechnung' else 0.0),
('ATP_PRAM_END_TARIF', self.pram_betrag
if self.pram_aktiv
and self.pram_zahlungsart == 'spaeter_faellig' else 0.0),
]
slip.input_line_ids = [
(0, 0, {
@@ -444,6 +540,12 @@ class L10nAtEndabrechnungWizard(models.TransientModel):
if self.vergleich_aktiv else 0.0),
('ATP_VERGLEICH_BV', self.vergleich_bv_betrag
if self.vergleich_aktiv else 0.0),
('ATP_PRAM_END', self.pram_betrag
if self.pram_aktiv
and self.pram_zahlungsart == 'mit_endabrechnung' else 0.0),
('ATP_PRAM_END_TARIF', self.pram_betrag
if self.pram_aktiv
and self.pram_zahlungsart == 'spaeter_faellig' else 0.0),
]
def _abf_alt_validieren(self):
@@ -535,6 +637,34 @@ class L10nAtEndabrechnungWizard(models.TransientModel):
'Zeiträume voraus (§ 67 Abs 8 lit a letzter '
'Satz).'))
def _pram_validieren(self):
self.ensure_one()
if not self.pram_aktiv:
return
if not self.pram_leistung_bestaetigt:
raise UserError(_('Bitte die Leistungs-/Anspruchsgrundlage '
'der Prämie bestätigen (lb-end-18).'))
if self.pram_betrag <= 0:
raise UserError(_('Der zu zahlende Prämien-Betrag muss '
'größer als null sein.'))
if (self.pram_anspruchszeitraum_monate < 0
or self.pram_dienstzeit_monate < 0
or self.pram_voller_betrag < 0):
raise UserError(_('Anspruchszeitraum, Dienstzeit und '
'voller Betrag dürfen nicht negativ '
'sein.'))
if (self.pram_anspruchszeitraum_monate
and self.pram_dienstzeit_monate
> self.pram_anspruchszeitraum_monate):
raise UserError(_('Die zurückgelegte Dienstzeit kann den '
'Anspruchszeitraum nicht übersteigen.'))
if (self.pram_zahlungsart == 'spaeter_faellig'
and not self.pram_beitragsmonat):
raise UserError(_('Bei späterer Fälligkeit ist der SV-'
'Beitragsmonat der Rollung ins '
'Austrittsjahr erforderlich '
'(E-MVB 044-01-00-006).'))
def action_payslip(self):
"""Endabrechnungs-Lauf im Austrittsmonat anlegen und die
gesammelten offenen Posten als Eingaben vorbefüllen."""
@@ -542,6 +672,7 @@ class L10nAtEndabrechnungWizard(models.TransientModel):
self._abf_alt_validieren()
self._abf_frei_validieren()
self._vergleich_validieren()
self._pram_validieren()
erster = self.austrittsdatum.replace(day=1)
letzter = (erster + timedelta(days=32)).replace(day=1) \
- timedelta(days=1)
@@ -587,6 +718,11 @@ class L10nAtEndabrechnungWizard(models.TransientModel):
self.abf_frei_nachweis if self.abf_frei_aktiv else False,
'l10n_at_vergleich_nachweis':
self.vergleich_nachweis if self.vergleich_aktiv else False,
'l10n_at_pram_nachweis':
self.pram_nachweis if self.pram_aktiv else False,
'l10n_at_pram_beitragsmonat': self.pram_beitragsmonat
if (self.pram_aktiv
and self.pram_zahlungsart == 'spaeter_faellig') else False,
})
typen = {code: self.env['hr.payslip.input.type'].search(
[('code', '=', code)], limit=1)