From 850e99df203fff6cea36c3083311e51ed73a41da Mon Sep 17 00:00:00 2001 From: fegger Date: Tue, 15 Sep 2026 13:33:49 +0200 Subject: [PATCH] =?UTF-8?q?[IMP]=20l10n=5Fat=5Fhr=5Fpayroll=5Fprivate:=20a?= =?UTF-8?q?dd=20premiums=20at=20termination=20under=20=C2=A7=2016=20AngG?= =?UTF-8?q?=20(AP14-B)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .agents/MEMORY.md | 38 +++++ .../__manifest__.py | 2 +- .../input_types_endabrechnung_private.xml | 8 + .../salary_rules_endabrechnung_private.xml | 47 ++++++ .../docs/rechenfaelle/endabrechnung.md | 57 ++++++- .../models/account_chart_private.py | 4 + .../models/endabrechnung.py | 27 ++- .../tests/test_endabrechnung_private.py | 154 +++++++++++++++++- .../views/endabrechnung_views.xml | 33 ++++ .../wizard/endabrechnung.py | 138 +++++++++++++++- 10 files changed, 499 insertions(+), 9 deletions(-) diff --git a/.agents/MEMORY.md b/.agents/MEMORY.md index ea7ee48..0532c22 100644 --- a/.agents/MEMORY.md +++ b/.agents/MEMORY.md @@ -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**): diff --git a/addons/l10n_at_hr_payroll_private/__manifest__.py b/addons/l10n_at_hr_payroll_private/__manifest__.py index 4aeb06e..2325739 100644 --- a/addons/l10n_at_hr_payroll_private/__manifest__.py +++ b/addons/l10n_at_hr_payroll_private/__manifest__.py @@ -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', diff --git a/addons/l10n_at_hr_payroll_private/data/input_types_endabrechnung_private.xml b/addons/l10n_at_hr_payroll_private/data/input_types_endabrechnung_private.xml index 06f5b02..089446a 100644 --- a/addons/l10n_at_hr_payroll_private/data/input_types_endabrechnung_private.xml +++ b/addons/l10n_at_hr_payroll_private/data/input_types_endabrechnung_private.xml @@ -47,4 +47,12 @@ Vergleichssumme — BV-Anwartschafts-Teil (Brutto) ATP_VERGLEICH_BV + + Aliquote Prämie bei Beendigung (Brutto, § 16 AngG) + ATP_PRAM_END + + + Später fällige anteilige Prämie (Brutto, § 67 Abs 10) + ATP_PRAM_END_TARIF + \ No newline at end of file diff --git a/addons/l10n_at_hr_payroll_private/data/salary_rules_endabrechnung_private.xml b/addons/l10n_at_hr_payroll_private/data/salary_rules_endabrechnung_private.xml index 79a078d..29a4036 100644 --- a/addons/l10n_at_hr_payroll_private/data/salary_rules_endabrechnung_private.xml +++ b/addons/l10n_at_hr_payroll_private/data/salary_rules_endabrechnung_private.xml @@ -15,6 +15,11 @@ + + + + + @@ -173,6 +178,48 @@ result = inputs['ATP_VERGLEICH_BV'].amount ]]> + + + Aliquote Prämie bei Beendigung (§ 16 AngG) + PRAM_END + + + + python + result = 'ATP_PRAM_END' in inputs and inputs['ATP_PRAM_END'].amount + sonstig + code + + + + + + Später fällige anteilige Prämie (§ 67 Abs 10) + PRAM_END_TARIF + + + + python + result = 'ATP_PRAM_END_TARIF' in inputs and inputs['ATP_PRAM_END_TARIF'].amount + laufend + + code + + + Zeitguthaben Normalarbeitszeit + 50 % (§ 19e Abs 2 AZG) diff --git a/addons/l10n_at_hr_payroll_private/docs/rechenfaelle/endabrechnung.md b/addons/l10n_at_hr_payroll_private/docs/rechenfaelle/endabrechnung.md index 408d1d8..b6fa90a 100644 --- a/addons/l10n_at_hr_payroll_private/docs/rechenfaelle/endabrechnung.md +++ b/addons/l10n_at_hr_payroll_private/docs/rechenfaelle/endabrechnung.md @@ -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). diff --git a/addons/l10n_at_hr_payroll_private/models/account_chart_private.py b/addons/l10n_at_hr_payroll_private/models/account_chart_private.py index 57f5a22..6fa9c23 100644 --- a/addons/l10n_at_hr_payroll_private/models/account_chart_private.py +++ b/addons/l10n_at_hr_payroll_private/models/account_chart_private.py @@ -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 (650–655 SV Arbeiter · 656–659 SV Angestellte) diff --git a/addons/l10n_at_hr_payroll_private/models/endabrechnung.py b/addons/l10n_at_hr_payroll_private/models/endabrechnung.py index 8b09124..b3c6752 100644 --- a/addons/l10n_at_hr_payroll_private/models/endabrechnung.py +++ b/addons/l10n_at_hr_payroll_private/models/endabrechnung.py @@ -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: diff --git a/addons/l10n_at_hr_payroll_private/tests/test_endabrechnung_private.py b/addons/l10n_at_hr_payroll_private/tests/test_endabrechnung_private.py index 8f59464..b81a617 100644 --- a/addons/l10n_at_hr_payroll_private/tests/test_endabrechnung_private.py +++ b/addons/l10n_at_hr_payroll_private/tests/test_endabrechnung_private.py @@ -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) diff --git a/addons/l10n_at_hr_payroll_private/views/endabrechnung_views.xml b/addons/l10n_at_hr_payroll_private/views/endabrechnung_views.xml index aba8478..05ff3d8 100644 --- a/addons/l10n_at_hr_payroll_private/views/endabrechnung_views.xml +++ b/addons/l10n_at_hr_payroll_private/views/endabrechnung_views.xml @@ -81,6 +81,21 @@ + + + + + + + + + + + + + + @@ -169,6 +184,24 @@ + + hr.payslip.form.pram.end + hr.payslip + + + + + + + + + + + + Endabrechnung (Beendigung) l10n.at.payroll.endabrechnung.wizard diff --git a/addons/l10n_at_hr_payroll_private/wizard/endabrechnung.py b/addons/l10n_at_hr_payroll_private/wizard/endabrechnung.py index 3e28443..0785614 100644 --- a/addons/l10n_at_hr_payroll_private/wizard/endabrechnung.py +++ b/addons/l10n_at_hr_payroll_private/wizard/endabrechnung.py @@ -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)