Files
odoo-at-payroll/addons/l10n_at_hr_payroll_private/models/pendler.py
T
fegger 9ff977e0d4 [IMP] l10n_at_hr_payroll_private: add Pendlerpauschale, Pendlereuro and job ticket data (AP13-B)
Model commuter declarations as effective-dated L 34 records so monthly changes, L 34/Pendlerrechner evidence and historical payslip reproduction stay auditable. PP enters as an aliquoted § 66 freibetrag of the running tax base while PE joins the annual absetzbetraege, matching the cumulative engine's rollup. Ticket handling follows § 26 Z 5: employer ticket costs reduce PP pro rata (lit i), Werkverkehr is data-driven via the residual route and salary conversion does not block PP/PE. Also register the AP13-A tests that were accidentally left out of the previous commit.
2026-09-14 14:47:23 +02:00

118 lines
5.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Part of the odoo-at-payroll project. License: LGPL-3.
"""AP13-B: L-34-backed commuter allowance and commuter tax credit."""
from odoo import api, fields, models, _
from odoo.exceptions import UserError
class L10nAtPayrollPendlerErklaerung(models.Model):
_name = 'l10n.at.payroll.pendler.erklaerung'
_description = 'AT Pendlererklärung (L 34 EDV)'
_order = 'date_start desc, id desc'
employee_id = fields.Many2one('hr.employee', required=True, index=True)
company_id = fields.Many2one(
related='employee_id.company_id', readonly=True, required=True)
date_start = fields.Date(string='Gültig ab', required=True, index=True)
date_end = fields.Date(string='Gültig bis')
strecke_km = fields.Integer(string='Einfache Wegstrecke (km)', required=True)
pauschale_art = fields.Selection(
[('klein', 'Kleines Pendlerpauschale'),
('gross', 'Großes Pendlerpauschale')], required=True)
fahrten_stufe = fields.Selection(
[('drittel', '47 Fahrten (1/3)'),
('zwei_drittel', '810 Fahrten (2/3)'),
('voll', 'Ab 11 Fahrten (voll)')], required=True, default='voll')
pendlerrechner_datum = fields.Date(string='Pendlerrechner-Abfragedatum', required=True)
l34_attachment_ids = fields.Many2many('ir.attachment', string='L 34 EDV / Pendlerrechner')
arbeitgeber_kfz = fields.Boolean(string='Arbeitgebereigenes Kfz für Pendelstrecke')
ticket_art = fields.Selection(
[('keines', 'Kein Werkverkehr/Öffi-Ticket'),
('werkverkehr', '§ 26 Z 5 lit a Werkverkehr'),
('oeffi', '§ 26 Z 5 lit b Öffi-Ticket')], default='keines', required=True)
ticket_kosten_monatlich = fields.Monetary(string='AG-Ticketkosten pro Monat', currency_field='currency_id')
ticket_bezugsumwandlung = fields.Boolean(string='Ticket durch Bezugsumwandlung')
currency_id = fields.Many2one(related='company_id.currency_id', readonly=True)
note = fields.Text(string='Bemerkung')
@api.constrains('date_start', 'date_end', 'strecke_km', 'ticket_art', 'ticket_kosten_monatlich', 'ticket_bezugsumwandlung')
def _check_values(self):
for rec in self:
if rec.date_end and rec.date_end < rec.date_start:
raise UserError(_('The commuter declaration end date cannot precede its start date.'))
if rec.strecke_km < 0:
raise UserError(_('The commuter distance cannot be negative.'))
if rec.ticket_kosten_monatlich and rec.ticket_art == 'keines':
raise UserError(_('Ticket costs require a ticket classification.'))
if rec.ticket_bezugsumwandlung and rec.ticket_art == 'keines':
raise UserError(_('Salary conversion requires a ticket classification.'))
def _l10n_at_pendler_werte(self, payslip):
"""(PP-LZP-Freibetrag, PE-Jahreswert mit Stufenfaktor).
PP ist ein §-66-Abs-3-aliquotierter Freibetrag: Jahreswert ×
Fahrten-Stufe / 360 × Lohnsteuertage des LZP (lb-pen-09); die
kumulative Engine annualisiert ihn über 360/tage.
Der Pendlereuro ist ein Absetzbetrag auf der Steuer (lb-pen-04)
und muss daher — wie der Verkehrsabsetzbetrag — auf der
annualisierten Basis als Jahreswert einfließen: km × €/J ×
Fahrten-Stufe.
§ 26 Z 5 lit b: die AG-Ticketkosten mindern das PP anteilig
(§ 16 Abs 1 Z 6 lit i); § 26 Z 5 lit a (Werkverkehr) ist
datengetrieben über die L-34-Reststrecke abgebildet. Eine
Bezugsumwandlung sperrt PP/PE nicht (Ticket = steuerpflichtiger
laufender Bezug, der DN trägt die Kosten). PE ist mit dem
PP-Anspruch Fixkopplung (lb-pen-04): ohne PP-Anspruch kein PE.
"""
self.ensure_one()
if self.arbeitgeber_kfz:
return 0.0, 0.0
params = payslip._l10n_at_param('at_pendler')
km = self.strecke_km
grenzen = params['gross' if self.pauschale_art == 'gross' else 'klein']
annual = 0.0
for minimum, value in grenzen:
if km >= minimum:
annual = float(value)
factor = {'drittel': 1.0 / 3.0, 'zwei_drittel': 2.0 / 3.0,
'voll': 1.0}[self.fahrten_stufe]
pp = annual / 12.0 * factor \
* payslip._l10n_at_lohnsteuertage() / 30.0
if self.ticket_art == 'oeffi':
pp = max(pp - self.ticket_kosten_monatlich, 0.0)
pe = km * float(params['pendlereuro_km_jahr']) * factor
if pp <= 0.0:
pe = 0.0
return pp, pe
class HrPayslip(models.Model):
_inherit = 'hr.payslip'
def _l10n_at_pendler_erklaerung(self):
self.ensure_one()
return self.env['l10n.at.payroll.pendler.erklaerung'].search([
('employee_id', '=', self.employee_id.id),
('date_start', '<=', self.date_to),
'|', ('date_end', '=', False), ('date_end', '>=', self.date_from),
], order='date_start desc, id desc', limit=1)
def _l10n_at_pendler_werte(self):
self.ensure_one()
erklaerung = self._l10n_at_pendler_erklaerung()
return erklaerung._l10n_at_pendler_werte(self) if erklaerung else (0.0, 0.0)
def _l10n_at_bemessung_laufen(self, result_rules=None):
basis = super()._l10n_at_bemessung_laufen(result_rules)
pp, _pe = self._l10n_at_pendler_werte()
return basis - pp
def _l10n_at_absetzbetraege(self, jbg):
betrag = super()._l10n_at_absetzbetraege(jbg)
_pp, pe = self._l10n_at_pendler_werte()
# Pendlereuro = direkter LSt-Absetzbetrag (§ 33 Abs 5 Z 4):
# Jahreswert mit Fahrten-Stufe; der Jahresverbrauch läuft über
# die kumulative Engine (wie der Verkehrsabsetzbetrag).
return betrag + pe