# Part of the odoo-at-payroll project. License: LGPL-3. """Abfertigung Neu (BMSVG) — GP5: laufender BV-Beitrag (§ 6) mit EIGENER BV-Grundlage, Anwartschafts-Fortschreibung (§ 15) und Endabrechnung (§§ 14-17). Verifizierte Grundlagen (RECHTSQUELLEN-Privat Abschn. 8 ✅; KB lb-vor-03/lb-vor-06/lb-end-03, Stand 2026-08): - § 6 Abs 1: DG-Beitrag 1,53 vH des monatlichen Entgelts und der Sonderzahlungen an die Mitarbeitervorsorgekasse (Abfuhr über den KV-Träger, Fälligkeit 15. Folgemonat, Baubereich BUAK — GP7). - § 6 Abs 5 iVm § 49 ASVG: Bemessung ohne GFG und HBG → eigene, ungecappte BV-Grundlage. Bewusst KEIN Rückgriff auf die SV-SZ-Basis des Kerns: deren §-49-Abs-3-Z-7-Ausschluss für §-67-Abs-3/6-Bezüge (Abfertigung/Abfindung) ist dort nicht umgesetzt; die BV-Grundlage schließt diese Zeilen korrekt aus (Nutzer-Entscheidung 2026-09-11). - Geltungsbereich § 46/§ 1 Abs 2 Z 1: private Arbeitsverhältnisse ab 1. 1. 2003 (Flag am Vertrag); geringfügig Beschäftigte und Lehrlinge sind ERFASST — BVG läuft auch ohne SV-Hauptbeiträge. - Beitragsfreier erster Monat (§ 6 Abs 1), taggenau: Fenster = [Eintritt, Eintritt + 1 Monat), Beitragspflicht ab Monatsjubiläum (Eintritt 15. 06. → frei bis 14. 07., Pflicht ab 15. 07.; KB-Beispiel 4.500/30 × 17 = 2.550). Monatsende-Klippe (Eintritt 31. 01. → kein 31. 02.): Fenster bis Monatsultimo, Pflicht ab Folgemonatserstem (ABGB-Fristrechnung). AV ≤ 1 Monat → zur Gänze beitragsfrei (auch Urlaubsersatzleistung). Wiederzulassung binnen 12 Monaten → Beitragspflicht ab dem 1. TAG; Vor-AV beliebiger Dauer (auch < 1 Monat; OGH 9 ObA 30/16a) — schlägt auch die Kurz-AV-Freiheit des laufenden AV (Nutzer-Entscheidung 2026-09-11). - Sonderzahlungen: keine aliquote Kürzung um den beitragsfreien Monat; beitragsfrei nur, wenn ihr Beitragsmonat ins Fenster fällt. - §§ 14-17: Anwartschaft = gutgebrachte Beiträge + Erträge (KEINE Monatsentgelt-Staffel); Verfügungsausschluss bei AN-Kündigung, verschuldeter Entlassung, unberechtigtem vorzeitigem Austritt oder weniger als 36 Beitragsmonaten (alle Beitragszeiten zählen, daher manuelle Vor-Arbeitgeber-Monate am Mitarbeiter); Fälligkeit am Ende des zweitfolgenden Monats nach Geltendmachung (§ 16); fünf Verfügungsoptionen, ohne Erklärung binnen 6 Monaten Weiterveranlagung (§ 17). """ from datetime import timedelta from dateutil.relativedelta import relativedelta from odoo import fields, models class HrVersion(models.Model): _inherit = 'hr.version' l10n_at_abfertigung_neu = fields.Boolean( string='AT Abfertigung Neu (BMSVG)', default=True, groups="hr_payroll.group_hr_payroll_user", tracking=True, help='Dienstverhältnis unterliegt dem BMSVG (§ 46: AV ab ' '1. 1. 2003; § 1 Abs 2 Z 1: Länder/Gemeinden ausgenommen). ' 'Für Alt-Verhältnisse (§§ 23 f AngG) und § 47-Übertritte ' 'abwählen — dann läuft keine BVG-Regel. Geringfügig ' 'Beschäftigte und Lehrlinge sind erfasst.') l10n_at_bv_kasse_id = fields.Many2one( 'l10n.at.bv.kasse', string='AT Mitarbeitervorsorgekasse', index=True, groups="hr_payroll.group_hr_payroll_user", tracking=True, help='Gewählte BV-Kasse dieses Dienstverhältnisses (lb-vor-02); ' 'Anwartschaftskonto und §-17-Verfügungen laufen über diese ' 'Kasse.') class HrEmployee(models.Model): _inherit = 'hr.employee' l10n_at_abfertigung_neu = fields.Boolean( related="version_id.l10n_at_abfertigung_neu", inherited=True, readonly=False, groups="hr_payroll.group_hr_payroll_user") l10n_at_bv_kasse_id = fields.Many2one( related="version_id.l10n_at_bv_kasse_id", inherited=True, readonly=False, groups="hr_payroll.group_hr_payroll_user") l10n_at_bv_ertraege = fields.Monetary( string='AT BV-Erträge (Kassen-Auszug)', currency_field='currency_id', groups="hr_payroll.group_hr_payroll_user", help='Veranlagungserträge der Mitarbeitervorsorgekasse laut ' 'Kassen-Auszug (§ 15 BMSVG: Auszahlung der Anwartschaft = ' 'gutgebrachte Beiträge + Erträge) — manuell pflegen.') l10n_at_bv_monate_extern = fields.Integer( string='AT BV-Beitragsmonate Vor-Arbeitgeber', groups="hr_payroll.group_hr_payroll_user", help='Beitragsmonate aus Vor-Beschäftigungen (§ 14 Abs 2 BMSVG: ' 'alle Beitragszeiten über Arbeitgeber hinweg summierbar — ' '36-Monats-Kriterium) — laut Auszug der MV-Kasse pflegen.') def _at_bv_anwartschaft(self): """Anwartschafts-Fortschreibung (§ 15 BMSVG): gutgebrachte Beiträge aus validierten Läufen, je Beitragsmonat/Jahr gruppiert (Refund-Läufe netten über negative Linien; Monate zählen auch aus negativen Linien — der Monat ist belegt).""" self.ensure_one() monate = set() total = 0.0 jahre = {} slips = self.env['hr.payslip'].search([ ('employee_id', '=', self.id), ('state', 'not in', ('draft', 'cancel')), ]) for line in slips.line_ids.filtered( lambda l: l.code == 'BVG'): if not line.total: continue monat = (line.l10n_at_beitragsmonat or line.date_from).replace(day=1) monate.add(monat) total += line.total eintrag = jahre.setdefault( monat.year, {'monate': set(), 'summe': 0.0}) eintrag['monate'].add(monat) eintrag['summe'] += line.total return { 'monate': len(monate), 'beitraege': total, 'jahre': [(jahr, len(w['monate']), w['summe']) for jahr, w in sorted(jahre.items())], } class HrPayslip(models.Model): _inherit = 'hr.payslip' # ------------------------------------------------------------------ # Beitragsfreies Erstfenster (§ 6 Abs 1 BMSVG, taggenau) # ------------------------------------------------------------------ def _at_bv_fenster(self): """(pflichtig_ab, kurz_av) des Arbeitsverhältnisses: pflichtig_ab = erstes beitragspflichtiges Datum (None = Beitragspflicht ab dem 1. Tag bzw. ohne bewertbare Vertragsdaten); kurz_av = True → AV ≤ 1 Monat, zur Gänze beitragsfrei. Reihenfolge: Wiederzulassung schlägt alles, dann Kurz-AV-Freiheit, dann taggenaues Fenster.""" self.ensure_one() version = self.version_id start = version.contract_date_start if not start: # Ohne Vertragsdaten ist die Befreiung nicht bewertbar → # konservativ Vollpflicht. return None, False # Wiederzulassung binnen 12 Monaten: Beitragspflicht ab dem # 1. Tag; Vor-Dienstverhältnis beliebiger Dauer (auch # < 1 Monat) löst die Ausnahme aus. prior = self.env['hr.version'].search([ ('employee_id', '=', self.employee_id.id), ('id', '!=', version.id), ('contract_date_end', '!=', False), ('contract_date_end', '>=', start - relativedelta(months=12)), ('contract_date_end', '<', start), ], limit=1) if prior: return None, False # Monatsjubiläum, taggenau; Monatsende-Klippe: existiert der # Jubiläumstag im Folgemonat nicht (Eintritt 31. 01.), endet # das Fenster am Monatsultimo, Pflicht ab Folgemonatserstem. jubilaeum = start + relativedelta(months=1) if jubilaeum.day < start.day: jubilaeum += timedelta(days=1) end = version.contract_date_end if end and end < jubilaeum: # AV ≤ 1 Monat → zur Gänze beitragsfrei (lb-vor-03, auch # eine bloße Urlaubsersatzleistung). return None, True return jubilaeum, False # ------------------------------------------------------------------ # Eigene BV-Grundlage (§ 6 Abs 5 iVm § 49 ASVG — ohne GFG/HBG) # ------------------------------------------------------------------ def _at_bv_sonst_monate(self, result_rules=None): """[(Beitragsmonat, sonstige BV-Basis)]: sonstige Bezüge ohne §-67-Abs-3/6-Zeilen (Abfertigung/Abfindung sind nach § 49 Abs 3 Z 7 ASVG beitragsfrei, damit auch nicht BV- pflichtig), gruppiert nach Regel-Beitragsmonat mit voller Tagesgenauigkeit (Anspruchsprinzip, Muster _l10n_at_sv_basis_laufen_monate) — die Fenster-Prüfung braucht die Fälligkeit taggenau (SZ fällig 20. 07. ist bei Fenster- Ende 15. 07. pflichtig, der Monatsanfang wäre es nicht).""" self.ensure_one() gruppen = {} for rule in self.struct_id.rule_ids: if (rule.l10n_at_bemessung != 'sonstig' or rule.l10n_at_s67_abs != 'keine'): 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 monat = rule.l10n_at_beitragsmonat or self.date_from gruppen[monat] = gruppen.get(monat, 0.0) + werte['total'] return sorted(gruppen.items()) def _at_bv_basis_laufen(self, result_rules=None): """Laufende BV-Grundlage: §-49-bereinigtes Entgelt ohne HBG-Cap (§ 6 Abs 5), aliquotiert um das beitragsfreie Erstfenster — 30-Tage-Monat (KB-Beispiel 4.500/30 × 17 = 2.550); Teilmonate über die Lohnsteuertage, um Doppelabzüge zu vermeiden (das Entgelt ist für die Teilzeit bereits aliquotiert). AP16 (§ 6 Abs 4 BMSVG): während einer Arbeitszeit-Reduzierung mit gesetztem Feld l10n_at_bv_vor_entgelt (z. B. Pflegeteilzeit § 14d AVRAG) läuft der Beitrag auf dem Vor-Reduzierungs-Entgelt.""" self.ensure_one() pflichtig_ab, kurz_av = self._at_bv_fenster() if kurz_av: return 0.0 vor_entgelt = self.version_id.l10n_at_bv_vor_entgelt basis = self._l10n_at_sv_basis_roh(result_rules) if vor_entgelt: basis = max(basis, float(vor_entgelt)) if pflichtig_ab is None or pflichtig_ab <= self.date_from: # AP4: fiktive BG während des Krankengeldbezugs (§ 7 Abs 3 # BMSVG) und des Wochengeldbezugs (§ 7 Abs 4, AP7) — nur # in beitragspflichtigen Läufen (Erstfenster-Teilmonate: # dokumentierter Randfall). return basis + self._at_bv_fiktiv_krankengeld() \ + self._at_bv_fiktiv_wochengeld() if pflichtig_ab > self.date_to: # LZP liegt zur Gänze im beitragsfreien Erstfenster. return 0.0 tage = (min(self.date_to, self.version_id.contract_date_end or self.date_to) - pflichtig_ab).days + 1 if tage <= 0: return 0.0 return basis * tage / max(self._l10n_at_lohnsteuertage(), 1) def _at_bv_basis_sonst(self, result_rules=None): """BV-Grundlage sonstiger Bezüge (Sonderzahlungen & Co.): ungecappt (kein HBG-/Jahresdeckel, § 6 Abs 5) und ohne §-67-Abs-3/6-Zeilen; keine aliquote Kürzung um das freie Fenster — frei bleibt nur, was im Fenster fällig wird (Beitragsmonat im Fenster, lb-vor-03).""" self.ensure_one() pflichtig_ab, kurz_av = self._at_bv_fenster() if kurz_av: return 0.0 total = 0.0 for monat, basis in self._at_bv_sonst_monate(result_rules): if pflichtig_ab is not None and monat < pflichtig_ab: continue total += basis return total def _at_bv_beitrag(self, result_rules=None): """BV-Beitrag (§ 6 Abs 1 BMSVG): 1,53 % DG (at_sv_bv_dg) auf die eigene Grundlage laufend + sonstig; ATP_DG-Zeile ohne Nettoeffekt (FI-Zuordnung über hr_payroll_account, GP6). Läuft bewusst auch für geringfügig Beschäftigte und Lehrlinge (§ 46, § 1 Abs 2 Z 1; Jahreszahlung + 2,5 % Zinsenzuschlag der Geringfügigen = Zahlungsmodus GP7/GP8).""" self.ensure_one() if not self.version_id.l10n_at_abfertigung_neu: return 0.0 basis = (self._at_bv_basis_laufen(result_rules) + self._at_bv_basis_sonst(result_rules)) satz = float(self._l10n_at_param('at_sv_bv_dg')) return basis * satz / 100.0