Files
odoo-at-payroll/addons/l10n_at_hr_payroll/models/lohnsteuer.py
T
fegger 9add58fe83 [IMP] l10n_at_hr_payroll_private: add statutory old-system severance (AP15-A)
Pay the pre-2003 AngG severance from the end-of-employment run as its own neutral benefit line: free of SV (§ 49 Abs 3 Z 7 ASVG) and payroll levies, outside the annual sixth, and collected into the § 291d EO termination pot net of its own wage tax. Taxation follows the binding better-of rule of § 67 Abs 3 EStG: multiple-of-method (tariff wage tax on the running monthly wage × the statutory multiple of the payout) versus the flat 6 % rate, applied mandatorily in favour of the employee. The tariff wage tax is captured as a separately confirmed value instead of deriving it from gross pay through the § 62 engine. The wizard enforces § 23a Abs 3 AngG (parental exit needs five uninterrupted years), blocks BMSVG-new contracts except frozen claims and validates AGRD 02/06 against the claim type. The LNK basis subtracts § 67 Abs 3/6 rules only when they actually run in the tax base, the L16 generator reports LST_ABF in key 260 outside 210/220, and result_rules access in the garnishment paths avoids DefaultDictPayroll materialisation. Verified against the original statute texts in .ris/ (AngG §§ 23/23a, EStG § 67 Abs 3).

Version 19.0.13.0.0 (private), 19.0.9.0.0 (core)
2026-09-15 11:01:55 +02:00

499 lines
23 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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); 620Jahressechstel 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 %.')
l10n_at_st_frei = fields.Selection(
[('keine', 'Keine § 3/§ 26-Steuerfreiheit'),
('mahlzeit', 'Mahlzeiten-Gutschein (§ 3 Abs 1 Z 17; 8 €/AT)'),
('rabatt', 'Mitarbeiterrabatt (§ 3 Abs 1 Z 21; 1.000 €/J)'),
('betriebsveranstaltung',
'Betriebsveranstaltung (§ 3 Abs 1 Z 14; 365 €/J)'),
('jubilaeum',
'Sachzuwendung/Jubiläum/Geschenk (§ 3 Abs 1 Z 14; 186 €/J)'),
('homeoffice',
'Homeoffice-Pauschale (§ 26 Z 9; 3 €/Tag, max 100 Tage/J)')],
string='AT § 3/§ 26 (steuerfrei)',
default='keine',
help='Steuerfreie Bezugsbestandteile (Freibeträge): der '
'steuerfreie Anteil wird je Bezugsmonat von der LSt-'
'Bemessungsgrundlage und von der LNK-Bemessungsgrundlage '
'abgezogen; Grenzen als Jahres-Kumulative je Dienstnehmer '
'(at_st_grenzen). Nur der übersteigende Teil ist '
'steuerpflichtig. Menge = Arbeitstage (Z 17/Z 9).')
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
# ------------------------------------------------------------------
# § 3/§ 26 — Steuerfreiheit (Buckets je Regel-Flag, versionierte
# Caps; Jahres-Kumulative — Spieglung der §-49-SV-Seite)
# ------------------------------------------------------------------
def _l10n_at_st_frei_ytd(self, kategorie, menge=False):
"""Jahresverbrauch einer §-3/§-26-Kategorie aus früheren
validierten Läufen (refundbewusst über _l10n_at_ytd_slaeufe)."""
self.ensure_one()
total = 0.0
for slip in self._l10n_at_ytd_slaeufe():
linien = slip.line_ids.filtered(
lambda l: l.salary_rule_id.l10n_at_st_frei == kategorie)
if menge:
total += sum(linien.mapped('quantity'))
else:
total += sum(linien.mapped('total'))
return total
def _l10n_at_st_frei_freie_laufen(self, result_rules=None):
"""Steuerfreie Anteile laufender Bezüge dieses LZP: je Kategorie
Tages-/Jahresgrenzen (at_st_grenzen) minus Jahresverbrauch;
Freibeträge — nur der übersteigende Teil steuerpflichtig
(lb-sac-07/17, lb-ges-02, lb-sac-02)."""
self.ensure_one()
grenzen = self.env['hr.rule.parameter']._get_parameter_from_code(
'at_st_grenzen', self.date_to, raise_if_not_found=False)
if not grenzen:
return 0.0
frei_gesamt = 0.0
for rule in self.struct_id.rule_ids:
kategorie = rule.l10n_at_st_frei
if (not kategorie or kategorie == 'keine'
or rule.l10n_at_bemessung != 'laufend'):
continue
werte = None
if result_rules is not None:
if rule.code in result_rules:
werte = result_rules[rule.code]
else:
linie = self.line_ids.filtered(lambda l: l.code == rule.code)
if linie:
werte = {'total': sum(linie.mapped('total')),
'quantity': sum(linie.mapped('quantity'))}
if not werte:
continue
total = werte['total']
menge = werte.get('quantity', 0.0)
if kategorie == 'mahlzeit':
frei = min(
total, float(grenzen.get('mahlzeit_tag')) * menge)
elif kategorie == 'homeoffice':
rest_tage = max(
float(grenzen.get('homeoffice_tage'))
- self._l10n_at_st_frei_ytd('homeoffice', menge=True),
0.0)
frei = min(
total,
float(grenzen.get('homeoffice_tag'))
* min(menge, rest_tage))
else:
pool = {'rabatt': 'rabatt_jahr',
'betriebsveranstaltung': 'betriebsveranstaltung_jahr',
'jubilaeum': 'jubilaeum_jahr'}.get(kategorie)
rest = max(
float(grenzen.get(pool))
- self._l10n_at_st_frei_ytd(kategorie), 0.0)
frei = min(total, rest)
frei_gesamt += max(frei, 0.0)
return frei_gesamt
# ------------------------------------------------------------------
# DN-SV (Bemessungsbestandteil § 62) — Basis/Sätze/Geringfügigkeit/
# §-49-Kumulative/DG-Seite: models/sozialversicherung.py (GP4)
# ------------------------------------------------------------------
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);
Wien-WF über Satzvektor (l10n_at_wf_wien/Bundesland, GP4)."""
self.ensure_one()
return self._l10n_at_sv_beitrag('dn', komponente, result_rules)
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_sv_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_sv_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); Satzausschal-
tungen/Steffel über _l10n_at_sv_satz (GP4)."""
self.ensure_one()
if self._l10n_at_geringfuegig(result_rules):
return 0.0
basis = self._l10n_at_svdn_basis_sz(result_rules)
satz = sum(self._l10n_at_sv_satz(
'dn', k, staffel_basis=self._l10n_at_sv_basis_roh(result_rules))
for k in ('kv', 'pv', 'av'))
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 steuerfreie Sachbezüge
§ 3/§ 26 (Freibeträge, Jahres-Kumulative). 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']
- self._l10n_at_st_frei_freie_laufen(result_rules))
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