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)