diff --git a/addons/l10n_at_hr_payroll_private/__manifest__.py b/addons/l10n_at_hr_payroll_private/__manifest__.py index e76a513..dcdac1b 100644 --- a/addons/l10n_at_hr_payroll_private/__manifest__.py +++ b/addons/l10n_at_hr_payroll_private/__manifest__.py @@ -1,7 +1,7 @@ # Part of the odoo-at-payroll project. License: LGPL-3. { 'name': 'Österreich - Privatwirtschaft Personalverrechnung (General-AT)', - 'version': '19.0.9.0.0', + 'version': '19.0.9.2.0', 'author': 'ixsol / gem360', 'website': 'https://gem360.at', 'category': 'Human Resources/Payroll', @@ -106,10 +106,13 @@ gem360-Brücke). 'data/input_types_private.xml', 'data/salary_rules_private.xml', 'data/salary_rules_lst_private.xml', + 'data/salary_rules_efzg_private.xml', 'data/salary_rules_sv_private.xml', 'data/salary_rules_bvg_private.xml', 'data/hr_payroll_account_private.xml', 'data/rule_parameters_mbgm_private.xml', + 'data/rule_parameters_wien_private.xml', + 'data/rule_parameters_efzg_private.xml', 'data/bv_kasse_seed.xml', 'views/kv_views.xml', 'views/bv_views.xml', @@ -117,6 +120,7 @@ gem360-Brücke). 'views/vertragsfelder_views.xml', 'views/mitarbeiter_views.xml', 'report/gp8_reports.xml', + 'report/buchungsbeleg_report.xml', 'report/bv_report.xml', ], 'demo': ['data/demo.xml'], diff --git a/addons/l10n_at_hr_payroll_private/data/l10n.at.payroll.kv.gruppe.stufe.csv b/addons/l10n_at_hr_payroll_private/data/l10n.at.payroll.kv.gruppe.stufe.csv index ddb8dc8..26018e1 100644 --- a/addons/l10n_at_hr_payroll_private/data/l10n.at.payroll.kv.gruppe.stufe.csv +++ b/addons/l10n_at_hr_payroll_private/data/l10n.at.payroll.kv.gruppe.stufe.csv @@ -1,132 +1,132 @@ -id,gruppe_id:id,stufe,bezeichnung,gehalt,gueltig_ab -kv2203_st_ii_1,l10n_at_hr_payroll_private.kv2203_grp_ii,1,im 1. u 2. VWGJ,2059.41,2026-01-01 -kv2203_st_ii_2,l10n_at_hr_payroll_private.kv2203_grp_ii,2,nach 2 VWGJ,2179.68,2026-01-01 -kv2203_st_ii_3,l10n_at_hr_payroll_private.kv2203_grp_ii,3,nach 4 VWGJ,2315.46,2026-01-01 -kv2203_st_ii_4,l10n_at_hr_payroll_private.kv2203_grp_ii,4,nach 6 VWGJ,2457.65,2026-01-01 -kv2203_st_ii_5,l10n_at_hr_payroll_private.kv2203_grp_ii,5,nach 8 VWGJ,2599.83,2026-01-01 -kv2203_st_ii_6,l10n_at_hr_payroll_private.kv2203_grp_ii,6,nach 10 VWGJ,2742.03,2026-01-01 -kv2203_st_ii_7,l10n_at_hr_payroll_private.kv2203_grp_ii,7,nach 12 VWGJ,2863.88,2026-01-01 -kv2203_st_ii_8,l10n_at_hr_payroll_private.kv2203_grp_ii,8,nach 15 VWGJ,3087.28,2026-01-01 -kv2203_st_iii_1,l10n_at_hr_payroll_private.kv2203_grp_iii,1,im 1. u 2. VWGJ,2528.04,2026-01-01 -kv2203_st_iii_2,l10n_at_hr_payroll_private.kv2203_grp_iii,2,nach 2 VWGJ,2705.03,2026-01-01 -kv2203_st_iii_3,l10n_at_hr_payroll_private.kv2203_grp_iii,3,nach 4 VWGJ,2881.97,2026-01-01 -kv2203_st_iii_4,l10n_at_hr_payroll_private.kv2203_grp_iii,4,nach 6 VWGJ,3058.96,2026-01-01 -kv2203_st_iii_5,l10n_at_hr_payroll_private.kv2203_grp_iii,5,nach 8 VWGJ,3232.73,2026-01-01 -kv2203_st_iii_6,l10n_at_hr_payroll_private.kv2203_grp_iii,6,nach 10 VWGJ,3409.53,2026-01-01 -kv2203_st_iii_7,l10n_at_hr_payroll_private.kv2203_grp_iii,7,nach 12 VWGJ,3561.04,2026-01-01 -kv2203_st_iii_8,l10n_at_hr_payroll_private.kv2203_grp_iii,8,nach 15 VWGJ,3838.85,2026-01-01 -kv2203_st_iv_1,l10n_at_hr_payroll_private.kv2203_grp_iv,1,im 1. u 2. VWGJ,3146.27,2026-01-01 -kv2203_st_iv_2,l10n_at_hr_payroll_private.kv2203_grp_iv,2,nach 2 VWGJ,3366.49,2026-01-01 -kv2203_st_iv_3,l10n_at_hr_payroll_private.kv2203_grp_iv,3,nach 4 VWGJ,3586.73,2026-01-01 -kv2203_st_iv_4,l10n_at_hr_payroll_private.kv2203_grp_iv,4,nach 6 VWGJ,3806.97,2026-01-01 -kv2203_st_iv_5,l10n_at_hr_payroll_private.kv2203_grp_iv,5,nach 8 VWGJ,4027.20,2026-01-01 -kv2203_st_iv_6,l10n_at_hr_payroll_private.kv2203_grp_iv,6,nach 10 VWGJ,4247.47,2026-01-01 -kv2203_st_iv_7,l10n_at_hr_payroll_private.kv2203_grp_iv,7,nach 12 VWGJ,4436.22,2026-01-01 -kv2203_st_iv_8,l10n_at_hr_payroll_private.kv2203_grp_iv,8,nach 15 VWGJ,4782.32,2026-01-01 -kv2203_st_v_1,l10n_at_hr_payroll_private.kv2203_grp_v,1,im 1. u 2. VWGJ,3919.67,2026-01-01 -kv2203_st_v_2,l10n_at_hr_payroll_private.kv2203_grp_v,2,nach 2 VWGJ,4194.02,2026-01-01 -kv2203_st_v_3,l10n_at_hr_payroll_private.kv2203_grp_v,3,nach 4 VWGJ,4468.41,2026-01-01 -kv2203_st_v_4,l10n_at_hr_payroll_private.kv2203_grp_v,4,nach 6 VWGJ,4742.77,2026-01-01 -kv2203_st_v_5,l10n_at_hr_payroll_private.kv2203_grp_v,5,nach 8 VWGJ,5017.15,2026-01-01 -kv2203_st_v_6,l10n_at_hr_payroll_private.kv2203_grp_v,6,nach 10 VWGJ,5291.57,2026-01-01 -kv2203_st_v_7,l10n_at_hr_payroll_private.kv2203_grp_v,7,nach 12 VWGJ,5526.70,2026-01-01 -kv2203_st_v_8,l10n_at_hr_payroll_private.kv2203_grp_v,8,nach 15 VWGJ,5957.86,2026-01-01 -kv2203_st_vi_1,l10n_at_hr_payroll_private.kv2203_grp_vi,1,im 1. u 2. VWGJ,5482.40,2026-01-01 -kv2203_st_vi_2,l10n_at_hr_payroll_private.kv2203_grp_vi,2,nach 2 VWGJ,6167.67,2026-01-01 -kv2203_st_vi_3,l10n_at_hr_payroll_private.kv2203_grp_vi,3,nach 5 VWGJ,6853.01,2026-01-01 -kv2203_st_m1_1,l10n_at_hr_payroll_private.kv2203_grp_m1,1,im 1. u 2. VWGJ,2427.72,2026-01-01 -kv2203_st_m1_2,l10n_at_hr_payroll_private.kv2203_grp_m1,2,nach 2 VWGJ,2427.72,2026-01-01 -kv2203_st_m1_3,l10n_at_hr_payroll_private.kv2203_grp_m1,3,nach 4 VWGJ,2586.57,2026-01-01 -kv2203_st_m1_4,l10n_at_hr_payroll_private.kv2203_grp_m1,4,nach 6 VWGJ,2745.39,2026-01-01 -kv2203_st_m1_5,l10n_at_hr_payroll_private.kv2203_grp_m1,5,nach 8 VWGJ,2904.19,2026-01-01 -kv2203_st_m1_6,l10n_at_hr_payroll_private.kv2203_grp_m1,6,nach 10 VWGJ,3063.05,2026-01-01 -kv2203_st_m1_7,l10n_at_hr_payroll_private.kv2203_grp_m1,7,nach 12 VWGJ,3199.16,2026-01-01 -kv2203_st_m1_8,l10n_at_hr_payroll_private.kv2203_grp_m1,8,nach 15 VWGJ,3448.74,2026-01-01 -kv2203_st_m2_1,l10n_at_hr_payroll_private.kv2203_grp_m2,1,im 1. u 2. VWGJ,3095.50,2026-01-01 -kv2203_st_m2_2,l10n_at_hr_payroll_private.kv2203_grp_m2,2,nach 2 VWGJ,3095.50,2026-01-01 -kv2203_st_m2_3,l10n_at_hr_payroll_private.kv2203_grp_m2,3,nach 4 VWGJ,3297.98,2026-01-01 -kv2203_st_m2_4,l10n_at_hr_payroll_private.kv2203_grp_m2,4,nach 6 VWGJ,3500.52,2026-01-01 -kv2203_st_m2_5,l10n_at_hr_payroll_private.kv2203_grp_m2,5,nach 8 VWGJ,3703.01,2026-01-01 -kv2203_st_m2_6,l10n_at_hr_payroll_private.kv2203_grp_m2,6,nach 10 VWGJ,3905.53,2026-01-01 -kv2203_st_m2_7,l10n_at_hr_payroll_private.kv2203_grp_m2,7,nach 12 VWGJ,4079.13,2026-01-01 -kv2203_st_m2_8,l10n_at_hr_payroll_private.kv2203_grp_m2,8,nach 15 VWGJ,4397.37,2026-01-01 -kv2203_st_m2fs_1,l10n_at_hr_payroll_private.kv2203_grp_m2fs,1,im 1. u 2. VWGJ,3241.68,2026-01-01 -kv2203_st_m2fs_2,l10n_at_hr_payroll_private.kv2203_grp_m2fs,2,nach 2 VWGJ,3241.68,2026-01-01 -kv2203_st_m2fs_3,l10n_at_hr_payroll_private.kv2203_grp_m2fs,3,nach 4 VWGJ,3453.74,2026-01-01 -kv2203_st_m2fs_4,l10n_at_hr_payroll_private.kv2203_grp_m2fs,4,nach 6 VWGJ,3665.79,2026-01-01 -kv2203_st_m2fs_5,l10n_at_hr_payroll_private.kv2203_grp_m2fs,5,nach 8 VWGJ,3877.83,2026-01-01 -kv2203_st_m2fs_6,l10n_at_hr_payroll_private.kv2203_grp_m2fs,6,nach 10 VWGJ,4089.90,2026-01-01 -kv2203_st_m2fs_7,l10n_at_hr_payroll_private.kv2203_grp_m2fs,7,nach 12 VWGJ,4271.68,2026-01-01 -kv2203_st_m2fs_8,l10n_at_hr_payroll_private.kv2203_grp_m2fs,8,nach 15 VWGJ,4605.01,2026-01-01 -kv2203_st_m3_1,l10n_at_hr_payroll_private.kv2203_grp_m3,1,im 1. u 2. VWGJ,3542.72,2026-01-01 -kv2203_st_m3_2,l10n_at_hr_payroll_private.kv2203_grp_m3,2,nach 2 VWGJ,3542.72,2026-01-01 -kv2203_st_m3_3,l10n_at_hr_payroll_private.kv2203_grp_m3,3,nach 4 VWGJ,3774.51,2026-01-01 -kv2203_st_m3_4,l10n_at_hr_payroll_private.kv2203_grp_m3,4,nach 6 VWGJ,4006.27,2026-01-01 -kv2203_st_m3_5,l10n_at_hr_payroll_private.kv2203_grp_m3,5,nach 8 VWGJ,4238.05,2026-01-01 -kv2203_st_m3_6,l10n_at_hr_payroll_private.kv2203_grp_m3,6,nach 10 VWGJ,4469.81,2026-01-01 -kv2203_st_m3_7,l10n_at_hr_payroll_private.kv2203_grp_m3,7,nach 12 VWGJ,4668.45,2026-01-01 -kv2203_st_m3_8,l10n_at_hr_payroll_private.kv2203_grp_m3,8,nach 15 VWGJ,5032.68,2026-01-01 -kv2748_st_i_1,l10n_at_hr_payroll_private.kv2748_grp_i,1,im 1. u 2. VWGJ,1960.54,2026-01-01 -kv2748_st_i_2,l10n_at_hr_payroll_private.kv2748_grp_i,2,nach 2 VWGJ,2058.43,2026-01-01 -kv2748_st_i_3,l10n_at_hr_payroll_private.kv2748_grp_i,3,nach 4 VWGJ,2190.76,2026-01-01 -kv2748_st_i_4,l10n_at_hr_payroll_private.kv2748_grp_i,4,nach 6 VWGJ,2323.12,2026-01-01 -kv2748_st_i_5,l10n_at_hr_payroll_private.kv2748_grp_i,5,nach 8 VWGJ,2455.45,2026-01-01 -kv2748_st_i_6,l10n_at_hr_payroll_private.kv2748_grp_i,6,nach 10 VWGJ,2588.72,2026-01-01 -kv2748_st_i_7,l10n_at_hr_payroll_private.kv2748_grp_i,7,nach 12 VWGJ,2703.75,2026-01-01 -kv2748_st_i_8,l10n_at_hr_payroll_private.kv2748_grp_i,8,nach 15 VWGJ,2914.69,2026-01-01 -kv2748_st_ii_1,l10n_at_hr_payroll_private.kv2748_grp_ii,1,im 1. u 2. VWGJ,2199.15,2026-01-01 -kv2748_st_ii_2,l10n_at_hr_payroll_private.kv2748_grp_ii,2,nach 2 VWGJ,2350.62,2026-01-01 -kv2748_st_ii_3,l10n_at_hr_payroll_private.kv2748_grp_ii,3,nach 4 VWGJ,2502.07,2026-01-01 -kv2748_st_ii_4,l10n_at_hr_payroll_private.kv2748_grp_ii,4,nach 6 VWGJ,2655.45,2026-01-01 -kv2748_st_ii_5,l10n_at_hr_payroll_private.kv2748_grp_ii,5,nach 8 VWGJ,2809.08,2026-01-01 -kv2748_st_ii_6,l10n_at_hr_payroll_private.kv2748_grp_ii,6,nach 10 VWGJ,2962.72,2026-01-01 -kv2748_st_ii_7,l10n_at_hr_payroll_private.kv2748_grp_ii,7,nach 12 VWGJ,3094.32,2026-01-01 -kv2748_st_ii_8,l10n_at_hr_payroll_private.kv2748_grp_ii,8,nach 15 VWGJ,3335.74,2026-01-01 -kv2748_st_iii_1,l10n_at_hr_payroll_private.kv2748_grp_iii,1,im 1. u 2. VWGJ,2727.40,2026-01-01 -kv2748_st_iii_2,l10n_at_hr_payroll_private.kv2748_grp_iii,2,nach 2 VWGJ,2918.34,2026-01-01 -kv2748_st_iii_3,l10n_at_hr_payroll_private.kv2748_grp_iii,3,nach 4 VWGJ,3109.21,2026-01-01 -kv2748_st_iii_4,l10n_at_hr_payroll_private.kv2748_grp_iii,4,nach 6 VWGJ,3300.16,2026-01-01 -kv2748_st_iii_5,l10n_at_hr_payroll_private.kv2748_grp_iii,5,nach 8 VWGJ,3491.06,2026-01-01 -kv2748_st_iii_6,l10n_at_hr_payroll_private.kv2748_grp_iii,6,nach 10 VWGJ,3682.00,2026-01-01 -kv2748_st_iii_7,l10n_at_hr_payroll_private.kv2748_grp_iii,7,nach 12 VWGJ,3845.61,2026-01-01 -kv2748_st_iii_8,l10n_at_hr_payroll_private.kv2748_grp_iii,8,nach 15 VWGJ,4145.62,2026-01-01 -kv2748_st_iv_1,l10n_at_hr_payroll_private.kv2748_grp_iv,1,im 1. u 2. VWGJ,3415.01,2026-01-01 -kv2748_st_iv_2,l10n_at_hr_payroll_private.kv2748_grp_iv,2,nach 2 VWGJ,3654.05,2026-01-01 -kv2748_st_iv_3,l10n_at_hr_payroll_private.kv2748_grp_iv,3,nach 4 VWGJ,3893.09,2026-01-01 -kv2748_st_iv_4,l10n_at_hr_payroll_private.kv2748_grp_iv,4,nach 6 VWGJ,4132.15,2026-01-01 -kv2748_st_iv_5,l10n_at_hr_payroll_private.kv2748_grp_iv,5,nach 8 VWGJ,4371.19,2026-01-01 -kv2748_st_iv_6,l10n_at_hr_payroll_private.kv2748_grp_iv,6,nach 10 VWGJ,4610.26,2026-01-01 -kv2748_st_iv_7,l10n_at_hr_payroll_private.kv2748_grp_iv,7,nach 12 VWGJ,4815.15,2026-01-01 -kv2748_st_iv_8,l10n_at_hr_payroll_private.kv2748_grp_iv,8,nach 15 VWGJ,5190.77,2026-01-01 -kv2748_st_ivm_1,l10n_at_hr_payroll_private.kv2748_grp_ivm,1,im 1.-4. VWGJ,3415.01,2026-01-01 -kv2748_st_ivm_2,l10n_at_hr_payroll_private.kv2748_grp_ivm,2,nach 4 VWGJ,3654.05,2026-01-01 -kv2748_st_ivm_3,l10n_at_hr_payroll_private.kv2748_grp_ivm,3,nach 6 VWGJ,3893.09,2026-01-01 -kv2748_st_ivm_4,l10n_at_hr_payroll_private.kv2748_grp_ivm,4,nach 8 VWGJ,4132.15,2026-01-01 -kv2748_st_ivm_5,l10n_at_hr_payroll_private.kv2748_grp_ivm,5,nach 10 VWGJ,4371.19,2026-01-01 -kv2748_st_ivm_6,l10n_at_hr_payroll_private.kv2748_grp_ivm,6,nach 12 VWGJ,4610.26,2026-01-01 -kv2748_st_ivm_7,l10n_at_hr_payroll_private.kv2748_grp_ivm,7,nach 14 VWGJ,4815.15,2026-01-01 -kv2748_st_ivm_8,l10n_at_hr_payroll_private.kv2748_grp_ivm,8,nach 17 VWGJ,5047.10,2026-01-01 -kv2748_st_v_1,l10n_at_hr_payroll_private.kv2748_grp_v,1,im 1. u 2. VWGJ,4299.70,2026-01-01 -kv2748_st_v_2,l10n_at_hr_payroll_private.kv2748_grp_v,2,nach 2 VWGJ,4594.04,2026-01-01 -kv2748_st_v_3,l10n_at_hr_payroll_private.kv2748_grp_v,3,nach 4 VWGJ,4888.37,2026-01-01 -kv2748_st_v_4,l10n_at_hr_payroll_private.kv2748_grp_v,4,nach 6 VWGJ,5182.71,2026-01-01 -kv2748_st_v_5,l10n_at_hr_payroll_private.kv2748_grp_v,5,nach 8 VWGJ,5477.00,2026-01-01 -kv2748_st_v_6,l10n_at_hr_payroll_private.kv2748_grp_v,6,nach 10 VWGJ,5771.34,2026-01-01 -kv2748_st_v_7,l10n_at_hr_payroll_private.kv2748_grp_v,7,nach 12 VWGJ,6023.65,2026-01-01 -kv2748_st_v_8,l10n_at_hr_payroll_private.kv2748_grp_v,8,nach 15 VWGJ,6486.15,2026-01-01 -kv2748_st_vom_1,l10n_at_hr_payroll_private.kv2748_grp_vom,1,im 1.-6. VWGJ,4299.70,2026-01-01 -kv2748_st_vom_2,l10n_at_hr_payroll_private.kv2748_grp_vom,2,nach 6 VWGJ,4594.04,2026-01-01 -kv2748_st_vom_3,l10n_at_hr_payroll_private.kv2748_grp_vom,3,nach 8 VWGJ,4888.37,2026-01-01 -kv2748_st_vom_4,l10n_at_hr_payroll_private.kv2748_grp_vom,4,nach 10 VWGJ,5182.71,2026-01-01 -kv2748_st_vom_5,l10n_at_hr_payroll_private.kv2748_grp_vom,5,nach 12 VWGJ,5477.00,2026-01-01 -kv2748_st_vi_1,l10n_at_hr_payroll_private.kv2748_grp_vi,1,im 1. u 2. VWGJ,6021.80,2026-01-01 -kv2748_st_vi_2,l10n_at_hr_payroll_private.kv2748_grp_vi,2,nach 2 VWGJ,6762.62,2026-01-01 -kv2748_st_vi_3,l10n_at_hr_payroll_private.kv2748_grp_vi,3,nach 5 VWGJ,7503.50,2026-01-01 -kv2748_st_meister_1,l10n_at_hr_payroll_private.kv2748_grp_meister,1,im 1. u 2. VWGJ,4130.76,2026-01-01 -kv2748_st_meister_2,l10n_at_hr_payroll_private.kv2748_grp_meister,2,nach 2 VWGJ,4130.76,2026-01-01 -kv2748_st_meister_3,l10n_at_hr_payroll_private.kv2748_grp_meister,3,nach 4 VWGJ,4303.13,2026-01-01 -kv2748_st_meister_4,l10n_at_hr_payroll_private.kv2748_grp_meister,4,nach 6 VWGJ,4475.48,2026-01-01 -kv2748_st_meister_5,l10n_at_hr_payroll_private.kv2748_grp_meister,5,nach 8 VWGJ,4647.84,2026-01-01 -kv2748_st_meister_6,l10n_at_hr_payroll_private.kv2748_grp_meister,6,nach 10 VWGJ,4820.17,2026-01-01 -kv2748_st_meister_7,l10n_at_hr_payroll_private.kv2748_grp_meister,7,nach 12 VWGJ,4992.53,2026-01-01 -kv2748_st_meister_8,l10n_at_hr_payroll_private.kv2748_grp_meister,8,nach 15 VWGJ,5270.29,2026-01-01 \ No newline at end of file +id,gruppe_id:id,stufe,bezeichnung,gehalt,gueltig_ab,vwgj_ab +kv2203_st_ii_1,l10n_at_hr_payroll_private.kv2203_grp_ii,1,im 1. u 2. VWGJ,2059.41,2026-01-01,0 +kv2203_st_ii_2,l10n_at_hr_payroll_private.kv2203_grp_ii,2,nach 2 VWGJ,2179.68,2026-01-01,2 +kv2203_st_ii_3,l10n_at_hr_payroll_private.kv2203_grp_ii,3,nach 4 VWGJ,2315.46,2026-01-01,4 +kv2203_st_ii_4,l10n_at_hr_payroll_private.kv2203_grp_ii,4,nach 6 VWGJ,2457.65,2026-01-01,6 +kv2203_st_ii_5,l10n_at_hr_payroll_private.kv2203_grp_ii,5,nach 8 VWGJ,2599.83,2026-01-01,8 +kv2203_st_ii_6,l10n_at_hr_payroll_private.kv2203_grp_ii,6,nach 10 VWGJ,2742.03,2026-01-01,10 +kv2203_st_ii_7,l10n_at_hr_payroll_private.kv2203_grp_ii,7,nach 12 VWGJ,2863.88,2026-01-01,12 +kv2203_st_ii_8,l10n_at_hr_payroll_private.kv2203_grp_ii,8,nach 15 VWGJ,3087.28,2026-01-01,15 +kv2203_st_iii_1,l10n_at_hr_payroll_private.kv2203_grp_iii,1,im 1. u 2. VWGJ,2528.04,2026-01-01,0 +kv2203_st_iii_2,l10n_at_hr_payroll_private.kv2203_grp_iii,2,nach 2 VWGJ,2705.03,2026-01-01,2 +kv2203_st_iii_3,l10n_at_hr_payroll_private.kv2203_grp_iii,3,nach 4 VWGJ,2881.97,2026-01-01,4 +kv2203_st_iii_4,l10n_at_hr_payroll_private.kv2203_grp_iii,4,nach 6 VWGJ,3058.96,2026-01-01,6 +kv2203_st_iii_5,l10n_at_hr_payroll_private.kv2203_grp_iii,5,nach 8 VWGJ,3232.73,2026-01-01,8 +kv2203_st_iii_6,l10n_at_hr_payroll_private.kv2203_grp_iii,6,nach 10 VWGJ,3409.53,2026-01-01,10 +kv2203_st_iii_7,l10n_at_hr_payroll_private.kv2203_grp_iii,7,nach 12 VWGJ,3561.04,2026-01-01,12 +kv2203_st_iii_8,l10n_at_hr_payroll_private.kv2203_grp_iii,8,nach 15 VWGJ,3838.85,2026-01-01,15 +kv2203_st_iv_1,l10n_at_hr_payroll_private.kv2203_grp_iv,1,im 1. u 2. VWGJ,3146.27,2026-01-01,0 +kv2203_st_iv_2,l10n_at_hr_payroll_private.kv2203_grp_iv,2,nach 2 VWGJ,3366.49,2026-01-01,2 +kv2203_st_iv_3,l10n_at_hr_payroll_private.kv2203_grp_iv,3,nach 4 VWGJ,3586.73,2026-01-01,4 +kv2203_st_iv_4,l10n_at_hr_payroll_private.kv2203_grp_iv,4,nach 6 VWGJ,3806.97,2026-01-01,6 +kv2203_st_iv_5,l10n_at_hr_payroll_private.kv2203_grp_iv,5,nach 8 VWGJ,4027.20,2026-01-01,8 +kv2203_st_iv_6,l10n_at_hr_payroll_private.kv2203_grp_iv,6,nach 10 VWGJ,4247.47,2026-01-01,10 +kv2203_st_iv_7,l10n_at_hr_payroll_private.kv2203_grp_iv,7,nach 12 VWGJ,4436.22,2026-01-01,12 +kv2203_st_iv_8,l10n_at_hr_payroll_private.kv2203_grp_iv,8,nach 15 VWGJ,4782.32,2026-01-01,15 +kv2203_st_v_1,l10n_at_hr_payroll_private.kv2203_grp_v,1,im 1. u 2. VWGJ,3919.67,2026-01-01,0 +kv2203_st_v_2,l10n_at_hr_payroll_private.kv2203_grp_v,2,nach 2 VWGJ,4194.02,2026-01-01,2 +kv2203_st_v_3,l10n_at_hr_payroll_private.kv2203_grp_v,3,nach 4 VWGJ,4468.41,2026-01-01,4 +kv2203_st_v_4,l10n_at_hr_payroll_private.kv2203_grp_v,4,nach 6 VWGJ,4742.77,2026-01-01,6 +kv2203_st_v_5,l10n_at_hr_payroll_private.kv2203_grp_v,5,nach 8 VWGJ,5017.15,2026-01-01,8 +kv2203_st_v_6,l10n_at_hr_payroll_private.kv2203_grp_v,6,nach 10 VWGJ,5291.57,2026-01-01,10 +kv2203_st_v_7,l10n_at_hr_payroll_private.kv2203_grp_v,7,nach 12 VWGJ,5526.70,2026-01-01,12 +kv2203_st_v_8,l10n_at_hr_payroll_private.kv2203_grp_v,8,nach 15 VWGJ,5957.86,2026-01-01,15 +kv2203_st_vi_1,l10n_at_hr_payroll_private.kv2203_grp_vi,1,im 1. u 2. VWGJ,5482.40,2026-01-01,0 +kv2203_st_vi_2,l10n_at_hr_payroll_private.kv2203_grp_vi,2,nach 2 VWGJ,6167.67,2026-01-01,2 +kv2203_st_vi_3,l10n_at_hr_payroll_private.kv2203_grp_vi,3,nach 5 VWGJ,6853.01,2026-01-01,5 +kv2203_st_m1_1,l10n_at_hr_payroll_private.kv2203_grp_m1,1,im 1. u 2. VWGJ,2427.72,2026-01-01,0 +kv2203_st_m1_2,l10n_at_hr_payroll_private.kv2203_grp_m1,2,nach 2 VWGJ,2427.72,2026-01-01,2 +kv2203_st_m1_3,l10n_at_hr_payroll_private.kv2203_grp_m1,3,nach 4 VWGJ,2586.57,2026-01-01,4 +kv2203_st_m1_4,l10n_at_hr_payroll_private.kv2203_grp_m1,4,nach 6 VWGJ,2745.39,2026-01-01,6 +kv2203_st_m1_5,l10n_at_hr_payroll_private.kv2203_grp_m1,5,nach 8 VWGJ,2904.19,2026-01-01,8 +kv2203_st_m1_6,l10n_at_hr_payroll_private.kv2203_grp_m1,6,nach 10 VWGJ,3063.05,2026-01-01,10 +kv2203_st_m1_7,l10n_at_hr_payroll_private.kv2203_grp_m1,7,nach 12 VWGJ,3199.16,2026-01-01,12 +kv2203_st_m1_8,l10n_at_hr_payroll_private.kv2203_grp_m1,8,nach 15 VWGJ,3448.74,2026-01-01,15 +kv2203_st_m2_1,l10n_at_hr_payroll_private.kv2203_grp_m2,1,im 1. u 2. VWGJ,3095.50,2026-01-01,0 +kv2203_st_m2_2,l10n_at_hr_payroll_private.kv2203_grp_m2,2,nach 2 VWGJ,3095.50,2026-01-01,2 +kv2203_st_m2_3,l10n_at_hr_payroll_private.kv2203_grp_m2,3,nach 4 VWGJ,3297.98,2026-01-01,4 +kv2203_st_m2_4,l10n_at_hr_payroll_private.kv2203_grp_m2,4,nach 6 VWGJ,3500.52,2026-01-01,6 +kv2203_st_m2_5,l10n_at_hr_payroll_private.kv2203_grp_m2,5,nach 8 VWGJ,3703.01,2026-01-01,8 +kv2203_st_m2_6,l10n_at_hr_payroll_private.kv2203_grp_m2,6,nach 10 VWGJ,3905.53,2026-01-01,10 +kv2203_st_m2_7,l10n_at_hr_payroll_private.kv2203_grp_m2,7,nach 12 VWGJ,4079.13,2026-01-01,12 +kv2203_st_m2_8,l10n_at_hr_payroll_private.kv2203_grp_m2,8,nach 15 VWGJ,4397.37,2026-01-01,15 +kv2203_st_m2fs_1,l10n_at_hr_payroll_private.kv2203_grp_m2fs,1,im 1. u 2. VWGJ,3241.68,2026-01-01,0 +kv2203_st_m2fs_2,l10n_at_hr_payroll_private.kv2203_grp_m2fs,2,nach 2 VWGJ,3241.68,2026-01-01,2 +kv2203_st_m2fs_3,l10n_at_hr_payroll_private.kv2203_grp_m2fs,3,nach 4 VWGJ,3453.74,2026-01-01,4 +kv2203_st_m2fs_4,l10n_at_hr_payroll_private.kv2203_grp_m2fs,4,nach 6 VWGJ,3665.79,2026-01-01,6 +kv2203_st_m2fs_5,l10n_at_hr_payroll_private.kv2203_grp_m2fs,5,nach 8 VWGJ,3877.83,2026-01-01,8 +kv2203_st_m2fs_6,l10n_at_hr_payroll_private.kv2203_grp_m2fs,6,nach 10 VWGJ,4089.90,2026-01-01,10 +kv2203_st_m2fs_7,l10n_at_hr_payroll_private.kv2203_grp_m2fs,7,nach 12 VWGJ,4271.68,2026-01-01,12 +kv2203_st_m2fs_8,l10n_at_hr_payroll_private.kv2203_grp_m2fs,8,nach 15 VWGJ,4605.01,2026-01-01,15 +kv2203_st_m3_1,l10n_at_hr_payroll_private.kv2203_grp_m3,1,im 1. u 2. VWGJ,3542.72,2026-01-01,0 +kv2203_st_m3_2,l10n_at_hr_payroll_private.kv2203_grp_m3,2,nach 2 VWGJ,3542.72,2026-01-01,2 +kv2203_st_m3_3,l10n_at_hr_payroll_private.kv2203_grp_m3,3,nach 4 VWGJ,3774.51,2026-01-01,4 +kv2203_st_m3_4,l10n_at_hr_payroll_private.kv2203_grp_m3,4,nach 6 VWGJ,4006.27,2026-01-01,6 +kv2203_st_m3_5,l10n_at_hr_payroll_private.kv2203_grp_m3,5,nach 8 VWGJ,4238.05,2026-01-01,8 +kv2203_st_m3_6,l10n_at_hr_payroll_private.kv2203_grp_m3,6,nach 10 VWGJ,4469.81,2026-01-01,10 +kv2203_st_m3_7,l10n_at_hr_payroll_private.kv2203_grp_m3,7,nach 12 VWGJ,4668.45,2026-01-01,12 +kv2203_st_m3_8,l10n_at_hr_payroll_private.kv2203_grp_m3,8,nach 15 VWGJ,5032.68,2026-01-01,15 +kv2748_st_i_1,l10n_at_hr_payroll_private.kv2748_grp_i,1,im 1. u 2. VWGJ,1960.54,2026-01-01,0 +kv2748_st_i_2,l10n_at_hr_payroll_private.kv2748_grp_i,2,nach 2 VWGJ,2058.43,2026-01-01,2 +kv2748_st_i_3,l10n_at_hr_payroll_private.kv2748_grp_i,3,nach 4 VWGJ,2190.76,2026-01-01,4 +kv2748_st_i_4,l10n_at_hr_payroll_private.kv2748_grp_i,4,nach 6 VWGJ,2323.12,2026-01-01,6 +kv2748_st_i_5,l10n_at_hr_payroll_private.kv2748_grp_i,5,nach 8 VWGJ,2455.45,2026-01-01,8 +kv2748_st_i_6,l10n_at_hr_payroll_private.kv2748_grp_i,6,nach 10 VWGJ,2588.72,2026-01-01,10 +kv2748_st_i_7,l10n_at_hr_payroll_private.kv2748_grp_i,7,nach 12 VWGJ,2703.75,2026-01-01,12 +kv2748_st_i_8,l10n_at_hr_payroll_private.kv2748_grp_i,8,nach 15 VWGJ,2914.69,2026-01-01,15 +kv2748_st_ii_1,l10n_at_hr_payroll_private.kv2748_grp_ii,1,im 1. u 2. VWGJ,2199.15,2026-01-01,0 +kv2748_st_ii_2,l10n_at_hr_payroll_private.kv2748_grp_ii,2,nach 2 VWGJ,2350.62,2026-01-01,2 +kv2748_st_ii_3,l10n_at_hr_payroll_private.kv2748_grp_ii,3,nach 4 VWGJ,2502.07,2026-01-01,4 +kv2748_st_ii_4,l10n_at_hr_payroll_private.kv2748_grp_ii,4,nach 6 VWGJ,2655.45,2026-01-01,6 +kv2748_st_ii_5,l10n_at_hr_payroll_private.kv2748_grp_ii,5,nach 8 VWGJ,2809.08,2026-01-01,8 +kv2748_st_ii_6,l10n_at_hr_payroll_private.kv2748_grp_ii,6,nach 10 VWGJ,2962.72,2026-01-01,10 +kv2748_st_ii_7,l10n_at_hr_payroll_private.kv2748_grp_ii,7,nach 12 VWGJ,3094.32,2026-01-01,12 +kv2748_st_ii_8,l10n_at_hr_payroll_private.kv2748_grp_ii,8,nach 15 VWGJ,3335.74,2026-01-01,15 +kv2748_st_iii_1,l10n_at_hr_payroll_private.kv2748_grp_iii,1,im 1. u 2. VWGJ,2727.40,2026-01-01,0 +kv2748_st_iii_2,l10n_at_hr_payroll_private.kv2748_grp_iii,2,nach 2 VWGJ,2918.34,2026-01-01,2 +kv2748_st_iii_3,l10n_at_hr_payroll_private.kv2748_grp_iii,3,nach 4 VWGJ,3109.21,2026-01-01,4 +kv2748_st_iii_4,l10n_at_hr_payroll_private.kv2748_grp_iii,4,nach 6 VWGJ,3300.16,2026-01-01,6 +kv2748_st_iii_5,l10n_at_hr_payroll_private.kv2748_grp_iii,5,nach 8 VWGJ,3491.06,2026-01-01,8 +kv2748_st_iii_6,l10n_at_hr_payroll_private.kv2748_grp_iii,6,nach 10 VWGJ,3682.00,2026-01-01,10 +kv2748_st_iii_7,l10n_at_hr_payroll_private.kv2748_grp_iii,7,nach 12 VWGJ,3845.61,2026-01-01,12 +kv2748_st_iii_8,l10n_at_hr_payroll_private.kv2748_grp_iii,8,nach 15 VWGJ,4145.62,2026-01-01,15 +kv2748_st_iv_1,l10n_at_hr_payroll_private.kv2748_grp_iv,1,im 1. u 2. VWGJ,3415.01,2026-01-01,0 +kv2748_st_iv_2,l10n_at_hr_payroll_private.kv2748_grp_iv,2,nach 2 VWGJ,3654.05,2026-01-01,2 +kv2748_st_iv_3,l10n_at_hr_payroll_private.kv2748_grp_iv,3,nach 4 VWGJ,3893.09,2026-01-01,4 +kv2748_st_iv_4,l10n_at_hr_payroll_private.kv2748_grp_iv,4,nach 6 VWGJ,4132.15,2026-01-01,6 +kv2748_st_iv_5,l10n_at_hr_payroll_private.kv2748_grp_iv,5,nach 8 VWGJ,4371.19,2026-01-01,8 +kv2748_st_iv_6,l10n_at_hr_payroll_private.kv2748_grp_iv,6,nach 10 VWGJ,4610.26,2026-01-01,10 +kv2748_st_iv_7,l10n_at_hr_payroll_private.kv2748_grp_iv,7,nach 12 VWGJ,4815.15,2026-01-01,12 +kv2748_st_iv_8,l10n_at_hr_payroll_private.kv2748_grp_iv,8,nach 15 VWGJ,5190.77,2026-01-01,15 +kv2748_st_ivm_1,l10n_at_hr_payroll_private.kv2748_grp_ivm,1,im 1.-4. VWGJ,3415.01,2026-01-01,0 +kv2748_st_ivm_2,l10n_at_hr_payroll_private.kv2748_grp_ivm,2,nach 4 VWGJ,3654.05,2026-01-01,4 +kv2748_st_ivm_3,l10n_at_hr_payroll_private.kv2748_grp_ivm,3,nach 6 VWGJ,3893.09,2026-01-01,6 +kv2748_st_ivm_4,l10n_at_hr_payroll_private.kv2748_grp_ivm,4,nach 8 VWGJ,4132.15,2026-01-01,8 +kv2748_st_ivm_5,l10n_at_hr_payroll_private.kv2748_grp_ivm,5,nach 10 VWGJ,4371.19,2026-01-01,10 +kv2748_st_ivm_6,l10n_at_hr_payroll_private.kv2748_grp_ivm,6,nach 12 VWGJ,4610.26,2026-01-01,12 +kv2748_st_ivm_7,l10n_at_hr_payroll_private.kv2748_grp_ivm,7,nach 14 VWGJ,4815.15,2026-01-01,14 +kv2748_st_ivm_8,l10n_at_hr_payroll_private.kv2748_grp_ivm,8,nach 17 VWGJ,5047.10,2026-01-01,17 +kv2748_st_v_1,l10n_at_hr_payroll_private.kv2748_grp_v,1,im 1. u 2. VWGJ,4299.70,2026-01-01,0 +kv2748_st_v_2,l10n_at_hr_payroll_private.kv2748_grp_v,2,nach 2 VWGJ,4594.04,2026-01-01,2 +kv2748_st_v_3,l10n_at_hr_payroll_private.kv2748_grp_v,3,nach 4 VWGJ,4888.37,2026-01-01,4 +kv2748_st_v_4,l10n_at_hr_payroll_private.kv2748_grp_v,4,nach 6 VWGJ,5182.71,2026-01-01,6 +kv2748_st_v_5,l10n_at_hr_payroll_private.kv2748_grp_v,5,nach 8 VWGJ,5477.00,2026-01-01,8 +kv2748_st_v_6,l10n_at_hr_payroll_private.kv2748_grp_v,6,nach 10 VWGJ,5771.34,2026-01-01,10 +kv2748_st_v_7,l10n_at_hr_payroll_private.kv2748_grp_v,7,nach 12 VWGJ,6023.65,2026-01-01,12 +kv2748_st_v_8,l10n_at_hr_payroll_private.kv2748_grp_v,8,nach 15 VWGJ,6486.15,2026-01-01,15 +kv2748_st_vom_1,l10n_at_hr_payroll_private.kv2748_grp_vom,1,im 1.-6. VWGJ,4299.70,2026-01-01,0 +kv2748_st_vom_2,l10n_at_hr_payroll_private.kv2748_grp_vom,2,nach 6 VWGJ,4594.04,2026-01-01,6 +kv2748_st_vom_3,l10n_at_hr_payroll_private.kv2748_grp_vom,3,nach 8 VWGJ,4888.37,2026-01-01,8 +kv2748_st_vom_4,l10n_at_hr_payroll_private.kv2748_grp_vom,4,nach 10 VWGJ,5182.71,2026-01-01,10 +kv2748_st_vom_5,l10n_at_hr_payroll_private.kv2748_grp_vom,5,nach 12 VWGJ,5477.00,2026-01-01,12 +kv2748_st_vi_1,l10n_at_hr_payroll_private.kv2748_grp_vi,1,im 1. u 2. VWGJ,6021.80,2026-01-01,0 +kv2748_st_vi_2,l10n_at_hr_payroll_private.kv2748_grp_vi,2,nach 2 VWGJ,6762.62,2026-01-01,2 +kv2748_st_vi_3,l10n_at_hr_payroll_private.kv2748_grp_vi,3,nach 5 VWGJ,7503.50,2026-01-01,5 +kv2748_st_meister_1,l10n_at_hr_payroll_private.kv2748_grp_meister,1,im 1. u 2. VWGJ,4130.76,2026-01-01,0 +kv2748_st_meister_2,l10n_at_hr_payroll_private.kv2748_grp_meister,2,nach 2 VWGJ,4130.76,2026-01-01,2 +kv2748_st_meister_3,l10n_at_hr_payroll_private.kv2748_grp_meister,3,nach 4 VWGJ,4303.13,2026-01-01,4 +kv2748_st_meister_4,l10n_at_hr_payroll_private.kv2748_grp_meister,4,nach 6 VWGJ,4475.48,2026-01-01,6 +kv2748_st_meister_5,l10n_at_hr_payroll_private.kv2748_grp_meister,5,nach 8 VWGJ,4647.84,2026-01-01,8 +kv2748_st_meister_6,l10n_at_hr_payroll_private.kv2748_grp_meister,6,nach 10 VWGJ,4820.17,2026-01-01,10 +kv2748_st_meister_7,l10n_at_hr_payroll_private.kv2748_grp_meister,7,nach 12 VWGJ,4992.53,2026-01-01,12 +kv2748_st_meister_8,l10n_at_hr_payroll_private.kv2748_grp_meister,8,nach 15 VWGJ,5270.29,2026-01-01,15 diff --git a/addons/l10n_at_hr_payroll_private/data/rule_parameters_efzg_private.xml b/addons/l10n_at_hr_payroll_private/data/rule_parameters_efzg_private.xml new file mode 100644 index 0000000..613b8b3 --- /dev/null +++ b/addons/l10n_at_hr_payroll_private/data/rule_parameters_efzg_private.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + EFZ-Stufen Krankenstand (AngG § 8 / EFZG § 2) + at_efz_stufen + + + + + 2018-07-01 + {'stufen': [[0, 6], [1, 8], [15, 10], [25, 12]], 'halb_wochen': 4, 'au_wochen': 8, 'au2_wochen': 10, 'au_ab_jahre': 15} + + \ No newline at end of file diff --git a/addons/l10n_at_hr_payroll_private/data/rule_parameters_wien_private.xml b/addons/l10n_at_hr_payroll_private/data/rule_parameters_wien_private.xml new file mode 100644 index 0000000..bd5fea8 --- /dev/null +++ b/addons/l10n_at_hr_payroll_private/data/rule_parameters_wien_private.xml @@ -0,0 +1,20 @@ + + + + + + + + + Wiener Dienstgeberabgabe je Woche (§ 5 Wr. DAG) + at_wien_dag_satz + + + + + 2012-06-01 + 2.0 + + \ No newline at end of file diff --git a/addons/l10n_at_hr_payroll_private/data/salary_rules_efzg_private.xml b/addons/l10n_at_hr_payroll_private/data/salary_rules_efzg_private.xml new file mode 100644 index 0000000..418c0b8 --- /dev/null +++ b/addons/l10n_at_hr_payroll_private/data/salary_rules_efzg_private.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + Teilentgelt Krankenstand (halbes Entgelt, 4 Wochen) + TEIL_ENTGELT_HALB + + + + python + + code + laufend + + + + + Krankengeld-Phase (ÖGK; kein Dienstgeber-Entgelt) + KRANKENTGELT_PHASE + + + + python + + code + laufend + + + \ No newline at end of file diff --git a/addons/l10n_at_hr_payroll_private/data/salary_rules_sv_private.xml b/addons/l10n_at_hr_payroll_private/data/salary_rules_sv_private.xml index 03800e9..ef50421 100644 --- a/addons/l10n_at_hr_payroll_private/data/salary_rules_sv_private.xml +++ b/addons/l10n_at_hr_payroll_private/data/salary_rules_sv_private.xml @@ -250,4 +250,26 @@ result = payslip._at_kommst(result_rules) result = payslip._at_dz(result_rules) ]]> + + + + + + Wiener Dienstgeberabgabe (Wr. DAG) + WIEN_DAG + + + + python + 0) + ]]> + code + neutral + + \ No newline at end of file diff --git a/addons/l10n_at_hr_payroll_private/docs/rechenfaelle/efzg.md b/addons/l10n_at_hr_payroll_private/docs/rechenfaelle/efzg.md new file mode 100644 index 0000000..0ee8ca4 --- /dev/null +++ b/addons/l10n_at_hr_payroll_private/docs/rechenfaelle/efzg.md @@ -0,0 +1,92 @@ +# Rechenfälle EFZG/Teilentgelt (AP4) + +Modell: `models/efzg.py` (EFZ-Phasenmaschine, fiktive BV), +Regeln `TEIL_ENTGELT_HALB`/`KRANKENTGELT_PHASE` +(`data/salary_rules_efzg_private.xml`), Parameter `at_efz_stufen`. + +## Rechtsquellen (verifiziert 2026-09-12) + +- **AngG § 8 Abs 1/2/2a** (lokal, RIS) und **EFZG § 2** (RIS GesNr + 10008308, geltende Fassung): Stufen 6/8/10/12 Wochen je vollendeten + Dienstjahren 0 / 1 / 15 / 25, zuzüglich je 4 Wochen halbes Entgelt; + Gleichlauf seit 1. 7. 2018 (BGBl I 153/2017). +- **Wissensbasis lb-krs-08** (Arbeitsjahrprinzip, Kalendertage, + Stufensprung verlängert, Feiertage zählen nicht, AU-Sonderkontingent + 8/10 Wochen ohne Halbsatz), **lb-krs-07** (SV/Lohnsteuer/BMSVG: + laufender Bezug; fiktive BV = 50 % Vormonatsentgelt ohne SZ, + § 7 Abs 3 BMSVG; EFZ < 50 % beitragsfrei — Halbsatz 50 % ist + pflichtig), **lb-krs-02** (Krankengeld zahlt ÖGK, nicht der + Payslip). +- „Krankengeldumlage § 62 ASVG" = Falsch-Prämise + (RECHTSQUELLEN-Privat, 2026-09-10 ausgeräumt) — kein Baustein. + +## Maschinenmodell + +- **Kalendertage-Maschine:** Kontingentverbrauch in Kalendertagen je + Arbeitsjahr (Eintrittsdatum-Rhythmus); Krankheits- und AU-Topf + getrennt (AU nur voll, § 8 Abs 2a AngG / § 2 Abs 5 EFZG). + Stufenbewertung je Tag (Stufensprung im laufenden Stand verlängert). +- **Entgelt-Minderung je Arbeitstag** (Work Entries ATP_KRANK/ATP_AU): + Tagesatz = laufende Monatsbasis (ohne Teilentgelt-Reduktionen) ÷ + Arbeitstage des Kalendermonats — Entgeltausfallsprinzip; negative + Brutto-Regeln `TEIL_ENTGELT_HALB` (−50 %/AT) und + `KRANKENTGELT_PHASE` (−100 %/AT) mit Bemessung `laufend` → + SV/Lohnsteuer/LNK folgen automatisch über die reduzierte Basis. +- **Fiktive BV (§ 7 Abs 3 BMSVG):** während des Krankengeldbezugs + (Halb- + ÖGK-Phase) 50 % des Entgelts des Kalendermonats vor dem + Versicherungsfall (Episode = zusammenhängender Block), aliquot nach + Kalendertagen (30-Tage-Konvention), im + `_at_bv_basis_laufen`-Hook — fließt in BVG-Zeile und mBGM-BV-Basis. + +## RF-EFZ-1 — Voll/Halb/ÖGK-Übergang (1. Arbeitsjahr, 6+4 Wochen) + +Eintritt 1. 7. 2026 (Stufe 6 Wochen), KV-Gruppe II/1 +(2.059,41 €), Krankenstand 1. 9.–15. 11. 2026 (76 Kalendertage): + +- Voll: Kalendertage 1–42 (1. 9.–12. 10.) → September ohne Reduktion; + Oktober Arbeitstage 13.–31. 10. (14 AT) → Halbphase. +- Halb: Kalendertage 43–70 (13. 10.–9. 11.); ÖGK ab 71 (10. 11.). +- November (21 AT): halb 6 AT (2.–6., 9. 11.), ÖGK 4 AT + (10.–13. 11.); Tagesatz 2.059,41 ÷ 21 = 98,07 € → + `TEIL_ENTGELT_HALB` = −294,21 €, `KRANKENTGELT_PHASE` = −392,28 €. + +## RF-EFZ-2 — Kontingent-Zusammenrechnung + +Wiederholte Verhinderung im selben Arbeitsjahr verbraucht dasselbe +Kalendertage-Kontingent (§ 8 Abs 2 AngG): 1.9.–20.9. (20 KT) + +1.10.–20.10. (20 KT) + 25.10.–15.11. → KT 41–42 voll, ab KT 43 halb +— der dritte Krankenstand startet in der Halbphase. + +## RF-EFZ-3 — Stufe 8 Wochen (2.–15. Jahr) + +Eintritt 1. 7. 2024 (1 vollendetes Jahr): Stufe 8 Wochen — erst +Kalendertag 57 fällt aus der Vollphase. + +## RF-EFZ-4 — AU-Sonderkontingent + +Nach erschöpftem Krankheits-Kontingent gebührt für Arbeitsunfall/ +Berufskrankheit (eigene Urlaubsart, WET `ATP_AU`) das +Sonderkontingent 8 Wochen (ab 15 Dienstjahren 10) vollständig — +unabhängig vom Krankheits-Topf, ohne Halbsatz. + +## RF-EFZ-5 — Fiktive BV in der Krankengeldphase + +November 2026 (RF-EFZ-1): Halb- + ÖGK-Kalendertage 15 → fiktive BG += 50 % × Vormonatsbasis (Oktoberslip, 2.059,41 €) × 15/30 = 514,85 €; +BV-Basis = reduziertes laufendes Entgelt (1.372,93 €) + 514,85 €; +BVG = 1,53 % × 1.887,78 € = 28,92 €. + +## Offene Punkte / dokumentierte Reste + +- **Kalenderjahr-Umstellung** (§ 8 Abs 9 AngG / § 2 Abs 8 EFZG per + KV/BV): Merkmal + Sonderrechnung Umstellungsjahr — Option. +- **ÜStd-/Provisionsdurchschnitte** (13 Wochen / 12 Monate) bei + wechselnden Entgelten — vorbereitete Bezugsbasis folgt mit der + Zeit-Zuschlags-Engine. +- **SZ-Kürzung** bei nicht vollem EFZ (9 ObA 209/00a, „kann"-Regel) + — KV-abhängig, Option. +- **Meldung/Nachweis, Urlaubsunterbrechung § 5 UrlG, Beendigung + § 5 EFZG** (EFZ über Ende hinaus): prozessuale Regeln — + Fristen-/Hinweismodell mit AP8 (Arbeits- und Entgeltbestätigung). +- Erstfenster-Teilmonate mit Krankengeld-Phase: fiktive BG nur in + beitragspflichtigen Läufen (Randfall, dokumentiert). \ No newline at end of file diff --git a/addons/l10n_at_hr_payroll_private/models/__init__.py b/addons/l10n_at_hr_payroll_private/models/__init__.py index 0ae1b18..abe2236 100644 --- a/addons/l10n_at_hr_payroll_private/models/__init__.py +++ b/addons/l10n_at_hr_payroll_private/models/__init__.py @@ -9,4 +9,7 @@ from . import account_chart_private from . import account_posting_private from . import mbgm from . import versichertenmeldung -from . import fristen_private \ No newline at end of file +from . import fristen_private +from . import buchungsbeleg +from . import wien_dag +from . import efzg \ No newline at end of file diff --git a/addons/l10n_at_hr_payroll_private/models/abfertigung_private.py b/addons/l10n_at_hr_payroll_private/models/abfertigung_private.py index 02c8968..0c58c1d 100644 --- a/addons/l10n_at_hr_payroll_private/models/abfertigung_private.py +++ b/addons/l10n_at_hr_payroll_private/models/abfertigung_private.py @@ -213,13 +213,18 @@ class HrPayslip(models.Model): return 0.0 basis = self._l10n_at_sv_basis_roh(result_rules) if pflichtig_ab is None or pflichtig_ab <= self.date_from: - return basis + # AP4: fiktive BG während des Krankengeldbezugs (§ 7 Abs 3 + # BMSVG) — 50 % des Vormonatsentgelts aliquot nach + # Kalendertagen der Halb-/ÖGK-Phase; nur in beitrags- + # pflichtigen Läufen (Erstfenster-Teilmonate: dokumentierter + # Randfall). + return basis + self._at_bv_fiktiv_krankengeld() if pflichtig_ab > self.date_to: # LZP liegt zur Gänze im beitragsfreien Erstfenster. return 0.0 - ende = min(self.date_to, + tage = (min(self.date_to, self.version_id.contract_date_end or self.date_to) - tage = (ende - pflichtig_ab).days + 1 + - pflichtig_ab).days + 1 if tage <= 0: return 0.0 return basis * tage / max(self._l10n_at_lohnsteuertage(), 1) diff --git a/addons/l10n_at_hr_payroll_private/models/account_chart_private.py b/addons/l10n_at_hr_payroll_private/models/account_chart_private.py index 86c44fc..dff1358 100644 --- a/addons/l10n_at_hr_payroll_private/models/account_chart_private.py +++ b/addons/l10n_at_hr_payroll_private/models/account_chart_private.py @@ -103,6 +103,10 @@ L10N_AT_BUCHUNGSMATRIX = { 'credit': ('3550',)}, 'rule_dz': {'debit': ('6662',), 'debit_arbeiter': ('6602',), 'credit': ('3542',)}, + # Wr. DAG („U-Bahn-Steuer“): Aufwand 6663 (l10n_at-Template, + # KB lb-lnk-04 verifiziert); Credit 3560 Referenzbuchhaltungs- + # Rest (Stadtkasse Wien — Kandidat, fehlend-Warnung des Hooks). + 'rule_wien_dag': {'debit': ('6663',), 'credit': ('3560',)}, } diff --git a/addons/l10n_at_hr_payroll_private/models/buchungsbeleg.py b/addons/l10n_at_hr_payroll_private/models/buchungsbeleg.py new file mode 100644 index 0000000..3c9247d --- /dev/null +++ b/addons/l10n_at_hr_payroll_private/models/buchungsbeleg.py @@ -0,0 +1,32 @@ +# Part of the odoo-at-payroll project. License: LGPL-3. +"""Buchungsbeleg der Lohnbuchhaltung (AP2d): Druck der FI-Buchung +eines Lohnlaufs — die account.move je Lohnlauf (bzw. je Payslip, wenn +nicht gebatcht bucht wird) mit Konto, Belegtext, Soll/Haben und +Summen; Kontrollgrößen stimmen mit dem Monatsjournal überein +(GP6-Matrix, KFS/BW 6).""" +from odoo import models + + +class L10nAtPayrollBuchungsbeleg(models.AbstractModel): + _name = 'report.l10n_at_hr_payroll_private.report_buchungsbeleg' + _description = 'Buchungsbeleg Lohnlauf (AP2d)' + + def _get_report_values(self, docids, data=None): + runs = self.env['hr.payslip.run'].browse(docids) + laeufe = [] + for run in runs: + # Batch-Buchung hängt am Lauf, sonst je Payslip-Move + # (hr_payroll_account schreibt move_id je Slip bzw. je Run). + moves = (run.move_id + run.slip_ids.mapped('move_id')) \ + .filtered(lambda m: m.state != 'cancel') + zeilen = moves.line_ids.filtered( + lambda l: l.debit or l.credit).sorted( + key=lambda l: (l.account_id.code or '', l.id)) + laeufe.append({ + 'run': run, + 'moves': moves, + 'zeilen': zeilen, + 'summe_soll': sum(zeilen.mapped('debit')), + 'summe_haben': sum(zeilen.mapped('credit')), + }) + return {'docs': runs, 'laeufe': laeufe} \ No newline at end of file diff --git a/addons/l10n_at_hr_payroll_private/models/efzg.py b/addons/l10n_at_hr_payroll_private/models/efzg.py new file mode 100644 index 0000000..c642ab6 --- /dev/null +++ b/addons/l10n_at_hr_payroll_private/models/efzg.py @@ -0,0 +1,270 @@ +# Part of the odoo-at-payroll project. License: LGPL-3. +"""EFZG/Teilentgelt (AP4) — Entgeltfortzahlung im Krankenstand. + +Rechtsgrundlagen (verifiziert 2026-09-12): +- **AngG § 8 Abs 1/2/2a** und **EFZG § 2 Abs 1/3, § 2 Abs 5** + (Gleichlauf seit 1. 7. 2018, BGBl I 153/2017; EFZG RIS GesNr + 10008308, AngG lokal — Stufen 6/8/10/12 Wochen je vollendeten + Dienstjahren <1 / 1-14 / 15-24 / ≥25, zuzüglich je 4 Wochen halbes + Entgelt; wiederholte Verhinderungen im selben Arbeitsjahr + zusammengerechnet; AU/BK-Sonderkontingent 8/10 Wochen ohne + Halbsatz, je Anlassfall, unabhängig vom Krankheits-Topf). +- **lb-krs-08/02/07** (Wissensbasis, Layer 2): Arbeitsjahrprinzip, + Kalendertage als Zähleinheit, Stufensprung im laufenden Stand + verlängert; SV: volle EFZ beitragspflichtig, EFZ < 50 % + beitragsfrei (hier: Halbsatz = 50 % → pflichtig); Lohnsteuer: + laufender Bezug; **BMSVG § 7 Abs 3**: während des Krankengeldbezugs + (Halb- und ÖGK-Phase) fiktive BG = 50 % des Entgelts des + Kalendermonats vor dem Versicherungsfall, SZ ausgenommen. +- Krankengeld selbst zahlt die ÖGK (läuft nicht durch den + Dienstgeber-Payslip; lb-krs-02) — der Payroll-Anteil ist die + Entgelt-Reduktion des Dienstgebers, die fiktive BV-Grundlage und + später die Arbeits-/Entgeltbestätigung (AP8). +- „Krankengeldumlage § 62 ASVG" ist eine Falsch-Prämise + (RECHTSQUELLEN-Privat 2026-09-10) — kein Umlage-Baustein. + +Zähleinheiten: Kontingente in **Kalendertagen** (lb-krs-08: Praxis), +bezogen aus den validierten Abwesenheiten (hr.leave der AT-Urlaubsarten +Krankenstand/Arbeitsunfall); die **Entgelt-Minderung** je +Arbeitstag aus dem Arbeitseintragssatz (Tagesatz = laufende +Monatsbasis ÷ Arbeitstage des Monats — Entgeltausfallsprinzip). +Feiertage im Krankenstand werden vom Work-Entry-Modell automatisch +nicht als Krankenstandstage erzeugt (Kalender) — die 100 %- +Feiertagsentgelt-Regel lb-krs-08 bleibt damit gewahrt. +""" +from datetime import date, timedelta + +from odoo import fields, models + +# WET-Codes der Abwesenheits-Töpfe (AP5-Seed). +_KRANK_WET = 'ATP_KRANK' +_AU_WET = 'ATP_AU' + + +class HrPayslip(models.Model): + _inherit = 'hr.payslip' + + l10n_at_efz_info = fields.Char( + string='AT EFZ-Status (Krankenstand)', compute='_compute_efz_info', + help='Phasenstatus des Krankenstands im Abrechnungszeitraum: ' + 'volle/halbe Entgeltfortzahlung, ÖGK-Krankengeld-Phase, ' + 'Kontingente und Stufe (AngG § 8 / EFZG § 2).') + + def _compute_efz_info(self): + for slip in self: + efz = slip._at_efz_status() + if not efz: + slip.l10n_at_efz_info = '' + continue + slip.l10n_at_efz_info = ( + 'Stufe %d (%d+%d Wo) · voll %d AT · halb %d AT · ' + 'ÖGK %d AT' % ( + efz['stufe_wochen'], efz['stufe_wochen'], + efz['halb_wochen'], len(efz['voll_arbeitstage']), + len(efz['halb_arbeitstage']), + len(efz['oegk_arbeitstage']))) + + # ------------------------------------------------------------------ + # EFZ-Phasenbestimmung + # ------------------------------------------------------------------ + + def _at_efz_leaves(self): + """Validierte Krankenstand-/AU-Abwesenheiten des AN als + (start, ende, ist_au)-Kalenderspannen.""" + self.ensure_one() + typen = self.env['hr.leave.type'].search([ + ('work_entry_type_id.code', 'in', + (_KRANK_WET, _AU_WET))]) + leaves = self.env['hr.leave'].search([ + ('employee_id', '=', self.employee_id.id), + ('holiday_status_id', 'in', typen.ids), + ('state', 'in', ('validate', 'validate1')), + ]) + return [ + (leave.request_date_from, leave.request_date_to, + leave.holiday_status_id.work_entry_type_id.code == _AU_WET) + for leave in leaves + if leave.request_date_from and leave.request_date_to] + + def _at_efz_arbeitsjahr_start(self): + """Beginn des Arbeitsjahres, das die Abrechnungsperiode + überwiegend enthält (Eintrittsdatum-Rhythmus, lb-krs-08).""" + self.ensure_one() + beginn = self.version_id.contract_date_start + if not beginn: + return None + jahre = beginn.year + while True: + aj_start = date(jahre, beginn.month, beginn.day) \ + if not (beginn.month == 2 and beginn.day == 29) \ + else date(jahre, 3, 1) + aj_naechstes = date(jahre + 1, aj_start.month, aj_start.day) + if aj_start <= self.date_from < aj_naechstes: + return aj_start + jahre += 1 + if jahre > beginn.year + 80: + return None + + def _at_efz_status(self): + """EFZ-Phasen je Abrechnungsperiode: {'stufe_wochen', + 'halb_wochen', 'voll_arbeitstage', 'halb_arbeitstage', + 'oegk_arbeitstage', 'oegk_kalendertage', 'episode_beginn', + 'info'} — None ohne Krankenstand/AU im Arbeitsjahr. + + Kalendertage-Maschine: Die Krankheits- und AU-Tage des + Arbeitsjahres werden in Kalenderreihenfolge den Phasen + zugeteilt (voll bis Stufen-Kontingent, halb zuzüglich + 4 Wochen, danach ÖGK); AU ausschließlich voll (kein + Halbsatz, § 8 Abs 2a AngG / § 2 Abs 5 EFZG). Ein + Stufensprung im laufenden Stand verlängert das Kontingent + (Stufe je Tag neu bewertet, lb-krs-08).""" + self.ensure_one() + version = self.version_id + aj_start = self._at_efz_arbeitsjahr_start() + if not aj_start: + return None + endtag = max(self.date_to, aj_start) + krank_tage, au_tage = {}, {} + episode_start = None + for von, bis, ist_au in self._at_efz_leaves(): + tag = von + while tag <= bis: + if aj_start <= tag <= endtag: + topf = au_tage if ist_au else krank_tage + topf[tag] = True + tag += timedelta(days=1) + if not krank_tage and not au_tage: + return None + params = self.env['hr.rule.parameter']._get_parameter_from_code( + 'at_efz_stufen', self.date_from, raise_if_not_found=False) or {} + stufen = sorted( + (int(jahre), int(wochen)) + for jahre, wochen in params.get('stufen', [])) + halb_wochen = int(params.get('halb_wochen', 4)) + au_wochen = int(params.get('au_wochen', 8)) + au2_wochen = int(params.get('au2_wochen', 10)) + au_ab_jahre = int(params.get('au_ab_jahre', 15)) + + def stufe_fuer(dienstjahre, tag_ist_au): + """Anspruchs-Wochen am Stichtag (vollendete Jahre).""" + wochen = stufen[0][1] if stufen else 6 + for jahre, stufe in stufen: + if dienstjahre >= jahre: + wochen = stufe + if tag_ist_au: + wochen = au2_wochen if dienstjahre >= au_ab_jahre \ + else au_wochen + return wochen + + beginn = version.contract_date_start + phasen = {} + verbraucht_krank_voll = verbraucht_krank_halb = 0 + verbraucht_au = 0 + for tag in sorted(set(krank_tage) | set(au_tage)): + dienstjahre = (tag.year - beginn.year + - ((tag.month, tag.day) < (beginn.month, + beginn.day))) + if tag in au_tage: + if verbraucht_au < stufe_fuer(dienstjahre, True) * 7: + phasen[tag] = 'voll' + else: + phasen[tag] = 'oegk' + verbraucht_au += 1 + continue + wochen = stufe_fuer(dienstjahre, False) + if verbraucht_krank_voll < wochen * 7: + phasen[tag] = 'voll' + verbraucht_krank_voll += 1 + elif verbraucht_krank_halb < halb_wochen * 7: + phasen[tag] = 'halb' + verbraucht_krank_halb += 1 + else: + phasen[tag] = 'oegk' + # Arbeitstage je Phase im Abrechnungszeitraum (Work Entries) + eintraege = self.env['hr.work.entry'].search([ + ('employee_id', '=', self.employee_id.id), + ('date', '>=', self.date_from), + ('date', '<=', self.date_to), + ('state', '!=', 'cancelled'), + ('work_entry_type_id.code', 'in', (_KRANK_WET, _AU_WET)), + ]) + voll, halb, oegk = [], [], [] + for eintrag in eintraege: + phase = phasen.get(eintrag.date) + if phase == 'voll': + voll.append(eintrag.date) + elif phase == 'halb': + halb.append(eintrag.date) + elif phase == 'oegk': + oegk.append(eintrag.date) + # Episode-Beginn für die fiktive BV (§ 7 Abs 3 BMSVG): + # frühester zusammenhängender Verhinderungsblock (Lücke = neue + # Episode), dessen Phase die Halb-/ÖGK-Phase erreicht. + episode_beginn = None + for tag in sorted(phasen): + if phasen[tag] in ('halb', 'oegk'): + block = tag + while (block - timedelta(days=1)) in phasen: + block = block - timedelta(days=1) + episode_beginn = block + break + dienstjahre = (self.date_from.year - beginn.year + - ((self.date_from.month, self.date_from.day) + < (beginn.month, beginn.day))) + return { + 'stufe_wochen': stufe_fuer(dienstjahre, False), + 'halb_wochen': halb_wochen, + 'voll_arbeitstage': sorted(set(voll)), + 'halb_arbeitstage': sorted(set(halb)), + 'oegk_arbeitstage': sorted(set(oegk)), + # Kalendertage der Phasen IM ABRECHNUNGSZEITRAUM — Basis + # der fiktiven-BV-Aliquote (monatsscharf). + 'oegk_kalendertage': sorted( + tag for tag in phasen + if self.date_from <= tag <= self.date_to + and phasen[tag] == 'oegk'), + 'halb_kalendertage': sorted( + tag for tag in phasen + if self.date_from <= tag <= self.date_to + and phasen[tag] == 'halb'), + 'episode_beginn': episode_beginn, + } + + def _at_efz_tagesatz(self, result_rules=None): + """Voller Tages-Entgeltsatz des Monats (Entgeltausfalls- + prinzip): laufende Monatsbasis ohne die eigenen + Teilentgelt-Reduktionen ÷ Arbeitstage des Kalendermonats.""" + self.ensure_one() + basis = self._l10n_at_sv_basis_roh(result_rules) + for code in ('TEIL_ENTGELT_HALB', 'KRANKENTGELT_PHASE'): + if result_rules is not None and code in result_rules \ + and result_rules[code]['total']: + basis -= result_rules[code]['total'] + arbeitstage = self._at_arbeitstage(self.date_from, self.date_to) + return basis / max(arbeitstage, 1) + + def _at_bv_fiktiv_krankengeld(self): + """Fiktive BV-Grundlage während des Krankengeldbezugs + (§ 7 Abs 3 BMSVG, lb-krs-07): 50 % des Entgelts des + Kalendermonats vor dem Versicherungsfall (SZ ausgenommen), + aliquot nach Kalendertagen des Bezugs (Halb- + ÖGK-Phase, + 30-Tage-Konvention). Bezug = letzter validierter Lauf vor + Episode-Beginn; ohne solchen: der laufende Monat (kurze + Dienstverhältnisse, lb-krs-02).""" + self.ensure_one() + efz = self._at_efz_status() + if not efz or not (efz['halb_kalendertage'] + or efz['oegk_kalendertage']): + return 0.0 + tage = len(efz['halb_kalendertage']) \ + + len(efz['oegk_kalendertage']) + vormonat = None + if efz['episode_beginn']: + vormonat = self.env['hr.payslip'].search([ + ('employee_id', '=', self.employee_id.id), + ('state', 'in', ('validated', 'paid')), + ('date_to', '<', efz['episode_beginn']), + ], order='date_from desc', limit=1) + basis_monat = vormonat._l10n_at_sv_basis_roh() if vormonat \ + else self._l10n_at_sv_basis_roh() + return 0.5 * basis_monat * tage / 30.0 \ No newline at end of file diff --git a/addons/l10n_at_hr_payroll_private/models/hr_version_private.py b/addons/l10n_at_hr_payroll_private/models/hr_version_private.py index 22849e0..c6bd3d5 100644 --- a/addons/l10n_at_hr_payroll_private/models/hr_version_private.py +++ b/addons/l10n_at_hr_payroll_private/models/hr_version_private.py @@ -3,11 +3,20 @@ hr.employee — Muster ``hr_version_bgld.py`` des GemBG-Moduls. Die Stufenlogik der Referenz-KVs läuft über Verwendungsgruppenjahre -(VWGJ, Biennal-/Triennalsprünge, § 17 der Rahmen-KVs); die Vorrückung -selbst ist GP2-Thema (Wizard analog GemBG), hier werden die -Vertragsfelder und der Katalog-Lookup bereitgestellt. +(VWGJ, Biennal-/Triennalsprünge, § 17 der Rahmen-KVs — verifiziert +SI-2203: Abs 3 Einreihung, Abs 4 Anzahl der angerechneten VWGJ durch +die Firmenleitung mit Dienstzettel-Mitteilung, Abs 5 Vorrückung am +Ersten des Erreichungsmonats). AP9 ergänzt die Vordienstzeiten: +Die Firma erkennt Vordienstzeiten in Monaten an (Dienstzettel), +daraus leitet der Einstufungs-Vorschlag die Erfahrungsstufe ab +(``vwgj_ab`` der Stufenzeilen) und den nächsten Vorrückungstermin. +Freigabe bewusst manuell (action_einstufung_uebernehmen). """ -from odoo import fields, models +from datetime import date + +from dateutil.relativedelta import relativedelta + +from odoo import api, fields, models class HrVersion(models.Model): @@ -31,6 +40,35 @@ class HrVersion(models.Model): help="Individuell vereinbartes Monatsgehalt; überschreibt das " "Kataloggehalt der Gruppe/Stufe (Günstigkeitsvergleich, " "z. B. § 18 Anrechnung der Rahmen-KVs).") + l10n_at_vordienstzeit_monate = fields.Integer( + string='AT Angerechnete Vordienstzeiten (Monate)', + groups="hr_payroll.group_hr_payroll_user", tracking=True, + help="Von der Firmenleitung anerkannte Vordienstzeiten in " + "Monaten (§ 17 Abs 4 Rahmen-KV: Anzahl der angerechneten " + "Verwendungsgruppenjahre — Dienstzettel-Mitteilungspflicht). " + "Fließen in den Einstufungs-Vorschlag und den nächsten " + "Vorrückungstermin ein. AP9.") + l10n_at_vwgj_monate = fields.Integer( + string='AT VWGJ gesamt (Monate)', + compute='_compute_l10n_at_vwgj', + groups="hr_payroll.group_hr_payroll_user", + help="Verwendungsgruppenjahre-Stand heute: Dienstzeit-Monate " + "seit Vertragsbeginn zuzüglich angerechnener " + "Vordienstzeiten.") + l10n_at_vwgj_stufe_vorschlag = fields.Integer( + string='AT Erfahrungsstufen-Vorschlag (VWGJ)', + compute='_compute_l10n_at_vwgj', + groups="hr_payroll.group_hr_payroll_user", + help="Vorgeschlagene Erfahrungsstufe aus den vwgj_ab-Schwellen " + "der Gruppen-Tabelle (nach N VWGJ); 0 = Schwellen " + "nicht gepflegt → manuelle Einstufung maßgeblich.") + l10n_at_naechste_vorrueckung = fields.Date( + string='AT Nächste VWGJ-Vorrückung', + compute='_compute_l10n_at_vwgj', + groups="hr_payroll.group_hr_payroll_user", + help="§ 17 Abs 5 Rahmen-KV: Erster des Monats, in dem die " + "nächste VWGJ-Schwelle erreicht wird (Vertragsbeginn + " + "Schwelle − Vordienstzeiten).") currency_id = fields.Many2one( 'res.currency', related='company_id.currency_id') l10n_at_personalgruppe = fields.Selection( @@ -50,10 +88,73 @@ class HrVersion(models.Model): self.ensure_one() return self.kv_gruppe_id._get_gehalt(self.erfahrungsstufe, datum) + # ------------------------------------------------------------------ + # AP9: Vordienstzeiten → VWGJ-Einstufungsvorschlag (§ 17 Rahmen-KV) + # ------------------------------------------------------------------ + @api.depends('kv_gruppe_id', 'contract_date_start', + 'l10n_at_vordienstzeit_monate', 'erfahrungsstufe') + def _compute_l10n_at_vwgj(self): + """VWGJ-Stand heute (Dienstzeit + Vordienstzeiten), Stufen- + Vorschlag aus den vwgj_ab-Schwellen der Gruppe und der nächste + Vorrückungstermin (Erster des Erreichungsmonats, § 17 Abs 5).""" + heute = fields.Date.context_today(self) + for version in self: + version.l10n_at_vwgj_monate = 0 + version.l10n_at_vwgj_stufe_vorschlag = 0 + version.l10n_at_naechste_vorrueckung = False + if not (version.kv_gruppe_id + and version.contract_date_start): + continue + delta = relativedelta(heute, version.contract_date_start) + dienstzeit_monate = delta.years * 12 + delta.months + version.l10n_at_vwgj_monate = max( + dienstzeit_monate + + (version.l10n_at_vordienstzeit_monate or 0), 0) + jahre = version.l10n_at_vwgj_monate // 12 + version.l10n_at_vwgj_stufe_vorschlag = \ + version.kv_gruppe_id._get_stufe_fuer_vwgj(jahre, heute) + naechste = version.kv_gruppe_id._get_naechste_vwgj( + jahre, heute) + if naechste: + erreichung = version.contract_date_start \ + + relativedelta( + months=naechste * 12 + - (version.l10n_at_vordienstzeit_monate or 0)) + # § 17 Abs 5: Erster des Monats, in dem die erhöhte + # Anzahl der VWGJ erreicht wird. + version.l10n_at_naechste_vorrueckung = erreichung \ + .replace(day=1) + + def action_einstufung_uebernehmen(self): + """Vorschlag als Erfahrungsstufe übernehmen — bewusst ein + expliziter Verwaltungsakt (§ 17 Abs 4: Firmenleitung, mit + Dienstzettel bekanntzugeben); kein automatisches Schreiben.""" + for version in self: + if version.l10n_at_vwgj_stufe_vorschlag: + version.erfahrungsstufe = \ + version.l10n_at_vwgj_stufe_vorschlag + class HrEmployee(models.Model): _inherit = 'hr.employee' + l10n_at_vordienstzeit_monate = fields.Integer( + related="version_id.l10n_at_vordienstzeit_monate", inherited=True, + readonly=False, groups="hr_payroll.group_hr_payroll_user") + l10n_at_vwgj_monate = fields.Integer( + related="version_id.l10n_at_vwgj_monate", inherited=True, + groups="hr_payroll.group_hr_payroll_user") + l10n_at_vwgj_stufe_vorschlag = fields.Integer( + related="version_id.l10n_at_vwgj_stufe_vorschlag", inherited=True, + groups="hr_payroll.group_hr_payroll_user") + l10n_at_naechste_vorrueckung = fields.Date( + related="version_id.l10n_at_naechste_vorrueckung", inherited=True, + groups="hr_payroll.group_hr_payroll_user") + + def action_einstufung_uebernehmen(self): + """Delegation an die Version (Button am Mitarbeiterformular).""" + self.mapped('version_id').action_einstufung_uebernehmen() + kv_id = fields.Many2one( related="version_id.kv_id", inherited=True, readonly=False, groups="hr_payroll.group_hr_payroll_user") diff --git a/addons/l10n_at_hr_payroll_private/models/kollektivvertrag.py b/addons/l10n_at_hr_payroll_private/models/kollektivvertrag.py index 186d63c..8cec739 100644 --- a/addons/l10n_at_hr_payroll_private/models/kollektivvertrag.py +++ b/addons/l10n_at_hr_payroll_private/models/kollektivvertrag.py @@ -176,6 +176,42 @@ class L10nAtPayrollKvGruppe(models.Model): ('gueltig_ab', '<=', datum)], order='gueltig_ab desc', limit=1) + def _get_stufe_fuer_vwgj(self, vwgj_jahre, datum=None): + """Erfahrungsstufe für vollendete Verwendungsgruppenjahre + (VWGJ) zum Stichtag — AP9. Basis: vwgj_ab der Stufenzeilen + („nach N VWGJ"; 0 = ab Beginn). Gibt die höchste Stufe mit + vwgj_ab <= vwgj_jahre zurück; 0, wenn die Gruppe keine + gepflegten Schwellen hat (manuelle Einstufung maßgeblich). + Rahmen-KV § 17 Abs 3-5 (Biennal-/Triennalsprünge, Vorrückung + am Ersten des Erreichungsmonats).""" + self.ensure_one() + datum = datum or fields.Date.context_today(self) + # -1 = Schwelle nicht gepflegt (ORM liest Integer-NULL als 0 — + # deshalb Konvention -1 statt NULL; Domain > -1 filtert beides). + zeilen = self.env['l10n.at.payroll.kv.gruppe.stufe'].search( + [('gruppe_id', '=', self.id), + ('gueltig_ab', '<=', datum), + ('vwgj_ab', '>', -1)]) + beste = zeilen.filtered(lambda z: z.vwgj_ab <= vwgj_jahre) + if not beste: + return 0 + return max(beste.mapped('stufe')) + + def _get_naechste_vwgj(self, vwgj_jahre, datum=None): + """Nächste VWGJ-Schwelle oberhalb der erreichten Jahre (für + den Vorrückungstermin, § 17 Abs 5) — None an der Höchststufe.""" + self.ensure_one() + datum = datum or fields.Date.context_today(self) + zeilen = self.env['l10n.at.payroll.kv.gruppe.stufe'].search( + [('gruppe_id', '=', self.id), + ('gueltig_ab', '<=', datum), + ('vwgj_ab', '>', -1)]) + schwellen = sorted(set(zeilen.mapped('vwgj_ab'))) + for schwelle in schwellen: + if schwelle > vwgj_jahre: + return schwelle + return None + def _max_stufe(self, datum=None): """Höchste Stufe mit gültigem Monatsgehalt zum Stichtag.""" self.ensure_one() @@ -202,6 +238,13 @@ class L10nAtPayrollKvGruppeStufe(models.Model): bezeichnung = fields.Char( help="Stufen-Label der KV-Tabelle, z. B. 'im 1. u 2. VWGJ' " "oder 'nach 15 VWGJ'.") + vwgj_ab = fields.Integer( + string='Ab (vollendete) VWGJ', index=True, default=-1, + help="Vollendete Verwendungsgruppenjahre, ab denen die Stufe " + "gebührt — aus dem Stufen-Label (§ 17 Abs 3-5 " + "Rahmen-KV: Biennal-/Triennalsprünge). 0 = ab Beginn; " + "-1 = Schwelle nicht gepflegt → kein Einstufungs-" + "Vorschlag (manuelle Einstufung maßgeblich). AP9.") gehalt = fields.Monetary(currency_field='currency_id') currency_id = fields.Many2one( 'res.currency', default=lambda self: self.env.company.currency_id) diff --git a/addons/l10n_at_hr_payroll_private/models/wien_dag.py b/addons/l10n_at_hr_payroll_private/models/wien_dag.py new file mode 100644 index 0000000..e59f623 --- /dev/null +++ b/addons/l10n_at_hr_payroll_private/models/wien_dag.py @@ -0,0 +1,113 @@ +# Part of the odoo-at-payroll project. License: LGPL-3. +"""Wiener Dienstgeberabgabe („U-Bahn-Steuer") — Wr. DAG, RIS LrW +Gesetzesnummer 20000133, geltende Fassung 12. 09. 2026 (verifiziert, +Quelle .firecrawl/ris/wien/WDGAG_*.html; § 9: die Einzahlungen fließen +der Stadt Wien zu und sind zur Errichtung einer Untergrundbahn zu +verwenden): + +- **§ 5:** 2 € je Dienstnehmer:in und je angefangene Woche eines + bestehenden Dienstverhältnisses; **§ 2:** Beschäftigungsort Wien + (feste Arbeitsstätte, sonst Wohnsitz des Dienstnehmers; Bau- + ausführungen > 6 Monate). +- **§ 3 Befreiungen:** Gebietskörperschaften; Überschreiten des + 55. Lebensjahres; Behinderte/Opferfürsorge/BEinstG; Lehrverhältnisse + (BAG); ≤ 10 Wochenstunden; Hausbesorger; gesetzliche Mutter- + schaftsverbote und Karenzurlaub auf gesetzlichem Anspruch; + Präsenzdienst/Zivildienst. +- **§ 6:** Selbstberechnung — Vormonatsschuld bis 15. des Folgemonats + an die Stadtkasse; Jahreserklärung bis 31. 3. inklusive befreiter + Dienstverhältnisse (Fristenkalender-Arten wien_dag/wien_dag_jahres). + +Wochenlogik (Praxis-Annahme, im Parallellauf gegen die Magistrats- +Praxis zu verifizieren): „angefangene Woche" = Kalenderwoche, deren +Montag im Abrechnungsmonat liegt; abgabepflichtig, wenn das Dienst- +verhältnis an mindestens einem Tag der Woche besteht. Karenz-Wochen +(validierte ATP_KARENZ-Work Entries) sind befreit. Nicht abgebildet +(bis AP 7): Mutterschaftsverbote, Präsenzdienst/Zivildienst, Haus- +besorger, Behinderten-Status — manuelle Prüfung, Rest dokumentiert. +""" +from datetime import timedelta + +from dateutil.relativedelta import relativedelta + +from odoo import models + + +class HrPayslip(models.Model): + _inherit = 'hr.payslip' + + def _at_wien_dag(self): + """Wr. DAG je Beitragsmonat: {'wochen', 'satz', 'betrag', + 'info'} — None, wenn die Company nicht erfasst ist; 0-Wochen + bei Befreiung. Kopfpauschale, nicht lohnabhängig (§ 5).""" + self.ensure_one() + if not self.company_id.l10n_at_wien_dag: + return None + version = self.version_id + employee = self.employee_id + monat_start = self.date_from + monat_ende = self.date_to + info = [] + # Befreiungen § 3 (monatsweise Bewertung) + if version.l10n_at_lehrling: + return {'wochen': 0, 'satz': 0.0, 'betrag': 0.0, + 'info': 'Befreit: Lehrverhältnis (§ 3 lit d).'} + if employee.birthday: + vollendet55 = employee.birthday + relativedelta(years=55) + if vollendet55 <= monat_ende: + return {'wochen': 0, 'satz': 0.0, 'betrag': 0.0, + 'info': 'Befreit: 55. Lebensjahr überschritten ' + '(§ 3 lit b, %s).' % vollendet55} + kalender = version.resource_calendar_id + wochenstunden = sum( + (att.hour_to or 0.0) - (att.hour_from or 0.0) + for att in kalender.attendance_ids) \ + if kalender else 0.0 + if wochenstunden <= 10.0: + return {'wochen': 0, 'satz': 0.0, 'betrag': 0.0, + 'info': 'Befreit: ≤ 10 Wochenstunden (§ 3 lit e).'} + # Dienstverhältnis-Bestand im Monat (§ 1) + von = max(monat_start, + version.contract_date_start or monat_start) + bis = min(monat_ende, + version.contract_date_end or monat_ende) + if von > bis: + return {'wochen': 0, 'satz': 0.0, 'betrag': 0.0, + 'info': 'Kein bestehendes Dienstverhältnis im ' + 'Abrechnungsmonat.'} + # Karenz-Tage des Monats (§ 3 lit g — Rest via WET ATP_KARENZ) + karenz_tage = set() + if self.env['hr.work.entry']._fields.get('leave_id'): + eintraege = self.env['hr.work.entry'].search([ + ('employee_id', '=', employee.id), + ('date', '>=', monat_start), + ('date', '<=', monat_ende), + ('state', '!=', 'cancelled'), + ('work_entry_type_id.code', '=', 'ATP_KARENZ'), + ]) + karenz_tage = {e.date for e in eintraege} + # Wochen mit Montag im Monat; abgabepflichtig, wenn das + # Dienstverhältnis an mindestens einem Tag der Woche besteht + # (Schnittmenge [DV] ∩ [Woche] nicht leer). + wochen = 0 + tag = monat_start + timedelta( + days=(7 - monat_start.weekday()) % 7) + while tag <= monat_ende: + woche_ende = tag + timedelta(days=6) + if von <= woche_ende and bis >= tag: + karenz_in_woche = any( + tag + timedelta(days=i) in karenz_tage + for i in range((woche_ende - tag).days + 1) + if tag + timedelta(days=i) <= monat_ende) + if karenz_in_woche: + info.append('Karenz-Woche ab %s befreit (§ 3 lit g)' + % tag) + else: + wochen += 1 + tag += timedelta(days=7) + satz = float(self.env['hr.rule.parameter'] + ._get_parameter_from_code( + 'at_wien_dag_satz', monat_start, + raise_if_not_found=False) or 0.0) + return {'wochen': wochen, 'satz': satz, + 'betrag': wochen * satz, 'info': '; '.join(info)} \ No newline at end of file diff --git a/addons/l10n_at_hr_payroll_private/report/buchungsbeleg_report.xml b/addons/l10n_at_hr_payroll_private/report/buchungsbeleg_report.xml new file mode 100644 index 0000000..3d55fba --- /dev/null +++ b/addons/l10n_at_hr_payroll_private/report/buchungsbeleg_report.xml @@ -0,0 +1,69 @@ + + + + + + + + Buchungsbeleg (Lohnbuchhaltung) + hr.payslip.run + qweb-pdf + l10n_at_hr_payroll_private.report_buchungsbeleg + l10n_at_hr_payroll_private.report_buchungsbeleg + + + report + + + + + + + + Buchungsbeleg — + + + Beleg (Journal , + Datum , + Status ); + + + + + + Konto + Kontobezeichnung + Belegtext + Soll + Haben + + + + + + + + + + + + + + + + Summe + + + + + + + + + + + \ No newline at end of file diff --git a/addons/l10n_at_hr_payroll_private/tests/__init__.py b/addons/l10n_at_hr_payroll_private/tests/__init__.py index 087e704..c49d65c 100644 --- a/addons/l10n_at_hr_payroll_private/tests/__init__.py +++ b/addons/l10n_at_hr_payroll_private/tests/__init__.py @@ -7,3 +7,6 @@ from . import test_bv_private from . import test_account_private from . import test_meldewesen_private from . import test_reports_private +from . import test_wien_dag_private +from . import test_einstufung_private +from . import test_efzg_private diff --git a/addons/l10n_at_hr_payroll_private/tests/test_account_private.py b/addons/l10n_at_hr_payroll_private/tests/test_account_private.py index 87bad16..8a5983b 100644 --- a/addons/l10n_at_hr_payroll_private/tests/test_account_private.py +++ b/addons/l10n_at_hr_payroll_private/tests/test_account_private.py @@ -52,10 +52,10 @@ class AccountPrivateTest(TransactionCase): (RF-BUCH-4: Fallback statt Erfindung).""" for code in ('6000', '6200', '6225', '6230', '6240', '6310', '6440', '6500', '6560', '6600', '6601', '6602', - '6660', '6661', '6662'): + '6660', '6661', '6662', '6663'): if code not in ohne: self._konto(code, 'expense') - for code in ('3540', '3541', '3542', '3550', '3600'): + for code in ('3540', '3541', '3542', '3550', '3560', '3600'): self._konto(code, 'liability_current', reconcile=True) if '3850' not in ohne: self._konto('3850', 'liability_payable', reconcile=True) @@ -276,4 +276,33 @@ class AccountPrivateTest(TransactionCase): 'SVDG_KV', 'SVDG_PV', 'SVDG_AV', 'SVDG_UV', 'SVDG_WF', 'SVDG_IESG', 'SVDG_SZ_KV', 'SVDG_SZ_PV', 'SVDG_SZ_AV', 'SVDG_SZ_UV', 'SVDG_SZ_IESG')) - self.assertAlmostEqual(self._saldo(lines, '6500'), dg, places=2) \ No newline at end of file + self.assertAlmostEqual(self._saldo(lines, '6500'), dg, places=2) + + # ------------------------------------------------------------------ + # AP2d: Buchungsbeleg-Report am Lohnlauf + # ------------------------------------------------------------------ + def test_05_buchungsbeleg_report(self): + """Buchungsbeleg am Lohnlauf: Konto/Belegtext/Soll/Haben der + FI-Buchung, Summen balanciert (Soll = Haben).""" + self._kontenplan() + self._hook() + employee = self._employee() + slip = self._slip(employee, 11, confirm=True) + run = self.env['hr.payslip.run'].create({ + 'name': 'Beleg-Lauf 11/2026', + 'date_start': date(2026, 11, 1), + 'date_end': date(2026, 11, 30), + }) + slip.payslip_run_id = run.id + werte = self.env[ + 'report.l10n_at_hr_payroll_private.report_buchungsbeleg' + ]._get_report_values(run.ids) + self.assertEqual(len(werte['laeufe']), 1) + lauf = werte['laeufe'][0] + self.assertTrue(lauf['zeilen']) + self.assertAlmostEqual(lauf['summe_soll'], lauf['summe_haben'], + places=2) + konten = {z.account_id.code for z in lauf['zeilen']} + self.assertIn('6200', konten) # Gehälter (Debit) + self.assertIn('3850', konten) # NET-Verbindlichkeit + self.assertIn('3600', konten) # SV-Verrechnung \ No newline at end of file diff --git a/addons/l10n_at_hr_payroll_private/tests/test_efzg_private.py b/addons/l10n_at_hr_payroll_private/tests/test_efzg_private.py new file mode 100644 index 0000000..ed00338 --- /dev/null +++ b/addons/l10n_at_hr_payroll_private/tests/test_efzg_private.py @@ -0,0 +1,232 @@ +# Part of the odoo-at-payroll project. License: LGPL-3. +"""AP4: EFZG/Teilentgelt — Phasenmaschine (Voll/Halb/ÖGK), +Kontingent-Zusammenrechnung je Arbeitsjahr, Stufen, AU-Sonderkontingent, +fiktive BV (§ 7 Abs 3 BMSVG). Rechenfälle: docs/rechenfaelle/efzg.md.""" +import datetime + +from odoo import SUPERUSER_ID +from odoo.tests import TransactionCase, tagged + + +@tagged('post_install', '-at_install') +class EfzgPrivateTest(TransactionCase): + + def setUp(self): + super().setUp() + self.company = self.env.company + self.structure = self.env.ref( + 'l10n_at_hr_payroll_private.structure_at_privat') + self.kv2203 = self.env.ref( + 'l10n_at_hr_payroll_private.kv_si_2203') + self.gruppe_ii = self.env.ref( + 'l10n_at_hr_payroll_private.kv2203_grp_ii') + self.calendar = self.env['resource.calendar'].create({ + 'name': 'AT EFZG 40h', + 'tz': 'Europe/Vienna', + 'hours_per_day': 8, + 'attendance_ids': [ + (0, 0, {'name': 'Tag %d' % wd, 'dayofweek': str(wd), + 'hour_from': 8.0, 'hour_to': 16.0}) + for wd in range(5) + ], + }) + self.company.resource_calendar_id = self.calendar + self.krankenstand_typ = self.env.ref( + 'l10n_at_hr_payroll_private.leave_type_at_krankenstand') + self.au_typ = self.env.ref( + 'l10n_at_hr_payroll_private.leave_type_at_arbeitsunfall') + + def _employee(self, eintritt): + employee = self.env['hr.employee'].create({ + 'name': 'EFZG Test'}) + version = self.env['hr.version'].search( + [('employee_id', '=', employee.id)], limit=1) + version.write({ + 'contract_date_start': eintritt, + 'kv_id': self.kv2203.id, + 'kv_gruppe_id': self.gruppe_ii.id, + 'erfahrungsstufe': 1, + 'resource_calendar_id': self.calendar.id, + }) + version.generate_work_entries( + datetime.date(2026, 9, 1), datetime.date(2026, 12, 31)) + return employee + + def _krankenstand(self, employee, von, bis, typ=None): + leave = self.env['hr.leave'].create({ + 'name': 'Krank %s' % von, + 'employee_id': employee.id, + 'holiday_status_id': (typ or self.krankenstand_typ).id, + 'request_date_from': von, + 'request_date_to': bis, + }) + leave.with_user(SUPERUSER_ID)._action_validate() + return leave + + def _slip(self, employee, monat): + erster = datetime.date(2026, monat, 1) + letzter = (erster.replace(day=28) + + datetime.timedelta(days=4)).replace(day=1) \ + - datetime.timedelta(days=1) + slip = self.env['hr.payslip'].create({ + 'name': 'EFZG %d/2026' % monat, + 'employee_id': employee.id, + 'struct_id': self.structure.id, + 'date_from': erster, + 'date_to': letzter, + }) + slip.compute_sheet() + slip.action_payslip_done() + return slip + + def _total(self, slip, code): + return sum(slip.line_ids.filtered( + lambda l: l.code == code).mapped('total')) + + def test_01_voll_halb_oegk_uebergang(self): + """RF-EFZ-1: 1. Arbeitsjahr 6+4 Wochen — September voll, + Oktober Halbphase ab 13. 10., November halb (6 AT) + ÖGK + (4 AT) mit Reduktionslinien.""" + employee = self._employee(datetime.date(2026, 7, 1)) + self._krankenstand( + employee, datetime.date(2026, 9, 1), + datetime.date(2026, 11, 15)) + slip_sep = self._slip(employee, 9) + slip_nov = self._slip(employee, 11) + efz = slip_nov._at_efz_status() + self.assertEqual(efz['stufe_wochen'], 6) + self.assertEqual(efz['halb_wochen'], 4) + self.assertEqual(efz['halb_arbeitstage'], [ + datetime.date(2026, 11, 2), datetime.date(2026, 11, 3), + datetime.date(2026, 11, 4), datetime.date(2026, 11, 5), + datetime.date(2026, 11, 6), datetime.date(2026, 11, 9)]) + self.assertEqual(efz['oegk_arbeitstage'], [ + datetime.date(2026, 11, 10), datetime.date(2026, 11, 11), + datetime.date(2026, 11, 12), datetime.date(2026, 11, 13)]) + # Tagesatz 2.059,41 / 21 AT = 98,07 € (die Regeln rechnen + # mit der unreduzierten laufenden Basis — Ergebnis der + # Regelzeilen prüfen) + tagesatz = 2059.41 / 21 + self.assertAlmostEqual(self._total(slip_nov, 'TEIL_ENTGELT_HALB'), + -tagesatz * 0.5 * 6, places=2) + self.assertAlmostEqual( + self._total(slip_nov, 'KRANKENTGELT_PHASE'), + -tagesatz * 4, places=2) + # September: zur Gänze Vollphase — keine Reduktion. + efz_sep = slip_sep._at_efz_status() + self.assertEqual(len(efz_sep['voll_arbeitstage']), 22) + self.assertFalse(efz_sep['halb_arbeitstage']) + self.assertFalse(efz_sep['oegk_arbeitstage']) + self.assertFalse(self._total(slip_sep, 'TEIL_ENTGELT_HALB')) + self.assertFalse(self._total(slip_sep, 'KRANKENTGELT_PHASE')) + + def test_02_kontingent_wiederholung(self): + """RF-EFZ-2: Drei Krankenstände im selben Arbeitsjahr zählen + auf dasselbe Kalendertage-Kontingent (42 KT) — der dritte + startet in der Halbphase.""" + employee = self._employee(datetime.date(2026, 7, 1)) + self._krankenstand( + employee, datetime.date(2026, 9, 1), + datetime.date(2026, 9, 20)) # KT 1–20 + self._krankenstand( + employee, datetime.date(2026, 10, 1), + datetime.date(2026, 10, 20)) # KT 21–40 + self._krankenstand( + employee, datetime.date(2026, 10, 25), + datetime.date(2026, 11, 15)) # KT 41 ff. + slip_nov = self._slip(employee, 11) + efz = slip_nov._at_efz_status() + # 25./26. 10. (So/Mo) sind KT 41/42 (voll), ab 27. 10. halb + self.assertIn(datetime.date(2026, 11, 2), efz['halb_arbeitstage']) + self.assertFalse(datetime.date(2026, 11, 2) + in efz['oegk_arbeitstage']) + + def test_03_stufe_acht_wochen(self): + """RF-EFZ-3: Ab 1 vollendeten Dienstjahr 8 Wochen — die + Halbphase beginnt erst bei Kalendertag 57.""" + employee = self._employee(datetime.date(2024, 7, 1)) + self._krankenstand( + employee, datetime.date(2026, 9, 1), + datetime.date(2026, 11, 30)) + slip_nov = self._slip(employee, 11) + efz = slip_nov._at_efz_status() + self.assertEqual(efz['stufe_wochen'], 8) + # Voll bis 26. 10. (KT 56), halb ab 27. 10., ÖGK ab 24. 11. + self.assertIn(datetime.date(2026, 11, 2), efz['halb_arbeitstage']) + self.assertIn(datetime.date(2026, 11, 24), efz['oegk_arbeitstage']) + + def test_04_au_sonderkontingent(self): + """RF-EFZ-4: Erschöpfter Krankheits-Topf — Arbeitsunfall + gebührt trotzdem voll (eigener Topf, kein Halbsatz).""" + employee = self._employee(datetime.date(2026, 7, 1)) + self._krankenstand( + employee, datetime.date(2026, 9, 1), + datetime.date(2026, 11, 15)) # Topf erschöpft + self._krankenstand( + employee, datetime.date(2026, 11, 16), + datetime.date(2026, 11, 30), typ=self.au_typ) + slip_nov = self._slip(employee, 11) + efz = slip_nov._at_efz_status() + # Krank: 1.–6. 11. halb, ab 10. 11. ÖGK; AU-Tage 16.–30. 11. + # bleiben VOLL (Sonderkontingent). + for tag in (16, 17, 18, 19, 20): + self.assertIn(datetime.date(2026, 11, tag), + efz['voll_arbeitstage']) + self.assertFalse(datetime.date(2026, 11, 16) + in efz['oegk_arbeitstage']) + + def test_05_fiktive_bv_krankengeldphase(self): + """RF-EFZ-5: Fiktive BG in der Krankengeldphase — 50 % des + Vormonatsentgelts × Halb-/ÖGK-Kalendertage/30 (§ 7 Abs 3 + BMSVG); BV-Basis = reduziertes Entgelt + fiktiver Anteil.""" + employee = self._employee(datetime.date(2026, 7, 1)) + self._slip(employee, 8) # Vormonats-Bezug + self._krankenstand( + employee, datetime.date(2026, 9, 1), + datetime.date(2026, 11, 15)) + self._slip(employee, 10) + slip_nov = self._slip(employee, 11) + efz = slip_nov._at_efz_status() + # Episode-Beginn 1. 9. — Vormonat = August (voll entlohnt, + # unverkürzt), nicht der teilvergütete Oktober. + self.assertEqual(efz['episode_beginn'], + datetime.date(2026, 9, 1)) + halb_kt = len(efz['halb_kalendertage']) + oegk_kt = len(efz['oegk_kalendertage']) + self.assertEqual(halb_kt, 9) # 1.–9. 11. + self.assertEqual(oegk_kt, 6) # 10.–15. 11. + # Fiktive BG = 50 % August-Entgelt × 15/30 Kalendertage. + fiktiv = slip_nov._at_bv_fiktiv_krankengeld() + vormonat = slip_nov.env['hr.payslip'].search([ + ('employee_id', '=', slip_nov.employee_id.id), + ('state', 'in', ('validated', 'paid')), + ('date_to', '<', efz['episode_beginn']), + ], order='date_from desc', limit=1) + self.assertEqual(vormonat.date_from, datetime.date(2026, 8, 1)) + self.assertAlmostEqual(fiktiv, 0.5 * vormonat._l10n_at_sv_basis_roh() + * 15 / 30.0, places=2) + # BV-Grundlage = reduziertes laufendes Entgelt + fiktiver + # Anteil (komponentenweise — der November-Lauf trägt + # zusätzlich eine B13-SZ-Position in der sonstigen Grundlage, + # § 6 BMSVG: 1,53 % inkl. SZ). + laufend = slip_nov._l10n_at_sv_basis_roh() + self.assertAlmostEqual(slip_nov._at_bv_basis_laufen(), + laufend + fiktiv, places=2) + self.assertAlmostEqual(self._total(slip_nov, 'BVG'), + (slip_nov._at_bv_basis_laufen() + + slip_nov._at_bv_basis_sonst()) * 0.0153, + places=2) + # Ohne Krankengeld-Phase wäre die Grundlage um den fiktiven + # Anteil kleiner. + self.assertAlmostEqual(self._total(slip_nov, 'TEIL_ENTGELT_HALB'), + -(2059.41 / 21) * 0.5 * 6, places=2) + + def test_06_ohne_krankenstand_keine_wirkung(self): + """Ohne Abwesenheit: keine Phasen, keine fiktive BG, keine + Teilentgelt-Zeilen.""" + employee = self._employee(datetime.date(2026, 7, 1)) + slip = self._slip(employee, 11) + self.assertFalse(slip._at_efz_status()) + self.assertEqual(slip._at_bv_fiktiv_krankengeld(), 0.0) + self.assertFalse(self._total(slip, 'TEIL_ENTGELT_HALB')) + self.assertFalse(self._total(slip, 'KRANKENTGELT_PHASE')) \ No newline at end of file diff --git a/addons/l10n_at_hr_payroll_private/tests/test_einstufung_private.py b/addons/l10n_at_hr_payroll_private/tests/test_einstufung_private.py new file mode 100644 index 0000000..b4ccfd6 --- /dev/null +++ b/addons/l10n_at_hr_payroll_private/tests/test_einstufung_private.py @@ -0,0 +1,138 @@ +# Part of the odoo-at-payroll project. License: LGPL-3. +"""AP9: Vordienstzeiten → Einstufung. Privat: VWGJ-Ermittlung nach +§ 17 Rahmen-KV (Schwellen aus den Stufen-Labels, Vorrückung am +Ersten des Erreichungsmonats), Vorschlag mit manueller Freigabe.""" +import datetime + +from freezegun import freeze_time + +from odoo.tests import TransactionCase, tagged + + +@tagged('post_install', '-at_install') +class EinstufungPrivateTest(TransactionCase): + + def setUp(self): + super().setUp() + self.kv2203 = self.env.ref( + 'l10n_at_hr_payroll_private.kv_si_2203') + self.gruppe_ii = self.env.ref( + 'l10n_at_hr_payroll_private.kv2203_grp_ii') + employee = self.env['hr.employee'].create({ + 'name': 'Einstufung Test'}) + self.version = self.env['hr.version'].search( + [('employee_id', '=', employee.id)], limit=1) + self.employee = employee + + def test_01_vwgj_schwellen(self): + """vwgj_ab-Schwellen aus den Stufen-Labels: 0/2/4/…/15 → + Stufenzuordnung je vollendeten VWGJ (Höchststufe gedeckelt).""" + self.assertEqual( + self.gruppe_ii._get_stufe_fuer_vwgj(0, + datetime.date(2026, 7, 1)), + 1) + self.assertEqual( + self.gruppe_ii._get_stufe_fuer_vwgj(1, + datetime.date(2026, 7, 1)), + 1) + self.assertEqual( + self.gruppe_ii._get_stufe_fuer_vwgj(2, + datetime.date(2026, 7, 1)), + 2) + self.assertEqual( + self.gruppe_ii._get_stufe_fuer_vwgj(4, + datetime.date(2026, 7, 1)), + 3) + self.assertEqual( + self.gruppe_ii._get_stufe_fuer_vwgj(8, + datetime.date(2026, 7, 1)), + 5) + self.assertEqual( + self.gruppe_ii._get_stufe_fuer_vwgj(15, + datetime.date(2026, 7, 1)), + 8) + # Höchststufe bleibt auch nach weiteren Jahren + self.assertEqual( + self.gruppe_ii._get_stufe_fuer_vwgj(20, + datetime.date(2026, 7, 1)), + 8) + # Nächste Schwelle über den erreichten Jahren (§ 17 Abs 5) + self.assertEqual( + self.gruppe_ii._get_naechste_vwgj(2, + datetime.date(2026, 7, 1)), + 4) + self.assertFalse(self.gruppe_ii._get_naechste_vwgj( + 15, datetime.date(2026, 7, 1))) + # Schwellen erst ab Katalog-Gültigkeit + self.assertEqual( + self.gruppe_ii._get_stufe_fuer_vwgj(8, + datetime.date(2025, 12, 31)), + 0) + + def test_02_vwgj_vorschlag_und_uebernahme(self): + """Dienstzeit + Vordienstzeiten → Stufenvorschlag; Übernahme + schreibt die Erfahrungsstufe (manueller Verwaltungsakt).""" + with freeze_time('2026-09-12'): + self.version.write({ + 'contract_date_start': datetime.date(2020, 1, 1), + 'kv_id': self.kv2203.id, + 'kv_gruppe_id': self.gruppe_ii.id, + 'erfahrungsstufe': 1, + 'l10n_at_vordienstzeit_monate': 24, + }) + # Dienstzeit 80 Monate (2020-01 bis 2026-09) + 24 = 104 + # VWGJ-Monate → 8 vollendete Jahre → Stufe 5 (nach 8 VWGJ). + self.assertEqual(self.version.l10n_at_vwgj_monate, 104) + self.assertEqual(self.version.l10n_at_vwgj_stufe_vorschlag, 5) + # Nächste Schwelle 10 VWGJ: erreicht im Monat + # 2020-01 + (120 − 24) Monate = 2028-01-01 (§ 17 Abs 5). + self.assertEqual(self.version.l10n_at_naechste_vorrueckung, + datetime.date(2028, 1, 1)) + self.assertEqual(self.version.erfahrungsstufe, 1) + self.version.action_einstufung_uebernehmen() + self.assertEqual(self.version.erfahrungsstufe, 5) + + def test_03_ohne_vordienstzeiten(self): + """Ohne Vordienstzeiten zählt allein die Dienstzeit; ohne + Gruppen-Schwellen gibt es keinen Vorschlag.""" + with freeze_time('2026-09-12'): + self.version.write({ + 'contract_date_start': datetime.date(2026, 7, 1), + 'kv_id': self.kv2203.id, + 'kv_gruppe_id': self.gruppe_ii.id, + 'l10n_at_vordienstzeit_monate': 0, + }) + self.assertEqual(self.version.l10n_at_vwgj_monate, 2) + self.assertEqual(self.version.l10n_at_vwgj_stufe_vorschlag, 1) + # Nächste Schwelle 2 VWGJ: erreicht 2026-07 + 24 Monate + # → 1. 7. 2028 (§ 17 Abs 5). + self.assertEqual(self.version.l10n_at_naechste_vorrueckung, + datetime.date(2028, 7, 1)) + # Ohne Gruppe: kein Vorschlag + self.version.kv_gruppe_id = False + self.assertEqual(self.version.l10n_at_vwgj_stufe_vorschlag, 0) + self.assertFalse(self.version.l10n_at_naechste_vorrueckung) + + def test_04_vwgj_ohne_schwellen_kein_vorschlag(self): + """Gruppe ohne gepflegte vwgj_ab (Import ohne Labels) → + Vorschlag 0, manuelle Einstufung bleibt maßgeblich.""" + gruppe = self.env['l10n.at.payroll.kv.gruppe'].create({ + 'kv_id': self.kv2203.id, + 'code': 'TEST-NS', + 'name': 'Test ohne Schwellen', + 'lohn_basis': 'monatlich', + }) + self.env['l10n.at.payroll.kv.gruppe.stufe'].create({ + 'gruppe_id': gruppe.id, + 'stufe': 1, + 'bezeichnung': 'Erste Stufe', + 'gehalt': 2000.0, + 'gueltig_ab': datetime.date(2026, 1, 1), + }) + with freeze_time('2026-09-12'): + self.version.write({ + 'contract_date_start': datetime.date(2020, 1, 1), + 'kv_gruppe_id': gruppe.id, + 'l10n_at_vordienstzeit_monate': 60, + }) + self.assertEqual(self.version.l10n_at_vwgj_stufe_vorschlag, 0) \ No newline at end of file diff --git a/addons/l10n_at_hr_payroll_private/tests/test_wien_dag_private.py b/addons/l10n_at_hr_payroll_private/tests/test_wien_dag_private.py new file mode 100644 index 0000000..72470da --- /dev/null +++ b/addons/l10n_at_hr_payroll_private/tests/test_wien_dag_private.py @@ -0,0 +1,179 @@ +# Part of the odoo-at-payroll project. License: LGPL-3. +"""AP3: Wiener Dienstgeberabgabe (Wr. DAG, „U-Bahn-Steuer") — +Wochenlogik, Befreiungen § 3, Regel, Fristen. Rechtsgrundlage RIS LrW +GesNr 20000133 (Fassung 12. 09. 2026, verifiziert).""" +import datetime + +from odoo.tests import TransactionCase, tagged + + +@tagged('post_install', '-at_install') +class WienDagPrivateTest(TransactionCase): + + def setUp(self): + super().setUp() + self.company = self.env.company + self.company.write({ + 'l10n_at_beitragskontonummer': '1412345678', + 'l10n_at_wien_dag': True, + }) + self.structure = self.env.ref( + 'l10n_at_hr_payroll_private.structure_at_privat') + self.kv2203 = self.env.ref( + 'l10n_at_hr_payroll_private.kv_si_2203') + self.gruppe_ii = self.env.ref( + 'l10n_at_hr_payroll_private.kv2203_grp_ii') + self.calendar = self.env['resource.calendar'].create({ + 'name': 'AT Wien-DAG 40h', + 'tz': 'Europe/Vienna', + 'hours_per_day': 8, + 'attendance_ids': [ + (0, 0, {'name': 'Tag %d' % wd, 'dayofweek': str(wd), + 'hour_from': 8.0, 'hour_to': 16.0}) + for wd in range(5) + ], + }) + self.company.resource_calendar_id = self.calendar + employee = self.env['hr.employee'].create({ + 'name': 'Wien-DAG Test', 'birthday': '1990-05-15'}) + version = self.env['hr.version'].search( + [('employee_id', '=', employee.id)], limit=1) + version.write({ + 'contract_date_start': datetime.date(2020, 1, 1), + 'kv_id': self.kv2203.id, + 'kv_gruppe_id': self.gruppe_ii.id, + 'erfahrungsstufe': 1, + 'resource_calendar_id': self.calendar.id, + }) + self.employee = employee + self.version = version + + def _slip(self, monat, jahr=2026): + erster = datetime.date(jahr, monat, 1) + letzter = (erster.replace(day=28) + + datetime.timedelta(days=4)).replace(day=1) \ + - datetime.timedelta(days=1) + slip = self.env['hr.payslip'].create({ + 'name': 'Wien-DAG %d/%d' % (monat, jahr), + 'employee_id': self.employee.id, + 'struct_id': self.structure.id, + 'date_from': erster, + 'date_to': letzter, + }) + slip.compute_sheet() + return slip + + def _line_total(self, slip, code): + return sum(slip.line_ids.filtered( + lambda l: l.code == code).mapped('total')) + + def test_01_voller_monat_fuenf_wochen(self): + """November 2026: Montage 2./9./16./23./30. → 5 angefangene + Wochen × 2 € = 10 € Regelbetrag WIEN_DAG.""" + slip = self._slip(11) + ergebnis = slip._at_wien_dag() + self.assertEqual(ergebnis['wochen'], 5) + self.assertAlmostEqual(ergebnis['betrag'], 10.0, places=2) + self.assertAlmostEqual(self._line_total(slip, 'WIEN_DAG'), + 10.0, places=2) + + def test_02_eintritt_und_wochenlogik(self): + """Eintritt 15. 11. (Sonntag): Wochen ab 9. 11. mit DV-Bestand + (9./16./23./30.) → 4 × 2 = 8 €.""" + self.version.contract_date_start = datetime.date(2026, 11, 15) + slip = self._slip(11) + ergebnis = slip._at_wien_dag() + self.assertEqual(ergebnis['wochen'], 4) + self.assertAlmostEqual(ergebnis['betrag'], 8.0, places=2) + + def test_03_befreiungen(self): + """§ 3: Lehrling, 55. Lebensjahr, ≤ 10 Wochenstunden — je 0 €; + ohne Company-Flag kein Ergebnis.""" + # Lehrling + self.version.l10n_at_lehrling = True + slip = self._slip(11) + self.assertEqual(slip._at_wien_dag()['betrag'], 0.0) + self.version.l10n_at_lehrling = False + # 55. Lebensjahr überschritten (Geburtstag 1. 5. 1971 → 55 am + # 1. 5. 2026, vor Monatsende) + self.employee.birthday = '1971-05-01' + slip = self._slip(11) + ergebnis = slip._at_wien_dag() + self.assertEqual(ergebnis['betrag'], 0.0) + self.assertIn('55. Lebensjahr', ergebnis['info']) + self.employee.birthday = '1990-05-15' + # ≤ 10 Wochenstunden (Kalender 2 Tage à 4 h = 8 h) + klein = self.env['resource.calendar'].create({ + 'name': 'AT gering 8h', + 'tz': 'Europe/Vienna', + 'hours_per_day': 4, + 'attendance_ids': [ + (0, 0, {'name': 'Tag %d' % wd, 'dayofweek': str(wd), + 'hour_from': 8.0, 'hour_to': 12.0}) + for wd in range(2) + ], + }) + self.version.resource_calendar_id = klein + slip = self._slip(11) + ergebnis = slip._at_wien_dag() + self.assertEqual(ergebnis['betrag'], 0.0) + self.assertIn('10 Wochenstunden', ergebnis['info']) + self.version.resource_calendar_id = self.calendar + # Ohne Flag: keine Abgabe + self.company.l10n_at_wien_dag = False + slip = self._slip(11) + self.assertFalse(slip._at_wien_dag()) + self.assertFalse(self._line_total(slip, 'WIEN_DAG')) + + def test_04_karenz_woche_befreit(self): + """Eine Karenz-Woche (ATP_KARENZ-Work Entry) senkt die + abgabepflichtigen Wochen (5 → 4).""" + self.version.generate_work_entries( + datetime.date(2026, 11, 1), datetime.date(2026, 11, 30)) + karenz_wet = self.env['hr.work.entry.type'].search([ + ('code', '=', 'ATP_KARENZ')]) + karenz_typ = self.env.ref( + 'l10n_at_hr_payroll_private.leave_type_at_karenz') + leave = self.env['hr.leave'].create({ + 'name': 'Karenz Test', + 'employee_id': self.employee.id, + 'holiday_status_id': karenz_typ.id, + 'request_date_from': datetime.date(2026, 11, 16), + 'request_date_to': datetime.date(2026, 11, 20), + }) + from odoo import SUPERUSER_ID + leave.with_user(SUPERUSER_ID)._action_validate() + slip = self._slip(11) + ergebnis = slip._at_wien_dag() + self.assertEqual(ergebnis['wochen'], 4) + self.assertAlmostEqual(ergebnis['betrag'], 8.0, places=2) + self.assertIn('Karenz', ergebnis['info']) + + def test_05_fristenkalender(self): + """Cron erzeugt wien_dag-Fristen nur für Flag-Companies: 15. + des Folgemonats monatlich, 31. 3. Jahreserklärung.""" + fristen = self.env['l10n.at.payroll.frist'] + heute = datetime.date.today() + fristen._cron_fristen_erzeugen() + monat_frist = fristen.search([ + ('company_id', '=', self.company.id), + ('fristart', '=', 'wien_dag')], limit=1) + self.assertTrue(monat_frist) + self.assertEqual(monat_frist.deadline.day, 15) + # Jahresfrist entsteht im Cron nur am 1. Jänner — direkter + # Aufruf der Sicherstellung prüft die Art selbst: + fristen._sicherstellen( + self.company, 'wien_dag_jahres', heute.year - 1, None, + datetime.date(heute.year, 3, 31)) + self.assertTrue(fristen.search([ + ('company_id', '=', self.company.id), + ('fristart', '=', 'wien_dag_jahres')])) + # Ohne Flag: keine Wien-Fristen + self.company.l10n_at_wien_dag = False + fristen.search([ + ('company_id', '=', self.company.id), + ('fristart', 'like', 'wien_dag')]).unlink() + fristen._cron_fristen_erzeugen() + self.assertFalse(fristen.search([ + ('company_id', '=', self.company.id), + ('fristart', 'like', 'wien_dag')])) \ No newline at end of file diff --git a/addons/l10n_at_hr_payroll_private/views/kv_views.xml b/addons/l10n_at_hr_payroll_private/views/kv_views.xml index 21dcf65..29ebb9f 100644 --- a/addons/l10n_at_hr_payroll_private/views/kv_views.xml +++ b/addons/l10n_at_hr_payroll_private/views/kv_views.xml @@ -180,6 +180,19 @@ invisible="not kv_gruppe_id"/> + + + + +
+ + Beleg (Journal , + Datum , + Status ); + +