[IMP] l10n_at_hr_payroll: GP3 Lohnsteuer-Kern (tariff, cumulative monthly tax, 67 sechstel engine, 68 buckets) + TASY runtime fixes
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
# Part of the odoo-at-payroll project. License: LGPL-3.
|
# Part of the odoo-at-payroll project. License: LGPL-3.
|
||||||
{
|
{
|
||||||
'name': 'Österreich - Personalverrechnung Kern (AT Payroll Core)',
|
'name': 'Österreich - Personalverrechnung Kern (AT Payroll Core)',
|
||||||
'version': '19.0.1.0.0',
|
'version': '19.0.2.0.0',
|
||||||
'author': 'ixsol',
|
'author': 'ixsol',
|
||||||
'website': 'https://gem360.at',
|
'website': 'https://gem360.at',
|
||||||
'category': 'Human Resources/Payroll',
|
'category': 'Human Resources/Payroll',
|
||||||
@@ -31,6 +31,7 @@ Kein VRV-Bezug: die Gemeinde-Buchhaltung (Anlage 3b, Ansatz/MVAG) läuft
|
|||||||
'data': [
|
'data': [
|
||||||
'security/ir.model.access.csv',
|
'security/ir.model.access.csv',
|
||||||
'data/rule_parameters_2026.xml',
|
'data/rule_parameters_2026.xml',
|
||||||
|
'data/rule_parameters_lst_2026.xml',
|
||||||
'data/tasy_mapping.xml',
|
'data/tasy_mapping.xml',
|
||||||
'views/tasy_views.xml',
|
'views/tasy_views.xml',
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -0,0 +1,106 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<!-- ============================================================ -->
|
||||||
|
<!-- Lohnsteuer-Parameter 2026 (GP3, AP3-Kern). Werte verifiziert -->
|
||||||
|
<!-- am EStG-Volltext (RIS-Fassung 10. 09. 2026, lokal; Anm.-Werte -->
|
||||||
|
<!-- je §-Anker in RECHTSQUELLEN-Privat Abschnitt 5). Versioniert -->
|
||||||
|
<!-- über date_from (Jahreswechsel GP8). -->
|
||||||
|
<!-- ============================================================ -->
|
||||||
|
|
||||||
|
<!-- § 33 Abs 1 Tarif 2026: Zonen [Untergrenze, Grenzsteuersatz %]. -->
|
||||||
|
<record id="hr_rule_parameter_at_lst_tarif" model="hr.rule.parameter">
|
||||||
|
<field name="name">EStG § 33 Abs 1 Tarif (Zonen)</field>
|
||||||
|
<field name="code">at_lst_tarif</field>
|
||||||
|
<field name="country_id" ref="base.at"/>
|
||||||
|
</record>
|
||||||
|
<record id="hr_rule_parameter_value_at_lst_tarif" model="hr.rule.parameter.value">
|
||||||
|
<field name="rule_parameter_id" ref="hr_rule_parameter_at_lst_tarif"/>
|
||||||
|
<field name="date_from">2026-01-01</field>
|
||||||
|
<field name="parameter_value">{'zonen': [[0.0, 0.0], [13539.0, 20.0], [21992.0, 30.0], [36458.0, 40.0], [70365.0, 48.0], [104859.0, 50.0], [1000000.0, 55.0]]}</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- § 66 Abs 2 iVm § 16 Abs 3 / § 18 Abs 2: jahresbezogene
|
||||||
|
Pauschbeträge (nach der Hochrechnung von der JBG abzuziehen). -->
|
||||||
|
<record id="hr_rule_parameter_at_lst_pauschalen" model="hr.rule.parameter">
|
||||||
|
<field name="name">Lohnsteuer Jahrespauschalen (Werbe-/SA-Pauschbetrag)</field>
|
||||||
|
<field name="code">at_lst_pauschalen</field>
|
||||||
|
<field name="country_id" ref="base.at"/>
|
||||||
|
</record>
|
||||||
|
<record id="hr_rule_parameter_value_at_lst_pauschalen" model="hr.rule.parameter.value">
|
||||||
|
<field name="rule_parameter_id" ref="hr_rule_parameter_at_lst_pauschalen"/>
|
||||||
|
<field name="date_from">2026-01-01</field>
|
||||||
|
<field name="parameter_value">{'werbepauschbetrag': 132.0, 'sonderausgaben_pauschbetrag': 60.0}</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- § 66 iVm § 33: Absetzbeträge (AN-AB entfallen — Reform, im Tarif
|
||||||
|
integriert; Verkehrs-AB 496 €; AVB/AVAB 612/828/+273). -->
|
||||||
|
<record id="hr_rule_parameter_at_lst_absetzbetraege" model="hr.rule.parameter">
|
||||||
|
<field name="name">Lohnsteuer Absetzbeträge (§ 33 Abs 4/5 idF)</field>
|
||||||
|
<field name="code">at_lst_absetzbetraege</field>
|
||||||
|
<field name="country_id" ref="base.at"/>
|
||||||
|
</record>
|
||||||
|
<record id="hr_rule_parameter_value_at_lst_absetzbetraege" model="hr.rule.parameter.value">
|
||||||
|
<field name="rule_parameter_id" ref="hr_rule_parameter_at_lst_absetzbetraege"/>
|
||||||
|
<field name="date_from">2026-01-01</field>
|
||||||
|
<field name="parameter_value">{'verkehrsabsetzbetrag': 496.0, 'verkehrsabsetzbetrag_pendler': 853.0, 'avb_kind1': 612.0, 'avb_kinder2': 828.0, 'avb_weiteres_kind': 273.0}</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- § 67 Abs 1: Fixsätze sonstige Bezüge — Freibetrag 620 € (0 %),
|
||||||
|
dann Pakete [Breite, Satz %]: 24.380 → 6 %, 25.000 → 27 %,
|
||||||
|
33.333 → 35,75 %; darüber § 67 Abs 10 tarifisch (LSt-lfd). -->
|
||||||
|
<record id="hr_rule_parameter_at_lst_67_fixsaetze" model="hr.rule.parameter">
|
||||||
|
<field name="name">EStG § 67 Abs 1 Fixsätze sonstige Bezüge</field>
|
||||||
|
<field name="code">at_lst_67_fixsaetze</field>
|
||||||
|
<field name="country_id" ref="base.at"/>
|
||||||
|
</record>
|
||||||
|
<record id="hr_rule_parameter_value_at_lst_67_fixsaetze" model="hr.rule.parameter.value">
|
||||||
|
<field name="rule_parameter_id" ref="hr_rule_parameter_at_lst_67_fixsaetze"/>
|
||||||
|
<field name="date_from">2026-01-01</field>
|
||||||
|
<field name="parameter_value">{'freibetrag': 620.0, 'pakete': [[24380.0, 6.0], [25000.0, 27.0], [33333.0, 35.75]]}</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- § 67 Abs 1: Freigrenze Jahressechstel (Pauschalierung unterbleibt
|
||||||
|
darunter — SZ tarifisch wie laufender Bezug). -->
|
||||||
|
<record id="hr_rule_parameter_at_lst_67_freigrenze" model="hr.rule.parameter">
|
||||||
|
<field name="name">EStG § 67 Abs 1 Freigrenze Jahressechstel</field>
|
||||||
|
<field name="code">at_lst_67_freigrenze</field>
|
||||||
|
<field name="country_id" ref="base.at"/>
|
||||||
|
</record>
|
||||||
|
<record id="hr_rule_parameter_value_at_lst_67_freigrenze" model="hr.rule.parameter.value">
|
||||||
|
<field name="rule_parameter_id" ref="hr_rule_parameter_at_lst_67_freigrenze"/>
|
||||||
|
<field name="date_from">2026-01-01</field>
|
||||||
|
<field name="parameter_value">2615.0</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- § 68 Abs 1: SZFN-Zuschläge steuerfrei bis 400 €/Monat
|
||||||
|
(600 € bei überwiegender Nachtarbeit in der NAZ). -->
|
||||||
|
<record id="hr_rule_parameter_at_68_szfn_grenze" model="hr.rule.parameter">
|
||||||
|
<field name="name">EStG § 68 Abs 1 SZFN-Freibetrag</field>
|
||||||
|
<field name="code">at_68_szfn_grenze</field>
|
||||||
|
<field name="country_id" ref="base.at"/>
|
||||||
|
</record>
|
||||||
|
<record id="hr_rule_parameter_value_at_68_szfn_grenze" model="hr.rule.parameter.value">
|
||||||
|
<field name="rule_parameter_id" ref="hr_rule_parameter_at_68_szfn_grenze"/>
|
||||||
|
<field name="date_from">2026-01-01</field>
|
||||||
|
<field name="parameter_value">{'grenze': 400.0, 'grenze_ueberwiegend': 600.0}</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- § 68 Abs 2 iVm § 124b Z 440 lit c idF BGBl I 4/2026: ÜStd-Zuschläge
|
||||||
|
(bis 50 % Grundlohn) steuerfrei — 2026: erste 15 ÜStd, max 170 €/Monat
|
||||||
|
(Aufrollung bis 31. 5. 2026); ab 2027 Grundregel § 68 Abs 2: 10/120. -->
|
||||||
|
<record id="hr_rule_parameter_at_68_ue_std" model="hr.rule.parameter">
|
||||||
|
<field name="name">EStG § 68 Abs 2 ÜStd-Freibetrag (Stunden/Betrag)</field>
|
||||||
|
<field name="code">at_68_ue_std</field>
|
||||||
|
<field name="country_id" ref="base.at"/>
|
||||||
|
</record>
|
||||||
|
<record id="hr_rule_parameter_value_at_68_ue_std_2026" model="hr.rule.parameter.value">
|
||||||
|
<field name="rule_parameter_id" ref="hr_rule_parameter_at_68_ue_std"/>
|
||||||
|
<field name="date_from">2026-01-01</field>
|
||||||
|
<field name="parameter_value">{'stunden': 15.0, 'betrag': 170.0}</field>
|
||||||
|
</record>
|
||||||
|
<record id="hr_rule_parameter_value_at_68_ue_std_2027" model="hr.rule.parameter.value">
|
||||||
|
<field name="rule_parameter_id" ref="hr_rule_parameter_at_68_ue_std"/>
|
||||||
|
<field name="date_from">2027-01-01</field>
|
||||||
|
<field name="parameter_value">{'stunden': 10.0, 'betrag': 120.0}</field>
|
||||||
|
</record>
|
||||||
|
</odoo>
|
||||||
@@ -1,2 +1,3 @@
|
|||||||
# Part of the odoo-at-payroll project. License: LGPL-3.
|
# Part of the odoo-at-payroll project. License: LGPL-3.
|
||||||
from . import tasy
|
from . import tasy
|
||||||
|
from . import lohnsteuer
|
||||||
@@ -0,0 +1,418 @@
|
|||||||
|
# Part of the odoo-at-payroll project. License: LGPL-3.
|
||||||
|
"""Lohnsteuer-Kern (GP3/AP3): § 66 Monatslohnsteuer kumulativ mit
|
||||||
|
Rollung (Zuflussprinzip), § 67 sonstige Bezüge mit Sechstel-
|
||||||
|
Aufrollung, § 68-Steuerfreiheiten, DN-SV als Bemessungsbestandteil.
|
||||||
|
|
||||||
|
Architektur (D4-verifiziert): hr.salary.rule.struct_id ist required und
|
||||||
|
rule_ids One2many — Kern-Regeln sind nicht teilbar. Der Kern liefert
|
||||||
|
Compute + Parameter + Flags; die Lokalisationen binden dünne Regeln,
|
||||||
|
die diese Methoden aufrufen (kein Wertefork, keine Logikfork).
|
||||||
|
|
||||||
|
Laufzeit-Datenfluss: compute_sheet legt die Zeilen erst NACH der
|
||||||
|
Regelschleife an — die Regeln reichen daher die Zwischensummen über
|
||||||
|
``result_rules`` (localdict, s. hr_payslip._compute_line L1225) an
|
||||||
|
die Compute-Methoden; frühere (validierte) Läufe werden über deren
|
||||||
|
line_ids gelesen (YTD/Rollung).
|
||||||
|
|
||||||
|
User-Entscheidungen 2026-09-11:
|
||||||
|
- Lohnsteuertage: 30 je vollem Monat; Teilmonat = Kalendertage der
|
||||||
|
Beschäftigung inkl. arbeitsfreier Tage (§ 66 Abs 3), max 30.
|
||||||
|
- Bemessung laufend = Arbeitslohn − SV-DN − steuerfreie Bezüge
|
||||||
|
(user-Formel „SV-Beitragsgrundlage − SV-DN").
|
||||||
|
- Bemessung SZ = sonstiger Bezug − SV-DN(SZ) (Abs 12) − 620 €
|
||||||
|
(jahreskumulativ); 620–Jahressechstel 6 %, darüber LSt-lfd (Abs 10).
|
||||||
|
- Aufrollung: Vormonatskorrekturen rollen über die kumulative Engine
|
||||||
|
in den Zuflussmonat; SV-Allokation auf Beitragsmonate = GP4,
|
||||||
|
Nachtrags-mBGM = GP7.
|
||||||
|
"""
|
||||||
|
from datetime import date, timedelta
|
||||||
|
|
||||||
|
from odoo import fields, models
|
||||||
|
|
||||||
|
|
||||||
|
class HrSalaryRule(models.Model):
|
||||||
|
_inherit = 'hr.salary.rule'
|
||||||
|
|
||||||
|
l10n_at_bemessung = fields.Selection(
|
||||||
|
[('neutral', 'Kein Arbeitslohn (Abzug/Netto/DG)'),
|
||||||
|
('laufend', 'Laufender Arbeitslohn (§ 25/§ 66)'),
|
||||||
|
('sonstig', 'Sonstiger Bezug (§ 67)')],
|
||||||
|
string='AT Bemessungsart',
|
||||||
|
default='neutral',
|
||||||
|
help='Laufend: §-66-Kette (Hochrechnung); sonstig: § 67 '
|
||||||
|
'(Jahressechstel/Fixsätze); neutral: kein Arbeitslohn.')
|
||||||
|
l10n_at_steuerbefreiung_68 = fields.Selection(
|
||||||
|
[('keine', 'Keine §-68-Steuerfreiheit'),
|
||||||
|
('szfn', '§ 68 Abs 1 (Sonn-/Feiertags-/Nachtzuschläge)'),
|
||||||
|
('ue_std', '§ 68 Abs 2 (ÜStd-Zuschläge bis 50 % Grundlohn)')],
|
||||||
|
string='AT § 68-Freibetrag',
|
||||||
|
default='keine')
|
||||||
|
l10n_at_zuschlagsanteil = fields.Float(
|
||||||
|
string='AT Zuschlagsanteil (%)',
|
||||||
|
help='Anteil des Zeilenbetrags, der Zuschlag (und damit '
|
||||||
|
'§-68-potentiell steuerfrei) ist — z. B. 50 %-Regel mit '
|
||||||
|
'Grundvergütung: 0,5/1,5 = 33,33 %.')
|
||||||
|
|
||||||
|
|
||||||
|
class HrVersion(models.Model):
|
||||||
|
_inherit = 'hr.version'
|
||||||
|
|
||||||
|
l10n_at_familienabsetzbetrag = fields.Selection(
|
||||||
|
[('keine', 'Keine'), ('avb', 'Alleinverdienerabsetzbetrag'),
|
||||||
|
('avab', 'Alleinerzieherabsetzbetrag')],
|
||||||
|
string='AT Familienabsetzbetrag',
|
||||||
|
default='keine',
|
||||||
|
help='AVB nur bei Partnereinkommen ≤ Grenze (§ 33 Abs 4; '
|
||||||
|
'Nachweis mandantenseitig). Betrag nach Anzahl Kinder.')
|
||||||
|
l10n_at_ueberw_nachtarbeit = fields.Boolean(
|
||||||
|
string='AT Überwiegende Nachtarbeit (§ 68 Abs 6)',
|
||||||
|
help='Erhöht den SZFN-Freibetrag 400 → 600 € (Nachtarbeit '
|
||||||
|
'≥ 3 h zwischen 19 und 7 Uhr, überwiegend in der NAZ).')
|
||||||
|
|
||||||
|
|
||||||
|
class ResCompany(models.Model):
|
||||||
|
_inherit = 'res.company'
|
||||||
|
|
||||||
|
l10n_at_wf_wien = fields.Boolean(
|
||||||
|
string='AT Wien (WF-Zuschlag)',
|
||||||
|
help='Wiener WF-Zuschlag: WF gesamt 1,5 % (DN 0,75 %) ab '
|
||||||
|
'1. 1. 2026 statt 1,0 % (Wiener WF-Beitragstarif 2018 § 1).')
|
||||||
|
|
||||||
|
|
||||||
|
class HrPayslip(models.Model):
|
||||||
|
_inherit = 'hr.payslip'
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Basis-Helfer
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
def _l10n_at_param(self, code, datum=None):
|
||||||
|
return self.env['hr.rule.parameter']._get_parameter_from_code(
|
||||||
|
code, datum or self.date_to)
|
||||||
|
|
||||||
|
def _l10n_at_linien(self, art):
|
||||||
|
self.ensure_one()
|
||||||
|
return self.line_ids.filtered(
|
||||||
|
lambda l: l.salary_rule_id.l10n_at_bemessung == art)
|
||||||
|
|
||||||
|
def _l10n_at_brutto(self, art, result_rules=None):
|
||||||
|
"""Arbeitslohn je Bemessungsart (user-Formel-Basis): im
|
||||||
|
laufenden compute über result_rules (Zwischensummen), bei
|
||||||
|
abgeschlossenen Läufen über line_ids."""
|
||||||
|
self.ensure_one()
|
||||||
|
if result_rules is not None:
|
||||||
|
total = 0.0
|
||||||
|
for rule in self.struct_id.rule_ids:
|
||||||
|
if (rule.l10n_at_bemessung == art
|
||||||
|
and rule.code in result_rules):
|
||||||
|
total += result_rules[rule.code]['total']
|
||||||
|
return total
|
||||||
|
return sum(self._l10n_at_linien(art).mapped('total'))
|
||||||
|
|
||||||
|
def _l10n_at_lohnsteuertage(self):
|
||||||
|
"""Lohnsteuertage des eigenen LZP: 30 je vollem Monat
|
||||||
|
(user decision; § 66 Abs 3 360-Tage-Jahr); Teilmonat =
|
||||||
|
Kalendertage der Beschäftigung inkl. arbeitsfreier Tage,
|
||||||
|
max 30."""
|
||||||
|
self.ensure_one()
|
||||||
|
erster = self.date_from.replace(day=1)
|
||||||
|
naechster = (erster + timedelta(days=32)).replace(day=1)
|
||||||
|
vollen_monat = (self.date_from == erster
|
||||||
|
and self.date_to == naechster - timedelta(days=1))
|
||||||
|
version = self.version_id
|
||||||
|
von = max(self.date_from,
|
||||||
|
version.contract_date_start or self.date_from)
|
||||||
|
bis = min(self.date_to, version.contract_date_end or self.date_to)
|
||||||
|
if von > bis:
|
||||||
|
return 0
|
||||||
|
if vollen_monat and von == self.date_from and bis == self.date_to:
|
||||||
|
return 30
|
||||||
|
return min((bis - von).days + 1, 30)
|
||||||
|
|
||||||
|
def _l10n_at_ytd_slaeufe(self):
|
||||||
|
"""Validierte, nicht stornierte Läufe des Kalenderjahres vor
|
||||||
|
diesem LZP (Rollungs-/Aufrollungsbasis; refundbewusst:
|
||||||
|
Negativ-Linien des Refund-Laufs neutralisieren den Original)."""
|
||||||
|
self.ensure_one()
|
||||||
|
jahr = self.date_to.year
|
||||||
|
return self.env['hr.payslip'].search([
|
||||||
|
('employee_id', '=', self.employee_id.id),
|
||||||
|
('date_to', '>=', date(jahr, 1, 1)),
|
||||||
|
('date_to', '<', self.date_from),
|
||||||
|
]).filtered(lambda p: p.state not in ('draft', 'cancel'))
|
||||||
|
|
||||||
|
def _l10n_at_ytd_linien_total(self, code):
|
||||||
|
"""YTD-Summe einer Regel über frühere Läufe (refundbewusst)."""
|
||||||
|
self.ensure_one()
|
||||||
|
total = 0.0
|
||||||
|
for slip in self._l10n_at_ytd_slaeufe():
|
||||||
|
total += sum(
|
||||||
|
slip.line_ids.filtered(
|
||||||
|
lambda l: l.code == code).mapped('total'))
|
||||||
|
return total
|
||||||
|
|
||||||
|
def _l10n_at_lohnsteuertage_ytd(self):
|
||||||
|
self.ensure_one()
|
||||||
|
return sum(slip._l10n_at_lohnsteuertage()
|
||||||
|
for slip in self._l10n_at_ytd_slaeufe()) \
|
||||||
|
+ self._l10n_at_lohnsteuertage()
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# § 68 — Steuerfreiheiten (Buckets je Regel-Flag, versionierte Caps)
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
def _l10n_at_freibetraege_68(self, result_rules=None):
|
||||||
|
"""Steuerfreie Bezüge je LZP: {'szfn': x, 'ue_std': y} —
|
||||||
|
Abs 1: SFN-Zuschläge bis 400 € (600 € überw. Nachtarbeit);
|
||||||
|
Abs 2 iVm § 124b Z 440: ÜStd-Zuschläge bis 50 % Grundlohn,
|
||||||
|
erste 15 ÜStd, max 170 € (2026; ab 2027 10/120 — versioniert)."""
|
||||||
|
self.ensure_one()
|
||||||
|
ergebnis = {'szfn': 0.0, 'ue_std': 0.0}
|
||||||
|
grenzen = self._l10n_at_param('at_68_szfn_grenze')
|
||||||
|
cap_szfn = (grenzen['grenze_ueberwiegend']
|
||||||
|
if self.version_id.l10n_at_ueberw_nachtarbeit
|
||||||
|
else grenzen['grenze'])
|
||||||
|
ue = self._l10n_at_param('at_68_ue_std')
|
||||||
|
szfn = 0.0
|
||||||
|
betrag_ue, stunden_ue = 0.0, 0.0
|
||||||
|
for rule in self.struct_id.rule_ids:
|
||||||
|
werte = None
|
||||||
|
if result_rules is not None:
|
||||||
|
if rule.code in result_rules:
|
||||||
|
werte = result_rules[rule.code]
|
||||||
|
elif rule.l10n_at_bemessung != 'neutral':
|
||||||
|
line = self.line_ids.filtered(lambda l: l.code == rule.code)
|
||||||
|
if line:
|
||||||
|
werte = {'total': sum(line.mapped('total')),
|
||||||
|
'quantity': sum(line.mapped('quantity'))}
|
||||||
|
if not werte:
|
||||||
|
continue
|
||||||
|
anteil = (rule.l10n_at_zuschlagsanteil or 0.0) / 100.0
|
||||||
|
if rule.l10n_at_steuerbefreiung_68 == 'szfn':
|
||||||
|
szfn += werte['total'] * anteil
|
||||||
|
elif rule.l10n_at_steuerbefreiung_68 == 'ue_std':
|
||||||
|
betrag_ue += werte['total'] * anteil
|
||||||
|
stunden_ue += werte.get('quantity', 0.0)
|
||||||
|
ergebnis['szfn'] = min(max(szfn, 0.0), cap_szfn)
|
||||||
|
if stunden_ue > float(ue['stunden']):
|
||||||
|
betrag_ue = betrag_ue * float(ue['stunden']) / stunden_ue
|
||||||
|
ergebnis['ue_std'] = min(max(betrag_ue, 0.0), float(ue['betrag']))
|
||||||
|
return ergebnis
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# DN-SV (Bemessungsbestandteil § 62; Werte at_sv_* — GP4 baut
|
||||||
|
# DG-Seite/Geringfügigkeit/beitragsfreie Kumulative darauf)
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
def _l10n_at_svdn_basis_laufen(self, result_rules=None):
|
||||||
|
"""SV-Beitragsgrundlage laufend (HBG-capped, at_sv_*)."""
|
||||||
|
self.ensure_one()
|
||||||
|
hbg = float(self._l10n_at_param('at_sv_hbg_monatlich'))
|
||||||
|
return min(max(self._l10n_at_brutto('laufend', result_rules), 0.0), hbg)
|
||||||
|
|
||||||
|
def _l10n_at_svdn(self, komponente, result_rules=None):
|
||||||
|
"""DN-SV-Komponente auf laufender Basis: kv/pv/av/ak/wf —
|
||||||
|
AK § 61 AKG und WF § 62 Z 5 nur laufend (nicht auf §-67-Bezüge);
|
||||||
|
WF-Wien über Company-Flag (DN 0,75 statt 0,50)."""
|
||||||
|
self.ensure_one()
|
||||||
|
basis = self._l10n_at_svdn_basis_laufen(result_rules)
|
||||||
|
codes = {'kv': 'at_sv_kv_dn', 'pv': 'at_sv_pv_dn',
|
||||||
|
'av': 'at_sv_av_dn', 'ak': 'at_sv_ak_dn'}
|
||||||
|
if komponente == 'wf':
|
||||||
|
satz = 0.75 if self.company_id.l10n_at_wf_wien \
|
||||||
|
else float(self._l10n_at_param('at_sv_wf_dn'))
|
||||||
|
else:
|
||||||
|
satz = float(self._l10n_at_param(codes[komponente]))
|
||||||
|
return basis * satz / 100.0
|
||||||
|
|
||||||
|
def _l10n_at_svdn_gesamt(self, result_rules=None):
|
||||||
|
self.ensure_one()
|
||||||
|
return sum(self._l10n_at_svdn(k, result_rules)
|
||||||
|
for k in ('kv', 'pv', 'av', 'ak', 'wf'))
|
||||||
|
|
||||||
|
def _l10n_at_svdn_basis_sz(self, result_rules=None):
|
||||||
|
"""SV-BG sonstige Bezüge: HBG monatlich und SZ-Jahreshöchstbetrag
|
||||||
|
(13.860) minus YTD-SZ-Basen (Anspruchsprinzip-Vorlauf GP4)."""
|
||||||
|
self.ensure_one()
|
||||||
|
sz = max(self._l10n_at_brutto('sonstig', result_rules), 0.0)
|
||||||
|
hbg = float(self._l10n_at_param('at_sv_hbg_monatlich'))
|
||||||
|
jahreshbg = float(self._l10n_at_param('at_sv_hbg_sz_jaehrlich'))
|
||||||
|
for slip in self._l10n_at_ytd_slaeufe():
|
||||||
|
jahreshbg -= max(slip._l10n_at_brutto('sonstig'), 0.0)
|
||||||
|
return min(sz, hbg, max(jahreshbg, 0.0))
|
||||||
|
|
||||||
|
def _l10n_at_svdn_sz(self, result_rules=None):
|
||||||
|
"""SV-DN auf sonstigen Bezügen (§ 67 Abs 12 — vor Fixsätzen
|
||||||
|
abzuziehen): KV/PV/AV, ohne AK/WF (§ 62 Z 3/Z 5)."""
|
||||||
|
self.ensure_one()
|
||||||
|
basis = self._l10n_at_svdn_basis_sz(result_rules)
|
||||||
|
satz = sum(float(self._l10n_at_param(code)) for code in
|
||||||
|
('at_sv_kv_dn', 'at_sv_pv_dn', 'at_sv_av_dn'))
|
||||||
|
return basis * satz / 100.0
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Bemessung (user-Formeln) — laufend / sonstig
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
def _l10n_at_bemessung_laufen(self, result_rules=None):
|
||||||
|
"""LSt-Bemessung laufend: Arbeitslohn − SV-DN (§ 62-Kette,
|
||||||
|
inkl. AK/WF) − steuerfreie Bezüge § 68. Der volle Arbeitslohn
|
||||||
|
ist lohnsteuerlich unbegrenzt (keine HBG-Cap für die LSt;
|
||||||
|
die Cap gilt nur für die SV-Sätze)."""
|
||||||
|
self.ensure_one()
|
||||||
|
freibe = self._l10n_at_freibetraege_68(result_rules)
|
||||||
|
return (self._l10n_at_brutto('laufend', result_rules)
|
||||||
|
- self._l10n_at_svdn_gesamt(result_rules)
|
||||||
|
- freibe['szfn'] - freibe['ue_std'])
|
||||||
|
|
||||||
|
def _l10n_at_sz_bemessung(self, result_rules=None):
|
||||||
|
"""LSt-Bemessung sonstiger Bezug: Bezug − SV-DN (Abs 12);
|
||||||
|
Freibetrag/Sechstel-Portionierung erfolgt in
|
||||||
|
_l10n_at_lst_sonstige."""
|
||||||
|
self.ensure_one()
|
||||||
|
return (self._l10n_at_brutto('sonstig', result_rules)
|
||||||
|
- self._l10n_at_svdn_sz(result_rules))
|
||||||
|
|
||||||
|
def _l10n_at_brutto_laufen_ytd(self, result_rules=None):
|
||||||
|
"""Kumulierte laufende Bruttobezüge des Jahres (Sechstel-Basis
|
||||||
|
ist Brutto, § 67 Abs 2 — inkl. Auszahlungsmonat-Laufendem)."""
|
||||||
|
self.ensure_one()
|
||||||
|
total = self._l10n_at_brutto('laufend', result_rules)
|
||||||
|
for slip in self._l10n_at_ytd_slaeufe():
|
||||||
|
total += slip._l10n_at_brutto('laufend')
|
||||||
|
return total
|
||||||
|
|
||||||
|
def _l10n_at_sechstel(self, result_rules=None):
|
||||||
|
"""Jahressechstel = kumulierte laufende Brutto × 60 ÷
|
||||||
|
Lohnsteuertage (YTD inkl. aktueller LZP)."""
|
||||||
|
self.ensure_one()
|
||||||
|
tage = self._l10n_at_lohnsteuertage_ytd()
|
||||||
|
if not tage:
|
||||||
|
return 0.0
|
||||||
|
return self._l10n_at_brutto_laufen_ytd(result_rules) * 60.0 / tage
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# § 33 Tarif + Absetzbeträge
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
def _l10n_at_tarif(self, jbg):
|
||||||
|
"""§ 33 Abs 1: Zonentarif auf die Jahresbemessungsgrundlage."""
|
||||||
|
self.ensure_one()
|
||||||
|
zonen = self._l10n_at_param('at_lst_tarif')['zonen']
|
||||||
|
steuer = 0.0
|
||||||
|
for index, (grenze, satz) in enumerate(zonen):
|
||||||
|
obere = zonen[index + 1][0] if index + 1 < len(zonen) else jbg
|
||||||
|
if jbg > grenze:
|
||||||
|
steuer += (min(jbg, obere) - grenze) * float(satz) / 100.0
|
||||||
|
return steuer
|
||||||
|
|
||||||
|
def _l10n_at_absetzbetraege(self, jbg):
|
||||||
|
"""§ 66 zitierte Absetzbeträge: Verkehrsabsetzbetrag (496 €)
|
||||||
|
+ AVB/AVAB je Vertrags-Feld und Kinderzahl (§ 33 Abs 4/5)."""
|
||||||
|
self.ensure_one()
|
||||||
|
absetz = self._l10n_at_param('at_lst_absetzbetraege')
|
||||||
|
betrag = float(absetz['verkehrsabsetzbetrag'])
|
||||||
|
fam = self.version_id.l10n_at_familienabsetzbetrag
|
||||||
|
kinder = self.version_id.children or 0
|
||||||
|
if fam in ('avb', 'avab') and kinder > 0:
|
||||||
|
if kinder == 1:
|
||||||
|
betrag += float(absetz['avb_kind1'])
|
||||||
|
else:
|
||||||
|
betrag += float(absetz['avb_kinder2']) \
|
||||||
|
+ (kinder - 2) * float(absetz['avb_weiteres_kind'])
|
||||||
|
return betrag
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# § 66 Monatslohnsteuer — kumulativ mit Rollung
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
def _l10n_at_lst_laufend(self, result_rules=None):
|
||||||
|
"""LSt auf laufende Bezüge (§ 66): kumulative Hochrechnung
|
||||||
|
(360-Tage-Jahr) minus bereits einbehaltene LSt — Vormonats-
|
||||||
|
korrekturen rollen automatisch in den Zuflussmonat (Aufrollung,
|
||||||
|
user decision). §-67-Bezüge unter der Freigrenze sowie der
|
||||||
|
Sechstel-Überschuss (§ 67 Abs 10) laufen tarifisch hier mit."""
|
||||||
|
self.ensure_one()
|
||||||
|
bem_ytd = self._l10n_at_bemessung_laufen(result_rules)
|
||||||
|
sz_tarifisch_aktuell = self._l10n_at_sz_tarifisch_aktuell(result_rules)
|
||||||
|
for slip in self._l10n_at_ytd_slaeufe():
|
||||||
|
bem_ytd += (slip._l10n_at_bemessung_laufen()
|
||||||
|
+ slip._l10n_at_sz_tarifisch_aktuell())
|
||||||
|
bem_ytd += sz_tarifisch_aktuell
|
||||||
|
tage = self._l10n_at_lohnsteuertage_ytd()
|
||||||
|
if not tage or bem_ytd <= 0:
|
||||||
|
return 0.0
|
||||||
|
faktor = 360.0 / tage
|
||||||
|
pauschalen = self._l10n_at_param('at_lst_pauschalen')
|
||||||
|
jbg = (bem_ytd * faktor
|
||||||
|
- float(pauschalen['werbepauschbetrag'])
|
||||||
|
- float(pauschalen['sonderausgaben_pauschbetrag']))
|
||||||
|
steuer = max(0.0,
|
||||||
|
self._l10n_at_tarif(jbg) - self._l10n_at_absetzbetraege(jbg))
|
||||||
|
soll_kumuliert = steuer / faktor
|
||||||
|
bereits = self._l10n_at_ytd_linien_total('LST_LFD')
|
||||||
|
return max(0.0, soll_kumuliert - bereits)
|
||||||
|
|
||||||
|
def _l10n_at_sz_tarifisch_aktuell(self, result_rules=None):
|
||||||
|
"""Anteil des aktuellen §-67-Bezugs, der tarifisch (LSt-lfd)
|
||||||
|
läuft: (a) gesamter Bezug bei Sechstel ≤ Freigrenze (2.615 €,
|
||||||
|
Pauschalierung unterbleibt) oder (b) Überschuss über das
|
||||||
|
Jahressechstel (§ 67 Abs 10)."""
|
||||||
|
self.ensure_one()
|
||||||
|
sz_bem = self._l10n_at_sz_bemessung(result_rules)
|
||||||
|
if sz_bem <= 0:
|
||||||
|
return 0.0
|
||||||
|
sechstel = self._l10n_at_sechstel(result_rules)
|
||||||
|
freigrenze = float(self._l10n_at_param('at_lst_67_freigrenze'))
|
||||||
|
if sechstel <= freigrenze:
|
||||||
|
return sz_bem
|
||||||
|
ytd_sz_bem = self._l10n_at_ytd_sz_bemessung()
|
||||||
|
verbleibend = max(sechstel - ytd_sz_bem, 0.0)
|
||||||
|
return max(sz_bem - verbleibend, 0.0)
|
||||||
|
|
||||||
|
def _l10n_at_ytd_sz_bemessung(self):
|
||||||
|
"""Kumulierte §-67-Bemessungen früherer Läufe (für Sechstel-
|
||||||
|
Verbrauch und Aufrollung)."""
|
||||||
|
self.ensure_one()
|
||||||
|
total = 0.0
|
||||||
|
for slip in self._l10n_at_ytd_slaeufe():
|
||||||
|
total += slip._l10n_at_sz_bemessung()
|
||||||
|
return total
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# § 67 — sonstige Bezüge (Fixsätze im Sechstel + Aufrollung)
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
def _l10n_at_lst_sonstige(self, result_rules=None):
|
||||||
|
"""LSt auf sonstige Bezüge: Fixsätze 0/6/27/35,75 % auf die
|
||||||
|
Bemessung (Abs 12-SV vorher, Freibetrag 620 € jahreskumulativ)
|
||||||
|
innerhalb des Jahressechstels; Neuberechnung des gesamten
|
||||||
|
Jahres-SZ-Steuer bei jeder SZ-Zahlung (Günstig-Aufrollung,
|
||||||
|
lb-son-04): Soll = f(Jahres-Bemessung, aktuelles Sechstel)
|
||||||
|
minus bereits gezahlte SZ-Steuer."""
|
||||||
|
self.ensure_one()
|
||||||
|
sz_bem = self._l10n_at_sz_bemessung(result_rules)
|
||||||
|
if sz_bem <= 0:
|
||||||
|
return 0.0
|
||||||
|
sechstel = self._l10n_at_sechstel(result_rules)
|
||||||
|
freigrenze = float(self._l10n_at_param('at_lst_67_freigrenze'))
|
||||||
|
if sechstel <= freigrenze:
|
||||||
|
return 0.0 # tarifisch — läuft über LST_LFD
|
||||||
|
ytd_bem = max(self._l10n_at_ytd_sz_bemessung(), 0.0)
|
||||||
|
total_in_sechstel = min(ytd_bem + sz_bem, sechstel)
|
||||||
|
fixsaetze = self._l10n_at_param('at_lst_67_fixsaetze')
|
||||||
|
freibetrag = float(fixsaetze['freibetrag'])
|
||||||
|
soll_steuer = 0.0
|
||||||
|
offen = max(total_in_sechstel - freibetrag, 0.0)
|
||||||
|
for breite, satz in fixsaetze['pakete']:
|
||||||
|
portion = min(offen, float(breite))
|
||||||
|
soll_steuer += portion * float(satz) / 100.0
|
||||||
|
offen -= portion
|
||||||
|
if offen <= 0:
|
||||||
|
break
|
||||||
|
bereits = self._l10n_at_ytd_linien_total('LST_SZ')
|
||||||
|
return max(soll_steuer - bereits, 0.0)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Nachzahlungsfälle (plan 4.3 — Rollung automatisch; Fünftelregelung
|
||||||
|
# § 67 Abs 8 lit c als Utility; 15.2.-Cutoff/Willkür→Tarif GP8)
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
def _l10n_at_fuenftelregelung(self, betrag):
|
||||||
|
"""§ 67 Abs 8 lit c: zwangsläufige Verschiebung eines sonstigen
|
||||||
|
Bezugs über den 15. 2. — 1/5 des begünstigten Teils steuerfrei,
|
||||||
|
4/5 mit Tarif (Utility für GP8-Nachzahlungskonfiguration)."""
|
||||||
|
self.ensure_one()
|
||||||
|
return betrag * 4.0 / 5.0
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
# Part of the odoo-at-payroll project. License: LGPL-3.
|
# Part of the odoo-at-payroll project. License: LGPL-3.
|
||||||
from lxml import etree
|
from lxml import etree
|
||||||
|
|
||||||
|
import base64
|
||||||
|
|
||||||
from odoo import _, api, fields, models
|
from odoo import _, api, fields, models
|
||||||
from odoo.exceptions import UserError
|
from odoo.exceptions import UserError
|
||||||
|
|
||||||
@@ -101,7 +103,14 @@ class L10nAtPayrollTasyImportWizard(models.TransientModel):
|
|||||||
def _tasy_root(self):
|
def _tasy_root(self):
|
||||||
self.ensure_one()
|
self.ensure_one()
|
||||||
parser = etree.XMLParser(resolve_entities=False, no_network=True)
|
parser = etree.XMLParser(resolve_entities=False, no_network=True)
|
||||||
return etree.fromstring(self.file, parser)
|
data = self.file or b''
|
||||||
|
if isinstance(data, str):
|
||||||
|
data = data.encode()
|
||||||
|
# Binary kann XML-Bytes ODER base64 (str/Bytes) liefern:
|
||||||
|
# dekodieren, wenn kein '<' am Anfang steht.
|
||||||
|
if not data.lstrip().startswith(b'<'):
|
||||||
|
data = base64.b64decode(data)
|
||||||
|
return etree.fromstring(data, parser)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _tasy_text(node, tag):
|
def _tasy_text(node, tag):
|
||||||
@@ -233,7 +242,9 @@ class L10nAtPayrollTasyImportWizard(models.TransientModel):
|
|||||||
self.ensure_one()
|
self.ensure_one()
|
||||||
old = parameter_obj._get_parameter_from_code(code, date_from,
|
old = parameter_obj._get_parameter_from_code(code, date_from,
|
||||||
raise_if_not_found=False)
|
raise_if_not_found=False)
|
||||||
old_value = old.parameter_value if old else ''
|
# _get_parameter_from_code liefert den geparsten Wert (float/dict)
|
||||||
|
# oder None — nicht den Parameter-Record.
|
||||||
|
old_value = repr(old) if old is not None else ''
|
||||||
old_float = None
|
old_float = None
|
||||||
try:
|
try:
|
||||||
old_float = float(old_value)
|
old_float = float(old_value)
|
||||||
|
|||||||
@@ -1,2 +1,3 @@
|
|||||||
# Part of the odoo-at-payroll project. License: LGPL-3.
|
# Part of the odoo-at-payroll project. License: LGPL-3.
|
||||||
from . import test_tasy_import
|
from . import test_tasy_import
|
||||||
|
from . import test_lohnsteuer
|
||||||
@@ -0,0 +1,284 @@
|
|||||||
|
# Part of the odoo-at-payroll project. License: LGPL-3.
|
||||||
|
"""GP3 Lohnsteuer-Kern: § 33 Tarif, Absetzbeträge, § 66 kumulative
|
||||||
|
Monatslohnsteuer (Rollung), § 68-Freibeträge, § 67 Sechstel/Fixsätze
|
||||||
|
mit Günstig-Aufrollung, DN-SV-Basis. Rechenfälle s.
|
||||||
|
personalverrechnung/RECHTSQUELLEN-Privat.md Abschnitt 5 (P0-GP3)."""
|
||||||
|
from datetime import date, timedelta
|
||||||
|
|
||||||
|
from odoo.tests import TransactionCase, tagged
|
||||||
|
|
||||||
|
|
||||||
|
@tagged('post_install', '-at_install')
|
||||||
|
class LohnsteuerTest(TransactionCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super().setUp()
|
||||||
|
self.category = self.env['hr.salary.rule.category'].create({
|
||||||
|
'name': 'Test AT Kern',
|
||||||
|
'code': 'TEST_AT_KERN',
|
||||||
|
})
|
||||||
|
self.structure_type = self.env['hr.payroll.structure.type'].create({
|
||||||
|
'name': 'Test AT Kern Type',
|
||||||
|
})
|
||||||
|
self.structure = self.env['hr.payroll.structure'].create({
|
||||||
|
'name': 'Test AT Lohnsteuer',
|
||||||
|
'code': 'TEST_AT_LST',
|
||||||
|
'type_id': self.structure_type.id,
|
||||||
|
})
|
||||||
|
|
||||||
|
def _regel(self, code, betrag, bemessung='laufend', flag68='keine',
|
||||||
|
anteil=0.0, quantity='1.0', python=None):
|
||||||
|
vals = {
|
||||||
|
'name': code,
|
||||||
|
'code': code,
|
||||||
|
'struct_id': self.structure.id,
|
||||||
|
'category_id': self.category.id,
|
||||||
|
'condition_select': 'none',
|
||||||
|
'l10n_at_bemessung': bemessung,
|
||||||
|
'l10n_at_steuerbefreiung_68': flag68,
|
||||||
|
'l10n_at_zuschlagsanteil': anteil,
|
||||||
|
}
|
||||||
|
if python:
|
||||||
|
vals.update({'amount_select': 'code',
|
||||||
|
'amount_python_compute': python})
|
||||||
|
else:
|
||||||
|
vals.update({'amount_select': 'fix', 'amount_fix': betrag,
|
||||||
|
'quantity': quantity})
|
||||||
|
return self.env['hr.salary.rule'].create(vals)
|
||||||
|
|
||||||
|
def _employee(self, version_vals=None):
|
||||||
|
employee = self.env['hr.employee'].create({'name': 'Test LSt'})
|
||||||
|
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.write({'contract_date_start': date(2020, 1, 1)})
|
||||||
|
if version_vals:
|
||||||
|
version.write(version_vals)
|
||||||
|
return employee
|
||||||
|
|
||||||
|
def _slip(self, employee, monat, jahr=2026, confirm=False):
|
||||||
|
erster = date(jahr, monat, 1)
|
||||||
|
naechster = (erster + timedelta(days=32)).replace(day=1)
|
||||||
|
slip = self.env['hr.payslip'].create({
|
||||||
|
'name': 'Test-LSt %d/%d' % (monat, jahr),
|
||||||
|
'employee_id': employee.id,
|
||||||
|
'struct_id': self.structure.id,
|
||||||
|
'date_from': erster,
|
||||||
|
'date_to': naechster - timedelta(days=1),
|
||||||
|
})
|
||||||
|
slip.compute_sheet()
|
||||||
|
if confirm:
|
||||||
|
slip.action_payslip_done()
|
||||||
|
return slip
|
||||||
|
|
||||||
|
def _total(self, slip, code):
|
||||||
|
return sum(slip.line_ids.filtered(
|
||||||
|
lambda l: l.code == code).mapped('total'))
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# § 33 Tarif / Absetzbeträge
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
def test_01_tarif_zonen(self):
|
||||||
|
employee = self._employee()
|
||||||
|
slip = self._slip(employee, 1)
|
||||||
|
self.assertAlmostEqual(slip._l10n_at_tarif(13539.0), 0.0, places=2)
|
||||||
|
# 15.000: 20 % auf 1.461 = 292,20
|
||||||
|
self.assertAlmostEqual(slip._l10n_at_tarif(15000.0), 292.20, places=2)
|
||||||
|
# 30.000: 1.690,60 + (30.000−21.992) × 0,30 = 4.093,00
|
||||||
|
self.assertAlmostEqual(slip._l10n_at_tarif(30000.0), 4093.00, places=2)
|
||||||
|
# 80.000: 1.690,60 + 4.339,80 + 13.562,80 + 4.624,80 = 24.218,00
|
||||||
|
self.assertAlmostEqual(slip._l10n_at_tarif(80000.0), 24218.00, places=2)
|
||||||
|
|
||||||
|
def test_02_absetzbetraege(self):
|
||||||
|
employee = self._employee()
|
||||||
|
slip = self._slip(employee, 1)
|
||||||
|
self.assertAlmostEqual(slip._l10n_at_absetzbetraege(20000.0), 496.0, places=2)
|
||||||
|
version = slip.version_id
|
||||||
|
version.write({'l10n_at_familienabsetzbetrag': 'avb', 'children': 1})
|
||||||
|
self.assertAlmostEqual(
|
||||||
|
slip._l10n_at_absetzbetraege(20000.0), 496.0 + 612.0, places=2)
|
||||||
|
version.write({'children': 3})
|
||||||
|
self.assertAlmostEqual(
|
||||||
|
slip._l10n_at_absetzbetraege(20000.0),
|
||||||
|
496.0 + 828.0 + 273.0, places=2)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Lohnsteuertage (user decision: 30 je vollem Monat)
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
def test_03_lohnsteuertage(self):
|
||||||
|
employee = self._employee()
|
||||||
|
slip = self._slip(employee, 1)
|
||||||
|
self.assertEqual(slip._l10n_at_lohnsteuertage(), 30)
|
||||||
|
slip.version_id.write({
|
||||||
|
'contract_date_start': date(2026, 1, 16)})
|
||||||
|
# 16.–31. 1. = 16 Kalendertage (inkl. arbeitsfreier, § 66 Abs 3)
|
||||||
|
self.assertEqual(slip._l10n_at_lohnsteuertage(), 16)
|
||||||
|
slip.version_id.write({
|
||||||
|
'contract_date_start': date(2026, 1, 1),
|
||||||
|
'contract_date_end': date(2026, 1, 10)})
|
||||||
|
self.assertEqual(slip._l10n_at_lohnsteuertage(), 10)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# § 68 Freibeträge (Buckets über Regel-Flags)
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
def test_68_freibetraege(self):
|
||||||
|
employee = self._employee()
|
||||||
|
self._regel('L68A', 300.0, flag68='szfn', anteil=50.0)
|
||||||
|
self._regel('L68B', 300.0, flag68='szfn', anteil=50.0) # 300 gesamt > 400? 2x150=300
|
||||||
|
self._regel('L68C', 41.19, flag68='ue_std', anteil=33.33, quantity='2.0')
|
||||||
|
slip = self._slip(employee, 1)
|
||||||
|
freibe = slip._l10n_at_freibetraege_68()
|
||||||
|
# szfn: 150 + 150 = 300 ≤ 400
|
||||||
|
self.assertAlmostEqual(freibe['szfn'], 300.0, places=2)
|
||||||
|
# ue_std: 41,19 × 2 h? total = 41,19 × 2 = 82,38; Anteil 33,33 %
|
||||||
|
# → 27,46; 2 h ≤ 15; ≤ 170
|
||||||
|
self.assertAlmostEqual(freibe['ue_std'], 27.46, places=2)
|
||||||
|
slip.version_id.write({'l10n_at_ueberw_nachtarbeit': True})
|
||||||
|
freibe = slip._l10n_at_freibetraege_68()
|
||||||
|
self.assertAlmostEqual(freibe['szfn'], 300.0, places=2)
|
||||||
|
|
||||||
|
def test_68_ue_std_cap(self):
|
||||||
|
"""§ 68 Abs 2 iVm § 124b Z 440: 15 ÜStd/170 € (2026)."""
|
||||||
|
employee = self._employee()
|
||||||
|
# 20 ÜStd à 41,19 (Anteil 33,33 %) → Zuschlag 274,60; > 15 h
|
||||||
|
# → anteilig 15/20; cap 170 greift
|
||||||
|
self._regel('LUE', 41.19, flag68='ue_std', anteil=33.33, quantity='20.0')
|
||||||
|
slip = self._slip(employee, 1)
|
||||||
|
freibe = slip._l10n_at_freibetraege_68()
|
||||||
|
self.assertAlmostEqual(
|
||||||
|
freibe['ue_std'], min(41.19 * 20 * 0.3333 * 15 / 20, 170.0), places=2)
|
||||||
|
self.assertAlmostEqual(freibe['ue_std'], 170.0, places=1)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# DN-SV / Bemessung (user-Formel)
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
def test_05_svdn_und_bemessung(self):
|
||||||
|
"""SV-DN 18,07 % (KV/PV/AV/AK/WF) auf laufender Basis; Bemessung
|
||||||
|
= Arbeitslohn − SV-DN (user-Formel)."""
|
||||||
|
employee = self._employee()
|
||||||
|
self._regel('LFD', 2059.41)
|
||||||
|
slip = self._slip(employee, 1)
|
||||||
|
svdn = slip._l10n_at_svdn_gesamt()
|
||||||
|
self.assertAlmostEqual(svdn, 2059.41 * 0.1807, places=2)
|
||||||
|
bem = slip._l10n_at_bemessung_laufen()
|
||||||
|
self.assertAlmostEqual(bem, 2059.41 - svdn, places=2)
|
||||||
|
|
||||||
|
def test_06_svdn_wien_wf(self):
|
||||||
|
employee = self._employee()
|
||||||
|
self._regel('LFD', 2059.41)
|
||||||
|
slip = self._slip(employee, 1)
|
||||||
|
slip.company_id.write({'l10n_at_wf_wien': True})
|
||||||
|
self.assertAlmostEqual(
|
||||||
|
slip._l10n_at_svdn('wf'), 2059.41 * 0.0075, places=2)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# § 66 kumulative Monatslohnsteuer + Rollung
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
def test_07_lst_laufend_monat(self):
|
||||||
|
"""Vollmonat: JBG = Bemessung × 12 − 192; Tarif − Verkehrs-AB."""
|
||||||
|
employee = self._employee()
|
||||||
|
self._regel('LFD', 2059.41)
|
||||||
|
slip = self._slip(employee, 1)
|
||||||
|
svdn = slip._l10n_at_svdn_gesamt()
|
||||||
|
bem = 2059.41 - svdn
|
||||||
|
jbg = bem * 12 - 132 - 60
|
||||||
|
erwartet = max(0.0, (0.20 * (jbg - 13539)) - 496.0) / 12
|
||||||
|
self.assertAlmostEqual(
|
||||||
|
slip._l10n_at_lst_laufend(), erwartet, places=2)
|
||||||
|
|
||||||
|
def test_08_lst_rollung_zweiter_monat(self):
|
||||||
|
"""Kumulativ mit LST-Linie: Februar zahlt die Differenz zum
|
||||||
|
Jänner-Soll (Rollung über result_rules-Mechanik)."""
|
||||||
|
employee = self._employee()
|
||||||
|
self._regel('LFD', 2059.41)
|
||||||
|
self._regel(
|
||||||
|
'LST_LFD', 0.0, bemessung='neutral',
|
||||||
|
python='result = payslip._l10n_at_lst_laufend(result_rules)')
|
||||||
|
slip_jan = self._slip(employee, 1, confirm=True)
|
||||||
|
jan = self._total(slip_jan, 'LST_LFD')
|
||||||
|
slip_feb = self._slip(employee, 2)
|
||||||
|
feb = self._total(slip_feb, 'LST_LFD')
|
||||||
|
# Identisches Gehalt: Feb-Soll ≈ Jan-Soll (kumulative Neuberechnung
|
||||||
|
# minus bereits einbehaltene LSt)
|
||||||
|
self.assertAlmostEqual(feb, jan, places=2)
|
||||||
|
# Rollung: dritter Monat zieht die Differenz nach
|
||||||
|
slip_mar = self._slip(employee, 3)
|
||||||
|
self.assertAlmostEqual(self._total(slip_mar, 'LST_LFD'), jan, places=2)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# § 67 sonstige Bezüge
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
def test_09_lst_sonstige_b13(self):
|
||||||
|
"""B13 über Freigrenze: Fixsatz 6 % nach 620-Freibetrag;
|
||||||
|
Abs 12: SV-DN vom SZ vorher. SZ-Regel erst NACH dem Oktober-
|
||||||
|
lauf anlegen (sonst zahlt Oktober fälschlich SZ)."""
|
||||||
|
employee = self._employee()
|
||||||
|
self._regel('LFD', 2059.41)
|
||||||
|
slip_okt = self._slip(employee, 10, confirm=True)
|
||||||
|
self._regel('SZ', 2059.41, bemessung='sonstig')
|
||||||
|
slip_nov = self._slip(employee, 11)
|
||||||
|
sz_bem = slip_nov._l10n_at_sz_bemessung()
|
||||||
|
sv_sz = 2059.41 * 0.1707 # KV+PV+AV auf SZ-Basis
|
||||||
|
self.assertAlmostEqual(sz_bem, 2059.41 - sv_sz, places=2)
|
||||||
|
# Sechstel = 2 × 2059,41 × 60 ÷ 60 = 4.118,82 > Freigrenze
|
||||||
|
self.assertAlmostEqual(
|
||||||
|
slip_nov._l10n_at_sechstel(), 2 * 2059.41, places=2)
|
||||||
|
erwartet = (sz_bem - 620.0) * 0.06
|
||||||
|
self.assertAlmostEqual(
|
||||||
|
slip_nov._l10n_at_lst_sonstige(), erwartet, places=2)
|
||||||
|
self.assertAlmostEqual(slip_nov._l10n_at_sz_tarifisch_aktuell(), 0.0, places=2)
|
||||||
|
|
||||||
|
def test_10_sechstel_freigrenze_tarifisch(self):
|
||||||
|
"""Sechstel ≤ 2.615 €: Pauschalierung unterbleibt — SZ läuft
|
||||||
|
tarifisch über die §-66-Kette."""
|
||||||
|
employee = self._employee()
|
||||||
|
self._regel('LFD', 500.0)
|
||||||
|
self._regel('SZ', 300.0, bemessung='sonstig')
|
||||||
|
slip = self._slip(employee, 1)
|
||||||
|
# Sechstel = 500 × 60/30 = 1.000 ≤ 2.615
|
||||||
|
self.assertAlmostEqual(slip._l10n_at_sechstel(), 1000.0, places=2)
|
||||||
|
self.assertAlmostEqual(slip._l10n_at_lst_sonstige(), 0.0, places=2)
|
||||||
|
sz_bem = slip._l10n_at_sz_bemessung()
|
||||||
|
self.assertAlmostEqual(
|
||||||
|
slip._l10n_at_sz_tarifisch_aktuell(), sz_bem, places=2)
|
||||||
|
|
||||||
|
def test_11_guenstig_aufrollung_zweite_sz(self):
|
||||||
|
"""Günstig-Aufrollung: zweite SZ-Zahlung rechnet die Jahres-
|
||||||
|
SZ-Steuer mit dem dann aktuellen Sechstel neu."""
|
||||||
|
employee = self._employee()
|
||||||
|
self._regel('LFD', 2059.41)
|
||||||
|
slip_okt = self._slip(employee, 10, confirm=True)
|
||||||
|
self._regel('SZ', 2059.41, bemessung='sonstig')
|
||||||
|
self._regel(
|
||||||
|
'LST_SZ', 0.0, bemessung='neutral',
|
||||||
|
python='result = payslip._l10n_at_lst_sonstige(result_rules)')
|
||||||
|
slip_nov = self._slip(employee, 11, confirm=True)
|
||||||
|
sz_nov = self._total(slip_nov, 'LST_SZ')
|
||||||
|
# SV-SZ = 2.059,41 × 17,07 % = 351,54 (KV+PV+AV auf SZ-Basis)
|
||||||
|
self.assertAlmostEqual(
|
||||||
|
sz_nov, (2059.41 * (1 - 0.1707) - 620.0) * 0.06, places=1)
|
||||||
|
slip_dez = self._slip(employee, 12)
|
||||||
|
sz_dez = self._total(slip_dez, 'LST_SZ')
|
||||||
|
# Jahres-SZ-Bemessung = 2 × (2.059,41 − SV-SZ)
|
||||||
|
jahres_bem = 2 * 2059.41 * (1 - 0.1707)
|
||||||
|
soll_jahr = (jahres_bem - 620.0) * 0.06
|
||||||
|
self.assertAlmostEqual(sz_dez, soll_jahr - sz_nov, places=1)
|
||||||
|
|
||||||
|
def test_12_sz_ueberschuss_tarifisch(self):
|
||||||
|
"""§ 67 Abs 10: SZ über dem Sechstel läuft tarifisch (LSt-lfd);
|
||||||
|
Sechstel muss über der Freigrenze 2.615 € liegen."""
|
||||||
|
employee = self._employee()
|
||||||
|
self._regel('LFD', 2000.0)
|
||||||
|
self._regel('SZ', 6000.0, bemessung='sonstig')
|
||||||
|
slip = self._slip(employee, 1)
|
||||||
|
# Sechstel = 2.000 × 60/30 = 4.000 > 2.615 → Pauschalierung
|
||||||
|
self.assertAlmostEqual(slip._l10n_at_sechstel(), 4000.0, places=2)
|
||||||
|
sz_bem = slip._l10n_at_sz_bemessung() # 6.000 − SV-SZ (1.024,20)
|
||||||
|
self.assertAlmostEqual(sz_bem, 6000.0 - 6000.0 * 0.1707, places=2)
|
||||||
|
self.assertAlmostEqual(
|
||||||
|
slip._l10n_at_sz_tarifisch_aktuell(), sz_bem - 4000.0, places=2)
|
||||||
|
self.assertAlmostEqual(
|
||||||
|
slip._l10n_at_lst_sonstige(), (4000.0 - 620.0) * 0.06, places=1)
|
||||||
@@ -106,22 +106,22 @@ class TasyImportTest(TransactionCase):
|
|||||||
parameter = self.env['hr.rule.parameter']
|
parameter = self.env['hr.rule.parameter']
|
||||||
stichtag = date(2026, 7, 1)
|
stichtag = date(2026, 7, 1)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
float(parameter._get_parameter_from_code('at_sv_kv_dn', stichtag).parameter_value),
|
float(parameter._get_parameter_from_code('at_sv_kv_dn', stichtag)),
|
||||||
3.87)
|
3.87)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
float(parameter._get_parameter_from_code('at_sv_kv_dg', stichtag).parameter_value),
|
float(parameter._get_parameter_from_code('at_sv_kv_dg', stichtag)),
|
||||||
3.78)
|
3.78)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
float(parameter._get_parameter_from_code('at_sv_gfg_monatlich', stichtag).parameter_value),
|
float(parameter._get_parameter_from_code('at_sv_gfg_monatlich', stichtag)),
|
||||||
551.10)
|
551.10)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
float(parameter._get_parameter_from_code('at_sv_hbg_monatlich', stichtag).parameter_value),
|
float(parameter._get_parameter_from_code('at_sv_hbg_monatlich', stichtag)),
|
||||||
6930.00)
|
6930.00)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
float(parameter._get_parameter_from_code('at_sv_uv_dg', stichtag).parameter_value),
|
float(parameter._get_parameter_from_code('at_sv_uv_dg', stichtag)),
|
||||||
1.10)
|
1.10)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
float(parameter._get_parameter_from_code('at_sv_iesg_dg', stichtag).parameter_value),
|
float(parameter._get_parameter_from_code('at_sv_iesg_dg', stichtag)),
|
||||||
0.10)
|
0.10)
|
||||||
|
|
||||||
def test_02_wizard_parse_fixture(self):
|
def test_02_wizard_parse_fixture(self):
|
||||||
|
|||||||
Reference in New Issue
Block a user