mirror of
http://100.103.83.12:3003/fegger/odoo-at-payroll.git
synced 2026-09-17 08:52:45 +00:00
[IMP] l10n_at_hr_payroll_private: GP5 Abfertigung Neu (BVG-Regel, eigene BV-Grundlage, Anwartschaft/Report/Wizard)
Laufender Beitrag (§ 6 BMSVG): Regel BVG (Kategorie ATP_DG, ohne Nettoeffekt) auf EIGENER, ungecappter BV-Grundlage — §-49-bereinigt, ohne HBG-/Jahresdeckel (§ 6 Abs 5 iVm § 49 ASVG) und ohne §-67-Abs-3/6-Zeilen (§ 49 Abs 3 Z 7 ASVG); bewusst kein Rückgriff auf die SV-SZ-Basis des Kerns (Nutzer-Entscheidung eigene BV-Grundlage; deren Z-7-Ausschluss fehlt — GP4-Beobachtung bleibt offen dokumentiert). Beitragsfreier erster Monat taggenau: Fenster [Eintritt, Eintritt + 1 Monat), Beitragspflicht ab Monatsjubiläum (15. 06. → 15. 07.; KB-Beispiel lb-vor-03: 4.500/30 × 17 = 2.550 — die ab-Folgetag-Lesart war ein Off-by-one und wurde verworfen); 30-Tage-Aliquotierung über die Lohnsteuertage (Teilmonate ohne Doppelabzug); Monatsende-Klippe (Eintritt 31. 01.): Fenster bis Monatsultimo, Pflicht ab Folgemonatserstem (ABGB-Fristrechnung); AV ≤ 1 Monat zur Gänze beitragsfrei. Wiederzulassung binnen 12 Monaten → Beitragspflicht ab dem 1. Tag; Vor-AV beliebiger Dauer (auch < 1 Monat, Nutzer-Klarstellung) und schlägt auch die Kurz-AV-Freiheit des laufenden AV (OGH 9 ObA 30/16a; Suche über frühere hr.version mit contract_date_end im 12-Monats-Fenster). Sonderzahlungen werden nicht um das Fenster gekürzt, bleiben frei nur bei Beitragsmonat (voller Datumstag, taggenau) im Fenster. Geringfügig Beschäftigte und Lehrlinge sind erfasst — BVG läuft auch im §-53a-Fall. Anwartschaft (§ 15: gutgebrachte Beiträge + Erträge, keine Monatsentgelt-Staffel) on-the-fly aus validierten Läufen; Anwartschaftsreport (QWeb auf hr.employee) und Endabrechnungs-Wizard (§§ 14-17: §-14-Abs-2-Verfügungsmatrix, < 36 Beitragsmonate sperrt immer, §-16-Fälligkeit am Ende des zweitfolgenden Monats, 6-Monats-Erklärungsfrist, 5 Verfügungsoptionen) — die Auszahlung erfolgt durch die MV-Kasse, nicht über den Payslip. Stammdaten: Vertrags-Flag l10n_at_abfertigung_neu (§ 46) + Modell l10n.at.bv.kasse (Leitzahl für eSV/mBGM; Seed der 7 Kassen-Namen noupdate, lb-gsv-11 — Leitzahlen aus Mandats-Unterlagen) + Mitarbeiter-Felder l10n_at_bv_ertraege/l10n_at_bv_monate_extern (Kassen-Auszug). Bewusst nicht: §-7-BMSVG-Fiktivfälle (dokumentierte Option), Geringfügigen-Jahreszahlung + 2,5 % (GP7), BV-Provider (Option). Tests: test_bv_private (10 Fälle, RF-BV1-10); gesamt 84/84 grün (frischer -i-Lauf auf Wegwerf-DB). Rechenfälle docs/rechenfaelle/abfertigung.md. 19.0.5.0.0.
This commit is contained in:
@@ -1,4 +1,7 @@
|
||||
# Part of the odoo-at-payroll project. License: LGPL-3.
|
||||
from . import kollektivvertrag
|
||||
from . import hr_version_private
|
||||
from . import payslip_private
|
||||
from . import payslip_private
|
||||
from . import bv_kasse
|
||||
from . import abfertigung_private
|
||||
from . import bv_report
|
||||
@@ -0,0 +1,257 @@
|
||||
# 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)."""
|
||||
self.ensure_one()
|
||||
pflichtig_ab, kurz_av = self._at_bv_fenster()
|
||||
if kurz_av:
|
||||
return 0.0
|
||||
basis = self._l10n_at_sv_basis_roh(result_rules)
|
||||
if pflichtig_ab is None or pflichtig_ab <= self.date_from:
|
||||
return basis
|
||||
if pflichtig_ab > self.date_to:
|
||||
# LZP liegt zur Gänze im beitragsfreien Erstfenster.
|
||||
return 0.0
|
||||
ende = min(self.date_to,
|
||||
self.version_id.contract_date_end or self.date_to)
|
||||
tage = (ende - pflichtig_ab).days + 1
|
||||
if tage <= 0:
|
||||
return 0.0
|
||||
return basis * tage / max(self._l10n_at_lohnsteuertage(), 1)
|
||||
|
||||
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
|
||||
@@ -0,0 +1,20 @@
|
||||
# Part of the odoo-at-payroll project. License: LGPL-3.
|
||||
"""Betriebliche Vorsorgekasse (BMSVG): Auswahl je Arbeitsverhältnis
|
||||
(lb-vor-02 — Voraussetzung der Beitragsabfuhr; die Kasse führt das
|
||||
Anwartschaftskonto und nimmt die §-17-Verfügungen entgegen). Die
|
||||
Leitzahl ist eSV-/mBGM-Angabe (GP7) und ist aus den Kassen-Unterlagen
|
||||
des Mandanten zu übernehmen — nicht aus Vermutung."""
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class L10nAtBvKasse(models.Model):
|
||||
_name = 'l10n.at.bv.kasse'
|
||||
_description = 'Betriebliche Vorsorgekasse (BMSVG)'
|
||||
_order = 'name'
|
||||
|
||||
name = fields.Char(required=True)
|
||||
leitzahl = fields.Char(
|
||||
string='Leitzahl (eSV/mBGM)',
|
||||
help='Leitzahl der Mitarbeitervorsorgekasse gemäß DM-ORG/'
|
||||
'mBGM-Angaben (GP7); aus den Kassen-Unterlagen übernehmen.')
|
||||
active = fields.Boolean(default=True)
|
||||
@@ -0,0 +1,20 @@
|
||||
# Part of the odoo-at-payroll project. License: LGPL-3.
|
||||
"""QWeb-Datenlieferant für den BV-Anwartschaftsreport (GP5): die
|
||||
Anwartschaftswerte werden im Python-Teil aufbereitet (Muster
|
||||
AbstractModel-Report), der QWeb-Teil bleibt frei von Methodenaufrufen."""
|
||||
from odoo import models
|
||||
|
||||
|
||||
class L10nAtBvAnwartschaftReport(models.AbstractModel):
|
||||
_name = 'report.l10n_at_hr_payroll_private.report_bv_anwartschaft'
|
||||
_description = 'BV-Anwartschaftsreport (Abfertigung Neu, § 15 BMSVG)'
|
||||
|
||||
def _get_report_values(self, docids, data=None):
|
||||
docs = self.env['hr.employee'].browse(docids)
|
||||
return {
|
||||
'docs': docs,
|
||||
'anwartschaft': {
|
||||
employee.id: employee._at_bv_anwartschaft()
|
||||
for employee in docs
|
||||
},
|
||||
}
|
||||
Reference in New Issue
Block a user