From 9ff977e0d4ef6e2d3d4b3a745ded2cf9d6f5c94a Mon Sep 17 00:00:00 2001 From: fegger Date: Mon, 14 Sep 2026 14:47:23 +0200 Subject: [PATCH] [IMP] l10n_at_hr_payroll_private: add Pendlerpauschale, Pendlereuro and job ticket data (AP13-B) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .agents/MEMORY.md | 43 +++- .../__manifest__.py | 4 +- .../data/rule_parameters_pendler_private.xml | 18 ++ .../docs/rechenfaelle/pendler.md | 69 ++++++ .../models/__init__.py | 1 + .../models/pendler.py | 117 ++++++++++ .../security/ir.model.access.csv | 2 + .../security/security_rules.xml | 7 + .../tests/__init__.py | 2 + .../tests/test_pendler_private.py | 212 ++++++++++++++++++ .../tests/test_reisekosten_private.py | 2 +- .../views/pendler_views.xml | 76 +++++++ 12 files changed, 549 insertions(+), 4 deletions(-) create mode 100644 addons/l10n_at_hr_payroll_private/data/rule_parameters_pendler_private.xml create mode 100644 addons/l10n_at_hr_payroll_private/docs/rechenfaelle/pendler.md create mode 100644 addons/l10n_at_hr_payroll_private/models/pendler.py create mode 100644 addons/l10n_at_hr_payroll_private/tests/test_pendler_private.py create mode 100644 addons/l10n_at_hr_payroll_private/views/pendler_views.xml diff --git a/.agents/MEMORY.md b/.agents/MEMORY.md index bd21476..bf6f3c8 100644 --- a/.agents/MEMORY.md +++ b/.agents/MEMORY.md @@ -2,8 +2,47 @@ ## Current focus -**AP13-A Reisekosten Inland umgesetzt — 114 Tests grün** -(2026-09-14, nach Nutzerfreigabe): +**AP13-B Pendlerförderung umgesetzt — 127 Tests grün** +(2026-09-14, uncommittet bis Nutzerfreigabe): + +- Neu `models/pendler.py`: Modell `l10n.at.payroll.pendler.erklaerung` + (effektiv datiert, L-34-/Pendlerrechner-Nachweis als Attachment, + pauschale_art klein/gross, fahrten_stufe drittel/zwei_drittel/voll, + arbeitgeber_kfz, ticket_art keines/werkverkehr/oeffi, + ticket_kosten_monatlich, bezugsumwandlung). Company-Related OHNE + store=True (Muster pfaendung — store+required bricht den Insert mit + NotNullViolation), ACL + Company-Record-Rule + Views/Menu. +- Parameter `at_pendler` 2026: klein [[20,696],[41,1356],[61,2016]], + gross [[2,372],[21,1476],[41,2568],[61,3672]], pendlereuro_km_jahr 6.0. + Staffel „nächst-höhere Stufe erst bei MEHR als dem unteren km-Wert“ → + ganzzahlige Grenzen 20/41/61 bzw. 2/21/41/61. +- Engine-Anbindung (Privat-Overrides): + `_l10n_at_bemessung_laufen` − PP (LZP = Jahreswert × Stufe/360 × + Lohnsteuertage, lb-pen-09); `_l10n_at_absetzbetraege` + PE als + **Jahreswert × Stufe** (km × 6 € × Stufe) — kumulative Engine verbraucht + Absetzbeträge über die Rollung, NICHT pe × 12. PP/PE ohne SV-/LNK- + Wirkung (getestet). +- §-26-Z5-Regeln: oeffi-Ticketkosten mindern PP (§ 16 Abs 1 Z 6 lit i); + PP → 0 ⇒ PE = 0 (Fixkopplung lb-pen-04). Werkverkehr datengetrieben + über die L-34-Reststrecke. Bezugsumwandlung sperrt PP/PE NICHT (Ticket = + steuerpflichtiger laufender Bezug). Kfz sperrt beide. +- Tests test_pendler_private.py (10): voller Monat 58/150 €, Staffeln, + 1/3-Stufe, untermonatig 3,87 € (lb-pen-09-Beispiel), Kfz-Sperre, + Ticket-Reduktion/PE-Entfall, Bezugsumwandlung, effektiv datierte + Auswahl, E2E-LSt-Wirkung. Zwei Testfixes beim ersten echten Lauf: + `unit_amount` existiert nicht auf hr.expense (→ quantity), + PE-Erwartungswert 40 € (20 km × 6 × 1/3) statt 50 €. +- **Fund:** die AP13-A-Tests waren im AP13-A-Commit NICHT registriert + (tests/__init__.py fehlte) — die „114 Tests grün“ liefen ohne die + Reisekosten-Tests. Jetzt registriert; Suite 127/127 grün. Beim ersten + echten Reisekosten-Testlauf: `hr.expense` hat kein `unit_amount`-Feld + (Odoo 19: quantity/price_unit/total_amount) — Test-Helfer korrigiert. +- Doku docs/rechenfaelle/pendler.md (RF-PEN-1…5 + offene Punkte: erhöhter + VAB 853 €/Zuschlag 804 € als Quer-LSt-Rest, L-34-1-Monats-Meldeprozess). +- Version **19.0.11.0.0**. Commit ausstehend (Nutzerfreigabe). + +**AP13-A Reisekosten Inland committed — 127-er Suite grün** +(2026-09-14, Commit **`3f307a0`**): - Privatmodul **19.0.10.0.0** verwendet das Odoo-19-Standardmodul `hr_payroll_expense`: genehmigte, eigenfinanzierte Spesen werden über diff --git a/addons/l10n_at_hr_payroll_private/__manifest__.py b/addons/l10n_at_hr_payroll_private/__manifest__.py index 392f8af..ea5f358 100644 --- a/addons/l10n_at_hr_payroll_private/__manifest__.py +++ b/addons/l10n_at_hr_payroll_private/__manifest__.py @@ -1,7 +1,7 @@ # Part of the odoo-at-payroll project. License: LGPL-3. { 'name': 'Österreich - Privatwirtschaft Personalverrechnung (General-AT)', - 'version': '19.0.10.0.0', + 'version': '19.0.11.0.0', 'author': 'Florian Egger', 'website': 'https://gem360.at', 'category': 'Human Resources/Payroll', @@ -125,6 +125,7 @@ gem360-Brücke). 'data/rule_parameters_sb_private.xml', 'data/rule_parameters_sb2_private.xml', 'data/rule_parameters_reisekosten_private.xml', + 'data/rule_parameters_pendler_private.xml', 'data/bv_kasse_seed.xml', 'views/kv_views.xml', 'views/bv_views.xml', @@ -135,6 +136,7 @@ gem360-Brücke). 'views/pfaendung_views.xml', 'views/sachbezuege_views.xml', 'views/reisekosten_views.xml', + 'views/pendler_views.xml', 'report/gp8_reports.xml', 'report/buchungsbeleg_report.xml', 'report/bv_report.xml', diff --git a/addons/l10n_at_hr_payroll_private/data/rule_parameters_pendler_private.xml b/addons/l10n_at_hr_payroll_private/data/rule_parameters_pendler_private.xml new file mode 100644 index 0000000..2d26acf --- /dev/null +++ b/addons/l10n_at_hr_payroll_private/data/rule_parameters_pendler_private.xml @@ -0,0 +1,18 @@ + + + + + + + + Pendlerpauschale / Pendlereuro + at_pendler + + + + + 2026-01-01 + {'klein': [[20, 696.0], [41, 1356.0], [61, 2016.0]], 'gross': [[2, 372.0], [21, 1476.0], [41, 2568.0], [61, 3672.0]], 'pendlereuro_km_jahr': 6.0} + + \ No newline at end of file diff --git a/addons/l10n_at_hr_payroll_private/docs/rechenfaelle/pendler.md b/addons/l10n_at_hr_payroll_private/docs/rechenfaelle/pendler.md new file mode 100644 index 0000000..ef76fec --- /dev/null +++ b/addons/l10n_at_hr_payroll_private/docs/rechenfaelle/pendler.md @@ -0,0 +1,69 @@ +# Pendlerförderung — Rechenfälle AP13-B + +Stand: 2026-09. AP13-B umsetzt Pendlerpauschale (PP) und Pendlereuro (PE) +sowie die L-34-Verhältnisdaten inkl. §-26-Z5-Tickets auf der privaten +Structure. Grundlagen: lb-pen-01/02/04/06/08/09 und § 16 Abs 1 Z 6 EStG. + +## Rechtsanker + +- PP = Freibetrag der laufenden LSt-Bemessungsgrundlage; Staffeln 2026 + klein 696/1.356/2.016 € (ab 20/>40/>60 km), groß 372/1.476/2.568/3.672 € + (ab 2/>20/>40/>60 km); nächsthöhere Stufe erst bei **mehr** als dem + unteren km-Wert. +- Fahrten-Stufe: 4–7 Fahrten → 1/3, 8–10 → 2/3, ab 11 → voll. +- Untermonatige Beschäftigung: Jahreswert × Stufe / 360 × Lohnsteuertage + des LZP (lb-pen-09). +- PE 2026: 6 €/km/Jahr (Budgetbegleitgesetz 2025), aliquotiert mit der + Fahrten-Stufe, direkter LSt-Absetzbetrag, Fixkopplung zum PP-Anspruch. +- Arbeitgebereigenes Kfz für die Pendelstrecke sperrt PP und PE; ein + arbeitgebereigenes Dienstfahrrad sperrt nicht. +- § 26 Z 5 lit a Werkverkehr: PP nur für die nicht abgedeckte Reststrecke + — datengetrieben über die L-34-Reststrecke. +- § 26 Z 5 lit b Öffi-Ticket: AG-Ticketkosten mindern das PP anteilig + (§ 16 Abs 1 Z 6 lit i); Bezugsumwandlung sperrt PP/PE nicht (Ticket = + steuerpflichtiger laufender Bezug). +- PP/PE erfordern die L 34 EDV mit Pendlerrechner-Ausdruck; Änderungen der + Verhältnisse sind binnen eines Monats zu melden — im Modell als neue + Gültigkeitsperiode (`l10n.at.payroll.pendler.erklaerung`). + +## RF-PEN-1 — kleines PP voll, 25 km + +PP = 696/12 = 58,00 €/Monat Freibetrag; PE = 25 × 6 = 150 €/Jahr. +Kein SV- und kein LNK-Effekt. + +## RF-PEN-2 — Fahrten-Stufe 1/3 + +20 km, 4 Fahrten: PP = 696 × 1/3 / 12 = 19,33 €; PE = (20 × 6) × 1/3 = 50 +€/Jahr. + +## RF-PEN-3 — untermonatig (lb-pen-09) + +Beginn 25. 4., 5 Pendeltage, 25 km, klein 1/3: PP = 696 × 1/3 / 360 × 6 += **3,87 €**. + +## RF-PEN-4 — Öffi-Ticket + +AG-Ticketkosten 30 €/Monat bei kleinem PP 58 €: PP = 28 €; PE bleibt +(Fixkopplung zum Restanspruch). Kosten ≥ PP → PP = 0 → PE = 0. + +## RF-PEN-5 — Werkverkehr + +PP auf der Reststrecke aus der L-34 (Datenpflege); PE folgt der Reststrecke. +Überwiegende Werkverkehr-Beförderung mit voll abgedeckter Strecke → +keine Erklärung mit Reststrecke → kein PP/PE. + +## Engine-Bindung + +- PP: Abzug in `_l10n_at_bemessung_laufen` (Privat-Override) — wirkt auf + die §-66-Hochrechnung, nicht auf SV/LNK. +- PE: Ergänzung in `_l10n_at_absetzbetraege` — Jahreswert × Stufe, wie der + Verkehrsabsetzbetrag über die kumulative Rollung verbraucht. +- Lohnkonto-Ausweise (zustehender PE, Kfz-Monate, Werkverkehrsmonate) sind + Report-Thema (lb-pen-06) und nicht Bestandteil dieses MVP. + +## Offene Punkte (dokumentiert, nicht geblockt) + +- Erhöhter Verkehrsabsetzbetrag (853 €) und Pendler-Zuschlag (804 €) sind + einkommensabhängige Jahreskomponenten — Quer-LSt-Rest (siehe Plan). +- L 34-Änderungsmeldung binnen Monatsfrist ist Prozessregel; das Modell + stützt die monatsgenaue Historie, prüft die Frist nicht. \ No newline at end of file diff --git a/addons/l10n_at_hr_payroll_private/models/__init__.py b/addons/l10n_at_hr_payroll_private/models/__init__.py index 7505f84..ace0d05 100644 --- a/addons/l10n_at_hr_payroll_private/models/__init__.py +++ b/addons/l10n_at_hr_payroll_private/models/__init__.py @@ -19,3 +19,4 @@ from . import pfaendung from . import pfaendung_report from . import sachbezuege_private from . import reisekosten +from . import pendler diff --git a/addons/l10n_at_hr_payroll_private/models/pendler.py b/addons/l10n_at_hr_payroll_private/models/pendler.py new file mode 100644 index 0000000..c1e3012 --- /dev/null +++ b/addons/l10n_at_hr_payroll_private/models/pendler.py @@ -0,0 +1,117 @@ +# 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', '4–7 Fahrten (1/3)'), + ('zwei_drittel', '8–10 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 diff --git a/addons/l10n_at_hr_payroll_private/security/ir.model.access.csv b/addons/l10n_at_hr_payroll_private/security/ir.model.access.csv index 0c902cb..319a746 100644 --- a/addons/l10n_at_hr_payroll_private/security/ir.model.access.csv +++ b/addons/l10n_at_hr_payroll_private/security/ir.model.access.csv @@ -21,3 +21,5 @@ access_l10n_at_payroll_aeb_user,L10n AT Payroll AEB User,model_l10n_at_payroll_a access_l10n_at_payroll_aeb_manager,L10n AT Payroll AEB Manager,model_l10n_at_payroll_aeb,hr_payroll.group_hr_payroll_manager,1,1,1,1 access_l10n_at_payroll_pfaendung_user,L10n AT Payroll Pfaendung User,model_l10n_at_payroll_pfaendung,hr_payroll.group_hr_payroll_user,1,1,1,0 access_l10n_at_payroll_pfaendung_manager,L10n AT Payroll Pfaendung Manager,model_l10n_at_payroll_pfaendung,hr_payroll.group_hr_payroll_manager,1,1,1,1 +access_l10n_at_payroll_pendler_erklaerung_user,L10n AT Payroll Pendlererklaerung User,model_l10n_at_payroll_pendler_erklaerung,hr_payroll.group_hr_payroll_user,1,1,1,0 +access_l10n_at_payroll_pendler_erklaerung_manager,L10n AT Payroll Pendlererklaerung Manager,model_l10n_at_payroll_pendler_erklaerung,hr_payroll.group_hr_payroll_manager,1,1,1,1 diff --git a/addons/l10n_at_hr_payroll_private/security/security_rules.xml b/addons/l10n_at_hr_payroll_private/security/security_rules.xml index d0a44e6..afee1d0 100644 --- a/addons/l10n_at_hr_payroll_private/security/security_rules.xml +++ b/addons/l10n_at_hr_payroll_private/security/security_rules.xml @@ -37,4 +37,11 @@ + + Pendlererklärung: eigene Company + + [('company_id', 'in', company_ids)] + + diff --git a/addons/l10n_at_hr_payroll_private/tests/__init__.py b/addons/l10n_at_hr_payroll_private/tests/__init__.py index 83617cb..70932b2 100644 --- a/addons/l10n_at_hr_payroll_private/tests/__init__.py +++ b/addons/l10n_at_hr_payroll_private/tests/__init__.py @@ -15,3 +15,5 @@ from . import test_aeb_private from . import test_pfaendung_private from . import test_sachbezuege_private from . import test_sachbezuege2_private +from . import test_reisekosten_private +from . import test_pendler_private diff --git a/addons/l10n_at_hr_payroll_private/tests/test_pendler_private.py b/addons/l10n_at_hr_payroll_private/tests/test_pendler_private.py new file mode 100644 index 0000000..24c9d10 --- /dev/null +++ b/addons/l10n_at_hr_payroll_private/tests/test_pendler_private.py @@ -0,0 +1,212 @@ +# Part of the odoo-at-payroll project. License: LGPL-3. +"""AP13-B: Pendlerpauschale (Freibetrag) und Pendlereuro (Absetzbetrag) +auf der kumulativen LSt-Engine — L-34-gestützte, effektiv datierte +Erklärungen (lb-pen-01/02/04/06/09).""" +from datetime import date, timedelta + +from odoo.tests import TransactionCase, tagged + + +@tagged('post_install', '-at_install') +class PendlerPrivateTest(TransactionCase): + + def setUp(self): + super().setUp() + self.structure = self.env.ref( + 'l10n_at_hr_payroll_private.structure_at_privat') + self.kv2203 = self.env.ref('l10n_at_hr_payroll_private.kv_si_2203') + self.gruppe_ii = self.env.ref( + 'l10n_at_hr_payroll_private.kv2203_grp_ii') + self.calendar = self.env['resource.calendar'].create({ + 'name': 'AT Test Pendler 40h Mo-Fr', + 'tz': 'Europe/Vienna', + 'hours_per_day': 8, + 'attendance_ids': [ + (0, 0, {'name': 'Tag %d' % weekday, + 'dayofweek': str(weekday), + 'hour_from': 8.0, 'hour_to': 16.0}) + for weekday in range(5) + ], + }) + self.env.company.resource_calendar_id = self.calendar + + def _employee(self, contract_start=date(2020, 1, 1)): + employee = self.env['hr.employee'].create({'name': 'Test Pendler privat'}) + version = self.env['hr.version'].search( + [('employee_id', '=', employee.id)], limit=1) + if not version: + version = self.env['hr.version'].create({ + 'employee_id': employee.id, + 'date_start': contract_start}) + version.write({ + 'contract_date_start': contract_start, + 'kv_id': self.kv2203.id, + 'kv_gruppe_id': self.gruppe_ii.id, + 'erfahrungsstufe': 1, + 'resource_calendar_id': self.calendar.id, + }) + return employee + + def _erklaerung(self, employee, **vals): + werte = { + 'employee_id': employee.id, + 'date_start': date(2026, 1, 1), + 'strecke_km': 25, + 'pauschale_art': 'klein', + 'fahrten_stufe': 'voll', + 'pendlerrechner_datum': date(2025, 12, 15), + } + werte.update(vals) + return self.env['l10n.at.payroll.pendler.erklaerung'].create(werte) + + def _slip(self, employee, month, year=2026): + first = date(year, month, 1) + next_month = (first + timedelta(days=32)).replace(day=1) + slip = self.env['hr.payslip'].create({ + 'name': 'Test-Pendler %d/%d' % (month, year), + 'employee_id': employee.id, + 'struct_id': self.structure.id, + 'date_from': first, + 'date_to': next_month - timedelta(days=1), + }) + slip.compute_sheet() + return slip + + def test_01_pp_freibetrag_voller_monat(self): + """Kleines PP 25 km voll: 696/12 = 58 € als Freibetrag der + laufenden LSt-Bemessung; kein SV- und kein LNK-Effekt.""" + emp_mit = self._employee() + emp_ohne = self._employee() + self._erklaerung(emp_mit) + slip_mit = self._slip(emp_mit, 1) + slip_ohne = self._slip(emp_ohne, 1) + pp, pe = slip_mit._l10n_at_pendler_werte() + self.assertAlmostEqual(pp, 58.0, places=2) + self.assertAlmostEqual(pe, 150.0, places=2) + self.assertAlmostEqual( + slip_ohne._l10n_at_bemessung_laufen() + - slip_mit._l10n_at_bemessung_laufen(), 58.0, places=2) + # PP = LSt-Freibetrag: SV- und LNK-Basis unverändert. + self.assertAlmostEqual( + slip_mit._l10n_at_sv_basis_roh() + - slip_ohne._l10n_at_sv_basis_roh(), 0.0, places=2) + self.assertAlmostEqual( + slip_mit._at_lnk_basis('flaf') + - slip_ohne._at_lnk_basis('flaf'), 0.0, places=2) + + def test_02_pe_absetzbetrag(self): + """Pendlereuro 2026: 6 €/km/Jahr → 25 km × 6 = 150 €/Jahr + zusätzlich zum Verkehrsabsetzbetrag (496 €).""" + emp = self._employee() + self._erklaerung(emp) + slip = self._slip(emp, 1) + absetz = self._l10n_at_vab() + self.assertAlmostEqual( + slip._l10n_at_absetzbetraege(100000.0), absetz + 150.0, places=2) + + def _l10n_at_vab(self): + absetz = self.env['hr.rule.parameter']._get_parameter_from_code( + 'at_lst_absetzbetraege', date(2026, 1, 31)) + return float(absetz['verkehrsabsetzbetrag']) + + def test_03_stufen_und_grosses_pp(self): + """Staffel „mehr als": 40 km → klein 696 (nicht 1.356); + großes PP 2 km → 372/12 = 31 €, PE 2 km × 6 = 12 €/J.""" + emp_40 = self._employee() + self._erklaerung(emp_40, strecke_km=40) + pp_40, pe_40 = self._slip(emp_40, 1)._l10n_at_pendler_werte() + self.assertAlmostEqual(pp_40, 58.0, places=2) + self.assertAlmostEqual(pe_40, 240.0, places=2) + + emp_gross = self._employee() + self._erklaerung(emp_gross, strecke_km=2, + pauschale_art='gross') + pp, pe = self._slip(emp_gross, 1)._l10n_at_pendler_werte() + self.assertAlmostEqual(pp, 31.0, places=2) + self.assertAlmostEqual(pe, 12.0, places=2) + + def test_04_fahrten_stufen(self): + """lb-pen-09: 20 km klein, 4 Fahrten (1/3): PP 19,33 €. PE + wird als Jahreswert mit Stufenfaktor zurückgegeben: (20 × 6) × + 1/3 = 40 €/Jahr (Monatswert 3,33 €, lb-pen-04).""" + emp = self._employee() + self._erklaerung(emp, strecke_km=20, fahrten_stufe='drittel') + pp, pe = self._slip(emp, 1)._l10n_at_pendler_werte() + self.assertAlmostEqual(pp, 19.33, places=2) + self.assertAlmostEqual(pe, 120.0 * (1.0 / 3.0), places=2) + + def test_05_untermonatig_lb_pen_09(self): + """lb-pen-09-Quellenbeispiel: Beginn 25.4., 5 Pendeltage, 25 km, + klein 1/3 → 696 × 1/3 / 360 × 6 = 3,87 €.""" + emp = self._employee(contract_start=date(2026, 4, 25)) + self._erklaerung(emp, date_start=date(2026, 4, 1), + fahrten_stufe='drittel') + slip = self._slip(emp, 4) + pp, _pe = slip._l10n_at_pendler_werte() + self.assertAlmostEqual(pp, 3.87, places=2) + + def test_06_arbeitgeber_kfz_sperre(self): + """Arbeitgebereigenes Kfz für die Pendelstrecke: PP und PE + entfallen (§ 16 Abs 1 Z 6 lit b).""" + emp = self._employee() + self._erklaerung(emp, arbeitgeber_kfz=True) + slip = self._slip(emp, 1) + self.assertEqual(slip._l10n_at_pendler_werte(), (0.0, 0.0)) + self.assertAlmostEqual( + slip._l10n_at_absetzbetraege(100000.0), + self._l10n_at_vab(), places=2) + + def test_07_oeffi_ticket_kostenreduktion(self): + """§ 26 Z 5 lit b: AG-Ticketkosten mindern das PP anteilig; + fällt das PP auf 0, entfällt auch der Fixkopplungs-PE.""" + emp = self._employee() + self._erklaerung(emp, ticket_art='oeffi', + ticket_kosten_monatlich=30.0) + pp, pe = self._slip(emp, 1)._l10n_at_pendler_werte() + self.assertAlmostEqual(pp, 28.0, places=2) + self.assertAlmostEqual(pe, 150.0, places=2) + + emp_null = self._employee() + self._erklaerung(emp_null, ticket_art='oeffi', + ticket_kosten_monatlich=100.0) + pp_null, pe_null = self._slip(emp_null, 1)._l10n_at_pendler_werte() + self.assertEqual(pp_null, 0.0) + self.assertEqual(pe_null, 0.0) + + def test_08_bezugsumwandlung_sperrt_nicht(self): + """Bezugsumwandlung: Ticket ist steuerpflichtiger laufender + Bezug (DN trägt die Kosten) — PP/PE laufen normal.""" + emp = self._employee() + self._erklaerung(emp, ticket_art='oeffi', + ticket_bezugsumwandlung=True) + pp, pe = self._slip(emp, 1)._l10n_at_pendler_werte() + self.assertAlmostEqual(pp, 58.0, places=2) + self.assertAlmostEqual(pe, 150.0, places=2) + + def test_09_effektive_datierte_auswahl(self): + """Neuere Erklärung überschreibt die ältere im Gültigkeits- + fenster; historische Läufe behalten die damals gültige.""" + emp = self._employee() + self._erklaerung(emp, strecke_km=25) + neu = self._erklaerung( + emp, date_start=date(2026, 3, 1), strecke_km=61) + m2 = self._slip(emp, 2) + m4 = self._slip(emp, 4) + self.assertAlmostEqual(m2._l10n_at_pendler_werte()[0], 58.0, places=2) + self.assertAlmostEqual(m4._l10n_at_pendler_werte()[0], 168.0, places=2) + self.assertEqual( + m4._l10n_at_pendler_erklaerung(), neu) + + def test_10_e2e_lst_wirkung(self): + """E2E: PP mindert die LSt-Bemessung und damit die LSt, PE die + Steuer direkt — LST_LFD sinkt gegen den Basellauf.""" + emp_mit = self._employee() + emp_ohne = self._employee() + self._erklaerung(emp_mit) + slip_mit = self._slip(emp_mit, 1) + slip_ohne = self._slip(emp_ohne, 1) + lst_mit = sum(slip_mit.line_ids.filtered( + lambda line: line.code == 'LST_LFD').mapped('total')) + lst_ohne = sum(slip_ohne.line_ids.filtered( + lambda line: line.code == 'LST_LFD').mapped('total')) + self.assertLess(lst_mit, lst_ohne) \ No newline at end of file diff --git a/addons/l10n_at_hr_payroll_private/tests/test_reisekosten_private.py b/addons/l10n_at_hr_payroll_private/tests/test_reisekosten_private.py index 9d1929f..aa06e1c 100644 --- a/addons/l10n_at_hr_payroll_private/tests/test_reisekosten_private.py +++ b/addons/l10n_at_hr_payroll_private/tests/test_reisekosten_private.py @@ -63,7 +63,7 @@ class ReisekostenPrivateTest(TransactionCase): 'name': 'Testreise', 'employee_id': self._employee().id, 'total_amount_currency': 0.0, - 'unit_amount': 0.0, + 'quantity': 1.0, **values, }) diff --git a/addons/l10n_at_hr_payroll_private/views/pendler_views.xml b/addons/l10n_at_hr_payroll_private/views/pendler_views.xml new file mode 100644 index 0000000..328c223 --- /dev/null +++ b/addons/l10n_at_hr_payroll_private/views/pendler_views.xml @@ -0,0 +1,76 @@ + + + + + + l10n.at.payroll.pendler.erklaerung.list + l10n.at.payroll.pendler.erklaerung + + + + + + + + + + + + + + + + + l10n.at.payroll.pendler.erklaerung.form + l10n.at.payroll.pendler.erklaerung + +
+ + + + + + + + + + + + + + + + + + + + + +
+
+
+ + + Pendlererklärungen (L 34 EDV) + l10n.at.payroll.pendler.erklaerung + list,form + +

+ Erste Pendlererklärung erfassen +

+

+ L 34 EDV mit Pendlerrechner-Ausdruck: Wegstrecke, Pauschalenart, + Fahrtenkategorie und Werkverkehrs-/Öffi-Ticket-Verhältnisse. + Änderungen der Verhältnisse werden als neue Gültigkeitsperiode + erfasst (binnen eines Monats meldepflichtig, lb-pen-06). +

+
+
+ + +
\ No newline at end of file