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
**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
abgeschlossen, Projekt M8 final** (2026-09-12): Kern 19.0.5.1.0 —
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.
- Odoo-Upgrades künftig in beiden Repos (gem360 + dieses) gegen denselben
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
@@ -1,7 +1,7 @@
# Part of the odoo-at-payroll project. License: LGPL-3.
{
'name': 'Österreich - Gemeinde-Personalverrechnung (Bgld. GemBG)',
'version': '19.0.1.4.0',
'version': '19.0.1.6.0',
'author': 'ixsol / gem360',
'website': 'https://gem360.at',
'category': 'Human Resources/Payroll',
@@ -16,7 +16,6 @@
<field name="name">Gemeindebedienstete (Bgld. GemBG 2014)</field>
<field name="code">GEMBG.BGLD</field>
<field name="type_id" ref="structure_type_gembg_bgld"/>
<field name="regular_pay" eval="True"/>
</record>
<!-- ============================================================ -->
@@ -42,47 +41,32 @@
<!-- ============================================================ -->
<!-- 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">
<field name="name">Überstunden (tagsüber, § 76 GemBG)</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 id="work_entry_type_ueberstunden_nacht" model="hr.work.entry.type">
<field name="name">Überstunden (Nachtzeit 22-6 Uhr, § 76 GemBG)</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 id="work_entry_type_mehrdienst" model="hr.work.entry.type">
<field name="name">Mehrdienstleistung (§ 76 GemBG)</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 id="work_entry_type_journaldienst" model="hr.work.entry.type">
<field name="name">Journaldienst (§ 79 GemBG)</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 id="work_entry_type_bereitschaft" model="hr.work.entry.type">
<field name="name">Bereitschaftsdienst (§ 80 GemBG)</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 id="work_entry_type_sonn_feiertag" model="hr.work.entry.type">
<field name="name">Sonn-/Feiertagsdienst (§ 78 GemBG)</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>
<!-- ============================================================ -->
@@ -7,11 +7,11 @@
<field name="name">Monatsentgelt</field>
<field name="code">ME</field>
<field name="sequence" eval="1"/>
<field name="category_id" ref="payroll_structure_bgld.category_gembg_brutto"/>
<field name="struct_id" ref="payroll_structure_bgld.structure_gembg_bgld"/>
<field name="category_id" ref="category_gembg_brutto"/>
<field name="struct_id" ref="structure_gembg_bgld"/>
<field name="condition_select">python</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[
# Monatsentgelt nach Bgld. GemBG 2014 (IVa. Hauptstück §§ 133 ff).
# Katalog-Lookup: Entlohnungsgruppe + Stufe + Stichtag.
@@ -24,9 +24,10 @@ else:
# Ausbildungsphase: 5 % Abzug (§ 133h GemBG)
if employee.ausbildungsphase:
base *= 0.95
# Teilzeit: anteilig über work_time_rate (§ 59 GemBG)
work_rate = employee.work_time_rate or 100.0
result = base * work_rate / 100.0
# Teilzeit: anteilig über work_time_rate (§ 59 GemBG) — Odoo 19
# speichert work_time_rate als Bruchteil 0..1 (nicht Prozent).
work_rate = employee.work_time_rate or 1.0
result = base * work_rate
]]></field>
</record>
@@ -37,8 +38,8 @@ else:
<field name="name">Kinderzulage (§ 61 GemBG)</field>
<field name="code">KIZU</field>
<field name="sequence" eval="2"/>
<field name="category_id" ref="payroll_structure_bgld.category_gembg_brutto"/>
<field name="struct_id" ref="payroll_structure_bgld.structure_gembg_bgld"/>
<field name="category_id" ref="category_gembg_brutto"/>
<field name="struct_id" ref="structure_gembg_bgld"/>
<field name="condition_select">python</field>
<field name="condition_python">result = employee.kinder_anzahl > 0</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="code">FUZU</field>
<field name="sequence" eval="3"/>
<field name="category_id" ref="payroll_structure_bgld.category_gembg_brutto"/>
<field name="struct_id" ref="payroll_structure_bgld.structure_gembg_bgld"/>
<field name="category_id" ref="category_gembg_brutto"/>
<field name="struct_id" ref="structure_gembg_bgld"/>
<field name="condition_select">python</field>
<field name="condition_python">result = employee.funktionszulage > 0</field>
<field name="amount_select">code</field>
<field name="amount_python_compute"><![CDATA[
# Funktionszulage § 62 GemBG — Satz am Dienstvertrag (hr.version).
# Bei Teilzeit anteilig (wie Monatsentgelt).
work_rate = employee.work_time_rate or 100.0
result = employee.funktionszulage * work_rate / 100.0
# Bei Teilzeit anteilig (wie Monatsentgelt); work_time_rate = Bruchteil.
work_rate = employee.work_time_rate or 1.0
result = employee.funktionszulage * work_rate
]]></field>
</record>
@@ -78,8 +79,8 @@ result = employee.funktionszulage * work_rate / 100.0
<field name="name">Überstundenvergütung tagsüber (§ 76 GemBG)</field>
<field name="code">UEZ_TAG</field>
<field name="sequence" eval="10"/>
<field name="category_id" ref="payroll_structure_bgld.category_gembg_brutto"/>
<field name="struct_id" ref="payroll_structure_bgld.structure_gembg_bgld"/>
<field name="category_id" ref="category_gembg_brutto"/>
<field name="struct_id" ref="structure_gembg_bgld"/>
<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="amount_select">code</field>
@@ -100,8 +101,8 @@ else:
<field name="name">Überstundenvergütung Nachtzeit (§ 76 GemBG)</field>
<field name="code">UEZ_NACHT</field>
<field name="sequence" eval="11"/>
<field name="category_id" ref="payroll_structure_bgld.category_gembg_brutto"/>
<field name="struct_id" ref="payroll_structure_bgld.structure_gembg_bgld"/>
<field name="category_id" ref="category_gembg_brutto"/>
<field name="struct_id" ref="structure_gembg_bgld"/>
<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="amount_select">code</field>
@@ -125,8 +126,8 @@ else:
<field name="name">Journaldienstzulage (§ 79 GemBG)</field>
<field name="code">JZU</field>
<field name="sequence" eval="12"/>
<field name="category_id" ref="payroll_structure_bgld.category_gembg_brutto"/>
<field name="struct_id" ref="payroll_structure_bgld.structure_gembg_bgld"/>
<field name="category_id" ref="category_gembg_brutto"/>
<field name="struct_id" ref="structure_gembg_bgld"/>
<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="amount_select">code</field>
@@ -146,8 +147,8 @@ result = tage * satz
<field name="name">Sonn-/Feiertagszulage (§ 78 GemBG)</field>
<field name="code">SFZU</field>
<field name="sequence" eval="13"/>
<field name="category_id" ref="payroll_structure_bgld.category_gembg_brutto"/>
<field name="struct_id" ref="payroll_structure_bgld.structure_gembg_bgld"/>
<field name="category_id" ref="category_gembg_brutto"/>
<field name="struct_id" ref="structure_gembg_bgld"/>
<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="amount_select">code</field>
@@ -174,8 +175,8 @@ else:
<field name="name">Bereitschaftsentschädigung (§ 80 GemBG)</field>
<field name="code">BERB</field>
<field name="sequence" eval="14"/>
<field name="category_id" ref="payroll_structure_bgld.category_gembg_brutto"/>
<field name="struct_id" ref="payroll_structure_bgld.structure_gembg_bgld"/>
<field name="category_id" ref="category_gembg_brutto"/>
<field name="struct_id" ref="structure_gembg_bgld"/>
<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="amount_select">code</field>
@@ -195,8 +196,8 @@ result = stunden * satz
<field name="name">Erschwerniszulage (§ 83 GemBG)</field>
<field name="code">ERZU</field>
<field name="sequence" eval="15"/>
<field name="category_id" ref="payroll_structure_bgld.category_gembg_brutto"/>
<field name="struct_id" ref="payroll_structure_bgld.structure_gembg_bgld"/>
<field name="category_id" ref="category_gembg_brutto"/>
<field name="struct_id" ref="structure_gembg_bgld"/>
<field name="condition_select">python</field>
<field name="condition_python">result = 'ERZU_INPUT' in inputs and inputs['ERZU_INPUT'].amount > 0</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="code">JUB</field>
<field name="sequence" eval="16"/>
<field name="category_id" ref="payroll_structure_bgld.category_gembg_brutto"/>
<field name="struct_id" ref="payroll_structure_bgld.structure_gembg_bgld"/>
<field name="category_id" ref="category_gembg_brutto"/>
<field name="struct_id" ref="structure_gembg_bgld"/>
<field name="condition_select">python</field>
<field name="condition_python">result = 'JUB_INPUT' in inputs and inputs['JUB_INPUT'].amount > 0</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="code">FKZ</field>
<field name="sequence" eval="17"/>
<field name="category_id" ref="payroll_structure_bgld.category_gembg_brutto"/>
<field name="struct_id" ref="payroll_structure_bgld.structure_gembg_bgld"/>
<field name="category_id" ref="category_gembg_brutto"/>
<field name="struct_id" ref="structure_gembg_bgld"/>
<field name="condition_select">python</field>
<field name="condition_python">result = 'FKZ_INPUT' in inputs and inputs['FKZ_INPUT'].amount > 0</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="code">SZQ</field>
<field name="sequence" eval="30"/>
<field name="category_id" ref="payroll_structure_bgld.category_gembg_brutto"/>
<field name="struct_id" ref="payroll_structure_bgld.structure_gembg_bgld"/>
<field name="category_id" ref="category_gembg_brutto"/>
<field name="struct_id" ref="structure_gembg_bgld"/>
<field name="condition_select">python</field>
<field name="condition_python"><![CDATA[
# § 133n GemBG: 50 % des Monatsentgelts (und der Kinderzulage)
@@ -255,7 +256,6 @@ result = inputs['FKZ_INPUT'].amount
# Quartalsende (März, Juni, September, Dezember).
# 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.).
import datetime
month = payslip.date_to.month
result = month in (3, 6, 9, 12)
]]></field>
@@ -266,8 +266,9 @@ result = month in (3, 6, 9, 12)
# Kinderzulage), die ihnen für den Monat der Auszahlung zustehen").
# Bei unterbrochenem Vierteljahr: nur der entsprechende Teil
# (R1: Vollzeit-Rechnung; Anteil folgt mit der Zeit-Erfassung).
me_total = rules['ME'].total or 0
kizu_total = rules['KIZU'].total or 0
# Odoo 19 befült result_rules (nicht rules) mit den Regelsummen.
me_total = result_rules['ME']['total'] or 0
kizu_total = result_rules['KIZU']['total'] or 0
result = (me_total + kizu_total) * 0.5
]]></field>
</record>
@@ -279,12 +280,12 @@ result = (me_total + kizu_total) * 0.5
<field name="name">Netto</field>
<field name="code">NETTO</field>
<field name="sequence" eval="100"/>
<field name="category_id" ref="payroll_structure_bgld.category_gembg_netto"/>
<field name="struct_id" ref="payroll_structure_bgld.structure_gembg_bgld"/>
<field name="category_id" ref="category_gembg_netto"/>
<field name="struct_id" ref="structure_gembg_bgld"/>
<field name="condition_select">none</field>
<field name="amount_select">code</field>
<field name="amount_python_compute"><![CDATA[
result = categories.GEMBG_BRUTTO - categories.GEMBG_ABZUG
result = categories['GEMBG_BRUTTO'] - categories['GEMBG_ABZUG']
]]></field>
</record>
</odoo>
@@ -24,12 +24,55 @@ class HrVersion(models.Model):
besoldungsdienstalter = fields.Date(
string='Besoldungsdienstalter (ab)',
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(
compute='_compute_vorrueckungstermin', store=True, readonly=False,
groups="hr_payroll.group_hr_payroll_user", tracking=True,
help="Vorschlag nach § 66 Abs 2 GemBG (BDA + Vorrückungszeitraum); "
"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')
def _compute_vorrueckungstermin(self):
for version in self:
@@ -67,6 +110,16 @@ class HrEmployee(models.Model):
besoldungsdienstalter = fields.Date(
related="version_id.besoldungsdienstalter", inherited=True,
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(
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 = self.env['hr.payslip'].create({
'name': 'GemBG-Test %d/2026' % slip_month,
'employee_id': employee.id,
'struct_id': self.structure.id,
'date_from': date(2026, slip_month, 1),
@@ -1,6 +1,8 @@
# Part of the gem360 onboarding. See LICENSE / project docs for details.
from datetime import date
from freezegun import freeze_time
from odoo.tests import TransactionCase, tagged
@@ -53,9 +55,14 @@ class KatalogTest(TransactionCase):
if not version:
version = self.env['hr.version'].create({
'employee_id': employee.id,
'date_start': date(2020, 1, 1),
'contract_date_start': date(2020, 1, 1),
})
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':
self.env.ref('l10n_at_gemeinde_payroll.bgld_grp_av_av1').id,
'entlohnungsstufe': stufe,
@@ -76,10 +83,13 @@ class KatalogTest(TransactionCase):
"""Wizard erhöht die Stufe fälliger Versionen und überspringt
die Höchststufe."""
version = self._version_mit_katalog(date(2014, 3, 15), stufe=3)
version.vorrueckungstermin = date(2024, 4, 1)
max_stufe = version.entlohnungsgruppe_id._max_stufe(date(2024, 4, 1))
# Stichtag ab Katalog-Gültigkeit (Seed 2026-07-01) — vor dem
# 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({
'stichtag': date(2024, 4, 1),
'stichtag': date(2028, 4, 1),
})
self.assertIn(version, wizard.version_ids)
wizard.action_apply()
@@ -87,9 +97,29 @@ class KatalogTest(TransactionCase):
version_top = self._version_mit_katalog(date(2014, 3, 15),
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({
'stichtag': date(2024, 4, 1),
'stichtag': date(2028, 4, 1),
})
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">
<field name="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="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 name="gembg_zulagen" string="GemBG Zulagen"
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.
{
'name': 'Österreich - Personalverrechnung Kern (AT Payroll Core)',
'version': '19.0.5.1.0',
'version': '19.0.6.0.0',
'author': 'ixsol',
'website': 'https://gem360.at',
'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_2027.xml',
'data/tasy_mapping.xml',
'data/hr_departure_reason_seed.xml',
'views/tasy_views.xml',
'views/tasy_katalog_views.xml',
'views/company_views.xml',
'views/meldewesen_views.xml',
'views/fristen_views.xml',
'report/belege_reports.xml',
'report/jahresreports.xml',
],
'countries': ['at'],
'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>
+3 -1
View File
@@ -1,5 +1,6 @@
# Part of the odoo-at-payroll project. License: LGPL-3.
from . import tasy
from . import tasy_katalog
from . import lohnsteuer
from . import sozialversicherung
from . import elda
@@ -8,4 +9,5 @@ from . import kind
from . import lohnzettel_l16
from . import fristenkalender
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-'
'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(
folgemonat.year, folgemonat.month,
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(
company, art, vormonat.year, vormonat.month,
deadline_monat)
@@ -209,6 +238,11 @@ class L10nAtPayrollFrist(models.Model):
self._sicherstellen(
company, 'kommst_jahres', heute.year - 1, None,
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
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/
Plausibilität, Rest dokumentiert).
- ``hr.departure.reason``: Mapping auf den eSV-Abmeldegrund (AGRD,
DM-ORG D.22; Standard ``00`` sonstiger Grund mit Ende des
Beschäftigungsverhältnisses)."""
DM-ORG D.22 — vollständige Code-Liste als Selection, Standard
``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.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):
_inherit = 'res.company'
@@ -36,6 +79,16 @@ class ResCompany(models.Model):
help='9-stellig: 2 Stellen Finanzamtsnummer + 7 Stellen\n'
'Steuernummer (Lohnzettel Finanz I1 STNRA/STNRL;\n'
'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):
@@ -89,8 +142,10 @@ class HrVersion(models.Model):
class HrDepartureReason(models.Model):
_inherit = 'hr.departure.reason'
l10n_at_agrd = fields.Char(
l10n_at_agrd = fields.Selection(
L10N_AT_AGRD,
string='AT eSV-Abmeldegrund (AGRD)',
help='2-stelliger Abmeldegrund-Code der Versichertenmeldung '
'(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.')
+215 -17
View File
@@ -1,16 +1,29 @@
# Part of the odoo-at-payroll project. License: LGPL-3.
"""Monatsdienstliste und Monatsjournal (GP7/AP6): QWeb-Datenlieferanten
auf dem Lohnlauf (hr.payslip.run) — voraberechnete Aggregation über
`hr.payslip.line` (read_group-Muster des Contribution Register), damit
die Templates reine Darstellung bleiben.
"""Monatsreports und Jahresberichte der AT-Lohnverrechnung.
- **Monatsdienstliste**: je AN eine Zeile mit den mBGM-Kontrollgrößen
(Versicherungstage/Beitragsgrundlagen je Beitragsmonat, DN- und
DG-Beiträge, DG-Anteile) — Abstimmung Abrechnung ↔ SV-Meldung.
- **Monatsjournal**: je Lohnart die Summe über den Lauf (Brutto,
Abzüge, DG-Anteile, Netto) mit Kontrollsummen gegen die
GP6-Buchung."""
from odoo import models
- **Monatsdienstliste** (GP7/AP6): je AN die mBGM-Kontrollgrößen
Abstimmung Abrechnung ↔ SV-Meldung.
- **Monatsjournal** (GP7/AP6, AP2a erweitert): je Lohnart die Summe
über den Lauf, gegliedert nach Bezügen/Abzügen (Bemessung ≠
neutral) und — seit AP2a — **eigenem Block Dienstgeberanteile**
(Kategorie ``ATP_DG``: SV-DG je Zweig, FLAF-DB, KommSt, DZ, BV —
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):
@@ -61,24 +74,32 @@ class L10nAtPayrollMonatsReports(models.AbstractModel):
class L10nAtPayrollMonatsjournal(models.AbstractModel):
_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):
runs = self.env['hr.payslip.run'].browse(docids)
zeilen = []
for run in runs:
gruppen = {}
dg_gruppen = {}
for slip in run.slip_ids.filtered(
lambda p: p.state not in ('draft', 'cancel')):
for line in slip.line_ids:
regel = line.salary_rule_id
if not line.total or regel.l10n_at_bemessung \
== 'neutral':
if not line.total:
continue
kategorie = regel.category_id.code or ''
schluessel = (regel.code, regel.name,
regel.l10n_at_bemessung)
eintrag = gruppen.setdefault(schluessel, 0.0)
gruppen[schluessel] = eintrag + line.total
if kategorie == 'ATP_DG':
# 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({
'run': run,
'arten': [
@@ -86,5 +107,182 @@ class L10nAtPayrollMonatsjournal(models.AbstractModel):
'summe': summe}
for (code, name, bemessung), summe
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.
"""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
import base64
@@ -7,6 +20,19 @@ from odoo import _, api, fields, models
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):
"""Mapping TASY-Code/-Zweig -> SV-Regelparameter (hr.rule.parameter).
@@ -102,15 +128,7 @@ class L10nAtPayrollTasyImportWizard(models.TransientModel):
def _tasy_root(self):
self.ensure_one()
parser = etree.XMLParser(resolve_entities=False, no_network=True)
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)
return _l10n_at_tasy_root(self.file)
@staticmethod
def _tasy_text(node, tag):
@@ -299,4 +317,392 @@ class L10nAtPayrollTasyImportWizard(models.TransientModel):
count=applied),
'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>
</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,
DG-Anteile (SV-DG, FLAF, KommSt, DZ, BV), Netto. -->
<template id="report_lohnzettel_at">
@@ -170,13 +181,15 @@
</t>
</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">
<t t-call="web.html_container">
<t t-foreach="journal" t-as="j">
<t t-call="web.external_layout">
<div class="page">
<h2>Monatsjournal — <t t-out="j['run'].name"/></h2>
<h4>Bezüge &amp; Abzüge</h4>
<table class="table table-sm">
<thead>
<tr>
@@ -193,6 +206,92 @@
<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 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>
</table>
</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>
@@ -12,4 +12,24 @@ access_l10n_at_payroll_frist_user,L10n AT Payroll Frist User,model_l10n_at_payro
access_l10n_at_payroll_frist_manager,L10n AT Payroll Frist Manager,model_l10n_at_payroll_frist,hr_payroll.group_hr_payroll_manager,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_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
12 access_l10n_at_payroll_frist_manager L10n AT Payroll Frist Manager model_l10n_at_payroll_frist hr_payroll.group_hr_payroll_manager 1 1 1 1
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.
from . import test_tasy_import
from . import test_tasy_katalog
from . import test_lohnsteuer
from . import test_sozialversicherung
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_wk_mitglied"/>
<field name="l10n_at_wf_wien"/>
<field name="l10n_at_wien_dag"/>
</group>
</group>
</page>
@@ -124,4 +124,26 @@
</notebook>
</field>
</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>
@@ -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.
{
'name': 'Österreich - Privatwirtschaft Personalverrechnung (General-AT)',
'version': '19.0.8.1.0',
'version': '19.0.9.2.0',
'author': 'ixsol / gem360',
'website': 'https://gem360.at',
'category': 'Human Resources/Payroll',
@@ -91,7 +91,8 @@ Kein VRV-Bezug (Gemeinde-Schiene bleibt ``l10n_at_gemeinde_payroll`` +
gem360-Brücke).
""",
'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': [
'security/ir.model.access.csv',
'security/security_rules.xml',
@@ -101,13 +102,17 @@ gem360-Brücke).
'data/l10n.at.payroll.kv.wert.csv',
'data/structure_private.xml',
'data/work_entry_types_private.xml',
'data/absence_private.xml',
'data/input_types_private.xml',
'data/salary_rules_private.xml',
'data/salary_rules_lst_private.xml',
'data/salary_rules_efzg_private.xml',
'data/salary_rules_sv_private.xml',
'data/salary_rules_bvg_private.xml',
'data/hr_payroll_account_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',
'views/kv_views.xml',
'views/bv_views.xml',
@@ -115,6 +120,7 @@ gem360-Brücke).
'views/vertragsfelder_views.xml',
'views/mitarbeiter_views.xml',
'report/gp8_reports.xml',
'report/buchungsbeleg_report.xml',
'report/bv_report.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
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_2,l10n_at_hr_payroll_private.kv2203_grp_ii,2,nach 2 VWGJ,2179.68,2026-01-01
kv2203_st_ii_3,l10n_at_hr_payroll_private.kv2203_grp_ii,3,nach 4 VWGJ,2315.46,2026-01-01
kv2203_st_ii_4,l10n_at_hr_payroll_private.kv2203_grp_ii,4,nach 6 VWGJ,2457.65,2026-01-01
kv2203_st_ii_5,l10n_at_hr_payroll_private.kv2203_grp_ii,5,nach 8 VWGJ,2599.83,2026-01-01
kv2203_st_ii_6,l10n_at_hr_payroll_private.kv2203_grp_ii,6,nach 10 VWGJ,2742.03,2026-01-01
kv2203_st_ii_7,l10n_at_hr_payroll_private.kv2203_grp_ii,7,nach 12 VWGJ,2863.88,2026-01-01
kv2203_st_ii_8,l10n_at_hr_payroll_private.kv2203_grp_ii,8,nach 15 VWGJ,3087.28,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
kv2203_st_iii_2,l10n_at_hr_payroll_private.kv2203_grp_iii,2,nach 2 VWGJ,2705.03,2026-01-01
kv2203_st_iii_3,l10n_at_hr_payroll_private.kv2203_grp_iii,3,nach 4 VWGJ,2881.97,2026-01-01
kv2203_st_iii_4,l10n_at_hr_payroll_private.kv2203_grp_iii,4,nach 6 VWGJ,3058.96,2026-01-01
kv2203_st_iii_5,l10n_at_hr_payroll_private.kv2203_grp_iii,5,nach 8 VWGJ,3232.73,2026-01-01
kv2203_st_iii_6,l10n_at_hr_payroll_private.kv2203_grp_iii,6,nach 10 VWGJ,3409.53,2026-01-01
kv2203_st_iii_7,l10n_at_hr_payroll_private.kv2203_grp_iii,7,nach 12 VWGJ,3561.04,2026-01-01
kv2203_st_iii_8,l10n_at_hr_payroll_private.kv2203_grp_iii,8,nach 15 VWGJ,3838.85,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
kv2203_st_iv_2,l10n_at_hr_payroll_private.kv2203_grp_iv,2,nach 2 VWGJ,3366.49,2026-01-01
kv2203_st_iv_3,l10n_at_hr_payroll_private.kv2203_grp_iv,3,nach 4 VWGJ,3586.73,2026-01-01
kv2203_st_iv_4,l10n_at_hr_payroll_private.kv2203_grp_iv,4,nach 6 VWGJ,3806.97,2026-01-01
kv2203_st_iv_5,l10n_at_hr_payroll_private.kv2203_grp_iv,5,nach 8 VWGJ,4027.20,2026-01-01
kv2203_st_iv_6,l10n_at_hr_payroll_private.kv2203_grp_iv,6,nach 10 VWGJ,4247.47,2026-01-01
kv2203_st_iv_7,l10n_at_hr_payroll_private.kv2203_grp_iv,7,nach 12 VWGJ,4436.22,2026-01-01
kv2203_st_iv_8,l10n_at_hr_payroll_private.kv2203_grp_iv,8,nach 15 VWGJ,4782.32,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
kv2203_st_v_2,l10n_at_hr_payroll_private.kv2203_grp_v,2,nach 2 VWGJ,4194.02,2026-01-01
kv2203_st_v_3,l10n_at_hr_payroll_private.kv2203_grp_v,3,nach 4 VWGJ,4468.41,2026-01-01
kv2203_st_v_4,l10n_at_hr_payroll_private.kv2203_grp_v,4,nach 6 VWGJ,4742.77,2026-01-01
kv2203_st_v_5,l10n_at_hr_payroll_private.kv2203_grp_v,5,nach 8 VWGJ,5017.15,2026-01-01
kv2203_st_v_6,l10n_at_hr_payroll_private.kv2203_grp_v,6,nach 10 VWGJ,5291.57,2026-01-01
kv2203_st_v_7,l10n_at_hr_payroll_private.kv2203_grp_v,7,nach 12 VWGJ,5526.70,2026-01-01
kv2203_st_v_8,l10n_at_hr_payroll_private.kv2203_grp_v,8,nach 15 VWGJ,5957.86,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
kv2203_st_vi_2,l10n_at_hr_payroll_private.kv2203_grp_vi,2,nach 2 VWGJ,6167.67,2026-01-01
kv2203_st_vi_3,l10n_at_hr_payroll_private.kv2203_grp_vi,3,nach 5 VWGJ,6853.01,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
kv2203_st_m1_2,l10n_at_hr_payroll_private.kv2203_grp_m1,2,nach 2 VWGJ,2427.72,2026-01-01
kv2203_st_m1_3,l10n_at_hr_payroll_private.kv2203_grp_m1,3,nach 4 VWGJ,2586.57,2026-01-01
kv2203_st_m1_4,l10n_at_hr_payroll_private.kv2203_grp_m1,4,nach 6 VWGJ,2745.39,2026-01-01
kv2203_st_m1_5,l10n_at_hr_payroll_private.kv2203_grp_m1,5,nach 8 VWGJ,2904.19,2026-01-01
kv2203_st_m1_6,l10n_at_hr_payroll_private.kv2203_grp_m1,6,nach 10 VWGJ,3063.05,2026-01-01
kv2203_st_m1_7,l10n_at_hr_payroll_private.kv2203_grp_m1,7,nach 12 VWGJ,3199.16,2026-01-01
kv2203_st_m1_8,l10n_at_hr_payroll_private.kv2203_grp_m1,8,nach 15 VWGJ,3448.74,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
kv2203_st_m2_2,l10n_at_hr_payroll_private.kv2203_grp_m2,2,nach 2 VWGJ,3095.50,2026-01-01
kv2203_st_m2_3,l10n_at_hr_payroll_private.kv2203_grp_m2,3,nach 4 VWGJ,3297.98,2026-01-01
kv2203_st_m2_4,l10n_at_hr_payroll_private.kv2203_grp_m2,4,nach 6 VWGJ,3500.52,2026-01-01
kv2203_st_m2_5,l10n_at_hr_payroll_private.kv2203_grp_m2,5,nach 8 VWGJ,3703.01,2026-01-01
kv2203_st_m2_6,l10n_at_hr_payroll_private.kv2203_grp_m2,6,nach 10 VWGJ,3905.53,2026-01-01
kv2203_st_m2_7,l10n_at_hr_payroll_private.kv2203_grp_m2,7,nach 12 VWGJ,4079.13,2026-01-01
kv2203_st_m2_8,l10n_at_hr_payroll_private.kv2203_grp_m2,8,nach 15 VWGJ,4397.37,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
kv2203_st_m2fs_2,l10n_at_hr_payroll_private.kv2203_grp_m2fs,2,nach 2 VWGJ,3241.68,2026-01-01
kv2203_st_m2fs_3,l10n_at_hr_payroll_private.kv2203_grp_m2fs,3,nach 4 VWGJ,3453.74,2026-01-01
kv2203_st_m2fs_4,l10n_at_hr_payroll_private.kv2203_grp_m2fs,4,nach 6 VWGJ,3665.79,2026-01-01
kv2203_st_m2fs_5,l10n_at_hr_payroll_private.kv2203_grp_m2fs,5,nach 8 VWGJ,3877.83,2026-01-01
kv2203_st_m2fs_6,l10n_at_hr_payroll_private.kv2203_grp_m2fs,6,nach 10 VWGJ,4089.90,2026-01-01
kv2203_st_m2fs_7,l10n_at_hr_payroll_private.kv2203_grp_m2fs,7,nach 12 VWGJ,4271.68,2026-01-01
kv2203_st_m2fs_8,l10n_at_hr_payroll_private.kv2203_grp_m2fs,8,nach 15 VWGJ,4605.01,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
kv2203_st_m3_2,l10n_at_hr_payroll_private.kv2203_grp_m3,2,nach 2 VWGJ,3542.72,2026-01-01
kv2203_st_m3_3,l10n_at_hr_payroll_private.kv2203_grp_m3,3,nach 4 VWGJ,3774.51,2026-01-01
kv2203_st_m3_4,l10n_at_hr_payroll_private.kv2203_grp_m3,4,nach 6 VWGJ,4006.27,2026-01-01
kv2203_st_m3_5,l10n_at_hr_payroll_private.kv2203_grp_m3,5,nach 8 VWGJ,4238.05,2026-01-01
kv2203_st_m3_6,l10n_at_hr_payroll_private.kv2203_grp_m3,6,nach 10 VWGJ,4469.81,2026-01-01
kv2203_st_m3_7,l10n_at_hr_payroll_private.kv2203_grp_m3,7,nach 12 VWGJ,4668.45,2026-01-01
kv2203_st_m3_8,l10n_at_hr_payroll_private.kv2203_grp_m3,8,nach 15 VWGJ,5032.68,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
kv2748_st_i_2,l10n_at_hr_payroll_private.kv2748_grp_i,2,nach 2 VWGJ,2058.43,2026-01-01
kv2748_st_i_3,l10n_at_hr_payroll_private.kv2748_grp_i,3,nach 4 VWGJ,2190.76,2026-01-01
kv2748_st_i_4,l10n_at_hr_payroll_private.kv2748_grp_i,4,nach 6 VWGJ,2323.12,2026-01-01
kv2748_st_i_5,l10n_at_hr_payroll_private.kv2748_grp_i,5,nach 8 VWGJ,2455.45,2026-01-01
kv2748_st_i_6,l10n_at_hr_payroll_private.kv2748_grp_i,6,nach 10 VWGJ,2588.72,2026-01-01
kv2748_st_i_7,l10n_at_hr_payroll_private.kv2748_grp_i,7,nach 12 VWGJ,2703.75,2026-01-01
kv2748_st_i_8,l10n_at_hr_payroll_private.kv2748_grp_i,8,nach 15 VWGJ,2914.69,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
kv2748_st_ii_2,l10n_at_hr_payroll_private.kv2748_grp_ii,2,nach 2 VWGJ,2350.62,2026-01-01
kv2748_st_ii_3,l10n_at_hr_payroll_private.kv2748_grp_ii,3,nach 4 VWGJ,2502.07,2026-01-01
kv2748_st_ii_4,l10n_at_hr_payroll_private.kv2748_grp_ii,4,nach 6 VWGJ,2655.45,2026-01-01
kv2748_st_ii_5,l10n_at_hr_payroll_private.kv2748_grp_ii,5,nach 8 VWGJ,2809.08,2026-01-01
kv2748_st_ii_6,l10n_at_hr_payroll_private.kv2748_grp_ii,6,nach 10 VWGJ,2962.72,2026-01-01
kv2748_st_ii_7,l10n_at_hr_payroll_private.kv2748_grp_ii,7,nach 12 VWGJ,3094.32,2026-01-01
kv2748_st_ii_8,l10n_at_hr_payroll_private.kv2748_grp_ii,8,nach 15 VWGJ,3335.74,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
kv2748_st_iii_2,l10n_at_hr_payroll_private.kv2748_grp_iii,2,nach 2 VWGJ,2918.34,2026-01-01
kv2748_st_iii_3,l10n_at_hr_payroll_private.kv2748_grp_iii,3,nach 4 VWGJ,3109.21,2026-01-01
kv2748_st_iii_4,l10n_at_hr_payroll_private.kv2748_grp_iii,4,nach 6 VWGJ,3300.16,2026-01-01
kv2748_st_iii_5,l10n_at_hr_payroll_private.kv2748_grp_iii,5,nach 8 VWGJ,3491.06,2026-01-01
kv2748_st_iii_6,l10n_at_hr_payroll_private.kv2748_grp_iii,6,nach 10 VWGJ,3682.00,2026-01-01
kv2748_st_iii_7,l10n_at_hr_payroll_private.kv2748_grp_iii,7,nach 12 VWGJ,3845.61,2026-01-01
kv2748_st_iii_8,l10n_at_hr_payroll_private.kv2748_grp_iii,8,nach 15 VWGJ,4145.62,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
kv2748_st_iv_2,l10n_at_hr_payroll_private.kv2748_grp_iv,2,nach 2 VWGJ,3654.05,2026-01-01
kv2748_st_iv_3,l10n_at_hr_payroll_private.kv2748_grp_iv,3,nach 4 VWGJ,3893.09,2026-01-01
kv2748_st_iv_4,l10n_at_hr_payroll_private.kv2748_grp_iv,4,nach 6 VWGJ,4132.15,2026-01-01
kv2748_st_iv_5,l10n_at_hr_payroll_private.kv2748_grp_iv,5,nach 8 VWGJ,4371.19,2026-01-01
kv2748_st_iv_6,l10n_at_hr_payroll_private.kv2748_grp_iv,6,nach 10 VWGJ,4610.26,2026-01-01
kv2748_st_iv_7,l10n_at_hr_payroll_private.kv2748_grp_iv,7,nach 12 VWGJ,4815.15,2026-01-01
kv2748_st_iv_8,l10n_at_hr_payroll_private.kv2748_grp_iv,8,nach 15 VWGJ,5190.77,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
kv2748_st_ivm_2,l10n_at_hr_payroll_private.kv2748_grp_ivm,2,nach 4 VWGJ,3654.05,2026-01-01
kv2748_st_ivm_3,l10n_at_hr_payroll_private.kv2748_grp_ivm,3,nach 6 VWGJ,3893.09,2026-01-01
kv2748_st_ivm_4,l10n_at_hr_payroll_private.kv2748_grp_ivm,4,nach 8 VWGJ,4132.15,2026-01-01
kv2748_st_ivm_5,l10n_at_hr_payroll_private.kv2748_grp_ivm,5,nach 10 VWGJ,4371.19,2026-01-01
kv2748_st_ivm_6,l10n_at_hr_payroll_private.kv2748_grp_ivm,6,nach 12 VWGJ,4610.26,2026-01-01
kv2748_st_ivm_7,l10n_at_hr_payroll_private.kv2748_grp_ivm,7,nach 14 VWGJ,4815.15,2026-01-01
kv2748_st_ivm_8,l10n_at_hr_payroll_private.kv2748_grp_ivm,8,nach 17 VWGJ,5047.10,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
kv2748_st_v_2,l10n_at_hr_payroll_private.kv2748_grp_v,2,nach 2 VWGJ,4594.04,2026-01-01
kv2748_st_v_3,l10n_at_hr_payroll_private.kv2748_grp_v,3,nach 4 VWGJ,4888.37,2026-01-01
kv2748_st_v_4,l10n_at_hr_payroll_private.kv2748_grp_v,4,nach 6 VWGJ,5182.71,2026-01-01
kv2748_st_v_5,l10n_at_hr_payroll_private.kv2748_grp_v,5,nach 8 VWGJ,5477.00,2026-01-01
kv2748_st_v_6,l10n_at_hr_payroll_private.kv2748_grp_v,6,nach 10 VWGJ,5771.34,2026-01-01
kv2748_st_v_7,l10n_at_hr_payroll_private.kv2748_grp_v,7,nach 12 VWGJ,6023.65,2026-01-01
kv2748_st_v_8,l10n_at_hr_payroll_private.kv2748_grp_v,8,nach 15 VWGJ,6486.15,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
kv2748_st_vom_2,l10n_at_hr_payroll_private.kv2748_grp_vom,2,nach 6 VWGJ,4594.04,2026-01-01
kv2748_st_vom_3,l10n_at_hr_payroll_private.kv2748_grp_vom,3,nach 8 VWGJ,4888.37,2026-01-01
kv2748_st_vom_4,l10n_at_hr_payroll_private.kv2748_grp_vom,4,nach 10 VWGJ,5182.71,2026-01-01
kv2748_st_vom_5,l10n_at_hr_payroll_private.kv2748_grp_vom,5,nach 12 VWGJ,5477.00,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
kv2748_st_vi_2,l10n_at_hr_payroll_private.kv2748_grp_vi,2,nach 2 VWGJ,6762.62,2026-01-01
kv2748_st_vi_3,l10n_at_hr_payroll_private.kv2748_grp_vi,3,nach 5 VWGJ,7503.50,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
kv2748_st_meister_2,l10n_at_hr_payroll_private.kv2748_grp_meister,2,nach 2 VWGJ,4130.76,2026-01-01
kv2748_st_meister_3,l10n_at_hr_payroll_private.kv2748_grp_meister,3,nach 4 VWGJ,4303.13,2026-01-01
kv2748_st_meister_4,l10n_at_hr_payroll_private.kv2748_grp_meister,4,nach 6 VWGJ,4475.48,2026-01-01
kv2748_st_meister_5,l10n_at_hr_payroll_private.kv2748_grp_meister,5,nach 8 VWGJ,4647.84,2026-01-01
kv2748_st_meister_6,l10n_at_hr_payroll_private.kv2748_grp_meister,6,nach 10 VWGJ,4820.17,2026-01-01
kv2748_st_meister_7,l10n_at_hr_payroll_private.kv2748_grp_meister,7,nach 12 VWGJ,4992.53,2026-01-01
kv2748_st_meister_8,l10n_at_hr_payroll_private.kv2748_grp_meister,8,nach 15 VWGJ,5270.29,2026-01-01
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,0
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,4
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,8
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,12
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,0
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,4
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,8
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,12
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,0
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,4
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,8
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,12
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,0
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,4
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,8
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,12
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,0
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,5
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,2
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,6
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,10
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,15
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,2
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,6
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,10
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,15
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,2
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,6
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,10
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,15
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,2
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,6
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,10
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,15
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,2
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,6
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,10
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,15
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,2
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,6
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,10
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,15
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,2
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,6
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,10
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,15
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,2
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,6
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,10
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,15
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,4
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,8
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,12
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,17
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,2
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,6
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,10
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,15
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,6
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,10
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,0
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,5
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,2
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,6
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,10
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,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)
]]></field>
</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>
@@ -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).
@@ -9,4 +9,7 @@ from . import account_chart_private
from . import account_posting_private
from . import mbgm
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
basis = self._l10n_at_sv_basis_roh(result_rules)
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:
# LZP liegt zur Gänze im beitragsfreien Erstfenster.
return 0.0
ende = min(self.date_to,
tage = (min(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:
return 0.0
return basis * tage / max(self._l10n_at_lohnsteuertage(), 1)
@@ -103,6 +103,10 @@ L10N_AT_BUCHUNGSMATRIX = {
'credit': ('3550',)},
'rule_dz': {'debit': ('6662',), 'debit_arbeiter': ('6602',),
'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.
Die Stufenlogik der Referenz-KVs läuft über Verwendungsgruppenjahre
(VWGJ, Biennal-/Triennalsprünge, § 17 der Rahmen-KVs); die Vorrückung
selbst ist GP2-Thema (Wizard analog GemBG), hier werden die
Vertragsfelder und der Katalog-Lookup bereitgestellt.
(VWGJ, Biennal-/Triennalsprünge, § 17 der Rahmen-KVs — verifiziert
SI-2203: Abs 3 Einreihung, Abs 4 Anzahl der angerechneten VWGJ durch
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):
@@ -31,6 +40,35 @@ class HrVersion(models.Model):
help="Individuell vereinbartes Monatsgehalt; überschreibt das "
"Kataloggehalt der Gruppe/Stufe (Günstigkeitsvergleich, "
"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(
'res.currency', related='company_id.currency_id')
l10n_at_personalgruppe = fields.Selection(
@@ -50,10 +88,73 @@ class HrVersion(models.Model):
self.ensure_one()
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):
_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(
related="version_id.kv_id", inherited=True,
readonly=False, groups="hr_payroll.group_hr_payroll_user")
@@ -176,6 +176,42 @@ class L10nAtPayrollKvGruppe(models.Model):
('gueltig_ab', '<=', datum)],
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):
"""Höchste Stufe mit gültigem Monatsgehalt zum Stichtag."""
self.ensure_one()
@@ -202,6 +238,13 @@ class L10nAtPayrollKvGruppeStufe(models.Model):
bezeichnung = fields.Char(
help="Stufen-Label der KV-Tabelle, z. B. 'im 1. u 2. 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')
currency_id = fields.Many2one(
'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:
- **KEIN DN/DG- oder Zweig-Split**: die Standardposition ``T01``
trägt den GESAMT-Prozentsatz des Tarifsystems (Parameter
``at_mbgm_tarif`` — BSGR-spezifisch; Werte aus dem ÖGK-
Beitragstarifsystem, `.additional_docs/oegk/`, jeweils
parametrisiert zu pflegen). Fehlende BSGR-Sätze brechen den Lauf
trägt den GESAMT-Prozentsatz des Tarifsystems. Quelle ist der
TASY-Katalog (AP0, Variante b): Beschäftigtengruppe + Ergänzungen
(ERGB am Vertrag) + Dienstgebergruppe (Company) → Tarifgruppe →
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).
- **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):
A01/A02/A03 = Minderung AV auf 2/1/0 % (§ 2a AMPFG, DN-seitig —
Abschlag = AB × (2,95 Zielsatz) %), A09 = UV-Entfall ab 60
@@ -171,27 +181,54 @@ class L10nAtPayrollMbgm(models.Model):
('state', 'in', ('validated', 'paid'))], limit=1)
def _l10n_at_bsgr(self, version, slip):
"""Beschäftigtengruppe (SV-Tarifsystem): B001 Arbeiter, B002
Angestellte; Lehrlinge B044/B045 und geringfügig/fallweise
(B010/B030) ohne Default-Parameter → Lauf bricht ab, bis
`at_mbgm_tarif` gepflegt ist (Tarifsystem-Werte, keine
Erfindung)."""
"""Beschäftigtengruppe (SV-Tarifsystem, TASY-Bezeichner):
B001 Arbeiter, B002 Angestellte; Lehrlinge B045
(Arbeiterlehrlinge) / B044 (Angestelltenlehrlinge);
geringfügig/fallweise B010 (Arbeiter) / B030 (Angestellte).
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:
return 'B044' if getattr(
version, 'l10n_at_personalgruppe', '') == 'arbeiter' \
else 'B045'
return 'B045' if personalgruppe == 'arbeiter' else 'B044'
if slip.l10n_at_geringfuegig or version.l10n_at_fallweise:
return 'B030'
return 'B001' if getattr(
version, 'l10n_at_personalgruppe', '') == 'arbeiter' \
else 'B002'
return 'B010' if personalgruppe == 'arbeiter' else 'B030'
return 'B001' if 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()
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(
'at_mbgm_tarif', self.beitragsmonat,
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):
"""Verrechnungspositionen je Tarifblock: T01 (Gesamtsatz,
@@ -202,7 +239,11 @@ class L10nAtPayrollMbgm(models.Model):
version = slip.version_id
ab = sum(basis for _m, basis
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
and tarif.get('quelle') != 'katalog'
and not version.l10n_at_lehrling
and getattr(version, 'l10n_at_personalgruppe',
'') == 'arbeiter') else 0.0
@@ -329,12 +370,12 @@ class L10nAtPayrollMbgm(models.Model):
anzahl += 1
continue
bsgr = deklaration._l10n_at_bsgr(version, slip)
tarif = deklaration._l10n_at_tarif(bsgr)
tarif = deklaration._l10n_at_tarif(bsgr, version)
if not tarif:
fehler.append(
'Mitarbeiter %s: kein Tarifsatz für BSGR '
'%s parametrisiert (at_mbgm_tarif, Werte '
'aus dem SV-Tarifsystem pflegen).'
'%s (weder TASY-Katalog noch Parameter '
'at_mbgm_tarif am Beitragsmonat).'
% (employee.name, bsgr))
continue
if slip.l10n_at_geringfuegig \
@@ -442,6 +483,12 @@ class L10nAtPayrollMbgm(models.Model):
bezeichnung='Tarifblock %s' % employee.name)
tarifblock.setze('SART', 'T1')
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')
saetze = [mbgm_satz, tarifblock]
for code, satz_prozent, rsum, basis_typ in positionen:
@@ -479,12 +526,39 @@ class ResCompany(models.Model):
_inherit = 'res.company'
l10n_at_nschab = fields.Boolean(
string='AT Schlechtwetterentschädigungsbeitrag-Branche',
help='Betrieb mit Schlechtwetterentschädigung (ÖGK-'
'Tarifsystem „Normalbetriebe mit '
'Schlechtwetterentschädigung“): Arbeiter (BSGR B001) '
'erhalten im mBGM-T01 den um NSchAB 3,80 % erhöhten '
'Gesamtsatz.')
string='AT Nachtschwerarbeitsbeitrags-Betrieb (mBGM-Fallback)',
help='Betrieb mit Nachtschwerarbeit (ÖGK-Tarifsystem '
'„Normalbetriebe mit Nachtschwerarbeit“, Dienstgebergruppe '
'NBNSA): ohne TASY-Katalog erhalten Arbeiter (BSGR B001) '
'im mBGM-T01 den um NSchAB 3,80 % erhöhten 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):
@@ -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>
@@ -5,4 +5,8 @@ from . import test_lst_private
from . import test_sv_private
from . import test_bv_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)."""
for code in ('6000', '6200', '6225', '6230', '6240', '6310',
'6440', '6500', '6560', '6600', '6601', '6602',
'6660', '6661', '6662'):
'6660', '6661', '6662', '6663'):
if code not in ohne:
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)
if '3850' not in ohne:
self._konto('3850', 'liability_payable', reconcile=True)
@@ -276,4 +276,33 @@ class AccountPrivateTest(TransactionCase):
'SVDG_KV', 'SVDG_PV', 'SVDG_AV', 'SVDG_UV', 'SVDG_WF',
'SVDG_IESG', 'SVDG_SZ_KV', 'SVDG_SZ_PV', 'SVDG_SZ_AV',
'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.
"""GP7 privat: mBGM (Paket/mBGM/Tarifblock/Basis/Position-Hierarchie
aus GP4-Beitragssummen, T01-Gesamtsatz aus at_mbgm_tarif, Abschläge
A01/A02/A03 §-2a-AMPFG, A09/A10/A15, BV-Basis V01) und
aus GP4-Beitragssummen, T01-Gesamtsatz aus TASY-Katalog vor
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,
Meldungs-Registry, AGRD)."""
import base64
@@ -10,6 +11,10 @@ import datetime
from odoo.exceptions import UserError
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')
class MeldewesenPrivateTest(TransactionCase):
@@ -165,10 +170,16 @@ class MeldewesenPrivateTest(TransactionCase):
})
with self.assertRaises(UserError):
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({
'contract_date_end': datetime.date(2026, 2, 15),
'departure_reason_id': self.env['hr.departure.reason']
.search([], limit=1).id,
'departure_reason_id': grund_ohne.id,
})
dritter = self.env['l10n.at.payroll.vr'].create({
'company_id': self.company.id,
@@ -176,8 +187,134 @@ class MeldewesenPrivateTest(TransactionCase):
})
dritter.action_erzeugen()
roh = base64.b64decode(dritter.datei).decode('ISO-8859-15')
zeilen = roh.strip('\r\n').split('\r\n')
m4 = [z for z in zeilen if z[0:2] == 'M4']
self.assertEqual(len(m4), 1)
self.assertEqual(m4[0][512:520], '15022026') # ADAT
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][512:520], '15022026') # ADAT
self.assertEqual(m4[0][548:550], '00') # AGRD Default
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"/>
<field name="ueberzahlung"
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>
</field>
@@ -23,7 +23,7 @@
</field>
</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">
<field name="name">res.company.form.l10n.at.nschab</field>
<field name="model">res.company</field>
@@ -31,6 +31,8 @@
ref="l10n_at_hr_payroll.res_company_view_form_l10n_at"/>
<field name="arch" type="xml">
<group name="l10n_at_abgaben" position="inside">
<field name="l10n_at_dienstgebergruppe"
placeholder="z. B. NBALG"/>
<field name="l10n_at_nschab"/>
</group>
</field>
@@ -22,6 +22,9 @@
<group string="Status &amp; Abrechnung">
<field name="l10n_at_lehrling"/>
<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_ueberw_nachtarbeit"/>
</group>