Files
odoo-at-payroll/addons/l10n_at_hr_payroll_private/wizard/endabrechnung.py
T
fegger 9add58fe83 [IMP] l10n_at_hr_payroll_private: add statutory old-system severance (AP15-A)
Pay the pre-2003 AngG severance from the end-of-employment run as its own neutral benefit line: free of SV (§ 49 Abs 3 Z 7 ASVG) and payroll levies, outside the annual sixth, and collected into the § 291d EO termination pot net of its own wage tax. Taxation follows the binding better-of rule of § 67 Abs 3 EStG: multiple-of-method (tariff wage tax on the running monthly wage × the statutory multiple of the payout) versus the flat 6 % rate, applied mandatorily in favour of the employee. The tariff wage tax is captured as a separately confirmed value instead of deriving it from gross pay through the § 62 engine. The wizard enforces § 23a Abs 3 AngG (parental exit needs five uninterrupted years), blocks BMSVG-new contracts except frozen claims and validates AGRD 02/06 against the claim type. The LNK basis subtracts § 67 Abs 3/6 rules only when they actually run in the tax base, the L16 generator reports LST_ABF in key 260 outside 210/220, and result_rules access in the garnishment paths avoids DefaultDictPayroll materialisation. Verified against the original statute texts in .ris/ (AngG §§ 23/23a, EStG § 67 Abs 3).

Version 19.0.13.0.0 (private), 19.0.9.0.0 (core)
2026-09-15 11:01:55 +02:00

354 lines
17 KiB
Python

# 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,
}