# Part of the odoo-at-payroll project. License: LGPL-3. """AP14-A: Endabrechnungs-Wizard — sammelt die offenen Posten der Beendigungs-Abrechnung (Urlaubsersatzleistung, Abgangsentschädigung, Zeitguthaben/-schulden, offene Vorschüsse) und legt den Endabrechnungs-Lauf mit vorbefüllten Eingaben an. Reiner Sammel-/Anlageprozess; die BV-Anwartschaft läuft über den eigenen BV-Wizard (GP5), die Pfändung über die AP11-Maschine.""" from datetime import timedelta from dateutil.relativedelta import relativedelta from odoo import _, api, fields, models from odoo.exceptions import UserError class L10nAtEndabrechnungWizard(models.TransientModel): _name = 'l10n.at.payroll.endabrechnung.wizard' _description = 'Endabrechnung (Beendigung des Dienstverhältnisses)' employee_id = fields.Many2one( 'hr.employee', string='Dienstnehmer:in', required=True) austrittsdatum = fields.Date( string='Austritt (Ende des Arbeitsverhältnisses)', required=True) departure_reason_id = fields.Many2one( 'hr.departure.reason', string='Abgangsgrund', required=True, domain="['|', ('country_id', '=', False), ('country_id.code', '=', 'AT')]", help='Odoo-Abgangsgrund des Beschäftigtendatensatzes; die ' 'Abfertigungsberechtigung wird zusätzlich ausdrücklich bestätigt.') uel_tage = fields.Float( string='Offene Urlaubstage (Arbeitstage)', help='Resturlaub früherer Urlaubsjahre zur Gänze plus aliquoter ' 'Anspruch des laufenden Urlaubsjahres abzüglich verbrauchter ' 'Tage, ohne Rundung (§ 10 UrlG, lb-url-16). Für ' 'unberechtigten vorzeitigen Austritt sind die Tage der 5. ' 'und 6. Urlaubswoche des laufenden Jahres herauszurechnen.') abgang_betrag = fields.Monetary( string='Abgangsentschädigung (Brutto, § 67 Abs 8 lit b EStG)', currency_field='currency_id', help='Entschädigung für den Verzicht auf die Arbeitsleistung ' 'während der (fiktiven) Kündigungsfrist — LSt: Fünftelregelung; ' 'SV: Umlegung auf den Kündigungsfrist-Zeitraum (§ 11 Abs 2 ' 'ASVG); voll lohnnebenkostenpflichtig.') abgang_ffrist_tage = fields.Float( string='Kündigungsfrist (Kalendertage)', help='Kalendertage der (fiktiven) Kündigungsfrist, auf die die ' 'pauschalierte Abgangsentschädigung sv-rechtlich umzulegen ' 'ist (§ 11 Abs 2 ASVG).') zeit_guthaben_stunden = fields.Float( string='Zeitguthaben an Normalarbeitszeit (Stunden)', help='Offenes Guthaben an Normalarbeitszeit aus durchrechenbarem ' 'Arbeitszeitmodell — Abgeltung mit 50 % Zuschlag (§ 19e ' 'Abs 2 AZG); bei unberechtigtem vorzeitigem Austritt kein ' 'Zuschlag.') zeit_schulden_stunden = fields.Float( string='Zeitschulden (Stunden)', help='Offene Zeitschulden (negative Zeitguthaben), abgerechnet ' 'mit der aktuellen Normalstunde (lb-end-08).') vorschuss_saldo = fields.Monetary( string='Offener Vorschuss-/Darlehenssaldo', currency_field='currency_id', help='Bei Beendigung zur Gänze fälliger, noch offener ' 'Vorschusssaldo (lb-end-06) — Rückverrechnung vom Netto ' 'nach SV und Lohnsteuer, ohne Abgabenberührung ' '(§ 293 Abs 3 EO).') abf_alt_aktiv = fields.Boolean(string='Gesetzliche Abfertigung Alt') abf_alt_anspruch_bestaetigt = fields.Boolean( string='Anspruch und Nachweise geprüft', help='Bestätigung, dass Alt-Anwartschaft, Beendigungsgrund, ' 'Dienstzeiten und Bemessungsgrundlage geprüft wurden.') 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='Anspruchsart', default='normal') abf_alt_dienstzeit_monate = fields.Integer( string='Nachgewiesene Abfertigungsdienstzeit (Monate)') abf_alt_monatsentgelt = fields.Monetary( string='Maßgebliches Monatsentgelt', currency_field='currency_id', help='Bestätigte arbeitsrechtliche Bemessungsgrundlage: letzter ' 'Monat einschließlich regelmäßiger Bezugsteile, Sachbezüge ' 'und aliquoter Sonderzahlungen.') abf_alt_laufender_arbeitslohn = fields.Monetary( string='Laufender Monatsarbeitslohn (§ 67 Abs 3)', currency_field='currency_id', help='Laufender Arbeitslohn für den steuerlichen Vervielfacher; ' 'aliquote Sonderzahlungen gehören hier nicht dazu.') abf_alt_monatslohnsteuer = fields.Monetary( string='Tarif-LSt des laufenden Monats', currency_field='currency_id', help='Die auf den maßgeblichen laufenden Monatsarbeitslohn ' 'entfallende tarifmäßige Lohnsteuer nach den persönlichen ' 'Verhältnissen. Bei steuerfreiem Monatslohn ausdrücklich 0 ' 'bestätigen.') abf_alt_mehrfach = fields.Float( string='Bestätigter Anspruchsfaktor', help='§-23-Staffel, gegebenenfalls halbiert/gedeckelt oder als ' 'eingefrorener beziehungsweise nachgewiesener Faktor.') abf_alt_nachweis = fields.Text( string='Nachweis/Begründung', help='Quelle der Dienstzeiten, Anspruchsprüfung, Alt-/Neu-Status ' 'und allfällige manuelle Abweichung dokumentieren.') currency_id = fields.Many2one( 'res.currency', default=lambda self: self.env.company.currency_id) uel_lfd_vorschau = fields.Monetary( string='UEL laufender Anteil (Vorschau)', currency_field='currency_id', compute='_compute_vorschau') uel_sz_vorschau = fields.Monetary( string='UEL Sonderzahlungsanteil (Vorschau)', currency_field='currency_id', compute='_compute_vorschau') sv_tage_vorschau = fields.Integer( string='SV-Verlängerungstage (§ 11 Abs 2)', compute='_compute_vorschau') abf_alt_betrag_vorschau = fields.Monetary( string='Abfertigung Alt brutto', currency_field='currency_id', compute='_compute_vorschau') abf_alt_steuer_vervielfachung_vorschau = fields.Monetary( string='LSt Vervielfachungsmethode', currency_field='currency_id', compute='_compute_vorschau') abf_alt_steuer_6_vorschau = fields.Monetary( string='LSt 6 %', currency_field='currency_id', compute='_compute_vorschau') abf_alt_lohnsteuer_vorschau = fields.Monetary( string='Angewendete günstigere LSt', currency_field='currency_id', compute='_compute_vorschau') abf_alt_netto_vorschau = fields.Monetary( string='Abfertigung Alt netto', currency_field='currency_id', compute='_compute_vorschau') @api.onchange('employee_id') def _onchange_employee(self): for wizard in self: if not wizard.employee_id: continue version = wizard.employee_id.version_id wizard.austrittsdatum = ( version.contract_date_end or fields.Date.today()) wizard.departure_reason_id = version.departure_reason_id wizard._at_abf_alt_vorschlag_setzen() @api.onchange('austrittsdatum', 'abf_alt_anspruchsart', 'abf_alt_dienstzeit_monate') def _onchange_abf_alt(self): for wizard in self: wizard._at_abf_alt_vorschlag_setzen() def _at_abf_alt_vorschlag_setzen(self): self.ensure_one() if not self.employee_id or not self.austrittsdatum: return version = self.employee_id.version_id start = version.contract_date_start if start and not self.abf_alt_dienstzeit_monate: dauer = relativedelta( self.austrittsdatum + timedelta(days=1), start) self.abf_alt_dienstzeit_monate = dauer.years * 12 + dauer.months slip = self._vorschau_payslip() if not self.abf_alt_laufender_arbeitslohn: self.abf_alt_laufender_arbeitslohn = slip._at_kv_gehalt_basis() if not self.abf_alt_monatsentgelt: self.abf_alt_monatsentgelt = ( self.abf_alt_laufender_arbeitslohn + 2.0 * slip._at_sz_basis(self.austrittsdatum) / 12.0) vorgeschlagen = slip._at_abf_alt_mehrfach_vorschlag( self.abf_alt_dienstzeit_monate, self.abf_alt_anspruchsart) if vorgeschlagen or not self.abf_alt_mehrfach: self.abf_alt_mehrfach = vorgeschlagen @api.depends('employee_id', 'austrittsdatum', 'uel_tage', 'abf_alt_aktiv', 'abf_alt_monatsentgelt', 'abf_alt_laufender_arbeitslohn', 'abf_alt_monatslohnsteuer', 'abf_alt_mehrfach', 'abf_alt_dienstzeit_monate', 'abf_alt_anspruchsart') def _compute_vorschau(self): for wizard in self: wizard.uel_lfd_vorschau = 0.0 wizard.uel_sz_vorschau = 0.0 wizard.sv_tage_vorschau = 0 wizard.abf_alt_betrag_vorschau = 0.0 wizard.abf_alt_steuer_vervielfachung_vorschau = 0.0 wizard.abf_alt_steuer_6_vorschau = 0.0 wizard.abf_alt_lohnsteuer_vorschau = 0.0 wizard.abf_alt_netto_vorschau = 0.0 if not wizard.employee_id or not wizard.austrittsdatum: continue slip = wizard._vorschau_payslip() wizard.uel_lfd_vorschau = slip._at_uel_lfd() wizard.uel_sz_vorschau = slip._at_uel_sz() wizard.sv_tage_vorschau = slip._at_end_sv_tage( wizard.uel_tage) if wizard.abf_alt_aktiv: betrag = slip._at_abf_alt_betrag() steuer_vervielfachung = \ slip._at_abf_alt_steuer_vervielfachung() steuer_6 = slip._at_abf_alt_steuer_6() steuer = min(steuer_vervielfachung, steuer_6) wizard.abf_alt_betrag_vorschau = betrag wizard.abf_alt_steuer_vervielfachung_vorschau = \ steuer_vervielfachung wizard.abf_alt_steuer_6_vorschau = steuer_6 wizard.abf_alt_lohnsteuer_vorschau = steuer wizard.abf_alt_netto_vorschau = betrag - steuer def _vorschau_payslip(self): """Flüchtiger Payslip-Kontext für die Vorschau-Berechnung (kein Speichern): trägt Austrittsmonat, Struktur und die Eingaben.""" self.ensure_one() erster = self.austrittsdatum.replace(day=1) letzter = (erster + timedelta(days=32)).replace(day=1) \ - timedelta(days=1) struct = self.employee_id.version_id.structure_id if not struct: struct = self.env.ref( 'l10n_at_hr_payroll_private.structure_at_privat') slip = self.env['hr.payslip'].new({ 'employee_id': self.employee_id.id, 'struct_id': struct.id, 'date_from': erster, 'date_to': letzter, 'l10n_at_abf_alt_anspruchsart': self.abf_alt_anspruchsart, 'l10n_at_abf_alt_dienstzeit_monate': self.abf_alt_dienstzeit_monate, 'l10n_at_abf_alt_monatsentgelt': self.abf_alt_monatsentgelt, 'l10n_at_abf_alt_laufender_arbeitslohn': self.abf_alt_laufender_arbeitslohn, 'l10n_at_abf_alt_monatslohnsteuer': self.abf_alt_monatslohnsteuer, 'l10n_at_abf_alt_mehrfach': self.abf_alt_mehrfach, 'l10n_at_abf_alt_nachweis': self.abf_alt_nachweis, }) eingaben = [ ('ATP_UEL_TAGE', self.uel_tage), ('ATP_ABF_ALT', self.abf_alt_monatsentgelt * self.abf_alt_mehrfach if self.abf_alt_aktiv else 0.0), ] slip.input_line_ids = [ (0, 0, { 'input_type_id': self.env['hr.payslip.input.type'] .search([('code', '=', code)], limit=1).id, 'amount': betrag, }) for code, betrag in eingaben if betrag ] return slip def _eingaben(self): self.ensure_one() return [ ('ATP_UEL_TAGE', self.uel_tage), ('ATP_ABGANG', self.abgang_betrag), ('ATP_ABGANG_FFRIST_TAGE', self.abgang_ffrist_tage), ('ATP_ZEIT_GUTHABEN', self.zeit_guthaben_stunden), ('ATP_ZEIT_SCHULDEN', self.zeit_schulden_stunden), ('ATP_ABRECHNUNG_VORSCHUSS', self.vorschuss_saldo), ('ATP_ABF_ALT', self.abf_alt_monatsentgelt * self.abf_alt_mehrfach if self.abf_alt_aktiv else 0.0), ] def _abf_alt_validieren(self): self.ensure_one() if not self.abf_alt_aktiv: return if not self.abf_alt_anspruch_bestaetigt: raise UserError(_('Bitte Anspruch und Nachweise der ' 'Abfertigung Alt bestätigen.')) if self.employee_id.version_id.l10n_at_abfertigung_neu \ and self.abf_alt_anspruchsart != 'eingefroren': raise UserError(_( 'Das Dienstverhältnis ist als Abfertigung Neu markiert. ' 'Eine Alt-Abfertigung ist nur als nachgewiesener ' 'eingefrorener Anspruch zulässig.')) if self.abf_alt_anspruchsart == 'eltern' \ and self.abf_alt_dienstzeit_monate < 60: raise UserError(_( 'Ein Elternaustritt nach § 23a Abs. 3/4 AngG setzt ein ' 'ununterbrochenes Dienstverhältnis von fünf Jahren voraus.')) if (self.abf_alt_dienstzeit_monate <= 0 or self.abf_alt_monatsentgelt <= 0 or self.abf_alt_laufender_arbeitslohn <= 0 or self.abf_alt_monatslohnsteuer < 0 or self.abf_alt_mehrfach <= 0): raise UserError(_('Dienstzeit, Monatsentgelt, laufender ' 'Arbeitslohn und Anspruchsfaktor müssen ' 'größer als null sein; die Tarif-LSt darf ' 'nicht negativ sein.')) agrd = self.departure_reason_id.l10n_at_agrd if agrd == '02' and self.abf_alt_anspruchsart not in ( 'eltern', 'pension', 'manuell'): raise UserError(_( 'Eine gewöhnliche Arbeitnehmerkündigung begründet keinen ' 'Anspruch nach § 23 AngG. Bitte einen §-23a-Sonderfall ' 'wählen oder den Anspruch manuell nachweisen.')) if agrd == '06' and self.abf_alt_anspruchsart != 'manuell': raise UserError(_( 'Der Abgangsgrund „Fristlose Entlassung“ unterscheidet ' 'nicht zwischen verschuldeter und unverschuldeter ' 'Entlassung. Bitte den Anspruch manuell bestätigen und ' 'begründen.')) def action_payslip(self): """Endabrechnungs-Lauf im Austrittsmonat anlegen und die gesammelten offenen Posten als Eingaben vorbefüllen.""" self.ensure_one() self._abf_alt_validieren() erster = self.austrittsdatum.replace(day=1) letzter = (erster + timedelta(days=32)).replace(day=1) \ - timedelta(days=1) version = self.employee_id.version_id version.write({ 'contract_date_end': self.austrittsdatum, 'departure_reason_id': self.departure_reason_id.id, }) struct = version.structure_id \ or self.env.ref('l10n_at_hr_payroll_private.structure_at_privat') slip = self.env['hr.payslip'].create({ 'name': _('Endabrechnung %s') % self.employee_id.name, 'employee_id': self.employee_id.id, 'struct_id': struct.id, 'date_from': erster, 'date_to': letzter, 'l10n_at_abf_alt_anspruchsart': self.abf_alt_anspruchsart if self.abf_alt_aktiv else False, 'l10n_at_abf_alt_dienstzeit_monate': self.abf_alt_dienstzeit_monate if self.abf_alt_aktiv else 0, 'l10n_at_abf_alt_monatsentgelt': self.abf_alt_monatsentgelt if self.abf_alt_aktiv else 0.0, 'l10n_at_abf_alt_laufender_arbeitslohn': self.abf_alt_laufender_arbeitslohn if self.abf_alt_aktiv else 0.0, 'l10n_at_abf_alt_monatslohnsteuer': self.abf_alt_monatslohnsteuer if self.abf_alt_aktiv else 0.0, 'l10n_at_abf_alt_mehrfach': self.abf_alt_mehrfach if self.abf_alt_aktiv else 0.0, 'l10n_at_abf_alt_nachweis': self.abf_alt_nachweis if self.abf_alt_aktiv else False, }) typen = {code: self.env['hr.payslip.input.type'].search( [('code', '=', code)], limit=1) for code, _betrag in self._eingaben()} self.env['hr.payslip.input'].create([{ 'payslip_id': slip.id, 'input_type_id': typen[code].id, 'amount': betrag, } for code, betrag in self._eingaben() if betrag]) slip.compute_sheet() return { 'type': 'ir.actions.act_window', 'name': _('Endabrechnung'), 'res_model': 'hr.payslip', 'view_mode': 'form', 'res_id': slip.id, }