diff --git a/addons/l10n_at_hr_payroll_private/README.md b/addons/l10n_at_hr_payroll_private/README.md index eda7eeb..b6c3617 100644 --- a/addons/l10n_at_hr_payroll_private/README.md +++ b/addons/l10n_at_hr_payroll_private/README.md @@ -67,13 +67,44 @@ Abs 6/7 ASVG, § 12 IESG), WF-Satzvektor Wien, Beitragsmonats- Allokation (Anspruchsprinzip) und DAG-Bewertung (§ 1 DAG). Rechenfälle `docs/rechenfaelle/sozialversicherung.md`. -**Offen (GP5+):** Abfertigung Neu (BMSVG), Meldewesen/Belege, -FI-Buchung UGB; All-in-UI-Bericht (GP8); B14 längster-Urlaubs-Teil- -Verfeinerung; DZ-WKO-Verifikation (8.11) vor Wien-Mandanten. +**GP5 Abfertigung Neu (umgesetzt 2026-09-11):** BMSVG-Regel `BVG` +(§ 6 Abs 1: 1,53 % DG auf Entgelt **und** SZ; Kern-Parameter +`at_sv_bv_dg`) auf **EIGENER, ungecappter BV-Grundlage** (§ 6 Abs 5 +iVm § 49 ASVG — kein HBG-/Jahresdeckel, ohne §-67-Abs-3/6-Zeilen gemäß +§ 49 Abs 3 Z 7 ASVG) — `models/abfertigung_private.py`: + +- **Beitragsfreier erster Monat taggenau** (§ 6 Abs 1): Fenster + [Eintritt, Eintritt + 1 Monat), Pflicht ab Monatsjubiläum + (15. 06. → 15. 07.); 30-Tage-Aliquotierung (KB 4.500/30 × 17 = + 2.550); Monatsende-Klippe (31. 01.) → frei bis Monatsultimo, + Pflicht ab Folgemonatserstem; AV ≤ 1 Monat zur Gänze beitragsfrei; + **Wiederzulassung binnen 12 Monaten → Pflicht ab dem 1. Tag** + (OGH 9 ObA 30/16a; Vor-AV beliebiger Dauer — auch < 1 Monat); +- **SZ ohne Fenster-Kürzung**; frei nur bei Beitragsmonat im Fenster + (taggenau); läuft **auch für geringfügig Beschäftigte und + Lehrlinge** (§ 46, § 1 Abs 2 Z 1); +- **Anwartschaft** (§ 15: gutgebrachte Beiträge + Erträge aus dem + Kassen-Auszug, KEINE Monatsentgelt-Staffel) aus validierten + Läufen; Vertragsfelder Abfertigung-Neu/BV-Kasse + (`l10n.at.bv.kasse` mit Leitzahl für eSV/mBGM) + Erträge/Vor-Beitrags- + monate am Mitarbeiter; **Anwartschaftsreport** (QWeb, am Mitarbeiter + druckbar) + **Endabrechnungs-Wizard** (§§ 14-17: §-14-Abs-2- + Verfügungsmatrix mit 36-Beitragsmonate-Kriterium, §-16-Fälligkeit, + 6-Monats-Erklärungsfrist, 5 §-17-Optionen) — Auszahlung selbst + läuft über die MV-Kasse, nicht über den Payslip. + +Rechenfälle `docs/rechenfaelle/abfertigung.md`. + +**Offen (GP6+):** Meldewesen/Belege (GP7), FI-Buchung UGB (GP6), +Jahreswechsel (GP8); All-in-UI-Bericht (GP8); B14 längster-Urlaubs-Teil- +Verfeinerung; DZ-WKO-Verifikation (8.11) vor Wien-Mandanten; +§-7-BMSVG-Fiktivfälle (Präsenzdienst/Kranken-/Wochengeld) als +dokumentierte Option (Rechenfälle-Datei). Tests: `test_kv_katalog` + `test_entgelt_private` (GP2) + -`test_lst_private` (GP3) + `test_sv_private` (GP4) — +`test_lst_private` (GP3) + `test_sv_private` (GP4) + +`test_bv_private` (GP5) — `.venv/bin/python -m odoo -d -u l10n_at_hr_payroll,l10n_at_hr_payroll_private --test-tags /l10n_at_hr_payroll,/l10n_at_hr_payroll_private --test-enable --stop-after-init --addons-path=odoo_19.0+e.20260910/odoo-19.0+e.20260910/odoo/addons,addons` -(Lokal grün am 2026-09-11: **74/74 Tests**.) \ No newline at end of file +(Lokal grün am 2026-09-11: **84/84 Tests**.) \ No newline at end of file diff --git a/addons/l10n_at_hr_payroll_private/__manifest__.py b/addons/l10n_at_hr_payroll_private/__manifest__.py index 4465b74..6ff1bed 100644 --- a/addons/l10n_at_hr_payroll_private/__manifest__.py +++ b/addons/l10n_at_hr_payroll_private/__manifest__.py @@ -1,7 +1,7 @@ # Part of the odoo-at-payroll project. License: LGPL-3. { 'name': 'Österreich - Privatwirtschaft Personalverrechnung (General-AT)', - 'version': '19.0.4.0.0', + 'version': '19.0.5.0.0', 'author': 'ixsol / gem360', 'website': 'https://gem360.at', 'category': 'Human Resources/Payroll', @@ -34,18 +34,30 @@ Grundlagen: ``personalverrechnung/IMPLEMENTIERUNGSPLAN-Privat.md`` und - **GP3 Lohnsteuer (umgesetzt):** dünne Regeln SVDN_KV/PV/AV/AK/WF + LST_LFD/LST_SZ auf dem AP3-Kern (§ 66 kumulativ, § 67 Sechstel/ Fixsätze, § 68-Steuerfreiheiten). -- **GP4 SV/DB (dieser Stand):** SVDN_SZ-Nachtrag (§ 67 Abs 12 — Netto- +- **GP4 SV/DB (umgesetzt):** SVDN_SZ-Nachtrag (§ 67 Abs 12 — Netto- Korrektur in §-67-Monaten), DG-Block SVDG_KV/PV/AV/UV/WF/IESG + SVDG_SZ_* + §-53a-UV Geringfügiger (ATP_DG, ohne Nettoeffekt; ÖGK 2026: DG 20,98 % / Wien 21,23 %), Lohnnebenkosten FLAFDB (§ 41 FLAG), KOMMST (§ 9 KommStG) und DZ (§§ 122/126 WKG, company-flag-gated) mit Freibetragsstaffel 1.095/1.460 € — Logik/Werte im Bundes-Kern (D3-Vorfinanzierung). +- **GP5 Abfertigung Neu (dieser Stand):** BMSVG-Regel ``BVG`` + (§ 6: 1,53 % DG auf Entgelt und SZ, Kern-Parameter + ``at_sv_bv_dg``) auf EIGENER, ungecappter BV-Grundlage (§ 6 Abs 5 + iVm § 49 ASVG — kein HBG-Cap, ohne §-67-Abs-3/6-Zeilen), + beitragsfreier erster Monat taggenau (30-Tage-Aliquotierung; + Monatsende-Klippe), Wiederzulassung binnen 12 Monaten ab Tag 1 + (Vor-AV beliebiger Dauer), läuft auch für Geringfügige und + Lehrlinge; Vertragsfeld Abfertigung-Neu/BV-Kasse + Erträge/Vor- + Monate am Mitarbeiter; Anwartschaftsreport (QWeb) + Endabrechnungs- + Wizard (§§ 14-17: Verfügungsausschluss-Matrix, 36-Beitragsmonate, + §-16-Fälligkeit, §-17-Optionen). - **Seed D2 (Referenz-KVs R1):** SI-2203 „Handwerk und Gewerbe" + SI-2748 „Metallgewerbe" (Gehaltstabellen ab 1. 1. 2026, kuratiert aus den KV-Library-Volltexten). -- **Folgen (GP5+):** Abfertigung Neu (BMSVG), Meldewesen/Belege, - FI-Buchung UGB, Jahreswechsel. +- **Folgen (GP6+):** FI-Buchung UGB (GP6), Meldewesen/Belege (GP7), + Jahreswechsel (GP8); §-7-BMSVG-Fiktivfälle (Präsenzdienst/ + Kranken-/Wochengeld) als dokumentierte Option. Kein VRV-Bezug (Gemeinde-Schiene bleibt ``l10n_at_gemeinde_payroll`` + gem360-Brücke). @@ -63,7 +75,11 @@ gem360-Brücke). 'data/salary_rules_private.xml', 'data/salary_rules_lst_private.xml', 'data/salary_rules_sv_private.xml', + 'data/salary_rules_bvg_private.xml', + 'data/bv_kasse_seed.xml', 'views/kv_views.xml', + 'views/bv_views.xml', + 'report/bv_report.xml', ], 'countries': ['at'], 'license': 'LGPL-3', diff --git a/addons/l10n_at_hr_payroll_private/data/bv_kasse_seed.xml b/addons/l10n_at_hr_payroll_private/data/bv_kasse_seed.xml new file mode 100644 index 0000000..af80316 --- /dev/null +++ b/addons/l10n_at_hr_payroll_private/data/bv_kasse_seed.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + Allianz + + + APK + + + BONUS + + + BUAK + + + + + + Valida Plus + + + VBV + + + \ No newline at end of file diff --git a/addons/l10n_at_hr_payroll_private/data/salary_rules_bvg_private.xml b/addons/l10n_at_hr_payroll_private/data/salary_rules_bvg_private.xml new file mode 100644 index 0000000..78bb05f --- /dev/null +++ b/addons/l10n_at_hr_payroll_private/data/salary_rules_bvg_private.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + BV-Beitrag Abfertigung Neu (§ 6 BMSVG; DG) + BVG + + + + python + result = payslip._at_bv_beitrag(result_rules) > 0 + code + neutral + + + \ No newline at end of file diff --git a/addons/l10n_at_hr_payroll_private/docs/rechenfaelle/abfertigung.md b/addons/l10n_at_hr_payroll_private/docs/rechenfaelle/abfertigung.md new file mode 100644 index 0000000..42105e6 --- /dev/null +++ b/addons/l10n_at_hr_payroll_private/docs/rechenfaelle/abfertigung.md @@ -0,0 +1,105 @@ +# Rechenfälle Abfertigung Neu (GP5) — BMSVG + +**Quellen (✅ verifiziert):** `RECHTSQUELLEN-Privat.md` Abschnitt 8 +(konsolidierter BMSVG-Volltext, Stand 26. 07. 2025); Layer-2-Belege +lb-vor-03 (Beitragszahlung, Stand 2026-08), lb-vor-06 (Geltungs- +bereich), lb-end-03 (Verfügungsmöglichkeiten, Stand 2026-08), +lb-vor-10 (Übertritt). Umsetzung: Kern-Parameter `at_sv_bv_dg` +(`rule_parameters_sv_2026.xml`); privat `models/abfertigung_private.py` ++ Regel `BVG` (`salary_rules_bvg_private.xml`) + Endabrechnungs-Wizard +`l10n.at.bv.endabrechnung.wizard`. Tests: `test_bv_private.py`. + +## Systematik (GP5, verifiziert) + +- **Beitrag (§ 6 Abs 1):** DG zahlt **1,53 vH** des monatlichen Entgelts + **und** der Sonderzahlungen an die Mitarbeitervorsorgekasse (Abfuhr + über den KV-Träger, Fälligkeit 15. Folgemonat, Baubereich BUAK — + Zahlungs-/Meldeseite GP7). +- **Eigene BV-Grundlage (§ 6 Abs 5 iVm § 49 ASVG):** GFG und HBG + **unberücksichtigt** → kein HBG-Cap, kein SZ-Jahresdeckel; §-49- + bereinigt (laufend: `_l10n_at_sv_basis_roh`; sonstig: alle + §-67-Zeilen). **§-67-Abs-3/6-Zeilen (Abfertigung Alt/Abfindung) + scheiden aus** (§ 49 Abs 3 Z 7 ASVG, beitragsfrei) — bewusst eigene + Grundlage statt Rückgriff auf die SV-SZ-Basis des Kerns, deren + Z-7-Ausschluss nicht umgesetzt ist (GP4-Beobachtung, offen + dokumentiert). +- **Geltungsbereich (§ 46, § 1 Abs 2 Z 1):** private AV ab 1. 1. 2003 + (Vertrags-Flag `l10n_at_abfertigung_neu`, default True; Alt-Fälle/ + § 47-Übertritte abwählen); Länder/Gemeinden ausgenommen (Sektor- + Trennung zum GemBG-Modul § 130). **Geringfügig Beschäftigte und + Lehrlinge sind erfasst** — BVG läuft auch ohne SV-Hauptbeiträge + (§-53a-Fall); Geringfügigen-Jahreszahlung + 2,5 % Zinsenzuschlag = + Zahlungsmodus (GP7/GP8). +- **Beitragsfreier erster Monat (§ 6 Abs 1), taggenau:** + - Fenster = [Eintritt, Eintritt + 1 Monat); **Beitragspflicht ab + Monatsjubiläum** (Eintritt 15. 06. → frei 15. 06.–14. 07., Pflicht + ab 15. 07.). + - **Aliquotierung mit fixem 30-Tage-Monat** (Dachverband; KB-Beispiel + 4.500/30 × 17 = 2.550): laufende BG × pflichtige Kalendertage ÷ + Lohnsteuertage (Vollmonat = 30; Teilmonat = Kalendertage, damit das + für Teilmonate bereits aliquote Entgelt nicht doppelt gekürzt + wird). + - **Monatsende-Klippe** (Eintritt 31. 01., Jubiläum 31. 02. existiert + nicht): Fenster bis **Monatsultimo**, Pflicht ab **Folgemonats- + erstem** (ABGB-Fristrechnung). + - **AV ≤ 1 Monat → zur Gänze beitragsfrei** (auch bloße Urlaubs- + ersatzleistung; Ende < Jubiläum). + - **Wiederzulassung binnen 12 Monaten → Beitragspflicht ab dem + 1. Tag** (OGH 9 ObA 30/16a); Vor-AV **beliebiger Dauer** (auch + < 1 Monat — Nutzer-Entscheidung 2026-09-11) und schlägt auch die + Kurz-AV-Freiheit des laufenden AV. Suche über frühere + `hr.version` desselben Mitarbeiters mit `contract_date_end` im + 12-Monats-Fenster vor dem neuen Start. +- **Sonderzahlungen:** **keine** aliquote Kürzung um den beitragsfreien + Monat; beitragsfrei bleiben sie nur, wenn ihr **Beitragsmonat** + (Regelfeld `l10n_at_beitragsmonat`, sonst LZP-Beginn) **im Fenster** + liegt — taggenau verglichen (SZ fällig 20. 07. ist bei Fensterende + 15. 07. pflichtig). +- **EOSB (§§ 14-17):** Anwartschaft = **gutgebrachte Beiträge + + Erträge** (§ 15 — KEINE Monatsentgelt-Staffel); Erträge aus dem + Kassen-Auszug am Mitarbeiter pflegen (`l10n_at_bv_ertraege`). + **Verfügungsausschluss (§ 14 Abs 2):** AN-Kündigung (außer + MSchG/VKG-Teilzeit-Kündigung), verschuldete Entlassung, + unberechtigter vorzeitiger Austritt — und **IMMER bei < 36 + Beitragsmonaten** (alle Beitragszeiten über Arbeitgeber hinweg + zählen; Vor-Monate am Mitarbeiter: `l10n_at_bv_monate_extern`). + Fälligkeit am **Ende des zweitfolgenden Monats** nach + Geltendmachung (§ 16); **fünf** Verfügungsoptionen, ohne Erklärung + binnen **6 Monaten** Weiterveranlagung (§ 17). Auszahlung durch die + MV-Kasse — kein Payslip-Bestandteil (Wizard rein informativ). + +## Rechenfälle (Fixtures test_bv_private.py) + +| RF | Konstellation | Berechnung | Erwartung | +|---|---|---|---| +| RF-BV1 | Bestand (2020), November mit B13 (KV-Katalog 2.059,41) | (2.059,41 + 2.059,41) × 1,53 % | **BVG 63,02 €**; ATP_DG, NETTO unverändert | +| RF-BV2 | Entgelt 8.000 € (Jänner, kein B13) | SVDG_KV: HBG 6.930 × 3,78 %; BVG: 8.000 × 1,53 % | SVDG_KV 262,55; **BVG 122,40 €** (kein HBG-Cap) | +| RF-BV3 | Eintritt 15. 06. 2026, Entgelt 6.000 € | Juni: Fenster; Juli: 6.000/30 × 17; August: voll | Juni **0** · Juli 3.400 × 1,53 % = **52,02 €** · August **91,80 €** | +| RF-BV4 | Vor-AV 01.–15. 06. 2026 (14 Tage), neuer AV ab 01. 07. | Wiederzulassung → Pflicht ab Tag 1 | Juli **91,80 €** (kein freier Monat trotz kurzem Vor-AV) | +| RF-BV5 | AV 01.–30. 06. 2026 (genau 1 Monat) | Ende < Jubiläum 01. 07. | Juni **0** (zur Gänze beitragsfrei) | +| RF-BV6 | Geringfügig 500 € (Jänner) | §-53a-Fall: SVDG 0, aber BMSVG erfasst | **BVG 7,65 €** | +| RF-BV7 | Eintritt 15. 06.; Teststruktur LFD 2.000 / SZ 20. 06. / SZ 20. 07. / Abfertigung-Alt 10.000 (§ 67 Abs 3) | (2.000 × 17/30 + 1.000) × 1,53 %; SZ 20. 06. frei; Alt-Abfertigung ausgeschieden | **BVG 32,64 €** (1.133,33 + 1.000) | +| RF-BV8 | 3 validierte Läufe (Jän-März, je 2.059,41); Austritt 31. 08., Geltendmachung 11. 09. | Anwartschaft 3 × 31,51 = 94,53; §-14-Prüfung | Monate 3 → gesperrt; +33 extern → 36, AG-Kündigung verfügbar; AN-Kündigung gesperrt; Fälligkeit **30. 11.**, Erklärungsfrist **28. 02. 2027** | +| RF-BV9 | Alt-Verhältnis (Flag False), November | Regel aus | **BVG 0** | +| RF-BV10 | Eintritt 31. 01. 2026 | Klippe: Fenster bis 28. 02., Pflicht ab 01. 03. | Februar **0** · März **91,80 €** | + +## Offene Punkte / bewusst nicht in GP5 + +- **§ 7 BMSVG-Fiktivfälle** (Präsenz-/Ausbildungs-/Zivildienst: + fiktive BG = Kinderbetreuungsgeld 435,90 €/Monat Stand 2026-08; + Krankengeld: 50 % der BG vor dem Versicherungsfall ohne SZ; + Wochengeld: 3-Monats-Ø inkl. anteiliger SZ): dokumentierte Option — + setzt die Modellierung der Entgeltersatzleistungen voraus, die das + Modul (noch) nicht hat; kein toter Parameter. +- **SV-SZ-Basis des Kerns** (`_l10n_at_svdn_basis_sz`) schließt + §-67-Abs-3/6-Zeilen nicht aus (beitragsfrei nach § 49 Abs 3 Z 7 + ASVG) — GP4-Beobachtung, bewusst nicht in GP5 angefasst + (Nutzer-Entscheidung: eigene BV-Grundlage); möglicher Folgefix im + SV-Kern separat zu klären. +- **Abfuhr/Meldung** (mBGM: Leitzahl BV-Kasse, DG-Kontonummer; + Geringfügigen-Jahreszahlung + 2,5 %), **Fristenkalender** + (6-Monats-Erklärung, §-16-Fälligkeit als Aktivität), **BV-Provider- + Anbindung**: GP7/GP8 bzw. Option. +- Wiederzulassung wird je Mitarbeiter in der DB geprüft („gleicher + Arbeitgeber" = gleiches Unternehmen angenommen); Multi-Company- + Konstellationen sind dokumentierte Grenze. \ No newline at end of file diff --git a/addons/l10n_at_hr_payroll_private/models/__init__.py b/addons/l10n_at_hr_payroll_private/models/__init__.py index bdb4a83..6c08567 100644 --- a/addons/l10n_at_hr_payroll_private/models/__init__.py +++ b/addons/l10n_at_hr_payroll_private/models/__init__.py @@ -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 \ No newline at end of file +from . import payslip_private +from . import bv_kasse +from . import abfertigung_private +from . import bv_report \ No newline at end of file diff --git a/addons/l10n_at_hr_payroll_private/models/abfertigung_private.py b/addons/l10n_at_hr_payroll_private/models/abfertigung_private.py new file mode 100644 index 0000000..02c8968 --- /dev/null +++ b/addons/l10n_at_hr_payroll_private/models/abfertigung_private.py @@ -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 \ No newline at end of file diff --git a/addons/l10n_at_hr_payroll_private/models/bv_kasse.py b/addons/l10n_at_hr_payroll_private/models/bv_kasse.py new file mode 100644 index 0000000..1baed1a --- /dev/null +++ b/addons/l10n_at_hr_payroll_private/models/bv_kasse.py @@ -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) \ No newline at end of file diff --git a/addons/l10n_at_hr_payroll_private/models/bv_report.py b/addons/l10n_at_hr_payroll_private/models/bv_report.py new file mode 100644 index 0000000..fb033b6 --- /dev/null +++ b/addons/l10n_at_hr_payroll_private/models/bv_report.py @@ -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 + }, + } \ No newline at end of file diff --git a/addons/l10n_at_hr_payroll_private/report/bv_report.xml b/addons/l10n_at_hr_payroll_private/report/bv_report.xml new file mode 100644 index 0000000..ccb7909 --- /dev/null +++ b/addons/l10n_at_hr_payroll_private/report/bv_report.xml @@ -0,0 +1,104 @@ + + + + + + + + + + + BV-Anwartschaftsreport (Abfertigung Neu) + hr.employee + qweb-pdf + l10n_at_hr_payroll_private.report_bv_anwartschaft + l10n_at_hr_payroll_private.report_bv_anwartschaft + + + report + + + + \ No newline at end of file diff --git a/addons/l10n_at_hr_payroll_private/security/ir.model.access.csv b/addons/l10n_at_hr_payroll_private/security/ir.model.access.csv index a338f95..a26dac8 100644 --- a/addons/l10n_at_hr_payroll_private/security/ir.model.access.csv +++ b/addons/l10n_at_hr_payroll_private/security/ir.model.access.csv @@ -8,4 +8,7 @@ access_l10n_at_payroll_kv_gruppe_manager,L10n AT Payroll KV-Gruppe Manager,model access_l10n_at_payroll_kv_gruppe_stufe_user,L10n AT Payroll KV-Gruppe-Stufe User,model_l10n_at_payroll_kv_gruppe_stufe,hr_payroll.group_hr_payroll_user,1,0,0,0 access_l10n_at_payroll_kv_gruppe_stufe_manager,L10n AT Payroll KV-Gruppe-Stufe Manager,model_l10n_at_payroll_kv_gruppe_stufe,hr_payroll.group_hr_payroll_manager,1,1,1,1 access_l10n_at_payroll_kv_import_wizard,L10n AT Payroll KV-Import Wizard,model_l10n_at_payroll_kv_import_wizard,hr_payroll.group_hr_payroll_user,1,1,1,1 -access_l10n_at_payroll_kv_import_line,L10n AT Payroll KV-Import Line,model_l10n_at_payroll_kv_import_line,hr_payroll.group_hr_payroll_user,1,1,1,1 \ No newline at end of file +access_l10n_at_payroll_kv_import_line,L10n AT Payroll KV-Import Line,model_l10n_at_payroll_kv_import_line,hr_payroll.group_hr_payroll_user,1,1,1,1 +access_l10n_at_bv_kasse_user,L10n AT BV-Kasse User,model_l10n_at_bv_kasse,hr_payroll.group_hr_payroll_user,1,0,0,0 +access_l10n_at_bv_kasse_manager,L10n AT BV-Kasse Manager,model_l10n_at_bv_kasse,hr_payroll.group_hr_payroll_manager,1,1,1,1 +access_l10n_at_bv_endabrechnung_wizard,L10n AT BV-Endabrechnung Wizard,model_l10n_at_bv_endabrechnung_wizard,hr_payroll.group_hr_payroll_user,1,1,1,1 \ No newline at end of file diff --git a/addons/l10n_at_hr_payroll_private/tests/__init__.py b/addons/l10n_at_hr_payroll_private/tests/__init__.py index 30a83f8..f8b966e 100644 --- a/addons/l10n_at_hr_payroll_private/tests/__init__.py +++ b/addons/l10n_at_hr_payroll_private/tests/__init__.py @@ -2,4 +2,5 @@ from . import test_kv_katalog from . import test_entgelt_private from . import test_lst_private -from . import test_sv_private \ No newline at end of file +from . import test_sv_private +from . import test_bv_private \ No newline at end of file diff --git a/addons/l10n_at_hr_payroll_private/tests/test_bv_private.py b/addons/l10n_at_hr_payroll_private/tests/test_bv_private.py new file mode 100644 index 0000000..6983b17 --- /dev/null +++ b/addons/l10n_at_hr_payroll_private/tests/test_bv_private.py @@ -0,0 +1,297 @@ +# Part of the odoo-at-payroll project. License: LGPL-3. +"""GP5 Abfertigung Neu (BMSVG) auf der privatwirtschaftlichen +Structure: BVG-Regel (§ 6: 1,53 % DG auf Entgelt und SZ, EIGENE +ungecappte Grundlage, beitragsfreier erster Monat taggenau mit +Monatsende-Klippe, Wiederzulassung binnen 12 Monaten ab Tag 1 — auch +bei kurzem Vor-AV —, SZ-Fälligkeit taggenau gegen das Fenster, +§-67-Abs-3/6-Ausschluss, kein HBG-Cap, läuft auch bei Geringfügigkeit) +sowie Anwartschafts-Fortschreibung (§ 15) und Endabrechnungs-Wizard +(§§ 14-17: Verfügungsmatrix, 36-Beitragsmonate, §-16/§-17-Fristen). + +Rechenfälle: docs/rechenfaelle/abfertigung.md.""" +from datetime import date, timedelta + +from odoo.tests import TransactionCase, tagged + + +@tagged('post_install', '-at_install') +class BvPrivateTest(TransactionCase): + + def setUp(self): + super().setUp() + self.structure = self.env.ref( + 'l10n_at_hr_payroll_private.structure_at_privat') + self.kv2203 = self.env.ref( + 'l10n_at_hr_payroll_private.kv_si_2203') + self.gruppe_ii = self.env.ref( + 'l10n_at_hr_payroll_private.kv2203_grp_ii') + self.calendar = self.env['resource.calendar'].create({ + 'name': 'AT Test 40h Mo-Fr', + 'tz': 'Europe/Vienna', + 'hours_per_day': 8, + 'attendance_ids': [ + (0, 0, {'name': 'Tag %d' % wd, 'dayofweek': str(wd), + 'hour_from': 8.0, 'hour_to': 16.0}) + for wd in range(5) + ], + }) + self.env.company.resource_calendar_id = self.calendar + + def _employee(self, version_vals=None): + employee = self.env['hr.employee'].create({ + 'name': 'Test BV privat', + }) + version = self.env['hr.version'].search( + [('employee_id', '=', employee.id)], limit=1) + if not version: + version = self.env['hr.version'].create({ + 'employee_id': employee.id, + 'date_start': date(2020, 1, 1)}) + version.write({ + 'contract_date_start': date(2020, 1, 1), + 'kv_id': self.kv2203.id, + 'kv_gruppe_id': self.gruppe_ii.id, + 'erfahrungsstufe': 1, + 'resource_calendar_id': self.calendar.id, + }) + if version_vals: + version.write(version_vals) + return employee + + def _slip(self, employee, monat, jahr=2026, confirm=False, + struct=None): + erster = date(jahr, monat, 1) + naechster = (erster + timedelta(days=32)).replace(day=1) + slip = self.env['hr.payslip'].create({ + 'name': 'Test-BV-Privat %d/%d' % (monat, jahr), + 'employee_id': employee.id, + 'struct_id': (struct or self.structure).id, + 'date_from': erster, + 'date_to': naechster - timedelta(days=1), + }) + slip.compute_sheet() + if confirm: + slip.action_payslip_done() + return slip + + def _total(self, slip, code): + return sum(slip.line_ids.filtered( + lambda l: l.code == code).mapped('total')) + + # Teststruktur mit Fixbeträgen + eigener BVG-Regel (Muster + # _lnk_struktur), um Fenster-/SZ-/§-67-Eckfälle zielgenau zu + # treffen, ohne vom KV-Katalog-Entgelt abzuhängen. + def _bv_struktur(self, code, regeln): + struktur = self.env['hr.payroll.structure'].create({ + 'name': 'Test AT BV %s' % code, + 'code': 'TEST_AT_BV_%s' % code, + 'type_id': self.structure.type_id.id, + 'country_id': self.env.ref('base.at').id, + }) + brutto = self.env.ref( + 'l10n_at_hr_payroll_private.category_atp_brutto') + dg = self.env.ref( + 'l10n_at_hr_payroll_private.category_atp_dg') + for regel_code, betrag, bemessung, s67, beitragsmonat in regeln: + vals = { + 'name': regel_code, + 'code': regel_code, + 'struct_id': struktur.id, + 'category_id': brutto.id, + 'condition_select': 'none', + 'l10n_at_bemessung': bemessung, + 'l10n_at_s67_abs': s67, + 'amount_select': 'fix', + 'amount_fix': betrag, + } + if beitragsmonat: + vals['l10n_at_beitragsmonat'] = beitragsmonat + self.env['hr.salary.rule'].create(vals) + self.env['hr.salary.rule'].create({ + 'name': 'BV-Beitrag Abfertigung Neu (Test)', + 'code': 'BVG', + 'struct_id': struktur.id, + 'category_id': dg.id, + 'condition_select': 'python', + 'condition_python': + 'result = payslip._at_bv_beitrag(result_rules) > 0', + 'amount_select': 'code', + 'l10n_at_bemessung': 'neutral', + 'amount_python_compute': + 'result = payslip._at_bv_beitrag(result_rules)', + }) + return struktur + + # ------------------------------------------------------------------ + # RF-BV1/2: Standardlauf und kein HBG-Cap (§ 6 Abs 5) + # ------------------------------------------------------------------ + def test_01_standard_b13(self): + """November mit B13: BVG 1,53 % auf KVGEO + B13 (4.118,82 € + → 63,02 €), Kategorie ATP_DG ohne Nettoeffekt.""" + employee = self._employee() + slip = self._slip(employee, 11) + basis = (self._total(slip, 'KVGEO') + self._total(slip, 'B13')) + self.assertAlmostEqual(basis, 4118.82, places=2) + self.assertAlmostEqual(self._total(slip, 'BVG'), + 4118.82 * 0.0153, places=2) + # Kein Nettoeffekt: ATP_DG bleibt außerhalb der NETTO-Rechnung. + abzug = sum(self._total(slip, code) for code in + ('SVDN_KV', 'SVDN_PV', 'SVDN_AV', 'SVDN_AK', + 'SVDN_WF', 'SVDN_SZ', 'LST_LFD', 'LST_SZ')) + self.assertAlmostEqual(self._total(slip, 'NETTO'), + basis - abzug, places=1) + + def test_02_kein_hbg_cap(self): + """Entgelt 8.000 € über der HBG 6.930 €: SVDG_KV cappt auf + 6.930, BVG läuft auf die volle Grundlage (§ 6 Abs 5 iVm + § 49 ASVG — GFG und HBG unberücksichtigt).""" + employee = self._employee({'ueberzahlung': 8000.0}) + slip = self._slip(employee, 1) + self.assertAlmostEqual(self._total(slip, 'KVGEO'), 8000.0, + places=2) + self.assertAlmostEqual(self._total(slip, 'SVDG_KV'), + 6930.0 * 0.0378, places=2) + self.assertAlmostEqual(self._total(slip, 'BVG'), + 8000.0 * 0.0153, places=2) + + # ------------------------------------------------------------------ + # RF-BV3/4/5/10: Beitragsfreier erster Monat (§ 6 Abs 1) + # ------------------------------------------------------------------ + def test_03_erster_monat_fenster(self): + """Eintritt 15. 06.: Juni zur Gänze beitragsfrei, Juli aliquot + mit 30-Tage-Monat (KB-Beispiel: × 17/30), August voll.""" + employee = self._employee( + {'ueberzahlung': 6000.0, + 'contract_date_start': date(2026, 6, 15)}) + slip_jun = self._slip(employee, 6) + self.assertAlmostEqual(self._total(slip_jun, 'BVG'), 0.0, + places=2) + slip_jul = self._slip(employee, 7) + self.assertAlmostEqual(self._total(slip_jul, 'KVGEO'), 6000.0, + places=2) + self.assertAlmostEqual( + self._total(slip_jul, 'BVG'), + 6000.0 * 17 / 30 * 0.0153, places=2) + slip_aug = self._slip(employee, 8) + self.assertAlmostEqual(self._total(slip_aug, 'BVG'), + 6000.0 * 0.0153, places=2) + + def test_04_wiederzulassung(self): + """Vor-AV nur 14 Tage, Ende binnen 12 Monaten → Wieder- + zulassung: neuer AV beitragspflichtig ab dem 1. Tag (auch + bei kurzem Vor-AV; OGH 9 ObA 30/16a).""" + employee = self._employee( + {'ueberzahlung': 6000.0, + 'contract_date_start': date(2026, 7, 1)}) + employee.version_id.write({'date_version': date(2026, 7, 1)}) + self.env['hr.version'].create({ + 'employee_id': employee.id, + 'date_version': date(2026, 6, 1), + 'contract_date_start': date(2026, 6, 1), + 'contract_date_end': date(2026, 6, 15), + }) + slip = self._slip(employee, 7) + self.assertEqual(slip.version_id, employee.version_id) + self.assertAlmostEqual(self._total(slip, 'BVG'), + 6000.0 * 0.0153, places=2) + + def test_05_kurz_av(self): + """AV genau 1 Monat (1.-30. 06.) → zur Gänze beitragsfrei + (lb-vor-03; auch bloße Urlaubsersatzleistungen).""" + employee = self._employee( + {'ueberzahlung': 6000.0, + 'contract_date_start': date(2026, 6, 1), + 'contract_date_end': date(2026, 6, 30)}) + slip = self._slip(employee, 6) + self.assertAlmostEqual(self._total(slip, 'KVGEO'), 6000.0, + places=2) + self.assertAlmostEqual(self._total(slip, 'BVG'), 0.0, places=2) + + def test_10_monatsende_klippe(self): + """Eintritt 31. 01. — der Jubiläumstag 31. 02. existiert + nicht → Fenster bis Monatsultimo (28. 02.), Pflicht ab + 01. 03. (ABGB-Fristrechnung).""" + employee = self._employee( + {'ueberzahlung': 6000.0, + 'contract_date_start': date(2026, 1, 31)}) + slip_feb = self._slip(employee, 2) + self.assertAlmostEqual(self._total(slip_feb, 'BVG'), 0.0, + places=2) + slip_mrz = self._slip(employee, 3) + self.assertAlmostEqual(self._total(slip_mrz, 'BVG'), + 6000.0 * 0.0153, places=2) + + # ------------------------------------------------------------------ + # RF-BV6/7/9: Geringfügige, SZ-Fälligkeit/§-67-Ausschluss, Alt-Fall + # ------------------------------------------------------------------ + def test_06_geringfuegig(self): + """Geringfügig Beschäftigte sind BMSVG-erfasst: BVG läuft + (500 € → 7,65 €), obwohl der SVDG-Block auf 0 geht.""" + employee = self._employee({'ueberzahlung': 500.0}) + slip = self._slip(employee, 1) + self.assertTrue(slip.l10n_at_geringfuegig) + self.assertAlmostEqual(self._total(slip, 'SVDG_KV'), 0.0, + places=2) + self.assertAlmostEqual(self._total(slip, 'BVG'), + 500.0 * 0.0153, places=2) + + def test_07_sz_fenster_und_s67(self): + """SZ mit Beitragsmonat im freien Fenster bleibt frei + (Fälligkeit taggenau: 20. 06. frei, 20. 07. pflichtig); + §-67-Abs-3-Abfertigung scheidet aus der BV-Grundlage aus + (§ 49 Abs 3 Z 7 ASVG); laufender Anteil 17/30.""" + employee = self._employee( + {'contract_date_start': date(2026, 6, 15)}) + struktur = self._bv_struktur('A', [ + ('LFD', 2000.0, 'laufend', 'keine', None), + ('SZ_FENSTER', 1000.0, 'sonstig', 'keine', + date(2026, 6, 20)), + ('SZ_PFLECHTIG', 1000.0, 'sonstig', 'keine', + date(2026, 7, 20)), + ('ABF_ALT', 10000.0, 'sonstig', 'abs3', None), + ]) + slip = self._slip(employee, 7, struct=struktur) + basis = 2000.0 * 17 / 30 + 1000.0 + self.assertAlmostEqual(self._total(slip, 'BVG'), + basis * 0.0153, places=2) + + def test_09_alt_fall_flag(self): + """Alt-Verhältnis (Flag abgewählt) läuft ohne BVG.""" + employee = self._employee({'l10n_at_abfertigung_neu': False}) + slip = self._slip(employee, 11) + self.assertAlmostEqual(self._total(slip, 'KVGEO'), 2059.41, + places=2) + self.assertAlmostEqual(self._total(slip, 'BVG'), 0.0, places=2) + + # ------------------------------------------------------------------ + # RF-BV8: Anwartschaft + Endabrechnungs-Wizard (§§ 14-17) + # ------------------------------------------------------------------ + def test_08_wizard_verfuegbarkeit(self): + """Anwartschaft aus 3 validierten Läufen (3 × 31,51 €); + §-14-Matrix: < 36 Beitragsmonate sperrt IMMER, unschädliche + AG-Kündigung + 36 Monate verfügbar, AN-Kündigung sperrt; + Fristen § 16 (Ende des zweitfolgenden Monats) und § 17 + (6 Monate Erklärungsfrist).""" + employee = self._employee() + for monat in (1, 2, 3): + self._slip(employee, monat, confirm=True) + fort = employee._at_bv_anwartschaft() + self.assertEqual(fort['monate'], 3) + self.assertAlmostEqual(fort['beitraege'], + 3 * 2059.41 * 0.0153, places=1) + self.assertEqual(fort['jahre'], [(2026, 3, fort['beitraege'])]) + wizard = self.env['l10n.at.bv.endabrechnung.wizard'].create({ + 'employee_id': employee.id, + 'austrittsdatum': date(2026, 8, 31), + 'beendigungsart': 'ag_kuendigung', + 'geltendmachung': date(2026, 9, 11), + }) + self.assertFalse(wizard.verfuegbar) # 3 < 36 Beitragsmonate + self.assertIn('36', wizard.sperrgrund) + wizard.write({'monate_extern': 33}) + self.assertEqual(wizard.monate_gesamt, 36) + self.assertTrue(wizard.verfuegbar) # unschädliche Beendigung + wizard.write({'beendigungsart': 'an_kuendigung'}) + self.assertFalse(wizard.verfuegbar) # § 14 Abs 2-Gruppe + self.assertEqual(wizard.faelligkeit, date(2026, 11, 30)) + self.assertEqual(wizard.erklaerungsfrist, date(2027, 2, 28)) \ No newline at end of file diff --git a/addons/l10n_at_hr_payroll_private/views/bv_views.xml b/addons/l10n_at_hr_payroll_private/views/bv_views.xml new file mode 100644 index 0000000..0221147 --- /dev/null +++ b/addons/l10n_at_hr_payroll_private/views/bv_views.xml @@ -0,0 +1,146 @@ + + + + + + + + + + l10n.at.bv.kasse.list + l10n.at.bv.kasse + + + + + + + + + + + l10n.at.bv.kasse.form + l10n.at.bv.kasse + +
+ + + + + + + + + + + +
+
+
+ + + Mitarbeitervorsorgekassen + l10n.at.bv.kasse + list,form + + + + + hr.employee.form.bv + hr.employee + + + + + + + + + + + + + + + + l10n.at.bv.endabrechnung.wizard.form + l10n.at.bv.endabrechnung.wizard + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+
+ + + BV-Endabrechnung (Austritt) + l10n.at.bv.endabrechnung.wizard + form + new + + + + + +
\ No newline at end of file diff --git a/addons/l10n_at_hr_payroll_private/wizard/__init__.py b/addons/l10n_at_hr_payroll_private/wizard/__init__.py index 0397378..6e926da 100644 --- a/addons/l10n_at_hr_payroll_private/wizard/__init__.py +++ b/addons/l10n_at_hr_payroll_private/wizard/__init__.py @@ -1,2 +1,3 @@ # Part of the odoo-at-payroll project. License: LGPL-3. -from . import kv_katalog_import \ No newline at end of file +from . import kv_katalog_import +from . import bv_endabrechnung \ No newline at end of file diff --git a/addons/l10n_at_hr_payroll_private/wizard/bv_endabrechnung.py b/addons/l10n_at_hr_payroll_private/wizard/bv_endabrechnung.py new file mode 100644 index 0000000..bce1f53 --- /dev/null +++ b/addons/l10n_at_hr_payroll_private/wizard/bv_endabrechnung.py @@ -0,0 +1,156 @@ +# Part of the odoo-at-payroll project. License: LGPL-3. +"""BV-Endabrechnungs-Wizard (GP5): Anwartschaftsausweis bei Austritt +mit §-14-Abs-2-Verfügbarkeitsprüfung und Fristenausweis (§§ 16 f. +BMSVG). Reiner Informations-/Dokumentationsprozess — die Auszahlung +der Anwartschaft erfolgt durch die MV-Kasse (§ 15: gutgebrachte +Beiträge + Erträge, KEINE Monatsentgelt-Staffel), nicht über die +Lohnverrechnung. Verfügungsmatrix (lb-end-03, Stand 2026-08): +Verfügungsausschluss bei AN-Kündigung (außer MSchG/VKG-Teilzeit- +Kündigung), verschuldeter Entlassung, unberechtigtem vorzeitigem +Austritt — und IMMER bei weniger als 36 Beitragsmonaten seit der +ersten Beitragszahlung bzw. letztmaligen Verfügung (alle +Beitragszeiten zählen, Vor-Arbeitgeber manuell erfassen).""" +from datetime import timedelta + +from dateutil.relativedelta import relativedelta + +from odoo import _, api, fields, models + +ABS14_GRUPPEN = ('an_kuendigung', 'verschuldete_entlassung', + 'unberechtigter_austritt') + + +class L10nAtBvEndabrechnungWizard(models.TransientModel): + _name = 'l10n.at.bv.endabrechnung.wizard' + _description = 'BV-Endabrechnung (Abfertigung Neu, §§ 14-17 BMSVG)' + + employee_id = fields.Many2one( + 'hr.employee', string='Dienstnehmer:in', required=True) + austrittsdatum = fields.Date( + string='Austritt (Ende des Arbeitsverhältnisses)', + help='Beginn der 6-Monats-Erklärungsfrist (§ 17 BMSVG): gibt der ' + 'Berechtigte binnen sechs Monaten keine ' + 'Verwendungserklärung ab, wird weiterveranlagt.') + beendigungsart = fields.Selection( + [('ag_kuendigung', 'Kündigung durch den Arbeitgeber (unschädlich)'), + ('einvernehmlich', 'Einvernehmliche Auflösung (unschädlich)'), + ('berechtigter_austritt', + 'Berechtigter vorzeitiger Austritt (unschädlich)'), + ('unverschuldete_entlassung', + 'Unverschuldete/unrechtmäßige Entlassung (unschädlich)'), + ('an_kuendigung', 'Kündigung durch den AN (§ 14 Abs 2 BMSVG)'), + ('verschuldete_entlassung', + 'Verschuldete Entlassung (§ 14 Abs 2 BMSVG)'), + ('unberechtigter_austritt', + 'Unberechtigter vorzeitiger Austritt (§ 14 Abs 2 BMSVG)'), + ('tod', 'Tod (ungekürzt an unterhaltsberechtigte Erben)')], + string='Beendigungsart', required=True) + monate_extern = fields.Integer( + string='Beitragsmonate Vor-Arbeitgeber', + help='Beitragsmonate aus Vor-Beschäftigungen laut Auszug der ' + 'MV-Kasse (§ 14 Abs 2 BMSVG: alle Beitragszeiten über ' + 'Arbeitgeber hinweg summierbar). Vorbelegt aus dem ' + 'Mitarbeiter-Stamm.') + ertraege = fields.Monetary( + string='Erträge laut Kassen-Auszug', + currency_field='currency_id', + help='Veranlagungserträge laut Auszug der MV-Kasse; die ' + 'Anwartschaft = gutgebrachte Beiträge + Erträge (§ 15 ' + 'BMSVG).') + currency_id = fields.Many2one( + 'res.currency', + default=lambda self: self.env.company.currency_id) + geltendmachung = fields.Date( + string='Geltendmachung', + default=fields.Date.today, + help='Datum der Geltendmachung gegenüber dem Arbeitgeber; die ' + 'Fälligkeit der Auszahlung tritt am Ende des ' + 'zweitfolgenden Monats ein (§ 16 BMSVG).') + + kasse_id = fields.Many2one( + 'l10n.at.bv.kasse', string='Mitarbeitervorsorgekasse', + compute='_compute_ergebnis') + monate_lokal = fields.Integer( + string='Beitragsmonate (dieser DG)', + compute='_compute_ergebnis', + help='Beitragsmonate aus validierten Lohnläufen mit BVG-Beitrag.') + beitraege_lokal = fields.Monetary( + string='Gutgebrachte Beiträge (dieser DG)', + currency_field='currency_id', compute='_compute_ergebnis') + monate_gesamt = fields.Integer( + string='Beitragsmonate gesamt', compute='_compute_ergebnis', + help='Beitragsmonate gesamt inkl. Vor-Arbeitgeber — ' + '36-Monats-Kriterium des § 14 Abs 2 BMSVG.') + anwartschaft = fields.Monetary( + string='Anwartschaft (Beiträge + Erträge)', + currency_field='currency_id', compute='_compute_ergebnis') + verfuegbar = fields.Boolean( + string='Verfügbar (§ 14 Abs 2 BMSVG)', compute='_compute_ergebnis') + sperrgrund = fields.Text( + string='Verfügungsausschluss', compute='_compute_ergebnis') + faelligkeit = fields.Date( + string='Fälligkeit Auszahlung (§ 16 BMSVG)', + compute='_compute_ergebnis') + erklaerungsfrist = fields.Date( + string='Erklärungsfrist (§ 17 BMSVG)', compute='_compute_ergebnis') + + @api.onchange('employee_id') + def _onchange_employee(self): + for wizard in self: + if not wizard.employee_id: + continue + wizard.austrittsdatum = ( + wizard.employee_id.version_id.contract_date_end + or fields.Date.today()) + wizard.monate_extern = \ + wizard.employee_id.l10n_at_bv_monate_extern + wizard.ertraege = wizard.employee_id.l10n_at_bv_ertraege + + @api.depends('employee_id', 'beendigungsart', 'monate_extern', + 'ertraege', 'geltendmachung', 'austrittsdatum') + def _compute_ergebnis(self): + for wizard in self: + if wizard.employee_id: + fort = wizard.employee_id._at_bv_anwartschaft() + wizard.kasse_id = wizard.employee_id.l10n_at_bv_kasse_id + wizard.monate_lokal = fort['monate'] + wizard.beitraege_lokal = fort['beitraege'] + else: + wizard.kasse_id = False + wizard.monate_lokal = 0 + wizard.beitraege_lokal = 0.0 + wizard.monate_gesamt = wizard.monate_lokal \ + + wizard.monate_extern + wizard.anwartschaft = wizard.beitraege_lokal + wizard.ertraege + grund = False + if wizard.beendigungsart in ABS14_GRUPPEN: + grund = _('Verfügungsausschluss nach § 14 Abs 2 BMSVG ' + 'wegen der Beendigungsart (Kündigung durch ' + 'den AN, verschuldete Entlassung oder ' + 'unberechtigter vorzeitiger Austritt); die ' + 'Anwartschaft bleibt bei der MV-Kasse ' + 'weiterveranlagt.') + elif wizard.monate_gesamt < 36: + grund = _('Verfügungsausschluss nach § 14 Abs 2 BMSVG: ' + 'noch keine 36 Beitragsmonate (%(monaten)s von ' + '36; alle Beitragszeiten zählen).', + monaten=wizard.monate_gesamt) + wizard.verfuegbar = not grund + wizard.sperrgrund = grund + if wizard.geltendmachung: + # § 16: Fälligkeit am Ende des zweitfolgenden Monats. + zweit = wizard.geltendmachung + relativedelta(months=2) + wizard.faelligkeit = (zweit.replace(day=1) + + relativedelta(months=1) + - timedelta(days=1)) + else: + wizard.faelligkeit = False + wizard.erklaerungsfrist = ( + wizard.austrittsdatum + relativedelta(months=6) + if wizard.austrittsdatum else False) + + def action_report(self): + self.ensure_one() + return self.env.ref( + 'l10n_at_hr_payroll_private.action_report_bv_anwartschaft' + ).report_action(self.employee_id) \ No newline at end of file