[IMP] l10n_at_hr_payroll_private: add voluntary severance under § 67 Abs 6 (AP15-B)

Split the voluntary severance into its privileged part and a tariff remainder on the exit run: the privileged part combines the quarter rule (1/4 of the last twelve months' running pay, capped at nine times the monthly HBG) with the twelfth rule (2/12 to 12/12 by proven service, capped at n × 3 × HBG), reduced per Z 3 by severance already received and the statutory claim due at this termination, all taxed at the flat 6 % rate. The excess runs as its own rule flagged l10n_at_s67_abs6_ueberhang: taxed with the running tariff of the payment month but excluded from the annual sixth, since Z 5 exempts only the Z 1/Z 2 amounts and the excess is a special payment, not running pay. The twelve-month basis aggregates validated prior runs plus own lines, excluding the flagged excess and § 26 Z 4 travel overhang via a dedicated rule flag. Free of SV (§ 49 Abs 3 Z 7 ASVG), payroll levies regardless of the 6 %/tarif split (§ 41 Abs 4 lit b FLAG, § 122 Abs 8 WKG, § 5 Abs 2 lit b KommStG) and BV contributions; the former BV-Anwartschaft exclusion Z 7 is repealed (BGBl I 118/2015). The wizard proposes the rolling basis and the statutory claim, previews Z 1/Z 2/reduction/privileged/excess/tax and requires explicit confirmation against claim-conversion risk. The § 291d EO termination pot nets both severance wage-tax rules; key 260 now includes LST_ABF_FREI. Verified against the original statute text in .ris/ (EStG § 67 Abs 6) and the knowledge-base sources lb-end-12/13/15; KZ positioning of Abs-6 amounts and the excess's § 291d pot allocation remain documented verification points against LStR Rz 1087 ff.

Version 19.0.14.0.0 (private), 19.0.10.0.0 (core)
This commit is contained in:
2026-09-15 11:34:09 +02:00
parent 9add58fe83
commit 4b86bcb790
19 changed files with 692 additions and 16 deletions
@@ -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')
@@ -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))