Files
odoo-at-payroll/addons/l10n_at_hr_payroll/models/sozialversicherung.py
T
fegger a2a823ad35 [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.
2026-09-11 23:21:55 +02:00

489 lines
24 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Part of the odoo-at-payroll project. License: LGPL-3.
"""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))