850e99df20
Close the premium-at-termination gap of the end-of-employment cycle: an aliquote periodic premium (annual, sales, bonus, 15th payment) whose entitlement period outlasts the termination is covered by § 16 AngG — the pro-rata amount follows the ratio of the elapsed service to the entitlement period, is due for employees regardless of the termination ground (loss clauses void, 9 ObA 82/13v), while performance-linked premiums for workers may validly hinge on non-fault grounds and premiums tied to a specific service (e.g. Bilanzgeld) are not aliquote without the service (lb-end-18). Two routes: PRAM_END (bemessung sonstig) covers premiums paid with the end-of-employment run — a special payment inside the annual sixth at the flat 6 % rate with the 620 EUR free amount and 2 615 EUR free threshold checked by the core, SV as special-payment contributions (KV/PV/AV without AK/WF) against the monthly HBG and the annual SZ cap, fully payroll-levy liable and BV-contributory at the uncapped 1.53 % rate (§ 6 Abs 1 iVm Abs 5 BMSVG). PRAM_END_TARIF (bemessung laufend plus the core l10n_at_s67_abs6_ueberhang flag) covers premiums due at a pre-agreed maturity because their amount only becomes determinable after the entitlement period ends (e.g. balance-sheet premiums): taxed like running pay with the payment-month tariff without touching the annual sixth (§ 67 Abs 10, not a § 67 Abs 8 lit c case per 98/14/0009 = ARD 5483/14/2004) — the SV contribution month rolls back into the termination year via a new payslip field consumed by an extended branch of the existing SV-basis-by-Beitragsmonat override (historical caps and rates, E-MVB 044-01-00-006), while the LSt stays in the payment month. The wizard section previews the § 16 pro-rata amount from the full premium, entitlement period and elapsed service, keeps the payable amount confirmable (KB agreement), requires the service/claim-basis confirmation, enforces the contribution month for the deferred case, and lands the memo on the payslip for the separate Lohnzettel workflow (LStR Rz 911a); the existing manual PRAM rule is untouched. The account matrix posts both rules to 6230. Verified against the original statute text in .ris/ (AngG § 16, BMSVG § 6 Abs 5) and the knowledge-base source lb-end-18; the full SZ roll-forward (storno plus recomputation against the termination year's SZ annual cap) and the mBGM storno/re-transmission remain open in the GP7 payroll-reporting track, and the BV attribution of the deferred premium remains in the payment month as a documented approximation. Version 19.0.16.0.0 (private)
656 lines
32 KiB
Python
656 lines
32 KiB
Python
# 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).')
|
||
l10n_at_pram_nachweis = fields.Text(
|
||
string='AT Prämie bei Beendigung — Nachweis/Begründung', copy=False,
|
||
help='Anspruchsgrundlage (KV-/Vertragsvereinbarung, '
|
||
'Anspruchszeitraum), §-16-Aliquotierung, '
|
||
'Leistungsverknüpfung und Fälligkeitsvereinbarung '
|
||
'dokumentieren (lb-end-18).')
|
||
l10n_at_pram_beitragsmonat = fields.Date(
|
||
string='AT Prämie (§ 67 Abs 10) — SV-Beitragsmonat der Rollung',
|
||
copy=False,
|
||
help='Beitragsmonat, dem die später fällige anteilige Prämie '
|
||
'sv-rechtlich zuzuordnen ist — der Zeitraum, in dem die '
|
||
'Leistungen erbracht wurden (E-MVB 044-01-00-006, lb-end-18). '
|
||
'Leer = Zuflussmonat.')
|
||
|
||
# ------------------------------------------------------------------
|
||
# 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 7−N
|
||
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).
|
||
AP14-B: die später fällige Prämie (PRAM_END_TARIF) rollt über
|
||
l10n_at_pram_beitragsmonat in den Beitragsmonat des
|
||
Austrittsjahres (E-MVB 044, lb-end-18); die LSt bleibt im
|
||
Zuflussmonat (§ 67 Abs 10)."""
|
||
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
|
||
if rule.code == 'PRAM_END_TARIF' \
|
||
and self.l10n_at_pram_beitragsmonat:
|
||
monat = self.l10n_at_pram_beitragsmonat.replace(day=1)
|
||
else:
|
||
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 |