[ADD] l10n_at_hr_payroll_private: KV framework (GP1)
GP1 of the approved General-AT implementation plan: the KV framework for the Austrian private economy on the Odoo 19 hr_payroll engine. - l10n.at.payroll.kv: collective agreement master data with chamber, union, validity date and a trace link to the KV library (personalverrechnung/quellen/kv/) - l10n.at.payroll.kv.wert: versioned KV parameters following the hr.rule.parameter.value pattern (Python literal, unique per KV/code/date); consumed via employee.kv_id._wert(code, date) - l10n.at.payroll.kv.gruppe / .stufe: Verwendungsgruppen with Erfahrungsstufen year tables, unique(gruppe, stufe, gueltig_ab), Stichtag lookup - hr.version / hr.employee contract fields: kv_id, kv_gruppe_id, erfahrungsstufe, ueberzahlung - import wizard for KV intake and yearly updates: table paste (Gruppe;Stufe;Gehalt[;Bezeichnung]), preview, >10% jump warning (warn lines never applied automatically), new gueltig_ab versions, historical values untouched - seed D2 (SI-2203 Handwerk und Gewerbe, SI-2748 Metallgewerbe): wage tables from 1. 1. 2026 curated from the KV library texts - 18 groups, 131 stage values, 27 parameters, including the special progression groups IV-M / V-OM (SI-2748) and the discontinued VG I (SI-2203) - tests: test_kv_katalog (seed lookups, Stichtag behaviour, versioning, parameters, wizard, contract fields) Statically validated (py_compile, XML well-formedness, CSV consistency, wizard logic simulation); runtime acceptance pending PostgreSQL on the dev host.
This commit is contained in:
@@ -1,20 +1,53 @@
|
||||
# l10n_at_hr_payroll_private (Gerüst)
|
||||
# l10n_at_hr_payroll_private (General-AT)
|
||||
|
||||
General-AT-Payroll (KV-basierte Privatwirtschaft: KV-Gehaltskataloge,
|
||||
AZG/UrlG-Arbeitszeit, 13./14. Bezüge, Abfertigung Neu).
|
||||
AZG/UrlG-Arbeitszeit, 13./14. Bezüge, Abfertigung Neu) für Odoo 19.
|
||||
|
||||
**Implementierungsplan (freigegeben, M0):**
|
||||
[`personalverrechnung/IMPLEMENTIERUNGSPLAN-Privat.md`](../../personalverrechnung/IMPLEMENTIERUNGSPLAN-Privat.md)
|
||||
· verbindliche Rechtslage:
|
||||
[`personalverrechnung/RECHTSQUELLEN-Privat.md`](../../personalverrechnung/RECHTSQUELLEN-Privat.md)
|
||||
|
||||
Umsetzung startet mit **M1 (GP0)**: `RECHTSQUELLEN-Privat.md`,
|
||||
RIS-/findok-Verifikationen, Odoo-API-Neuverifikation,
|
||||
Pilotmandant-Definition, UGB-Buchungsmatrix. Kein Manifest/Code
|
||||
bis M2 (GP1).
|
||||
**GP1 umgesetzt (KV-Framework):**
|
||||
|
||||
**Kern-Vorfinanzierung (D3):** AP3 (Lohnsteuer) und AP4 (SV/DB-Regeln)
|
||||
werden im Kern-Modul `l10n_at_hr_payroll` auf General-AT-Kosten gebaut
|
||||
und von beiden Produkten konsumiert (die GemBG-Structure erbt);
|
||||
bei Bgld.-Auftragserteilung Anrechnung/Umlegung per Change-Request.
|
||||
- `l10n.at.payroll.kv` — KV-Stammdaten mit Trace-Link zur KV-Library
|
||||
(`library_variant_id` → `personalverrechnung/quellen/kv/kv-catalog.json`)
|
||||
und Quellen-URL der Arbeitgeberkammer;
|
||||
- `l10n.at.payroll.kv.wert` — versionierte KV-Eckwerte (Muster
|
||||
`hr.rule.parameter.value`, KV-Scope): Regelkonsum über
|
||||
`employee.kv_id._wert('code', payslip.date_to)`;
|
||||
- `l10n.at.payroll.kv.gruppe` / `.stufe` — Verwendungs-/Lohngruppen mit
|
||||
Erfahrungsstufen, Jahrestabellen je `gueltig_ab`
|
||||
(unique(Gruppe, Stufe, Stichtag));
|
||||
- Vertragsfelder auf `hr.version` (durchgereicht auf `hr.employee`):
|
||||
`kv_id`, `kv_gruppe_id`, `erfahrungsstufe`, `ueberzahlung`;
|
||||
- Import-Wizard „KV-Katalog importieren": Tabellenpaste/CSV
|
||||
(Gruppe;Stufe;Gehalt[;Bezeichnung]) mit Vorschau, Sprungwarnung
|
||||
> 10 % (Warnzeilen werden nie automatisch übernommen) und Versionierung
|
||||
über `gueltig_ab` — historische Werte bleiben unangetastet.
|
||||
|
||||
**Referenz-KVs R1 (D2):** SI-2203 Handwerk und Gewerbe +
|
||||
SI-2748 Metallgewerbe (Feinjustierung je Pilotmandant in GP0).
|
||||
**Seed D2 (Referenz-KVs R1):** SI-2203 „Handwerk und Gewerbe" +
|
||||
SI-2748 „Metallgewerbe", Gehaltstabellen ab 1. 1. 2026 kuratiert aus den
|
||||
KV-Library-Volltexten (`quellen/kv/oegb/texts/`). Besonderheiten im Seed:
|
||||
|
||||
- SI-2203: VG I ist ab 1. 1. 2026 aufgelassen (Vorrückung nach § 17 Abs 6);
|
||||
Meistergruppen M I–M III als eigene Gruppen (M II ohne/mit Fachschule);
|
||||
- SI-2748: eigene Vorrückungsverläufe als eigene Gruppen (IV-M = Meister
|
||||
in VG IV bis „nach 17 VWGJ", V-OM = Obermeister bis „nach 12 VWGJ");
|
||||
Nacht-Sondervergütung 2,71 € (2026) bzw. 2,76 € (2027) als
|
||||
Jahresversionen.
|
||||
|
||||
**Kern-Vorfinanzierung (D3):** Lohnsteuer (AP3) und SV/DB-Regeln (AP4)
|
||||
werden im Kern-Modul `l10n_at_hr_payroll` gebaut und von beiden
|
||||
Produkten konsumiert; bei Bgld.-Auftragserteilung Anrechnung per
|
||||
Change-Request.
|
||||
|
||||
**Offen (GP2+):** Entgelt-Engine (KVGEO, AZG-Zuschläge, B13/B14,
|
||||
Urlaubsentgelt), Lohnsteuer/SV, Abfertigung Neu, Meldewesen/Belege,
|
||||
FI-Buchung UGB.
|
||||
|
||||
Tests: `test_kv_katalog` (Seed, Stichtags-Lookup, Versionierung,
|
||||
Eckwerte, Wizard/Sprungwarnung, Vertragsfelder) —
|
||||
`python3 odoo/odoo-bin -d <testdb> -i l10n_at_hr_payroll_private
|
||||
--test-tags /l10n_at_hr_payroll_private --test-enable --stop-after-init
|
||||
--addons-path=odoo_enterprise/odoo/addons,addons`
|
||||
@@ -0,0 +1,3 @@
|
||||
# Part of the odoo-at-payroll project. License: LGPL-3.
|
||||
from . import models
|
||||
from . import wizard
|
||||
@@ -0,0 +1,49 @@
|
||||
# Part of the odoo-at-payroll project. License: LGPL-3.
|
||||
{
|
||||
'name': 'Österreich - Privatwirtschaft Personalverrechnung (General-AT)',
|
||||
'version': '19.0.1.0.0',
|
||||
'author': 'ixsol / gem360',
|
||||
'website': 'https://gem360.at',
|
||||
'category': 'Human Resources/Payroll',
|
||||
'description': """
|
||||
General-AT-Personalverrechnung (KV-basierte Privatwirtschaft)
|
||||
==============================================================
|
||||
|
||||
Monatsverrechnung für Beschäftigte der österreichischen Privatwirtschaft
|
||||
nach dem **KV-Regime** auf der Odoo-19 ``hr_payroll``-Engine. Fachliche
|
||||
Grundlagen: ``personalverrechnung/IMPLEMENTIERUNGSPLAN-Privat.md`` und
|
||||
``personalverrechnung/RECHTSQUELLEN-Privat.md`` (GP0-Verifikationen).
|
||||
|
||||
- **GP1 KV-Framework (dieser Stand):** Kollektivvertrags-Stammdaten
|
||||
(``l10n.at.payroll.kv``) mit Trace-Link zur KV-Library, versionierte
|
||||
KV-Eckwerte (``kv.wert`` — Muster ``hr.rule.parameter.value``,
|
||||
Konsum über ``employee.kv_id._wert(code, datum)``), Verwendungs-/
|
||||
Lohngruppen mit Erfahrungsstufen (``kv.gruppe`` / ``kv.gruppe.stufe``,
|
||||
Jahrestabellen je ``gueltig_ab``), KV-Einstufung am Vertrag
|
||||
(``hr.version``: KV, Gruppe, Erfahrungsstufe, Überzahlung) sowie
|
||||
ein Import-Wizard für KV-Aufnahme und Jahresupdate (Vorschau,
|
||||
Sprungwarnung > 10 %, versionierte Werte — TASY-Muster).
|
||||
- **Seed D2 (Referenz-KVs R1):** SI-2203 „Handwerk und Gewerbe" +
|
||||
SI-2748 „Metallgewerbe" (Gehaltstabellen ab 1. 1. 2026, kuratiert aus
|
||||
den KV-Library-Volltexten).
|
||||
- **Folgen (GP2+):** Entgelt-Engine (KVGEO, AZG-Zuschläge, 13./14.
|
||||
Bezüge), Lohnsteuer/SV im Bundes-Kern (D3-Vorfinanzierung),
|
||||
Abfertigung Neu, Meldewesen/Belege, FI-Buchung UGB.
|
||||
|
||||
Kein VRV-Bezug (Gemeinde-Schiene bleibt ``l10n_at_gemeinde_payroll`` +
|
||||
gem360-Brücke).
|
||||
""",
|
||||
'depends': ['l10n_at_hr_payroll', 'hr_payroll_account'],
|
||||
'data': [
|
||||
'security/ir.model.access.csv',
|
||||
'data/kv_seed.xml',
|
||||
'data/l10n.at.payroll.kv.gruppe.csv',
|
||||
'data/l10n.at.payroll.kv.gruppe.stufe.csv',
|
||||
'data/l10n.at.payroll.kv.wert.csv',
|
||||
'views/kv_views.xml',
|
||||
],
|
||||
'countries': ['at'],
|
||||
'license': 'LGPL-3',
|
||||
'installable': True,
|
||||
'auto_install': False,
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<!-- Referenz-KVs R1 (Plan-Entscheidung D2), kuratiert aus den
|
||||
KV-Library-Volltexten (Gehaltstabellen der Lohnrunde ab
|
||||
1. 1. 2026). Gruppen/Stufen/Werte siehe CSV-Datendateien. -->
|
||||
<record id="kv_si_2203" model="l10n.at.payroll.kv">
|
||||
<field name="name">Handwerk und Gewerbe (Angestellte)</field>
|
||||
<field name="code">SI-2203</field>
|
||||
<field name="arbeitgeberkammer">wko</field>
|
||||
<field name="gewerkschaft">GPA</field>
|
||||
<field name="gueltig_ab">2026-01-01</field>
|
||||
<field name="library_variant_id">SI-2203_de</field>
|
||||
<field name="quelle_url">https://www.wko.at/kollektivvertrag/kollektivvertrag-gewerbe-handwerk-und-dienstleistung-2026</field>
|
||||
</record>
|
||||
<record id="kv_si_2748" model="l10n.at.payroll.kv">
|
||||
<field name="name">Metallgewerbe (Angestellte)</field>
|
||||
<field name="code">SI-2748</field>
|
||||
<field name="arbeitgeberkammer">wko</field>
|
||||
<field name="gewerkschaft">GPA</field>
|
||||
<field name="gueltig_ab">2026-01-01</field>
|
||||
<field name="library_variant_id">SI-2748_de</field>
|
||||
<field name="quelle_url">https://www.wko.at/kollektivvertrag/metallgewerbe-kollektivvertrag-angestellte</field>
|
||||
</record>
|
||||
</odoo>
|
||||
@@ -0,0 +1,19 @@
|
||||
id,kv_id:id,code,name,lohn_basis
|
||||
kv2203_grp_ii,l10n_at_hr_payroll_private.kv_si_2203,II,Verwendungsgruppe II,monatlich
|
||||
kv2203_grp_iii,l10n_at_hr_payroll_private.kv_si_2203,III,Verwendungsgruppe III,monatlich
|
||||
kv2203_grp_iv,l10n_at_hr_payroll_private.kv_si_2203,IV,Verwendungsgruppe IV,monatlich
|
||||
kv2203_grp_v,l10n_at_hr_payroll_private.kv_si_2203,V,Verwendungsgruppe V,monatlich
|
||||
kv2203_grp_vi,l10n_at_hr_payroll_private.kv_si_2203,VI,Verwendungsgruppe VI,monatlich
|
||||
kv2203_grp_m1,l10n_at_hr_payroll_private.kv_si_2203,M1,Meistergruppe M I (Hilfsmeister / Betriebsaufseher),monatlich
|
||||
kv2203_grp_m2,l10n_at_hr_payroll_private.kv_si_2203,M2,Meistergruppe M II (Meister ohne Fachschule),monatlich
|
||||
kv2203_grp_m2fs,l10n_at_hr_payroll_private.kv_si_2203,M2FS,Meistergruppe M II (Meister mit Fachschule),monatlich
|
||||
kv2203_grp_m3,l10n_at_hr_payroll_private.kv_si_2203,M3,Meistergruppe M III (Obermeister),monatlich
|
||||
kv2748_grp_i,l10n_at_hr_payroll_private.kv_si_2748,I,Verwendungsgruppe I,monatlich
|
||||
kv2748_grp_ii,l10n_at_hr_payroll_private.kv_si_2748,II,Verwendungsgruppe II,monatlich
|
||||
kv2748_grp_iii,l10n_at_hr_payroll_private.kv_si_2748,III,Verwendungsgruppe III,monatlich
|
||||
kv2748_grp_iv,l10n_at_hr_payroll_private.kv_si_2748,IV,Verwendungsgruppe IV,monatlich
|
||||
kv2748_grp_ivm,l10n_at_hr_payroll_private.kv_si_2748,IV-M,Verwendungsgruppe IV - Meister (eigener Vorrückungsverlauf),monatlich
|
||||
kv2748_grp_v,l10n_at_hr_payroll_private.kv_si_2748,V,Verwendungsgruppe V,monatlich
|
||||
kv2748_grp_vom,l10n_at_hr_payroll_private.kv_si_2748,V-OM,Verwendungsgruppe V - Obermeister (eigener Vorrückungsverlauf),monatlich
|
||||
kv2748_grp_vi,l10n_at_hr_payroll_private.kv_si_2748,VI,Verwendungsgruppe VI,monatlich
|
||||
kv2748_grp_meister,l10n_at_hr_payroll_private.kv_si_2748,MEISTER,Meistergruppe,monatlich
|
||||
|
@@ -0,0 +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
|
||||
|
@@ -0,0 +1,28 @@
|
||||
id,kv_id:id,code,name,parameter_value,date_from
|
||||
kv2203_wert_naz,l10n_at_hr_payroll_private.kv_si_2203,naz,Normalarbeitszeit (Wochenstunden - § 4 Abs 1),40,2026-01-01
|
||||
kv2203_wert_teilungsfaktor_ue,l10n_at_hr_payroll_private.kv_si_2203,teilungsfaktor_ue,Teilungsfaktor Überstunde (1/x des Monatsgehalts - § 5 Abs 6),150,2026-01-01
|
||||
kv2203_wert_prorate,l10n_at_hr_payroll_private.kv_si_2203,prorate_divisor,Aliquoter Divisor Ein-/Austritt (§ 17 Abs 2),26,2026-01-01
|
||||
kv2203_wert_ue_tag,l10n_at_hr_payroll_private.kv_si_2203,ue_zuschlag_tag,Überstundenzuschlag Tag (%),50,2026-01-01
|
||||
kv2203_wert_ue_nacht,l10n_at_hr_payroll_private.kv_si_2203,ue_zuschlag_nacht,Überstundenzuschlag 20-6 Uhr (%),100,2026-01-01
|
||||
kv2203_wert_sofei,l10n_at_hr_payroll_private.kv_si_2203,so_feiertag_zuschlag,Zuschlag Sonn-/Feiertagsüberstunden (%),100,2026-01-01
|
||||
kv2203_wert_nacht_eur,l10n_at_hr_payroll_private.kv_si_2203,nacht_sonderverguetung,Sondervergütung Nachtarbeit 22-6 Uhr (EUR/h - § 6 Abs 1),2.62,2026-01-01
|
||||
kv2203_wert_sz13_monat,l10n_at_hr_payroll_private.kv_si_2203,sz13_monat,Auszahlmonat 13. Bezug (spätestens 30. 11.),11,2026-01-01
|
||||
kv2203_wert_sz14_faelligkeit,l10n_at_hr_payroll_private.kv_si_2203,sz14_faelligkeit,Fälligkeit 14. Bezug (§ 11 Abs 3),'urlaub',2026-01-01
|
||||
kv2203_wert_sz13_name,l10n_at_hr_payroll_private.kv_si_2203,sz13_name,Bezeichnung 13. Bezug,'Weihnachtsremuneration',2026-01-01
|
||||
kv2203_wert_sz14_name,l10n_at_hr_payroll_private.kv_si_2203,sz14_name,Bezeichnung 14. Bezug,'Urlaubszuschuss',2026-01-01
|
||||
kv2203_wert_ue_hoechster,l10n_at_hr_payroll_private.kv_si_2203,ue_hoechster_zuschlag,Bei Zuschlagskumulation nur der höchste Zuschlag (§ 5 Abs 7),True,2026-01-01
|
||||
kv2748_wert_naz,l10n_at_hr_payroll_private.kv_si_2748,naz,Normalarbeitszeit (Wochenstunden - § 4 Abs 1),38.5,2026-01-01
|
||||
kv2748_wert_teilungsfaktor_ue,l10n_at_hr_payroll_private.kv_si_2748,teilungsfaktor_ue,Teilungsfaktor Überstunde (1/x des Monatsgehalts - § 5 Abs 6),143,2026-01-01
|
||||
kv2748_wert_teilungsfaktor_norm,l10n_at_hr_payroll_private.kv_si_2748,teilungsfaktor_norm,Teilungsfaktor Normalarbeitsstunde/Mehrarbeit (§ 5 Abs 6),167,2026-01-01
|
||||
kv2748_wert_mehrarbeit,l10n_at_hr_payroll_private.kv_si_2748,mehrarbeit_zuschlag,Mehrarbeit Zuschlag § 4a (Stunden zwischen NAZ und 40),50,2026-01-01
|
||||
kv2748_wert_prorate,l10n_at_hr_payroll_private.kv_si_2748,prorate_divisor,Aliquoter Divisor Ein-/Austritt (§ 17 Abs 2),26,2026-01-01
|
||||
kv2748_wert_ue_tag,l10n_at_hr_payroll_private.kv_si_2748,ue_zuschlag_tag,Überstundenzuschlag Tag (%),50,2026-01-01
|
||||
kv2748_wert_ue_nacht,l10n_at_hr_payroll_private.kv_si_2748,ue_zuschlag_nacht,Überstundenzuschlag 20-6 Uhr bzw. 3.+ ÜStd nach 19 Uhr (%),100,2026-01-01
|
||||
kv2748_wert_sofei,l10n_at_hr_payroll_private.kv_si_2748,so_feiertag_zuschlag,Zuschlag Sonn-/Feiertagsüberstunden (%),100,2026-01-01
|
||||
kv2748_wert_nacht_eur,l10n_at_hr_payroll_private.kv_si_2748,nacht_sonderverguetung,Sondervergütung Nachtarbeit 22-6 Uhr (EUR/h - § 6),2.71,2026-01-01
|
||||
kv2748_wert_nacht_eur_2027,l10n_at_hr_payroll_private.kv_si_2748,nacht_sonderverguetung,Sondervergütung Nachtarbeit 22-6 Uhr (EUR/h - § 6),2.76,2027-01-01
|
||||
kv2748_wert_sz13_monat,l10n_at_hr_payroll_private.kv_si_2748,sz13_monat,Auszahlmonat 13. Bezug (spätestens 1. 12.),12,2026-01-01
|
||||
kv2748_wert_sz14_faelligkeit,l10n_at_hr_payroll_private.kv_si_2748,sz14_faelligkeit,Fälligkeit 14. Bezug (§ 11 Abs 3),'urlaub',2026-01-01
|
||||
kv2748_wert_sz13_name,l10n_at_hr_payroll_private.kv_si_2748,sz13_name,Bezeichnung 13. Bezug,'13. Monatsgehalt (Weihnachtsremuneration)',2026-01-01
|
||||
kv2748_wert_sz14_name,l10n_at_hr_payroll_private.kv_si_2748,sz14_name,Bezeichnung 14. Bezug,'14. Monatsgehalt (Urlaubszuschuss)',2026-01-01
|
||||
kv2748_wert_ue_hoechster,l10n_at_hr_payroll_private.kv_si_2748,ue_hoechster_zuschlag,Bei Zuschlagskumulation nur der höchste Zuschlag (§ 5 Abs 7),True,2026-01-01
|
||||
|
@@ -0,0 +1,3 @@
|
||||
# Part of the odoo-at-payroll project. License: LGPL-3.
|
||||
from . import kollektivvertrag
|
||||
from . import hr_version_private
|
||||
@@ -0,0 +1,58 @@
|
||||
# Part of the odoo-at-payroll project. License: LGPL-3.
|
||||
"""KV-Einstufung am Arbeitsvertrag (hr.version) und Durchreichung auf
|
||||
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.
|
||||
"""
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class HrVersion(models.Model):
|
||||
_inherit = 'hr.version'
|
||||
|
||||
kv_id = fields.Many2one(
|
||||
'l10n.at.payroll.kv', string='Kollektivvertrag', index=True,
|
||||
groups="hr_payroll.group_hr_payroll_user", tracking=True)
|
||||
kv_gruppe_id = fields.Many2one(
|
||||
'l10n.at.payroll.kv.gruppe', string='Verwendungsgruppe', index=True,
|
||||
groups="hr_payroll.group_hr_payroll_user", tracking=True)
|
||||
erfahrungsstufe = fields.Integer(
|
||||
string='Erfahrungsstufe',
|
||||
groups="hr_payroll.group_hr_payroll_user", tracking=True,
|
||||
help="Stufenindex der KV-Gehaltstabelle (1 = unterste Stufe, z. B. "
|
||||
"'im 1. u 2. Verwendungsgruppenjahr').")
|
||||
ueberzahlung = fields.Monetary(
|
||||
string='Überzahlung (individuelles Gehalt)',
|
||||
currency_field='currency_id',
|
||||
groups="hr_payroll.group_hr_payroll_user", tracking=True,
|
||||
help="Individuell vereinbartes Monatsgehalt; überschreibt das "
|
||||
"Kataloggehalt der Gruppe/Stufe (Günstigkeitsvergleich, "
|
||||
"z. B. § 18 Anrechnung der Rahmen-KVs).")
|
||||
currency_id = fields.Many2one(
|
||||
'res.currency', related='company_id.currency_id')
|
||||
|
||||
def _get_kv_gehalt(self, datum=None):
|
||||
"""Katalog-Position (Monatsgehalt) der Version zum Stichtag."""
|
||||
self.ensure_one()
|
||||
return self.kv_gruppe_id._get_gehalt(self.erfahrungsstufe, datum)
|
||||
|
||||
|
||||
class HrEmployee(models.Model):
|
||||
_inherit = 'hr.employee'
|
||||
|
||||
kv_id = fields.Many2one(
|
||||
related="version_id.kv_id", inherited=True,
|
||||
readonly=False, groups="hr_payroll.group_hr_payroll_user")
|
||||
kv_gruppe_id = fields.Many2one(
|
||||
related="version_id.kv_gruppe_id", inherited=True,
|
||||
readonly=False, groups="hr_payroll.group_hr_payroll_user")
|
||||
erfahrungsstufe = fields.Integer(
|
||||
related="version_id.erfahrungsstufe", inherited=True,
|
||||
readonly=False, groups="hr_payroll.group_hr_payroll_user")
|
||||
ueberzahlung = fields.Monetary(
|
||||
related="version_id.ueberzahlung", inherited=True,
|
||||
readonly=False, groups="hr_payroll.group_hr_payroll_user",
|
||||
currency_field='currency_id')
|
||||
@@ -0,0 +1,225 @@
|
||||
# Part of the odoo-at-payroll project. License: LGPL-3.
|
||||
"""KV-Katalogmodelle (GP1): Kollektivvertrag, versionierte KV-Eckwerte,
|
||||
Verwendungs-/Lohngruppen mit Erfahrungsstufen.
|
||||
|
||||
Datenbasis ist die KV-Library (``personalverrechnung/quellen/kv/``);
|
||||
die Seed-Kataloge der Referenz-KVs SI-2203/SI-2748 (Plan-Entscheidung
|
||||
D2) sind aus den Volltexten kuratiert. Eckwerte folgen dem Muster
|
||||
``hr.rule.parameter.value`` (Python-Literal, Versionierung über den
|
||||
Stichtag), sind aber KV-scoped und werden daher bewusst nicht über
|
||||
``payslip._rule_parameter`` konsumiert, sondern über
|
||||
``employee.kv_id._wert('code', datum)``.
|
||||
"""
|
||||
from odoo import _, api, fields, models
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.tools.safe_eval import safe_eval
|
||||
|
||||
|
||||
class L10nAtPayrollKv(models.Model):
|
||||
"""Kollektivvertrag als Entgelt-Rechtsquelle der Privatwirtschaft.
|
||||
|
||||
Normative Wirkung über ArbVG § 12; Quellenpolitik der KV-Library:
|
||||
Main Source = Arbeitgeberkammer, kollektivvertrag.at als
|
||||
Crosscheck-Baseline.
|
||||
"""
|
||||
_name = 'l10n.at.payroll.kv'
|
||||
_description = 'Kollektivvertrag (AT Privatwirtschaft)'
|
||||
_order = 'name'
|
||||
|
||||
name = fields.Char(required=True)
|
||||
code = fields.Char(
|
||||
required=True, index=True,
|
||||
help="Kurzbezeichnung, üblicherweise die KV-Library-Variante "
|
||||
"(z. B. SI-2203).")
|
||||
arbeitgeberkammer = fields.Selection([
|
||||
('wko', 'Wirtschaftskammer Österreich (WKO)'),
|
||||
('lwk', 'Landwirtschaftskammer'),
|
||||
('aerztekammer', 'Ärztekammer'),
|
||||
('apotheke', 'Apothekerkammer'),
|
||||
('rechtsanwaltskammer', 'Rechtsanwaltskammer'),
|
||||
('notariatskammer', 'Notariatskammer'),
|
||||
])
|
||||
gewerkschaft = fields.Char(
|
||||
help="Verantwortliche Gewerkschaft(en), z. B. GPA; mehrere "
|
||||
"kommagetrennt.")
|
||||
gueltig_ab = fields.Date(
|
||||
string='Fassung gültig ab',
|
||||
help="Gültigkeit der hinterlegten KV-Fassung (Lohnrunde), z. B. "
|
||||
"2026-01-01 für die Gehaltstabellen ab 1. 1. 2026.")
|
||||
library_variant_id = fields.Char(
|
||||
string='KV-Library-Variante',
|
||||
help="Trace-Link: variant_id im Katalog "
|
||||
"personalverrechnung/quellen/kv/kv-catalog.json.")
|
||||
quelle_url = fields.Char(
|
||||
string='Quelle (Arbeitgeberkammer)',
|
||||
help="Veröffentlichung der zuständigen Arbeitgeberkammer "
|
||||
"(Main Source gemäß KV-Library-Quellenarchitektur).")
|
||||
active = fields.Boolean(default=True)
|
||||
gruppe_ids = fields.One2many(
|
||||
'l10n.at.payroll.kv.gruppe', 'kv_id', string='Verwendungsgruppen')
|
||||
wert_ids = fields.One2many(
|
||||
'l10n.at.payroll.kv.wert', 'kv_id', string='KV-Eckwerte')
|
||||
|
||||
_unique_kv = models.Constraint(
|
||||
'unique (code)',
|
||||
"Ein Kollektivvertrag existiert je Code nur einmal.")
|
||||
|
||||
def _wert(self, code, datum=None, raise_if_not_found=True):
|
||||
"""Eckwert zum Stichtag (z. B. 'naz', 'sz13_monat').
|
||||
|
||||
Rückgabe wie hr.rule.parameter: Python-Literal aus
|
||||
parameter_value (Zahl, String oder Boolean).
|
||||
"""
|
||||
self.ensure_one()
|
||||
datum = datum or fields.Date.context_today(self)
|
||||
wert = self.env['l10n.at.payroll.kv.wert'].search([
|
||||
('kv_id', '=', self.id),
|
||||
('code', '=', code),
|
||||
('date_from', '<=', datum)], order='date_from desc', limit=1)
|
||||
if wert:
|
||||
return safe_eval(wert.parameter_value)
|
||||
if raise_if_not_found:
|
||||
raise UserError(_(
|
||||
"Kein KV-Eckwert %(code)s für %(kv)s zum %(datum)s "
|
||||
"gefunden.", code=code, kv=self.display_name,
|
||||
datum=datum))
|
||||
return None
|
||||
|
||||
@api.depends('name', 'code')
|
||||
def _compute_display_name(self):
|
||||
for kv in self:
|
||||
kv.display_name = f"{kv.name} ({kv.code})" if kv.code else kv.name
|
||||
|
||||
|
||||
class L10nAtPayrollKvWert(models.Model):
|
||||
"""Versionierter KV-Eckwert (Muster: hr.rule.parameter.value)."""
|
||||
_name = 'l10n.at.payroll.kv.wert'
|
||||
_description = 'KV-Eckwert (versioniert)'
|
||||
_order = 'kv_id, code, date_from desc'
|
||||
|
||||
kv_id = fields.Many2one(
|
||||
'l10n.at.payroll.kv', required=True, index=True, ondelete='cascade')
|
||||
code = fields.Char(
|
||||
required=True, index=True,
|
||||
help="Code für den Regelkonsum, z. B. 'naz' "
|
||||
"(Normalarbeitszeit), 'teilungsfaktor_ue', "
|
||||
"'sz13_monat', 'sz14_faelligkeit'.")
|
||||
name = fields.Char(string='Bezeichnung')
|
||||
parameter_value = fields.Text(
|
||||
required=True,
|
||||
help="Python-Literal (Zahl, String oder Boolean) — Muster "
|
||||
"hr.rule.parameter.value.")
|
||||
date_from = fields.Date(
|
||||
string='Gültig ab', required=True, index=True,
|
||||
default=lambda self: fields.Date.today().replace(month=1, day=1))
|
||||
|
||||
_unique_kv_wert = models.Constraint(
|
||||
'unique (kv_id, code, date_from)',
|
||||
"Je KV, Code und Stichtag gibt es nur einen Wert.")
|
||||
|
||||
@api.constrains('parameter_value')
|
||||
def _check_parameter_value(self):
|
||||
for wert in self:
|
||||
try:
|
||||
safe_eval(wert.parameter_value)
|
||||
except Exception:
|
||||
raise UserError(_(
|
||||
"Ungültiger KV-Eckwert für %(code)s: "
|
||||
"Python-Literal (Zahl, 'String' oder Boolean) erwartet.",
|
||||
code=wert.code))
|
||||
|
||||
|
||||
class L10nAtPayrollKvGruppe(models.Model):
|
||||
"""Verwendungs-/Lohngruppe eines Kollektivvertrages (z. B. VG III).
|
||||
|
||||
Die Monatsgehälter je Erfahrungsstufe stehen auf
|
||||
``l10n.at.payroll.kv.gruppe.stufe`` (Jahreswerte, ein Wert je
|
||||
Stichtag). Angestellte/Arbeiter-Unterschiede laufen über die
|
||||
``lohn_basis`` und die KV-Eckwerte, nicht über eigene Strukturen
|
||||
(Plan 4.2).
|
||||
"""
|
||||
_name = 'l10n.at.payroll.kv.gruppe'
|
||||
_description = 'KV-Verwendungs-/Lohngruppe'
|
||||
_order = 'kv_id, code'
|
||||
|
||||
kv_id = fields.Many2one(
|
||||
'l10n.at.payroll.kv', required=True, index=True, ondelete='cascade')
|
||||
code = fields.Char(required=True, index=True)
|
||||
name = fields.Char(string='Bezeichnung')
|
||||
lohn_basis = fields.Selection([
|
||||
('monatlich', 'Monatsgehalt (vollzeit)'),
|
||||
('wochenstunden', 'Wochenstunden-Basis (Arbeiter)'),
|
||||
], required=True, default='monatlich')
|
||||
active = fields.Boolean(default=True)
|
||||
stufe_ids = fields.One2many(
|
||||
'l10n.at.payroll.kv.gruppe.stufe', 'gruppe_id',
|
||||
string='Erfahrungsstufen')
|
||||
|
||||
_unique_kv_gruppe = models.Constraint(
|
||||
'unique (kv_id, code)',
|
||||
"Je Kollektivvertrag existiert eine Gruppe nur einmal.")
|
||||
|
||||
@api.depends('kv_id', 'code', 'name')
|
||||
def _compute_display_name(self):
|
||||
for gruppe in self:
|
||||
gruppe.display_name = (
|
||||
f"{gruppe.name or gruppe.code} "
|
||||
f"[{gruppe.kv_id.display_name}]")
|
||||
|
||||
def _get_gehalt(self, stufe, datum=None):
|
||||
"""Stufen-Position (Monatsgehalt) zum Stichtag (oder leer)."""
|
||||
self.ensure_one()
|
||||
datum = datum or fields.Date.context_today(self)
|
||||
return self.env['l10n.at.payroll.kv.gruppe.stufe'].search(
|
||||
[('gruppe_id', '=', self.id),
|
||||
('stufe', '=', stufe),
|
||||
('gueltig_ab', '<=', datum)],
|
||||
order='gueltig_ab desc', limit=1)
|
||||
|
||||
def _max_stufe(self, datum=None):
|
||||
"""Höchste Stufe mit gültigem Monatsgehalt zum Stichtag."""
|
||||
self.ensure_one()
|
||||
datum = datum or fields.Date.context_today(self)
|
||||
stufe = self.env['l10n.at.payroll.kv.gruppe.stufe'].search(
|
||||
[('gruppe_id', '=', self.id),
|
||||
('gueltig_ab', '<=', datum)],
|
||||
order='stufe desc', limit=1)
|
||||
return stufe.stufe or 0
|
||||
|
||||
|
||||
class L10nAtPayrollKvGruppeStufe(models.Model):
|
||||
"""Monatsgehalt einer KV-Gruppe in einer Erfahrungsstufe ab einem
|
||||
Stichtag (Jahrestabellen aus der KV-Lohnrunde)."""
|
||||
_name = 'l10n.at.payroll.kv.gruppe.stufe'
|
||||
_description = 'KV-Monatsgehalt je Erfahrungsstufe'
|
||||
_order = 'gruppe_id, stufe, gueltig_ab desc'
|
||||
_rec_names_search = ['gruppe_id']
|
||||
|
||||
gruppe_id = fields.Many2one(
|
||||
'l10n.at.payroll.kv.gruppe', required=True, index=True,
|
||||
ondelete='cascade')
|
||||
stufe = fields.Integer(required=True, index=True)
|
||||
bezeichnung = fields.Char(
|
||||
help="Stufen-Label der KV-Tabelle, z. B. 'im 1. u 2. VWGJ' "
|
||||
"oder 'nach 15 VWGJ'.")
|
||||
gehalt = fields.Monetary(currency_field='currency_id')
|
||||
currency_id = fields.Many2one(
|
||||
'res.currency', default=lambda self: self.env.company.currency_id)
|
||||
gueltig_ab = fields.Date(
|
||||
required=True, index=True,
|
||||
default=lambda self: fields.Date.today().replace(month=1, day=1))
|
||||
|
||||
_unique_kv_stufe = models.Constraint(
|
||||
'unique (gruppe_id, stufe, gueltig_ab)',
|
||||
"Je Gruppe und Stufe gibt es nur einen Wert je Stichtag.")
|
||||
|
||||
@api.depends('gruppe_id', 'stufe', 'gehalt', 'gueltig_ab')
|
||||
def _compute_display_name(self):
|
||||
for stufe in self:
|
||||
stufe.display_name = _(
|
||||
"%(gruppe)s, Stufe %(stufe)s (%(betrag)s ab %(ab)s)",
|
||||
gruppe=stufe.gruppe_id.code,
|
||||
stufe=stufe.stufe,
|
||||
betrag=stufe.gehalt,
|
||||
ab=stufe.gueltig_ab,
|
||||
)
|
||||
@@ -0,0 +1,11 @@
|
||||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
access_l10n_at_payroll_kv_user,L10n AT Payroll KV User,model_l10n_at_payroll_kv,hr_payroll.group_hr_payroll_user,1,0,0,0
|
||||
access_l10n_at_payroll_kv_manager,L10n AT Payroll KV Manager,model_l10n_at_payroll_kv,hr_payroll.group_hr_payroll_manager,1,1,1,1
|
||||
access_l10n_at_payroll_kv_wert_user,L10n AT Payroll KV-Wert User,model_l10n_at_payroll_kv_wert,hr_payroll.group_hr_payroll_user,1,0,0,0
|
||||
access_l10n_at_payroll_kv_wert_manager,L10n AT Payroll KV-Wert Manager,model_l10n_at_payroll_kv_wert,hr_payroll.group_hr_payroll_manager,1,1,1,1
|
||||
access_l10n_at_payroll_kv_gruppe_user,L10n AT Payroll KV-Gruppe User,model_l10n_at_payroll_kv_gruppe,hr_payroll.group_hr_payroll_user,1,0,0,0
|
||||
access_l10n_at_payroll_kv_gruppe_manager,L10n AT Payroll KV-Gruppe Manager,model_l10n_at_payroll_kv_gruppe,hr_payroll.group_hr_payroll_manager,1,1,1,1
|
||||
access_l10n_at_payroll_kv_gruppe_stufe_user,L10n AT Payroll KV-Gruppe-Stufe User,model_l10n_at_payroll_kv_gruppe_stufe,hr_payroll.group_hr_payroll_user,1,0,0,0
|
||||
access_l10n_at_payroll_kv_gruppe_stufe_manager,L10n AT Payroll KV-Gruppe-Stufe Manager,model_l10n_at_payroll_kv_gruppe_stufe,hr_payroll.group_hr_payroll_manager,1,1,1,1
|
||||
access_l10n_at_payroll_kv_import_wizard,L10n AT Payroll KV-Import Wizard,model_l10n_at_payroll_kv_import_wizard,hr_payroll.group_hr_payroll_user,1,1,1,1
|
||||
access_l10n_at_payroll_kv_import_line,L10n AT Payroll KV-Import Line,model_l10n_at_payroll_kv_import_line,hr_payroll.group_hr_payroll_user,1,1,1,1
|
||||
|
@@ -0,0 +1,2 @@
|
||||
# Part of the odoo-at-payroll project. License: LGPL-3.
|
||||
from . import test_kv_katalog
|
||||
@@ -0,0 +1,192 @@
|
||||
# Part of the odoo-at-payroll project. License: LGPL-3.
|
||||
"""GP1-Rechenfälle: Seed D2 (SI-2203/SI-2748), Stichtags-Lookup,
|
||||
Versionierung, KV-Eckwerte, Import-Wizard (Sprungwarnung) und
|
||||
Vertragsfelder (Implementierungsplan-Privat, Tests 6.1)."""
|
||||
from datetime import date
|
||||
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.tests import TransactionCase, tagged
|
||||
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
class KvKatalogTest(TransactionCase):
|
||||
"""KV-Framework: Katalog, versionierte Eckwerte, Wizard, Vertrag."""
|
||||
|
||||
def test_01_seed_2203_values(self):
|
||||
"""Seed SI-2203: Gehaltstabelle ab 1. 1. 2026 (KV-Library-Volltext)."""
|
||||
stichtag = date(2026, 6, 1)
|
||||
grp_iii = self.env.ref(
|
||||
'l10n_at_hr_payroll_private.kv2203_grp_iii')
|
||||
self.assertEqual(grp_iii._get_gehalt(1, stichtag).gehalt, 2528.04)
|
||||
self.assertEqual(grp_iii._get_gehalt(3, stichtag).gehalt, 2881.97)
|
||||
self.assertEqual(grp_iii._get_gehalt(8, stichtag).gehalt, 3838.85)
|
||||
# VG VI endet bei Stufe 3 (nach 5 VWGJ)
|
||||
grp_vi = self.env.ref('l10n_at_hr_payroll_private.kv2203_grp_vi')
|
||||
self.assertEqual(grp_vi._max_stufe(stichtag), 3)
|
||||
self.assertEqual(grp_vi._get_gehalt(3, stichtag).gehalt, 6853.01)
|
||||
# Meistergruppe M II mit abgeschlossener Fachschule
|
||||
m2fs = self.env.ref('l10n_at_hr_payroll_private.kv2203_grp_m2fs')
|
||||
self.assertEqual(m2fs._get_gehalt(1, stichtag).gehalt, 3241.68)
|
||||
# Stufen-Label der Tabelle bleibt erhalten
|
||||
self.assertEqual(
|
||||
grp_iii._get_gehalt(3, stichtag).bezeichnung, 'nach 4 VWGJ')
|
||||
|
||||
def test_02_seed_2748_values(self):
|
||||
"""Seed SI-2748: Gehaltstabelle ab 1. 1. 2026 inkl. der
|
||||
Sonder-Vorrückungsverläufe (Meister VG IV, Obermeister VG V)."""
|
||||
stichtag = date(2026, 6, 1)
|
||||
grp_i = self.env.ref('l10n_at_hr_payroll_private.kv2748_grp_i')
|
||||
self.assertEqual(grp_i._get_gehalt(1, stichtag).gehalt, 1960.54)
|
||||
grp_ivm = self.env.ref('l10n_at_hr_payroll_private.kv2748_grp_ivm')
|
||||
self.assertEqual(grp_ivm._get_gehalt(8, stichtag).gehalt, 5047.10)
|
||||
self.assertEqual(
|
||||
grp_ivm._get_gehalt(8, stichtag).bezeichnung, 'nach 17 VWGJ')
|
||||
grp_vom = self.env.ref('l10n_at_hr_payroll_private.kv2748_grp_vom')
|
||||
self.assertEqual(grp_vom._get_gehalt(2, stichtag).gehalt, 4594.04)
|
||||
meister = self.env.ref(
|
||||
'l10n_at_hr_payroll_private.kv2748_grp_meister')
|
||||
self.assertEqual(meister._get_gehalt(8, stichtag).gehalt, 5270.29)
|
||||
self.assertEqual(meister._max_stufe(stichtag), 8)
|
||||
|
||||
def test_03_wert_lookup(self):
|
||||
"""KV-Eckwerte (Muster hr.rule.parameter.value): Konsum über
|
||||
kv_id._wert(code, datum); Strings als Python-Literal."""
|
||||
kv2203 = self.env.ref('l10n_at_hr_payroll_private.kv_si_2203')
|
||||
kv2748 = self.env.ref('l10n_at_hr_payroll_private.kv_si_2748')
|
||||
stichtag = date(2026, 6, 1)
|
||||
self.assertEqual(kv2203._wert('naz', stichtag), 40)
|
||||
self.assertEqual(kv2203._wert('teilungsfaktor_ue', stichtag), 150)
|
||||
self.assertEqual(kv2203._wert('sz13_monat', stichtag), 11)
|
||||
self.assertEqual(kv2203._wert('sz14_faelligkeit', stichtag), 'urlaub')
|
||||
self.assertTrue(kv2203._wert('ue_hoechster_zuschlag', stichtag))
|
||||
self.assertEqual(kv2748._wert('naz', stichtag), 38.5)
|
||||
self.assertEqual(kv2748._wert('teilungsfaktor_norm', stichtag), 167)
|
||||
self.assertEqual(kv2748._wert('mehrarbeit_zuschlag', stichtag), 50)
|
||||
# Jahresversion: Nacht-Sondervergütung 2,71 € (2026) / 2,76 € (2027)
|
||||
self.assertEqual(
|
||||
kv2748._wert('nacht_sonderverguetung', stichtag), 2.71)
|
||||
self.assertEqual(kv2748._wert(
|
||||
'nacht_sonderverguetung', date(2027, 6, 1)), 2.76)
|
||||
|
||||
def test_04_lookup_before_gueltig_ab(self):
|
||||
"""Vor dem Stichtag gibt es keinen Wert (laut, nicht 0)."""
|
||||
grp_iii = self.env.ref(
|
||||
'l10n_at_hr_payroll_private.kv2203_grp_iii')
|
||||
self.assertFalse(grp_iii._get_gehalt(1, date(2025, 12, 31)))
|
||||
kv2203 = self.env.ref('l10n_at_hr_payroll_private.kv_si_2203')
|
||||
with self.assertRaises(UserError):
|
||||
kv2203._wert('naz', date(2025, 12, 31))
|
||||
self.assertIsNone(
|
||||
kv2203._wert('naz', date(2025, 12, 31),
|
||||
raise_if_not_found=False))
|
||||
|
||||
def test_05_versioning(self):
|
||||
"""Jahresversionen: neuer Wert greift erst ab seinem Stichtag."""
|
||||
grp_iii = self.env.ref(
|
||||
'l10n_at_hr_payroll_private.kv2203_grp_iii')
|
||||
self.env['l10n.at.payroll.kv.gruppe.stufe'].create({
|
||||
'gruppe_id': grp_iii.id,
|
||||
'stufe': 1,
|
||||
'gehalt': 2600.00,
|
||||
'gueltig_ab': date(2027, 1, 1),
|
||||
})
|
||||
self.assertEqual(
|
||||
grp_iii._get_gehalt(1, date(2026, 12, 31)).gehalt, 2528.04)
|
||||
self.assertEqual(
|
||||
grp_iii._get_gehalt(1, date(2027, 1, 1)).gehalt, 2600.00)
|
||||
|
||||
def test_06_wizard_import(self):
|
||||
"""Import schreibt neue gueltig_ab-Versionen (Jahresupdate) und
|
||||
legt neue Gruppen an (Aufnahme); Sprünge > 10 % werden verwarnt
|
||||
und nie automatisch übernommen (TASY-Muster)."""
|
||||
kv = self.env.ref('l10n_at_hr_payroll_private.kv_si_2203')
|
||||
wizard = self.env['l10n.at.payroll.kv.import.wizard'].create({
|
||||
'kv_id': kv.id,
|
||||
'gueltig_ab': date(2027, 1, 1),
|
||||
'eingabe': (
|
||||
'III;1;2900,00;nach 2 VWGJ\n' # +14,7 % -> Sprungwarnung
|
||||
'III;1;2600,00\n' # +2,8 % -> unauffällig
|
||||
'X1;1;2000,00\n' # neue Gruppe (Aufnahme)
|
||||
),
|
||||
})
|
||||
wizard.action_parse()
|
||||
self.assertEqual(len(wizard.line_ids), 3)
|
||||
sprung = wizard.line_ids.filtered(
|
||||
lambda line: line.neuer_wert == 2900.0)
|
||||
self.assertFalse(sprung.uebernehmen)
|
||||
self.assertIn('Sprung', sprung.warnung)
|
||||
unauffaellig = wizard.line_ids.filtered(
|
||||
lambda line: line.neuer_wert == 2600.0)
|
||||
self.assertTrue(unauffaellig.uebernehmen)
|
||||
self.assertFalse(unauffaellig.warnung)
|
||||
aufnahme = wizard.line_ids.filtered(
|
||||
lambda line: line.gruppe_code == 'X1')
|
||||
self.assertTrue(aufnahme.uebernehmen)
|
||||
self.assertIn('Neue Gruppe', aufnahme.warnung)
|
||||
wizard.action_apply()
|
||||
|
||||
grp_iii = self.env.ref(
|
||||
'l10n_at_hr_payroll_private.kv2203_grp_iii')
|
||||
self.assertEqual(
|
||||
grp_iii._get_gehalt(1, date(2027, 1, 1)).gehalt, 2600.00)
|
||||
# historischer Wert bleibt unangetastet
|
||||
self.assertEqual(
|
||||
grp_iii._get_gehalt(1, date(2026, 12, 31)).gehalt, 2528.04)
|
||||
# Warnzeile wurde nicht geschrieben
|
||||
self.assertFalse(
|
||||
grp_iii.stufe_ids.filtered(lambda s: s.gehalt == 2900.0))
|
||||
# neue Gruppe mit Stufe angelegt
|
||||
grp_x1 = self.env['l10n.at.payroll.kv.gruppe'].search([
|
||||
('kv_id', '=', kv.id), ('code', '=', 'X1')])
|
||||
self.assertTrue(grp_x1)
|
||||
self.assertEqual(
|
||||
grp_x1._get_gehalt(1, date(2027, 1, 1)).gehalt, 2000.00)
|
||||
|
||||
def test_07_wizard_bestehende_version_aktualisieren(self):
|
||||
"""Erneuter Import zum selben Stichtag aktualisiert die
|
||||
bestehende Version statt zu verdoppeln (unique-Constraint)."""
|
||||
kv = self.env.ref('l10n_at_hr_payroll_private.kv_si_2203')
|
||||
wizard = self.env['l10n.at.payroll.kv.import.wizard'].create({
|
||||
'kv_id': kv.id,
|
||||
'gueltig_ab': date(2026, 1, 1),
|
||||
'eingabe': 'III;1;2600,00\n',
|
||||
})
|
||||
wizard.action_parse()
|
||||
line = wizard.line_ids[0]
|
||||
self.assertTrue(line.uebernehmen)
|
||||
self.assertIn('aktualisiert', line.warnung)
|
||||
self.assertEqual(line.alter_wert, 2528.04)
|
||||
wizard.action_apply()
|
||||
grp_iii = self.env.ref(
|
||||
'l10n_at_hr_payroll_private.kv2203_grp_iii')
|
||||
self.assertEqual(len(grp_iii.stufe_ids.filtered(
|
||||
lambda s: s.stufe == 1)), 1)
|
||||
self.assertEqual(
|
||||
grp_iii._get_gehalt(1, date(2026, 6, 1)).gehalt, 2600.00)
|
||||
|
||||
def test_08_vertragsfelder(self):
|
||||
"""KV-Einstufung am Vertrag (hr.version) und Durchreichung auf
|
||||
hr.employee; Katalog-Lookup über die Version."""
|
||||
employee = self.env['hr.employee'].create({'name': 'Test KV'})
|
||||
version = self.env['hr.version'].search(
|
||||
[('employee_id', '=', employee.id)], limit=1)
|
||||
if not version:
|
||||
version = self.env['hr.version'].create({
|
||||
'employee_id': employee.id,
|
||||
'date_start': date(2026, 1, 1),
|
||||
})
|
||||
employee.write({
|
||||
'kv_id':
|
||||
self.env.ref('l10n_at_hr_payroll_private.kv_si_2203').id,
|
||||
'kv_gruppe_id':
|
||||
self.env.ref(
|
||||
'l10n_at_hr_payroll_private.kv2203_grp_iii').id,
|
||||
'erfahrungsstufe': 3,
|
||||
'ueberzahlung': 3000.00,
|
||||
})
|
||||
self.assertEqual(version.kv_id.code, 'SI-2203')
|
||||
self.assertEqual(version.kv_gruppe_id.code, 'III')
|
||||
self.assertEqual(version.erfahrungsstufe, 3)
|
||||
self.assertEqual(version.ueberzahlung, 3000.00)
|
||||
self.assertEqual(
|
||||
version._get_kv_gehalt(date(2026, 6, 1)).gehalt, 2881.97)
|
||||
@@ -0,0 +1,270 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="kv_list_view" model="ir.ui.view">
|
||||
<field name="name">l10n.at.payroll.kv.list</field>
|
||||
<field name="model">l10n.at.payroll.kv</field>
|
||||
<field name="arch" type="xml">
|
||||
<list string="Kollektivverträge">
|
||||
<field name="code"/>
|
||||
<field name="name"/>
|
||||
<field name="arbeitgeberkammer"/>
|
||||
<field name="gewerkschaft"/>
|
||||
<field name="gueltig_ab"/>
|
||||
<field name="active" column_invisible="1"/>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="kv_form_view" model="ir.ui.view">
|
||||
<field name="name">l10n.at.payroll.kv.form</field>
|
||||
<field name="model">l10n.at.payroll.kv</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Kollektivvertrag">
|
||||
<sheet>
|
||||
<group>
|
||||
<group>
|
||||
<field name="name"/>
|
||||
<field name="code"/>
|
||||
<field name="arbeitgeberkammer"/>
|
||||
<field name="gewerkschaft"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="gueltig_ab"/>
|
||||
<field name="library_variant_id"/>
|
||||
<field name="quelle_url" widget="url"/>
|
||||
<field name="active" widget="boolean_toggle"/>
|
||||
</group>
|
||||
</group>
|
||||
<notebook>
|
||||
<page string="Verwendungsgruppen" name="gruppen">
|
||||
<field name="gruppe_ids">
|
||||
<list editable="bottom">
|
||||
<field name="code"/>
|
||||
<field name="name"/>
|
||||
<field name="lohn_basis"/>
|
||||
<field name="active" widget="boolean_toggle"/>
|
||||
</list>
|
||||
</field>
|
||||
</page>
|
||||
<page string="KV-Eckwerte" name="werte">
|
||||
<field name="wert_ids">
|
||||
<list editable="bottom">
|
||||
<field name="code"/>
|
||||
<field name="name"/>
|
||||
<field name="parameter_value"/>
|
||||
<field name="date_from"/>
|
||||
</list>
|
||||
</field>
|
||||
</page>
|
||||
</notebook>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="kv_gruppe_form_view" model="ir.ui.view">
|
||||
<field name="name">l10n.at.payroll.kv.gruppe.form</field>
|
||||
<field name="model">l10n.at.payroll.kv.gruppe</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="KV-Verwendungsgruppe">
|
||||
<sheet>
|
||||
<group>
|
||||
<group>
|
||||
<field name="kv_id"/>
|
||||
<field name="code"/>
|
||||
<field name="name"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="lohn_basis"/>
|
||||
<field name="active" widget="boolean_toggle"/>
|
||||
</group>
|
||||
</group>
|
||||
<notebook>
|
||||
<page string="Erfahrungsstufen (Jahrestabellen)" name="stufen">
|
||||
<field name="stufe_ids">
|
||||
<list editable="bottom">
|
||||
<field name="stufe"/>
|
||||
<field name="bezeichnung"/>
|
||||
<field name="gehalt"/>
|
||||
<field name="gueltig_ab"/>
|
||||
</list>
|
||||
</field>
|
||||
</page>
|
||||
</notebook>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="kv_gruppe_list_view" model="ir.ui.view">
|
||||
<field name="name">l10n.at.payroll.kv.gruppe.list</field>
|
||||
<field name="model">l10n.at.payroll.kv.gruppe</field>
|
||||
<field name="arch" type="xml">
|
||||
<list string="KV-Verwendungsgruppen">
|
||||
<field name="kv_id"/>
|
||||
<field name="code"/>
|
||||
<field name="name"/>
|
||||
<field name="lohn_basis"/>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="kv_wert_list_view" model="ir.ui.view">
|
||||
<field name="name">l10n.at.payroll.kv.wert.list</field>
|
||||
<field name="model">l10n.at.payroll.kv.wert</field>
|
||||
<field name="arch" type="xml">
|
||||
<list string="KV-Eckwerte" multi_edit="1">
|
||||
<field name="kv_id"/>
|
||||
<field name="code"/>
|
||||
<field name="name"/>
|
||||
<field name="parameter_value"/>
|
||||
<field name="date_from"/>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="kv_gruppe_stufe_list_view" model="ir.ui.view">
|
||||
<field name="name">l10n.at.payroll.kv.gruppe.stufe.list</field>
|
||||
<field name="model">l10n.at.payroll.kv.gruppe.stufe</field>
|
||||
<field name="arch" type="xml">
|
||||
<list string="KV-Monatsgehälter">
|
||||
<field name="gruppe_id"/>
|
||||
<field name="stufe"/>
|
||||
<field name="bezeichnung"/>
|
||||
<field name="gehalt" sum="Summe"/>
|
||||
<field name="gueltig_ab"/>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_kv" model="ir.actions.act_window">
|
||||
<field name="name">Kollektivverträge</field>
|
||||
<field name="res_model">l10n.at.payroll.kv</field>
|
||||
<field name="view_mode">list,form</field>
|
||||
</record>
|
||||
|
||||
<record id="action_kv_gruppe" model="ir.actions.act_window">
|
||||
<field name="name">KV-Verwendungsgruppen</field>
|
||||
<field name="res_model">l10n.at.payroll.kv.gruppe</field>
|
||||
<field name="view_mode">list,form</field>
|
||||
</record>
|
||||
|
||||
<record id="action_kv_wert" model="ir.actions.act_window">
|
||||
<field name="name">KV-Eckwerte</field>
|
||||
<field name="res_model">l10n.at.payroll.kv.wert</field>
|
||||
<field name="view_mode">list</field>
|
||||
<field name="context">{'search_default_groupby_kv_id': 1}</field>
|
||||
</record>
|
||||
|
||||
<record id="action_kv_gruppe_stufe" model="ir.actions.act_window">
|
||||
<field name="name">KV-Monatsgehälter (Stufen)</field>
|
||||
<field name="res_model">l10n.at.payroll.kv.gruppe.stufe</field>
|
||||
<field name="view_mode">list</field>
|
||||
<field name="context">{'search_default_groupby_gruppe_id': 1}</field>
|
||||
</record>
|
||||
|
||||
<!-- KV-Einstufung am Mitarbeitervertrag (Muster hr_version_bgld) -->
|
||||
<record id="payroll_hr_employee_view_form_kv" model="ir.ui.view">
|
||||
<field name="name">hr.employee.form.kv</field>
|
||||
<field name="model">hr.employee</field>
|
||||
<field name="inherit_id" ref="hr_payroll.payroll_hr_employee_view_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<group name="payroll_group" position="inside">
|
||||
<group name="kv_einstufung" string="KV-Einstufung"
|
||||
groups="hr_payroll.group_hr_payroll_user">
|
||||
<field name="kv_id"/>
|
||||
<field name="kv_gruppe_id"
|
||||
domain="[('kv_id', '=', kv_id)]"
|
||||
invisible="not kv_id"/>
|
||||
<field name="erfahrungsstufe"
|
||||
invisible="not kv_gruppe_id"/>
|
||||
<field name="ueberzahlung"
|
||||
invisible="not kv_gruppe_id"/>
|
||||
</group>
|
||||
</group>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- KV-Katalog-Import -->
|
||||
<record id="kv_import_wizard_form_view" model="ir.ui.view">
|
||||
<field name="name">l10n.at.payroll.kv.import.wizard.form</field>
|
||||
<field name="model">l10n.at.payroll.kv.import.wizard</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="KV-Katalog importieren (Aufnahme/Jahresupdate)">
|
||||
<group>
|
||||
<group>
|
||||
<field name="kv_id"/>
|
||||
<field name="gueltig_ab"/>
|
||||
</group>
|
||||
</group>
|
||||
<field name="eingabe" placeholder="Gruppe;Stufe;Gehalt[;Bezeichnung] — z. B. III;1;2528,04;im 1. u 2. VWGJ"/>
|
||||
<notebook>
|
||||
<page string="Vorschau" name="vorschau">
|
||||
<field name="line_ids">
|
||||
<list editable="bottom" create="0" delete="0">
|
||||
<field name="uebernehmen" widget="boolean_toggle"/>
|
||||
<field name="gruppe_code"/>
|
||||
<field name="gruppe_name"/>
|
||||
<field name="stufe"/>
|
||||
<field name="bezeichnung"/>
|
||||
<field name="alter_wert"/>
|
||||
<field name="neuer_wert"/>
|
||||
<field name="currency_id" column_invisible="1"/>
|
||||
<field name="warnung" decoration-danger="warnung"/>
|
||||
</list>
|
||||
</field>
|
||||
</page>
|
||||
<page string="Protokoll" name="protokoll">
|
||||
<field name="protokoll"/>
|
||||
</page>
|
||||
</notebook>
|
||||
<footer>
|
||||
<button string="1) Tabelle analysieren" name="action_parse"
|
||||
type="object" class="btn-secondary"/>
|
||||
<button string="2) Ausgewählte Werte übernehmen"
|
||||
name="action_apply" type="object"
|
||||
class="btn-primary" invisible="not line_ids"/>
|
||||
<button string="Abbrechen" class="btn-secondary"
|
||||
special="cancel"/>
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_kv_import_wizard" model="ir.actions.act_window">
|
||||
<field name="name">KV-Katalog importieren</field>
|
||||
<field name="res_model">l10n.at.payroll.kv.import.wizard</field>
|
||||
<field name="view_mode">form</field>
|
||||
<field name="target">new</field>
|
||||
</record>
|
||||
|
||||
<menuitem id="menu_l10n_at_payroll_private_root"
|
||||
name="AT-Payroll (Privat)"
|
||||
parent="hr_work_entry_enterprise.menu_hr_payroll_root"
|
||||
sequence="95"/>
|
||||
|
||||
<menuitem id="menu_l10n_at_payroll_private_kv"
|
||||
name="Kollektivverträge"
|
||||
parent="menu_l10n_at_payroll_private_root"
|
||||
action="action_kv" sequence="10"/>
|
||||
|
||||
<menuitem id="menu_l10n_at_payroll_private_gruppen"
|
||||
name="KV-Verwendungsgruppen"
|
||||
parent="menu_l10n_at_payroll_private_root"
|
||||
action="action_kv_gruppe" sequence="15"/>
|
||||
|
||||
<menuitem id="menu_l10n_at_payroll_private_werte"
|
||||
name="KV-Eckwerte"
|
||||
parent="menu_l10n_at_payroll_private_root"
|
||||
action="action_kv_wert" sequence="20"/>
|
||||
|
||||
<menuitem id="menu_l10n_at_payroll_private_stufen"
|
||||
name="KV-Monatsgehälter (Stufen)"
|
||||
parent="menu_l10n_at_payroll_private_root"
|
||||
action="action_kv_gruppe_stufe" sequence="30"/>
|
||||
|
||||
<menuitem id="menu_l10n_at_payroll_private_import"
|
||||
name="KV-Katalog importieren"
|
||||
parent="menu_l10n_at_payroll_private_root"
|
||||
action="action_kv_import_wizard" sequence="40"/>
|
||||
</odoo>
|
||||
@@ -0,0 +1,2 @@
|
||||
# Part of the odoo-at-payroll project. License: LGPL-3.
|
||||
from . import kv_katalog_import
|
||||
@@ -0,0 +1,211 @@
|
||||
# Part of the odoo-at-payroll project. License: LGPL-3.
|
||||
"""KV-Katalog-Import (GP1): Neuaufnahme und Jahresupdate.
|
||||
|
||||
Tabellenpaste/CSV (Gruppe;Stufe;Gehalt[;Bezeichnung]) wird geparst,
|
||||
gegen den bestehenden Katalog plausibilisiert (Sprungwarnung > 10 %
|
||||
nach TASY-Muster; Warnzeilen werden nie automatisch übernommen) und
|
||||
schreibt nach expliziter Bestätigung **neue ``gueltig_ab``-Versionen**
|
||||
— historische Werte bleiben unangetastet. Quelle der Tabellen sind die
|
||||
KV-Library-Volltexte bzw. Kammer-Lohntafeln (Quellenarchitektur des
|
||||
RUNBOOK).
|
||||
"""
|
||||
import re
|
||||
|
||||
from odoo import _, fields, models
|
||||
|
||||
_BETRAG_MUSTER = re.compile(r'^\d{1,3}(?:\.\d{3})+(?:,\d+)?$')
|
||||
|
||||
|
||||
class L10nAtPayrollKvImportLine(models.TransientModel):
|
||||
_name = 'l10n.at.payroll.kv.import.line'
|
||||
_description = 'KV-Katalog-Import: Vorschauzeile'
|
||||
|
||||
wizard_id = fields.Many2one(
|
||||
'l10n.at.payroll.kv.import.wizard', required=True, ondelete='cascade')
|
||||
gruppe_code = fields.Char(string='Gruppe', required=True)
|
||||
gruppe_name = fields.Char(string='Gruppenbezeichnung', readonly=True)
|
||||
stufe = fields.Integer(required=True)
|
||||
bezeichnung = fields.Char(
|
||||
string='Stufen-Label',
|
||||
help="Optionales Stufen-Label der KV-Tabelle, z. B. 'nach 2 VWGJ'.")
|
||||
alter_wert = fields.Monetary(string='Alter Wert', readonly=True)
|
||||
neuer_wert = fields.Monetary(string='Neuer Wert', required=True)
|
||||
currency_id = fields.Many2one(related='wizard_id.currency_id')
|
||||
uebernehmen = fields.Boolean(default=True)
|
||||
warnung = fields.Char(
|
||||
help="Leer = unauffällig; sonst Plausibilitätshinweis. "
|
||||
"Warnzeilen (Sprung > 10 %) werden nie automatisch "
|
||||
"übernommen.")
|
||||
|
||||
|
||||
class L10nAtPayrollKvImportWizard(models.TransientModel):
|
||||
_name = 'l10n.at.payroll.kv.import.wizard'
|
||||
_description = 'KV-Katalog importieren (Aufnahme/Jahresupdate)'
|
||||
|
||||
kv_id = fields.Many2one(
|
||||
'l10n.at.payroll.kv', string='Kollektivvertrag', required=True)
|
||||
gueltig_ab = fields.Date(
|
||||
string='Gültig ab', required=True,
|
||||
default=lambda self: fields.Date.today().replace(month=1, day=1),
|
||||
help="Stichtag der neuen Version (z. B. 1. 1. des Jahresgehalts "
|
||||
"der KV-Runde). Bestehende Werte zu diesem Stichtag werden "
|
||||
"aktualisiert, historische nie verändert.")
|
||||
eingabe = fields.Text(
|
||||
string='Tabellen (CSV)',
|
||||
help="Eine Zeile je Stufenwert — Semikolon- oder "
|
||||
"Tabulator-getrennt (Dezimalkomma erlaubt):\n"
|
||||
"Gruppe;Stufe;Gehalt[;Bezeichnung]\n"
|
||||
"z. B. III;1;2528,04;im 1. u 2. VWGJ")
|
||||
line_ids = fields.One2many(
|
||||
'l10n.at.payroll.kv.import.line', 'wizard_id', string='Vorschau')
|
||||
protokoll = fields.Text(readonly=True)
|
||||
currency_id = fields.Many2one(
|
||||
'res.currency', default=lambda self: self.env.company.currency_id)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Parsing
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
@staticmethod
|
||||
def _parse_betrag(text):
|
||||
"""'2.059,41 €' / '2528.04' / '2 059,41' → float (oder None)."""
|
||||
text = (text or '').strip().replace('€', '').replace('EUR', '')
|
||||
text = re.sub(r'\s', '', text)
|
||||
if not text:
|
||||
return None
|
||||
if _BETRAG_MUSTER.match(text):
|
||||
# Österreichische Schreibweise: Punkt = Tausender
|
||||
text = text.replace('.', '').replace(',', '.')
|
||||
else:
|
||||
text = text.replace(',', '.')
|
||||
try:
|
||||
return float(text)
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
def _trenner(self, zeile):
|
||||
if ';' in zeile:
|
||||
return ';'
|
||||
if '\t' in zeile:
|
||||
return '\t'
|
||||
return ','
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Actions
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def action_parse(self):
|
||||
"""Tabellen parsen und Vorschau (inkl. Plausibilität) aufbauen."""
|
||||
self.ensure_one()
|
||||
stufen_obj = self.env['l10n.at.payroll.kv.gruppe.stufe']
|
||||
vortag = fields.Date.subtract(self.gueltig_ab, days=1)
|
||||
line_vals, protokoll = [], []
|
||||
for roh in (self.eingabe or '').splitlines():
|
||||
zeile = roh.strip()
|
||||
if not zeile or zeile.startswith('#'):
|
||||
continue
|
||||
teile = [t.strip() for t in zeile.split(self._trenner(zeile))]
|
||||
if len(teile) < 3:
|
||||
protokoll.append(
|
||||
_('IGNORIERT (zu kurz): %(zeile)s', zeile=zeile))
|
||||
continue
|
||||
gruppe_code, stufe_text, betrag_text = teile[:3]
|
||||
bezeichnung = teile[3] if len(teile) > 3 else ''
|
||||
try:
|
||||
stufe = int(stufe_text)
|
||||
except ValueError:
|
||||
protokoll.append(
|
||||
_('IGNORIERT (Stufe keine Zahl): %(zeile)s',
|
||||
zeile=zeile))
|
||||
continue
|
||||
betrag = self._parse_betrag(betrag_text)
|
||||
if betrag is None:
|
||||
protokoll.append(
|
||||
_('IGNORIERT (Betrag unlesbar): %(zeile)s',
|
||||
zeile=zeile))
|
||||
continue
|
||||
|
||||
gruppe = self.env['l10n.at.payroll.kv.gruppe'].search([
|
||||
('kv_id', '=', self.kv_id.id),
|
||||
('code', '=', gruppe_code)], limit=1)
|
||||
warnung = ''
|
||||
alter_wert = 0.0
|
||||
if not gruppe:
|
||||
warnung = _('Neue Gruppe — wird mit der Übernahme angelegt.')
|
||||
else:
|
||||
bestehend = stufen_obj.search([
|
||||
('gruppe_id', '=', gruppe.id),
|
||||
('stufe', '=', stufe),
|
||||
('gueltig_ab', '=', self.gueltig_ab)], limit=1)
|
||||
if bestehend:
|
||||
# Version zum Stichtag existiert schon: deren Wert
|
||||
# wird aktualisiert — Referenz für die Plausibilität.
|
||||
alter_wert = bestehend.gehalt
|
||||
warnung = _(
|
||||
'Bestehende Version vom %(ab)s wird aktualisiert.',
|
||||
ab=self.gueltig_ab)
|
||||
else:
|
||||
alt = gruppe._get_gehalt(stufe, vortag)
|
||||
alter_wert = alt.gehalt if alt else 0.0
|
||||
if alter_wert and abs(
|
||||
betrag - alter_wert) / alter_wert > 0.10:
|
||||
warnung += ' ' + _(
|
||||
'Sprung > 10 %% (%(alt)s → %(neu)s)',
|
||||
alt=alter_wert, neu=betrag)
|
||||
|
||||
line_vals.append((0, 0, {
|
||||
'gruppe_code': gruppe_code,
|
||||
'gruppe_name': gruppe.name or '',
|
||||
'stufe': stufe,
|
||||
'bezeichnung': bezeichnung,
|
||||
'alter_wert': alter_wert,
|
||||
'neuer_wert': betrag,
|
||||
'uebernehmen': 'Sprung' not in warnung,
|
||||
'warnung': warnung,
|
||||
}))
|
||||
self.line_ids = [(5, 0, 0)] + line_vals
|
||||
self.protokoll = '\n'.join(protokoll)
|
||||
return True
|
||||
|
||||
def action_apply(self):
|
||||
"""Ausgewählte Zeilen als neue ``gueltig_ab``-Versionen schreiben."""
|
||||
self.ensure_one()
|
||||
gruppen_obj = self.env['l10n.at.payroll.kv.gruppe']
|
||||
stufen_obj = self.env['l10n.at.payroll.kv.gruppe.stufe']
|
||||
geschrieben = 0
|
||||
for line in self.line_ids.filtered('uebernehmen'):
|
||||
gruppe = gruppen_obj.search([
|
||||
('kv_id', '=', self.kv_id.id),
|
||||
('code', '=', line.gruppe_code)], limit=1)
|
||||
if not gruppe:
|
||||
gruppe = gruppen_obj.create({
|
||||
'kv_id': self.kv_id.id,
|
||||
'code': line.gruppe_code,
|
||||
'name': line.gruppe_name or line.gruppe_code,
|
||||
})
|
||||
stufe = stufen_obj.search([
|
||||
('gruppe_id', '=', gruppe.id),
|
||||
('stufe', '=', line.stufe),
|
||||
('gueltig_ab', '=', self.gueltig_ab)], limit=1)
|
||||
werte = {
|
||||
'gehalt': line.neuer_wert,
|
||||
'bezeichnung': line.bezeichnung,
|
||||
}
|
||||
if stufe:
|
||||
stufe.write(werte)
|
||||
else:
|
||||
stufen_obj.create(dict(werte, gruppe_id=gruppe.id,
|
||||
stufe=line.stufe,
|
||||
gueltig_ab=self.gueltig_ab))
|
||||
geschrieben += 1
|
||||
return {
|
||||
'type': 'ir.actions.client',
|
||||
'tag': 'display_notification',
|
||||
'params': {
|
||||
'type': 'success',
|
||||
'message': _('%(count)s Stufenwert(e) geschrieben '
|
||||
'(gültig ab %(ab)s).',
|
||||
count=geschrieben, ab=self.gueltig_ab),
|
||||
'next': {'type': 'ir.actions.act_window_close'},
|
||||
},
|
||||
}
|
||||
Reference in New Issue
Block a user