[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
+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))