[IMP] l10n_at_hr_payroll: GP4 SV/DB-Kern (DG-Beiträge, Geringfügigkeit, § 49, Beitragsmonat, DAG)

AP4-Kernbau (D3-Vorfinanzierung, freigegebener GP4-Bauplan): Die
Dienstgeberseite der Sozialversicherung wird sektorneutral im Kern
gebaut, damit sowohl das General-AT-Produkt als auch die GemBG-Schiene
sie konsumieren (keine Fork-Werte, payroll-Skill-Architekturregel).

Neu in models/sozialversicherung.py:
- SVDG laufend (ÖGK Normalbetriebe 2026: 20,98 %; Wien 21,23 %) und auf
  Sonderzahlungen (§ 54 ASVG: 20,48 % inkl. UV 1,10 und IESG 0,10);
- Geringfügigkeit § 5 Abs 2 ASVG mit Hochrechnung bei untermonatiger
  Beschäftigung (SZ zählen nicht) und §-53a-Abs-1-UV als Monatspauschale;
- beitragsfreie Bezüge § 49 Abs 3 ASVG als Jahres-Kumulative je DN
  (8 €/AT, 1.000/365/186/300/2.000 €/J, Homeoffice 3 €/Tag × 100);
- altersabhängige Satzausschaltungen als Stichtags-Datumfelder am
  Vertrag: AlVG § 1 Abs 2 lit e (AV DN+DG und IESG), ASVG § 51 Abs 6
  (UV ab 60/Lehrlinge), § 51 Abs 7 (PV in der Bonusphase je hälftig
  DG+DN), FLAG § 41 Abs 4 lit f (LNK-Freiheit ab 60);
- ALV-DN-Staffel § 2a AMPFG, WF-Satzvektor Bundesland × Jahr (Wien
  0,75/0,75 ab 1.1.2026, löst die KB-Protokollnotiz), Beitragsmonats-
  Allokation (Anspruchsprinzip: Sätze/HBG historisch je Gruppe) und
  DAG-Bewertung § 1 DAG (19,4 %, Schwelle 1,5 × GFG, ≥ 2 Geringfügige).

Quellenverifikation (verbindlich): ÖGK-Beitragstarifsystem gültig ab
01.07.2026, TASY-LSWH-Export 2026-03-24, Beitragsrechtliche Werte 2026
und RIS-Normtexte. GFG 2026 = 551,10 € (Aufwertung ausgesetzt gem.
§ 810 Abs 3 ASVG — löst KB-Konflikt A7-Rest); §-19a-Selbstversicherung
2026 = 83,49 €/Monat, DG-UV daraus abgeleitet 6,50 €/Monat.

Rechtskorrektur § 2a AMPFG: Die Staffel mindert nur den DN-Anteil
(ÖGK-Abschläge 'Mind. AV auf 0/1/2 %') und gilt auch für SZ-Beiträge;
bei 2.059,41 € BG sinkt DN-SV von pauschal 18,07 % auf 15,12 %.
test_lohnsteuer wurde entsprechend angepasst (Rechenfälle RF-L1 ff.).

Tests: test_sozialversicherung (14 Fälle); Suite 74/74 grün auf
odoo_dev. Rechenfälle: l10n_at_hr_payroll_private/docs/rechenfaelle/
sozialversicherung.md.
This commit is contained in:
2026-09-11 23:21:55 +02:00
parent f638f5100d
commit a2a823ad35
9 changed files with 1136 additions and 38 deletions
+15 -3
View File
@@ -1,7 +1,7 @@
# Part of the odoo-at-payroll project. License: LGPL-3.
{
'name': 'Österreich - Personalverrechnung Kern (AT Payroll Core)',
'version': '19.0.2.0.0',
'version': '19.0.3.0.0',
'author': 'ixsol',
'website': 'https://gem360.at',
'category': 'Human Resources/Payroll',
@@ -19,10 +19,21 @@ Produkt (``l10n_at_hr_payroll_private``). Grundlagen: ``personalverrechnung/``
generiert und gegen den offiziellen SV-Werte-Report gespiegelt):
KV/PV/UV/AV/AK/WF/BV/IESG-Beitragssätze, Geringfügigkeitsgrenze,
Höchstbeitragsgrundlagen (monatlich/SZ), AV-Staffel § 2a AMPFG;
- **Lohnsteuer (AP3, GP3)**: ``models/lohnsteuer.py`` (§ 66 kumulativ mit
Rollung, § 67 Sechstel/Fixsätze, § 68-Steuerfreiheiten),
``rule_parameters_lst_2026.xml``;
- **SV/DB (AP4, GP4)**: ``models/sozialversicherung.py`` — DG-Beiträge
(SVDG, ÖGK 2026: 20,98 %), Geringfügigkeit (§ 5 Abs 2 iVm § 810 Abs 3
ASVG) + § 53a-UV, beitragsfreie Bezüge § 49 Abs 3 mit Jahres-
Kumulativen, altersabhängige Satzausschaltungen (AlVG § 1 Abs 2 lit e,
ASVG § 51 Abs 6/7, IESG § 12), WF-Satzvektor Bundesland × Jahr
(Wien 1,5 % ab 2026), ALV-DN-Staffel § 2a AMPFG, Beitragsmonats-
Allokation (Anspruchsprinzip), DAG-Bewertung § 1 DAG —
``rule_parameters_sv_2026.xml``;
- deklarative TASY-Mapping-Tabelle und Import-Wizard (Preview,
Sprungwarnung > 10 %, explizite Bestätigung, versionierte Werte);
- folgen nach Freigabe der Bgld.-APs: Lohnsteuer (§ 33/§ 68 EStG, AP3),
Meldewesen (L16/FinanzOnline, eSV/ELDA, AP5), Belege (AP6).
- folgen nach Freigabe der Bgld.-APs: Meldewesen (L16/FinanzOnline,
eSV/ELDA, AP5), Belege (AP6).
Kein VRV-Bezug: die Gemeinde-Buchhaltung (Anlage 3b, Ansatz/MVAG) läuft
über die Brücke ``l10n_at_gemeinde_payroll_vrv`` im gem360-Repo (AP7).
@@ -32,6 +43,7 @@ Kein VRV-Bezug: die Gemeinde-Buchhaltung (Anlage 3b, Ansatz/MVAG) läuft
'security/ir.model.access.csv',
'data/rule_parameters_2026.xml',
'data/rule_parameters_lst_2026.xml',
'data/rule_parameters_sv_2026.xml',
'data/tasy_mapping.xml',
'views/tasy_views.xml',
],
@@ -65,15 +65,31 @@
<field name="date_from">2024-01-01</field>
<field name="parameter_value">2.95</field>
</record>
<!-- ALV-Staffel § 2a Abs 1 Z 1-3 AMPFG: mindert NUR den DN-Anteil
(ÖGK-Tarifsystem Abschläge „Mind. AV auf 0/1/2 %" = -2,95/-1,95/
-0,95); Grenzen nicht zu aliquotieren; gilt auch für SZ-Beiträge
(lb-son-03). Werte 2025: 2.074/2.262/2.451 € (lb-son-03);
2026: 2.225/2.427/2.630 € (Beitragsrechtliche Werte 2026, Fn. 3);
§ 2a mit 31.12.2026 aufgehoben (BGBl I 62/2026, KB A6). -->
<record id="hr_rule_parameter_at_sv_av_staffel_ampfg" model="hr.rule.parameter">
<field name="name">SV AV-Beitragsstaffel bei geringem Einkommen (§ 2a AMPFG)</field>
<field name="name">SV ALV-DN-Staffel bei geringem Einkommen (§ 2a AMPFG)</field>
<field name="code">at_sv_av_staffel_ampfg</field>
<field name="country_id" ref="base.at"/>
</record>
<record id="hr_rule_parameter_value_at_sv_av_staffel_2025" model="hr.rule.parameter.value">
<field name="rule_parameter_id" ref="hr_rule_parameter_at_sv_av_staffel_ampfg"/>
<field name="date_from">2025-01-01</field>
<field name="parameter_value">{'tiers': [(0.0, 2074.00, 0.0), (2074.00, 2262.00, 1.0), (2262.00, 2451.00, 2.0)], 'dn_voll': 2.95}</field>
</record>
<record id="hr_rule_parameter_value_at_sv_av_staffel_ampfg" model="hr.rule.parameter.value">
<field name="rule_parameter_id" ref="hr_rule_parameter_at_sv_av_staffel_ampfg"/>
<field name="date_from">2026-07-01</field>
<field name="parameter_value">{'tiers': [(0.0, 2225.00, 0.0), (2225.00, 2427.00, 1.0), (2427.00, 2630.00, 2.0)], 'av_total': 5.9}</field>
<field name="date_from">2026-01-01</field>
<field name="parameter_value">{'tiers': [(0.0, 2225.00, 0.0), (2225.00, 2427.00, 1.0), (2427.00, 2630.00, 2.0)], 'dn_voll': 2.95}</field>
</record>
<record id="hr_rule_parameter_value_at_sv_av_staffel_2027" model="hr.rule.parameter.value">
<field name="rule_parameter_id" ref="hr_rule_parameter_at_sv_av_staffel_ampfg"/>
<field name="date_from">2027-01-01</field>
<field name="parameter_value">{'tiers': [], 'dn_voll': 2.95}</field>
</record>
<record id="hr_rule_parameter_at_sv_bv_dg" model="hr.rule.parameter">
<field name="name">Betriebliche Vorsorge Dienstgeberanteil</field>
@@ -110,6 +126,12 @@
<field name="code">at_sv_hbg_monatlich</field>
<field name="country_id" ref="base.at"/>
</record>
<!-- 2025: 30 × 215 €/Kalendertag (§ 108 Abs 3 VO-Reihe, KB A8) -->
<record id="hr_rule_parameter_value_at_sv_hbg_2025" model="hr.rule.parameter.value">
<field name="rule_parameter_id" ref="hr_rule_parameter_at_sv_hbg_monatlich"/>
<field name="date_from">2025-01-01</field>
<field name="parameter_value">6450.0</field>
</record>
<record id="hr_rule_parameter_value_at_sv_hbg_monatlich" model="hr.rule.parameter.value">
<field name="rule_parameter_id" ref="hr_rule_parameter_at_sv_hbg_monatlich"/>
<field name="date_from">2026-01-01</field>
@@ -0,0 +1,181 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- ============================================================ -->
<!-- GP4 SV/DB-Parameter (AP4). Quellen (verbindlich, 2026-09-11): -->
<!-- - ÖGK-Beitragstarifsystem Normalbetriebe Allgemeiner Teil, -->
<!-- gültig ab 01.07.2026 (.additional_docs/oegk/) -->
<!-- - TASY-LSWH-Export 2026-03-24 (personalverrechnung/quellen/) -->
<!-- - Beitragsrechtliche Werte 2026 (sozialversicherung.at, -->
<!-- .firecrawl/ris/sv-at/) -->
<!-- - RIS-Normtexte: ASVG §§ 5/45/49/51/53a/54/810, AKG § 61, -->
<!-- AlVG § 1 Abs 2 lit e, AMPFG § 2a, IESG § 12, FLAG § 41, -->
<!-- KommStG §§ 5/9, WKG §§ 122/126, DAG § 1 -->
<!-- ============================================================ -->
<!-- Wohnbauförderungsbeitrag: Satzvektor Bundesland × Jahr. -->
<!-- Bundesstandard 0,5/0,5; Wien ab 1.1.2026 gesamt 1,5 % = -->
<!-- 0,75 DN + 0,75 DG (Wiener WF-Beitragstarif 2018 § 1; -->
<!-- Beitragsrechtliche Werte 2026, Fn. 5; A2). Ohne 'wien'-Eintrag -->
<!-- greift 'default' (Wien-Zuschlag erst ab 1.1.2026). -->
<record id="hr_rule_parameter_at_sv_wf_vektor" model="hr.rule.parameter">
<field name="name">WF-Beitrag Satzvektor (Bundesland × Jahr)</field>
<field name="code">at_sv_wf_vektor</field>
<field name="country_id" ref="base.at"/>
</record>
<record id="hr_rule_parameter_value_at_sv_wf_vektor_2019" model="hr.rule.parameter.value">
<field name="rule_parameter_id" ref="hr_rule_parameter_at_sv_wf_vektor"/>
<field name="date_from">2019-01-01</field>
<field name="parameter_value">{'default': {'dn': 0.5, 'dg': 0.5}}</field>
</record>
<record id="hr_rule_parameter_value_at_sv_wf_vektor_2026" model="hr.rule.parameter.value">
<field name="rule_parameter_id" ref="hr_rule_parameter_at_sv_wf_vektor"/>
<field name="date_from">2026-01-01</field>
<field name="parameter_value">{'wien': {'dn': 0.75, 'dg': 0.75}, 'default': {'dn': 0.5, 'dg': 0.5}}</field>
</record>
<!-- Beitragsfreie Bezüge § 49 Abs 3 ASVG (Stand 2026-07, -->
<!-- lb-sva-03/lb-sva-04): Mahlzeiten-Gutscheine 8 €/Arbeitstag -->
<!-- (Z 12; Lebensmittel-Gutscheine 2 €/AT), Mitarbeiterrabatte -->
<!-- bis 20 % je Fall ODER 1.000 €/Jahr (Z 29; R1: Jahrespool), -->
<!-- Betriebsveranstaltungen 365 €/Jahr und Jubiläums-Sach- -->
<!-- zuwendungen 186 €/Jahr (Z 17), Zukunftssicherung 300 €/Jahr -->
<!-- (Z 18 lit a), Kinderbetreuungszuschuss 2.000 €/Kind/Jahr -->
<!-- (Z 11 lit d; R1: Pool je DN), Homeoffice-Pauschale 3 €/Tag -->
<!-- max 100 Tage (Z 31 iVm § 26 Z 9 EStG). Überschreiten = -->
<!-- beitragspflichtiger Sachbezug (Umwidmung). -->
<record id="hr_rule_parameter_at_sv49_grenzen" model="hr.rule.parameter">
<field name="name">Beitragsfreie Bezüge § 49 Abs 3 ASVG (Grenzen)</field>
<field name="code">at_sv49_grenzen</field>
<field name="country_id" ref="base.at"/>
</record>
<record id="hr_rule_parameter_value_at_sv49_grenzen" model="hr.rule.parameter.value">
<field name="rule_parameter_id" ref="hr_rule_parameter_at_sv49_grenzen"/>
<field name="date_from">2026-01-01</field>
<field name="parameter_value">{'mahlzeit_tag': 8.0, 'lebensmittel_tag': 2.0, 'rabatt_jahr': 1000.0, 'betriebsveranstaltung_jahr': 365.0, 'jubilaeum_jahr': 186.0, 'zukunftssicherung_jahr': 300.0, 'kinderbetreuung_jahr': 2000.0, 'homeoffice_tag': 3.0, 'homeoffice_tage': 100}</field>
</record>
<!-- § 53a ASVG geringfügig Beschäftigte: DG-UV 1,1 % der -->
<!-- allgemeinen Beitragsgrundlage (Abs 1) als Monatspauschale; -->
<!-- Selbstversicherung KV+PV 14,12 % der aBG (Abs 3) = -->
<!-- 83,49 €/Monat 2026 (§ 77 Abs 2a ASVG; TASY KVPVBTG19A: -->
<!-- 2024: 73,20 · 2025: 77,81 · 2026: 83,49). Direct-Zahlung der -->
<!-- versicherten Person an den Träger — KEIN Lohnabzug (GP4). -->
<!-- beitrag_uv abgeleitet: aBG 2026 = 83,49 ÷ 0,1412 = 591,29 → -->
<!-- 1,1 % = 6,50 €/Monat ⚠ direkter Beleg folgt (GP8-Parameter- -->
<!-- pflege). GFG 2026 = 551,10 € (Aufwertung ausgesetzt, -->
<!-- § 810 Abs 3 ASVG — A7-Rest gelöst). -->
<record id="hr_rule_parameter_at_sv_geringfuegig" model="hr.rule.parameter">
<field name="name">Geringfügig Beschäftigte — § 53a ASVG (UV/Selbstversicherung)</field>
<field name="code">at_sv_geringfuegig</field>
<field name="country_id" ref="base.at"/>
</record>
<record id="hr_rule_parameter_value_at_sv_geringfuegig_2025" model="hr.rule.parameter.value">
<field name="rule_parameter_id" ref="hr_rule_parameter_at_sv_geringfuegig"/>
<field name="date_from">2025-01-01</field>
<field name="parameter_value">{'beitrag_uv': 6.06, 'beitrag_selbstversicherung': 77.81}</field>
</record>
<record id="hr_rule_parameter_value_at_sv_geringfuegig_2026" model="hr.rule.parameter.value">
<field name="rule_parameter_id" ref="hr_rule_parameter_at_sv_geringfuegig"/>
<field name="date_from">2026-01-01</field>
<field name="parameter_value">{'beitrag_uv': 6.50, 'beitrag_selbstversicherung': 83.49}</field>
</record>
<!-- PV-Halbierung in der Bonusphase (§ 51 Abs 7 ASVG): normativer -->
<!-- Faktor 0,5 für DG- und DN-Anteil („A 15"; Pensionskonto- -->
<!-- Gutschrift bleibt ungekürzt — mBGM-Auswertung GP7/GP8). -->
<record id="hr_rule_parameter_at_pv_bonus_faktor" model="hr.rule.parameter">
<field name="name">PV-Faktor Bonusphase (§ 51 Abs 7 ASVG)</field>
<field name="code">at_pv_bonus_faktor</field>
<field name="country_id" ref="base.at"/>
</record>
<record id="hr_rule_parameter_value_at_pv_bonus_faktor" model="hr.rule.parameter.value">
<field name="rule_parameter_id" ref="hr_rule_parameter_at_pv_bonus_faktor"/>
<field name="date_from">2019-01-01</field>
<field name="parameter_value">0.5</field>
</record>
<!-- FLAF-Dienstgeberbeitrag § 41 Abs 5 FLAG: 3,7 % ab 2025, -->
<!-- 2,7 % ab 2028 (RQ Abschn. 6 ✅; lb-lnk-02: Satzhistorie -->
<!-- 2016: 4,5 · 2017: 4,1 · 2018: 3,9 · ab 2025 dauerhaft 3,7). -->
<record id="hr_rule_parameter_at_flaf_db" model="hr.rule.parameter">
<field name="name">FLAF-Dienstgeberbeitrag (§ 41 Abs 5 FLAG)</field>
<field name="code">at_flaf_db</field>
<field name="country_id" ref="base.at"/>
</record>
<record id="hr_rule_parameter_value_at_flaf_db_2018" model="hr.rule.parameter.value">
<field name="rule_parameter_id" ref="hr_rule_parameter_at_flaf_db"/>
<field name="date_from">2018-01-01</field>
<field name="parameter_value">3.9</field>
</record>
<record id="hr_rule_parameter_value_at_flaf_db" model="hr.rule.parameter.value">
<field name="rule_parameter_id" ref="hr_rule_parameter_at_flaf_db"/>
<field name="date_from">2025-01-01</field>
<field name="parameter_value">3.7</field>
</record>
<record id="hr_rule_parameter_value_at_flaf_db_2028" model="hr.rule.parameter.value">
<field name="rule_parameter_id" ref="hr_rule_parameter_at_flaf_db"/>
<field name="date_from">2028-01-01</field>
<field name="parameter_value">2.7</field>
</record>
<!-- Kommunalsteuer § 9 KommStG 1993: 3 % (RQ Abschn. 7 ✅). -->
<record id="hr_rule_parameter_at_kommst_satz" model="hr.rule.parameter">
<field name="name">Kommunalsteuer (§ 9 KommStG 1993)</field>
<field name="code">at_kommst_satz</field>
<field name="country_id" ref="base.at"/>
</record>
<record id="hr_rule_parameter_value_at_kommst_satz" model="hr.rule.parameter.value">
<field name="rule_parameter_id" ref="hr_rule_parameter_at_kommst_satz"/>
<field name="date_from">2025-01-01</field>
<field name="parameter_value">3.0</field>
</record>
<!-- LNK-Freibetragsstaffel (§ 41 Abs 4 FLAG wortgleich § 9 -->
<!-- KommStG; ident auch für den DZ, lb-lnk-07): monatliche BG -->
<!-- ≤ 1.095 € → keine Abgabe · 1.0951.460 € → Freibetrag 1.095 € -->
<!-- · > 1.460 € → volle BG. -->
<record id="hr_rule_parameter_at_lnk_freibetrag" model="hr.rule.parameter">
<field name="name">LNK-Freibetrag (FLAF/KommSt/DZ; § 41 Abs 4 FLAG)</field>
<field name="code">at_lnk_freibetrag</field>
<field name="country_id" ref="base.at"/>
</record>
<record id="hr_rule_parameter_value_at_lnk_freibetrag" model="hr.rule.parameter.value">
<field name="rule_parameter_id" ref="hr_rule_parameter_at_lnk_freibetrag"/>
<field name="date_from">2025-01-01</field>
<field name="parameter_value">{'freibetrag': 1095.0, 'grenze': 1460.0}</field>
</record>
<!-- Zuschlag zum Dienstgeberbeitrag (§§ 122/126 WKG): Landeskammer- -->
<!-- sätze 2026 INKL. Bundeskammeranteil 0,12 % (lb-lnk-07, Stand -->
<!-- 2026-08; ⚠ WKO/WKG-Volltext-Verifikation offen — 8.11). BG -->
<!-- identisch FLAF-DB inkl. §-41-Abs-4-Ausnahmen. -->
<record id="hr_rule_parameter_at_dz_vektor" model="hr.rule.parameter">
<field name="name">Zuschlag zum Dienstgeberbeitrag (WKG; je Bundesland)</field>
<field name="code">at_dz_vektor</field>
<field name="country_id" ref="base.at"/>
</record>
<record id="hr_rule_parameter_value_at_dz_vektor" model="hr.rule.parameter.value">
<field name="rule_parameter_id" ref="hr_rule_parameter_at_dz_vektor"/>
<field name="date_from">2026-01-01</field>
<field name="parameter_value">{'laender': {'burgenland': 0.40, 'kaernten': 0.37, 'niederoesterreich': 0.33, 'oberoesterreich': 0.31, 'salzburg': 0.35, 'steiermark': 0.34, 'tirol': 0.39, 'vorarlberg': 0.33, 'wien': 0.36}}</field>
</record>
<!-- Dienstgeberabgabe geringfügig Beschäftigter (§ 1 DAG; ÖGK- -->
<!-- Tarifsystem „Dienstgeberabgabe gem. § 1 DAG"): 19,4 % der -->
<!-- Lohnsumme (Beitragsgrundlagen ohne SZ) aller Geringfügigen je -->
<!-- Kalendermonat, wenn Lohnsumme > 1,5 × GFG (2026: 826,65 € — -->
<!-- TASY MINGRDAGP1, offene Gültigkeit ab 2025-01-01) und -->
<!-- mindestens 2 Geringfügige. UV läuft separat über § 53a Abs 1 -->
<!-- („mit UV 20,5 %"); ab 60. LJ entfällt der UV-Anteil. -->
<!-- Ausweis/Veranlagung: GP8. -->
<record id="hr_rule_parameter_at_dag" model="hr.rule.parameter">
<field name="name">Dienstgeberabgabe geringfügig Beschäftigter (§ 1 DAG)</field>
<field name="code">at_dag</field>
<field name="country_id" ref="base.at"/>
</record>
<record id="hr_rule_parameter_value_at_dag" model="hr.rule.parameter.value">
<field name="rule_parameter_id" ref="hr_rule_parameter_at_dag"/>
<field name="date_from">2025-01-01</field>
<field name="parameter_value">{'satz': 19.4, 'schwelle_faktor': 1.5}</field>
</record>
</odoo>
+3 -1
View File
@@ -1,3 +1,5 @@
# Part of the odoo-at-payroll project. License: LGPL-3.
from . import tasy
from . import lohnsteuer
from . import lohnsteuer
from . import sozialversicherung
from . import sozialversicherung
+11 -21
View File
@@ -198,29 +198,15 @@ class HrPayslip(models.Model):
return ergebnis
# ------------------------------------------------------------------
# DN-SV (Bemessungsbestandteil § 62; Werte at_sv_* — GP4 baut
# DG-Seite/Geringfügigkeit/beitragsfreie Kumulative darauf)
# DN-SV (Bemessungsbestandteil § 62) — Basis/Sätze/Geringfügigkeit/
# §-49-Kumulative/DG-Seite: models/sozialversicherung.py (GP4)
# ------------------------------------------------------------------
def _l10n_at_svdn_basis_laufen(self, result_rules=None):
"""SV-Beitragsgrundlage laufend (HBG-capped, at_sv_*)."""
self.ensure_one()
hbg = float(self._l10n_at_param('at_sv_hbg_monatlich'))
return min(max(self._l10n_at_brutto('laufend', result_rules), 0.0), hbg)
def _l10n_at_svdn(self, komponente, result_rules=None):
"""DN-SV-Komponente auf laufender Basis: kv/pv/av/ak/wf —
AK § 61 AKG und WF § 62 Z 5 nur laufend (nicht auf §-67-Bezüge);
WF-Wien über Company-Flag (DN 0,75 statt 0,50)."""
Wien-WF über Satzvektor (l10n_at_wf_wien/Bundesland, GP4)."""
self.ensure_one()
basis = self._l10n_at_svdn_basis_laufen(result_rules)
codes = {'kv': 'at_sv_kv_dn', 'pv': 'at_sv_pv_dn',
'av': 'at_sv_av_dn', 'ak': 'at_sv_ak_dn'}
if komponente == 'wf':
satz = 0.75 if self.company_id.l10n_at_wf_wien \
else float(self._l10n_at_param('at_sv_wf_dn'))
else:
satz = float(self._l10n_at_param(codes[komponente]))
return basis * satz / 100.0
return self._l10n_at_sv_beitrag('dn', komponente, result_rules)
def _l10n_at_svdn_gesamt(self, result_rules=None):
self.ensure_one()
@@ -240,11 +226,15 @@ class HrPayslip(models.Model):
def _l10n_at_svdn_sz(self, result_rules=None):
"""SV-DN auf sonstigen Bezügen (§ 67 Abs 12 — vor Fixsätzen
abzuziehen): KV/PV/AV, ohne AK/WF (§ 62 Z 3/Z 5)."""
abzuziehen): KV/PV/AV, ohne AK/WF (§ 62 Z 3/Z 5); Satzausschal-
tungen/Steffel über _l10n_at_sv_satz (GP4)."""
self.ensure_one()
if self._l10n_at_geringfuegig(result_rules):
return 0.0
basis = self._l10n_at_svdn_basis_sz(result_rules)
satz = sum(float(self._l10n_at_param(code)) for code in
('at_sv_kv_dn', 'at_sv_pv_dn', 'at_sv_av_dn'))
satz = sum(self._l10n_at_sv_satz(
'dn', k, staffel_basis=self._l10n_at_sv_basis_roh(result_rules))
for k in ('kv', 'pv', 'av'))
return basis * satz / 100.0
# ------------------------------------------------------------------
@@ -0,0 +1,489 @@
# Part of the odoo-at-payroll project. License: LGPL-3.
"""Sozialversicherungs-Kern (GP4/AP4): DG-Beiträge (SVDG), Geringfügigkeit
(§ 5 Abs 2 ASVG iVm § 810 Abs 3 — 2026-Aufwertung ausgestellt), §-53a-
UV der Geringfügigen, beitragsfreie Bezüge nach § 49 Abs 3 ASVG mit
Jahres-Kumulativen, altersabhängige Satzausschaltungen (§ 1 Abs 2 lit e
AlVG, § 51 Abs 6/7 ASVG, § 12 Abs 2 IESG), WF-Satzvektor Bundesland ×
Jahr (Wien 1,5 % ab 1.1.2026), ALV-DN-Staffel (§ 2a AMPFG) und
Beitragsmonats-Allokation (Anspruchsprinzip, historisch versionierte
Sätze/HBG je Beitragsmonat).
Verifizierte Grundlagen (2026-09-11, s. RECHTSQUELLEN-Privat Abschn. 4
und docs/rechenfaelle/sozialversicherung.md):
- ÖGK-Beitragstarifsystem „Normalbetriebe Allgemeiner Teil", gültig ab
01.07.2026 (DN 18,07 % · DG 20,98 % · Wien DN 18,32/DG 21,23 %);
- TASY-LSWH-Export 2026-03-24 (GFG 551,10 · DAG-Grenze 826,65 ·
§-19a-Pauschalbeitrag 83,49 €/Monat — Primärquellen-Entscheidung);
- „Beitragsrechtliche Werte 2026" (sozialversicherung.at): GFG-Aufwertung
2026 ausgesetzt (§ 810 Abs 3 ASVG); AV-Staffel bis 2.225/2.427/2.630 €
0/1/2 % DN; WF Wien 0,75/0,75 ab 1.1.2026;
- ASVG §§ 51 Abs 6 (UV-Entfall 60. LJ/Lehrlinge), 51 Abs 7 (PV in der
Bonusphase hälftig, DG und DN), 53a Abs 1/3/5 (UV 1,1 % der allgemeinen
Beitragsgrundlage; Pauschalbeitrag 14,12 %, auch von SZ);
IESG § 12 Abs 1 Z 4 iVm Abs 2 (Entfall ab Folgemonat nach 63);
AlVG § 1 Abs 2 lit e (AV-Entfall, ab 2027 inkl. Korridorpension).
Laufzeit-Datenfluss wie GP3: Regeln reichen Zwischensummen über
``result_rules`` an die Compute-Methoden; abgeschlossene Läufe werden
über line_ids gelesen. Der Beitragsmonat wird je Regel geführt
(``l10n_at_beitragsmonat``) und auf die Lohnkontolinie durchgereicht.
"""
from datetime import date, timedelta
from odoo import api, fields, models
from odoo.tools.safe_eval import safe_eval
class HrSalaryRule(models.Model):
_inherit = 'hr.salary.rule'
l10n_at_sv49_frei = fields.Selection(
[('keine', 'Keine §-49-Abs-3-Befreiung'),
('mahlzeit', 'Mahlzeiten-Gutschein (§ 49 Abs 3 Z 12; 8 €/AT)'),
('rabatt', 'Mitarbeiterrabatt (§ 49 Abs 3 Z 29; 1.000 €/J)'),
('betriebsveranstaltung', 'Betriebsveranstaltung (Z 17; 365 €/J)'),
('jubilaeum', 'Jubiläums-Sachzuwendung (Z 17; 186 €/J)'),
('zukunftssicherung', 'Zukunftssicherung (Z 18 lit a; 300 €/J)'),
('kinderbetreuung', 'Kinderbetreuungszuschuss (Z 11 lit d; 2.000 €/Kind/J)'),
('homeoffice', 'Homeoffice-Pauschale (Z 31; 3 €/Tag, max 100 Tage)')],
string='AT § 49 Abs 3 (beitragsfrei)',
default='keine',
help='Beitragsfreie Bezugsbestandteile: der beitragsfreie Anteil '
'wird je Bezugsmonat von der SV-Beitragsgrundlage abgezogen; '
'Grenzen als Jahres-Kumulative je Dienstnehmer '
'(at_sv49_grenzen). Überschreiten = beitragspflichtiger '
'Sachbezug (Umwidmung). Menge = Arbeitstage (Z 12/Z 31).')
l10n_at_s67_abs = fields.Selection(
[('keine', 'Keine §-67-Abs-3/6-Begünstigung'),
('abs3', 'Abfertigung (§ 67 Abs 3 EStG)'),
('abs6', 'Abfindung (§ 67 Abs 6 EStG)')],
string='AT § 67 Abs 3/6',
default='keine',
help='Markiert Bezüge, die beim FLAF-Dienstgeberbeitrag (§ 41 '
'Abs 4 FLAG), Zuschlag zum DG-Beitrag (WKG) und der '
'Kommunalsteuer (§ 5 Abs 2 KommStG) aus der Bemessungsgrund'
'lage ausgenommen sind.')
l10n_at_beitragsmonat = fields.Date(
string='AT Beitragsmonat (Anspruchsprinzip)',
help='Monat, in dem der Anspruch auf diesen Bezug fällig wurde '
'(§ 17 ASVG). Leer = Abrechnungsmonat. Für Nachzahlungs-/'
'Korrekturlinien setzen: SV-Sätze, HBG und GFG werden '
'historisch versioniert je Beitragsmonat aufgelöst '
'(Lohnsteuer rollt kumulativ in den Zuflussmonat, GP3).')
class HrPayslipLine(models.Model):
_inherit = 'hr.payslip.line'
l10n_at_beitragsmonat = fields.Date(
string='AT Beitragsmonat',
compute='_compute_l10n_at_beitragsmonat',
store=True,
readonly=False,
index=True,
help='Beitragsmonat der Linie (Anspruchsprinzip); default '
'Abrechnungsmonat, über die Regel übersteuerbar.')
@api.depends('salary_rule_id.l10n_at_beitragsmonat', 'date_from')
def _compute_l10n_at_beitragsmonat(self):
for line in self:
line.l10n_at_beitragsmonat = (
line.salary_rule_id.l10n_at_beitragsmonat or line.date_from)
class HrVersion(models.Model):
_inherit = 'hr.version'
l10n_at_lehrling = fields.Boolean(
string='AT Lehrling',
help='Keine AK-Umlage (§ 61 AKG) und kein UV-Beitrag (§ 51 Abs 6 '
'ASVG — aus Mitteln der Unfallversicherung); GFG gilt nicht '
'(§ 5 Abs 3 ASVG).')
l10n_at_alv_entfall_ab = fields.Date(
string='AT ALV-Entfall ab (§ 1 Abs 2 lit e AlVG)',
help='AV-Beitrag DN+DG und IESG-Zuschlag entfallen ab diesem '
'Beitragsmonat (63. Lebensjahr oder erfüllte Alterspensions-'
'Anwartschaft; ab 1.1.2027 inkl. Korridorpension, '
'NOR40280219; § 12 Abs 2 IESG). Stichtagsregel am Vertrag.')
l10n_at_uv_entfall_ab = fields.Date(
string='AT UV-Entfall ab (§ 51 Abs 6 ASVG)',
help='Unfallversicherungsbeitrag entfällt ab diesem '
'Beitragsmonat (Vollendung des 60. Lebensjahres); die '
'Person bleibt unfallversichert.')
l10n_at_pv_bonusphase_ab = fields.Date(
string='AT PV-Bonusphase ab (§ 51 Abs 7 ASVG)',
help='Pensionsversicherungsbeitrag je zur Hälfte reduziert für '
'DG und DN (Bonusphase/Pensionsanspruch; ÖGK-Abschlag '
'A 15; Pensionskonto-Gutschrift bleibt ungekürzt — Auswertung '
'GP8).')
l10n_at_lnkp_frei_ab = fields.Date(
string='AT LNK-frei ab (§ 41 Abs 4 lit f FLAG)',
help='Arbeitslöhne ab diesem Kalendermonat sind vom FLAF-'
'Dienstgeberbeitrag und vom Zuschlag zum Dienstgeberbeitrag '
'(WKG) ausgenommen (Folgemonat nach Vollendung des 60. '
'Lebensjahres). Kommunalsteuer: keine Befreiung.')
class ResCompany(models.Model):
_inherit = 'res.company'
l10n_at_bundesland = fields.Selection(
[('burgenland', 'Burgenland'), ('kaernten', 'Kärnten'),
('niederoesterreich', 'Niederösterreich'),
('oberoesterreich', 'Oberösterreich'), ('salzburg', 'Salzburg'),
('steiermark', 'Steiermark'), ('tirol', 'Tirol'),
('vorarlberg', 'Vorarlberg'), ('wien', 'Wien')],
string='AT Bundesland (Betriebsstätte)',
help='Betriebsstätten-Bundesland für Bundesland-Satzvektoren '
'(z. B. DZ-Landeskammeranteil; WF-Wien zusätzlich über das '
'Company-Flag l10n_at_wf_wien steuerbar).')
l10n_at_wk_mitglied = fields.Boolean(
string='AT WKO-Mitgliedschaft (DZ-pflichtig)',
help='Zuschlag zum Dienstgeberbeitrag (§§ 122/126 WKG) nur für '
'Mitglieder der Wirtschaftskammerorganisation (§ 2 WKG).')
def _l10n_at_dag(self, monat_start):
"""Dienstgeberabgabe geringfügig Beschäftigter (§ 1 DAG) für den
Kalendermonat: 19,4 % der Lohnsumme (Beitragsgrundlagen ohne SZ)
aller geringfügig Beschäftigten, wenn die Lohnsumme das 1,5-fache
der monatlichen GFG überschreitet und mindestens zwei
Geringfügige beschäftigt sind (lb-bes-06; TASY MINGRDAGP1 2026:
826,65 €; Ausweis/Veranlagung GP8)."""
monat_ende = (monat_start + timedelta(days=32)).replace(day=1) \
- timedelta(days=1)
slips = self.env['hr.payslip'].search([
('company_id', '=', self.id),
('date_from', '<=', monat_ende),
('date_to', '>=', monat_start),
('state', 'not in', ('draft', 'cancel')),
('l10n_at_geringfuegig', '=', True),
])
lohnsumme = sum(
slip._l10n_at_sv_basis_roh() for slip in slips)
anzahl = len({s.employee_id.id for s in slips})
params = self.env['hr.rule.parameter']._get_parameter_from_code(
'at_dag', monat_start, raise_if_not_found=False) or {}
gfg = float(self.env['hr.rule.parameter']._get_parameter_from_code(
'at_sv_gfg_monatlich', monat_start,
raise_if_not_found=False) or 0.0)
schwelle = float(params.get('schwelle_faktor', 0.0)) * gfg
abgabe = 0.0
if anzahl >= 2 and lohnsumme > schwelle:
abgabe = lohnsumme * float(params.get('satz', 0.0)) / 100.0
return {'anzahl': anzahl, 'lohnsumme': lohnsumme,
'schwelle': schwelle, 'abgabe': abgabe}
class HrPayslip(models.Model):
_inherit = 'hr.payslip'
l10n_at_geringfuegig = fields.Boolean(
string='AT Geringfügig beschäftigt',
compute='_compute_l10n_at_geringfuegig',
store=True,
help='Laufendes Entgelt (§-49-bereinigt, hochgerechnet bei '
'untermonatiger Beschäftigung) überschreitet die GFG nicht '
'(§ 5 Abs 2 ASVG). DAG-Monitoring-Grundlage (§ 1 DAG).')
@api.depends('line_ids', 'date_from', 'date_to', 'version_id')
def _compute_l10n_at_geringfuegig(self):
for slip in self:
try:
slip.l10n_at_geringfuegig = slip._l10n_at_geringfuegig()
except Exception:
# Alter Lauf ohne GFG-Parameterzeile: keine Bewertung.
slip.l10n_at_geringfuegig = False
# ------------------------------------------------------------------
# Geringfügigkeit (§ 5 Abs 2 ASVG; SZ zählen nicht — lb-bes-06)
# ------------------------------------------------------------------
def _l10n_at_geringfuegig(self, result_rules=None):
"""Hochgerechnetes laufendes Entgelt ≤ GFG (2026: 551,10 € —
Aufwertung gem. § 810 Abs 3 ASVG ausgesetzt). Lehrlinge sind
nie geringfügig (§ 5 Abs 3 ASVG). Verhältnisse kürzer als ein
Naturalmonat: Gesamtentgelt direkt gegen die Monatsgrenze."""
self.ensure_one()
version = self.version_id
if version.l10n_at_lehrling:
return False
roh = self._l10n_at_sv_basis_roh(result_rules)
if roh <= 0.0:
# Ohne laufenden Bezug keine Statusbewertung (z. B.
# reiner SZ-Lauf): Normalbehandlung der SZ-Beiträge.
return False
if (version.contract_date_start and version.contract_date_end
and (version.contract_date_end - version.contract_date_start)
.days < 29):
vergleich = roh
else:
tage = self._l10n_at_lohnsteuertage()
vergleich = roh * 30.0 / max(tage, 1) if tage < 30 else roh
gfg = float(self.env['hr.rule.parameter']
._get_parameter_from_code(
'at_sv_gfg_monatlich', self.date_to,
raise_if_not_found=False) or 0.0)
return vergleich <= gfg
# ------------------------------------------------------------------
# Beitragsfreie Bezüge § 49 Abs 3 ASVG (Jahres-Kumulative je DN)
# ------------------------------------------------------------------
def _l10n_at_sv49_ytd(self, kategorie, menge=False):
"""Jahresverbrauch einer §-49-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_sv49_frei == kategorie)
if menge:
total += sum(linien.mapped('quantity'))
else:
total += sum(linien.mapped('total'))
return total
def _l10n_at_sv49_freie_laufen(self, result_rules=None):
"""Beitragsfreie Anteile laufender Bezüge dieses LZP: je Kategorie
Tages-/Jahresgrenzen (at_sv49_grenzen) minus Jahresverbrauch;
Überschreiten = beitragspflichtig (Umwidmung, § 49 Abs 3)."""
self.ensure_one()
grenzen = self.env['hr.rule.parameter']._get_parameter_from_code(
'at_sv49_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_sv49_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_sv49_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',
'zukunftssicherung': 'zukunftssicherung_jahr',
'kinderbetreuung': 'kinderbetreuung_jahr'}.get(
kategorie)
rest = max(
float(grenzen.get(pool))
- self._l10n_at_sv49_ytd(kategorie), 0.0)
frei = min(total, rest)
frei_gesamt += max(frei, 0.0)
return frei_gesamt
# ------------------------------------------------------------------
# Laufende Beitragsgrundlage — je Beitragsmonat (Anspruchsprinzip)
# ------------------------------------------------------------------
def _l10n_at_sv_basis_roh(self, result_rules=None):
"""Laufender Arbeitslohn minus beitragsfreie §-49-Anteile
(vor HBG-Cap; Geringfügigkeits-/Staffel-Basis)."""
self.ensure_one()
roh = self._l10n_at_brutto('laufend', result_rules) \
- self._l10n_at_sv49_freie_laufen(result_rules)
return max(roh, 0.0)
def _l10n_at_sv_basis_laufen_monate(self, result_rules=None):
"""[(Beitragsmonat, HBG-gecappte Basis), …]: laufender Arbeitslohn
(§-49-bereinigt), gruppiert nach Regel-Beitragsmonat; HBG je
Beitragsmonat historisch aufgelöst (at_sv_hbg_monatlich)."""
self.ensure_one()
gruppen = {}
for rule in self.struct_id.rule_ids:
if 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'))}
if not werte:
continue
monat = (rule.l10n_at_beitragsmonat or self.date_from) \
.replace(day=1)
gruppen[monat] = gruppen.get(monat, 0.0) + werte['total']
standard = self.date_from.replace(day=1)
if standard in gruppen:
gruppen[standard] -= self._l10n_at_sv49_freie_laufen(
result_rules)
ergebnis = []
for monat, basis in sorted(gruppen.items()):
hbg = float(self._l10n_at_param('at_sv_hbg_monatlich', monat))
ergebnis.append((monat, min(max(basis, 0.0), hbg)))
return ergebnis
def _l10n_at_svdn_basis_laufen(self, result_rules=None):
"""SV-Beitragsgrundlage laufend: Summe der je Beitragsmonat
HBG-gecappten, §-49-bereinigten Basen (at_sv_*)."""
self.ensure_one()
return sum(basis for _monat, basis
in self._l10n_at_sv_basis_laufen_monate(result_rules))
# ------------------------------------------------------------------
# Effektive Beitragssätze (Alter/Stichtäge, Wien-WF, ALV-DN-Staffel)
# ------------------------------------------------------------------
def _l10n_at_sv_satz(self, seite, komponente, monat=None,
staffel_basis=None):
"""Effektiver Satz (vH) je Seite ('dn'/'dg') und Komponente
(kv/pv/av/uv/ak/wf/iesg) zum Beitragsmonat, unter Berücksich-
tigung der Stichtags-Regeln am Vertrag und der ALV-DN-Staffel
(§ 2a AMPFG: reduziert nur den DN-Anteil; Grenzen nicht zu
aliquotieren; gilt auch für SZ-Beiträge)."""
self.ensure_one()
version = self.version_id
monat = monat or self.date_from
if komponente == 'wf':
vektor = self.env['hr.rule.parameter']._get_parameter_from_code(
'at_sv_wf_vektor', monat, raise_if_not_found=False)
satz = None
if vektor:
wien = (self.company_id.l10n_at_wf_wien
or self.company_id.l10n_at_bundesland == 'wien')
eintrag = vektor.get('wien' if wien else 'default') \
or vektor.get('default')
if eintrag:
satz = float(eintrag.get(seite, 0.0))
if satz is None:
satz = float(self.env['hr.rule.parameter']
._get_parameter_from_code(
'at_sv_wf_%s' % seite, monat,
raise_if_not_found=False) or 0.0)
return satz
satz = float(self._l10n_at_param(
'at_sv_%s_%s' % (komponente, seite), monat))
entfall_alv = (version.l10n_at_alv_entfall_ab
and monat >= version.l10n_at_alv_entfall_ab)
if komponente in ('av', 'iesg') and entfall_alv:
# AV DN+DG entfallen (AlVG § 1 Abs 2 lit e) — der IESG-
# Zuschlag folgt dem AV-Beitrag (§ 12 Abs 1 Z 4 iVm
# § 12 Abs 2 IESG).
return 0.0
if komponente == 'uv':
if version.l10n_at_lehrling \
or (version.l10n_at_uv_entfall_ab
and monat >= version.l10n_at_uv_entfall_ab):
return 0.0
if komponente == 'ak' and version.l10n_at_lehrling:
return 0.0
if komponente == 'iesg' and version.l10n_at_lehrling:
# § 12 Abs 2 IESG: für Lehrlinge gesamte Lehrzeit kein Zuschlag
return 0.0
if komponente == 'pv' and (
version.l10n_at_pv_bonusphase_ab
and monat >= version.l10n_at_pv_bonusphase_ab):
faktor = float(self.env['hr.rule.parameter']
._get_parameter_from_code(
'at_pv_bonus_faktor', monat,
raise_if_not_found=False) or 0.5)
satz *= faktor
if komponente == 'av' and seite == 'dn':
staffel = self.env['hr.rule.parameter']._get_parameter_from_code(
'at_sv_av_staffel_ampfg', monat, raise_if_not_found=False)
if staffel and staffel.get('tiers'):
basis = staffel_basis if staffel_basis is not None \
else self._l10n_at_sv_basis_roh()
if basis > 0:
for _von, bis, dn_satz in staffel['tiers']:
if basis <= float(bis):
return float(dn_satz)
# über der obersten Stufe: voller DN-Satz
return float(staffel.get('dn_voll', satz))
return satz
# ------------------------------------------------------------------
# Beiträge laufend (DN- wie DG-seitig; geringfügig = 0)
# ------------------------------------------------------------------
def _l10n_at_sv_beitrag(self, seite, komponente, result_rules=None):
"""Beitragssumme je Beitragsmonat: Basis × historischen Satz;
geringfügige Beschäftigung → 0 (stattdessen §-53a-UV, s. u.)."""
self.ensure_one()
if self._l10n_at_geringfuegig(result_rules):
return 0.0
total = 0.0
for monat, basis in self._l10n_at_sv_basis_laufen_monate(
result_rules):
satz = self._l10n_at_sv_satz(seite, komponente, monat, basis)
total += basis * satz / 100.0
return total
def _l10n_at_svdn(self, komponente, result_rules=None):
"""DN-SV-Komponente laufend: kv/pv/av/ak/wf — AK (§ 61 AKG) und
WF nur laufend, HBGL-Cap; Wien über WF-Satzvektor."""
self.ensure_one()
return self._l10n_at_sv_beitrag('dn', komponente, result_rules)
def _l10n_at_svdg(self, komponente, result_rules=None):
"""DG-SV-Komponente laufend: kv/pv/av/uv/wf/iesg (AK = 0);
ÖGK-Tarif „Normalbetriebe" 2026: DG-Gesamt 20,98 % (Wien 21,23)."""
self.ensure_one()
return self._l10n_at_sv_beitrag('dg', komponente, result_rules)
def _l10n_at_svdg_gesamt(self, result_rules=None):
self.ensure_one()
return sum(self._l10n_at_svdg(k, result_rules)
for k in ('kv', 'pv', 'av', 'uv', 'wf', 'iesg'))
# ------------------------------------------------------------------
# Beiträge auf sonstigen Bezügen (§ 54 ASVG; 60faches HBG je Jahr)
# ------------------------------------------------------------------
def _l10n_at_svdg_sz(self, komponente, result_rules=None):
"""DG-Beitrag auf sonstigen Bezügen: KV/PV/AV/UV und IESG-Zuschlag
(folgt dem AV-Beitrag) — ohne AK/WF (§ 62 Z 3/Z 5; lb-son-03:
DN 17,07 %; DG 20,48 % inkl. UV 1,10 + IESG 0,10)."""
self.ensure_one()
if self._l10n_at_geringfuegig(result_rules):
return 0.0
basis = self._l10n_at_svdn_basis_sz(result_rules)
satz = self._l10n_at_sv_satz(
'dg', komponente, staffel_basis=self._l10n_at_sv_basis_roh(
result_rules))
return basis * satz / 100.0
def _l10n_at_svdg_sz_gesamt(self, result_rules=None):
self.ensure_one()
return sum(self._l10n_at_svdg_sz(k, result_rules)
for k in ('kv', 'pv', 'av', 'uv', 'iesg'))
# ------------------------------------------------------------------
# § 53a ASVG — Geringfügig Beschäftigte
# ------------------------------------------------------------------
def _l10n_at_svdg_uv_geringfuegig(self, result_rules=None):
"""DG-UV geringfügig Beschäftigter: 1,1 % der allgemeinen
Beitragsgrundlage (§ 53a Abs 1 ASVG) als Monatspauschale
(at_sv_geringfuegig['beitrag_uv']; aBG 2026 = 83,49 ÷ 14,12 % =
591,29 → 6,50 € — abgeleitet, s. Rechenfälle). Entfällt bei
UV-Stichtag (60. LJ) bzw. Lehrlingen (§ 51 Abs 6 ASVG)."""
self.ensure_one()
if not self._l10n_at_geringfuegig(result_rules):
return 0.0
if self._l10n_at_sv_satz('dg', 'uv') <= 0.0:
return 0.0
params = self.env['hr.rule.parameter']._get_parameter_from_code(
'at_sv_geringfuegig', self.date_to, raise_if_not_found=False) \
or {}
return float(params.get('beitrag_uv', 0.0))
+2 -1
View File
@@ -1,3 +1,4 @@
# Part of the odoo-at-payroll project. License: LGPL-3.
from . import test_tasy_import
from . import test_lohnsteuer
from . import test_lohnsteuer
from . import test_sozialversicherung
@@ -156,13 +156,14 @@ class LohnsteuerTest(TransactionCase):
# DN-SV / Bemessung (user-Formel)
# ------------------------------------------------------------------
def test_05_svdn_und_bemessung(self):
"""SV-DN 18,07 % (KV/PV/AV/AK/WF) auf laufender Basis; Bemessung
= Arbeitslohn SV-DN (user-Formel)."""
"""SV-DN auf 2.059,41 €: § 2a AMPFG mindert den DN-AV auf 0 %
(BG ≤ 2.225 €) → DN-Gesamt 15,12 % statt 18,07 % (GP4-Staffel;
DG-AV bleibt 2,95 %); Bemessung = Arbeitslohn SV-DN."""
employee = self._employee()
self._regel('LFD', 2059.41)
slip = self._slip(employee, 1)
svdn = slip._l10n_at_svdn_gesamt()
self.assertAlmostEqual(svdn, 2059.41 * 0.1807, places=2)
self.assertAlmostEqual(svdn, 2059.41 * 0.1512, places=2)
bem = slip._l10n_at_bemessung_laufen()
self.assertAlmostEqual(bem, 2059.41 - svdn, places=2)
@@ -221,7 +222,7 @@ class LohnsteuerTest(TransactionCase):
self._regel('SZ', 2059.41, bemessung='sonstig')
slip_nov = self._slip(employee, 11)
sz_bem = slip_nov._l10n_at_sz_bemessung()
sv_sz = 2059.41 * 0.1707 # KV+PV+AV auf SZ-Basis
sv_sz = 2059.41 * 0.1412 # KV+PV auf SZ-Basis; AV-DN 0 (§ 2a)
self.assertAlmostEqual(sz_bem, 2059.41 - sv_sz, places=2)
# Sechstel = 2 × 2059,41 × 60 ÷ 60 = 4.118,82 > Freigrenze
self.assertAlmostEqual(
@@ -257,13 +258,13 @@ class LohnsteuerTest(TransactionCase):
python='result = payslip._l10n_at_lst_sonstige(result_rules)')
slip_nov = self._slip(employee, 11, confirm=True)
sz_nov = self._total(slip_nov, 'LST_SZ')
# SV-SZ = 2.059,41 × 17,07 % = 351,54 (KV+PV+AV auf SZ-Basis)
# SV-SZ = 2.059,41 × 14,12 % (KV+PV; AV-DN 0 nach § 2a AMPFG)
self.assertAlmostEqual(
sz_nov, (2059.41 * (1 - 0.1707) - 620.0) * 0.06, places=1)
sz_nov, (2059.41 * (1 - 0.1412) - 620.0) * 0.06, places=1)
slip_dez = self._slip(employee, 12)
sz_dez = self._total(slip_dez, 'LST_SZ')
# Jahres-SZ-Bemessung = 2 × (2.059,41 SV-SZ)
jahres_bem = 2 * 2059.41 * (1 - 0.1707)
jahres_bem = 2 * 2059.41 * (1 - 0.1412)
soll_jahr = (jahres_bem - 620.0) * 0.06
self.assertAlmostEqual(sz_dez, soll_jahr - sz_nov, places=1)
@@ -276,8 +277,8 @@ class LohnsteuerTest(TransactionCase):
slip = self._slip(employee, 1)
# Sechstel = 2.000 × 60/30 = 4.000 > 2.615 → Pauschalierung
self.assertAlmostEqual(slip._l10n_at_sechstel(), 4000.0, places=2)
sz_bem = slip._l10n_at_sz_bemessung() # 6.000 SV-SZ (1.024,20)
self.assertAlmostEqual(sz_bem, 6000.0 - 6000.0 * 0.1707, places=2)
sz_bem = slip._l10n_at_sz_bemessung() # 6.000 SV-SZ (14,12 %)
self.assertAlmostEqual(sz_bem, 6000.0 - 6000.0 * 0.1412, places=2)
self.assertAlmostEqual(
slip._l10n_at_sz_tarifisch_aktuell(), sz_bem - 4000.0, places=2)
self.assertAlmostEqual(
@@ -0,0 +1,400 @@
# Part of the odoo-at-payroll project. License: LGPL-3.
"""GP4 SV/DB-Kern: DG-Beiträge (ÖGK 2026), SZ-Beiträge (§ 54 ASVG),
Geringfügigkeit (§ 5 Abs 2 iVm § 810 Abs 3 ASVG) + §-53a-UV,
beitragsfreie Bezüge § 49 Abs 3 mit Jahres-Kumulativen, Altersstichtage
(AlVG § 1 Abs 2 lit e, ASVG § 51 Abs 6/7, IESG § 12), ALV-DN-Staffel
(§ 2a AMPFG), Beitragsmonats-Allokation und DAG (§ 1 DAG). Rechenfälle
s. addons/l10n_at_hr_payroll_private/docs/rechenfaelle/
sozialversicherung.md und RECHTSQUELLEN-Privat Abschnitt 4."""
from datetime import date, timedelta
from odoo.tests import TransactionCase, tagged
@tagged('post_install', '-at_install')
class SozialversicherungTest(TransactionCase):
def setUp(self):
super().setUp()
self.category = self.env['hr.salary.rule.category'].create({
'name': 'Test AT SV',
'code': 'TEST_AT_SV',
})
self.structure_type = self.env['hr.payroll.structure.type'].create({
'name': 'Test AT Kern Type',
})
self.structure = self.env['hr.payroll.structure'].create({
'name': 'Test AT SV',
'code': 'TEST_AT_SV',
'type_id': self.structure_type.id,
})
def _struktur(self, code):
"""Eigene Structure je Testfall-Stufe (Regeln kumulieren auf
geteilten Structures — Muster test_lohnsteuer.py)."""
return self.env['hr.payroll.structure'].create({
'name': 'Test AT %s' % code,
'code': 'TEST_AT_%s' % code,
'type_id': self.structure_type.id,
})
def _regel(self, code, betrag, bemessung='laufend', sv49='keine',
beitragsmonat=None, quantity='1.0', struct=None):
return self.env['hr.salary.rule'].create({
'name': code,
'code': code,
'struct_id': (struct or self.structure).id,
'category_id': self.category.id,
'condition_select': 'none',
'l10n_at_bemessung': bemessung,
'l10n_at_sv49_frei': sv49,
'l10n_at_beitragsmonat': beitragsmonat,
'amount_select': 'fix',
'amount_fix': betrag,
'quantity': quantity,
})
def _employee(self, version_vals=None):
employee = self.env['hr.employee'].create({'name': 'Test SV'})
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)})
if version_vals:
version.write(version_vals)
return employee
def _slip(self, employee, monat, jahr=2026, confirm=False,
struct=None):
erster = date(jahr, monat, 1)
naechster = (erster + timedelta(days=32)).replace(day=1)
slip = self.env['hr.payslip'].create({
'name': 'Test-SV %d/%d' % (monat, jahr),
'employee_id': employee.id,
'struct_id': (struct or self.structure).id,
'date_from': erster,
'date_to': naechster - timedelta(days=1),
})
slip.compute_sheet()
if confirm:
slip.action_payslip_done()
return slip
# ------------------------------------------------------------------
# DG-Beiträge (ÖGK „Normalbetriebe" 2026: DG 20,98 % / Wien 21,23)
# ------------------------------------------------------------------
def test_01_svdg_gesamt(self):
"""DG-Summe 20,98 % = KV 3,78 + PV 12,55 + AV 2,95 + UV 1,10 +
WF 0,50 + IESG 0,10 auf laufender, HBG-gecappter Basis."""
employee = self._employee()
self._regel('LFD', 2059.41)
slip = self._slip(employee, 1)
self.assertAlmostEqual(
slip._l10n_at_svdg('kv'), 2059.41 * 0.0378, places=2)
self.assertAlmostEqual(
slip._l10n_at_svdg('pv'), 2059.41 * 0.1255, places=2)
self.assertAlmostEqual(
slip._l10n_at_svdg('av'), 2059.41 * 0.0295, places=2)
self.assertAlmostEqual(
slip._l10n_at_svdg('uv'), 2059.41 * 0.011, places=2)
self.assertAlmostEqual(
slip._l10n_at_svdg('wf'), 2059.41 * 0.005, places=2)
self.assertAlmostEqual(
slip._l10n_at_svdg('iesg'), 2059.41 * 0.001, places=2)
self.assertAlmostEqual(
slip._l10n_at_svdg_gesamt(), 2059.41 * 0.2098, places=2)
def test_02_svdg_wien(self):
"""WF-Wien 0,75 % DG ab 1.1.2026 (Satzvektor); 2025 nur 0,5 %."""
employee = self._employee()
self._regel('LFD', 2059.41)
slip = self._slip(employee, 1)
slip.company_id.write({'l10n_at_wf_wien': True})
self.assertAlmostEqual(
slip._l10n_at_svdg('wf'), 2059.41 * 0.0075, places=2)
self.assertAlmostEqual(
slip._l10n_at_svdg_gesamt(), 2059.41 * 0.2123, places=2)
# 2025: Vektor-Zeile ohne 'wien'-Eintrag → Bundesstandard 0,5 %
slip_2025 = self._slip(employee, 3, jahr=2025)
self.assertAlmostEqual(
slip_2025._l10n_at_svdg('wf'), 2059.41 * 0.005, places=2)
# HBG 2025 = 30 × 215 € = 6.450 € (KB A8, VO-Reihe § 108 Abs 3)
self.assertAlmostEqual(
slip_2025._l10n_at_svdn_basis_laufen(), 2059.41, places=2)
# Bundesland-Schlüssel wirkt identisch zum Company-Flag
slip.company_id.write(
{'l10n_at_wf_wien': False, 'l10n_at_bundesland': 'wien'})
self.assertAlmostEqual(
slip._l10n_at_svdg('wf'), 2059.41 * 0.0075, places=2)
def test_03_svdg_sz(self):
"""DG auf sonstigen Bezügen: KV/PV/AV/UV/IESG = 20,48 %
(§ 54 ASVG; HBG monatlich + SZ-Jahreshöchstbetrag 13.860)."""
employee = self._employee()
self._regel('LFD', 2059.41)
slip_okt = self._slip(employee, 10, confirm=True)
self._regel('SZ', 2059.41, bemessung='sonstig')
slip_nov = self._slip(employee, 11)
for komp, satz in (('kv', 0.0378), ('pv', 0.1255),
('av', 0.0295), ('uv', 0.011),
('iesg', 0.001)):
self.assertAlmostEqual(
slip_nov._l10n_at_svdg_sz(komp), 2059.41 * satz, places=2)
self.assertAlmostEqual(
slip_nov._l10n_at_svdg_sz_gesamt(), 2059.41 * 0.2048, places=2)
# Dezember: Jahresrest 13.860 2.059,41 lässt die zweite SZ zu
slip_dez = self._slip(employee, 12)
self.assertAlmostEqual(
slip_dez._l10n_at_svdn_basis_sz(), 2059.41, places=2)
# Deckel: SZ 20.000 im Januar 2027 (Jahresrest voll) → HBG 6.930
self._regel('SZ2', 20000.0, bemessung='sonstig')
slip_2027 = self._slip(employee, 1, jahr=2027)
self.assertAlmostEqual(
slip_2027._l10n_at_svdn_basis_sz(),
min(20000.0 + 2059.41, 6930.0, 13860.0), places=2)
# ------------------------------------------------------------------
# Geringfügigkeit (§ 5 Abs 2 ASVG; Aufwertung 2026 ausgesetzt) + § 53a
# ------------------------------------------------------------------
def test_04_geringfuegig_uv(self):
"""Laufendes Entgelt ≤ GFG 551,10 €: keine SVDN/SVDG; stattdessen
§-53a-Abs-1-UV 6,50 €/Monat (1,1 % der allgemeinen BG)."""
employee = self._employee()
self._regel('LFD', 500.0)
slip = self._slip(employee, 1)
self.assertTrue(slip._l10n_at_geringfuegig())
self.assertAlmostEqual(slip._l10n_at_svdn_gesamt(), 0.0, places=2)
self.assertAlmostEqual(slip._l10n_at_svdg_gesamt(), 0.0, places=2)
self.assertAlmostEqual(
slip._l10n_at_svdg_uv_geringfuegig(), 6.50, places=2)
self.assertTrue(slip.l10n_at_geringfuegig) # stored (DAG-Suche)
# Über der Grenze: Vollversicherung, kein §-53a-UV
employee_b = self._employee()
struktur = self._struktur('GFG2')
self._regel('LFD', 600.0, struct=struktur)
slip_b = self._slip(employee_b, 1, struct=struktur)
self.assertFalse(slip_b._l10n_at_geringfuegig())
self.assertAlmostEqual(
slip_b._l10n_at_svdg_uv_geringfuegig(), 0.0, places=2)
self.assertAlmostEqual(
slip_b._l10n_at_svdg_gesamt(), 600.0 * 0.2098, places=2)
def test_05_geringfuegig_hochrechnung(self):
"""Untermonatiger Beginn: Hochrechnung auf den Monat (lb-bes-06);
Verhältnis < 1 Naturalmonat: Gesamtentgelt direkt gegen die GFG."""
employee = self._employee(
{'contract_date_start': date(2026, 1, 16)})
self._regel('LFD', 400.0)
slip = self._slip(employee, 1)
# 400 × 30/16 = 750 > 551,10 → voll versichert; DN-AV 0 (§ 2a:
# 400 ≤ 2.225) → DN-Gesamt 15,12 %
self.assertFalse(slip._l10n_at_geringfuegig())
self.assertAlmostEqual(
slip._l10n_at_svdn_gesamt(), 400.0 * 0.1512, places=2)
# 250 × 30/16 = 468,75 ≤ 551,10 → geringfügig
struktur = self._struktur('HGR')
employee_b = self._employee(
{'contract_date_start': date(2026, 1, 16)})
self._regel('LFD', 250.0, struct=struktur)
slip_b = self._slip(employee_b, 1, struct=struktur)
self.assertTrue(slip_b._l10n_at_geringfuegig())
# auf unter einen Naturalmonat befristet: direkt gegen die GFG
employee_c = self._employee({
'contract_date_start': date(2026, 1, 16),
'contract_date_end': date(2026, 1, 25)})
struktur_c = self._struktur('KURZ')
self._regel('LFD', 500.0, struct=struktur_c)
slip_c = self._slip(employee_c, 1, struct=struktur_c)
self.assertTrue(slip_c._l10n_at_geringfuegig())
def test_06_lehrling(self):
"""Lehrling: keine AK-Umlage (§ 61 AKG), kein UV (§ 51 Abs 6
ASVG); nie geringfügig (§ 5 Abs 3 ASVG)."""
employee = self._employee({'l10n_at_lehrling': True})
self._regel('LFD', 300.0)
slip = self._slip(employee, 1)
self.assertFalse(slip._l10n_at_geringfuegig())
self.assertAlmostEqual(slip._l10n_at_svdn('ak'), 0.0, places=2)
self.assertAlmostEqual(slip._l10n_at_svdg('uv'), 0.0, places=2)
self.assertAlmostEqual(slip._l10n_at_svdg_uv_geringfuegig(), 0.0)
# KV/PV/WF regulär; AV 0 (§ 2a), AK 0, IESG 0 (§ 12 Abs 2 IESG):
# DN 14,62 % / DG 19,78 %
self.assertAlmostEqual(
slip._l10n_at_svdn_gesamt(), 300.0 * 0.1462, places=2)
self.assertAlmostEqual(
slip._l10n_at_svdg_gesamt(), 300.0 * 0.1978, places=2)
# ------------------------------------------------------------------
# § 49 Abs 3 ASVG — beitragsfreie Bezugsbestandteile (Kumulative)
# ------------------------------------------------------------------
def test_07_sv49_mahlzeit(self):
"""Mahlzeiten-Gutscheine 8 €/Arbeitstag (Z 12): 110 € auf
10 Arbeitstage → 80 € beitragsfrei, 30 € Umwidmung."""
employee = self._employee()
self._regel('LFD', 2059.41)
self._regel('MAH', 11.0, sv49='mahlzeit', quantity='10.0')
slip = self._slip(employee, 1)
self.assertAlmostEqual(
slip._l10n_at_sv49_freie_laufen(), 80.0, places=2)
basis = slip._l10n_at_sv_basis_roh()
self.assertAlmostEqual(basis, 2059.41 + 110.0 - 80.0, places=2)
# DN-AV 0 (§ 2a: Basis 2.089,41 ≤ 2.225) → DN-Gesamt 15,12 %
self.assertAlmostEqual(
slip._l10n_at_svdn_gesamt(), basis * 0.1512, places=2)
def test_08_sv49_jahrespools(self):
"""Jahres-Kumulative: Betriebsveranstaltung 365 €/Jahr und
Homeoffice-Pauschale 3 €/Tag max 100 Tage (YTD-Verbrauch)."""
employee = self._employee()
self._regel('LFD', 2059.41)
self._regel('BVER', 300.0, sv49='betriebsveranstaltung')
slip_a = self._slip(employee, 3, confirm=True)
self.assertAlmostEqual(
slip_a._l10n_at_sv49_freie_laufen(), 300.0, places=2)
# April: BVER-Rest 65 €; Homeoffice 70 Tage frei (3 × 70 = 210)
self._regel('HO', 3.0, sv49='homeoffice', quantity='70.0')
slip_b = self._slip(employee, 4, confirm=True)
self.assertAlmostEqual(
slip_b._l10n_at_sv49_freie_laufen(), 65.0 + 210.0, places=2)
# Mai: BVER-Pool verbraucht; Homeoffice-Rest 30 Tage → 90 €
slip_c = self._slip(employee, 5)
self.assertAlmostEqual(
slip_c._l10n_at_sv49_freie_laufen(), 90.0, places=2)
# ------------------------------------------------------------------
# Altersabhängige Satzausschaltungen (Stichtags-Regeln am Vertrag)
# ------------------------------------------------------------------
def test_09_alv_entfall(self):
"""§ 1 Abs 2 lit e AlVG: AV DN+DG und IESG entfallen ab
Stichtag (§ 12 Abs 1 Z 4 iVm Abs 2 IESG); Nachzahlungslinien mit
früherem Beitragsmonat zahlen weiter AV (Anspruchsprinzip)."""
employee = self._employee(
{'l10n_at_alv_entfall_ab': date(2026, 1, 1)})
self._regel('LFD', 2059.41)
slip = self._slip(employee, 1)
self.assertAlmostEqual(slip._l10n_at_svdn('av'), 0.0, places=2)
self.assertAlmostEqual(slip._l10n_at_svdg('av'), 0.0, places=2)
self.assertAlmostEqual(slip._l10n_at_svdg('iesg'), 0.0, places=2)
# DG ohne AV+IESG: 20,98 2,95 0,10 = 17,93 %
self.assertAlmostEqual(
slip._l10n_at_svdg_gesamt(), 2059.41 * 0.1793, places=2)
# DN ohne AV: 18,07 2,95 = 15,12 %
self.assertAlmostEqual(
slip._l10n_at_svdn_gesamt(), 2059.41 * 0.1512, places=2)
# Nachzahlungslinie Beitragsmonat 6/2025: AV greift weiter
self._regel('NACH', 1000.0, beitragsmonat=date(2025, 6, 1))
slip2 = self._slip(employee, 2)
self.assertAlmostEqual(
slip2._l10n_at_svdg('av'), 1000.0 * 0.0295, places=2)
def test_10_uv_entfall(self):
"""§ 51 Abs 6 ASVG: UV entfällt ab 60. LJ — laufend und SZ."""
employee = self._employee(
{'l10n_at_uv_entfall_ab': date(2026, 1, 1)})
self._regel('LFD', 2059.41)
self._regel('SZ', 2059.41, bemessung='sonstig')
slip = self._slip(employee, 1)
self.assertAlmostEqual(slip._l10n_at_svdg('uv'), 0.0, places=2)
self.assertAlmostEqual(slip._l10n_at_svdg_sz('uv'), 0.0, places=2)
self.assertAlmostEqual(
slip._l10n_at_svdg_gesamt(), 2059.41 * 0.1988, places=2)
def test_11_pv_bonusphase(self):
"""§ 51 Abs 7 ASVG: PV je zur Hälfte reduziert (DG und DN)."""
employee = self._employee(
{'l10n_at_pv_bonusphase_ab': date(2026, 1, 1)})
self._regel('LFD', 2059.41)
slip = self._slip(employee, 1)
self.assertAlmostEqual(
slip._l10n_at_svdn('pv'), 2059.41 * 0.05125, places=2)
self.assertAlmostEqual(
slip._l10n_at_svdg('pv'), 2059.41 * 0.06275, places=2)
# ------------------------------------------------------------------
# ALV-DN-Staffel (§ 2a AMPFG — reduziert nur den DN-Anteil)
# ------------------------------------------------------------------
def test_12_av_staffel(self):
"""Bis 2.225 €: DN-AV 0 %; bis 2.427: 1 %; bis 2.630: 2 %
DG-AV bleibt stets 2,95 % (ÖGK „Mind. AV auf 0/1/2 %")."""
employee_a = self._employee()
struktur_a = self._struktur('ST1')
self._regel('LFD', 2000.0, struct=struktur_a)
slip_a = self._slip(employee_a, 1, struct=struktur_a)
self.assertAlmostEqual(slip_a._l10n_at_svdn('av'), 0.0, places=2)
self.assertAlmostEqual(
slip_a._l10n_at_svdg('av'), 2000.0 * 0.0295, places=2)
employee_b = self._employee()
struktur_b = self._struktur('ST2')
self._regel('LFD', 2500.0, struct=struktur_b)
slip_b = self._slip(employee_b, 1, struct=struktur_b)
self.assertAlmostEqual(
slip_b._l10n_at_svdn('av'), 2500.0 * 0.02, places=2)
self.assertAlmostEqual(
slip_b._l10n_at_svdg('av'), 2500.0 * 0.0295, places=2)
employee_c = self._employee()
struktur_c = self._struktur('ST3')
self._regel('LFD', 3000.0, struct=struktur_c)
slip_c = self._slip(employee_c, 1, struct=struktur_c)
self.assertAlmostEqual(
slip_c._l10n_at_svdn('av'), 3000.0 * 0.0295, places=2)
self.assertAlmostEqual(
slip_c._l10n_at_svdg('av'), 3000.0 * 0.0295, places=2)
# ------------------------------------------------------------------
# Beitragsmonats-Allokation (Anspruchsprinzip)
# ------------------------------------------------------------------
def test_13_beitragsmonat_hbg(self):
"""Nachzahlungslinie mit Beitragsmonat 2025: HBG historisch
versioniert (2025: 6.450 € statt 6.930 €), je Gruppe gecapped."""
employee = self._employee()
self._regel('LFD', 7000.0)
self._regel('NACH', 7000.0, beitragsmonat=date(2025, 6, 1))
slip = self._slip(employee, 1)
monate = dict(slip._l10n_at_sv_basis_laufen_monate())
self.assertAlmostEqual(
monate[date(2026, 1, 1)], 6930.0, places=2)
self.assertAlmostEqual(
monate[date(2025, 6, 1)], 6450.0, places=2)
self.assertAlmostEqual(
slip._l10n_at_svdg('kv'),
(6930.0 + 6450.0) * 0.0378, places=2)
# ------------------------------------------------------------------
# Dienstgeberabgabe geringfügig Beschäftigter (§ 1 DAG)
# ------------------------------------------------------------------
def test_14_dag(self):
"""DAG 19,4 %: Lohnsumme (BG ohne SZ) aller Geringfügigen >
826,65 € (= 1,5 × GFG 551,10, TASY MINGRDAGP1) und ≥ 2 Personen."""
employee_a = self._employee()
struktur_a = self._struktur('DAGA')
self._regel('LFD', 500.0, struct=struktur_a)
self._slip(employee_a, 1, confirm=True, struct=struktur_a)
employee_b = self._employee()
struktur_b = self._struktur('DAGB')
self._regel('LFD', 550.0, struct=struktur_b)
self._slip(employee_b, 1, confirm=True, struct=struktur_b)
company = self.env.company
dag = company._l10n_at_dag(date(2026, 1, 1))
self.assertAlmostEqual(dag['schwelle'], 826.65, places=2)
self.assertEqual(dag['anzahl'], 2)
self.assertAlmostEqual(dag['lohnsumme'], 1050.0, places=2)
self.assertAlmostEqual(dag['abgabe'], 1050.0 * 0.194, places=2)
# Zwei Geringfügige unter der Schwelle: keine DAG
employee_c = self._employee()
struktur_c = self._struktur('DAGC')
self._regel('LFD', 400.0, struct=struktur_c)
self._slip(employee_c, 2, confirm=True, struct=struktur_c)
employee_d = self._employee()
struktur_d = self._struktur('DAGD')
self._regel('LFD', 400.0, struct=struktur_d)
self._slip(employee_d, 2, confirm=True, struct=struktur_d)
dag_tief = company._l10n_at_dag(date(2026, 2, 1))
self.assertEqual(dag_tief['anzahl'], 2)
self.assertAlmostEqual(dag_tief['lohnsumme'], 800.0, places=2)
self.assertAlmostEqual(dag_tief['abgabe'], 0.0, places=2)
# Nur ein Geringfügiger: keine DAG
dag_einzel = company._l10n_at_dag(date(2026, 3, 1))
self.assertAlmostEqual(dag_einzel['abgabe'], 0.0, places=2)