diff --git a/.agents/MEMORY.md b/.agents/MEMORY.md
index dfa9015..6784ad1 100644
--- a/.agents/MEMORY.md
+++ b/.agents/MEMORY.md
@@ -2,8 +2,53 @@
## Current focus
+**AP15-B freiwillige Abfertigung (§ 67 Abs 6) umgesetzt — Kern 55/55, Privat 146/146 Tests grün**
+(2026-09-15, uncommittet bis Nutzerfreigabe; Privat **19.0.14.0.0**, Kern **19.0.10.0.0**):
+
+- **Recht (RIS-Wortlaut § 67 Abs 6 verifiziert, lb-end-12/13/15):**
+ Z 1 Viertelregelung (¼ der letzten-12-Monats-Bezüge, Deckel 9×HBG)
+ + Z 2 Zwölftelregelung (Staffel 3→2/12 … 25→12/12 = §-23-Grenzen,
+ Deckel n × 3 × HBG), Z 3 Kürzung um erhaltene Abfertigungen (Abs 3
+ ODER 6) und bestehenden gesetzlichen Anspruch am aktuellen Ende,
+ Z 5 Sechstel-Exklusion nur für die 6 %-Beträge, Z 6 Überhang
+ „wie ein laufender Bezug“ zum Zuflussmonat-Tarif, Z 7 (BV-Ausschluss)
+ seit BGBl I 118/2015 AUFGEHOBEN. SV-frei § 49 Abs 3 Z 7, LNK-frei
+ (§ 41 Abs 4 lit b FLAG, § 122 Abs 8 WKG, § 5 Abs 2 lit b KommStG)
+ UNABHÄNGIG von 6 % vs Tarif, BV-frei (§ 6 Abs 5 BMSVG).
+- **Kern (19.0.10.0.0):** neuer Regel-Flag `l10n_at_s67_abs6_ueberhang`
+ (lohnsteuer.py); `_l10n_at_brutto_laufen_ytd` zieht Flag-Regeln aus
+ der Sechstel-Basis ab (Helfer `_l10n_at_abs6_ueberhang_total`, auch
+ je YTD-Lauf) — der Überhang läuft über bemessung='laufend' in die
+ laufende LSt-Bemessung und KZ 210, aber NICHT ins Sechstel;
+ `LST_ABF_FREI` in KZ-260-Salden. Test test_20 + L16-Test erweitert.
+- **Privat:** Regeln `ABF_FREI` (seq 31, neutral/keine/abs6/
+ pfandung_beendigung — begünstigter Teil), `ABF_FREI_TARIF` (seq 32,
+ laufend/keine/abs6 + Überhang-Flag — Tarifrest), `LST_ABF_FREI`
+ (seq 73, 6 %); Eingabe ATP_ABF_FREI; Helfer in endabrechnung.py:
+ `_at_bezuege_12_monate` (validierte Vormonats-Läufe + eigene Zeilen,
+ ohne Überhang-/§-26-Z-4-Regeln — neues privates Regel-Flag
+ `l10n_at_s26_z4_reise` an REISE_STEUERPFLICHTIG), `_at_abf_frei_split`
+ (Z 1/Z 2/Deckel aus at_sv_hbg_monatlich/Z-3-Kürzung),
+ `_at_abf_frei_lst_6`. Wizard-Abschnitt mit auto 12-Monats-Basis +
+ gesetzlicher Anspruch aus ABF_ALT, Vorschau Z 1/Z 2/Kürzung/
+ begünstigt/Überhang/6 %/Netto, Bestätigungspflicht (Anspruchs-
+ umwandlungs-Hinweis lb-end-15). Pfändungs-Beendigungstopf zieht
+ LST_ABF + LST_ABF_FREI ab (test_09c). Konten: rule_abf_frei(_tarif)
+ 6200/6000 vorläufig, rule_lst_abf_frei 3540. RF-ABF-4…6 + offene
+ Punkte in docs/rechenfaelle/endabrechnung.md.
+- **Tests:** endabrechnung 17 (Z 1/Z 2, Überhang ohne Sechstel mit
+ SV/LNK/BV/LST_LFD-Asserts, Kürzung+Deckel, Wizard-E2E), pfändung 13,
+ Kern-Suite 55/55, Privat-Suite 146/146 (ap13a_test); ein Testfehler
+ (zweite `_input`-Eingabezeile statt Update) korrigiert.
+- **Verifikationsreste (dokumentiert, nicht geblockt):** KZ-Positionierung
+ der Abs-6-Bezüge und §-291d-Zuordnung des Überhangs gegen LStR
+ Rz 1087 ff; geminderte Bezüge in der 12-Monats-Basis (LStR vs VwGH)
+ bewusst ohne Automatik.
+- **Offen:** AP15-C (§ 67 Abs 8 lit a Vergleichssummen). Nicht
+ committen ohne Nutzeranforderung.
+
**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**):
+(2026-09-15, Commit **`9add58f`** nach Nutzerfreigabe):
- **Recht am Originalwortlaut verifiziert (.ris/):** § 23a Abs 3 AngG
verlangt für den Elternaustritt ein **ununterbrochen fünfjähriges**
diff --git a/addons/l10n_at_hr_payroll/__manifest__.py b/addons/l10n_at_hr_payroll/__manifest__.py
index 8f818e1..d62305d 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.9.0.0',
+ 'version': '19.0.10.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 55adbe5..9d7ea2e 100644
--- a/addons/l10n_at_hr_payroll/models/lohnsteuer.py
+++ b/addons/l10n_at_hr_payroll/models/lohnsteuer.py
@@ -70,6 +70,15 @@ class HrSalaryRule(models.Model):
'abgezogen; Grenzen als Jahres-Kumulative je Dienstnehmer '
'(at_st_grenzen). Nur der übersteigende Teil ist '
'steuerpflichtig. Menge = Arbeitstage (Z 17/Z 9).')
+ l10n_at_s67_abs6_ueberhang = fields.Boolean(
+ string='AT § 67 Abs 6 Z 6 — Überhang',
+ help='Übersteigender Teil einer freiwilligen Abfertigung/Abfindung '
+ '(§ 67 Abs 6 Z 6 EStG): wird wie ein laufender Bezug zum '
+ 'Tarif des Zuflussmonats versteuert, erhöht aber das '
+ 'Jahressechstel nicht — Abs 2 ist nur auf die mit 6 % '
+ 'versteuerten Z-1/Z-2-Beträge nicht anzuwendbar (Z 5), der '
+ 'Überhang ist ein sonstiger Bezug, kein laufender. LNK-frei '
+ 'über § 67 Abs 6 (lb-end-12), SV-frei über § 49 Abs 3 Z 7.')
class HrVersion(models.Model):
@@ -351,11 +360,32 @@ class HrPayslip(models.Model):
def _l10n_at_brutto_laufen_ytd(self, result_rules=None):
"""Kumulierte laufende Bruttobezüge des Jahres (Sechstel-Basis
- ist Brutto, § 67 Abs 2 — inkl. Auszahlungsmonat-Laufendem)."""
+ ist Brutto, § 67 Abs 2 — inkl. Auszahlungsmonat-Laufendem).
+ Der §-67-Abs-6-Überhang (Flag) läuft tarifisch im Zuflussmonat,
+ ist aber ein sonstiger Bezug und erhöht das Jahressechstel
+ nicht."""
self.ensure_one()
- total = self._l10n_at_brutto('laufend', result_rules)
+ total = self._l10n_at_brutto('laufend', result_rules) \
+ - self._l10n_at_abs6_ueberhang_total(result_rules)
for slip in self._l10n_at_ytd_slaeufe():
- total += slip._l10n_at_brutto('laufend')
+ total += slip._l10n_at_brutto('laufend') \
+ - slip._l10n_at_abs6_ueberhang_total()
+ return total
+
+ def _l10n_at_abs6_ueberhang_total(self, result_rules=None):
+ """Summe der als §-67-Abs-6-Z-6-Überhang gekennzeichneten
+ Regel-Zeilen (tarifisch im Zuflussmonat, ohne Sechstel-Effekt)."""
+ self.ensure_one()
+ total = 0.0
+ for rule in self.struct_id.rule_ids:
+ if not rule.l10n_at_s67_abs6_ueberhang:
+ continue
+ if result_rules is not None:
+ if rule.code in result_rules:
+ total += result_rules[rule.code]['total']
+ else:
+ total += sum(self.line_ids.filtered(
+ lambda l, c=rule.code: l.code == c).mapped('total'))
return total
def _l10n_at_sechstel(self, result_rules=None):
diff --git a/addons/l10n_at_hr_payroll/models/lohnzettel_l16.py b/addons/l10n_at_hr_payroll/models/lohnzettel_l16.py
index 85c1526..aeec63f 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 + LST_ABF."""
+ 226; 260/BIEL = LST_LFD + LST_SZ + LST_ABF + LST_ABF_FREI."""
self.ensure_one()
salden = dict.fromkeys(
('kz210', 'kz215', 'kz220', 'kz225', 'kz226', 'kz230',
@@ -411,7 +411,8 @@ 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', 'LST_ABF'):
+ elif line.code in ('LST_LFD', 'LST_SZ', 'LST_ABF',
+ 'LST_ABF_FREI'):
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 48d35d7..a8813a1 100644
--- a/addons/l10n_at_hr_payroll/tests/test_elda_l16.py
+++ b/addons/l10n_at_hr_payroll/tests/test_elda_l16.py
@@ -235,6 +235,9 @@ class L16GeneratorTest(TransactionCase):
salary_rule_id=abs3_rule, code='ABF_ALT', total=12000.0),
types.SimpleNamespace(
salary_rule_id=lst_rule, code='LST_ABF', total=720.0),
+ types.SimpleNamespace(
+ salary_rule_id=lst_rule, code='LST_ABF_FREI',
+ total=300.0),
],
_l10n_at_freibetraege_68=lambda: {'szfn': 0.0, 'ue_std': 0.0},
)
@@ -243,7 +246,7 @@ class L16GeneratorTest(TransactionCase):
self.assertEqual(salden['kz210'], 0.0)
self.assertEqual(salden['kz220'], 0.0)
- self.assertEqual(salden['lohnsteuer'], 720.0)
+ self.assertEqual(salden['lohnsteuer'], 1020.0)
def test_02_arbeitsstaette(self):
"""§ 34 Abs 6 ASVG: je AN ein Satz 45 (Länge 300) mit BKNR,
diff --git a/addons/l10n_at_hr_payroll/tests/test_lohnsteuer.py b/addons/l10n_at_hr_payroll/tests/test_lohnsteuer.py
index e7c4013..3bb0658 100644
--- a/addons/l10n_at_hr_payroll/tests/test_lohnsteuer.py
+++ b/addons/l10n_at_hr_payroll/tests/test_lohnsteuer.py
@@ -125,6 +125,27 @@ class LohnsteuerTest(TransactionCase):
'contract_date_end': date(2026, 1, 10)})
self.assertEqual(slip._l10n_at_lohnsteuertage(), 10)
+ # ------------------------------------------------------------------
+ # § 67 Abs 6 Z 6 — Überhang (tarifisch, ohne Sechstel-Effekt)
+ # ------------------------------------------------------------------
+ def test_20_abs6_ueberhang_ohne_sechstel(self):
+ """Der gekennzeichnete Überhang läuft tarifisch in der laufenden
+ Bemessung, erhöht aber das Jahressechstel nicht."""
+ employee = self._employee()
+ slip_ohne = self._slip(employee, 1)
+ regel = self._regel('T_ABS6_UEB', 1000.0, sv_art='keine')
+ regel.l10n_at_s67_abs6_ueberhang = True
+ slip_mit = self._slip(employee, 2)
+
+ self.assertAlmostEqual(
+ slip_mit._l10n_at_abs6_ueberhang_total(), 1000.0, places=2)
+ self.assertAlmostEqual(
+ slip_mit._l10n_at_bemessung_laufen()
+ - slip_ohne._l10n_at_bemessung_laufen(), 1000.0, places=2)
+ self.assertAlmostEqual(
+ slip_mit._l10n_at_sechstel() - slip_ohne._l10n_at_sechstel(),
+ 0.0, places=2)
+
# ------------------------------------------------------------------
# § 68 Freibeträge (Buckets über Regel-Flags)
# ------------------------------------------------------------------
diff --git a/addons/l10n_at_hr_payroll_private/__manifest__.py b/addons/l10n_at_hr_payroll_private/__manifest__.py
index 1203acb..cddf45f 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.13.0.0',
+ 'version': '19.0.14.0.0',
'author': 'Florian Egger',
'website': 'https://gem360.at',
'category': 'Human Resources/Payroll',
diff --git a/addons/l10n_at_hr_payroll_private/data/input_types_endabrechnung_private.xml b/addons/l10n_at_hr_payroll_private/data/input_types_endabrechnung_private.xml
index 281f194..d5885dd 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
@@ -35,4 +35,8 @@
Gesetzliche Abfertigung Alt (Brutto)
ATP_ABF_ALT
+
+ Freiwillige Abfertigung (Brutto)
+ ATP_ABF_FREI
+
\ 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 d4d160f..90780b4 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
@@ -91,6 +91,45 @@ result = inputs['ATP_ABF_ALT'].amount
]]>
+
+
+ Freiwillige Abfertigung — begünstigter Teil (§ 67 Abs 6)
+ ABF_FREI
+
+
+
+ python
+ result = 'ATP_ABF_FREI' in inputs and inputs['ATP_ABF_FREI'].amount
+ neutral
+ keine
+ abs6
+
+ code
+
+
+
+
+ Freiwillige Abfertigung — Überhang (§ 67 Abs 6 Z 6)
+ ABF_FREI_TARIF
+
+
+
+ python
+ result = payslip._at_abf_frei_ueberhang(result_rules) > 0
+ laufend
+ keine
+ abs6
+
+ 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 b66d1cb..052e094 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
@@ -119,4 +119,19 @@ result = payslip._l10n_at_lst_sonstige(result_rules)
result = payslip._at_abf_alt_lohnsteuer(result_rules)
]]>
+
+ Lohnsteuer freiwillige Abfertigung (§ 67 Abs 6)
+ LST_ABF_FREI
+
+
+
+ python
+ result = payslip._at_abf_frei_lst_6(result_rules) > 0
+ code
+ neutral
+ keine
+
+
\ No newline at end of file
diff --git a/addons/l10n_at_hr_payroll_private/data/salary_rules_reisekosten_private.xml b/addons/l10n_at_hr_payroll_private/data/salary_rules_reisekosten_private.xml
index b7710ed..27611c0 100644
--- a/addons/l10n_at_hr_payroll_private/data/salary_rules_reisekosten_private.xml
+++ b/addons/l10n_at_hr_payroll_private/data/salary_rules_reisekosten_private.xml
@@ -33,6 +33,7 @@ result = inputs['EXPENSES'].amount
python
result = 'ATP_REISE_STEUERPFLICHTIG' in inputs and inputs['ATP_REISE_STEUERPFLICHTIG'].amount
laufend
+
code
=', start),
+ ('date_from', '<', self.date_from.replace(day=1)),
+ ]).filtered(lambda p: p.state not in ('draft', 'cancel'))
+ total = 0.0
+ for slip in list(vor_slips) + [self]:
+ for line in slip.line_ids:
+ rule = line.salary_rule_id
+ if rule.l10n_at_bemessung != 'laufend':
+ continue
+ if (rule.l10n_at_s67_abs6_ueberhang
+ or rule.l10n_at_s26_z4_reise):
+ continue
+ total += line.total
+ return total
+
+ def _at_abf_frei_zwoelftel(self):
+ """Zwölftel-Anzahl nach § 67 Abs 6 Z 2: die Dienstzeitstaffel
+ entspricht den §-23-Grenzen (36→2/12 … 300→12/12)."""
+ self.ensure_one()
+ return self._at_abf_alt_staffel(
+ self.l10n_at_abf_frei_dienstzeit_monate)
+
+ def _at_abf_frei_split(self, result_rules=None):
+ """§ 67 Abs 6: begünstigter Teil (Viertel- und Zwölftelregelung,
+ gedeckelt 9 × bzw. n × 3 × monatliche HBG, Z-3-Kürzung um
+ erhaltene Abfertigungen und den bestehenden gesetzlichen
+ Anspruch) und der tarifische Überhang (Z 6)."""
+ self.ensure_one()
+ brutto = max(self._at_input_amount('ATP_ABF_FREI'), 0.0)
+ basis = self.l10n_at_abf_frei_basis_12
+ if basis <= 0:
+ basis = self._at_bezuege_12_monate()
+ basis = max(basis, 0.0)
+ hbg = float(self._l10n_at_param('at_sv_hbg_monatlich'))
+ z1 = min(basis / 4.0, 9.0 * hbg)
+ zwoelftel = self._at_abf_frei_zwoelftel()
+ z2 = min(basis * zwoelftel / 12.0, zwoelftel * 3.0 * hbg)
+ kuerzung = max(
+ self.l10n_at_abf_frei_erhaltene_abfertigungen
+ + self.l10n_at_abf_frei_gesetzlicher_anspruch, 0.0)
+ z2 = max(z2 - kuerzung, 0.0)
+ beguenstigt = min(brutto, z1 + z2)
+ return {
+ 'basis': basis,
+ 'z1': z1,
+ 'z2': z2,
+ 'kuerzung': kuerzung,
+ 'beguenstigt': beguenstigt,
+ 'ueberhang': brutto - beguenstigt,
+ }
+
+ def _at_abf_frei_beguenstigt(self, result_rules=None):
+ self.ensure_one()
+ return self._at_abf_frei_split(result_rules)['beguenstigt']
+
+ def _at_abf_frei_ueberhang(self, result_rules=None):
+ self.ensure_one()
+ return self._at_abf_frei_split(result_rules)['ueberhang']
+
+ def _at_abf_frei_lst_6(self, result_rules=None):
+ """6 % auf den begünstigten Teil (§ 67 Abs 6, fester Steuersatz)."""
+ self.ensure_one()
+ satz = float(self._l10n_at_param(
+ 'at_endabrechnung')['abfertigung_steuersatz'])
+ return self._at_abf_frei_beguenstigt(result_rules) * satz / 100.0
+
# ------------------------------------------------------------------
# Urlaubsersatzleistung — § 67 Abs 8 lit d, lb-url-15/16
# ------------------------------------------------------------------
diff --git a/addons/l10n_at_hr_payroll_private/models/pfaendung.py b/addons/l10n_at_hr_payroll_private/models/pfaendung.py
index e3cf90b..9626802 100644
--- a/addons/l10n_at_hr_payroll_private/models/pfaendung.py
+++ b/addons/l10n_at_hr_payroll_private/models/pfaendung.py
@@ -386,7 +386,8 @@ class HrPayslip(models.Model):
linie = self.line_ids.filtered(
lambda l, c=rule.code: l.code == c)
total += sum(linie.mapped('total'))
- steuer = self._at_pfa_rule_amount(result_rules, 'LST_ABF')
+ steuer = sum(self._at_pfa_rule_amount(result_rules, c)
+ for c in ('LST_ABF', 'LST_ABF_FREI'))
return round(max(0.0, total - steuer), 2)
raise UserError('Unbekannter Pfändungstopf: %s' % topf)
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 e946317..ee7b5b0 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
@@ -479,3 +479,135 @@ class EndabrechnungPrivateTest(TransactionCase):
})
with self.assertRaises(UserError):
wizard.action_payslip()
+
+ # ------------------------------------------------------------------
+ # AP15-B: freiwillige Abfertigung (§ 67 Abs 6)
+ # ------------------------------------------------------------------
+ def test_14_abf_frei_viertel_und_zwoelftel(self):
+ """15 Jahre (6/12), Basis 36.000: Z 1 9.000 + Z 2 18.000; Brutto
+ 12.000 → voll begünstigt, 6 % = 720; kein Überhang."""
+ emp = self._employee()
+ slip = self._slip(emp)
+ slip.write({
+ 'l10n_at_abf_frei_basis_12': 36000.0,
+ 'l10n_at_abf_frei_dienstzeit_monate': 180,
+ })
+ self._input(slip, 'ATP_ABF_FREI', 12000.0)
+
+ split = slip._at_abf_frei_split()
+ self.assertAlmostEqual(split['z1'], 9000.0, places=2)
+ self.assertAlmostEqual(split['z2'], 18000.0, places=2)
+ self.assertAlmostEqual(split['beguenstigt'], 12000.0, places=2)
+ self.assertAlmostEqual(split['ueberhang'], 0.0, places=2)
+ self.assertAlmostEqual(
+ self._total(slip, 'ABF_FREI'), 12000.0, places=2)
+ self.assertAlmostEqual(
+ self._total(slip, 'LST_ABF_FREI'), 720.0, places=2)
+ self.assertFalse(self._total(slip, 'ABF_FREI_TARIF'))
+
+ def test_15_abf_frei_ueberhang_tarif_ohne_sechstel(self):
+ """Brutto 40.000: begünstigt 27.000 (Z 1 + Z 2), Überhang 13.000
+ läuft tarifisch über die LSt-Bemessung, ohne das Jahressechstel
+ zu erhöhen; SV, LNK und BV bleiben unberührt."""
+ emp_mit = self._employee()
+ emp_ohne = self._employee()
+ slip_mit = self._slip(emp_mit)
+ slip_ohne = self._slip(emp_ohne)
+ slip_mit.write({
+ 'l10n_at_abf_frei_basis_12': 36000.0,
+ 'l10n_at_abf_frei_dienstzeit_monate': 180,
+ })
+ self._input(slip_mit, 'ATP_ABF_FREI', 40000.0)
+
+ self.assertAlmostEqual(
+ self._total(slip_mit, 'ABF_FREI'), 27000.0, places=2)
+ self.assertAlmostEqual(
+ self._total(slip_mit, 'ABF_FREI_TARIF'), 13000.0, places=2)
+ lst6 = self.env.company.currency_id.round(27000.0 * 0.06)
+ self.assertAlmostEqual(
+ self._total(slip_mit, 'LST_ABF_FREI'), lst6, places=2)
+ self.assertAlmostEqual(
+ slip_mit._l10n_at_bemessung_laufen()
+ - slip_ohne._l10n_at_bemessung_laufen(), 13000.0, places=2)
+ self.assertGreater(
+ self._total(slip_mit, 'LST_LFD'), self._total(slip_ohne, 'LST_LFD'))
+ self.assertAlmostEqual(
+ slip_mit._l10n_at_sechstel() - slip_ohne._l10n_at_sechstel(),
+ 0.0, 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._at_pfaendung_basis(None, 'beendigung'),
+ 27000.0 - lst6, places=2)
+
+ def test_16_abf_frei_kuerzung_und_deckel(self):
+ """Z 3: erhaltene Abfertigungen plus gesetzlicher Anspruch kürzen
+ das Z-2-Ausmaß; bei großer Basis greifen die Deckel 9 × HBG
+ (Z 1) und n × 3 × HBG (Z 2)."""
+ emp = self._employee()
+ slip = self._slip(emp)
+ slip.write({
+ 'l10n_at_abf_frei_basis_12': 36000.0,
+ 'l10n_at_abf_frei_dienstzeit_monate': 180,
+ 'l10n_at_abf_frei_erhaltene_abfertigungen': 4000.0,
+ 'l10n_at_abf_frei_gesetzlicher_anspruch': 9000.0,
+ })
+ self._input(slip, 'ATP_ABF_FREI', 20000.0)
+
+ split = slip._at_abf_frei_split()
+ self.assertAlmostEqual(split['kuerzung'], 13000.0, places=2)
+ self.assertAlmostEqual(split['z2'], 5000.0, places=2)
+ self.assertAlmostEqual(split['beguenstigt'], 14000.0, places=2)
+ self.assertAlmostEqual(split['ueberhang'], 6000.0, places=2)
+
+ slip.write({
+ 'l10n_at_abf_frei_basis_12': 12000000.0,
+ 'l10n_at_abf_frei_erhaltene_abfertigungen': 0.0,
+ 'l10n_at_abf_frei_gesetzlicher_anspruch': 0.0,
+ })
+ slip.input_line_ids.filtered(
+ lambda i: i.input_type_id.code == 'ATP_ABF_FREI').write(
+ {'amount': 300000.0})
+ slip.compute_sheet()
+ split = slip._at_abf_frei_split()
+ self.assertAlmostEqual(split['z1'], 62370.0, places=2)
+ self.assertAlmostEqual(split['z2'], 124740.0, places=2)
+ self.assertAlmostEqual(split['beguenstigt'], 187110.0, places=2)
+ self.assertAlmostEqual(split['ueberhang'], 112890.0, places=2)
+
+ def test_17_abf_frei_wizard(self):
+ emp = self._employee()
+ emp.version_id.write({
+ 'structure_type_id': self.env.ref(
+ 'l10n_at_hr_payroll_private.structure_type_at_privat').id,
+ 'l10n_at_abfertigung_neu': False,
+ })
+ 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_frei_aktiv': True,
+ 'abf_frei_anspruch_bestaetigt': True,
+ 'abf_frei_betrag': 20000.0,
+ 'abf_frei_basis_12': 36000.0,
+ 'abf_frei_dienstzeit_monate': 180,
+ 'abf_frei_nachweis': 'Vordienstzeiten zum Lohnkonto nachgewiesen',
+ })
+ ergebnis = wizard.action_payslip()
+ slip = self.env['hr.payslip'].browse(ergebnis['res_id'])
+
+ self.assertAlmostEqual(self._total(slip, 'ABF_FREI'), 20000.0,
+ places=2)
+ self.assertAlmostEqual(self._total(slip, 'LST_ABF_FREI'), 1200.0,
+ places=2)
+ self.assertEqual(slip.l10n_at_abf_frei_dienstzeit_monate, 180)
+ self.assertEqual(slip.l10n_at_abf_frei_nachweis,
+ 'Vordienstzeiten zum Lohnkonto nachgewiesen')
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 64658cf..c0b23e0 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
@@ -209,6 +209,35 @@ class PfaendungPrivateTest(TransactionCase):
self.assertAlmostEqual(
self._total(slip, 'PFAENDUNG_END'), masse, places=2)
+ def test_09c_pfaendung_abf_frei_topf(self):
+ """AP15-B: begünstigter Teil der freiwilligen Abfertigung läuft
+ in den §-291d-Topf, abzüglich beider Abfertigungs-LSt-Arten; der
+ Überhang bleibt über die laufende Bemessung pfändbar."""
+ 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_frei_basis_12': 36000.0,
+ 'l10n_at_abf_frei_dienstzeit_monate': 180,
+ })
+ input_type = self.env.ref(
+ 'l10n_at_hr_payroll_private.input_type_atp_abf_frei')
+ self.env['hr.payslip.input'].create({
+ 'payslip_id': slip.id,
+ 'input_type_id': input_type.id,
+ 'amount': 40000.0,
+ })
+ slip.compute_sheet()
+
+ lst6 = self.env.company.currency_id.round(27000.0 * 0.06)
+ self.assertAlmostEqual(
+ self._total(slip, 'ABF_FREI'), 27000.0, places=2)
+ self.assertAlmostEqual(
+ self._total(slip, 'ABF_FREI_TARIF'), 13000.0, places=2)
+ self.assertAlmostEqual(
+ slip._at_pfaendung_basis(None, 'beendigung'),
+ 27000.0 - lst6, 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 6d3a611..64a6cec 100644
--- a/addons/l10n_at_hr_payroll_private/views/endabrechnung_views.xml
+++ b/addons/l10n_at_hr_payroll_private/views/endabrechnung_views.xml
@@ -44,6 +44,27 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -90,6 +111,30 @@
+
+ hr.payslip.form.abfertigung.frei
+ 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 bcf8d85..687d4d9 100644
--- a/addons/l10n_at_hr_payroll_private/wizard/endabrechnung.py
+++ b/addons/l10n_at_hr_payroll_private/wizard/endabrechnung.py
@@ -101,6 +101,43 @@ class L10nAtEndabrechnungWizard(models.TransientModel):
string='Nachweis/Begründung',
help='Quelle der Dienstzeiten, Anspruchsprüfung, Alt-/Neu-Status '
'und allfällige manuelle Abweichung dokumentieren.')
+ abf_frei_aktiv = fields.Boolean(
+ string='Freiwillige Abfertigung (§ 67 Abs 6)')
+ abf_frei_anspruch_bestaetigt = fields.Boolean(
+ string='Prüfung und Nachweise bestätigt',
+ help='Bestätigung, dass es sich um eine Beendigungsleistung ohne '
+ 'schädliche Anspruchsumwandlung handelt (KE/UE/SZ/Zeitguthaben '
+ 'müssen getrennt abrechnet werden — GPLB-Risiko, lb-end-15), '
+ 'dass die (Vor-)Dienstzeiten lückenlos zum Lohnkonto '
+ '(§ 76 EStG) nachgewiesen sind und erhaltene Abfertigungen '
+ 'angegeben wurden.')
+ abf_frei_betrag = fields.Monetary(
+ string='Bruttobetrag', currency_field='currency_id')
+ abf_frei_basis_12 = fields.Monetary(
+ string='Laufende Bezüge letzte 12 Monate',
+ currency_field='currency_id',
+ help='Basis der Viertel-/Zwölftelregelung: alle steuerbaren '
+ 'Bezugsteile (auch steuerfreie Zuschläge, Sachbezüge), '
+ 'ohne Sonderzahlungen und ohne §-26-Z-4-Reisevergütungen; '
+ 'wird aus den letzten zwölf validierten Läufen vorgeschlagen '
+ 'und ist bestätigbar (lb-end-13).')
+ abf_frei_dienstzeit_monate = fields.Integer(
+ string='Nachgewiesene Dienstzeit (Monate)')
+ abf_frei_erhaltene_abfertigungen = fields.Monetary(
+ string='Bereits erhaltene Abfertigungen', currency_field='currency_id',
+ help='In den nachgewiesenen Dienstzeiten erhaltene Abfertigungen '
+ '(§ 67 Abs 3 oder Abs 6 EStG) — Kürzung nach Z 3.')
+ abf_frei_gesetzlicher_anspruch = fields.Monetary(
+ string='Bestehender gesetzlicher Anspruch',
+ currency_field='currency_id',
+ help='Anspruch auf eine gesetzliche Abfertigung am aktuellen '
+ 'Dienstverhältnis-Ende (Z 3); bei aktiver Alt-Abfertigung '
+ 'automatisch aus dem Alt-Anspruch vorgeschlagen.')
+ abf_frei_nachweis = fields.Text(
+ string='Nachweis/Begründung',
+ help='Vordienstzeiten-Nachweis (Lohnkonto § 76 EStG), erhaltene '
+ 'Abfertigungen, Basis-Abweichungen und Anspruchsgrundlage '
+ 'dokumentieren.')
currency_id = fields.Many2one(
'res.currency', default=lambda self: self.env.company.currency_id)
@@ -128,6 +165,27 @@ class L10nAtEndabrechnungWizard(models.TransientModel):
abf_alt_netto_vorschau = fields.Monetary(
string='Abfertigung Alt netto', currency_field='currency_id',
compute='_compute_vorschau')
+ abf_frei_z1_vorschau = fields.Monetary(
+ string='Z 1 Viertelregelung (Vorschau)', currency_field='currency_id',
+ compute='_compute_vorschau')
+ abf_frei_z2_vorschau = fields.Monetary(
+ string='Z 2 Zwölftelregelung (Vorschau)', currency_field='currency_id',
+ compute='_compute_vorschau')
+ abf_frei_kuerzung_vorschau = fields.Monetary(
+ string='Z 3 Kürzung (Vorschau)', currency_field='currency_id',
+ compute='_compute_vorschau')
+ abf_frei_beguenstigt_vorschau = fields.Monetary(
+ string='Begünstigter Teil 6 % (Vorschau)', currency_field='currency_id',
+ compute='_compute_vorschau')
+ abf_frei_ueberhang_vorschau = fields.Monetary(
+ string='Überhang zum Tarif (Vorschau)', currency_field='currency_id',
+ compute='_compute_vorschau')
+ abf_frei_lst_vorschau = fields.Monetary(
+ string='LSt 6 % (Vorschau)', currency_field='currency_id',
+ compute='_compute_vorschau')
+ abf_frei_netto_vorschau = fields.Monetary(
+ string='Begünstigter Teil netto (Vorschau)',
+ currency_field='currency_id', compute='_compute_vorschau')
@api.onchange('employee_id')
def _onchange_employee(self):
@@ -139,12 +197,16 @@ class L10nAtEndabrechnungWizard(models.TransientModel):
version.contract_date_end or fields.Date.today())
wizard.departure_reason_id = version.departure_reason_id
wizard._at_abf_alt_vorschlag_setzen()
+ wizard._at_abf_frei_vorschlag_setzen()
@api.onchange('austrittsdatum', 'abf_alt_anspruchsart',
- 'abf_alt_dienstzeit_monate')
+ 'abf_alt_dienstzeit_monate', 'abf_alt_aktiv',
+ 'abf_alt_monatsentgelt', 'abf_alt_mehrfach',
+ 'abf_frei_aktiv', 'abf_frei_dienstzeit_monate')
def _onchange_abf_alt(self):
for wizard in self:
wizard._at_abf_alt_vorschlag_setzen()
+ wizard._at_abf_frei_vorschlag_setzen()
def _at_abf_alt_vorschlag_setzen(self):
self.ensure_one()
@@ -168,11 +230,27 @@ class L10nAtEndabrechnungWizard(models.TransientModel):
if vorgeschlagen or not self.abf_alt_mehrfach:
self.abf_alt_mehrfach = vorgeschlagen
+ def _at_abf_frei_vorschlag_setzen(self):
+ self.ensure_one()
+ if not self.employee_id or not self.austrittsdatum:
+ return
+ slip = self._vorschau_payslip()
+ if self.abf_frei_aktiv and not self.abf_frei_basis_12:
+ self.abf_frei_basis_12 = slip._at_bezuege_12_monate()
+ if (self.abf_frei_aktiv and self.abf_alt_aktiv
+ and not self.abf_frei_gesetzlicher_anspruch):
+ self.abf_frei_gesetzlicher_anspruch = (
+ self.abf_alt_monatsentgelt * self.abf_alt_mehrfach)
+
@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')
+ 'abf_alt_dienstzeit_monate', 'abf_alt_anspruchsart',
+ 'abf_frei_aktiv', 'abf_frei_betrag', 'abf_frei_basis_12',
+ 'abf_frei_dienstzeit_monate',
+ 'abf_frei_erhaltene_abfertigungen',
+ 'abf_frei_gesetzlicher_anspruch')
def _compute_vorschau(self):
for wizard in self:
wizard.uel_lfd_vorschau = 0.0
@@ -183,6 +261,13 @@ class L10nAtEndabrechnungWizard(models.TransientModel):
wizard.abf_alt_steuer_6_vorschau = 0.0
wizard.abf_alt_lohnsteuer_vorschau = 0.0
wizard.abf_alt_netto_vorschau = 0.0
+ wizard.abf_frei_z1_vorschau = 0.0
+ wizard.abf_frei_z2_vorschau = 0.0
+ wizard.abf_frei_kuerzung_vorschau = 0.0
+ wizard.abf_frei_beguenstigt_vorschau = 0.0
+ wizard.abf_frei_ueberhang_vorschau = 0.0
+ wizard.abf_frei_lst_vorschau = 0.0
+ wizard.abf_frei_netto_vorschau = 0.0
if not wizard.employee_id or not wizard.austrittsdatum:
continue
slip = wizard._vorschau_payslip()
@@ -202,6 +287,19 @@ class L10nAtEndabrechnungWizard(models.TransientModel):
wizard.abf_alt_steuer_6_vorschau = steuer_6
wizard.abf_alt_lohnsteuer_vorschau = steuer
wizard.abf_alt_netto_vorschau = betrag - steuer
+ if wizard.abf_frei_aktiv:
+ split = slip._at_abf_frei_split()
+ satz = float(slip._l10n_at_param(
+ 'at_endabrechnung')['abfertigung_steuersatz'])
+ wizard.abf_frei_z1_vorschau = split['z1']
+ wizard.abf_frei_z2_vorschau = split['z2']
+ wizard.abf_frei_kuerzung_vorschau = split['kuerzung']
+ wizard.abf_frei_beguenstigt_vorschau = split['beguenstigt']
+ wizard.abf_frei_ueberhang_vorschau = split['ueberhang']
+ wizard.abf_frei_lst_vorschau = \
+ split['beguenstigt'] * satz / 100.0
+ wizard.abf_frei_netto_vorschau = \
+ split['beguenstigt'] - wizard.abf_frei_lst_vorschau
def _vorschau_payslip(self):
"""Flüchtiger Payslip-Kontext für die Vorschau-Berechnung (kein
@@ -229,11 +327,21 @@ class L10nAtEndabrechnungWizard(models.TransientModel):
self.abf_alt_monatslohnsteuer,
'l10n_at_abf_alt_mehrfach': self.abf_alt_mehrfach,
'l10n_at_abf_alt_nachweis': self.abf_alt_nachweis,
+ 'l10n_at_abf_frei_basis_12': self.abf_frei_basis_12,
+ 'l10n_at_abf_frei_dienstzeit_monate':
+ self.abf_frei_dienstzeit_monate,
+ 'l10n_at_abf_frei_erhaltene_abfertigungen':
+ self.abf_frei_erhaltene_abfertigungen,
+ 'l10n_at_abf_frei_gesetzlicher_anspruch':
+ self.abf_frei_gesetzlicher_anspruch,
+ 'l10n_at_abf_frei_nachweis': self.abf_frei_nachweis,
})
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),
+ ('ATP_ABF_FREI', self.abf_frei_betrag
+ if self.abf_frei_aktiv else 0.0),
]
slip.input_line_ids = [
(0, 0, {
@@ -256,6 +364,8 @@ class L10nAtEndabrechnungWizard(models.TransientModel):
('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),
+ ('ATP_ABF_FREI', self.abf_frei_betrag
+ if self.abf_frei_aktiv else 0.0),
]
def _abf_alt_validieren(self):
@@ -299,11 +409,30 @@ class L10nAtEndabrechnungWizard(models.TransientModel):
'Entlassung. Bitte den Anspruch manuell bestätigen und '
'begründen.'))
+ def _abf_frei_validieren(self):
+ self.ensure_one()
+ if not self.abf_frei_aktiv:
+ return
+ if not self.abf_frei_anspruch_bestaetigt:
+ raise UserError(_('Bitte Prüfung und Nachweise der freiwilligen '
+ 'Abfertigung bestätigen.'))
+ if self.abf_frei_betrag <= 0:
+ raise UserError(_('Der Bruttobetrag der freiwilligen '
+ 'Abfertigung muss größer als null sein.'))
+ if (self.abf_frei_basis_12 < 0
+ or self.abf_frei_erhaltene_abfertigungen < 0
+ or self.abf_frei_gesetzlicher_anspruch < 0
+ or self.abf_frei_dienstzeit_monate < 0):
+ raise UserError(_('12-Monats-Basis, erhaltene Abfertigungen, '
+ 'gesetzlicher Anspruch und Dienstzeit dürfen '
+ 'nicht negativ sein.'))
+
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()
+ self._abf_frei_validieren()
erster = self.austrittsdatum.replace(day=1)
letzter = (erster + timedelta(days=32)).replace(day=1) \
- timedelta(days=1)
@@ -335,6 +464,18 @@ class L10nAtEndabrechnungWizard(models.TransientModel):
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,
+ 'l10n_at_abf_frei_basis_12':
+ self.abf_frei_basis_12 if self.abf_frei_aktiv else 0.0,
+ 'l10n_at_abf_frei_dienstzeit_monate':
+ self.abf_frei_dienstzeit_monate if self.abf_frei_aktiv else 0,
+ 'l10n_at_abf_frei_erhaltene_abfertigungen':
+ self.abf_frei_erhaltene_abfertigungen
+ if self.abf_frei_aktiv else 0.0,
+ 'l10n_at_abf_frei_gesetzlicher_anspruch':
+ self.abf_frei_gesetzlicher_anspruch
+ if self.abf_frei_aktiv else 0.0,
+ 'l10n_at_abf_frei_nachweis':
+ self.abf_frei_nachweis if self.abf_frei_aktiv else False,
})
typen = {code: self.env['hr.payslip.input.type'].search(
[('code', '=', code)], limit=1)