[IMP] l10n_at_hr_payroll_private: Sachbezüge (AP12)
This commit is contained in:
+96
-3
@@ -2,6 +2,84 @@
|
||||
|
||||
## Current focus
|
||||
|
||||
**AP 12b wertgebundene Sachbezüge umgesetzt — 166/166 Tests grün**
|
||||
(2026-09-14, freigegeben):
|
||||
|
||||
- **Kern** `l10n_at_hr_payroll` (19.0.8.0.0): neues
|
||||
`hr.salary.rule.l10n_at_sv_art` (`folgt` Default = rückwärtskompatibel,
|
||||
`laufend`/`sonstig`/`keine`) trennt die SV- von der LSt-Zuordnung;
|
||||
`_l10n_at_sv_art` / `_l10n_at_sv_brutto` und die SV-Basis-/§49-/SZ-
|
||||
Pfade angepasst. Notwendig für Hardware-Verkauf (LSt sonstig/SV
|
||||
laufend) und Bonusmeilen (LSt neutral/SV laufend). Kritischer Fix:
|
||||
bei Odoo `DefaultDictPayroll` nur nach `code in result_rules` lesen —
|
||||
`.get()` materialisiert eine leere künftige Regel und brach die LSt-
|
||||
Engine mit `KeyError('total')`. Tests test_17/18 in test_lohnsteuer.
|
||||
- **Privat** `l10n_at_hr_payroll_private` (19.0.9.7.0): neues
|
||||
`models/sachbezuege_private.py` — Model B `hr.version`-Stammdaten
|
||||
+ Mitarbeiter-Relateds für Wagen, Wohnung, Parkplatz, Handyprofil;
|
||||
Werte `at_sb_wagen`/`_co2`/`_wohnung`/`_parkplatz`; Regeln
|
||||
`SB_PARKPLATZ`/`_WOHNUNG`/`_WAGEN`/`_GEBRAUCHT`/`_BONUSMEILEN`/
|
||||
`_HANDY`, Eingaben `ATP_SB_HANDY`/`_GEBRAUCHT`/`_BONUSMEILEN`, View
|
||||
`views/sachbezuege_views.xml`; E2E tests test_sachbezuege2_private.py.
|
||||
- **Abgabenlogik**: Wagen/Wohnung/Parkplatz sowie Handy-Mehrkosten
|
||||
laufend voll abgabenpflichtig. Handy-Verkauf unter Buchwert: SV-
|
||||
laufend + LSt-sonstig + LNK. Bonusmeilen: Drittlohn, nur SV-laufend
|
||||
(Kategorie ATP_DG, also kein Auszahlungsnetto-Brutto), kein LSt-/LNK-
|
||||
Brutto; darauf entfallende DN-SV mindert aber nach §62-Kette die LSt-
|
||||
BG und damit die laufende LSt. Parkplatz als laufend: ⚠ Quelltext
|
||||
lb-sac-18 nennt die Abgabenkategorie nicht ausdrücklich — vor
|
||||
Produktivsetzung RIS/LStR prüfen.
|
||||
- **Recht/MVP-Grenzen**: Wagen §4 SBWVO (CO₂ EZJ, E=0, voller/halber
|
||||
Wert/Kostenbeitrag); Wohnung §2 Abs7a (m² BL, Norm/Besorger kombiniert
|
||||
55,5 %, klein ≤35=0, >35–45 −35 % erste 12 Mon.); Parkplatz §4a
|
||||
(14,53), Handy lb-sac-15, Bonusmeilen lb-sac-08. Nicht automatisiert:
|
||||
Wagen Mini/Pool/Wechsel/Mehrfach, Wohnungs-12-Monats-Retro/Mehrnutzer,
|
||||
Lohnkonto-Nachweise. Rechenfälle RF-SB-7…9 in
|
||||
`docs/rechenfaelle/sachbezuege.md`.
|
||||
- AP12 ist damit abgeschlossen; **AP13 Jobticket** als nächster AP.
|
||||
**Kein Commit** (nur auf Nutzer-Aufforderung).
|
||||
|
||||
**AP 12a steuerfreie Sachbezüge umgesetzt — 159/159 Tests grün**
|
||||
(2026-09-13, Freigabe 1/2: Ja/„erst a dann b“):
|
||||
|
||||
- **Kern** `l10n_at_hr_payroll` (19.0.7.0.0): `hr.salary.rule` Flag
|
||||
`l10n_at_st_frei` (mahlzeit/rabatt/betriebsveranstaltung/jubilaeum/
|
||||
homeoffice), `_l10n_at_st_frei_ytd`/`_l10n_at_st_frei_freie_laufen`
|
||||
(Spieglung der §-49-SV-Seite, YTD je validierter Lauf), Anbindung
|
||||
`_l10n_at_bemessung_laufen` (−st_frei), Parameter `at_st_grenzen`
|
||||
(8 €/AT · 1.000 €/J · 365 €/J · 186 €/J · 3 €/AT + 100 Tage);
|
||||
Tests test_13…16 in test_lohnsteuer.py.
|
||||
- **Privat** `l10n_at_hr_payroll_private` (19.0.9.6.0): Eingaben
|
||||
`ATP_SB_MAHLZEIT`/`_BETRIEBSVERANSTALTUNG`/`_SACHZUWENDUNG`/`_RABATT`/
|
||||
`_HOMEOFFICE` (Mahlzeit/HO is_quantity), Regeln `SB_*` (seq 41–44/48,
|
||||
setzen SV- UND LSt-Flag), Parameter `at_sb_werte` (mahlzeit_wert 8.0 =
|
||||
tatsächl. Bonwert, homeoffice_wert 3.0), `_at_lnk_basis` −st_frei
|
||||
(lohnnebenkostenfrei); 6 E2E-Tests tests/test_sachbezuege_private.py
|
||||
(RF-SB-1…6).
|
||||
- **Zwei Bug-Funde (Testlauf)**: (a) Odoo-19-Engine: Zeilen-Total =
|
||||
amount × qty × rate/100 — tagesbasierte Regeln MÜSSEN `result = wert`
|
||||
(Tageswert) + `result_qty = tage` setzen (erste Version `result =
|
||||
wert × tage` = Doppelzählung, Zeile 800 statt 80); (b) Test-Helfer
|
||||
muss Zeilen nach REGEL-Code (SB_*) filtern, nicht nach Input-Code
|
||||
(ATP_SB_*). (c) LSt-Bemessung: ΔLSt + ΔSVDN = Überhang (SV-DN
|
||||
abzugsfähig) — nicht pauschal ΔLSt = Überhang.
|
||||
- **Doku**: `docs/rechenfaelle/sachbezuege.md` (RF-SB-1…6;
|
||||
Rechtsquellen lb-sac-17/07/02, lb-ges-02, lb-tel-04/05 — Homeoffice-
|
||||
Anchor war in einem XML-Kommentar fälschlich lb-sac-15 = Handy,
|
||||
korrigiert).
|
||||
- **Vorbestehend (nicht AP12a)**: `salary_rules_bvg_private.xml` steht
|
||||
ZWEIMAL in der Data-List des privaten Manifests (Log-Warnung
|
||||
„imported twice“) — beim nächsten Touch bereinigen.
|
||||
- **Nächster Schritt AP12b** (wertgebundene Sachbezüge): Dienstwagen
|
||||
(SBV § 4, CO₂), Kleinwohnung (SBV § 2 Abs 7a, ≤35 m² = 0, 35–45 m²
|
||||
= −35 % ≤12 Mon.), Parkplatz (SBV § 4a € 14,53 — SV/LSt-Kategorie
|
||||
RIS-klären lb-sac-18), Handy (§ 26 Z 9, lb-sac-15), Bonusmeilen
|
||||
(nur SV, lb-sac-08). Freigegebene Stammdaten-Entscheidung:
|
||||
**Model B (`hr.version`-Felder) für wertgebundene, Model A
|
||||
(Eingaben je Lauf) für ereignisgesteuerte** + versionierte SBV-
|
||||
Parameter (CO₂/m² je BL). Jobticket → AP13. **Kein Commit** (Regel:
|
||||
nur auf Nutzer-Aufforderung).
|
||||
|
||||
**AP 11 Lohnpfändung umgesetzt — 103/103 Tests grün** (2026-09-13,
|
||||
freigegeben 2026-09-12/13, Plan: `personalverrechnung/PLAN-Restluecken-AP11.md` §AP11):
|
||||
|
||||
@@ -28,9 +106,10 @@ freigegeben 2026-09-12/13, Plan: `personalverrechnung/PLAN-Restluecken-AP11.md`
|
||||
in `_at_pfa_rule_amount`/`_at_pfa_bruto` (result_rules-Pfad) + finales
|
||||
`round` in `_at_pfaendung_basis` (alle Töpfe).
|
||||
- **Offene Reste** (dokumentiert, nicht geblockt): Unterhaltsexekution
|
||||
75 %/Koinzidenz, Vorschüsse § 290c, Sachbezüge (AP12), Beendigungs-
|
||||
Regeldetails (AP14/15), 2027-AZR-Werte. **Kein Commit** (Nutzer-
|
||||
Freigabe ausstehend).
|
||||
75 %/Koinzidenz, Vorschüsse § 290c, Sachbezüge (AP12 — AP12a jetzt
|
||||
umgesetzt, AP12b folgt), Beendigungs-Regeldetails (AP14/15),
|
||||
2027-AZR-Werte. **Commit `018554b`** (2026-09-13, nach Nutzer-
|
||||
Freigabe „commit, dann ap12").
|
||||
|
||||
**AP 8 Arbeits-/Entgeltbestätigungen umgesetzt — 152/152 Tests
|
||||
grün — damit ist der AP0–AP10-Nutzer-Katalog komplett** (2026-09-12,
|
||||
@@ -1364,6 +1443,20 @@ B17-Inhalt/B18; Kleinverifikationen s. KB-KONFLIKTE „Nächste Schritte“).
|
||||
|
||||
## Decisions & conventions
|
||||
|
||||
- **RIS-Referenzquelle `.ris/`** (2026-09-13, Nutzer): der neue lokale
|
||||
Ordner `.ris/` ist ZUKUNFT die verbindliche Referenzquelle für
|
||||
Gesetzestexte und übernimmt die Rolle von `.additional_docs/ris_normtexte/`
|
||||
(2026-09-11) bzw. `.firecrawl/ris/` als Norm-Verifikationsbasis. Format:
|
||||
je Norm ein Markdown-Auszug mit den in der Lexis360-WKB zitierten
|
||||
Paragraphen (Kopf mit RIS-GesNr + GeltendeFassung-URL, je § Fundstelle +
|
||||
Inkrafttretensdatum + NormDokument-URL), tagesaktuelle Fassung, Abruf
|
||||
2026-09-13; Bestand: ABGB, APSG, APflG, BMSVG, BS-V, BauV, DHG, FLAG,
|
||||
GmbHG, MSchG, MSchV, RStDG, Sachbezugswerteverordnung, StPO.
|
||||
**Auszug ≠ Volltext:** fehlt ein benötigter §, NICHT aus Trainingswerten
|
||||
ergänzen — Beschaffung (firecrawl-RIS-Download, findok-Regel) oder
|
||||
Nutzer-Beleg, Ablage in `.ris/`; gitignored wie die anderen
|
||||
D1-Quellordner.
|
||||
|
||||
- **GP4-Konventionen (2026-09-11, Nutzer-freigegebener Bauplan +**
|
||||
**Quellenverifikation):** (1) **§ 2a AMPFG wirkt nur DN-seitig**
|
||||
(tiers+dn_voll im Parameter; Grenzen nicht aliquotieren; gilt auch
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Part of the odoo-at-payroll project. License: LGPL-3.
|
||||
{
|
||||
'name': 'Österreich - Personalverrechnung Kern (AT Payroll Core)',
|
||||
'version': '19.0.6.0.0',
|
||||
'version': '19.0.8.0.0',
|
||||
'author': 'Florian Egger',
|
||||
'website': 'https://gem360.at',
|
||||
'category': 'Human Resources/Payroll',
|
||||
|
||||
@@ -103,4 +103,21 @@
|
||||
<field name="date_from">2027-01-01</field>
|
||||
<field name="parameter_value">{'stunden': 10.0, 'betrag': 120.0}</field>
|
||||
</record>
|
||||
|
||||
<!-- Steuerfreie Sachbezüge § 3/§ 26 EStG (Freibeträge, Jahres- -->
|
||||
<!-- Kumulative je DN; lb-sac-17/07/02, lb-ges-02). Mahlzeiten -->
|
||||
<!-- 8 €/AT (Z 17), Mitarbeiterrabatte 1.000 €/J (Z 21), -->
|
||||
<!-- Betriebsveranstaltungen 365 €/J (Z 14), Sachzuwendungen/ -->
|
||||
<!-- Jubiläum/Geschenke 186 €/J (Z 14), Homeoffice 3 €/Tag max 100 -->
|
||||
<!-- Tage/J (Z 9). Grenzen als Jahresversionen führen. -->
|
||||
<record id="hr_rule_parameter_at_st_grenzen" model="hr.rule.parameter">
|
||||
<field name="name">Steuerfreie Sachbezüge § 3/§ 26 EStG (Grenzen)</field>
|
||||
<field name="code">at_st_grenzen</field>
|
||||
<field name="country_id" ref="base.at"/>
|
||||
</record>
|
||||
<record id="hr_rule_parameter_value_at_st_grenzen" model="hr.rule.parameter.value">
|
||||
<field name="rule_parameter_id" ref="hr_rule_parameter_at_st_grenzen"/>
|
||||
<field name="date_from">2026-01-01</field>
|
||||
<field name="parameter_value">{'mahlzeit_tag': 8.0, 'rabatt_jahr': 1000.0, 'betriebsveranstaltung_jahr': 365.0, 'jubilaeum_jahr': 186.0, 'homeoffice_tag': 3.0, 'homeoffice_tage': 100}</field>
|
||||
</record>
|
||||
</odoo>
|
||||
@@ -52,6 +52,24 @@ class HrSalaryRule(models.Model):
|
||||
help='Anteil des Zeilenbetrags, der Zuschlag (und damit '
|
||||
'§-68-potentiell steuerfrei) ist — z. B. 50 %-Regel mit '
|
||||
'Grundvergütung: 0,5/1,5 = 33,33 %.')
|
||||
l10n_at_st_frei = fields.Selection(
|
||||
[('keine', 'Keine § 3/§ 26-Steuerfreiheit'),
|
||||
('mahlzeit', 'Mahlzeiten-Gutschein (§ 3 Abs 1 Z 17; 8 €/AT)'),
|
||||
('rabatt', 'Mitarbeiterrabatt (§ 3 Abs 1 Z 21; 1.000 €/J)'),
|
||||
('betriebsveranstaltung',
|
||||
'Betriebsveranstaltung (§ 3 Abs 1 Z 14; 365 €/J)'),
|
||||
('jubilaeum',
|
||||
'Sachzuwendung/Jubiläum/Geschenk (§ 3 Abs 1 Z 14; 186 €/J)'),
|
||||
('homeoffice',
|
||||
'Homeoffice-Pauschale (§ 26 Z 9; 3 €/Tag, max 100 Tage/J)')],
|
||||
string='AT § 3/§ 26 (steuerfrei)',
|
||||
default='keine',
|
||||
help='Steuerfreie Bezugsbestandteile (Freibeträge): der '
|
||||
'steuerfreie Anteil wird je Bezugsmonat von der LSt-'
|
||||
'Bemessungsgrundlage und von der LNK-Bemessungsgrundlage '
|
||||
'abgezogen; Grenzen als Jahres-Kumulative je Dienstnehmer '
|
||||
'(at_st_grenzen). Nur der übersteigende Teil ist '
|
||||
'steuerpflichtig. Menge = Arbeitstage (Z 17/Z 9).')
|
||||
|
||||
|
||||
class HrVersion(models.Model):
|
||||
@@ -197,6 +215,76 @@ class HrPayslip(models.Model):
|
||||
ergebnis['ue_std'] = min(max(betrag_ue, 0.0), float(ue['betrag']))
|
||||
return ergebnis
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# § 3/§ 26 — Steuerfreiheit (Buckets je Regel-Flag, versionierte
|
||||
# Caps; Jahres-Kumulative — Spieglung der §-49-SV-Seite)
|
||||
# ------------------------------------------------------------------
|
||||
def _l10n_at_st_frei_ytd(self, kategorie, menge=False):
|
||||
"""Jahresverbrauch einer §-3/§-26-Kategorie aus früheren
|
||||
validierten Läufen (refundbewusst über _l10n_at_ytd_slaeufe)."""
|
||||
self.ensure_one()
|
||||
total = 0.0
|
||||
for slip in self._l10n_at_ytd_slaeufe():
|
||||
linien = slip.line_ids.filtered(
|
||||
lambda l: l.salary_rule_id.l10n_at_st_frei == kategorie)
|
||||
if menge:
|
||||
total += sum(linien.mapped('quantity'))
|
||||
else:
|
||||
total += sum(linien.mapped('total'))
|
||||
return total
|
||||
|
||||
def _l10n_at_st_frei_freie_laufen(self, result_rules=None):
|
||||
"""Steuerfreie Anteile laufender Bezüge dieses LZP: je Kategorie
|
||||
Tages-/Jahresgrenzen (at_st_grenzen) minus Jahresverbrauch;
|
||||
Freibeträge — nur der übersteigende Teil steuerpflichtig
|
||||
(lb-sac-07/17, lb-ges-02, lb-sac-02)."""
|
||||
self.ensure_one()
|
||||
grenzen = self.env['hr.rule.parameter']._get_parameter_from_code(
|
||||
'at_st_grenzen', self.date_to, raise_if_not_found=False)
|
||||
if not grenzen:
|
||||
return 0.0
|
||||
frei_gesamt = 0.0
|
||||
for rule in self.struct_id.rule_ids:
|
||||
kategorie = rule.l10n_at_st_frei
|
||||
if (not kategorie or kategorie == 'keine'
|
||||
or rule.l10n_at_bemessung != 'laufend'):
|
||||
continue
|
||||
werte = None
|
||||
if result_rules is not None:
|
||||
if rule.code in result_rules:
|
||||
werte = result_rules[rule.code]
|
||||
else:
|
||||
linie = self.line_ids.filtered(lambda l: l.code == rule.code)
|
||||
if linie:
|
||||
werte = {'total': sum(linie.mapped('total')),
|
||||
'quantity': sum(linie.mapped('quantity'))}
|
||||
if not werte:
|
||||
continue
|
||||
total = werte['total']
|
||||
menge = werte.get('quantity', 0.0)
|
||||
if kategorie == 'mahlzeit':
|
||||
frei = min(
|
||||
total, float(grenzen.get('mahlzeit_tag')) * menge)
|
||||
elif kategorie == 'homeoffice':
|
||||
rest_tage = max(
|
||||
float(grenzen.get('homeoffice_tage'))
|
||||
- self._l10n_at_st_frei_ytd('homeoffice', menge=True),
|
||||
0.0)
|
||||
frei = min(
|
||||
total,
|
||||
float(grenzen.get('homeoffice_tag'))
|
||||
* min(menge, rest_tage))
|
||||
else:
|
||||
pool = {'rabatt': 'rabatt_jahr',
|
||||
'betriebsveranstaltung': 'betriebsveranstaltung_jahr',
|
||||
'jubilaeum': 'jubilaeum_jahr'}.get(kategorie)
|
||||
rest = max(
|
||||
float(grenzen.get(pool))
|
||||
- self._l10n_at_st_frei_ytd(kategorie), 0.0)
|
||||
frei = min(total, rest)
|
||||
frei_gesamt += max(frei, 0.0)
|
||||
return frei_gesamt
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# DN-SV (Bemessungsbestandteil § 62) — Basis/Sätze/Geringfügigkeit/
|
||||
# §-49-Kumulative/DG-Seite: models/sozialversicherung.py (GP4)
|
||||
@@ -217,11 +305,11 @@ class HrPayslip(models.Model):
|
||||
"""SV-BG sonstige Bezüge: HBG monatlich und SZ-Jahreshöchstbetrag
|
||||
(13.860) minus YTD-SZ-Basen (Anspruchsprinzip-Vorlauf GP4)."""
|
||||
self.ensure_one()
|
||||
sz = max(self._l10n_at_brutto('sonstig', result_rules), 0.0)
|
||||
sz = max(self._l10n_at_sv_brutto('sonstig', result_rules), 0.0)
|
||||
hbg = float(self._l10n_at_param('at_sv_hbg_monatlich'))
|
||||
jahreshbg = float(self._l10n_at_param('at_sv_hbg_sz_jaehrlich'))
|
||||
for slip in self._l10n_at_ytd_slaeufe():
|
||||
jahreshbg -= max(slip._l10n_at_brutto('sonstig'), 0.0)
|
||||
jahreshbg -= max(slip._l10n_at_sv_brutto('sonstig'), 0.0)
|
||||
return min(sz, hbg, max(jahreshbg, 0.0))
|
||||
|
||||
def _l10n_at_svdn_sz(self, result_rules=None):
|
||||
@@ -242,14 +330,16 @@ class HrPayslip(models.Model):
|
||||
# ------------------------------------------------------------------
|
||||
def _l10n_at_bemessung_laufen(self, result_rules=None):
|
||||
"""LSt-Bemessung laufend: Arbeitslohn − SV-DN (§ 62-Kette,
|
||||
inkl. AK/WF) − steuerfreie Bezüge § 68. Der volle Arbeitslohn
|
||||
ist lohnsteuerlich unbegrenzt (keine HBG-Cap für die LSt;
|
||||
die Cap gilt nur für die SV-Sätze)."""
|
||||
inkl. AK/WF) − steuerfreie Bezüge § 68 − steuerfreie Sachbezüge
|
||||
§ 3/§ 26 (Freibeträge, Jahres-Kumulative). Der volle
|
||||
Arbeitslohn ist lohnsteuerlich unbegrenzt (keine HBG-Cap für
|
||||
die LSt; die Cap gilt nur für die SV-Sätze)."""
|
||||
self.ensure_one()
|
||||
freibe = self._l10n_at_freibetraege_68(result_rules)
|
||||
return (self._l10n_at_brutto('laufend', result_rules)
|
||||
- self._l10n_at_svdn_gesamt(result_rules)
|
||||
- freibe['szfn'] - freibe['ue_std'])
|
||||
- freibe['szfn'] - freibe['ue_std']
|
||||
- self._l10n_at_st_frei_freie_laufen(result_rules))
|
||||
|
||||
def _l10n_at_sz_bemessung(self, result_rules=None):
|
||||
"""LSt-Bemessung sonstiger Bezug: Bezug − SV-DN (Abs 12);
|
||||
|
||||
@@ -37,6 +37,18 @@ from odoo.tools.safe_eval import safe_eval
|
||||
class HrSalaryRule(models.Model):
|
||||
_inherit = 'hr.salary.rule'
|
||||
|
||||
l10n_at_sv_art = fields.Selection(
|
||||
[('folgt', 'Folgt AT LSt-Bemessungsart'),
|
||||
('laufend', 'Laufender SV-Bezug'),
|
||||
('sonstig', 'Sonstiger SV-Bezug'),
|
||||
('keine', 'Kein SV-Bezug')],
|
||||
string='AT SV-Bemessungsart',
|
||||
default='folgt',
|
||||
help='Steuert ausschließlich die SV-Zuordnung. Standard „Folgt“ '
|
||||
'übernimmt die AT LSt-Bemessungsart und ist damit '
|
||||
'rückwärtskompatibel. Abweichung für Drittlohn oder '
|
||||
'abweichende LSt-/SV-Zuordnung, z. B. Bonusmeilen oder '
|
||||
'Hardware-Verkauf.')
|
||||
l10n_at_sv49_frei = fields.Selection(
|
||||
[('keine', 'Keine §-49-Abs-3-Befreiung'),
|
||||
('mahlzeit', 'Mahlzeiten-Gutschein (§ 49 Abs 3 Z 12; 8 €/AT)'),
|
||||
@@ -254,7 +266,7 @@ class HrPayslip(models.Model):
|
||||
for rule in self.struct_id.rule_ids:
|
||||
kategorie = rule.l10n_at_sv49_frei
|
||||
if (not kategorie or kategorie == 'keine'
|
||||
or rule.l10n_at_bemessung != 'laufend'):
|
||||
or self._l10n_at_sv_art(rule) != 'laufend'):
|
||||
continue
|
||||
werte = None
|
||||
if result_rules is not None:
|
||||
@@ -297,11 +309,33 @@ class HrPayslip(models.Model):
|
||||
# ------------------------------------------------------------------
|
||||
# Laufende Beitragsgrundlage — je Beitragsmonat (Anspruchsprinzip)
|
||||
# ------------------------------------------------------------------
|
||||
def _l10n_at_sv_art(self, rule):
|
||||
"""Effektive SV-Art einer Regel; Standard folgt der LSt-Art."""
|
||||
self.ensure_one()
|
||||
return (rule.l10n_at_bemessung if rule.l10n_at_sv_art == 'folgt'
|
||||
else rule.l10n_at_sv_art)
|
||||
|
||||
def _l10n_at_sv_brutto(self, art, result_rules=None):
|
||||
"""SV-Brutto je Art; abweichende SV-Zuordnung bleibt von der
|
||||
LSt-Bemessungsart getrennt (Drittlohn/Sonderfälle)."""
|
||||
self.ensure_one()
|
||||
total = 0.0
|
||||
for rule in self.struct_id.rule_ids:
|
||||
if self._l10n_at_sv_art(rule) != art:
|
||||
continue
|
||||
if result_rules is not None:
|
||||
if rule.code in result_rules:
|
||||
total += result_rules[rule.code].get('total', 0.0)
|
||||
else:
|
||||
total += sum(self.line_ids.filtered(
|
||||
lambda l: l.code == rule.code).mapped('total'))
|
||||
return total
|
||||
|
||||
def _l10n_at_sv_basis_roh(self, result_rules=None):
|
||||
"""Laufender Arbeitslohn minus beitragsfreie §-49-Anteile
|
||||
"""Laufendes SV-Brutto minus beitragsfreie §-49-Anteile
|
||||
(vor HBG-Cap; Geringfügigkeits-/Staffel-Basis)."""
|
||||
self.ensure_one()
|
||||
roh = self._l10n_at_brutto('laufend', result_rules) \
|
||||
roh = self._l10n_at_sv_brutto('laufend', result_rules) \
|
||||
- self._l10n_at_sv49_freie_laufen(result_rules)
|
||||
return max(roh, 0.0)
|
||||
|
||||
@@ -312,7 +346,7 @@ class HrPayslip(models.Model):
|
||||
self.ensure_one()
|
||||
gruppen = {}
|
||||
for rule in self.struct_id.rule_ids:
|
||||
if rule.l10n_at_bemessung != 'laufend':
|
||||
if self._l10n_at_sv_art(rule) != 'laufend':
|
||||
continue
|
||||
werte = None
|
||||
if result_rules is not None:
|
||||
|
||||
@@ -27,7 +27,8 @@ class LohnsteuerTest(TransactionCase):
|
||||
})
|
||||
|
||||
def _regel(self, code, betrag, bemessung='laufend', flag68='keine',
|
||||
anteil=0.0, quantity='1.0', python=None):
|
||||
anteil=0.0, quantity='1.0', python=None, st_frei='keine',
|
||||
sv_art='folgt'):
|
||||
vals = {
|
||||
'name': code,
|
||||
'code': code,
|
||||
@@ -37,6 +38,8 @@ class LohnsteuerTest(TransactionCase):
|
||||
'l10n_at_bemessung': bemessung,
|
||||
'l10n_at_steuerbefreiung_68': flag68,
|
||||
'l10n_at_zuschlagsanteil': anteil,
|
||||
'l10n_at_st_frei': st_frei,
|
||||
'l10n_at_sv_art': sv_art,
|
||||
}
|
||||
if python:
|
||||
vals.update({'amount_select': 'code',
|
||||
@@ -283,3 +286,113 @@ class LohnsteuerTest(TransactionCase):
|
||||
slip._l10n_at_sz_tarifisch_aktuell(), sz_bem - 4000.0, places=2)
|
||||
self.assertAlmostEqual(
|
||||
slip._l10n_at_lst_sonstige(), (4000.0 - 620.0) * 0.06, places=1)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# § 3/§ 26 — steuerfreie Sachbezüge (Freibeträge, Jahres-Kumulative)
|
||||
# ------------------------------------------------------------------
|
||||
def test_13_st_frei_mahlzeit(self):
|
||||
"""Mahlzeiten-Gutschein 11 €/AT × 10 AT = 110 €: 80 € steuerfrei
|
||||
(8 €/AT, § 3 Abs 1 Z 17), 30 € steuerpflichtig; LSt-Bemessung
|
||||
um 80 € reduziert (lb-sac-17)."""
|
||||
employee = self._employee()
|
||||
self._regel('LFD', 2000.0)
|
||||
self._regel('MAH', 11.0, quantity='10.0', st_frei='mahlzeit')
|
||||
slip = self._slip(employee, 1)
|
||||
self.assertAlmostEqual(
|
||||
slip._l10n_at_st_frei_freie_laufen(), 80.0, places=2)
|
||||
svdn = slip._l10n_at_svdn_gesamt()
|
||||
self.assertAlmostEqual(
|
||||
slip._l10n_at_bemessung_laufen(),
|
||||
2000.0 + 110.0 - svdn - 80.0, places=2)
|
||||
|
||||
def test_14_st_frei_jahrespools(self):
|
||||
"""Jahres-Kumulative: Betriebsveranstaltung 365 €/J (Freibetrag)
|
||||
und Sachzuwendung/Jubiläum 186 €/J als separate Pools
|
||||
(§ 3 Abs 1 Z 14; lb-sac-07, lb-ges-02)."""
|
||||
employee = self._employee()
|
||||
self._regel('LFD', 2000.0)
|
||||
self._regel('BV', 200.0, st_frei='betriebsveranstaltung')
|
||||
slip_a = self._slip(employee, 1, confirm=True)
|
||||
self.assertAlmostEqual(
|
||||
slip_a._l10n_at_st_frei_freie_laufen(), 200.0, places=2)
|
||||
# Februar: 165 € Rest (365 − 200) → 165 frei, 35 steuerpflichtig
|
||||
slip_b = self._slip(employee, 2, confirm=True)
|
||||
self.assertAlmostEqual(
|
||||
slip_b._l10n_at_st_frei_freie_laufen(), 165.0, places=2)
|
||||
# März: Pool verbraucht → 0 frei, 200 steuerpflichtig
|
||||
slip_c = self._slip(employee, 3)
|
||||
self.assertAlmostEqual(
|
||||
slip_c._l10n_at_st_frei_freie_laufen(), 0.0, places=2)
|
||||
# Sachzuwendung separater Pool: 300 € → 186 frei, 114 pflichtig
|
||||
self._regel('GSC', 300.0, st_frei='jubilaeum')
|
||||
slip_d = self._slip(employee, 4)
|
||||
self.assertAlmostEqual(
|
||||
slip_d._l10n_at_st_frei_freie_laufen(), 186.0, places=2)
|
||||
|
||||
def test_15_st_frei_homeoffice(self):
|
||||
"""Homeoffice 3 €/Tag, max 100 Tage/Jahr (§ 26 Z 9): 70 Tage im
|
||||
Jänner (210 frei), Februar nur 30 Tage frei (90), 40 Tage × 3 =
|
||||
120 steuerpflichtig."""
|
||||
employee = self._employee()
|
||||
self._regel('LFD', 2000.0)
|
||||
self._regel('HO', 3.0, quantity='70.0', st_frei='homeoffice')
|
||||
slip_a = self._slip(employee, 1, confirm=True)
|
||||
self.assertAlmostEqual(
|
||||
slip_a._l10n_at_st_frei_freie_laufen(), 210.0, places=2)
|
||||
# Februar: 30 Tage Rest (100 − 70) → 90 frei, 120 steuerpflichtig
|
||||
slip_b = self._slip(employee, 2)
|
||||
self.assertAlmostEqual(
|
||||
slip_b._l10n_at_st_frei_freie_laufen(), 90.0, places=2)
|
||||
|
||||
def test_16_st_frei_neutral(self):
|
||||
"""Ohne st_frei-Flag keine Wirkung auf die LSt-Bemessung
|
||||
(Regression)."""
|
||||
employee = self._employee()
|
||||
self._regel('LFD', 2000.0)
|
||||
slip = self._slip(employee, 1)
|
||||
self.assertAlmostEqual(slip._l10n_at_st_frei_freie_laufen(), 0.0)
|
||||
svdn = slip._l10n_at_svdn_gesamt()
|
||||
self.assertAlmostEqual(
|
||||
slip._l10n_at_bemessung_laufen(), 2000.0 - svdn, places=2)
|
||||
|
||||
def test_17_sv_art_entkoppelt_lst_art(self):
|
||||
"""SV-Art folgt standardmäßig der LSt-Art; Drittlohn kann nur
|
||||
SV-laufend sein, Hardware-Verkauf LSt-sonstig/SV-laufend."""
|
||||
employee = self._employee()
|
||||
self._regel('LFD', 100.0)
|
||||
self._regel('DRITT', 300.0, bemessung='neutral', sv_art='laufend')
|
||||
self._regel('HW', 200.0, bemessung='sonstig', sv_art='laufend')
|
||||
slip = self._slip(employee, 1)
|
||||
self.assertAlmostEqual(
|
||||
slip._l10n_at_sv_brutto('laufend'), 600.0, places=2)
|
||||
self.assertAlmostEqual(
|
||||
slip._l10n_at_sv_brutto('sonstig'), 0.0, places=2)
|
||||
self.assertAlmostEqual(slip._l10n_at_svdn_basis_sz(), 0.0, places=2)
|
||||
self.assertAlmostEqual(
|
||||
slip._l10n_at_brutto('laufend'), 100.0, places=2)
|
||||
self.assertAlmostEqual(
|
||||
slip._l10n_at_brutto('sonstig'), 200.0, places=2)
|
||||
|
||||
def test_18_sv_art_folgt_ist_neutral(self):
|
||||
"""Der Default 'folgt' erhält bestehende laufende und sonstige
|
||||
SV-Zuordnungen unverändert."""
|
||||
employee = self._employee()
|
||||
self._regel('LFD', 100.0)
|
||||
self._regel('SZ', 200.0, bemessung='sonstig')
|
||||
slip = self._slip(employee, 1)
|
||||
self.assertAlmostEqual(
|
||||
slip._l10n_at_sv_brutto('laufend'), 100.0, places=2)
|
||||
self.assertAlmostEqual(
|
||||
slip._l10n_at_sv_brutto('sonstig'), 200.0, places=2)
|
||||
|
||||
def test_19_sv_art_keine_nur_lst(self):
|
||||
"""SV-Art 'keine' schließt eine Regel ausschließlich aus der
|
||||
SV aus; die deklarierte LSt-Bemessung bleibt unverändert."""
|
||||
employee = self._employee()
|
||||
self._regel('LFD', 100.0)
|
||||
self._regel('NUR_LST', 300.0, sv_art='keine')
|
||||
slip = self._slip(employee, 1)
|
||||
self.assertAlmostEqual(
|
||||
slip._l10n_at_sv_brutto('laufend'), 100.0, places=2)
|
||||
self.assertAlmostEqual(
|
||||
slip._l10n_at_brutto('laufend'), 400.0, places=2)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Part of the odoo-at-payroll project. License: LGPL-3.
|
||||
{
|
||||
'name': 'Österreich - Privatwirtschaft Personalverrechnung (General-AT)',
|
||||
'version': '19.0.9.5.0',
|
||||
'version': '19.0.9.7.0',
|
||||
'author': 'Florian Egger',
|
||||
'website': 'https://gem360.at',
|
||||
'category': 'Human Resources/Payroll',
|
||||
@@ -110,6 +110,8 @@ gem360-Brücke).
|
||||
'data/salary_rules_msch_private.xml',
|
||||
|
||||
'data/salary_rules_pfaendung_private.xml',
|
||||
'data/salary_rules_sb_private.xml',
|
||||
'data/salary_rules_sb2_private.xml',
|
||||
'data/salary_rules_sv_private.xml',
|
||||
'data/salary_rules_bvg_private.xml',
|
||||
'data/salary_rules_bvg_private.xml',
|
||||
@@ -119,6 +121,8 @@ gem360-Brücke).
|
||||
'data/rule_parameters_efzg_private.xml',
|
||||
'data/rule_parameters_msch_private.xml',
|
||||
'data/rule_parameters_pfaendung_private.xml',
|
||||
'data/rule_parameters_sb_private.xml',
|
||||
'data/rule_parameters_sb2_private.xml',
|
||||
'data/bv_kasse_seed.xml',
|
||||
'views/kv_views.xml',
|
||||
'views/bv_views.xml',
|
||||
@@ -127,6 +131,7 @@ gem360-Brücke).
|
||||
'views/mitarbeiter_views.xml',
|
||||
'views/aeb_views.xml',
|
||||
'views/pfaendung_views.xml',
|
||||
'views/sachbezuege_views.xml',
|
||||
'report/gp8_reports.xml',
|
||||
'report/buchungsbeleg_report.xml',
|
||||
'report/bv_report.xml',
|
||||
|
||||
@@ -7,4 +7,47 @@
|
||||
<field name="name">Prämie/Ausgleich (manuell)</field>
|
||||
<field name="code">ATP_PRAMIE</field>
|
||||
</record>
|
||||
|
||||
<!-- AP12a: steuerfreie Sachbezüge (§ 3/§ 26 EStG, § 49 Abs 3 ASVG) -->
|
||||
<!-- als manuelle Eingaben. Menge (number_of_days) = Arbeitstage -->
|
||||
<!-- (Mahlzeiten/Homeoffice), amount = Tages-/Pauschalwert; die -->
|
||||
<!-- Freibetrags-/Kumulationslogik läuft über die SB_*-Regeln. -->
|
||||
<record id="input_type_atp_sb_mahlzeit" model="hr.payslip.input.type">
|
||||
<field name="name">Sachbezug Mahlzeiten — Anz. Gutschein-Arbeitstage (manuell)</field>
|
||||
<field name="code">ATP_SB_MAHLZEIT</field>
|
||||
<field name="is_quantity" eval="True"/>
|
||||
</record>
|
||||
<record id="input_type_atp_sb_betriebsveranstaltung" model="hr.payslip.input.type">
|
||||
<field name="name">Sachbezug Betriebsveranstaltung — Gesamtvalue (manuell)</field>
|
||||
<field name="code">ATP_SB_BETRIEBSVERANSTALTUNG</field>
|
||||
</record>
|
||||
<record id="input_type_atp_sb_sachzuwendung" model="hr.payslip.input.type">
|
||||
<field name="name">Sachbezug Sachzuwendung/Geschenk — Gesamtvalue (manuell)</field>
|
||||
<field name="code">ATP_SB_SACHZUWENDUNG</field>
|
||||
</record>
|
||||
<record id="input_type_atp_sb_rabatt" model="hr.payslip.input.type">
|
||||
<field name="name">Sachbezug Mitarbeiterrabatt — Gesamtvalue (manuell)</field>
|
||||
<field name="code">ATP_SB_RABATT</field>
|
||||
</record>
|
||||
<record id="input_type_atp_sb_homeoffice" model="hr.payslip.input.type">
|
||||
<field name="name">Sachbezug Homeoffice — Anz. Tage (manuell)</field>
|
||||
<field name="code">ATP_SB_HOMEOFFICE</field>
|
||||
<field name="is_quantity" eval="True"/>
|
||||
</record>
|
||||
|
||||
<!-- AP12b: ereignisbezogene Werte. Amount ist jeweils der bereits
|
||||
ermittelte Vorteil (tatsächliche Mehrkosten bzw. Buchwert minus
|
||||
Kaufpreis); Stammdaten steuern Wagen/Wohnung/Parkplatz. -->
|
||||
<record id="input_type_atp_sb_handy" model="hr.payslip.input.type">
|
||||
<field name="name">Sachbezug Handy — private Mehrkosten (manuell)</field>
|
||||
<field name="code">ATP_SB_HANDY</field>
|
||||
</record>
|
||||
<record id="input_type_atp_sb_gebraucht" model="hr.payslip.input.type">
|
||||
<field name="name">Sachbezug Arbeitsmittel-Verkauf unter Buchwert (manuell)</field>
|
||||
<field name="code">ATP_SB_GEBRAUCHT</field>
|
||||
</record>
|
||||
<record id="input_type_atp_sb_bonusmeilen" model="hr.payslip.input.type">
|
||||
<field name="name">Sachbezug Bonusmeilen — Einlösungswert (manuell)</field>
|
||||
<field name="code">ATP_SB_BONUSMEILEN</field>
|
||||
</record>
|
||||
</odoo>
|
||||
@@ -0,0 +1,45 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<!-- AP12b: Sachbezugswerte-VO — Jahresversionen, keine Literale in
|
||||
den Regeln. Rechtsanker: lb-sac-03/04/09/18. -->
|
||||
<record id="hr_rule_parameter_at_sb_wagen" model="hr.rule.parameter">
|
||||
<field name="name">Sachbezug Dienstwagen — Sätze und Höchstwerte</field>
|
||||
<field name="code">at_sb_wagen</field>
|
||||
<field name="country_id" ref="base.at"/>
|
||||
</record>
|
||||
<record id="hr_rule_parameter_value_at_sb_wagen" model="hr.rule.parameter.value">
|
||||
<field name="rule_parameter_id" ref="hr_rule_parameter_at_sb_wagen"/>
|
||||
<field name="date_from">2026-01-01</field>
|
||||
<field name="parameter_value">{'voll_hoch_satz': 2.0, 'voll_hoch_cap': 960.0, 'voll_niedrig_satz': 1.5, 'voll_niedrig_cap': 720.0, 'halb_hoch_satz': 1.0, 'halb_hoch_cap': 480.0, 'halb_niedrig_satz': 0.75, 'halb_niedrig_cap': 360.0}</field>
|
||||
</record>
|
||||
<record id="hr_rule_parameter_at_sb_wagen_co2" model="hr.rule.parameter">
|
||||
<field name="name">Sachbezug Dienstwagen — CO₂-Grenzwerte</field>
|
||||
<field name="code">at_sb_wagen_co2</field>
|
||||
<field name="country_id" ref="base.at"/>
|
||||
</record>
|
||||
<record id="hr_rule_parameter_value_at_sb_wagen_co2" model="hr.rule.parameter.value">
|
||||
<field name="rule_parameter_id" ref="hr_rule_parameter_at_sb_wagen_co2"/>
|
||||
<field name="date_from">2026-01-01</field>
|
||||
<field name="parameter_value">{'vor_2016': 130.0, '2017': 127.0, '2018': 124.0, '2019': 121.0, '2020': 141.0, '2021': 138.0, '2022': 135.0, '2023': 132.0, '2024': 129.0, 'ab_2025': 126.0}</field>
|
||||
</record>
|
||||
<record id="hr_rule_parameter_at_sb_wohnung" model="hr.rule.parameter">
|
||||
<field name="name">Sachbezug Dienstwohnung — m²-Werte und Abschläge</field>
|
||||
<field name="code">at_sb_wohnung</field>
|
||||
<field name="country_id" ref="base.at"/>
|
||||
</record>
|
||||
<record id="hr_rule_parameter_value_at_sb_wohnung" model="hr.rule.parameter.value">
|
||||
<field name="rule_parameter_id" ref="hr_rule_parameter_at_sb_wohnung"/>
|
||||
<field name="date_from">2026-01-01</field>
|
||||
<field name="parameter_value">{'burgenland': 6.09, 'kaernten': 7.81, 'niederoesterreich': 6.85, 'oberoesterreich': 7.23, 'salzburg': 9.22, 'steiermark': 9.21, 'tirol': 8.14, 'vorarlberg': 10.25, 'wien': 6.67, 'nicht_norm_abschlag': 0.30, 'besorger_abschlag': 0.35, 'kombiniert_abschlag': 0.555, 'klein_frei_m2': 35.0, 'klein_abschlag_bis_m2': 45.0, 'klein_abschlag': 0.35}</field>
|
||||
</record>
|
||||
<record id="hr_rule_parameter_at_sb_parkplatz" model="hr.rule.parameter">
|
||||
<field name="name">Sachbezug Parkplatz — Monatswert</field>
|
||||
<field name="code">at_sb_parkplatz</field>
|
||||
<field name="country_id" ref="base.at"/>
|
||||
</record>
|
||||
<record id="hr_rule_parameter_value_at_sb_parkplatz" model="hr.rule.parameter.value">
|
||||
<field name="rule_parameter_id" ref="hr_rule_parameter_at_sb_parkplatz"/>
|
||||
<field name="date_from">2026-01-01</field>
|
||||
<field name="parameter_value">14.53</field>
|
||||
</record>
|
||||
</odoo>
|
||||
@@ -0,0 +1,24 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<!-- ============================================================ -->
|
||||
<!-- AP12a: Sachbezüge — tagesbasierte Werte (tatsächlicher -->
|
||||
<!-- Wert pro Arbeitstag). Mahlzeiten: tatsächlicher Bonwert -->
|
||||
<!-- (Default = 8 €/AT = Freibetrag, d. h. vollständig frei; -->
|
||||
<!-- Company setzt den tatsächlichen Bonwert, z. B. 11,0 — der -->
|
||||
<!-- Überhang (Wert−8)×Tage ist steuer-/beitragspflichtig, -->
|
||||
<!-- lb-sac-17). Homeoffice: gesetzlich fix 3 €/AT (lb-tel-04/05, -->
|
||||
<!-- § 26 Z 9 EStG). Freibetrags-Caps selbst liegen im Kern -->
|
||||
<!-- (at_st_grenzen / at_sv49_grenzen). -->
|
||||
<!-- ============================================================ -->
|
||||
<record id="hr_rule_parameter_at_sb_werte" model="hr.rule.parameter">
|
||||
<field name="name">Sachbezüge — tagesbasierte Werte</field>
|
||||
<field name="code">at_sb_werte</field>
|
||||
<field name="country_id" ref="base.at"/>
|
||||
</record>
|
||||
<record id="hr_rule_parameter_value_at_sb_werte"
|
||||
model="hr.rule.parameter.value">
|
||||
<field name="rule_parameter_id" ref="hr_rule_parameter_at_sb_werte"/>
|
||||
<field name="date_from">2026-01-01</field>
|
||||
<field name="parameter_value">{'mahlzeit_wert': 8.0, 'homeoffice_wert': 3.0}</field>
|
||||
</record>
|
||||
</odoo>
|
||||
@@ -0,0 +1,81 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<!-- AP12b: wertgebundene Sachbezüge. Wagen/Wohnung/Parkplatz sind
|
||||
Version-Stammdaten; Handy/Verkauf/Bonusmeilen Ereignis-Eingaben.
|
||||
Die Regeln stehen vor B13/B14 (50ff.) und SV/LSt (60ff.). -->
|
||||
<record id="rule_sb_parkplatz" model="hr.salary.rule">
|
||||
<field name="name">Sachbezug Parkplatz am Dienstort</field>
|
||||
<field name="code">SB_PARKPLATZ</field>
|
||||
<field name="sequence" eval="53"/>
|
||||
<field name="category_id" ref="l10n_at_hr_payroll_private.category_atp_brutto"/>
|
||||
<field name="struct_id" ref="l10n_at_hr_payroll_private.structure_at_privat"/>
|
||||
<field name="condition_select">python</field>
|
||||
<field name="condition_python">result = payslip.version_id._l10n_at_sb_parkplatz_wert(payslip.date_to) > 0</field>
|
||||
<field name="amount_select">code</field>
|
||||
<field name="l10n_at_bemessung">laufend</field>
|
||||
<field name="amount_python_compute">result = payslip.version_id._l10n_at_sb_parkplatz_wert(payslip.date_to)</field>
|
||||
</record>
|
||||
<record id="rule_sb_wohnung" model="hr.salary.rule">
|
||||
<field name="name">Sachbezug Dienstwohnung</field>
|
||||
<field name="code">SB_WOHNUNG</field>
|
||||
<field name="sequence" eval="54"/>
|
||||
<field name="category_id" ref="l10n_at_hr_payroll_private.category_atp_brutto"/>
|
||||
<field name="struct_id" ref="l10n_at_hr_payroll_private.structure_at_privat"/>
|
||||
<field name="condition_select">python</field>
|
||||
<field name="condition_python">result = payslip.version_id._l10n_at_sb_wohnung_wert(payslip.date_to) > 0</field>
|
||||
<field name="amount_select">code</field>
|
||||
<field name="l10n_at_bemessung">laufend</field>
|
||||
<field name="amount_python_compute">result = payslip.version_id._l10n_at_sb_wohnung_wert(payslip.date_to)</field>
|
||||
</record>
|
||||
<record id="rule_sb_wagen" model="hr.salary.rule">
|
||||
<field name="name">Sachbezug Dienstwagen Privatnutzung</field>
|
||||
<field name="code">SB_WAGEN</field>
|
||||
<field name="sequence" eval="55"/>
|
||||
<field name="category_id" ref="l10n_at_hr_payroll_private.category_atp_brutto"/>
|
||||
<field name="struct_id" ref="l10n_at_hr_payroll_private.structure_at_privat"/>
|
||||
<field name="condition_select">python</field>
|
||||
<field name="condition_python">result = payslip.version_id._l10n_at_sb_wagen_wert(payslip.date_to) > 0</field>
|
||||
<field name="amount_select">code</field>
|
||||
<field name="l10n_at_bemessung">laufend</field>
|
||||
<field name="amount_python_compute">result = payslip.version_id._l10n_at_sb_wagen_wert(payslip.date_to)</field>
|
||||
</record>
|
||||
<record id="rule_sb_gebraucht" model="hr.salary.rule">
|
||||
<field name="name">Sachbezug Arbeitsmittel-Verkauf unter Buchwert</field>
|
||||
<field name="code">SB_GEBRAUCHT</field>
|
||||
<field name="sequence" eval="56"/>
|
||||
<field name="category_id" ref="l10n_at_hr_payroll_private.category_atp_brutto"/>
|
||||
<field name="struct_id" ref="l10n_at_hr_payroll_private.structure_at_privat"/>
|
||||
<field name="condition_select">python</field>
|
||||
<field name="condition_python">result = 'ATP_SB_GEBRAUCHT' in inputs and inputs['ATP_SB_GEBRAUCHT'].amount</field>
|
||||
<field name="amount_select">code</field>
|
||||
<field name="l10n_at_bemessung">sonstig</field>
|
||||
<field name="l10n_at_sv_art">laufend</field>
|
||||
<field name="amount_python_compute">result = inputs['ATP_SB_GEBRAUCHT'].amount</field>
|
||||
</record>
|
||||
<record id="rule_sb_bonusmeilen" model="hr.salary.rule">
|
||||
<field name="name">Sachbezug Bonusmeilen (Drittlohn, nur SV)</field>
|
||||
<field name="code">SB_BONUSMEILEN</field>
|
||||
<field name="sequence" eval="57"/>
|
||||
<!-- Drittlohn erhöht nicht den Auszahlungsnettoanspruch. -->
|
||||
<field name="category_id" ref="l10n_at_hr_payroll_private.category_atp_dg"/>
|
||||
<field name="struct_id" ref="l10n_at_hr_payroll_private.structure_at_privat"/>
|
||||
<field name="condition_select">python</field>
|
||||
<field name="condition_python">result = 'ATP_SB_BONUSMEILEN' in inputs and inputs['ATP_SB_BONUSMEILEN'].amount</field>
|
||||
<field name="amount_select">code</field>
|
||||
<field name="l10n_at_bemessung">neutral</field>
|
||||
<field name="l10n_at_sv_art">laufend</field>
|
||||
<field name="amount_python_compute">result = inputs['ATP_SB_BONUSMEILEN'].amount</field>
|
||||
</record>
|
||||
<record id="rule_sb_handy" model="hr.salary.rule">
|
||||
<field name="name">Sachbezug Handy — private Mehrkosten</field>
|
||||
<field name="code">SB_HANDY</field>
|
||||
<field name="sequence" eval="58"/>
|
||||
<field name="category_id" ref="l10n_at_hr_payroll_private.category_atp_brutto"/>
|
||||
<field name="struct_id" ref="l10n_at_hr_payroll_private.structure_at_privat"/>
|
||||
<field name="condition_select">python</field>
|
||||
<field name="condition_python">result = payslip.version_id.l10n_at_sb_handy_profil == 'ueberwiegend_privat' and 'ATP_SB_HANDY' in inputs and inputs['ATP_SB_HANDY'].amount</field>
|
||||
<field name="amount_select">code</field>
|
||||
<field name="l10n_at_bemessung">laufend</field>
|
||||
<field name="amount_python_compute">result = inputs['ATP_SB_HANDY'].amount</field>
|
||||
</record>
|
||||
</odoo>
|
||||
@@ -0,0 +1,129 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<!-- ============================================================ -->
|
||||
<!-- AP12a: steuerfreie Sachbezüge — Freibeträge nach -->
|
||||
<!-- § 3/§ 26 EStG, beitragsfrei § 49 Abs 3 ASVG, lohnneben- -->
|
||||
<!-- kostenfrei (lb-sac-17/07/02, lb-ges-02). -->
|
||||
<!-- Input-Semantik: -->
|
||||
<!-- - tagesbasiert (Mahlzeiten/Homeoffice): amount = Anzahl -->
|
||||
<!-- Arbeitstage; der Tageswert kommt aus dem versionierten -->
|
||||
<!-- Parameter at_sb_werte (Kern-Mechanik: total = Wert×Tage, -->
|
||||
<!-- Freibetrag = min(total, Cap×Tage)). -->
|
||||
<!-- - Pool-basiert (BV/Rabatt/Geschenk): amount = Gesamtvalue -->
|
||||
<!-- des Monats; Freibetrag = min(total, Jahrespool − YTD). -->
|
||||
<!-- Die Freibetrags-/Jahreskumulationslogik läuft im Kern über -->
|
||||
<!-- l10n_at_sv49_frei (SV) und l10n_at_st_frei (LSt/LNK). -->
|
||||
<!-- Nur der übersteigende Teil ist beitrags-/steuerpflichtig -->
|
||||
<!-- (Freibeträge, keine Freigrenzen). -->
|
||||
<!-- ============================================================ -->
|
||||
|
||||
<record id="rule_sb_mahlzeit" model="hr.salary.rule">
|
||||
<field name="name">Sachbezug Mahlzeiten-Gutschein (manuell)</field>
|
||||
<field name="code">SB_MAHLZEIT</field>
|
||||
<field name="sequence" eval="41"/>
|
||||
<field name="category_id" ref="l10n_at_hr_payroll_private.category_atp_brutto"/>
|
||||
<field name="struct_id" ref="l10n_at_hr_payroll_private.structure_at_privat"/>
|
||||
<field name="condition_select">python</field>
|
||||
<field name="condition_python">result = 'ATP_SB_MAHLZEIT' in inputs and inputs['ATP_SB_MAHLZEIT'].amount</field>
|
||||
<field name="amount_select">code</field>
|
||||
<field name="l10n_at_bemessung">laufend</field>
|
||||
<field name="l10n_at_sv49_frei">mahlzeit</field>
|
||||
<field name="l10n_at_st_frei">mahlzeit</field>
|
||||
<field name="amount_python_compute"><![CDATA[
|
||||
# § 3 Abs 1 Z 17 EStG / § 49 Abs 3 Z 12 ASVG: Gutschein bis 8 €/
|
||||
# Arbeitstag frei (Kern-Cap at_st_grenzen.mahlzeit_tag). Input =
|
||||
# Anz. Arbeitstage; Tageswert = tatsächlicher Bonwert (at_sb_werte).
|
||||
# total = Wert × Tage (Engine: amount×qty); Überhang (Wert−8)×Tage
|
||||
# pflichtig (lb-sac-17).
|
||||
tage = inputs['ATP_SB_MAHLZEIT'].amount
|
||||
wert = float(payslip._l10n_at_param('at_sb_werte')['mahlzeit_wert'])
|
||||
result = wert
|
||||
result_qty = tage
|
||||
]]></field>
|
||||
</record>
|
||||
|
||||
<record id="rule_sb_betriebsveranstaltung" model="hr.salary.rule">
|
||||
<field name="name">Sachbezug Betriebsveranstaltung (manuell)</field>
|
||||
<field name="code">SB_BETRIEBSVERANSTALTUNG</field>
|
||||
<field name="sequence" eval="42"/>
|
||||
<field name="category_id" ref="l10n_at_hr_payroll_private.category_atp_brutto"/>
|
||||
<field name="struct_id" ref="l10n_at_hr_payroll_private.structure_at_privat"/>
|
||||
<field name="condition_select">python</field>
|
||||
<field name="condition_python">result = 'ATP_SB_BETRIEBSVERANSTALTUNG' in inputs and inputs['ATP_SB_BETRIEBSVERANSTALTUNG'].amount</field>
|
||||
<field name="amount_select">code</field>
|
||||
<field name="l10n_at_bemessung">laufend</field>
|
||||
<field name="l10n_at_sv49_frei">betriebsveranstaltung</field>
|
||||
<field name="l10n_at_st_frei">betriebsveranstaltung</field>
|
||||
<field name="amount_python_compute"><![CDATA[
|
||||
# § 3 Abs 1 Z 14 EStG / § 49 Abs 3 Z 17 ASVG: Teilnahme bis 365 €/
|
||||
# AN/Kalenderjahr frei (Jahres-Kumulative je DN); Überhang
|
||||
# lohnsteuer-/beitragspflichtig (Freibetrag, lb-sac-07).
|
||||
result = inputs['ATP_SB_BETRIEBSVERANSTALTUNG'].amount
|
||||
]]></field>
|
||||
</record>
|
||||
|
||||
<record id="rule_sb_sachzuwendung" model="hr.salary.rule">
|
||||
<field name="name">Sachbezug Sachzuwendung/Jubiläum/Geschenk (manuell)</field>
|
||||
<field name="code">SB_SACHZUWENDUNG</field>
|
||||
<field name="sequence" eval="43"/>
|
||||
<field name="category_id" ref="l10n_at_hr_payroll_private.category_atp_brutto"/>
|
||||
<field name="struct_id" ref="l10n_at_hr_payroll_private.structure_at_privat"/>
|
||||
<field name="condition_select">python</field>
|
||||
<field name="condition_python">result = 'ATP_SB_SACHZUWENDUNG' in inputs and inputs['ATP_SB_SACHZUWENDUNG'].amount</field>
|
||||
<field name="amount_select">code</field>
|
||||
<field name="l10n_at_bemessung">laufend</field>
|
||||
<field name="l10n_at_sv49_frei">jubilaeum</field>
|
||||
<field name="l10n_at_st_frei">jubilaeum</field>
|
||||
<field name="amount_python_compute"><![CDATA[
|
||||
# § 3 Abs 1 Z 14 EStG / § 49 Abs 3 Z 17 ASVG: Sachzuwendungen
|
||||
# (Jubiläums-/Weihnachtsgeschenke) bis 186 €/AN/Kalenderjahr frei —
|
||||
# eigener Pool neben der Betriebsveranstaltung (lb-ges-02, lb-sac-07);
|
||||
# Barzuwendungen immer pflichtig (Regel nur für Sachwerte).
|
||||
result = inputs['ATP_SB_SACHZUWENDUNG'].amount
|
||||
]]></field>
|
||||
</record>
|
||||
|
||||
<record id="rule_sb_rabatt" model="hr.salary.rule">
|
||||
<field name="name">Sachbezug Mitarbeiterrabatt (manuell)</field>
|
||||
<field name="code">SB_RABATT</field>
|
||||
<field name="sequence" eval="44"/>
|
||||
<field name="category_id" ref="l10n_at_hr_payroll_private.category_atp_brutto"/>
|
||||
<field name="struct_id" ref="l10n_at_hr_payroll_private.structure_at_privat"/>
|
||||
<field name="condition_select">python</field>
|
||||
<field name="condition_python">result = 'ATP_SB_RABATT' in inputs and inputs['ATP_SB_RABATT'].amount</field>
|
||||
<field name="amount_select">code</field>
|
||||
<field name="l10n_at_bemessung">laufend</field>
|
||||
<field name="l10n_at_sv49_frei">rabatt</field>
|
||||
<field name="l10n_at_st_frei">rabatt</field>
|
||||
<field name="amount_python_compute"><![CDATA[
|
||||
# § 3 Abs 1 Z 21 EStG / § 49 Abs 3 Z 29 ASVG: Rabatte über der
|
||||
# 20 %-Freigrenze bis 1.000 €/AN/Jahr frei (Jahres-Kumulative);
|
||||
# darüber pflichtig (lb-sac-02). Aufzeichnungspflicht des
|
||||
# Arbeitgebers bleibt außen (Lohnkonto-Prüffeld).
|
||||
result = inputs['ATP_SB_RABATT'].amount
|
||||
]]></field>
|
||||
</record>
|
||||
|
||||
<record id="rule_sb_homeoffice" model="hr.salary.rule">
|
||||
<field name="name">Sachbezug Homeoffice-Pauschale (manuell)</field>
|
||||
<field name="code">SB_HOMEOFFICE</field>
|
||||
<field name="sequence" eval="48"/>
|
||||
<field name="category_id" ref="l10n_at_hr_payroll_private.category_atp_brutto"/>
|
||||
<field name="struct_id" ref="l10n_at_hr_payroll_private.structure_at_privat"/>
|
||||
<field name="condition_select">python</field>
|
||||
<field name="condition_python">result = 'ATP_SB_HOMEOFFICE' in inputs and inputs['ATP_SB_HOMEOFFICE'].amount</field>
|
||||
<field name="amount_select">code</field>
|
||||
<field name="l10n_at_bemessung">laufend</field>
|
||||
<field name="l10n_at_sv49_frei">homeoffice</field>
|
||||
<field name="l10n_at_st_frei">homeoffice</field>
|
||||
<field name="amount_python_compute"><![CDATA[
|
||||
# § 26 Z 9 EStG / § 49 Abs 3 Z 31 ASVG: Homeoffice-Pauschale 3 €/
|
||||
# Arbeitstag, max 100 Tage/Kalenderjahr (Jahres-Kumulative). Input =
|
||||
# Anz. Tage; Tageswert = 3 € (at_sb_werte.homeoffice_wert).
|
||||
tage = inputs['ATP_SB_HOMEOFFICE'].amount
|
||||
wert = float(payslip._l10n_at_param('at_sb_werte')['homeoffice_wert'])
|
||||
result = wert
|
||||
result_qty = tage
|
||||
]]></field>
|
||||
</record>
|
||||
</odoo>
|
||||
@@ -0,0 +1,173 @@
|
||||
# Rechenfälle steuerfreie Sachbezüge (AP12a)
|
||||
|
||||
Kern: Feld `hr.salary_rule.l10n_at_st_frei` + Methoden
|
||||
`_l10n_at_st_frei_ytd`/`_l10n_at_st_frei_freie_laufen` (Anbindung
|
||||
`_l10n_at_bemessung_laufen`) in `l10n_at_hr_payroll/models/lohnsteuer.py`,
|
||||
Parameter `at_st_grenzen` (`l10n_at_hr_payroll/data/rule_parameters_lst_2026.xml`).
|
||||
Privat: Eingabetypen `ATP_SB_*` (`data/input_types_private.xml`), Regeln
|
||||
`SB_MAHLZEIT`/`SB_BETRIEBSVERANSTALTUNG`/`SB_SACHZUWENDUNG`/`SB_RABATT`/
|
||||
`SB_HOMEOFFICE` (`data/salary_rules_sb_private.xml`), Parameter
|
||||
`at_sb_werte` (`data/rule_parameters_sb_private.xml`), LNK-Anbindung
|
||||
`_at_lnk_basis` (`models/payslip_private.py`). SV-Seite: bestehendes
|
||||
`l10n_at_sv49_frei`/`at_sv49_grenzen` (die SB-Regeln setzen beide Flags).
|
||||
|
||||
## Rechtsquellen (verifiziert 2026-09-13)
|
||||
|
||||
- **Mahlzeiten** (lb-sac-17): § 3 Abs 1 Z 17 EStG / § 49 Abs 3 Z 12 ASVG —
|
||||
„großer" Essensbon **€ 8,00/Arbeitstag** steuer- und beitragsfrei
|
||||
(Voraussetzungen: keine Verpflegungszulage u. a., sonst nur € 2,00);
|
||||
Jahresbemessung 220 Arbeitstage; Übersteigerung pflichtig.
|
||||
- **Betriebsveranstaltungen** (lb-sac-07, lb-sva-03): § 3 Abs 1 Z 14 EStG /
|
||||
§ 49 Abs 3 Z 17 ASVG — Teilnahme bis **€ 365,–/AN/Kalenderjahr** frei;
|
||||
keine Barablöse.
|
||||
- **Sachzuwendungen/Geschenke** (lb-ges-02): § 3 Abs 1 Z 14 EStG —
|
||||
Weihnachtsgeschenke sowie Dienst-/Firmenjubiläen je bis **€ 186,–/Jahr**
|
||||
(gesonderter Pool neben der BV); Barzuwendungen immer pflichtig.
|
||||
- **Mitarbeiterrabatte** (lb-sac-02): § 3 Abs 1 Z 21 EStG / § 49 Abs 3
|
||||
Z 29 ASVG — Rabatte **über 20 %** mit **€-1.000,–-Freibetrag/Jahr** je
|
||||
AN/AG; darunter keine Bezug; darüber abgabenpflichtig (laufend oder
|
||||
jährlicher Sachbezug).
|
||||
- **Homeoffice/Telearbeit** (lb-tel-04/05): § 26 Z 9 EStG / § 49 Abs 3
|
||||
Z 31 ASVG — Telearbeit-Pauschale **€ 3/Tag, max 100 Tage/Jahr**
|
||||
(= € 300/Jahr) nicht steuerbar; Lohnkonto: Anzahl Telearbeitstage
|
||||
erfassen.
|
||||
- Alle Freibeträge: **nur der übersteigende Teil** ist beitrags-/steuer-
|
||||
pflichtig (Freibeträge, keine Freigrenzen), lohnnebenkostenfrei.
|
||||
|
||||
## Maschinenmodell (2026)
|
||||
|
||||
- **Eingabe-Semantik** (manuell, `hr.payslip.input.amount`):
|
||||
- tagesbasiert (Mahlzeiten/Homeoffice): `amount` = **Anz. Arbeitstage**;
|
||||
Tageswert aus `at_sb_werte` (`mahlzeit_wert` Default 8,0 = tatsächl.
|
||||
Bonwert, `homeoffice_wert` 3,0 = gesetzlich fix); Regel: `result =
|
||||
wert`, `result_qty = tage` → Engine: `total = wert × tage`,
|
||||
`quantity = tage` (für die Cap-Berechnung Cap × Tage).
|
||||
- poolbasiert (BV/Sachzuwendung/Rabatt): `amount` = **Gesamtwert des
|
||||
Monats**; Regel: `result = amount` (`quantity` = 1).
|
||||
- **Freibetrag je Lauf** (Kern, `_l10n_at_st_frei_freie_laufen`,
|
||||
Spieglung `_l10n_at_sv49_freie_laufen`): tagesbasiert
|
||||
`frei = min(total, cap × menge)`; Homeoffice zusätzlich
|
||||
`rest_tage = 100 − YTD-Menge`, poolbasiert `frei = min(total,
|
||||
pool − YTD)`. YTD = Summe der Zeilen (Menge bzw. Total) **validierter**
|
||||
früherer Läufe des Kalenderjahres je DN (`_l10n_at_ytd_slaeufe`).
|
||||
- **Bemessungen**: LSt-BG laufend = Arbeitslohn − SVDN − § 68 −
|
||||
`st_frei`; LNK-BG = (laufend + im Monat gezahlter § 67) − `st_frei`
|
||||
− § 67 Abs 3/6 → SV-DN sind aus der LSt-BG abzugsfähig, aus der
|
||||
LNK-BG nicht (Zuflussprinzip, kein SV-Abzug).
|
||||
- **Caps** (`at_st_grenzen`, `at_sv49_grenzen`, 2026): Mahlzeit 8,0 €/AT ·
|
||||
Rabatt 1.000 €/J · BV 365 €/J · Jubiläum 186 €/J · Homeoffice
|
||||
3,0 €/AT, 100 Tage/J.
|
||||
|
||||
## RF-SB-1 — Mahlzeiten voll im Freibetrag
|
||||
|
||||
10 Gutschein-Arbeitstage à 8,0 € (`mahlzeit_wert`):
|
||||
|
||||
- `SB_MAHLZEIT`: total **80,00 €**, quantity 10.
|
||||
- frei (SV + LSt) = min(80, 8 × 10) = **80,00 €** → SV-BG, LSt-BG und
|
||||
LNK-BG **unverändert** gegenüber dem Lauf ohne SB (Tests test_01).
|
||||
|
||||
## RF-SB-2 — Mahlzeiten mit Überhang (Bonwert 11,00 €)
|
||||
|
||||
`at_sb_werte.mahlzeit_wert` = 11,00 (versioniert), 10 AT:
|
||||
|
||||
- `SB_MAHLZEIT`: total **110,00 €** (11 × 10), quantity 10.
|
||||
- frei = min(110, 8 × 10) = **80,00 €** → Überhang **30,00 €**.
|
||||
- SV-BG +30,00 · LNK-BG +30,00 · LSt-BG +30,00 **abzüglich** der darauf
|
||||
entfallenden SV-DN (SV abzugsfähig: ΔLSt + ΔSVDN = 30,00 €) (test_02).
|
||||
|
||||
## RF-SB-3 — Betriebsveranstaltungen (365-€-Pool)
|
||||
|
||||
Jänner 300 € (validiert), Februar 100 €:
|
||||
|
||||
- Jänner: frei min(300, 365−0) = **300 €** → voll frei.
|
||||
- Februar: YTD 300 → frei min(100, 365−300) = **65 €**, Überhang **35 €**
|
||||
in SV-/LSt-(netto SVDN)/LNK-Bemessung (test_03).
|
||||
|
||||
## RF-SB-4 — Eigene Pools: Sachzuwendung (186) & Rabatt (1.000)
|
||||
|
||||
- **Sachzuwendung**: Jänner 100 € (frei), Februar 100 € → frei
|
||||
186−100 = **86 €**, Überhang **14 €** (test_04).
|
||||
- **Rabatt**: Jänner 700 € (frei, über 20 %), Februar 500 € → frei
|
||||
1.000−700 = **300 €**, Überhang **200 €** (test_04).
|
||||
- Pools sind unabhängig (BV 365 ≠ Sachzuwendung 186).
|
||||
|
||||
## RF-SB-5 — Homeoffice (100-Tage-Limit)
|
||||
|
||||
70 Tage (validiert), dann 80 Tage:
|
||||
|
||||
- Jänner: 70 × 3 = **210 €** frei (70 ≤ 100 Tage).
|
||||
- Februar: rest 100−70 = 30 Tage → frei 30 × 3 = **90 €**; Zeile
|
||||
80 × 3 = 240 € → Überhang **150 €** pflichtig (test_05).
|
||||
|
||||
## RF-SB-6 — Ohne SB-Eingaben neutral
|
||||
|
||||
Keine `ATP_SB_*`-Eingaben → keine `SB_*`-Zeilen,
|
||||
`_l10n_at_st_frei_freie_laufen()` = `_l10n_at_sv49_freie_laufen()` = 0
|
||||
→ Bemessungen wie ohne AP12a (test_06).
|
||||
|
||||
## AP12b — Wertgebundene Sachbezüge (2026)
|
||||
|
||||
Stammdaten liegen auf `hr.version` (ein Fahrzeug/eine Wohnung pro
|
||||
Dienstverhältnis im MVP), ereignisbezogene Werte auf `ATP_SB_*`-Eingaben.
|
||||
Die Werte sind vollständig versioniert: `at_sb_wagen`, `at_sb_wagen_co2`,
|
||||
`at_sb_wohnung`, `at_sb_parkplatz`.
|
||||
|
||||
- **Dienstwagen** (lb-sac-03/04, § 4 SBWVO): AK × 2 % (über Grenzwert,
|
||||
cap 960) bzw. 1,5 % (Grenzwert erfüllt, cap 720); halber SB bei
|
||||
nachgewiesenen ≤ 500 Privat-km/Monat: 1 %/0,75 % (caps 480/360).
|
||||
CO₂-Grenzwert folgt dem Erstzulassungsjahr; 0 g/km = kein Ansatz;
|
||||
laufender Kostenbeitrag mindert nach § 4 Abs 7.
|
||||
- **Dienstwohnung** (lb-sac-09, § 2 Abs 7a SBWVO): m² × Richtwert je
|
||||
Bundesland; Normwohnung −30 %, Hausbesorger −35 % (kombiniert 55,5 %);
|
||||
arbeitsplatznah/nicht Lebensmittelpunkt: ≤ 35 m² = 0, über 35–45 m²
|
||||
−35 % während der ersten 12 Überlassungsmonate.
|
||||
- **Parkplatz** (lb-sac-18, § 4a SBWVO): 14,53 €/Monat minus DN-
|
||||
Kostenbeitrag, soweit keine dokumentierte Ausnahme vorliegt. AP12b
|
||||
behandelt ihn als laufenden Bezug; der Quelltext nennt die Kategorie
|
||||
nicht explizit (⚠ Annahme vor Produktivsetzung gegen LStR/RIS prüfen).
|
||||
- **Handy** (lb-sac-15): beruflich/fallweise privat = 0; überwiegend
|
||||
private Mehrkosten sind laufender manueller Bezug. Verkauf unter
|
||||
Buchwert ist als Eingabe „Buchwert minus Kaufpreis“: SV **laufend**,
|
||||
LSt **sonstig (§ 67)**, LNK-pflichtig.
|
||||
- **Bonusmeilen** (lb-sac-08): Einlösungswert ist Drittlohn, ausschließlich
|
||||
SV-laufend (§ 49 Abs 1 ASVG), kein eigenes LSt-/LNK-Brutto. Die darauf
|
||||
entfallende DN-SV mindert jedoch gemäß bestehender §-62-Kette die LSt-BG.
|
||||
|
||||
## RF-SB-7 — Dienstwagen
|
||||
|
||||
AK 36.700 €, EZ 2025, CO₂ 112 g/km (≤ 126): 36.700 × 1,5 % =
|
||||
**550,50 €**. Halber Sachbezug: **275,25 €**. Bei 130 g/km:
|
||||
36.700 × 2 % = **734,00 €**. E-Fahrzeug (0 g/km) = **0 €**.
|
||||
Kostenbeitrag 300 €: 550,50 − 300 = **250,50 €**.
|
||||
|
||||
## RF-SB-8 — Dienstwohnung und Parkplatz
|
||||
|
||||
- Wien, 72,4 m²: 72,4 × 6,67 = **482,91 €**.
|
||||
- Arbeitsplatznahe Kleinwohnung: 30 m² = **0 €**; 40 m² im Monat 1:
|
||||
40 × 6,67 × 65 % = **173,42 €**.
|
||||
- Parkplatz: 14,53 € minus 5 € Kostenbeitrag = **9,53 €**.
|
||||
|
||||
## RF-SB-9 — Handy und Bonusmeilen
|
||||
|
||||
- Private Handy-Mehrkosten 80 € nur bei Profil „überwiegend privat“:
|
||||
**80 € laufend**; berufliches Profil erzeugt keine Zeile.
|
||||
- Arbeitsmittel-Verkauf (Buchwert 800 € − Preis 100 €): **700 €**;
|
||||
SV-Basis laufend +700, SV-SZ-Basis 0, LSt-sonstig und LNK +700.
|
||||
- Bonusmeilen, Einlösungswert 300 €: SV-Basis +300, LNK 0, kein eigenes
|
||||
LSt-Brutto; Auszahlungsnetto vermindert sich nur um die DN-SV abzüglich
|
||||
der daraus folgenden LSt-Entlastung.
|
||||
|
||||
## Offene Punkte / dokumentierte Reste
|
||||
|
||||
- **Dienstwagen:** Mini-Sachbezug (Fahrtenbuch/km-Wert/Jahresvergleich),
|
||||
Fahrzeugwechsel, Pool/Fahrgemeinschaft, Mehrfachfahrzeuge und einmalige
|
||||
Kostenbeiträge (lb-sac-04) folgen außerhalb des MVP.
|
||||
- **Kleinwohnung:** Aufrollung/Nachversteuerung beim Überschreiten der
|
||||
12-Monats-Frist (§ 67 Abs 10), Mehrnutzer- und Gemeinschaftsflächen-
|
||||
aufteilung sind noch nicht automatisiert.
|
||||
- **Jobticket** (SBV § 4b): erst ab AP13.
|
||||
- **Lohnkonto-Aufzeichnungspflichten** (Mahlzeitenvoraussetzungen
|
||||
lb-sac-17, Telearbeitstage lb-tel-05, Rabatte > 20 % lb-sac-02 sowie
|
||||
Nachweise/Erklärungen AP12b): Arbeitgeber-Prüffeld, nicht Engine.
|
||||
- **2027-Werte**: alle `at_st_grenzen`/`at_sv49_grenzen`, `at_sb_werte`
|
||||
und AP12b-Parameter am Jahreswechsel nachführen (Wartung/RUNBOOK).
|
||||
@@ -17,3 +17,4 @@ from . import mutterschutz
|
||||
from . import aeb
|
||||
from . import pfaendung
|
||||
from . import pfaendung_report
|
||||
from . import sachbezuege_private
|
||||
|
||||
@@ -339,14 +339,17 @@ class HrPayslip(models.Model):
|
||||
"""LNK-Bemessungsgrundlage nach Freibetragsstaffel: monatlich
|
||||
gezahlte Arbeitslöhne (laufend + im Monat gezahlte §-67-Bezüge,
|
||||
Zuflussprinzip; kein SV-Abzug), ohne §-67-Abs-3/6-Bezüge
|
||||
(Abfertigung/Abfindung). 'flaf'/'dz' zusätzlich ohne lit-f-AN
|
||||
(Arbeitslöhne ab Folgemonat nach Vollendung des 60. Lebensjahres,
|
||||
§ 41 Abs 4 lit f FLAG — die Kommunalsteuer hat KEINE lit-f-
|
||||
Befreiung). Freibetrag: ≤ 1.095 € keine Abgabe, bis 1.460 € →
|
||||
Freibetrag 1.095 €, darüber volle BG (at_lnk_freibetrag)."""
|
||||
(Abfertigung/Abfindung), ohne steuerfreie Sachbezüge
|
||||
§ 3/§ 26 EStG (Freibeträge — lohnnebenkostenfrei, lb-sac-07/02).
|
||||
'flaf'/'dz' zusätzlich ohne lit-f-AN (Arbeitslöhne ab Folgemonat
|
||||
nach Vollendung des 60. Lebensjahres, § 41 Abs 4 lit f FLAG —
|
||||
die Kommunalsteuer hat KEINE lit-f-Befreiung). Freibetrag:
|
||||
≤ 1.095 € keine Abgabe, bis 1.460 € → Freibetrag 1.095 €, darüber
|
||||
volle BG (at_lnk_freibetrag)."""
|
||||
self.ensure_one()
|
||||
basis = (self._l10n_at_brutto('laufend', result_rules)
|
||||
+ self._l10n_at_brutto('sonstig', result_rules))
|
||||
+ self._l10n_at_brutto('sonstig', result_rules)
|
||||
- self._l10n_at_st_frei_freie_laufen(result_rules))
|
||||
for rule in self.struct_id.rule_ids:
|
||||
if rule.l10n_at_s67_abs in ('abs3', 'abs6'):
|
||||
if result_rules is not None and rule.code in result_rules:
|
||||
|
||||
@@ -0,0 +1,224 @@
|
||||
# Part of the odoo-at-payroll project. License: LGPL-3.
|
||||
"""AP12b: wertgebundene Sachbezüge auf dem Dienstverhältnis.
|
||||
|
||||
Ein Fahrzeug bzw. eine Wohnung je Version ist bewusst MVP-Grenze. Wechsel,
|
||||
Pool-/Mehrfachfahrzeuge, Mini-Sachbezug und Kleinwohnungs-Retroaktivität
|
||||
bleiben dokumentierte Folgefälle (lb-sac-03/04/09/18/15/08).
|
||||
"""
|
||||
from dateutil.relativedelta import relativedelta
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
BUNDESLAENDER = [
|
||||
('burgenland', 'Burgenland'), ('kaernten', 'Kärnten'),
|
||||
('niederoesterreich', 'Niederösterreich'),
|
||||
('oberoesterreich', 'Oberösterreich'), ('salzburg', 'Salzburg'),
|
||||
('steiermark', 'Steiermark'), ('tirol', 'Tirol'),
|
||||
('vorarlberg', 'Vorarlberg'), ('wien', 'Wien'),
|
||||
]
|
||||
|
||||
|
||||
class HrVersion(models.Model):
|
||||
_inherit = 'hr.version'
|
||||
|
||||
l10n_at_sb_wagen_aktiv = fields.Boolean(
|
||||
string='AT Dienstwagen privat nutzbar', tracking=True,
|
||||
groups='hr_payroll.group_hr_payroll_user')
|
||||
l10n_at_sb_wagen_ak = fields.Monetary(
|
||||
string='AT Dienstwagen Anschaffungskosten', currency_field='currency_id',
|
||||
tracking=True, groups='hr_payroll.group_hr_payroll_user',
|
||||
help='Maßgebliche AK inkl. USt/NoVA nach § 4 SBWVO; bei '
|
||||
'Gebrauchtwagen historischer Neupreis (lb-sac-03).')
|
||||
l10n_at_sb_wagen_erstzulassung = fields.Date(
|
||||
string='AT Dienstwagen Erstzulassung', tracking=True,
|
||||
groups='hr_payroll.group_hr_payroll_user',
|
||||
help='Bestimmt den CO₂-Grenzwert; bei Gebrauchtwagen ist das '
|
||||
'Erstzulassungsjahr maßgeblich (lb-sac-03).')
|
||||
l10n_at_sb_wagen_co2 = fields.Float(
|
||||
string='AT Dienstwagen CO₂ (g/km)', tracking=True,
|
||||
groups='hr_payroll.group_hr_payroll_user')
|
||||
l10n_at_sb_wagen_halb = fields.Boolean(
|
||||
string='AT Dienstwagen halber Sachbezug', tracking=True,
|
||||
groups='hr_payroll.group_hr_payroll_user',
|
||||
help='Nachweislich höchstens 500 Privat-km/Monat bzw. 6.000/Jahr; '
|
||||
'Nachweis/Fahrtenbuch durch Arbeitgeber prüfen (lb-sac-03).')
|
||||
l10n_at_sb_wagen_kostenbeitrag = fields.Monetary(
|
||||
string='AT Dienstwagen Kostenbeitrag monatlich', currency_field='currency_id',
|
||||
tracking=True, groups='hr_payroll.group_hr_payroll_user',
|
||||
help='Laufender DN-Kostenbeitrag; mindert nach § 4 Abs 7 SBWVO '
|
||||
'den Sachbezugswert (nicht einzelne Betriebskosten).')
|
||||
|
||||
l10n_at_sb_wohnung_aktiv = fields.Boolean(
|
||||
string='AT Dienstwohnung überlassen', tracking=True,
|
||||
groups='hr_payroll.group_hr_payroll_user')
|
||||
l10n_at_sb_wohnung_flaeche = fields.Float(
|
||||
string='AT Dienstwohnung Fläche (m²)', tracking=True,
|
||||
groups='hr_payroll.group_hr_payroll_user')
|
||||
l10n_at_sb_wohnung_bundesland = fields.Selection(
|
||||
BUNDESLAENDER, string='AT Dienstwohnung Bundesland', tracking=True,
|
||||
groups='hr_payroll.group_hr_payroll_user')
|
||||
l10n_at_sb_wohnung_norm = fields.Boolean(
|
||||
string='AT Dienstwohnung Normwohnung', default=True, tracking=True,
|
||||
groups='hr_payroll.group_hr_payroll_user')
|
||||
l10n_at_sb_wohnung_besorger = fields.Boolean(
|
||||
string='AT Dienstwohnung Hausbesorger/Betreuer', tracking=True,
|
||||
groups='hr_payroll.group_hr_payroll_user')
|
||||
l10n_at_sb_wohnung_arbeitsplatznah = fields.Boolean(
|
||||
string='AT Dienstwohnung arbeitsplatznah (≤ 15 Min.)', tracking=True,
|
||||
groups='hr_payroll.group_hr_payroll_user')
|
||||
l10n_at_sb_wohnung_kein_mittelpunkt = fields.Boolean(
|
||||
string='AT Dienstwohnung nicht Mittelpunkt Lebensinteressen', tracking=True,
|
||||
groups='hr_payroll.group_hr_payroll_user')
|
||||
l10n_at_sb_wohnung_ueberlassung_ab = fields.Date(
|
||||
string='AT Dienstwohnung überlassen ab', tracking=True,
|
||||
groups='hr_payroll.group_hr_payroll_user')
|
||||
l10n_at_sb_wohnung_kostenbeitrag = fields.Monetary(
|
||||
string='AT Dienstwohnung Kostenbeitrag monatlich', currency_field='currency_id',
|
||||
tracking=True, groups='hr_payroll.group_hr_payroll_user')
|
||||
|
||||
l10n_at_sb_parkplatz_aktiv = fields.Boolean(
|
||||
string='AT Parkplatz in Kurzparkzone berechtigt', tracking=True,
|
||||
groups='hr_payroll.group_hr_payroll_user')
|
||||
l10n_at_sb_parkplatz_ausnahme = fields.Boolean(
|
||||
string='AT Parkplatz sachbezugsbefreit', tracking=True,
|
||||
groups='hr_payroll.group_hr_payroll_user',
|
||||
help='Nachtdienst ohne Gebührenpflicht, Behinderung mit Pauschbetrag '
|
||||
'oder kontrollierter Verzicht/Nichtparken (lb-sac-18).')
|
||||
l10n_at_sb_parkplatz_kostenbeitrag = fields.Monetary(
|
||||
string='AT Parkplatz Kostenbeitrag monatlich', currency_field='currency_id',
|
||||
tracking=True, groups='hr_payroll.group_hr_payroll_user')
|
||||
|
||||
l10n_at_sb_handy_profil = fields.Selection(
|
||||
[('beruflich', 'Beruflich/fallweise privat (kein Sachbezug)'),
|
||||
('ueberwiegend_privat', 'Überwiegend privat (tatsächliche Kosten)'),
|
||||
('private_ueberlassung', 'Private Überlassung (Rabatt-Regime)')],
|
||||
string='AT Handy-/Arbeitsmittel-Nutzungsprofil', default='beruflich',
|
||||
tracking=True, groups='hr_payroll.group_hr_payroll_user')
|
||||
|
||||
def _l10n_at_sb_wagen_wert(self, datum):
|
||||
self.ensure_one()
|
||||
if not (self.l10n_at_sb_wagen_aktiv and self.l10n_at_sb_wagen_ak):
|
||||
return 0.0
|
||||
co2 = self.l10n_at_sb_wagen_co2
|
||||
if co2 == 0.0:
|
||||
return 0.0
|
||||
werte = self.env['hr.rule.parameter']._get_parameter_from_code(
|
||||
'at_sb_wagen', datum, raise_if_not_found=False) or {}
|
||||
grenzen = self.env['hr.rule.parameter']._get_parameter_from_code(
|
||||
'at_sb_wagen_co2', datum, raise_if_not_found=False) or {}
|
||||
ez_jahr = (self.l10n_at_sb_wagen_erstzulassung or datum).year
|
||||
grenze = float(grenzen.get('ab_2025' if ez_jahr >= 2025
|
||||
else str(ez_jahr),
|
||||
grenzen.get('vor_2016', 0.0)))
|
||||
niedrig = bool(grenze and co2 <= grenze)
|
||||
art = 'niedrig' if niedrig else 'hoch'
|
||||
prefix = 'halb' if self.l10n_at_sb_wagen_halb else 'voll'
|
||||
wert = self.l10n_at_sb_wagen_ak * float(
|
||||
werte.get('%s_%s_satz' % (prefix, art), 0.0)) / 100.0
|
||||
wert = min(wert, float(werte.get('%s_%s_cap' % (prefix, art), 0.0)))
|
||||
return max(wert - self.l10n_at_sb_wagen_kostenbeitrag, 0.0)
|
||||
|
||||
def _l10n_at_sb_wohnung_wert(self, datum):
|
||||
self.ensure_one()
|
||||
if not (self.l10n_at_sb_wohnung_aktiv
|
||||
and self.l10n_at_sb_wohnung_flaeche
|
||||
and self.l10n_at_sb_wohnung_bundesland):
|
||||
return 0.0
|
||||
werte = self.env['hr.rule.parameter']._get_parameter_from_code(
|
||||
'at_sb_wohnung', datum, raise_if_not_found=False) or {}
|
||||
flaeche = self.l10n_at_sb_wohnung_flaeche
|
||||
klein = (self.l10n_at_sb_wohnung_arbeitsplatznah
|
||||
and self.l10n_at_sb_wohnung_kein_mittelpunkt)
|
||||
if klein and flaeche <= float(werte.get('klein_frei_m2', 0.0)):
|
||||
return 0.0
|
||||
wert = flaeche * float(werte.get(
|
||||
self.l10n_at_sb_wohnung_bundesland, 0.0))
|
||||
if (not self.l10n_at_sb_wohnung_norm
|
||||
and self.l10n_at_sb_wohnung_besorger):
|
||||
wert *= 1.0 - float(werte.get('kombiniert_abschlag', 0.0))
|
||||
elif not self.l10n_at_sb_wohnung_norm:
|
||||
wert *= 1.0 - float(werte.get('nicht_norm_abschlag', 0.0))
|
||||
elif self.l10n_at_sb_wohnung_besorger:
|
||||
wert *= 1.0 - float(werte.get('besorger_abschlag', 0.0))
|
||||
if (klein and flaeche <= float(werte.get('klein_abschlag_bis_m2', 0.0))
|
||||
and self.l10n_at_sb_wohnung_ueberlassung_ab
|
||||
and datum < self.l10n_at_sb_wohnung_ueberlassung_ab
|
||||
+ relativedelta(months=12)):
|
||||
wert *= 1.0 - float(werte.get('klein_abschlag', 0.0))
|
||||
return max(wert - self.l10n_at_sb_wohnung_kostenbeitrag, 0.0)
|
||||
|
||||
def _l10n_at_sb_parkplatz_wert(self, datum):
|
||||
self.ensure_one()
|
||||
if not (self.l10n_at_sb_parkplatz_aktiv
|
||||
and not self.l10n_at_sb_parkplatz_ausnahme):
|
||||
return 0.0
|
||||
wert = float(self.env['hr.rule.parameter']._get_parameter_from_code(
|
||||
'at_sb_parkplatz', datum, raise_if_not_found=False) or 0.0)
|
||||
return max(wert - self.l10n_at_sb_parkplatz_kostenbeitrag, 0.0)
|
||||
|
||||
|
||||
class HrEmployee(models.Model):
|
||||
_inherit = 'hr.employee'
|
||||
|
||||
l10n_at_sb_wagen_aktiv = fields.Boolean(
|
||||
related='version_id.l10n_at_sb_wagen_aktiv', inherited=True,
|
||||
readonly=False, groups='hr_payroll.group_hr_payroll_user')
|
||||
l10n_at_sb_wagen_ak = fields.Monetary(
|
||||
related='version_id.l10n_at_sb_wagen_ak', inherited=True,
|
||||
readonly=False, currency_field='currency_id',
|
||||
groups='hr_payroll.group_hr_payroll_user')
|
||||
l10n_at_sb_wagen_erstzulassung = fields.Date(
|
||||
related='version_id.l10n_at_sb_wagen_erstzulassung', inherited=True,
|
||||
readonly=False, groups='hr_payroll.group_hr_payroll_user')
|
||||
l10n_at_sb_wagen_co2 = fields.Float(
|
||||
related='version_id.l10n_at_sb_wagen_co2', inherited=True,
|
||||
readonly=False, groups='hr_payroll.group_hr_payroll_user')
|
||||
l10n_at_sb_wagen_halb = fields.Boolean(
|
||||
related='version_id.l10n_at_sb_wagen_halb', inherited=True,
|
||||
readonly=False, groups='hr_payroll.group_hr_payroll_user')
|
||||
l10n_at_sb_wagen_kostenbeitrag = fields.Monetary(
|
||||
related='version_id.l10n_at_sb_wagen_kostenbeitrag', inherited=True,
|
||||
readonly=False, currency_field='currency_id',
|
||||
groups='hr_payroll.group_hr_payroll_user')
|
||||
l10n_at_sb_wohnung_aktiv = fields.Boolean(
|
||||
related='version_id.l10n_at_sb_wohnung_aktiv', inherited=True,
|
||||
readonly=False, groups='hr_payroll.group_hr_payroll_user')
|
||||
l10n_at_sb_wohnung_flaeche = fields.Float(
|
||||
related='version_id.l10n_at_sb_wohnung_flaeche', inherited=True,
|
||||
readonly=False, groups='hr_payroll.group_hr_payroll_user')
|
||||
l10n_at_sb_wohnung_bundesland = fields.Selection(
|
||||
related='version_id.l10n_at_sb_wohnung_bundesland', inherited=True,
|
||||
readonly=False, groups='hr_payroll.group_hr_payroll_user')
|
||||
l10n_at_sb_wohnung_norm = fields.Boolean(
|
||||
related='version_id.l10n_at_sb_wohnung_norm', inherited=True,
|
||||
readonly=False, groups='hr_payroll.group_hr_payroll_user')
|
||||
l10n_at_sb_wohnung_besorger = fields.Boolean(
|
||||
related='version_id.l10n_at_sb_wohnung_besorger', inherited=True,
|
||||
readonly=False, groups='hr_payroll.group_hr_payroll_user')
|
||||
l10n_at_sb_wohnung_arbeitsplatznah = fields.Boolean(
|
||||
related='version_id.l10n_at_sb_wohnung_arbeitsplatznah', inherited=True,
|
||||
readonly=False, groups='hr_payroll.group_hr_payroll_user')
|
||||
l10n_at_sb_wohnung_kein_mittelpunkt = fields.Boolean(
|
||||
related='version_id.l10n_at_sb_wohnung_kein_mittelpunkt', inherited=True,
|
||||
readonly=False, groups='hr_payroll.group_hr_payroll_user')
|
||||
l10n_at_sb_wohnung_ueberlassung_ab = fields.Date(
|
||||
related='version_id.l10n_at_sb_wohnung_ueberlassung_ab', inherited=True,
|
||||
readonly=False, groups='hr_payroll.group_hr_payroll_user')
|
||||
l10n_at_sb_wohnung_kostenbeitrag = fields.Monetary(
|
||||
related='version_id.l10n_at_sb_wohnung_kostenbeitrag', inherited=True,
|
||||
readonly=False, currency_field='currency_id',
|
||||
groups='hr_payroll.group_hr_payroll_user')
|
||||
l10n_at_sb_parkplatz_aktiv = fields.Boolean(
|
||||
related='version_id.l10n_at_sb_parkplatz_aktiv', inherited=True,
|
||||
readonly=False, groups='hr_payroll.group_hr_payroll_user')
|
||||
l10n_at_sb_parkplatz_ausnahme = fields.Boolean(
|
||||
related='version_id.l10n_at_sb_parkplatz_ausnahme', inherited=True,
|
||||
readonly=False, groups='hr_payroll.group_hr_payroll_user')
|
||||
l10n_at_sb_parkplatz_kostenbeitrag = fields.Monetary(
|
||||
related='version_id.l10n_at_sb_parkplatz_kostenbeitrag', inherited=True,
|
||||
readonly=False, currency_field='currency_id',
|
||||
groups='hr_payroll.group_hr_payroll_user')
|
||||
l10n_at_sb_handy_profil = fields.Selection(
|
||||
related='version_id.l10n_at_sb_handy_profil', inherited=True,
|
||||
readonly=False, groups='hr_payroll.group_hr_payroll_user')
|
||||
@@ -13,3 +13,5 @@ from . import test_efzg_private
|
||||
from . import test_mutterschutz_private
|
||||
from . import test_aeb_private
|
||||
from . import test_pfaendung_private
|
||||
from . import test_sachbezuege_private
|
||||
from . import test_sachbezuege2_private
|
||||
|
||||
@@ -0,0 +1,187 @@
|
||||
# Part of the odoo-at-payroll project. License: LGPL-3.
|
||||
"""AP12b: wertgebundene Sachbezüge — Dienstwagen, Dienstwohnung,
|
||||
Parkplatz, Handy und Bonusmeilen (lb-sac-03/04/09/18/15/08)."""
|
||||
from datetime import date, timedelta
|
||||
|
||||
from odoo.tests import TransactionCase, tagged
|
||||
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
class Sachbezug2PrivateTest(TransactionCase):
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.structure = self.env.ref(
|
||||
'l10n_at_hr_payroll_private.structure_at_privat')
|
||||
self.kv = self.env.ref('l10n_at_hr_payroll_private.kv_si_2203')
|
||||
self.gruppe = self.env.ref(
|
||||
'l10n_at_hr_payroll_private.kv2203_grp_ii')
|
||||
|
||||
def _employee(self, values=None):
|
||||
employee = self.env['hr.employee'].create({'name': 'Test AP12b'})
|
||||
version = self.env['hr.version'].search(
|
||||
[('employee_id', '=', employee.id)], limit=1)
|
||||
version.write({
|
||||
'contract_date_start': date(2020, 1, 1),
|
||||
'kv_id': self.kv.id,
|
||||
'kv_gruppe_id': self.gruppe.id,
|
||||
'erfahrungsstufe': 1,
|
||||
**(values or {}),
|
||||
})
|
||||
return employee
|
||||
|
||||
def _slip(self, employee, monat=1):
|
||||
erster = date(2026, monat, 1)
|
||||
naechster = (erster + timedelta(days=32)).replace(day=1)
|
||||
slip = self.env['hr.payslip'].create({
|
||||
'name': 'Test-AP12b %d' % monat,
|
||||
'employee_id': employee.id,
|
||||
'struct_id': self.structure.id,
|
||||
'date_from': erster,
|
||||
'date_to': naechster - timedelta(days=1),
|
||||
})
|
||||
slip.compute_sheet()
|
||||
return slip
|
||||
|
||||
def _input(self, slip, code, amount):
|
||||
input_type = self.env['hr.payslip.input.type'].search(
|
||||
[('code', '=', code)], limit=1)
|
||||
self.env['hr.payslip.input'].create({
|
||||
'payslip_id': slip.id,
|
||||
'input_type_id': input_type.id,
|
||||
'amount': amount,
|
||||
})
|
||||
slip.compute_sheet()
|
||||
|
||||
@staticmethod
|
||||
def _total(slip, code):
|
||||
return sum(slip.line_ids.filtered(
|
||||
lambda line: line.code == code).mapped('total'))
|
||||
|
||||
def test_01_dienstwagen_co2_halb_und_elektro(self):
|
||||
"""§ 4 SBWVO: 36.700 €, EZ 2025, 112 g/km → 1,5 % = 550,50;
|
||||
halber SB 275,25; CO₂ 130 → 2 % = 734; 0-g-Fahrzeug = 0."""
|
||||
basis = {
|
||||
'l10n_at_sb_wagen_aktiv': True,
|
||||
'l10n_at_sb_wagen_ak': 36700.0,
|
||||
'l10n_at_sb_wagen_erstzulassung': date(2025, 1, 1),
|
||||
}
|
||||
emp_niedrig = self._employee({**basis, 'l10n_at_sb_wagen_co2': 112})
|
||||
self.assertAlmostEqual(
|
||||
self._total(self._slip(emp_niedrig), 'SB_WAGEN'), 550.50, places=2)
|
||||
emp_halb = self._employee({**basis, 'l10n_at_sb_wagen_co2': 112,
|
||||
'l10n_at_sb_wagen_halb': True})
|
||||
self.assertAlmostEqual(
|
||||
self._total(self._slip(emp_halb), 'SB_WAGEN'), 275.25, places=2)
|
||||
emp_hoch = self._employee({**basis, 'l10n_at_sb_wagen_co2': 130})
|
||||
self.assertAlmostEqual(
|
||||
self._total(self._slip(emp_hoch), 'SB_WAGEN'), 734.0, places=2)
|
||||
emp_elektrisch = self._employee({**basis, 'l10n_at_sb_wagen_co2': 0})
|
||||
self.assertAlmostEqual(
|
||||
self._total(self._slip(emp_elektrisch), 'SB_WAGEN'), 0.0, places=2)
|
||||
|
||||
def test_02_dienstwagen_kostenbeitrag(self):
|
||||
"""Laufender Kostenbeitrag mindert nach § 4 Abs 7 SBWVO:
|
||||
550,50 − 300 = 250,50 € (lb-sac-04)."""
|
||||
employee = self._employee({
|
||||
'l10n_at_sb_wagen_aktiv': True,
|
||||
'l10n_at_sb_wagen_ak': 36700.0,
|
||||
'l10n_at_sb_wagen_erstzulassung': date(2025, 1, 1),
|
||||
'l10n_at_sb_wagen_co2': 112.0,
|
||||
'l10n_at_sb_wagen_kostenbeitrag': 300.0,
|
||||
})
|
||||
self.assertAlmostEqual(
|
||||
self._total(self._slip(employee), 'SB_WAGEN'), 250.50, places=2)
|
||||
|
||||
def test_03_dienstwohnung_und_parkplatz(self):
|
||||
"""§ 2/§ 4a SBWVO: Wien 72,4 m² = 482,91; Kleinwohnung 30 m²
|
||||
= 0, 40 m² = 173,42; Parkplatz 14,53 abzüglich Kostenbeitrag."""
|
||||
common = {
|
||||
'l10n_at_sb_wohnung_aktiv': True,
|
||||
'l10n_at_sb_wohnung_bundesland': 'wien',
|
||||
'l10n_at_sb_wohnung_arbeitsplatznah': True,
|
||||
'l10n_at_sb_wohnung_kein_mittelpunkt': True,
|
||||
'l10n_at_sb_wohnung_ueberlassung_ab': date(2026, 1, 1),
|
||||
}
|
||||
employee = self._employee({**common, 'l10n_at_sb_wohnung_flaeche': 72.4})
|
||||
self.assertAlmostEqual(
|
||||
self._total(self._slip(employee), 'SB_WOHNUNG'), 482.91, places=2)
|
||||
employee_klein = self._employee({**common, 'l10n_at_sb_wohnung_flaeche': 30})
|
||||
self.assertAlmostEqual(
|
||||
self._total(self._slip(employee_klein), 'SB_WOHNUNG'), 0.0, places=2)
|
||||
employee_kombiniert = self._employee({
|
||||
'l10n_at_sb_wohnung_aktiv': True,
|
||||
'l10n_at_sb_wohnung_bundesland': 'wien',
|
||||
'l10n_at_sb_wohnung_flaeche': 100,
|
||||
'l10n_at_sb_wohnung_norm': False,
|
||||
'l10n_at_sb_wohnung_besorger': True,
|
||||
})
|
||||
self.assertAlmostEqual(
|
||||
self._total(self._slip(employee_kombiniert), 'SB_WOHNUNG'),
|
||||
296.815, places=2)
|
||||
employee_abschlag = self._employee({**common, 'l10n_at_sb_wohnung_flaeche': 40})
|
||||
self.assertAlmostEqual(
|
||||
self._total(self._slip(employee_abschlag), 'SB_WOHNUNG'), 173.42,
|
||||
places=2)
|
||||
employee_parkplatz = self._employee({
|
||||
'l10n_at_sb_parkplatz_aktiv': True,
|
||||
'l10n_at_sb_parkplatz_kostenbeitrag': 5.0,
|
||||
})
|
||||
self.assertAlmostEqual(
|
||||
self._total(self._slip(employee_parkplatz), 'SB_PARKPLATZ'), 9.53,
|
||||
places=2)
|
||||
|
||||
def test_04_handy_und_gebrauchtverkauf(self):
|
||||
"""Handy-Mehrkosten sind nur bei überwiegend privater Nutzung
|
||||
laufend; Verkauf unter Buchwert ist SV-laufend/LSt-sonstig."""
|
||||
emp_handy = self._employee({
|
||||
'l10n_at_sb_handy_profil': 'ueberwiegend_privat'})
|
||||
slip_handy = self._slip(emp_handy)
|
||||
self._input(slip_handy, 'ATP_SB_HANDY', 80.0)
|
||||
self.assertAlmostEqual(self._total(slip_handy, 'SB_HANDY'), 80.0)
|
||||
emp_beruflich = self._employee()
|
||||
slip_beruflich = self._slip(emp_beruflich)
|
||||
self._input(slip_beruflich, 'ATP_SB_HANDY', 80.0)
|
||||
self.assertAlmostEqual(self._total(slip_beruflich, 'SB_HANDY'), 0.0)
|
||||
|
||||
emp_verkauf = self._employee()
|
||||
slip_verkauf = self._slip(emp_verkauf)
|
||||
slip_basis = self._slip(self._employee())
|
||||
self._input(slip_verkauf, 'ATP_SB_GEBRAUCHT', 700.0)
|
||||
self.assertAlmostEqual(self._total(slip_verkauf, 'SB_GEBRAUCHT'), 700.0)
|
||||
self.assertAlmostEqual(
|
||||
slip_verkauf._l10n_at_sv_basis_roh()
|
||||
- slip_basis._l10n_at_sv_basis_roh(), 700.0, places=2)
|
||||
self.assertAlmostEqual(
|
||||
slip_verkauf._l10n_at_svdn_basis_sz(), 0.0, places=2)
|
||||
self.assertAlmostEqual(
|
||||
slip_verkauf._l10n_at_brutto('sonstig')
|
||||
- slip_basis._l10n_at_brutto('sonstig'), 700.0, places=2)
|
||||
self.assertAlmostEqual(
|
||||
slip_verkauf._at_lnk_basis('flaf')
|
||||
- slip_basis._at_lnk_basis('flaf'), 700.0, places=2)
|
||||
|
||||
def test_05_bonusmeilen_nur_sv(self):
|
||||
"""Drittlohn aus privat eingelösten Bonusmeilen: ausschließlich
|
||||
laufende SV, kein LSt-/LNK- oder Auszahlungsnettoanspruch."""
|
||||
slip = self._slip(self._employee())
|
||||
basis = self._slip(self._employee())
|
||||
self._input(slip, 'ATP_SB_BONUSMEILEN', 300.0)
|
||||
self.assertAlmostEqual(self._total(slip, 'SB_BONUSMEILEN'), 300.0)
|
||||
self.assertAlmostEqual(
|
||||
slip._l10n_at_sv_basis_roh() - basis._l10n_at_sv_basis_roh(),
|
||||
300.0, places=2)
|
||||
self.assertAlmostEqual(
|
||||
slip._at_lnk_basis('flaf') - basis._at_lnk_basis('flaf'),
|
||||
0.0, places=2)
|
||||
svdn_diff = (slip._l10n_at_svdn_gesamt()
|
||||
- basis._l10n_at_svdn_gesamt())
|
||||
# Der Drittlohn selbst ist nicht LSt-pflichtig; seine DN-SV ist
|
||||
# jedoch nach der bestehenden §-62-Kette LSt-BG-mindernd.
|
||||
self.assertAlmostEqual(
|
||||
slip._l10n_at_bemessung_laufen()
|
||||
- basis._l10n_at_bemessung_laufen(), -svdn_diff, places=2)
|
||||
lst_diff = self._total(slip, 'LST_LFD') - self._total(basis, 'LST_LFD')
|
||||
self.assertAlmostEqual(
|
||||
self._total(slip, 'NET') - self._total(basis, 'NET'),
|
||||
-svdn_diff - lst_diff, places=1)
|
||||
@@ -0,0 +1,279 @@
|
||||
# Part of the odoo-at-payroll project. License: LGPL-3.
|
||||
"""AP12a: steuerfreie Sachbezüge auf der privatwirtschaftlichen
|
||||
Structure end-to-end — manuelle SB-Eingaben (ATP_SB_*) → SB_*-Regeln
|
||||
→ §-49-Abs-3-SV- und §-3/§-26-LSt-Freibeträge (Freibeträge, Jahres-
|
||||
Kumulative je DN, lohnnebenkostenfrei): Mahlzeiten (lb-sac-17),
|
||||
Betriebsveranstaltungen (lb-sac-07), Sachzuwendungen (lb-ges-02),
|
||||
Mitarbeiterrabatte (lb-sac-02), Homeoffice/Telearbeit (lb-tel-04/05)."""
|
||||
from datetime import date, timedelta
|
||||
|
||||
from odoo.tests import TransactionCase, tagged
|
||||
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
class SachbezugPrivateTest(TransactionCase):
|
||||
|
||||
SB_CODES = ('SB_MAHLZEIT', 'SB_BETRIEBSVERANSTALTUNG',
|
||||
'SB_SACHZUWENDUNG', 'SB_RABATT', 'SB_HOMEOFFICE')
|
||||
|
||||
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 SB 40h Mo-Fr',
|
||||
'tz': 'Europe/Vienna',
|
||||
'hours_per_day': 8,
|
||||
'attendance_ids': [
|
||||
(0, 0, {'name': 'Tag %d' % wd, 'dayofweek': str(wd),
|
||||
'hour_from': 8.0, 'hour_to': 16.0})
|
||||
for wd in range(5)
|
||||
],
|
||||
})
|
||||
self.env.company.resource_calendar_id = self.calendar
|
||||
|
||||
def _employee(self):
|
||||
employee = self.env['hr.employee'].create({
|
||||
'name': 'Test SB 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': date(2020, 1, 1)})
|
||||
version.write({
|
||||
'contract_date_start': date(2020, 1, 1),
|
||||
'kv_id': self.kv2203.id,
|
||||
'kv_gruppe_id': self.gruppe_ii.id,
|
||||
'erfahrungsstufe': 1,
|
||||
'resource_calendar_id': self.calendar.id,
|
||||
})
|
||||
return employee
|
||||
|
||||
def _slip(self, employee, monat, jahr=2026):
|
||||
erster = date(jahr, monat, 1)
|
||||
naechster = (erster + timedelta(days=32)).replace(day=1)
|
||||
slip = self.env['hr.payslip'].create({
|
||||
'name': 'Test-SB-Privat %d/%d' % (monat, jahr),
|
||||
'employee_id': employee.id,
|
||||
'struct_id': self.structure.id,
|
||||
'date_from': erster,
|
||||
'date_to': naechster - timedelta(days=1),
|
||||
})
|
||||
slip.compute_sheet()
|
||||
return slip
|
||||
|
||||
def _sb_input(self, slip, code, amount):
|
||||
"""SB-Eingabe anhängen und Lauf neu berechnen; liefert die
|
||||
zugehörige SB-Zeile (Regel-Code = SB_ + Eingabe-Code-Suffix)."""
|
||||
typ = self.env['hr.payslip.input.type'].search(
|
||||
[('code', '=', code)], limit=1)
|
||||
self.env['hr.payslip.input'].create({
|
||||
'payslip_id': slip.id,
|
||||
'input_type_id': typ.id,
|
||||
'amount': amount,
|
||||
})
|
||||
slip.compute_sheet()
|
||||
rule_code = 'SB_%s' % code.removeprefix('ATP_SB_')
|
||||
return slip.line_ids.filtered(lambda l: l.code == rule_code)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# RF-SB-1/2: Mahlzeiten-Gutscheine (8 €/AT, lb-sac-17)
|
||||
# ------------------------------------------------------------------
|
||||
def test_01_mahlzeit_voll_frei(self):
|
||||
"""10 Gutschein-Arbeitstage à 8 € (at_sb_werte.mahlzeit_wert):
|
||||
SB-Zeile 80 €, SV- und LSt-Basis wie ohne SB, LNK frei
|
||||
(§ 3 Z 17 / § 49 Z 12, lb-sac-17)."""
|
||||
emp_mit = self._employee()
|
||||
emp_ohne = self._employee()
|
||||
slip_mit = self._slip(emp_mit, 1)
|
||||
slip_ohne = self._slip(emp_ohne, 1)
|
||||
linie = self._sb_input(slip_mit, 'ATP_SB_MAHLZEIT', 10)
|
||||
self.assertAlmostEqual(linie.total, 80.0, places=2)
|
||||
self.assertAlmostEqual(linie.quantity, 10.0, places=2)
|
||||
self.assertAlmostEqual(
|
||||
slip_mit._l10n_at_sv49_freie_laufen(), 80.0, places=2)
|
||||
self.assertAlmostEqual(
|
||||
slip_mit._l10n_at_st_frei_freie_laufen(), 80.0, places=2)
|
||||
# Voll im Freibetrag: keine Wirkung auf SV-, LSt- und LNK-Basis
|
||||
self.assertAlmostEqual(
|
||||
slip_mit._l10n_at_sv_basis_roh()
|
||||
- slip_ohne._l10n_at_sv_basis_roh(), 0.0, places=2)
|
||||
self.assertAlmostEqual(
|
||||
slip_mit._l10n_at_bemessung_laufen()
|
||||
- slip_ohne._l10n_at_bemessung_laufen(), 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_mahlzeit_ueberhang(self):
|
||||
"""Tatsächlicher Bonwert 11 €/AT (at_sb_werte ab 6/2026):
|
||||
10 AT → SB-Zeile 110 €, frei 80 €, Überhang 30 € SV-/LSt-/
|
||||
LNK-pflichtig (lb-sac-17)."""
|
||||
self.env['hr.rule.parameter.value'].create({
|
||||
'rule_parameter_id': self.env.ref(
|
||||
'l10n_at_hr_payroll_private.hr_rule_parameter_at_sb_werte'
|
||||
).id,
|
||||
'date_from': date(2026, 6, 1),
|
||||
'parameter_value':
|
||||
"{'mahlzeit_wert': 11.0, 'homeoffice_wert': 3.0}",
|
||||
})
|
||||
emp_mit = self._employee()
|
||||
emp_ohne = self._employee()
|
||||
slip_mit = self._slip(emp_mit, 6)
|
||||
slip_ohne = self._slip(emp_ohne, 6)
|
||||
linie = self._sb_input(slip_mit, 'ATP_SB_MAHLZEIT', 10)
|
||||
self.assertAlmostEqual(linie.total, 110.0, places=2)
|
||||
self.assertAlmostEqual(
|
||||
slip_mit._l10n_at_sv49_freie_laufen(), 80.0, places=2)
|
||||
self.assertAlmostEqual(
|
||||
slip_mit._l10n_at_st_frei_freie_laufen(), 80.0, places=2)
|
||||
# Überhang 30 € in SV- und LNK-Bemessung; LSt-Bemessung:
|
||||
# Überhang abzüglich der darauf entfallenden SV-DN (SV ist aus
|
||||
# der LSt-BG abzuziehen — §-62-Kette)
|
||||
self.assertAlmostEqual(
|
||||
slip_mit._l10n_at_sv_basis_roh()
|
||||
- slip_ohne._l10n_at_sv_basis_roh(), 30.0, places=2)
|
||||
svdn_diff = (slip_mit._l10n_at_svdn_gesamt()
|
||||
- slip_ohne._l10n_at_svdn_gesamt())
|
||||
self.assertAlmostEqual(
|
||||
slip_mit._l10n_at_bemessung_laufen()
|
||||
- slip_ohne._l10n_at_bemessung_laufen() + svdn_diff,
|
||||
30.0, places=2)
|
||||
self.assertAlmostEqual(
|
||||
slip_mit._at_lnk_basis('flaf')
|
||||
- slip_ohne._at_lnk_basis('flaf'), 30.0, places=2)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# RF-SB-3/4: Jahrespools (§ 3 Z 14/Z 21, § 49 Z 17/Z 29)
|
||||
# ------------------------------------------------------------------
|
||||
def test_03_betriebsveranstaltung_pool(self):
|
||||
"""365 €/J-Pool je DN (lb-sac-07): Jänner 300 € voll frei
|
||||
(validiert), Februar 100 € → frei 65 €, Überhang 35 €
|
||||
pflichtig."""
|
||||
emp = self._employee()
|
||||
slip_jan = self._slip(emp, 1)
|
||||
linie_jan = self._sb_input(
|
||||
slip_jan, 'ATP_SB_BETRIEBSVERANSTALTUNG', 300)
|
||||
self.assertAlmostEqual(linie_jan.total, 300.0, places=2)
|
||||
self.assertAlmostEqual(
|
||||
slip_jan._l10n_at_st_frei_freie_laufen(), 300.0, places=2)
|
||||
slip_jan.action_payslip_done()
|
||||
|
||||
emp_ohne = self._employee()
|
||||
slip_feb = self._slip(emp, 2)
|
||||
linie_feb = self._sb_input(
|
||||
slip_feb, 'ATP_SB_BETRIEBSVERANSTALTUNG', 100)
|
||||
self.assertAlmostEqual(linie_feb.total, 100.0, places=2)
|
||||
self.assertAlmostEqual(
|
||||
slip_feb._l10n_at_st_frei_ytd('betriebsveranstaltung'),
|
||||
300.0, places=2)
|
||||
self.assertAlmostEqual(
|
||||
slip_feb._l10n_at_st_frei_freie_laufen(), 65.0, places=2)
|
||||
self.assertAlmostEqual(
|
||||
slip_feb._l10n_at_sv49_freie_laufen(), 65.0, places=2)
|
||||
slip_ohne = self._slip(emp_ohne, 2)
|
||||
self.assertAlmostEqual(
|
||||
slip_feb._l10n_at_sv_basis_roh()
|
||||
- slip_ohne._l10n_at_sv_basis_roh(), 35.0, places=2)
|
||||
svdn_diff = (slip_feb._l10n_at_svdn_gesamt()
|
||||
- slip_ohne._l10n_at_svdn_gesamt())
|
||||
self.assertAlmostEqual(
|
||||
slip_feb._l10n_at_bemessung_laufen()
|
||||
- slip_ohne._l10n_at_bemessung_laufen() + svdn_diff,
|
||||
35.0, places=2)
|
||||
|
||||
def test_04_sachzuwendung_und_rabatt_pools(self):
|
||||
"""Eigene Pools neben der Betriebsveranstaltung:
|
||||
Sachzuwendung 186 €/J (lb-ges-02), Rabatt 1.000 €/J
|
||||
(lb-sac-02) — Jänner-Verbrauch mindert den Februar-Freibetrag,
|
||||
der Überhang ist pflichtig."""
|
||||
emp_g = self._employee()
|
||||
slip_g_jan = self._slip(emp_g, 1)
|
||||
self._sb_input(slip_g_jan, 'ATP_SB_SACHZUWENDUNG', 100)
|
||||
self.assertAlmostEqual(
|
||||
slip_g_jan._l10n_at_st_frei_freie_laufen(), 100.0, places=2)
|
||||
slip_g_jan.action_payslip_done()
|
||||
emp_g_ohne = self._employee()
|
||||
slip_g_feb = self._slip(emp_g, 2)
|
||||
self._sb_input(slip_g_feb, 'ATP_SB_SACHZUWENDUNG', 100)
|
||||
# frei: 186 − 100 = 86 → Überhang 14
|
||||
self.assertAlmostEqual(
|
||||
slip_g_feb._l10n_at_st_frei_freie_laufen(), 86.0, places=2)
|
||||
slip_g_ohne = self._slip(emp_g_ohne, 2)
|
||||
self.assertAlmostEqual(
|
||||
slip_g_feb._l10n_at_sv_basis_roh()
|
||||
- slip_g_ohne._l10n_at_sv_basis_roh(), 14.0, places=2)
|
||||
|
||||
emp_r = self._employee()
|
||||
slip_r_jan = self._slip(emp_r, 1)
|
||||
self._sb_input(slip_r_jan, 'ATP_SB_RABATT', 700)
|
||||
self.assertAlmostEqual(
|
||||
slip_r_jan._l10n_at_st_frei_freie_laufen(), 700.0, places=2)
|
||||
slip_r_jan.action_payslip_done()
|
||||
emp_r_ohne = self._employee()
|
||||
slip_r_feb = self._slip(emp_r, 2)
|
||||
self._sb_input(slip_r_feb, 'ATP_SB_RABATT', 500)
|
||||
# frei: 1.000 − 700 = 300 → Überhang 200
|
||||
self.assertAlmostEqual(
|
||||
slip_r_feb._l10n_at_st_frei_freie_laufen(), 300.0, places=2)
|
||||
slip_r_ohne = self._slip(emp_r_ohne, 2)
|
||||
self.assertAlmostEqual(
|
||||
slip_r_feb._l10n_at_sv_basis_roh()
|
||||
- slip_r_ohne._l10n_at_sv_basis_roh(), 200.0, places=2)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# RF-SB-5: Homeoffice/Telearbeit (3 €/AT, max 100 Tage/J)
|
||||
# ------------------------------------------------------------------
|
||||
def test_05_homeoffice_jahreslimit(self):
|
||||
"""§ 26 Z 9: 3 €/AT, max 100 Tage/J (lb-tel-04/05) — 70 Tage
|
||||
(210 €) frei (validiert), dann 80 Tage → nur 30 Tage frei
|
||||
(90 €), Überhang 150 € pflichtig."""
|
||||
emp = self._employee()
|
||||
slip_jan = self._slip(emp, 1)
|
||||
linie_jan = self._sb_input(slip_jan, 'ATP_SB_HOMEOFFICE', 70)
|
||||
self.assertAlmostEqual(linie_jan.total, 210.0, places=2)
|
||||
self.assertAlmostEqual(linie_jan.quantity, 70.0, places=2)
|
||||
self.assertAlmostEqual(
|
||||
slip_jan._l10n_at_st_frei_freie_laufen(), 210.0, places=2)
|
||||
slip_jan.action_payslip_done()
|
||||
|
||||
emp_ohne = self._employee()
|
||||
slip_feb = self._slip(emp, 2)
|
||||
linie_feb = self._sb_input(slip_feb, 'ATP_SB_HOMEOFFICE', 80)
|
||||
self.assertAlmostEqual(linie_feb.total, 240.0, places=2)
|
||||
self.assertAlmostEqual(
|
||||
slip_feb._l10n_at_st_frei_ytd('homeoffice', menge=True),
|
||||
70.0, places=2)
|
||||
self.assertAlmostEqual(
|
||||
slip_feb._l10n_at_st_frei_freie_laufen(), 90.0, places=2)
|
||||
slip_ohne = self._slip(emp_ohne, 2)
|
||||
self.assertAlmostEqual(
|
||||
slip_feb._l10n_at_sv_basis_roh()
|
||||
- slip_ohne._l10n_at_sv_basis_roh(), 150.0, places=2)
|
||||
svdn_diff = (slip_feb._l10n_at_svdn_gesamt()
|
||||
- slip_ohne._l10n_at_svdn_gesamt())
|
||||
self.assertAlmostEqual(
|
||||
slip_feb._l10n_at_bemessung_laufen()
|
||||
- slip_ohne._l10n_at_bemessung_laufen() + svdn_diff,
|
||||
150.0, places=2)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# RF-SB-6: ohne SB-Eingaben neutral
|
||||
# ------------------------------------------------------------------
|
||||
def test_06_ohne_sb_eingaben_neutral(self):
|
||||
"""Ohne SB-Eingaben: keine SB-Zeilen und keine Freibeträge —
|
||||
Bemessungen wie ohne AP12a (flag 'keine' ist wirkungslos)."""
|
||||
emp = self._employee()
|
||||
slip = self._slip(emp, 1)
|
||||
self.assertFalse(
|
||||
slip.line_ids.filtered(lambda l: l.code in self.SB_CODES))
|
||||
self.assertAlmostEqual(
|
||||
slip._l10n_at_st_frei_freie_laufen(), 0.0, places=2)
|
||||
self.assertAlmostEqual(
|
||||
slip._l10n_at_sv49_freie_laufen(), 0.0, places=2)
|
||||
@@ -0,0 +1,40 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="payroll_hr_employee_view_form_sachbezuege" model="ir.ui.view">
|
||||
<field name="name">hr.employee.form.sachbezuege</field>
|
||||
<field name="model">hr.employee</field>
|
||||
<field name="inherit_id" ref="hr_payroll.payroll_hr_employee_view_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<group name="payroll_group" position="inside">
|
||||
<group name="sachbezug_stammdaten" string="Sachbezüge (Stammdaten)"
|
||||
groups="hr_payroll.group_hr_payroll_user">
|
||||
<group string="Dienstwagen (§ 4 SBWVO)">
|
||||
<field name="l10n_at_sb_wagen_aktiv"/>
|
||||
<field name="l10n_at_sb_wagen_ak" invisible="not l10n_at_sb_wagen_aktiv"/>
|
||||
<field name="l10n_at_sb_wagen_erstzulassung" invisible="not l10n_at_sb_wagen_aktiv"/>
|
||||
<field name="l10n_at_sb_wagen_co2" invisible="not l10n_at_sb_wagen_aktiv"/>
|
||||
<field name="l10n_at_sb_wagen_halb" invisible="not l10n_at_sb_wagen_aktiv"/>
|
||||
<field name="l10n_at_sb_wagen_kostenbeitrag" invisible="not l10n_at_sb_wagen_aktiv"/>
|
||||
</group>
|
||||
<group string="Dienstwohnung (§ 2 SBWVO)">
|
||||
<field name="l10n_at_sb_wohnung_aktiv"/>
|
||||
<field name="l10n_at_sb_wohnung_flaeche" invisible="not l10n_at_sb_wohnung_aktiv"/>
|
||||
<field name="l10n_at_sb_wohnung_bundesland" invisible="not l10n_at_sb_wohnung_aktiv"/>
|
||||
<field name="l10n_at_sb_wohnung_norm" invisible="not l10n_at_sb_wohnung_aktiv"/>
|
||||
<field name="l10n_at_sb_wohnung_besorger" invisible="not l10n_at_sb_wohnung_aktiv"/>
|
||||
<field name="l10n_at_sb_wohnung_arbeitsplatznah" invisible="not l10n_at_sb_wohnung_aktiv"/>
|
||||
<field name="l10n_at_sb_wohnung_kein_mittelpunkt" invisible="not l10n_at_sb_wohnung_aktiv"/>
|
||||
<field name="l10n_at_sb_wohnung_ueberlassung_ab" invisible="not l10n_at_sb_wohnung_aktiv"/>
|
||||
<field name="l10n_at_sb_wohnung_kostenbeitrag" invisible="not l10n_at_sb_wohnung_aktiv"/>
|
||||
</group>
|
||||
<group string="Parkplatz / Arbeitsmittel">
|
||||
<field name="l10n_at_sb_parkplatz_aktiv"/>
|
||||
<field name="l10n_at_sb_parkplatz_ausnahme" invisible="not l10n_at_sb_parkplatz_aktiv"/>
|
||||
<field name="l10n_at_sb_parkplatz_kostenbeitrag" invisible="not l10n_at_sb_parkplatz_aktiv"/>
|
||||
<field name="l10n_at_sb_handy_profil"/>
|
||||
</group>
|
||||
</group>
|
||||
</group>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
Reference in New Issue
Block a user