# 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 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).""" 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