Files
odoo-at-payroll/addons/l10n_at_hr_payroll_private/models/abfertigung_private.py
T
fegger ec33dfacf2 [IMP] l10n_at_hr_payroll(_private): Meldewesen-Reste (AP17) und Status-Sonderfälle (AP18/AP19)
AP17 — Lohnnachweis L17, Finanz-Mitteilungen § 109a/§ 109b, GfB-Jahreszahlung:
- L17 (DM-ORG E.24, BEST LF, SART A1/1100, Version 28): je
  Lohnzahlungszeitraum je DN aus den GP3-Kennzahlen-Flags; Prüfkatalog-
  Semantik (KZ 350 = Brutto gesamt, 351/352/354 Teilbeträge) und formale
  Prüfungen (350-Formel, 357/347-Caps, aliquotierte 354/HOPA-Caps);
  ODL-PDF-Extraktion der Kapitel E.24/E.16/E.26 (txt feldversetzt)
- Finanz-Mitteilungen § 109a (W1, E.16) und § 109b (B1, E.26): Modelle
  je Leistungserbringer/Auftragnehmer, Schwellen als hr.rule.parameter
  (450/900 — § 109a-VO; 100.000 — § 109b Abs 4 lit a, RIS NOR40263383),
  AL31/32/33, LKZ1-7, UID, bis 3 maßgebliche natürliche Personen,
  KORR K/S; Unter-Schwellen-Zeilen entfallen mit Protokollhinweis
- mBGM: geringfügig Beschäftigte (BSGR B010/B030) — T01 § 53a-UV-
  Pauschale + V01 BMSVG 1,53 %, kein T02 (GfB-SZ beitragsfrei,
  § 49 Abs 3); JAGB-Pakettrennung für die jährliche Abrechnung
  (§ 58 Abs 8 ASVG iVm § 6 Abs 2a BMSVG, DM-ORG D.52) mit
  2,5 vH-Zuschlag (Z04) in der GfB-Jahresabrechnung (Fälligkeit
  15. 1. Folgejahr)
- Fristenkalender: L17- und § 109b-Fristarten (Ende Februar;
  § 109a/§ 109b EStG und § 58 Abs 8 ASVG RIS-verifiziert)

AP18 — Schwerarbeit/NSchG (Beschaffungsgate erledigt, RIS):
- NSchG (RIS 10008502, BGBl 1981/354) Art XI Abs 3 (2 vH Stammnorm,
  3,80 % via Sistierung) und Abs 6 (Nachtschwerarbeitsmonat: 6-Tage-
  Kriterium, Rückblick-Fenster 12/18, Randmonat); ASchV (20004642)
  § 1/§ 4/§ 5; DM-ORG E.22.2-Erstellvorschriften (Z 1 analog NSchG,
  Z 2/4/5 ≥ 15 Tage, Z 3/GfB nicht melden)
- Kern: Schwerarbeitsmeldung (BEST SM, SART 65/66, Länge 800,
  Tätigkeitsblöcke 26 × 10) mit Altersfilter- und Erstellvorschrift-
  Prüfungen
- Privat: Nachtschwerarbeitsmonat-Zähler (WET ATP_NACHTSCHWER,
  Art-XI-Abs-6-Fenster inkl. Randmonat-Proxy) als stored Flag am Lauf

AP19 — Status- und Verhinderungs-Sonderfälle:
- hr.version-Status (Organ ≤/≥ 25 %, lb-vst-03): Organ ≥ 25 % — kein
  SV-Verhältnis (SVDN/SVDG 0 via SV-Satz-Override, mBGM-Ausschluss,
  LSt läuft weiter); Ferialpraktikanten-Flag (dokumentativ — BMSVG § 6
  nennt keinen Ausschluss, Entgelt-/SV-Logik bleibt normal)
- Präsenz-/Ausbildungs-/Zivildienst-Fiktion (§ 7 Abs 1/2 BMSVG):
  fiktive BV-Bemessungsgrundlage 435,90 € (KBGG § 3 Abs 1 idF vor
  BGBl I 53/2016, lb-vor-04) aliquotiert auf ATP_PRD_TAGE
- Dienstverhinderungs-Sonderfälle bleiben dokumentiert offen
  (lb-dvh-01/04, KB-); Feiertags-EFZ ist durch das AP4-WET-Modell
  abgedeckt

Tests: 21 neue (L17-Positionen/Prüfkatalog, W1/B1-Schwellen, GfB-mBGM/
JAGB/Jahresabrechnung, NSchM-Fenster/Randmonat, SM-Fixsatz, Organ-Sperre,
PRD-Fiktion); Suite 252/252 grün auf frischer -i-DB (85 Kern, 18 GemBG,
213 Privat, 10 Dokumente). Dokumentierte Reste: DZR-36-Fenster,
Randmonat-Proxy und UV-Pauschale-vs-mBGM im Parallellauf (GP8),
fallweise mBGM (G3/G4), 2027er-Schwellen (C7).
2026-09-16 19:12:08 +02:00

297 lines
14 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.
"""Abfertigung Neu (BMSVG) — GP5: laufender BV-Beitrag (§ 6) mit
EIGENER BV-Grundlage, Anwartschafts-Fortschreibung (§ 15) und
Endabrechnung (§§ 14-17).
Verifizierte Grundlagen (RECHTSQUELLEN-Privat Abschn. 8 ✅; KB
lb-vor-03/lb-vor-06/lb-end-03, Stand 2026-08):
- § 6 Abs 1: DG-Beitrag 1,53 vH des monatlichen Entgelts und der
Sonderzahlungen an die Mitarbeitervorsorgekasse (Abfuhr über den
KV-Träger, Fälligkeit 15. Folgemonat, Baubereich BUAK — GP7).
- § 6 Abs 5 iVm § 49 ASVG: Bemessung ohne GFG und HBG → eigene,
ungecappte BV-Grundlage. Bewusst KEIN Rückgriff auf die SV-SZ-Basis
des Kerns: deren §-49-Abs-3-Z-7-Ausschluss für §-67-Abs-3/6-Bezüge
(Abfertigung/Abfindung) ist dort nicht umgesetzt; die BV-Grundlage
schließt diese Zeilen korrekt aus (Nutzer-Entscheidung 2026-09-11).
- Geltungsbereich § 46/§ 1 Abs 2 Z 1: private Arbeitsverhältnisse ab
1. 1. 2003 (Flag am Vertrag); geringfügig Beschäftigte und Lehrlinge
sind ERFASST — BVG läuft auch ohne SV-Hauptbeiträge.
- Beitragsfreier erster Monat (§ 6 Abs 1), taggenau: Fenster =
[Eintritt, Eintritt + 1 Monat), Beitragspflicht ab Monatsjubiläum
(Eintritt 15. 06. → frei bis 14. 07., Pflicht ab 15. 07.; KB-Beispiel
4.500/30 × 17 = 2.550). Monatsende-Klippe (Eintritt 31. 01. → kein
31. 02.): Fenster bis Monatsultimo, Pflicht ab Folgemonatserstem
(ABGB-Fristrechnung). AV ≤ 1 Monat → zur Gänze beitragsfrei (auch
Urlaubsersatzleistung). Wiederzulassung binnen 12 Monaten →
Beitragspflicht ab dem 1. TAG; Vor-AV beliebiger Dauer (auch
< 1 Monat; OGH 9 ObA 30/16a) — schlägt auch die Kurz-AV-Freiheit
des laufenden AV (Nutzer-Entscheidung 2026-09-11).
- Sonderzahlungen: keine aliquote Kürzung um den beitragsfreien
Monat; beitragsfrei nur, wenn ihr Beitragsmonat ins Fenster fällt.
- §§ 14-17: Anwartschaft = gutgebrachte Beiträge + Erträge (KEINE
Monatsentgelt-Staffel); Verfügungsausschluss bei AN-Kündigung,
verschuldeter Entlassung, unberechtigtem vorzeitigem Austritt oder
weniger als 36 Beitragsmonaten (alle Beitragszeiten zählen, daher
manuelle Vor-Arbeitgeber-Monate am Mitarbeiter); Fälligkeit am Ende
des zweitfolgenden Monats nach Geltendmachung (§ 16); fünf
Verfügungsoptionen, ohne Erklärung binnen 6 Monaten
Weiterveranlagung (§ 17).
"""
from datetime import timedelta
from dateutil.relativedelta import relativedelta
from odoo import fields, models
class HrVersion(models.Model):
_inherit = 'hr.version'
l10n_at_abfertigung_neu = fields.Boolean(
string='AT Abfertigung Neu (BMSVG)',
default=True,
groups="hr_payroll.group_hr_payroll_user", tracking=True,
help='Dienstverhältnis unterliegt dem BMSVG (§ 46: AV ab '
'1. 1. 2003; § 1 Abs 2 Z 1: Länder/Gemeinden ausgenommen). '
'Für Alt-Verhältnisse (§§ 23 f AngG) und § 47-Übertritte '
'abwählen — dann läuft keine BVG-Regel. Geringfügig '
'Beschäftigte und Lehrlinge sind erfasst.')
l10n_at_bv_kasse_id = fields.Many2one(
'l10n.at.bv.kasse', string='AT Mitarbeitervorsorgekasse',
index=True, groups="hr_payroll.group_hr_payroll_user",
tracking=True,
help='Gewählte BV-Kasse dieses Dienstverhältnisses (lb-vor-02); '
'Anwartschaftskonto und §-17-Verfügungen laufen über diese '
'Kasse.')
class HrEmployee(models.Model):
_inherit = 'hr.employee'
l10n_at_abfertigung_neu = fields.Boolean(
related="version_id.l10n_at_abfertigung_neu", inherited=True,
readonly=False, groups="hr_payroll.group_hr_payroll_user")
l10n_at_bv_kasse_id = fields.Many2one(
related="version_id.l10n_at_bv_kasse_id", inherited=True,
readonly=False, groups="hr_payroll.group_hr_payroll_user")
l10n_at_bv_ertraege = fields.Monetary(
string='AT BV-Erträge (Kassen-Auszug)',
currency_field='currency_id',
groups="hr_payroll.group_hr_payroll_user",
help='Veranlagungserträge der Mitarbeitervorsorgekasse laut '
'Kassen-Auszug (§ 15 BMSVG: Auszahlung der Anwartschaft = '
'gutgebrachte Beiträge + Erträge) — manuell pflegen.')
l10n_at_bv_monate_extern = fields.Integer(
string='AT BV-Beitragsmonate Vor-Arbeitgeber',
groups="hr_payroll.group_hr_payroll_user",
help='Beitragsmonate aus Vor-Beschäftigungen (§ 14 Abs 2 BMSVG: '
'alle Beitragszeiten über Arbeitgeber hinweg summierbar — '
'36-Monats-Kriterium) — laut Auszug der MV-Kasse pflegen.')
def _at_bv_anwartschaft(self):
"""Anwartschafts-Fortschreibung (§ 15 BMSVG): gutgebrachte
Beiträge aus validierten Läufen, je Beitragsmonat/Jahr
gruppiert (Refund-Läufe netten über negative Linien; Monate
zählen auch aus negativen Linien — der Monat ist belegt)."""
self.ensure_one()
monate = set()
total = 0.0
jahre = {}
slips = self.env['hr.payslip'].search([
('employee_id', '=', self.id),
('state', 'not in', ('draft', 'cancel')),
])
for line in slips.line_ids.filtered(
lambda l: l.code == 'BVG'):
if not line.total:
continue
monat = (line.l10n_at_beitragsmonat
or line.date_from).replace(day=1)
monate.add(monat)
total += line.total
eintrag = jahre.setdefault(
monat.year, {'monate': set(), 'summe': 0.0})
eintrag['monate'].add(monat)
eintrag['summe'] += line.total
return {
'monate': len(monate),
'beitraege': total,
'jahre': [(jahr, len(w['monate']), w['summe'])
for jahr, w in sorted(jahre.items())],
}
class HrPayslip(models.Model):
_inherit = 'hr.payslip'
# ------------------------------------------------------------------
# Beitragsfreies Erstfenster (§ 6 Abs 1 BMSVG, taggenau)
# ------------------------------------------------------------------
def _at_bv_fenster(self):
"""(pflichtig_ab, kurz_av) des Arbeitsverhältnisses:
pflichtig_ab = erstes beitragspflichtiges Datum (None =
Beitragspflicht ab dem 1. Tag bzw. ohne bewertbare
Vertragsdaten); kurz_av = True → AV ≤ 1 Monat, zur Gänze
beitragsfrei. Reihenfolge: Wiederzulassung schlägt alles,
dann Kurz-AV-Freiheit, dann taggenaues Fenster."""
self.ensure_one()
version = self.version_id
start = version.contract_date_start
if not start:
# Ohne Vertragsdaten ist die Befreiung nicht bewertbar →
# konservativ Vollpflicht.
return None, False
# Wiederzulassung binnen 12 Monaten: Beitragspflicht ab dem
# 1. Tag; Vor-Dienstverhältnis beliebiger Dauer (auch
# < 1 Monat) löst die Ausnahme aus.
prior = self.env['hr.version'].search([
('employee_id', '=', self.employee_id.id),
('id', '!=', version.id),
('contract_date_end', '!=', False),
('contract_date_end', '>=',
start - relativedelta(months=12)),
('contract_date_end', '<', start),
], limit=1)
if prior:
return None, False
# Monatsjubiläum, taggenau; Monatsende-Klippe: existiert der
# Jubiläumstag im Folgemonat nicht (Eintritt 31. 01.), endet
# das Fenster am Monatsultimo, Pflicht ab Folgemonatserstem.
jubilaeum = start + relativedelta(months=1)
if jubilaeum.day < start.day:
jubilaeum += timedelta(days=1)
end = version.contract_date_end
if end and end < jubilaeum:
# AV ≤ 1 Monat → zur Gänze beitragsfrei (lb-vor-03, auch
# eine bloße Urlaubsersatzleistung).
return None, True
return jubilaeum, False
# ------------------------------------------------------------------
# Eigene BV-Grundlage (§ 6 Abs 5 iVm § 49 ASVG — ohne GFG/HBG)
# ------------------------------------------------------------------
def _at_bv_sonst_monate(self, result_rules=None):
"""[(Beitragsmonat, sonstige BV-Basis)]: sonstige Bezüge ohne
§-67-Abs-3/6-Zeilen (Abfertigung/Abfindung sind nach
§ 49 Abs 3 Z 7 ASVG beitragsfrei, damit auch nicht BV-
pflichtig), gruppiert nach Regel-Beitragsmonat mit voller
Tagesgenauigkeit (Anspruchsprinzip, Muster
_l10n_at_sv_basis_laufen_monate) — die Fenster-Prüfung braucht
die Fälligkeit taggenau (SZ fällig 20. 07. ist bei Fenster-
Ende 15. 07. pflichtig, der Monatsanfang wäre es nicht)."""
self.ensure_one()
gruppen = {}
for rule in self.struct_id.rule_ids:
if (rule.l10n_at_bemessung != 'sonstig'
or rule.l10n_at_s67_abs != 'keine'):
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
gruppen[monat] = gruppen.get(monat, 0.0) + werte['total']
return sorted(gruppen.items())
def _at_bv_basis_laufen(self, result_rules=None):
"""Laufende BV-Grundlage: §-49-bereinigtes Entgelt ohne
HBG-Cap (§ 6 Abs 5), aliquotiert um das beitragsfreie
Erstfenster — 30-Tage-Monat (KB-Beispiel 4.500/30 × 17 =
2.550); Teilmonate über die Lohnsteuertage, um Doppelabzüge
zu vermeiden (das Entgelt ist für die Teilzeit bereits
aliquotiert). AP16 (§ 6 Abs 4 BMSVG): während einer
Arbeitszeit-Reduzierung mit gesetztem Feld
l10n_at_bv_vor_entgelt (z. B. Pflegeteilzeit § 14d AVRAG)
läuft der Beitrag auf dem Vor-Reduzierungs-Entgelt."""
self.ensure_one()
pflichtig_ab, kurz_av = self._at_bv_fenster()
if kurz_av:
return 0.0
vor_entgelt = self.version_id.l10n_at_bv_vor_entgelt
basis = self._l10n_at_sv_basis_roh(result_rules)
if vor_entgelt:
basis = max(basis, float(vor_entgelt))
if pflichtig_ab is None or pflichtig_ab <= self.date_from:
# AP4: fiktive BG während des Krankengeldbezugs (§ 7 Abs 3
# BMSVG) und des Wochengeldbezugs (§ 7 Abs 4, AP7) — nur
# in beitragspflichtigen Läufen (Erstfenster-Teilmonate:
# dokumentierter Randfall). AP18/AP19: Präsenz-/Ausbildungs-
# und Zivildienst-Fiktion (§ 7 Abs 1/2, ATP_PRD_TAGE).
return basis + self._at_bv_fiktiv_krankengeld() \
+ self._at_bv_fiktiv_wochengeld() \
+ self._at_bv_fiktiv_prasenzenzdienst()
if pflichtig_ab > self.date_to:
# LZP liegt zur Gänze im beitragsfreien Erstfenster.
return 0.0
tage = (min(self.date_to,
self.version_id.contract_date_end or self.date_to)
- pflichtig_ab).days + 1
if tage <= 0:
return 0.0
return basis * tage / max(self._l10n_at_lohnsteuertage(), 1)
def _at_bv_fiktiv_prasenzenzdienst(self):
"""Fiktive BV-Grundlage während des Präsenz-/Ausbildungs-
und Zivildienstes (§ 7 Abs 1/2 BMSVG; RIS-Verifikation lokal
BMSVG-Volltext 2025-07, lb-vor-04): 1,53 % der fiktiven BG in
Höhe des KBGG § 3 Abs 1 idF vor BGBl I 53/2016 — 2026: 435,90 €
(Parameter at_bv_fiktiv_kbg, Stand lb-vor-04) — aliquot auf
die Dienst-Tage je Kalendertage des LZP. ⚠ >12 Monate
Zeitsoldat/Auslandseinsatz-/Ausbildungsdienst: AG-Beitrag
entfällt (Bund zahlt) — mandantenseitig berücksichtigen,
Eingabe nicht setzen."""
self.ensure_one()
tage = 0
for eingabe in self.input_line_ids:
if eingabe.input_type_id.code == 'ATP_PRD_TAGE' \
and eingabe.amount:
tage += int(eingabe.amount)
if not tage:
return 0.0
kbg = float(self.env['hr.rule.parameter']
._get_parameter_from_code(
'at_bv_fiktiv_kbg', self.date_from,
raise_if_not_found=False) or 0.0)
if not kbg:
return 0.0
monatstage = (self.date_to - self.date_from).days + 1
return kbg * min(tage, monatstage) / float(monatstage)
def _at_bv_basis_sonst(self, result_rules=None):
"""BV-Grundlage sonstiger Bezüge (Sonderzahlungen & Co.):
ungecappt (kein HBG-/Jahresdeckel, § 6 Abs 5) und ohne
§-67-Abs-3/6-Zeilen; keine aliquote Kürzung um das freie
Fenster — frei bleibt nur, was im Fenster fällig wird
(Beitragsmonat im Fenster, lb-vor-03)."""
self.ensure_one()
pflichtig_ab, kurz_av = self._at_bv_fenster()
if kurz_av:
return 0.0
total = 0.0
for monat, basis in self._at_bv_sonst_monate(result_rules):
if pflichtig_ab is not None and monat < pflichtig_ab:
continue
total += basis
return total
def _at_bv_beitrag(self, result_rules=None):
"""BV-Beitrag (§ 6 Abs 1 BMSVG): 1,53 % DG (at_sv_bv_dg) auf
die eigene Grundlage laufend + sonstig; ATP_DG-Zeile ohne
Nettoeffekt (FI-Zuordnung über hr_payroll_account, GP6).
Läuft bewusst auch für geringfügig Beschäftigte und
Lehrlinge (§ 46, § 1 Abs 2 Z 1; Jahreszahlung + 2,5 %
Zinsenzuschlag der Geringfügigen = Zahlungsmodus GP7/GP8)."""
self.ensure_one()
if not self.version_id.l10n_at_abfertigung_neu:
return 0.0
basis = (self._at_bv_basis_laufen(result_rules)
+ self._at_bv_basis_sonst(result_rules))
satz = float(self._l10n_at_param('at_sv_bv_dg'))
return basis * satz / 100.0