Files
odoo-at-payroll/addons/l10n_at_hr_payroll_private/models/endabrechnung.py
T
fegger 472b19a4b2 [IMP] l10n_at_hr_payroll_private: add settlement payments under § 67 Abs 8 lit a (AP15-C)
Complete the termination sequence with the settlement-sum path: comparison sums from court or out-of-court settlements, judgment/decision arrears and dismissal-objection payments are split between the statutory-severance and voluntary-severance rules and the settlement remainder. The remainder (VERGLEICH, bemessung neutral, sv_art laufend) reuses the lit-a/b fifth rule: after deducting the § 62 employee-rate contributions, one fifth stays tax-free capped at one fifth of nine times the monthly HBG (12 474 EUR 2026), and the remaining four fifths run with the payment-month tariff without touching the annual sixth. A new rule flag l10n_at_vergleich_bv marks the part paid for periods with a BV-Kasse entitlement: up to the parameterised cap at_lst_67_abs8_vergleich_bv (7 500 EUR 2026; § 67 Abs 8 is § 33a/§ 124b indexed) it is taxed at the flat 6 % rate with the contributions allocated pro rata (LStR Rz 1102b), while the excess falls back into the fifth rule. Both settlement rules stay payroll-levy liable (§ 41 Abs 4 lit b FLAG exempts only § 67 Abs 3 and 6) and SV-liable in the payment month, with per-Beitragsmonat roll-forward and insurance-period extension remaining documented manual options (lb-end-22, E-MVB 011-02-00-001). Identifiable severance components keep running through the existing statutory and voluntary severance machinery; a settlement expressly agreed as statutory severance is taxed in full under § 67 Abs 3 (VwGH 2013/13/0001 = ARD 6490/20/2016), and pension buy-outs (lit e) and social-plan payments (lit f) are out of scope. The wizard section requires confirmation of the settlement's itemisation and contested-claim status plus the BV entitlement, enforces the BV cap with the excess captured as remainder, and previews the free fifth, tariff basis and 6 % tax; the settlement memo lands on the payslip for the separate Lohnzettel workflow (LStR Rz 911a). The account matrix posts both settlement rules to the provisional 6200/6000 (workers 6000) and the new tax rule to 3540. Verified against the original statute text in .ris/ (EStG § 67 Abs 8 lit a) and the knowledge-base sources lb-end-21/22; KZ positioning of the lit-a tariff parts and their § 291d pot allocation remain documented verification points against LStR Rz 1087 ff.

Version 19.0.15.0.0 (private)
2026-09-15 12:38:19 +02:00

635 lines
31 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Part of the odoo-at-payroll project. License: LGPL-3.
"""AP14-A: Endabrechnungs-Zyklus — Urlaubsersatzleistung (UEL),
Abgangsentschädigung (§ 67 Abs 8 lit b), Aufrechnung offener Vorschüsse
und Zeitguthaben/-schulden (§ 19e AZG).
AP15-C: Vergleichssummen (§ 67 Abs 8 lit a).
Am Originalwortlaut verifiziert (.ris/, Stand 2026-09-14):
- § 11 Abs 2 ASVG: die Pflichtversicherung besteht für die Zeit des
Bezuges einer Ersatzleistung für Urlaubsentgelt (und einer
Kündigungsentschädigung) weiter; gebühren beide, ist zur Bestimmung
des Zeitraumes ZUERST die Kündigungsentschädigung und im Anschluss
die Ersatzleistung heranzuziehen (sequentielle Verlängerungsfenster).
Die 6-WT/5-AT-Tage-Umrechnung ist Verwaltungspraxis (lb-url-15),
nicht Gesetzeswortlaut.
- § 67 Abs 8 lit d EStG: UEL „soweit sie laufenden Arbeitslohn
betreffen, als laufender Arbeitslohn, soweit sie sonstige Bezüge
betreffen, als sonstiger Bezug im Kalendermonat der Zahlung" — die
Zweikomponenten-Abbildung läuft nativ über l10n_at_bemessung.
- § 67 Abs 8 lit b EStG (Kündigungs-/Abgangsentschädigung): nach
Abzug der §-62-Beiträge ist ein Fünftel steuerfrei zu belassen,
höchstens ein Fünftel des Neunfachen der monatlichen HBG; der Rest
läuft tarifisch im Zuflussmonat (Abs 10), Abs 2 (Sechstel) ist
NICHT anzuwenden. LNK-pflichtig (FLAG § 41 Abs 4 nennt nur
§ 67 Abs 3 und 6) und SV-pflichtig (keine §-49-Abs-3-Z-7-Befreiung)
mit Umlegung auf den Kündigungsfrist-Zeitraum.
- § 19e Abs 2 AZG: Guthaben an Normalarbeitszeit + 50 % Zuschlag
(Entfall bei unberechtigtem vorzeitigem Austritt; KV kann
abweichend regeln).
- § 67 Abs 8 lit a EStG (AP15-C, Stand 2026-09-15): Vergleichssummen
sind, SOWEIT sie nicht nach Abs 3, 6 oder dem letzten Satz mit dem
festen Steuersatz zu versteuern sind, nach Abs 10 im Kalendermonat
der Zahlung zu erfassen; nach Abzug der §-62-Z-3/4/5-Beiträge ist
ein Fünftel steuerfrei (Deckel ein Fünftel des Neunfachen der
monatlichen HBG), Abs 2 (Sechstel) ist nicht anzuwenden. Erkennbare
Abfertigungsanteile laufen getrennt über Abs 3 (VwGH
2013/13/0001 = ARD 6490/20/2016: ausdrücklich vereinbarte
gesetzliche Abfertigung → gesamt Abs 3) bzw. Abs 6. Fallen die
Vergleichssummen bei oder nach Beendigung an UND betreffen sie
Zeiträume mit BV-Kassen-Anwartschaft, sind sie bis 7 500 € mit 6 %
zu versteuern (parametrisiert, §-33a-Indexkette); der übersteigende
Teil fällt in die Fünftelregelung zurück. SV-Aufrollung aktiver
Ansprüche je Beitragsmonat bzw. PV-Verlängerung: lb-end-22
(E-MVB 011-02-00-001), Kern-Mechanismus l10n_at_beitragsmonat.
"""
from datetime import timedelta
from dateutil.relativedelta import relativedelta
from odoo import fields, models
class HrSalaryRule(models.Model):
_inherit = 'hr.salary.rule'
l10n_at_sv_verlaengerung = fields.Selection(
[('keine', 'Keine SV-Verlängerung (§ 11 Abs 2 ASVG)'),
('urlaubsersatz', 'UEL-Tage-Schema (Urlaubsersatzleistung)'),
('abgang', 'Kündigungsfrist (Abgangsentschädigung)')],
string='AT SV-Verlängerung', default='keine',
help='Verteilt die laufende SV-Basis dieser Regel auf die '
'Pflichtversicherungs-Verlängerungsmonate nach § 11 Abs 2 '
'ASVG statt auf den Abrechnungsmonat: Abgangsentschädigung '
'zuerst (Kündigungsfrist), UEL anschließend (Tage-Schema '
'lb-url-15).')
l10n_at_s67_fuenftel = fields.Boolean(
string='AT § 67 Abs 8 lit a/b (Fünftelregelung)',
help='LSt-Pfad der Vergleichs-/Kündigungsentschädigungen: nach '
'Abzug der §-62-Beiträge ist ein Fünftel steuerfrei '
'(Deckel: ein Fünftel des Neunfachen der monatlichen HBG), '
'der Rest läuft tarifisch im Zuflussmonat — ohne '
'Jahressechstel-Erhöhung (Abs 2 nicht anzuwenden).')
l10n_at_vergleich_bv = fields.Boolean(
string='AT § 67 Abs 8 lit a — BV-Anwartschafts-Teil',
help='Teil einer Vergleichssumme, der bei oder nach Beendigung '
'für Zeiträume mit BV-Kassen-Anwartschaft ausbezahlt wird: '
'bis zum Deckel (at_lst_67_abs8_vergleich_bv, 2026: 7 500 €) '
'mit 6 %, der übersteigende Teil fällt in die '
'Fünftelregelung zurück. SV-pflichtig und LNK-pflichtig.')
l10n_at_s26_z4_reise = fields.Boolean(
string='AT § 26 Z 4 Reisevergütung',
help='§-26-Z-4-Reisevergütungen zählen nicht in die '
'12-Monats-Basis der §-67-Abs-6-Begünstigung (lb-end-13); '
'Sonderzahlungen sind ohnehin nicht bemessung „laufend“.')
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)
l10n_at_abf_frei_basis_12 = fields.Float(
string='AT Freiwillige Abfertigung — 12-Monats-Basis', copy=False,
help='Laufende Bezüge der letzten zwölf Monate (§ 67 Abs 6 Z 1/2 '
'EStG): vom Wizard bestätigt — alle steuerbaren Bezugsteile '
'inklusive steuerfreier Zuschläge und Sachbezüge, ohne '
'Sonderzahlungen und ohne §-26-Z-4-Reisevergütungen; keine '
'Bezüge anderer Arbeitgeber, keine Hochrechnung kurzer DV.')
l10n_at_abf_frei_dienstzeit_monate = fields.Integer(
string='AT Freiwillige Abfertigung — nachgewiesene Dienstzeit '
'(Monate)', copy=False)
l10n_at_abf_frei_erhaltene_abfertigungen = fields.Float(
string='AT Freiwillige Abfertigung — erhaltene Abfertigungen',
copy=False,
help='Während der nachgewiesenen Dienstzeit bereits erhaltene '
'Abfertigungen nach § 67 Abs 3 oder Abs 6 EStG — kürzen das '
'Z-2-Ausmaß (§ 67 Abs 6 Z 3).')
l10n_at_abf_frei_gesetzlicher_anspruch = fields.Float(
string='AT Freiwillige Abfertigung — bestehender gesetzlicher '
'Anspruch', copy=False,
help='Bestehender Anspruch auf eine gesetzliche Abfertigung am '
'aktuellen Dienstverhältnis-Ende (§ 67 Abs 6 Z 3 EStG) — '
'kürzt das Z-2-Ausmaß.')
l10n_at_abf_frei_nachweis = fields.Text(
string='AT Freiwillige Abfertigung — Nachweis/Begründung',
copy=False)
l10n_at_vergleich_nachweis = fields.Text(
string='AT Vergleich — Nachweis/Begründung', copy=False,
help='Aufgliederung der Vergleichsvereinbarung (lb-end-21): '
'strittige Ansprüche, erkennbare Abfertigungsanteile, '
'BV-Anwartschafts-Zeiträume; beim Pauschalvergleich der '
'Verhältnisfaktor Forderungsbetrag : Vergleichssumme '
'(E-MVB 011-02-00-001).')
# ------------------------------------------------------------------
# SV-Tage-Umrechnung und Verlängerungsmonate (§ 11 Abs 2 ASVG)
# ------------------------------------------------------------------
def _at_end_arbeitsrechtliches_ende(self):
"""Arbeitsrechtliches Ende = letzter Arbeitstag; das SV-Ende
(„Ende des Entgeltanspruches") liegt darüber hinaus, wenn ein
Entgeltanspruch über das Ende hinaus läuft."""
self.ensure_one()
return self.version_id.contract_date_end or self.date_to
def _at_end_arbeitstage_woche(self):
"""Arbeitstage pro Woche des Vertragskalenders (Schema-Grundlage
der SV-Tage-Umrechnung: 5-AT-Woche, 6-WT-Woche, Teilzeitwoche)."""
self.ensure_one()
calendar = self.version_id.resource_calendar_id
days = {int(att.dayofweek) for att in calendar.attendance_ids} \
if calendar else set()
return len(days) or 5
def _at_end_sv_tage(self, tage):
"""SV-Tage aus abzurechnenden Urlaubstagen (Verwaltungspraxis
lb-url-15): je volle Woche à N Arbeitstage werden 7N
Kalendertage hinzugerechnet (5-AT-Woche +2, 6-WT-Woche +1,
2-Tage-Teilzeitwoche +5) — nur ganze Wochenblöcke."""
self.ensure_one()
at_woche = self._at_end_arbeitstage_woche()
blocks = int(tage // at_woche)
return tage + blocks * (7 - at_woche)
def _at_end_verlaengerungs_monate(self, sv_tage, start):
"""[(Monatsanfang, Tage im Monat)] eines Verlängerungsfensters
ab dem übergebenen Starttag (§ 11 Abs 2 ASVG)."""
self.ensure_one()
ergebnis = {}
rest = int(sv_tage)
tag = start
while rest > 0:
monatsende = (tag.replace(day=28) + timedelta(days=4)) \
.replace(day=1) - timedelta(days=1)
tage_dabei = min((monatsende - tag).days + 1, rest)
monat = tag.replace(day=1)
ergebnis[monat] = ergebnis.get(monat, 0) + tage_dabei
tag = monatsende + timedelta(days=1)
rest -= tage_dabei
return sorted(ergebnis.items())
def _at_end_rule_verlaengerung(self, rule, result_rules=None):
"""[(Monatsanfang, Tage, Anteil)] der Regel-Basis nach § 11
Abs 2: die Abgangsentschädigung belegt ZUERST die (fiktive)
Kündigungsfrist, die UEL SCHLIESSLICH die Urlaubstage — beide
Fenster laufen ab dem Tag nach dem arbeitsrechtlichen Ende
hintereinander (Gesetzeswortlaut: „zunächst die
Kündigungsentschädigung und im Anschluss die Ersatzleistung")."""
self.ensure_one()
start = self._at_end_arbeitsrechtliches_ende() + timedelta(days=1)
if rule.l10n_at_sv_verlaengerung == 'urlaubsersatz':
ffrist = self._at_input_amount('ATP_ABGANG_FFRIST_TAGE')
if ffrist:
start += timedelta(days=int(ffrist))
tage = self._at_end_sv_tage(self._at_uel_tage())
else:
ffrist = self._at_input_amount('ATP_ABGANG_FFRIST_TAGE')
tage = int(ffrist) if ffrist else 0
monate = self._at_end_verlaengerungs_monate(tage, start)
if not monate:
return []
gesamt = sum(tage for _monat, tage in monate)
return [(monat, tage, tage / float(gesamt))
for monat, tage in monate]
# ------------------------------------------------------------------
# Eingabe-Helfer
# ------------------------------------------------------------------
def _at_input_amount(self, code):
self.ensure_one()
linie = self.input_line_ids.filtered(
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))
# ------------------------------------------------------------------
# Freiwillige Abfertigung — § 67 Abs 6 EStG (Viertel-/Zwölftelregelung)
# ------------------------------------------------------------------
def _at_bezuege_12_monate(self):
"""Laufende Bezüge der letzten zwölf Monate (§ 67 Abs 6 Z 1/2):
validierte Vormonats-Läufe plus eigene Zeilen — ohne den als
Abs-6-Überhang gekennzeichneten Betrag (das ist ein sonstiger
Bezug, kein laufender) und ohne §-26-Z-4-Reisevergütungen;
Sonderzahlungen sind ohnehin nicht bemessung „laufend“
(lb-end-13)."""
self.ensure_one()
start = (self.date_from - relativedelta(months=11)).replace(day=1)
vor_slips = self.env['hr.payslip'].search([
('employee_id', '=', self.employee_id.id),
('id', '!=', self.id),
('date_from', '>=', 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
# ------------------------------------------------------------------
def _at_uel_tage(self):
"""Offene Urlaubstage (Arbeitstage; keine Rundung, lb-url-16)."""
self.ensure_one()
return self._at_input_amount('ATP_UEL_TAGE')
def _at_uel_divisor(self):
"""Tagewert-Divisor: /26 bei Werktags-, /22 bei 5-AT-Kalender
(lb-url-16)."""
self.ensure_one()
params = self._l10n_at_param('at_endabrechnung')
if self._at_end_arbeitstage_woche() >= 6:
return float(params['divisor_werktage'])
return float(params['divisor_arbeitstage'])
def _at_uel_lfd(self):
"""UEL-laufender Anteil: Tage × Monatsentgelt / Divisor —
läuft als laufender Bezug durch den Tarif des Austrittsmonats
und erhöht das Jahressechstel (nativ über bemessung='laufend')."""
self.ensure_one()
return self._at_uel_tage() * self._at_kv_gehalt_basis() \
/ self._at_uel_divisor()
def _at_uel_sz_tageswert(self):
"""SZ-Anteil je Urlaubstag: 2 Sonderzahlungen à Monatsentgelt
pro Jahr (KV § 11) → je Monat 2 × SZ-Basis/12, als Tageswert
(auch bei bereits überaliquot bezahlten SZ, lb-url-16)."""
self.ensure_one()
sz_monat = 2.0 * self._at_sz_basis(self.date_to) / 12.0
return sz_monat / self._at_uel_divisor()
def _at_uel_sz(self):
"""UEL-Sonderzahlungsanteil: Tage × SZ-Tageswert."""
self.ensure_one()
return self._at_uel_tage() * self._at_uel_sz_tageswert()
# ------------------------------------------------------------------
# Abgangsentschädigung — § 67 Abs 8 lit b (Fünftelregelung)
# ------------------------------------------------------------------
def _at_fuenftel_regeln(self):
self.ensure_one()
return self.struct_id.rule_ids.filtered(
lambda rule: rule.l10n_at_s67_fuenftel)
def _at_fuenftel_total(self, result_rules=None):
self.ensure_one()
total = 0.0
for rule in self._at_fuenftel_regeln():
if result_rules is not None:
if rule.code in result_rules:
total += result_rules[rule.code]['total']
else:
linie = self.line_ids.filtered(lambda l: l.code == rule.code)
total += sum(linie.mapped('total'))
# § 67 Abs 8 lit a: der über den BV-6-%-Deckel hinausgehende
# Teil der Vergleichssumme fällt in die Fünftelregelung zurück.
total += self._at_vergleich_bv_ueberhang(result_rules)
return total
def _at_fuenftel_tarif_basis(self, result_rules=None):
"""Tarifischer Teil (4/5) der Fünftel-Bezüge: Bemessung nach
Abzug der darauf entfallenden §-62-Beiträge (approximiert über
den effektiven DN-Satz der laufenden Basis; die HBG-Caps der
Verlängerungsmonate werden im SV-Pfad separat geführt); ein
Fünftel ist steuerfrei, gedeckelt mit einem Fünftel des
Neunfachen der monatlichen HBG (lb-end-21: 2026 6.930 × 9/5
= 12.474)."""
self.ensure_one()
total = self._at_fuenftel_total(result_rules)
if total <= 0:
return 0.0
satz = sum(self._l10n_at_sv_satz(
'dn', komponente,
staffel_basis=self._l10n_at_sv_basis_roh(result_rules))
for komponente in ('kv', 'pv', 'av', 'ak', 'wf'))
bemessung = max(total * (1.0 - satz / 100.0), 0.0)
deckel = float(self._l10n_at_param('at_sv_hbg_monatlich')) \
* 9.0 / 5.0
frei = min(bemessung / 5.0, deckel)
return bemessung - frei
# ------------------------------------------------------------------
# Vergleichssumme — § 67 Abs 8 lit a (Rest, BV-Anwartschafts-Teil)
# ------------------------------------------------------------------
def _at_vergleich_bv_total(self, result_rules=None):
"""Summe der als BV-Anwartschafts-Teil gekennzeichneten
Vergleichs-Zeilen (Zeiträume mit BV-Kassen-Anwartschaft,
§ 67 Abs 8 lit a letzter Satz)."""
self.ensure_one()
total = 0.0
for rule in self.struct_id.rule_ids:
if not rule.l10n_at_vergleich_bv:
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 _at_vergleich_bv_beguenstigt(self, result_rules=None):
"""Bis zum Deckel (2026: 7 500 €) begünstigter Teil — 6 %-Fahrspur
(parametrisiert: § 67 Abs 8 unterliegt der Inflationsanpassung
§ 33a/§ 124b EStG)."""
self.ensure_one()
deckel = float(self._l10n_at_param('at_lst_67_abs8_vergleich_bv'))
return min(self._at_vergleich_bv_total(result_rules), deckel)
def _at_vergleich_bv_ueberhang(self, result_rules=None):
"""Über den BV-Deckel hinausgehender Teil — fällt in die
Fünftelregelung zurück (erster Satz des lit a)."""
self.ensure_one()
deckel = float(self._l10n_at_param('at_lst_67_abs8_vergleich_bv'))
return max(self._at_vergleich_bv_total(result_rules) - deckel, 0.0)
def _at_vergleich_bv_lst_6(self, result_rules=None):
"""6 % auf den begünstigten BV-Teil nach anteiligem §-62-Abzug
(LStR Rz 1102b: die SV-Beiträge sind den Teilbeträgen
anteilsmäßig zuzuordnen — approximiert über den effektiven
DN-Satz der laufenden Basis, wie im Fünftelpfad)."""
self.ensure_one()
beguenstigt = self._at_vergleich_bv_beguenstigt(result_rules)
if beguenstigt <= 0:
return 0.0
satz = sum(self._l10n_at_sv_satz(
'dn', komponente,
staffel_basis=self._l10n_at_sv_basis_roh(result_rules))
for komponente in ('kv', 'pv', 'av', 'ak', 'wf'))
bemessung = beguenstigt * (1.0 - satz / 100.0)
steuersatz = float(self._l10n_at_param(
'at_endabrechnung')['abfertigung_steuersatz'])
return bemessung * steuersatz / 100.0
def _at_fuenftel_frei_betr(self, result_rules=None):
"""Steuerfreies Fünftel der Fünftel-Bezüge (inkl. BV-Überhang),
gedeckelt mit einem Fünftel des Neunfachen der monatlichen HBG
(lb-end-21: 2026 6.930 × 9/5 = 12.474) — getrennt vom
tarifischen Rest für die Wizard-Vorschau."""
self.ensure_one()
total = self._at_fuenftel_total(result_rules)
if total <= 0:
return 0.0
satz = sum(self._l10n_at_sv_satz(
'dn', komponente,
staffel_basis=self._l10n_at_sv_basis_roh(result_rules))
for komponente in ('kv', 'pv', 'av', 'ak', 'wf'))
bemessung = max(total * (1.0 - satz / 100.0), 0.0)
deckel = float(self._l10n_at_param('at_sv_hbg_monatlich')) \
* 9.0 / 5.0
return min(bemessung / 5.0, deckel)
# ------------------------------------------------------------------
# Engine-Anbindung: Tarif-Teil ohne Sechstel, LNK-Pflicht, SV-Umlegung
# ------------------------------------------------------------------
def _l10n_at_bemessung_laufen(self, result_rules=None):
basis = super()._l10n_at_bemessung_laufen(result_rules)
# § 67 Abs 8 lit b: 4/5 tarifisch im Zuflussmonat; die Regel
# selbst ist neutral und erhöht brutto('laufend') nicht → kein
# Sechstel-Effekt (Abs 2 nicht anzuwenden).
return basis + self._at_fuenftel_tarif_basis(result_rules)
def _at_lnk_basis(self, steuer, result_rules=None):
"""Upstream-Überschreibung (Muster account_posting_private;
Vorlage payslip_private._at_lnk_basis — bei Änderungen dort
gegen diesen Pin neu abgleichen): Kündigungsentschädigungen und
Vergleichssummen sind Arbeitslöhne iSd § 41 Abs 3 FLAG und
unterliegen NICHT den Abs-4-Ausschlüssen (lit b nennt nur
§ 67 Abs 3 und 6 — der lit-a-BV-Teil ist ebenfalls nicht
ausgenommen) — sie sind VOR der Freibetragsstaffel in die
LNK-Basis aufzunehmen."""
self.ensure_one()
basis = (self._l10n_at_brutto('laufend', result_rules)
+ 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_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:
linie = self.line_ids.filtered(
lambda l: l.code == rule.code)
basis -= sum(linie.mapped('total'))
basis += self._at_fuenftel_total(result_rules)
# BV-Anwartschafts-Teil: begünstigter ≤-Deckel-Teil hier, der
# Überhang läuft bereits über _at_fuenftel_total — zusammen
# die volle Vergleichssumme, LNK-pflichtig (lb-end-20).
basis += self._at_vergleich_bv_beguenstigt(result_rules)
basis = max(basis, 0.0)
if steuer in ('flaf', 'dz'):
version = self.version_id
if (version.l10n_at_lnkp_frei_ab
and self.date_from >= version.l10n_at_lnkp_frei_ab):
return 0.0
freibetrag = self.env['hr.rule.parameter']._get_parameter_from_code(
'at_lnk_freibetrag', self.date_to, raise_if_not_found=False) or {}
grenze = float(freibetrag.get('grenze', 0.0))
if basis <= 0.0:
return 0.0
if basis <= grenze:
basis -= float(freibetrag.get('freibetrag', 0.0))
return max(basis, 0.0)
def _l10n_at_sv_basis_laufen_monate(self, result_rules=None):
"""Upstream-Überschreibung (Muster account_posting_private;
Vorlage l10n_at_hr_payroll.sozialversicherung — bei Odoo- oder
Kern-Upgrades gegen diesen Pin neu abgleichen): identischer
Ablauf wie der Kern, zusätzlich werden Regeln mit AT-SV-
Verlängerung tageproportional auf die Verlängerungsmonate
gelegt. Der Standardanteil jedes Monats bleibt auf die volle
monatliche HBG gedeckelt, der Verlängerungsanteil auf den
Tageswert (monatliche HBG × Verlängerungstage/30, lb-url-15);
die Fenster laufen sequenziell (Abgang zuerst, UEL danach)."""
self.ensure_one()
gruppen = {}
verlaengerung = {}
tage_pro_monat = {}
for rule in self.struct_id.rule_ids:
if self._l10n_at_sv_art(rule) != 'laufend':
continue
werte = None
if result_rules is not None:
if rule.code in result_rules:
werte = result_rules[rule.code]
else:
linie = self.line_ids.filtered(
lambda l: l.code == rule.code)
if linie:
werte = {'total': sum(linie.mapped('total'))}
if not werte:
continue
if rule.l10n_at_sv_verlaengerung != 'keine':
for monat, tage, anteil in \
self._at_end_rule_verlaengerung(rule, result_rules):
verlaengerung[monat] = verlaengerung.get(monat, 0.0) \
+ werte['total'] * anteil
tage_pro_monat[monat] = \
tage_pro_monat.get(monat, 0) + tage
continue
monat = (rule.l10n_at_beitragsmonat or self.date_from) \
.replace(day=1)
gruppen[monat] = gruppen.get(monat, 0.0) + werte['total']
standard = self.date_from.replace(day=1)
if standard in gruppen:
gruppen[standard] -= self._l10n_at_sv49_freie_laufen(
result_rules)
ergebnis = []
for monat in sorted(set(gruppen) | set(verlaengerung)):
hbg = float(self._l10n_at_param('at_sv_hbg_monatlich', monat))
basis = min(max(gruppen.get(monat, 0.0), 0.0), hbg)
if monat in verlaengerung:
tageswert = hbg * tage_pro_monat[monat] / 30.0
basis += min(max(verlaengerung[monat], 0.0), tageswert)
ergebnis.append((monat, basis))
return ergebnis