Files
odoo-at-payroll/addons/l10n_at_hr_payroll/models/lohnsteuer.py
T
fegger 4b86bcb790 [IMP] l10n_at_hr_payroll_private: add voluntary severance under § 67 Abs 6 (AP15-B)
Split the voluntary severance into its privileged part and a tariff remainder on the exit run: the privileged part combines the quarter rule (1/4 of the last twelve months' running pay, capped at nine times the monthly HBG) with the twelfth rule (2/12 to 12/12 by proven service, capped at n × 3 × HBG), reduced per Z 3 by severance already received and the statutory claim due at this termination, all taxed at the flat 6 % rate. The excess runs as its own rule flagged l10n_at_s67_abs6_ueberhang: taxed with the running tariff of the payment month but excluded from the annual sixth, since Z 5 exempts only the Z 1/Z 2 amounts and the excess is a special payment, not running pay. The twelve-month basis aggregates validated prior runs plus own lines, excluding the flagged excess and § 26 Z 4 travel overhang via a dedicated rule flag. Free of SV (§ 49 Abs 3 Z 7 ASVG), payroll levies regardless of the 6 %/tarif split (§ 41 Abs 4 lit b FLAG, § 122 Abs 8 WKG, § 5 Abs 2 lit b KommStG) and BV contributions; the former BV-Anwartschaft exclusion Z 7 is repealed (BGBl I 118/2015). The wizard proposes the rolling basis and the statutory claim, previews Z 1/Z 2/reduction/privileged/excess/tax and requires explicit confirmation against claim-conversion risk. The § 291d EO termination pot nets both severance wage-tax rules; key 260 now includes LST_ABF_FREI. Verified against the original statute text in .ris/ (EStG § 67 Abs 6) and the knowledge-base sources lb-end-12/13/15; KZ positioning of Abs-6 amounts and the excess's § 291d pot allocation remain documented verification points against LStR Rz 1087 ff.

Version 19.0.14.0.0 (private), 19.0.10.0.0 (core)
2026-09-15 11:34:09 +02:00

529 lines
25 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).')
l10n_at_s67_abs6_ueberhang = fields.Boolean(
string='AT § 67 Abs 6 Z 6 — Überhang',
help='Übersteigender Teil einer freiwilligen Abfertigung/Abfindung '
'(§ 67 Abs 6 Z 6 EStG): wird wie ein laufender Bezug zum '
'Tarif des Zuflussmonats versteuert, erhöht aber das '
'Jahressechstel nicht — Abs 2 ist nur auf die mit 6 % '
'versteuerten Z-1/Z-2-Beträge nicht anzuwendbar (Z 5), der '
'Überhang ist ein sonstiger Bezug, kein laufender. LNK-frei '
'über § 67 Abs 6 (lb-end-12), SV-frei über § 49 Abs 3 Z 7.')
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).
Der §-67-Abs-6-Überhang (Flag) läuft tarifisch im Zuflussmonat,
ist aber ein sonstiger Bezug und erhöht das Jahressechstel
nicht."""
self.ensure_one()
total = self._l10n_at_brutto('laufend', result_rules) \
- self._l10n_at_abs6_ueberhang_total(result_rules)
for slip in self._l10n_at_ytd_slaeufe():
total += slip._l10n_at_brutto('laufend') \
- slip._l10n_at_abs6_ueberhang_total()
return total
def _l10n_at_abs6_ueberhang_total(self, result_rules=None):
"""Summe der als §-67-Abs-6-Z-6-Überhang gekennzeichneten
Regel-Zeilen (tarifisch im Zuflussmonat, ohne Sechstel-Effekt)."""
self.ensure_one()
total = 0.0
for rule in self.struct_id.rule_ids:
if not rule.l10n_at_s67_abs6_ueberhang:
continue
if result_rules is not None:
if rule.code in result_rules:
total += result_rules[rule.code]['total']
else:
total += sum(self.line_ids.filtered(
lambda l, c=rule.code: l.code == c).mapped('total'))
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