[IMP] l10n_at_hr_payroll_private: add end-of-employment payroll cycle (AP14-A)
Pay the untaken vacation, dismissal compensation, time balances and open advances from one exit run. The vacation replacement splits natively into a running and a special-payment component per § 67 Abs 8 lit d, only the running part extends the mandatory insurance under § 11 Abs 2 ASVG with sequential windows (dismissal compensation first) and daily-value ceilings. Dismissal compensation follows the one-fifth rule of § 67 Abs 8 lit b without raising the annual sixth, stays fully social-insurable by allocation over the notice period and fully subject to payroll taxes. Advances are offset against net pay only (§ 293 Abs 3 EO) and time credits carry the 50 % statutory surcharge without § 68 relief. Verified against the original statute texts in .ris/; § 67 Abs 6 lump-sum payments are deliberately deferred to AP15.
This commit is contained in:
+54
-2
@@ -2,9 +2,61 @@
|
|||||||
|
|
||||||
## Current focus
|
## Current focus
|
||||||
|
|
||||||
**AP13-B Pendlerförderung umgesetzt — 127 Tests grün**
|
**AP14-A Endabrechnungs-Zyklus umgesetzt — 133 Tests grün**
|
||||||
(2026-09-14, uncommittet bis Nutzerfreigabe):
|
(2026-09-14, uncommittet bis Nutzerfreigabe):
|
||||||
|
|
||||||
|
- **Rechtskorrektur gegen Originalwortlaut (.ris/, RIS-Gate erledigt):**
|
||||||
|
§ 67 Abs 6 (freiwillige Abfertigung/Abfindung) = 6 %-Bänder
|
||||||
|
(Z 1 ¼ der letzten-12-Monate-Bezüge, Deckel 9×HBG; Z 2 Dienstzeit-
|
||||||
|
bänder), NICHT Fünftel → nach AP15 verschoben. Fünftelregelung gilt
|
||||||
|
§ 67 Abs 8 lit b (Kündigungs-/Abgangsentschädigung) — diese sind
|
||||||
|
SV-PFLICHTIG (Umlegung auf Kündigungsfrist, § 11 Abs 2 ASVG) und
|
||||||
|
LNK-PFLICHTIG (FLAG § 41 Abs 4 lit b nennt nur Abs 3 und 6).
|
||||||
|
§ 11 Abs 2 ASVG verifiziert: PV-Verlängerung für UEL-Bezug UND
|
||||||
|
Kündigungsentschädigung, bei beiden ZUERST Kündigungsentschädigung,
|
||||||
|
dann UEL (sequentielle Fenster); 6-WT/5-AT-Tage-Umrechnung ist
|
||||||
|
Verwaltungspraxis (lb-url-15), nicht Gesetzeswortlaut. § 67 Abs 8
|
||||||
|
lit d (UEL-Zweikomponenten) wortwörtlich bestätigt. § 19e Abs 2
|
||||||
|
AZG (+50 % auf NAZ-Guthaben) verifiziert.
|
||||||
|
- **Umsetzung (Privat 19.0.12.0.0):** `models/endabrechnung.py` mit
|
||||||
|
Regel-Flags `l10n_at_sv_verlaengerung` (urlaubsersatz/abgang) und
|
||||||
|
`l10n_at_s67_fuenftel`; UEL-Helfer (Divisor 26/22, SZ-Tageswert
|
||||||
|
2×SZ-Basis/12); Upstream-Replicas mit Pin-Kommentar für
|
||||||
|
`_at_lnk_basis` (Abgang VOR der Freibetragsstaffel) und
|
||||||
|
`_l10n_at_sv_basis_laufen_monate` (Verlängerungsanteile mit
|
||||||
|
Tageswert-Deckel HBG×Tage/30, Standardanteile volle Monats-HBG);
|
||||||
|
`_l10n_at_bemessung_laufen` += Fünftel-Tarifteil (4/5, Deckel
|
||||||
|
12.474 = 9×HBG/5) ohne Sechstel-Effekt.
|
||||||
|
- **Regeln:** URL_ERSATZ_LFD (laufend, verlaengerung=urlaubsersatz),
|
||||||
|
URL_ERSATZ_SZ (sonstig), ABGANG (neutral, sv_art=laufend,
|
||||||
|
verlaengerung=abgang, fuenftel), ZEIT_GUTHABEN (laufend ×1,5,
|
||||||
|
KEIN §-68), ZEIT_SCHULDEN (negativ), END_ABRECHNUNG_VORSCHUSS
|
||||||
|
(neutral/keine — Netto-Abzug § 293 Abs 3 EO). Eingaben
|
||||||
|
ATP_UEL_TAGE/ABGANG/ABGANG_FFRIST_TAGE/ZEIT_GUTHABEN/ZEIT_SCHULDEN/
|
||||||
|
ABRECHNUNG_VORSCHUSS; Parameter at_endabrechnung (Divisoren).
|
||||||
|
- **Wizard** `l10n.at.payroll.endabrechnung.wizard` (ACL, Views,
|
||||||
|
Menü seq 42): sammelt UEL-Tage/Abgang/Frist/Zeit/Vorschuss, Vorschau
|
||||||
|
über flüchtigen new()-Payslip, Button legt Endabrechnungs-Lauf im
|
||||||
|
Austrittsmonat mit vorbefüllten Eingaben an. Achtung: hr.version-
|
||||||
|
Strukturfeld heißt `structure_id` (nicht struct_id).
|
||||||
|
- **Fund beim Testlauf:** Eingabetypen-Datei fehlte zunächst in der
|
||||||
|
Manifest-Datenliste → NOT-NULL input_type_id; zwei Testassertionen
|
||||||
|
mussten SV-DN-Differenzen bzw. den KVGEO-Standardanteil im März
|
||||||
|
berücksichtigen.
|
||||||
|
- **Tests** test_endabrechnung_private.py (6): UEL-Zweikomponenten
|
||||||
|
(1.320/220 €, Sechstel +5.280), SV-Tage-Schema (23→31, 12→14,
|
||||||
|
6→21) + Monatsverteilung, Abgang-Fünftel (Sechstel unverändert,
|
||||||
|
LNK +9.000, Umlegung 16/26 Tage), Vorschuss-Netto (Abgaben neutral),
|
||||||
|
Zeitguthaben ±, Wizard E2E. Suite **133/133** in ap13a_test.
|
||||||
|
- **Offen AP14-B/AP15:** Vergleich § 67 Abs 8 lit a + freiwillige
|
||||||
|
Abfertigung § 67 Abs 6 (beides mit Abf-Alt-Günstiger-Maschine,
|
||||||
|
rollierende 12-Monats-Basis), Prämien-Beendigungs-Rollung,
|
||||||
|
Meldewesen-Schnittstellen („Ende des Entgeltanspruchs“ in mBGM,
|
||||||
|
gesonderter Lohnzettel LStR Rz 911a).
|
||||||
|
|
||||||
|
**AP13-B Pendlerförderung umgesetzt — 127 Tests grün**
|
||||||
|
(2026-09-14, Commit **`9ff977e`** nach Nutzerfreigabe):
|
||||||
|
|
||||||
- Neu `models/pendler.py`: Modell `l10n.at.payroll.pendler.erklaerung`
|
- Neu `models/pendler.py`: Modell `l10n.at.payroll.pendler.erklaerung`
|
||||||
(effektiv datiert, L-34-/Pendlerrechner-Nachweis als Attachment,
|
(effektiv datiert, L-34-/Pendlerrechner-Nachweis als Attachment,
|
||||||
pauschale_art klein/gross, fahrten_stufe drittel/zwei_drittel/voll,
|
pauschale_art klein/gross, fahrten_stufe drittel/zwei_drittel/voll,
|
||||||
@@ -39,7 +91,7 @@
|
|||||||
(Odoo 19: quantity/price_unit/total_amount) — Test-Helfer korrigiert.
|
(Odoo 19: quantity/price_unit/total_amount) — Test-Helfer korrigiert.
|
||||||
- Doku docs/rechenfaelle/pendler.md (RF-PEN-1…5 + offene Punkte: erhöhter
|
- Doku docs/rechenfaelle/pendler.md (RF-PEN-1…5 + offene Punkte: erhöhter
|
||||||
VAB 853 €/Zuschlag 804 € als Quer-LSt-Rest, L-34-1-Monats-Meldeprozess).
|
VAB 853 €/Zuschlag 804 € als Quer-LSt-Rest, L-34-1-Monats-Meldeprozess).
|
||||||
- Version **19.0.11.0.0**. Commit ausstehend (Nutzerfreigabe).
|
- Version **19.0.11.0.0**.
|
||||||
|
|
||||||
**AP13-A Reisekosten Inland committed — 127-er Suite grün**
|
**AP13-A Reisekosten Inland committed — 127-er Suite grün**
|
||||||
(2026-09-14, Commit **`3f307a0`**):
|
(2026-09-14, Commit **`3f307a0`**):
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# Part of the odoo-at-payroll project. License: LGPL-3.
|
# Part of the odoo-at-payroll project. License: LGPL-3.
|
||||||
{
|
{
|
||||||
'name': 'Österreich - Privatwirtschaft Personalverrechnung (General-AT)',
|
'name': 'Österreich - Privatwirtschaft Personalverrechnung (General-AT)',
|
||||||
'version': '19.0.11.0.0',
|
'version': '19.0.12.0.0',
|
||||||
'author': 'Florian Egger',
|
'author': 'Florian Egger',
|
||||||
'website': 'https://gem360.at',
|
'website': 'https://gem360.at',
|
||||||
'category': 'Human Resources/Payroll',
|
'category': 'Human Resources/Payroll',
|
||||||
@@ -104,6 +104,7 @@ gem360-Brücke).
|
|||||||
'data/work_entry_types_private.xml',
|
'data/work_entry_types_private.xml',
|
||||||
'data/absence_private.xml',
|
'data/absence_private.xml',
|
||||||
'data/input_types_private.xml',
|
'data/input_types_private.xml',
|
||||||
|
'data/input_types_endabrechnung_private.xml',
|
||||||
'data/salary_rules_private.xml',
|
'data/salary_rules_private.xml',
|
||||||
'data/salary_rules_lst_private.xml',
|
'data/salary_rules_lst_private.xml',
|
||||||
'data/salary_rules_efzg_private.xml',
|
'data/salary_rules_efzg_private.xml',
|
||||||
@@ -113,6 +114,7 @@ gem360-Brücke).
|
|||||||
'data/salary_rules_sb_private.xml',
|
'data/salary_rules_sb_private.xml',
|
||||||
'data/salary_rules_sb2_private.xml',
|
'data/salary_rules_sb2_private.xml',
|
||||||
'data/salary_rules_reisekosten_private.xml',
|
'data/salary_rules_reisekosten_private.xml',
|
||||||
|
'data/salary_rules_endabrechnung_private.xml',
|
||||||
'data/salary_rules_sv_private.xml',
|
'data/salary_rules_sv_private.xml',
|
||||||
'data/salary_rules_bvg_private.xml',
|
'data/salary_rules_bvg_private.xml',
|
||||||
'data/salary_rules_bvg_private.xml',
|
'data/salary_rules_bvg_private.xml',
|
||||||
@@ -126,6 +128,7 @@ gem360-Brücke).
|
|||||||
'data/rule_parameters_sb2_private.xml',
|
'data/rule_parameters_sb2_private.xml',
|
||||||
'data/rule_parameters_reisekosten_private.xml',
|
'data/rule_parameters_reisekosten_private.xml',
|
||||||
'data/rule_parameters_pendler_private.xml',
|
'data/rule_parameters_pendler_private.xml',
|
||||||
|
'data/rule_parameters_endabrechnung_private.xml',
|
||||||
'data/bv_kasse_seed.xml',
|
'data/bv_kasse_seed.xml',
|
||||||
'views/kv_views.xml',
|
'views/kv_views.xml',
|
||||||
'views/bv_views.xml',
|
'views/bv_views.xml',
|
||||||
@@ -137,6 +140,7 @@ gem360-Brücke).
|
|||||||
'views/sachbezuege_views.xml',
|
'views/sachbezuege_views.xml',
|
||||||
'views/reisekosten_views.xml',
|
'views/reisekosten_views.xml',
|
||||||
'views/pendler_views.xml',
|
'views/pendler_views.xml',
|
||||||
|
'views/endabrechnung_views.xml',
|
||||||
'report/gp8_reports.xml',
|
'report/gp8_reports.xml',
|
||||||
'report/buchungsbeleg_report.xml',
|
'report/buchungsbeleg_report.xml',
|
||||||
'report/bv_report.xml',
|
'report/bv_report.xml',
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<!-- AP14-A: Endabrechnungs-Eingaben. Mengen-Eingaben (Tage/Stunden)
|
||||||
|
tragen die Menge im amount-Feld (Muster ATP_SB_MAHLZEIT); die
|
||||||
|
Regeln leiten daraus die Beträge ab. -->
|
||||||
|
<record id="input_type_atp_uel_tage" model="hr.payslip.input.type">
|
||||||
|
<field name="name">Urlaubsersatzleistung — offene Urlaubstage</field>
|
||||||
|
<field name="code">ATP_UEL_TAGE</field>
|
||||||
|
<field name="is_quantity" eval="True"/>
|
||||||
|
</record>
|
||||||
|
<record id="input_type_atp_abgang" model="hr.payslip.input.type">
|
||||||
|
<field name="name">Abgangsentschädigung (Brutto)</field>
|
||||||
|
<field name="code">ATP_ABGANG</field>
|
||||||
|
</record>
|
||||||
|
<record id="input_type_atp_abgang_ffrist_tage" model="hr.payslip.input.type">
|
||||||
|
<field name="name">Abgangsentschädigung — Kündigungsfrist (Kalendertage)</field>
|
||||||
|
<field name="code">ATP_ABGANG_FFRIST_TAGE</field>
|
||||||
|
<field name="is_quantity" eval="True"/>
|
||||||
|
</record>
|
||||||
|
<record id="input_type_atp_zeit_guthaben" model="hr.payslip.input.type">
|
||||||
|
<field name="name">Zeitguthaben Normalarbeitszeit (Stunden)</field>
|
||||||
|
<field name="code">ATP_ZEIT_GUTHABEN</field>
|
||||||
|
<field name="is_quantity" eval="True"/>
|
||||||
|
</record>
|
||||||
|
<record id="input_type_atp_zeit_schulden" model="hr.payslip.input.type">
|
||||||
|
<field name="name">Zeitschulden (Stunden)</field>
|
||||||
|
<field name="code">ATP_ZEIT_SCHULDEN</field>
|
||||||
|
<field name="is_quantity" eval="True"/>
|
||||||
|
</record>
|
||||||
|
<record id="input_type_atp_abrechnung_vorschuss" model="hr.payslip.input.type">
|
||||||
|
<field name="name">Aufrechnung offener Vorschuss (Netto)</field>
|
||||||
|
<field name="code">ATP_ABRECHNUNG_VORSCHUSS</field>
|
||||||
|
</record>
|
||||||
|
</odoo>
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<!-- AP14-A: Endabrechnungs-Parameter (lb-url-16 Tagewert-Divisoren;
|
||||||
|
§-10-Aliquotierung läuft über die Urlaubstage-Eingabe, der
|
||||||
|
Fünftel-Deckel dynamisch aus 9 × at_sv_hbg_monatlich / 5). -->
|
||||||
|
<record id="hr_rule_parameter_at_endabrechnung" model="hr.rule.parameter">
|
||||||
|
<field name="name">Endabrechnung — UEL-Tagewert-Divisoren</field>
|
||||||
|
<field name="code">at_endabrechnung</field>
|
||||||
|
<field name="country_id" ref="base.at"/>
|
||||||
|
</record>
|
||||||
|
<record id="hr_rule_parameter_value_at_endabrechnung"
|
||||||
|
model="hr.rule.parameter.value">
|
||||||
|
<field name="rule_parameter_id" ref="hr_rule_parameter_at_endabrechnung"/>
|
||||||
|
<field name="date_from">2026-01-01</field>
|
||||||
|
<field name="parameter_value">{'divisor_werktage': 26.0, 'divisor_arbeitstage': 22.0}</field>
|
||||||
|
</record>
|
||||||
|
</odoo>
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<!-- ============================================================ -->
|
||||||
|
<!-- AP14-A: Endabrechnungs-Regeln. -->
|
||||||
|
<!-- UEL: § 67 Abs 8 lit d — laufender Anteil als laufender -->
|
||||||
|
<!-- Bezug (Tarif + Sechstel nativ), SZ-Anteil als sonstiger Bezug -->
|
||||||
|
<!-- (6 %-Band, Freibetrag/Freigrenze im Kern). SV: nur der -->
|
||||||
|
<!-- laufende Anteil verlängert die Pflichtversicherung (§ 11 -->
|
||||||
|
<!-- Abs 2 ASVG, Tage-Umrechnung lb-url-15). -->
|
||||||
|
<!-- Abgang: § 67 Abs 8 lit b — neutral (kein Sechstel), LSt über -->
|
||||||
|
<!-- Fünftel-Override; SV-pflichtig mit Umlegung auf die -->
|
||||||
|
<!-- Kündigungsfrist; LNK-pflichtig (FLAG § 41 Abs 4 nennt nur -->
|
||||||
|
<!-- § 67 Abs 3 und 6). -->
|
||||||
|
<!-- ============================================================ -->
|
||||||
|
|
||||||
|
<!-- Urlaubsersatzleistung — laufender Anteil (§ 67 Abs 8 lit d). -->
|
||||||
|
<record id="rule_url_ersatz_lfd" model="hr.salary.rule">
|
||||||
|
<field name="name">Urlaubsersatzleistung — laufender Anteil (§ 67 Abs 8 lit d)</field>
|
||||||
|
<field name="code">URL_ERSATZ_LFD</field>
|
||||||
|
<field name="sequence" eval="25"/>
|
||||||
|
<field name="category_id" ref="l10n_at_hr_payroll_private.category_atp_brutto"/>
|
||||||
|
<field name="struct_id" ref="l10n_at_hr_payroll_private.structure_at_privat"/>
|
||||||
|
<field name="condition_select">python</field>
|
||||||
|
<field name="condition_python">result = 'ATP_UEL_TAGE' in inputs and inputs['ATP_UEL_TAGE'].amount</field>
|
||||||
|
<field name="l10n_at_bemessung">laufend</field>
|
||||||
|
<field name="l10n_at_sv_verlaengerung">urlaubsersatz</field>
|
||||||
|
<field name="amount_select">code</field>
|
||||||
|
<field name="amount_python_compute"><![CDATA[
|
||||||
|
# § 10 UrlG (lb-url-16): Tageswert = Monatsentgelt / Divisor
|
||||||
|
# (26 Werktage / 22 Arbeitstage); keine Rundung auf ganze Tage.
|
||||||
|
result = payslip._at_kv_gehalt_basis() / payslip._at_uel_divisor()
|
||||||
|
result_qty = inputs['ATP_UEL_TAGE'].amount
|
||||||
|
]]></field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Urlaubsersatzleistung — Sonderzahlungsanteil (§ 67 Abs 8 lit d). -->
|
||||||
|
<record id="rule_url_ersatz_sz" model="hr.salary.rule">
|
||||||
|
<field name="name">Urlaubsersatzleistung — Sonderzahlungsanteil (§ 67 Abs 8 lit d)</field>
|
||||||
|
<field name="code">URL_ERSATZ_SZ</field>
|
||||||
|
<field name="sequence" eval="26"/>
|
||||||
|
<field name="category_id" ref="l10n_at_hr_payroll_private.category_atp_brutto"/>
|
||||||
|
<field name="struct_id" ref="l10n_at_hr_payroll_private.structure_at_privat"/>
|
||||||
|
<field name="condition_select">python</field>
|
||||||
|
<field name="condition_python">result = 'ATP_UEL_TAGE' in inputs and inputs['ATP_UEL_TAGE'].amount</field>
|
||||||
|
<field name="l10n_at_bemessung">sonstig</field>
|
||||||
|
<field name="amount_select">code</field>
|
||||||
|
<field name="amount_python_compute"><![CDATA[
|
||||||
|
# SZ-Anteil der Urlaubsentgelt-Basis (lb-url-16): 2 Sonderzahlungen
|
||||||
|
# à Monatsentgelt pro Jahr → je Monat 2 × SZ-Basis/12, als Tageswert
|
||||||
|
# (auch bei bereits überaliquot bezahlten SZ).
|
||||||
|
result = payslip._at_uel_sz_tageswert()
|
||||||
|
result_qty = inputs['ATP_UEL_TAGE'].amount
|
||||||
|
]]></field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Abgangsentschädigung (§ 67 Abs 8 lit b — Kündigungsentschädigung). -->
|
||||||
|
<record id="rule_abgang" model="hr.salary.rule">
|
||||||
|
<field name="name">Abgangsentschädigung (§ 67 Abs 8 lit b)</field>
|
||||||
|
<field name="code">ABGANG</field>
|
||||||
|
<field name="sequence" eval="29"/>
|
||||||
|
<field name="category_id" ref="l10n_at_hr_payroll_private.category_atp_brutto"/>
|
||||||
|
<field name="struct_id" ref="l10n_at_hr_payroll_private.structure_at_privat"/>
|
||||||
|
<field name="condition_select">python</field>
|
||||||
|
<field name="condition_python">result = 'ATP_ABGANG' in inputs and inputs['ATP_ABGANG'].amount</field>
|
||||||
|
<field name="l10n_at_bemessung">neutral</field>
|
||||||
|
<field name="l10n_at_sv_art">laufend</field>
|
||||||
|
<field name="l10n_at_s67_fuenftel" eval="True"/>
|
||||||
|
<field name="l10n_at_sv_verlaengerung">abgang</field>
|
||||||
|
<field name="amount_select">code</field>
|
||||||
|
<field name="amount_python_compute"><![CDATA[
|
||||||
|
result = inputs['ATP_ABGANG'].amount
|
||||||
|
]]></field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Zeitguthaben an Normalarbeitszeit (§ 19e Abs 2 AZG: + 50 %). -->
|
||||||
|
<record id="rule_zeit_guthaben" model="hr.salary.rule">
|
||||||
|
<field name="name">Zeitguthaben Normalarbeitszeit + 50 % (§ 19e Abs 2 AZG)</field>
|
||||||
|
<field name="code">ZEIT_GUTHABEN</field>
|
||||||
|
<field name="sequence" eval="27"/>
|
||||||
|
<field name="category_id" ref="l10n_at_hr_payroll_private.category_atp_brutto"/>
|
||||||
|
<field name="struct_id" ref="l10n_at_hr_payroll_private.structure_at_privat"/>
|
||||||
|
<field name="condition_select">python</field>
|
||||||
|
<field name="condition_python">result = 'ATP_ZEIT_GUTHABEN' in inputs and inputs['ATP_ZEIT_GUTHABEN'].amount</field>
|
||||||
|
<field name="l10n_at_bemessung">laufend</field>
|
||||||
|
<field name="amount_select">code</field>
|
||||||
|
<field name="amount_python_compute"><![CDATA[
|
||||||
|
# § 19e Abs 2 AZG: Guthaben an Normalarbeitszeit + 50 % Zuschlag,
|
||||||
|
# bemessen mit der Normalstunde (lb-end-10); der Zuschlag ist KEIN
|
||||||
|
# Überstundenzuschlag iSd § 68 EStG (kein Freibetrags-Flag).
|
||||||
|
result = payslip._at_grundstunde_norm() * 1.5
|
||||||
|
result_qty = inputs['ATP_ZEIT_GUTHABEN'].amount
|
||||||
|
]]></field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Zeitschulden (Abrechnung mit der aktuellen Normalstunde). -->
|
||||||
|
<record id="rule_zeit_schulden" model="hr.salary.rule">
|
||||||
|
<field name="name">Zeitschulden-Abrechnung (negative Zeile)</field>
|
||||||
|
<field name="code">ZEIT_SCHULDEN</field>
|
||||||
|
<field name="sequence" eval="28"/>
|
||||||
|
<field name="category_id" ref="l10n_at_hr_payroll_private.category_atp_brutto"/>
|
||||||
|
<field name="struct_id" ref="l10n_at_hr_payroll_private.structure_at_privat"/>
|
||||||
|
<field name="condition_select">python</field>
|
||||||
|
<field name="condition_python">result = 'ATP_ZEIT_SCHULDEN' in inputs and inputs['ATP_ZEIT_SCHULDEN'].amount</field>
|
||||||
|
<field name="l10n_at_bemessung">laufend</field>
|
||||||
|
<field name="amount_select">code</field>
|
||||||
|
<field name="amount_python_compute"><![CDATA[
|
||||||
|
# Abrechnung offener Zeitschulden mit der aktuellen Normalstunde
|
||||||
|
# (lb-end-08); negative laufende Bezugszeile.
|
||||||
|
result = -payslip._at_grundstunde_norm()
|
||||||
|
result_qty = inputs['ATP_ZEIT_SCHULDEN'].amount
|
||||||
|
]]></field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Aufrechnung offener Vorschüsse: Rückverrechnung vom Netto
|
||||||
|
NACH SV und Lohnsteuer (§ 293 Abs 3 EO, lb-end-06) — neutral,
|
||||||
|
ohne SV-/LSt-/LNK-Wirkung. -->
|
||||||
|
<record id="rule_end_abrechnung_vorschuss" model="hr.salary.rule">
|
||||||
|
<field name="name">Aufrechnung offener Vorschuss (Netto)</field>
|
||||||
|
<field name="code">END_ABRECHNUNG_VORSCHUSS</field>
|
||||||
|
<field name="sequence" eval="80"/>
|
||||||
|
<field name="category_id" ref="l10n_at_hr_payroll_private.category_atp_abzug"/>
|
||||||
|
<field name="struct_id" ref="l10n_at_hr_payroll_private.structure_at_privat"/>
|
||||||
|
<field name="condition_select">python</field>
|
||||||
|
<field name="condition_python">result = 'ATP_ABRECHNUNG_VORSCHUSS' in inputs and inputs['ATP_ABRECHNUNG_VORSCHUSS'].amount</field>
|
||||||
|
<field name="l10n_at_bemessung">neutral</field>
|
||||||
|
<field name="l10n_at_sv_art">keine</field>
|
||||||
|
<field name="amount_select">code</field>
|
||||||
|
<field name="amount_python_compute"><![CDATA[
|
||||||
|
result = inputs['ATP_ABRECHNUNG_VORSCHUSS'].amount
|
||||||
|
]]></field>
|
||||||
|
</record>
|
||||||
|
</odoo>
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
# Endabrechnungs-Zyklus — Rechenfälle AP14-A
|
||||||
|
|
||||||
|
Stand: 2026-09. AP14-A bildet die Beendigungs-Abrechnung auf der
|
||||||
|
privaten Structure ab: Urlaubsersatzleistung (UEL), Abgangsentschädigung,
|
||||||
|
Aufrechnung offener Vorschüsse und Zeitguthaben/-schulden. Grundlagen
|
||||||
|
sind am Originalwortlaut verifiziert (`.ris/`, Stand 2026-09-14):
|
||||||
|
§ 11 Abs 2 ASVG, § 67 Abs 8 lit b und lit d EStG, § 19e Abs 2 AZG,
|
||||||
|
§ 41 Abs 4 FLAG.
|
||||||
|
|
||||||
|
## Rechts- und Systemgrenzen
|
||||||
|
|
||||||
|
- **UEL (§ 10 UrlG, § 67 Abs 8 lit d EStG):** Zweikomponenten — der
|
||||||
|
laufende Anteil läuft als laufender Bezug durch den Tarif des
|
||||||
|
Austrittsmonats und erhöht das Jahressechstel (nativ über
|
||||||
|
`bemessung='laufend'`); der Sonderzahlungsanteil läuft als sonstiger
|
||||||
|
Bezug durch das 6 %-Band (Freibetrag 620 €, Freigrenze 2.615 € im
|
||||||
|
Kern). Nur der laufende Anteil verlängert die Pflichtversicherung
|
||||||
|
(§ 11 Abs 2 ASVG). Tagewert-Divisoren 26 (Werktage) / 22 (5-AT); die
|
||||||
|
SV-Tage-Umrechnung (je volle Woche à N AT + 7−N KT, nur ganze
|
||||||
|
Blöcke) ist Verwaltungspraxis (lb-url-15), nicht Gesetzeswortlaut.
|
||||||
|
- **Abgangsentschädigung (§ 67 Abs 8 lit b):** nach Abzug der §-62-
|
||||||
|
Beiträge ein Fünftel steuerfrei, gedeckelt mit einem Fünftel des
|
||||||
|
Neunfachen der monatlichen HBG (2026: 6.930 × 9/5 = **12.474 €**);
|
||||||
|
der Rest läuft tarifisch im Zuflussmonat — Abs 2 (Sechstel) ist nicht
|
||||||
|
anzuwenden. SV-pflichtig mit Umlegung auf den Kündigungsfrist-
|
||||||
|
Zeitraum; voll lohnnebenkostenpflichtig (FLAG § 41 Abs 4 nennt nur
|
||||||
|
§ 67 Abs 3 und 6).
|
||||||
|
- **Sequentielle Verlängerungsfenster:** Gebühren sowohl eine
|
||||||
|
Kündigungsentschädigung als auch eine UEL, ist zur Bestimmung des
|
||||||
|
Zeitraumes zunächst die Kündigungsentschädigung und im Anschluss die
|
||||||
|
Ersatzleistung heranzuziehen (§ 11 Abs 2 ASVG) — die SV-Basen werden
|
||||||
|
fensterweise auf die Monate verteilt, Standardanteile bleiben auf
|
||||||
|
die volle monatliche HBG gedeckelt, Verlängerungsanteile auf den
|
||||||
|
Tageswert (HBG × Tage/30).
|
||||||
|
- **Aufrechnung offener Vorschüsse (§ 293 Abs 3 EO):** Rückverrechnung
|
||||||
|
vom Netto nach SV und Lohnsteuer — zulässig auch gegen das
|
||||||
|
Existenzminimum; keine Abgabenberührung (lb-end-06).
|
||||||
|
- **Zeitguthaben (§ 19e Abs 2 AZG):** Guthaben an Normalarbeitszeit
|
||||||
|
+ 50 % Zuschlag, bemessen mit der Normalstunde; der Zuschlag ist
|
||||||
|
KEIN Überstundenzuschlag iSd § 68 EStG (lb-end-10). Entfall bei
|
||||||
|
unberechtigtem vorzeitigem Austritt (Eingabe weglassen); KV kann
|
||||||
|
abweichen.
|
||||||
|
|
||||||
|
## RF-END-1 — UEL-Zweikomponenten
|
||||||
|
|
||||||
|
Ende 15. 3., 11 offene AT à 120 € (2.640 € Gehalt / 22):
|
||||||
|
|
||||||
|
- `URL_ERSATZ_LFD` = 1.320 €: tarifisch im März, Sechstel +5.280 €
|
||||||
|
(annualisiert 60/15);
|
||||||
|
- `URL_ERSATZ_SZ` = 220 € (2 × 2.640/12 = 440/Monat → 20/Tag):
|
||||||
|
6 %-Band;
|
||||||
|
- SV: 1.320 € auf die Verlängerungsmonate (Tage-Schema), 220 € als
|
||||||
|
sonstige Beitragsgrundlage gegen Monats- und Jahres-HBGl.
|
||||||
|
|
||||||
|
## RF-END-2 — SV-Tage-Umrechnung (§ 11 Abs 2)
|
||||||
|
|
||||||
|
- 23 AT (5-AT-Woche) → +4×2 KT = **31 SV-Tage**;
|
||||||
|
- 12 WT (6-WT-Woche) → +2×1 KT = **14**;
|
||||||
|
- 6 AT (2-Tage-Teilzeitwoche) → +3×5 KT = **21** (lb-url-15-Beispiel).
|
||||||
|
- Ende 15. 3., 31 SV-Tage → Verlängerung 16. 3.–15. 4.; UEL-lfd-Basis
|
||||||
|
tageproportional (März 16/31, April 15/31), Tageswert-Deckel je
|
||||||
|
Verlängerungsmonat.
|
||||||
|
|
||||||
|
## RF-END-3 — Abgangsentschädigung (Fünftelregelung)
|
||||||
|
|
||||||
|
9.000 € Entschädigung, 42 Tage Kündigungsfrist, DN-Satz s:
|
||||||
|
|
||||||
|
- Bemessung 9.000 × (1 − s/100); davon 1/5 steuerfrei (≤ 12.474 €);
|
||||||
|
4/5 tarifisch im Zuflussmonat, ohne Sechstelerhöhung;
|
||||||
|
- SV: Umlegung 16. 3.–26. 4. (Kündigungsfrist zuerst);
|
||||||
|
- LNK: volle 9.000 € in der Basis (§ 41 Abs 3 FLAG).
|
||||||
|
|
||||||
|
## RF-END-4 — Aufrechnung
|
||||||
|
|
||||||
|
2.000 € offener Vorschuss: `END_ABRECHNUNG_VORSCHUSS` mindert nur das
|
||||||
|
Netto; LSt-, SV- und LNK-Basen unverändert.
|
||||||
|
|
||||||
|
## RF-END-5 — Zeitguthaben/-schulden
|
||||||
|
|
||||||
|
10 h Normalarbeitszeit-Guthaben × 1,5 + 5 h Zeitschulden × (−1):
|
||||||
|
beides laufender Bezug, kein §-68-Freibetrag.
|
||||||
|
|
||||||
|
## Offene Punkte (dokumentiert, AP14-B/AP15)
|
||||||
|
|
||||||
|
- **Vergleich (§ 67 Abs 8 lit a)** und **freiwillige Abfertigung/
|
||||||
|
Abfindung (§ 67 Abs 6 6 %-Bänder)** → AP15 mit der Abfertigung-Alt-
|
||||||
|
Günstiger-Maschine (dort auch rollierende 12-Monats-Basis und
|
||||||
|
Dienstzeit-Nachweise).
|
||||||
|
- Prämien-Beendigung (§ 16 AngG-Aliquotierung, Rollung) — PRAM/SZ-Regeln
|
||||||
|
vorhanden; Feinschliff AP14-B.
|
||||||
|
- Meldewesen-Schnittstellen: „Ende des Entgeltanspruches" in mBGM/
|
||||||
|
Abmeldung (GP7), gesonderter Lohnzettel bei Zahlung außerhalb des
|
||||||
|
Beendigungsmonats (LStR 2002 Rz 911a).
|
||||||
|
- Der §-62-Abzug des Fünftelpfades ist über den effektiven DN-Satz
|
||||||
|
approximiert (HBG-Caps der Verlängerungsmonate laufen im SV-Pfad).
|
||||||
@@ -20,3 +20,4 @@ from . import pfaendung_report
|
|||||||
from . import sachbezuege_private
|
from . import sachbezuege_private
|
||||||
from . import reisekosten
|
from . import reisekosten
|
||||||
from . import pendler
|
from . import pendler
|
||||||
|
from . import endabrechnung
|
||||||
|
|||||||
@@ -0,0 +1,315 @@
|
|||||||
|
# Part of the odoo-at-payroll project. License: LGPL-3.
|
||||||
|
"""AP14-A: Endabrechnungs-Zyklus — Urlaubsersatzleistung (UEL),
|
||||||
|
Abgangsentschädigung (§ 67 Abs 8 lit b), Aufrechnung offener Vorschüsse
|
||||||
|
und Zeitguthaben/-schulden (§ 19e AZG).
|
||||||
|
|
||||||
|
Am Originalwortlaut verifiziert (.ris/, Stand 2026-09-14):
|
||||||
|
- § 11 Abs 2 ASVG: die Pflichtversicherung besteht für die Zeit des
|
||||||
|
Bezuges einer Ersatzleistung für Urlaubsentgelt (und einer
|
||||||
|
Kündigungsentschädigung) weiter; gebühren beide, ist zur Bestimmung
|
||||||
|
des Zeitraumes ZUERST die Kündigungsentschädigung und im Anschluss
|
||||||
|
die Ersatzleistung heranzuziehen (sequentielle Verlängerungsfenster).
|
||||||
|
Die 6-WT/5-AT-Tage-Umrechnung ist Verwaltungspraxis (lb-url-15),
|
||||||
|
nicht Gesetzeswortlaut.
|
||||||
|
- § 67 Abs 8 lit d EStG: UEL „soweit sie laufenden Arbeitslohn
|
||||||
|
betreffen, als laufender Arbeitslohn, soweit sie sonstige Bezüge
|
||||||
|
betreffen, als sonstiger Bezug im Kalendermonat der Zahlung" — die
|
||||||
|
Zweikomponenten-Abbildung läuft nativ über l10n_at_bemessung.
|
||||||
|
- § 67 Abs 8 lit b EStG (Kündigungs-/Abgangsentschädigung): nach
|
||||||
|
Abzug der §-62-Beiträge ist ein Fünftel steuerfrei zu belassen,
|
||||||
|
höchstens ein Fünftel des Neunfachen der monatlichen HBG; der Rest
|
||||||
|
läuft tarifisch im Zuflussmonat (Abs 10), Abs 2 (Sechstel) ist
|
||||||
|
NICHT anzuwenden. LNK-pflichtig (FLAG § 41 Abs 4 nennt nur
|
||||||
|
§ 67 Abs 3 und 6) und SV-pflichtig (keine §-49-Abs-3-Z-7-Befreiung)
|
||||||
|
mit Umlegung auf den Kündigungsfrist-Zeitraum.
|
||||||
|
- § 19e Abs 2 AZG: Guthaben an Normalarbeitszeit + 50 % Zuschlag
|
||||||
|
(Entfall bei unberechtigtem vorzeitigem Austritt; KV kann
|
||||||
|
abweichend regeln).
|
||||||
|
"""
|
||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
|
from odoo import fields, models
|
||||||
|
|
||||||
|
|
||||||
|
class HrSalaryRule(models.Model):
|
||||||
|
_inherit = 'hr.salary.rule'
|
||||||
|
|
||||||
|
l10n_at_sv_verlaengerung = fields.Selection(
|
||||||
|
[('keine', 'Keine SV-Verlängerung (§ 11 Abs 2 ASVG)'),
|
||||||
|
('urlaubsersatz', 'UEL-Tage-Schema (Urlaubsersatzleistung)'),
|
||||||
|
('abgang', 'Kündigungsfrist (Abgangsentschädigung)')],
|
||||||
|
string='AT SV-Verlängerung', default='keine',
|
||||||
|
help='Verteilt die laufende SV-Basis dieser Regel auf die '
|
||||||
|
'Pflichtversicherungs-Verlängerungsmonate nach § 11 Abs 2 '
|
||||||
|
'ASVG statt auf den Abrechnungsmonat: Abgangsentschädigung '
|
||||||
|
'zuerst (Kündigungsfrist), UEL anschließend (Tage-Schema '
|
||||||
|
'lb-url-15).')
|
||||||
|
l10n_at_s67_fuenftel = fields.Boolean(
|
||||||
|
string='AT § 67 Abs 8 lit a/b (Fünftelregelung)',
|
||||||
|
help='LSt-Pfad der Vergleichs-/Kündigungsentschädigungen: nach '
|
||||||
|
'Abzug der §-62-Beiträge ist ein Fünftel steuerfrei '
|
||||||
|
'(Deckel: ein Fünftel des Neunfachen der monatlichen HBG), '
|
||||||
|
'der Rest läuft tarifisch im Zuflussmonat — ohne '
|
||||||
|
'Jahressechstel-Erhöhung (Abs 2 nicht anzuwenden).')
|
||||||
|
|
||||||
|
|
||||||
|
class HrPayslip(models.Model):
|
||||||
|
_inherit = 'hr.payslip'
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# SV-Tage-Umrechnung und Verlängerungsmonate (§ 11 Abs 2 ASVG)
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
def _at_end_arbeitsrechtliches_ende(self):
|
||||||
|
"""Arbeitsrechtliches Ende = letzter Arbeitstag; das SV-Ende
|
||||||
|
(„Ende des Entgeltanspruches") liegt darüber hinaus, wenn ein
|
||||||
|
Entgeltanspruch über das Ende hinaus läuft."""
|
||||||
|
self.ensure_one()
|
||||||
|
return self.version_id.contract_date_end or self.date_to
|
||||||
|
|
||||||
|
def _at_end_arbeitstage_woche(self):
|
||||||
|
"""Arbeitstage pro Woche des Vertragskalenders (Schema-Grundlage
|
||||||
|
der SV-Tage-Umrechnung: 5-AT-Woche, 6-WT-Woche, Teilzeitwoche)."""
|
||||||
|
self.ensure_one()
|
||||||
|
calendar = self.version_id.resource_calendar_id
|
||||||
|
days = {int(att.dayofweek) for att in calendar.attendance_ids} \
|
||||||
|
if calendar else set()
|
||||||
|
return len(days) or 5
|
||||||
|
|
||||||
|
def _at_end_sv_tage(self, tage):
|
||||||
|
"""SV-Tage aus abzurechnenden Urlaubstagen (Verwaltungspraxis
|
||||||
|
lb-url-15): je volle Woche à N Arbeitstage werden 7−N
|
||||||
|
Kalendertage hinzugerechnet (5-AT-Woche +2, 6-WT-Woche +1,
|
||||||
|
2-Tage-Teilzeitwoche +5) — nur ganze Wochenblöcke."""
|
||||||
|
self.ensure_one()
|
||||||
|
at_woche = self._at_end_arbeitstage_woche()
|
||||||
|
blocks = int(tage // at_woche)
|
||||||
|
return tage + blocks * (7 - at_woche)
|
||||||
|
|
||||||
|
def _at_end_verlaengerungs_monate(self, sv_tage, start):
|
||||||
|
"""[(Monatsanfang, Tage im Monat)] eines Verlängerungsfensters
|
||||||
|
ab dem übergebenen Starttag (§ 11 Abs 2 ASVG)."""
|
||||||
|
self.ensure_one()
|
||||||
|
ergebnis = {}
|
||||||
|
rest = int(sv_tage)
|
||||||
|
tag = start
|
||||||
|
while rest > 0:
|
||||||
|
monatsende = (tag.replace(day=28) + timedelta(days=4)) \
|
||||||
|
.replace(day=1) - timedelta(days=1)
|
||||||
|
tage_dabei = min((monatsende - tag).days + 1, rest)
|
||||||
|
monat = tag.replace(day=1)
|
||||||
|
ergebnis[monat] = ergebnis.get(monat, 0) + tage_dabei
|
||||||
|
tag = monatsende + timedelta(days=1)
|
||||||
|
rest -= tage_dabei
|
||||||
|
return sorted(ergebnis.items())
|
||||||
|
|
||||||
|
def _at_end_rule_verlaengerung(self, rule, result_rules=None):
|
||||||
|
"""[(Monatsanfang, Tage, Anteil)] der Regel-Basis nach § 11
|
||||||
|
Abs 2: die Abgangsentschädigung belegt ZUERST die (fiktive)
|
||||||
|
Kündigungsfrist, die UEL SCHLIESSLICH die Urlaubstage — beide
|
||||||
|
Fenster laufen ab dem Tag nach dem arbeitsrechtlichen Ende
|
||||||
|
hintereinander (Gesetzeswortlaut: „zunächst die
|
||||||
|
Kündigungsentschädigung und im Anschluss die Ersatzleistung")."""
|
||||||
|
self.ensure_one()
|
||||||
|
start = self._at_end_arbeitsrechtliches_ende() + timedelta(days=1)
|
||||||
|
if rule.l10n_at_sv_verlaengerung == 'urlaubsersatz':
|
||||||
|
ffrist = self._at_input_amount('ATP_ABGANG_FFRIST_TAGE')
|
||||||
|
if ffrist:
|
||||||
|
start += timedelta(days=int(ffrist))
|
||||||
|
tage = self._at_end_sv_tage(self._at_uel_tage())
|
||||||
|
else:
|
||||||
|
ffrist = self._at_input_amount('ATP_ABGANG_FFRIST_TAGE')
|
||||||
|
tage = int(ffrist) if ffrist else 0
|
||||||
|
monate = self._at_end_verlaengerungs_monate(tage, start)
|
||||||
|
if not monate:
|
||||||
|
return []
|
||||||
|
gesamt = sum(tage for _monat, tage in monate)
|
||||||
|
return [(monat, tage, tage / float(gesamt))
|
||||||
|
for monat, tage in monate]
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Eingabe-Helfer
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
def _at_input_amount(self, code):
|
||||||
|
self.ensure_one()
|
||||||
|
linie = self.input_line_ids.filtered(
|
||||||
|
lambda i: i.input_type_id.code == code)
|
||||||
|
return linie[:1].amount or 0.0
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Urlaubsersatzleistung — § 67 Abs 8 lit d, lb-url-15/16
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
def _at_uel_tage(self):
|
||||||
|
"""Offene Urlaubstage (Arbeitstage; keine Rundung, lb-url-16)."""
|
||||||
|
self.ensure_one()
|
||||||
|
return self._at_input_amount('ATP_UEL_TAGE')
|
||||||
|
|
||||||
|
def _at_uel_divisor(self):
|
||||||
|
"""Tagewert-Divisor: /26 bei Werktags-, /22 bei 5-AT-Kalender
|
||||||
|
(lb-url-16)."""
|
||||||
|
self.ensure_one()
|
||||||
|
params = self._l10n_at_param('at_endabrechnung')
|
||||||
|
if self._at_end_arbeitstage_woche() >= 6:
|
||||||
|
return float(params['divisor_werktage'])
|
||||||
|
return float(params['divisor_arbeitstage'])
|
||||||
|
|
||||||
|
def _at_uel_lfd(self):
|
||||||
|
"""UEL-laufender Anteil: Tage × Monatsentgelt / Divisor —
|
||||||
|
läuft als laufender Bezug durch den Tarif des Austrittsmonats
|
||||||
|
und erhöht das Jahressechstel (nativ über bemessung='laufend')."""
|
||||||
|
self.ensure_one()
|
||||||
|
return self._at_uel_tage() * self._at_kv_gehalt_basis() \
|
||||||
|
/ self._at_uel_divisor()
|
||||||
|
|
||||||
|
def _at_uel_sz_tageswert(self):
|
||||||
|
"""SZ-Anteil je Urlaubstag: 2 Sonderzahlungen à Monatsentgelt
|
||||||
|
pro Jahr (KV § 11) → je Monat 2 × SZ-Basis/12, als Tageswert
|
||||||
|
(auch bei bereits überaliquot bezahlten SZ, lb-url-16)."""
|
||||||
|
self.ensure_one()
|
||||||
|
sz_monat = 2.0 * self._at_sz_basis(self.date_to) / 12.0
|
||||||
|
return sz_monat / self._at_uel_divisor()
|
||||||
|
|
||||||
|
def _at_uel_sz(self):
|
||||||
|
"""UEL-Sonderzahlungsanteil: Tage × SZ-Tageswert."""
|
||||||
|
self.ensure_one()
|
||||||
|
return self._at_uel_tage() * self._at_uel_sz_tageswert()
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Abgangsentschädigung — § 67 Abs 8 lit b (Fünftelregelung)
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
def _at_fuenftel_regeln(self):
|
||||||
|
self.ensure_one()
|
||||||
|
return self.struct_id.rule_ids.filtered(
|
||||||
|
lambda rule: rule.l10n_at_s67_fuenftel)
|
||||||
|
|
||||||
|
def _at_fuenftel_total(self, result_rules=None):
|
||||||
|
self.ensure_one()
|
||||||
|
total = 0.0
|
||||||
|
for rule in self._at_fuenftel_regeln():
|
||||||
|
if result_rules is not None:
|
||||||
|
if rule.code in result_rules:
|
||||||
|
total += result_rules[rule.code]['total']
|
||||||
|
else:
|
||||||
|
linie = self.line_ids.filtered(lambda l: l.code == rule.code)
|
||||||
|
total += sum(linie.mapped('total'))
|
||||||
|
return total
|
||||||
|
|
||||||
|
def _at_fuenftel_tarif_basis(self, result_rules=None):
|
||||||
|
"""Tarifischer Teil (4/5) der Fünftel-Bezüge: Bemessung nach
|
||||||
|
Abzug der darauf entfallenden §-62-Beiträge (approximiert über
|
||||||
|
den effektiven DN-Satz der laufenden Basis; die HBG-Caps der
|
||||||
|
Verlängerungsmonate werden im SV-Pfad separat geführt); ein
|
||||||
|
Fünftel ist steuerfrei, gedeckelt mit einem Fünftel des
|
||||||
|
Neunfachen der monatlichen HBG (lb-end-21: 2026 6.930 × 9/5
|
||||||
|
= 12.474)."""
|
||||||
|
self.ensure_one()
|
||||||
|
total = self._at_fuenftel_total(result_rules)
|
||||||
|
if total <= 0:
|
||||||
|
return 0.0
|
||||||
|
satz = sum(self._l10n_at_sv_satz(
|
||||||
|
'dn', komponente,
|
||||||
|
staffel_basis=self._l10n_at_sv_basis_roh(result_rules))
|
||||||
|
for komponente in ('kv', 'pv', 'av', 'ak', 'wf'))
|
||||||
|
bemessung = max(total * (1.0 - satz / 100.0), 0.0)
|
||||||
|
deckel = float(self._l10n_at_param('at_sv_hbg_monatlich')) \
|
||||||
|
* 9.0 / 5.0
|
||||||
|
frei = min(bemessung / 5.0, deckel)
|
||||||
|
return bemessung - frei
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Engine-Anbindung: Tarif-Teil ohne Sechstel, LNK-Pflicht, SV-Umlegung
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
def _l10n_at_bemessung_laufen(self, result_rules=None):
|
||||||
|
basis = super()._l10n_at_bemessung_laufen(result_rules)
|
||||||
|
# § 67 Abs 8 lit b: 4/5 tarifisch im Zuflussmonat; die Regel
|
||||||
|
# selbst ist neutral und erhöht brutto('laufend') nicht → kein
|
||||||
|
# Sechstel-Effekt (Abs 2 nicht anzuwenden).
|
||||||
|
return basis + self._at_fuenftel_tarif_basis(result_rules)
|
||||||
|
|
||||||
|
def _at_lnk_basis(self, steuer, result_rules=None):
|
||||||
|
"""Upstream-Überschreibung (Muster account_posting_private;
|
||||||
|
Vorlage payslip_private._at_lnk_basis — bei Änderungen dort
|
||||||
|
gegen diesen Pin neu abgleichen): Kündigungsentschädigungen
|
||||||
|
sind Arbeitslöhne iSd § 41 Abs 3 FLAG und unterliegen NICHT
|
||||||
|
den Abs-4-Ausschlüssen (lit b nennt nur § 67 Abs 3 und 6) —
|
||||||
|
sie sind VOR der Freibetragsstaffel in die LNK-Basis
|
||||||
|
aufzunehmen."""
|
||||||
|
self.ensure_one()
|
||||||
|
basis = (self._l10n_at_brutto('laufend', result_rules)
|
||||||
|
+ self._l10n_at_brutto('sonstig', result_rules)
|
||||||
|
- self._l10n_at_st_frei_freie_laufen(result_rules))
|
||||||
|
for rule in self.struct_id.rule_ids:
|
||||||
|
if rule.l10n_at_s67_abs in ('abs3', 'abs6'):
|
||||||
|
if result_rules is not None and rule.code in result_rules:
|
||||||
|
basis -= result_rules[rule.code]['total']
|
||||||
|
elif result_rules is None:
|
||||||
|
linie = self.line_ids.filtered(
|
||||||
|
lambda l: l.code == rule.code)
|
||||||
|
basis -= sum(linie.mapped('total'))
|
||||||
|
basis += self._at_fuenftel_total(result_rules)
|
||||||
|
basis = max(basis, 0.0)
|
||||||
|
if steuer in ('flaf', 'dz'):
|
||||||
|
version = self.version_id
|
||||||
|
if (version.l10n_at_lnkp_frei_ab
|
||||||
|
and self.date_from >= version.l10n_at_lnkp_frei_ab):
|
||||||
|
return 0.0
|
||||||
|
freibetrag = self.env['hr.rule.parameter']._get_parameter_from_code(
|
||||||
|
'at_lnk_freibetrag', self.date_to, raise_if_not_found=False) or {}
|
||||||
|
grenze = float(freibetrag.get('grenze', 0.0))
|
||||||
|
if basis <= 0.0:
|
||||||
|
return 0.0
|
||||||
|
if basis <= grenze:
|
||||||
|
basis -= float(freibetrag.get('freibetrag', 0.0))
|
||||||
|
return max(basis, 0.0)
|
||||||
|
|
||||||
|
def _l10n_at_sv_basis_laufen_monate(self, result_rules=None):
|
||||||
|
"""Upstream-Überschreibung (Muster account_posting_private;
|
||||||
|
Vorlage l10n_at_hr_payroll.sozialversicherung — bei Odoo- oder
|
||||||
|
Kern-Upgrades gegen diesen Pin neu abgleichen): identischer
|
||||||
|
Ablauf wie der Kern, zusätzlich werden Regeln mit AT-SV-
|
||||||
|
Verlängerung tageproportional auf die Verlängerungsmonate
|
||||||
|
gelegt. Der Standardanteil jedes Monats bleibt auf die volle
|
||||||
|
monatliche HBG gedeckelt, der Verlängerungsanteil auf den
|
||||||
|
Tageswert (monatliche HBG × Verlängerungstage/30, lb-url-15);
|
||||||
|
die Fenster laufen sequenziell (Abgang zuerst, UEL danach)."""
|
||||||
|
self.ensure_one()
|
||||||
|
gruppen = {}
|
||||||
|
verlaengerung = {}
|
||||||
|
tage_pro_monat = {}
|
||||||
|
for rule in self.struct_id.rule_ids:
|
||||||
|
if self._l10n_at_sv_art(rule) != '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
|
||||||
|
if rule.l10n_at_sv_verlaengerung != 'keine':
|
||||||
|
for monat, tage, anteil in \
|
||||||
|
self._at_end_rule_verlaengerung(rule, result_rules):
|
||||||
|
verlaengerung[monat] = verlaengerung.get(monat, 0.0) \
|
||||||
|
+ werte['total'] * anteil
|
||||||
|
tage_pro_monat[monat] = \
|
||||||
|
tage_pro_monat.get(monat, 0) + tage
|
||||||
|
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 in sorted(set(gruppen) | set(verlaengerung)):
|
||||||
|
hbg = float(self._l10n_at_param('at_sv_hbg_monatlich', monat))
|
||||||
|
basis = min(max(gruppen.get(monat, 0.0), 0.0), hbg)
|
||||||
|
if monat in verlaengerung:
|
||||||
|
tageswert = hbg * tage_pro_monat[monat] / 30.0
|
||||||
|
basis += min(max(verlaengerung[monat], 0.0), tageswert)
|
||||||
|
ergebnis.append((monat, basis))
|
||||||
|
return ergebnis
|
||||||
@@ -23,3 +23,4 @@ access_l10n_at_payroll_pfaendung_user,L10n AT Payroll Pfaendung User,model_l10n_
|
|||||||
access_l10n_at_payroll_pfaendung_manager,L10n AT Payroll Pfaendung Manager,model_l10n_at_payroll_pfaendung,hr_payroll.group_hr_payroll_manager,1,1,1,1
|
access_l10n_at_payroll_pfaendung_manager,L10n AT Payroll Pfaendung Manager,model_l10n_at_payroll_pfaendung,hr_payroll.group_hr_payroll_manager,1,1,1,1
|
||||||
access_l10n_at_payroll_pendler_erklaerung_user,L10n AT Payroll Pendlererklaerung User,model_l10n_at_payroll_pendler_erklaerung,hr_payroll.group_hr_payroll_user,1,1,1,0
|
access_l10n_at_payroll_pendler_erklaerung_user,L10n AT Payroll Pendlererklaerung User,model_l10n_at_payroll_pendler_erklaerung,hr_payroll.group_hr_payroll_user,1,1,1,0
|
||||||
access_l10n_at_payroll_pendler_erklaerung_manager,L10n AT Payroll Pendlererklaerung Manager,model_l10n_at_payroll_pendler_erklaerung,hr_payroll.group_hr_payroll_manager,1,1,1,1
|
access_l10n_at_payroll_pendler_erklaerung_manager,L10n AT Payroll Pendlererklaerung Manager,model_l10n_at_payroll_pendler_erklaerung,hr_payroll.group_hr_payroll_manager,1,1,1,1
|
||||||
|
access_l10n_at_payroll_endabrechnung_wizard,L10n AT Payroll Endabrechnung Wizard,model_l10n_at_payroll_endabrechnung_wizard,hr_payroll.group_hr_payroll_user,1,1,1,1
|
||||||
|
|||||||
|
@@ -17,3 +17,4 @@ from . import test_sachbezuege_private
|
|||||||
from . import test_sachbezuege2_private
|
from . import test_sachbezuege2_private
|
||||||
from . import test_reisekosten_private
|
from . import test_reisekosten_private
|
||||||
from . import test_pendler_private
|
from . import test_pendler_private
|
||||||
|
from . import test_endabrechnung_private
|
||||||
|
|||||||
@@ -0,0 +1,298 @@
|
|||||||
|
# Part of the odoo-at-payroll project. License: LGPL-3.
|
||||||
|
"""AP14-A: Endabrechnungs-Zyklus — UEL-Zweikomponenten (§ 67 Abs 8
|
||||||
|
lit d), SV-Verlängerung (§ 11 Abs 2 ASVG, sequentielle Fenster),
|
||||||
|
Abgangsentschädigung (§ 67 Abs 8 lit b Fünftel), Aufrechnung offener
|
||||||
|
Vorschüsse (§ 293 Abs 3 EO) und Zeitguthaben (§ 19e Abs 2 AZG)."""
|
||||||
|
from datetime import date, timedelta
|
||||||
|
|
||||||
|
from odoo.tests import TransactionCase, tagged
|
||||||
|
|
||||||
|
|
||||||
|
@tagged('post_install', '-at_install')
|
||||||
|
class EndabrechnungPrivateTest(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 End 40h Mo-Fr',
|
||||||
|
'tz': 'Europe/Vienna',
|
||||||
|
'hours_per_day': 8,
|
||||||
|
'attendance_ids': [
|
||||||
|
(0, 0, {'name': 'Tag %d' % weekday,
|
||||||
|
'dayofweek': str(weekday),
|
||||||
|
'hour_from': 8.0, 'hour_to': 16.0})
|
||||||
|
for weekday in range(5)
|
||||||
|
],
|
||||||
|
})
|
||||||
|
self.env.company.resource_calendar_id = self.calendar
|
||||||
|
|
||||||
|
def _calendar(self, weekdays):
|
||||||
|
return self.env['resource.calendar'].create({
|
||||||
|
'name': 'AT Test End %dTage' % len(weekdays),
|
||||||
|
'tz': 'Europe/Vienna',
|
||||||
|
'hours_per_day': 8,
|
||||||
|
'attendance_ids': [
|
||||||
|
(0, 0, {'name': 'Tag %d' % weekday,
|
||||||
|
'dayofweek': str(weekday),
|
||||||
|
'hour_from': 8.0, 'hour_to': 16.0})
|
||||||
|
for weekday in weekdays
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
def _employee(self, ueberzahlung=2640.0,
|
||||||
|
ende=date(2026, 3, 15), calendar=None):
|
||||||
|
employee = self.env['hr.employee'].create(
|
||||||
|
{'name': 'Test End 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),
|
||||||
|
'contract_date_end': ende,
|
||||||
|
'ueberzahlung': ueberzahlung,
|
||||||
|
'kv_id': self.kv2203.id,
|
||||||
|
'kv_gruppe_id': self.gruppe_ii.id,
|
||||||
|
'erfahrungsstufe': 1,
|
||||||
|
'resource_calendar_id': (calendar or self.calendar).id,
|
||||||
|
})
|
||||||
|
return employee
|
||||||
|
|
||||||
|
def _slip(self, employee, monat=3, jahr=2026):
|
||||||
|
erster = date(jahr, monat, 1)
|
||||||
|
naechster = (erster + timedelta(days=32)).replace(day=1)
|
||||||
|
slip = self.env['hr.payslip'].create({
|
||||||
|
'name': 'Test-End %d/%d' % (monat, jahr),
|
||||||
|
'employee_id': employee.id,
|
||||||
|
'struct_id': self.structure.id,
|
||||||
|
'date_from': erster,
|
||||||
|
'date_to': naechster - timedelta(days=1),
|
||||||
|
})
|
||||||
|
slip.compute_sheet()
|
||||||
|
return slip
|
||||||
|
|
||||||
|
def _input(self, slip, code, amount):
|
||||||
|
typ = self.env['hr.payslip.input.type'].search(
|
||||||
|
[('code', '=', code)], limit=1)
|
||||||
|
self.env['hr.payslip.input'].create({
|
||||||
|
'payslip_id': slip.id,
|
||||||
|
'input_type_id': typ.id,
|
||||||
|
'amount': amount,
|
||||||
|
})
|
||||||
|
slip.compute_sheet()
|
||||||
|
|
||||||
|
def _total(self, slip, code):
|
||||||
|
return sum(slip.line_ids.filtered(
|
||||||
|
lambda line: line.code == code).mapped('total'))
|
||||||
|
|
||||||
|
def _basis_monate(self, slip):
|
||||||
|
return dict(slip._l10n_at_sv_basis_laufen_monate())
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# RF-END-1: UEL-Zweikomponenten (§ 67 Abs 8 lit d)
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
def test_01_uel_zwei_komponenten(self):
|
||||||
|
"""11 offene AT à 120 € (2.640/22): UEL-lfd 1.320 € läuft
|
||||||
|
tarifisch und sechstelerhöhend; UEL-SZ 220 € (2×2.640/12/22)
|
||||||
|
als sonstiger Bezug; nur der laufende Anteil verlängert die
|
||||||
|
Pflichtversicherung."""
|
||||||
|
emp_mit = self._employee()
|
||||||
|
emp_ohne = self._employee()
|
||||||
|
slip_mit = self._slip(emp_mit)
|
||||||
|
slip_ohne = self._slip(emp_ohne)
|
||||||
|
self._input(slip_mit, 'ATP_UEL_TAGE', 11)
|
||||||
|
|
||||||
|
self.assertAlmostEqual(
|
||||||
|
self._total(slip_mit, 'URL_ERSATZ_LFD'), 1320.0, places=2)
|
||||||
|
self.assertAlmostEqual(
|
||||||
|
self._total(slip_mit, 'URL_ERSATZ_SZ'), 220.0, places=2)
|
||||||
|
svdn_diff = (slip_mit._l10n_at_svdn_gesamt()
|
||||||
|
- slip_ohne._l10n_at_svdn_gesamt())
|
||||||
|
self.assertAlmostEqual(
|
||||||
|
slip_mit._l10n_at_bemessung_laufen()
|
||||||
|
- slip_ohne._l10n_at_bemessung_laufen() + svdn_diff,
|
||||||
|
1320.0, places=2)
|
||||||
|
# Sechstel wächst um den annualisierten laufenden Anteil
|
||||||
|
# (60/Lohnsteuertage × 1.320; Teilmonat März: 15 Tage).
|
||||||
|
self.assertAlmostEqual(
|
||||||
|
slip_mit._l10n_at_sechstel() - slip_ohne._l10n_at_sechstel(),
|
||||||
|
1320.0 * 60.0 / 15.0, places=2)
|
||||||
|
# SZ-Bemessung enthält den UEL-SZ-Anteil (SV-DN nach § 67
|
||||||
|
# Abs 12 abziehen, um den Brutto-Anteil zu isolieren)
|
||||||
|
svdn_sz_diff = (slip_mit._l10n_at_svdn_sz()
|
||||||
|
- slip_ohne._l10n_at_svdn_sz())
|
||||||
|
self.assertAlmostEqual(
|
||||||
|
slip_mit._l10n_at_sz_bemessung()
|
||||||
|
- slip_ohne._l10n_at_sz_bemessung() + svdn_sz_diff,
|
||||||
|
220.0, places=2)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# RF-END-2: SV-Tage-Umrechnung und Verlängerungsmonate (§ 11 Abs 2)
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
def test_02_sv_tage_und_verlaengerung(self):
|
||||||
|
"""Schema je voller Woche à N AT + (7−N) KT: 23 AT (5-Woche)
|
||||||
|
→ 31 SV-Tage; 12 WT (6-Woche) → 14; 6 AT (2-Tage-Woche) → 21
|
||||||
|
(lb-url-15). Ende 15. 3.: Verlängerung 16. 3.–15. 4. — die
|
||||||
|
UEL-lfd-Basis liegt tageproportional auf den Monaten."""
|
||||||
|
emp = self._employee()
|
||||||
|
slip = self._slip(emp)
|
||||||
|
self.assertEqual(slip._at_end_sv_tage(23), 31)
|
||||||
|
self.assertEqual(slip._at_end_sv_tage(10), 14)
|
||||||
|
|
||||||
|
emp_6 = self._employee(calendar=self._calendar(range(6)))
|
||||||
|
self.assertEqual(self._slip(emp_6)._at_end_sv_tage(12), 14)
|
||||||
|
|
||||||
|
emp_2 = self._employee(calendar=self._calendar([0, 3]))
|
||||||
|
self.assertEqual(self._slip(emp_2)._at_end_sv_tage(6), 21)
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
slip._at_end_verlaengerungs_monate(
|
||||||
|
31, date(2026, 3, 16)),
|
||||||
|
[(date(2026, 3, 1), 16), (date(2026, 4, 1), 15)])
|
||||||
|
|
||||||
|
self._input(slip, 'ATP_UEL_TAGE', 23)
|
||||||
|
basis = self._basis_monate(slip)
|
||||||
|
self.assertAlmostEqual(
|
||||||
|
basis[date(2026, 4, 1)], 2760.0 * 15.0 / 31.0, places=2)
|
||||||
|
# März: KVGEO-Standardanteil + UEL-März-Anteil
|
||||||
|
self.assertAlmostEqual(
|
||||||
|
basis[date(2026, 3, 1)] - 2760.0 * 16.0 / 31.0,
|
||||||
|
2640.0 * 10.0 / 22.0, places=2)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# RF-END-3: Abgangsentschädigung (§ 67 Abs 8 lit b, Fünftel)
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
def test_03_abgang_fuenftel(self):
|
||||||
|
"""9.000 € Abgangsentschädigung: Fünftel-LSt (1/5 steuerfrei
|
||||||
|
nach §-62-Abzug, Deckel 12.474 €), 4/5 tarifisch OHNE
|
||||||
|
Sechstelerhöhung; SV-Umlegung auf die 42 Tage Kündigungsfrist;
|
||||||
|
voll LNK-pflichtig."""
|
||||||
|
emp_mit = self._employee(ueberzahlung=3000.0)
|
||||||
|
emp_ohne = self._employee(ueberzahlung=3000.0)
|
||||||
|
slip_mit = self._slip(emp_mit)
|
||||||
|
slip_ohne = self._slip(emp_ohne)
|
||||||
|
self._input(slip_mit, 'ATP_ABGANG', 9000.0)
|
||||||
|
self._input(slip_mit, 'ATP_ABGANG_FFRIST_TAGE', 42)
|
||||||
|
|
||||||
|
self.assertAlmostEqual(self._total(slip_mit, 'ABGANG'), 9000.0,
|
||||||
|
places=2)
|
||||||
|
# Sechstel unverändert (Abs 2 nicht anzuwenden)
|
||||||
|
self.assertAlmostEqual(
|
||||||
|
slip_mit._l10n_at_sechstel() - slip_ohne._l10n_at_sechstel(),
|
||||||
|
0.0, places=2)
|
||||||
|
# Tarifischer Teil: 9.000 minus §-62-Abzug, 1/5 frei
|
||||||
|
satz = sum(slip_mit._l10n_at_sv_satz(
|
||||||
|
'dn', k, staffel_basis=slip_mit._l10n_at_sv_basis_roh())
|
||||||
|
for k in ('kv', 'pv', 'av', 'ak', 'wf'))
|
||||||
|
bemessung = 9000.0 * (1.0 - satz / 100.0)
|
||||||
|
deckel = float(slip_mit._l10n_at_param('at_sv_hbg_monatlich')) \
|
||||||
|
* 9.0 / 5.0
|
||||||
|
erwartet = bemessung - min(bemessung / 5.0, deckel)
|
||||||
|
self.assertAlmostEqual(
|
||||||
|
slip_mit._at_fuenftel_tarif_basis(), erwartet, places=2)
|
||||||
|
svdn_diff = (slip_mit._l10n_at_svdn_gesamt()
|
||||||
|
- slip_ohne._l10n_at_svdn_gesamt())
|
||||||
|
self.assertAlmostEqual(
|
||||||
|
slip_mit._l10n_at_bemessung_laufen()
|
||||||
|
- slip_ohne._l10n_at_bemessung_laufen() + svdn_diff,
|
||||||
|
erwartet, places=2)
|
||||||
|
# LNK-Basis um die volle Entschädigung erhöht
|
||||||
|
self.assertAlmostEqual(
|
||||||
|
slip_mit._at_lnk_basis('flaf')
|
||||||
|
- slip_ohne._at_lnk_basis('flaf'), 9000.0, places=2)
|
||||||
|
# SV-Umlegung: 16. 3.–26. 4. (Kündigungsfrist zuerst); im März
|
||||||
|
# liegt zusätzlich der aliquote KVGEO-Standardanteil
|
||||||
|
basis = self._basis_monate(slip_mit)
|
||||||
|
self.assertAlmostEqual(
|
||||||
|
basis[date(2026, 4, 1)], 9000.0 * 26.0 / 42.0, places=2)
|
||||||
|
self.assertAlmostEqual(
|
||||||
|
basis[date(2026, 3, 1)] - 3000.0 * 10.0 / 22.0,
|
||||||
|
9000.0 * 16.0 / 42.0, places=2)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# RF-END-4: Aufrechnung offener Vorschüsse (§ 293 Abs 3 EO)
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
def test_04_aufrechnung_vorschuss(self):
|
||||||
|
"""2.000 € offener Vorschuss wird vom Netto abgezogen — ohne
|
||||||
|
jede Wirkung auf LSt-, SV- oder LNK-Basen (lb-end-06)."""
|
||||||
|
emp_mit = self._employee()
|
||||||
|
emp_ohne = self._employee()
|
||||||
|
slip_mit = self._slip(emp_mit)
|
||||||
|
slip_ohne = self._slip(emp_ohne)
|
||||||
|
self._input(slip_mit, 'ATP_ABRECHNUNG_VORSCHUSS', 2000.0)
|
||||||
|
|
||||||
|
self.assertAlmostEqual(self._total(slip_mit, 'END_ABRECHNUNG_VORSCHUSS'),
|
||||||
|
2000.0, places=2)
|
||||||
|
self.assertAlmostEqual(
|
||||||
|
slip_mit._l10n_at_bemessung_laufen()
|
||||||
|
- slip_ohne._l10n_at_bemessung_laufen(), 0.0, places=2)
|
||||||
|
self.assertAlmostEqual(
|
||||||
|
slip_mit._l10n_at_sv_basis_roh()
|
||||||
|
- slip_ohne._l10n_at_sv_basis_roh(), 0.0, places=2)
|
||||||
|
self.assertAlmostEqual(
|
||||||
|
slip_mit._at_lnk_basis('flaf')
|
||||||
|
- slip_ohne._at_lnk_basis('flaf'), 0.0, places=2)
|
||||||
|
self.assertAlmostEqual(
|
||||||
|
self._total(slip_mit, 'NET') - self._total(slip_ohne, 'NET'),
|
||||||
|
-2000.0, places=2)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# RF-END-5: Zeitguthaben (§ 19e Abs 2 AZG) und Zeitschulden
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
def test_05_zeitguthaben_und_schulden(self):
|
||||||
|
"""10 h Normalarbeitszeit-Guthaben + 50 % Zuschlag, 5 h
|
||||||
|
Zeitschulden negativ — beides laufender Bezug, KEIN §-68-
|
||||||
|
Freibetrag (lb-end-10)."""
|
||||||
|
emp = self._employee(ueberzahlung=3000.0)
|
||||||
|
slip = self._slip(emp)
|
||||||
|
self._input(slip, 'ATP_ZEIT_GUTHABEN', 10.0)
|
||||||
|
self._input(slip, 'ATP_ZEIT_SCHULDEN', 5.0)
|
||||||
|
|
||||||
|
stunde = slip._at_grundstunde_norm()
|
||||||
|
self.assertAlmostEqual(
|
||||||
|
self._total(slip, 'ZEIT_GUTHABEN'), 10.0 * stunde * 1.5,
|
||||||
|
places=2)
|
||||||
|
self.assertAlmostEqual(
|
||||||
|
self._total(slip, 'ZEIT_SCHULDEN'), -5.0 * stunde, places=2)
|
||||||
|
regel = slip.struct_id.rule_ids.filtered(
|
||||||
|
lambda rule: rule.code == 'ZEIT_GUTHABEN')
|
||||||
|
self.assertEqual(regel.l10n_at_steuerbefreiung_68, 'keine')
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# RF-END-6: Wizard legt den Endabrechnungs-Lauf vorbefüllt an
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
def test_06_wizard(self):
|
||||||
|
"""Der Wizard sammelt UEL, Abgang, Zeitguthaben und Vorschuss
|
||||||
|
und erzeugt den Payslip mit vorbefüllten Eingaben."""
|
||||||
|
emp = self._employee()
|
||||||
|
# Version auf die private Structure-Type binden, damit der
|
||||||
|
# Wizard über version.struct_id die richtige Struktur findet.
|
||||||
|
emp.version_id.write({'structure_type_id': self.env.ref(
|
||||||
|
'l10n_at_hr_payroll_private.structure_type_at_privat').id})
|
||||||
|
wizard = self.env['l10n.at.payroll.endabrechnung.wizard'].create({
|
||||||
|
'employee_id': emp.id,
|
||||||
|
'austrittsdatum': date(2026, 3, 15),
|
||||||
|
'beendigungsart': 'einvernehmlich',
|
||||||
|
'uel_tage': 11.0,
|
||||||
|
'abgang_betrag': 9000.0,
|
||||||
|
'abgang_ffrist_tage': 42.0,
|
||||||
|
'zeit_guthaben_stunden': 10.0,
|
||||||
|
'vorschuss_saldo': 500.0,
|
||||||
|
})
|
||||||
|
ergebnis = wizard.action_payslip()
|
||||||
|
slip = self.env['hr.payslip'].browse(ergebnis['res_id'])
|
||||||
|
codes = set(slip.input_line_ids.mapped(
|
||||||
|
lambda line: line.input_type_id.code))
|
||||||
|
self.assertIn('ATP_UEL_TAGE', codes)
|
||||||
|
self.assertIn('ATP_ABGANG', codes)
|
||||||
|
self.assertIn('ATP_ABRECHNUNG_VORSCHUSS', codes)
|
||||||
|
self.assertAlmostEqual(
|
||||||
|
self._total(slip, 'URL_ERSATZ_LFD'), 1320.0, places=2)
|
||||||
|
self.assertAlmostEqual(self._total(slip, 'ABGANG'), 9000.0,
|
||||||
|
places=2)
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<!-- AP14-A: Endabrechnungs-Wizard (Beendigung) — sammelt UEL, -->
|
||||||
|
<!-- Abgangsentschädigung, Zeitguthaben/-schulden und offene -->
|
||||||
|
<!-- Vorschüsse und legt den Endabrechnungs-Lauf vorbefüllt an. -->
|
||||||
|
<record id="endabrechnung_wizard_form_view" model="ir.ui.view">
|
||||||
|
<field name="name">l10n.at.payroll.endabrechnung.wizard.form</field>
|
||||||
|
<field name="model">l10n.at.payroll.endabrechnung.wizard</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<form string="Endabrechnung (Beendigung)">
|
||||||
|
<group>
|
||||||
|
<group string="Beendigung">
|
||||||
|
<field name="employee_id"/>
|
||||||
|
<field name="austrittsdatum"/>
|
||||||
|
<field name="beendigungsart"/>
|
||||||
|
</group>
|
||||||
|
<group string="Offene Posten">
|
||||||
|
<field name="uel_tage"/>
|
||||||
|
<field name="zeit_guthaben_stunden"/>
|
||||||
|
<field name="zeit_schulden_stunden"/>
|
||||||
|
<field name="abgang_betrag"/>
|
||||||
|
<field name="abgang_ffrist_tage"
|
||||||
|
invisible="not abgang_betrag"/>
|
||||||
|
<field name="vorschuss_saldo"/>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
<group string="Vorschau (§ 10 UrlG, § 11 Abs 2 ASVG)">
|
||||||
|
<group>
|
||||||
|
<field name="uel_lfd_vorschau"/>
|
||||||
|
<field name="uel_sz_vorschau"/>
|
||||||
|
</group>
|
||||||
|
<group>
|
||||||
|
<field name="sv_tage_vorschau"/>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
<footer>
|
||||||
|
<button name="action_payslip" type="object"
|
||||||
|
string="Endabrechnungs-Lauf anlegen"
|
||||||
|
class="btn-primary"/>
|
||||||
|
<button string="Abbrechen" special="cancel"
|
||||||
|
class="btn-secondary"/>
|
||||||
|
</footer>
|
||||||
|
</form>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="action_endabrechnung_wizard" model="ir.actions.act_window">
|
||||||
|
<field name="name">Endabrechnung (Beendigung)</field>
|
||||||
|
<field name="res_model">l10n.at.payroll.endabrechnung.wizard</field>
|
||||||
|
<field name="view_mode">form</field>
|
||||||
|
<field name="target">new</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<menuitem id="menu_l10n_at_payroll_endabrechnung"
|
||||||
|
name="Endabrechnung (Beendigung)"
|
||||||
|
parent="hr_work_entry_enterprise.menu_hr_payroll_root"
|
||||||
|
action="action_endabrechnung_wizard" sequence="42"/>
|
||||||
|
</odoo>
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
# Part of the odoo-at-payroll project. License: LGPL-3.
|
# Part of the odoo-at-payroll project. License: LGPL-3.
|
||||||
from . import kv_katalog_import
|
from . import kv_katalog_import
|
||||||
from . import bv_endabrechnung
|
from . import bv_endabrechnung
|
||||||
|
from . import endabrechnung
|
||||||
|
|||||||
@@ -0,0 +1,176 @@
|
|||||||
|
# Part of the odoo-at-payroll project. License: LGPL-3.
|
||||||
|
"""AP14-A: Endabrechnungs-Wizard — sammelt die offenen Posten der
|
||||||
|
Beendigungs-Abrechnung (Urlaubsersatzleistung, Abgangsentschädigung,
|
||||||
|
Zeitguthaben/-schulden, offene Vorschüsse) und legt den
|
||||||
|
Endabrechnungs-Lauf mit vorbefüllten Eingaben an. Reiner
|
||||||
|
Sammel-/Anlageprozess; die BV-Anwartschaft läuft über den eigenen
|
||||||
|
BV-Wizard (GP5), die Pfändung über die AP11-Maschine."""
|
||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
|
from odoo import _, api, fields, models
|
||||||
|
|
||||||
|
|
||||||
|
class L10nAtEndabrechnungWizard(models.TransientModel):
|
||||||
|
_name = 'l10n.at.payroll.endabrechnung.wizard'
|
||||||
|
_description = 'Endabrechnung (Beendigung des Dienstverhältnisses)'
|
||||||
|
|
||||||
|
employee_id = fields.Many2one(
|
||||||
|
'hr.employee', string='Dienstnehmer:in', required=True)
|
||||||
|
austrittsdatum = fields.Date(
|
||||||
|
string='Austritt (Ende des Arbeitsverhältnisses)', required=True)
|
||||||
|
beendigungsart = fields.Selection(
|
||||||
|
[('ag_kuendigung', 'Kündigung durch den Arbeitgeber'),
|
||||||
|
('an_kuendigung', 'Kündigung durch den Dienstnehmer'),
|
||||||
|
('einvernehmlich', 'Einvernehmliche Auflösung'),
|
||||||
|
('berechtigter_austritt', 'Berechtigter vorzeitiger Austritt'),
|
||||||
|
('unberechtigter_austritt',
|
||||||
|
'Unberechtigter vorzeitiger Austritt '
|
||||||
|
'(keine UEL für die 5./6. Woche, § 10 Abs 2 UrlG; '
|
||||||
|
'kein Zeitguthaben-Zuschlag, § 19e Abs 2 AZG)'),
|
||||||
|
('verschuldete_entlassung', 'Verschuldete Entlassung'),
|
||||||
|
('unverschuldete_entlassung', 'Unverschuldete Entlassung'),
|
||||||
|
('befristung', 'Zeitablauf (befristetes Dienstverhältnis)'),
|
||||||
|
('tod', 'Tod (UEL an die Erben, § 10 Abs 5 UrlG)')],
|
||||||
|
string='Beendigungsart', required=True)
|
||||||
|
uel_tage = fields.Float(
|
||||||
|
string='Offene Urlaubstage (Arbeitstage)',
|
||||||
|
help='Resturlaub früherer Urlaubsjahre zur Gänze plus aliquoter '
|
||||||
|
'Anspruch des laufenden Urlaubsjahres abzüglich verbrauchter '
|
||||||
|
'Tage, ohne Rundung (§ 10 UrlG, lb-url-16). Für '
|
||||||
|
'unberechtigten vorzeitigen Austritt sind die Tage der 5. '
|
||||||
|
'und 6. Urlaubswoche des laufenden Jahres herauszurechnen.')
|
||||||
|
abgang_betrag = fields.Monetary(
|
||||||
|
string='Abgangsentschädigung (Brutto, § 67 Abs 8 lit b EStG)',
|
||||||
|
currency_field='currency_id',
|
||||||
|
help='Entschädigung für den Verzicht auf die Arbeitsleistung '
|
||||||
|
'während der (fiktiven) Kündigungsfrist — LSt: Fünftelregelung; '
|
||||||
|
'SV: Umlegung auf den Kündigungsfrist-Zeitraum (§ 11 Abs 2 '
|
||||||
|
'ASVG); voll lohnnebenkostenpflichtig.')
|
||||||
|
abgang_ffrist_tage = fields.Float(
|
||||||
|
string='Kündigungsfrist (Kalendertage)',
|
||||||
|
help='Kalendertage der (fiktiven) Kündigungsfrist, auf die die '
|
||||||
|
'pauschalierte Abgangsentschädigung sv-rechtlich umzulegen '
|
||||||
|
'ist (§ 11 Abs 2 ASVG).')
|
||||||
|
zeit_guthaben_stunden = fields.Float(
|
||||||
|
string='Zeitguthaben an Normalarbeitszeit (Stunden)',
|
||||||
|
help='Offenes Guthaben an Normalarbeitszeit aus durchrechenbarem '
|
||||||
|
'Arbeitszeitmodell — Abgeltung mit 50 % Zuschlag (§ 19e '
|
||||||
|
'Abs 2 AZG); bei unberechtigtem vorzeitigem Austritt kein '
|
||||||
|
'Zuschlag.')
|
||||||
|
zeit_schulden_stunden = fields.Float(
|
||||||
|
string='Zeitschulden (Stunden)',
|
||||||
|
help='Offene Zeitschulden (negative Zeitguthaben), abgerechnet '
|
||||||
|
'mit der aktuellen Normalstunde (lb-end-08).')
|
||||||
|
vorschuss_saldo = fields.Monetary(
|
||||||
|
string='Offener Vorschuss-/Darlehenssaldo',
|
||||||
|
currency_field='currency_id',
|
||||||
|
help='Bei Beendigung zur Gänze fälliger, noch offener '
|
||||||
|
'Vorschusssaldo (lb-end-06) — Rückverrechnung vom Netto '
|
||||||
|
'nach SV und Lohnsteuer, ohne Abgabenberührung '
|
||||||
|
'(§ 293 Abs 3 EO).')
|
||||||
|
currency_id = fields.Many2one(
|
||||||
|
'res.currency', default=lambda self: self.env.company.currency_id)
|
||||||
|
|
||||||
|
uel_lfd_vorschau = fields.Monetary(
|
||||||
|
string='UEL laufender Anteil (Vorschau)',
|
||||||
|
currency_field='currency_id', compute='_compute_vorschau')
|
||||||
|
uel_sz_vorschau = fields.Monetary(
|
||||||
|
string='UEL Sonderzahlungsanteil (Vorschau)',
|
||||||
|
currency_field='currency_id', compute='_compute_vorschau')
|
||||||
|
sv_tage_vorschau = fields.Integer(
|
||||||
|
string='SV-Verlängerungstage (§ 11 Abs 2)',
|
||||||
|
compute='_compute_vorschau')
|
||||||
|
|
||||||
|
@api.onchange('employee_id')
|
||||||
|
def _onchange_employee(self):
|
||||||
|
for wizard in self:
|
||||||
|
if wizard.employee_id:
|
||||||
|
wizard.austrittsdatum = (
|
||||||
|
wizard.employee_id.version_id.contract_date_end
|
||||||
|
or fields.Date.today())
|
||||||
|
|
||||||
|
@api.depends('employee_id', 'austrittsdatum', 'uel_tage')
|
||||||
|
def _compute_vorschau(self):
|
||||||
|
for wizard in self:
|
||||||
|
wizard.uel_lfd_vorschau = 0.0
|
||||||
|
wizard.uel_sz_vorschau = 0.0
|
||||||
|
wizard.sv_tage_vorschau = 0
|
||||||
|
if not wizard.employee_id or not wizard.austrittsdatum:
|
||||||
|
continue
|
||||||
|
slip = wizard._vorschau_payslip()
|
||||||
|
wizard.uel_lfd_vorschau = slip._at_uel_lfd()
|
||||||
|
wizard.uel_sz_vorschau = slip._at_uel_sz()
|
||||||
|
wizard.sv_tage_vorschau = slip._at_end_sv_tage(
|
||||||
|
wizard.uel_tage)
|
||||||
|
|
||||||
|
def _vorschau_payslip(self):
|
||||||
|
"""Flüchtiger Payslip-Kontext für die Vorschau-Berechnung (kein
|
||||||
|
Speichern): trägt Austrittsmonat, Struktur und die Eingaben."""
|
||||||
|
self.ensure_one()
|
||||||
|
erster = self.austrittsdatum.replace(day=1)
|
||||||
|
letzter = (erster + timedelta(days=32)).replace(day=1) \
|
||||||
|
- timedelta(days=1)
|
||||||
|
struct = self.employee_id.version_id.structure_id
|
||||||
|
if not struct:
|
||||||
|
struct = self.env.ref(
|
||||||
|
'l10n_at_hr_payroll_private.structure_at_privat')
|
||||||
|
slip = self.env['hr.payslip'].new({
|
||||||
|
'employee_id': self.employee_id.id,
|
||||||
|
'struct_id': struct.id,
|
||||||
|
'date_from': erster,
|
||||||
|
'date_to': letzter,
|
||||||
|
})
|
||||||
|
eingaben = [('ATP_UEL_TAGE', self.uel_tage)]
|
||||||
|
slip.input_line_ids = [
|
||||||
|
(0, 0, {
|
||||||
|
'input_type_id': self.env['hr.payslip.input.type']
|
||||||
|
.search([('code', '=', code)], limit=1).id,
|
||||||
|
'amount': betrag,
|
||||||
|
})
|
||||||
|
for code, betrag in eingaben if betrag
|
||||||
|
]
|
||||||
|
return slip
|
||||||
|
|
||||||
|
def _eingaben(self):
|
||||||
|
self.ensure_one()
|
||||||
|
return [
|
||||||
|
('ATP_UEL_TAGE', self.uel_tage),
|
||||||
|
('ATP_ABGANG', self.abgang_betrag),
|
||||||
|
('ATP_ABGANG_FFRIST_TAGE', self.abgang_ffrist_tage),
|
||||||
|
('ATP_ZEIT_GUTHABEN', self.zeit_guthaben_stunden),
|
||||||
|
('ATP_ZEIT_SCHULDEN', self.zeit_schulden_stunden),
|
||||||
|
('ATP_ABRECHNUNG_VORSCHUSS', self.vorschuss_saldo),
|
||||||
|
]
|
||||||
|
|
||||||
|
def action_payslip(self):
|
||||||
|
"""Endabrechnungs-Lauf im Austrittsmonat anlegen und die
|
||||||
|
gesammelten offenen Posten als Eingaben vorbefüllen."""
|
||||||
|
self.ensure_one()
|
||||||
|
erster = self.austrittsdatum.replace(day=1)
|
||||||
|
letzter = (erster + timedelta(days=32)).replace(day=1) \
|
||||||
|
- timedelta(days=1)
|
||||||
|
struct = self.employee_id.version_id.structure_id \
|
||||||
|
or self.env.ref('l10n_at_hr_payroll_private.structure_at_privat')
|
||||||
|
slip = self.env['hr.payslip'].create({
|
||||||
|
'name': _('Endabrechnung %s') % self.employee_id.name,
|
||||||
|
'employee_id': self.employee_id.id,
|
||||||
|
'struct_id': struct.id,
|
||||||
|
'date_from': erster,
|
||||||
|
'date_to': letzter,
|
||||||
|
})
|
||||||
|
typen = {code: self.env['hr.payslip.input.type'].search(
|
||||||
|
[('code', '=', code)], limit=1)
|
||||||
|
for code, _betrag in self._eingaben()}
|
||||||
|
self.env['hr.payslip.input'].create([{
|
||||||
|
'payslip_id': slip.id,
|
||||||
|
'input_type_id': typen[code].id,
|
||||||
|
'amount': betrag,
|
||||||
|
} for code, betrag in self._eingaben() if betrag])
|
||||||
|
slip.compute_sheet()
|
||||||
|
return {
|
||||||
|
'type': 'ir.actions.act_window',
|
||||||
|
'name': _('Endabrechnung'),
|
||||||
|
'res_model': 'hr.payslip',
|
||||||
|
'view_mode': 'form',
|
||||||
|
'res_id': slip.id,
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user