Compare commits

...

4 Commits

Author SHA1 Message Date
fegger 0abc1383d1 [FIX] l10n_at_gemeinde_payroll: Modul erstmals installierbar — 9 Odoo-19-Latenzfehler; BDA-Vordienstzeiten-Vorschlag (AP10+AP9)
AP10 — Latenzfehler des nie installierten Vorlaufs:
- regular_pay auf hr.payroll.structure entfernt (Feld existiert nicht)
- is_paid/round_days/round_days_type von WET-Records entfernt
- Falscher XML-ID-Praefix payroll_structure_bgld. -> Modul-Referenzen
- amount_select 'python' -> 'code' (Selection)
- import in Regelcode entfernt (safe_eval verbietet IMPORT_NAME)
- rules['ME'].total -> result_rules['ME']['total'] (Odoo 19 befuellt
  rules nie; SZQ-Sonderzahlung rechnete gegen Null)
- categories.X -> categories['X'] (NETTO-Regel)
- work_time_rate /100 entfernt (Odoo 19 speichert Bruchteil 0..1)
- nie gelaufene Tests: Payslip-name-Pflichtfeld, Stichtag vor
  Katalog-Gueltigkeit (Wizard-Skip korrekt), date_start ist Compute
  aus contract_date_start

AP9 — BDA-Vordienstzeiten (GemBG S 67 Abs 1-3, RIS LrB GesNr
20001002 verifiziert): Feld vordienstzeiten_monate (Help dokumentiert
Anrechnungskatalog inkl. Obergrenzen), Compute bda_vorschlag
(Vertragsbeginn - Monate), Uebernahme-Button als Verwaltungsakt,
View-Erweiterung
2026-09-12 23:18:07 +02:00
fegger 9b4221fcef [IMP] l10n_at_hr_payroll_private: Wr. DAG, Buchungsbeleg, VWGJ-Einstufung, EFZG-Teilentgelt mit fiktiver BV (AP2d+AP3+AP9+AP4)
AP3 — Wiener Dienstgeberabgabe (U-Bahn-Steuer, RIS LrW GesNr
20000133 verifiziert): Regel WIEN_DAG (Kopfpauschale 2 Euro je DN
und angefangene Woche, Wochenlogik Montag-im-Monat, Befreiungen
Lehrling/55+/<=10 WStd/Karenz-Woche), Company-Flag-Verwertung,
Parameter at_wien_dag_satz, FI 6663/3560 (Referenz-Rest)

AP2d — Buchungsbeleg-Report am Lohnlauf (Konto/Belegtext/Soll/Haben
der FI-Buchung, balancierte Summen; test_account_private-Erweiterung)

AP9 — Vordienstzeiten -> Einstufung: Stufen-Schwelle vwgj_ab an
l10n.at.payroll.kv.gruppe.stufe (aus Labels 'nach N VWGJ'; -1 =
nicht gepflegt, ORM liest Integer-NULL als 0), CSV-Seed,
_get_stufe_fuer_vwgj/_get_naechste_vwgj, Vertragsfeld
l10n_at_vordienstzeit_monate + VWGJ-Vorschlag + Vorrueckungstermin
(S 17 Abs 5 Rahmen-KV, Erster des Erreichungsmonats) + Uebernahme-
Button (manueller Verwaltungsakt)

AP4 — EFZG/Teilentgelt (AngG S 8/EFZG S 2 verifiziert, Gleichlauf
6/8/10/12+4 Wochen; lb-krs-07/08): EFZ-Phasenmaschine models/efzg.py
(Kalendertage-Kontingente je Arbeitsjahr, Krank-/AU-Topf getrennt,
AU-Urlaubsart ATP_AU), Regeln TEIL_ENTGELT_HALB/-50 % und
KRANKENTGELT_PHASE/-100 % je Arbeitstag (Tagesatz aus unreduzierter
Basis), fiktive BV-Grundlage S 7 Abs 3 BMSVG im _at_bv_basis_laufen-
Hook (50 % Vormonatsentgelt x Bezugs-KT/30), Parameter at_efz_stufen;
Krankengeldumlage S 62 ASVG = Falsch-Praemise (kein Baustein);
Rechenfaelle docs/rechenfaelle/efzg.md
2026-09-12 23:17:57 +02:00
fegger 34e6750fe3 [IMP] l10n_at_hr_payroll_private: mBGM liest TASY-Katalog (BSGR/ERGB), AT-Abwesenheiten mit hr_holidays (AP0+AP1+AP5)
AP0 — mBGM-Tarifierung aus dem TASY-Katalog:
- Katalog vor Hand-Parameter at_mbgm_tarif (quelle katalog/parameter);
  Company-Feld l10n_at_dienstgebergruppe (Default NBALG),
  Vertrags-m2m l10n_at_ergaenzung_ids; ERGB wird im T1 real belegt
- BSGR-Korrektur nach TASY-Bezeichnern: Lehrlinge Arbeiter->B045 /
  Angestellte->B044 (invertiert bisher), geringfuegig/fallweise
  B010/B030 je Personalgruppe
- NSchAB-Feld: Falschbezeichnung Schlechtwetter -> Nachtschwerarbeit
  (3,80 % = NB-Zweig DG ab 2020); Flag nur noch Parameter-Fallback
- Tests: Katalog schlaegt manipulierten Parameter, ERGB-Emission,
  BSGR-Mapping

AP1 — M4-Test auf explizit ungemappte Abgangsursache (AGRD 00-Fallback)

AP5 — hr_holidays/hr_work_entry_holidays als Pflicht-Dependence:
- AT-WETs ATP_URLAUB/ATP_KRANK/ATP_KARENZ (is_leave) + AT-Urlaubsarten
  (UrlG, Krankenstand mit Krankmeldungspflicht, unbezahlte Karenz)
  nach Odoo-Lokalisierungsmuster; ATP_AU (Arbeitsunfall) folgt mit AP4
- Test: validierter Krankenstand erzeugt Work Entries mit AT-WET
2026-09-12 23:17:38 +02:00
fegger 3c85cc6790 [IMP] l10n_at_hr_payroll: TASY-Tarifsystem-Katalog, AGRD-Auswahl, DG-Kosten-/Lohnkonten-Reports, Wr.-DAG-Grundlagen (AP0-AP3)
AP0 Variante b — TASY-Tarifsystem-Katalog:
- Neue Katalogmodelle (BSGR/ERGB/Dienstgebergruppen/Tarifgruppen/
  Tarifsets/Tarife/Zuordnungen/Zu-Abschläge inkl. Steuerung),
  models/tasy_katalog.py — Auflösung Tarifgruppe (BSGR+ERGB+DGG)
  -> Sätze je Verrechnungsbasis
- Import-Wizard l10n.at.payroll.tasy.katalog.wizard (alle Träger,
  sektionsscharf, idempotent), Views/Menüs, Security
- Realexport-Test gegen TASY-LSWH-Export (B001/B002 = 39,05/37,55 %)

AP1 — eSV-Abmeldegründe (DM-ORG D.22):
- l10n_at_agrd Char -> vollständige Selection (33 Codes),
  Seed der Standard-Abgangsursachen (noupdate, hr-Defaults vorverbunden),
  Form-/Listen-Erbe

AP2a-c — Auswertungen:
- Monatsjournal: eigener Dienstgeberanteile-Block (ATP_DG vorher
  still ausgefiltert); Fix: models/monatsreports.py war nie in
  models/__init__.py importiert (GP7-Reports waren toter Code)
- Neuer Report Dienstgeber-Gesamtkosten (Brutto + LNK aufgeschlüsselt/
  kumuliert je AN + Laufsummen, Spalten FLAF/KommSt/DZ/BV/Wr. DAG)
- Jahreslohnkonto je DN + Betriebsjahreslohnkonto (Jahresbericht-
  Wizard, Monatsmatrix je Lohnart), report/jahresreports.xml

AP3 — Wr. Dienstgeberabgabe (RIS LrW GesNr 20000133 verifiziert):
- Company-Flag l10n_at_wien_dag (Kern), Fristenarten wien_dag/
  wien_dag_jahres (cron-geflaggt, 15./31. 3.), Company-View-Feld,
  Wr.-DAG-Spalte im DG-Kosten-Report
2026-09-12 23:17:00 +02:00
52 changed files with 4493 additions and 280 deletions
+165
View File
@@ -2,6 +2,153 @@
## Current focus ## Current focus
**AP 4 EFZG/Teilentgelt umgesetzt — 144/144 Tests grün**
(2026-09-12):
- **Rechtsverifikation:** lb-krs-08-⚠Marke erledigt — EFZG von RIS
beschafft (Bundesnormen **GesNr 10008308**,
.firecrawl/ris/EFZG_*GeltendeFassung.html) und **§ 2 Abs 1/3/5
verifiziert**; § 8 Abs 1/2/2a AngG lokal verifiziert —
**Gleichlauf bestätigt** (6/8/10/12 Wochen bei 0/1/15/25
vollendeten DJ + je 4 Wochen halb). „56 Tage" = AU-Topf 8 Wochen
(lb-asc-15), KEINE eigene Regel. **KUG § 62 ASVG = Falsch-
Prämisse** (RECHTSQUELLEN-Privat 2026-09-10: § 62 regelt
Beitragsabrechnung; deutsches U1-Vorbild) — kein Baustein.
- **EFZ-Maschine** `models/efzg.py`: `_at_efz_status()`
Kalendertage-Kontingente je Arbeitsjahr (Eintrittsdatum-Rhythmus),
Krank- und AU-Topf getrennt (AU nur voll), Stufe je Tag
(Stufensprung verlängert), Phasen je Arbeitstag aus den
ATP_KRANK/ATP_AU-Work Entries (neue Urlaubsart Arbeitsunfall);
Episode-Beginn für fiktive BV; Info-Feld am Payslip.
- **Regeln** TEIL_ENTGELT_HALB (50 %/AT) + KRANKENTGELT_PHASE
(100 %/AT) als negative Brutto-Zeilen (seq 45/46, Tagesatz =
unreduzierte laufende Basis ÷ Arbeitstage —
`_at_efz_tagesatz(result_rules)` rechnet die eigenen Reduktionen
heraus); SV/LSt/LNK folgen über die reduzierte laufende Basis
(lb-krs-07).
- **Fiktive BV § 7 Abs 3 BMSVG** in `_at_bv_basis_laufen`-Hook:
50 % Vormonatsentgelt (Episode-Beginn-Monat davor, SZ ausgenommen)
× Halb-/ÖGK-Kalendertage/30 — Halbphase zählt mit (Krankengeld
ruht zur Hälfte, Quellbeispiel lb-krs-07); fließt in BVG-Zeile
und mBGM-BV-Basis.
- Parameter `at_efz_stufen` (data/rule_parameters_efzg_private.xml);
Rechenfälle docs/rechenfaelle/efzg.md; 6 Tests (Übergang,
Kontingent, Stufe 8 Wo, AU-Topf, fiktive BV, Wirkungslos ohne
Stand). November-B13-SZ-Bezug in BV-Grundlage inklusive (§ 6
BMSVG) — Tests komponentenweise.
- Version: Privat **19.0.9.2.0**. 144/144 (64+106+18) frischer
-i-Lauf, py/XML-Sweep.
- **Offen:** AP 7 (Wochengeld/Mutterschutz), AP 8 (AEB SART 70/75)
— EFZ-WETs sind verdrahtet; dokumentierte Reste: Kalenderjahr-
Umstellung § 8 Abs 9 AngG, ÜStd-/Provisionsdurchschnitte, SZ-
Kürzung „kann"-Regel.
**AP 9 Vordienstzeiten/Einstufung umgesetzt — 138/138 Tests grün**
(2026-09-12):
- **Rechtsbeschaffung:** Bgld. GemBG 2014 via RIS **LrB GesNr
20001002** (geltende Fassung, Quelle
`.firecrawl/ris/bgld/GemBG_2014_GeltendeFassung.html`) — **§ 67
verifiziert**: BDA = Dienstzeit + anrechenbare Vordienstzeiten
(Abs 1); Pflicht-Anrechnung EWR-ÖD/EU-Dienste (Abs 2 Z 1 f),
Grundwehr/Ausbildung max 6 M, Zivil/Ersatz max 9 M (Z 4),
einschlägige Berufstätigkeit/Verwaltungspraktikum **max 10 Jahre**
(Abs 3). Rahmen-KV § 17 (SI-2203 Volltext) verifiziert: Abs 4
Anzahl angerechneter VWGJ durch Firmenleitung + Dienstzettel,
Abs 5 Vorrückung am **Ersten des Erreichungsmonats**.
- **Privat (AP9b):** Stufen-Neu-Feld `vwgj_ab` (aus Labels "nach N
VWGJ", 0=ab Beginn, **-1=nicht gepflegt** — ORM liest Integer-
NULL als 0, deshalb -1-Konvention + Domain `> -1`); CSV-Seed um
vwgj_ab ergänzt (131 Zeilen); Gruppen-Methoden
`_get_stufe_fuer_vwgj`/`_get_naechste_vwgj` (kollektivvertrag.py);
Vertragsfeld `l10n_at_vordienstzeit_monate` + Compute
`l10n_at_vwgj_monate`/`l10n_at_vwgj_stufe_vorschlag`/
`l10n_at_naechste_vorrueckung` (§ 17 Abs 5) + Button
`action_einstufung_uebernehmen` (bewusst manueller
Verwaltungsakt); KV-Einstufungs-Gruppe im View erweitert.
Tests: test_einstufung_private 4 (freeze_time für Determinismus).
- **GemBG (AP9a):** `vordienstzeiten_monate` (Help dokumentiert den
kompletten §-67-Katalog) + Compute `bda_vorschlag` (= Beginn
Monate) + Button `action_bda_uebernehmen`; View am
Besoldungs-Block. Test test_06 (BDA-Übernahme triggert
§-66-Abs-2-Vorrückungstermin).
- Versionen: Privat **19.0.9.1.0**, GemBG **19.0.1.6.0**.
Validierung: 138/138 frischer -i-Lauf, py/XML-Sweep, DB entfernt.
- **Offen:** AP 4 (EFZG/Teilentgelt + fiktive BV), AP 7
(Wochengeld/Mutterschutz), AP 8 (AEB SART 70/75).
**AP 5 + AP 2ad + AP 3 + AP 10 umgesetzt — 133/133 Tests grün**
(2026-09-12, Folgeturn nach AP0/AP1, Reihenfolge lt. Plan):
- **AP5 Abwesenheiten:** hr_holidays + hr_work_entry_holidays als
Pflicht-Dependence des Privatmoduls; AT-WETs ATP_URLAUB/ATP_KRANK/
ATP_KARENZ (is_leave) + country-gebundene AT-Urlaubsarten mit
WET-Verknüpfung (data/absence_private.xml, Odoo-Lokalisations-
muster); Karenz-Wochen erkennbar via Work Entries.
- **AP2ad Reports:** Monatsjournal mit eigenem **Dienstgeberanteile-
Block** (ATP_DG-Kategorie, vorher still ausfiltert), neuer Report
**Dienstgeber-Gesamtkosten** (je AN Brutto + LNK aufgeschlüsselt/kumuliert
+ Laufsummen, Spalten FLAF/KommSt/DZ/BV/Wr. DAG), **Jahreslohnkonto
je DN** + **Betriebsjahreslohnkonto** (Wizard + Monatsmatrix,
models/monatsreports.py + report/jahresreports.xml), **Buchungsbeleg**
am Lohnlauf (private, models/buchungsbeleg.py).
**Latenz-Bug:** models/monatsreports.py war NIE in models/__init__.py
importiert — sämtliche GP7-Reportmodelle waren toter Code!
- **AP3 U-Bahn-Steuer:** Wr. DAG via RIS **verifiziert** (LrW GesNr
20000133, Fassung 12.09.2026, Quelle .firecrawl/ris/wien/WDGAG_*.html;
2 €/Woche § 5, Befreiungen § 3, 15./31.3. § 6, Rückerstattung § 7
218,02/2.180,19 €) — Implementierung: Company-Flag l10n_at_wien_dag
(Kern), Regel WIEN_DAG (models/wien_dag.py — Wochenlogik Montag-im-
Monat + Befreiungen Lehrling/55+/≤10WStd/Karenz-Woche), Fristenarten
wien_dag/wien_dag_jahres (cron-geflaggt), FI 6663 (3560 Referenz-
rest), DG-Kosten-Spalte. **Wochenlektüre „Montag beginnt im Monat" =
Praxis-Annahme, Parallellauf verifizieren.**
- **AP10 GemBG-Fixpaket:** Modul erstmals **installierbar** — 9
Latenzfehler behoben: regular_pay, is_paid+round_days* auf WETs,
falscher XML-ID-Präfix (payroll_structure_bgld.), amount_select
'python'→'code', safe_eval-Import, rules→result_rules (Odoo 19
befült rules NIE), categories['X'], work_time_rate-÷100 (Bruchteil
0..1), nie gelaufene Tests (Payslip-name, Stichtag vor Katalog-
gültigkeit, date_start ist Compute aus contract_date_start).
- Versionen: Kern **19.0.6.0.0**, Privat **19.0.9.0.0**, GemBG
**19.0.1.5.0**. Validierung: 133/133 (17 GemBG + 64 Kern + 92
Privat), frischer -i-Lauf aller 3 Module, py/XML-Sweep, DB entfernt.
- **Offen:** AP 9 (Vordienstzeiten — jetzt entblockt), AP 4 (EFZG/
Teilentgelt), AP 7 (Wochengeld/Mutterschutz), AP 8 (AEB SART 70/75).
**AP0 TASY-Katalog (Variante b) + AP1 AGRD umgesetzt und grün (109/109
Tests)** (2026-09-12, Nutzer-Freigabe des AP-Plans mit
Scope-Entscheidungen): — Kernmodul: neue Katalogmodelle
`models/tasy_katalog.py` (10 Modelle: tasy.bsgr/ergaenzung/
dienstgebergruppe/tarifgruppe/tarifset/tarif/tarifset.zuord/
zuabschlag/zuabschlag.steuerung [+ m2m]), Import-Wizard
`l10n.at.payroll.tasy.katalog.wizard` in `models/tasy.py`
(sektionsscharf — root.iter() fasste auch Referenz-Elemente!),
Sicherheit, Views/Menus `views/tasy_katalog_views.xml`;
Auflösungs-API `_l10n_at_find(bsgr, ergs, dgg, stichtag)`
`_l10n_at_saetze(basis, stichtag)`. Privatmodul: mBGM liest Katalog
vor `at_mbgm_tarif` (quellen-Fallback), Company-Feld
`l10n_at_dienstgebergruppe` (Default NBALG), Vertrags-m2m
`l10n_at_ergaenzung_ids`, **ERGB wird real belegt** (15 Stellen im T1),
NSchAB-Flag nur noch Parameter-Fallback. **BSGR-Korrektur:**
Lehrlinge arbeiter→**B045**, angestellte→**B044** (TASY: B044 =
„Angestelltenlehrlinge"), geringf./fallweise→**B010/B030** je
Personalgruppe; **NSchAB-Feldumbenennung** Schlechtwetter→Nachtschwer-
arbeit (3,80 % = NB-Zweig DG ab 2020, verifiziert; SW-Zweig = 0,7/0,7).
**AP1**`l10n_at_agrd` Char→Selection mit vollständiger D.22-Liste
(33 Codes, lokal verifiziert), Seed `data/hr_departure_reason_seed.xml`
(noupdate; hr-Defaults fired→01/resigned→02/retired→16 vorverbunden +
7 AT-Ursachen), View-Erbe auf Abgangsursachen-Form/Liste.
Nutzer-Entscheidungen: hr_holidays als **Pflicht-Dependence**;
Wochengeld **volle Berechnung**; TASY-Import **alle Träger**; AGRD
Migration OK; Buchungsbeleg **Lohnlauf-Ebene**; GemBG-Fix-Paket **ja**.
Validierung: 109/109 grün auf Wegwerf-DB (frischer -i-Lauf;
test_tasy_katalog 5 inkl. Realexport — Fund: altersgrenzeMinJahr
ist **Dezimal** (60.5) → Float-Felder), py/XML-Sweep ok, DB entfernt.
**Nächste APs:** AP 5 (holidays/WETs), AP 2a-d (Reports), AP 3
(U-Bahn nach RIS-Klärung), AP 4/7/8 (EFZG/Wochengeld/AEB), AP 10/9
(GemBG-Fix → Vordienstzeiten).
**GP9 UI/Security/Doku umgesetzt — GP1GP9 komplett, AP9 **GP9 UI/Security/Doku umgesetzt — GP1GP9 komplett, AP9
abgeschlossen, Projekt M8 final** (2026-09-12): Kern 19.0.5.1.0 — abgeschlossen, Projekt M8 final** (2026-09-12): Kern 19.0.5.1.0 —
Company-Stammdaten-UI (BKNR/LStNr/Bundesland/WKO/WF-Wien — Reiter Company-Stammdaten-UI (BKNR/LStNr/Bundesland/WKO/WF-Wien — Reiter
@@ -1078,6 +1225,24 @@ B17-Inhalt/B18; Kleinverifikationen s. KB-KONFLIKTE „Nächste Schritte“).
`IMPLEMENTIERUNGSPLAN-Bgld.md` Abschnitt 8. `IMPLEMENTIERUNGSPLAN-Bgld.md` Abschnitt 8.
- Odoo-Upgrades künftig in beiden Repos (gem360 + dieses) gegen denselben - Odoo-Upgrades künftig in beiden Repos (gem360 + dieses) gegen denselben
Odoo-Pin validieren. Odoo-Pin validieren.
- **Audit ÖGK-Tarifgruppensystem (2026-09-12, Nutzer-Audit-Frage):** Um-
fang nur „Normalbetriebe Allgemeiner Teil": mBGM-BSGR B001/B002 mit
normativ korrekten T01 39,05 %/T02 37,55 % (gegen TASY-LSWH-Export
2026-07-01 verifiziert: KV 3,87/3,78, PV 10,25/12,55, AK 0,5 DN gilt
für B001 **und** B002, Kern-Splitting stimmt); ERGB (E01 NSch, E02 SW,
E03 Schulpflicht …) nie belegt — NSchAB läuft stattdessen über Company-
Flag +3,80 % auf B001; nur 6 Abschläge (A01A03/A09/A10/A15) von ~30
ÖGK-Handbuch-Abschlägen; keine Dienstgebergruppen-Konzept. **Zwei
BSGR-Befunde vor Parametrisierung fixen:** (1) `_l10n_at_bsgr` mappt
Lehrling+`arbeiter` auf **B044** — TASY: B044=Angestelltenlehrlinge,
B045=Arbeiterlehrlinge → **invertiert** (BBER-Mapping in versicher-
tenmeldung.py ist korrekt, D.39: 03=Arbeiterlehrling); (2) gering-
fügig/fallweise immer **B030** (Gfg. Ang.), B010 (Gfg. Arb.) wird nie
emittiert. Beide heute folgenlos (Lauf bricht ohne B044/B045/B010/
B030-Parameter listenweise ab), falsch sobald parametrisiert. TASY-
Import mappt nur 3 BBB + 8 Tarifzweige (erweiterbares Mapping-Modell,
kein BSGR/ERGB/Tarifgruppen-Import). Kein Code-Fix ohne Nutzer-
Freigabe (Audit-only).
## Decisions & conventions ## Decisions & conventions
@@ -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 - Gemeinde-Personalverrechnung (Bgld. GemBG)', 'name': 'Österreich - Gemeinde-Personalverrechnung (Bgld. GemBG)',
'version': '19.0.1.4.0', 'version': '19.0.1.6.0',
'author': 'ixsol / gem360', 'author': 'ixsol / gem360',
'website': 'https://gem360.at', 'website': 'https://gem360.at',
'category': 'Human Resources/Payroll', 'category': 'Human Resources/Payroll',
@@ -16,7 +16,6 @@
<field name="name">Gemeindebedienstete (Bgld. GemBG 2014)</field> <field name="name">Gemeindebedienstete (Bgld. GemBG 2014)</field>
<field name="code">GEMBG.BGLD</field> <field name="code">GEMBG.BGLD</field>
<field name="type_id" ref="structure_type_gembg_bgld"/> <field name="type_id" ref="structure_type_gembg_bgld"/>
<field name="regular_pay" eval="True"/>
</record> </record>
<!-- ============================================================ --> <!-- ============================================================ -->
@@ -42,47 +41,32 @@
<!-- ============================================================ --> <!-- ============================================================ -->
<!-- Work-Entry-Types für GemBG-Komponenten --> <!-- Work-Entry-Types für GemBG-Komponenten -->
<!-- ============================================================ --> <!-- ============================================================ -->
<!-- Odoo 19: hr.work.entry.type hat weder is_paid noch
round_days/round_days_type mehr (Latenzfehler des Bgld-
Vorlaufs, entfernt). -->
<record id="work_entry_type_ueberstunden_tag" model="hr.work.entry.type"> <record id="work_entry_type_ueberstunden_tag" model="hr.work.entry.type">
<field name="name">Überstunden (tagsüber, § 76 GemBG)</field> <field name="name">Überstunden (tagsüber, § 76 GemBG)</field>
<field name="code">GEMBG_UEB_TAG</field> <field name="code">GEMBG_UEB_TAG</field>
<field name="round_days">NO</field>
<field name="round_days_type">DOWN</field>
<field name="is_paid" eval="True"/>
</record> </record>
<record id="work_entry_type_ueberstunden_nacht" model="hr.work.entry.type"> <record id="work_entry_type_ueberstunden_nacht" model="hr.work.entry.type">
<field name="name">Überstunden (Nachtzeit 22-6 Uhr, § 76 GemBG)</field> <field name="name">Überstunden (Nachtzeit 22-6 Uhr, § 76 GemBG)</field>
<field name="code">GEMBG_UEB_NACHT</field> <field name="code">GEMBG_UEB_NACHT</field>
<field name="round_days">NO</field>
<field name="round_days_type">DOWN</field>
<field name="is_paid" eval="True"/>
</record> </record>
<record id="work_entry_type_mehrdienst" model="hr.work.entry.type"> <record id="work_entry_type_mehrdienst" model="hr.work.entry.type">
<field name="name">Mehrdienstleistung (§ 76 GemBG)</field> <field name="name">Mehrdienstleistung (§ 76 GemBG)</field>
<field name="code">GEMBG_MEHR</field> <field name="code">GEMBG_MEHR</field>
<field name="round_days">NO</field>
<field name="round_days_type">DOWN</field>
<field name="is_paid" eval="True"/>
</record> </record>
<record id="work_entry_type_journaldienst" model="hr.work.entry.type"> <record id="work_entry_type_journaldienst" model="hr.work.entry.type">
<field name="name">Journaldienst (§ 79 GemBG)</field> <field name="name">Journaldienst (§ 79 GemBG)</field>
<field name="code">GEMBG_JOURNAL</field> <field name="code">GEMBG_JOURNAL</field>
<field name="round_days">NO</field>
<field name="round_days_type">DOWN</field>
<field name="is_paid" eval="True"/>
</record> </record>
<record id="work_entry_type_bereitschaft" model="hr.work.entry.type"> <record id="work_entry_type_bereitschaft" model="hr.work.entry.type">
<field name="name">Bereitschaftsdienst (§ 80 GemBG)</field> <field name="name">Bereitschaftsdienst (§ 80 GemBG)</field>
<field name="code">GEMBG_BEREIT</field> <field name="code">GEMBG_BEREIT</field>
<field name="round_days">NO</field>
<field name="round_days_type">DOWN</field>
<field name="is_paid" eval="True"/>
</record> </record>
<record id="work_entry_type_sonn_feiertag" model="hr.work.entry.type"> <record id="work_entry_type_sonn_feiertag" model="hr.work.entry.type">
<field name="name">Sonn-/Feiertagsdienst (§ 78 GemBG)</field> <field name="name">Sonn-/Feiertagsdienst (§ 78 GemBG)</field>
<field name="code">GEMBG_SONNFEI</field> <field name="code">GEMBG_SONNFEI</field>
<field name="round_days">NO</field>
<field name="round_days_type">DOWN</field>
<field name="is_paid" eval="True"/>
</record> </record>
<!-- ============================================================ --> <!-- ============================================================ -->
@@ -7,11 +7,11 @@
<field name="name">Monatsentgelt</field> <field name="name">Monatsentgelt</field>
<field name="code">ME</field> <field name="code">ME</field>
<field name="sequence" eval="1"/> <field name="sequence" eval="1"/>
<field name="category_id" ref="payroll_structure_bgld.category_gembg_brutto"/> <field name="category_id" ref="category_gembg_brutto"/>
<field name="struct_id" ref="payroll_structure_bgld.structure_gembg_bgld"/> <field name="struct_id" ref="structure_gembg_bgld"/>
<field name="condition_select">python</field> <field name="condition_select">python</field>
<field name="condition_python">result = bool(employee.entlohnungsgruppe_id and employee.entlohnungsstufe)</field> <field name="condition_python">result = bool(employee.entlohnungsgruppe_id and employee.entlohnungsstufe)</field>
<field name="amount_select">python</field> <field name="amount_select">code</field>
<field name="amount_python_compute"><![CDATA[ <field name="amount_python_compute"><![CDATA[
# Monatsentgelt nach Bgld. GemBG 2014 (IVa. Hauptstück §§ 133 ff). # Monatsentgelt nach Bgld. GemBG 2014 (IVa. Hauptstück §§ 133 ff).
# Katalog-Lookup: Entlohnungsgruppe + Stufe + Stichtag. # Katalog-Lookup: Entlohnungsgruppe + Stufe + Stichtag.
@@ -24,9 +24,10 @@ else:
# Ausbildungsphase: 5 % Abzug (§ 133h GemBG) # Ausbildungsphase: 5 % Abzug (§ 133h GemBG)
if employee.ausbildungsphase: if employee.ausbildungsphase:
base *= 0.95 base *= 0.95
# Teilzeit: anteilig über work_time_rate (§ 59 GemBG) # Teilzeit: anteilig über work_time_rate (§ 59 GemBG) — Odoo 19
work_rate = employee.work_time_rate or 100.0 # speichert work_time_rate als Bruchteil 0..1 (nicht Prozent).
result = base * work_rate / 100.0 work_rate = employee.work_time_rate or 1.0
result = base * work_rate
]]></field> ]]></field>
</record> </record>
@@ -37,8 +38,8 @@ else:
<field name="name">Kinderzulage (§ 61 GemBG)</field> <field name="name">Kinderzulage (§ 61 GemBG)</field>
<field name="code">KIZU</field> <field name="code">KIZU</field>
<field name="sequence" eval="2"/> <field name="sequence" eval="2"/>
<field name="category_id" ref="payroll_structure_bgld.category_gembg_brutto"/> <field name="category_id" ref="category_gembg_brutto"/>
<field name="struct_id" ref="payroll_structure_bgld.structure_gembg_bgld"/> <field name="struct_id" ref="structure_gembg_bgld"/>
<field name="condition_select">python</field> <field name="condition_select">python</field>
<field name="condition_python">result = employee.kinder_anzahl > 0</field> <field name="condition_python">result = employee.kinder_anzahl > 0</field>
<field name="amount_select">code</field> <field name="amount_select">code</field>
@@ -58,16 +59,16 @@ result = employee.kinder_anzahl * kiz_satz
<field name="name">Funktionszulage (§ 62 GemBG)</field> <field name="name">Funktionszulage (§ 62 GemBG)</field>
<field name="code">FUZU</field> <field name="code">FUZU</field>
<field name="sequence" eval="3"/> <field name="sequence" eval="3"/>
<field name="category_id" ref="payroll_structure_bgld.category_gembg_brutto"/> <field name="category_id" ref="category_gembg_brutto"/>
<field name="struct_id" ref="payroll_structure_bgld.structure_gembg_bgld"/> <field name="struct_id" ref="structure_gembg_bgld"/>
<field name="condition_select">python</field> <field name="condition_select">python</field>
<field name="condition_python">result = employee.funktionszulage > 0</field> <field name="condition_python">result = employee.funktionszulage > 0</field>
<field name="amount_select">code</field> <field name="amount_select">code</field>
<field name="amount_python_compute"><![CDATA[ <field name="amount_python_compute"><![CDATA[
# Funktionszulage § 62 GemBG — Satz am Dienstvertrag (hr.version). # Funktionszulage § 62 GemBG — Satz am Dienstvertrag (hr.version).
# Bei Teilzeit anteilig (wie Monatsentgelt). # Bei Teilzeit anteilig (wie Monatsentgelt); work_time_rate = Bruchteil.
work_rate = employee.work_time_rate or 100.0 work_rate = employee.work_time_rate or 1.0
result = employee.funktionszulage * work_rate / 100.0 result = employee.funktionszulage * work_rate
]]></field> ]]></field>
</record> </record>
@@ -78,8 +79,8 @@ result = employee.funktionszulage * work_rate / 100.0
<field name="name">Überstundenvergütung tagsüber (§ 76 GemBG)</field> <field name="name">Überstundenvergütung tagsüber (§ 76 GemBG)</field>
<field name="code">UEZ_TAG</field> <field name="code">UEZ_TAG</field>
<field name="sequence" eval="10"/> <field name="sequence" eval="10"/>
<field name="category_id" ref="payroll_structure_bgld.category_gembg_brutto"/> <field name="category_id" ref="category_gembg_brutto"/>
<field name="struct_id" ref="payroll_structure_bgld.structure_gembg_bgld"/> <field name="struct_id" ref="structure_gembg_bgld"/>
<field name="condition_select">python</field> <field name="condition_select">python</field>
<field name="condition_python">result = 'GEMBG_UEB_TAG' in worked_days and worked_days['GEMBG_UEB_TAG'].number_of_hours > 0</field> <field name="condition_python">result = 'GEMBG_UEB_TAG' in worked_days and worked_days['GEMBG_UEB_TAG'].number_of_hours > 0</field>
<field name="amount_select">code</field> <field name="amount_select">code</field>
@@ -100,8 +101,8 @@ else:
<field name="name">Überstundenvergütung Nachtzeit (§ 76 GemBG)</field> <field name="name">Überstundenvergütung Nachtzeit (§ 76 GemBG)</field>
<field name="code">UEZ_NACHT</field> <field name="code">UEZ_NACHT</field>
<field name="sequence" eval="11"/> <field name="sequence" eval="11"/>
<field name="category_id" ref="payroll_structure_bgld.category_gembg_brutto"/> <field name="category_id" ref="category_gembg_brutto"/>
<field name="struct_id" ref="payroll_structure_bgld.structure_gembg_bgld"/> <field name="struct_id" ref="structure_gembg_bgld"/>
<field name="condition_select">python</field> <field name="condition_select">python</field>
<field name="condition_python">result = 'GEMBG_UEB_NACHT' in worked_days and worked_days['GEMBG_UEB_NACHT'].number_of_hours > 0</field> <field name="condition_python">result = 'GEMBG_UEB_NACHT' in worked_days and worked_days['GEMBG_UEB_NACHT'].number_of_hours > 0</field>
<field name="amount_select">code</field> <field name="amount_select">code</field>
@@ -125,8 +126,8 @@ else:
<field name="name">Journaldienstzulage (§ 79 GemBG)</field> <field name="name">Journaldienstzulage (§ 79 GemBG)</field>
<field name="code">JZU</field> <field name="code">JZU</field>
<field name="sequence" eval="12"/> <field name="sequence" eval="12"/>
<field name="category_id" ref="payroll_structure_bgld.category_gembg_brutto"/> <field name="category_id" ref="category_gembg_brutto"/>
<field name="struct_id" ref="payroll_structure_bgld.structure_gembg_bgld"/> <field name="struct_id" ref="structure_gembg_bgld"/>
<field name="condition_select">python</field> <field name="condition_select">python</field>
<field name="condition_python">result = 'GEMBG_JOURNAL' in worked_days and worked_days['GEMBG_JOURNAL'].number_of_hours > 0</field> <field name="condition_python">result = 'GEMBG_JOURNAL' in worked_days and worked_days['GEMBG_JOURNAL'].number_of_hours > 0</field>
<field name="amount_select">code</field> <field name="amount_select">code</field>
@@ -146,8 +147,8 @@ result = tage * satz
<field name="name">Sonn-/Feiertagszulage (§ 78 GemBG)</field> <field name="name">Sonn-/Feiertagszulage (§ 78 GemBG)</field>
<field name="code">SFZU</field> <field name="code">SFZU</field>
<field name="sequence" eval="13"/> <field name="sequence" eval="13"/>
<field name="category_id" ref="payroll_structure_bgld.category_gembg_brutto"/> <field name="category_id" ref="category_gembg_brutto"/>
<field name="struct_id" ref="payroll_structure_bgld.structure_gembg_bgld"/> <field name="struct_id" ref="structure_gembg_bgld"/>
<field name="condition_select">python</field> <field name="condition_select">python</field>
<field name="condition_python">result = 'GEMBG_SONNFEI' in worked_days and worked_days['GEMBG_SONNFEI'].number_of_hours > 0</field> <field name="condition_python">result = 'GEMBG_SONNFEI' in worked_days and worked_days['GEMBG_SONNFEI'].number_of_hours > 0</field>
<field name="amount_select">code</field> <field name="amount_select">code</field>
@@ -174,8 +175,8 @@ else:
<field name="name">Bereitschaftsentschädigung (§ 80 GemBG)</field> <field name="name">Bereitschaftsentschädigung (§ 80 GemBG)</field>
<field name="code">BERB</field> <field name="code">BERB</field>
<field name="sequence" eval="14"/> <field name="sequence" eval="14"/>
<field name="category_id" ref="payroll_structure_bgld.category_gembg_brutto"/> <field name="category_id" ref="category_gembg_brutto"/>
<field name="struct_id" ref="payroll_structure_bgld.structure_gembg_bgld"/> <field name="struct_id" ref="structure_gembg_bgld"/>
<field name="condition_select">python</field> <field name="condition_select">python</field>
<field name="condition_python">result = 'GEMBG_BEREIT' in worked_days and worked_days['GEMBG_BEREIT'].number_of_hours > 0</field> <field name="condition_python">result = 'GEMBG_BEREIT' in worked_days and worked_days['GEMBG_BEREIT'].number_of_hours > 0</field>
<field name="amount_select">code</field> <field name="amount_select">code</field>
@@ -195,8 +196,8 @@ result = stunden * satz
<field name="name">Erschwerniszulage (§ 83 GemBG)</field> <field name="name">Erschwerniszulage (§ 83 GemBG)</field>
<field name="code">ERZU</field> <field name="code">ERZU</field>
<field name="sequence" eval="15"/> <field name="sequence" eval="15"/>
<field name="category_id" ref="payroll_structure_bgld.category_gembg_brutto"/> <field name="category_id" ref="category_gembg_brutto"/>
<field name="struct_id" ref="payroll_structure_bgld.structure_gembg_bgld"/> <field name="struct_id" ref="structure_gembg_bgld"/>
<field name="condition_select">python</field> <field name="condition_select">python</field>
<field name="condition_python">result = 'ERZU_INPUT' in inputs and inputs['ERZU_INPUT'].amount > 0</field> <field name="condition_python">result = 'ERZU_INPUT' in inputs and inputs['ERZU_INPUT'].amount > 0</field>
<field name="amount_select">code</field> <field name="amount_select">code</field>
@@ -212,8 +213,8 @@ result = inputs['ERZU_INPUT'].amount
<field name="name">Jubiläumszuwendung (§ 89 GemBG)</field> <field name="name">Jubiläumszuwendung (§ 89 GemBG)</field>
<field name="code">JUB</field> <field name="code">JUB</field>
<field name="sequence" eval="16"/> <field name="sequence" eval="16"/>
<field name="category_id" ref="payroll_structure_bgld.category_gembg_brutto"/> <field name="category_id" ref="category_gembg_brutto"/>
<field name="struct_id" ref="payroll_structure_bgld.structure_gembg_bgld"/> <field name="struct_id" ref="structure_gembg_bgld"/>
<field name="condition_select">python</field> <field name="condition_select">python</field>
<field name="condition_python">result = 'JUB_INPUT' in inputs and inputs['JUB_INPUT'].amount > 0</field> <field name="condition_python">result = 'JUB_INPUT' in inputs and inputs['JUB_INPUT'].amount > 0</field>
<field name="amount_select">code</field> <field name="amount_select">code</field>
@@ -229,8 +230,8 @@ result = inputs['JUB_INPUT'].amount
<field name="name">Fahrtkostenzuschuss (§ 88 GemBG)</field> <field name="name">Fahrtkostenzuschuss (§ 88 GemBG)</field>
<field name="code">FKZ</field> <field name="code">FKZ</field>
<field name="sequence" eval="17"/> <field name="sequence" eval="17"/>
<field name="category_id" ref="payroll_structure_bgld.category_gembg_brutto"/> <field name="category_id" ref="category_gembg_brutto"/>
<field name="struct_id" ref="payroll_structure_bgld.structure_gembg_bgld"/> <field name="struct_id" ref="structure_gembg_bgld"/>
<field name="condition_select">python</field> <field name="condition_select">python</field>
<field name="condition_python">result = 'FKZ_INPUT' in inputs and inputs['FKZ_INPUT'].amount > 0</field> <field name="condition_python">result = 'FKZ_INPUT' in inputs and inputs['FKZ_INPUT'].amount > 0</field>
<field name="amount_select">code</field> <field name="amount_select">code</field>
@@ -246,8 +247,8 @@ result = inputs['FKZ_INPUT'].amount
<field name="name">Sonderzahlung (quartalsweise, § 133n GemBG)</field> <field name="name">Sonderzahlung (quartalsweise, § 133n GemBG)</field>
<field name="code">SZQ</field> <field name="code">SZQ</field>
<field name="sequence" eval="30"/> <field name="sequence" eval="30"/>
<field name="category_id" ref="payroll_structure_bgld.category_gembg_brutto"/> <field name="category_id" ref="category_gembg_brutto"/>
<field name="struct_id" ref="payroll_structure_bgld.structure_gembg_bgld"/> <field name="struct_id" ref="structure_gembg_bgld"/>
<field name="condition_select">python</field> <field name="condition_select">python</field>
<field name="condition_python"><![CDATA[ <field name="condition_python"><![CDATA[
# § 133n GemBG: 50 % des Monatsentgelts (und der Kinderzulage) # § 133n GemBG: 50 % des Monatsentgelts (und der Kinderzulage)
@@ -255,7 +256,6 @@ result = inputs['FKZ_INPUT'].amount
# Quartalsende (März, Juni, September, Dezember). # Quartalsende (März, Juni, September, Dezember).
# Der Monat der Auszahlung ist der Monat, für den das Entgelt # Der Monat der Auszahlung ist der Monat, für den das Entgelt
# gebührt (§ 56 Abs 2 GemBG: 15.3., 15.6., 15.9., 15.12.). # gebührt (§ 56 Abs 2 GemBG: 15.3., 15.6., 15.9., 15.12.).
import datetime
month = payslip.date_to.month month = payslip.date_to.month
result = month in (3, 6, 9, 12) result = month in (3, 6, 9, 12)
]]></field> ]]></field>
@@ -266,8 +266,9 @@ result = month in (3, 6, 9, 12)
# Kinderzulage), die ihnen für den Monat der Auszahlung zustehen"). # Kinderzulage), die ihnen für den Monat der Auszahlung zustehen").
# Bei unterbrochenem Vierteljahr: nur der entsprechende Teil # Bei unterbrochenem Vierteljahr: nur der entsprechende Teil
# (R1: Vollzeit-Rechnung; Anteil folgt mit der Zeit-Erfassung). # (R1: Vollzeit-Rechnung; Anteil folgt mit der Zeit-Erfassung).
me_total = rules['ME'].total or 0 # Odoo 19 befült result_rules (nicht rules) mit den Regelsummen.
kizu_total = rules['KIZU'].total or 0 me_total = result_rules['ME']['total'] or 0
kizu_total = result_rules['KIZU']['total'] or 0
result = (me_total + kizu_total) * 0.5 result = (me_total + kizu_total) * 0.5
]]></field> ]]></field>
</record> </record>
@@ -279,12 +280,12 @@ result = (me_total + kizu_total) * 0.5
<field name="name">Netto</field> <field name="name">Netto</field>
<field name="code">NETTO</field> <field name="code">NETTO</field>
<field name="sequence" eval="100"/> <field name="sequence" eval="100"/>
<field name="category_id" ref="payroll_structure_bgld.category_gembg_netto"/> <field name="category_id" ref="category_gembg_netto"/>
<field name="struct_id" ref="payroll_structure_bgld.structure_gembg_bgld"/> <field name="struct_id" ref="structure_gembg_bgld"/>
<field name="condition_select">none</field> <field name="condition_select">none</field>
<field name="amount_select">code</field> <field name="amount_select">code</field>
<field name="amount_python_compute"><![CDATA[ <field name="amount_python_compute"><![CDATA[
result = categories.GEMBG_BRUTTO - categories.GEMBG_ABZUG result = categories['GEMBG_BRUTTO'] - categories['GEMBG_ABZUG']
]]></field> ]]></field>
</record> </record>
</odoo> </odoo>
@@ -24,12 +24,55 @@ class HrVersion(models.Model):
besoldungsdienstalter = fields.Date( besoldungsdienstalter = fields.Date(
string='Besoldungsdienstalter (ab)', string='Besoldungsdienstalter (ab)',
groups="hr_payroll.group_hr_payroll_user", tracking=True) groups="hr_payroll.group_hr_payroll_user", tracking=True)
vordienstzeiten_monate = fields.Integer(
string='Anerkannte Vordienstzeiten (Monate)',
groups="hr_payroll.group_hr_payroll_user", tracking=True,
help='AP9 — Bgld. GemBG § 67: Das Besoldungsdienstalter umfasst '
'die im Dienstverhältnis verbrachten wirksamen Zeiten '
'ZUSAMMEN mit den anrechenbaren Vordienstzeiten '
'(Abs 1). Anzurechnen sind u. a. Zeiten in einem '
'Dienstverhältnis zu einer Gebietskörperschaft/einem '
'Gemeindeverband (EWR/Türkei/Schweiz) sowie EU- und '
'zwischenstaatliche Dienstverhältnisse (Abs 2 Z 1 f); '
'Grundwehr-/Ausbildungsdienst zusammen max. 6 Monate, '
'Zivil-/Ersatzdienst max. 9 Monate (Abs 2 Z 4); über '
'die Pflichtzeiten hinaus einschlägige Berufstätigkeit '
'oder ein Verwaltungspraktikum max. 10 Jahre (Abs 3). '
'Doppelanrechnung desselben Zeitraums unzulässig. '
'Hier: die vom Bescheid anerkannten Monate gesamt.')
bda_vorschlag = fields.Date(
string='BDA-Vorschlag (Vordienstzeiten)',
compute='_compute_bda_vorschlag',
groups="hr_payroll.group_hr_payroll_user",
help='Vorschlag: Vertragsbeginn zurückverlegt um die '
'anerkannten Vordienstzeiten (§ 67 Abs 1). Übernahme '
'bewusst manuell — Besoldungsdienstalter ist ein '
'dokumentationspflichtiger Verwaltungsakt (s. Vorrückungs-'
'Wizard).')
vorrueckungstermin = fields.Date( vorrueckungstermin = fields.Date(
compute='_compute_vorrueckungstermin', store=True, readonly=False, compute='_compute_vorrueckungstermin', store=True, readonly=False,
groups="hr_payroll.group_hr_payroll_user", tracking=True, groups="hr_payroll.group_hr_payroll_user", tracking=True,
help="Vorschlag nach § 66 Abs 2 GemBG (BDA + Vorrückungszeitraum); " help="Vorschlag nach § 66 Abs 2 GemBG (BDA + Vorrückungszeitraum); "
"bei Überleitungsfällen manuell korrigierbar.") "bei Überleitungsfällen manuell korrigierbar.")
@api.depends('contract_date_start', 'vordienstzeiten_monate')
def _compute_bda_vorschlag(self):
for version in self:
if not (version.contract_date_start
and version.vordienstzeiten_monate):
version.bda_vorschlag = False
continue
version.bda_vorschlag = version.contract_date_start \
- relativedelta(months=version.vordienstzeiten_monate)
def action_bda_uebernehmen(self):
"""BDA-Vorschlag als Besoldungsdienstalter übernehmen — ein
expliziter Verwaltungsakt, kein stilles Schreiben im Lohnlauf
(Muster: Vorrückungs-Wizard § 66 Abs 2)."""
for version in self:
if version.bda_vorschlag:
version.besoldungsdienstalter = version.bda_vorschlag
@api.depends('entlohnungsgruppe_id', 'besoldungsdienstalter') @api.depends('entlohnungsgruppe_id', 'besoldungsdienstalter')
def _compute_vorrueckungstermin(self): def _compute_vorrueckungstermin(self):
for version in self: for version in self:
@@ -67,6 +110,16 @@ class HrEmployee(models.Model):
besoldungsdienstalter = fields.Date( besoldungsdienstalter = fields.Date(
related="version_id.besoldungsdienstalter", inherited=True, related="version_id.besoldungsdienstalter", inherited=True,
readonly=False, groups="hr_payroll.group_hr_payroll_user") readonly=False, groups="hr_payroll.group_hr_payroll_user")
vordienstzeiten_monate = fields.Integer(
related="version_id.vordienstzeiten_monate", inherited=True,
readonly=False, groups="hr_payroll.group_hr_payroll_user")
bda_vorschlag = fields.Date(
related="version_id.bda_vorschlag", inherited=True,
groups="hr_payroll.group_hr_payroll_user")
vorrueckungstermin = fields.Date( vorrueckungstermin = fields.Date(
related="version_id.vorrueckungstermin", inherited=True, related="version_id.vorrueckungstermin", inherited=True,
readonly=False, groups="hr_payroll.group_hr_payroll_user") readonly=False, groups="hr_payroll.group_hr_payroll_user")
def action_bda_uebernehmen(self):
"""Delegation an die Version (Button am Mitarbeiterformular)."""
self.mapped('version_id').action_bda_uebernehmen()
@@ -44,6 +44,7 @@ class EntgeltTest(TransactionCase):
slip_month = month or 7 slip_month = month or 7
slip = self.env['hr.payslip'].create({ slip = self.env['hr.payslip'].create({
'name': 'GemBG-Test %d/2026' % slip_month,
'employee_id': employee.id, 'employee_id': employee.id,
'struct_id': self.structure.id, 'struct_id': self.structure.id,
'date_from': date(2026, slip_month, 1), 'date_from': date(2026, slip_month, 1),
@@ -1,6 +1,8 @@
# Part of the gem360 onboarding. See LICENSE / project docs for details. # Part of the gem360 onboarding. See LICENSE / project docs for details.
from datetime import date from datetime import date
from freezegun import freeze_time
from odoo.tests import TransactionCase, tagged from odoo.tests import TransactionCase, tagged
@@ -53,9 +55,14 @@ class KatalogTest(TransactionCase):
if not version: if not version:
version = self.env['hr.version'].create({ version = self.env['hr.version'].create({
'employee_id': employee.id, 'employee_id': employee.id,
'date_start': date(2020, 1, 1), 'contract_date_start': date(2020, 1, 1),
}) })
version.write({ version.write({
# contract_date_start ins Suchfenster des Vorrückungs-
# Wizards rücken — hr.version.date_start ist ein nicht-
# gespeicherter Compute daraus (nie gelaufener Test,
# Odoo-19-Latenzkorrektur).
'contract_date_start': date(2014, 1, 1),
'entlohnungsgruppe_id': 'entlohnungsgruppe_id':
self.env.ref('l10n_at_gemeinde_payroll.bgld_grp_av_av1').id, self.env.ref('l10n_at_gemeinde_payroll.bgld_grp_av_av1').id,
'entlohnungsstufe': stufe, 'entlohnungsstufe': stufe,
@@ -76,10 +83,13 @@ class KatalogTest(TransactionCase):
"""Wizard erhöht die Stufe fälliger Versionen und überspringt """Wizard erhöht die Stufe fälliger Versionen und überspringt
die Höchststufe.""" die Höchststufe."""
version = self._version_mit_katalog(date(2014, 3, 15), stufe=3) version = self._version_mit_katalog(date(2014, 3, 15), stufe=3)
version.vorrueckungstermin = date(2024, 4, 1) # Stichtag ab Katalog-Gültigkeit (Seed 2026-07-01) — vor dem
max_stufe = version.entlohnungsgruppe_id._max_stufe(date(2024, 4, 1)) # 1. 7. 2026 gibt es keine Katalogwerte, der Wizard überspringt
# korrekt (nie gelaufener Test, Odoo-19-Latenzkorrektur).
version.vorrueckungstermin = date(2028, 4, 1)
max_stufe = version.entlohnungsgruppe_id._max_stufe(date(2028, 4, 1))
wizard = self.env['gemeinde.payroll.vorrueckung.wizard'].create({ wizard = self.env['gemeinde.payroll.vorrueckung.wizard'].create({
'stichtag': date(2024, 4, 1), 'stichtag': date(2028, 4, 1),
}) })
self.assertIn(version, wizard.version_ids) self.assertIn(version, wizard.version_ids)
wizard.action_apply() wizard.action_apply()
@@ -87,9 +97,29 @@ class KatalogTest(TransactionCase):
version_top = self._version_mit_katalog(date(2014, 3, 15), version_top = self._version_mit_katalog(date(2014, 3, 15),
stufe=max_stufe) stufe=max_stufe)
version_top.vorrueckungstermin = date(2024, 4, 1) version_top.vorrueckungstermin = date(2028, 4, 1)
wizard2 = self.env['gemeinde.payroll.vorrueckung.wizard'].create({ wizard2 = self.env['gemeinde.payroll.vorrueckung.wizard'].create({
'stichtag': date(2024, 4, 1), 'stichtag': date(2028, 4, 1),
}) })
wizard2.action_apply() wizard2.action_apply()
self.assertEqual(version_top.entlohnungsstufe, max_stufe) self.assertEqual(version_top.entlohnungsstufe, max_stufe)
def test_06_bda_vorschlag_aus_vordienstzeiten(self):
"""AP9 — § 67 Abs 1 GemBG: BDA-Vorschlag = Vertragsbeginn
zurückverlegt um die anerkannten Vordienstzeiten; Übernahme
setzt das BDA und den Vorrückungstermin neu (§ 66 Abs 2)."""
version = self._version_mit_katalog(date(2014, 3, 15))
version.write({'contract_date_start': date(2026, 7, 1)})
self.assertFalse(version.bda_vorschlag)
version.vordienstzeiten_monate = 48
self.assertEqual(version.bda_vorschlag, date(2022, 7, 1))
# Ohne Übernahme bleibt das gepflegte BDA (Setup) unangetastet.
self.assertEqual(version.besoldungsdienstalter, date(2014, 3, 15))
with freeze_time('2026-09-12'):
version.action_bda_uebernehmen()
self.assertEqual(version.besoldungsdienstalter, date(2022, 7, 1))
# Vorrückungstermin nach § 66 Abs 2: Die 4-Jahres-Vollendung
# (1. 7. 2026) ist am 12. 9. 2026 bereits verstrichen → der
# Termin der NÄCHSTFÄLLIGEN Vorrückung: BDA + 8 Jahre =
# 1. 7. 2030 → 1. 8. 2030.
self.assertEqual(version.vorrueckungstermin, date(2030, 8, 1))
@@ -10,8 +10,15 @@
groups="hr_payroll.group_hr_payroll_user"> groups="hr_payroll.group_hr_payroll_user">
<field name="entlohnungsgruppe_id"/> <field name="entlohnungsgruppe_id"/>
<field name="entlohnungsstufe" invisible="not entlohnungsgruppe_id"/> <field name="entlohnungsstufe" invisible="not entlohnungsgruppe_id"/>
<field name="vordienstzeiten_monate" invisible="not entlohnungsgruppe_id"/>
<field name="bda_vorschlag" invisible="not entlohnungsgruppe_id"/>
<field name="besoldungsdienstalter" invisible="not entlohnungsgruppe_id"/> <field name="besoldungsdienstalter" invisible="not entlohnungsgruppe_id"/>
<field name="vorrueckungstermin" invisible="not entlohnungsgruppe_id"/> <field name="vorrueckungstermin" invisible="not entlohnungsgruppe_id"/>
<button name="action_bda_uebernehmen"
string="BDA-Vorschlag übernehmen" type="object"
class="btn-secondary"
invisible="not entlohnungsgruppe_id or not bda_vorschlag"
groups="hr_payroll.group_hr_payroll_manager"/>
</group> </group>
<group name="gembg_zulagen" string="GemBG Zulagen" <group name="gembg_zulagen" string="GemBG Zulagen"
groups="hr_payroll.group_hr_payroll_user"> groups="hr_payroll.group_hr_payroll_user">
+4 -1
View File
@@ -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 - Personalverrechnung Kern (AT Payroll Core)', 'name': 'Österreich - Personalverrechnung Kern (AT Payroll Core)',
'version': '19.0.5.1.0', 'version': '19.0.6.0.0',
'author': 'ixsol', 'author': 'ixsol',
'website': 'https://gem360.at', 'website': 'https://gem360.at',
'category': 'Human Resources/Payroll', 'category': 'Human Resources/Payroll',
@@ -74,11 +74,14 @@ Kein VRV-Bezug: die Gemeinde-Buchhaltung (Anlage 3b, Ansatz/MVAG) läuft
'data/rule_parameters_sv_2026.xml', 'data/rule_parameters_sv_2026.xml',
'data/rule_parameters_2027.xml', 'data/rule_parameters_2027.xml',
'data/tasy_mapping.xml', 'data/tasy_mapping.xml',
'data/hr_departure_reason_seed.xml',
'views/tasy_views.xml', 'views/tasy_views.xml',
'views/tasy_katalog_views.xml',
'views/company_views.xml', 'views/company_views.xml',
'views/meldewesen_views.xml', 'views/meldewesen_views.xml',
'views/fristen_views.xml', 'views/fristen_views.xml',
'report/belege_reports.xml', 'report/belege_reports.xml',
'report/jahresreports.xml',
], ],
'countries': ['at'], 'countries': ['at'],
'license': 'LGPL-3', 'license': 'LGPL-3',
@@ -0,0 +1,68 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo noupdate="1">
<!-- ============================================================ -->
<!-- AP1: eSV-Abmeldegründe (AGRD, DM-ORG D.22) an den -->
<!-- Abgangsursachen. -->
<!-- -->
<!-- Die Odoo-Standardursachen (hr.departure_fired/resigned/ -->
<!-- retired) werden vorverbunden statt dupliziert; die übrigen -->
<!-- AT-typischen Ursachen kommen hinzu. noupdate: mandantenseitig -->
<!-- geänderte oder ergänzte Ursachen bleiben unberührt. -->
<!-- ============================================================ -->
<record id="hr.departure_fired" model="hr.departure.reason">
<field name="l10n_at_agrd">01</field>
</record>
<record id="hr.departure_resigned" model="hr.departure.reason">
<field name="l10n_at_agrd">02</field>
</record>
<record id="hr.departure_retired" model="hr.departure.reason">
<field name="l10n_at_agrd">16</field>
</record>
<record id="departure_reason_einvernehmlich"
model="hr.departure.reason">
<field name="name">Einvernehmliche Lösung</field>
<field name="country_id" ref="base.at"/>
<field name="sequence">40</field>
<field name="l10n_at_agrd">03</field>
</record>
<record id="departure_reason_zeitablauf" model="hr.departure.reason">
<field name="name">Zeitablauf</field>
<field name="country_id" ref="base.at"/>
<field name="sequence">41</field>
<field name="l10n_at_agrd">04</field>
</record>
<record id="departure_reason_berechtigter_austritt"
model="hr.departure.reason">
<field name="name">Berechtigter vorzeitiger Austritt</field>
<field name="country_id" ref="base.at"/>
<field name="sequence">42</field>
<field name="l10n_at_agrd">05</field>
</record>
<record id="departure_reason_fristlose" model="hr.departure.reason">
<field name="name">Fristlose Entlassung</field>
<field name="country_id" ref="base.at"/>
<field name="sequence">43</field>
<field name="l10n_at_agrd">06</field>
</record>
<record id="departure_reason_karenz" model="hr.departure.reason">
<field name="name">Karenz nach MSchG / VKG</field>
<field name="country_id" ref="base.at"/>
<field name="sequence">44</field>
<field name="l10n_at_agrd">07</field>
</record>
<record id="departure_reason_tod" model="hr.departure.reason">
<field name="name">Tod des/der Dienstnehmers:in</field>
<field name="country_id" ref="base.at"/>
<field name="sequence">45</field>
<field name="l10n_at_agrd">13</field>
</record>
<record id="departure_reason_sonstiges_ende"
model="hr.departure.reason">
<field name="name">Sonstiger Grund mit Ende des Dienstverhältnisses</field>
<field name="country_id" ref="base.at"/>
<field name="sequence">46</field>
<field name="l10n_at_agrd">00</field>
</record>
</odoo>
@@ -1,5 +1,6 @@
# Part of the odoo-at-payroll project. License: LGPL-3. # Part of the odoo-at-payroll project. License: LGPL-3.
from . import tasy from . import tasy
from . import tasy_katalog
from . import lohnsteuer from . import lohnsteuer
from . import sozialversicherung from . import sozialversicherung
from . import elda from . import elda
@@ -9,3 +10,4 @@ from . import lohnzettel_l16
from . import fristenkalender from . import fristenkalender
from . import jahresretro from . import jahresretro
from . import mitteilungsfile from . import mitteilungsfile
from . import monatsreports
@@ -105,6 +105,32 @@ FRISTART_DEFINITIONEN = {
'ab 40 M / 35 F (⚠ blockiert — ASchV-Daten-' 'ab 40 M / 35 F (⚠ blockiert — ASchV-Daten-'
'modell fehlt im Modul).', 'modell fehlt im Modul).',
}, },
'wien_dag': {
'name': 'Wr. Dienstgeberabgabe entrichten (Stadtkasse)',
'monatlich': True,
'tag': 15,
'action': None,
'wien_dag': True,
'hilfetext': 'Wr. DAG („U-Bahn-Steuer“, GesNr 20000133): '
'Selbstberechnungsabgabe — die im Vormonat '
'entstandene Abgabenschuld bis zum 15. des '
'Folgemonats an die Stadtkasse (Magistrat) '
'entrichten (§ 6 Abs 1). Nur Firmen mit Flag '
'l10n_at_wien_dag (Beschäftigungsort Wien).',
},
'wien_dag_jahres': {
'name': 'Wr. DAG-Jahreserklärung (Magistrat)',
'monatlich': False,
'monat': 3,
'tag_im_monat': 31,
'action': None,
'wien_dag': True,
'hilfetext': 'Bis 31. März die im Vorjahr entstandene '
'Abgabenschuld beim Magistrat schriftlich '
'erklären; darin auch die befreiten '
'Dienstverhältnisse angeben (§ 6 Abs 2 Wr. '
'DAG). Nur Firmen mit Flag l10n_at_wien_dag.',
},
} }
@@ -188,7 +214,10 @@ class L10nAtPayrollFrist(models.Model):
deadline_monat = date( deadline_monat = date(
folgemonat.year, folgemonat.month, folgemonat.year, folgemonat.month,
FRISTART_DEFINITIONEN['sv_mbgm']['tag']) FRISTART_DEFINITIONEN['sv_mbgm']['tag'])
for art in ('sv_mbgm', 'db_dz_kommst'): monatliche_arten = ['sv_mbgm', 'db_dz_kommst']
if company.l10n_at_wien_dag:
monatliche_arten.append('wien_dag')
for art in monatliche_arten:
self._sicherstellen( self._sicherstellen(
company, art, vormonat.year, vormonat.month, company, art, vormonat.year, vormonat.month,
deadline_monat) deadline_monat)
@@ -209,6 +238,11 @@ class L10nAtPayrollFrist(models.Model):
self._sicherstellen( self._sicherstellen(
company, 'kommst_jahres', heute.year - 1, None, company, 'kommst_jahres', heute.year - 1, None,
date(heute.year, 3, 31)) date(heute.year, 3, 31))
# Wr. DAG-Jahreserklärung (31. 3., § 6 Abs 2).
if company.l10n_at_wien_dag:
self._sicherstellen(
company, 'wien_dag_jahres', heute.year - 1,
None, date(heute.year, 3, 31))
@api.model @api.model
def _sicherstellen(self, company, art, jahr, monat, deadline): def _sicherstellen(self, company, art, jahr, monat, deadline):
+59 -4
View File
@@ -15,13 +15,56 @@ Dienstgeberin und der Arbeitsstätten.
Prüfzifferalgorithmus der SVS ist nicht beschafft → nur Format/ Prüfzifferalgorithmus der SVS ist nicht beschafft → nur Format/
Plausibilität, Rest dokumentiert). Plausibilität, Rest dokumentiert).
- ``hr.departure.reason``: Mapping auf den eSV-Abmeldegrund (AGRD, - ``hr.departure.reason``: Mapping auf den eSV-Abmeldegrund (AGRD,
DM-ORG D.22; Standard ``00`` sonstiger Grund mit Ende des DM-ORG D.22 — vollständige Code-Liste als Selection, Standard
Beschäftigungsverhältnisses).""" ``00`` sonstiger Grund mit Ende des Beschäftigungsverhältnisses;
die üblichen Abgangsursachen sind im Seed vorverbunden,
noupdate — mandantenseitig ergänzte Ursachen bleiben unberührt)."""
from odoo import api, fields, models from odoo import api, fields, models
from odoo.exceptions import ValidationError from odoo.exceptions import ValidationError
# eSV-Abmeldegründe, DM-ORG 42.7.0 Kapitel D.22 (AGRD Abmeldegrund,
# Code) — verifiziert gegen .firecrawl/ris/elda/DM-ORG_v42.7.0.txt
# (42. Ergänzung 07/2026). Codes 26/28 existieren nicht (Lücken im
# Katalog), Code 00 = sonstiger Grund mit Ende des DV.
L10N_AT_AGRD = [
('01', '01 — Kündigung durch den Dienstgeber'),
('02', '02 — Kündigung durch den Dienstnehmer'),
('03', '03 — Einvernehmliche Lösung'),
('04', '04 — Zeitablauf'),
('05', '05 — Berechtigter vorzeitiger Austritt'),
('06', '06 — Fristlose Entlassung'),
('07', '07 — Karenz nach MSchG / VKG'),
('08', '08 — Präsenzdienstleistung im Bundesheer'),
('09', '09 — Zivildienst'),
('10', '10 — Pragmatisierung'),
('11', '11 — Länger als 1 Monat währender unbezahlter Urlaub'),
('12', '12 — Ummeldung'),
('13', '13 — Tod des Dienstnehmers'),
('14', '14 — Änderung der SV-Pflicht'),
('15', '15 — Truppenübung'),
('16', '16 — Pensionierung'),
('17', '17 — Ende freier Dienstvertrag (§ 4 Abs 4 ASVG)'),
('18', '18 — Enthebung von der Gerichtspraxis'),
('19', '19 — Unterbrechung der Gerichtspraxis'),
('20', '20 — Entlassung aus der Bundesbetreuung'),
('21', '21 — Kündigung durch DN während Elternteilzeit (MSchG/VKG)'),
('22', '22 — Unberechtigter vorzeitiger Austritt'),
('23', '23 — Bildungskarenz gemäß § 11 AVRAG'),
('24', '24 — Vorzeitiger Austritt gem. § 25 IO durch Dienstnehmer'),
('25', '25 — Kündigung gem. § 25 IO durch Insolvenzverwalter:in'),
('27', '27 — Kündigung gem. § 25 IO durch Dienstgeber'),
('29', '29 — SV-Ende Beschäftigung aufrecht'),
('30', '30 — Lösung in der Probezeit durch Dienstgeber'),
('31', '31 — Pflegekarenz (interner SV-Abmeldegrund)'),
('32', '32 — Bildungskarenz gemäß § 12 AVRAG'),
('33', '33 — Familienhospizkarenz (interner SV-Abmeldegrund)'),
('34', '34 — Lösung in der Probezeit durch Dienstnehmer'),
('00', '00 — Sonstiger Grund mit Ende des DV'),
]
class ResCompany(models.Model): class ResCompany(models.Model):
_inherit = 'res.company' _inherit = 'res.company'
@@ -36,6 +79,16 @@ class ResCompany(models.Model):
help='9-stellig: 2 Stellen Finanzamtsnummer + 7 Stellen\n' help='9-stellig: 2 Stellen Finanzamtsnummer + 7 Stellen\n'
'Steuernummer (Lohnzettel Finanz I1 STNRA/STNRL;\n' 'Steuernummer (Lohnzettel Finanz I1 STNRA/STNRL;\n'
'Arbeitsstättenmeldung STNR).') 'Arbeitsstättenmeldung STNR).')
l10n_at_wien_dag = fields.Boolean(
string='AT Beschäftigungsort Wien (Wr. DAG)',
help='Wiener Dienstgeberabgabe („U-Bahn-Steuer“, Wr. DAG,\n'
'RIS LrW GesNr 20000133): 2 € je Dienstnehmer:in und\n'
'angefangene Woche je Bestehen eines Dienstverhältnisses\n'
'mit Beschäftigungsort Wien (§§ 1, 2, 5); Befreiungen § 3\n'
'(z. B. Überschreiten des 55. Lebensjahres, Lehrlinge,\n'
'≤ 10 Wochenstunden, Mutterschaftsverbote/Karenz,\n'
'Präsenzdienst, Gebietskörperschaften); Selbstberechnung\n'
'15. des Folgemonats, Jahreserklärung bis 31. 3. (§ 6).')
class ResPartner(models.Model): class ResPartner(models.Model):
@@ -89,8 +142,10 @@ class HrVersion(models.Model):
class HrDepartureReason(models.Model): class HrDepartureReason(models.Model):
_inherit = 'hr.departure.reason' _inherit = 'hr.departure.reason'
l10n_at_agrd = fields.Char( l10n_at_agrd = fields.Selection(
L10N_AT_AGRD,
string='AT eSV-Abmeldegrund (AGRD)', string='AT eSV-Abmeldegrund (AGRD)',
help='2-stelliger Abmeldegrund-Code der Versichertenmeldung ' help='2-stelliger Abmeldegrund-Code der Versichertenmeldung '
'(DM-ORG D.22); Standard 00 „Sonstiger Grund mit Ende des ' '(DM-ORG D.22); Standard 00 „Sonstiger Grund mit Ende des '
'Beschäftigungsverhältnisses“.') 'Beschäftigungsverhältnisses“. Die Abmeldung M4 liest das '
'Mapping dieser Abgangsursache.')
+214 -16
View File
@@ -1,16 +1,29 @@
# Part of the odoo-at-payroll project. License: LGPL-3. # Part of the odoo-at-payroll project. License: LGPL-3.
"""Monatsdienstliste und Monatsjournal (GP7/AP6): QWeb-Datenlieferanten """Monatsreports und Jahresberichte der AT-Lohnverrechnung.
auf dem Lohnlauf (hr.payslip.run) — voraberechnete Aggregation über
`hr.payslip.line` (read_group-Muster des Contribution Register), damit
die Templates reine Darstellung bleiben.
- **Monatsdienstliste**: je AN eine Zeile mit den mBGM-Kontrollgrößen - **Monatsdienstliste** (GP7/AP6): je AN die mBGM-Kontrollgrößen
(Versicherungstage/Beitragsgrundlagen je Beitragsmonat, DN- und Abstimmung Abrechnung ↔ SV-Meldung.
DG-Beiträge, DG-Anteile) — Abstimmung Abrechnung ↔ SV-Meldung. - **Monatsjournal** (GP7/AP6, AP2a erweitert): je Lohnart die Summe
- **Monatsjournal**: je Lohnart die Summe über den Lauf (Brutto, über den Lauf, gegliedert nach Bezügen/Abzügen (Bemessung ≠
Abzüge, DG-Anteile, Netto) mit Kontrollsummen gegen die neutral) und — seit AP2a — **eigenem Block Dienstgeberanteile**
GP6-Buchung.""" (Kategorie ``ATP_DG``: SV-DG je Zweig, FLAF-DB, KommSt, DZ, BV —
from odoo import models zuvor wurden die neutralen DG-Regeln still ausgefiltert) mit
Kontrollsummen gegen die GP6-Buchung.
- **Dienstgeber-Gesamtkosten** (AP2a): je AN Bruttolohn und die
Lohnnebenkosten aufgeschlüsselt (SV-DG laufend/SZ, FLAF-DB,
KommSt, DZ, BV, weitere DG-Positionen) und kumuliert
(Σ Lohnnebenkosten, Gesamtkosten = Brutto + LNK) mit Laufsummen.
- **Jahreslohnkonto je Dienstnehmer:in** (AP2b) und
**Betriebsjahreslohnkonto** (AP2c): Monatsmatrix je Lohnart aus
``hr.payslip.line`` — Datenbereitstellung über den Wizard
``l10n.at.payroll.jahresbericht.wizard``.
Die Templates bleiben reine Darstellung (QWeb); alle Zahlen werden
hier vorberechnet (read_group-Muster des Contribution Register).
"""
from datetime import date
from odoo import api, fields, models
class L10nAtPayrollMonatsReports(models.AbstractModel): class L10nAtPayrollMonatsReports(models.AbstractModel):
@@ -61,24 +74,32 @@ class L10nAtPayrollMonatsReports(models.AbstractModel):
class L10nAtPayrollMonatsjournal(models.AbstractModel): class L10nAtPayrollMonatsjournal(models.AbstractModel):
_name = 'report.l10n_at_hr_payroll.report_monatsjournal' _name = 'report.l10n_at_hr_payroll.report_monatsjournal'
_description = 'Monatsjournal (GP7/AP6)' _description = 'Monatsjournal (GP7/AP6, AP2a)'
def _get_report_values(self, docids, data=None): def _get_report_values(self, docids, data=None):
runs = self.env['hr.payslip.run'].browse(docids) runs = self.env['hr.payslip.run'].browse(docids)
zeilen = [] zeilen = []
for run in runs: for run in runs:
gruppen = {} gruppen = {}
dg_gruppen = {}
for slip in run.slip_ids.filtered( for slip in run.slip_ids.filtered(
lambda p: p.state not in ('draft', 'cancel')): lambda p: p.state not in ('draft', 'cancel')):
for line in slip.line_ids: for line in slip.line_ids:
regel = line.salary_rule_id regel = line.salary_rule_id
if not line.total or regel.l10n_at_bemessung \ if not line.total:
== 'neutral':
continue continue
kategorie = regel.category_id.code or ''
schluessel = (regel.code, regel.name, schluessel = (regel.code, regel.name,
regel.l10n_at_bemessung) regel.l10n_at_bemessung)
eintrag = gruppen.setdefault(schluessel, 0.0) if kategorie == 'ATP_DG':
gruppen[schluessel] = eintrag + line.total # AP2a: Dienstgeberanteile eigener Block —
# die neutralen DG-Regeln (SVDG_*, FLAFDB,
# KOMMST, DZ, BVG) nicht mehr ausfiltern.
dg_gruppen[schluessel] = \
dg_gruppen.get(schluessel, 0.0) + line.total
elif regel.l10n_at_bemessung != 'neutral':
gruppen[schluessel] = \
gruppen.get(schluessel, 0.0) + line.total
zeilen.append({ zeilen.append({
'run': run, 'run': run,
'arten': [ 'arten': [
@@ -86,5 +107,182 @@ class L10nAtPayrollMonatsjournal(models.AbstractModel):
'summe': summe} 'summe': summe}
for (code, name, bemessung), summe for (code, name, bemessung), summe
in sorted(gruppen.items())], in sorted(gruppen.items())],
'dg_arten': [
{'code': code, 'name': name, 'bemessung': bemessung,
'summe': summe}
for (code, name, bemessung), summe
in sorted(dg_gruppen.items())],
'summe_arten': sum(gruppen.values()),
'summe_dg': sum(dg_gruppen.values()),
}) })
return {'docs': runs, 'journal': zeilen} return {'docs': runs, 'journal': zeilen}
class L10nAtPayrollDgKosten(models.AbstractModel):
_name = 'report.l10n_at_hr_payroll.report_dg_kosten'
_description = 'Dienstgeber-Gesamtkosten (AP2a)'
# Festspalten des Reports; weitere DG-Positionen (z. B. §-53a-UV
# der Geringfügigen) laufen unter 'weitere'.
_SPALTEN = ('FLAFDB', 'KOMMST', 'DZ', 'BVG', 'WIEN_DAG')
def _get_report_values(self, docids, data=None):
runs = self.env['hr.payslip.run'].browse(docids)
zeilen = []
summen = {
'brutto': 0.0, 'svdg': 0.0, 'dg_summe': 0.0,
'gesamtkosten': 0.0,
'spalten': {code: 0.0 for code in self._SPALTEN},
'weitere': 0.0,
}
for run in runs:
for slip in run.slip_ids.filtered(
lambda p: p.state not in ('draft', 'cancel')):
brutto = sum(
slip.line_ids.filtered(
lambda l: l.total
and l.salary_rule_id.category_id.code
== 'ATP_BRUTTO'
and l.salary_rule_id.l10n_at_bemessung != 'neutral'
).mapped('total'))
dg_komponenten = {}
for line in slip.line_ids:
regel = line.salary_rule_id
if not line.total \
or regel.category_id.code != 'ATP_DG':
continue
dg_komponenten[regel.code] = \
dg_komponenten.get(regel.code, 0.0) + line.total
svdg = (slip._l10n_at_svdg_gesamt()
+ sum(slip._l10n_at_svdg_sz(k) for k in
('kv', 'pv', 'av', 'uv', 'iesg')))
spalten = {code: dg_komponenten.get(code, 0.0)
for code in self._SPALTEN}
dg_summe = sum(dg_komponenten.values())
weitere = dg_summe - svdg - sum(spalten.values())
zeilen.append({
'slip': slip,
'brutto': brutto,
'svdg': svdg,
'spalten': spalten,
'weitere': max(weitere, 0.0),
'dg_summe': dg_summe,
'gesamtkosten': brutto + dg_summe,
})
summen['brutto'] += brutto
summen['svdg'] += svdg
summen['dg_summe'] += dg_summe
summen['gesamtkosten'] += brutto + dg_summe
summen['weitere'] += max(weitere, 0.0)
for code in self._SPALTEN:
summen['spalten'][code] += spalten[code]
return {'docs': runs, 'zeilen': zeilen, 'summen': summen}
class L10nAtPayrollJahresberichtWizard(models.TransientModel):
"""Druck-Wrapper für Jahreslohnkonto (je DN) und
Betriebsjahreslohnkonto (Summen je Lohnart) — AP2b/AP2c."""
_name = 'l10n.at.payroll.jahresbericht.wizard'
_description = 'Jahresberichte: Lohnkonten drucken'
jahr = fields.Integer(
required=True,
default=lambda self: fields.Date.context_today(self).year)
art = fields.Selection(
[('dn', 'Jahreslohnkonto je Dienstnehmer:in'),
('betrieb', 'Betriebsjahreslohnkonto (Summen je Lohnart)')],
required=True, default='dn')
company_id = fields.Many2one(
'res.company', required=True,
default=lambda self: self.env.company)
employee_ids = fields.Many2many(
'hr.employee', string='Dienstnehmer:innen',
help='Leer = alle Dienstnehmer:innen des Unternehmens.')
def action_drucken(self):
self.ensure_one()
if self.art == 'dn':
employees = self.employee_ids or self.env[
'hr.employee'].search(
[('company_id', '=', self.company_id.id)])
report = self.env.ref(
'l10n_at_hr_payroll.action_report_jahreslohnkonto')
return report.report_action(
employees, data={'jahr': self.jahr})
report = self.env.ref(
'l10n_at_hr_payroll.action_report_betriebslohnkonto')
return report.report_action(
self.company_id, data={'jahr': self.jahr})
@api.model
def _l10n_at_lohnkonto_zeilen(self, jahr, company_id,
employee_id=None):
"""Aggregierte Lohnartzeilen des Jahres: [(code, name,
bemessung, kategorie, [Jan..Dez], total), …] — sortiert nach
Kategorie und Code."""
domain = [
('company_id', '=', company_id),
('date_from', '>=', date(jahr, 1, 1)),
('date_from', '<=', date(jahr, 12, 31)),
('slip_id.state', 'not in', ('draft', 'cancel')),
]
if employee_id:
domain.append(('employee_id', '=', employee_id))
zeilen = {}
for line in self.env['hr.payslip.line'].search(domain):
regel = line.salary_rule_id
schluessel = (regel.code, regel.name,
regel.l10n_at_bemessung,
regel.category_id.code or '')
eintrag = zeilen.setdefault(
schluessel, {'monate': [0.0] * 12, 'total': 0.0})
eintrag['monate'][line.date_from.month - 1] += line.total
eintrag['total'] += line.total
result = []
for (code, name, bemessung, kategorie), eintrag in sorted(
zeilen.items()):
result.append({
'code': code, 'name': name, 'bemessung': bemessung,
'kategorie': kategorie,
'monate': eintrag['monate'], 'total': eintrag['total'],
})
return result
class L10nAtPayrollJahreslohnkonto(models.AbstractModel):
_name = 'report.l10n_at_hr_payroll.report_jahreslohnkonto'
_description = 'Jahreslohnkonto je Dienstnehmer:in (AP2b)'
def _get_report_values(self, docids, data=None):
jahr = (data or {}).get('jahr') \
or fields.Date.context_today(self).year
employees = self.env['hr.employee'].browse(docids)
berichte = []
for employee in employees:
berichte.append({
'employee': employee,
'zeilen': self.env[
'l10n.at.payroll.jahresbericht.wizard'
]._l10n_at_lohnkonto_zeilen(
jahr, employee.company_id.id, employee.id),
})
return {'docs': employees, 'jahr': jahr, 'berichte': berichte}
class L10nAtPayrollBetriebslohnkonto(models.AbstractModel):
_name = 'report.l10n_at_hr_payroll.report_betriebslohnkonto'
_description = 'Betriebsjahreslohnkonto (AP2c)'
def _get_report_values(self, docids, data=None):
jahr = (data or {}).get('jahr') \
or fields.Date.context_today(self).year
companies = self.env['res.company'].browse(docids)
berichte = []
for company in companies:
berichte.append({
'company': company,
'zeilen': self.env[
'l10n.at.payroll.jahresbericht.wizard'
]._l10n_at_lohnkonto_zeilen(jahr, company.id),
})
return {'docs': companies, 'jahr': jahr, 'berichte': berichte}
+415 -9
View File
@@ -1,4 +1,17 @@
# Part of the odoo-at-payroll project. License: LGPL-3. # Part of the odoo-at-payroll project. License: LGPL-3.
"""TASY-LSWH-Import (SV-Tarifsystem, sozialversicherung.at/tarifsystem).
Zwei Stufen:
1. ``l10n.at.payroll.tasy.import.wizard`` — SV-Werte als
hr.rule.parameter (Mapping-gesteuert, Vorschau, historische
Werte bleiben erhalten; GP4).
2. ``l10n.at.payroll.tasy.katalog.wizard`` — Vollständiger
Tarifsystem-Katalog (AP0, Variante b): BSGR/ERGB/Dienstgeber-
gruppen/Tarifgruppen/Tarifsets/Tarife/Zu-/Abschläge, alle
Träger, versioniert über von/bis. Die mBGM-Tarifierung liest
daraus; der Hand-Parameter ``at_mbgm_tarif`` bleibt Fallback.
"""
from lxml import etree from lxml import etree
import base64 import base64
@@ -7,6 +20,19 @@ from odoo import _, api, fields, models
from odoo.exceptions import UserError from odoo.exceptions import UserError
def _l10n_at_tasy_root(file_field):
"""TASY-XML robust laden: rohe XML-Bytes ODER base64."""
parser = etree.XMLParser(resolve_entities=False, no_network=True)
data = file_field or b''
if isinstance(data, str):
data = data.encode()
# Binary kann XML-Bytes ODER base64 (str/Bytes) liefern:
# dekodieren, wenn kein '<' am Anfang steht.
if not data.lstrip().startswith(b'<'):
data = base64.b64decode(data)
return etree.fromstring(data, parser)
class L10nAtPayrollTasyMapping(models.Model): class L10nAtPayrollTasyMapping(models.Model):
"""Mapping TASY-Code/-Zweig -> SV-Regelparameter (hr.rule.parameter). """Mapping TASY-Code/-Zweig -> SV-Regelparameter (hr.rule.parameter).
@@ -102,15 +128,7 @@ class L10nAtPayrollTasyImportWizard(models.TransientModel):
def _tasy_root(self): def _tasy_root(self):
self.ensure_one() self.ensure_one()
parser = etree.XMLParser(resolve_entities=False, no_network=True) return _l10n_at_tasy_root(self.file)
data = self.file or b''
if isinstance(data, str):
data = data.encode()
# Binary kann XML-Bytes ODER base64 (str/Bytes) liefern:
# dekodieren, wenn kein '<' am Anfang steht.
if not data.lstrip().startswith(b'<'):
data = base64.b64decode(data)
return etree.fromstring(data, parser)
@staticmethod @staticmethod
def _tasy_text(node, tag): def _tasy_text(node, tag):
@@ -300,3 +318,391 @@ class L10nAtPayrollTasyImportWizard(models.TransientModel):
'next': {'type': 'ir.actions.act_window_close'}, 'next': {'type': 'ir.actions.act_window_close'},
}, },
} }
class L10nAtPayrollTasyKatalogWizard(models.TransientModel):
"""Lädt den vollständigen Tarifsystem-Katalog aus dem
TASY-LSWH-Export (AP0, Variante b) — alle Trägergruppen,
versioniert über von/bis-Datum. Idempotent: wiederholter Import
derselben Datei aktualisiert statt zu duplizieren; historische
Gültigkeitsfenster bleiben erhalten (Jahreswechsel = neuer
Import, keine Löschung).
Die Sätze selbst werden NICHT hieraus in hr.rule.parameter
geschrieben — die mBGM-Tarifierung liest den Katalog direkt
(models/tasy_katalog.py); nur die Zweig-Split-Werte für den
GP4-Kern laufen über den Wert-Import-Wizard.
"""
_name = 'l10n.at.payroll.tasy.katalog.wizard'
_description = 'SV-Tarifsystem-Katalog importieren (TASY)'
file = fields.Binary(required=True, string='TASY-Export (XML)')
filename = fields.Char()
stichtag = fields.Date(
readonly=True,
help="Gültigkeitstichtag des Exports (nur Dokumentation — "
"die Katalogzeilen tragen eigene von/bis-Fenster).")
protocol = fields.Text(readonly=True)
@staticmethod
def _tasy_text(node, tag):
child = node.find(tag)
return (child.text or '').strip() if child is not None else ''
@staticmethod
def _tasy_texts(node, tag):
"""Alle Kindelemente dieses Tags — TASY nutzt wiederholte
Referenz-Elemente (z. B. <tarife>xmlId</tarife>)."""
return [child.text.strip() for child in node.findall(tag)
if child.text and child.text.strip()]
@staticmethod
def _tasy_bool(text):
return (text or '').strip().lower() in ('true', '1', 'j', 'ja',
'yes', 'y')
def _tasy_root(self):
self.ensure_one()
return _l10n_at_tasy_root(self.file)
@staticmethod
def _section(root, tag):
"""Sektionsscharfe Iteration — root.iter() erfasste auch die
Referenz-Elemente (z. B. <tarifgruppe>xmlId</tarifgruppe> in
Zuordnungszeilen); die Sektionen enthalten nur echte Zeilen."""
section = root.find(tag)
return list(section) if section is not None else []
# ------------------------------------------------------------------
# Upsert-Helfer
# ------------------------------------------------------------------
def _upsert_code_katalog(self, model, nodes, protocol, label,
extra_vals=None):
"""Katalogzeilen mit (Trägergruppe, Code, von) als Schlüssel."""
created = updated = 0
for node in nodes:
code = self._tasy_text(node, 'code')
von = self._tasy_text(node, 'vonDatum')
if not code or not von:
continue
vals = {
'code': code,
'bezeichnung': self._tasy_text(node, 'bezeichnung'),
'kurzbezeichnung': self._tasy_text(
node, 'kurzbezeichnung'),
'beschreibung': self._tasy_text(node, 'beschreibung'),
'traegergruppe': self._tasy_text(node, 'traegergruppe'),
'von_datum': fields.Date.to_date(von),
'bis_datum': fields.Date.to_date(
self._tasy_text(node, 'bisDatum') or '2999-12-31'),
'tasy_xml_id': node.get('xmlId'),
}
vals.update(extra_vals(node) if extra_vals else {})
rec = self.env[model].search([
('traegergruppe', '=', vals['traegergruppe']),
('code', '=', code),
('von_datum', '=', vals['von_datum'])], limit=1)
if rec:
rec.write(vals)
updated += 1
else:
self.env[model].create(vals)
created += 1
protocol.append('%s: %d neu, %d aktualisiert'
% (label, created, updated))
def _upsert_tarif(self, nodes, protocol):
"""Tarife ohne Code — Identität ist der Inhalt (Zweig, Sätze,
Gültigkeit); xmlId zuerst, dann Inhaltsvergleich."""
created = updated = 0
for node in nodes:
zweig = self._tasy_text(node, 'tarifZweig')
von = self._tasy_text(node, 'vonDatum')
if not zweig or not von:
continue
vals = {
'zweig': zweig,
'dn': float(self._tasy_text(
node, 'dienstnehmerAnteil') or 0.0),
'dg': float(self._tasy_text(
node, 'dienstgeberAnteil') or 0.0),
'traegergruppe': self._tasy_text(node, 'traegergruppe'),
'von_datum': fields.Date.to_date(von),
'bis_datum': fields.Date.to_date(
self._tasy_text(node, 'bisDatum') or '2999-12-31'),
'tasy_xml_id': node.get('xmlId'),
}
rec = self.env['l10n.at.payroll.tasy.tarif'].search([
('zweig', '=', zweig),
('traegergruppe', '=', vals['traegergruppe']),
('von_datum', '=', vals['von_datum'])]).filtered(
lambda t, v=vals: abs(t.dn - v['dn']) < 1e-9
and abs(t.dg - v['dg']) < 1e-9)[:1]
if rec:
rec.write(vals)
updated += 1
else:
self.env['l10n.at.payroll.tasy.tarif'].create(vals)
created += 1
protocol.append('Tarife: %d neu, %d aktualisiert'
% (created, updated))
def _upsert_tarifset(self, nodes, tarif_map, protocol):
"""Tarifsets — Schlüssel (Träger, Kurzbezeichnung, von);
Tarif-Verknüpfungen werden je Import neu gesetzt."""
created = updated = 0
for node in nodes:
von = self._tasy_text(node, 'vonDatum')
kurz = self._tasy_text(node, 'kurzbezeichnung')
if not von:
continue
vals = {
'bezeichnung': self._tasy_text(node, 'bezeichnung'),
'kurzbezeichnung': kurz,
'traegergruppe': self._tasy_text(node, 'traegergruppe'),
'von_datum': fields.Date.to_date(von),
'bis_datum': fields.Date.to_date(
self._tasy_text(node, 'bisDatum') or '2999-12-31'),
'tasy_xml_id': node.get('xmlId'),
}
rec = self.env['l10n.at.payroll.tasy.tarifset'].search([
('traegergruppe', '=', vals['traegergruppe']),
('kurzbezeichnung', '=', kurz),
('von_datum', '=', vals['von_datum'])], limit=1)
tarife = [tarif_map[ref] for ref in self._tasy_texts(
node, 'tarife') if ref in tarif_map]
vals['tarif_ids'] = [(6, 0, [t.id for t in tarife])]
if rec:
rec.write(vals)
updated += 1
else:
rec = self.env['l10n.at.payroll.tasy.tarifset'].create(
vals)
created += 1
protocol.append('Tarifsets: %d neu, %d aktualisiert'
% (created, updated))
# ------------------------------------------------------------------
# Import
# ------------------------------------------------------------------
def action_import(self):
self.ensure_one()
root = self._tasy_root()
stichtag = (root.findtext('gueltigkeitstichtag') or '').strip()
if not stichtag:
raise UserError(_('Ungültiger TASY-Export: '
'Gültigkeitstichtag fehlt.'))
self.stichtag = fields.Date.to_date(stichtag)
protocol = ['Stichtag des Exports: %s' % stichtag]
# 1) Code-Kataloge: BSGR, Ergänzungen, Dienstgebergruppen,
# Zu-/Abschläge (inkl. Steuerung).
self._upsert_code_katalog(
'l10n.at.payroll.tasy.bsgr',
self._section(root, 'beschaeftigtengruppen'), protocol,
'BSGR')
self._upsert_code_katalog(
'l10n.at.payroll.tasy.ergaenzung',
self._section(root, 'ergaenzungen'), protocol,
'Ergänzungen')
self._upsert_code_katalog(
'l10n.at.payroll.tasy.dienstgebergruppe',
self._section(root, 'dienstgebergruppen'), protocol,
'Dienstgebergruppen')
bsgr_map = {b.tasy_xml_id: b for b in self.env[
'l10n.at.payroll.tasy.bsgr'].search([])}
erg_map = {e.tasy_xml_id: e for e in self.env[
'l10n.at.payroll.tasy.ergaenzung'].search([])}
dgg_map = {d.tasy_xml_id: d for d in self.env[
'l10n.at.payroll.tasy.dienstgebergruppe'].search([])}
zuabschlag_map = {}
zuabschlag_nodes = self._section(root, 'zuAbschlaege')
self._upsert_code_katalog(
'l10n.at.payroll.tasy.zuabschlag',
zuabschlag_nodes, protocol, 'Zu-/Abschläge',
extra_vals=lambda n: {
'zuabschlagkz': self._tasy_text(n, 'zuabschlagkz'),
'versicherungsrelevant': self._tasy_bool(
self._tasy_text(n, 'versicherungsRelevant')),
})
for za in self.env['l10n.at.payroll.tasy.zuabschlag'].search([]):
zuabschlag_map[za.tasy_xml_id] = za
steuerung_created = 0
steuerung_obj = self.env[
'l10n.at.payroll.tasy.zuabschlag.steuerung']
for node in self._section(root, 'zuAbschlaegeSteuerung'):
zuabschlag = zuabschlag_map.get(
self._tasy_text(node, 'zuAbschlag'))
von = self._tasy_text(node, 'vonDatum')
if not zuabschlag or not von:
continue
vals = {
'zuabschlag_id': zuabschlag.id,
'altersgrenze_min_jahr': float(
self._tasy_text(node, 'altersgrenzeMinJahr') or 0.0),
'altersgrenze_soll_jahr': float(
self._tasy_text(node, 'altersgrenzeSollJahr') or 0.0),
'geboren_bis': fields.Date.to_date(
self._tasy_text(node, 'geborenBis'))
if self._tasy_text(node, 'geborenBis') else False,
'geschlecht': self._tasy_text(node, 'geschlecht'),
'traegergruppe': self._tasy_text(node, 'traegergruppe'),
'von_datum': fields.Date.to_date(von),
'bis_datum': fields.Date.to_date(
self._tasy_text(node, 'bisDatum') or '2999-12-31'),
'tasy_xml_id': node.get('xmlId'),
}
if not steuerung_obj.search([
('tasy_xml_id', '=', vals['tasy_xml_id'])], limit=1):
steuerung_obj.create(vals)
steuerung_created += 1
protocol.append('Zu-/Abschlag-Steuerungen: %d neu'
% steuerung_created)
# 2) Tarife, dann Tarifgruppen (m2m auf die Kataloge),
# dann Tarifsets (m2m auf die Tarife).
self._upsert_tarif(self._section(root, 'tarife'), protocol)
tarif_map = {t.tasy_xml_id: t for t in self.env[
'l10n.at.payroll.tasy.tarif'].search([])}
tarifgruppe_obj = self.env['l10n.at.payroll.tasy.tarifgruppe']
created = updated = 0
for node in self._section(root, 'tarifgruppen'):
code = self._tasy_text(node, 'code')
von = self._tasy_text(node, 'vonDatum')
if not code or not von:
continue
vals = {
'code': code,
'bezeichnung': self._tasy_text(node, 'bezeichnung'),
'traegergruppe': self._tasy_text(node, 'traegergruppe'),
'von_datum': fields.Date.to_date(von),
'bis_datum': fields.Date.to_date(
self._tasy_text(node, 'bisDatum') or '2999-12-31'),
'tasy_xml_id': node.get('xmlId'),
'fallweise_zulaessig': self._tasy_bool(
self._tasy_text(node,
'fallweiseBeschaeftigungZulaessig')),
'bv_zulaessig': self._tasy_bool(
self._tasy_text(node,
'betrieblicheVorsorgeZulaessig')),
}
rec = tarifgruppe_obj.search([
('traegergruppe', '=', vals['traegergruppe']),
('code', '=', code),
('von_datum', '=', vals['von_datum'])], limit=1)
if not rec:
rec = tarifgruppe_obj.create(vals)
created += 1
else:
rec.write(vals)
updated += 1
rec.write({
'bsgr_ids': [(6, 0, [
bsgr_map[r].id for r in self._tasy_texts(
node, 'beschaeftigtengruppen')
if r in bsgr_map])],
'ergaenzung_ids': [(6, 0, [
erg_map[r].id for r in self._tasy_texts(
node, 'ergaenzungen') if r in erg_map])],
'dienstgebergruppe_ids': [(6, 0, [
dgg_map[r].id for r in self._tasy_texts(
node, 'dienstgeberGruppen')
if r in dgg_map])],
})
protocol.append('Tarifgruppen: %d neu, %d aktualisiert'
% (created, updated))
self._upsert_tarifset(self._section(root, 'tarifsets'),
tarif_map, protocol)
tarifset_map = {s.tasy_xml_id: s for s in self.env[
'l10n.at.payroll.tasy.tarifset'].search([])}
tarifgruppe_map = {t.tasy_xml_id: t for t in tarifgruppe_obj
.search([])}
# 3) Verrechnungsbasis-/Positionstypen + Kombinationen
# auflösen, dann tarifgruppeSetZuord importieren.
basis_typ = {b.get('xmlId'): self._tasy_text(b, 'code')
for b in self._section(root, 'verrechBasisTypen')}
pos_typ = {p.get('xmlId'): self._tasy_text(p, 'code')
for p in self._section(root, 'verrechPosTypen')}
kombinationen = {}
for k in self._section(root, 'verrechBasisPositionKombinationen'):
kombinationen[k.get('xmlId')] = (
basis_typ.get(self._tasy_text(k, 'verrechnungBasisTyp'),
''),
pos_typ.get(self._tasy_text(k, 'verrechnungPosTyp'), ''),
)
zuord_obj = self.env['l10n.at.payroll.tasy.tarifset.zuord']
created = updated = 0
for node in self._section(root, 'tarifgruppeSetZuordnungen'):
tarifgruppe = tarifgruppe_map.get(
self._tasy_text(node, 'tarifgruppe'))
tarifset = tarifset_map.get(self._tasy_text(node, 'tarifset'))
von = self._tasy_text(node, 'vonDatum')
if not tarifgruppe or not tarifset or not von:
continue
basis, pos = kombinationen.get(
self._tasy_text(node, 'verrechBasPosKombination'),
('', ''))
vals = {
'tarifgruppe_id': tarifgruppe.id,
'tarifset_id': tarifset.id,
'basis_typ': basis,
'pos_typ': pos,
'traegergruppe': self._tasy_text(node, 'traegergruppe'),
'von_datum': fields.Date.to_date(von),
'bis_datum': fields.Date.to_date(
self._tasy_text(node, 'bisDatum') or '2999-12-31'),
'tasy_xml_id': node.get('xmlId'),
}
rec = zuord_obj.search([
('tarifgruppe_id', '=', tarifgruppe.id),
('tarifset_id', '=', tarifset.id),
('basis_typ', '=', basis),
('pos_typ', '=', pos),
('von_datum', '=', vals['von_datum'])], limit=1)
if rec:
rec.write(vals)
updated += 1
else:
zuord_obj.create(vals)
created += 1
protocol.append('Tarifgruppe-Tarifset-Zuordnungen: '
'%d neu, %d aktualisiert' % (created, updated))
# 4) Zu-/Abschlag-Zuordnungen (m2m auf Tarifsets/Tarifgruppen).
verknuepft = 0
for node in self._section(root, 'zuAbschlagTarifsetZuordnungen'):
zuabschlag = zuabschlag_map.get(
self._tasy_text(node, 'zuAbschlag'))
if not zuabschlag:
continue
tarifset = tarifset_map.get(
self._tasy_text(node, 'tarifsetByTarifsetId'))
tarifgruppen = [
tarifgruppe_map[r] for r in self._tasy_texts(
node, 'tarifgruppe') if r in tarifgruppe_map]
if tarifset:
zuabschlag.tarifset_ids = [(4, tarifset.id)]
for tarifgruppe in tarifgruppen:
zuabschlag.tarifgruppe_ids = [(4, tarifgruppe.id)]
verknuepft += 1
protocol.append('Zu-/Abschlag-Zuordnungen: %d verknüpft'
% verknuepft)
self.protocol = '\n'.join(protocol)
return {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'type': 'success',
'message': _('Tarifsystem-Katalog importiert.'),
'next': {'type': 'ir.actions.act_window_close'},
},
}
@@ -0,0 +1,300 @@
# Part of the odoo-at-payroll project. License: LGPL-3.
"""SV-Tarifsystem-Katalog (AP0, Variante b): Beschäftigtengruppen
(BSGR), Ergänzungen (ERGB), Dienstgebergruppen, Tarifgruppen,
Tarifsets/Tarife und Zu-/Abschläge — geladen aus dem TASY-LSWH-Export
(sozialversicherung.at/tarifsystem; DM-ORG D.48/D.49: Tarifgruppe =
BSGR + ERGB).
Der Katalog wird zur Laufzeit über den Import-Wizard
(``l10n.at.payroll.tasy.katalog.wizard``, models/tasy.py) gepflegt —
alle Trägergruppen, versioniert über von/bis-Datum; kein XML-Seed
(keine erfundenen Werte). Die mBGM-Tarifierung des Privatmoduls liest
die Sätze von hier (Katalog vor Hand-Parameter ``at_mbgm_tarif``).
"""
from odoo import api, fields, models
class L10nAtPayrollTasyBsgr(models.Model):
_name = 'l10n.at.payroll.tasy.bsgr'
_description = 'SV-Tarifsystem: Beschäftigtengruppe (BSGR)'
_order = 'code, von_datum'
code = fields.Char(required=True, index=True)
bezeichnung = fields.Char()
kurzbezeichnung = fields.Char()
beschreibung = fields.Char()
traegergruppe = fields.Char(index=True)
von_datum = fields.Date(string='Gültig ab', required=True)
bis_datum = fields.Date(string='Gültig bis', required=True)
tasy_xml_id = fields.Char(index=True)
_unique_bsgr = models.Constraint(
'unique (traegergruppe, code, von_datum)',
"Je Trägergruppe, Code und Gültig-ab gibt es nur eine "
"Beschäftigtengruppe.")
class L10nAtPayrollTasyErgaenzung(models.Model):
_name = 'l10n.at.payroll.tasy.ergaenzung'
_description = 'SV-Tarifsystem: Ergänzung zur BSGR (ERGB)'
_order = 'code, von_datum'
code = fields.Char(required=True, index=True)
bezeichnung = fields.Char()
kurzbezeichnung = fields.Char()
beschreibung = fields.Char()
traegergruppe = fields.Char(index=True)
von_datum = fields.Date(string='Gültig ab', required=True)
bis_datum = fields.Date(string='Gültig bis', required=True)
tasy_xml_id = fields.Char(index=True)
_unique_ergaenzung = models.Constraint(
'unique (traegergruppe, code, von_datum)',
"Je Trägergruppe, Code und Gültig-ab gibt es nur eine "
"Ergänzung.")
class L10nAtPayrollTasyDienstgebergruppe(models.Model):
_name = 'l10n.at.payroll.tasy.dienstgebergruppe'
_description = 'SV-Tarifsystem: Dienstgebergruppe'
_order = 'code, von_datum'
code = fields.Char(required=True, index=True)
bezeichnung = fields.Char()
kurzbezeichnung = fields.Char()
beschreibung = fields.Char()
traegergruppe = fields.Char(index=True)
von_datum = fields.Date(string='Gültig ab', required=True)
bis_datum = fields.Date(string='Gültig bis', required=True)
tasy_xml_id = fields.Char(index=True)
_unique_dgg = models.Constraint(
'unique (traegergruppe, code, von_datum)',
"Je Trägergruppe, Code und Gültig-ab gibt es nur eine "
"Dienstgebergruppe.")
class L10nAtPayrollTasyTarif(models.Model):
"""Einzelner Tarifsatz (Zweig × DN/DG-Anteil × Gültigkeit).
Kein natürlicher Code im Export — Identität ist der Inhalt
(Zweig, Sätze, Gültigkeit); der Katalog-Import vereinheitlicht
über diese Inhaltsmerkmale statt über die wechselnden xmlIds.
"""
_name = 'l10n.at.payroll.tasy.tarif'
_description = 'SV-Tarifsystem: Tarif (Zweig-Satz)'
_order = 'zweig, von_datum'
zweig = fields.Char(required=True, index=True)
dn = fields.Float(string='DN-Anteil (%)', digits=(16, 3))
dg = fields.Float(string='DG-Anteil (%)', digits=(16, 3))
traegergruppe = fields.Char(index=True)
von_datum = fields.Date(string='Gültig ab', required=True)
bis_datum = fields.Date(string='Gültig bis', required=True)
tasy_xml_id = fields.Char(index=True)
tarifset_ids = fields.Many2many(
'l10n.at.payroll.tasy.tarifset',
'l10n_at_payroll_tasy_tarif_tarifset_rel',
'tarif_id', 'tarifset_id', string='Tarifsets')
class L10nAtPayrollTasyTarifset(models.Model):
_name = 'l10n.at.payroll.tasy.tarifset'
_description = 'SV-Tarifsystem: Tarifset'
_order = 'kurzbezeichnung, von_datum'
bezeichnung = fields.Char()
kurzbezeichnung = fields.Char()
traegergruppe = fields.Char(index=True)
von_datum = fields.Date(string='Gültig ab', required=True)
bis_datum = fields.Date(string='Gültig bis', required=True)
tasy_xml_id = fields.Char(index=True)
tarif_ids = fields.Many2many(
'l10n.at.payroll.tasy.tarif',
'l10n_at_payroll_tasy_tarif_tarifset_rel',
'tarifset_id', 'tarif_id', string='Tarife')
_unique_tarifset = models.Constraint(
'unique (traegergruppe, kurzbezeichnung, von_datum)',
"Je Trägergruppe, Kurzbezeichnung und Gültig-ab gibt es nur "
"ein Tarifset.")
class L10nAtPayrollTasyTarifgruppe(models.Model):
"""Tarifgruppe = BSGR (+ Ergänzungen) × Dienstgebergruppen.
Auflösung: ``_l10n_at_find`` (BSGR + ERGB-Menge + DGG am Stichtag)
→ ``_l10n_at_saetze`` (Sätze je Verrechnungsbasis aus dem
zugeordneten Tarifset).
"""
_name = 'l10n.at.payroll.tasy.tarifgruppe'
_description = 'SV-Tarifsystem: Tarifgruppe'
_order = 'code, von_datum'
code = fields.Char(required=True, index=True)
bezeichnung = fields.Char()
traegergruppe = fields.Char(index=True)
von_datum = fields.Date(string='Gültig ab', required=True)
bis_datum = fields.Date(string='Gültig bis', required=True)
tasy_xml_id = fields.Char(index=True)
bsgr_ids = fields.Many2many(
'l10n.at.payroll.tasy.bsgr',
'l10n_at_payroll_tasy_tarifgruppe_bsgr_rel',
'tarifgruppe_id', 'bsgr_id', string='Beschäftigtengruppen')
ergaenzung_ids = fields.Many2many(
'l10n.at.payroll.tasy.ergaenzung',
'l10n_at_payroll_tasy_tarifgruppe_ergaenzung_rel',
'tarifgruppe_id', 'ergaenzung_id', string='Ergänzungen (ERGB)')
dienstgebergruppe_ids = fields.Many2many(
'l10n.at.payroll.tasy.dienstgebergruppe',
'l10n_at_payroll_tasy_tarifgruppe_dgg_rel',
'tarifgruppe_id', 'dienstgebergruppe_id',
string='Dienstgebergruppen')
fallweise_zulaessig = fields.Boolean(string='Fallweise zulässig')
bv_zulaessig = fields.Boolean(
string='Betriebliche Vorsorge zulässig')
zuord_ids = fields.One2many(
'l10n.at.payroll.tasy.tarifset.zuord', 'tarifgruppe_id',
string='Tarifset-Zuordnungen')
_unique_tarifgruppe = models.Constraint(
'unique (traegergruppe, code, von_datum)',
"Je Trägergruppe, Code und Gültig-ab gibt es nur eine "
"Tarifgruppe.")
@api.model
def _l10n_at_find(self, bsgr_code, ergaenzung_codes, dgg_code,
stichtag):
"""Tarifgruppe zu BSGR + ERGB-Menge + DGG am Stichtag.
Die ERGB-Menge muss exakt passen (leere Menge = Normalfall
ohne Ergänzung) — DM-ORG D.48/D.49. Liefert leere Recordset,
wenn keine Zeile existiert (Aufrufer entscheidet über
Fallback/Abbruch).
"""
ergaenzung_codes = set(ergaenzung_codes or ())
kandidaten = self.search([
('bsgr_ids.code', '=', bsgr_code),
('von_datum', '<=', stichtag),
('bis_datum', '>=', stichtag),
])
if dgg_code:
kandidaten = kandidaten.filtered(
lambda t: dgg_code
in t.dienstgebergruppe_ids.mapped('code'))
for tarifgruppe in kandidaten:
if {e.code for e in tarifgruppe.ergaenzung_ids} \
== ergaenzung_codes:
return tarifgruppe
return self.env['l10n.at.payroll.tasy.tarifgruppe']
def _l10n_at_saetze(self, basis_typ, stichtag):
"""Sätze je Verrechnungsbasis (AB/SZ/BV/UU …) am Stichtag.
Rückgabe ``None`` ohne gültige Tarifset-Zuordnung, sonst
``{'dn': x, 'dg': y, 'gesamt': x + y, 'komponenten': tarif}``.
Je Zweig zählt der späteste gültige Tarif (Schutz gegen
duplette Import-Zeilen).
"""
self.ensure_one()
zuord = self.zuord_ids.filtered(
lambda z: z.basis_typ == basis_typ
and z.von_datum <= stichtag <= z.bis_datum)
if not zuord:
return None
zuord = zuord.sorted(key=lambda z: z.von_datum)[-1]
tarife = zuord.tarifset_id.tarif_ids.filtered(
lambda t: t.von_datum <= stichtag <= t.bis_datum)
komponenten = []
gesehene_zweige = set()
for tarif in tarife.sorted(key=lambda t: t.von_datum,
reverse=True):
if tarif.zweig in gesehene_zweige:
continue
gesehene_zweige.add(tarif.zweig)
komponenten.append(tarif)
dn = sum(t.dn for t in komponenten)
dg = sum(t.dg for t in komponenten)
return {'dn': dn, 'dg': dg, 'gesamt': dn + dg,
'komponenten': komponenten}
class L10nAtPayrollTasyTarifsetZuord(models.Model):
"""tarifgruppeSetZuord: Tarifgruppe × Tarifset × Basis/Position."""
_name = 'l10n.at.payroll.tasy.tarifset.zuord'
_description = 'SV-Tarifsystem: Tarifgruppe-Tarifset-Zuordnung'
_order = 'tarifgruppe_id, basis_typ, von_datum'
tarifgruppe_id = fields.Many2one(
'l10n.at.payroll.tasy.tarifgruppe', required=True, index=True,
ondelete='cascade')
tarifset_id = fields.Many2one(
'l10n.at.payroll.tasy.tarifset', required=True, index=True,
ondelete='cascade')
basis_typ = fields.Char(string='Verrechnungsbasis', index=True)
pos_typ = fields.Char(string='Verrechnungsposition', index=True)
traegergruppe = fields.Char(index=True)
von_datum = fields.Date(string='Gültig ab', required=True)
bis_datum = fields.Date(string='Gültig bis', required=True)
tasy_xml_id = fields.Char(index=True)
_unique_zuord = models.Constraint(
'unique (tarifgruppe_id, tarifset_id, basis_typ, pos_typ, '
'von_datum)',
"Diese Tarifgruppe-Tarifset-Zuordnung existiert bereits.")
class L10nAtPayrollTasyZuabschlag(models.Model):
_name = 'l10n.at.payroll.tasy.zuabschlag'
_description = 'SV-Tarifsystem: Zu-/Abschlag'
_order = 'code, von_datum'
code = fields.Char(required=True, index=True)
bezeichnung = fields.Char()
kurzbezeichnung = fields.Char()
beschreibung = fields.Char()
zuabschlagkz = fields.Char(string='Zu-/Abschlag-Kennzeichen')
versicherungsrelevant = fields.Boolean()
traegergruppe = fields.Char(index=True)
von_datum = fields.Date(string='Gültig ab', required=True)
bis_datum = fields.Date(string='Gültig bis', required=True)
tasy_xml_id = fields.Char(index=True)
steuerung_ids = fields.One2many(
'l10n.at.payroll.tasy.zuabschlag.steuerung', 'zuabschlag_id',
string='Steuerung (Alters-/Einkommensgrenzen)')
tarifset_ids = fields.Many2many(
'l10n.at.payroll.tasy.tarifset',
'l10n_at_payroll_tasy_zuabschlag_tarifset_rel',
'zuabschlag_id', 'tarifset_id', string='Tarifsets')
tarifgruppe_ids = fields.Many2many(
'l10n.at.payroll.tasy.tarifgruppe',
'l10n_at_payroll_tasy_zuabschlag_tarifgruppe_rel',
'zuabschlag_id', 'tarifgruppe_id', string='Tarifgruppen')
_unique_zuabschlag = models.Constraint(
'unique (traegergruppe, code, von_datum)',
"Je Trägergruppe, Code und Gültig-ab gibt es nur einen "
"Zu-/Abschlag.")
class L10nAtPayrollTasyZuabschlagSteuerung(models.Model):
"""Steuerung eines Zu-/Abschlags (Altersgrenzen, Geschlecht,
Geburtsjahrgang) — informativ; die mBGM-Abschlagslogik rechnet
aus den GP4-Stichtagsflags, nicht aus dieser Tabelle."""
_name = 'l10n.at.payroll.tasy.zuabschlag.steuerung'
_description = 'SV-Tarifsystem: Zu-/Abschlag-Steuerung'
_order = 'zuabschlag_id, von_datum'
zuabschlag_id = fields.Many2one(
'l10n.at.payroll.tasy.zuabschlag', required=True, index=True,
ondelete='cascade')
altersgrenze_min_jahr = fields.Float(
string='Altersgrenze min. (Jahre)', digits=(16, 1))
altersgrenze_soll_jahr = fields.Float(
string='Altersgrenze soll (Jahre)', digits=(16, 1))
geboren_bis = fields.Date(string='Geboren bis')
geschlecht = fields.Char()
traegergruppe = fields.Char(index=True)
von_datum = fields.Date(string='Gültig ab', required=True)
bis_datum = fields.Date(string='Gültig bis', required=True)
tasy_xml_id = fields.Char(index=True)
@@ -38,6 +38,17 @@
<field name="binding_type">report</field> <field name="binding_type">report</field>
</record> </record>
<record id="action_report_dg_kosten" model="ir.actions.report">
<field name="name">Dienstgeber-Gesamtkosten</field>
<field name="model">hr.payslip.run</field>
<field name="report_type">qweb-pdf</field>
<field name="report_name">l10n_at_hr_payroll.report_dg_kosten</field>
<field name="report_file">l10n_at_hr_payroll.report_dg_kosten</field>
<field name="paperformat_id" ref="hr_payroll.paperformat_euro_light"/>
<field name="binding_model_id" ref="hr_payroll.model_hr_payslip_run"/>
<field name="binding_type">report</field>
</record>
<!-- Lohnzettel AT: AT-Layout — Lohnarten nach Kategorie, DN-Abzüge, <!-- Lohnzettel AT: AT-Layout — Lohnarten nach Kategorie, DN-Abzüge,
DG-Anteile (SV-DG, FLAF, KommSt, DZ, BV), Netto. --> DG-Anteile (SV-DG, FLAF, KommSt, DZ, BV), Netto. -->
<template id="report_lohnzettel_at"> <template id="report_lohnzettel_at">
@@ -170,13 +181,15 @@
</t> </t>
</template> </template>
<!-- Monatsjournal: je Lohnart die Summe über den Lauf --> <!-- Monatsjournal: je Lohnart die Summe über den Lauf — Bezüge/ -->
<!-- Abzüge und (seit AP2a) eigener Block Dienstgeberanteile. -->
<template id="report_monatsjournal"> <template id="report_monatsjournal">
<t t-call="web.html_container"> <t t-call="web.html_container">
<t t-foreach="journal" t-as="j"> <t t-foreach="journal" t-as="j">
<t t-call="web.external_layout"> <t t-call="web.external_layout">
<div class="page"> <div class="page">
<h2>Monatsjournal — <t t-out="j['run'].name"/></h2> <h2>Monatsjournal — <t t-out="j['run'].name"/></h2>
<h4>Bezüge &amp; Abzüge</h4>
<table class="table table-sm"> <table class="table table-sm">
<thead> <thead>
<tr> <tr>
@@ -193,6 +206,92 @@
<td><t t-out="art['bemessung']"/></td> <td><t t-out="art['bemessung']"/></td>
<td class="text-end"><span t-out="art['summe']" t-options="{&quot;widget&quot;: &quot;monetary&quot;, &quot;display_currency&quot;: j['run'].company_id.currency_id}"/></td> <td class="text-end"><span t-out="art['summe']" t-options="{&quot;widget&quot;: &quot;monetary&quot;, &quot;display_currency&quot;: j['run'].company_id.currency_id}"/></td>
</tr> </tr>
<tr>
<td colspan="3"><strong>Summe Bezüge &amp; Abzüge</strong></td>
<td class="text-end"><strong><span t-out="j['summe_arten']" t-options="{&quot;widget&quot;: &quot;monetary&quot;, &quot;display_currency&quot;: j['run'].company_id.currency_id}"/></strong></td>
</tr>
</tbody>
</table>
<h4>Dienstgeberanteile (Lohnnebenkosten)</h4>
<table class="table table-sm">
<thead>
<tr>
<th>Code</th>
<th>Position</th>
<th>Bemessung</th>
<th class="text-end">Summe</th>
</tr>
</thead>
<tbody>
<tr t-foreach="j['dg_arten']" t-as="art">
<td><t t-out="art['code']"/></td>
<td><t t-out="art['name']"/></td>
<td><t t-out="art['bemessung']"/></td>
<td class="text-end"><span t-out="art['summe']" t-options="{&quot;widget&quot;: &quot;monetary&quot;, &quot;display_currency&quot;: j['run'].company_id.currency_id}"/></td>
</tr>
<tr>
<td colspan="3"><strong>Summe Dienstgeberanteile</strong></td>
<td class="text-end"><strong><span t-out="j['summe_dg']" t-options="{&quot;widget&quot;: &quot;monetary&quot;, &quot;display_currency&quot;: j['run'].company_id.currency_id}"/></strong></td>
</tr>
</tbody>
</table>
</div>
</t>
</t>
</t>
</template>
<!-- Dienstgeber-Gesamtkosten: Brutto + Lohnnebenkosten je AN, -->
<!-- aufgeschlüsselt und kumuliert (AP2a). -->
<template id="report_dg_kosten">
<t t-call="web.html_container">
<t t-foreach="docs" t-as="run">
<t t-call="web.external_layout">
<div class="page">
<h2>Dienstgeber-Gesamtkosten — <t t-out="run.name"/></h2>
<table class="table table-sm">
<thead>
<tr>
<th>Mitarbeiter:in</th>
<th class="text-end">Bruttolohn</th>
<th class="text-end">SV-DG (laufend+SZ)</th>
<th class="text-end">FLAF-DB</th>
<th class="text-end">KommSt</th>
<th class="text-end">DZ (WKO)</th>
<th class="text-end">BV</th>
<th class="text-end">Wr. DAG</th>
<th class="text-end">Weitere DG</th>
<th class="text-end">Σ Lohnnebenkosten</th>
<th class="text-end">Gesamtkosten</th>
</tr>
</thead>
<tbody>
<tr t-foreach="zeilen" t-as="z">
<td><t t-out="z['slip'].employee_id.name"/></td>
<td class="text-end"><span t-out="z['brutto']" t-options="{&quot;widget&quot;: &quot;monetary&quot;, &quot;display_currency&quot;: run.company_id.currency_id}"/></td>
<td class="text-end"><span t-out="z['svdg']" t-options="{&quot;widget&quot;: &quot;monetary&quot;, &quot;display_currency&quot;: run.company_id.currency_id}"/></td>
<td class="text-end"><span t-out="z['spalten']['FLAFDB']" t-options="{&quot;widget&quot;: &quot;monetary&quot;, &quot;display_currency&quot;: run.company_id.currency_id}"/></td>
<td class="text-end"><span t-out="z['spalten']['KOMMST']" t-options="{&quot;widget&quot;: &quot;monetary&quot;, &quot;display_currency&quot;: run.company_id.currency_id}"/></td>
<td class="text-end"><span t-out="z['spalten']['DZ']" t-options="{&quot;widget&quot;: &quot;monetary&quot;, &quot;display_currency&quot;: run.company_id.currency_id}"/></td>
<td class="text-end"><span t-out="z['spalten']['BVG']" t-options="{&quot;widget&quot;: &quot;monetary&quot;, &quot;display_currency&quot;: run.company_id.currency_id}"/></td>
<td class="text-end"><span t-out="z['spalten']['WIEN_DAG']" t-options="{&quot;widget&quot;: &quot;monetary&quot;, &quot;display_currency&quot;: run.company_id.currency_id}"/></td>
<td class="text-end"><span t-out="z['weitere']" t-options="{&quot;widget&quot;: &quot;monetary&quot;, &quot;display_currency&quot;: run.company_id.currency_id}"/></td>
<td class="text-end"><span t-out="z['dg_summe']" t-options="{&quot;widget&quot;: &quot;monetary&quot;, &quot;display_currency&quot;: run.company_id.currency_id}"/></td>
<td class="text-end"><strong><span t-out="z['gesamtkosten']" t-options="{&quot;widget&quot;: &quot;monetary&quot;, &quot;display_currency&quot;: run.company_id.currency_id}"/></strong></td>
</tr>
<tr>
<td><strong>Summe Lauf</strong></td>
<td class="text-end"><strong><span t-out="summen['brutto']" t-options="{&quot;widget&quot;: &quot;monetary&quot;, &quot;display_currency&quot;: run.company_id.currency_id}"/></strong></td>
<td class="text-end"><strong><span t-out="summen['svdg']" t-options="{&quot;widget&quot;: &quot;monetary&quot;, &quot;display_currency&quot;: run.company_id.currency_id}"/></strong></td>
<td class="text-end"><strong><span t-out="summen['spalten']['FLAFDB']" t-options="{&quot;widget&quot;: &quot;monetary&quot;, &quot;display_currency&quot;: run.company_id.currency_id}"/></strong></td>
<td class="text-end"><strong><span t-out="summen['spalten']['KOMMST']" t-options="{&quot;widget&quot;: &quot;monetary&quot;, &quot;display_currency&quot;: run.company_id.currency_id}"/></strong></td>
<td class="text-end"><strong><span t-out="summen['spalten']['DZ']" t-options="{&quot;widget&quot;: &quot;monetary&quot;, &quot;display_currency&quot;: run.company_id.currency_id}"/></strong></td>
<td class="text-end"><strong><span t-out="summen['spalten']['BVG']" t-options="{&quot;widget&quot;: &quot;monetary&quot;, &quot;display_currency&quot;: run.company_id.currency_id}"/></strong></td>
<td class="text-end"><strong><span t-out="summen['spalten']['WIEN_DAG']" t-options="{&quot;widget&quot;: &quot;monetary&quot;, &quot;display_currency&quot;: run.company_id.currency_id}"/></strong></td>
<td class="text-end"><strong><span t-out="summen['weitere']" t-options="{&quot;widget&quot;: &quot;monetary&quot;, &quot;display_currency&quot;: run.company_id.currency_id}"/></strong></td>
<td class="text-end"><strong><span t-out="summen['dg_summe']" t-options="{&quot;widget&quot;: &quot;monetary&quot;, &quot;display_currency&quot;: run.company_id.currency_id}"/></strong></td>
<td class="text-end"><strong><span t-out="summen['gesamtkosten']" t-options="{&quot;widget&quot;: &quot;monetary&quot;, &quot;display_currency&quot;: run.company_id.currency_id}"/></strong></td>
</tr>
</tbody> </tbody>
</table> </table>
</div> </div>
@@ -0,0 +1,138 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- ============================================================ -->
<!-- AP2b/AP2c: Jahreslohnkonto je Dienstnehmer:in und -->
<!-- Betriebsjahreslohnkonto — Monatsmatrix je Lohnart; Druck -->
<!-- über den Wizard l10n.at.payroll.jahresbericht.wizard. -->
<!-- ============================================================ -->
<record id="action_report_jahreslohnkonto" model="ir.actions.report">
<field name="name">Jahreslohnkonto (Dienstnehmer:in)</field>
<field name="model">hr.employee</field>
<field name="report_type">qweb-pdf</field>
<field name="report_name">l10n_at_hr_payroll.report_jahreslohnkonto</field>
<field name="report_file">l10n_at_hr_payroll.report_jahreslohnkonto</field>
<field name="paperformat_id" ref="hr_payroll.paperformat_euro_light"/>
</record>
<record id="action_report_betriebslohnkonto" model="ir.actions.report">
<field name="name">Betriebsjahreslohnkonto</field>
<field name="model">res.company</field>
<field name="report_type">qweb-pdf</field>
<field name="report_name">l10n_at_hr_payroll.report_betriebslohnkonto</field>
<field name="report_file">l10n_at_hr_payroll.report_betriebslohnkonto</field>
<field name="paperformat_id" ref="hr_payroll.paperformat_euro_light"/>
</record>
<template id="report_jahreslohnkonto">
<t t-call="web.html_container">
<t t-foreach="berichte" t-as="b">
<t t-call="web.external_layout">
<div class="page">
<h2>Jahreslohnkonto <t t-out="jahr"/><t t-out="b['employee'].name"/></h2>
<table class="table table-sm">
<thead>
<tr>
<th>Code</th>
<th>Lohnart</th>
<th>Kategorie</th>
<th class="text-end" t-foreach="range(1, 13)" t-as="monat">
<t t-out="monat"/>
</th>
<th class="text-end">Jahr</th>
</tr>
</thead>
<tbody>
<tr t-foreach="b['zeilen']" t-as="zeile">
<td><t t-out="zeile['code']"/></td>
<td><t t-out="zeile['name']"/></td>
<td><t t-out="zeile['kategorie']"/></td>
<td class="text-end" t-foreach="zeile['monate']" t-as="wert">
<span t-out="wert" t-options='{"widget": "monetary", "display_currency": b["employee"].company_id.currency_id}'/>
</td>
<td class="text-end"><strong><span t-out="zeile['total']" t-options='{"widget": "monetary", "display_currency": b["employee"].company_id.currency_id}'/></strong></td>
</tr>
</tbody>
</table>
</div>
</t>
</t>
</t>
</template>
<template id="report_betriebslohnkonto">
<t t-call="web.html_container">
<t t-foreach="berichte" t-as="b">
<t t-call="web.external_layout">
<div class="page">
<h2>Betriebsjahreslohnkonto <t t-out="jahr"/><t t-out="b['company'].name"/></h2>
<table class="table table-sm">
<thead>
<tr>
<th>Code</th>
<th>Lohnart</th>
<th>Kategorie</th>
<th class="text-end" t-foreach="range(1, 13)" t-as="monat">
<t t-out="monat"/>
</th>
<th class="text-end">Jahr</th>
</tr>
</thead>
<tbody>
<tr t-foreach="b['zeilen']" t-as="zeile">
<td><t t-out="zeile['code']"/></td>
<td><t t-out="zeile['name']"/></td>
<td><t t-out="zeile['kategorie']"/></td>
<td class="text-end" t-foreach="zeile['monate']" t-as="wert">
<span t-out="wert" t-options='{"widget": "monetary", "display_currency": b["company"].currency_id}'/>
</td>
<td class="text-end"><strong><span t-out="zeile['total']" t-options='{"widget": "monetary", "display_currency": b["company"].currency_id}'/></strong></td>
</tr>
</tbody>
</table>
</div>
</t>
</t>
</t>
</template>
<!-- Druck-Wizard -->
<record id="jahresbericht_wizard_form_view" model="ir.ui.view">
<field name="name">l10n.at.payroll.jahresbericht.wizard.form</field>
<field name="model">l10n.at.payroll.jahresbericht.wizard</field>
<field name="arch" type="xml">
<form string="Jahresberichte drucken">
<group>
<group>
<field name="jahr"/>
<field name="art"/>
</group>
<group>
<field name="company_id"
groups="base.group_multi_company"/>
<field name="employee_ids" widget="many2many_tags"
invisible="art != 'dn'"/>
</group>
</group>
<footer>
<button string="Drucken" name="action_drucken"
type="object" class="btn-primary"/>
<button string="Abbrechen" class="btn-secondary"
special="cancel"/>
</footer>
</form>
</field>
</record>
<record id="action_jahresbericht_wizard" model="ir.actions.act_window">
<field name="name">Jahresberichte (Lohnkonten)</field>
<field name="res_model">l10n.at.payroll.jahresbericht.wizard</field>
<field name="view_mode">form</field>
<field name="target">new</field>
</record>
<menuitem id="menu_l10n_at_payroll_jahresberichte"
name="Jahresberichte (Lohnkonten)"
parent="menu_l10n_at_payroll_root"
action="action_jahresbericht_wizard" sequence="80"/>
</odoo>
@@ -13,3 +13,23 @@ access_l10n_at_payroll_frist_manager,L10n AT Payroll Frist Manager,model_l10n_at
access_l10n_at_payroll_jahresretro_wizard,L10n AT Payroll Jahresretro Wizard,model_l10n_at_payroll_jahresretro_wizard,hr_payroll.group_hr_payroll_user,1,1,1,1 access_l10n_at_payroll_jahresretro_wizard,L10n AT Payroll Jahresretro Wizard,model_l10n_at_payroll_jahresretro_wizard,hr_payroll.group_hr_payroll_user,1,1,1,1
access_l10n_at_payroll_jahresretro_ergebnis,L10n AT Payroll Jahresretro Ergebnis,model_l10n_at_payroll_jahresretro_ergebnis,hr_payroll.group_hr_payroll_user,1,1,1,1 access_l10n_at_payroll_jahresretro_ergebnis,L10n AT Payroll Jahresretro Ergebnis,model_l10n_at_payroll_jahresretro_ergebnis,hr_payroll.group_hr_payroll_user,1,1,1,1
access_l10n_at_payroll_mitteilungsfile_wizard,L10n AT Payroll Mitteilungsfile Wizard,model_l10n_at_payroll_mitteilungsfile_wizard,hr_payroll.group_hr_payroll_user,1,1,1,1 access_l10n_at_payroll_mitteilungsfile_wizard,L10n AT Payroll Mitteilungsfile Wizard,model_l10n_at_payroll_mitteilungsfile_wizard,hr_payroll.group_hr_payroll_user,1,1,1,1
access_l10n_at_payroll_jahresbericht_wizard,L10n AT Payroll Jahresbericht Wizard,model_l10n_at_payroll_jahresbericht_wizard,hr_payroll.group_hr_payroll_user,1,1,1,1
access_l10n_at_payroll_tasy_katalog_wizard,L10n AT Payroll TASY-Katalog Wizard,model_l10n_at_payroll_tasy_katalog_wizard,hr_payroll.group_hr_payroll_user,1,1,1,1
access_l10n_at_payroll_tasy_bsgr_user,L10n AT Payroll TASY-BSGR User,model_l10n_at_payroll_tasy_bsgr,hr_payroll.group_hr_payroll_user,1,0,0,0
access_l10n_at_payroll_tasy_bsgr_manager,L10n AT Payroll TASY-BSGR Manager,model_l10n_at_payroll_tasy_bsgr,hr_payroll.group_hr_payroll_manager,1,1,1,1
access_l10n_at_payroll_tasy_ergaenzung_user,L10n AT Payroll TASY-Ergänzung User,model_l10n_at_payroll_tasy_ergaenzung,hr_payroll.group_hr_payroll_user,1,0,0,0
access_l10n_at_payroll_tasy_ergaenzung_manager,L10n AT Payroll TASY-Ergänzung Manager,model_l10n_at_payroll_tasy_ergaenzung,hr_payroll.group_hr_payroll_manager,1,1,1,1
access_l10n_at_payroll_tasy_dgg_user,L10n AT Payroll TASY-Dienstgebergruppe User,model_l10n_at_payroll_tasy_dienstgebergruppe,hr_payroll.group_hr_payroll_user,1,0,0,0
access_l10n_at_payroll_tasy_dgg_manager,L10n AT Payroll TASY-Dienstgebergruppe Manager,model_l10n_at_payroll_tasy_dienstgebergruppe,hr_payroll.group_hr_payroll_manager,1,1,1,1
access_l10n_at_payroll_tasy_tarifgruppe_user,L10n AT Payroll TASY-Tarifgruppe User,model_l10n_at_payroll_tasy_tarifgruppe,hr_payroll.group_hr_payroll_user,1,0,0,0
access_l10n_at_payroll_tasy_tarifgruppe_manager,L10n AT Payroll TASY-Tarifgruppe Manager,model_l10n_at_payroll_tasy_tarifgruppe,hr_payroll.group_hr_payroll_manager,1,1,1,1
access_l10n_at_payroll_tasy_tarifset_user,L10n AT Payroll TASY-Tarifset User,model_l10n_at_payroll_tasy_tarifset,hr_payroll.group_hr_payroll_user,1,0,0,0
access_l10n_at_payroll_tasy_tarifset_manager,L10n AT Payroll TASY-Tarifset Manager,model_l10n_at_payroll_tasy_tarifset,hr_payroll.group_hr_payroll_manager,1,1,1,1
access_l10n_at_payroll_tasy_tarif_user,L10n AT Payroll TASY-Tarif User,model_l10n_at_payroll_tasy_tarif,hr_payroll.group_hr_payroll_user,1,0,0,0
access_l10n_at_payroll_tasy_tarif_manager,L10n AT Payroll TASY-Tarif Manager,model_l10n_at_payroll_tasy_tarif,hr_payroll.group_hr_payroll_manager,1,1,1,1
access_l10n_at_payroll_tasy_zuord_user,L10n AT Payroll TASY-Zuordnung User,model_l10n_at_payroll_tasy_tarifset_zuord,hr_payroll.group_hr_payroll_user,1,0,0,0
access_l10n_at_payroll_tasy_zuord_manager,L10n AT Payroll TASY-Zuordnung Manager,model_l10n_at_payroll_tasy_tarifset_zuord,hr_payroll.group_hr_payroll_manager,1,1,1,1
access_l10n_at_payroll_tasy_zuabschlag_user,L10n AT Payroll TASY-Zuabschlag User,model_l10n_at_payroll_tasy_zuabschlag,hr_payroll.group_hr_payroll_user,1,0,0,0
access_l10n_at_payroll_tasy_zuabschlag_manager,L10n AT Payroll TASY-Zuabschlag Manager,model_l10n_at_payroll_tasy_zuabschlag,hr_payroll.group_hr_payroll_manager,1,1,1,1
access_l10n_at_payroll_tasy_zuabschlag_steuerung_user,L10n AT Payroll TASY-Zuabschlag-Steuerung User,model_l10n_at_payroll_tasy_zuabschlag_steuerung,hr_payroll.group_hr_payroll_user,1,0,0,0
access_l10n_at_payroll_tasy_zuabschlag_steuerung_manager,L10n AT Payroll TASY-Zuabschlag-Steuerung Manager,model_l10n_at_payroll_tasy_zuabschlag_steuerung,hr_payroll.group_hr_payroll_manager,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
13 access_l10n_at_payroll_jahresretro_wizard L10n AT Payroll Jahresretro Wizard model_l10n_at_payroll_jahresretro_wizard hr_payroll.group_hr_payroll_user 1 1 1 1
14 access_l10n_at_payroll_jahresretro_ergebnis L10n AT Payroll Jahresretro Ergebnis model_l10n_at_payroll_jahresretro_ergebnis hr_payroll.group_hr_payroll_user 1 1 1 1
15 access_l10n_at_payroll_mitteilungsfile_wizard L10n AT Payroll Mitteilungsfile Wizard model_l10n_at_payroll_mitteilungsfile_wizard hr_payroll.group_hr_payroll_user 1 1 1 1
16 access_l10n_at_payroll_jahresbericht_wizard L10n AT Payroll Jahresbericht Wizard model_l10n_at_payroll_jahresbericht_wizard hr_payroll.group_hr_payroll_user 1 1 1 1
17 access_l10n_at_payroll_tasy_katalog_wizard L10n AT Payroll TASY-Katalog Wizard model_l10n_at_payroll_tasy_katalog_wizard hr_payroll.group_hr_payroll_user 1 1 1 1
18 access_l10n_at_payroll_tasy_bsgr_user L10n AT Payroll TASY-BSGR User model_l10n_at_payroll_tasy_bsgr hr_payroll.group_hr_payroll_user 1 0 0 0
19 access_l10n_at_payroll_tasy_bsgr_manager L10n AT Payroll TASY-BSGR Manager model_l10n_at_payroll_tasy_bsgr hr_payroll.group_hr_payroll_manager 1 1 1 1
20 access_l10n_at_payroll_tasy_ergaenzung_user L10n AT Payroll TASY-Ergänzung User model_l10n_at_payroll_tasy_ergaenzung hr_payroll.group_hr_payroll_user 1 0 0 0
21 access_l10n_at_payroll_tasy_ergaenzung_manager L10n AT Payroll TASY-Ergänzung Manager model_l10n_at_payroll_tasy_ergaenzung hr_payroll.group_hr_payroll_manager 1 1 1 1
22 access_l10n_at_payroll_tasy_dgg_user L10n AT Payroll TASY-Dienstgebergruppe User model_l10n_at_payroll_tasy_dienstgebergruppe hr_payroll.group_hr_payroll_user 1 0 0 0
23 access_l10n_at_payroll_tasy_dgg_manager L10n AT Payroll TASY-Dienstgebergruppe Manager model_l10n_at_payroll_tasy_dienstgebergruppe hr_payroll.group_hr_payroll_manager 1 1 1 1
24 access_l10n_at_payroll_tasy_tarifgruppe_user L10n AT Payroll TASY-Tarifgruppe User model_l10n_at_payroll_tasy_tarifgruppe hr_payroll.group_hr_payroll_user 1 0 0 0
25 access_l10n_at_payroll_tasy_tarifgruppe_manager L10n AT Payroll TASY-Tarifgruppe Manager model_l10n_at_payroll_tasy_tarifgruppe hr_payroll.group_hr_payroll_manager 1 1 1 1
26 access_l10n_at_payroll_tasy_tarifset_user L10n AT Payroll TASY-Tarifset User model_l10n_at_payroll_tasy_tarifset hr_payroll.group_hr_payroll_user 1 0 0 0
27 access_l10n_at_payroll_tasy_tarifset_manager L10n AT Payroll TASY-Tarifset Manager model_l10n_at_payroll_tasy_tarifset hr_payroll.group_hr_payroll_manager 1 1 1 1
28 access_l10n_at_payroll_tasy_tarif_user L10n AT Payroll TASY-Tarif User model_l10n_at_payroll_tasy_tarif hr_payroll.group_hr_payroll_user 1 0 0 0
29 access_l10n_at_payroll_tasy_tarif_manager L10n AT Payroll TASY-Tarif Manager model_l10n_at_payroll_tasy_tarif hr_payroll.group_hr_payroll_manager 1 1 1 1
30 access_l10n_at_payroll_tasy_zuord_user L10n AT Payroll TASY-Zuordnung User model_l10n_at_payroll_tasy_tarifset_zuord hr_payroll.group_hr_payroll_user 1 0 0 0
31 access_l10n_at_payroll_tasy_zuord_manager L10n AT Payroll TASY-Zuordnung Manager model_l10n_at_payroll_tasy_tarifset_zuord hr_payroll.group_hr_payroll_manager 1 1 1 1
32 access_l10n_at_payroll_tasy_zuabschlag_user L10n AT Payroll TASY-Zuabschlag User model_l10n_at_payroll_tasy_zuabschlag hr_payroll.group_hr_payroll_user 1 0 0 0
33 access_l10n_at_payroll_tasy_zuabschlag_manager L10n AT Payroll TASY-Zuabschlag Manager model_l10n_at_payroll_tasy_zuabschlag hr_payroll.group_hr_payroll_manager 1 1 1 1
34 access_l10n_at_payroll_tasy_zuabschlag_steuerung_user L10n AT Payroll TASY-Zuabschlag-Steuerung User model_l10n_at_payroll_tasy_zuabschlag_steuerung hr_payroll.group_hr_payroll_user 1 0 0 0
35 access_l10n_at_payroll_tasy_zuabschlag_steuerung_manager L10n AT Payroll TASY-Zuabschlag-Steuerung Manager model_l10n_at_payroll_tasy_zuabschlag_steuerung hr_payroll.group_hr_payroll_manager 1 1 1 1
@@ -1,5 +1,6 @@
# Part of the odoo-at-payroll project. License: LGPL-3. # Part of the odoo-at-payroll project. License: LGPL-3.
from . import test_tasy_import from . import test_tasy_import
from . import test_tasy_katalog
from . import test_lohnsteuer from . import test_lohnsteuer
from . import test_sozialversicherung from . import test_sozialversicherung
from . import test_elda_l16 from . import test_elda_l16
@@ -0,0 +1,422 @@
# Part of the odoo-at-payroll project. License: LGPL-3.
"""AP0 (Variante b): TASY-Katalog-Import und Tarifgruppen-Auflösung.
Fixture-Slice mit den normativen Normalbetriebe-Werten 2026 (AB 39,05 %,
SZ 37,55 % — verifiziert gegen den echten TASY-LSWH-Export 2026-07-01)
plus E01-Ergänzung (NB-Zweig 3,80 % DG, Dienstgebergruppe NBNSA).
"""
import base64
from datetime import date
from odoo.tests import TransactionCase, tagged
TASY_KATALOG_FIXTURE = """<?xml version="1.0" encoding="UTF-8"?>
<tarifssystemlsh>
<gueltigkeitstichtag>2026-07-01</gueltigkeitstichtag>
<beschaeftigtengruppen>
<beschaeftigtengruppe xmlId="G001">
<code>B001</code>
<bezeichnung>Arbeiter</bezeichnung>
<kurzbezeichnung>Arb.</kurzbezeichnung>
<traegergruppe>OEGK</traegergruppe>
<vonDatum>2019-01-01</vonDatum>
<bisDatum>2999-12-31</bisDatum>
</beschaeftigtengruppe>
<beschaeftigtengruppe xmlId="G002">
<code>B002</code>
<bezeichnung>Angestellte</bezeichnung>
<kurzbezeichnung>Ang.</kurzbezeichnung>
<traegergruppe>OEGK</traegergruppe>
<vonDatum>2019-01-01</vonDatum>
<bisDatum>2999-12-31</bisDatum>
</beschaeftigtengruppe>
<beschaeftigtengruppe xmlId="G044">
<code>B044</code>
<bezeichnung>Angestelltenlehrlinge</bezeichnung>
<kurzbezeichnung>Ang. Lg.</kurzbezeichnung>
<traegergruppe>OEGK</traegergruppe>
<vonDatum>2019-01-01</vonDatum>
<bisDatum>2999-12-31</bisDatum>
</beschaeftigtengruppe>
<beschaeftigtengruppe xmlId="G045">
<code>B045</code>
<bezeichnung>Arbeiterlehrlinge</bezeichnung>
<kurzbezeichnung>Arb. Lg.</kurzbezeichnung>
<traegergruppe>OEGK</traegergruppe>
<vonDatum>2019-01-01</vonDatum>
<bisDatum>2999-12-31</bisDatum>
</beschaeftigtengruppe>
<beschaeftigtengruppe xmlId="G010">
<code>B010</code>
<bezeichnung>Geringfügig beschäftigte Arbeiter</bezeichnung>
<kurzbezeichnung>Gfg. Arb.</kurzbezeichnung>
<traegergruppe>OEGK</traegergruppe>
<vonDatum>2019-01-01</vonDatum>
<bisDatum>2999-12-31</bisDatum>
</beschaeftigtengruppe>
</beschaeftigtengruppen>
<ergaenzungen>
<ergaenzung xmlId="E01X">
<code>E01</code>
<bezeichnung>Nachtschwerarbeitsbeitrag</bezeichnung>
<kurzbezeichnung>NSchAB</kurzbezeichnung>
<traegergruppe>OEGK</traegergruppe>
<vonDatum>2019-01-01</vonDatum>
<bisDatum>2999-12-31</bisDatum>
</ergaenzung>
</ergaenzungen>
<dienstgebergruppen>
<dienstgebergruppe xmlId="NBAL">
<code>NBALG</code>
<bezeichnung>Normalbetriebe - Allgemeiner Teil</bezeichnung>
<kurzbezeichnung>NBALG</kurzbezeichnung>
<traegergruppe>OEGK</traegergruppe>
<vonDatum>2019-01-01</vonDatum>
<bisDatum>2999-12-31</bisDatum>
</dienstgebergruppe>
<dienstgebergruppe xmlId="NBNS">
<code>NBNSA</code>
<bezeichnung>Normalbetriebe mit Nachtschwerarbeit</bezeichnung>
<kurzbezeichnung>NBNSA</kurzbezeichnung>
<traegergruppe>OEGK</traegergruppe>
<vonDatum>2019-01-01</vonDatum>
<bisDatum>2999-12-31</bisDatum>
</dienstgebergruppe>
</dienstgebergruppen>
<tarifgruppen>
<tarifgruppe xmlId="T001">
<beschaeftigtengruppen>G001</beschaeftigtengruppen>
<dienstgeberGruppen>NBAL</dienstgeberGruppen>
<code>B001</code>
<bezeichnung>Arbeiter</bezeichnung>
<traegergruppe>OEGK</traegergruppe>
<vonDatum>2019-01-01</vonDatum>
<bisDatum>2999-12-31</bisDatum>
<fallweiseBeschaeftigungZulaessig>true</fallweiseBeschaeftigungZulaessig>
<betrieblicheVorsorgeZulaessig>true</betrieblicheVorsorgeZulaessig>
</tarifgruppe>
<tarifgruppe xmlId="T002">
<beschaeftigtengruppen>G002</beschaeftigtengruppen>
<dienstgeberGruppen>NBAL</dienstgeberGruppen>
<code>B002</code>
<bezeichnung>Angestellte</bezeichnung>
<traegergruppe>OEGK</traegergruppe>
<vonDatum>2019-01-01</vonDatum>
<bisDatum>2999-12-31</bisDatum>
<fallweiseBeschaeftigungZulaessig>false</fallweiseBeschaeftigungZulaessig>
<betrieblicheVorsorgeZulaessig>true</betrieblicheVorsorgeZulaessig>
</tarifgruppe>
<tarifgruppe xmlId="T002E01">
<beschaeftigtengruppen>G002</beschaeftigtengruppen>
<ergaenzungen>E01X</ergaenzungen>
<dienstgeberGruppen>NBNS</dienstgeberGruppen>
<code>B002E01</code>
<bezeichnung>Angestellte/Nachtschwerarbeitsbeitrag</bezeichnung>
<traegergruppe>OEGK</traegergruppe>
<vonDatum>2019-01-01</vonDatum>
<bisDatum>2999-12-31</bisDatum>
<betrieblicheVorsorgeZulaessig>true</betrieblicheVorsorgeZulaessig>
</tarifgruppe>
<tarifgruppe xmlId="T045">
<beschaeftigtengruppen>G045</beschaeftigtengruppen>
<dienstgeberGruppen>NBAL</dienstgeberGruppen>
<code>B045</code>
<bezeichnung>Arbeiterlehrlinge</bezeichnung>
<traegergruppe>OEGK</traegergruppe>
<vonDatum>2019-01-01</vonDatum>
<bisDatum>2999-12-31</bisDatum>
</tarifgruppe>
</tarifgruppen>
<tarife>
<tarif xmlId="R001">
<tarifZweig>KV</tarifZweig>
<dienstnehmerAnteil>3.87</dienstnehmerAnteil>
<dienstgeberAnteil>3.78</dienstgeberAnteil>
<traegergruppe>OEGK</traegergruppe>
<vonDatum>2019-01-01</vonDatum>
<bisDatum>2999-12-31</bisDatum>
</tarif>
<tarif xmlId="R002">
<tarifZweig>PV</tarifZweig>
<dienstnehmerAnteil>10.25</dienstnehmerAnteil>
<dienstgeberAnteil>12.55</dienstgeberAnteil>
<traegergruppe>OEGK</traegergruppe>
<vonDatum>2019-01-01</vonDatum>
<bisDatum>2999-12-31</bisDatum>
</tarif>
<tarif xmlId="R003">
<tarifZweig>AV</tarifZweig>
<dienstnehmerAnteil>2.95</dienstnehmerAnteil>
<dienstgeberAnteil>2.95</dienstgeberAnteil>
<traegergruppe>OEGK</traegergruppe>
<vonDatum>2019-01-01</vonDatum>
<bisDatum>2999-12-31</bisDatum>
</tarif>
<tarif xmlId="R004">
<tarifZweig>UV</tarifZweig>
<dienstnehmerAnteil>0</dienstnehmerAnteil>
<dienstgeberAnteil>1.1</dienstgeberAnteil>
<traegergruppe>OEGK</traegergruppe>
<vonDatum>2019-01-01</vonDatum>
<bisDatum>2999-12-31</bisDatum>
</tarif>
<tarif xmlId="R005">
<tarifZweig>WF</tarifZweig>
<dienstnehmerAnteil>0.5</dienstnehmerAnteil>
<dienstgeberAnteil>0.5</dienstgeberAnteil>
<traegergruppe>OEGK</traegergruppe>
<vonDatum>2019-01-01</vonDatum>
<bisDatum>2999-12-31</bisDatum>
</tarif>
<tarif xmlId="R006">
<tarifZweig>AK</tarifZweig>
<dienstnehmerAnteil>0.5</dienstnehmerAnteil>
<dienstgeberAnteil>0</dienstgeberAnteil>
<traegergruppe>OEGK</traegergruppe>
<vonDatum>2019-01-01</vonDatum>
<bisDatum>2999-12-31</bisDatum>
</tarif>
<tarif xmlId="R007">
<tarifZweig>IE</tarifZweig>
<dienstnehmerAnteil>0</dienstnehmerAnteil>
<dienstgeberAnteil>0.1</dienstgeberAnteil>
<traegergruppe>OEGK</traegergruppe>
<vonDatum>2019-01-01</vonDatum>
<bisDatum>2999-12-31</bisDatum>
</tarif>
<tarif xmlId="R008">
<tarifZweig>NB</tarifZweig>
<dienstnehmerAnteil>0</dienstnehmerAnteil>
<dienstgeberAnteil>3.8</dienstgeberAnteil>
<traegergruppe>OEGK</traegergruppe>
<vonDatum>2020-01-01</vonDatum>
<bisDatum>2999-12-31</bisDatum>
</tarif>
</tarife>
<tarifsets>
<tarifset xmlId="S001">
<bezeichnung>40/AK/AV/IE/KV/PV/UV/WF</bezeichnung>
<kurzbezeichnung>40/AK/AV/IE/KV/PV/UV/WF</kurzbezeichnung>
<traegergruppe>OEGK</traegergruppe>
<vonDatum>2019-01-01</vonDatum>
<bisDatum>2999-12-31</bisDatum>
<tarife>R001</tarife>
<tarife>R002</tarife>
<tarife>R003</tarife>
<tarife>R004</tarife>
<tarife>R005</tarife>
<tarife>R006</tarife>
<tarife>R007</tarife>
</tarifset>
<tarifset xmlId="S002">
<bezeichnung>47/AV/IE/KV/PV/UV</bezeichnung>
<kurzbezeichnung>47/AV/IE/KV/PV/UV</kurzbezeichnung>
<traegergruppe>OEGK</traegergruppe>
<vonDatum>2019-01-01</vonDatum>
<bisDatum>2999-12-31</bisDatum>
<tarife>R001</tarife>
<tarife>R002</tarife>
<tarife>R003</tarife>
<tarife>R004</tarife>
<tarife>R007</tarife>
</tarifset>
<tarifset xmlId="S003">
<bezeichnung>40/AK/AV/IE/KV/PV/UV/WF/NB</bezeichnung>
<kurzbezeichnung>40/AK/AV/IE/KV/PV/UV/WF/NB</kurzbezeichnung>
<traegergruppe>OEGK</traegergruppe>
<vonDatum>2019-01-01</vonDatum>
<bisDatum>2999-12-31</bisDatum>
<tarife>R001</tarife>
<tarife>R002</tarife>
<tarife>R003</tarife>
<tarife>R004</tarife>
<tarife>R005</tarife>
<tarife>R006</tarife>
<tarife>R007</tarife>
<tarife>R008</tarife>
</tarifset>
</tarifsets>
<verrechBasisTypen>
<verrechBasisTyp xmlId="VBTAB"><code>AB</code><bezeichnung>Arbeitsbezug</bezeichnung><traegergruppe>OEGK</traegergruppe><vonDatum>2019-01-01</vonDatum><bisDatum>2999-12-31</bisDatum></verrechBasisTyp>
<verrechBasisTyp xmlId="VBTSZ"><code>SZ</code><bezeichnung>Sonderzahlung</bezeichnung><traegergruppe>OEGK</traegergruppe><vonDatum>2019-01-01</vonDatum><bisDatum>2999-12-31</bisDatum></verrechBasisTyp>
</verrechBasisTypen>
<verrechPosTypen>
<verrechPosTyp xmlId="VPT01"><code>T01</code><bezeichnung>Gesamtsatz laufend</bezeichnung><traegergruppe>OEGK</traegergruppe><vonDatum>2019-01-01</vonDatum><bisDatum>2999-12-31</bisDatum></verrechPosTyp>
<verrechPosTyp xmlId="VPT02"><code>T02</code><bezeichnung>Gesamtsatz Sonderzahlung</bezeichnung><traegergruppe>OEGK</traegergruppe><vonDatum>2019-01-01</vonDatum><bisDatum>2999-12-31</bisDatum></verrechPosTyp>
</verrechPosTypen>
<verrechBasisPositionKombinationen>
<verrechBasisPositionKombination xmlId="K01"><verrechnungBasisTyp>VBTAB</verrechnungBasisTyp><verrechnungPosTyp>VPT01</verrechnungPosTyp><traegergruppe>OEGK</traegergruppe><vonDatum>2019-01-01</vonDatum><bisDatum>2999-12-31</bisDatum></verrechBasisPositionKombination>
<verrechBasisPositionKombination xmlId="K02"><verrechnungBasisTyp>VBTSZ</verrechnungBasisTyp><verrechnungPosTyp>VPT02</verrechnungPosTyp><traegergruppe>OEGK</traegergruppe><vonDatum>2019-01-01</vonDatum><bisDatum>2999-12-31</bisDatum></verrechBasisPositionKombination>
</verrechBasisPositionKombinationen>
<tarifgruppeSetZuordnungen>
<tarifgruppeSetZuord xmlId="Z001"><tarifgruppe>T001</tarifgruppe><tarifset>S001</tarifset><verrechBasPosKombination>K01</verrechBasPosKombination><traegergruppe>OEGK</traegergruppe><vonDatum>2019-01-01</vonDatum><bisDatum>2999-12-31</bisDatum></tarifgruppeSetZuord>
<tarifgruppeSetZuord xmlId="Z002"><tarifgruppe>T001</tarifgruppe><tarifset>S002</tarifset><verrechBasPosKombination>K02</verrechBasPosKombination><traegergruppe>OEGK</traegergruppe><vonDatum>2019-01-01</vonDatum><bisDatum>2999-12-31</bisDatum></tarifgruppeSetZuord>
<tarifgruppeSetZuord xmlId="Z003"><tarifgruppe>T002</tarifgruppe><tarifset>S001</tarifset><verrechBasPosKombination>K01</verrechBasPosKombination><traegergruppe>OEGK</traegergruppe><vonDatum>2019-01-01</vonDatum><bisDatum>2999-12-31</bisDatum></tarifgruppeSetZuord>
<tarifgruppeSetZuord xmlId="Z004"><tarifgruppe>T002</tarifgruppe><tarifset>S002</tarifset><verrechBasPosKombination>K02</verrechBasPosKombination><traegergruppe>OEGK</traegergruppe><vonDatum>2019-01-01</vonDatum><bisDatum>2999-12-31</bisDatum></tarifgruppeSetZuord>
<tarifgruppeSetZuord xmlId="Z005"><tarifgruppe>T002E01</tarifgruppe><tarifset>S003</tarifset><verrechBasPosKombination>K01</verrechBasPosKombination><traegergruppe>OEGK</traegergruppe><vonDatum>2019-01-01</vonDatum><bisDatum>2999-12-31</bisDatum></tarifgruppeSetZuord>
<tarifgruppeSetZuord xmlId="Z006"><tarifgruppe>T002E01</tarifgruppe><tarifset>S002</tarifset><verrechBasPosKombination>K02</verrechBasPosKombination><traegergruppe>OEGK</traegergruppe><vonDatum>2019-01-01</vonDatum><bisDatum>2999-12-31</bisDatum></tarifgruppeSetZuord>
<tarifgruppeSetZuord xmlId="Z007"><tarifgruppe>T045</tarifgruppe><tarifset>S001</tarifset><verrechBasPosKombination>K01</verrechBasPosKombination><traegergruppe>OEGK</traegergruppe><vonDatum>2019-01-01</vonDatum><bisDatum>2999-12-31</bisDatum></tarifgruppeSetZuord>
<tarifgruppeSetZuord xmlId="Z008"><tarifgruppe>T045</tarifgruppe><tarifset>S002</tarifset><verrechBasPosKombination>K02</verrechBasPosKombination><traegergruppe>OEGK</traegergruppe><vonDatum>2019-01-01</vonDatum><bisDatum>2999-12-31</bisDatum></tarifgruppeSetZuord>
</tarifgruppeSetZuordnungen>
<zuAbschlaege>
<zuAbschlag xmlId="A09X">
<code>A09</code>
<bezeichnung>Entfall UV 60. Lj</bezeichnung>
<kurzbezeichnung>UV 60</kurzbezeichnung>
<zuabschlagkz>A</zuabschlagkz>
<versicherungsRelevant>false</versicherungsRelevant>
<traegergruppe>OEGK</traegergruppe>
<vonDatum>2019-01-01</vonDatum>
<bisDatum>2999-12-31</bisDatum>
</zuAbschlag>
</zuAbschlaege>
<zuAbschlaegeSteuerung>
<zuAbschlagSteuerung xmlId="A09S1">
<zuAbschlag>A09X</zuAbschlag>
<altersgrenzeMinJahr>60</altersgrenzeMinJahr>
<traegergruppe>OEGK</traegergruppe>
<vonDatum>2019-01-01</vonDatum>
<bisDatum>2999-12-31</bisDatum>
</zuAbschlagSteuerung>
</zuAbschlaegeSteuerung>
<zuAbschlagTarifsetZuordnungen>
<zuAbschlagTarifsetZuord xmlId="AZ01">
<zuAbschlag>A09X</zuAbschlag>
<tarifsetByTarifsetId>S001</tarifsetByTarifsetId>
<tarifgruppe>T001</tarifgruppe>
<tarifgruppe>T002</tarifgruppe>
<traegergruppe>OEGK</traegergruppe>
<vonDatum>2019-01-01</vonDatum>
<bisDatum>2999-12-31</bisDatum>
</zuAbschlagTarifsetZuord>
</zuAbschlagTarifsetZuordnungen>
</tarifssystemlsh>
"""
@tagged('post_install', '-at_install')
class TasyKatalogTest(TransactionCase):
"""Katalog-Import (Fixture + Realexport) und Tarifgruppen-Auflösung."""
def _import(self, raw=None):
import os
if raw is None:
path = os.path.join(os.path.dirname(__file__), *[
'..', '..', '..', 'personalverrechnung', 'quellen',
'TASY-LSWH-Export-20260324.xml'])
if not os.path.exists(path):
self.skipTest('TASY-Quelldatei nicht im Workingtree')
with open(path, 'rb') as fh:
raw = fh.read()
wizard = self.env['l10n.at.payroll.tasy.katalog.wizard'].create({
'file': base64.b64encode(raw).decode(),
'filename': 'fixture.xml',
})
wizard.action_import()
return wizard
def test_01_import_fixture_und_aufloesung(self):
"""Import erzeugt den Katalog; B001/B002 lösen zu den
normativen Normalbetriebe-Sätzen auf (AB 39,05, SZ 37,55)."""
self._import(TASY_KATALOG_FIXTURE.encode())
self.assertEqual(
self.env['l10n.at.payroll.tasy.bsgr'].search_count([]), 5)
self.assertEqual(
self.env['l10n.at.payroll.tasy.ergaenzung'].search_count([]),
1)
self.assertEqual(
self.env['l10n.at.payroll.tasy.dienstgebergruppe']
.search_count([]), 2)
self.assertEqual(
self.env['l10n.at.payroll.tasy.tarifgruppe'].search_count([]),
4)
self.assertEqual(
self.env['l10n.at.payroll.tasy.tarifset'].search_count([]), 3)
self.assertEqual(
self.env['l10n.at.payroll.tasy.tarifset.zuord']
.search_count([]), 8)
stichtag = date(2026, 7, 1)
for code in ('B001', 'B002'):
tarifgruppe = self.env[
'l10n.at.payroll.tasy.tarifgruppe']._l10n_at_find(
code, [], 'NBALG', stichtag)
self.assertTrue(tarifgruppe, code)
ab = tarifgruppe._l10n_at_saetze('AB', stichtag)
sz = tarifgruppe._l10n_at_saetze('SZ', stichtag)
self.assertAlmostEqual(ab['gesamt'], 39.05, places=2)
self.assertAlmostEqual(ab['dn'], 18.07, places=2)
self.assertAlmostEqual(ab['dg'], 20.98, places=2)
self.assertAlmostEqual(sz['gesamt'], 37.55, places=2)
def test_02_import_idempotent(self):
"""Erneuter Import derselben Datei dupliziert nichts."""
self._import(TASY_KATALOG_FIXTURE.encode())
self._import(TASY_KATALOG_FIXTURE.encode())
self.assertEqual(
self.env['l10n.at.payroll.tasy.tarifgruppe'].search_count([]),
4)
self.assertEqual(
self.env['l10n.at.payroll.tasy.tarif'].search_count([]), 8)
self.assertEqual(
self.env['l10n.at.payroll.tasy.tarifset.zuord']
.search_count([]), 8)
def test_03_ergaenzung_e01(self):
"""B002 + E01 + NBNSA → T01 inkl. NB-Zweig 3,80 % DG
(42,85 %); ohne Ergänzung unter NBNSA keine Tarifgruppe."""
self._import(TASY_KATALOG_FIXTURE.encode())
stichtag = date(2026, 7, 1)
find = self.env['l10n.at.payroll.tasy.tarifgruppe']._l10n_at_find
# B002 + E01 unter NBALG existiert nicht (E01 gehört zu NBNSA)
self.assertFalse(find('B002', ['E01'], 'NBALG', stichtag))
tarifgruppe = find('B002', ['E01'], 'NBNSA', stichtag)
self.assertTrue(tarifgruppe)
ab = tarifgruppe._l10n_at_saetze('AB', stichtag)
self.assertAlmostEqual(ab['gesamt'], 42.85, places=2)
zweige = {t.zweig for t in ab['komponenten']}
self.assertIn('NB', zweige)
# SZ ohne NB (Sonderzahlungen tragen keinen NSchAB)
sz = tarifgruppe._l10n_at_saetze('SZ', stichtag)
self.assertAlmostEqual(sz['gesamt'], 37.55, places=2)
def test_04_lehrlings_bsg(self):
"""B045 (Arbeiterlehrlinge) ist auflösbar — die
Lehrlings-BSGR-Korrektur beruht auf diesen Bezeichnern."""
self._import(TASY_KATALOG_FIXTURE.encode())
stichtag = date(2026, 7, 1)
b044 = self.env['l10n.at.payroll.tasy.bsgr'].search([
('code', '=', 'B044')])
b045 = self.env['l10n.at.payroll.tasy.bsgr'].search([
('code', '=', 'B045')])
self.assertEqual(b044.bezeichnung, 'Angestelltenlehrlinge')
self.assertEqual(b045.bezeichnung, 'Arbeiterlehrlinge')
tarifgruppe = self.env[
'l10n.at.payroll.tasy.tarifgruppe']._l10n_at_find(
'B045', [], 'NBALG', stichtag)
self.assertTrue(tarifgruppe)
def test_05_real_export(self):
"""Echter TASY-LSWH-Export: Katalog-Volumen und die
normativen 2026er Sätze aus der Katalogauflösung."""
self._import()
stichtag = date(2026, 7, 1)
find = self.env['l10n.at.payroll.tasy.tarifgruppe']._l10n_at_find
for code in ('B001', 'B002'):
tarifgruppe = find(code, [], 'NBALG', stichtag)
self.assertTrue(tarifgruppe, code)
ab = tarifgruppe._l10n_at_saetze('AB', stichtag)
sz = tarifgruppe._l10n_at_saetze('SZ', stichtag)
self.assertAlmostEqual(ab['gesamt'], 39.05, places=2)
self.assertAlmostEqual(sz['gesamt'], 37.55, places=2)
# Lehrlings-BSGR-Bezeichner (Korrektur-Grundlage)
self.assertEqual(self.env['l10n.at.payroll.tasy.bsgr'].search([
('code', '=', 'B044')]).bezeichnung, 'Angestelltenlehrlinge')
self.assertEqual(self.env['l10n.at.payroll.tasy.bsgr'].search([
('code', '=', 'B045')]).bezeichnung, 'Arbeiterlehrlinge')
# Geringfügige je Personalgruppe
self.assertEqual(self.env['l10n.at.payroll.tasy.bsgr'].search([
('code', '=', 'B010')]).bezeichnung,
'Geringfügig beschäftigte Arbeiter')
# Ergänzungen sind im Katalog
self.assertTrue(self.env['l10n.at.payroll.tasy.ergaenzung'].search([
('code', '=', 'E01')]))
@@ -28,6 +28,7 @@
<field name="l10n_at_bundesland"/> <field name="l10n_at_bundesland"/>
<field name="l10n_at_wk_mitglied"/> <field name="l10n_at_wk_mitglied"/>
<field name="l10n_at_wf_wien"/> <field name="l10n_at_wf_wien"/>
<field name="l10n_at_wien_dag"/>
</group> </group>
</group> </group>
</page> </page>
@@ -124,4 +124,26 @@
</notebook> </notebook>
</field> </field>
</record> </record>
<!-- AGRD-Mapping an der Abgangsursache (AP1) -->
<record id="hr_departure_reason_view_form_agrd" model="ir.ui.view">
<field name="name">hr.departure.reason.form.l10n_at_agrd</field>
<field name="model">hr.departure.reason</field>
<field name="inherit_id" ref="hr.hr_departure_reason_view_form"/>
<field name="arch" type="xml">
<field name="name" position="after">
<field name="l10n_at_agrd"/>
</field>
</field>
</record>
<record id="hr_departure_reason_view_list_agrd" model="ir.ui.view">
<field name="name">hr.departure.reason.list.l10n_at_agrd</field>
<field name="model">hr.departure.reason</field>
<field name="inherit_id" ref="hr.hr_departure_reason_view_list"/>
<field name="arch" type="xml">
<field name="name" position="after">
<field name="l10n_at_agrd" optional="show"/>
</field>
</field>
</record>
</odoo> </odoo>
@@ -0,0 +1,195 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- ============================================================ -->
<!-- AP0 (Variante b): SV-Tarifsystem-Katalog — BSGR/ERGB/DGG/ -->
<!-- Tarifgruppen/Tarifsets/Zu-/Abschläge aus dem TASY-Import. -->
<!-- ============================================================ -->
<record id="tasy_tarifgruppe_list_view" model="ir.ui.view">
<field name="name">l10n.at.payroll.tasy.tarifgruppe.list</field>
<field name="model">l10n.at.payroll.tasy.tarifgruppe</field>
<field name="arch" type="xml">
<list string="Tarifgruppen">
<field name="code"/>
<field name="bezeichnung"/>
<field name="bsgr_ids" widget="many2many_tags"/>
<field name="ergaenzung_ids" widget="many2many_tags"/>
<field name="dienstgebergruppe_ids" widget="many2many_tags"/>
<field name="von_datum" optional="show"/>
<field name="bis_datum" optional="show"/>
<field name="fallweise_zulaessig" optional="hide"/>
<field name="bv_zulaessig" optional="hide"/>
</list>
</field>
</record>
<record id="tasy_tarifgruppe_form_view" model="ir.ui.view">
<field name="name">l10n.at.payroll.tasy.tarifgruppe.form</field>
<field name="model">l10n.at.payroll.tasy.tarifgruppe</field>
<field name="arch" type="xml">
<form string="Tarifgruppe">
<sheet>
<group>
<group>
<field name="code"/>
<field name="bezeichnung"/>
<field name="traegergruppe"/>
<field name="von_datum"/>
<field name="bis_datum"/>
</group>
<group>
<field name="bsgr_ids" widget="many2many_tags"/>
<field name="ergaenzung_ids"
widget="many2many_tags"/>
<field name="dienstgebergruppe_ids"
widget="many2many_tags"/>
<field name="fallweise_zulaessig"/>
<field name="bv_zulaessig"/>
</group>
</group>
<notebook>
<page string="Tarifset-Zuordnungen"
name="zuordnungen">
<field name="zuord_ids" readonly="1">
<list>
<field name="basis_typ"/>
<field name="pos_typ"/>
<field name="tarifset_id"/>
<field name="von_datum"/>
<field name="bis_datum"/>
</list>
</field>
</page>
</notebook>
</sheet>
</form>
</field>
</record>
<record id="action_tasy_tarifgruppe" model="ir.actions.act_window">
<field name="name">Tarifgruppen (SV-Tarifsystem)</field>
<field name="res_model">l10n.at.payroll.tasy.tarifgruppe</field>
<field name="view_mode">list,form</field>
</record>
<record id="tasy_bsgr_list_view" model="ir.ui.view">
<field name="name">l10n.at.payroll.tasy.bsgr.list</field>
<field name="model">l10n.at.payroll.tasy.bsgr</field>
<field name="arch" type="xml">
<list string="Beschäftigtengruppen">
<field name="code"/>
<field name="kurzbezeichnung"/>
<field name="bezeichnung"/>
<field name="traegergruppe"/>
<field name="von_datum" optional="show"/>
<field name="bis_datum" optional="hide"/>
</list>
</field>
</record>
<record id="action_tasy_bsgr" model="ir.actions.act_window">
<field name="name">Beschäftigtengruppen (BSGR)</field>
<field name="res_model">l10n.at.payroll.tasy.bsgr</field>
<field name="view_mode">list</field>
</record>
<record id="tasy_ergaenzung_list_view" model="ir.ui.view">
<field name="name">l10n.at.payroll.tasy.ergaenzung.list</field>
<field name="model">l10n.at.payroll.tasy.ergaenzung</field>
<field name="arch" type="xml">
<list string="Ergänzungen">
<field name="code"/>
<field name="kurzbezeichnung"/>
<field name="bezeichnung"/>
<field name="traegergruppe"/>
<field name="von_datum" optional="show"/>
<field name="bis_datum" optional="hide"/>
</list>
</field>
</record>
<record id="action_tasy_ergaenzung" model="ir.actions.act_window">
<field name="name">Ergänzungen (ERGB)</field>
<field name="res_model">l10n.at.payroll.tasy.ergaenzung</field>
<field name="view_mode">list</field>
</record>
<record id="tasy_zuabschlag_list_view" model="ir.ui.view">
<field name="name">l10n.at.payroll.tasy.zuabschlag.list</field>
<field name="model">l10n.at.payroll.tasy.zuabschlag</field>
<field name="arch" type="xml">
<list string="Zu-/Abschläge">
<field name="code"/>
<field name="kurzbezeichnung"/>
<field name="bezeichnung"/>
<field name="zuabschlagkz" optional="show"/>
<field name="versicherungsrelevant" optional="show"/>
<field name="von_datum" optional="show"/>
<field name="bis_datum" optional="hide"/>
</list>
</field>
</record>
<record id="action_tasy_zuabschlag" model="ir.actions.act_window">
<field name="name">Zu-/Abschläge</field>
<field name="res_model">l10n.at.payroll.tasy.zuabschlag</field>
<field name="view_mode">list</field>
</record>
<!-- Katalog-Import-Wizard -->
<record id="tasy_katalog_wizard_form_view" model="ir.ui.view">
<field name="name">l10n.at.payroll.tasy.katalog.wizard.form</field>
<field name="model">l10n.at.payroll.tasy.katalog.wizard</field>
<field name="arch" type="xml">
<form string="SV-Tarifsystem-Katalog importieren">
<group>
<field name="file" filename="filename"/>
<field name="filename" invisible="1"/>
<field name="stichtag" readonly="1"/>
</group>
<group string="Protokoll">
<field name="protocol" readonly="1"/>
</group>
<footer>
<button string="Katalog importieren"
name="action_import" type="object"
class="btn-primary"/>
<button string="Abbrechen" class="btn-secondary"
special="cancel"/>
</footer>
</form>
</field>
</record>
<record id="action_tasy_katalog_wizard" model="ir.actions.act_window">
<field name="name">Tarifsystem-Katalog importieren (TASY)</field>
<field name="res_model">l10n.at.payroll.tasy.katalog.wizard</field>
<field name="view_mode">form</field>
<field name="target">new</field>
</record>
<menuitem id="menu_l10n_at_payroll_tasy_tarifgruppen"
name="Tarifsystem: Tarifgruppen"
parent="menu_l10n_at_payroll_root"
action="action_tasy_tarifgruppe" sequence="30"/>
<menuitem id="menu_l10n_at_payroll_tasy_bsgr"
name="Tarifsystem: Beschäftigtengruppen"
parent="menu_l10n_at_payroll_root"
action="action_tasy_bsgr" sequence="40"/>
<menuitem id="menu_l10n_at_payroll_tasy_ergaenzung"
name="Tarifsystem: Ergänzungen"
parent="menu_l10n_at_payroll_root"
action="action_tasy_ergaenzung" sequence="50"/>
<menuitem id="menu_l10n_at_payroll_tasy_zuabschlaege"
name="Tarifsystem: Zu-/Abschläge"
parent="menu_l10n_at_payroll_root"
action="action_tasy_zuabschlag" sequence="60"/>
<menuitem id="menu_l10n_at_payroll_tasy_katalog_import"
name="Tarifsystem-Katalog importieren"
parent="menu_l10n_at_payroll_root"
action="action_tasy_katalog_wizard" sequence="70"/>
</odoo>
@@ -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.8.1.0', 'version': '19.0.9.2.0',
'author': 'ixsol / gem360', 'author': 'ixsol / gem360',
'website': 'https://gem360.at', 'website': 'https://gem360.at',
'category': 'Human Resources/Payroll', 'category': 'Human Resources/Payroll',
@@ -91,7 +91,8 @@ Kein VRV-Bezug (Gemeinde-Schiene bleibt ``l10n_at_gemeinde_payroll`` +
gem360-Brücke). gem360-Brücke).
""", """,
'depends': ['l10n_at_hr_payroll', 'hr_payroll_account', 'depends': ['l10n_at_hr_payroll', 'hr_payroll_account',
'l10n_at', 'hr_payroll_account_iso20022'], 'l10n_at', 'hr_payroll_account_iso20022',
'hr_holidays', 'hr_work_entry_holidays'],
'data': [ 'data': [
'security/ir.model.access.csv', 'security/ir.model.access.csv',
'security/security_rules.xml', 'security/security_rules.xml',
@@ -101,13 +102,17 @@ gem360-Brücke).
'data/l10n.at.payroll.kv.wert.csv', 'data/l10n.at.payroll.kv.wert.csv',
'data/structure_private.xml', 'data/structure_private.xml',
'data/work_entry_types_private.xml', 'data/work_entry_types_private.xml',
'data/absence_private.xml',
'data/input_types_private.xml', 'data/input_types_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_sv_private.xml', 'data/salary_rules_sv_private.xml',
'data/salary_rules_bvg_private.xml', 'data/salary_rules_bvg_private.xml',
'data/hr_payroll_account_private.xml', 'data/hr_payroll_account_private.xml',
'data/rule_parameters_mbgm_private.xml', 'data/rule_parameters_mbgm_private.xml',
'data/rule_parameters_wien_private.xml',
'data/rule_parameters_efzg_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',
@@ -115,6 +120,7 @@ gem360-Brücke).
'views/vertragsfelder_views.xml', 'views/vertragsfelder_views.xml',
'views/mitarbeiter_views.xml', 'views/mitarbeiter_views.xml',
'report/gp8_reports.xml', 'report/gp8_reports.xml',
'report/buchungsbeleg_report.xml',
'report/bv_report.xml', 'report/bv_report.xml',
], ],
'demo': ['data/demo.xml'], 'demo': ['data/demo.xml'],
@@ -0,0 +1,95 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- ============================================================ -->
<!-- AP5: AT-Abwesenheiten — Work-Entry-Types und Urlaubsarten. -->
<!-- -->
<!-- hr_holidays (Time Off) ist Pflicht-Dependence des AT- -->
<!-- Privatprodukts; hr_work_entry_holidays erzeugt aus validierten -->
<!-- Abwesenheiten automatisch Work Entries über das work_entry_ -->
<!-- type_id der Urlaubsart (Odoo-Muster der Lokalisierungen). -->
<!-- Die AT-Arten sind country-gebunden (base.at) und ergänzen die -->
<!-- globalen Standardarten — bewusst additiv, nichts überschrieben. -->
<!-- ============================================================ -->
<!-- Work-Entry-Types -->
<record id="wett_atp_urlaub" model="hr.work.entry.type">
<field name="name">Urlaub (UrlG)</field>
<field name="color">3</field>
<field name="display_code">URL</field>
<field name="code">ATP_URLAUB</field>
<field name="country_id" ref="base.at"/>
<field name="is_leave" eval="True"/>
</record>
<record id="wett_atp_krank" model="hr.work.entry.type">
<field name="name">Krankenstand (EFZG)</field>
<field name="color">2</field>
<field name="display_code">KRK</field>
<field name="code">ATP_KRANK</field>
<field name="country_id" ref="base.at"/>
<field name="is_leave" eval="True"/>
</record>
<!-- AP4: Arbeitsunfall/Berufskrankheit — eigenes EFZ-Sonderkontingent
(§ 8 Abs 2a AngG / § 2 Abs 5 EFZG), getrennter Topf. -->
<record id="wett_atp_au" model="hr.work.entry.type">
<field name="name">Arbeitsunfall/Berufskrankheit (EFZG)</field>
<field name="color">9</field>
<field name="display_code">AU</field>
<field name="code">ATP_AU</field>
<field name="country_id" ref="base.at"/>
<field name="is_leave" eval="True"/>
</record>
<record id="wett_atp_karenz" model="hr.work.entry.type">
<field name="name">Unbezahlte Karenz (MSchG/VKG)</field>
<field name="color">6</field>
<field name="display_code">KAR</field>
<field name="code">ATP_KARENZ</field>
<field name="country_id" ref="base.at"/>
<field name="is_leave" eval="True"/>
</record>
<!-- Urlaubsarten (hr.leave.type) mit WET-Verknüpfung -->
<record id="leave_type_at_urlaub" model="hr.leave.type">
<field name="name">Urlaub (UrlG)</field>
<field name="requires_allocation" eval="True"/>
<field name="employee_requests" eval="True"/>
<field name="leave_validation_type">both</field>
<field name="allocation_validation_type">hr</field>
<field name="request_unit">day</field>
<field name="company_id" eval="False"/>
<field name="country_id" ref="base.at"/>
<field name="work_entry_type_id"
ref="l10n_at_hr_payroll_private.wett_atp_urlaub"/>
</record>
<record id="leave_type_at_krankenstand" model="hr.leave.type">
<field name="name">Krankenstand</field>
<field name="requires_allocation" eval="False"/>
<field name="leave_validation_type">hr</field>
<field name="request_unit">day</field>
<field name="support_document" eval="True"/>
<field name="company_id" eval="False"/>
<field name="country_id" ref="base.at"/>
<field name="work_entry_type_id"
ref="l10n_at_hr_payroll_private.wett_atp_krank"/>
</record>
<record id="leave_type_at_arbeitsunfall" model="hr.leave.type">
<field name="name">Arbeitsunfall/Berufskrankheit</field>
<field name="requires_allocation" eval="False"/>
<field name="leave_validation_type">hr</field>
<field name="request_unit">day</field>
<field name="support_document" eval="True"/>
<field name="company_id" eval="False"/>
<field name="country_id" ref="base.at"/>
<field name="work_entry_type_id"
ref="l10n_at_hr_payroll_private.wett_atp_au"/>
</record>
<record id="leave_type_at_karenz" model="hr.leave.type">
<field name="name">Unbezahlte Karenz</field>
<field name="requires_allocation" eval="False"/>
<field name="leave_validation_type">hr</field>
<field name="request_unit">day</field>
<field name="company_id" eval="False"/>
<field name="country_id" ref="base.at"/>
<field name="work_entry_type_id"
ref="l10n_at_hr_payroll_private.wett_atp_karenz"/>
</record>
</odoo>
@@ -1,132 +1,132 @@
id,gruppe_id:id,stufe,bezeichnung,gehalt,gueltig_ab id,gruppe_id:id,stufe,bezeichnung,gehalt,gueltig_ab,vwgj_ab
kv2203_st_ii_1,l10n_at_hr_payroll_private.kv2203_grp_ii,1,im 1. u 2. VWGJ,2059.41,2026-01-01 kv2203_st_ii_1,l10n_at_hr_payroll_private.kv2203_grp_ii,1,im 1. u 2. VWGJ,2059.41,2026-01-01,0
kv2203_st_ii_2,l10n_at_hr_payroll_private.kv2203_grp_ii,2,nach 2 VWGJ,2179.68,2026-01-01 kv2203_st_ii_2,l10n_at_hr_payroll_private.kv2203_grp_ii,2,nach 2 VWGJ,2179.68,2026-01-01,2
kv2203_st_ii_3,l10n_at_hr_payroll_private.kv2203_grp_ii,3,nach 4 VWGJ,2315.46,2026-01-01 kv2203_st_ii_3,l10n_at_hr_payroll_private.kv2203_grp_ii,3,nach 4 VWGJ,2315.46,2026-01-01,4
kv2203_st_ii_4,l10n_at_hr_payroll_private.kv2203_grp_ii,4,nach 6 VWGJ,2457.65,2026-01-01 kv2203_st_ii_4,l10n_at_hr_payroll_private.kv2203_grp_ii,4,nach 6 VWGJ,2457.65,2026-01-01,6
kv2203_st_ii_5,l10n_at_hr_payroll_private.kv2203_grp_ii,5,nach 8 VWGJ,2599.83,2026-01-01 kv2203_st_ii_5,l10n_at_hr_payroll_private.kv2203_grp_ii,5,nach 8 VWGJ,2599.83,2026-01-01,8
kv2203_st_ii_6,l10n_at_hr_payroll_private.kv2203_grp_ii,6,nach 10 VWGJ,2742.03,2026-01-01 kv2203_st_ii_6,l10n_at_hr_payroll_private.kv2203_grp_ii,6,nach 10 VWGJ,2742.03,2026-01-01,10
kv2203_st_ii_7,l10n_at_hr_payroll_private.kv2203_grp_ii,7,nach 12 VWGJ,2863.88,2026-01-01 kv2203_st_ii_7,l10n_at_hr_payroll_private.kv2203_grp_ii,7,nach 12 VWGJ,2863.88,2026-01-01,12
kv2203_st_ii_8,l10n_at_hr_payroll_private.kv2203_grp_ii,8,nach 15 VWGJ,3087.28,2026-01-01 kv2203_st_ii_8,l10n_at_hr_payroll_private.kv2203_grp_ii,8,nach 15 VWGJ,3087.28,2026-01-01,15
kv2203_st_iii_1,l10n_at_hr_payroll_private.kv2203_grp_iii,1,im 1. u 2. VWGJ,2528.04,2026-01-01 kv2203_st_iii_1,l10n_at_hr_payroll_private.kv2203_grp_iii,1,im 1. u 2. VWGJ,2528.04,2026-01-01,0
kv2203_st_iii_2,l10n_at_hr_payroll_private.kv2203_grp_iii,2,nach 2 VWGJ,2705.03,2026-01-01 kv2203_st_iii_2,l10n_at_hr_payroll_private.kv2203_grp_iii,2,nach 2 VWGJ,2705.03,2026-01-01,2
kv2203_st_iii_3,l10n_at_hr_payroll_private.kv2203_grp_iii,3,nach 4 VWGJ,2881.97,2026-01-01 kv2203_st_iii_3,l10n_at_hr_payroll_private.kv2203_grp_iii,3,nach 4 VWGJ,2881.97,2026-01-01,4
kv2203_st_iii_4,l10n_at_hr_payroll_private.kv2203_grp_iii,4,nach 6 VWGJ,3058.96,2026-01-01 kv2203_st_iii_4,l10n_at_hr_payroll_private.kv2203_grp_iii,4,nach 6 VWGJ,3058.96,2026-01-01,6
kv2203_st_iii_5,l10n_at_hr_payroll_private.kv2203_grp_iii,5,nach 8 VWGJ,3232.73,2026-01-01 kv2203_st_iii_5,l10n_at_hr_payroll_private.kv2203_grp_iii,5,nach 8 VWGJ,3232.73,2026-01-01,8
kv2203_st_iii_6,l10n_at_hr_payroll_private.kv2203_grp_iii,6,nach 10 VWGJ,3409.53,2026-01-01 kv2203_st_iii_6,l10n_at_hr_payroll_private.kv2203_grp_iii,6,nach 10 VWGJ,3409.53,2026-01-01,10
kv2203_st_iii_7,l10n_at_hr_payroll_private.kv2203_grp_iii,7,nach 12 VWGJ,3561.04,2026-01-01 kv2203_st_iii_7,l10n_at_hr_payroll_private.kv2203_grp_iii,7,nach 12 VWGJ,3561.04,2026-01-01,12
kv2203_st_iii_8,l10n_at_hr_payroll_private.kv2203_grp_iii,8,nach 15 VWGJ,3838.85,2026-01-01 kv2203_st_iii_8,l10n_at_hr_payroll_private.kv2203_grp_iii,8,nach 15 VWGJ,3838.85,2026-01-01,15
kv2203_st_iv_1,l10n_at_hr_payroll_private.kv2203_grp_iv,1,im 1. u 2. VWGJ,3146.27,2026-01-01 kv2203_st_iv_1,l10n_at_hr_payroll_private.kv2203_grp_iv,1,im 1. u 2. VWGJ,3146.27,2026-01-01,0
kv2203_st_iv_2,l10n_at_hr_payroll_private.kv2203_grp_iv,2,nach 2 VWGJ,3366.49,2026-01-01 kv2203_st_iv_2,l10n_at_hr_payroll_private.kv2203_grp_iv,2,nach 2 VWGJ,3366.49,2026-01-01,2
kv2203_st_iv_3,l10n_at_hr_payroll_private.kv2203_grp_iv,3,nach 4 VWGJ,3586.73,2026-01-01 kv2203_st_iv_3,l10n_at_hr_payroll_private.kv2203_grp_iv,3,nach 4 VWGJ,3586.73,2026-01-01,4
kv2203_st_iv_4,l10n_at_hr_payroll_private.kv2203_grp_iv,4,nach 6 VWGJ,3806.97,2026-01-01 kv2203_st_iv_4,l10n_at_hr_payroll_private.kv2203_grp_iv,4,nach 6 VWGJ,3806.97,2026-01-01,6
kv2203_st_iv_5,l10n_at_hr_payroll_private.kv2203_grp_iv,5,nach 8 VWGJ,4027.20,2026-01-01 kv2203_st_iv_5,l10n_at_hr_payroll_private.kv2203_grp_iv,5,nach 8 VWGJ,4027.20,2026-01-01,8
kv2203_st_iv_6,l10n_at_hr_payroll_private.kv2203_grp_iv,6,nach 10 VWGJ,4247.47,2026-01-01 kv2203_st_iv_6,l10n_at_hr_payroll_private.kv2203_grp_iv,6,nach 10 VWGJ,4247.47,2026-01-01,10
kv2203_st_iv_7,l10n_at_hr_payroll_private.kv2203_grp_iv,7,nach 12 VWGJ,4436.22,2026-01-01 kv2203_st_iv_7,l10n_at_hr_payroll_private.kv2203_grp_iv,7,nach 12 VWGJ,4436.22,2026-01-01,12
kv2203_st_iv_8,l10n_at_hr_payroll_private.kv2203_grp_iv,8,nach 15 VWGJ,4782.32,2026-01-01 kv2203_st_iv_8,l10n_at_hr_payroll_private.kv2203_grp_iv,8,nach 15 VWGJ,4782.32,2026-01-01,15
kv2203_st_v_1,l10n_at_hr_payroll_private.kv2203_grp_v,1,im 1. u 2. VWGJ,3919.67,2026-01-01 kv2203_st_v_1,l10n_at_hr_payroll_private.kv2203_grp_v,1,im 1. u 2. VWGJ,3919.67,2026-01-01,0
kv2203_st_v_2,l10n_at_hr_payroll_private.kv2203_grp_v,2,nach 2 VWGJ,4194.02,2026-01-01 kv2203_st_v_2,l10n_at_hr_payroll_private.kv2203_grp_v,2,nach 2 VWGJ,4194.02,2026-01-01,2
kv2203_st_v_3,l10n_at_hr_payroll_private.kv2203_grp_v,3,nach 4 VWGJ,4468.41,2026-01-01 kv2203_st_v_3,l10n_at_hr_payroll_private.kv2203_grp_v,3,nach 4 VWGJ,4468.41,2026-01-01,4
kv2203_st_v_4,l10n_at_hr_payroll_private.kv2203_grp_v,4,nach 6 VWGJ,4742.77,2026-01-01 kv2203_st_v_4,l10n_at_hr_payroll_private.kv2203_grp_v,4,nach 6 VWGJ,4742.77,2026-01-01,6
kv2203_st_v_5,l10n_at_hr_payroll_private.kv2203_grp_v,5,nach 8 VWGJ,5017.15,2026-01-01 kv2203_st_v_5,l10n_at_hr_payroll_private.kv2203_grp_v,5,nach 8 VWGJ,5017.15,2026-01-01,8
kv2203_st_v_6,l10n_at_hr_payroll_private.kv2203_grp_v,6,nach 10 VWGJ,5291.57,2026-01-01 kv2203_st_v_6,l10n_at_hr_payroll_private.kv2203_grp_v,6,nach 10 VWGJ,5291.57,2026-01-01,10
kv2203_st_v_7,l10n_at_hr_payroll_private.kv2203_grp_v,7,nach 12 VWGJ,5526.70,2026-01-01 kv2203_st_v_7,l10n_at_hr_payroll_private.kv2203_grp_v,7,nach 12 VWGJ,5526.70,2026-01-01,12
kv2203_st_v_8,l10n_at_hr_payroll_private.kv2203_grp_v,8,nach 15 VWGJ,5957.86,2026-01-01 kv2203_st_v_8,l10n_at_hr_payroll_private.kv2203_grp_v,8,nach 15 VWGJ,5957.86,2026-01-01,15
kv2203_st_vi_1,l10n_at_hr_payroll_private.kv2203_grp_vi,1,im 1. u 2. VWGJ,5482.40,2026-01-01 kv2203_st_vi_1,l10n_at_hr_payroll_private.kv2203_grp_vi,1,im 1. u 2. VWGJ,5482.40,2026-01-01,0
kv2203_st_vi_2,l10n_at_hr_payroll_private.kv2203_grp_vi,2,nach 2 VWGJ,6167.67,2026-01-01 kv2203_st_vi_2,l10n_at_hr_payroll_private.kv2203_grp_vi,2,nach 2 VWGJ,6167.67,2026-01-01,2
kv2203_st_vi_3,l10n_at_hr_payroll_private.kv2203_grp_vi,3,nach 5 VWGJ,6853.01,2026-01-01 kv2203_st_vi_3,l10n_at_hr_payroll_private.kv2203_grp_vi,3,nach 5 VWGJ,6853.01,2026-01-01,5
kv2203_st_m1_1,l10n_at_hr_payroll_private.kv2203_grp_m1,1,im 1. u 2. VWGJ,2427.72,2026-01-01 kv2203_st_m1_1,l10n_at_hr_payroll_private.kv2203_grp_m1,1,im 1. u 2. VWGJ,2427.72,2026-01-01,0
kv2203_st_m1_2,l10n_at_hr_payroll_private.kv2203_grp_m1,2,nach 2 VWGJ,2427.72,2026-01-01 kv2203_st_m1_2,l10n_at_hr_payroll_private.kv2203_grp_m1,2,nach 2 VWGJ,2427.72,2026-01-01,2
kv2203_st_m1_3,l10n_at_hr_payroll_private.kv2203_grp_m1,3,nach 4 VWGJ,2586.57,2026-01-01 kv2203_st_m1_3,l10n_at_hr_payroll_private.kv2203_grp_m1,3,nach 4 VWGJ,2586.57,2026-01-01,4
kv2203_st_m1_4,l10n_at_hr_payroll_private.kv2203_grp_m1,4,nach 6 VWGJ,2745.39,2026-01-01 kv2203_st_m1_4,l10n_at_hr_payroll_private.kv2203_grp_m1,4,nach 6 VWGJ,2745.39,2026-01-01,6
kv2203_st_m1_5,l10n_at_hr_payroll_private.kv2203_grp_m1,5,nach 8 VWGJ,2904.19,2026-01-01 kv2203_st_m1_5,l10n_at_hr_payroll_private.kv2203_grp_m1,5,nach 8 VWGJ,2904.19,2026-01-01,8
kv2203_st_m1_6,l10n_at_hr_payroll_private.kv2203_grp_m1,6,nach 10 VWGJ,3063.05,2026-01-01 kv2203_st_m1_6,l10n_at_hr_payroll_private.kv2203_grp_m1,6,nach 10 VWGJ,3063.05,2026-01-01,10
kv2203_st_m1_7,l10n_at_hr_payroll_private.kv2203_grp_m1,7,nach 12 VWGJ,3199.16,2026-01-01 kv2203_st_m1_7,l10n_at_hr_payroll_private.kv2203_grp_m1,7,nach 12 VWGJ,3199.16,2026-01-01,12
kv2203_st_m1_8,l10n_at_hr_payroll_private.kv2203_grp_m1,8,nach 15 VWGJ,3448.74,2026-01-01 kv2203_st_m1_8,l10n_at_hr_payroll_private.kv2203_grp_m1,8,nach 15 VWGJ,3448.74,2026-01-01,15
kv2203_st_m2_1,l10n_at_hr_payroll_private.kv2203_grp_m2,1,im 1. u 2. VWGJ,3095.50,2026-01-01 kv2203_st_m2_1,l10n_at_hr_payroll_private.kv2203_grp_m2,1,im 1. u 2. VWGJ,3095.50,2026-01-01,0
kv2203_st_m2_2,l10n_at_hr_payroll_private.kv2203_grp_m2,2,nach 2 VWGJ,3095.50,2026-01-01 kv2203_st_m2_2,l10n_at_hr_payroll_private.kv2203_grp_m2,2,nach 2 VWGJ,3095.50,2026-01-01,2
kv2203_st_m2_3,l10n_at_hr_payroll_private.kv2203_grp_m2,3,nach 4 VWGJ,3297.98,2026-01-01 kv2203_st_m2_3,l10n_at_hr_payroll_private.kv2203_grp_m2,3,nach 4 VWGJ,3297.98,2026-01-01,4
kv2203_st_m2_4,l10n_at_hr_payroll_private.kv2203_grp_m2,4,nach 6 VWGJ,3500.52,2026-01-01 kv2203_st_m2_4,l10n_at_hr_payroll_private.kv2203_grp_m2,4,nach 6 VWGJ,3500.52,2026-01-01,6
kv2203_st_m2_5,l10n_at_hr_payroll_private.kv2203_grp_m2,5,nach 8 VWGJ,3703.01,2026-01-01 kv2203_st_m2_5,l10n_at_hr_payroll_private.kv2203_grp_m2,5,nach 8 VWGJ,3703.01,2026-01-01,8
kv2203_st_m2_6,l10n_at_hr_payroll_private.kv2203_grp_m2,6,nach 10 VWGJ,3905.53,2026-01-01 kv2203_st_m2_6,l10n_at_hr_payroll_private.kv2203_grp_m2,6,nach 10 VWGJ,3905.53,2026-01-01,10
kv2203_st_m2_7,l10n_at_hr_payroll_private.kv2203_grp_m2,7,nach 12 VWGJ,4079.13,2026-01-01 kv2203_st_m2_7,l10n_at_hr_payroll_private.kv2203_grp_m2,7,nach 12 VWGJ,4079.13,2026-01-01,12
kv2203_st_m2_8,l10n_at_hr_payroll_private.kv2203_grp_m2,8,nach 15 VWGJ,4397.37,2026-01-01 kv2203_st_m2_8,l10n_at_hr_payroll_private.kv2203_grp_m2,8,nach 15 VWGJ,4397.37,2026-01-01,15
kv2203_st_m2fs_1,l10n_at_hr_payroll_private.kv2203_grp_m2fs,1,im 1. u 2. VWGJ,3241.68,2026-01-01 kv2203_st_m2fs_1,l10n_at_hr_payroll_private.kv2203_grp_m2fs,1,im 1. u 2. VWGJ,3241.68,2026-01-01,0
kv2203_st_m2fs_2,l10n_at_hr_payroll_private.kv2203_grp_m2fs,2,nach 2 VWGJ,3241.68,2026-01-01 kv2203_st_m2fs_2,l10n_at_hr_payroll_private.kv2203_grp_m2fs,2,nach 2 VWGJ,3241.68,2026-01-01,2
kv2203_st_m2fs_3,l10n_at_hr_payroll_private.kv2203_grp_m2fs,3,nach 4 VWGJ,3453.74,2026-01-01 kv2203_st_m2fs_3,l10n_at_hr_payroll_private.kv2203_grp_m2fs,3,nach 4 VWGJ,3453.74,2026-01-01,4
kv2203_st_m2fs_4,l10n_at_hr_payroll_private.kv2203_grp_m2fs,4,nach 6 VWGJ,3665.79,2026-01-01 kv2203_st_m2fs_4,l10n_at_hr_payroll_private.kv2203_grp_m2fs,4,nach 6 VWGJ,3665.79,2026-01-01,6
kv2203_st_m2fs_5,l10n_at_hr_payroll_private.kv2203_grp_m2fs,5,nach 8 VWGJ,3877.83,2026-01-01 kv2203_st_m2fs_5,l10n_at_hr_payroll_private.kv2203_grp_m2fs,5,nach 8 VWGJ,3877.83,2026-01-01,8
kv2203_st_m2fs_6,l10n_at_hr_payroll_private.kv2203_grp_m2fs,6,nach 10 VWGJ,4089.90,2026-01-01 kv2203_st_m2fs_6,l10n_at_hr_payroll_private.kv2203_grp_m2fs,6,nach 10 VWGJ,4089.90,2026-01-01,10
kv2203_st_m2fs_7,l10n_at_hr_payroll_private.kv2203_grp_m2fs,7,nach 12 VWGJ,4271.68,2026-01-01 kv2203_st_m2fs_7,l10n_at_hr_payroll_private.kv2203_grp_m2fs,7,nach 12 VWGJ,4271.68,2026-01-01,12
kv2203_st_m2fs_8,l10n_at_hr_payroll_private.kv2203_grp_m2fs,8,nach 15 VWGJ,4605.01,2026-01-01 kv2203_st_m2fs_8,l10n_at_hr_payroll_private.kv2203_grp_m2fs,8,nach 15 VWGJ,4605.01,2026-01-01,15
kv2203_st_m3_1,l10n_at_hr_payroll_private.kv2203_grp_m3,1,im 1. u 2. VWGJ,3542.72,2026-01-01 kv2203_st_m3_1,l10n_at_hr_payroll_private.kv2203_grp_m3,1,im 1. u 2. VWGJ,3542.72,2026-01-01,0
kv2203_st_m3_2,l10n_at_hr_payroll_private.kv2203_grp_m3,2,nach 2 VWGJ,3542.72,2026-01-01 kv2203_st_m3_2,l10n_at_hr_payroll_private.kv2203_grp_m3,2,nach 2 VWGJ,3542.72,2026-01-01,2
kv2203_st_m3_3,l10n_at_hr_payroll_private.kv2203_grp_m3,3,nach 4 VWGJ,3774.51,2026-01-01 kv2203_st_m3_3,l10n_at_hr_payroll_private.kv2203_grp_m3,3,nach 4 VWGJ,3774.51,2026-01-01,4
kv2203_st_m3_4,l10n_at_hr_payroll_private.kv2203_grp_m3,4,nach 6 VWGJ,4006.27,2026-01-01 kv2203_st_m3_4,l10n_at_hr_payroll_private.kv2203_grp_m3,4,nach 6 VWGJ,4006.27,2026-01-01,6
kv2203_st_m3_5,l10n_at_hr_payroll_private.kv2203_grp_m3,5,nach 8 VWGJ,4238.05,2026-01-01 kv2203_st_m3_5,l10n_at_hr_payroll_private.kv2203_grp_m3,5,nach 8 VWGJ,4238.05,2026-01-01,8
kv2203_st_m3_6,l10n_at_hr_payroll_private.kv2203_grp_m3,6,nach 10 VWGJ,4469.81,2026-01-01 kv2203_st_m3_6,l10n_at_hr_payroll_private.kv2203_grp_m3,6,nach 10 VWGJ,4469.81,2026-01-01,10
kv2203_st_m3_7,l10n_at_hr_payroll_private.kv2203_grp_m3,7,nach 12 VWGJ,4668.45,2026-01-01 kv2203_st_m3_7,l10n_at_hr_payroll_private.kv2203_grp_m3,7,nach 12 VWGJ,4668.45,2026-01-01,12
kv2203_st_m3_8,l10n_at_hr_payroll_private.kv2203_grp_m3,8,nach 15 VWGJ,5032.68,2026-01-01 kv2203_st_m3_8,l10n_at_hr_payroll_private.kv2203_grp_m3,8,nach 15 VWGJ,5032.68,2026-01-01,15
kv2748_st_i_1,l10n_at_hr_payroll_private.kv2748_grp_i,1,im 1. u 2. VWGJ,1960.54,2026-01-01 kv2748_st_i_1,l10n_at_hr_payroll_private.kv2748_grp_i,1,im 1. u 2. VWGJ,1960.54,2026-01-01,0
kv2748_st_i_2,l10n_at_hr_payroll_private.kv2748_grp_i,2,nach 2 VWGJ,2058.43,2026-01-01 kv2748_st_i_2,l10n_at_hr_payroll_private.kv2748_grp_i,2,nach 2 VWGJ,2058.43,2026-01-01,2
kv2748_st_i_3,l10n_at_hr_payroll_private.kv2748_grp_i,3,nach 4 VWGJ,2190.76,2026-01-01 kv2748_st_i_3,l10n_at_hr_payroll_private.kv2748_grp_i,3,nach 4 VWGJ,2190.76,2026-01-01,4
kv2748_st_i_4,l10n_at_hr_payroll_private.kv2748_grp_i,4,nach 6 VWGJ,2323.12,2026-01-01 kv2748_st_i_4,l10n_at_hr_payroll_private.kv2748_grp_i,4,nach 6 VWGJ,2323.12,2026-01-01,6
kv2748_st_i_5,l10n_at_hr_payroll_private.kv2748_grp_i,5,nach 8 VWGJ,2455.45,2026-01-01 kv2748_st_i_5,l10n_at_hr_payroll_private.kv2748_grp_i,5,nach 8 VWGJ,2455.45,2026-01-01,8
kv2748_st_i_6,l10n_at_hr_payroll_private.kv2748_grp_i,6,nach 10 VWGJ,2588.72,2026-01-01 kv2748_st_i_6,l10n_at_hr_payroll_private.kv2748_grp_i,6,nach 10 VWGJ,2588.72,2026-01-01,10
kv2748_st_i_7,l10n_at_hr_payroll_private.kv2748_grp_i,7,nach 12 VWGJ,2703.75,2026-01-01 kv2748_st_i_7,l10n_at_hr_payroll_private.kv2748_grp_i,7,nach 12 VWGJ,2703.75,2026-01-01,12
kv2748_st_i_8,l10n_at_hr_payroll_private.kv2748_grp_i,8,nach 15 VWGJ,2914.69,2026-01-01 kv2748_st_i_8,l10n_at_hr_payroll_private.kv2748_grp_i,8,nach 15 VWGJ,2914.69,2026-01-01,15
kv2748_st_ii_1,l10n_at_hr_payroll_private.kv2748_grp_ii,1,im 1. u 2. VWGJ,2199.15,2026-01-01 kv2748_st_ii_1,l10n_at_hr_payroll_private.kv2748_grp_ii,1,im 1. u 2. VWGJ,2199.15,2026-01-01,0
kv2748_st_ii_2,l10n_at_hr_payroll_private.kv2748_grp_ii,2,nach 2 VWGJ,2350.62,2026-01-01 kv2748_st_ii_2,l10n_at_hr_payroll_private.kv2748_grp_ii,2,nach 2 VWGJ,2350.62,2026-01-01,2
kv2748_st_ii_3,l10n_at_hr_payroll_private.kv2748_grp_ii,3,nach 4 VWGJ,2502.07,2026-01-01 kv2748_st_ii_3,l10n_at_hr_payroll_private.kv2748_grp_ii,3,nach 4 VWGJ,2502.07,2026-01-01,4
kv2748_st_ii_4,l10n_at_hr_payroll_private.kv2748_grp_ii,4,nach 6 VWGJ,2655.45,2026-01-01 kv2748_st_ii_4,l10n_at_hr_payroll_private.kv2748_grp_ii,4,nach 6 VWGJ,2655.45,2026-01-01,6
kv2748_st_ii_5,l10n_at_hr_payroll_private.kv2748_grp_ii,5,nach 8 VWGJ,2809.08,2026-01-01 kv2748_st_ii_5,l10n_at_hr_payroll_private.kv2748_grp_ii,5,nach 8 VWGJ,2809.08,2026-01-01,8
kv2748_st_ii_6,l10n_at_hr_payroll_private.kv2748_grp_ii,6,nach 10 VWGJ,2962.72,2026-01-01 kv2748_st_ii_6,l10n_at_hr_payroll_private.kv2748_grp_ii,6,nach 10 VWGJ,2962.72,2026-01-01,10
kv2748_st_ii_7,l10n_at_hr_payroll_private.kv2748_grp_ii,7,nach 12 VWGJ,3094.32,2026-01-01 kv2748_st_ii_7,l10n_at_hr_payroll_private.kv2748_grp_ii,7,nach 12 VWGJ,3094.32,2026-01-01,12
kv2748_st_ii_8,l10n_at_hr_payroll_private.kv2748_grp_ii,8,nach 15 VWGJ,3335.74,2026-01-01 kv2748_st_ii_8,l10n_at_hr_payroll_private.kv2748_grp_ii,8,nach 15 VWGJ,3335.74,2026-01-01,15
kv2748_st_iii_1,l10n_at_hr_payroll_private.kv2748_grp_iii,1,im 1. u 2. VWGJ,2727.40,2026-01-01 kv2748_st_iii_1,l10n_at_hr_payroll_private.kv2748_grp_iii,1,im 1. u 2. VWGJ,2727.40,2026-01-01,0
kv2748_st_iii_2,l10n_at_hr_payroll_private.kv2748_grp_iii,2,nach 2 VWGJ,2918.34,2026-01-01 kv2748_st_iii_2,l10n_at_hr_payroll_private.kv2748_grp_iii,2,nach 2 VWGJ,2918.34,2026-01-01,2
kv2748_st_iii_3,l10n_at_hr_payroll_private.kv2748_grp_iii,3,nach 4 VWGJ,3109.21,2026-01-01 kv2748_st_iii_3,l10n_at_hr_payroll_private.kv2748_grp_iii,3,nach 4 VWGJ,3109.21,2026-01-01,4
kv2748_st_iii_4,l10n_at_hr_payroll_private.kv2748_grp_iii,4,nach 6 VWGJ,3300.16,2026-01-01 kv2748_st_iii_4,l10n_at_hr_payroll_private.kv2748_grp_iii,4,nach 6 VWGJ,3300.16,2026-01-01,6
kv2748_st_iii_5,l10n_at_hr_payroll_private.kv2748_grp_iii,5,nach 8 VWGJ,3491.06,2026-01-01 kv2748_st_iii_5,l10n_at_hr_payroll_private.kv2748_grp_iii,5,nach 8 VWGJ,3491.06,2026-01-01,8
kv2748_st_iii_6,l10n_at_hr_payroll_private.kv2748_grp_iii,6,nach 10 VWGJ,3682.00,2026-01-01 kv2748_st_iii_6,l10n_at_hr_payroll_private.kv2748_grp_iii,6,nach 10 VWGJ,3682.00,2026-01-01,10
kv2748_st_iii_7,l10n_at_hr_payroll_private.kv2748_grp_iii,7,nach 12 VWGJ,3845.61,2026-01-01 kv2748_st_iii_7,l10n_at_hr_payroll_private.kv2748_grp_iii,7,nach 12 VWGJ,3845.61,2026-01-01,12
kv2748_st_iii_8,l10n_at_hr_payroll_private.kv2748_grp_iii,8,nach 15 VWGJ,4145.62,2026-01-01 kv2748_st_iii_8,l10n_at_hr_payroll_private.kv2748_grp_iii,8,nach 15 VWGJ,4145.62,2026-01-01,15
kv2748_st_iv_1,l10n_at_hr_payroll_private.kv2748_grp_iv,1,im 1. u 2. VWGJ,3415.01,2026-01-01 kv2748_st_iv_1,l10n_at_hr_payroll_private.kv2748_grp_iv,1,im 1. u 2. VWGJ,3415.01,2026-01-01,0
kv2748_st_iv_2,l10n_at_hr_payroll_private.kv2748_grp_iv,2,nach 2 VWGJ,3654.05,2026-01-01 kv2748_st_iv_2,l10n_at_hr_payroll_private.kv2748_grp_iv,2,nach 2 VWGJ,3654.05,2026-01-01,2
kv2748_st_iv_3,l10n_at_hr_payroll_private.kv2748_grp_iv,3,nach 4 VWGJ,3893.09,2026-01-01 kv2748_st_iv_3,l10n_at_hr_payroll_private.kv2748_grp_iv,3,nach 4 VWGJ,3893.09,2026-01-01,4
kv2748_st_iv_4,l10n_at_hr_payroll_private.kv2748_grp_iv,4,nach 6 VWGJ,4132.15,2026-01-01 kv2748_st_iv_4,l10n_at_hr_payroll_private.kv2748_grp_iv,4,nach 6 VWGJ,4132.15,2026-01-01,6
kv2748_st_iv_5,l10n_at_hr_payroll_private.kv2748_grp_iv,5,nach 8 VWGJ,4371.19,2026-01-01 kv2748_st_iv_5,l10n_at_hr_payroll_private.kv2748_grp_iv,5,nach 8 VWGJ,4371.19,2026-01-01,8
kv2748_st_iv_6,l10n_at_hr_payroll_private.kv2748_grp_iv,6,nach 10 VWGJ,4610.26,2026-01-01 kv2748_st_iv_6,l10n_at_hr_payroll_private.kv2748_grp_iv,6,nach 10 VWGJ,4610.26,2026-01-01,10
kv2748_st_iv_7,l10n_at_hr_payroll_private.kv2748_grp_iv,7,nach 12 VWGJ,4815.15,2026-01-01 kv2748_st_iv_7,l10n_at_hr_payroll_private.kv2748_grp_iv,7,nach 12 VWGJ,4815.15,2026-01-01,12
kv2748_st_iv_8,l10n_at_hr_payroll_private.kv2748_grp_iv,8,nach 15 VWGJ,5190.77,2026-01-01 kv2748_st_iv_8,l10n_at_hr_payroll_private.kv2748_grp_iv,8,nach 15 VWGJ,5190.77,2026-01-01,15
kv2748_st_ivm_1,l10n_at_hr_payroll_private.kv2748_grp_ivm,1,im 1.-4. VWGJ,3415.01,2026-01-01 kv2748_st_ivm_1,l10n_at_hr_payroll_private.kv2748_grp_ivm,1,im 1.-4. VWGJ,3415.01,2026-01-01,0
kv2748_st_ivm_2,l10n_at_hr_payroll_private.kv2748_grp_ivm,2,nach 4 VWGJ,3654.05,2026-01-01 kv2748_st_ivm_2,l10n_at_hr_payroll_private.kv2748_grp_ivm,2,nach 4 VWGJ,3654.05,2026-01-01,4
kv2748_st_ivm_3,l10n_at_hr_payroll_private.kv2748_grp_ivm,3,nach 6 VWGJ,3893.09,2026-01-01 kv2748_st_ivm_3,l10n_at_hr_payroll_private.kv2748_grp_ivm,3,nach 6 VWGJ,3893.09,2026-01-01,6
kv2748_st_ivm_4,l10n_at_hr_payroll_private.kv2748_grp_ivm,4,nach 8 VWGJ,4132.15,2026-01-01 kv2748_st_ivm_4,l10n_at_hr_payroll_private.kv2748_grp_ivm,4,nach 8 VWGJ,4132.15,2026-01-01,8
kv2748_st_ivm_5,l10n_at_hr_payroll_private.kv2748_grp_ivm,5,nach 10 VWGJ,4371.19,2026-01-01 kv2748_st_ivm_5,l10n_at_hr_payroll_private.kv2748_grp_ivm,5,nach 10 VWGJ,4371.19,2026-01-01,10
kv2748_st_ivm_6,l10n_at_hr_payroll_private.kv2748_grp_ivm,6,nach 12 VWGJ,4610.26,2026-01-01 kv2748_st_ivm_6,l10n_at_hr_payroll_private.kv2748_grp_ivm,6,nach 12 VWGJ,4610.26,2026-01-01,12
kv2748_st_ivm_7,l10n_at_hr_payroll_private.kv2748_grp_ivm,7,nach 14 VWGJ,4815.15,2026-01-01 kv2748_st_ivm_7,l10n_at_hr_payroll_private.kv2748_grp_ivm,7,nach 14 VWGJ,4815.15,2026-01-01,14
kv2748_st_ivm_8,l10n_at_hr_payroll_private.kv2748_grp_ivm,8,nach 17 VWGJ,5047.10,2026-01-01 kv2748_st_ivm_8,l10n_at_hr_payroll_private.kv2748_grp_ivm,8,nach 17 VWGJ,5047.10,2026-01-01,17
kv2748_st_v_1,l10n_at_hr_payroll_private.kv2748_grp_v,1,im 1. u 2. VWGJ,4299.70,2026-01-01 kv2748_st_v_1,l10n_at_hr_payroll_private.kv2748_grp_v,1,im 1. u 2. VWGJ,4299.70,2026-01-01,0
kv2748_st_v_2,l10n_at_hr_payroll_private.kv2748_grp_v,2,nach 2 VWGJ,4594.04,2026-01-01 kv2748_st_v_2,l10n_at_hr_payroll_private.kv2748_grp_v,2,nach 2 VWGJ,4594.04,2026-01-01,2
kv2748_st_v_3,l10n_at_hr_payroll_private.kv2748_grp_v,3,nach 4 VWGJ,4888.37,2026-01-01 kv2748_st_v_3,l10n_at_hr_payroll_private.kv2748_grp_v,3,nach 4 VWGJ,4888.37,2026-01-01,4
kv2748_st_v_4,l10n_at_hr_payroll_private.kv2748_grp_v,4,nach 6 VWGJ,5182.71,2026-01-01 kv2748_st_v_4,l10n_at_hr_payroll_private.kv2748_grp_v,4,nach 6 VWGJ,5182.71,2026-01-01,6
kv2748_st_v_5,l10n_at_hr_payroll_private.kv2748_grp_v,5,nach 8 VWGJ,5477.00,2026-01-01 kv2748_st_v_5,l10n_at_hr_payroll_private.kv2748_grp_v,5,nach 8 VWGJ,5477.00,2026-01-01,8
kv2748_st_v_6,l10n_at_hr_payroll_private.kv2748_grp_v,6,nach 10 VWGJ,5771.34,2026-01-01 kv2748_st_v_6,l10n_at_hr_payroll_private.kv2748_grp_v,6,nach 10 VWGJ,5771.34,2026-01-01,10
kv2748_st_v_7,l10n_at_hr_payroll_private.kv2748_grp_v,7,nach 12 VWGJ,6023.65,2026-01-01 kv2748_st_v_7,l10n_at_hr_payroll_private.kv2748_grp_v,7,nach 12 VWGJ,6023.65,2026-01-01,12
kv2748_st_v_8,l10n_at_hr_payroll_private.kv2748_grp_v,8,nach 15 VWGJ,6486.15,2026-01-01 kv2748_st_v_8,l10n_at_hr_payroll_private.kv2748_grp_v,8,nach 15 VWGJ,6486.15,2026-01-01,15
kv2748_st_vom_1,l10n_at_hr_payroll_private.kv2748_grp_vom,1,im 1.-6. VWGJ,4299.70,2026-01-01 kv2748_st_vom_1,l10n_at_hr_payroll_private.kv2748_grp_vom,1,im 1.-6. VWGJ,4299.70,2026-01-01,0
kv2748_st_vom_2,l10n_at_hr_payroll_private.kv2748_grp_vom,2,nach 6 VWGJ,4594.04,2026-01-01 kv2748_st_vom_2,l10n_at_hr_payroll_private.kv2748_grp_vom,2,nach 6 VWGJ,4594.04,2026-01-01,6
kv2748_st_vom_3,l10n_at_hr_payroll_private.kv2748_grp_vom,3,nach 8 VWGJ,4888.37,2026-01-01 kv2748_st_vom_3,l10n_at_hr_payroll_private.kv2748_grp_vom,3,nach 8 VWGJ,4888.37,2026-01-01,8
kv2748_st_vom_4,l10n_at_hr_payroll_private.kv2748_grp_vom,4,nach 10 VWGJ,5182.71,2026-01-01 kv2748_st_vom_4,l10n_at_hr_payroll_private.kv2748_grp_vom,4,nach 10 VWGJ,5182.71,2026-01-01,10
kv2748_st_vom_5,l10n_at_hr_payroll_private.kv2748_grp_vom,5,nach 12 VWGJ,5477.00,2026-01-01 kv2748_st_vom_5,l10n_at_hr_payroll_private.kv2748_grp_vom,5,nach 12 VWGJ,5477.00,2026-01-01,12
kv2748_st_vi_1,l10n_at_hr_payroll_private.kv2748_grp_vi,1,im 1. u 2. VWGJ,6021.80,2026-01-01 kv2748_st_vi_1,l10n_at_hr_payroll_private.kv2748_grp_vi,1,im 1. u 2. VWGJ,6021.80,2026-01-01,0
kv2748_st_vi_2,l10n_at_hr_payroll_private.kv2748_grp_vi,2,nach 2 VWGJ,6762.62,2026-01-01 kv2748_st_vi_2,l10n_at_hr_payroll_private.kv2748_grp_vi,2,nach 2 VWGJ,6762.62,2026-01-01,2
kv2748_st_vi_3,l10n_at_hr_payroll_private.kv2748_grp_vi,3,nach 5 VWGJ,7503.50,2026-01-01 kv2748_st_vi_3,l10n_at_hr_payroll_private.kv2748_grp_vi,3,nach 5 VWGJ,7503.50,2026-01-01,5
kv2748_st_meister_1,l10n_at_hr_payroll_private.kv2748_grp_meister,1,im 1. u 2. VWGJ,4130.76,2026-01-01 kv2748_st_meister_1,l10n_at_hr_payroll_private.kv2748_grp_meister,1,im 1. u 2. VWGJ,4130.76,2026-01-01,0
kv2748_st_meister_2,l10n_at_hr_payroll_private.kv2748_grp_meister,2,nach 2 VWGJ,4130.76,2026-01-01 kv2748_st_meister_2,l10n_at_hr_payroll_private.kv2748_grp_meister,2,nach 2 VWGJ,4130.76,2026-01-01,2
kv2748_st_meister_3,l10n_at_hr_payroll_private.kv2748_grp_meister,3,nach 4 VWGJ,4303.13,2026-01-01 kv2748_st_meister_3,l10n_at_hr_payroll_private.kv2748_grp_meister,3,nach 4 VWGJ,4303.13,2026-01-01,4
kv2748_st_meister_4,l10n_at_hr_payroll_private.kv2748_grp_meister,4,nach 6 VWGJ,4475.48,2026-01-01 kv2748_st_meister_4,l10n_at_hr_payroll_private.kv2748_grp_meister,4,nach 6 VWGJ,4475.48,2026-01-01,6
kv2748_st_meister_5,l10n_at_hr_payroll_private.kv2748_grp_meister,5,nach 8 VWGJ,4647.84,2026-01-01 kv2748_st_meister_5,l10n_at_hr_payroll_private.kv2748_grp_meister,5,nach 8 VWGJ,4647.84,2026-01-01,8
kv2748_st_meister_6,l10n_at_hr_payroll_private.kv2748_grp_meister,6,nach 10 VWGJ,4820.17,2026-01-01 kv2748_st_meister_6,l10n_at_hr_payroll_private.kv2748_grp_meister,6,nach 10 VWGJ,4820.17,2026-01-01,10
kv2748_st_meister_7,l10n_at_hr_payroll_private.kv2748_grp_meister,7,nach 12 VWGJ,4992.53,2026-01-01 kv2748_st_meister_7,l10n_at_hr_payroll_private.kv2748_grp_meister,7,nach 12 VWGJ,4992.53,2026-01-01,12
kv2748_st_meister_8,l10n_at_hr_payroll_private.kv2748_grp_meister,8,nach 15 VWGJ,5270.29,2026-01-01 kv2748_st_meister_8,l10n_at_hr_payroll_private.kv2748_grp_meister,8,nach 15 VWGJ,5270.29,2026-01-01,15
1 id gruppe_id:id stufe bezeichnung gehalt gueltig_ab vwgj_ab
2 kv2203_st_ii_1 l10n_at_hr_payroll_private.kv2203_grp_ii 1 im 1. u 2. VWGJ 2059.41 2026-01-01 0
3 kv2203_st_ii_2 l10n_at_hr_payroll_private.kv2203_grp_ii 2 nach 2 VWGJ 2179.68 2026-01-01 2
4 kv2203_st_ii_3 l10n_at_hr_payroll_private.kv2203_grp_ii 3 nach 4 VWGJ 2315.46 2026-01-01 4
5 kv2203_st_ii_4 l10n_at_hr_payroll_private.kv2203_grp_ii 4 nach 6 VWGJ 2457.65 2026-01-01 6
6 kv2203_st_ii_5 l10n_at_hr_payroll_private.kv2203_grp_ii 5 nach 8 VWGJ 2599.83 2026-01-01 8
7 kv2203_st_ii_6 l10n_at_hr_payroll_private.kv2203_grp_ii 6 nach 10 VWGJ 2742.03 2026-01-01 10
8 kv2203_st_ii_7 l10n_at_hr_payroll_private.kv2203_grp_ii 7 nach 12 VWGJ 2863.88 2026-01-01 12
9 kv2203_st_ii_8 l10n_at_hr_payroll_private.kv2203_grp_ii 8 nach 15 VWGJ 3087.28 2026-01-01 15
10 kv2203_st_iii_1 l10n_at_hr_payroll_private.kv2203_grp_iii 1 im 1. u 2. VWGJ 2528.04 2026-01-01 0
11 kv2203_st_iii_2 l10n_at_hr_payroll_private.kv2203_grp_iii 2 nach 2 VWGJ 2705.03 2026-01-01 2
12 kv2203_st_iii_3 l10n_at_hr_payroll_private.kv2203_grp_iii 3 nach 4 VWGJ 2881.97 2026-01-01 4
13 kv2203_st_iii_4 l10n_at_hr_payroll_private.kv2203_grp_iii 4 nach 6 VWGJ 3058.96 2026-01-01 6
14 kv2203_st_iii_5 l10n_at_hr_payroll_private.kv2203_grp_iii 5 nach 8 VWGJ 3232.73 2026-01-01 8
15 kv2203_st_iii_6 l10n_at_hr_payroll_private.kv2203_grp_iii 6 nach 10 VWGJ 3409.53 2026-01-01 10
16 kv2203_st_iii_7 l10n_at_hr_payroll_private.kv2203_grp_iii 7 nach 12 VWGJ 3561.04 2026-01-01 12
17 kv2203_st_iii_8 l10n_at_hr_payroll_private.kv2203_grp_iii 8 nach 15 VWGJ 3838.85 2026-01-01 15
18 kv2203_st_iv_1 l10n_at_hr_payroll_private.kv2203_grp_iv 1 im 1. u 2. VWGJ 3146.27 2026-01-01 0
19 kv2203_st_iv_2 l10n_at_hr_payroll_private.kv2203_grp_iv 2 nach 2 VWGJ 3366.49 2026-01-01 2
20 kv2203_st_iv_3 l10n_at_hr_payroll_private.kv2203_grp_iv 3 nach 4 VWGJ 3586.73 2026-01-01 4
21 kv2203_st_iv_4 l10n_at_hr_payroll_private.kv2203_grp_iv 4 nach 6 VWGJ 3806.97 2026-01-01 6
22 kv2203_st_iv_5 l10n_at_hr_payroll_private.kv2203_grp_iv 5 nach 8 VWGJ 4027.20 2026-01-01 8
23 kv2203_st_iv_6 l10n_at_hr_payroll_private.kv2203_grp_iv 6 nach 10 VWGJ 4247.47 2026-01-01 10
24 kv2203_st_iv_7 l10n_at_hr_payroll_private.kv2203_grp_iv 7 nach 12 VWGJ 4436.22 2026-01-01 12
25 kv2203_st_iv_8 l10n_at_hr_payroll_private.kv2203_grp_iv 8 nach 15 VWGJ 4782.32 2026-01-01 15
26 kv2203_st_v_1 l10n_at_hr_payroll_private.kv2203_grp_v 1 im 1. u 2. VWGJ 3919.67 2026-01-01 0
27 kv2203_st_v_2 l10n_at_hr_payroll_private.kv2203_grp_v 2 nach 2 VWGJ 4194.02 2026-01-01 2
28 kv2203_st_v_3 l10n_at_hr_payroll_private.kv2203_grp_v 3 nach 4 VWGJ 4468.41 2026-01-01 4
29 kv2203_st_v_4 l10n_at_hr_payroll_private.kv2203_grp_v 4 nach 6 VWGJ 4742.77 2026-01-01 6
30 kv2203_st_v_5 l10n_at_hr_payroll_private.kv2203_grp_v 5 nach 8 VWGJ 5017.15 2026-01-01 8
31 kv2203_st_v_6 l10n_at_hr_payroll_private.kv2203_grp_v 6 nach 10 VWGJ 5291.57 2026-01-01 10
32 kv2203_st_v_7 l10n_at_hr_payroll_private.kv2203_grp_v 7 nach 12 VWGJ 5526.70 2026-01-01 12
33 kv2203_st_v_8 l10n_at_hr_payroll_private.kv2203_grp_v 8 nach 15 VWGJ 5957.86 2026-01-01 15
34 kv2203_st_vi_1 l10n_at_hr_payroll_private.kv2203_grp_vi 1 im 1. u 2. VWGJ 5482.40 2026-01-01 0
35 kv2203_st_vi_2 l10n_at_hr_payroll_private.kv2203_grp_vi 2 nach 2 VWGJ 6167.67 2026-01-01 2
36 kv2203_st_vi_3 l10n_at_hr_payroll_private.kv2203_grp_vi 3 nach 5 VWGJ 6853.01 2026-01-01 5
37 kv2203_st_m1_1 l10n_at_hr_payroll_private.kv2203_grp_m1 1 im 1. u 2. VWGJ 2427.72 2026-01-01 0
38 kv2203_st_m1_2 l10n_at_hr_payroll_private.kv2203_grp_m1 2 nach 2 VWGJ 2427.72 2026-01-01 2
39 kv2203_st_m1_3 l10n_at_hr_payroll_private.kv2203_grp_m1 3 nach 4 VWGJ 2586.57 2026-01-01 4
40 kv2203_st_m1_4 l10n_at_hr_payroll_private.kv2203_grp_m1 4 nach 6 VWGJ 2745.39 2026-01-01 6
41 kv2203_st_m1_5 l10n_at_hr_payroll_private.kv2203_grp_m1 5 nach 8 VWGJ 2904.19 2026-01-01 8
42 kv2203_st_m1_6 l10n_at_hr_payroll_private.kv2203_grp_m1 6 nach 10 VWGJ 3063.05 2026-01-01 10
43 kv2203_st_m1_7 l10n_at_hr_payroll_private.kv2203_grp_m1 7 nach 12 VWGJ 3199.16 2026-01-01 12
44 kv2203_st_m1_8 l10n_at_hr_payroll_private.kv2203_grp_m1 8 nach 15 VWGJ 3448.74 2026-01-01 15
45 kv2203_st_m2_1 l10n_at_hr_payroll_private.kv2203_grp_m2 1 im 1. u 2. VWGJ 3095.50 2026-01-01 0
46 kv2203_st_m2_2 l10n_at_hr_payroll_private.kv2203_grp_m2 2 nach 2 VWGJ 3095.50 2026-01-01 2
47 kv2203_st_m2_3 l10n_at_hr_payroll_private.kv2203_grp_m2 3 nach 4 VWGJ 3297.98 2026-01-01 4
48 kv2203_st_m2_4 l10n_at_hr_payroll_private.kv2203_grp_m2 4 nach 6 VWGJ 3500.52 2026-01-01 6
49 kv2203_st_m2_5 l10n_at_hr_payroll_private.kv2203_grp_m2 5 nach 8 VWGJ 3703.01 2026-01-01 8
50 kv2203_st_m2_6 l10n_at_hr_payroll_private.kv2203_grp_m2 6 nach 10 VWGJ 3905.53 2026-01-01 10
51 kv2203_st_m2_7 l10n_at_hr_payroll_private.kv2203_grp_m2 7 nach 12 VWGJ 4079.13 2026-01-01 12
52 kv2203_st_m2_8 l10n_at_hr_payroll_private.kv2203_grp_m2 8 nach 15 VWGJ 4397.37 2026-01-01 15
53 kv2203_st_m2fs_1 l10n_at_hr_payroll_private.kv2203_grp_m2fs 1 im 1. u 2. VWGJ 3241.68 2026-01-01 0
54 kv2203_st_m2fs_2 l10n_at_hr_payroll_private.kv2203_grp_m2fs 2 nach 2 VWGJ 3241.68 2026-01-01 2
55 kv2203_st_m2fs_3 l10n_at_hr_payroll_private.kv2203_grp_m2fs 3 nach 4 VWGJ 3453.74 2026-01-01 4
56 kv2203_st_m2fs_4 l10n_at_hr_payroll_private.kv2203_grp_m2fs 4 nach 6 VWGJ 3665.79 2026-01-01 6
57 kv2203_st_m2fs_5 l10n_at_hr_payroll_private.kv2203_grp_m2fs 5 nach 8 VWGJ 3877.83 2026-01-01 8
58 kv2203_st_m2fs_6 l10n_at_hr_payroll_private.kv2203_grp_m2fs 6 nach 10 VWGJ 4089.90 2026-01-01 10
59 kv2203_st_m2fs_7 l10n_at_hr_payroll_private.kv2203_grp_m2fs 7 nach 12 VWGJ 4271.68 2026-01-01 12
60 kv2203_st_m2fs_8 l10n_at_hr_payroll_private.kv2203_grp_m2fs 8 nach 15 VWGJ 4605.01 2026-01-01 15
61 kv2203_st_m3_1 l10n_at_hr_payroll_private.kv2203_grp_m3 1 im 1. u 2. VWGJ 3542.72 2026-01-01 0
62 kv2203_st_m3_2 l10n_at_hr_payroll_private.kv2203_grp_m3 2 nach 2 VWGJ 3542.72 2026-01-01 2
63 kv2203_st_m3_3 l10n_at_hr_payroll_private.kv2203_grp_m3 3 nach 4 VWGJ 3774.51 2026-01-01 4
64 kv2203_st_m3_4 l10n_at_hr_payroll_private.kv2203_grp_m3 4 nach 6 VWGJ 4006.27 2026-01-01 6
65 kv2203_st_m3_5 l10n_at_hr_payroll_private.kv2203_grp_m3 5 nach 8 VWGJ 4238.05 2026-01-01 8
66 kv2203_st_m3_6 l10n_at_hr_payroll_private.kv2203_grp_m3 6 nach 10 VWGJ 4469.81 2026-01-01 10
67 kv2203_st_m3_7 l10n_at_hr_payroll_private.kv2203_grp_m3 7 nach 12 VWGJ 4668.45 2026-01-01 12
68 kv2203_st_m3_8 l10n_at_hr_payroll_private.kv2203_grp_m3 8 nach 15 VWGJ 5032.68 2026-01-01 15
69 kv2748_st_i_1 l10n_at_hr_payroll_private.kv2748_grp_i 1 im 1. u 2. VWGJ 1960.54 2026-01-01 0
70 kv2748_st_i_2 l10n_at_hr_payroll_private.kv2748_grp_i 2 nach 2 VWGJ 2058.43 2026-01-01 2
71 kv2748_st_i_3 l10n_at_hr_payroll_private.kv2748_grp_i 3 nach 4 VWGJ 2190.76 2026-01-01 4
72 kv2748_st_i_4 l10n_at_hr_payroll_private.kv2748_grp_i 4 nach 6 VWGJ 2323.12 2026-01-01 6
73 kv2748_st_i_5 l10n_at_hr_payroll_private.kv2748_grp_i 5 nach 8 VWGJ 2455.45 2026-01-01 8
74 kv2748_st_i_6 l10n_at_hr_payroll_private.kv2748_grp_i 6 nach 10 VWGJ 2588.72 2026-01-01 10
75 kv2748_st_i_7 l10n_at_hr_payroll_private.kv2748_grp_i 7 nach 12 VWGJ 2703.75 2026-01-01 12
76 kv2748_st_i_8 l10n_at_hr_payroll_private.kv2748_grp_i 8 nach 15 VWGJ 2914.69 2026-01-01 15
77 kv2748_st_ii_1 l10n_at_hr_payroll_private.kv2748_grp_ii 1 im 1. u 2. VWGJ 2199.15 2026-01-01 0
78 kv2748_st_ii_2 l10n_at_hr_payroll_private.kv2748_grp_ii 2 nach 2 VWGJ 2350.62 2026-01-01 2
79 kv2748_st_ii_3 l10n_at_hr_payroll_private.kv2748_grp_ii 3 nach 4 VWGJ 2502.07 2026-01-01 4
80 kv2748_st_ii_4 l10n_at_hr_payroll_private.kv2748_grp_ii 4 nach 6 VWGJ 2655.45 2026-01-01 6
81 kv2748_st_ii_5 l10n_at_hr_payroll_private.kv2748_grp_ii 5 nach 8 VWGJ 2809.08 2026-01-01 8
82 kv2748_st_ii_6 l10n_at_hr_payroll_private.kv2748_grp_ii 6 nach 10 VWGJ 2962.72 2026-01-01 10
83 kv2748_st_ii_7 l10n_at_hr_payroll_private.kv2748_grp_ii 7 nach 12 VWGJ 3094.32 2026-01-01 12
84 kv2748_st_ii_8 l10n_at_hr_payroll_private.kv2748_grp_ii 8 nach 15 VWGJ 3335.74 2026-01-01 15
85 kv2748_st_iii_1 l10n_at_hr_payroll_private.kv2748_grp_iii 1 im 1. u 2. VWGJ 2727.40 2026-01-01 0
86 kv2748_st_iii_2 l10n_at_hr_payroll_private.kv2748_grp_iii 2 nach 2 VWGJ 2918.34 2026-01-01 2
87 kv2748_st_iii_3 l10n_at_hr_payroll_private.kv2748_grp_iii 3 nach 4 VWGJ 3109.21 2026-01-01 4
88 kv2748_st_iii_4 l10n_at_hr_payroll_private.kv2748_grp_iii 4 nach 6 VWGJ 3300.16 2026-01-01 6
89 kv2748_st_iii_5 l10n_at_hr_payroll_private.kv2748_grp_iii 5 nach 8 VWGJ 3491.06 2026-01-01 8
90 kv2748_st_iii_6 l10n_at_hr_payroll_private.kv2748_grp_iii 6 nach 10 VWGJ 3682.00 2026-01-01 10
91 kv2748_st_iii_7 l10n_at_hr_payroll_private.kv2748_grp_iii 7 nach 12 VWGJ 3845.61 2026-01-01 12
92 kv2748_st_iii_8 l10n_at_hr_payroll_private.kv2748_grp_iii 8 nach 15 VWGJ 4145.62 2026-01-01 15
93 kv2748_st_iv_1 l10n_at_hr_payroll_private.kv2748_grp_iv 1 im 1. u 2. VWGJ 3415.01 2026-01-01 0
94 kv2748_st_iv_2 l10n_at_hr_payroll_private.kv2748_grp_iv 2 nach 2 VWGJ 3654.05 2026-01-01 2
95 kv2748_st_iv_3 l10n_at_hr_payroll_private.kv2748_grp_iv 3 nach 4 VWGJ 3893.09 2026-01-01 4
96 kv2748_st_iv_4 l10n_at_hr_payroll_private.kv2748_grp_iv 4 nach 6 VWGJ 4132.15 2026-01-01 6
97 kv2748_st_iv_5 l10n_at_hr_payroll_private.kv2748_grp_iv 5 nach 8 VWGJ 4371.19 2026-01-01 8
98 kv2748_st_iv_6 l10n_at_hr_payroll_private.kv2748_grp_iv 6 nach 10 VWGJ 4610.26 2026-01-01 10
99 kv2748_st_iv_7 l10n_at_hr_payroll_private.kv2748_grp_iv 7 nach 12 VWGJ 4815.15 2026-01-01 12
100 kv2748_st_iv_8 l10n_at_hr_payroll_private.kv2748_grp_iv 8 nach 15 VWGJ 5190.77 2026-01-01 15
101 kv2748_st_ivm_1 l10n_at_hr_payroll_private.kv2748_grp_ivm 1 im 1.-4. VWGJ 3415.01 2026-01-01 0
102 kv2748_st_ivm_2 l10n_at_hr_payroll_private.kv2748_grp_ivm 2 nach 4 VWGJ 3654.05 2026-01-01 4
103 kv2748_st_ivm_3 l10n_at_hr_payroll_private.kv2748_grp_ivm 3 nach 6 VWGJ 3893.09 2026-01-01 6
104 kv2748_st_ivm_4 l10n_at_hr_payroll_private.kv2748_grp_ivm 4 nach 8 VWGJ 4132.15 2026-01-01 8
105 kv2748_st_ivm_5 l10n_at_hr_payroll_private.kv2748_grp_ivm 5 nach 10 VWGJ 4371.19 2026-01-01 10
106 kv2748_st_ivm_6 l10n_at_hr_payroll_private.kv2748_grp_ivm 6 nach 12 VWGJ 4610.26 2026-01-01 12
107 kv2748_st_ivm_7 l10n_at_hr_payroll_private.kv2748_grp_ivm 7 nach 14 VWGJ 4815.15 2026-01-01 14
108 kv2748_st_ivm_8 l10n_at_hr_payroll_private.kv2748_grp_ivm 8 nach 17 VWGJ 5047.10 2026-01-01 17
109 kv2748_st_v_1 l10n_at_hr_payroll_private.kv2748_grp_v 1 im 1. u 2. VWGJ 4299.70 2026-01-01 0
110 kv2748_st_v_2 l10n_at_hr_payroll_private.kv2748_grp_v 2 nach 2 VWGJ 4594.04 2026-01-01 2
111 kv2748_st_v_3 l10n_at_hr_payroll_private.kv2748_grp_v 3 nach 4 VWGJ 4888.37 2026-01-01 4
112 kv2748_st_v_4 l10n_at_hr_payroll_private.kv2748_grp_v 4 nach 6 VWGJ 5182.71 2026-01-01 6
113 kv2748_st_v_5 l10n_at_hr_payroll_private.kv2748_grp_v 5 nach 8 VWGJ 5477.00 2026-01-01 8
114 kv2748_st_v_6 l10n_at_hr_payroll_private.kv2748_grp_v 6 nach 10 VWGJ 5771.34 2026-01-01 10
115 kv2748_st_v_7 l10n_at_hr_payroll_private.kv2748_grp_v 7 nach 12 VWGJ 6023.65 2026-01-01 12
116 kv2748_st_v_8 l10n_at_hr_payroll_private.kv2748_grp_v 8 nach 15 VWGJ 6486.15 2026-01-01 15
117 kv2748_st_vom_1 l10n_at_hr_payroll_private.kv2748_grp_vom 1 im 1.-6. VWGJ 4299.70 2026-01-01 0
118 kv2748_st_vom_2 l10n_at_hr_payroll_private.kv2748_grp_vom 2 nach 6 VWGJ 4594.04 2026-01-01 6
119 kv2748_st_vom_3 l10n_at_hr_payroll_private.kv2748_grp_vom 3 nach 8 VWGJ 4888.37 2026-01-01 8
120 kv2748_st_vom_4 l10n_at_hr_payroll_private.kv2748_grp_vom 4 nach 10 VWGJ 5182.71 2026-01-01 10
121 kv2748_st_vom_5 l10n_at_hr_payroll_private.kv2748_grp_vom 5 nach 12 VWGJ 5477.00 2026-01-01 12
122 kv2748_st_vi_1 l10n_at_hr_payroll_private.kv2748_grp_vi 1 im 1. u 2. VWGJ 6021.80 2026-01-01 0
123 kv2748_st_vi_2 l10n_at_hr_payroll_private.kv2748_grp_vi 2 nach 2 VWGJ 6762.62 2026-01-01 2
124 kv2748_st_vi_3 l10n_at_hr_payroll_private.kv2748_grp_vi 3 nach 5 VWGJ 7503.50 2026-01-01 5
125 kv2748_st_meister_1 l10n_at_hr_payroll_private.kv2748_grp_meister 1 im 1. u 2. VWGJ 4130.76 2026-01-01 0
126 kv2748_st_meister_2 l10n_at_hr_payroll_private.kv2748_grp_meister 2 nach 2 VWGJ 4130.76 2026-01-01 2
127 kv2748_st_meister_3 l10n_at_hr_payroll_private.kv2748_grp_meister 3 nach 4 VWGJ 4303.13 2026-01-01 4
128 kv2748_st_meister_4 l10n_at_hr_payroll_private.kv2748_grp_meister 4 nach 6 VWGJ 4475.48 2026-01-01 6
129 kv2748_st_meister_5 l10n_at_hr_payroll_private.kv2748_grp_meister 5 nach 8 VWGJ 4647.84 2026-01-01 8
130 kv2748_st_meister_6 l10n_at_hr_payroll_private.kv2748_grp_meister 6 nach 10 VWGJ 4820.17 2026-01-01 10
131 kv2748_st_meister_7 l10n_at_hr_payroll_private.kv2748_grp_meister 7 nach 12 VWGJ 4992.53 2026-01-01 12
132 kv2748_st_meister_8 l10n_at_hr_payroll_private.kv2748_grp_meister 8 nach 15 VWGJ 5270.29 2026-01-01 15
@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- AP4: EFZ-Stufen — AngG § 8 Abs 1 / EFZG § 2 Abs 1 (Gleichlauf -->
<!-- BGBl I 153/2017; EFZG RIS GesNr 10008308, AngG lokal — beide -->
<!-- verifiziert 2026-09-12): 6/8/10/12 Wochen je vollendete -->
<!-- Dienstjahre 0/1/15/25; zuzüglich je 4 Wochen halbes Entgelt. -->
<!-- AU/BK-Sonderkontingent § 8 Abs 2a AngG / § 2 Abs 5 EFZG: -->
<!-- 8 Wochen, ab 15 Dienstjahren 10 Wochen — ohne Halbsatz, je -->
<!-- Anlassfall. KV/BV-Kalenderjahr-Umstellung (§ 8 Abs 9 AngG) = -->
<!-- dokumentierte Option, hier nicht abgebildet. -->
<record id="hr_rule_parameter_at_efz_stufen" model="hr.rule.parameter">
<field name="name">EFZ-Stufen Krankenstand (AngG § 8 / EFZG § 2)</field>
<field name="code">at_efz_stufen</field>
<field name="country_id" ref="base.at"/>
</record>
<record id="hr_rule_parameter_value_at_efz_stufen"
model="hr.rule.parameter.value">
<field name="rule_parameter_id"
ref="hr_rule_parameter_at_efz_stufen"/>
<field name="date_from">2018-07-01</field>
<field name="parameter_value">{'stufen': [[0, 6], [1, 8], [15, 10], [25, 12]], 'halb_wochen': 4, 'au_wochen': 8, 'au2_wochen': 10, 'au_ab_jahre': 15}</field>
</record>
</odoo>
@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- Wr. Dienstgeberabgabe („U-Bahn-Steuer“): § 5 — 2 € je -->
<!-- Dienstnehmer:in und angefangene Woche. Quelle: RIS LrW, -->
<!-- Gesetzesnummer 20000133, geltende Fassung 12. 09. 2026. -->
<!-- Rückerstattungsgrenzen § 7 (218,02 / 2.180,19 €, +20/+10 %) -->
<!-- sind Jahreswerte der Erklärung — kein Abrechnungsparameter. -->
<record id="hr_rule_parameter_at_wien_dag_satz" model="hr.rule.parameter">
<field name="name">Wiener Dienstgeberabgabe je Woche (§ 5 Wr. DAG)</field>
<field name="code">at_wien_dag_satz</field>
<field name="country_id" ref="base.at"/>
</record>
<record id="hr_rule_parameter_value_at_wien_dag_satz"
model="hr.rule.parameter.value">
<field name="rule_parameter_id"
ref="hr_rule_parameter_at_wien_dag_satz"/>
<field name="date_from">2012-06-01</field>
<field name="parameter_value">2.0</field>
</record>
</odoo>
@@ -0,0 +1,57 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- ============================================================ -->
<!-- AP4 EFZG/Teilentgelt: Reduktion des Dienstgeber-Entgelts in -->
<!-- der halben Phase (50 %) und der ÖGK-Krankengeld-Phase (100 %) -->
<!-- — Entgeltausfallsprinzip je Arbeitstag (AngG § 8 / EFZG § 2; -->
<!-- SV/Lohnsteuer laufen über die reduzierte laufende Basis, -->
<!-- lb-krs-07). Reihenfolge nach allen laufenden Bezügen (PRAM -->
<!-- seq 40), vor B13/SZ. -->
<!-- ============================================================ -->
<record id="rule_teil_entgelt_halb" model="hr.salary.rule">
<field name="name">Teilentgelt Krankenstand (halbes Entgelt, 4 Wochen)</field>
<field name="code">TEIL_ENTGELT_HALB</field>
<field name="sequence" eval="45"/>
<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"><![CDATA[
efz = payslip._at_efz_status()
result = bool(efz and efz['halb_arbeitstage'])
]]></field>
<field name="amount_select">code</field>
<field name="l10n_at_bemessung">laufend</field>
<field name="amount_python_compute"><![CDATA[
# Halbes Entgelt für die Halb-Phase-Arbeitstage (AngG § 8 Abs 1:
# je weitere vier Wochen das halbe Entgelt): Minderung um 50 % des
# vollen Tagesentgelts (Tagesatz ohne Teilentgelt-Reduktionen).
efz = payslip._at_efz_status()
result = -payslip._at_efz_tagesatz(result_rules) * 0.5 \
* len(efz['halb_arbeitstage'])
]]></field>
</record>
<record id="rule_krankentgelt_phase" model="hr.salary.rule">
<field name="name">Krankengeld-Phase (ÖGK; kein Dienstgeber-Entgelt)</field>
<field name="code">KRANKENTGELT_PHASE</field>
<field name="sequence" eval="46"/>
<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"><![CDATA[
efz = payslip._at_efz_status()
result = bool(efz and efz['oegk_arbeitstage'])
]]></field>
<field name="amount_select">code</field>
<field name="l10n_at_bemessung">laufend</field>
<field name="amount_python_compute"><![CDATA[
# ÖGK-Krankengeld-Phase: erschöpfter EFZ-Anspruch — der Dienstgeber
# schuldet kein Entgelt (Krankengeld zahlt die ÖGK, lb-krs-02);
# die fiktive BV-Grundlage läuft separat (§ 7 Abs 3 BMSVG).
efz = payslip._at_efz_status()
result = -payslip._at_efz_tagesatz(result_rules) \
* len(efz['oegk_arbeitstage'])
]]></field>
</record>
</odoo>
@@ -250,4 +250,26 @@ result = payslip._at_kommst(result_rules)
result = payslip._at_dz(result_rules) result = payslip._at_dz(result_rules)
]]></field> ]]></field>
</record> </record>
<!-- Wiener Dienstgeberabgabe („U-Bahn-Steuer“, Wr. DAG § 5): -->
<!-- Kopfpauschale 2 € je DN/angefangene Woche, Company-Flag -->
<!-- l10n_at_wien_dag; Befreiungen/Wochenlogik s. models/wien_dag.py -->
<record id="rule_wien_dag" model="hr.salary.rule">
<field name="name">Wiener Dienstgeberabgabe (Wr. DAG)</field>
<field name="code">WIEN_DAG</field>
<field name="sequence" eval="95"/>
<field name="category_id" ref="l10n_at_hr_payroll_private.category_atp_dg"/>
<field name="struct_id" ref="l10n_at_hr_payroll_private.structure_at_privat"/>
<field name="condition_select">python</field>
<field name="condition_python"><![CDATA[
ergebnis = payslip._at_wien_dag()
result = bool(ergebnis and ergebnis['betrag'] > 0)
]]></field>
<field name="amount_select">code</field>
<field name="l10n_at_bemessung">neutral</field>
<field name="amount_python_compute"><![CDATA[
ergebnis = payslip._at_wien_dag()
result = ergebnis['betrag']
]]></field>
</record>
</odoo> </odoo>
@@ -0,0 +1,92 @@
# Rechenfälle EFZG/Teilentgelt (AP4)
Modell: `models/efzg.py` (EFZ-Phasenmaschine, fiktive BV),
Regeln `TEIL_ENTGELT_HALB`/`KRANKENTGELT_PHASE`
(`data/salary_rules_efzg_private.xml`), Parameter `at_efz_stufen`.
## Rechtsquellen (verifiziert 2026-09-12)
- **AngG § 8 Abs 1/2/2a** (lokal, RIS) und **EFZG § 2** (RIS GesNr
10008308, geltende Fassung): Stufen 6/8/10/12 Wochen je vollendeten
Dienstjahren 0 / 1 / 15 / 25, zuzüglich je 4 Wochen halbes Entgelt;
Gleichlauf seit 1. 7. 2018 (BGBl I 153/2017).
- **Wissensbasis lb-krs-08** (Arbeitsjahrprinzip, Kalendertage,
Stufensprung verlängert, Feiertage zählen nicht, AU-Sonderkontingent
8/10 Wochen ohne Halbsatz), **lb-krs-07** (SV/Lohnsteuer/BMSVG:
laufender Bezug; fiktive BV = 50 % Vormonatsentgelt ohne SZ,
§ 7 Abs 3 BMSVG; EFZ < 50 % beitragsfrei — Halbsatz 50 % ist
pflichtig), **lb-krs-02** (Krankengeld zahlt ÖGK, nicht der
Payslip).
- „Krankengeldumlage § 62 ASVG" = Falsch-Prämise
(RECHTSQUELLEN-Privat, 2026-09-10 ausgeräumt) — kein Baustein.
## Maschinenmodell
- **Kalendertage-Maschine:** Kontingentverbrauch in Kalendertagen je
Arbeitsjahr (Eintrittsdatum-Rhythmus); Krankheits- und AU-Topf
getrennt (AU nur voll, § 8 Abs 2a AngG / § 2 Abs 5 EFZG).
Stufenbewertung je Tag (Stufensprung im laufenden Stand verlängert).
- **Entgelt-Minderung je Arbeitstag** (Work Entries ATP_KRANK/ATP_AU):
Tagesatz = laufende Monatsbasis (ohne Teilentgelt-Reduktionen) ÷
Arbeitstage des Kalendermonats — Entgeltausfallsprinzip; negative
Brutto-Regeln `TEIL_ENTGELT_HALB` (50 %/AT) und
`KRANKENTGELT_PHASE` (100 %/AT) mit Bemessung `laufend`
SV/Lohnsteuer/LNK folgen automatisch über die reduzierte Basis.
- **Fiktive BV (§ 7 Abs 3 BMSVG):** während des Krankengeldbezugs
(Halb- + ÖGK-Phase) 50 % des Entgelts des Kalendermonats vor dem
Versicherungsfall (Episode = zusammenhängender Block), aliquot nach
Kalendertagen (30-Tage-Konvention), im
`_at_bv_basis_laufen`-Hook — fließt in BVG-Zeile und mBGM-BV-Basis.
## RF-EFZ-1 — Voll/Halb/ÖGK-Übergang (1. Arbeitsjahr, 6+4 Wochen)
Eintritt 1. 7. 2026 (Stufe 6 Wochen), KV-Gruppe II/1
(2.059,41 €), Krankenstand 1. 9.15. 11. 2026 (76 Kalendertage):
- Voll: Kalendertage 142 (1. 9.12. 10.) → September ohne Reduktion;
Oktober Arbeitstage 13.31. 10. (14 AT) → Halbphase.
- Halb: Kalendertage 4370 (13. 10.9. 11.); ÖGK ab 71 (10. 11.).
- November (21 AT): halb 6 AT (2.6., 9. 11.), ÖGK 4 AT
(10.13. 11.); Tagesatz 2.059,41 ÷ 21 = 98,07 € →
`TEIL_ENTGELT_HALB` = 294,21 €, `KRANKENTGELT_PHASE` = 392,28 €.
## RF-EFZ-2 — Kontingent-Zusammenrechnung
Wiederholte Verhinderung im selben Arbeitsjahr verbraucht dasselbe
Kalendertage-Kontingent (§ 8 Abs 2 AngG): 1.9.20.9. (20 KT) +
1.10.20.10. (20 KT) + 25.10.15.11. → KT 4142 voll, ab KT 43 halb
— der dritte Krankenstand startet in der Halbphase.
## RF-EFZ-3 — Stufe 8 Wochen (2.15. Jahr)
Eintritt 1. 7. 2024 (1 vollendetes Jahr): Stufe 8 Wochen — erst
Kalendertag 57 fällt aus der Vollphase.
## RF-EFZ-4 — AU-Sonderkontingent
Nach erschöpftem Krankheits-Kontingent gebührt für Arbeitsunfall/
Berufskrankheit (eigene Urlaubsart, WET `ATP_AU`) das
Sonderkontingent 8 Wochen (ab 15 Dienstjahren 10) vollständig —
unabhängig vom Krankheits-Topf, ohne Halbsatz.
## RF-EFZ-5 — Fiktive BV in der Krankengeldphase
November 2026 (RF-EFZ-1): Halb- + ÖGK-Kalendertage 15 → fiktive BG
= 50 % × Vormonatsbasis (Oktoberslip, 2.059,41 €) × 15/30 = 514,85 €;
BV-Basis = reduziertes laufendes Entgelt (1.372,93 €) + 514,85 €;
BVG = 1,53 % × 1.887,78 € = 28,92 €.
## Offene Punkte / dokumentierte Reste
- **Kalenderjahr-Umstellung** (§ 8 Abs 9 AngG / § 2 Abs 8 EFZG per
KV/BV): Merkmal + Sonderrechnung Umstellungsjahr — Option.
- **ÜStd-/Provisionsdurchschnitte** (13 Wochen / 12 Monate) bei
wechselnden Entgelten — vorbereitete Bezugsbasis folgt mit der
Zeit-Zuschlags-Engine.
- **SZ-Kürzung** bei nicht vollem EFZ (9 ObA 209/00a, „kann"-Regel)
— KV-abhängig, Option.
- **Meldung/Nachweis, Urlaubsunterbrechung § 5 UrlG, Beendigung
§ 5 EFZG** (EFZ über Ende hinaus): prozessuale Regeln —
Fristen-/Hinweismodell mit AP8 (Arbeits- und Entgeltbestätigung).
- Erstfenster-Teilmonate mit Krankengeld-Phase: fiktive BG nur in
beitragspflichtigen Läufen (Randfall, dokumentiert).
@@ -10,3 +10,6 @@ from . import account_posting_private
from . import mbgm from . import mbgm
from . import versichertenmeldung from . import versichertenmeldung
from . import fristen_private from . import fristen_private
from . import buchungsbeleg
from . import wien_dag
from . import efzg
@@ -213,13 +213,18 @@ class HrPayslip(models.Model):
return 0.0 return 0.0
basis = self._l10n_at_sv_basis_roh(result_rules) basis = self._l10n_at_sv_basis_roh(result_rules)
if pflichtig_ab is None or pflichtig_ab <= self.date_from: if pflichtig_ab is None or pflichtig_ab <= self.date_from:
return basis # AP4: fiktive BG während des Krankengeldbezugs (§ 7 Abs 3
# BMSVG) — 50 % des Vormonatsentgelts aliquot nach
# Kalendertagen der Halb-/ÖGK-Phase; nur in beitrags-
# pflichtigen Läufen (Erstfenster-Teilmonate: dokumentierter
# Randfall).
return basis + self._at_bv_fiktiv_krankengeld()
if pflichtig_ab > self.date_to: if pflichtig_ab > self.date_to:
# LZP liegt zur Gänze im beitragsfreien Erstfenster. # LZP liegt zur Gänze im beitragsfreien Erstfenster.
return 0.0 return 0.0
ende = min(self.date_to, tage = (min(self.date_to,
self.version_id.contract_date_end or self.date_to) self.version_id.contract_date_end or self.date_to)
tage = (ende - pflichtig_ab).days + 1 - pflichtig_ab).days + 1
if tage <= 0: if tage <= 0:
return 0.0 return 0.0
return basis * tage / max(self._l10n_at_lohnsteuertage(), 1) return basis * tage / max(self._l10n_at_lohnsteuertage(), 1)
@@ -103,6 +103,10 @@ L10N_AT_BUCHUNGSMATRIX = {
'credit': ('3550',)}, 'credit': ('3550',)},
'rule_dz': {'debit': ('6662',), 'debit_arbeiter': ('6602',), 'rule_dz': {'debit': ('6662',), 'debit_arbeiter': ('6602',),
'credit': ('3542',)}, 'credit': ('3542',)},
# Wr. DAG („U-Bahn-Steuer“): Aufwand 6663 (l10n_at-Template,
# KB lb-lnk-04 verifiziert); Credit 3560 Referenzbuchhaltungs-
# Rest (Stadtkasse Wien — Kandidat, fehlend-Warnung des Hooks).
'rule_wien_dag': {'debit': ('6663',), 'credit': ('3560',)},
} }
@@ -0,0 +1,32 @@
# Part of the odoo-at-payroll project. License: LGPL-3.
"""Buchungsbeleg der Lohnbuchhaltung (AP2d): Druck der FI-Buchung
eines Lohnlaufs — die account.move je Lohnlauf (bzw. je Payslip, wenn
nicht gebatcht bucht wird) mit Konto, Belegtext, Soll/Haben und
Summen; Kontrollgrößen stimmen mit dem Monatsjournal überein
(GP6-Matrix, KFS/BW 6)."""
from odoo import models
class L10nAtPayrollBuchungsbeleg(models.AbstractModel):
_name = 'report.l10n_at_hr_payroll_private.report_buchungsbeleg'
_description = 'Buchungsbeleg Lohnlauf (AP2d)'
def _get_report_values(self, docids, data=None):
runs = self.env['hr.payslip.run'].browse(docids)
laeufe = []
for run in runs:
# Batch-Buchung hängt am Lauf, sonst je Payslip-Move
# (hr_payroll_account schreibt move_id je Slip bzw. je Run).
moves = (run.move_id + run.slip_ids.mapped('move_id')) \
.filtered(lambda m: m.state != 'cancel')
zeilen = moves.line_ids.filtered(
lambda l: l.debit or l.credit).sorted(
key=lambda l: (l.account_id.code or '', l.id))
laeufe.append({
'run': run,
'moves': moves,
'zeilen': zeilen,
'summe_soll': sum(zeilen.mapped('debit')),
'summe_haben': sum(zeilen.mapped('credit')),
})
return {'docs': runs, 'laeufe': laeufe}
@@ -0,0 +1,270 @@
# Part of the odoo-at-payroll project. License: LGPL-3.
"""EFZG/Teilentgelt (AP4) — Entgeltfortzahlung im Krankenstand.
Rechtsgrundlagen (verifiziert 2026-09-12):
- **AngG § 8 Abs 1/2/2a** und **EFZG § 2 Abs 1/3, § 2 Abs 5**
(Gleichlauf seit 1. 7. 2018, BGBl I 153/2017; EFZG RIS GesNr
10008308, AngG lokal — Stufen 6/8/10/12 Wochen je vollendeten
Dienstjahren <1 / 1-14 / 15-24 / ≥25, zuzüglich je 4 Wochen halbes
Entgelt; wiederholte Verhinderungen im selben Arbeitsjahr
zusammengerechnet; AU/BK-Sonderkontingent 8/10 Wochen ohne
Halbsatz, je Anlassfall, unabhängig vom Krankheits-Topf).
- **lb-krs-08/02/07** (Wissensbasis, Layer 2): Arbeitsjahrprinzip,
Kalendertage als Zähleinheit, Stufensprung im laufenden Stand
verlängert; SV: volle EFZ beitragspflichtig, EFZ < 50 %
beitragsfrei (hier: Halbsatz = 50 % → pflichtig); Lohnsteuer:
laufender Bezug; **BMSVG § 7 Abs 3**: während des Krankengeldbezugs
(Halb- und ÖGK-Phase) fiktive BG = 50 % des Entgelts des
Kalendermonats vor dem Versicherungsfall, SZ ausgenommen.
- Krankengeld selbst zahlt die ÖGK (läuft nicht durch den
Dienstgeber-Payslip; lb-krs-02) — der Payroll-Anteil ist die
Entgelt-Reduktion des Dienstgebers, die fiktive BV-Grundlage und
später die Arbeits-/Entgeltbestätigung (AP8).
- „Krankengeldumlage § 62 ASVG" ist eine Falsch-Prämise
(RECHTSQUELLEN-Privat 2026-09-10) — kein Umlage-Baustein.
Zähleinheiten: Kontingente in **Kalendertagen** (lb-krs-08: Praxis),
bezogen aus den validierten Abwesenheiten (hr.leave der AT-Urlaubsarten
Krankenstand/Arbeitsunfall); die **Entgelt-Minderung** je
Arbeitstag aus dem Arbeitseintragssatz (Tagesatz = laufende
Monatsbasis ÷ Arbeitstage des Monats — Entgeltausfallsprinzip).
Feiertage im Krankenstand werden vom Work-Entry-Modell automatisch
nicht als Krankenstandstage erzeugt (Kalender) — die 100 %-
Feiertagsentgelt-Regel lb-krs-08 bleibt damit gewahrt.
"""
from datetime import date, timedelta
from odoo import fields, models
# WET-Codes der Abwesenheits-Töpfe (AP5-Seed).
_KRANK_WET = 'ATP_KRANK'
_AU_WET = 'ATP_AU'
class HrPayslip(models.Model):
_inherit = 'hr.payslip'
l10n_at_efz_info = fields.Char(
string='AT EFZ-Status (Krankenstand)', compute='_compute_efz_info',
help='Phasenstatus des Krankenstands im Abrechnungszeitraum: '
'volle/halbe Entgeltfortzahlung, ÖGK-Krankengeld-Phase, '
'Kontingente und Stufe (AngG § 8 / EFZG § 2).')
def _compute_efz_info(self):
for slip in self:
efz = slip._at_efz_status()
if not efz:
slip.l10n_at_efz_info = ''
continue
slip.l10n_at_efz_info = (
'Stufe %d (%d+%d Wo) · voll %d AT · halb %d AT · '
'ÖGK %d AT' % (
efz['stufe_wochen'], efz['stufe_wochen'],
efz['halb_wochen'], len(efz['voll_arbeitstage']),
len(efz['halb_arbeitstage']),
len(efz['oegk_arbeitstage'])))
# ------------------------------------------------------------------
# EFZ-Phasenbestimmung
# ------------------------------------------------------------------
def _at_efz_leaves(self):
"""Validierte Krankenstand-/AU-Abwesenheiten des AN als
(start, ende, ist_au)-Kalenderspannen."""
self.ensure_one()
typen = self.env['hr.leave.type'].search([
('work_entry_type_id.code', 'in',
(_KRANK_WET, _AU_WET))])
leaves = self.env['hr.leave'].search([
('employee_id', '=', self.employee_id.id),
('holiday_status_id', 'in', typen.ids),
('state', 'in', ('validate', 'validate1')),
])
return [
(leave.request_date_from, leave.request_date_to,
leave.holiday_status_id.work_entry_type_id.code == _AU_WET)
for leave in leaves
if leave.request_date_from and leave.request_date_to]
def _at_efz_arbeitsjahr_start(self):
"""Beginn des Arbeitsjahres, das die Abrechnungsperiode
überwiegend enthält (Eintrittsdatum-Rhythmus, lb-krs-08)."""
self.ensure_one()
beginn = self.version_id.contract_date_start
if not beginn:
return None
jahre = beginn.year
while True:
aj_start = date(jahre, beginn.month, beginn.day) \
if not (beginn.month == 2 and beginn.day == 29) \
else date(jahre, 3, 1)
aj_naechstes = date(jahre + 1, aj_start.month, aj_start.day)
if aj_start <= self.date_from < aj_naechstes:
return aj_start
jahre += 1
if jahre > beginn.year + 80:
return None
def _at_efz_status(self):
"""EFZ-Phasen je Abrechnungsperiode: {'stufe_wochen',
'halb_wochen', 'voll_arbeitstage', 'halb_arbeitstage',
'oegk_arbeitstage', 'oegk_kalendertage', 'episode_beginn',
'info'} — None ohne Krankenstand/AU im Arbeitsjahr.
Kalendertage-Maschine: Die Krankheits- und AU-Tage des
Arbeitsjahres werden in Kalenderreihenfolge den Phasen
zugeteilt (voll bis Stufen-Kontingent, halb zuzüglich
4 Wochen, danach ÖGK); AU ausschließlich voll (kein
Halbsatz, § 8 Abs 2a AngG / § 2 Abs 5 EFZG). Ein
Stufensprung im laufenden Stand verlängert das Kontingent
(Stufe je Tag neu bewertet, lb-krs-08)."""
self.ensure_one()
version = self.version_id
aj_start = self._at_efz_arbeitsjahr_start()
if not aj_start:
return None
endtag = max(self.date_to, aj_start)
krank_tage, au_tage = {}, {}
episode_start = None
for von, bis, ist_au in self._at_efz_leaves():
tag = von
while tag <= bis:
if aj_start <= tag <= endtag:
topf = au_tage if ist_au else krank_tage
topf[tag] = True
tag += timedelta(days=1)
if not krank_tage and not au_tage:
return None
params = self.env['hr.rule.parameter']._get_parameter_from_code(
'at_efz_stufen', self.date_from, raise_if_not_found=False) or {}
stufen = sorted(
(int(jahre), int(wochen))
for jahre, wochen in params.get('stufen', []))
halb_wochen = int(params.get('halb_wochen', 4))
au_wochen = int(params.get('au_wochen', 8))
au2_wochen = int(params.get('au2_wochen', 10))
au_ab_jahre = int(params.get('au_ab_jahre', 15))
def stufe_fuer(dienstjahre, tag_ist_au):
"""Anspruchs-Wochen am Stichtag (vollendete Jahre)."""
wochen = stufen[0][1] if stufen else 6
for jahre, stufe in stufen:
if dienstjahre >= jahre:
wochen = stufe
if tag_ist_au:
wochen = au2_wochen if dienstjahre >= au_ab_jahre \
else au_wochen
return wochen
beginn = version.contract_date_start
phasen = {}
verbraucht_krank_voll = verbraucht_krank_halb = 0
verbraucht_au = 0
for tag in sorted(set(krank_tage) | set(au_tage)):
dienstjahre = (tag.year - beginn.year
- ((tag.month, tag.day) < (beginn.month,
beginn.day)))
if tag in au_tage:
if verbraucht_au < stufe_fuer(dienstjahre, True) * 7:
phasen[tag] = 'voll'
else:
phasen[tag] = 'oegk'
verbraucht_au += 1
continue
wochen = stufe_fuer(dienstjahre, False)
if verbraucht_krank_voll < wochen * 7:
phasen[tag] = 'voll'
verbraucht_krank_voll += 1
elif verbraucht_krank_halb < halb_wochen * 7:
phasen[tag] = 'halb'
verbraucht_krank_halb += 1
else:
phasen[tag] = 'oegk'
# Arbeitstage je Phase im Abrechnungszeitraum (Work Entries)
eintraege = self.env['hr.work.entry'].search([
('employee_id', '=', self.employee_id.id),
('date', '>=', self.date_from),
('date', '<=', self.date_to),
('state', '!=', 'cancelled'),
('work_entry_type_id.code', 'in', (_KRANK_WET, _AU_WET)),
])
voll, halb, oegk = [], [], []
for eintrag in eintraege:
phase = phasen.get(eintrag.date)
if phase == 'voll':
voll.append(eintrag.date)
elif phase == 'halb':
halb.append(eintrag.date)
elif phase == 'oegk':
oegk.append(eintrag.date)
# Episode-Beginn für die fiktive BV (§ 7 Abs 3 BMSVG):
# frühester zusammenhängender Verhinderungsblock (Lücke = neue
# Episode), dessen Phase die Halb-/ÖGK-Phase erreicht.
episode_beginn = None
for tag in sorted(phasen):
if phasen[tag] in ('halb', 'oegk'):
block = tag
while (block - timedelta(days=1)) in phasen:
block = block - timedelta(days=1)
episode_beginn = block
break
dienstjahre = (self.date_from.year - beginn.year
- ((self.date_from.month, self.date_from.day)
< (beginn.month, beginn.day)))
return {
'stufe_wochen': stufe_fuer(dienstjahre, False),
'halb_wochen': halb_wochen,
'voll_arbeitstage': sorted(set(voll)),
'halb_arbeitstage': sorted(set(halb)),
'oegk_arbeitstage': sorted(set(oegk)),
# Kalendertage der Phasen IM ABRECHNUNGSZEITRAUM — Basis
# der fiktiven-BV-Aliquote (monatsscharf).
'oegk_kalendertage': sorted(
tag for tag in phasen
if self.date_from <= tag <= self.date_to
and phasen[tag] == 'oegk'),
'halb_kalendertage': sorted(
tag for tag in phasen
if self.date_from <= tag <= self.date_to
and phasen[tag] == 'halb'),
'episode_beginn': episode_beginn,
}
def _at_efz_tagesatz(self, result_rules=None):
"""Voller Tages-Entgeltsatz des Monats (Entgeltausfalls-
prinzip): laufende Monatsbasis ohne die eigenen
Teilentgelt-Reduktionen ÷ Arbeitstage des Kalendermonats."""
self.ensure_one()
basis = self._l10n_at_sv_basis_roh(result_rules)
for code in ('TEIL_ENTGELT_HALB', 'KRANKENTGELT_PHASE'):
if result_rules is not None and code in result_rules \
and result_rules[code]['total']:
basis -= result_rules[code]['total']
arbeitstage = self._at_arbeitstage(self.date_from, self.date_to)
return basis / max(arbeitstage, 1)
def _at_bv_fiktiv_krankengeld(self):
"""Fiktive BV-Grundlage während des Krankengeldbezugs
(§ 7 Abs 3 BMSVG, lb-krs-07): 50 % des Entgelts des
Kalendermonats vor dem Versicherungsfall (SZ ausgenommen),
aliquot nach Kalendertagen des Bezugs (Halb- + ÖGK-Phase,
30-Tage-Konvention). Bezug = letzter validierter Lauf vor
Episode-Beginn; ohne solchen: der laufende Monat (kurze
Dienstverhältnisse, lb-krs-02)."""
self.ensure_one()
efz = self._at_efz_status()
if not efz or not (efz['halb_kalendertage']
or efz['oegk_kalendertage']):
return 0.0
tage = len(efz['halb_kalendertage']) \
+ len(efz['oegk_kalendertage'])
vormonat = None
if efz['episode_beginn']:
vormonat = self.env['hr.payslip'].search([
('employee_id', '=', self.employee_id.id),
('state', 'in', ('validated', 'paid')),
('date_to', '<', efz['episode_beginn']),
], order='date_from desc', limit=1)
basis_monat = vormonat._l10n_at_sv_basis_roh() if vormonat \
else self._l10n_at_sv_basis_roh()
return 0.5 * basis_monat * tage / 30.0
@@ -3,11 +3,20 @@
hr.employee — Muster ``hr_version_bgld.py`` des GemBG-Moduls. hr.employee — Muster ``hr_version_bgld.py`` des GemBG-Moduls.
Die Stufenlogik der Referenz-KVs läuft über Verwendungsgruppenjahre Die Stufenlogik der Referenz-KVs läuft über Verwendungsgruppenjahre
(VWGJ, Biennal-/Triennalsprünge, § 17 der Rahmen-KVs); die Vorrückung (VWGJ, Biennal-/Triennalsprünge, § 17 der Rahmen-KVs — verifiziert
selbst ist GP2-Thema (Wizard analog GemBG), hier werden die SI-2203: Abs 3 Einreihung, Abs 4 Anzahl der angerechneten VWGJ durch
Vertragsfelder und der Katalog-Lookup bereitgestellt. die Firmenleitung mit Dienstzettel-Mitteilung, Abs 5 Vorrückung am
Ersten des Erreichungsmonats). AP9 ergänzt die Vordienstzeiten:
Die Firma erkennt Vordienstzeiten in Monaten an (Dienstzettel),
daraus leitet der Einstufungs-Vorschlag die Erfahrungsstufe ab
(``vwgj_ab`` der Stufenzeilen) und den nächsten Vorrückungstermin.
Freigabe bewusst manuell (action_einstufung_uebernehmen).
""" """
from odoo import fields, models from datetime import date
from dateutil.relativedelta import relativedelta
from odoo import api, fields, models
class HrVersion(models.Model): class HrVersion(models.Model):
@@ -31,6 +40,35 @@ class HrVersion(models.Model):
help="Individuell vereinbartes Monatsgehalt; überschreibt das " help="Individuell vereinbartes Monatsgehalt; überschreibt das "
"Kataloggehalt der Gruppe/Stufe (Günstigkeitsvergleich, " "Kataloggehalt der Gruppe/Stufe (Günstigkeitsvergleich, "
"z. B. § 18 Anrechnung der Rahmen-KVs).") "z. B. § 18 Anrechnung der Rahmen-KVs).")
l10n_at_vordienstzeit_monate = fields.Integer(
string='AT Angerechnete Vordienstzeiten (Monate)',
groups="hr_payroll.group_hr_payroll_user", tracking=True,
help="Von der Firmenleitung anerkannte Vordienstzeiten in "
"Monaten (§ 17 Abs 4 Rahmen-KV: Anzahl der angerechneten "
"Verwendungsgruppenjahre — Dienstzettel-Mitteilungspflicht). "
"Fließen in den Einstufungs-Vorschlag und den nächsten "
"Vorrückungstermin ein. AP9.")
l10n_at_vwgj_monate = fields.Integer(
string='AT VWGJ gesamt (Monate)',
compute='_compute_l10n_at_vwgj',
groups="hr_payroll.group_hr_payroll_user",
help="Verwendungsgruppenjahre-Stand heute: Dienstzeit-Monate "
"seit Vertragsbeginn zuzüglich angerechnener "
"Vordienstzeiten.")
l10n_at_vwgj_stufe_vorschlag = fields.Integer(
string='AT Erfahrungsstufen-Vorschlag (VWGJ)',
compute='_compute_l10n_at_vwgj',
groups="hr_payroll.group_hr_payroll_user",
help="Vorgeschlagene Erfahrungsstufe aus den vwgj_ab-Schwellen "
"der Gruppen-Tabelle (nach N VWGJ); 0 = Schwellen "
"nicht gepflegt → manuelle Einstufung maßgeblich.")
l10n_at_naechste_vorrueckung = fields.Date(
string='AT Nächste VWGJ-Vorrückung',
compute='_compute_l10n_at_vwgj',
groups="hr_payroll.group_hr_payroll_user",
help="§ 17 Abs 5 Rahmen-KV: Erster des Monats, in dem die "
"nächste VWGJ-Schwelle erreicht wird (Vertragsbeginn + "
"Schwelle Vordienstzeiten).")
currency_id = fields.Many2one( currency_id = fields.Many2one(
'res.currency', related='company_id.currency_id') 'res.currency', related='company_id.currency_id')
l10n_at_personalgruppe = fields.Selection( l10n_at_personalgruppe = fields.Selection(
@@ -50,10 +88,73 @@ class HrVersion(models.Model):
self.ensure_one() self.ensure_one()
return self.kv_gruppe_id._get_gehalt(self.erfahrungsstufe, datum) return self.kv_gruppe_id._get_gehalt(self.erfahrungsstufe, datum)
# ------------------------------------------------------------------
# AP9: Vordienstzeiten → VWGJ-Einstufungsvorschlag (§ 17 Rahmen-KV)
# ------------------------------------------------------------------
@api.depends('kv_gruppe_id', 'contract_date_start',
'l10n_at_vordienstzeit_monate', 'erfahrungsstufe')
def _compute_l10n_at_vwgj(self):
"""VWGJ-Stand heute (Dienstzeit + Vordienstzeiten), Stufen-
Vorschlag aus den vwgj_ab-Schwellen der Gruppe und der nächste
Vorrückungstermin (Erster des Erreichungsmonats, § 17 Abs 5)."""
heute = fields.Date.context_today(self)
for version in self:
version.l10n_at_vwgj_monate = 0
version.l10n_at_vwgj_stufe_vorschlag = 0
version.l10n_at_naechste_vorrueckung = False
if not (version.kv_gruppe_id
and version.contract_date_start):
continue
delta = relativedelta(heute, version.contract_date_start)
dienstzeit_monate = delta.years * 12 + delta.months
version.l10n_at_vwgj_monate = max(
dienstzeit_monate
+ (version.l10n_at_vordienstzeit_monate or 0), 0)
jahre = version.l10n_at_vwgj_monate // 12
version.l10n_at_vwgj_stufe_vorschlag = \
version.kv_gruppe_id._get_stufe_fuer_vwgj(jahre, heute)
naechste = version.kv_gruppe_id._get_naechste_vwgj(
jahre, heute)
if naechste:
erreichung = version.contract_date_start \
+ relativedelta(
months=naechste * 12
- (version.l10n_at_vordienstzeit_monate or 0))
# § 17 Abs 5: Erster des Monats, in dem die erhöhte
# Anzahl der VWGJ erreicht wird.
version.l10n_at_naechste_vorrueckung = erreichung \
.replace(day=1)
def action_einstufung_uebernehmen(self):
"""Vorschlag als Erfahrungsstufe übernehmen — bewusst ein
expliziter Verwaltungsakt (§ 17 Abs 4: Firmenleitung, mit
Dienstzettel bekanntzugeben); kein automatisches Schreiben."""
for version in self:
if version.l10n_at_vwgj_stufe_vorschlag:
version.erfahrungsstufe = \
version.l10n_at_vwgj_stufe_vorschlag
class HrEmployee(models.Model): class HrEmployee(models.Model):
_inherit = 'hr.employee' _inherit = 'hr.employee'
l10n_at_vordienstzeit_monate = fields.Integer(
related="version_id.l10n_at_vordienstzeit_monate", inherited=True,
readonly=False, groups="hr_payroll.group_hr_payroll_user")
l10n_at_vwgj_monate = fields.Integer(
related="version_id.l10n_at_vwgj_monate", inherited=True,
groups="hr_payroll.group_hr_payroll_user")
l10n_at_vwgj_stufe_vorschlag = fields.Integer(
related="version_id.l10n_at_vwgj_stufe_vorschlag", inherited=True,
groups="hr_payroll.group_hr_payroll_user")
l10n_at_naechste_vorrueckung = fields.Date(
related="version_id.l10n_at_naechste_vorrueckung", inherited=True,
groups="hr_payroll.group_hr_payroll_user")
def action_einstufung_uebernehmen(self):
"""Delegation an die Version (Button am Mitarbeiterformular)."""
self.mapped('version_id').action_einstufung_uebernehmen()
kv_id = fields.Many2one( kv_id = fields.Many2one(
related="version_id.kv_id", inherited=True, related="version_id.kv_id", inherited=True,
readonly=False, groups="hr_payroll.group_hr_payroll_user") readonly=False, groups="hr_payroll.group_hr_payroll_user")
@@ -176,6 +176,42 @@ class L10nAtPayrollKvGruppe(models.Model):
('gueltig_ab', '<=', datum)], ('gueltig_ab', '<=', datum)],
order='gueltig_ab desc', limit=1) order='gueltig_ab desc', limit=1)
def _get_stufe_fuer_vwgj(self, vwgj_jahre, datum=None):
"""Erfahrungsstufe für vollendete Verwendungsgruppenjahre
(VWGJ) zum Stichtag — AP9. Basis: vwgj_ab der Stufenzeilen
(„nach N VWGJ"; 0 = ab Beginn). Gibt die höchste Stufe mit
vwgj_ab <= vwgj_jahre zurück; 0, wenn die Gruppe keine
gepflegten Schwellen hat (manuelle Einstufung maßgeblich).
Rahmen-KV § 17 Abs 3-5 (Biennal-/Triennalsprünge, Vorrückung
am Ersten des Erreichungsmonats)."""
self.ensure_one()
datum = datum or fields.Date.context_today(self)
# -1 = Schwelle nicht gepflegt (ORM liest Integer-NULL als 0 —
# deshalb Konvention -1 statt NULL; Domain > -1 filtert beides).
zeilen = self.env['l10n.at.payroll.kv.gruppe.stufe'].search(
[('gruppe_id', '=', self.id),
('gueltig_ab', '<=', datum),
('vwgj_ab', '>', -1)])
beste = zeilen.filtered(lambda z: z.vwgj_ab <= vwgj_jahre)
if not beste:
return 0
return max(beste.mapped('stufe'))
def _get_naechste_vwgj(self, vwgj_jahre, datum=None):
"""Nächste VWGJ-Schwelle oberhalb der erreichten Jahre (für
den Vorrückungstermin, § 17 Abs 5) — None an der Höchststufe."""
self.ensure_one()
datum = datum or fields.Date.context_today(self)
zeilen = self.env['l10n.at.payroll.kv.gruppe.stufe'].search(
[('gruppe_id', '=', self.id),
('gueltig_ab', '<=', datum),
('vwgj_ab', '>', -1)])
schwellen = sorted(set(zeilen.mapped('vwgj_ab')))
for schwelle in schwellen:
if schwelle > vwgj_jahre:
return schwelle
return None
def _max_stufe(self, datum=None): def _max_stufe(self, datum=None):
"""Höchste Stufe mit gültigem Monatsgehalt zum Stichtag.""" """Höchste Stufe mit gültigem Monatsgehalt zum Stichtag."""
self.ensure_one() self.ensure_one()
@@ -202,6 +238,13 @@ class L10nAtPayrollKvGruppeStufe(models.Model):
bezeichnung = fields.Char( bezeichnung = fields.Char(
help="Stufen-Label der KV-Tabelle, z. B. 'im 1. u 2. VWGJ' " help="Stufen-Label der KV-Tabelle, z. B. 'im 1. u 2. VWGJ' "
"oder 'nach 15 VWGJ'.") "oder 'nach 15 VWGJ'.")
vwgj_ab = fields.Integer(
string='Ab (vollendete) VWGJ', index=True, default=-1,
help="Vollendete Verwendungsgruppenjahre, ab denen die Stufe "
"gebührt — aus dem Stufen-Label (§ 17 Abs 3-5 "
"Rahmen-KV: Biennal-/Triennalsprünge). 0 = ab Beginn; "
"-1 = Schwelle nicht gepflegt → kein Einstufungs-"
"Vorschlag (manuelle Einstufung maßgeblich). AP9.")
gehalt = fields.Monetary(currency_field='currency_id') gehalt = fields.Monetary(currency_field='currency_id')
currency_id = fields.Many2one( currency_id = fields.Many2one(
'res.currency', default=lambda self: self.env.company.currency_id) 'res.currency', default=lambda self: self.env.company.currency_id)
+101 -27
View File
@@ -9,11 +9,21 @@ Hierarchie: Paket (SART ``PS`` Selbstabrechner, Länge 305) → mBGM
Fachliche Grundlagen: Fachliche Grundlagen:
- **KEIN DN/DG- oder Zweig-Split**: die Standardposition ``T01`` - **KEIN DN/DG- oder Zweig-Split**: die Standardposition ``T01``
trägt den GESAMT-Prozentsatz des Tarifsystems (Parameter trägt den GESAMT-Prozentsatz des Tarifsystems. Quelle ist der
``at_mbgm_tarif`` — BSGR-spezifisch; Werte aus dem ÖGK- TASY-Katalog (AP0, Variante b): Beschäftigtengruppe + Ergänzungen
Beitragstarifsystem, `.additional_docs/oegk/`, jeweils (ERGB am Vertrag) + Dienstgebergruppe (Company) → Tarifgruppe →
parametrisiert zu pflegen). Fehlende BSGR-Sätze brechen den Lauf Tarifset; fehlt der Katalog, greift der Hand-Parameter
``at_mbgm_tarif`` als Fallback (Werte aus dem ÖGK-Handbuch,
`.additional_docs/oegk/`). Fehlende Sätze brechen den Lauf
listenweise ab (keine Erfindung von Tarifen). listenweise ab (keine Erfindung von Tarifen).
- **BSGR** (DM-ORG D.48/TASY): B001 Arbeiter, B002 Angestellte,
B045 Arbeiterlehrlinge, B044 Angestelltenlehrlinge, B010 gering-
fällige Arbeiter, B030 geringfügige Angestellte — TASY-Bezeichner
verifiziert (B044 = „Ang. Lg.“, B045 = „Arb. Lg.“).
- **ERGB** (D.49): Ergänzungen am Vertrag (z. B. E01 Nachtschwer-
arbeit, E02 Schlechtwetterentschädigung) werden im Tarifblock
belegt und steuern die Tarifgruppen-Auflösung im Katalog; das
Company-Flag ``l10n_at_nschab`` bleibt Fallback ohne Katalog.
- **Abschläge als eigene Verrechnungspositionen** (D.60, verifiziert): - **Abschläge als eigene Verrechnungspositionen** (D.60, verifiziert):
A01/A02/A03 = Minderung AV auf 2/1/0 % (§ 2a AMPFG, DN-seitig — A01/A02/A03 = Minderung AV auf 2/1/0 % (§ 2a AMPFG, DN-seitig —
Abschlag = AB × (2,95 Zielsatz) %), A09 = UV-Entfall ab 60 Abschlag = AB × (2,95 Zielsatz) %), A09 = UV-Entfall ab 60
@@ -171,27 +181,54 @@ class L10nAtPayrollMbgm(models.Model):
('state', 'in', ('validated', 'paid'))], limit=1) ('state', 'in', ('validated', 'paid'))], limit=1)
def _l10n_at_bsgr(self, version, slip): def _l10n_at_bsgr(self, version, slip):
"""Beschäftigtengruppe (SV-Tarifsystem): B001 Arbeiter, B002 """Beschäftigtengruppe (SV-Tarifsystem, TASY-Bezeichner):
Angestellte; Lehrlinge B044/B045 und geringfügig/fallweise B001 Arbeiter, B002 Angestellte; Lehrlinge B045
(B010/B030) ohne Default-Parameter → Lauf bricht ab, bis (Arbeiterlehrlinge) / B044 (Angestelltenlehrlinge);
`at_mbgm_tarif` gepflegt ist (Tarifsystem-Werte, keine geringfügig/fallweise B010 (Arbeiter) / B030 (Angestellte).
Erfindung).""" Ohne Default-Parameter → Lauf bricht ab, bis der Katalog
importiert oder ``at_mbgm_tarif`` gepflegt ist (Tarifsystem-
Werte, keine Erfindung)."""
personalgruppe = getattr(
version, 'l10n_at_personalgruppe', '')
if version.l10n_at_lehrling: if version.l10n_at_lehrling:
return 'B044' if getattr( return 'B045' if personalgruppe == 'arbeiter' else 'B044'
version, 'l10n_at_personalgruppe', '') == 'arbeiter' \
else 'B045'
if slip.l10n_at_geringfuegig or version.l10n_at_fallweise: if slip.l10n_at_geringfuegig or version.l10n_at_fallweise:
return 'B030' return 'B010' if personalgruppe == 'arbeiter' else 'B030'
return 'B001' if getattr( return 'B001' if personalgruppe == 'arbeiter' else 'B002'
version, 'l10n_at_personalgruppe', '') == 'arbeiter' \
else 'B002'
def _l10n_at_tarif(self, bsgr): def _l10n_at_tarif(self, bsgr, version=None):
"""T01/T02-Gesamtsätze je BSGR — Katalog vor Hand-Parameter.
1. TASY-Katalog: Tarifgruppe aus BSGR + ERGB am Vertrag +
Dienstgebergruppe der Company am Stichtag; Sätze = Summe
der Tarifset-Komponenten je Verrechnungsbasis (AB/SZ).
2. Fallback: ``at_mbgm_tarif``-Parameter (BSGR-Schlüssel).
Rückgabe ``{'laufend', 'sz', 'quelle'}`` oder None → Lauf
bricht listenweise ab.
"""
self.ensure_one() self.ensure_one()
ergaenzungen = version.l10n_at_ergaenzung_ids.mapped('code') \
if version is not None else []
dgg = self.company_id.l10n_at_dienstgebergruppe or 'NBALG'
tarifgruppe = self.env[
'l10n.at.payroll.tasy.tarifgruppe']._l10n_at_find(
bsgr, ergaenzungen, dgg, self.beitragsmonat)
if tarifgruppe:
ab = tarifgruppe._l10n_at_saetze('AB', self.beitragsmonat)
sz = tarifgruppe._l10n_at_saetze('SZ', self.beitragsmonat)
if ab is None or sz is None:
return None
return {'laufend': ab['gesamt'], 'sz': sz['gesamt'],
'quelle': 'katalog'}
tarif = self.env['hr.rule.parameter']._get_parameter_from_code( tarif = self.env['hr.rule.parameter']._get_parameter_from_code(
'at_mbgm_tarif', self.beitragsmonat, 'at_mbgm_tarif', self.beitragsmonat,
raise_if_not_found=False) or {} raise_if_not_found=False) or {}
return tarif.get(bsgr) eintrag = tarif.get(bsgr)
if eintrag:
return {'laufend': eintrag['laufend'], 'sz': eintrag['sz'],
'quelle': 'parameter'}
return None
def _l10n_at_positionen(self, slip, tarif): def _l10n_at_positionen(self, slip, tarif):
"""Verrechnungspositionen je Tarifblock: T01 (Gesamtsatz, """Verrechnungspositionen je Tarifblock: T01 (Gesamtsatz,
@@ -202,7 +239,11 @@ class L10nAtPayrollMbgm(models.Model):
version = slip.version_id version = slip.version_id
ab = sum(basis for _m, basis ab = sum(basis for _m, basis
in slip._l10n_at_sv_basis_laufen_monate()) in slip._l10n_at_sv_basis_laufen_monate())
# NSchAB über das Company-Flag nur im Parameter-Fallback —
# im Katalog-Pfad steckt der NB-Zweig über die ERGB-E01-
# Tarifgruppe (Dienstgebergruppe NBNSA) bereits im T01.
nschab = 3.80 if (self.company_id.l10n_at_nschab nschab = 3.80 if (self.company_id.l10n_at_nschab
and tarif.get('quelle') != 'katalog'
and not version.l10n_at_lehrling and not version.l10n_at_lehrling
and getattr(version, 'l10n_at_personalgruppe', and getattr(version, 'l10n_at_personalgruppe',
'') == 'arbeiter') else 0.0 '') == 'arbeiter') else 0.0
@@ -329,12 +370,12 @@ class L10nAtPayrollMbgm(models.Model):
anzahl += 1 anzahl += 1
continue continue
bsgr = deklaration._l10n_at_bsgr(version, slip) bsgr = deklaration._l10n_at_bsgr(version, slip)
tarif = deklaration._l10n_at_tarif(bsgr) tarif = deklaration._l10n_at_tarif(bsgr, version)
if not tarif: if not tarif:
fehler.append( fehler.append(
'Mitarbeiter %s: kein Tarifsatz für BSGR ' 'Mitarbeiter %s: kein Tarifsatz für BSGR '
'%s parametrisiert (at_mbgm_tarif, Werte ' '%s (weder TASY-Katalog noch Parameter '
'aus dem SV-Tarifsystem pflegen).' 'at_mbgm_tarif am Beitragsmonat).'
% (employee.name, bsgr)) % (employee.name, bsgr))
continue continue
if slip.l10n_at_geringfuegig \ if slip.l10n_at_geringfuegig \
@@ -442,6 +483,12 @@ class L10nAtPayrollMbgm(models.Model):
bezeichnung='Tarifblock %s' % employee.name) bezeichnung='Tarifblock %s' % employee.name)
tarifblock.setze('SART', 'T1') tarifblock.setze('SART', 'T1')
tarifblock.setze('BSGR', bsgr) tarifblock.setze('BSGR', bsgr)
# ERGB (DM-ORG D.49): bis zu 5 Ergänzungen à 3 Stellen —
# Grundstellung blank, wenn keine gepflegt (Katalog-
# Normalfall ohne Ergänzung).
ergaenzungen = version.l10n_at_ergaenzung_ids.mapped('code')[:5]
if ergaenzungen:
tarifblock.setze('ERGB', ''.join(ergaenzungen))
tarifblock.setze('VVON', '1') tarifblock.setze('VVON', '1')
saetze = [mbgm_satz, tarifblock] saetze = [mbgm_satz, tarifblock]
for code, satz_prozent, rsum, basis_typ in positionen: for code, satz_prozent, rsum, basis_typ in positionen:
@@ -479,12 +526,39 @@ class ResCompany(models.Model):
_inherit = 'res.company' _inherit = 'res.company'
l10n_at_nschab = fields.Boolean( l10n_at_nschab = fields.Boolean(
string='AT Schlechtwetterentschädigungsbeitrag-Branche', string='AT Nachtschwerarbeitsbeitrags-Betrieb (mBGM-Fallback)',
help='Betrieb mit Schlechtwetterentschädigung (ÖGK-' help='Betrieb mit Nachtschwerarbeit (ÖGK-Tarifsystem '
'Tarifsystem „Normalbetriebe mit ' '„Normalbetriebe mit Nachtschwerarbeit“, Dienstgebergruppe '
'Schlechtwetterentschädigung“): Arbeiter (BSGR B001) ' 'NBNSA): ohne TASY-Katalog erhalten Arbeiter (BSGR B001) '
'erhalten im mBGM-T01 den um NSchAB 3,80 % erhöhten ' 'im mBGM-T01 den um NSchAB 3,80 % erhöhten Gesamtsatz. '
'Gesamtsatz.') 'Mit Katalog stattdessen die Ergänzung E01 am Vertrag '
'setzen — der NB-Zweig ist dann Teil der E01-Tarifgruppe.')
l10n_at_dienstgebergruppe = fields.Char(
string='AT Dienstgebergruppe (SV-Tarifsystem)',
default='NBALG',
help='Code der Dienstgebergruppe des TASY-Tarifsystems, z. B. '
'NBALG „Normalbetriebe — Allgemeiner Teil“, NBNSA '
'(Nachtschwerarbeit), NBBAU (Schlechtwetter), HAUSH '
'(Haushalte), DGONO (ohne Niederlassung). Bestimmt mit '
'BSGR + Ergänzungen die Tarifgruppe der mBGM-Tarifierung '
'(AP0). Vollständige Liste: Tarifsystem-Katalog-Menü '
'nach dem TASY-Import.')
class HrVersion(models.Model):
_inherit = 'hr.version'
l10n_at_ergaenzung_ids = fields.Many2many(
'l10n.at.payroll.tasy.ergaenzung',
'l10n_at_hr_payroll_version_ergaenzung_rel',
'version_id', 'ergaenzung_id',
string='AT Ergänzungen (ERGB, SV-Tarifsystem)',
groups="hr_payroll.group_hr_payroll_user",
help='Ergänzungen zur Beschäftigtengruppe (DM-ORG D.49) für die '
'mBGM-Tarifierung — gemeinsam mit BSGR und Dienstgebergruppe '
'bilden sie die Tarifgruppe. Z. B. E01 Nachtschwerarbeit, '
'E02 Schlechtwetterentschädigung. Katalog nach dem TASY-'
'Import; ohne Eintrag gilt der Normalfall ohne Ergänzung.')
class L10nAtPayrollMbgmLine(models.Model): class L10nAtPayrollMbgmLine(models.Model):
@@ -0,0 +1,113 @@
# Part of the odoo-at-payroll project. License: LGPL-3.
"""Wiener Dienstgeberabgabe („U-Bahn-Steuer") — Wr. DAG, RIS LrW
Gesetzesnummer 20000133, geltende Fassung 12. 09. 2026 (verifiziert,
Quelle .firecrawl/ris/wien/WDGAG_*.html; § 9: die Einzahlungen fließen
der Stadt Wien zu und sind zur Errichtung einer Untergrundbahn zu
verwenden):
- **§ 5:** 2 € je Dienstnehmer:in und je angefangene Woche eines
bestehenden Dienstverhältnisses; **§ 2:** Beschäftigungsort Wien
(feste Arbeitsstätte, sonst Wohnsitz des Dienstnehmers; Bau-
ausführungen > 6 Monate).
- **§ 3 Befreiungen:** Gebietskörperschaften; Überschreiten des
55. Lebensjahres; Behinderte/Opferfürsorge/BEinstG; Lehrverhältnisse
(BAG); ≤ 10 Wochenstunden; Hausbesorger; gesetzliche Mutter-
schaftsverbote und Karenzurlaub auf gesetzlichem Anspruch;
Präsenzdienst/Zivildienst.
- **§ 6:** Selbstberechnung — Vormonatsschuld bis 15. des Folgemonats
an die Stadtkasse; Jahreserklärung bis 31. 3. inklusive befreiter
Dienstverhältnisse (Fristenkalender-Arten wien_dag/wien_dag_jahres).
Wochenlogik (Praxis-Annahme, im Parallellauf gegen die Magistrats-
Praxis zu verifizieren): „angefangene Woche" = Kalenderwoche, deren
Montag im Abrechnungsmonat liegt; abgabepflichtig, wenn das Dienst-
verhältnis an mindestens einem Tag der Woche besteht. Karenz-Wochen
(validierte ATP_KARENZ-Work Entries) sind befreit. Nicht abgebildet
(bis AP 7): Mutterschaftsverbote, Präsenzdienst/Zivildienst, Haus-
besorger, Behinderten-Status — manuelle Prüfung, Rest dokumentiert.
"""
from datetime import timedelta
from dateutil.relativedelta import relativedelta
from odoo import models
class HrPayslip(models.Model):
_inherit = 'hr.payslip'
def _at_wien_dag(self):
"""Wr. DAG je Beitragsmonat: {'wochen', 'satz', 'betrag',
'info'} — None, wenn die Company nicht erfasst ist; 0-Wochen
bei Befreiung. Kopfpauschale, nicht lohnabhängig (§ 5)."""
self.ensure_one()
if not self.company_id.l10n_at_wien_dag:
return None
version = self.version_id
employee = self.employee_id
monat_start = self.date_from
monat_ende = self.date_to
info = []
# Befreiungen § 3 (monatsweise Bewertung)
if version.l10n_at_lehrling:
return {'wochen': 0, 'satz': 0.0, 'betrag': 0.0,
'info': 'Befreit: Lehrverhältnis (§ 3 lit d).'}
if employee.birthday:
vollendet55 = employee.birthday + relativedelta(years=55)
if vollendet55 <= monat_ende:
return {'wochen': 0, 'satz': 0.0, 'betrag': 0.0,
'info': 'Befreit: 55. Lebensjahr überschritten '
'(§ 3 lit b, %s).' % vollendet55}
kalender = version.resource_calendar_id
wochenstunden = sum(
(att.hour_to or 0.0) - (att.hour_from or 0.0)
for att in kalender.attendance_ids) \
if kalender else 0.0
if wochenstunden <= 10.0:
return {'wochen': 0, 'satz': 0.0, 'betrag': 0.0,
'info': 'Befreit: ≤ 10 Wochenstunden (§ 3 lit e).'}
# Dienstverhältnis-Bestand im Monat (§ 1)
von = max(monat_start,
version.contract_date_start or monat_start)
bis = min(monat_ende,
version.contract_date_end or monat_ende)
if von > bis:
return {'wochen': 0, 'satz': 0.0, 'betrag': 0.0,
'info': 'Kein bestehendes Dienstverhältnis im '
'Abrechnungsmonat.'}
# Karenz-Tage des Monats (§ 3 lit g — Rest via WET ATP_KARENZ)
karenz_tage = set()
if self.env['hr.work.entry']._fields.get('leave_id'):
eintraege = self.env['hr.work.entry'].search([
('employee_id', '=', employee.id),
('date', '>=', monat_start),
('date', '<=', monat_ende),
('state', '!=', 'cancelled'),
('work_entry_type_id.code', '=', 'ATP_KARENZ'),
])
karenz_tage = {e.date for e in eintraege}
# Wochen mit Montag im Monat; abgabepflichtig, wenn das
# Dienstverhältnis an mindestens einem Tag der Woche besteht
# (Schnittmenge [DV] ∩ [Woche] nicht leer).
wochen = 0
tag = monat_start + timedelta(
days=(7 - monat_start.weekday()) % 7)
while tag <= monat_ende:
woche_ende = tag + timedelta(days=6)
if von <= woche_ende and bis >= tag:
karenz_in_woche = any(
tag + timedelta(days=i) in karenz_tage
for i in range((woche_ende - tag).days + 1)
if tag + timedelta(days=i) <= monat_ende)
if karenz_in_woche:
info.append('Karenz-Woche ab %s befreit (§ 3 lit g)'
% tag)
else:
wochen += 1
tag += timedelta(days=7)
satz = float(self.env['hr.rule.parameter']
._get_parameter_from_code(
'at_wien_dag_satz', monat_start,
raise_if_not_found=False) or 0.0)
return {'wochen': wochen, 'satz': satz,
'betrag': wochen * satz, 'info': '; '.join(info)}
@@ -0,0 +1,69 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- ============================================================ -->
<!-- AP2d: Buchungsbeleg der Lohnbuchhaltung am Lohnlauf. -->
<!-- ============================================================ -->
<record id="action_report_buchungsbeleg" model="ir.actions.report">
<field name="name">Buchungsbeleg (Lohnbuchhaltung)</field>
<field name="model">hr.payslip.run</field>
<field name="report_type">qweb-pdf</field>
<field name="report_name">l10n_at_hr_payroll_private.report_buchungsbeleg</field>
<field name="report_file">l10n_at_hr_payroll_private.report_buchungsbeleg</field>
<field name="paperformat_id" ref="hr_payroll.paperformat_euro_light"/>
<field name="binding_model_id" ref="hr_payroll.model_hr_payslip_run"/>
<field name="binding_type">report</field>
</record>
<template id="report_buchungsbeleg">
<t t-call="web.html_container">
<t t-foreach="laeufe" t-as="l">
<t t-call="web.external_layout">
<div class="page">
<h2>Buchungsbeleg — <t t-out="l['run'].name"/></h2>
<p class="text-muted">
<t t-foreach="l['moves']" t-as="m">
Beleg <t t-out="m.name"/> (Journal <t t-out="m.journal_id.name"/>,
Datum <t t-out="m.date"/>,
Status <t t-out="m.state"/>);
</t>
</p>
<table class="table table-sm">
<thead>
<tr>
<th>Konto</th>
<th>Kontobezeichnung</th>
<th>Belegtext</th>
<th class="text-end">Soll</th>
<th class="text-end">Haben</th>
</tr>
</thead>
<tbody>
<tr t-foreach="l['zeilen']" t-as="z">
<td><t t-out="z.account_id.code"/></td>
<td><t t-out="z.account_id.name"/></td>
<td><t t-out="z.name"/></td>
<td class="text-end">
<span t-if="z.debit" t-out="z.debit"
t-options='{"widget": "monetary", "display_currency": l["run"].company_id.currency_id}'/>
</td>
<td class="text-end">
<span t-if="z.credit" t-out="z.credit"
t-options='{"widget": "monetary", "display_currency": l["run"].company_id.currency_id}'/>
</td>
</tr>
<tr>
<td colspan="3"><strong>Summe</strong></td>
<td class="text-end"><strong><span t-out="l['summe_soll']"
t-options='{"widget": "monetary", "display_currency": l["run"].company_id.currency_id}'/></strong></td>
<td class="text-end"><strong><span t-out="l['summe_haben']"
t-options='{"widget": "monetary", "display_currency": l["run"].company_id.currency_id}'/></strong></td>
</tr>
</tbody>
</table>
</div>
</t>
</t>
</t>
</template>
</odoo>
@@ -6,3 +6,7 @@ from . import test_sv_private
from . import test_bv_private from . import test_bv_private
from . import test_account_private from . import test_account_private
from . import test_meldewesen_private from . import test_meldewesen_private
from . import test_reports_private
from . import test_wien_dag_private
from . import test_einstufung_private
from . import test_efzg_private
@@ -52,10 +52,10 @@ class AccountPrivateTest(TransactionCase):
(RF-BUCH-4: Fallback statt Erfindung).""" (RF-BUCH-4: Fallback statt Erfindung)."""
for code in ('6000', '6200', '6225', '6230', '6240', '6310', for code in ('6000', '6200', '6225', '6230', '6240', '6310',
'6440', '6500', '6560', '6600', '6601', '6602', '6440', '6500', '6560', '6600', '6601', '6602',
'6660', '6661', '6662'): '6660', '6661', '6662', '6663'):
if code not in ohne: if code not in ohne:
self._konto(code, 'expense') self._konto(code, 'expense')
for code in ('3540', '3541', '3542', '3550', '3600'): for code in ('3540', '3541', '3542', '3550', '3560', '3600'):
self._konto(code, 'liability_current', reconcile=True) self._konto(code, 'liability_current', reconcile=True)
if '3850' not in ohne: if '3850' not in ohne:
self._konto('3850', 'liability_payable', reconcile=True) self._konto('3850', 'liability_payable', reconcile=True)
@@ -277,3 +277,32 @@ class AccountPrivateTest(TransactionCase):
'SVDG_IESG', 'SVDG_SZ_KV', 'SVDG_SZ_PV', 'SVDG_SZ_AV', 'SVDG_IESG', 'SVDG_SZ_KV', 'SVDG_SZ_PV', 'SVDG_SZ_AV',
'SVDG_SZ_UV', 'SVDG_SZ_IESG')) 'SVDG_SZ_UV', 'SVDG_SZ_IESG'))
self.assertAlmostEqual(self._saldo(lines, '6500'), dg, places=2) self.assertAlmostEqual(self._saldo(lines, '6500'), dg, places=2)
# ------------------------------------------------------------------
# AP2d: Buchungsbeleg-Report am Lohnlauf
# ------------------------------------------------------------------
def test_05_buchungsbeleg_report(self):
"""Buchungsbeleg am Lohnlauf: Konto/Belegtext/Soll/Haben der
FI-Buchung, Summen balanciert (Soll = Haben)."""
self._kontenplan()
self._hook()
employee = self._employee()
slip = self._slip(employee, 11, confirm=True)
run = self.env['hr.payslip.run'].create({
'name': 'Beleg-Lauf 11/2026',
'date_start': date(2026, 11, 1),
'date_end': date(2026, 11, 30),
})
slip.payslip_run_id = run.id
werte = self.env[
'report.l10n_at_hr_payroll_private.report_buchungsbeleg'
]._get_report_values(run.ids)
self.assertEqual(len(werte['laeufe']), 1)
lauf = werte['laeufe'][0]
self.assertTrue(lauf['zeilen'])
self.assertAlmostEqual(lauf['summe_soll'], lauf['summe_haben'],
places=2)
konten = {z.account_id.code for z in lauf['zeilen']}
self.assertIn('6200', konten) # Gehälter (Debit)
self.assertIn('3850', konten) # NET-Verbindlichkeit
self.assertIn('3600', konten) # SV-Verrechnung
@@ -0,0 +1,232 @@
# Part of the odoo-at-payroll project. License: LGPL-3.
"""AP4: EFZG/Teilentgelt — Phasenmaschine (Voll/Halb/ÖGK),
Kontingent-Zusammenrechnung je Arbeitsjahr, Stufen, AU-Sonderkontingent,
fiktive BV (§ 7 Abs 3 BMSVG). Rechenfälle: docs/rechenfaelle/efzg.md."""
import datetime
from odoo import SUPERUSER_ID
from odoo.tests import TransactionCase, tagged
@tagged('post_install', '-at_install')
class EfzgPrivateTest(TransactionCase):
def setUp(self):
super().setUp()
self.company = self.env.company
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 EFZG 40h',
'tz': 'Europe/Vienna',
'hours_per_day': 8,
'attendance_ids': [
(0, 0, {'name': 'Tag %d' % wd, 'dayofweek': str(wd),
'hour_from': 8.0, 'hour_to': 16.0})
for wd in range(5)
],
})
self.company.resource_calendar_id = self.calendar
self.krankenstand_typ = self.env.ref(
'l10n_at_hr_payroll_private.leave_type_at_krankenstand')
self.au_typ = self.env.ref(
'l10n_at_hr_payroll_private.leave_type_at_arbeitsunfall')
def _employee(self, eintritt):
employee = self.env['hr.employee'].create({
'name': 'EFZG Test'})
version = self.env['hr.version'].search(
[('employee_id', '=', employee.id)], limit=1)
version.write({
'contract_date_start': eintritt,
'kv_id': self.kv2203.id,
'kv_gruppe_id': self.gruppe_ii.id,
'erfahrungsstufe': 1,
'resource_calendar_id': self.calendar.id,
})
version.generate_work_entries(
datetime.date(2026, 9, 1), datetime.date(2026, 12, 31))
return employee
def _krankenstand(self, employee, von, bis, typ=None):
leave = self.env['hr.leave'].create({
'name': 'Krank %s' % von,
'employee_id': employee.id,
'holiday_status_id': (typ or self.krankenstand_typ).id,
'request_date_from': von,
'request_date_to': bis,
})
leave.with_user(SUPERUSER_ID)._action_validate()
return leave
def _slip(self, employee, monat):
erster = datetime.date(2026, monat, 1)
letzter = (erster.replace(day=28)
+ datetime.timedelta(days=4)).replace(day=1) \
- datetime.timedelta(days=1)
slip = self.env['hr.payslip'].create({
'name': 'EFZG %d/2026' % monat,
'employee_id': employee.id,
'struct_id': self.structure.id,
'date_from': erster,
'date_to': letzter,
})
slip.compute_sheet()
slip.action_payslip_done()
return slip
def _total(self, slip, code):
return sum(slip.line_ids.filtered(
lambda l: l.code == code).mapped('total'))
def test_01_voll_halb_oegk_uebergang(self):
"""RF-EFZ-1: 1. Arbeitsjahr 6+4 Wochen — September voll,
Oktober Halbphase ab 13. 10., November halb (6 AT) + ÖGK
(4 AT) mit Reduktionslinien."""
employee = self._employee(datetime.date(2026, 7, 1))
self._krankenstand(
employee, datetime.date(2026, 9, 1),
datetime.date(2026, 11, 15))
slip_sep = self._slip(employee, 9)
slip_nov = self._slip(employee, 11)
efz = slip_nov._at_efz_status()
self.assertEqual(efz['stufe_wochen'], 6)
self.assertEqual(efz['halb_wochen'], 4)
self.assertEqual(efz['halb_arbeitstage'], [
datetime.date(2026, 11, 2), datetime.date(2026, 11, 3),
datetime.date(2026, 11, 4), datetime.date(2026, 11, 5),
datetime.date(2026, 11, 6), datetime.date(2026, 11, 9)])
self.assertEqual(efz['oegk_arbeitstage'], [
datetime.date(2026, 11, 10), datetime.date(2026, 11, 11),
datetime.date(2026, 11, 12), datetime.date(2026, 11, 13)])
# Tagesatz 2.059,41 / 21 AT = 98,07 € (die Regeln rechnen
# mit der unreduzierten laufenden Basis — Ergebnis der
# Regelzeilen prüfen)
tagesatz = 2059.41 / 21
self.assertAlmostEqual(self._total(slip_nov, 'TEIL_ENTGELT_HALB'),
-tagesatz * 0.5 * 6, places=2)
self.assertAlmostEqual(
self._total(slip_nov, 'KRANKENTGELT_PHASE'),
-tagesatz * 4, places=2)
# September: zur Gänze Vollphase — keine Reduktion.
efz_sep = slip_sep._at_efz_status()
self.assertEqual(len(efz_sep['voll_arbeitstage']), 22)
self.assertFalse(efz_sep['halb_arbeitstage'])
self.assertFalse(efz_sep['oegk_arbeitstage'])
self.assertFalse(self._total(slip_sep, 'TEIL_ENTGELT_HALB'))
self.assertFalse(self._total(slip_sep, 'KRANKENTGELT_PHASE'))
def test_02_kontingent_wiederholung(self):
"""RF-EFZ-2: Drei Krankenstände im selben Arbeitsjahr zählen
auf dasselbe Kalendertage-Kontingent (42 KT) — der dritte
startet in der Halbphase."""
employee = self._employee(datetime.date(2026, 7, 1))
self._krankenstand(
employee, datetime.date(2026, 9, 1),
datetime.date(2026, 9, 20)) # KT 120
self._krankenstand(
employee, datetime.date(2026, 10, 1),
datetime.date(2026, 10, 20)) # KT 2140
self._krankenstand(
employee, datetime.date(2026, 10, 25),
datetime.date(2026, 11, 15)) # KT 41 ff.
slip_nov = self._slip(employee, 11)
efz = slip_nov._at_efz_status()
# 25./26. 10. (So/Mo) sind KT 41/42 (voll), ab 27. 10. halb
self.assertIn(datetime.date(2026, 11, 2), efz['halb_arbeitstage'])
self.assertFalse(datetime.date(2026, 11, 2)
in efz['oegk_arbeitstage'])
def test_03_stufe_acht_wochen(self):
"""RF-EFZ-3: Ab 1 vollendeten Dienstjahr 8 Wochen — die
Halbphase beginnt erst bei Kalendertag 57."""
employee = self._employee(datetime.date(2024, 7, 1))
self._krankenstand(
employee, datetime.date(2026, 9, 1),
datetime.date(2026, 11, 30))
slip_nov = self._slip(employee, 11)
efz = slip_nov._at_efz_status()
self.assertEqual(efz['stufe_wochen'], 8)
# Voll bis 26. 10. (KT 56), halb ab 27. 10., ÖGK ab 24. 11.
self.assertIn(datetime.date(2026, 11, 2), efz['halb_arbeitstage'])
self.assertIn(datetime.date(2026, 11, 24), efz['oegk_arbeitstage'])
def test_04_au_sonderkontingent(self):
"""RF-EFZ-4: Erschöpfter Krankheits-Topf — Arbeitsunfall
gebührt trotzdem voll (eigener Topf, kein Halbsatz)."""
employee = self._employee(datetime.date(2026, 7, 1))
self._krankenstand(
employee, datetime.date(2026, 9, 1),
datetime.date(2026, 11, 15)) # Topf erschöpft
self._krankenstand(
employee, datetime.date(2026, 11, 16),
datetime.date(2026, 11, 30), typ=self.au_typ)
slip_nov = self._slip(employee, 11)
efz = slip_nov._at_efz_status()
# Krank: 1.6. 11. halb, ab 10. 11. ÖGK; AU-Tage 16.30. 11.
# bleiben VOLL (Sonderkontingent).
for tag in (16, 17, 18, 19, 20):
self.assertIn(datetime.date(2026, 11, tag),
efz['voll_arbeitstage'])
self.assertFalse(datetime.date(2026, 11, 16)
in efz['oegk_arbeitstage'])
def test_05_fiktive_bv_krankengeldphase(self):
"""RF-EFZ-5: Fiktive BG in der Krankengeldphase — 50 % des
Vormonatsentgelts × Halb-/ÖGK-Kalendertage/30 (§ 7 Abs 3
BMSVG); BV-Basis = reduziertes Entgelt + fiktiver Anteil."""
employee = self._employee(datetime.date(2026, 7, 1))
self._slip(employee, 8) # Vormonats-Bezug
self._krankenstand(
employee, datetime.date(2026, 9, 1),
datetime.date(2026, 11, 15))
self._slip(employee, 10)
slip_nov = self._slip(employee, 11)
efz = slip_nov._at_efz_status()
# Episode-Beginn 1. 9. — Vormonat = August (voll entlohnt,
# unverkürzt), nicht der teilvergütete Oktober.
self.assertEqual(efz['episode_beginn'],
datetime.date(2026, 9, 1))
halb_kt = len(efz['halb_kalendertage'])
oegk_kt = len(efz['oegk_kalendertage'])
self.assertEqual(halb_kt, 9) # 1.9. 11.
self.assertEqual(oegk_kt, 6) # 10.15. 11.
# Fiktive BG = 50 % August-Entgelt × 15/30 Kalendertage.
fiktiv = slip_nov._at_bv_fiktiv_krankengeld()
vormonat = slip_nov.env['hr.payslip'].search([
('employee_id', '=', slip_nov.employee_id.id),
('state', 'in', ('validated', 'paid')),
('date_to', '<', efz['episode_beginn']),
], order='date_from desc', limit=1)
self.assertEqual(vormonat.date_from, datetime.date(2026, 8, 1))
self.assertAlmostEqual(fiktiv, 0.5 * vormonat._l10n_at_sv_basis_roh()
* 15 / 30.0, places=2)
# BV-Grundlage = reduziertes laufendes Entgelt + fiktiver
# Anteil (komponentenweise — der November-Lauf trägt
# zusätzlich eine B13-SZ-Position in der sonstigen Grundlage,
# § 6 BMSVG: 1,53 % inkl. SZ).
laufend = slip_nov._l10n_at_sv_basis_roh()
self.assertAlmostEqual(slip_nov._at_bv_basis_laufen(),
laufend + fiktiv, places=2)
self.assertAlmostEqual(self._total(slip_nov, 'BVG'),
(slip_nov._at_bv_basis_laufen()
+ slip_nov._at_bv_basis_sonst()) * 0.0153,
places=2)
# Ohne Krankengeld-Phase wäre die Grundlage um den fiktiven
# Anteil kleiner.
self.assertAlmostEqual(self._total(slip_nov, 'TEIL_ENTGELT_HALB'),
-(2059.41 / 21) * 0.5 * 6, places=2)
def test_06_ohne_krankenstand_keine_wirkung(self):
"""Ohne Abwesenheit: keine Phasen, keine fiktive BG, keine
Teilentgelt-Zeilen."""
employee = self._employee(datetime.date(2026, 7, 1))
slip = self._slip(employee, 11)
self.assertFalse(slip._at_efz_status())
self.assertEqual(slip._at_bv_fiktiv_krankengeld(), 0.0)
self.assertFalse(self._total(slip, 'TEIL_ENTGELT_HALB'))
self.assertFalse(self._total(slip, 'KRANKENTGELT_PHASE'))
@@ -0,0 +1,138 @@
# Part of the odoo-at-payroll project. License: LGPL-3.
"""AP9: Vordienstzeiten → Einstufung. Privat: VWGJ-Ermittlung nach
§ 17 Rahmen-KV (Schwellen aus den Stufen-Labels, Vorrückung am
Ersten des Erreichungsmonats), Vorschlag mit manueller Freigabe."""
import datetime
from freezegun import freeze_time
from odoo.tests import TransactionCase, tagged
@tagged('post_install', '-at_install')
class EinstufungPrivateTest(TransactionCase):
def setUp(self):
super().setUp()
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')
employee = self.env['hr.employee'].create({
'name': 'Einstufung Test'})
self.version = self.env['hr.version'].search(
[('employee_id', '=', employee.id)], limit=1)
self.employee = employee
def test_01_vwgj_schwellen(self):
"""vwgj_ab-Schwellen aus den Stufen-Labels: 0/2/4/…/15 →
Stufenzuordnung je vollendeten VWGJ (Höchststufe gedeckelt)."""
self.assertEqual(
self.gruppe_ii._get_stufe_fuer_vwgj(0,
datetime.date(2026, 7, 1)),
1)
self.assertEqual(
self.gruppe_ii._get_stufe_fuer_vwgj(1,
datetime.date(2026, 7, 1)),
1)
self.assertEqual(
self.gruppe_ii._get_stufe_fuer_vwgj(2,
datetime.date(2026, 7, 1)),
2)
self.assertEqual(
self.gruppe_ii._get_stufe_fuer_vwgj(4,
datetime.date(2026, 7, 1)),
3)
self.assertEqual(
self.gruppe_ii._get_stufe_fuer_vwgj(8,
datetime.date(2026, 7, 1)),
5)
self.assertEqual(
self.gruppe_ii._get_stufe_fuer_vwgj(15,
datetime.date(2026, 7, 1)),
8)
# Höchststufe bleibt auch nach weiteren Jahren
self.assertEqual(
self.gruppe_ii._get_stufe_fuer_vwgj(20,
datetime.date(2026, 7, 1)),
8)
# Nächste Schwelle über den erreichten Jahren (§ 17 Abs 5)
self.assertEqual(
self.gruppe_ii._get_naechste_vwgj(2,
datetime.date(2026, 7, 1)),
4)
self.assertFalse(self.gruppe_ii._get_naechste_vwgj(
15, datetime.date(2026, 7, 1)))
# Schwellen erst ab Katalog-Gültigkeit
self.assertEqual(
self.gruppe_ii._get_stufe_fuer_vwgj(8,
datetime.date(2025, 12, 31)),
0)
def test_02_vwgj_vorschlag_und_uebernahme(self):
"""Dienstzeit + Vordienstzeiten → Stufenvorschlag; Übernahme
schreibt die Erfahrungsstufe (manueller Verwaltungsakt)."""
with freeze_time('2026-09-12'):
self.version.write({
'contract_date_start': datetime.date(2020, 1, 1),
'kv_id': self.kv2203.id,
'kv_gruppe_id': self.gruppe_ii.id,
'erfahrungsstufe': 1,
'l10n_at_vordienstzeit_monate': 24,
})
# Dienstzeit 80 Monate (2020-01 bis 2026-09) + 24 = 104
# VWGJ-Monate → 8 vollendete Jahre → Stufe 5 (nach 8 VWGJ).
self.assertEqual(self.version.l10n_at_vwgj_monate, 104)
self.assertEqual(self.version.l10n_at_vwgj_stufe_vorschlag, 5)
# Nächste Schwelle 10 VWGJ: erreicht im Monat
# 2020-01 + (120 24) Monate = 2028-01-01 (§ 17 Abs 5).
self.assertEqual(self.version.l10n_at_naechste_vorrueckung,
datetime.date(2028, 1, 1))
self.assertEqual(self.version.erfahrungsstufe, 1)
self.version.action_einstufung_uebernehmen()
self.assertEqual(self.version.erfahrungsstufe, 5)
def test_03_ohne_vordienstzeiten(self):
"""Ohne Vordienstzeiten zählt allein die Dienstzeit; ohne
Gruppen-Schwellen gibt es keinen Vorschlag."""
with freeze_time('2026-09-12'):
self.version.write({
'contract_date_start': datetime.date(2026, 7, 1),
'kv_id': self.kv2203.id,
'kv_gruppe_id': self.gruppe_ii.id,
'l10n_at_vordienstzeit_monate': 0,
})
self.assertEqual(self.version.l10n_at_vwgj_monate, 2)
self.assertEqual(self.version.l10n_at_vwgj_stufe_vorschlag, 1)
# Nächste Schwelle 2 VWGJ: erreicht 2026-07 + 24 Monate
# → 1. 7. 2028 (§ 17 Abs 5).
self.assertEqual(self.version.l10n_at_naechste_vorrueckung,
datetime.date(2028, 7, 1))
# Ohne Gruppe: kein Vorschlag
self.version.kv_gruppe_id = False
self.assertEqual(self.version.l10n_at_vwgj_stufe_vorschlag, 0)
self.assertFalse(self.version.l10n_at_naechste_vorrueckung)
def test_04_vwgj_ohne_schwellen_kein_vorschlag(self):
"""Gruppe ohne gepflegte vwgj_ab (Import ohne Labels) →
Vorschlag 0, manuelle Einstufung bleibt maßgeblich."""
gruppe = self.env['l10n.at.payroll.kv.gruppe'].create({
'kv_id': self.kv2203.id,
'code': 'TEST-NS',
'name': 'Test ohne Schwellen',
'lohn_basis': 'monatlich',
})
self.env['l10n.at.payroll.kv.gruppe.stufe'].create({
'gruppe_id': gruppe.id,
'stufe': 1,
'bezeichnung': 'Erste Stufe',
'gehalt': 2000.0,
'gueltig_ab': datetime.date(2026, 1, 1),
})
with freeze_time('2026-09-12'):
self.version.write({
'contract_date_start': datetime.date(2020, 1, 1),
'kv_gruppe_id': gruppe.id,
'l10n_at_vordienstzeit_monate': 60,
})
self.assertEqual(self.version.l10n_at_vwgj_stufe_vorschlag, 0)
@@ -1,7 +1,8 @@
# Part of the odoo-at-payroll project. License: LGPL-3. # Part of the odoo-at-payroll project. License: LGPL-3.
"""GP7 privat: mBGM (Paket/mBGM/Tarifblock/Basis/Position-Hierarchie """GP7 privat: mBGM (Paket/mBGM/Tarifblock/Basis/Position-Hierarchie
aus GP4-Beitragssummen, T01-Gesamtsatz aus at_mbgm_tarif, Abschläge aus GP4-Beitragssummen, T01-Gesamtsatz aus TASY-Katalog vor
A01/A02/A03 §-2a-AMPFG, A09/A10/A15, BV-Basis V01) und at_mbgm_tarif, Abschläge A01/A02/A03 §-2a-AMPFG, A09/A10/A15,
BV-Basis V01, BSGR-Lehrlinge/Geringfügige korrigiert, ERGB) und
Versichertenmeldung M3/M4 (deklarative Event-Erkennung, Versichertenmeldung M3/M4 (deklarative Event-Erkennung,
Meldungs-Registry, AGRD).""" Meldungs-Registry, AGRD)."""
import base64 import base64
@@ -10,6 +11,10 @@ import datetime
from odoo.exceptions import UserError from odoo.exceptions import UserError
from odoo.tests import TransactionCase, tagged from odoo.tests import TransactionCase, tagged
from odoo.addons.l10n_at_hr_payroll.tests.test_tasy_katalog import (
TASY_KATALOG_FIXTURE,
)
@tagged('post_install', '-at_install') @tagged('post_install', '-at_install')
class MeldewesenPrivateTest(TransactionCase): class MeldewesenPrivateTest(TransactionCase):
@@ -165,10 +170,16 @@ class MeldewesenPrivateTest(TransactionCase):
}) })
with self.assertRaises(UserError): with self.assertRaises(UserError):
zweiter.action_erzeugen() zweiter.action_erzeugen()
# Ursache OHNE AGRD-Mapping → M4-Fallback 00 (D.22-Standard).
# (Suche-beliebig wäre nach dem AGRD-Seed nicht mehr deterministisch
# — der Seed verbindet die üblichen Ursachen vor.)
grund_ohne = self.env['hr.departure.reason'].create({
'name': 'Testursache ohne AGRD',
'country_id': self.env.ref('base.at').id,
})
self.version.write({ self.version.write({
'contract_date_end': datetime.date(2026, 2, 15), 'contract_date_end': datetime.date(2026, 2, 15),
'departure_reason_id': self.env['hr.departure.reason'] 'departure_reason_id': grund_ohne.id,
.search([], limit=1).id,
}) })
dritter = self.env['l10n.at.payroll.vr'].create({ dritter = self.env['l10n.at.payroll.vr'].create({
'company_id': self.company.id, 'company_id': self.company.id,
@@ -176,8 +187,134 @@ class MeldewesenPrivateTest(TransactionCase):
}) })
dritter.action_erzeugen() dritter.action_erzeugen()
roh = base64.b64decode(dritter.datei).decode('ISO-8859-15') roh = base64.b64decode(dritter.datei).decode('ISO-8859-15')
m4 = [z for z in roh.strip('\r\n').split('\r\n') zeilen = roh.strip('\r\n').split('\r\n')
if z[0:2] == 'M4'] m4 = [z for z in zeilen if z[0:2] == 'M4']
self.assertEqual(len(m4), 1) self.assertEqual(len(m4), 1)
self.assertEqual(m4[0][512:520], '15022026') # ADAT self.assertEqual(m4[0][512:520], '15022026') # ADAT
self.assertEqual(m4[0][548:550], '00') # AGRD Default self.assertEqual(m4[0][548:550], '00') # AGRD Default
def test_06_agrd_vorverbunden(self):
"""Der AGRD-Seed verbindet die üblichen Abgangsursachen vor:
M4 der Pensionierung trägt Code 16 (D.22), hr-Defaults sind
gemappt (fired→01, resigned→02, retired→16)."""
self.assertEqual(self.env.ref('hr.departure_fired').l10n_at_agrd,
'01')
self.assertEqual(self.env.ref('hr.departure_resigned')
.l10n_at_agrd, '02')
employee = self.env['hr.employee'].create({
'name': 'AGRD Test', 'birthday': '1985-03-20'})
version = self.env['hr.version'].search(
[('employee_id', '=', employee.id)], limit=1)
version.write({
'contract_date_start': datetime.date(2020, 1, 1),
'contract_date_end': datetime.date(2026, 3, 31),
'ssnid': '5678200385',
'sex': 'male',
'departure_reason_id': self.env.ref(
'hr.departure_retired').id,
})
lauf = self.env['l10n.at.payroll.vr'].create({
'company_id': self.company.id,
'stichtag': datetime.date(2026, 4, 30),
})
lauf.action_erzeugen()
roh = base64.b64decode(lauf.datei).decode('ISO-8859-15')
m4 = [z for z in roh.strip('\r\n').split('\r\n')
if z[0:2] == 'M4']
self.assertEqual(len(m4), 1)
self.assertEqual(m4[0][548:550], '16') # AGRD Pensionierung
def _katalog_importieren(self):
wizard = self.env['l10n.at.payroll.tasy.katalog.wizard'].create({
'file': base64.b64encode(TASY_KATALOG_FIXTURE.encode())
.decode(),
'filename': 'fixture.xml',
})
wizard.action_import()
def test_03_bsgr_mapping(self):
"""BSGR nach TASY-Bezeichnern: Lehrling Arbeiter→B045,
Lehrling Angestellte→B044, fallweise/gfg Arbeiter→B010,
Angestellte→B030, Normalfall B001/B002."""
deklaration = self.env['l10n.at.payroll.mbgm'].new({
'company_id': self.company.id,
'beitragsmonat': datetime.date(2026, 11, 1),
})
slip = self.env['hr.payslip'].new({})
# Normalfall
self.assertEqual(
deklaration._l10n_at_bsgr(self.version, slip), 'B002')
self.version.l10n_at_personalgruppe = 'arbeiter'
self.assertEqual(
deklaration._l10n_at_bsgr(self.version, slip), 'B001')
# Lehrlinge — korrigierte Zuordnung (B045 = Arbeiterlehrlinge)
self.version.l10n_at_lehrling = True
self.assertEqual(
deklaration._l10n_at_bsgr(self.version, slip), 'B045')
self.version.l10n_at_personalgruppe = 'angestellte'
self.assertEqual(
deklaration._l10n_at_bsgr(self.version, slip), 'B044')
self.version.l10n_at_lehrling = False
# Geringfügige/fallweise je Personalgruppe
self.version.l10n_at_fallweise = True
self.assertEqual(
deklaration._l10n_at_bsgr(self.version, slip), 'B030')
self.version.l10n_at_personalgruppe = 'arbeiter'
self.assertEqual(
deklaration._l10n_at_bsgr(self.version, slip), 'B010')
def test_04_mbgm_katalog_vor_parameter(self):
"""Katalog schlägt den Hand-Parameter: B002-T01 bleibt 39,05 %
(39050), selbst wenn at_mbgm_tarif einen abweichenden Satz
trägt."""
self._katalog_importieren()
parameter = self.env['hr.rule.parameter'].search([
('code', '=', 'at_mbgm_tarif')])
parameter.parameter_version_ids.write(
{'parameter_value':
"{'B001': {'laufend': 11.0, 'sz': 11.0}, "
"'B002': {'laufend': 11.0, 'sz': 11.0}}"})
slip = self._slip(11)
deklaration = self.env['l10n.at.payroll.mbgm'].create({
'company_id': self.company.id,
'beitragsmonat': datetime.date(2026, 11, 1),
'art': 'MB',
})
deklaration.action_erzeugen()
self.assertEqual(deklaration.state, 'done')
roh = base64.b64decode(deklaration.datei).decode('ISO-8859-15')
zeilen = roh.strip('\r\n').split('\r\n')
daten = zeilen[2:-1]
v1_t01 = daten[4]
self.assertEqual(v1_t01[20:23], 'T01')
self.assertEqual(v1_t01[24:30], '039050') # Katalog 39,05 %
v1_t02 = daten[8]
self.assertEqual(v1_t02[24:30], '037550') # Katalog 37,55 %
# ERGB Grundstellung: blank (keine Ergänzung gepflegt)
t1 = daten[2]
self.assertEqual(t1[24:39], ' ' * 15)
def test_05_mbgm_ergb_e01(self):
"""Ergänzung E01 + Dienstgebergruppe NBNSA: T01 enthält den
NB-Zweig (42,85 % → 042850), ERGB-Feld trägt E01."""
self._katalog_importieren()
e01 = self.env['l10n.at.payroll.tasy.ergaenzung'].search([
('code', '=', 'E01')])
self.company.l10n_at_dienstgebergruppe = 'NBNSA'
self.version.l10n_at_ergaenzung_ids = [(6, 0, e01.ids)]
self._slip(11)
deklaration = self.env['l10n.at.payroll.mbgm'].create({
'company_id': self.company.id,
'beitragsmonat': datetime.date(2026, 11, 1),
'art': 'MB',
})
deklaration.action_erzeugen()
self.assertEqual(deklaration.state, 'done')
roh = base64.b64decode(deklaration.datei).decode('ISO-8859-15')
zeilen = roh.strip('\r\n').split('\r\n')
daten = zeilen[2:-1]
t1 = daten[2]
self.assertEqual(t1[24:39], 'E01' + ' ' * 12) # ERGB
v1_t01 = daten[4]
self.assertEqual(v1_t01[20:23], 'T01')
self.assertEqual(v1_t01[24:30], '042850') # 42,85 % inkl. NB
@@ -0,0 +1,242 @@
# Part of the odoo-at-payroll project. License: LGPL-3.
"""AP5 Abwesenheiten (hr_holidays-Anbindung, AT-WETs) und AP2a-c
Reports: Dienstgeber-Gesamtkosten, Monatsjournal-DG-Block,
Jahreslohnkonto je DN, Betriebsjahreslohnkonto."""
import base64
import datetime
from odoo import SUPERUSER_ID
from odoo.tests import TransactionCase, tagged
@tagged('post_install', '-at_install')
class ReportsPrivateTest(TransactionCase):
def setUp(self):
super().setUp()
self.company = self.env.company
self.company.write({
'l10n_at_beitragskontonummer': '1412345678',
})
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 Reports 40h',
'tz': 'Europe/Vienna',
'hours_per_day': 8,
'attendance_ids': [
(0, 0, {'name': 'Tag %d' % wd, 'dayofweek': str(wd),
'hour_from': 8.0, 'hour_to': 16.0})
for wd in range(5)
],
})
self.company.resource_calendar_id = self.calendar
employee = self.env['hr.employee'].create({
'name': 'Reports Test'})
version = self.env['hr.version'].search(
[('employee_id', '=', employee.id)], limit=1)
version.write({
'contract_date_start': datetime.date(2020, 1, 1),
'kv_id': self.kv2203.id,
'kv_gruppe_id': self.gruppe_ii.id,
'erfahrungsstufe': 1,
'resource_calendar_id': self.calendar.id,
})
self.employee = employee
self.version = version
def _slip(self, monat, jahr=2026, confirm=True):
erster = datetime.date(jahr, monat, 1)
letzter = (erster.replace(day=28)
+ datetime.timedelta(days=4)).replace(day=1) \
- datetime.timedelta(days=1)
slip = self.env['hr.payslip'].create({
'name': 'Reports %d/%d' % (monat, jahr),
'employee_id': self.employee.id,
'struct_id': self.structure.id,
'date_from': erster,
'date_to': letzter,
})
slip.compute_sheet()
if confirm:
slip.action_payslip_done()
return slip
def _run(self, monat, jahr=2026):
"""Lohnlauf mit einem bestätigten Slip des Monats."""
slip = self._slip(monat, jahr)
run = self.env['hr.payslip.run'].create({
'name': 'Reports-Lauf %02d/%d' % (monat, jahr),
'date_start': datetime.date(jahr, monat, 1),
'date_end': (datetime.date(jahr, monat, 1)
.replace(day=28)
+ datetime.timedelta(days=4)).replace(day=1)
- datetime.timedelta(days=1),
})
slip.payslip_run_id = run.id
return run, slip
def _total(self, slip, predicate):
return sum(slip.line_ids.filtered(predicate).mapped('total'))
# ------------------------------------------------------------------
# AP5: AT-Abwesenheiten
# ------------------------------------------------------------------
def test_01_absenzen_wets_und_leave_typen(self):
"""AT-WETs (Urlaub/Krankenstand/Karenz) sind is_leave und an
die country-gebundenen Urlaubsarten verknüpft; ein validierter
Krankenstand erzeugt Work Entries mit dem AT-WET."""
wets = self.env['hr.work.entry.type'].search([
('country_id.code', '=', 'AT'), ('is_leave', '=', True)])
codes = set(wets.mapped('code'))
self.assertTrue({'ATP_URLAUB', 'ATP_KRANK', 'ATP_KARENZ'}
<= codes, codes)
urlaub_typ = self.env.ref(
'l10n_at_hr_payroll_private.leave_type_at_urlaub')
krankenstand_typ = self.env.ref(
'l10n_at_hr_payroll_private.leave_type_at_krankenstand')
self.assertEqual(urlaub_typ.work_entry_type_id.code, 'ATP_URLAUB')
self.assertTrue(urlaub_typ.requires_allocation)
self.assertEqual(krankenstand_typ.work_entry_type_id.code,
'ATP_KRANK')
self.assertTrue(krankenstand_typ.support_document)
# Validierter Krankenstand → Work Entry mit AT-WET (Mo, 2.11.2026).
# Work Entries müssen für den Zeitraum bereits generiert sein
# (hr_work_entry_holidays erzeugt Leave-Einträge nur „in bereits
# generierte Zeiträume“ — _cancel_work_entry_conflict).
self.version.generate_work_entries(
datetime.date(2026, 11, 1), datetime.date(2026, 11, 30))
leave = self.env['hr.leave'].create({
'name': 'Krankenstand Test',
'employee_id': self.employee.id,
'holiday_status_id': krankenstand_typ.id,
'request_date_from': datetime.date(2026, 11, 2),
'request_date_to': datetime.date(2026, 11, 2),
})
leave.with_user(SUPERUSER_ID)._action_validate()
work_entries = self.env['hr.work.entry'].search([
('leave_id', '=', leave.id)])
self.assertTrue(work_entries)
self.assertTrue(all(
w.work_entry_type_id.code == 'ATP_KRANK'
for w in work_entries))
# ------------------------------------------------------------------
# AP2a: Dienstgeber-Gesamtkosten
# ------------------------------------------------------------------
def test_02_dg_kosten_report(self):
"""Je AN: Brutto, aufgeschlüsselte LNK (SV-DG, FLAF, KommSt,
BV) und kumulierte Gesamtkosten = Brutto + Σ LNK."""
run, slip = self._run(11)
werte = self.env[
'report.l10n_at_hr_payroll.report_dg_kosten'
]._get_report_values(run.ids)
self.assertEqual(len(werte['zeilen']), 1)
zeile = werte['zeilen'][0]
brutto = self._total(
slip, lambda l: l.salary_rule_id.category_id.code
== 'ATP_BRUTTO'
and l.salary_rule_id.l10n_at_bemessung != 'neutral')
dg = self._total(
slip, lambda l: l.salary_rule_id.category_id.code
== 'ATP_DG')
self.assertAlmostEqual(zeile['brutto'], brutto, places=2)
self.assertAlmostEqual(zeile['dg_summe'], dg, places=2)
self.assertAlmostEqual(zeile['gesamtkosten'],
brutto + dg, places=2)
self.assertAlmostEqual(zeile['spalten']['FLAFDB'],
self._total(slip, lambda l: l.code
== 'FLAFDB'), places=2)
self.assertAlmostEqual(zeile['spalten']['KOMMST'],
self._total(slip, lambda l: l.code
== 'KOMMST'), places=2)
self.assertAlmostEqual(zeile['spalten']['BVG'],
self._total(slip, lambda l: l.code
== 'BVG'), places=2)
self.assertAlmostEqual(zeile['svdg'],
slip._l10n_at_svdg_gesamt()
+ sum(slip._l10n_at_svdg_sz(k) for k in
('kv', 'pv', 'av', 'uv', 'iesg')),
places=2)
# Kumuliert über den Lauf
self.assertAlmostEqual(werte['summen']['gesamtkosten'],
zeile['gesamtkosten'], places=2)
def test_03_monatsjournal_dg_block(self):
"""Monatsjournal führt die Dienstgeberanteile (ATP_DG) als
eigenen Block — SVDG_KV/FLAFDB/KOMMST/BVG erscheinen."""
run, slip = self._run(11)
werte = self.env[
'report.l10n_at_hr_payroll.report_monatsjournal'
]._get_report_values(run.ids)
eintrag = werte['journal'][0]
dg_codes = {art['code'] for art in eintrag['dg_arten']}
self.assertIn('SVDG_KV', dg_codes)
self.assertIn('FLAFDB', dg_codes)
self.assertIn('KOMMST', dg_codes)
self.assertIn('BVG', dg_codes)
self.assertAlmostEqual(
eintrag['summe_dg'],
self._total(slip, lambda l: l.salary_rule_id.category_id
.code == 'ATP_DG'), places=2)
# Bezüge/Abzüge ohne DG-Zeilen (Bemessungsfilter)
art_codes = {art['code'] for art in eintrag['arten']}
self.assertNotIn('SVDG_KV', art_codes)
# ------------------------------------------------------------------
# AP2b/2c: Jahreslohnkonto / Betriebsjahreslohnkonto
# ------------------------------------------------------------------
def test_04_jahreslohnkonto_je_dn(self):
"""Monatsmatrix je Lohnart: KVGEO in November und Dezember,
Jahressumme = Summe beider Monate."""
slip_nov = self._slip(11)
slip_dez = self._slip(12)
wizard = self.env[
'l10n.at.payroll.jahresbericht.wizard'
].create({'jahr': 2026, 'art': 'dn',
'employee_ids': [(6, 0, self.employee.ids)]})
zeilen = wizard._l10n_at_lohnkonto_zeilen(
2026, self.company.id, self.employee.id)
kvgeo = next(z for z in zeilen if z['code'] == 'KVGEO')
total_soll = sum(
slip_nov.line_ids.filtered(lambda l: l.code == 'KVGEO')
.mapped('total')) + sum(
slip_dez.line_ids.filtered(lambda l: l.code == 'KVGEO')
.mapped('total'))
self.assertAlmostEqual(kvgeo['monate'][10],
sum(slip_nov.line_ids.filtered(
lambda l: l.code == 'KVGEO')
.mapped('total')), places=2)
self.assertAlmostEqual(kvgeo['monate'][11],
sum(slip_dez.line_ids.filtered(
lambda l: l.code == 'KVGEO')
.mapped('total')), places=2)
self.assertAlmostEqual(kvgeo['total'], total_soll, places=2)
# Report-Wrapper liefert denselben Datenstand je DN
werte = self.env[
'report.l10n_at_hr_payroll.report_jahreslohnkonto'
]._get_report_values(self.employee.ids, {'jahr': 2026})
self.assertEqual(len(werte['berichte']), 1)
self.assertEqual(werte['jahr'], 2026)
def test_05_betriebslohnkonto(self):
"""Betriebsjahreslohnkonto: je Lohnart Betriebssummen (ohne
DN-Split), Monate konsolidiert."""
self._slip(11)
self._slip(12)
wizard = self.env[
'l10n.at.payroll.jahresbericht.wizard'
].create({'jahr': 2026, 'art': 'betrieb'})
zeilen = wizard._l10n_at_lohnkonto_zeilen(2026, self.company.id)
self.assertTrue(zeilen)
kvgeo = next(z for z in zeilen if z['code'] == 'KVGEO')
self.assertAlmostEqual(
kvgeo['total'], sum(kvgeo['monate']), places=2)
werte = self.env[
'report.l10n_at_hr_payroll.report_betriebslohnkonto'
]._get_report_values(self.company.ids, {'jahr': 2026})
self.assertEqual(len(werte['berichte']), 1)
@@ -0,0 +1,179 @@
# Part of the odoo-at-payroll project. License: LGPL-3.
"""AP3: Wiener Dienstgeberabgabe (Wr. DAG, „U-Bahn-Steuer") —
Wochenlogik, Befreiungen § 3, Regel, Fristen. Rechtsgrundlage RIS LrW
GesNr 20000133 (Fassung 12. 09. 2026, verifiziert)."""
import datetime
from odoo.tests import TransactionCase, tagged
@tagged('post_install', '-at_install')
class WienDagPrivateTest(TransactionCase):
def setUp(self):
super().setUp()
self.company = self.env.company
self.company.write({
'l10n_at_beitragskontonummer': '1412345678',
'l10n_at_wien_dag': True,
})
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 Wien-DAG 40h',
'tz': 'Europe/Vienna',
'hours_per_day': 8,
'attendance_ids': [
(0, 0, {'name': 'Tag %d' % wd, 'dayofweek': str(wd),
'hour_from': 8.0, 'hour_to': 16.0})
for wd in range(5)
],
})
self.company.resource_calendar_id = self.calendar
employee = self.env['hr.employee'].create({
'name': 'Wien-DAG Test', 'birthday': '1990-05-15'})
version = self.env['hr.version'].search(
[('employee_id', '=', employee.id)], limit=1)
version.write({
'contract_date_start': datetime.date(2020, 1, 1),
'kv_id': self.kv2203.id,
'kv_gruppe_id': self.gruppe_ii.id,
'erfahrungsstufe': 1,
'resource_calendar_id': self.calendar.id,
})
self.employee = employee
self.version = version
def _slip(self, monat, jahr=2026):
erster = datetime.date(jahr, monat, 1)
letzter = (erster.replace(day=28)
+ datetime.timedelta(days=4)).replace(day=1) \
- datetime.timedelta(days=1)
slip = self.env['hr.payslip'].create({
'name': 'Wien-DAG %d/%d' % (monat, jahr),
'employee_id': self.employee.id,
'struct_id': self.structure.id,
'date_from': erster,
'date_to': letzter,
})
slip.compute_sheet()
return slip
def _line_total(self, slip, code):
return sum(slip.line_ids.filtered(
lambda l: l.code == code).mapped('total'))
def test_01_voller_monat_fuenf_wochen(self):
"""November 2026: Montage 2./9./16./23./30. → 5 angefangene
Wochen × 2 € = 10 € Regelbetrag WIEN_DAG."""
slip = self._slip(11)
ergebnis = slip._at_wien_dag()
self.assertEqual(ergebnis['wochen'], 5)
self.assertAlmostEqual(ergebnis['betrag'], 10.0, places=2)
self.assertAlmostEqual(self._line_total(slip, 'WIEN_DAG'),
10.0, places=2)
def test_02_eintritt_und_wochenlogik(self):
"""Eintritt 15. 11. (Sonntag): Wochen ab 9. 11. mit DV-Bestand
(9./16./23./30.) → 4 × 2 = 8 €."""
self.version.contract_date_start = datetime.date(2026, 11, 15)
slip = self._slip(11)
ergebnis = slip._at_wien_dag()
self.assertEqual(ergebnis['wochen'], 4)
self.assertAlmostEqual(ergebnis['betrag'], 8.0, places=2)
def test_03_befreiungen(self):
"""§ 3: Lehrling, 55. Lebensjahr, ≤ 10 Wochenstunden — je 0 €;
ohne Company-Flag kein Ergebnis."""
# Lehrling
self.version.l10n_at_lehrling = True
slip = self._slip(11)
self.assertEqual(slip._at_wien_dag()['betrag'], 0.0)
self.version.l10n_at_lehrling = False
# 55. Lebensjahr überschritten (Geburtstag 1. 5. 1971 → 55 am
# 1. 5. 2026, vor Monatsende)
self.employee.birthday = '1971-05-01'
slip = self._slip(11)
ergebnis = slip._at_wien_dag()
self.assertEqual(ergebnis['betrag'], 0.0)
self.assertIn('55. Lebensjahr', ergebnis['info'])
self.employee.birthday = '1990-05-15'
# ≤ 10 Wochenstunden (Kalender 2 Tage à 4 h = 8 h)
klein = self.env['resource.calendar'].create({
'name': 'AT gering 8h',
'tz': 'Europe/Vienna',
'hours_per_day': 4,
'attendance_ids': [
(0, 0, {'name': 'Tag %d' % wd, 'dayofweek': str(wd),
'hour_from': 8.0, 'hour_to': 12.0})
for wd in range(2)
],
})
self.version.resource_calendar_id = klein
slip = self._slip(11)
ergebnis = slip._at_wien_dag()
self.assertEqual(ergebnis['betrag'], 0.0)
self.assertIn('10 Wochenstunden', ergebnis['info'])
self.version.resource_calendar_id = self.calendar
# Ohne Flag: keine Abgabe
self.company.l10n_at_wien_dag = False
slip = self._slip(11)
self.assertFalse(slip._at_wien_dag())
self.assertFalse(self._line_total(slip, 'WIEN_DAG'))
def test_04_karenz_woche_befreit(self):
"""Eine Karenz-Woche (ATP_KARENZ-Work Entry) senkt die
abgabepflichtigen Wochen (5 → 4)."""
self.version.generate_work_entries(
datetime.date(2026, 11, 1), datetime.date(2026, 11, 30))
karenz_wet = self.env['hr.work.entry.type'].search([
('code', '=', 'ATP_KARENZ')])
karenz_typ = self.env.ref(
'l10n_at_hr_payroll_private.leave_type_at_karenz')
leave = self.env['hr.leave'].create({
'name': 'Karenz Test',
'employee_id': self.employee.id,
'holiday_status_id': karenz_typ.id,
'request_date_from': datetime.date(2026, 11, 16),
'request_date_to': datetime.date(2026, 11, 20),
})
from odoo import SUPERUSER_ID
leave.with_user(SUPERUSER_ID)._action_validate()
slip = self._slip(11)
ergebnis = slip._at_wien_dag()
self.assertEqual(ergebnis['wochen'], 4)
self.assertAlmostEqual(ergebnis['betrag'], 8.0, places=2)
self.assertIn('Karenz', ergebnis['info'])
def test_05_fristenkalender(self):
"""Cron erzeugt wien_dag-Fristen nur für Flag-Companies: 15.
des Folgemonats monatlich, 31. 3. Jahreserklärung."""
fristen = self.env['l10n.at.payroll.frist']
heute = datetime.date.today()
fristen._cron_fristen_erzeugen()
monat_frist = fristen.search([
('company_id', '=', self.company.id),
('fristart', '=', 'wien_dag')], limit=1)
self.assertTrue(monat_frist)
self.assertEqual(monat_frist.deadline.day, 15)
# Jahresfrist entsteht im Cron nur am 1. Jänner — direkter
# Aufruf der Sicherstellung prüft die Art selbst:
fristen._sicherstellen(
self.company, 'wien_dag_jahres', heute.year - 1, None,
datetime.date(heute.year, 3, 31))
self.assertTrue(fristen.search([
('company_id', '=', self.company.id),
('fristart', '=', 'wien_dag_jahres')]))
# Ohne Flag: keine Wien-Fristen
self.company.l10n_at_wien_dag = False
fristen.search([
('company_id', '=', self.company.id),
('fristart', 'like', 'wien_dag')]).unlink()
fristen._cron_fristen_erzeugen()
self.assertFalse(fristen.search([
('company_id', '=', self.company.id),
('fristart', 'like', 'wien_dag')]))
@@ -180,6 +180,19 @@
invisible="not kv_gruppe_id"/> invisible="not kv_gruppe_id"/>
<field name="ueberzahlung" <field name="ueberzahlung"
invisible="not kv_gruppe_id"/> invisible="not kv_gruppe_id"/>
<field name="l10n_at_vordienstzeit_monate"
invisible="not kv_gruppe_id"/>
<field name="l10n_at_vwgj_monate"
invisible="not kv_gruppe_id"/>
<field name="l10n_at_vwgj_stufe_vorschlag"
invisible="not kv_gruppe_id"/>
<field name="l10n_at_naechste_vorrueckung"
invisible="not kv_gruppe_id"/>
<button name="action_einstufung_uebernehmen"
string="Vorschlag übernehmen" type="object"
class="btn-secondary"
invisible="not kv_gruppe_id or not l10n_at_vwgj_stufe_vorschlag"
groups="hr_payroll.group_hr_payroll_manager"/>
</group> </group>
</group> </group>
</field> </field>
@@ -23,7 +23,7 @@
</field> </field>
</record> </record>
<!-- NSchAB (GP6-Company-Flag) am Lohnverrechnungs-Reiter --> <!-- NSchAB-Fallback + Dienstgebergruppe (AP0) am Lohnverrechnungs-Reiter -->
<record id="res_company_view_form_l10n_at_nschab" model="ir.ui.view"> <record id="res_company_view_form_l10n_at_nschab" model="ir.ui.view">
<field name="name">res.company.form.l10n.at.nschab</field> <field name="name">res.company.form.l10n.at.nschab</field>
<field name="model">res.company</field> <field name="model">res.company</field>
@@ -31,6 +31,8 @@
ref="l10n_at_hr_payroll.res_company_view_form_l10n_at"/> ref="l10n_at_hr_payroll.res_company_view_form_l10n_at"/>
<field name="arch" type="xml"> <field name="arch" type="xml">
<group name="l10n_at_abgaben" position="inside"> <group name="l10n_at_abgaben" position="inside">
<field name="l10n_at_dienstgebergruppe"
placeholder="z. B. NBALG"/>
<field name="l10n_at_nschab"/> <field name="l10n_at_nschab"/>
</group> </group>
</field> </field>
@@ -22,6 +22,9 @@
<group string="Status &amp; Abrechnung"> <group string="Status &amp; Abrechnung">
<field name="l10n_at_lehrling"/> <field name="l10n_at_lehrling"/>
<field name="l10n_at_fallweise"/> <field name="l10n_at_fallweise"/>
<field name="l10n_at_ergaenzung_ids"
widget="many2many_tags"
placeholder="z. B. E01 Nachtschwerarbeit"/>
<field name="l10n_at_familienabsetzbetrag"/> <field name="l10n_at_familienabsetzbetrag"/>
<field name="l10n_at_ueberw_nachtarbeit"/> <field name="l10n_at_ueberw_nachtarbeit"/>
</group> </group>