[IMP] l10n_at_gemeinde_payroll: implement SV value layer (AP4) from OEGK TASY
SV-Werte-Implementierung nach dem freigegebenen TASY-Plan (PLAN-TASY-SV-Import):
- hr.rule.parameter-Seed 2026 (20 Parameter, generator-erzeugt aus dem
ÖGK TASY-LSWH-Export, Stichtag 2026-07-01): Geringfügigkeit 551,10 EUR
(GFGBTG, ab 2025 — Aufwertungsaussetzung korrekt abgebildet), HBG
monatlich 6.930 EUR (HBGALLG tgl. x 30), HBG SZ 13.860 EUR, KV
3,87/3,78, PV 10,25/12,55, AV 2,95/2,95, UV 0/1,10, IESG 0/0,10,
BV 1,53, AK/WF geführt, § 2a AMPFG-Staffel aus dem SV-Werte-Report.
Generator mit Herkunftsnachweis + Erwartungswert-Ankern
(personalverrechnung/tools/gen_sv_parameters_2026.py, Exit 1 bei
Abweichung von den offiziellen Ankern).
- TASY-Quelldatei committet (personalverrechnung/quellen/, 5 MB —
öffentliches Tarifdatenmaterial ohne Personendaten) für Seed- und
Testreproduzierbarkeit.
- Deklarative TASY-Mapping-Tabelle (gemeinde.payroll.tasy.mapping,
data XML): bbb-Codes (Cent, Faktor z. B. x 30) und Tarifzweige
(_dn/_dg-Suffix-Konvention) — keine festen Werte im Code.
- Import-Wizard (TransientModel, lxml safe parsing):
Beschaeftigtengruppe B002 ("Angestellte") -> Tarifgruppe ->
tarifgruppeSetZuord -> Tarifset -> Tarif; bei Varianten gewinnt die
Split-Variante (DN>0 und DG>0), dann die einseitige DG-Variante (UV/BV);
freie-DN-Varianten verlieren. Preview mit Delta, Sprungwarnung > 10 %,
Warnzeilen werden nie automatisch übernommen; Schreiben versionierter
hr.rule.parameter.value-Sätze erst nach expliziter Bestätigung.
- Tests (4 Suiten): Seed-Werte am Stichtag, Fixture-Parser
(Split-Präferenz, Cent-Rechnung), Versionsverhalten (Update statt
Duplikat), Echtdaten-Test gegen den 5-MB-Export (Auflösung == Seed,
keine Warnzeilen).
- Security/Views/Menüs für Mapping + Wizard; Modul-Version 19.0.1.2.0.
Statische Validierung grün (py_compile, XML, Manifest); Runtime-Testlauf
auf dem Dev-Host ausständig.
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
# Part of the gem360 onboarding. See LICENSE / project docs for details.
|
||||
{
|
||||
'name': 'Österreich - Gemeinde-Personalverrechnung (Bgld. GemBG)',
|
||||
'version': '19.0.1.1.0',
|
||||
'version': '19.0.1.2.0',
|
||||
'author': 'ixsol / gem360',
|
||||
'website': 'https://gem360.at',
|
||||
'category': 'Human Resources/Payroll',
|
||||
@@ -30,15 +30,20 @@ Gemeindebedienstetengesetz 2014** (LGBl. Nr. 42/2014):
|
||||
Mehr-Bundesland-Architektur: Landes-Kataloge und landesspezifische
|
||||
Regeln sind von Anfang an getrennt (Burgenland zuerst).
|
||||
|
||||
**Status:** AP1 (Katalogmodell) implementiert: Entlohnungsgruppen/-stufen
|
||||
mit Jahresversionierung aus den Bezügeanpassungs-Tabellen (Seed 2026:
|
||||
35 Gruppen, 433 Werte), GemBG-Felder auf der Payroll-Version
|
||||
(Entlohnungsgruppe, Stufe, Besoldungsdienstalter, Vorrückungstermin
|
||||
nach § 66 Abs 2) und Vorrückungs-Wizard mit Protokoll. Strukturen,
|
||||
Regeln, Meldewesen und VRV-Buchung folgen dem freigegebenen
|
||||
Implementierungsplan — Grundlagen: ``personalverrechnung/PLAN-payroll-bgld.md``,
|
||||
**Status:** AP1 (Katalogmodell) und SV-Wertelayer (AP4-Basis, TASY) implementiert:
|
||||
Entlohnungsgruppen/-stufen mit Jahresversionierung aus den Bezüge-
|
||||
anpassungs-Tabellen (Seed 2026: 35 Gruppen, 433 Werte), GemBG-Felder auf
|
||||
der Payroll-Version (Entlohnungsgruppe, Stufe, Besoldungsdienstalter,
|
||||
Vorrückungstermin nach § 66 Abs 2) mit Vorrückungs-Wizard; SV-Werte 2026
|
||||
als ``hr.rule.parameter``-Seed (aus ÖGK TASY-LSWH-Export generiert und
|
||||
gegen den offiziellen SV-Werte-Report gespiegelt) inkl. deklarativer
|
||||
TASY-Mapping-Tabelle und Import-Wizard (Preview, Sprungwarnung,
|
||||
explizite Bestätigung, versionierte Werte). Strukturen, Regeln,
|
||||
Meldewesen und VRV-Buchung folgen dem freigegebenen Implementierungsplan —
|
||||
Grundlagen: ``personalverrechnung/PLAN-payroll-bgld.md``,
|
||||
``personalverrechnung/RECHTSQUELLEN-Bgld.md``,
|
||||
``personalverrechnung/IMPLEMENTIERUNGSPLAN-Bgld.md`` und der
|
||||
``personalverrechnung/IMPLEMENTIERUNGSPLAN-Bgld.md``,
|
||||
``personalverrechnung/PLAN-TASY-SV-Import.md`` und der
|
||||
``payroll``-Agent-Skill.
|
||||
""",
|
||||
'depends': ['hr_payroll', 'hr_payroll_account', 'l10n_at_vrv2015'],
|
||||
@@ -46,8 +51,11 @@ Implementierungsplan — Grundlagen: ``personalverrechnung/PLAN-payroll-bgld.md`
|
||||
'security/ir.model.access.csv',
|
||||
'data/gemeinde.payroll.entlohnungsgruppe.csv',
|
||||
'data/gemeinde.payroll.entlohnungsstufe.csv',
|
||||
'data/rule_parameters_2026.xml',
|
||||
'data/tasy_mapping.xml',
|
||||
'views/entlohnung_views.xml',
|
||||
'views/hr_version_bgld_views.xml',
|
||||
'views/tasy_views.xml',
|
||||
],
|
||||
'countries': ['at'],
|
||||
'license': 'LGPL-3',
|
||||
|
||||
@@ -0,0 +1,203 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="hr_rule_parameter_at_sv_ak_dg" model="hr.rule.parameter">
|
||||
<field name="name">Arbeiterkammerumlage Dienstgeberanteil</field>
|
||||
<field name="code">at_sv_ak_dg</field>
|
||||
<field name="country_id" ref="base.at"/>
|
||||
</record>
|
||||
<record id="hr_rule_parameter_value_at_sv_ak_dg" model="hr.rule.parameter.value">
|
||||
<field name="rule_parameter_id" ref="hr_rule_parameter_at_sv_ak_dg"/>
|
||||
<field name="date_from">2019-01-01</field>
|
||||
<field name="parameter_value">0.0</field>
|
||||
</record>
|
||||
<record id="hr_rule_parameter_at_sv_ak_dn" model="hr.rule.parameter">
|
||||
<field name="name">Arbeiterkammerumlage Dienstnehmeranteil</field>
|
||||
<field name="code">at_sv_ak_dn</field>
|
||||
<field name="country_id" ref="base.at"/>
|
||||
</record>
|
||||
<record id="hr_rule_parameter_value_at_sv_ak_dn" model="hr.rule.parameter.value">
|
||||
<field name="rule_parameter_id" ref="hr_rule_parameter_at_sv_ak_dn"/>
|
||||
<field name="date_from">2019-01-01</field>
|
||||
<field name="parameter_value">0.5</field>
|
||||
</record>
|
||||
<record id="hr_rule_parameter_at_sv_av_dg" model="hr.rule.parameter">
|
||||
<field name="name">SV Arbeitslosenversicherung Dienstgeberanteil</field>
|
||||
<field name="code">at_sv_av_dg</field>
|
||||
<field name="country_id" ref="base.at"/>
|
||||
</record>
|
||||
<record id="hr_rule_parameter_value_at_sv_av_dg" model="hr.rule.parameter.value">
|
||||
<field name="rule_parameter_id" ref="hr_rule_parameter_at_sv_av_dg"/>
|
||||
<field name="date_from">2024-01-01</field>
|
||||
<field name="parameter_value">2.95</field>
|
||||
</record>
|
||||
<record id="hr_rule_parameter_at_sv_av_dn" model="hr.rule.parameter">
|
||||
<field name="name">SV Arbeitslosenversicherung Dienstnehmeranteil</field>
|
||||
<field name="code">at_sv_av_dn</field>
|
||||
<field name="country_id" ref="base.at"/>
|
||||
</record>
|
||||
<record id="hr_rule_parameter_value_at_sv_av_dn" model="hr.rule.parameter.value">
|
||||
<field name="rule_parameter_id" ref="hr_rule_parameter_at_sv_av_dn"/>
|
||||
<field name="date_from">2024-01-01</field>
|
||||
<field name="parameter_value">2.95</field>
|
||||
</record>
|
||||
<record id="hr_rule_parameter_at_sv_av_staffel_ampfg" model="hr.rule.parameter">
|
||||
<field name="name">SV AV-Beitragsstaffel bei geringem Einkommen (§ 2a AMPFG)</field>
|
||||
<field name="code">at_sv_av_staffel_ampfg</field>
|
||||
<field name="country_id" ref="base.at"/>
|
||||
</record>
|
||||
<record id="hr_rule_parameter_value_at_sv_av_staffel_ampfg" model="hr.rule.parameter.value">
|
||||
<field name="rule_parameter_id" ref="hr_rule_parameter_at_sv_av_staffel_ampfg"/>
|
||||
<field name="date_from">2026-07-01</field>
|
||||
<field name="parameter_value">{'tiers': [(0.0, 2225.00, 0.0), (2225.00, 2427.00, 1.0), (2427.00, 2630.00, 2.0)], 'av_total': 5.9}</field>
|
||||
</record>
|
||||
<record id="hr_rule_parameter_at_sv_bv_dg" model="hr.rule.parameter">
|
||||
<field name="name">Betriebliche Vorsorge Dienstgeberanteil</field>
|
||||
<field name="code">at_sv_bv_dg</field>
|
||||
<field name="country_id" ref="base.at"/>
|
||||
</record>
|
||||
<record id="hr_rule_parameter_value_at_sv_bv_dg" model="hr.rule.parameter.value">
|
||||
<field name="rule_parameter_id" ref="hr_rule_parameter_at_sv_bv_dg"/>
|
||||
<field name="date_from">2019-01-01</field>
|
||||
<field name="parameter_value">1.53</field>
|
||||
</record>
|
||||
<record id="hr_rule_parameter_at_sv_bv_dn" model="hr.rule.parameter">
|
||||
<field name="name">Betriebliche Vorsorge Dienstnehmeranteil</field>
|
||||
<field name="code">at_sv_bv_dn</field>
|
||||
<field name="country_id" ref="base.at"/>
|
||||
</record>
|
||||
<record id="hr_rule_parameter_value_at_sv_bv_dn" model="hr.rule.parameter.value">
|
||||
<field name="rule_parameter_id" ref="hr_rule_parameter_at_sv_bv_dn"/>
|
||||
<field name="date_from">2019-01-01</field>
|
||||
<field name="parameter_value">0.0</field>
|
||||
</record>
|
||||
<record id="hr_rule_parameter_at_sv_gfg_monatlich" model="hr.rule.parameter">
|
||||
<field name="name">SV Geringfügigkeitsgrenze (monatlich, § 5 Abs 2 ASVG)</field>
|
||||
<field name="code">at_sv_gfg_monatlich</field>
|
||||
<field name="country_id" ref="base.at"/>
|
||||
</record>
|
||||
<record id="hr_rule_parameter_value_at_sv_gfg_monatlich" model="hr.rule.parameter.value">
|
||||
<field name="rule_parameter_id" ref="hr_rule_parameter_at_sv_gfg_monatlich"/>
|
||||
<field name="date_from">2025-01-01</field>
|
||||
<field name="parameter_value">551.1</field>
|
||||
</record>
|
||||
<record id="hr_rule_parameter_at_sv_hbg_monatlich" model="hr.rule.parameter">
|
||||
<field name="name">SV Höchstbeitragsgrundlage (monatlich, § 45 Abs 1 ASVG)</field>
|
||||
<field name="code">at_sv_hbg_monatlich</field>
|
||||
<field name="country_id" ref="base.at"/>
|
||||
</record>
|
||||
<record id="hr_rule_parameter_value_at_sv_hbg_monatlich" model="hr.rule.parameter.value">
|
||||
<field name="rule_parameter_id" ref="hr_rule_parameter_at_sv_hbg_monatlich"/>
|
||||
<field name="date_from">2026-01-01</field>
|
||||
<field name="parameter_value">6930.0</field>
|
||||
</record>
|
||||
<record id="hr_rule_parameter_at_sv_hbg_sz_jaehrlich" model="hr.rule.parameter">
|
||||
<field name="name">SV Höchstbeitragsgrundlage Sonderzahlungen (§ 54 ASVG)</field>
|
||||
<field name="code">at_sv_hbg_sz_jaehrlich</field>
|
||||
<field name="country_id" ref="base.at"/>
|
||||
</record>
|
||||
<record id="hr_rule_parameter_value_at_sv_hbg_sz_jaehrlich" model="hr.rule.parameter.value">
|
||||
<field name="rule_parameter_id" ref="hr_rule_parameter_at_sv_hbg_sz_jaehrlich"/>
|
||||
<field name="date_from">2026-01-01</field>
|
||||
<field name="parameter_value">13860.0</field>
|
||||
</record>
|
||||
<record id="hr_rule_parameter_at_sv_iesg_dg" model="hr.rule.parameter">
|
||||
<field name="name">IESG-Zuschlag Dienstgeberanteil (Insolvenz)</field>
|
||||
<field name="code">at_sv_iesg_dg</field>
|
||||
<field name="country_id" ref="base.at"/>
|
||||
</record>
|
||||
<record id="hr_rule_parameter_value_at_sv_iesg_dg" model="hr.rule.parameter.value">
|
||||
<field name="rule_parameter_id" ref="hr_rule_parameter_at_sv_iesg_dg"/>
|
||||
<field name="date_from">2022-01-01</field>
|
||||
<field name="parameter_value">0.1</field>
|
||||
</record>
|
||||
<record id="hr_rule_parameter_at_sv_iesg_dn" model="hr.rule.parameter">
|
||||
<field name="name">IESG-Zuschlag Dienstnehmeranteil</field>
|
||||
<field name="code">at_sv_iesg_dn</field>
|
||||
<field name="country_id" ref="base.at"/>
|
||||
</record>
|
||||
<record id="hr_rule_parameter_value_at_sv_iesg_dn" model="hr.rule.parameter.value">
|
||||
<field name="rule_parameter_id" ref="hr_rule_parameter_at_sv_iesg_dn"/>
|
||||
<field name="date_from">2022-01-01</field>
|
||||
<field name="parameter_value">0.0</field>
|
||||
</record>
|
||||
<record id="hr_rule_parameter_at_sv_kv_dg" model="hr.rule.parameter">
|
||||
<field name="name">SV Krankenversicherung Dienstgeberanteil</field>
|
||||
<field name="code">at_sv_kv_dg</field>
|
||||
<field name="country_id" ref="base.at"/>
|
||||
</record>
|
||||
<record id="hr_rule_parameter_value_at_sv_kv_dg" model="hr.rule.parameter.value">
|
||||
<field name="rule_parameter_id" ref="hr_rule_parameter_at_sv_kv_dg"/>
|
||||
<field name="date_from">2019-01-01</field>
|
||||
<field name="parameter_value">3.78</field>
|
||||
</record>
|
||||
<record id="hr_rule_parameter_at_sv_kv_dn" model="hr.rule.parameter">
|
||||
<field name="name">SV Krankenversicherung Dienstnehmeranteil</field>
|
||||
<field name="code">at_sv_kv_dn</field>
|
||||
<field name="country_id" ref="base.at"/>
|
||||
</record>
|
||||
<record id="hr_rule_parameter_value_at_sv_kv_dn" model="hr.rule.parameter.value">
|
||||
<field name="rule_parameter_id" ref="hr_rule_parameter_at_sv_kv_dn"/>
|
||||
<field name="date_from">2019-01-01</field>
|
||||
<field name="parameter_value">3.87</field>
|
||||
</record>
|
||||
<record id="hr_rule_parameter_at_sv_pv_dg" model="hr.rule.parameter">
|
||||
<field name="name">SV Pensionsversicherung Dienstgeberanteil</field>
|
||||
<field name="code">at_sv_pv_dg</field>
|
||||
<field name="country_id" ref="base.at"/>
|
||||
</record>
|
||||
<record id="hr_rule_parameter_value_at_sv_pv_dg" model="hr.rule.parameter.value">
|
||||
<field name="rule_parameter_id" ref="hr_rule_parameter_at_sv_pv_dg"/>
|
||||
<field name="date_from">2019-01-01</field>
|
||||
<field name="parameter_value">12.55</field>
|
||||
</record>
|
||||
<record id="hr_rule_parameter_at_sv_pv_dn" model="hr.rule.parameter">
|
||||
<field name="name">SV Pensionsversicherung Dienstnehmeranteil</field>
|
||||
<field name="code">at_sv_pv_dn</field>
|
||||
<field name="country_id" ref="base.at"/>
|
||||
</record>
|
||||
<record id="hr_rule_parameter_value_at_sv_pv_dn" model="hr.rule.parameter.value">
|
||||
<field name="rule_parameter_id" ref="hr_rule_parameter_at_sv_pv_dn"/>
|
||||
<field name="date_from">2019-01-01</field>
|
||||
<field name="parameter_value">10.25</field>
|
||||
</record>
|
||||
<record id="hr_rule_parameter_at_sv_uv_dg" model="hr.rule.parameter">
|
||||
<field name="name">SV Unfallversicherung Dienstgeberanteil</field>
|
||||
<field name="code">at_sv_uv_dg</field>
|
||||
<field name="country_id" ref="base.at"/>
|
||||
</record>
|
||||
<record id="hr_rule_parameter_value_at_sv_uv_dg" model="hr.rule.parameter.value">
|
||||
<field name="rule_parameter_id" ref="hr_rule_parameter_at_sv_uv_dg"/>
|
||||
<field name="date_from">2023-01-01</field>
|
||||
<field name="parameter_value">1.1</field>
|
||||
</record>
|
||||
<record id="hr_rule_parameter_at_sv_uv_dn" model="hr.rule.parameter">
|
||||
<field name="name">SV Unfallversicherung Dienstnehmeranteil</field>
|
||||
<field name="code">at_sv_uv_dn</field>
|
||||
<field name="country_id" ref="base.at"/>
|
||||
</record>
|
||||
<record id="hr_rule_parameter_value_at_sv_uv_dn" model="hr.rule.parameter.value">
|
||||
<field name="rule_parameter_id" ref="hr_rule_parameter_at_sv_uv_dn"/>
|
||||
<field name="date_from">2023-01-01</field>
|
||||
<field name="parameter_value">0.0</field>
|
||||
</record>
|
||||
<record id="hr_rule_parameter_at_sv_wf_dg" model="hr.rule.parameter">
|
||||
<field name="name">Wohnbauförderungsbeitrag Dienstgeberanteil</field>
|
||||
<field name="code">at_sv_wf_dg</field>
|
||||
<field name="country_id" ref="base.at"/>
|
||||
</record>
|
||||
<record id="hr_rule_parameter_value_at_sv_wf_dg" model="hr.rule.parameter.value">
|
||||
<field name="rule_parameter_id" ref="hr_rule_parameter_at_sv_wf_dg"/>
|
||||
<field name="date_from">2019-01-01</field>
|
||||
<field name="parameter_value">0.5</field>
|
||||
</record>
|
||||
<record id="hr_rule_parameter_at_sv_wf_dn" model="hr.rule.parameter">
|
||||
<field name="name">Wohnbauförderungsbeitrag Dienstnehmeranteil</field>
|
||||
<field name="code">at_sv_wf_dn</field>
|
||||
<field name="country_id" ref="base.at"/>
|
||||
</record>
|
||||
<record id="hr_rule_parameter_value_at_sv_wf_dn" model="hr.rule.parameter.value">
|
||||
<field name="rule_parameter_id" ref="hr_rule_parameter_at_sv_wf_dn"/>
|
||||
<field name="date_from">2019-01-01</field>
|
||||
<field name="parameter_value">0.5</field>
|
||||
</record>
|
||||
</odoo>
|
||||
@@ -0,0 +1,67 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<!-- Beitragsbemessungsbeträge (Cent-Werte, TASY-LSWH) -->
|
||||
<record id="tasy_mapping_gfg" model="gemeinde.payroll.tasy.mapping">
|
||||
<field name="tasy_art">bbb</field>
|
||||
<field name="tasy_code">GFGBTG</field>
|
||||
<field name="parameter_code">at_sv_gfg_monatlich</field>
|
||||
<field name="cent_werte" eval="True"/>
|
||||
<field name="faktor">1.0</field>
|
||||
</record>
|
||||
<record id="tasy_mapping_hbg_mon" model="gemeinde.payroll.tasy.mapping">
|
||||
<field name="tasy_art">bbb</field>
|
||||
<field name="tasy_code">HBGALLG</field>
|
||||
<field name="parameter_code">at_sv_hbg_monatlich</field>
|
||||
<field name="cent_werte" eval="True"/>
|
||||
<field name="faktor">30</field>
|
||||
</record>
|
||||
<record id="tasy_mapping_hbg_sz" model="gemeinde.payroll.tasy.mapping">
|
||||
<field name="tasy_art">bbb</field>
|
||||
<field name="tasy_code">HBGSZ</field>
|
||||
<field name="parameter_code">at_sv_hbg_sz_jaehrlich</field>
|
||||
<field name="cent_werte" eval="True"/>
|
||||
<field name="faktor">1.0</field>
|
||||
</record>
|
||||
|
||||
<!-- Tarifzweige (Beitragssätze; _dn/_dg wird angehängt) -->
|
||||
<record id="tasy_mapping_kv" model="gemeinde.payroll.tasy.mapping">
|
||||
<field name="tasy_art">zweig</field>
|
||||
<field name="tasy_code">KV</field>
|
||||
<field name="parameter_code">at_sv_kv</field>
|
||||
</record>
|
||||
<record id="tasy_mapping_pv" model="gemeinde.payroll.tasy.mapping">
|
||||
<field name="tasy_art">zweig</field>
|
||||
<field name="tasy_code">PV</field>
|
||||
<field name="parameter_code">at_sv_pv</field>
|
||||
</record>
|
||||
<record id="tasy_mapping_av" model="gemeinde.payroll.tasy.mapping">
|
||||
<field name="tasy_art">zweig</field>
|
||||
<field name="tasy_code">AV</field>
|
||||
<field name="parameter_code">at_sv_av</field>
|
||||
</record>
|
||||
<record id="tasy_mapping_uv" model="gemeinde.payroll.tasy.mapping">
|
||||
<field name="tasy_art">zweig</field>
|
||||
<field name="tasy_code">UV</field>
|
||||
<field name="parameter_code">at_sv_uv</field>
|
||||
</record>
|
||||
<record id="tasy_mapping_iesg" model="gemeinde.payroll.tasy.mapping">
|
||||
<field name="tasy_art">zweig</field>
|
||||
<field name="tasy_code">IE</field>
|
||||
<field name="parameter_code">at_sv_iesg</field>
|
||||
</record>
|
||||
<record id="tasy_mapping_bv" model="gemeinde.payroll.tasy.mapping">
|
||||
<field name="tasy_art">zweig</field>
|
||||
<field name="tasy_code">BV</field>
|
||||
<field name="parameter_code">at_sv_bv</field>
|
||||
</record>
|
||||
<record id="tasy_mapping_ak" model="gemeinde.payroll.tasy.mapping">
|
||||
<field name="tasy_art">zweig</field>
|
||||
<field name="tasy_code">AK</field>
|
||||
<field name="parameter_code">at_sv_ak</field>
|
||||
</record>
|
||||
<record id="tasy_mapping_wf" model="gemeinde.payroll.tasy.mapping">
|
||||
<field name="tasy_art">zweig</field>
|
||||
<field name="tasy_code">WF</field>
|
||||
<field name="parameter_code">at_sv_wf</field>
|
||||
</record>
|
||||
</odoo>
|
||||
@@ -1,3 +1,4 @@
|
||||
# Part of the gem360 onboarding. See LICENSE / project docs for details.
|
||||
from . import entlohnung
|
||||
from . import hr_version_bgld
|
||||
from . import hr_version_bgld
|
||||
from . import tasy
|
||||
@@ -0,0 +1,291 @@
|
||||
# Part of the gem360 onboarding. See LICENSE / project docs for details.
|
||||
from lxml import etree
|
||||
|
||||
from odoo import _, api, fields, models
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
|
||||
class GemeindePayrollTasyMapping(models.Model):
|
||||
"""Mapping TASY-Code/-Zweig -> SV-Regelparameter (hr.rule.parameter).
|
||||
|
||||
Hält die Zuordnung deklarativ (data XML), damit der TASY-Import ohne
|
||||
feste Werte im Code funktioniert und für künftige Export-Formate
|
||||
(z. B. BVAEB) erweiterbar bleibt. Für Tarifzweige wird an den
|
||||
Parameter-Code ``_dn``/``_dg`` angehängt.
|
||||
"""
|
||||
_name = 'gemeinde.payroll.tasy.mapping'
|
||||
_description = 'TASY-Mapping (SV-Regelparameter)'
|
||||
_order = 'tasy_art, tasy_code'
|
||||
|
||||
tasy_art = fields.Selection(
|
||||
[('bbb', 'Beitragsbemessungsbetrag (Grenzwert)'),
|
||||
('zweig', 'Tarifzweig (Beitragssatz)')],
|
||||
required=True)
|
||||
tasy_code = fields.Char(required=True, index=True)
|
||||
parameter_code = fields.Char(
|
||||
required=True, index=True,
|
||||
help="Code des hr.rule.parameter; bei Tarifzweigen wird _dn/_dg "
|
||||
"angehängt.")
|
||||
cent_werte = fields.Boolean(
|
||||
default=True, string='Cent-Werte',
|
||||
help="TASY speichert Beitragsbemessungsbeträge in Cent.")
|
||||
faktor = fields.Float(
|
||||
default=1.0,
|
||||
help="Multiplikator, z. B. 30 für die monatliche HBG aus dem "
|
||||
"täglichen Wert (§ 45 Abs 1 ASVG).")
|
||||
active = fields.Boolean(default=True)
|
||||
|
||||
_unique_mapping = models.Constraint(
|
||||
'unique (tasy_art, tasy_code)',
|
||||
"Je TASY-Art und -Code gibt es nur ein Mapping.")
|
||||
|
||||
@api.depends('tasy_art', 'tasy_code', 'parameter_code')
|
||||
def _compute_display_name(self):
|
||||
art_label = dict(self._fields['tasy_art'].selection)
|
||||
for mapping in self:
|
||||
art = art_label.get(mapping.tasy_art, '')
|
||||
mapping.display_name = (
|
||||
f"{mapping.tasy_code} → {mapping.parameter_code}"
|
||||
f" ({art})" if art else mapping.tasy_code)
|
||||
|
||||
|
||||
class GemeindePayrollTasyImportLine(models.TransientModel):
|
||||
_name = 'gemeinde.payroll.tasy.import.line'
|
||||
_description = 'TASY-Import: Vorschauzeile'
|
||||
|
||||
wizard_id = fields.Many2one(
|
||||
'gemeinde.payroll.tasy.import.wizard', required=True, ondelete='cascade')
|
||||
parameter_code = fields.Char(required=True)
|
||||
name = fields.Char()
|
||||
alter_wert = fields.Char(string='Alter Wert', readonly=True)
|
||||
neuer_wert = fields.Char(string='Neuer Wert', required=True)
|
||||
date_from = fields.Date(required=True)
|
||||
uebernehmen = fields.Boolean(string='Übernehmen', default=True)
|
||||
warnung = fields.Char(help="Leer = eindeutig aufgelöst; sonst "
|
||||
"Plausibilitäts-/Auflösungshinweis. "
|
||||
"Warnzeilen werden nie automatisch übernommen.")
|
||||
|
||||
|
||||
class GemeindePayrollTasyImportWizard(models.TransientModel):
|
||||
"""Lädt einen jährlichen ÖGK TASY-LSWH-Export und schreibt nach
|
||||
expliziter Bestätigung SV-Werte als hr.rule.parameter-Werte
|
||||
(Versionierung über date_from, historische Werte bleiben erhalten).
|
||||
|
||||
Auflösung am Gültigkeitstichtag: Beitragsbemessungsbeträge direkt
|
||||
über den Code; Beitragssätze über die Kette Beschäftigtengruppe
|
||||
(Standard: B002 „Angestellte") → Tarifgruppe → tarifgruppeSetZuord →
|
||||
Tarifset → Tarif. Bei mehreren Varianten gewinnt die Split-Variante
|
||||
(DN>0 und DG>0), danach die einseitige Dienstgeber-Variante (UV/BV);
|
||||
freie-Dienstnehmer-Varianten (nur DN) verlieren. Mehrdeutige
|
||||
Auflösungen erzeugen Warnzeilen ohne automatische Übernahme.
|
||||
"""
|
||||
_name = 'gemeinde.payroll.tasy.import.wizard'
|
||||
_description = 'SV-Werte aus TASY-LSWH-Export importieren'
|
||||
|
||||
file = fields.Binary(required=True, string='TASY-Export (XML)')
|
||||
filename = fields.Char()
|
||||
stichtag = fields.Date(readonly=True,
|
||||
help="Gültigkeitstichtag des Exports.")
|
||||
beschaeftigtengruppe_code = fields.Char(
|
||||
default='B002', required=True,
|
||||
help="TASY-Code der Beschäftigtengruppe (B002 = Angestellte).")
|
||||
line_ids = fields.One2many(
|
||||
'gemeinde.payroll.tasy.import.line', 'wizard_id',
|
||||
string='Vorschau')
|
||||
protocol = fields.Text(readonly=True)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Parsing
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _tasy_root(self):
|
||||
self.ensure_one()
|
||||
parser = etree.XMLParser(resolve_entities=False, no_network=True)
|
||||
return etree.fromstring(self.file, parser)
|
||||
|
||||
@staticmethod
|
||||
def _tasy_text(node, tag):
|
||||
child = node.find(tag)
|
||||
return (child.text or '').strip() if child is not None else ''
|
||||
|
||||
def _parse_bbbwerte(self, root):
|
||||
codes = {b.get('xmlId'): self._tasy_text(b, 'code')
|
||||
for b in root.iter('beitragbemessungsbetrag')}
|
||||
werte = []
|
||||
for w in root.iter('bbbwert'):
|
||||
werte.append({
|
||||
'code': codes.get(self._tasy_text(w, 'beitragbemessungsbetrag_id')),
|
||||
'betrag': float(self._tasy_text(w, 'betrag')),
|
||||
'von': self._tasy_text(w, 'vonDatum'),
|
||||
'bis': self._tasy_text(w, 'bisDatum'),
|
||||
})
|
||||
return werte
|
||||
|
||||
def _parse_tarife(self, root):
|
||||
return {t.get('xmlId'): {
|
||||
'zweig': self._tasy_text(t, 'tarifZweig'),
|
||||
'dn': float(self._tasy_text(t, 'dienstnehmerAnteil')),
|
||||
'dg': float(self._tasy_text(t, 'dienstgeberAnteil')),
|
||||
'von': self._tasy_text(t, 'vonDatum'),
|
||||
'bis': self._tasy_text(t, 'bisDatum'),
|
||||
} for t in root.iter('tarif')}
|
||||
|
||||
def _resolve_zweig_pairs(self, root, stichtag):
|
||||
"""Standard-Beitragssätze je Zweig über die Gruppenkette."""
|
||||
self.ensure_one()
|
||||
gruppen_ids = {g.get('xmlId') for g in root.iter('beschaeftigtengruppe')
|
||||
if self._tasy_text(g, 'code') == self.beschaeftigtengruppe_code}
|
||||
tarifgruppen = {tg.get('xmlId') for tg in root.iter('tarifgruppe')
|
||||
if any(self._tasy_text(tg, 'beschaeftigtengruppen') == gid
|
||||
for gid in gruppen_ids)}
|
||||
sets = {s.get('xmlId'): [t.text.strip() for t in s.iter('tarife')]
|
||||
for s in root.iter('tarifset')}
|
||||
valid_sets = {self._tasy_text(z, 'tarifset')
|
||||
for z in root.iter('tarifgruppeSetZuord')
|
||||
if self._tasy_text(z, 'tarifgruppe') in tarifgruppen
|
||||
and self._tasy_text(z, 'vonDatum') <= stichtag
|
||||
<= self._tasy_text(z, 'bisDatum')}
|
||||
tarife = self._parse_tarife(root)
|
||||
pairs = {}
|
||||
for sid in valid_sets:
|
||||
for tid in sets.get(sid, []):
|
||||
t = tarife.get(tid)
|
||||
if not t or not (t['von'] <= stichtag <= t['bis']):
|
||||
continue
|
||||
pairs.setdefault(t['zweig'], {}).setdefault(
|
||||
(t['dn'], t['dg']), t['von'])
|
||||
return pairs
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Actions
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def action_parse(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 = stichtag
|
||||
lines, protocol = self._build_lines(root, stichtag)
|
||||
self.line_ids = [(5, 0, 0)] + lines
|
||||
self.protocol = "\n".join(protocol)
|
||||
return True
|
||||
|
||||
def _build_lines(self, root, stichtag):
|
||||
self.ensure_one()
|
||||
line_vals, protocol = [], []
|
||||
parameter_obj = self.env['hr.rule.parameter']
|
||||
|
||||
bbbwerte = self._parse_bbbwerte(root)
|
||||
for mapping in self.env['gemeinde.payroll.tasy.mapping'].search(
|
||||
[('tasy_art', '=', 'bbb')]):
|
||||
candidates = [w for w in bbbwerte if w['code'] == mapping.tasy_code
|
||||
and w['von'] <= stichtag <= w['bis']]
|
||||
if not candidates:
|
||||
protocol.append(
|
||||
f"KEIN WERT: {mapping.tasy_code} "
|
||||
f"({mapping.parameter_code}) am Stichtag")
|
||||
continue
|
||||
w = max(candidates, key=lambda x: x['von'])
|
||||
value = w['betrag'] * mapping.faktor
|
||||
if mapping.cent_werte:
|
||||
value /= 100.0
|
||||
line_vals.append(self._line_vals(
|
||||
mapping.parameter_code, round(value, 2), w['von'],
|
||||
parameter_obj, protocol))
|
||||
|
||||
pairs = self._resolve_zweig_pairs(root, stichtag)
|
||||
for mapping in self.env['gemeinde.payroll.tasy.mapping'].search(
|
||||
[('tasy_art', '=', 'zweig')]):
|
||||
variants = pairs.get(mapping.tasy_code, {})
|
||||
chosen = None
|
||||
split = [(dn, dg, von) for (dn, dg), von in variants.items()
|
||||
if dn > 0 and dg > 0]
|
||||
employer = [(dn, dg, von) for (dn, dg), von in variants.items()
|
||||
if dg > 0 and dn == 0]
|
||||
if len(variants) == 1:
|
||||
(dn, dg), von = next(iter(variants.items()))
|
||||
chosen = (dn, dg, von)
|
||||
elif len(split) == 1:
|
||||
chosen = split[0]
|
||||
elif len(employer) == 1:
|
||||
chosen = employer[0]
|
||||
if not chosen:
|
||||
protocol.append(
|
||||
f"NICHT EINDEUTIG: Zweig {mapping.tasy_code} "
|
||||
f"({mapping.parameter_code}): {dict(variants)}")
|
||||
for (dn, dg), von in variants.items():
|
||||
line_vals.append(self._line_vals(
|
||||
f"{mapping.parameter_code}_dn", dn, von, parameter_obj,
|
||||
protocol, warnung=f"Mehrdeutig: DN {dn} / DG {dg}"))
|
||||
continue
|
||||
dn, dg, von = chosen
|
||||
line_vals.append(self._line_vals(
|
||||
f"{mapping.parameter_code}_dn", dn, von, parameter_obj, protocol))
|
||||
line_vals.append(self._line_vals(
|
||||
f"{mapping.parameter_code}_dg", dg, von, parameter_obj, protocol))
|
||||
return line_vals, protocol
|
||||
|
||||
def _line_vals(self, code, value, date_from, parameter_obj, protocol,
|
||||
warnung=None):
|
||||
self.ensure_one()
|
||||
old = parameter_obj._get_parameter_from_code(code, date_from,
|
||||
raise_if_not_found=False)
|
||||
old_value = old.parameter_value if old else ''
|
||||
old_float = None
|
||||
try:
|
||||
old_float = float(old_value)
|
||||
except (TypeError, ValueError):
|
||||
old_float = None
|
||||
if old_float and abs(value - old_float) / old_float > 0.10:
|
||||
warnung = (warnung or '') + f" Sprung > 10 % ({old_value} → {value})"
|
||||
return (0, 0, {
|
||||
'parameter_code': code,
|
||||
'name': parameter_obj.search(
|
||||
[('code', '=', code)]).name or code,
|
||||
'alter_wert': old_value,
|
||||
'neuer_wert': repr(value),
|
||||
'date_from': date_from,
|
||||
'uebernehmen': not warnung,
|
||||
'warnung': warnung or '',
|
||||
})
|
||||
|
||||
def action_apply(self):
|
||||
self.ensure_one()
|
||||
parameter_obj = self.env['hr.rule.parameter']
|
||||
value_obj = self.env['hr.rule.parameter.value']
|
||||
country = self.env.ref('base.at')
|
||||
applied = 0
|
||||
for line in self.line_ids.filtered('uebernehmen'):
|
||||
parameter = parameter_obj.search(
|
||||
[('code', '=', line.parameter_code)], limit=1)
|
||||
if not parameter:
|
||||
parameter = parameter_obj.create({
|
||||
'name': line.name or line.parameter_code,
|
||||
'code': line.parameter_code,
|
||||
'country_id': country.id,
|
||||
})
|
||||
value = value_obj.search([
|
||||
('rule_parameter_id', '=', parameter.id),
|
||||
('date_from', '=', line.date_from)], limit=1)
|
||||
if value:
|
||||
value.parameter_value = line.neuer_wert
|
||||
else:
|
||||
value_obj.create({
|
||||
'rule_parameter_id': parameter.id,
|
||||
'date_from': line.date_from,
|
||||
'parameter_value': line.neuer_wert,
|
||||
})
|
||||
applied += 1
|
||||
return {
|
||||
'type': 'ir.actions.client',
|
||||
'tag': 'display_notification',
|
||||
'params': {
|
||||
'type': 'success',
|
||||
'message': _("%(count)s Parameterwert(e) geschrieben.",
|
||||
count=applied),
|
||||
'next': {'type': 'ir.actions.act_window_close'},
|
||||
},
|
||||
}
|
||||
@@ -2,4 +2,8 @@ id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
access_gemeinde_payroll_entlohnungsgruppe_user,Gemeinde Payroll Entlohnungsgruppe User,model_gemeinde_payroll_entlohnungsgruppe,hr_payroll.group_hr_payroll_user,1,0,0,0
|
||||
access_gemeinde_payroll_entlohnungsgruppe_manager,Gemeinde Payroll Entlohnungsgruppe Manager,model_gemeinde_payroll_entlohnungsgruppe,hr_payroll.group_hr_payroll_manager,1,1,1,1
|
||||
access_gemeinde_payroll_entlohnungsstufe_user,Gemeinde Payroll Entlohnungsstufe User,model_gemeinde_payroll_entlohnungsstufe,hr_payroll.group_hr_payroll_user,1,0,0,0
|
||||
access_gemeinde_payroll_entlohnungsstufe_manager,Gemeinde Payroll Entlohnungsstufe Manager,model_gemeinde_payroll_entlohnungsstufe,hr_payroll.group_hr_payroll_manager,1,1,1,1
|
||||
access_gemeinde_payroll_entlohnungsstufe_manager,Gemeinde Payroll Entlohnungsstufe Manager,model_gemeinde_payroll_entlohnungsstufe,hr_payroll.group_hr_payroll_manager,1,1,1,1
|
||||
access_gemeinde_payroll_tasy_mapping_user,Gemeinde Payroll TASY-Mapping User,model_gemeinde_payroll_tasy_mapping,hr_payroll.group_hr_payroll_user,1,0,0,0
|
||||
access_gemeinde_payroll_tasy_mapping_manager,Gemeinde Payroll TASY-Mapping Manager,model_gemeinde_payroll_tasy_mapping,hr_payroll.group_hr_payroll_manager,1,1,1,1
|
||||
access_gemeinde_payroll_tasy_import_wizard,Gemeinde Payroll TASY-Import Wizard,model_gemeinde_payroll_tasy_import_wizard,hr_payroll.group_hr_payroll_user,1,1,1,1
|
||||
access_gemeinde_payroll_tasy_import_line,Gemeinde Payroll TASY-Import Line,model_gemeinde_payroll_tasy_import_line,hr_payroll.group_hr_payroll_user,1,1,1,1
|
||||
|
@@ -1,2 +1,3 @@
|
||||
# Part of the gem360 onboarding. See LICENSE / project docs for details.
|
||||
from . import test_katalog
|
||||
from . import test_katalog
|
||||
from . import test_tasy_import
|
||||
@@ -0,0 +1,194 @@
|
||||
# Part of the gem360 onboarding. See LICENSE / project docs for details.
|
||||
import base64
|
||||
from datetime import date
|
||||
|
||||
from odoo.tests import TransactionCase, tagged
|
||||
|
||||
# Minimaler TASY-LSWH-Strukturslice für Parser-Tests (B002 „Angestellte",
|
||||
# KV Split- + freie-DN-Variante, GFGBTG in Cent).
|
||||
TASY_FIXTURE = """<?xml version="1.0" encoding="UTF-8"?>
|
||||
<tarifssystemlsh>
|
||||
<gueltigkeitstichtag>2026-07-01</gueltigkeitstichtag>
|
||||
<beitragbemessungsbetraege>
|
||||
<beitragbemessungsbetrag xmlId="B100">
|
||||
<code>GFGBTG</code>
|
||||
<bezeichnung>Test Geringfügigkeitsgrenze</bezeichnung>
|
||||
<vonDatum>2019-01-01</vonDatum>
|
||||
<bisDatum>2999-12-31</bisDatum>
|
||||
</beitragbemessungsbetrag>
|
||||
</beitragbemessungsbetraege>
|
||||
<bbbwerte>
|
||||
<bbbwert xmlId="W100">
|
||||
<beitragbemessungsbetrag_id>B100</beitragbemessungsbetrag_id>
|
||||
<betrag>55110</betrag>
|
||||
<vonDatum>2025-01-01</vonDatum>
|
||||
<bisDatum>2999-12-31</bisDatum>
|
||||
</bbbwert>
|
||||
</bbbwerte>
|
||||
<beschaeftigtengruppen>
|
||||
<beschaeftigtengruppe xmlId="G200">
|
||||
<code>B002</code>
|
||||
<bezeichnung>Angestellte</bezeichnung>
|
||||
<vonDatum>2019-01-01</vonDatum>
|
||||
<bisDatum>2999-12-31</bisDatum>
|
||||
</beschaeftigtengruppe>
|
||||
</beschaeftigtengruppen>
|
||||
<tarifgruppen>
|
||||
<tarifgruppe xmlId="T300">
|
||||
<beschaeftigtengruppen>G200</beschaeftigtengruppen>
|
||||
<code>B002</code>
|
||||
<vonDatum>2019-01-01</vonDatum>
|
||||
<bisDatum>2999-12-31</bisDatum>
|
||||
</tarifgruppe>
|
||||
</tarifgruppen>
|
||||
<tarife>
|
||||
<tarif xmlId="K400">
|
||||
<vonDatum>2019-01-01</vonDatum>
|
||||
<bisDatum>2999-12-31</bisDatum>
|
||||
<dienstgeberAnteil>3.78</dienstgeberAnteil>
|
||||
<dienstnehmerAnteil>3.87</dienstnehmerAnteil>
|
||||
<tarifZweig>KV</tarifZweig>
|
||||
</tarif>
|
||||
<tarif xmlId="K401">
|
||||
<vonDatum>2019-01-01</vonDatum>
|
||||
<bisDatum>2999-12-31</bisDatum>
|
||||
<dienstgeberAnteil>0</dienstgeberAnteil>
|
||||
<dienstnehmerAnteil>7.65</dienstnehmerAnteil>
|
||||
<tarifZweig>KV</tarifZweig>
|
||||
</tarif>
|
||||
<tarif xmlId="U402">
|
||||
<vonDatum>2023-01-01</vonDatum>
|
||||
<bisDatum>2999-12-31</bisDatum>
|
||||
<dienstgeberAnteil>1.1</dienstgeberAnteil>
|
||||
<dienstnehmerAnteil>0</dienstnehmerAnteil>
|
||||
<tarifZweig>UV</tarifZweig>
|
||||
</tarif>
|
||||
</tarife>
|
||||
<tarifsets>
|
||||
<tarifset xmlId="S500">
|
||||
<bezeichnung>Test Standard</bezeichnung>
|
||||
<vonDatum>2019-01-01</vonDatum>
|
||||
<bisDatum>2999-12-31</bisDatum>
|
||||
<tarife>K400</tarife>
|
||||
<tarife>U402</tarife>
|
||||
</tarifset>
|
||||
<tarifset xmlId="S501">
|
||||
<bezeichnung>Test freie DN</bezeichnung>
|
||||
<vonDatum>2019-01-01</vonDatum>
|
||||
<bisDatum>2999-12-31</bisDatum>
|
||||
<tarife>K401</tarife>
|
||||
</tarifset>
|
||||
</tarifsets>
|
||||
<tarifgruppeSetZuordnungen>
|
||||
<tarifgruppeSetZuord xmlId="Z600">
|
||||
<tarifgruppe>T300</tarifgruppe>
|
||||
<tarifset>S500</tarifset>
|
||||
<vonDatum>2019-01-01</vonDatum>
|
||||
<bisDatum>2999-12-31</bisDatum>
|
||||
</tarifgruppeSetZuord>
|
||||
<tarifgruppeSetZuord xmlId="Z601">
|
||||
<tarifgruppe>T300</tarifgruppe>
|
||||
<tarifset>S501</tarifset>
|
||||
<vonDatum>2019-01-01</vonDatum>
|
||||
<bisDatum>2999-12-31</bisDatum>
|
||||
</tarifgruppeSetZuord>
|
||||
</tarifgruppeSetZuordnungen>
|
||||
</tarifssystemlsh>
|
||||
"""
|
||||
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
class TasyImportTest(TransactionCase):
|
||||
"""TASY-Import: Seed-Werte, Parser/Resolution, Schreibverhalten."""
|
||||
|
||||
def test_01_seed_parameter_values(self):
|
||||
"""Seed 2026 (aus echtem TASY-Export generiert) am Stichtag."""
|
||||
parameter = self.env['hr.rule.parameter']
|
||||
stichtag = date(2026, 7, 1)
|
||||
self.assertEqual(
|
||||
float(parameter._get_parameter_from_code('at_sv_kv_dn', stichtag).parameter_value),
|
||||
3.87)
|
||||
self.assertEqual(
|
||||
float(parameter._get_parameter_from_code('at_sv_kv_dg', stichtag).parameter_value),
|
||||
3.78)
|
||||
self.assertEqual(
|
||||
float(parameter._get_parameter_from_code('at_sv_gfg_monatlich', stichtag).parameter_value),
|
||||
551.10)
|
||||
self.assertEqual(
|
||||
float(parameter._get_parameter_from_code('at_sv_hbg_monatlich', stichtag).parameter_value),
|
||||
6930.00)
|
||||
self.assertEqual(
|
||||
float(parameter._get_parameter_from_code('at_sv_uv_dg', stichtag).parameter_value),
|
||||
1.10)
|
||||
self.assertEqual(
|
||||
float(parameter._get_parameter_from_code('at_sv_iesg_dg', stichtag).parameter_value),
|
||||
0.10)
|
||||
|
||||
def test_02_wizard_parse_fixture(self):
|
||||
"""Fixture: bbb Cent-Werte + Split-Variante gewinnt über freie DN."""
|
||||
wizard = self.env['gemeinde.payroll.tasy.import.wizard'].create({
|
||||
'file': base64.b64encode(TASY_FIXTURE.encode()).decode(),
|
||||
'filename': 'fixture.xml',
|
||||
})
|
||||
wizard.action_parse()
|
||||
self.assertEqual(wizard.stichtag, date(2026, 7, 1))
|
||||
lines = {line.parameter_code: line for line in wizard.line_ids}
|
||||
self.assertIn('at_sv_gfg_monatlich', lines)
|
||||
self.assertEqual(float(lines['at_sv_gfg_monatlich'].neuer_wert), 551.10)
|
||||
# KV: Split-Variante 3.87/3.78 statt freier DN 7.65/0
|
||||
self.assertEqual(float(lines['at_sv_kv_dn'].neuer_wert), 3.87)
|
||||
self.assertEqual(float(lines['at_sv_kv_dg'].neuer_wert), 3.78)
|
||||
# UV: einseitige DG-Variante
|
||||
self.assertEqual(float(lines['at_sv_uv_dg'].neuer_wert), 1.10)
|
||||
# eindeutige Auflösungen sind zur Übernahme vorgeschlagen
|
||||
self.assertTrue(lines['at_sv_kv_dn'].uebernehmen)
|
||||
self.assertFalse(lines['at_sv_kv_dn'].warnung)
|
||||
|
||||
def test_03_wizard_apply_versions(self):
|
||||
"""Übernehmen schreibt versionierte Werte; Update am Stichtag."""
|
||||
wizard = self.env['gemeinde.payroll.tasy.import.wizard'].create({
|
||||
'file': base64.b64encode(TASY_FIXTURE.encode()).decode(),
|
||||
'filename': 'fixture.xml',
|
||||
})
|
||||
wizard.action_parse()
|
||||
wizard.action_apply()
|
||||
value = self.env['hr.rule.parameter.value'].search([
|
||||
('code', '=', 'at_sv_gfg_monatlich'),
|
||||
('date_from', '=', date(2025, 1, 1))], limit=1)
|
||||
self.assertTrue(value)
|
||||
self.assertEqual(float(value.parameter_value), 551.10)
|
||||
# erneuter Import am selben Stichtag aktualisiert statt zu duplizieren
|
||||
wizard2 = self.env['gemeinde.payroll.tasy.import.wizard'].create({
|
||||
'file': base64.b64encode(TASY_FIXTURE.encode()).decode(),
|
||||
'filename': 'fixture.xml',
|
||||
})
|
||||
wizard2.action_parse()
|
||||
wizard2.action_apply()
|
||||
count = self.env['hr.rule.parameter.value'].search_count([
|
||||
('code', '=', 'at_sv_gfg_monatlich'),
|
||||
('date_from', '=', date(2025, 1, 1))])
|
||||
self.assertEqual(count, 1)
|
||||
|
||||
def test_04_wizard_real_export(self):
|
||||
"""Echter TASY-Export aus personalverrechnung/quellen (5 MB):
|
||||
Auflösung muss die offiziellen 2026er Werte liefern."""
|
||||
import os
|
||||
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:
|
||||
wizard = self.env['gemeinde.payroll.tasy.import.wizard'].create({
|
||||
'file': base64.b64encode(fh.read()).decode(),
|
||||
'filename': 'TASY-LSWH-Export-20260324.xml',
|
||||
})
|
||||
wizard.action_parse()
|
||||
lines = {line.parameter_code: line for line in wizard.line_ids}
|
||||
self.assertEqual(wizard.stichtag, date(2026, 7, 1))
|
||||
self.assertEqual(float(lines['at_sv_kv_dn'].neuer_wert), 3.87)
|
||||
self.assertEqual(float(lines['at_sv_pv_dg'].neuer_wert), 12.55)
|
||||
self.assertEqual(float(lines['at_sv_hbg_sz_jaehrlich'].neuer_wert), 13860.00)
|
||||
# Seed liegt bereits auf denselben Stichtagen -> keine Warnzeilen
|
||||
for line in wizard.line_ids:
|
||||
self.assertFalse(line.warnung, line.warnung)
|
||||
@@ -0,0 +1,106 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="tasy_mapping_list_view" model="ir.ui.view">
|
||||
<field name="name">gemeinde.payroll.tasy.mapping.list</field>
|
||||
<field name="model">gemeinde.payroll.tasy.mapping</field>
|
||||
<field name="arch" type="xml">
|
||||
<list string="TASY-Mappings">
|
||||
<field name="tasy_art"/>
|
||||
<field name="tasy_code"/>
|
||||
<field name="parameter_code"/>
|
||||
<field name="cent_werte"/>
|
||||
<field name="faktor"/>
|
||||
<field name="active" widget="boolean_toggle"/>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="tasy_mapping_form_view" model="ir.ui.view">
|
||||
<field name="name">gemeinde.payroll.tasy.mapping.form</field>
|
||||
<field name="model">gemeinde.payroll.tasy.mapping</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="TASY-Mapping">
|
||||
<sheet>
|
||||
<group>
|
||||
<group>
|
||||
<field name="tasy_art"/>
|
||||
<field name="tasy_code"/>
|
||||
<field name="parameter_code"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="cent_werte" invisible="tasy_art != 'bbb'"/>
|
||||
<field name="faktor" invisible="tasy_art != 'bbb'"/>
|
||||
<field name="active" widget="boolean_toggle"/>
|
||||
</group>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_tasy_mapping" model="ir.actions.act_window">
|
||||
<field name="name">TASY-Mappings (SV-Werte)</field>
|
||||
<field name="res_model">gemeinde.payroll.tasy.mapping</field>
|
||||
<field name="view_mode">list,form</field>
|
||||
</record>
|
||||
|
||||
<record id="tasy_import_wizard_form_view" model="ir.ui.view">
|
||||
<field name="name">gemeinde.payroll.tasy.import.wizard.form</field>
|
||||
<field name="model">gemeinde.payroll.tasy.import.wizard</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="SV-Werte aus TASY-LSWH-Export importieren">
|
||||
<group>
|
||||
<field name="file" filename="filename"/>
|
||||
<field name="filename" invisible="1"/>
|
||||
<field name="beschaeftigtengruppe_code"/>
|
||||
<field name="stichtag" readonly="1"/>
|
||||
</group>
|
||||
<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="parameter_code"/>
|
||||
<field name="name"/>
|
||||
<field name="alter_wert"/>
|
||||
<field name="neuer_wert"/>
|
||||
<field name="date_from"/>
|
||||
<field name="warnung"
|
||||
decoration-danger="warnung"/>
|
||||
</list>
|
||||
</field>
|
||||
</page>
|
||||
<page string="Protokoll" name="protokoll">
|
||||
<field name="protocol"/>
|
||||
</page>
|
||||
</notebook>
|
||||
<footer>
|
||||
<button string="1) Datei 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_tasy_import_wizard" model="ir.actions.act_window">
|
||||
<field name="name">SV-Werte importieren (TASY)</field>
|
||||
<field name="res_model">gemeinde.payroll.tasy.import.wizard</field>
|
||||
<field name="view_mode">form</field>
|
||||
<field name="target">new</field>
|
||||
</record>
|
||||
|
||||
<menuitem id="menu_gemeinde_payroll_tasy_mapping"
|
||||
name="TASY-Mappings (SV-Werte)"
|
||||
parent="menu_gemeinde_payroll_root"
|
||||
action="action_tasy_mapping" sequence="40"/>
|
||||
|
||||
<menuitem id="menu_gemeinde_payroll_tasy_import"
|
||||
name="SV-Werte importieren (TASY)"
|
||||
parent="menu_gemeinde_payroll_root"
|
||||
action="action_tasy_import_wizard" sequence="50"/>
|
||||
</odoo>
|
||||
Reference in New Issue
Block a user