From 9add58fe83579c68cf658ff2744091986372b3af Mon Sep 17 00:00:00 2001 From: fegger Date: Tue, 15 Sep 2026 11:01:55 +0200 Subject: [PATCH] [IMP] l10n_at_hr_payroll_private: add statutory old-system severance (AP15-A) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pay the pre-2003 AngG severance from the end-of-employment run as its own neutral benefit line: free of SV (§ 49 Abs 3 Z 7 ASVG) and payroll levies, outside the annual sixth, and collected into the § 291d EO termination pot net of its own wage tax. Taxation follows the binding better-of rule of § 67 Abs 3 EStG: multiple-of-method (tariff wage tax on the running monthly wage × the statutory multiple of the payout) versus the flat 6 % rate, applied mandatorily in favour of the employee. The tariff wage tax is captured as a separately confirmed value instead of deriving it from gross pay through the § 62 engine. The wizard enforces § 23a Abs 3 AngG (parental exit needs five uninterrupted years), blocks BMSVG-new contracts except frozen claims and validates AGRD 02/06 against the claim type. The LNK basis subtracts § 67 Abs 3/6 rules only when they actually run in the tax base, the L16 generator reports LST_ABF in key 260 outside 210/220, and result_rules access in the garnishment paths avoids DefaultDictPayroll materialisation. Verified against the original statute texts in .ris/ (AngG §§ 23/23a, EStG § 67 Abs 3). Version 19.0.13.0.0 (private), 19.0.9.0.0 (core) --- .agents/MEMORY.md | 44 +++- addons/l10n_at_hr_payroll/__manifest__.py | 2 +- .../l10n_at_hr_payroll/models/lohnsteuer.py | 1 + .../models/lohnzettel_l16.py | 4 +- .../l10n_at_hr_payroll/tests/test_elda_l16.py | 32 +++ .../tests/test_lohnsteuer.py | 1 + .../__manifest__.py | 3 +- .../input_types_endabrechnung_private.xml | 4 + .../rule_parameters_endabrechnung_private.xml | 2 +- .../salary_rules_endabrechnung_private.xml | 19 ++ .../data/salary_rules_lst_private.xml | 15 ++ .../data/salary_rules_pfaendung_private.xml | 19 ++ .../docs/rechenfaelle/endabrechnung.md | 52 ++++- .../models/account_chart_private.py | 5 + .../models/endabrechnung.py | 100 +++++++- .../models/payslip_private.py | 3 +- .../models/pfaendung.py | 9 +- .../tests/test_endabrechnung_private.py | 187 ++++++++++++++- .../tests/test_pfaendung_private.py | 24 ++ .../views/endabrechnung_views.xml | 48 +++- .../wizard/endabrechnung.py | 220 ++++++++++++++++-- 21 files changed, 753 insertions(+), 41 deletions(-) diff --git a/.agents/MEMORY.md b/.agents/MEMORY.md index 6f1c010..dfa9015 100644 --- a/.agents/MEMORY.md +++ b/.agents/MEMORY.md @@ -2,8 +2,50 @@ ## Current focus +**AP15-A gesetzliche Abfertigung Alt umgesetzt — Privat 141/141, Kern 54/54 Tests grün** +(2026-09-15, uncommittet bis Nutzerfreigabe; Privat **19.0.13.0.0**, Kern **19.0.9.0.0**): + +- **Recht am Originalwortlaut verifiziert (.ris/):** § 23a Abs 3 AngG + verlangt für den Elternaustritt ein **ununterbrochen fünfjähriges** + DV (Hälfte des §-23-Betrags, Deckel 3 Monatsentgelte) — Vorschlag + bei < 60 Monaten korrigiert (0-Faktor + Wizard-UserError). § 67 + Abs 3 EStG (RIS-Wortlaut): Tarif-LSt **auf den laufenden + Arbeitslohn** × Mehrfaches, zwingend günstiger als 6 %. Neu: + getrenntes, bestätigtes Feld `abf_alt_monatslohnsteuer`/ + `l10n_at_abf_alt_monatslohnsteuer` (Tarif-LSt des laufenden + Monats nach persönlichen Verhältnissen) — Bruttolohn wird NICHT + über die §-62-Engine tarifiert. Rechenfall lb-end-01 exakt: + 101,96 × 10,5 = 1.070,58 (< 1.134 = 6 %). +- **Privat:** `models/endabrechnung.py` ABF_ALT-Helfer (Staffel, + Mehrfach-Vorschlag eltern/tod, 6 %, Vervielfachung, günstige LSt), + Regel `ABF_ALT` (neutral/keine/abs3/pfandung_beendigung), + `LST_ABF` seq 72, `PFAENDUNG_END` seq 77 (Beendigungstopf − + LST_ABF, Gesamttopf + beendigung), LNK-Fix in beiden Kopien + (`_at_lnk_basis` zieht Abs-3/6-Regeln nur mit bemessung + laufend/sonstig ab), Wizard-Felder + Validierungen (BMSVG-Neu- + Sperre außer eingefroren, AGRD 02/06-Regeln, Eltern < 5 J), + Payslip-Tab, Konten `rule_abf_alt` 6200/6000 (vorläufig), + `rule_lst_abf` → 3540. `.get()`-Zugriffe auf `result_rules` in + `models/pfaendung.py` durch Membership-Prüfung ersetzt. +- **Kern:** LST_ABF in KZ-260-Salden (Lohnsteuer), außerhalb + KZ 210/220 (test_01b). Kern-Hilfer `_l10n_at_monatslohnsteuer` + des Subagenten wieder ENTFERNT (ungenutzt, semantisch + irreführend) samt test_02b. +- **Tests:** endabrechnung 13 (inkl. Arbeitnehmerkündigung- + UserError, Eltern < 5 J, Wizard-E2E, Pfändungsbasis), pfändung + 12 (test_09b Beendigungstopf), Privat-Suite 141/141 (`ap13a_test`), + Kern-Suite 54/54 (auf `ap13a_test` mit Privatmodul — **Achtung: + L16-Tests referenzieren `l10n_at_hr_payroll_private.` + `structure_at_privat`, Kern-Rein-DB scheitert an setUp, ist + Bestandsverhalten**). Python/XML/`git diff --check` grün. +- **Offen:** AP15-B (freiwillige Abfertigung § 67 Abs 6, 6 %-Bänder + + rollierende 12-Monats-Basis), AP15-C (§ 67 Abs 8 lit a + Vergleichssummen). Wizard-Transient NOT-NULL-Warnung für + `departure_reason_id` ist kosmetisch (Alt-Zeilen). Nicht + committen ohne Nutzeranforderung. + **AP14-A Endabrechnungs-Zyklus umgesetzt — 133 Tests grün** -(2026-09-14, uncommittet bis Nutzerfreigabe): +(2026-09-14, Commit **`ee94f46`** nach Nutzerfreigabe): - **Rechtskorrektur gegen Originalwortlaut (.ris/, RIS-Gate erledigt):** § 67 Abs 6 (freiwillige Abfertigung/Abfindung) = 6 %-Bänder diff --git a/addons/l10n_at_hr_payroll/__manifest__.py b/addons/l10n_at_hr_payroll/__manifest__.py index b7d38e9..8f818e1 100644 --- a/addons/l10n_at_hr_payroll/__manifest__.py +++ b/addons/l10n_at_hr_payroll/__manifest__.py @@ -1,7 +1,7 @@ # Part of the odoo-at-payroll project. License: LGPL-3. { 'name': 'Österreich - Personalverrechnung Kern (AT Payroll Core)', - 'version': '19.0.8.0.0', + 'version': '19.0.9.0.0', 'author': 'Florian Egger', 'website': 'https://gem360.at', 'category': 'Human Resources/Payroll', diff --git a/addons/l10n_at_hr_payroll/models/lohnsteuer.py b/addons/l10n_at_hr_payroll/models/lohnsteuer.py index 646cae0..55adbe5 100644 --- a/addons/l10n_at_hr_payroll/models/lohnsteuer.py +++ b/addons/l10n_at_hr_payroll/models/lohnsteuer.py @@ -400,6 +400,7 @@ class HrPayslip(models.Model): # ------------------------------------------------------------------ # § 66 Monatslohnsteuer — kumulativ mit Rollung # ------------------------------------------------------------------ + def _l10n_at_lst_laufend(self, result_rules=None): """LSt auf laufende Bezüge (§ 66): kumulative Hochrechnung (360-Tage-Jahr) minus bereits einbehaltene LSt — Vormonats- diff --git a/addons/l10n_at_hr_payroll/models/lohnzettel_l16.py b/addons/l10n_at_hr_payroll/models/lohnzettel_l16.py index c074d61..85c1526 100644 --- a/addons/l10n_at_hr_payroll/models/lohnzettel_l16.py +++ b/addons/l10n_at_hr_payroll/models/lohnzettel_l16.py @@ -390,7 +390,7 @@ class L10nAtPayrollL16Line(models.Model): GP3-Kerns (Monats-Caps 400/600 und ÜStd 15/170 eingerechnet); 220 = sonstige § 67 Abs 1/2/5 ohne Abs-3/6; 225 = SV-DN auf KZ 220; 230 = laufende SV-DN + AK + WF; VIEB = 230 + 225 + - 226; 260/BIEL = LST_LFD + LST_SZ.""" + 226; 260/BIEL = LST_LFD + LST_SZ + LST_ABF.""" self.ensure_one() salden = dict.fromkeys( ('kz210', 'kz215', 'kz220', 'kz225', 'kz226', 'kz230', @@ -411,7 +411,7 @@ class L10nAtPayrollL16Line(models.Model): elif line.code in ('SVDN_KV', 'SVDN_PV', 'SVDN_AV', 'SVDN_AK', 'SVDN_WF'): salden['kz230'] += line.total - elif line.code in ('LST_LFD', 'LST_SZ'): + elif line.code in ('LST_LFD', 'LST_SZ', 'LST_ABF'): salden['lohnsteuer'] += line.total freibe = slip._l10n_at_freibetraege_68() salden['kz215'] += freibe['szfn'] + freibe['ue_std'] diff --git a/addons/l10n_at_hr_payroll/tests/test_elda_l16.py b/addons/l10n_at_hr_payroll/tests/test_elda_l16.py index 6143beb..48d35d7 100644 --- a/addons/l10n_at_hr_payroll/tests/test_elda_l16.py +++ b/addons/l10n_at_hr_payroll/tests/test_elda_l16.py @@ -5,6 +5,7 @@ Satz-Assertions, CRLF/ISO-8859-15-Encoding, SANR/SANZ-Kette, KZ-245-Formel-Konsistenz aus GP3-Daten.""" import base64 import datetime +import types from odoo.tests import TransactionCase, tagged @@ -213,6 +214,37 @@ class L16GeneratorTest(TransactionCase): self.assertEqual(schluss[0:2], '99') self.assertEqual(schluss[20:26], '000004') # SANZ + def test_01b_abfertigung_lohnsteuer_salden(self): + """Abs.-3-Bezug bleibt außerhalb KZ 210/220; LST_ABF zählt zu 260.""" + deklaration = self.env['l10n.at.payroll.l16'].create({ + 'company_id': self.company.id, + 'jahr': 2026, + 'art': '01', + }) + l16_line = self.env['l10n.at.payroll.l16.line'].create({ + 'l16_id': deklaration.id, + 'employee_id': self.employee.id, + }) + abs3_rule = types.SimpleNamespace( + l10n_at_bemessung='sonstig', l10n_at_s67_abs='abs3') + lst_rule = types.SimpleNamespace( + l10n_at_bemessung='neutral', l10n_at_s67_abs='keine') + slip = types.SimpleNamespace( + line_ids=[ + types.SimpleNamespace( + salary_rule_id=abs3_rule, code='ABF_ALT', total=12000.0), + types.SimpleNamespace( + salary_rule_id=lst_rule, code='LST_ABF', total=720.0), + ], + _l10n_at_freibetraege_68=lambda: {'szfn': 0.0, 'ue_std': 0.0}, + ) + + salden = l16_line._kz_salden([slip]) + + self.assertEqual(salden['kz210'], 0.0) + self.assertEqual(salden['kz220'], 0.0) + self.assertEqual(salden['lohnsteuer'], 720.0) + def test_02_arbeitsstaette(self): """§ 34 Abs 6 ASVG: je AN ein Satz 45 (Länge 300) mit BKNR, VSNR, STNR und GKZ aus dem Dienstort-Partner; Warnung, wenn diff --git a/addons/l10n_at_hr_payroll/tests/test_lohnsteuer.py b/addons/l10n_at_hr_payroll/tests/test_lohnsteuer.py index d5a49bd..e7c4013 100644 --- a/addons/l10n_at_hr_payroll/tests/test_lohnsteuer.py +++ b/addons/l10n_at_hr_payroll/tests/test_lohnsteuer.py @@ -108,6 +108,7 @@ class LohnsteuerTest(TransactionCase): slip._l10n_at_absetzbetraege(20000.0), 496.0 + 828.0 + 273.0, places=2) + # ------------------------------------------------------------------ # Lohnsteuertage (user decision: 30 je vollem Monat) # ------------------------------------------------------------------ diff --git a/addons/l10n_at_hr_payroll_private/__manifest__.py b/addons/l10n_at_hr_payroll_private/__manifest__.py index 0d91e92..1203acb 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.12.0.0', + 'version': '19.0.13.0.0', 'author': 'Florian Egger', 'website': 'https://gem360.at', 'category': 'Human Resources/Payroll', @@ -117,7 +117,6 @@ gem360-Brücke). 'data/salary_rules_endabrechnung_private.xml', 'data/salary_rules_sv_private.xml', 'data/salary_rules_bvg_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', 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 7b1c7c9..281f194 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 @@ -31,4 +31,8 @@ Aufrechnung offener Vorschuss (Netto) ATP_ABRECHNUNG_VORSCHUSS + + Gesetzliche Abfertigung Alt (Brutto) + ATP_ABF_ALT + \ No newline at end of file diff --git a/addons/l10n_at_hr_payroll_private/data/rule_parameters_endabrechnung_private.xml b/addons/l10n_at_hr_payroll_private/data/rule_parameters_endabrechnung_private.xml index 5088a8f..ea1b502 100644 --- a/addons/l10n_at_hr_payroll_private/data/rule_parameters_endabrechnung_private.xml +++ b/addons/l10n_at_hr_payroll_private/data/rule_parameters_endabrechnung_private.xml @@ -12,6 +12,6 @@ model="hr.rule.parameter.value"> 2026-01-01 - {'divisor_werktage': 26.0, 'divisor_arbeitstage': 22.0} + {'divisor_werktage': 26.0, 'divisor_arbeitstage': 22.0, 'abfertigung_alt_staffel': [[36, 2.0], [60, 3.0], [120, 4.0], [180, 6.0], [240, 9.0], [300, 12.0]], 'abfertigung_steuersatz': 6.0} \ 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 2bc5040..d4d160f 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 @@ -72,6 +72,25 @@ result = inputs['ATP_ABGANG'].amount ]]> + + + Gesetzliche Abfertigung Alt (§ 67 Abs 3) + ABF_ALT + + + + python + result = 'ATP_ABF_ALT' in inputs and inputs['ATP_ABF_ALT'].amount + neutral + keine + abs3 + + code + + + Zeitguthaben Normalarbeitszeit + 50 % (§ 19e Abs 2 AZG) diff --git a/addons/l10n_at_hr_payroll_private/data/salary_rules_lst_private.xml b/addons/l10n_at_hr_payroll_private/data/salary_rules_lst_private.xml index ff7bd62..b66d1cb 100644 --- a/addons/l10n_at_hr_payroll_private/data/salary_rules_lst_private.xml +++ b/addons/l10n_at_hr_payroll_private/data/salary_rules_lst_private.xml @@ -104,4 +104,19 @@ result = payslip._l10n_at_lst_laufend(result_rules) result = payslip._l10n_at_lst_sonstige(result_rules) ]]> + + Lohnsteuer Abfertigung Alt (§ 67 Abs 3) + LST_ABF + + + + python + result = payslip._at_abf_alt_lohnsteuer(result_rules) > 0 + code + neutral + keine + + \ No newline at end of file diff --git a/addons/l10n_at_hr_payroll_private/data/salary_rules_pfaendung_private.xml b/addons/l10n_at_hr_payroll_private/data/salary_rules_pfaendung_private.xml index b55ecf3..0186d4a 100644 --- a/addons/l10n_at_hr_payroll_private/data/salary_rules_pfaendung_private.xml +++ b/addons/l10n_at_hr_payroll_private/data/salary_rules_pfaendung_private.xml @@ -56,4 +56,23 @@ result = (payslip._at_pfaendung_masse(result_rules, 'sz13') + payslip._at_pfaendung_masse(result_rules, 'sz14')) ]]> + + + Lohnpfändung — Beendigungsansprüche (§ 291d EO) + PFAENDUNG_END + + + + python + 0 + ]]> + code + neutral + keine + + 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 1a61b03..0d8a9b6 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 +# Endabrechnungs-Zyklus — Rechenfälle AP14-A / AP15-A Stand: 2026-09. AP14-A bildet die Beendigungs-Abrechnung auf der privaten Structure ab: Urlaubsersatzleistung (UEL), Abgangsentschädigung, @@ -80,12 +80,54 @@ Netto; LSt-, SV- und LNK-Basen unverändert. 10 h Normalarbeitszeit-Guthaben × 1,5 + 5 h Zeitschulden × (−1): beides laufender Bezug, kein §-68-Freibetrag. -## Offene Punkte (dokumentiert, AP14-B/AP15) +## RF-ABF-1 — Abfertigung Alt, Vervielfachung günstiger + +21 Dienstjahre ergeben nach § 23 Abs 1 AngG 9 Monatsentgelte. Bei +1.800 € laufendem Monatsarbeitslohn und 2.100 € arbeitsrechtlicher +Bemessungsgrundlage einschließlich 2/12 Sonderzahlungen: + +- Bruttoabfertigung: 2.100 × 9 = **18.900 €**; +- steuerlicher Vervielfacher: 18.900 / 1.800 = **10,5** — nicht 9; +- auf den maßgeblichen laufenden Bezug entfallende Tarif-LSt laut + Ausgangsfall: **101,96 €**; +- Vervielfachungssteuer: 101,96 × 10,5 = **1.070,58 €**; +- Vergleich mit 18.900 × 6 % = **1.134 €**; +- zwingend wird der niedrigere Steuerbetrag angesetzt. + +`ABF_ALT` ist SV-, BV-, DB-, DZ- und KommSt-frei, verbraucht kein +Jahressechstel und läuft als Beendigungsanspruch in den §-291d-EO-Topf. + +## RF-ABF-2 — 6 % günstiger + +Bei hohem laufendem Monatsarbeitslohn kann die vervielfachte Tarif-LSt +den Fixsatz überschreiten. Für 9.333,34 € Abfertigung wird dann zwingend +**560,00 €** Lohnsteuer (6 %) angesetzt. + +## RF-ABF-3 — Staffel und Sonderfälle + +- 3/5/10/15/20/25 Jahre → 2/3/4/6/9/12 Monatsentgelte; +- Elternaustritt nach § 23a Abs 3/4: erst nach mindestens fünf Jahren, + dann Hälfte, maximal 3 Monatsentgelte; +- Tod nach § 23 Abs 6: Hälfte des normalen Anspruchs; +- normale Arbeitnehmerkündigung, unberechtigter Austritt und + verschuldete Entlassung begründen grundsätzlich keinen Anspruch; +- Abfertigung-Neu-Verträge sind gesperrt, außer ein eingefrorener + Altanspruch wird ausdrücklich nachgewiesen. + +Der Wizard schlägt Dienstzeit, Faktor und eine Basis aus laufendem Entgelt +plus aliquoten Sonderzahlungen vor. Die auf den maßgeblichen laufenden +Monatsarbeitslohn entfallende Tarif-LSt wird getrennt bestätigt, weil sie +von der §-62-Bemessung und den persönlichen Absetzbeträgen abhängt und nicht +mit dem Bruttolohn gleichgesetzt werden darf. Wegen regelmäßiger Zulagen, +Durchschnittsbezügen, Lehrzeiten, Teilübertritten und §-23a-Sonderfällen +müssen Anspruch, Basis und Nachweis vor Anlage des Laufs bestätigt werden. +Die geprüften Angaben werden am Payslip gespeichert. + +## Offene Punkte (AP15-B/AP15-C) - **Vergleich (§ 67 Abs 8 lit a)** und **freiwillige Abfertigung/ - Abfindung (§ 67 Abs 6 6 %-Bänder)** → AP15 mit der Abfertigung-Alt- - Günstiger-Maschine (dort auch rollierende 12-Monats-Basis und - Dienstzeit-Nachweise). + Abfindung (§ 67 Abs 6 6 %-Bänder)** → AP15-B/C mit rollierender + 12-Monats-Basis, Dienstzeit-Nachweisen und Alt-/Neu-Aufteilung. - Prämien-Beendigung (§ 16 AngG-Aliquotierung, Rollung) — PRAM/SZ-Regeln vorhanden; Feinschliff AP14-B. - Meldewesen-Schnittstellen: „Ende des Entgeltanspruches" in mBGM/ 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 09d8d83..94ff38e 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 @@ -68,6 +68,11 @@ L10N_AT_BUCHUNGSMATRIX = { 'rule_svdn_sz': {'credit': ('3600',)}, 'rule_lst_lfd': {'credit': ('3540',)}, 'rule_lst_sz': {'credit': ('3540',)}, + 'rule_lst_abf': {'credit': ('3540',)}, + # Gesetzliche Abfertigung Alt: bis zur Referenzbuchhaltungs- + # Fixierung allgemeiner Gehalts-/Lohnaufwand, company-dependent + # Regelkonto bleibt übersteuerbar. + 'rule_abf_alt': {'debit': ('6200',), 'debit_arbeiter': ('6000',)}, # 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 38c9281..28926e1 100644 --- a/addons/l10n_at_hr_payroll_private/models/endabrechnung.py +++ b/addons/l10n_at_hr_payroll_private/models/endabrechnung.py @@ -56,6 +56,34 @@ class HrSalaryRule(models.Model): class HrPayslip(models.Model): _inherit = 'hr.payslip' + l10n_at_abf_alt_anspruchsart = fields.Selection( + [('normal', 'Normalanspruch (§ 23 AngG)'), + ('eltern', 'Elternaustritt (§ 23a Abs 3/4 AngG)'), + ('tod', 'Tod (§ 23 Abs 6 AngG)'), + ('pension', 'Pension/Invalidität (§ 23a Abs 1 AngG)'), + ('eingefroren', 'Eingefrorener Altanspruch (§ 47 BMSVG)'), + ('manuell', 'Sonstiger nachgewiesener Anspruch')], + string='AT Abfertigung Alt — Anspruchsart', copy=False) + l10n_at_abf_alt_dienstzeit_monate = fields.Integer( + string='AT Abfertigung Alt — Dienstzeitmonate', copy=False) + l10n_at_abf_alt_monatsentgelt = fields.Float( + string='AT Abfertigung Alt — Monatsentgelt', copy=False, + help='Bestätigte arbeitsrechtliche Bemessungsgrundlage inklusive ' + 'regelmäßiger Bezugsbestandteile und aliquoter Sonderzahlungen.') + l10n_at_abf_alt_laufender_arbeitslohn = fields.Float( + string='AT Abfertigung Alt — laufender Arbeitslohn', copy=False, + help='Laufender Monatsarbeitslohn für den steuerlichen ' + 'Vervielfacher nach § 67 Abs 3 EStG.') + l10n_at_abf_alt_monatslohnsteuer = fields.Float( + string='AT Abfertigung Alt — tarifmäßige Monatslohnsteuer', + copy=False, + help='Auf den maßgeblichen laufenden Monatsarbeitslohn entfallende ' + 'tarifmäßige Lohnsteuer nach den persönlichen Verhältnissen.') + l10n_at_abf_alt_mehrfach = fields.Float( + string='AT Abfertigung Alt — Anspruchsfaktor', copy=False) + l10n_at_abf_alt_nachweis = fields.Text( + string='AT Abfertigung Alt — Nachweis/Begründung', copy=False) + # ------------------------------------------------------------------ # SV-Tage-Umrechnung und Verlängerungsmonate (§ 11 Abs 2 ASVG) # ------------------------------------------------------------------ @@ -135,6 +163,75 @@ class HrPayslip(models.Model): lambda i: i.input_type_id.code == code) return linie[:1].amount or 0.0 + # ------------------------------------------------------------------ + # Abfertigung Alt — §§ 23/23a AngG, § 67 Abs 3 EStG + # ------------------------------------------------------------------ + def _at_abf_alt_staffel(self, dienstzeit_monate=None): + """Arbeitsrechtlicher Grundfaktor nach § 23 Abs 1 AngG.""" + self.ensure_one() + monate = dienstzeit_monate + if monate is None: + monate = self.l10n_at_abf_alt_dienstzeit_monate + staffel = self._l10n_at_param('at_endabrechnung').get( + 'abfertigung_alt_staffel', []) + mehrfach = 0.0 + for grenze, faktor in staffel: + if monate >= int(grenze): + mehrfach = float(faktor) + return mehrfach + + def _at_abf_alt_mehrfach_vorschlag(self, dienstzeit_monate=None, + anspruchsart=None): + """§-23-Faktor mit Halb-/Deckelregeln für Elternaustritt/Tod.""" + self.ensure_one() + art = anspruchsart or self.l10n_at_abf_alt_anspruchsart + monate = dienstzeit_monate + if monate is None: + monate = self.l10n_at_abf_alt_dienstzeit_monate + mehrfach = self._at_abf_alt_staffel(monate) + if art == 'eltern': + if monate < 60: + return 0.0 + return min(mehrfach / 2.0, 3.0) if mehrfach else 0.0 + if art == 'tod': + return mehrfach / 2.0 + return mehrfach + + def _at_abf_alt_betrag(self, result_rules=None): + """Gesetzliche Alt-Abfertigung als eigene Bezugszeile.""" + self.ensure_one() + return self._at_input_amount('ATP_ABF_ALT') + + def _at_abf_alt_steuer_6(self, result_rules=None): + self.ensure_one() + satz = float(self._l10n_at_param( + 'at_endabrechnung')['abfertigung_steuersatz']) + return max(self._at_abf_alt_betrag(result_rules), 0.0) * satz / 100.0 + + def _at_abf_alt_steuer_vervielfachung(self, result_rules=None): + """§ 67 Abs 3: Monats-Tarif-LSt × steuerlicher Vervielfacher. + + Der steuerliche Vervielfacher ist Abfertigungsbetrag dividiert + durch den laufenden Arbeitslohn. Er kann vom arbeitsrechtlichen + Staffel-Faktor abweichen, weil die Abfertigungsbasis etwa aliquote + Sonderzahlungen enthält (lb-end-01, RF 21 Dienstjahre: 10,5 statt 9). + """ + self.ensure_one() + betrag = self._at_abf_alt_betrag(result_rules) + laufend = self.l10n_at_abf_alt_laufender_arbeitslohn + if betrag <= 0 or laufend <= 0: + return self._at_abf_alt_steuer_6(result_rules) + vervielfacher = betrag / laufend + return self.l10n_at_abf_alt_monatslohnsteuer * vervielfacher + + def _at_abf_alt_lohnsteuer(self, result_rules=None): + """Zwingend günstigere Steuer aus Vervielfachung und 6 %.""" + self.ensure_one() + if self._at_abf_alt_betrag(result_rules) <= 0: + return 0.0 + return min(self._at_abf_alt_steuer_vervielfachung(result_rules), + self._at_abf_alt_steuer_6(result_rules)) + # ------------------------------------------------------------------ # Urlaubsersatzleistung — § 67 Abs 8 lit d, lb-url-15/16 # ------------------------------------------------------------------ @@ -238,7 +335,8 @@ class HrPayslip(models.Model): + self._l10n_at_brutto('sonstig', result_rules) - self._l10n_at_st_frei_freie_laufen(result_rules)) for rule in self.struct_id.rule_ids: - if rule.l10n_at_s67_abs in ('abs3', 'abs6'): + if (rule.l10n_at_bemessung in ('laufend', 'sonstig') + and rule.l10n_at_s67_abs in ('abs3', 'abs6')): if result_rules is not None and rule.code in result_rules: basis -= result_rules[rule.code]['total'] elif result_rules is None: diff --git a/addons/l10n_at_hr_payroll_private/models/payslip_private.py b/addons/l10n_at_hr_payroll_private/models/payslip_private.py index 67cc52e..8b11808 100644 --- a/addons/l10n_at_hr_payroll_private/models/payslip_private.py +++ b/addons/l10n_at_hr_payroll_private/models/payslip_private.py @@ -351,7 +351,8 @@ class HrPayslip(models.Model): + self._l10n_at_brutto('sonstig', result_rules) - self._l10n_at_st_frei_freie_laufen(result_rules)) for rule in self.struct_id.rule_ids: - if rule.l10n_at_s67_abs in ('abs3', 'abs6'): + if (rule.l10n_at_bemessung in ('laufend', 'sonstig') + and rule.l10n_at_s67_abs in ('abs3', 'abs6')): if result_rules is not None and rule.code in result_rules: basis -= result_rules[rule.code]['total'] elif result_rules is None: diff --git a/addons/l10n_at_hr_payroll_private/models/pfaendung.py b/addons/l10n_at_hr_payroll_private/models/pfaendung.py index a9a38e5..e3cf90b 100644 --- a/addons/l10n_at_hr_payroll_private/models/pfaendung.py +++ b/addons/l10n_at_hr_payroll_private/models/pfaendung.py @@ -322,7 +322,7 @@ class HrPayslip(models.Model): # ------------------------------------------------------------------ def _at_pfa_rule_amount(self, result_rules, code): if result_rules is not None: - werte = result_rules.get(code) + werte = result_rules[code] if code in result_rules else None # result_rules enthält ungerundete Rule-Summen; die # Bezugsart ist der gerundete Zeilenbetrag (Cents) — # mit diesem wird auch die Pfändungsberechnungsgrundlage @@ -378,14 +378,16 @@ class HrPayslip(models.Model): if not rule.l10n_at_pfandung_beendigung: continue if result_rules is not None: - werte = result_rules.get(rule.code) + werte = result_rules[rule.code] \ + if rule.code in result_rules else None if werte: total += float(werte['total']) else: linie = self.line_ids.filtered( lambda l, c=rule.code: l.code == c) total += sum(linie.mapped('total')) - return round(max(0.0, total), 2) + steuer = self._at_pfa_rule_amount(result_rules, 'LST_ABF') + return round(max(0.0, total - steuer), 2) raise UserError('Unbekannter Pfändungstopf: %s' % topf) # ------------------------------------------------------------------ @@ -420,6 +422,7 @@ class HrPayslip(models.Model): total = self._at_pfaendung_masse(result_rules, 'laufend') total += self._at_pfaendung_masse(result_rules, 'sz13') total += self._at_pfaendung_masse(result_rules, 'sz14') + total += self._at_pfaendung_masse(result_rules, 'beendigung') return round(total, 2) def _at_pfaendung_kostenersatz(self, result_rules): 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 7a4f158..e946317 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 @@ -5,6 +5,7 @@ Abgangsentschädigung (§ 67 Abs 8 lit b Fünftel), Aufrechnung offener Vorschüsse (§ 293 Abs 3 EO) und Zeitguthaben (§ 19e Abs 2 AZG).""" from datetime import date, timedelta +from odoo.exceptions import UserError from odoo.tests import TransactionCase, tagged @@ -278,7 +279,8 @@ class EndabrechnungPrivateTest(TransactionCase): wizard = self.env['l10n.at.payroll.endabrechnung.wizard'].create({ 'employee_id': emp.id, 'austrittsdatum': date(2026, 3, 15), - 'beendigungsart': 'einvernehmlich', + 'departure_reason_id': self.env.ref( + 'l10n_at_hr_payroll.departure_reason_einvernehmlich').id, 'uel_tage': 11.0, 'abgang_betrag': 9000.0, 'abgang_ffrist_tage': 42.0, @@ -295,4 +297,185 @@ class EndabrechnungPrivateTest(TransactionCase): self.assertAlmostEqual( self._total(slip, 'URL_ERSATZ_LFD'), 1320.0, places=2) self.assertAlmostEqual(self._total(slip, 'ABGANG'), 9000.0, - places=2) \ No newline at end of file + places=2) + + # ------------------------------------------------------------------ + # AP15-A: gesetzliche Abfertigung Alt (§§ 23/23a, § 67 Abs 3) + # ------------------------------------------------------------------ + def test_07_abfertigung_alt_staffel_und_sonderfaelle(self): + slip = self._slip(self._employee()) + for monate, erwartet in ( + (35, 0.0), (36, 2.0), (60, 3.0), (120, 4.0), + (180, 6.0), (240, 9.0), (300, 12.0)): + self.assertEqual(slip._at_abf_alt_staffel(monate), erwartet) + self.assertEqual( + slip._at_abf_alt_mehrfach_vorschlag(59, 'eltern'), 0.0) + self.assertEqual( + slip._at_abf_alt_mehrfach_vorschlag(60, 'eltern'), 1.5) + self.assertEqual( + slip._at_abf_alt_mehrfach_vorschlag(180, 'eltern'), 3.0) + self.assertEqual( + slip._at_abf_alt_mehrfach_vorschlag(240, 'tod'), 4.5) + + def test_08_abfertigung_alt_vervielfachung_guenstiger(self): + """21 Jahre: 9 Monatsentgelte inklusive 2/12 SZ ergeben den + steuerlichen Vervielfacher 10,5; der Tarif ist günstiger als 6 %.""" + emp_mit = self._employee(ueberzahlung=1800.0) + emp_ohne = self._employee(ueberzahlung=1800.0) + slip_mit = self._slip(emp_mit) + slip_ohne = self._slip(emp_ohne) + slip_mit.write({ + 'l10n_at_abf_alt_anspruchsart': 'normal', + 'l10n_at_abf_alt_dienstzeit_monate': 252, + 'l10n_at_abf_alt_monatsentgelt': 2100.0, + 'l10n_at_abf_alt_laufender_arbeitslohn': 1800.0, + 'l10n_at_abf_alt_monatslohnsteuer': 101.96, + 'l10n_at_abf_alt_mehrfach': 9.0, + 'l10n_at_abf_alt_nachweis': 'Testnachweis', + }) + self._input(slip_mit, 'ATP_ABF_ALT', 18900.0) + + erwartet = 101.96 * 10.5 + self.assertAlmostEqual(self._total(slip_mit, 'ABF_ALT'), 18900.0, + places=2) + self.assertAlmostEqual( + slip_mit._at_abf_alt_steuer_vervielfachung(), erwartet, places=2) + self.assertLess(erwartet, 18900.0 * 0.06) + self.assertAlmostEqual( + self._total(slip_mit, 'LST_ABF'), + self.env.company.currency_id.round(erwartet), places=2) + self.assertAlmostEqual( + slip_mit._l10n_at_sv_basis_roh() + - slip_ohne._l10n_at_sv_basis_roh(), 0.0, places=2) + self.assertAlmostEqual( + slip_mit._at_lnk_basis('flaf') + - slip_ohne._at_lnk_basis('flaf'), 0.0, places=2) + self.assertAlmostEqual( + slip_mit._at_bv_basis_sonst() + - slip_ohne._at_bv_basis_sonst(), 0.0, places=2) + self.assertAlmostEqual( + slip_mit._l10n_at_sechstel() - slip_ohne._l10n_at_sechstel(), + 0.0, places=2) + self.assertAlmostEqual( + slip_mit._at_pfaendung_basis(None, 'beendigung'), + 18900.0 - self._total(slip_mit, 'LST_ABF'), places=2) + + def test_09_abfertigung_alt_sechs_prozent_guenstiger(self): + emp = self._employee(ueberzahlung=4000.0) + slip = self._slip(emp) + slip.write({ + 'l10n_at_abf_alt_laufender_arbeitslohn': 4000.0, + 'l10n_at_abf_alt_monatslohnsteuer': 300.0, + 'l10n_at_abf_alt_mehrfach': 2.0, + }) + self._input(slip, 'ATP_ABF_ALT', 9333.34) + + self.assertGreater( + slip._at_abf_alt_steuer_vervielfachung(), 9333.34 * 0.06) + self.assertAlmostEqual( + self._total(slip, 'LST_ABF'), 9333.34 * 0.06, places=2) + + def test_10_abfertigung_alt_wizard(self): + emp = self._employee(ueberzahlung=3000.0) + version = emp.version_id + version.write({ + 'structure_type_id': self.env.ref( + 'l10n_at_hr_payroll_private.structure_type_at_privat').id, + 'l10n_at_abfertigung_neu': False, + }) + reason = self.env.ref( + 'l10n_at_hr_payroll.departure_reason_einvernehmlich') + wizard = self.env['l10n.at.payroll.endabrechnung.wizard'].create({ + 'employee_id': emp.id, + 'austrittsdatum': date(2026, 3, 15), + 'departure_reason_id': reason.id, + 'abf_alt_aktiv': True, + 'abf_alt_anspruch_bestaetigt': True, + 'abf_alt_anspruchsart': 'normal', + 'abf_alt_dienstzeit_monate': 74, + 'abf_alt_monatsentgelt': 3500.0, + 'abf_alt_laufender_arbeitslohn': 3000.0, + 'abf_alt_monatslohnsteuer': 200.0, + 'abf_alt_mehrfach': 3.0, + 'abf_alt_nachweis': 'Dienstzeiten und Altstatus geprüft', + }) + ergebnis = wizard.action_payslip() + slip = self.env['hr.payslip'].browse(ergebnis['res_id']) + + self.assertAlmostEqual(self._total(slip, 'ABF_ALT'), 10500.0, + places=2) + self.assertGreater(self._total(slip, 'LST_ABF'), 0.0) + self.assertEqual(slip.l10n_at_abf_alt_dienstzeit_monate, 74) + self.assertEqual(slip.l10n_at_abf_alt_nachweis, + 'Dienstzeiten und Altstatus geprüft') + self.assertEqual(version.departure_reason_id, reason) + + def test_11_abfertigung_alt_neu_sperre(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': True, + }) + wizard = self.env['l10n.at.payroll.endabrechnung.wizard'].create({ + 'employee_id': emp.id, + 'austrittsdatum': date(2026, 3, 15), + 'departure_reason_id': self.env.ref( + 'l10n_at_hr_payroll.departure_reason_einvernehmlich').id, + 'abf_alt_aktiv': True, + 'abf_alt_anspruch_bestaetigt': True, + 'abf_alt_anspruchsart': 'normal', + 'abf_alt_dienstzeit_monate': 74, + 'abf_alt_monatsentgelt': 3000.0, + 'abf_alt_laufender_arbeitslohn': 2640.0, + 'abf_alt_monatslohnsteuer': 150.0, + 'abf_alt_mehrfach': 3.0, + }) + with self.assertRaises(UserError): + wizard.action_payslip() + + def test_12_abfertigung_alt_arbeitnehmerkuendigung_ohne_sonderfall(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, + }) + wizard = self.env['l10n.at.payroll.endabrechnung.wizard'].create({ + 'employee_id': emp.id, + 'austrittsdatum': date(2026, 3, 15), + 'departure_reason_id': self.env.ref('hr.departure_resigned').id, + 'abf_alt_aktiv': True, + 'abf_alt_anspruch_bestaetigt': True, + 'abf_alt_anspruchsart': 'normal', + 'abf_alt_dienstzeit_monate': 74, + 'abf_alt_monatsentgelt': 3000.0, + 'abf_alt_laufender_arbeitslohn': 2640.0, + 'abf_alt_monatslohnsteuer': 150.0, + 'abf_alt_mehrfach': 3.0, + }) + with self.assertRaises(UserError): + wizard.action_payslip() + + def test_13_abfertigung_alt_elternaustritt_unter_fuenf_jahren(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, + }) + wizard = self.env['l10n.at.payroll.endabrechnung.wizard'].create({ + 'employee_id': emp.id, + 'austrittsdatum': date(2026, 3, 15), + 'departure_reason_id': self.env.ref('hr.departure_resigned').id, + 'abf_alt_aktiv': True, + 'abf_alt_anspruch_bestaetigt': True, + 'abf_alt_anspruchsart': 'eltern', + 'abf_alt_dienstzeit_monate': 59, + 'abf_alt_monatsentgelt': 3000.0, + 'abf_alt_laufender_arbeitslohn': 2640.0, + 'abf_alt_monatslohnsteuer': 150.0, + 'abf_alt_mehrfach': 1.0, + }) + with self.assertRaises(UserError): + wizard.action_payslip() diff --git a/addons/l10n_at_hr_payroll_private/tests/test_pfaendung_private.py b/addons/l10n_at_hr_payroll_private/tests/test_pfaendung_private.py index 40df519..64658cf 100644 --- a/addons/l10n_at_hr_payroll_private/tests/test_pfaendung_private.py +++ b/addons/l10n_at_hr_payroll_private/tests/test_pfaendung_private.py @@ -185,6 +185,30 @@ class PfaendungPrivateTest(TransactionCase): self.assertAlmostEqual(self._total(slip, 'PFAENDUNG'), pfaendbar, places=2) + def test_09b_pfaendung_regel_beendigung(self): + """AP15-A: Netto-Abfertigung läuft über den eigenen §-291d-Topf.""" + employee = self._employee(datetime.date(2020, 1, 1), unterhalt=0) + self._pfaendung(employee, datetime.date(2026, 2, 1)) + slip = self._slip(employee, 3) + slip.write({ + 'l10n_at_abf_alt_laufender_arbeitslohn': 3000.0, + 'l10n_at_abf_alt_monatslohnsteuer': 200.0, + 'l10n_at_abf_alt_mehrfach': 3.0, + }) + input_type = self.env.ref( + 'l10n_at_hr_payroll_private.input_type_atp_abf_alt') + self.env['hr.payslip.input'].create({ + 'payslip_id': slip.id, + 'input_type_id': input_type.id, + 'amount': 10500.0, + }) + slip.compute_sheet() + + masse = slip._at_pfaendung_masse(None, 'beendigung') + self.assertGreater(masse, 0.0) + self.assertAlmostEqual( + self._total(slip, 'PFAENDUNG_END'), masse, places=2) + def test_10_rang(self): """Zwei Pfändungen → Rang 1 (früheres Einlangen) und Rang 2.""" employee = self._employee(datetime.date(2026, 1, 1)) 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 5cfd5e8..6d3a611 100644 --- a/addons/l10n_at_hr_payroll_private/views/endabrechnung_views.xml +++ b/addons/l10n_at_hr_payroll_private/views/endabrechnung_views.xml @@ -12,7 +12,7 @@ - + @@ -24,6 +24,26 @@ + + + + + + + + + + + + + + + + + + + + @@ -44,6 +64,32 @@ + + hr.payslip.form.abfertigung.alt + 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 3af43da..bcf8d85 100644 --- a/addons/l10n_at_hr_payroll_private/wizard/endabrechnung.py +++ b/addons/l10n_at_hr_payroll_private/wizard/endabrechnung.py @@ -7,7 +7,10 @@ Sammel-/Anlageprozess; die BV-Anwartschaft läuft über den eigenen BV-Wizard (GP5), die Pfändung über die AP11-Maschine.""" from datetime import timedelta +from dateutil.relativedelta import relativedelta + from odoo import _, api, fields, models +from odoo.exceptions import UserError class L10nAtEndabrechnungWizard(models.TransientModel): @@ -18,20 +21,11 @@ class L10nAtEndabrechnungWizard(models.TransientModel): 'hr.employee', string='Dienstnehmer:in', required=True) austrittsdatum = fields.Date( string='Austritt (Ende des Arbeitsverhältnisses)', required=True) - beendigungsart = fields.Selection( - [('ag_kuendigung', 'Kündigung durch den Arbeitgeber'), - ('an_kuendigung', 'Kündigung durch den Dienstnehmer'), - ('einvernehmlich', 'Einvernehmliche Auflösung'), - ('berechtigter_austritt', 'Berechtigter vorzeitiger Austritt'), - ('unberechtigter_austritt', - 'Unberechtigter vorzeitiger Austritt ' - '(keine UEL für die 5./6. Woche, § 10 Abs 2 UrlG; ' - 'kein Zeitguthaben-Zuschlag, § 19e Abs 2 AZG)'), - ('verschuldete_entlassung', 'Verschuldete Entlassung'), - ('unverschuldete_entlassung', 'Unverschuldete Entlassung'), - ('befristung', 'Zeitablauf (befristetes Dienstverhältnis)'), - ('tod', 'Tod (UEL an die Erben, § 10 Abs 5 UrlG)')], - string='Beendigungsart', required=True) + departure_reason_id = fields.Many2one( + 'hr.departure.reason', string='Abgangsgrund', required=True, + domain="['|', ('country_id', '=', False), ('country_id.code', '=', 'AT')]", + help='Odoo-Abgangsgrund des Beschäftigtendatensatzes; die ' + 'Abfertigungsberechtigung wird zusätzlich ausdrücklich bestätigt.') uel_tage = fields.Float( string='Offene Urlaubstage (Arbeitstage)', help='Resturlaub früherer Urlaubsjahre zur Gänze plus aliquoter ' @@ -68,6 +62,45 @@ class L10nAtEndabrechnungWizard(models.TransientModel): 'Vorschusssaldo (lb-end-06) — Rückverrechnung vom Netto ' 'nach SV und Lohnsteuer, ohne Abgabenberührung ' '(§ 293 Abs 3 EO).') + abf_alt_aktiv = fields.Boolean(string='Gesetzliche Abfertigung Alt') + abf_alt_anspruch_bestaetigt = fields.Boolean( + string='Anspruch und Nachweise geprüft', + help='Bestätigung, dass Alt-Anwartschaft, Beendigungsgrund, ' + 'Dienstzeiten und Bemessungsgrundlage geprüft wurden.') + abf_alt_anspruchsart = fields.Selection( + [('normal', 'Normalanspruch (§ 23 AngG)'), + ('eltern', 'Elternaustritt (§ 23a Abs 3/4 AngG)'), + ('tod', 'Tod (§ 23 Abs 6 AngG)'), + ('pension', 'Pension/Invalidität (§ 23a Abs 1 AngG)'), + ('eingefroren', 'Eingefrorener Altanspruch (§ 47 BMSVG)'), + ('manuell', 'Sonstiger nachgewiesener Anspruch')], + string='Anspruchsart', default='normal') + abf_alt_dienstzeit_monate = fields.Integer( + string='Nachgewiesene Abfertigungsdienstzeit (Monate)') + abf_alt_monatsentgelt = fields.Monetary( + string='Maßgebliches Monatsentgelt', currency_field='currency_id', + help='Bestätigte arbeitsrechtliche Bemessungsgrundlage: letzter ' + 'Monat einschließlich regelmäßiger Bezugsteile, Sachbezüge ' + 'und aliquoter Sonderzahlungen.') + abf_alt_laufender_arbeitslohn = fields.Monetary( + string='Laufender Monatsarbeitslohn (§ 67 Abs 3)', + currency_field='currency_id', + help='Laufender Arbeitslohn für den steuerlichen Vervielfacher; ' + 'aliquote Sonderzahlungen gehören hier nicht dazu.') + abf_alt_monatslohnsteuer = fields.Monetary( + string='Tarif-LSt des laufenden Monats', currency_field='currency_id', + help='Die auf den maßgeblichen laufenden Monatsarbeitslohn ' + 'entfallende tarifmäßige Lohnsteuer nach den persönlichen ' + 'Verhältnissen. Bei steuerfreiem Monatslohn ausdrücklich 0 ' + 'bestätigen.') + abf_alt_mehrfach = fields.Float( + string='Bestätigter Anspruchsfaktor', + help='§-23-Staffel, gegebenenfalls halbiert/gedeckelt oder als ' + 'eingefrorener beziehungsweise nachgewiesener Faktor.') + abf_alt_nachweis = fields.Text( + string='Nachweis/Begründung', + help='Quelle der Dienstzeiten, Anspruchsprüfung, Alt-/Neu-Status ' + 'und allfällige manuelle Abweichung dokumentieren.') currency_id = fields.Many2one( 'res.currency', default=lambda self: self.env.company.currency_id) @@ -80,21 +113,76 @@ class L10nAtEndabrechnungWizard(models.TransientModel): sv_tage_vorschau = fields.Integer( string='SV-Verlängerungstage (§ 11 Abs 2)', compute='_compute_vorschau') + abf_alt_betrag_vorschau = fields.Monetary( + string='Abfertigung Alt brutto', currency_field='currency_id', + compute='_compute_vorschau') + abf_alt_steuer_vervielfachung_vorschau = fields.Monetary( + string='LSt Vervielfachungsmethode', currency_field='currency_id', + compute='_compute_vorschau') + abf_alt_steuer_6_vorschau = fields.Monetary( + string='LSt 6 %', currency_field='currency_id', + compute='_compute_vorschau') + abf_alt_lohnsteuer_vorschau = fields.Monetary( + string='Angewendete günstigere LSt', currency_field='currency_id', + compute='_compute_vorschau') + abf_alt_netto_vorschau = fields.Monetary( + string='Abfertigung Alt netto', currency_field='currency_id', + compute='_compute_vorschau') @api.onchange('employee_id') def _onchange_employee(self): for wizard in self: - if wizard.employee_id: - wizard.austrittsdatum = ( - wizard.employee_id.version_id.contract_date_end - or fields.Date.today()) + if not wizard.employee_id: + continue + version = wizard.employee_id.version_id + wizard.austrittsdatum = ( + version.contract_date_end or fields.Date.today()) + wizard.departure_reason_id = version.departure_reason_id + wizard._at_abf_alt_vorschlag_setzen() - @api.depends('employee_id', 'austrittsdatum', 'uel_tage') + @api.onchange('austrittsdatum', 'abf_alt_anspruchsart', + 'abf_alt_dienstzeit_monate') + def _onchange_abf_alt(self): + for wizard in self: + wizard._at_abf_alt_vorschlag_setzen() + + def _at_abf_alt_vorschlag_setzen(self): + self.ensure_one() + if not self.employee_id or not self.austrittsdatum: + return + version = self.employee_id.version_id + start = version.contract_date_start + if start and not self.abf_alt_dienstzeit_monate: + dauer = relativedelta( + self.austrittsdatum + timedelta(days=1), start) + self.abf_alt_dienstzeit_monate = dauer.years * 12 + dauer.months + slip = self._vorschau_payslip() + if not self.abf_alt_laufender_arbeitslohn: + self.abf_alt_laufender_arbeitslohn = slip._at_kv_gehalt_basis() + if not self.abf_alt_monatsentgelt: + self.abf_alt_monatsentgelt = ( + self.abf_alt_laufender_arbeitslohn + + 2.0 * slip._at_sz_basis(self.austrittsdatum) / 12.0) + vorgeschlagen = slip._at_abf_alt_mehrfach_vorschlag( + self.abf_alt_dienstzeit_monate, self.abf_alt_anspruchsart) + if vorgeschlagen or not self.abf_alt_mehrfach: + self.abf_alt_mehrfach = vorgeschlagen + + @api.depends('employee_id', 'austrittsdatum', 'uel_tage', + 'abf_alt_aktiv', 'abf_alt_monatsentgelt', + 'abf_alt_laufender_arbeitslohn', 'abf_alt_monatslohnsteuer', + 'abf_alt_mehrfach', + 'abf_alt_dienstzeit_monate', 'abf_alt_anspruchsart') def _compute_vorschau(self): for wizard in self: wizard.uel_lfd_vorschau = 0.0 wizard.uel_sz_vorschau = 0.0 wizard.sv_tage_vorschau = 0 + wizard.abf_alt_betrag_vorschau = 0.0 + wizard.abf_alt_steuer_vervielfachung_vorschau = 0.0 + wizard.abf_alt_steuer_6_vorschau = 0.0 + wizard.abf_alt_lohnsteuer_vorschau = 0.0 + wizard.abf_alt_netto_vorschau = 0.0 if not wizard.employee_id or not wizard.austrittsdatum: continue slip = wizard._vorschau_payslip() @@ -102,6 +190,18 @@ class L10nAtEndabrechnungWizard(models.TransientModel): wizard.uel_sz_vorschau = slip._at_uel_sz() wizard.sv_tage_vorschau = slip._at_end_sv_tage( wizard.uel_tage) + if wizard.abf_alt_aktiv: + betrag = slip._at_abf_alt_betrag() + steuer_vervielfachung = \ + slip._at_abf_alt_steuer_vervielfachung() + steuer_6 = slip._at_abf_alt_steuer_6() + steuer = min(steuer_vervielfachung, steuer_6) + wizard.abf_alt_betrag_vorschau = betrag + wizard.abf_alt_steuer_vervielfachung_vorschau = \ + steuer_vervielfachung + wizard.abf_alt_steuer_6_vorschau = steuer_6 + wizard.abf_alt_lohnsteuer_vorschau = steuer + wizard.abf_alt_netto_vorschau = betrag - steuer def _vorschau_payslip(self): """Flüchtiger Payslip-Kontext für die Vorschau-Berechnung (kein @@ -119,8 +219,22 @@ class L10nAtEndabrechnungWizard(models.TransientModel): 'struct_id': struct.id, 'date_from': erster, 'date_to': letzter, + 'l10n_at_abf_alt_anspruchsart': self.abf_alt_anspruchsart, + 'l10n_at_abf_alt_dienstzeit_monate': + self.abf_alt_dienstzeit_monate, + 'l10n_at_abf_alt_monatsentgelt': self.abf_alt_monatsentgelt, + 'l10n_at_abf_alt_laufender_arbeitslohn': + self.abf_alt_laufender_arbeitslohn, + 'l10n_at_abf_alt_monatslohnsteuer': + self.abf_alt_monatslohnsteuer, + 'l10n_at_abf_alt_mehrfach': self.abf_alt_mehrfach, + 'l10n_at_abf_alt_nachweis': self.abf_alt_nachweis, }) - eingaben = [('ATP_UEL_TAGE', self.uel_tage)] + eingaben = [ + ('ATP_UEL_TAGE', self.uel_tage), + ('ATP_ABF_ALT', self.abf_alt_monatsentgelt + * self.abf_alt_mehrfach if self.abf_alt_aktiv else 0.0), + ] slip.input_line_ids = [ (0, 0, { 'input_type_id': self.env['hr.payslip.input.type'] @@ -140,16 +254,65 @@ class L10nAtEndabrechnungWizard(models.TransientModel): ('ATP_ZEIT_GUTHABEN', self.zeit_guthaben_stunden), ('ATP_ZEIT_SCHULDEN', self.zeit_schulden_stunden), ('ATP_ABRECHNUNG_VORSCHUSS', self.vorschuss_saldo), + ('ATP_ABF_ALT', self.abf_alt_monatsentgelt + * self.abf_alt_mehrfach if self.abf_alt_aktiv else 0.0), ] + def _abf_alt_validieren(self): + self.ensure_one() + if not self.abf_alt_aktiv: + return + if not self.abf_alt_anspruch_bestaetigt: + raise UserError(_('Bitte Anspruch und Nachweise der ' + 'Abfertigung Alt bestätigen.')) + if self.employee_id.version_id.l10n_at_abfertigung_neu \ + and self.abf_alt_anspruchsart != 'eingefroren': + raise UserError(_( + 'Das Dienstverhältnis ist als Abfertigung Neu markiert. ' + 'Eine Alt-Abfertigung ist nur als nachgewiesener ' + 'eingefrorener Anspruch zulässig.')) + if self.abf_alt_anspruchsart == 'eltern' \ + and self.abf_alt_dienstzeit_monate < 60: + raise UserError(_( + 'Ein Elternaustritt nach § 23a Abs. 3/4 AngG setzt ein ' + 'ununterbrochenes Dienstverhältnis von fünf Jahren voraus.')) + if (self.abf_alt_dienstzeit_monate <= 0 + or self.abf_alt_monatsentgelt <= 0 + or self.abf_alt_laufender_arbeitslohn <= 0 + or self.abf_alt_monatslohnsteuer < 0 + or self.abf_alt_mehrfach <= 0): + raise UserError(_('Dienstzeit, Monatsentgelt, laufender ' + 'Arbeitslohn und Anspruchsfaktor müssen ' + 'größer als null sein; die Tarif-LSt darf ' + 'nicht negativ sein.')) + agrd = self.departure_reason_id.l10n_at_agrd + if agrd == '02' and self.abf_alt_anspruchsart not in ( + 'eltern', 'pension', 'manuell'): + raise UserError(_( + 'Eine gewöhnliche Arbeitnehmerkündigung begründet keinen ' + 'Anspruch nach § 23 AngG. Bitte einen §-23a-Sonderfall ' + 'wählen oder den Anspruch manuell nachweisen.')) + if agrd == '06' and self.abf_alt_anspruchsart != 'manuell': + raise UserError(_( + 'Der Abgangsgrund „Fristlose Entlassung“ unterscheidet ' + 'nicht zwischen verschuldeter und unverschuldeter ' + 'Entlassung. Bitte den Anspruch manuell bestätigen und ' + 'begründen.')) + def action_payslip(self): """Endabrechnungs-Lauf im Austrittsmonat anlegen und die gesammelten offenen Posten als Eingaben vorbefüllen.""" self.ensure_one() + self._abf_alt_validieren() erster = self.austrittsdatum.replace(day=1) letzter = (erster + timedelta(days=32)).replace(day=1) \ - timedelta(days=1) - struct = self.employee_id.version_id.structure_id \ + version = self.employee_id.version_id + version.write({ + 'contract_date_end': self.austrittsdatum, + 'departure_reason_id': self.departure_reason_id.id, + }) + struct = version.structure_id \ or self.env.ref('l10n_at_hr_payroll_private.structure_at_privat') slip = self.env['hr.payslip'].create({ 'name': _('Endabrechnung %s') % self.employee_id.name, @@ -157,6 +320,21 @@ class L10nAtEndabrechnungWizard(models.TransientModel): 'struct_id': struct.id, 'date_from': erster, 'date_to': letzter, + 'l10n_at_abf_alt_anspruchsart': self.abf_alt_anspruchsart + if self.abf_alt_aktiv else False, + 'l10n_at_abf_alt_dienstzeit_monate': + self.abf_alt_dienstzeit_monate if self.abf_alt_aktiv else 0, + 'l10n_at_abf_alt_monatsentgelt': + self.abf_alt_monatsentgelt if self.abf_alt_aktiv else 0.0, + 'l10n_at_abf_alt_laufender_arbeitslohn': + self.abf_alt_laufender_arbeitslohn + if self.abf_alt_aktiv else 0.0, + 'l10n_at_abf_alt_monatslohnsteuer': + self.abf_alt_monatslohnsteuer if self.abf_alt_aktiv else 0.0, + 'l10n_at_abf_alt_mehrfach': + self.abf_alt_mehrfach if self.abf_alt_aktiv else 0.0, + 'l10n_at_abf_alt_nachweis': + self.abf_alt_nachweis if self.abf_alt_aktiv else False, }) typen = {code: self.env['hr.payslip.input.type'].search( [('code', '=', code)], limit=1)