# Part of the odoo-at-payroll project. License: LGPL-3. """EFZG/Teilentgelt (AP4) — Entgeltfortzahlung im Krankenstand. Rechtsgrundlagen (verifiziert 2026-09-12): - **AngG § 8 Abs 1/2/2a** und **EFZG § 2 Abs 1/3, § 2 Abs 5** (Gleichlauf seit 1. 7. 2018, BGBl I 153/2017; EFZG RIS GesNr 10008308, AngG lokal — Stufen 6/8/10/12 Wochen je vollendeten Dienstjahren <1 / 1-14 / 15-24 / ≥25, zuzüglich je 4 Wochen halbes Entgelt; wiederholte Verhinderungen im selben Arbeitsjahr zusammengerechnet; AU/BK-Sonderkontingent 8/10 Wochen ohne Halbsatz, je Anlassfall, unabhängig vom Krankheits-Topf). - **lb-krs-08/02/07** (Wissensbasis, Layer 2): Arbeitsjahrprinzip, Kalendertage als Zähleinheit, Stufensprung im laufenden Stand verlängert; SV: volle EFZ beitragspflichtig, EFZ < 50 % beitragsfrei (hier: Halbsatz = 50 % → pflichtig); Lohnsteuer: laufender Bezug; **BMSVG § 7 Abs 3**: während des Krankengeldbezugs (Halb- und ÖGK-Phase) fiktive BG = 50 % des Entgelts des Kalendermonats vor dem Versicherungsfall, SZ ausgenommen. - Krankengeld selbst zahlt die ÖGK (läuft nicht durch den Dienstgeber-Payslip; lb-krs-02) — der Payroll-Anteil ist die Entgelt-Reduktion des Dienstgebers, die fiktive BV-Grundlage und später die Arbeits-/Entgeltbestätigung (AP8). - „Krankengeldumlage § 62 ASVG" ist eine Falsch-Prämise (RECHTSQUELLEN-Privat 2026-09-10) — kein Umlage-Baustein. Zähleinheiten: Kontingente in **Kalendertagen** (lb-krs-08: Praxis), bezogen aus den validierten Abwesenheiten (hr.leave der AT-Urlaubsarten Krankenstand/Arbeitsunfall); die **Entgelt-Minderung** je Arbeitstag aus dem Arbeitseintragssatz (Tagesatz = laufende Monatsbasis ÷ Arbeitstage des Monats — Entgeltausfallsprinzip). Feiertage im Krankenstand werden vom Work-Entry-Modell automatisch nicht als Krankenstandstage erzeugt (Kalender) — die 100 %- Feiertagsentgelt-Regel lb-krs-08 bleibt damit gewahrt. """ from datetime import date, timedelta from odoo import fields, models # WET-Codes der Abwesenheits-Töpfe (AP5-Seed). _KRANK_WET = 'ATP_KRANK' _AU_WET = 'ATP_AU' class HrVersion(models.Model): _inherit = 'hr.version' def _l10n_at_efz_arbeitsjahr_start(self, stichtag): """Beginn des Arbeitsjahres, das den Stichtag enthält (Eintrittsdatum-Rhythmus, lb-krs-08). None ohne Vertragsbeginn.""" self.ensure_one() beginn = self.contract_date_start if not beginn: return None jahre = beginn.year while True: aj_start = date(jahre, beginn.month, beginn.day) \ if not (beginn.month == 2 and beginn.day == 29) \ else date(jahre, 3, 1) aj_naechstes = date(jahre + 1, aj_start.month, aj_start.day) if aj_start <= stichtag < aj_naechstes: return aj_start jahre += 1 if jahre > beginn.year + 80: return None def _l10n_at_efz_phasen(self, aj_start, endtag, stichtag=None): """Komplette EFZ-Phasen-Karte des Arbeitsjahres über alle Krank-/AU-Kalendertage bis ``endtag``: {datum: 'voll'/'halb'/ 'oegk'} plus Kontext-Parameter — gemeinsame Basis der Payslip-Auswertung (AP4) und der Arbeits- und Entgeltbestätigung (AP8). Stufen je Tag (Stufensprung verlängert, lb-krs-08), Krank-/AU-Topf getrennt (AU ohne Halbsatz, § 8 Abs 2a AngG / § 2 Abs 5 EFZG).""" self.ensure_one() stichtag = stichtag or fields.Date.context_today(self) params = self.env['hr.rule.parameter']._get_parameter_from_code( 'at_efz_stufen', stichtag, raise_if_not_found=False) or {} stufen = sorted( (int(jahre), int(wochen)) for jahre, wochen in params.get('stufen', [])) halb_wochen = int(params.get('halb_wochen', 4)) au_wochen = int(params.get('au_wochen', 8)) au2_wochen = int(params.get('au2_wochen', 10)) au_ab_jahre = int(params.get('au_ab_jahre', 15)) typen = self.env['hr.leave.type'].search([ ('work_entry_type_id.code', 'in', (_KRANK_WET, _AU_WET))]) leaves = self.env['hr.leave'].search([ ('employee_id', '=', self.employee_id.id), ('holiday_status_id', 'in', typen.ids), ('state', 'in', ('validate', 'validate1')), ]) krank_tage, au_tage = {}, {} for leave in leaves: if not (leave.request_date_from and leave.request_date_to): continue tag = leave.request_date_from while tag <= leave.request_date_to: if aj_start <= tag <= endtag: topf = au_tage if leave.holiday_status_id \ .work_entry_type_id.code == _AU_WET else krank_tage topf[tag] = True tag += timedelta(days=1) beginn = self.contract_date_start def stufe_fuer(dienstjahre, tag_ist_au): wochen = stufen[0][1] if stufen else 6 for jahre, stufe in stufen: if dienstjahre >= jahre: wochen = stufe if tag_ist_au: wochen = au2_wochen if dienstjahre >= au_ab_jahre \ else au_wochen return wochen phasen = {} verbraucht_krank_voll = verbraucht_krank_halb = 0 verbraucht_au = 0 for tag in sorted(set(krank_tage) | set(au_tage)): dienstjahre = (tag.year - beginn.year - ((tag.month, tag.day) < (beginn.month, beginn.day))) if tag in au_tage: if verbraucht_au < stufe_fuer(dienstjahre, True) * 7: phasen[tag] = 'voll' else: phasen[tag] = 'oegk' verbraucht_au += 1 continue wochen = stufe_fuer(dienstjahre, False) if verbraucht_krank_voll < wochen * 7: phasen[tag] = 'voll' verbraucht_krank_voll += 1 elif verbraucht_krank_halb < halb_wochen * 7: phasen[tag] = 'halb' verbraucht_krank_halb += 1 else: phasen[tag] = 'oegk' return { 'phasen': phasen, 'stufen': stufen, 'halb_wochen': halb_wochen, 'au_wochen': au_wochen, 'au2_wochen': au2_wochen, 'au_ab_jahre': au_ab_jahre, } def _l10n_at_efz_stufe_am(self, stichtag, ist_au=False): """EFZ-Stufen-Wochen am Stichtag (vollendete Dienstjahre).""" self.ensure_one() aj_start = self._l10n_at_efz_arbeitsjahr_start(stichtag) if not aj_start: return None kontext = self._l10n_at_efz_phasen(aj_start, stichtag) beginn = self.contract_date_start dienstjahre = (stichtag.year - beginn.year - ((stichtag.month, stichtag.day) < (beginn.month, beginn.day))) stufen = kontext['stufen'] wochen = stufen[0][1] if stufen else 6 for jahre, stufe in stufen: if dienstjahre >= jahre: wochen = stufe if ist_au: wochen = kontext['au2_wochen'] \ if dienstjahre >= kontext['au_ab_jahre'] \ else kontext['au_wochen'] return wochen class HrPayslip(models.Model): _inherit = 'hr.payslip' l10n_at_efz_info = fields.Char( string='AT EFZ-Status (Krankenstand)', compute='_compute_efz_info', help='Phasenstatus des Krankenstands im Abrechnungszeitraum: ' 'volle/halbe Entgeltfortzahlung, ÖGK-Krankengeld-Phase, ' 'Kontingente und Stufe (AngG § 8 / EFZG § 2).') def _compute_efz_info(self): for slip in self: efz = slip._at_efz_status() if not efz: slip.l10n_at_efz_info = '' continue slip.l10n_at_efz_info = ( 'Stufe %d (%d+%d Wo) · voll %d AT · halb %d AT · ' 'ÖGK %d AT' % ( efz['stufe_wochen'], efz['stufe_wochen'], efz['halb_wochen'], len(efz['voll_arbeitstage']), len(efz['halb_arbeitstage']), len(efz['oegk_arbeitstage']))) # ------------------------------------------------------------------ # EFZ-Phasenbestimmung (Phasen-Karte: hr.version, AP8-Refaktur) # ------------------------------------------------------------------ def _at_efz_status(self): """EFZ-Phasen je Abrechnungsperiode: {'stufe_wochen', 'halb_wochen', 'voll_arbeitstage', 'halb_arbeitstage', 'oegk_arbeitstage', 'oegk_kalendertage', 'episode_beginn'} — None ohne Krankenstand/AU im Arbeitsjahr. Die Phasen-Karte (Kalendertage, Stufen je Tag, Krank-/AU-Topf getrennt, § 8 Abs 2a AngG / § 2 Abs 5 EFZG) liefert ``hr.version._l10n_at_efz_phasen`` — gemeinsame Basis mit der Arbeits- und Entgeltbestätigung (AP8).""" self.ensure_one() version = self.version_id aj_start = version._l10n_at_efz_arbeitsjahr_start(self.date_from) if not aj_start: return None endtag = max(self.date_to, aj_start) kontext = version._l10n_at_efz_phasen(aj_start, endtag, stichtag=self.date_from) phasen = kontext['phasen'] if not phasen: return None halb_wochen = kontext['halb_wochen'] # Arbeitstage je Phase im Abrechnungszeitraum (Work Entries) eintraege = self.env['hr.work.entry'].search([ ('employee_id', '=', self.employee_id.id), ('date', '>=', self.date_from), ('date', '<=', self.date_to), ('state', '!=', 'cancelled'), ('work_entry_type_id.code', 'in', (_KRANK_WET, _AU_WET)), ]) voll, halb, oegk = [], [], [] for eintrag in eintraege: phase = phasen.get(eintrag.date) if phase == 'voll': voll.append(eintrag.date) elif phase == 'halb': halb.append(eintrag.date) elif phase == 'oegk': oegk.append(eintrag.date) # Episode-Beginn für die fiktive BV (§ 7 Abs 3 BMSVG): # frühester zusammenhängender Verhinderungsblock (Lücke = neue # Episode), dessen Phase die Halb-/ÖGK-Phase erreicht. episode_beginn = None for tag in sorted(phasen): if phasen[tag] in ('halb', 'oegk'): block = tag while (block - timedelta(days=1)) in phasen: block = block - timedelta(days=1) episode_beginn = block break return { 'stufe_wochen': version._l10n_at_efz_stufe_am(self.date_from), 'halb_wochen': halb_wochen, 'voll_arbeitstage': sorted(set(voll)), 'halb_arbeitstage': sorted(set(halb)), 'oegk_arbeitstage': sorted(set(oegk)), # Kalendertage der Phasen IM ABRECHNUNGSZEITRAUM — Basis # der fiktiven-BV-Aliquote (monatsscharf). 'oegk_kalendertage': sorted( tag for tag in phasen if self.date_from <= tag <= self.date_to and phasen[tag] == 'oegk'), 'halb_kalendertage': sorted( tag for tag in phasen if self.date_from <= tag <= self.date_to and phasen[tag] == 'halb'), 'episode_beginn': episode_beginn, } def _at_efz_tagesatz(self, result_rules=None): """Voller Tages-Entgeltsatz des Monats (Entgeltausfalls- prinzip): laufende Monatsbasis ohne die eigenen Teilentgelt-Reduktionen ÷ Arbeitstage des Kalendermonats. Geteilter Helper von EFZG (TEIL_ENTGELT_HALB, KRANKENTGELT_PHASE) und Mutterschutz (MSCH_ENTGELT, AP7).""" self.ensure_one() basis = self._l10n_at_sv_basis_roh(result_rules) for code in ('TEIL_ENTGELT_HALB', 'KRANKENTGELT_PHASE', 'MSCH_ENTGELT', 'KARENZ_ENTGELT'): if result_rules is not None and code in result_rules \ and result_rules[code]['total']: basis -= result_rules[code]['total'] arbeitstage = self._at_arbeitstage(self.date_from, self.date_to) return basis / max(arbeitstage, 1) def _at_bv_fiktiv_krankengeld(self): """Fiktive BV-Grundlage während des Krankengeldbezugs (§ 7 Abs 3 BMSVG, lb-krs-07): 50 % des Entgelts des Kalendermonats vor dem Versicherungsfall (SZ ausgenommen), aliquot nach Kalendertagen des Bezugs (Halb- + ÖGK-Phase, 30-Tage-Konvention). Bezug = letzter validierter Lauf vor Episode-Beginn; ohne solchen: der laufende Monat (kurze Dienstverhältnisse, lb-krs-02).""" self.ensure_one() efz = self._at_efz_status() if not efz or not (efz['halb_kalendertage'] or efz['oegk_kalendertage']): return 0.0 tage = len(efz['halb_kalendertage']) \ + len(efz['oegk_kalendertage']) vormonat = None if efz['episode_beginn']: vormonat = self.env['hr.payslip'].search([ ('employee_id', '=', self.employee_id.id), ('state', 'in', ('validated', 'paid')), ('date_to', '<', efz['episode_beginn']), ], order='date_from desc', limit=1) basis_monat = vormonat._l10n_at_sv_basis_roh() if vormonat \ else self._l10n_at_sv_basis_roh() return 0.5 * basis_monat * tage / 30.0