mirror of
http://100.103.83.12:3003/fegger/odoo-at-payroll.git
synced 2026-09-17 16:56:42 +00:00
ec33dfacf2
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).
297 lines
14 KiB
Python
297 lines
14 KiB
Python
# 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 |