[IMP] l10n_at_gemeinde_payroll: implement AP2 pay computation engine (GemBG Bgld.)

Entgelt-Engine nach Implementierungsplan AP2 (freigegeben 2026-09-09):

- Structure Type "Bgld. GemBG 2014" + Structure "Gemeindebedienstete"
  mit 4 Kategorien (GEMBG_BRUTTO, GEMBG_ABZUG, GEMBG_NETTO, GEMBG_DG).
- Work-Entry-Types: Überstunden Tag/Nacht (§ 76), Mehrdienstleistung,
  Journaldienst (§ 79), Bereitschaft (§ 80), Sonn-/Feiertag (§ 78).
- Input-Types: Erschwerniszulage (§ 83), Fahrtkostenzuschuss (§ 88),
  Jubiläumszuwendung (§ 89).
- Entgelt-Regeln (13 Rules):
  * ME — Monatsentgelt: Katalog-Lookup (Gruppe+Stufe+Stichtag),
    Ausbildungsphase 5 % Abzug (§ 133h), Teilzeit via work_time_rate.
  * KIZU — Kinderzulage: kinder_anzahl × 15,60 EUR (§ 61 GemBG),
    bei Teilzeit UNGEKÜRZT (§ 59 Abs 2).
  * FUZU — Funktionszulage: hr.version-Feld × work_time_rate (§ 62).
  * UEZ_TAG/UEZ_NACHT — Überstundenvergütung (§ 76):
    Grund = ME / (4,33 × Wochenstunden); Zuschlag 50 % Tag / 100 % Nacht.
  * JZU — Journaldienstzulage (§ 79): Work-Entry × Parametersatz.
  * SFZU — Sonn-/Feiertagszulage (§ 78): Stunden × Grundvergütung × %.
  * BERB — Bereitschaftsentschädigung (§ 80): Stunden × Parametersatz.
  * ERZU/FKZ/JUB — Input-basierte Bezüge (§§ 83/88/89).
  * SZQ — Sonderzahlung QUARTALSWEISE (§ 133n): in Quartalsendmonaten
    (März, Juni, September, Dezember) 50 % × (ME + KIZU).
  * NETTO — Kategoriensumme (wird mit AP3/AP4 erweitert).
- hr.version-Felder: kinder_anzahl, funktionszulage, ausbildungsphase
  (§ 61/§ 62/§ 133h) mit Employee-Spiegel nach Engine-Muster.
- Rule-Parameter: Kinderzulage 15,60 EUR, Journaldienstzulage,
  Sonn-/Feiertags-%, Bereitschaftsstunde (letzte 3 als AP0-Platzhalter
  mit 0.0 — Werte aus der Bezügeanpassung nach Beschaffung).
- Tests (8 Rechenfälle): Vollzeit av1 Stufe 1, Kinderzulage 2 Kinder
  = 31,20 EUR, Funktionszulage, Ausbildungsphase 5 % Abzug,
  SZ Quartalsende + Nicht-Quartalsende, Netto=Brutto (AP3/AP4 offen),
  Stufe 12 Max.
- Employee-Form um Zulagen-Gruppe erweitert.

Modul-Version 19.0.1.3.0. Statische Validierung grün; Runtime-Testlauf
auf dem Dev-Host ausständig.
This commit is contained in:
2026-09-09 13:01:00 +02:00
parent 42bb759fd1
commit a7959c1259
9 changed files with 627 additions and 2 deletions
@@ -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.2.0',
'version': '19.0.1.3.0',
'author': 'ixsol / gem360',
'website': 'https://gem360.at',
'category': 'Human Resources/Payroll',
@@ -52,7 +52,10 @@ Grundlagen: ``personalverrechnung/PLAN-payroll-bgld.md``,
'data/gemeinde.payroll.entlohnungsgruppe.csv',
'data/gemeinde.payroll.entlohnungsstufe.csv',
'data/rule_parameters_2026.xml',
'data/rule_parameters_entgelt.xml',
'data/tasy_mapping.xml',
'data/payroll_structure_bgld.xml',
'data/salary_rules_bgld_entgelt.xml',
'views/entlohnung_views.xml',
'views/hr_version_bgld_views.xml',
'views/tasy_views.xml',
@@ -0,0 +1,107 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- ============================================================ -->
<!-- Structure Type: Bgld. GemBG -->
<!-- ============================================================ -->
<record id="structure_type_gembg_bgld" model="hr.payroll.structure.type">
<field name="name">Bgld. GemBG 2014</field>
<field name="country_id" ref="base.at"/>
<field name="default_schedule_pay">monthly</field>
</record>
<!-- ============================================================ -->
<!-- Structure: Gemeindebedienstete Bgld. -->
<!-- ============================================================ -->
<record id="structure_gembg_bgld" model="hr.payroll.structure">
<field name="name">Gemeindebedienstete (Bgld. GemBG 2014)</field>
<field name="code">GEMBG.BGLD</field>
<field name="type_id" ref="structure_type_gembg_bgld"/>
<field name="regular_pay" eval="True"/>
</record>
<!-- ============================================================ -->
<!-- Kategorien -->
<!-- ============================================================ -->
<record id="category_gembg_brutto" model="hr.salary.rule.category">
<field name="name">GemBG Brutto (Entgelt)</field>
<field name="code">GEMBG_BRUTTO</field>
</record>
<record id="category_gembg_abzug" model="hr.salary.rule.category">
<field name="name">GemBG Abzüge (LSt, SV)</field>
<field name="code">GEMBG_ABZUG</field>
</record>
<record id="category_gembg_netto" model="hr.salary.rule.category">
<field name="name">GemBG Netto</field>
<field name="code">GEMBG_NETTO</field>
</record>
<record id="category_gembg_dg" model="hr.salary.rule.category">
<field name="name">GemBG Arbeitgeberanteile</field>
<field name="code">GEMBG_DG</field>
</record>
<!-- ============================================================ -->
<!-- Work-Entry-Types für GemBG-Komponenten -->
<!-- ============================================================ -->
<record id="work_entry_type_ueberstunden_tag" model="hr.work.entry.type">
<field name="name">Überstunden (tagsüber, § 76 GemBG)</field>
<field name="code">GEMBG_UEB_TAG</field>
<field name="round_days">NO</field>
<field name="round_days_type">DOWN</field>
<field name="is_paid" eval="True"/>
</record>
<record id="work_entry_type_ueberstunden_nacht" model="hr.work.entry.type">
<field name="name">Überstunden (Nachtzeit 22-6 Uhr, § 76 GemBG)</field>
<field name="code">GEMBG_UEB_NACHT</field>
<field name="round_days">NO</field>
<field name="round_days_type">DOWN</field>
<field name="is_paid" eval="True"/>
</record>
<record id="work_entry_type_mehrdienst" model="hr.work.entry.type">
<field name="name">Mehrdienstleistung (§ 76 GemBG)</field>
<field name="code">GEMBG_MEHR</field>
<field name="round_days">NO</field>
<field name="round_days_type">DOWN</field>
<field name="is_paid" eval="True"/>
</record>
<record id="work_entry_type_journaldienst" model="hr.work.entry.type">
<field name="name">Journaldienst (§ 79 GemBG)</field>
<field name="code">GEMBG_JOURNAL</field>
<field name="round_days">NO</field>
<field name="round_days_type">DOWN</field>
<field name="is_paid" eval="True"/>
</record>
<record id="work_entry_type_bereitschaft" model="hr.work.entry.type">
<field name="name">Bereitschaftsdienst (§ 80 GemBG)</field>
<field name="code">GEMBG_BEREIT</field>
<field name="round_days">NO</field>
<field name="round_days_type">DOWN</field>
<field name="is_paid" eval="True"/>
</record>
<record id="work_entry_type_sonn_feiertag" model="hr.work.entry.type">
<field name="name">Sonn-/Feiertagsdienst (§ 78 GemBG)</field>
<field name="code">GEMBG_SONNFEI</field>
<field name="round_days">NO</field>
<field name="round_days_type">DOWN</field>
<field name="is_paid" eval="True"/>
</record>
<!-- ============================================================ -->
<!-- Input-Types für manuelle Zulagen -->
<!-- ============================================================ -->
<record id="input_type_funktionszulage" model="hr.payslip.input.type">
<field name="name">Funktionszulage Überschreibung (§ 62)</field>
<field name="code">FUZU_INPUT</field>
</record>
<record id="input_type_erschwerniszulage" model="hr.payslip.input.type">
<field name="name">Erschwerniszulage (§ 83 GemBG)</field>
<field name="code">ERZU_INPUT</field>
</record>
<record id="input_type_fahrtkosten" model="hr.payslip.input.type">
<field name="name">Fahrtkostenzuschuss (§ 88 GemBG)</field>
<field name="code">FKZ_INPUT</field>
</record>
<record id="input_type_jubilaeum" model="hr.payslip.input.type">
<field name="name">Jubiläumszuwendung (§ 89 GemBG)</field>
<field name="code">JUB_INPUT</field>
</record>
</odoo>
@@ -0,0 +1,52 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- Entgelt-Parameter aus GemBG 2014 (konsolidiert 09.09.2026) -->
<!-- Kinderzulage § 61 GemBG: 15,60 EUR je Kind monatlich -->
<record id="hr_rule_parameter_gembg_kinderzulage_rate" model="hr.rule.parameter">
<field name="name">GemBG Kinderzulage je Kind (monatlich, § 61)</field>
<field name="code">gembg_kinderzulage_rate</field>
<field name="country_id" ref="base.at"/>
</record>
<record id="hr_rule_parameter_value_gembg_kinderzulage_rate" model="hr.rule.parameter.value">
<field name="rule_parameter_id" ref="hr_rule_parameter_gembg_kinderzulage_rate"/>
<field name="date_from">2019-01-01</field>
<field name="parameter_value">15.60</field>
</record>
<!-- Journaldienstzulage § 79 GemBG — pro Journaldienst-Tag -->
<record id="hr_rule_parameter_gembg_journaldienstzulage" model="hr.rule.parameter">
<field name="name">GemBG Journaldienstzulage je Werktag (§ 79)</field>
<field name="code">gembg_journaldienstzulage</field>
<field name="country_id" ref="base.at"/>
</record>
<record id="hr_rule_parameter_value_gembg_journaldienstzulage" model="hr.rule.parameter.value">
<field name="rule_parameter_id" ref="hr_rule_parameter_gembg_journaldienstzulage"/>
<field name="date_from">2019-01-01</field>
<field name="parameter_value">0.0</field>
</record>
<!-- Sonn-/Feiertagszulage § 78 GemBG — Prozentsatz auf Grundvergütung -->
<record id="hr_rule_parameter_gembg_sonnfeiertags_prozent" model="hr.rule.parameter">
<field name="name">GemBG Sonn-/Feiertagszulage Prozentsatz (§ 78)</field>
<field name="code">gembg_sonnfeiertags_prozent</field>
<field name="country_id" ref="base.at"/>
</record>
<record id="hr_rule_parameter_value_gembg_sonnfeiertags_prozent" model="hr.rule.parameter.value">
<field name="rule_parameter_id" ref="hr_rule_parameter_gembg_sonnfeiertags_prozent"/>
<field name="date_from">2019-01-01</field>
<field name="parameter_value">0.0</field>
</record>
<!-- Bereitschaftsentschädigung § 80 GemBG — je Bereitschaftsstunde -->
<record id="hr_rule_parameter_gembg_bereitschaftsstunde" model="hr.rule.parameter">
<field name="name">GemBG Bereitschaftsentschädigung je Stunde (§ 80)</field>
<field name="code">gembg_bereitschaftsstunde</field>
<field name="country_id" ref="base.at"/>
</record>
<record id="hr_rule_parameter_value_gembg_bereitschaftsstunde" model="hr.rule.parameter.value">
<field name="rule_parameter_id" ref="hr_rule_parameter_gembg_bereitschaftsstunde"/>
<field name="date_from">2019-01-01</field>
<field name="parameter_value">0.0</field>
</record>
</odoo>
@@ -0,0 +1,290 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- ============================================================ -->
<!-- Monatsentgelt (§ 55 GemBG / IVa §§ 133 ff) -->
<!-- ============================================================ -->
<record id="rule_me" model="hr.salary.rule">
<field name="name">Monatsentgelt</field>
<field name="code">ME</field>
<field name="sequence" eval="1"/>
<field name="category_id" ref="payroll_structure_bgld.category_gembg_brutto"/>
<field name="struct_id" ref="payroll_structure_bgld.structure_gembg_bgld"/>
<field name="condition_select">python</field>
<field name="condition_python">result = bool(employee.entlohnungsgruppe_id and employee.entlohnungsstufe)</field>
<field name="amount_select">python</field>
<field name="amount_python_compute"><![CDATA[
# Monatsentgelt nach Bgld. GemBG 2014 (IVa. Hauptstück §§ 133 ff).
# Katalog-Lookup: Entlohnungsgruppe + Stufe + Stichtag.
katalog_pos = employee.entlohnungsgruppe_id._get_entgelt(
employee.entlohnungsstufe, payslip.date_to)
if not katalog_pos:
result = 0
else:
base = katalog_pos.monatsentgelt
# Ausbildungsphase: 5 % Abzug (§ 133h GemBG)
if employee.ausbildungsphase:
base *= 0.95
# Teilzeit: anteilig über work_time_rate (§ 59 GemBG)
work_rate = employee.work_time_rate or 100.0
result = base * work_rate / 100.0
]]></field>
</record>
<!-- ============================================================ -->
<!-- Kinderzulage (§ 61 GemBG: 15,60 EUR je Kind, ungekürzt) -->
<!-- ============================================================ -->
<record id="rule_kizu" model="hr.salary.rule">
<field name="name">Kinderzulage (§ 61 GemBG)</field>
<field name="code">KIZU</field>
<field name="sequence" eval="2"/>
<field name="category_id" ref="payroll_structure_bgld.category_gembg_brutto"/>
<field name="struct_id" ref="payroll_structure_bgld.structure_gembg_bgld"/>
<field name="condition_select">python</field>
<field name="condition_python">result = employee.kinder_anzahl > 0</field>
<field name="amount_select">code</field>
<field name="amount_python_compute"><![CDATA[
# Kinderzulage § 61 GemBG: 15,60 EUR je Kind monatlich.
# Bei Teilzeit UNGEKÜRZT (§ 59 Abs 2: "die Kinderzulage gebührt
# jedoch in ungekürzter Höhe").
kiz_satz = payslip._rule_parameter('gembg_kinderzulage_rate')
result = employee.kinder_anzahl * kiz_satz
]]></field>
</record>
<!-- ============================================================ -->
<!-- Funktionszulage (§ 62 GemBG) -->
<!-- ============================================================ -->
<record id="rule_fuzu" model="hr.salary.rule">
<field name="name">Funktionszulage (§ 62 GemBG)</field>
<field name="code">FUZU</field>
<field name="sequence" eval="3"/>
<field name="category_id" ref="payroll_structure_bgld.category_gembg_brutto"/>
<field name="struct_id" ref="payroll_structure_bgld.structure_gembg_bgld"/>
<field name="condition_select">python</field>
<field name="condition_python">result = employee.funktionszulage > 0</field>
<field name="amount_select">code</field>
<field name="amount_python_compute"><![CDATA[
# Funktionszulage § 62 GemBG — Satz am Dienstvertrag (hr.version).
# Bei Teilzeit anteilig (wie Monatsentgelt).
work_rate = employee.work_time_rate or 100.0
result = employee.funktionszulage * work_rate / 100.0
]]></field>
</record>
<!-- ============================================================ -->
<!-- Überstundenvergütung (§ 76 GemBG) -->
<!-- ============================================================ -->
<record id="rule_uez_tag" model="hr.salary.rule">
<field name="name">Überstundenvergütung tagsüber (§ 76 GemBG)</field>
<field name="code">UEZ_TAG</field>
<field name="sequence" eval="10"/>
<field name="category_id" ref="payroll_structure_bgld.category_gembg_brutto"/>
<field name="struct_id" ref="payroll_structure_bgld.structure_gembg_bgld"/>
<field name="condition_select">python</field>
<field name="condition_python">result = 'GEMBG_UEB_TAG' in worked_days and worked_days['GEMBG_UEB_TAG'].number_of_hours > 0</field>
<field name="amount_select">code</field>
<field name="amount_python_compute"><![CDATA[
# § 76 GemBG: Grundvergütung + 50 % Zuschlag (tagsüber).
# Grundvergütung/ÜStd = Monatsentgelt / (4,33 × Wochenstundenzahl)
base_entgelt = rules['ME'].total
if base_entgelt and employee.entlohnungsgruppe_id:
wochenstunden = employee.hours_per_week or 40.0
grund_pro_uestd = base_entgelt / (4.33 * wochenstunden)
ueb_stunden = worked_days['GEMBG_UEB_TAG'].number_of_hours
result = ueb_stunden * grund_pro_uestd * 1.5 # 100 % + 50 % Zuschlag
else:
result = 0
]]></field>
</record>
<record id="rule_uez_nacht" model="hr.salary.rule">
<field name="name">Überstundenvergütung Nachtzeit (§ 76 GemBG)</field>
<field name="code">UEZ_NACHT</field>
<field name="sequence" eval="11"/>
<field name="category_id" ref="payroll_structure_bgld.category_gembg_brutto"/>
<field name="struct_id" ref="payroll_structure_bgld.structure_gembg_bgld"/>
<field name="condition_select">python</field>
<field name="condition_python">result = 'GEMBG_UEB_NACHT' in worked_days and worked_days['GEMBG_UEB_NACHT'].number_of_hours > 0</field>
<field name="amount_select">code</field>
<field name="amount_python_compute"><![CDATA[
# § 76 GemBG: Grundvergütung + 100 % Zuschlag (Nachtzeit 22-6 Uhr).
base_entgelt = rules['ME'].total
if base_entgelt and employee.entlohnungsgruppe_id:
wochenstunden = employee.hours_per_week or 40.0
grund_pro_uestd = base_entgelt / (4.33 * wochenstunden)
ueb_stunden = worked_days['GEMBG_UEB_NACHT'].number_of_hours
result = ueb_stunden * grund_pro_uestd * 2.0 # 100 % + 100 % Zuschlag
else:
result = 0
]]></field>
</record>
<!-- ============================================================ -->
<!-- Journaldienstzulage (§ 79 GemBG) — Input -->
<!-- ============================================================ -->
<record id="rule_jzu" model="hr.salary.rule">
<field name="name">Journaldienstzulage (§ 79 GemBG)</field>
<field name="code">JZU</field>
<field name="sequence" eval="12"/>
<field name="category_id" ref="payroll_structure_bgld.category_gembg_brutto"/>
<field name="struct_id" ref="payroll_structure_bgld.structure_gembg_bgld"/>
<field name="condition_select">python</field>
<field name="condition_python">result = 'GEMBG_JOURNAL' in worked_days and worked_days['GEMBG_JOURNAL'].number_of_hours > 0</field>
<field name="amount_select">code</field>
<field name="amount_python_compute"><![CDATA[
# Journaldienstzulage § 79 GemBG: pro Journaldienst (Werktag).
# Pauschaler Betrag je Journaldienst — Parameter aus Bezügeanpassung.
satz = payslip._rule_parameter('gembg_journaldienstzulage')
tage = worked_days['GEMBG_JOURNAL'].number_of_days or 0
result = tage * satz
]]></field>
</record>
<!-- ============================================================ -->
<!-- Sonn-/Feiertagszulage (§ 78 GemBG) — Input -->
<!-- ============================================================ -->
<record id="rule_sfzu" model="hr.salary.rule">
<field name="name">Sonn-/Feiertagszulage (§ 78 GemBG)</field>
<field name="code">SFZU</field>
<field name="sequence" eval="13"/>
<field name="category_id" ref="payroll_structure_bgld.category_gembg_brutto"/>
<field name="struct_id" ref="payroll_structure_bgld.structure_gembg_bgld"/>
<field name="condition_select">python</field>
<field name="condition_python">result = 'GEMBG_SONNFEI' in worked_days and worked_days['GEMBG_SONNFEI'].number_of_hours > 0</field>
<field name="amount_select">code</field>
<field name="amount_python_compute"><![CDATA[
# Sonn-/Feiertagszulage § 78 GemBG: Zuschlag auf Grundvergütung.
# Der Prozentsatz steht in der Bezügeanpassung (Parameter).
# R1: Pauschal je Stunde — Detailabbildung nach Bestandsaufnahme.
base_entgelt = rules['ME'].total
if base_entgelt:
wochenstunden = employee.hours_per_week or 40.0
grund_pro_stunde = base_entgelt / (4.33 * wochenstunden)
stunden = worked_days['GEMBG_SONNFEI'].number_of_hours
satz = payslip._rule_parameter('gembg_sonnfeiertags_prozent') / 100.0
result = stunden * grund_pro_stunde * satz
else:
result = 0
]]></field>
</record>
<!-- ============================================================ -->
<!-- Bereitschaftsentschädigung (§ 80 GemBG) — Input -->
<!-- ============================================================ -->
<record id="rule_berb" model="hr.salary.rule">
<field name="name">Bereitschaftsentschädigung (§ 80 GemBG)</field>
<field name="code">BERB</field>
<field name="sequence" eval="14"/>
<field name="category_id" ref="payroll_structure_bgld.category_gembg_brutto"/>
<field name="struct_id" ref="payroll_structure_bgld.structure_gembg_bgld"/>
<field name="condition_select">python</field>
<field name="condition_python">result = 'GEMBG_BEREIT' in worked_days and worked_days['GEMBG_BEREIT'].number_of_hours > 0</field>
<field name="amount_select">code</field>
<field name="amount_python_compute"><![CDATA[
# Bereitschaftsentschädigung § 80 GemBG.
# Pauschaler Betrag je Bereitschaftsstunde — Parameter.
satz = payslip._rule_parameter('gembg_bereitschaftsstunde')
stunden = worked_days['GEMBG_BEREIT'].number_of_hours
result = stunden * satz
]]></field>
</record>
<!-- ============================================================ -->
<!-- Erschwerniszulage (§ 83 GemBG) — Input -->
<!-- ============================================================ -->
<record id="rule_erzu" model="hr.salary.rule">
<field name="name">Erschwerniszulage (§ 83 GemBG)</field>
<field name="code">ERZU</field>
<field name="sequence" eval="15"/>
<field name="category_id" ref="payroll_structure_bgld.category_gembg_brutto"/>
<field name="struct_id" ref="payroll_structure_bgld.structure_gembg_bgld"/>
<field name="condition_select">python</field>
<field name="condition_python">result = 'ERZU_INPUT' in inputs and inputs['ERZU_INPUT'].amount > 0</field>
<field name="amount_select">code</field>
<field name="amount_python_compute"><![CDATA[
result = inputs['ERZU_INPUT'].amount
]]></field>
</record>
<!-- ============================================================ -->
<!-- Jubiläumszuwendung (§ 89 GemBG) — Input -->
<!-- ============================================================ -->
<record id="rule_jub" model="hr.salary.rule">
<field name="name">Jubiläumszuwendung (§ 89 GemBG)</field>
<field name="code">JUB</field>
<field name="sequence" eval="16"/>
<field name="category_id" ref="payroll_structure_bgld.category_gembg_brutto"/>
<field name="struct_id" ref="payroll_structure_bgld.structure_gembg_bgld"/>
<field name="condition_select">python</field>
<field name="condition_python">result = 'JUB_INPUT' in inputs and inputs['JUB_INPUT'].amount > 0</field>
<field name="amount_select">code</field>
<field name="amount_python_compute"><![CDATA[
result = inputs['JUB_INPUT'].amount
]]></field>
</record>
<!-- ============================================================ -->
<!-- Fahrtkostenzuschuss (§ 88 GemBG) — Input -->
<!-- ============================================================ -->
<record id="rule_fkz" model="hr.salary.rule">
<field name="name">Fahrtkostenzuschuss (§ 88 GemBG)</field>
<field name="code">FKZ</field>
<field name="sequence" eval="17"/>
<field name="category_id" ref="payroll_structure_bgld.category_gembg_brutto"/>
<field name="struct_id" ref="payroll_structure_bgld.structure_gembg_bgld"/>
<field name="condition_select">python</field>
<field name="condition_python">result = 'FKZ_INPUT' in inputs and inputs['FKZ_INPUT'].amount > 0</field>
<field name="amount_select">code</field>
<field name="amount_python_compute"><![CDATA[
result = inputs['FKZ_INPUT'].amount
]]></field>
</record>
<!-- ============================================================ -->
<!-- Sonderzahlung (§ 133n GemBG: 50 % je Kalendervierteljahr) -->
<!-- ============================================================ -->
<record id="rule_szq" model="hr.salary.rule">
<field name="name">Sonderzahlung (quartalsweise, § 133n GemBG)</field>
<field name="code">SZQ</field>
<field name="sequence" eval="30"/>
<field name="category_id" ref="payroll_structure_bgld.category_gembg_brutto"/>
<field name="struct_id" ref="payroll_structure_bgld.structure_gembg_bgld"/>
<field name="condition_select">python</field>
<field name="condition_python"><![CDATA[
# § 133n GemBG: 50 % des Monatsentgelts (und der Kinderzulage)
# für jedes Kalendervierteljahr. Auszahlung in den Monaten am
# Quartalsende (März, Juni, September, Dezember).
# Der Monat der Auszahlung ist der Monat, für den das Entgelt
# gebührt (§ 56 Abs 2 GemBG: 15.3., 15.6., 15.9., 15.12.).
import datetime
month = payslip.date_to.month
result = month in (3, 6, 9, 12)
]]></field>
<field name="amount_select">code</field>
<field name="amount_python_compute"><![CDATA[
# 50 % des Monatsentgelts + Kinderzulage des AUSZAHLUNGSMONATS
# (§ 133n: "in der Höhe von 50% des Monatsentgelts (und der
# Kinderzulage), die ihnen für den Monat der Auszahlung zustehen").
# Bei unterbrochenem Vierteljahr: nur der entsprechende Teil
# (R1: Vollzeit-Rechnung; Anteil folgt mit der Zeit-Erfassung).
me_total = rules['ME'].total or 0
kizu_total = rules['KIZU'].total or 0
result = (me_total + kizu_total) * 0.5
]]></field>
</record>
<!-- ============================================================ -->
<!-- NETTO (Kategoriensumme; wird mit AP3/AP4 erweitert) -->
<!-- ============================================================ -->
<record id="rule_netto" model="hr.salary.rule">
<field name="name">Netto</field>
<field name="code">NETTO</field>
<field name="sequence" eval="100"/>
<field name="category_id" ref="payroll_structure_bgld.category_gembg_netto"/>
<field name="struct_id" ref="payroll_structure_bgld.structure_gembg_bgld"/>
<field name="condition_select">none</field>
<field name="amount_select">code</field>
<field name="amount_python_compute"><![CDATA[
result = categories.GEMBG_BRUTTO - categories.GEMBG_ABZUG
]]></field>
</record>
</odoo>
@@ -1,4 +1,5 @@
# Part of the gem360 onboarding. See LICENSE / project docs for details.
from . import entlohnung
from . import hr_version_bgld
from . import hr_version_entgelt
from . import tasy
@@ -0,0 +1,48 @@
# Part of the gem360 onboarding. See LICENSE / project docs for details.
from odoo import api, fields, models
class HrVersion(models.Model):
"""GemBG AP2: Zulagen-Felder auf der Payroll-Version.
Funktionszulage § 62 GemBG (Sätze aus der Bezügeanpassung, am
Dienstvertrag anwendbar); Kinderzahl § 61 GemBG für die Kinderzulage
(15,60 EUR je Kind monatlich, bei Teilzeit ungekürzt).
"""
_inherit = 'hr.version'
funktionszulage = fields.Monetary(
string='Funktionszulage (§ 62 GemBG)',
currency_field='currency_id',
groups="hr_payroll.group_hr_payroll_user", tracking=True,
help="Monatliche Funktionszulage gemäß § 62 Abs 1 Bgld. GemBG "
"(Wert aus der aktuellen Bezügeanpassung).")
kinder_anzahl = fields.Integer(
string='Anzahl Kinder (Kinderzulage § 61)',
default=0,
groups="hr_payroll.group_hr_payroll_user",
help="Anzahl der Kinder, für die Familienbeihilfe bezogen wird "
"und Kinderzulage gebührt (§ 61 GemBG).")
ausbildungsphase = fields.Boolean(
string='In Ausbildungsphase (§ 133h)',
default=False,
groups="hr_payroll.group_hr_payroll_user",
help="Während der Ausbildungsphase wird das Monatsentgelt um "
"5 % vermindert (§ 133h GemBG).")
currency_id = fields.Many2one(
related='employee_id.company_id.currency_id', store=True)
class HrEmployee(models.Model):
_inherit = 'hr.employee'
funktionszulage = fields.Monetary(
related="version_id.funktionszulage", inherited=True,
readonly=False, groups="hr_payroll.group_hr_payroll_user")
kinder_anzahl = fields.Integer(
related="version_id.kinder_anzahl", inherited=True,
readonly=False, groups="hr_payroll.group_hr_payroll_user")
ausbildungsphase = fields.Boolean(
related="version_id.ausbildungsphase", inherited=True,
readonly=False, groups="hr_payroll.group_hr_payroll_user")
@@ -1,3 +1,4 @@
# Part of the gem360 onboarding. See LICENSE / project docs for details.
from . import test_katalog
from . import test_tasy_import
from . import test_tasy_import
from . import test_entgelt
@@ -0,0 +1,117 @@
# Part of the gem360 onboarding. See LICENSE / project docs for details.
from datetime import date
from odoo.tests import TransactionCase, tagged
@tagged('post_install', '-at_install')
class EntgeltTest(TransactionCase):
"""AP2 Rechenfälle: Monatsentgelt, Kinderzulage, Funktionszulage,
Sonderzahlung (quartalsweise), Ausbildungsphase."""
def setUp(self):
super().setUp()
self.structure = self.env.ref(
'l10n_at_gemeinde_payroll.structure_gembg_bgld')
self.gruppe_av1 = self.env.ref(
'l10n_at_gemeinde_payroll.bgld_grp_av_av1')
self.parameter = self.env['hr.rule.parameter']
def _create_employee_with_payslip(self, month=None, **kwargs):
employee = self.env['hr.employee'].create({
'name': 'Test GemBG Entgelt',
})
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(2020, 1, 1),
})
version_vals = {
'entlohnungsgruppe_id': self.gruppe_av1.id,
'entlohnungsstufe': kwargs.get('stufe', 1),
}
version_vals.update(kwargs.get('version_vals', {}))
version.write(version_vals)
# Ausbildungsphase und Zulagen direkt auf Version
if 'ausbildungsphase' in kwargs:
version.ausbildungsphase = kwargs['ausbildungsphase']
if 'funktionszulage' in kwargs:
version.funktionszulage = kwargs['funktionszulage']
if 'kinder_anzahl' in kwargs:
version.kinder_anzahl = kwargs['kinder_anzahl']
slip_month = month or 7
slip = self.env['hr.payslip'].create({
'employee_id': employee.id,
'struct_id': self.structure.id,
'date_from': date(2026, slip_month, 1),
'date_to': date(2026, slip_month, 28),
})
slip.compute_sheet()
return employee, slip
def _get_rule_total(self, slip, code):
line = slip.line_ids.filtered(
lambda l: l.code == code)
return line.total if line else 0.0
def test_01_monatsentgelt_av1_stufe1(self):
"""Vollzeit av1 Stufe 1 = 7.135,20 € (Wert 1.7.2026)."""
_, slip = self._create_employee_with_payslip(month=7)
self.assertAlmostEqual(
self._get_rule_total(slip, 'ME'), 7135.20, places=2)
def test_02_kinderzulage_ungekuerzt_bei_teilzeit(self):
"""Kinderzulage: 2 Kinder = 31,20 €, auch bei Teilzeit."""
_, slip = self._create_employee_with_payslip(
month=7, kinder_anzahl=2)
# 2 × 15,60 = 31,20
self.assertAlmostEqual(
self._get_rule_total(slip, 'KIZU'), 31.20, places=2)
def test_03_funktionszulage(self):
"""Funktionszulage: 200 € am Vertrag."""
_, slip = self._create_employee_with_payslip(
month=7, funktionszulage=200.0)
self.assertAlmostEqual(
self._get_rule_total(slip, 'FUZU'), 200.0, places=2)
def test_04_ausbildungsphase_5_prozent_abzug(self):
"""§ 133h: 5 % Abzug auf Monatsentgelt."""
_, slip = self._create_employee_with_payslip(
month=7, ausbildungsphase=True)
# 7.135,20 × 0,95 = 6.778,44
self.assertAlmostEqual(
self._get_rule_total(slip, 'ME'), 6778.44, places=2)
def test_05_sonderzahlung_quartalsende(self):
"""SZ in September (Q3-Ende) = 50 % × (ME + KIZU)."""
_, slip = self._create_employee_with_payslip(
month=9, kinder_anzahl=1)
me = self._get_rule_total(slip, 'ME')
kizu = self._get_rule_total(slip, 'KIZU')
sz = self._get_rule_total(slip, 'SZQ')
self.assertAlmostEqual(sz, (me + kizu) * 0.5, places=2)
def test_06_sonderzahlung_kein_quartalsende(self):
"""Kein SZ außerhalb der Quartalsendmonate."""
_, slip = self._create_employee_with_payslip(month=7)
self.assertEqual(self._get_rule_total(slip, 'SZQ'), 0.0)
def test_07_netto_ohne_abzuege(self):
"""R1: Netto = Brutto (AP3/AP4 Abzüge noch nicht implementiert)."""
_, slip = self._create_employee_with_payslip(
month=7, kinder_anzahl=1, funktionszulage=100.0)
brutto = self._get_rule_total(slip, 'ME') \
+ self._get_rule_total(slip, 'KIZU') \
+ self._get_rule_total(slip, 'FUZU')
netto = self._get_rule_total(slip, 'NETTO')
self.assertAlmostEqual(netto, brutto, places=2)
def test_08_stufe_12_max(self):
"""Stufe 12 von av1 = 8.712,50 €."""
_, slip = self._create_employee_with_payslip(month=7, stufe=12)
self.assertAlmostEqual(
self._get_rule_total(slip, 'ME'), 8712.50, places=2)
@@ -13,6 +13,12 @@
<field name="besoldungsdienstalter" invisible="not entlohnungsgruppe_id"/>
<field name="vorrueckungstermin" invisible="not entlohnungsgruppe_id"/>
</group>
<group name="gembg_zulagen" string="GemBG Zulagen"
groups="hr_payroll.group_hr_payroll_user">
<field name="kinder_anzahl"/>
<field name="funktionszulage"/>
<field name="ausbildungsphase"/>
</group>
</group>
</field>
</record>