diff --git a/addons/l10n_at_hr_payroll/__manifest__.py b/addons/l10n_at_hr_payroll/__manifest__.py index 09d1f9d..630f1c4 100644 --- a/addons/l10n_at_hr_payroll/__manifest__.py +++ b/addons/l10n_at_hr_payroll/__manifest__.py @@ -1,7 +1,7 @@ # Part of the odoo-at-payroll project. License: LGPL-3. { 'name': 'Österreich - Personalverrechnung Kern (AT Payroll Core)', - 'version': '19.0.2.0.0', + 'version': '19.0.3.0.0', 'author': 'ixsol', 'website': 'https://gem360.at', 'category': 'Human Resources/Payroll', @@ -19,10 +19,21 @@ Produkt (``l10n_at_hr_payroll_private``). Grundlagen: ``personalverrechnung/`` generiert und gegen den offiziellen SV-Werte-Report gespiegelt): KV/PV/UV/AV/AK/WF/BV/IESG-Beitragssätze, Geringfügigkeitsgrenze, Höchstbeitragsgrundlagen (monatlich/SZ), AV-Staffel § 2a AMPFG; +- **Lohnsteuer (AP3, GP3)**: ``models/lohnsteuer.py`` (§ 66 kumulativ mit + Rollung, § 67 Sechstel/Fixsätze, § 68-Steuerfreiheiten), + ``rule_parameters_lst_2026.xml``; +- **SV/DB (AP4, GP4)**: ``models/sozialversicherung.py`` — DG-Beiträge + (SVDG, ÖGK 2026: 20,98 %), Geringfügigkeit (§ 5 Abs 2 iVm § 810 Abs 3 + ASVG) + § 53a-UV, beitragsfreie Bezüge § 49 Abs 3 mit Jahres- + Kumulativen, altersabhängige Satzausschaltungen (AlVG § 1 Abs 2 lit e, + ASVG § 51 Abs 6/7, IESG § 12), WF-Satzvektor Bundesland × Jahr + (Wien 1,5 % ab 2026), ALV-DN-Staffel § 2a AMPFG, Beitragsmonats- + Allokation (Anspruchsprinzip), DAG-Bewertung § 1 DAG — + ``rule_parameters_sv_2026.xml``; - deklarative TASY-Mapping-Tabelle und Import-Wizard (Preview, Sprungwarnung > 10 %, explizite Bestätigung, versionierte Werte); -- folgen nach Freigabe der Bgld.-APs: Lohnsteuer (§ 33/§ 68 EStG, AP3), - Meldewesen (L16/FinanzOnline, eSV/ELDA, AP5), Belege (AP6). +- folgen nach Freigabe der Bgld.-APs: Meldewesen (L16/FinanzOnline, + eSV/ELDA, AP5), Belege (AP6). Kein VRV-Bezug: die Gemeinde-Buchhaltung (Anlage 3b, Ansatz/MVAG) läuft über die Brücke ``l10n_at_gemeinde_payroll_vrv`` im gem360-Repo (AP7). @@ -32,6 +43,7 @@ Kein VRV-Bezug: die Gemeinde-Buchhaltung (Anlage 3b, Ansatz/MVAG) läuft 'security/ir.model.access.csv', 'data/rule_parameters_2026.xml', 'data/rule_parameters_lst_2026.xml', + 'data/rule_parameters_sv_2026.xml', 'data/tasy_mapping.xml', 'views/tasy_views.xml', ], diff --git a/addons/l10n_at_hr_payroll/data/rule_parameters_2026.xml b/addons/l10n_at_hr_payroll/data/rule_parameters_2026.xml index 5585b15..c514ba3 100644 --- a/addons/l10n_at_hr_payroll/data/rule_parameters_2026.xml +++ b/addons/l10n_at_hr_payroll/data/rule_parameters_2026.xml @@ -65,15 +65,31 @@ 2024-01-01 2.95 + - SV AV-Beitragsstaffel bei geringem Einkommen (§ 2a AMPFG) + SV ALV-DN-Staffel bei geringem Einkommen (§ 2a AMPFG) at_sv_av_staffel_ampfg + + + 2025-01-01 + {'tiers': [(0.0, 2074.00, 0.0), (2074.00, 2262.00, 1.0), (2262.00, 2451.00, 2.0)], 'dn_voll': 2.95} + - 2026-07-01 - {'tiers': [(0.0, 2225.00, 0.0), (2225.00, 2427.00, 1.0), (2427.00, 2630.00, 2.0)], 'av_total': 5.9} + 2026-01-01 + {'tiers': [(0.0, 2225.00, 0.0), (2225.00, 2427.00, 1.0), (2427.00, 2630.00, 2.0)], 'dn_voll': 2.95} + + + + 2027-01-01 + {'tiers': [], 'dn_voll': 2.95} Betriebliche Vorsorge Dienstgeberanteil @@ -110,6 +126,12 @@ at_sv_hbg_monatlich + + + + 2025-01-01 + 6450.0 + 2026-01-01 diff --git a/addons/l10n_at_hr_payroll/data/rule_parameters_sv_2026.xml b/addons/l10n_at_hr_payroll/data/rule_parameters_sv_2026.xml new file mode 100644 index 0000000..fea33bc --- /dev/null +++ b/addons/l10n_at_hr_payroll/data/rule_parameters_sv_2026.xml @@ -0,0 +1,181 @@ + + + + + + + + + + + + + + + + + + + + + WF-Beitrag Satzvektor (Bundesland × Jahr) + at_sv_wf_vektor + + + + + 2019-01-01 + {'default': {'dn': 0.5, 'dg': 0.5}} + + + + 2026-01-01 + {'wien': {'dn': 0.75, 'dg': 0.75}, 'default': {'dn': 0.5, 'dg': 0.5}} + + + + + + + + + + + + + + Beitragsfreie Bezüge § 49 Abs 3 ASVG (Grenzen) + at_sv49_grenzen + + + + + 2026-01-01 + {'mahlzeit_tag': 8.0, 'lebensmittel_tag': 2.0, 'rabatt_jahr': 1000.0, 'betriebsveranstaltung_jahr': 365.0, 'jubilaeum_jahr': 186.0, 'zukunftssicherung_jahr': 300.0, 'kinderbetreuung_jahr': 2000.0, 'homeoffice_tag': 3.0, 'homeoffice_tage': 100} + + + + + + + + + + + + + + Geringfügig Beschäftigte — § 53a ASVG (UV/Selbstversicherung) + at_sv_geringfuegig + + + + + 2025-01-01 + {'beitrag_uv': 6.06, 'beitrag_selbstversicherung': 77.81} + + + + 2026-01-01 + {'beitrag_uv': 6.50, 'beitrag_selbstversicherung': 83.49} + + + + + + + PV-Faktor Bonusphase (§ 51 Abs 7 ASVG) + at_pv_bonus_faktor + + + + + 2019-01-01 + 0.5 + + + + + + + FLAF-Dienstgeberbeitrag (§ 41 Abs 5 FLAG) + at_flaf_db + + + + + 2018-01-01 + 3.9 + + + + 2025-01-01 + 3.7 + + + + 2028-01-01 + 2.7 + + + + + Kommunalsteuer (§ 9 KommStG 1993) + at_kommst_satz + + + + + 2025-01-01 + 3.0 + + + + + + + + LNK-Freibetrag (FLAF/KommSt/DZ; § 41 Abs 4 FLAG) + at_lnk_freibetrag + + + + + 2025-01-01 + {'freibetrag': 1095.0, 'grenze': 1460.0} + + + + + + + + Zuschlag zum Dienstgeberbeitrag (WKG; je Bundesland) + at_dz_vektor + + + + + 2026-01-01 + {'laender': {'burgenland': 0.40, 'kaernten': 0.37, 'niederoesterreich': 0.33, 'oberoesterreich': 0.31, 'salzburg': 0.35, 'steiermark': 0.34, 'tirol': 0.39, 'vorarlberg': 0.33, 'wien': 0.36}} + + + + + + + + + + + + Dienstgeberabgabe geringfügig Beschäftigter (§ 1 DAG) + at_dag + + + + + 2025-01-01 + {'satz': 19.4, 'schwelle_faktor': 1.5} + + \ No newline at end of file diff --git a/addons/l10n_at_hr_payroll/models/__init__.py b/addons/l10n_at_hr_payroll/models/__init__.py index a2b8219..97bafe9 100644 --- a/addons/l10n_at_hr_payroll/models/__init__.py +++ b/addons/l10n_at_hr_payroll/models/__init__.py @@ -1,3 +1,5 @@ # Part of the odoo-at-payroll project. License: LGPL-3. from . import tasy -from . import lohnsteuer \ No newline at end of file +from . import lohnsteuer +from . import sozialversicherung +from . import sozialversicherung \ No newline at end of file diff --git a/addons/l10n_at_hr_payroll/models/lohnsteuer.py b/addons/l10n_at_hr_payroll/models/lohnsteuer.py index 559ebca..1c63f9f 100644 --- a/addons/l10n_at_hr_payroll/models/lohnsteuer.py +++ b/addons/l10n_at_hr_payroll/models/lohnsteuer.py @@ -198,29 +198,15 @@ class HrPayslip(models.Model): return ergebnis # ------------------------------------------------------------------ - # DN-SV (Bemessungsbestandteil § 62; Werte at_sv_* — GP4 baut - # DG-Seite/Geringfügigkeit/beitragsfreie Kumulative darauf) + # DN-SV (Bemessungsbestandteil § 62) — Basis/Sätze/Geringfügigkeit/ + # §-49-Kumulative/DG-Seite: models/sozialversicherung.py (GP4) # ------------------------------------------------------------------ - def _l10n_at_svdn_basis_laufen(self, result_rules=None): - """SV-Beitragsgrundlage laufend (HBG-capped, at_sv_*).""" - self.ensure_one() - hbg = float(self._l10n_at_param('at_sv_hbg_monatlich')) - return min(max(self._l10n_at_brutto('laufend', result_rules), 0.0), hbg) - def _l10n_at_svdn(self, komponente, result_rules=None): """DN-SV-Komponente auf laufender Basis: kv/pv/av/ak/wf — AK § 61 AKG und WF § 62 Z 5 nur laufend (nicht auf §-67-Bezüge); - WF-Wien über Company-Flag (DN 0,75 statt 0,50).""" + Wien-WF über Satzvektor (l10n_at_wf_wien/Bundesland, GP4).""" self.ensure_one() - basis = self._l10n_at_svdn_basis_laufen(result_rules) - codes = {'kv': 'at_sv_kv_dn', 'pv': 'at_sv_pv_dn', - 'av': 'at_sv_av_dn', 'ak': 'at_sv_ak_dn'} - if komponente == 'wf': - satz = 0.75 if self.company_id.l10n_at_wf_wien \ - else float(self._l10n_at_param('at_sv_wf_dn')) - else: - satz = float(self._l10n_at_param(codes[komponente])) - return basis * satz / 100.0 + return self._l10n_at_sv_beitrag('dn', komponente, result_rules) def _l10n_at_svdn_gesamt(self, result_rules=None): self.ensure_one() @@ -240,11 +226,15 @@ class HrPayslip(models.Model): def _l10n_at_svdn_sz(self, result_rules=None): """SV-DN auf sonstigen Bezügen (§ 67 Abs 12 — vor Fixsätzen - abzuziehen): KV/PV/AV, ohne AK/WF (§ 62 Z 3/Z 5).""" + abzuziehen): KV/PV/AV, ohne AK/WF (§ 62 Z 3/Z 5); Satzausschal- + tungen/Steffel über _l10n_at_sv_satz (GP4).""" self.ensure_one() + if self._l10n_at_geringfuegig(result_rules): + return 0.0 basis = self._l10n_at_svdn_basis_sz(result_rules) - satz = sum(float(self._l10n_at_param(code)) for code in - ('at_sv_kv_dn', 'at_sv_pv_dn', 'at_sv_av_dn')) + satz = sum(self._l10n_at_sv_satz( + 'dn', k, staffel_basis=self._l10n_at_sv_basis_roh(result_rules)) + for k in ('kv', 'pv', 'av')) return basis * satz / 100.0 # ------------------------------------------------------------------ diff --git a/addons/l10n_at_hr_payroll/models/sozialversicherung.py b/addons/l10n_at_hr_payroll/models/sozialversicherung.py new file mode 100644 index 0000000..b3b81b3 --- /dev/null +++ b/addons/l10n_at_hr_payroll/models/sozialversicherung.py @@ -0,0 +1,489 @@ +# Part of the odoo-at-payroll project. License: LGPL-3. +"""Sozialversicherungs-Kern (GP4/AP4): DG-Beiträge (SVDG), Geringfügigkeit +(§ 5 Abs 2 ASVG iVm § 810 Abs 3 — 2026-Aufwertung ausgestellt), §-53a- +UV der Geringfügigen, beitragsfreie Bezüge nach § 49 Abs 3 ASVG mit +Jahres-Kumulativen, altersabhängige Satzausschaltungen (§ 1 Abs 2 lit e +AlVG, § 51 Abs 6/7 ASVG, § 12 Abs 2 IESG), WF-Satzvektor Bundesland × +Jahr (Wien 1,5 % ab 1.1.2026), ALV-DN-Staffel (§ 2a AMPFG) und +Beitragsmonats-Allokation (Anspruchsprinzip, historisch versionierte +Sätze/HBG je Beitragsmonat). + +Verifizierte Grundlagen (2026-09-11, s. RECHTSQUELLEN-Privat Abschn. 4 +und docs/rechenfaelle/sozialversicherung.md): +- ÖGK-Beitragstarifsystem „Normalbetriebe Allgemeiner Teil", gültig ab + 01.07.2026 (DN 18,07 % · DG 20,98 % · Wien DN 18,32/DG 21,23 %); +- TASY-LSWH-Export 2026-03-24 (GFG 551,10 · DAG-Grenze 826,65 · + §-19a-Pauschalbeitrag 83,49 €/Monat — Primärquellen-Entscheidung); +- „Beitragsrechtliche Werte 2026" (sozialversicherung.at): GFG-Aufwertung + 2026 ausgesetzt (§ 810 Abs 3 ASVG); AV-Staffel bis 2.225/2.427/2.630 € + 0/1/2 % DN; WF Wien 0,75/0,75 ab 1.1.2026; +- ASVG §§ 51 Abs 6 (UV-Entfall 60. LJ/Lehrlinge), 51 Abs 7 (PV in der + Bonusphase hälftig, DG und DN), 53a Abs 1/3/5 (UV 1,1 % der allgemeinen + Beitragsgrundlage; Pauschalbeitrag 14,12 %, auch von SZ); + IESG § 12 Abs 1 Z 4 iVm Abs 2 (Entfall ab Folgemonat nach 63); + AlVG § 1 Abs 2 lit e (AV-Entfall, ab 2027 inkl. Korridorpension). + +Laufzeit-Datenfluss wie GP3: Regeln reichen Zwischensummen über +``result_rules`` an die Compute-Methoden; abgeschlossene Läufe werden +über line_ids gelesen. Der Beitragsmonat wird je Regel geführt +(``l10n_at_beitragsmonat``) und auf die Lohnkontolinie durchgereicht. +""" +from datetime import date, timedelta + +from odoo import api, fields, models +from odoo.tools.safe_eval import safe_eval + + +class HrSalaryRule(models.Model): + _inherit = 'hr.salary.rule' + + l10n_at_sv49_frei = fields.Selection( + [('keine', 'Keine §-49-Abs-3-Befreiung'), + ('mahlzeit', 'Mahlzeiten-Gutschein (§ 49 Abs 3 Z 12; 8 €/AT)'), + ('rabatt', 'Mitarbeiterrabatt (§ 49 Abs 3 Z 29; 1.000 €/J)'), + ('betriebsveranstaltung', 'Betriebsveranstaltung (Z 17; 365 €/J)'), + ('jubilaeum', 'Jubiläums-Sachzuwendung (Z 17; 186 €/J)'), + ('zukunftssicherung', 'Zukunftssicherung (Z 18 lit a; 300 €/J)'), + ('kinderbetreuung', 'Kinderbetreuungszuschuss (Z 11 lit d; 2.000 €/Kind/J)'), + ('homeoffice', 'Homeoffice-Pauschale (Z 31; 3 €/Tag, max 100 Tage)')], + string='AT § 49 Abs 3 (beitragsfrei)', + default='keine', + help='Beitragsfreie Bezugsbestandteile: der beitragsfreie Anteil ' + 'wird je Bezugsmonat von der SV-Beitragsgrundlage abgezogen; ' + 'Grenzen als Jahres-Kumulative je Dienstnehmer ' + '(at_sv49_grenzen). Überschreiten = beitragspflichtiger ' + 'Sachbezug (Umwidmung). Menge = Arbeitstage (Z 12/Z 31).') + l10n_at_s67_abs = fields.Selection( + [('keine', 'Keine §-67-Abs-3/6-Begünstigung'), + ('abs3', 'Abfertigung (§ 67 Abs 3 EStG)'), + ('abs6', 'Abfindung (§ 67 Abs 6 EStG)')], + string='AT § 67 Abs 3/6', + default='keine', + help='Markiert Bezüge, die beim FLAF-Dienstgeberbeitrag (§ 41 ' + 'Abs 4 FLAG), Zuschlag zum DG-Beitrag (WKG) und der ' + 'Kommunalsteuer (§ 5 Abs 2 KommStG) aus der Bemessungsgrund' + 'lage ausgenommen sind.') + l10n_at_beitragsmonat = fields.Date( + string='AT Beitragsmonat (Anspruchsprinzip)', + help='Monat, in dem der Anspruch auf diesen Bezug fällig wurde ' + '(§ 17 ASVG). Leer = Abrechnungsmonat. Für Nachzahlungs-/' + 'Korrekturlinien setzen: SV-Sätze, HBG und GFG werden ' + 'historisch versioniert je Beitragsmonat aufgelöst ' + '(Lohnsteuer rollt kumulativ in den Zuflussmonat, GP3).') + + +class HrPayslipLine(models.Model): + _inherit = 'hr.payslip.line' + + l10n_at_beitragsmonat = fields.Date( + string='AT Beitragsmonat', + compute='_compute_l10n_at_beitragsmonat', + store=True, + readonly=False, + index=True, + help='Beitragsmonat der Linie (Anspruchsprinzip); default ' + 'Abrechnungsmonat, über die Regel übersteuerbar.') + + @api.depends('salary_rule_id.l10n_at_beitragsmonat', 'date_from') + def _compute_l10n_at_beitragsmonat(self): + for line in self: + line.l10n_at_beitragsmonat = ( + line.salary_rule_id.l10n_at_beitragsmonat or line.date_from) + + +class HrVersion(models.Model): + _inherit = 'hr.version' + + l10n_at_lehrling = fields.Boolean( + string='AT Lehrling', + help='Keine AK-Umlage (§ 61 AKG) und kein UV-Beitrag (§ 51 Abs 6 ' + 'ASVG — aus Mitteln der Unfallversicherung); GFG gilt nicht ' + '(§ 5 Abs 3 ASVG).') + l10n_at_alv_entfall_ab = fields.Date( + string='AT ALV-Entfall ab (§ 1 Abs 2 lit e AlVG)', + help='AV-Beitrag DN+DG und IESG-Zuschlag entfallen ab diesem ' + 'Beitragsmonat (63. Lebensjahr oder erfüllte Alterspensions-' + 'Anwartschaft; ab 1.1.2027 inkl. Korridorpension, ' + 'NOR40280219; § 12 Abs 2 IESG). Stichtagsregel am Vertrag.') + l10n_at_uv_entfall_ab = fields.Date( + string='AT UV-Entfall ab (§ 51 Abs 6 ASVG)', + help='Unfallversicherungsbeitrag entfällt ab diesem ' + 'Beitragsmonat (Vollendung des 60. Lebensjahres); die ' + 'Person bleibt unfallversichert.') + l10n_at_pv_bonusphase_ab = fields.Date( + string='AT PV-Bonusphase ab (§ 51 Abs 7 ASVG)', + help='Pensionsversicherungsbeitrag je zur Hälfte reduziert für ' + 'DG und DN (Bonusphase/Pensionsanspruch; ÖGK-Abschlag ' + 'A 15; Pensionskonto-Gutschrift bleibt ungekürzt — Auswertung ' + 'GP8).') + l10n_at_lnkp_frei_ab = fields.Date( + string='AT LNK-frei ab (§ 41 Abs 4 lit f FLAG)', + help='Arbeitslöhne ab diesem Kalendermonat sind vom FLAF-' + 'Dienstgeberbeitrag und vom Zuschlag zum Dienstgeberbeitrag ' + '(WKG) ausgenommen (Folgemonat nach Vollendung des 60. ' + 'Lebensjahres). Kommunalsteuer: keine Befreiung.') + + +class ResCompany(models.Model): + _inherit = 'res.company' + + l10n_at_bundesland = fields.Selection( + [('burgenland', 'Burgenland'), ('kaernten', 'Kärnten'), + ('niederoesterreich', 'Niederösterreich'), + ('oberoesterreich', 'Oberösterreich'), ('salzburg', 'Salzburg'), + ('steiermark', 'Steiermark'), ('tirol', 'Tirol'), + ('vorarlberg', 'Vorarlberg'), ('wien', 'Wien')], + string='AT Bundesland (Betriebsstätte)', + help='Betriebsstätten-Bundesland für Bundesland-Satzvektoren ' + '(z. B. DZ-Landeskammeranteil; WF-Wien zusätzlich über das ' + 'Company-Flag l10n_at_wf_wien steuerbar).') + l10n_at_wk_mitglied = fields.Boolean( + string='AT WKO-Mitgliedschaft (DZ-pflichtig)', + help='Zuschlag zum Dienstgeberbeitrag (§§ 122/126 WKG) nur für ' + 'Mitglieder der Wirtschaftskammerorganisation (§ 2 WKG).') + + def _l10n_at_dag(self, monat_start): + """Dienstgeberabgabe geringfügig Beschäftigter (§ 1 DAG) für den + Kalendermonat: 19,4 % der Lohnsumme (Beitragsgrundlagen ohne SZ) + aller geringfügig Beschäftigten, wenn die Lohnsumme das 1,5-fache + der monatlichen GFG überschreitet und mindestens zwei + Geringfügige beschäftigt sind (lb-bes-06; TASY MINGRDAGP1 2026: + 826,65 €; Ausweis/Veranlagung GP8).""" + monat_ende = (monat_start + timedelta(days=32)).replace(day=1) \ + - timedelta(days=1) + slips = self.env['hr.payslip'].search([ + ('company_id', '=', self.id), + ('date_from', '<=', monat_ende), + ('date_to', '>=', monat_start), + ('state', 'not in', ('draft', 'cancel')), + ('l10n_at_geringfuegig', '=', True), + ]) + lohnsumme = sum( + slip._l10n_at_sv_basis_roh() for slip in slips) + anzahl = len({s.employee_id.id for s in slips}) + params = self.env['hr.rule.parameter']._get_parameter_from_code( + 'at_dag', monat_start, raise_if_not_found=False) or {} + gfg = float(self.env['hr.rule.parameter']._get_parameter_from_code( + 'at_sv_gfg_monatlich', monat_start, + raise_if_not_found=False) or 0.0) + schwelle = float(params.get('schwelle_faktor', 0.0)) * gfg + abgabe = 0.0 + if anzahl >= 2 and lohnsumme > schwelle: + abgabe = lohnsumme * float(params.get('satz', 0.0)) / 100.0 + return {'anzahl': anzahl, 'lohnsumme': lohnsumme, + 'schwelle': schwelle, 'abgabe': abgabe} + + +class HrPayslip(models.Model): + _inherit = 'hr.payslip' + + l10n_at_geringfuegig = fields.Boolean( + string='AT Geringfügig beschäftigt', + compute='_compute_l10n_at_geringfuegig', + store=True, + help='Laufendes Entgelt (§-49-bereinigt, hochgerechnet bei ' + 'untermonatiger Beschäftigung) überschreitet die GFG nicht ' + '(§ 5 Abs 2 ASVG). DAG-Monitoring-Grundlage (§ 1 DAG).') + + @api.depends('line_ids', 'date_from', 'date_to', 'version_id') + def _compute_l10n_at_geringfuegig(self): + for slip in self: + try: + slip.l10n_at_geringfuegig = slip._l10n_at_geringfuegig() + except Exception: + # Alter Lauf ohne GFG-Parameterzeile: keine Bewertung. + slip.l10n_at_geringfuegig = False + + # ------------------------------------------------------------------ + # Geringfügigkeit (§ 5 Abs 2 ASVG; SZ zählen nicht — lb-bes-06) + # ------------------------------------------------------------------ + def _l10n_at_geringfuegig(self, result_rules=None): + """Hochgerechnetes laufendes Entgelt ≤ GFG (2026: 551,10 € — + Aufwertung gem. § 810 Abs 3 ASVG ausgesetzt). Lehrlinge sind + nie geringfügig (§ 5 Abs 3 ASVG). Verhältnisse kürzer als ein + Naturalmonat: Gesamtentgelt direkt gegen die Monatsgrenze.""" + self.ensure_one() + version = self.version_id + if version.l10n_at_lehrling: + return False + roh = self._l10n_at_sv_basis_roh(result_rules) + if roh <= 0.0: + # Ohne laufenden Bezug keine Statusbewertung (z. B. + # reiner SZ-Lauf): Normalbehandlung der SZ-Beiträge. + return False + if (version.contract_date_start and version.contract_date_end + and (version.contract_date_end - version.contract_date_start) + .days < 29): + vergleich = roh + else: + tage = self._l10n_at_lohnsteuertage() + vergleich = roh * 30.0 / max(tage, 1) if tage < 30 else roh + gfg = float(self.env['hr.rule.parameter'] + ._get_parameter_from_code( + 'at_sv_gfg_monatlich', self.date_to, + raise_if_not_found=False) or 0.0) + return vergleich <= gfg + + # ------------------------------------------------------------------ + # Beitragsfreie Bezüge § 49 Abs 3 ASVG (Jahres-Kumulative je DN) + # ------------------------------------------------------------------ + def _l10n_at_sv49_ytd(self, kategorie, menge=False): + """Jahresverbrauch einer §-49-Kategorie aus früheren validierten + Läufen (refundbewusst über _l10n_at_ytd_slaeufe).""" + self.ensure_one() + total = 0.0 + for slip in self._l10n_at_ytd_slaeufe(): + linien = slip.line_ids.filtered( + lambda l: l.salary_rule_id.l10n_at_sv49_frei == kategorie) + if menge: + total += sum(linien.mapped('quantity')) + else: + total += sum(linien.mapped('total')) + return total + + def _l10n_at_sv49_freie_laufen(self, result_rules=None): + """Beitragsfreie Anteile laufender Bezüge dieses LZP: je Kategorie + Tages-/Jahresgrenzen (at_sv49_grenzen) minus Jahresverbrauch; + Überschreiten = beitragspflichtig (Umwidmung, § 49 Abs 3).""" + self.ensure_one() + grenzen = self.env['hr.rule.parameter']._get_parameter_from_code( + 'at_sv49_grenzen', self.date_to, raise_if_not_found=False) + if not grenzen: + return 0.0 + frei_gesamt = 0.0 + for rule in self.struct_id.rule_ids: + kategorie = rule.l10n_at_sv49_frei + if (not kategorie or kategorie == 'keine' + or rule.l10n_at_bemessung != 'laufend'): + continue + werte = None + if result_rules is not None: + if rule.code in result_rules: + werte = result_rules[rule.code] + else: + linie = self.line_ids.filtered(lambda l: l.code == rule.code) + if linie: + werte = {'total': sum(linie.mapped('total')), + 'quantity': sum(linie.mapped('quantity'))} + if not werte: + continue + total = werte['total'] + menge = werte.get('quantity', 0.0) + if kategorie == 'mahlzeit': + frei = min( + total, float(grenzen.get('mahlzeit_tag')) * menge) + elif kategorie == 'homeoffice': + rest_tage = max( + float(grenzen.get('homeoffice_tage')) + - self._l10n_at_sv49_ytd('homeoffice', menge=True), 0.0) + frei = min( + total, + float(grenzen.get('homeoffice_tag')) + * min(menge, rest_tage)) + else: + pool = {'rabatt': 'rabatt_jahr', + 'betriebsveranstaltung': 'betriebsveranstaltung_jahr', + 'jubilaeum': 'jubilaeum_jahr', + 'zukunftssicherung': 'zukunftssicherung_jahr', + 'kinderbetreuung': 'kinderbetreuung_jahr'}.get( + kategorie) + rest = max( + float(grenzen.get(pool)) + - self._l10n_at_sv49_ytd(kategorie), 0.0) + frei = min(total, rest) + frei_gesamt += max(frei, 0.0) + return frei_gesamt + + # ------------------------------------------------------------------ + # Laufende Beitragsgrundlage — je Beitragsmonat (Anspruchsprinzip) + # ------------------------------------------------------------------ + def _l10n_at_sv_basis_roh(self, result_rules=None): + """Laufender Arbeitslohn minus beitragsfreie §-49-Anteile + (vor HBG-Cap; Geringfügigkeits-/Staffel-Basis).""" + self.ensure_one() + roh = self._l10n_at_brutto('laufend', result_rules) \ + - self._l10n_at_sv49_freie_laufen(result_rules) + return max(roh, 0.0) + + def _l10n_at_sv_basis_laufen_monate(self, result_rules=None): + """[(Beitragsmonat, HBG-gecappte Basis), …]: laufender Arbeitslohn + (§-49-bereinigt), gruppiert nach Regel-Beitragsmonat; HBG je + Beitragsmonat historisch aufgelöst (at_sv_hbg_monatlich).""" + self.ensure_one() + gruppen = {} + for rule in self.struct_id.rule_ids: + if rule.l10n_at_bemessung != 'laufend': + continue + werte = None + if result_rules is not None: + if rule.code in result_rules: + werte = result_rules[rule.code] + else: + linie = self.line_ids.filtered(lambda l: l.code == rule.code) + if linie: + werte = {'total': sum(linie.mapped('total'))} + if not werte: + continue + monat = (rule.l10n_at_beitragsmonat or self.date_from) \ + .replace(day=1) + gruppen[monat] = gruppen.get(monat, 0.0) + werte['total'] + standard = self.date_from.replace(day=1) + if standard in gruppen: + gruppen[standard] -= self._l10n_at_sv49_freie_laufen( + result_rules) + ergebnis = [] + for monat, basis in sorted(gruppen.items()): + hbg = float(self._l10n_at_param('at_sv_hbg_monatlich', monat)) + ergebnis.append((monat, min(max(basis, 0.0), hbg))) + return ergebnis + + def _l10n_at_svdn_basis_laufen(self, result_rules=None): + """SV-Beitragsgrundlage laufend: Summe der je Beitragsmonat + HBG-gecappten, §-49-bereinigten Basen (at_sv_*).""" + self.ensure_one() + return sum(basis for _monat, basis + in self._l10n_at_sv_basis_laufen_monate(result_rules)) + + # ------------------------------------------------------------------ + # Effektive Beitragssätze (Alter/Stichtäge, Wien-WF, ALV-DN-Staffel) + # ------------------------------------------------------------------ + def _l10n_at_sv_satz(self, seite, komponente, monat=None, + staffel_basis=None): + """Effektiver Satz (vH) je Seite ('dn'/'dg') und Komponente + (kv/pv/av/uv/ak/wf/iesg) zum Beitragsmonat, unter Berücksich- + tigung der Stichtags-Regeln am Vertrag und der ALV-DN-Staffel + (§ 2a AMPFG: reduziert nur den DN-Anteil; Grenzen nicht zu + aliquotieren; gilt auch für SZ-Beiträge).""" + self.ensure_one() + version = self.version_id + monat = monat or self.date_from + if komponente == 'wf': + vektor = self.env['hr.rule.parameter']._get_parameter_from_code( + 'at_sv_wf_vektor', monat, raise_if_not_found=False) + satz = None + if vektor: + wien = (self.company_id.l10n_at_wf_wien + or self.company_id.l10n_at_bundesland == 'wien') + eintrag = vektor.get('wien' if wien else 'default') \ + or vektor.get('default') + if eintrag: + satz = float(eintrag.get(seite, 0.0)) + if satz is None: + satz = float(self.env['hr.rule.parameter'] + ._get_parameter_from_code( + 'at_sv_wf_%s' % seite, monat, + raise_if_not_found=False) or 0.0) + return satz + satz = float(self._l10n_at_param( + 'at_sv_%s_%s' % (komponente, seite), monat)) + entfall_alv = (version.l10n_at_alv_entfall_ab + and monat >= version.l10n_at_alv_entfall_ab) + if komponente in ('av', 'iesg') and entfall_alv: + # AV DN+DG entfallen (AlVG § 1 Abs 2 lit e) — der IESG- + # Zuschlag folgt dem AV-Beitrag (§ 12 Abs 1 Z 4 iVm + # § 12 Abs 2 IESG). + return 0.0 + if komponente == 'uv': + if version.l10n_at_lehrling \ + or (version.l10n_at_uv_entfall_ab + and monat >= version.l10n_at_uv_entfall_ab): + return 0.0 + if komponente == 'ak' and version.l10n_at_lehrling: + return 0.0 + if komponente == 'iesg' and version.l10n_at_lehrling: + # § 12 Abs 2 IESG: für Lehrlinge gesamte Lehrzeit kein Zuschlag + return 0.0 + if komponente == 'pv' and ( + version.l10n_at_pv_bonusphase_ab + and monat >= version.l10n_at_pv_bonusphase_ab): + faktor = float(self.env['hr.rule.parameter'] + ._get_parameter_from_code( + 'at_pv_bonus_faktor', monat, + raise_if_not_found=False) or 0.5) + satz *= faktor + if komponente == 'av' and seite == 'dn': + staffel = self.env['hr.rule.parameter']._get_parameter_from_code( + 'at_sv_av_staffel_ampfg', monat, raise_if_not_found=False) + if staffel and staffel.get('tiers'): + basis = staffel_basis if staffel_basis is not None \ + else self._l10n_at_sv_basis_roh() + if basis > 0: + for _von, bis, dn_satz in staffel['tiers']: + if basis <= float(bis): + return float(dn_satz) + # über der obersten Stufe: voller DN-Satz + return float(staffel.get('dn_voll', satz)) + return satz + + # ------------------------------------------------------------------ + # Beiträge laufend (DN- wie DG-seitig; geringfügig = 0) + # ------------------------------------------------------------------ + def _l10n_at_sv_beitrag(self, seite, komponente, result_rules=None): + """Beitragssumme je Beitragsmonat: Basis × historischen Satz; + geringfügige Beschäftigung → 0 (stattdessen §-53a-UV, s. u.).""" + self.ensure_one() + if self._l10n_at_geringfuegig(result_rules): + return 0.0 + total = 0.0 + for monat, basis in self._l10n_at_sv_basis_laufen_monate( + result_rules): + satz = self._l10n_at_sv_satz(seite, komponente, monat, basis) + total += basis * satz / 100.0 + return total + + def _l10n_at_svdn(self, komponente, result_rules=None): + """DN-SV-Komponente laufend: kv/pv/av/ak/wf — AK (§ 61 AKG) und + WF nur laufend, HBGL-Cap; Wien über WF-Satzvektor.""" + self.ensure_one() + return self._l10n_at_sv_beitrag('dn', komponente, result_rules) + + def _l10n_at_svdg(self, komponente, result_rules=None): + """DG-SV-Komponente laufend: kv/pv/av/uv/wf/iesg (AK = 0); + ÖGK-Tarif „Normalbetriebe" 2026: DG-Gesamt 20,98 % (Wien 21,23).""" + self.ensure_one() + return self._l10n_at_sv_beitrag('dg', komponente, result_rules) + + def _l10n_at_svdg_gesamt(self, result_rules=None): + self.ensure_one() + return sum(self._l10n_at_svdg(k, result_rules) + for k in ('kv', 'pv', 'av', 'uv', 'wf', 'iesg')) + + # ------------------------------------------------------------------ + # Beiträge auf sonstigen Bezügen (§ 54 ASVG; 60faches HBG je Jahr) + # ------------------------------------------------------------------ + def _l10n_at_svdg_sz(self, komponente, result_rules=None): + """DG-Beitrag auf sonstigen Bezügen: KV/PV/AV/UV und IESG-Zuschlag + (folgt dem AV-Beitrag) — ohne AK/WF (§ 62 Z 3/Z 5; lb-son-03: + DN 17,07 %; DG 20,48 % inkl. UV 1,10 + IESG 0,10).""" + self.ensure_one() + if self._l10n_at_geringfuegig(result_rules): + return 0.0 + basis = self._l10n_at_svdn_basis_sz(result_rules) + satz = self._l10n_at_sv_satz( + 'dg', komponente, staffel_basis=self._l10n_at_sv_basis_roh( + result_rules)) + return basis * satz / 100.0 + + def _l10n_at_svdg_sz_gesamt(self, result_rules=None): + self.ensure_one() + return sum(self._l10n_at_svdg_sz(k, result_rules) + for k in ('kv', 'pv', 'av', 'uv', 'iesg')) + + # ------------------------------------------------------------------ + # § 53a ASVG — Geringfügig Beschäftigte + # ------------------------------------------------------------------ + def _l10n_at_svdg_uv_geringfuegig(self, result_rules=None): + """DG-UV geringfügig Beschäftigter: 1,1 % der allgemeinen + Beitragsgrundlage (§ 53a Abs 1 ASVG) als Monatspauschale + (at_sv_geringfuegig['beitrag_uv']; aBG 2026 = 83,49 ÷ 14,12 % = + 591,29 → 6,50 € — abgeleitet, s. Rechenfälle). Entfällt bei + UV-Stichtag (60. LJ) bzw. Lehrlingen (§ 51 Abs 6 ASVG).""" + self.ensure_one() + if not self._l10n_at_geringfuegig(result_rules): + return 0.0 + if self._l10n_at_sv_satz('dg', 'uv') <= 0.0: + return 0.0 + params = self.env['hr.rule.parameter']._get_parameter_from_code( + 'at_sv_geringfuegig', self.date_to, raise_if_not_found=False) \ + or {} + return float(params.get('beitrag_uv', 0.0)) \ No newline at end of file diff --git a/addons/l10n_at_hr_payroll/tests/__init__.py b/addons/l10n_at_hr_payroll/tests/__init__.py index 5ced461..3b5c5c4 100644 --- a/addons/l10n_at_hr_payroll/tests/__init__.py +++ b/addons/l10n_at_hr_payroll/tests/__init__.py @@ -1,3 +1,4 @@ # Part of the odoo-at-payroll project. License: LGPL-3. from . import test_tasy_import -from . import test_lohnsteuer \ No newline at end of file +from . import test_lohnsteuer +from . import test_sozialversicherung \ No newline at end of file diff --git a/addons/l10n_at_hr_payroll/tests/test_lohnsteuer.py b/addons/l10n_at_hr_payroll/tests/test_lohnsteuer.py index 26b63fd..cfaf55d 100644 --- a/addons/l10n_at_hr_payroll/tests/test_lohnsteuer.py +++ b/addons/l10n_at_hr_payroll/tests/test_lohnsteuer.py @@ -156,13 +156,14 @@ class LohnsteuerTest(TransactionCase): # DN-SV / Bemessung (user-Formel) # ------------------------------------------------------------------ def test_05_svdn_und_bemessung(self): - """SV-DN 18,07 % (KV/PV/AV/AK/WF) auf laufender Basis; Bemessung - = Arbeitslohn − SV-DN (user-Formel).""" + """SV-DN auf 2.059,41 €: § 2a AMPFG mindert den DN-AV auf 0 % + (BG ≤ 2.225 €) → DN-Gesamt 15,12 % statt 18,07 % (GP4-Staffel; + DG-AV bleibt 2,95 %); Bemessung = Arbeitslohn − SV-DN.""" employee = self._employee() self._regel('LFD', 2059.41) slip = self._slip(employee, 1) svdn = slip._l10n_at_svdn_gesamt() - self.assertAlmostEqual(svdn, 2059.41 * 0.1807, places=2) + self.assertAlmostEqual(svdn, 2059.41 * 0.1512, places=2) bem = slip._l10n_at_bemessung_laufen() self.assertAlmostEqual(bem, 2059.41 - svdn, places=2) @@ -221,7 +222,7 @@ class LohnsteuerTest(TransactionCase): self._regel('SZ', 2059.41, bemessung='sonstig') slip_nov = self._slip(employee, 11) sz_bem = slip_nov._l10n_at_sz_bemessung() - sv_sz = 2059.41 * 0.1707 # KV+PV+AV auf SZ-Basis + sv_sz = 2059.41 * 0.1412 # KV+PV auf SZ-Basis; AV-DN 0 (§ 2a) self.assertAlmostEqual(sz_bem, 2059.41 - sv_sz, places=2) # Sechstel = 2 × 2059,41 × 60 ÷ 60 = 4.118,82 > Freigrenze self.assertAlmostEqual( @@ -257,13 +258,13 @@ class LohnsteuerTest(TransactionCase): python='result = payslip._l10n_at_lst_sonstige(result_rules)') slip_nov = self._slip(employee, 11, confirm=True) sz_nov = self._total(slip_nov, 'LST_SZ') - # SV-SZ = 2.059,41 × 17,07 % = 351,54 (KV+PV+AV auf SZ-Basis) + # SV-SZ = 2.059,41 × 14,12 % (KV+PV; AV-DN 0 nach § 2a AMPFG) self.assertAlmostEqual( - sz_nov, (2059.41 * (1 - 0.1707) - 620.0) * 0.06, places=1) + sz_nov, (2059.41 * (1 - 0.1412) - 620.0) * 0.06, places=1) slip_dez = self._slip(employee, 12) sz_dez = self._total(slip_dez, 'LST_SZ') # Jahres-SZ-Bemessung = 2 × (2.059,41 − SV-SZ) - jahres_bem = 2 * 2059.41 * (1 - 0.1707) + jahres_bem = 2 * 2059.41 * (1 - 0.1412) soll_jahr = (jahres_bem - 620.0) * 0.06 self.assertAlmostEqual(sz_dez, soll_jahr - sz_nov, places=1) @@ -276,8 +277,8 @@ class LohnsteuerTest(TransactionCase): slip = self._slip(employee, 1) # Sechstel = 2.000 × 60/30 = 4.000 > 2.615 → Pauschalierung self.assertAlmostEqual(slip._l10n_at_sechstel(), 4000.0, places=2) - sz_bem = slip._l10n_at_sz_bemessung() # 6.000 − SV-SZ (1.024,20) - self.assertAlmostEqual(sz_bem, 6000.0 - 6000.0 * 0.1707, places=2) + sz_bem = slip._l10n_at_sz_bemessung() # 6.000 − SV-SZ (14,12 %) + self.assertAlmostEqual(sz_bem, 6000.0 - 6000.0 * 0.1412, places=2) self.assertAlmostEqual( slip._l10n_at_sz_tarifisch_aktuell(), sz_bem - 4000.0, places=2) self.assertAlmostEqual( diff --git a/addons/l10n_at_hr_payroll/tests/test_sozialversicherung.py b/addons/l10n_at_hr_payroll/tests/test_sozialversicherung.py new file mode 100644 index 0000000..c95cf90 --- /dev/null +++ b/addons/l10n_at_hr_payroll/tests/test_sozialversicherung.py @@ -0,0 +1,400 @@ +# Part of the odoo-at-payroll project. License: LGPL-3. +"""GP4 SV/DB-Kern: DG-Beiträge (ÖGK 2026), SZ-Beiträge (§ 54 ASVG), +Geringfügigkeit (§ 5 Abs 2 iVm § 810 Abs 3 ASVG) + §-53a-UV, +beitragsfreie Bezüge § 49 Abs 3 mit Jahres-Kumulativen, Altersstichtage +(AlVG § 1 Abs 2 lit e, ASVG § 51 Abs 6/7, IESG § 12), ALV-DN-Staffel +(§ 2a AMPFG), Beitragsmonats-Allokation und DAG (§ 1 DAG). Rechenfälle +s. addons/l10n_at_hr_payroll_private/docs/rechenfaelle/ +sozialversicherung.md und RECHTSQUELLEN-Privat Abschnitt 4.""" +from datetime import date, timedelta + +from odoo.tests import TransactionCase, tagged + + +@tagged('post_install', '-at_install') +class SozialversicherungTest(TransactionCase): + + def setUp(self): + super().setUp() + self.category = self.env['hr.salary.rule.category'].create({ + 'name': 'Test AT SV', + 'code': 'TEST_AT_SV', + }) + self.structure_type = self.env['hr.payroll.structure.type'].create({ + 'name': 'Test AT Kern Type', + }) + self.structure = self.env['hr.payroll.structure'].create({ + 'name': 'Test AT SV', + 'code': 'TEST_AT_SV', + 'type_id': self.structure_type.id, + }) + + def _struktur(self, code): + """Eigene Structure je Testfall-Stufe (Regeln kumulieren auf + geteilten Structures — Muster test_lohnsteuer.py).""" + return self.env['hr.payroll.structure'].create({ + 'name': 'Test AT %s' % code, + 'code': 'TEST_AT_%s' % code, + 'type_id': self.structure_type.id, + }) + + def _regel(self, code, betrag, bemessung='laufend', sv49='keine', + beitragsmonat=None, quantity='1.0', struct=None): + return self.env['hr.salary.rule'].create({ + 'name': code, + 'code': code, + 'struct_id': (struct or self.structure).id, + 'category_id': self.category.id, + 'condition_select': 'none', + 'l10n_at_bemessung': bemessung, + 'l10n_at_sv49_frei': sv49, + 'l10n_at_beitragsmonat': beitragsmonat, + 'amount_select': 'fix', + 'amount_fix': betrag, + 'quantity': quantity, + }) + + def _employee(self, version_vals=None): + employee = self.env['hr.employee'].create({'name': 'Test SV'}) + 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)}) + 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-SV %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 + + # ------------------------------------------------------------------ + # DG-Beiträge (ÖGK „Normalbetriebe" 2026: DG 20,98 % / Wien 21,23) + # ------------------------------------------------------------------ + def test_01_svdg_gesamt(self): + """DG-Summe 20,98 % = KV 3,78 + PV 12,55 + AV 2,95 + UV 1,10 + + WF 0,50 + IESG 0,10 auf laufender, HBG-gecappter Basis.""" + employee = self._employee() + self._regel('LFD', 2059.41) + slip = self._slip(employee, 1) + self.assertAlmostEqual( + slip._l10n_at_svdg('kv'), 2059.41 * 0.0378, places=2) + self.assertAlmostEqual( + slip._l10n_at_svdg('pv'), 2059.41 * 0.1255, places=2) + self.assertAlmostEqual( + slip._l10n_at_svdg('av'), 2059.41 * 0.0295, places=2) + self.assertAlmostEqual( + slip._l10n_at_svdg('uv'), 2059.41 * 0.011, places=2) + self.assertAlmostEqual( + slip._l10n_at_svdg('wf'), 2059.41 * 0.005, places=2) + self.assertAlmostEqual( + slip._l10n_at_svdg('iesg'), 2059.41 * 0.001, places=2) + self.assertAlmostEqual( + slip._l10n_at_svdg_gesamt(), 2059.41 * 0.2098, places=2) + + def test_02_svdg_wien(self): + """WF-Wien 0,75 % DG ab 1.1.2026 (Satzvektor); 2025 nur 0,5 %.""" + employee = self._employee() + self._regel('LFD', 2059.41) + slip = self._slip(employee, 1) + slip.company_id.write({'l10n_at_wf_wien': True}) + self.assertAlmostEqual( + slip._l10n_at_svdg('wf'), 2059.41 * 0.0075, places=2) + self.assertAlmostEqual( + slip._l10n_at_svdg_gesamt(), 2059.41 * 0.2123, places=2) + # 2025: Vektor-Zeile ohne 'wien'-Eintrag → Bundesstandard 0,5 % + slip_2025 = self._slip(employee, 3, jahr=2025) + self.assertAlmostEqual( + slip_2025._l10n_at_svdg('wf'), 2059.41 * 0.005, places=2) + # HBG 2025 = 30 × 215 € = 6.450 € (KB A8, VO-Reihe § 108 Abs 3) + self.assertAlmostEqual( + slip_2025._l10n_at_svdn_basis_laufen(), 2059.41, places=2) + # Bundesland-Schlüssel wirkt identisch zum Company-Flag + slip.company_id.write( + {'l10n_at_wf_wien': False, 'l10n_at_bundesland': 'wien'}) + self.assertAlmostEqual( + slip._l10n_at_svdg('wf'), 2059.41 * 0.0075, places=2) + + def test_03_svdg_sz(self): + """DG auf sonstigen Bezügen: KV/PV/AV/UV/IESG = 20,48 % + (§ 54 ASVG; HBG monatlich + SZ-Jahreshöchstbetrag 13.860).""" + employee = self._employee() + self._regel('LFD', 2059.41) + slip_okt = self._slip(employee, 10, confirm=True) + self._regel('SZ', 2059.41, bemessung='sonstig') + slip_nov = self._slip(employee, 11) + for komp, satz in (('kv', 0.0378), ('pv', 0.1255), + ('av', 0.0295), ('uv', 0.011), + ('iesg', 0.001)): + self.assertAlmostEqual( + slip_nov._l10n_at_svdg_sz(komp), 2059.41 * satz, places=2) + self.assertAlmostEqual( + slip_nov._l10n_at_svdg_sz_gesamt(), 2059.41 * 0.2048, places=2) + # Dezember: Jahresrest 13.860 − 2.059,41 lässt die zweite SZ zu + slip_dez = self._slip(employee, 12) + self.assertAlmostEqual( + slip_dez._l10n_at_svdn_basis_sz(), 2059.41, places=2) + # Deckel: SZ 20.000 im Januar 2027 (Jahresrest voll) → HBG 6.930 + self._regel('SZ2', 20000.0, bemessung='sonstig') + slip_2027 = self._slip(employee, 1, jahr=2027) + self.assertAlmostEqual( + slip_2027._l10n_at_svdn_basis_sz(), + min(20000.0 + 2059.41, 6930.0, 13860.0), places=2) + + # ------------------------------------------------------------------ + # Geringfügigkeit (§ 5 Abs 2 ASVG; Aufwertung 2026 ausgesetzt) + § 53a + # ------------------------------------------------------------------ + def test_04_geringfuegig_uv(self): + """Laufendes Entgelt ≤ GFG 551,10 €: keine SVDN/SVDG; stattdessen + §-53a-Abs-1-UV 6,50 €/Monat (1,1 % der allgemeinen BG).""" + employee = self._employee() + self._regel('LFD', 500.0) + slip = self._slip(employee, 1) + self.assertTrue(slip._l10n_at_geringfuegig()) + self.assertAlmostEqual(slip._l10n_at_svdn_gesamt(), 0.0, places=2) + self.assertAlmostEqual(slip._l10n_at_svdg_gesamt(), 0.0, places=2) + self.assertAlmostEqual( + slip._l10n_at_svdg_uv_geringfuegig(), 6.50, places=2) + self.assertTrue(slip.l10n_at_geringfuegig) # stored (DAG-Suche) + # Über der Grenze: Vollversicherung, kein §-53a-UV + employee_b = self._employee() + struktur = self._struktur('GFG2') + self._regel('LFD', 600.0, struct=struktur) + slip_b = self._slip(employee_b, 1, struct=struktur) + self.assertFalse(slip_b._l10n_at_geringfuegig()) + self.assertAlmostEqual( + slip_b._l10n_at_svdg_uv_geringfuegig(), 0.0, places=2) + self.assertAlmostEqual( + slip_b._l10n_at_svdg_gesamt(), 600.0 * 0.2098, places=2) + + def test_05_geringfuegig_hochrechnung(self): + """Untermonatiger Beginn: Hochrechnung auf den Monat (lb-bes-06); + Verhältnis < 1 Naturalmonat: Gesamtentgelt direkt gegen die GFG.""" + employee = self._employee( + {'contract_date_start': date(2026, 1, 16)}) + self._regel('LFD', 400.0) + slip = self._slip(employee, 1) + # 400 × 30/16 = 750 > 551,10 → voll versichert; DN-AV 0 (§ 2a: + # 400 ≤ 2.225) → DN-Gesamt 15,12 % + self.assertFalse(slip._l10n_at_geringfuegig()) + self.assertAlmostEqual( + slip._l10n_at_svdn_gesamt(), 400.0 * 0.1512, places=2) + # 250 × 30/16 = 468,75 ≤ 551,10 → geringfügig + struktur = self._struktur('HGR') + employee_b = self._employee( + {'contract_date_start': date(2026, 1, 16)}) + self._regel('LFD', 250.0, struct=struktur) + slip_b = self._slip(employee_b, 1, struct=struktur) + self.assertTrue(slip_b._l10n_at_geringfuegig()) + # auf unter einen Naturalmonat befristet: direkt gegen die GFG + employee_c = self._employee({ + 'contract_date_start': date(2026, 1, 16), + 'contract_date_end': date(2026, 1, 25)}) + struktur_c = self._struktur('KURZ') + self._regel('LFD', 500.0, struct=struktur_c) + slip_c = self._slip(employee_c, 1, struct=struktur_c) + self.assertTrue(slip_c._l10n_at_geringfuegig()) + + def test_06_lehrling(self): + """Lehrling: keine AK-Umlage (§ 61 AKG), kein UV (§ 51 Abs 6 + ASVG); nie geringfügig (§ 5 Abs 3 ASVG).""" + employee = self._employee({'l10n_at_lehrling': True}) + self._regel('LFD', 300.0) + slip = self._slip(employee, 1) + self.assertFalse(slip._l10n_at_geringfuegig()) + self.assertAlmostEqual(slip._l10n_at_svdn('ak'), 0.0, places=2) + self.assertAlmostEqual(slip._l10n_at_svdg('uv'), 0.0, places=2) + self.assertAlmostEqual(slip._l10n_at_svdg_uv_geringfuegig(), 0.0) + # KV/PV/WF regulär; AV 0 (§ 2a), AK 0, IESG 0 (§ 12 Abs 2 IESG): + # DN 14,62 % / DG 19,78 % + self.assertAlmostEqual( + slip._l10n_at_svdn_gesamt(), 300.0 * 0.1462, places=2) + self.assertAlmostEqual( + slip._l10n_at_svdg_gesamt(), 300.0 * 0.1978, places=2) + + # ------------------------------------------------------------------ + # § 49 Abs 3 ASVG — beitragsfreie Bezugsbestandteile (Kumulative) + # ------------------------------------------------------------------ + def test_07_sv49_mahlzeit(self): + """Mahlzeiten-Gutscheine 8 €/Arbeitstag (Z 12): 110 € auf + 10 Arbeitstage → 80 € beitragsfrei, 30 € Umwidmung.""" + employee = self._employee() + self._regel('LFD', 2059.41) + self._regel('MAH', 11.0, sv49='mahlzeit', quantity='10.0') + slip = self._slip(employee, 1) + self.assertAlmostEqual( + slip._l10n_at_sv49_freie_laufen(), 80.0, places=2) + basis = slip._l10n_at_sv_basis_roh() + self.assertAlmostEqual(basis, 2059.41 + 110.0 - 80.0, places=2) + # DN-AV 0 (§ 2a: Basis 2.089,41 ≤ 2.225) → DN-Gesamt 15,12 % + self.assertAlmostEqual( + slip._l10n_at_svdn_gesamt(), basis * 0.1512, places=2) + + def test_08_sv49_jahrespools(self): + """Jahres-Kumulative: Betriebsveranstaltung 365 €/Jahr und + Homeoffice-Pauschale 3 €/Tag max 100 Tage (YTD-Verbrauch).""" + employee = self._employee() + self._regel('LFD', 2059.41) + self._regel('BVER', 300.0, sv49='betriebsveranstaltung') + slip_a = self._slip(employee, 3, confirm=True) + self.assertAlmostEqual( + slip_a._l10n_at_sv49_freie_laufen(), 300.0, places=2) + # April: BVER-Rest 65 €; Homeoffice 70 Tage frei (3 × 70 = 210) + self._regel('HO', 3.0, sv49='homeoffice', quantity='70.0') + slip_b = self._slip(employee, 4, confirm=True) + self.assertAlmostEqual( + slip_b._l10n_at_sv49_freie_laufen(), 65.0 + 210.0, places=2) + # Mai: BVER-Pool verbraucht; Homeoffice-Rest 30 Tage → 90 € + slip_c = self._slip(employee, 5) + self.assertAlmostEqual( + slip_c._l10n_at_sv49_freie_laufen(), 90.0, places=2) + + # ------------------------------------------------------------------ + # Altersabhängige Satzausschaltungen (Stichtags-Regeln am Vertrag) + # ------------------------------------------------------------------ + def test_09_alv_entfall(self): + """§ 1 Abs 2 lit e AlVG: AV DN+DG und IESG entfallen ab + Stichtag (§ 12 Abs 1 Z 4 iVm Abs 2 IESG); Nachzahlungslinien mit + früherem Beitragsmonat zahlen weiter AV (Anspruchsprinzip).""" + employee = self._employee( + {'l10n_at_alv_entfall_ab': date(2026, 1, 1)}) + self._regel('LFD', 2059.41) + slip = self._slip(employee, 1) + self.assertAlmostEqual(slip._l10n_at_svdn('av'), 0.0, places=2) + self.assertAlmostEqual(slip._l10n_at_svdg('av'), 0.0, places=2) + self.assertAlmostEqual(slip._l10n_at_svdg('iesg'), 0.0, places=2) + # DG ohne AV+IESG: 20,98 − 2,95 − 0,10 = 17,93 % + self.assertAlmostEqual( + slip._l10n_at_svdg_gesamt(), 2059.41 * 0.1793, places=2) + # DN ohne AV: 18,07 − 2,95 = 15,12 % + self.assertAlmostEqual( + slip._l10n_at_svdn_gesamt(), 2059.41 * 0.1512, places=2) + # Nachzahlungslinie Beitragsmonat 6/2025: AV greift weiter + self._regel('NACH', 1000.0, beitragsmonat=date(2025, 6, 1)) + slip2 = self._slip(employee, 2) + self.assertAlmostEqual( + slip2._l10n_at_svdg('av'), 1000.0 * 0.0295, places=2) + + def test_10_uv_entfall(self): + """§ 51 Abs 6 ASVG: UV entfällt ab 60. LJ — laufend und SZ.""" + employee = self._employee( + {'l10n_at_uv_entfall_ab': date(2026, 1, 1)}) + self._regel('LFD', 2059.41) + self._regel('SZ', 2059.41, bemessung='sonstig') + slip = self._slip(employee, 1) + self.assertAlmostEqual(slip._l10n_at_svdg('uv'), 0.0, places=2) + self.assertAlmostEqual(slip._l10n_at_svdg_sz('uv'), 0.0, places=2) + self.assertAlmostEqual( + slip._l10n_at_svdg_gesamt(), 2059.41 * 0.1988, places=2) + + def test_11_pv_bonusphase(self): + """§ 51 Abs 7 ASVG: PV je zur Hälfte reduziert (DG und DN).""" + employee = self._employee( + {'l10n_at_pv_bonusphase_ab': date(2026, 1, 1)}) + self._regel('LFD', 2059.41) + slip = self._slip(employee, 1) + self.assertAlmostEqual( + slip._l10n_at_svdn('pv'), 2059.41 * 0.05125, places=2) + self.assertAlmostEqual( + slip._l10n_at_svdg('pv'), 2059.41 * 0.06275, places=2) + + # ------------------------------------------------------------------ + # ALV-DN-Staffel (§ 2a AMPFG — reduziert nur den DN-Anteil) + # ------------------------------------------------------------------ + def test_12_av_staffel(self): + """Bis 2.225 €: DN-AV 0 %; bis 2.427: 1 %; bis 2.630: 2 % — + DG-AV bleibt stets 2,95 % (ÖGK „Mind. AV auf 0/1/2 %").""" + employee_a = self._employee() + struktur_a = self._struktur('ST1') + self._regel('LFD', 2000.0, struct=struktur_a) + slip_a = self._slip(employee_a, 1, struct=struktur_a) + self.assertAlmostEqual(slip_a._l10n_at_svdn('av'), 0.0, places=2) + self.assertAlmostEqual( + slip_a._l10n_at_svdg('av'), 2000.0 * 0.0295, places=2) + employee_b = self._employee() + struktur_b = self._struktur('ST2') + self._regel('LFD', 2500.0, struct=struktur_b) + slip_b = self._slip(employee_b, 1, struct=struktur_b) + self.assertAlmostEqual( + slip_b._l10n_at_svdn('av'), 2500.0 * 0.02, places=2) + self.assertAlmostEqual( + slip_b._l10n_at_svdg('av'), 2500.0 * 0.0295, places=2) + employee_c = self._employee() + struktur_c = self._struktur('ST3') + self._regel('LFD', 3000.0, struct=struktur_c) + slip_c = self._slip(employee_c, 1, struct=struktur_c) + self.assertAlmostEqual( + slip_c._l10n_at_svdn('av'), 3000.0 * 0.0295, places=2) + self.assertAlmostEqual( + slip_c._l10n_at_svdg('av'), 3000.0 * 0.0295, places=2) + + # ------------------------------------------------------------------ + # Beitragsmonats-Allokation (Anspruchsprinzip) + # ------------------------------------------------------------------ + def test_13_beitragsmonat_hbg(self): + """Nachzahlungslinie mit Beitragsmonat 2025: HBG historisch + versioniert (2025: 6.450 € statt 6.930 €), je Gruppe gecapped.""" + employee = self._employee() + self._regel('LFD', 7000.0) + self._regel('NACH', 7000.0, beitragsmonat=date(2025, 6, 1)) + slip = self._slip(employee, 1) + monate = dict(slip._l10n_at_sv_basis_laufen_monate()) + self.assertAlmostEqual( + monate[date(2026, 1, 1)], 6930.0, places=2) + self.assertAlmostEqual( + monate[date(2025, 6, 1)], 6450.0, places=2) + self.assertAlmostEqual( + slip._l10n_at_svdg('kv'), + (6930.0 + 6450.0) * 0.0378, places=2) + + # ------------------------------------------------------------------ + # Dienstgeberabgabe geringfügig Beschäftigter (§ 1 DAG) + # ------------------------------------------------------------------ + def test_14_dag(self): + """DAG 19,4 %: Lohnsumme (BG ohne SZ) aller Geringfügigen > + 826,65 € (= 1,5 × GFG 551,10, TASY MINGRDAGP1) und ≥ 2 Personen.""" + employee_a = self._employee() + struktur_a = self._struktur('DAGA') + self._regel('LFD', 500.0, struct=struktur_a) + self._slip(employee_a, 1, confirm=True, struct=struktur_a) + employee_b = self._employee() + struktur_b = self._struktur('DAGB') + self._regel('LFD', 550.0, struct=struktur_b) + self._slip(employee_b, 1, confirm=True, struct=struktur_b) + company = self.env.company + dag = company._l10n_at_dag(date(2026, 1, 1)) + self.assertAlmostEqual(dag['schwelle'], 826.65, places=2) + self.assertEqual(dag['anzahl'], 2) + self.assertAlmostEqual(dag['lohnsumme'], 1050.0, places=2) + self.assertAlmostEqual(dag['abgabe'], 1050.0 * 0.194, places=2) + # Zwei Geringfügige unter der Schwelle: keine DAG + employee_c = self._employee() + struktur_c = self._struktur('DAGC') + self._regel('LFD', 400.0, struct=struktur_c) + self._slip(employee_c, 2, confirm=True, struct=struktur_c) + employee_d = self._employee() + struktur_d = self._struktur('DAGD') + self._regel('LFD', 400.0, struct=struktur_d) + self._slip(employee_d, 2, confirm=True, struct=struktur_d) + dag_tief = company._l10n_at_dag(date(2026, 2, 1)) + self.assertEqual(dag_tief['anzahl'], 2) + self.assertAlmostEqual(dag_tief['lohnsumme'], 800.0, places=2) + self.assertAlmostEqual(dag_tief['abgabe'], 0.0, places=2) + # Nur ein Geringfügiger: keine DAG + dag_einzel = company._l10n_at_dag(date(2026, 3, 1)) + self.assertAlmostEqual(dag_einzel['abgabe'], 0.0, places=2) \ No newline at end of file