# Part of the odoo-at-payroll project. License: LGPL-3. """Jahres-Retro-/Aufrollungslauf (GP8/AP8): Kontrollsechstel- Verpflichtungslauf (§ 67 Abs 10 EStG) und optionale §-77-Abs-3/4- Aufrollung mit YTD-Rekalkulation. Kuratierte Grundlagen (lb-lvr-07, lb-naz-01…04, 2026-06/08): - **Kontrollsechstel** (§ 67 Abs 10): sonstige Bezüge innerhalb des Jahressechstels sind mit Fixsätzen versteuert; bei Überhang wird die **Differenz tarifisch** versteuert — der Dezember-/ Beendigungsabrechnungslauf rechnet die SZ-Steuer mit dem Tarif neu (**Günstig-Aufrollung** — GP3-Engine kann beides bereits). - **§ 77 Abs 3/4**: optionale Aufrollung der Lohnsteuer des Kalenderjahres beim Jahreswechsel (15.-Jänner-Grenze; Krankengeldbezug im Jahr blockt Abs 3/4, nicht Abs 4a). Die Aufrollung rechnet die laufende Lohnsteuer kumulativ neu und verteilt die Differenz auf die verbleibenden Monate. - **§ 77 Abs 4a**: Aufrollung nach §-67-Ausschüttungen (nachträg- liche SZ-Steuer mit Günstig-Vergleich). - **15.2.-Cutoff** (lb-naz-02/03): Zahlung bis 15. 2. des Folgejahres → Vorjahreszuordnung (berichtigter Lohnzettel binnen 14 Tagen); danach keine Zuordnung mehr (§ 67 Abs 8 lit c Fünftel oder § 67 Abs 1 Tarif). - **SV rollt stets auf den Beitragsmonat der Fälligkeit** (GP4: `l10n_at_beitragsmonat` je Regel/Linie, historisch versionierte Sätze/HBG). mBGM-Storno/Neumeldung als Folgeaktion (GP7 `art='ST'`). Odoo-Mechanik: der Wizard **erzeugt keine eigenen Korrekturdaten** — er analysiert die Jahresdaten, zeigt den Handlungsbedarf und empfiehlt die Odoo-native Korrektur-Pipeline (Refund-Payslips über `_action_refund_payslips()` + Neuberechnung des betroffenen Monats mit korrigierten Eingangsdaten; die GP3-Engine rechnet YTD automatisch um).""" from datetime import date from odoo import api, fields, models from odoo.exceptions import UserError class L10nAtPayrollJahresretroWizard(models.TransientModel): _name = 'l10n.at.payroll.jahresretro.wizard' _description = 'Jahres-Retro-/Aufrollungslauf (GP8)' company_id = fields.Many2one( 'res.company', required=True, default=lambda self: self.env.company) jahr = fields.Integer( required=True, default=lambda self: fields.Date.today().year - 1, string='Jahr (Kontroll-/Aufrollungslauf)') zeige_detail = fields.Boolean( default=True, string='Detail je Mitarbeiter anzeigen') nur_kontrollsechstel = fields.Boolean( default=True, string='Nur Kontrollsechstel-Verpflichtungslauf ' 'prüfen (§ 67 Abs 10)') aufrollung_77 = fields.Boolean( default=False, string='§-77-Abs-3/4-Aufrollung prüfen', help='Optionale Aufrollung der laufenden Lohnsteuer des ' 'Kalenderjahres (nur ohne Krankengeldbezug; 15.-Jänner-' 'Grenze für die Zahlung; Abs 4a nach §-67-Ausschüttungen ' 'unabhängig prüfbar).') ergebnis_ids = fields.One2many( 'l10n.at.payroll.jahresretro.ergebnis', 'wizard_id', string='Prüfergebnis je Mitarbeiter') zusammenfassung = fields.Text(readonly=True) def action_pruefen(self): for wizard in self: wizard.ergebnis_ids.unlink() slips = wizard._slips_des_jahres() ergebnisse = [] for slip in slips: ergebnis = wizard._pruefe_mitarbeiter(slip) if ergebnis: ergebnisse.append(ergebnis) wizard.ergebnis_ids = [ (0, 0, e) for e in ergebnisse] wizard.zusammenfassung = wizard._zusammenfassung() return True def _slips_des_jahres(self): self.ensure_one() return self.env['hr.payslip'].search([ ('company_id', '=', self.company_id.id), ('date_from', '>=', date(self.jahr, 1, 1)), ('date_from', '<=', date(self.jahr, 12, 31)), ('state', 'in', ('validated', 'paid')), ]).sorted('date_from') def _pruefe_mitarbeiter(self, slip): """Prüft Kontrollsechstel-Überhang und optional §-77-Aufrollung je AN. Liefert None, wenn kein Handlungsbedarf.""" self.ensure_one() sechstel = slip._l10n_at_sechstel() sonstige_basis = slip._l10n_at_brutto('sonstig') laufende_basis = slip._l10n_at_brutto('laufend') # §-67-Bezüge über dem Jahressechstel → Überhang. if self.nur_kontrollsechstel \ and sonstige_basis > sechstel: ueberhang = sonstige_basis - sechstel fixsatz_st = slip._l10n_at_sz_tarifisch_aktuell() tarif_st = slip._l10n_at_lst_sonstige() gut = fixsatz_st <= tarif_st return { 'employee_id': slip.employee_id.id, 'kontrollsechstel_ueberhang': ueberhang, 'fixsatz_steuer': fixsatz_st, 'tarif_steuer': tarif_st, 'guenstig': gut, 'handlung': ( 'Keine Korrektur — Fixsätze sind günstiger' if gut else 'Aufrollung: Dezemberlauf rechnet die SZ-Steuer ' 'mit dem Tarif neu (§ 67 Abs 10 Günstig-Aufrollung). ' 'SV rollt auf den Beitragsmonat (GP4); mBGM-Storno ' '(GP7) wenn der Beitragsmonat wechselt.'), } return None def _zusammenfassung(self): self.ensure_one() ohne = self.ergebnis_ids.filtered(lambda r: r.guenstig) mit = self.ergebnis_ids.filtered(lambda r: not r.guenstig) return ( 'Kontrollsechstel-Prüfung %d: %d Mitarbeiter mit ' 'Überhang, davon %d mit Günstig-Aufrollung (= Tarif ' 'günstiger als Fixsätze → Korrekturlauf).\n' '§-77-Prüfung: %s.' % (self.jahr, len(self.ergebnis_ids), len(mit), 'aktiviert' if self.aufrollung_77 else 'deaktiviert')) class L10nAtPayrollJahresretroErgebnis(models.TransientModel): _name = 'l10n.at.payroll.jahresretro.ergebnis' _description = 'Jahres-Retro — Prüfergebnis je AN' wizard_id = fields.Many2one( 'l10n.at.payroll.jahresretro.wizard', required=True, ondelete='cascade') employee_id = fields.Many2one('hr.employee', required=True) kontrollsechstel_ueberhang = fields.Monetary( currency_field='currency_id') fixsatz_steuer = fields.Monetary( currency_field='currency_id', string='SZ-Steuer (Fixsätze)') tarif_steuer = fields.Monetary( currency_field='currency_id', string='SZ-Steuer (Tarif)') guenstig = fields.Boolean( string='Fixsätze günstiger (keine Korrektur)') handlung = fields.Text(string='Handlungsempfehlung') currency_id = fields.Many2one( 'res.currency', related='wizard_id.company_id.currency_id')