27654211fb
Buchungsmatrix nach dem Oesterreichischen Einheitskontenrahmen (KFS/BW 6; Nutzer-Referenz-Exemplar .additional_docs/KFSBW6_13072017_RF.pdf): Hook _configure_payroll_account_at (models/account_chart_private.py) ordnet Regelkonten, Journal SLR (Default = Gehaelterkonto 6200, BE-Muster) und das NET-Konto 3850 "Verbindlichkeiten aus Lohn und Gehalt" (Nutzer-Entscheidung 5.1; Anlage + reconcile, l10n_at-Standardchart hat es nicht) je Company zu — getriggert beim Chart-Load (_post_load_data) UND ueber die <function>-Datendatei data/hr_payroll_account_private.xml fuer Bestands-Companies (erster <value> einer function ist die Record-ID-Liste, convert.py-Semantik), idempotent. Konto-Suche exakt zuerst, Praefix-Fallback nur bis eine Stelle laenger — verhindert Matches auf Odoo-Scaffold-Konten chartloser Firmen (6000 darf nicht 600000 "Expenses" matchen). EKR-Debit-Split Arbeiter/Angestellte (Nutzer-Klaerung: 600-619 Loehne, 620-639 Gehaelter — "6000 Gehaelter/6200 Loehne" war gegen die eigene KFS/BW-6-Referenz vertauscht): l10n_at_personalgruppe am Vertrag (models/hr_version_private.py) + Override-Feld l10n_at_account_debit_arbeiter je Regel; Option A minimal (Nutzer-Entscheidung 5.2: Grundentgelt 6000, Sozialaufwand 6500, Abgaben 6600/6601/6602; Zuschlaege/UeStd/Praemien/SZ und BV (EKR 644 ohne Split) buchen fuer Arbeiter bis zur Referenzbuchhaltungs-Fixierung auf die Angestellten-Konten — fehlende Konten warnen, keine Erfindung). models/account_posting_private.py: Gesamtueberschreibung von _prepare_slip_lines nach Upstream-Muster (Odoo-Pin 19.0+e.20260910) mit einzeiliger Kontosubstitution — Odoo bietet dafuer keinen Override-Punkt; Verhalten testgepinnt. Matrix: KVGEO/URL_UE 6200/6000, Zuschlaege 6225, UeStd 6310, Praemien 6230, B13/B14 6240, DN-SV/LSt auf 3600/3540, NET 3850, SVDG-Block 6560/6500 auf 3600, BVG 6440 (Fallback 6400) auf Credit 3600 (Abfuhr via KV-Traeger, Nutzer 5.3), FLAFDB 6661/6601 auf 3541, KOMMST 6660/6600 auf 3550, DZ 6662/6602 auf 3542. NET-Umbenennung NETTO -> NET (Odoo-Konvention: action_register_payment, net_wage und Negativ-Netto-Erkennung filtern code == NET) inkl. 4 Bestands-Suiten. Odoo-19-Fund: das Anlegen einer Structure kopiert die Regeln des hr_payroll.default_structure (BASIC/GROSS/NET/...) via Feld-Default in rule_ids — die kopierte Default-NET-Regel kollidierte im result_rules-Dict (Netto fiel auf 0); Fix: <function> in salary_rules_private.xml archiviert die Kopie (active=False; KEIN Unlink — struct_id ist NOT NULL und historische Lohnkontolinien behalten ihre Regel-Referenz; One2many blendet Archivierte aus). Journal-Default-Konten duerfen nicht Receivable/Payable sein (Odoo-Constraint); Rundungscents bucht Odoo als Adjustment Entry aufs Journal-Default. depends: + l10n_at, hr_payroll_account_iso20022 (SEPA-Auszahlung, Plan 4.5; kein VRV/MVAG — gem360-Bruecke). Tests: test_account_private (4: RF-BUCH-1..4 — Kontenplan-Mock ohne Chart-Install, Move-Balance, Kontenzeilen je Matrix, Arbeiter-Split, Geringfuegiger, Fallback/Warnung) — gesamt 88/88 gruen auf Wegwerf-DB (frischer -i-Lauf). Rechenfaelle docs/rechenfaelle/buchhaltung.md. 19.0.6.0.0.
438 lines
20 KiB
Python
438 lines
20 KiB
Python
# Part of the odoo-at-payroll project. License: LGPL-3.
|
||
from datetime import date, timedelta
|
||
|
||
from odoo import api, fields, models
|
||
|
||
|
||
class HrWorkEntryType(models.Model):
|
||
_inherit = 'hr.work.entry.type'
|
||
|
||
l10n_at_nacht_sv = fields.Boolean(
|
||
string='AT Nacht-Sondervergütung (22–6)',
|
||
help='Stunden dieses Typs fallen ins KV-Nachtfenster 22–6 Uhr '
|
||
'und gebühren der Nacht-Sondervergütung nach KV § 6 '
|
||
'(zuschlagsunabhängig, kumulativ).')
|
||
|
||
|
||
class HrPayrollStructure(models.Model):
|
||
_inherit = 'hr.payroll.structure'
|
||
|
||
def _l10n_at_entferne_default_net_kopie(self):
|
||
"""Odoo kopiert beim Anlegen einer Structure die Regeln des
|
||
``hr_payroll.default_structure`` (BASIC/GROSS/NET/ASSIG_
|
||
SALARY/…) in ``rule_ids`` (Feld-Default ``_get_default_
|
||
rule_ids``). Die kopierte NET-Regel kollidiert ab GP6 mit
|
||
unserer Netto-Regel (Code ``NET`` nach Odoo-Konvention —
|
||
Zahlungsregistrierung, net_wage und Negativ-Netto-Erkennung
|
||
filtern ``code == 'NET'``): zwei Regeln mit Code NET in einer
|
||
Structure überschreiben sich im ``result_rules``-Dict und die
|
||
Netto-Zeile fällt auf 0. Die Kopie wird daher archiviert —
|
||
One2many-Liesungen (rule_ids, Compute-Schleife, NET-Filter)
|
||
blenden Archivierte aus, historische Lohnkontolinien behalten
|
||
aber ihre Regel-Referenz (struct_id ist NOT NULL, ein Abhängen
|
||
ist nicht möglich). Idempotent (Aufruf via <function> in
|
||
salary_rules_private.xml bei Install und Update)."""
|
||
self.ensure_one()
|
||
unsere = self.env.ref(
|
||
'l10n_at_hr_payroll_private.rule_netto', False)
|
||
kopien = self.rule_ids.filtered(
|
||
lambda r: r.code == 'NET' and (not unsere or r != unsere))
|
||
kopien.write({'active': False})
|
||
|
||
|
||
class HrVersionPrivate(models.Model):
|
||
_inherit = 'hr.version'
|
||
|
||
# All-in / Überstundenpauschale § 2g AVRAG (Deckungsrechnung GP2):
|
||
# zwingender Grundlohnausweis im Dienstzettel; Pauschale gilt bei
|
||
# Unterdeckung nur als Akontozahlung (Deckungsprüfbericht).
|
||
l10n_at_ue_pauschale_monatlich = fields.Monetary(
|
||
string='ÜStd-Pauschale/Monat (All-in § 2g AVRAG)',
|
||
currency_field='currency_id',
|
||
help='Monatliche Überstundenpauschale lt. Arbeitsvertrag; '
|
||
'0 = kein All-in.')
|
||
l10n_at_grundlohn_ausweis = fields.Monetary(
|
||
string='Grundlohn lt. Dienstzettel (§ 2g AVRAG)',
|
||
currency_field='currency_id',
|
||
help='Zwingend ausgewiesener Grundlohn (ohne ÜStd-Anteil) im '
|
||
'Arbeitsvertrag/Dienstzettel.')
|
||
|
||
|
||
class HrPayslip(models.Model):
|
||
_inherit = 'hr.payslip'
|
||
|
||
# ------------------------------------------------------------------
|
||
# Arbeitstage (resource_calendar) — § 17 Abs 2 KV, user convention:
|
||
# Werktage des KV werden als Arbeitstage aus dem Ressourcenkalender
|
||
# gelesen (Mo–Fr-Kalender: Faktor = AT Dienstspanne / AT Zeitraum;
|
||
# für Mo–Sa-Kalender identisch mit ÷26 × Werktage).
|
||
# ------------------------------------------------------------------
|
||
def _at_arbeitstage(self, von, bis):
|
||
"""Arbeitstage (Kalender-Arbeitstage) im Intervall [von, bis]."""
|
||
self.ensure_one()
|
||
calendar = self.version_id.resource_calendar_id
|
||
if not calendar or not von or not bis or von > bis:
|
||
return 0
|
||
days = {
|
||
int(att.dayofweek)
|
||
for att in calendar.attendance_ids
|
||
}
|
||
if not days:
|
||
return 0
|
||
count = 0
|
||
tag = von
|
||
while tag <= bis:
|
||
if tag.weekday() in days:
|
||
count += 1
|
||
tag += timedelta(days=1)
|
||
return count
|
||
|
||
def _at_arbeitstage_faktor(self):
|
||
"""Prorate-Faktor Ein-/Austritt: Arbeitstage der Beschäftigungs-
|
||
spanne (contract_date_*) im Abrechnungszeitraum ÷ Arbeitstage des
|
||
vollen Zeitraums."""
|
||
self.ensure_one()
|
||
version = self.version_id
|
||
von = max(self.date_from, version.contract_date_start) \
|
||
if version.contract_date_start else self.date_from
|
||
bis = min(self.date_to, version.contract_date_end) \
|
||
if version.contract_date_end else self.date_to
|
||
if von > bis:
|
||
return 0.0
|
||
total = self._at_arbeitstage(self.date_from, self.date_to)
|
||
if not total:
|
||
return 1.0
|
||
return self._at_arbeitstage(von, bis) / float(total)
|
||
|
||
# ------------------------------------------------------------------
|
||
# KV-Basis / Grundstunden (Teilungsfaktoren § 5 Abs 6 KV)
|
||
# ------------------------------------------------------------------
|
||
def _at_kv_gehalt_basis(self, datum=None):
|
||
"""Volles Monatsgehalt: Überzahlung überschreibt Katalog;
|
||
Teilzeit über work_time_rate (Enterprise-Fracht: Bruchteil 0..1,
|
||
compute aus Versionskalender ÷ Firmenzalender — D4-verifiziert)."""
|
||
self.ensure_one()
|
||
version = self.version_id
|
||
datum = datum or self.date_to
|
||
if version.ueberzahlung:
|
||
gehalt = version.ueberzahlung
|
||
else:
|
||
pos = self.employee_id.kv_gruppe_id._get_gehalt(
|
||
self.employee_id.erfahrungsstufe, datum)
|
||
gehalt = pos.gehalt if pos else 0.0
|
||
return gehalt * (version.work_time_rate or 1.0)
|
||
|
||
def _at_grundstunde_ue(self):
|
||
"""ÜStd-Grundvergütung (und Zuschlagsbasis) = Monatsgehalt ÷
|
||
Teilungsfaktor ÜStd (KV § 5 Abs 6: 2203 ÷ 150, 2748 ÷ 143)."""
|
||
self.ensure_one()
|
||
teiler = self.employee_id.kv_id._wert(
|
||
'teilungsfaktor_ue', self.date_to, raise_if_not_found=False)
|
||
if not teiler:
|
||
return 0.0
|
||
return self._at_kv_gehalt_basis() / float(teiler)
|
||
|
||
def _at_grundstunde_norm(self):
|
||
"""Normalarbeitsstunde/Mehrarbeit = Monatsgehalt ÷
|
||
Teilungsfaktor Normalstunde (2748: ÷ 167) bzw. Fallback
|
||
÷ (4,33 × KV-Wochenstunden) ohne KV-Teilungsfaktor."""
|
||
self.ensure_one()
|
||
kv = self.employee_id.kv_id
|
||
teiler = kv._wert(
|
||
'teilungsfaktor_norm', self.date_to, raise_if_not_found=False)
|
||
basis = self._at_kv_gehalt_basis()
|
||
if teiler:
|
||
return basis / float(teiler)
|
||
naz = kv._wert('naz', self.date_to, raise_if_not_found=False) or 40
|
||
return basis / (4.33 * float(naz))
|
||
|
||
def _at_nacht_sv_stunden(self):
|
||
"""Stunden der Nacht-Sondervergütung: Work-Entry-Types mit
|
||
Nacht-Flag (22–6-Fenster), unabhängig vom Zuschlagsstatus."""
|
||
self.ensure_one()
|
||
return sum(
|
||
(wd.number_of_hours or 0.0)
|
||
for wd in self.worked_days_line_ids
|
||
if wd.work_entry_type_id.l10n_at_nacht_sv)
|
||
|
||
# ------------------------------------------------------------------
|
||
# 13./14. Bezug (§ 11 KV) — Basis, Fälligkeit, Aliquotierung
|
||
# ------------------------------------------------------------------
|
||
def _at_sz_basis_datum(self, code):
|
||
"""Stichtag der B13-/B14-Basis: 'november' → 30. 11. des
|
||
Jahres (2748 B13), sonst Auszahlungsmonat = payslip-Stichtag."""
|
||
self.ensure_one()
|
||
methode = self.employee_id.kv_id._wert(
|
||
code, self.date_to, raise_if_not_found=False) or 'auszahlung'
|
||
if methode == 'november':
|
||
return date(self.date_to.year, 11, 30)
|
||
return self.date_to
|
||
|
||
def _at_jahres_versionen(self):
|
||
"""Versionen des Mitarbeiters mit Überlappung im Kalenderjahr
|
||
der Abrechnung (Mischberechnung Beschäftigungsausmaß, 2203
|
||
§ 11 Abs 2)."""
|
||
self.ensure_one()
|
||
jahr = self.date_to.year
|
||
von, bis = date(jahr, 1, 1), date(jahr, 12, 31)
|
||
versionen = []
|
||
for version in self.env['hr.version'].search([
|
||
('employee_id', '=', self.employee_id.id),
|
||
]):
|
||
v_von = version.date_start or von
|
||
v_bis = version.date_end or bis
|
||
if max(v_von, von) <= min(v_bis, bis):
|
||
versionen.append((version, max(v_von, von), min(v_bis, bis)))
|
||
return versionen
|
||
|
||
def _at_sz_basis(self, datum):
|
||
"""B13-/B14-Basis zum Stichtag: einheitliches Beschäftigungs-
|
||
ausmaß = aktuelles Monatsgehalt; Änderungen des Ausmaßes im
|
||
Kalenderjahr → Kalendertag-gewichtete Mischung über die
|
||
Jahres-Versionen (Konvention GP2, s. docs/rechenfaelle/b13b14.md)."""
|
||
self.ensure_one()
|
||
versionen = self._at_jahres_versionen()
|
||
if len(versionen) <= 1:
|
||
return self._at_kv_gehalt_basis(datum)
|
||
if len({(v.work_time_rate or 100.0) for v, _, _ in versionen}) == 1:
|
||
return self._at_kv_gehalt_basis(datum)
|
||
# Mischberechnung: Kalendertag-Anteile je Version im Jahr
|
||
jahr_von, jahr_bis = date(self.date_to.year, 1, 1), date(self.date_to.year, 12, 31)
|
||
tage_jahr = (jahr_bis - jahr_von).days + 1
|
||
summe_anteile, summe_gehaelter = 0.0, 0.0
|
||
for version, v_von, v_bis in versionen:
|
||
anteil = ((v_bis - v_von).days + 1) / float(tage_jahr)
|
||
if version.ueberzahlung:
|
||
gehalt = version.ueberzahlung
|
||
else:
|
||
pos = version.kv_gruppe_id._get_gehalt(
|
||
version.erfahrungsstufe, v_bis)
|
||
gehalt = pos.gehalt if pos else 0.0
|
||
summe_anteile += anteil
|
||
summe_gehaelter += gehalt * anteil \
|
||
* (version.work_time_rate or 1.0)
|
||
if not summe_anteile:
|
||
return 0.0
|
||
return summe_gehaelter / summe_anteile
|
||
|
||
def _at_sz_dienstzeit_faktor(self):
|
||
"""Aliquote Ein-/Austritt (§ 11 Abs 4 KV): Methode je
|
||
`sz_aliquote_methode` — 'kalendertage' (Default, user decision:
|
||
Gehalt/365 × Kalendertage Dienstzeit) oder 'monate' (volle
|
||
Monate ÷ 12, anteilige Monate taggenau)."""
|
||
self.ensure_one()
|
||
jahr = self.date_to.year
|
||
jahr_von, jahr_bis = date(jahr, 1, 1), date(jahr, 12, 31)
|
||
methode = self.employee_id.kv_id._wert(
|
||
'sz_aliquote_methode', self.date_to,
|
||
raise_if_not_found=False) or 'kalendertage'
|
||
kt_gesamt, monate_gesamt = 0, 0.0
|
||
for version, v_von, v_bis in self._at_jahres_versionen():
|
||
kt_gesamt += (v_bis - v_von).days + 1
|
||
tag = date(jahr, 1, 1)
|
||
while tag <= date(jahr, 12, 31):
|
||
if v_von <= tag <= v_bis:
|
||
monate_gesamt += 1.0 / ((tag.replace(day=28) + timedelta(days=4)).day)
|
||
tag += timedelta(days=1)
|
||
if methode == 'monate':
|
||
return min(monate_gesamt / 12.0, 1.0)
|
||
return min(kt_gesamt / 365.0, 1.0)
|
||
|
||
def _at_sz_bereits_bezahlt(self, code):
|
||
"""Summe bereits bezahlter B13-/B14-Linien im Kalenderjahr
|
||
(andere, bestätigte Läufe)."""
|
||
self.ensure_one()
|
||
jahr = self.date_to.year
|
||
linien = self.env['hr.payslip.line'].search([
|
||
('employee_id', '=', self.employee_id.id),
|
||
('code', '=', code),
|
||
('date_from', '>=', date(jahr, 1, 1)),
|
||
('date_from', '<=', date(jahr, 12, 31)),
|
||
])
|
||
linien = linien.filtered(
|
||
lambda l: l.slip_id.state not in ('draft', 'cancel')
|
||
and l.slip_id.id != self.id)
|
||
return sum(linien.mapped('total'))
|
||
|
||
def _at_urlaubstage_jahr(self):
|
||
"""Urlaubs-Work-Entries (is_leave) des Kalenderjahres."""
|
||
self.ensure_one()
|
||
jahr = self.date_to.year
|
||
eintraege = self.env['hr.work.entry'].search([
|
||
('version_id', 'in', self.employee_id.version_ids.ids),
|
||
('work_entry_type_id.is_leave', '=', True),
|
||
])
|
||
return sum(
|
||
1 for we in eintraege
|
||
if we.date.year == jahr)
|
||
|
||
def _at_b14_faellig(self):
|
||
"""B14-Fälligkeit § 11 Abs 3 KV: 'urlaub' → bei Urlaubsantritt
|
||
(längster Teil — R1: Auszahlung im erstfälligen Urlaubsmonat,
|
||
sofern nicht bereits bezahlt); ohne angetretenen Urlaub im
|
||
Dezember (`sz14_no_urlaub_monat`); bei Austritt aliquote
|
||
Endabrechnung. Fallback ohne KV-Code: Juni-Abrechnung
|
||
(user decision)."""
|
||
self.ensure_one()
|
||
kv = self.employee_id.kv_id
|
||
faelligkeit = kv._wert(
|
||
'sz14_faelligkeit', self.date_to, raise_if_not_found=False)
|
||
if faelligkeit != 'urlaub':
|
||
return self.date_to.month == 6 # Fallback Juni-Abrechnung
|
||
if self._at_sz_bereits_bezahlt('B14'):
|
||
return False
|
||
austritt = (
|
||
self.version_id.contract_date_end
|
||
and self.date_from <= self.version_id.contract_date_end <= self.date_to)
|
||
urlaub_im_zeitraum = any(
|
||
wd.work_entry_type_id.is_leave
|
||
for wd in self.worked_days_line_ids)
|
||
if urlaub_im_zeitraum or austritt:
|
||
return True
|
||
no_urlaub_monat = int(kv._wert(
|
||
'sz14_no_urlaub_monat', self.date_to,
|
||
raise_if_not_found=False) or 12)
|
||
return (self.date_to.month == no_urlaub_monat
|
||
and not self._at_urlaubstage_jahr())
|
||
|
||
def _at_b13_faellig(self):
|
||
"""B13-Fälligkeit: Auszahlmonat je KV (`sz13_monat`) bzw.
|
||
bei Austritt aliquote Endabrechnung (§ 11 Abs 4 KV)."""
|
||
self.ensure_one()
|
||
if self._at_sz_bereits_bezahlt('B13'):
|
||
return False
|
||
monat = int(self.employee_id.kv_id._wert(
|
||
'sz13_monat', self.date_to, raise_if_not_found=False) or 12)
|
||
austritt = (
|
||
self.version_id.contract_date_end
|
||
and self.date_from <= self.version_id.contract_date_end <= self.date_to)
|
||
return self.date_to.month == monat or austritt
|
||
|
||
# ------------------------------------------------------------------
|
||
# Urlaubsentgelt — regelmäßige Überstunden (§ 2 Abs 2 GKV;
|
||
# 7-von-12-Regel: KV § 5 Abs 11 (2203) / Abs 10 (2748))
|
||
# ------------------------------------------------------------------
|
||
def _at_urlaub_ue_avg(self):
|
||
"""(regelmaessig, durchschnittlicher monatlicher ÜStd-Betrag
|
||
der letzten 12 Monate) — nur ÜStd-Regeln, ohne Mehrarbeit.
|
||
Achtung: Lohnzettel-Lines tragen Regel-Codes (UE_*), nicht
|
||
WET-Codes (ATP_*)."""
|
||
self.ensure_one()
|
||
codes = ('UE_STD', 'UE_NACHT', 'UE_NACHT_FRUEH',
|
||
'UE_SOFEI', 'UE_SOFEI_NACHT')
|
||
von = fields.Date.subtract(self.date_from, months=12)
|
||
linien = self.env['hr.payslip.line'].search([
|
||
('employee_id', '=', self.employee_id.id),
|
||
('code', 'in', list(codes)),
|
||
('date_from', '>=', von),
|
||
]).filtered(lambda l: l.slip_id.state not in ('draft', 'cancel'))
|
||
if not linien:
|
||
return False, 0.0
|
||
monate = {l.slip_id.id for l in linien}
|
||
return len(monate) >= 7, sum(l.total for l in linien) / 12.0
|
||
|
||
# ------------------------------------------------------------------
|
||
# Lohnnebenkosten FLAF-DG-Beitrag / KommSt / DZ (GP4; § 41 FLAG,
|
||
# § 5+§ 9 KommStG, §§ 122/126 WKG — Werte/Staffeln im Kern)
|
||
# ------------------------------------------------------------------
|
||
def _at_lnk_basis(self, steuer, result_rules=None):
|
||
"""LNK-Bemessungsgrundlage nach Freibetragsstaffel: monatlich
|
||
gezahlte Arbeitslöhne (laufend + im Monat gezahlte §-67-Bezüge,
|
||
Zuflussprinzip; kein SV-Abzug), ohne §-67-Abs-3/6-Bezüge
|
||
(Abfertigung/Abfindung). 'flaf'/'dz' zusätzlich ohne lit-f-AN
|
||
(Arbeitslöhne ab Folgemonat nach Vollendung des 60. Lebensjahres,
|
||
§ 41 Abs 4 lit f FLAG — die Kommunalsteuer hat KEINE lit-f-
|
||
Befreiung). Freibetrag: ≤ 1.095 € keine Abgabe, bis 1.460 € →
|
||
Freibetrag 1.095 €, darüber volle BG (at_lnk_freibetrag)."""
|
||
self.ensure_one()
|
||
basis = (self._l10n_at_brutto('laufend', result_rules)
|
||
+ self._l10n_at_brutto('sonstig', result_rules))
|
||
for rule in self.struct_id.rule_ids:
|
||
if rule.l10n_at_s67_abs in ('abs3', 'abs6'):
|
||
if result_rules is not None and rule.code in result_rules:
|
||
basis -= result_rules[rule.code]['total']
|
||
elif result_rules is None:
|
||
linie = self.line_ids.filtered(
|
||
lambda l: l.code == rule.code)
|
||
basis -= sum(linie.mapped('total'))
|
||
basis = max(basis, 0.0)
|
||
if steuer in ('flaf', 'dz'):
|
||
version = self.version_id
|
||
if (version.l10n_at_lnkp_frei_ab
|
||
and self.date_from >= version.l10n_at_lnkp_frei_ab):
|
||
return 0.0
|
||
freibetrag = self.env['hr.rule.parameter']._get_parameter_from_code(
|
||
'at_lnk_freibetrag', self.date_to, raise_if_not_found=False) or {}
|
||
grenze = float(freibetrag.get('grenze', 0.0))
|
||
if basis <= 0.0:
|
||
return 0.0
|
||
if basis <= grenze:
|
||
basis -= float(freibetrag.get('freibetrag', 0.0))
|
||
return max(basis, 0.0)
|
||
|
||
def _at_flaf_db(self, result_rules=None):
|
||
"""FLAF-Dienstgeberbeitrag § 41 FLAG (3,7 % ab 2025, 2,7 % ab
|
||
2028 — at_flaf_db; BG s. _at_lnk_basis)."""
|
||
self.ensure_one()
|
||
basis = self._at_lnk_basis('flaf', result_rules)
|
||
satz = float(self.env['hr.rule.parameter']._get_parameter_from_code(
|
||
'at_flaf_db', self.date_to, raise_if_not_found=False) or 0.0)
|
||
return basis * satz / 100.0
|
||
|
||
def _at_kommst(self, result_rules=None):
|
||
"""Kommunalsteuer § 9 KommStG 1993 (3 % — at_kommst_satz; BG
|
||
§ 5 Abs 2: ohne §-67-Abs-3/6-Bezüge, ohne lit-f-Befreiung)."""
|
||
self.ensure_one()
|
||
basis = self._at_lnk_basis('kommst', result_rules)
|
||
satz = float(self.env['hr.rule.parameter']._get_parameter_from_code(
|
||
'at_kommst_satz', self.date_to, raise_if_not_found=False) or 0.0)
|
||
return basis * satz / 100.0
|
||
|
||
def _at_dz(self, result_rules=None):
|
||
"""Zuschlag zum Dienstgeberbeitrag (§§ 122/126 WKG) für WKO-
|
||
Mitgliedsbetriebe: Landeskammersatz je Betriebsstätten-Bundesland
|
||
inkl. Bundeskammeranteil (at_dz_vektor; ⚠ WKO-Verifikation 8.11);
|
||
BG identisch FLAF-DB inkl. lit f (lb-lnk-07)."""
|
||
self.ensure_one()
|
||
if not self.company_id.l10n_at_wk_mitglied:
|
||
return 0.0
|
||
basis = self._at_lnk_basis('dz', result_rules)
|
||
vektor = self.env['hr.rule.parameter']._get_parameter_from_code(
|
||
'at_dz_vektor', self.date_to, raise_if_not_found=False) or {}
|
||
satz = float((vektor.get('laender') or {}).get(
|
||
self.company_id.l10n_at_bundesland or '', 0.0))
|
||
return basis * satz / 100.0
|
||
|
||
# ------------------------------------------------------------------
|
||
# All-in-Deckungsrechnung (§ 2g AVRAG) — minimale GP2-Umsetzung:
|
||
# Jahresvergleich geschuldete ÜStd-Vergütung vs. erhaltene Pauschale
|
||
# ------------------------------------------------------------------
|
||
def _at_allin_deckung(self):
|
||
"""Deckungsrechnung für Verträge mit ÜStd-Pauschale:
|
||
{'pauschale': x, 'geschuldet': y, 'unterdeckung': z}."""
|
||
self.ensure_one()
|
||
version = self.version_id
|
||
pauschale = version.l10n_at_ue_pauschale_monatlich or 0.0
|
||
if not pauschale:
|
||
return {'pauschale': 0.0, 'geschuldet': 0.0, 'unterdeckung': 0.0}
|
||
jahr = self.date_to.year
|
||
codes = ('UE_STD', 'UE_NACHT', 'UE_NACHT_FRUEH',
|
||
'UE_SOFEI', 'UE_SOFEI_NACHT')
|
||
linien = self.env['hr.payslip.line'].search([
|
||
('employee_id', '=', self.employee_id.id),
|
||
('code', 'in', list(codes)),
|
||
('date_from', '>=', date(jahr, 1, 1)),
|
||
('date_from', '<=', date(jahr, 12, 31)),
|
||
]).filtered(lambda l: l.slip_id.state not in ('draft', 'cancel'))
|
||
geschuldet = sum(linien.mapped('total'))
|
||
monate = 12 \
|
||
if not version.contract_date_end \
|
||
or version.contract_date_end.year > jahr \
|
||
else version.contract_date_end.month
|
||
erhaltene = pauschale * monate
|
||
unterdeckung = max(geschuldet - erhaltene, 0.0)
|
||
return {
|
||
'pauschale': erhaltene,
|
||
'geschuldet': geschuldet,
|
||
'unterdeckung': unterdeckung,
|
||
} |