mirror of
http://100.103.83.12:3003/fegger/odoo-at-payroll.git
synced 2026-09-17 16:56:42 +00:00
[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,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)
|
||||
Reference in New Issue
Block a user