mirror of
http://100.103.83.12:3003/fegger/odoo-at-payroll.git
synced 2026-09-17 08:52:45 +00:00
[IMP] l10n_at_hr_payroll_private: Wr. DAG, Buchungsbeleg, VWGJ-Einstufung, EFZG-Teilentgelt mit fiktiver BV (AP2d+AP3+AP9+AP4)
AP3 — Wiener Dienstgeberabgabe (U-Bahn-Steuer, RIS LrW GesNr 20000133 verifiziert): Regel WIEN_DAG (Kopfpauschale 2 Euro je DN und angefangene Woche, Wochenlogik Montag-im-Monat, Befreiungen Lehrling/55+/<=10 WStd/Karenz-Woche), Company-Flag-Verwertung, Parameter at_wien_dag_satz, FI 6663/3560 (Referenz-Rest) AP2d — Buchungsbeleg-Report am Lohnlauf (Konto/Belegtext/Soll/Haben der FI-Buchung, balancierte Summen; test_account_private-Erweiterung) AP9 — Vordienstzeiten -> Einstufung: Stufen-Schwelle vwgj_ab an l10n.at.payroll.kv.gruppe.stufe (aus Labels 'nach N VWGJ'; -1 = nicht gepflegt, ORM liest Integer-NULL als 0), CSV-Seed, _get_stufe_fuer_vwgj/_get_naechste_vwgj, Vertragsfeld l10n_at_vordienstzeit_monate + VWGJ-Vorschlag + Vorrueckungstermin (S 17 Abs 5 Rahmen-KV, Erster des Erreichungsmonats) + Uebernahme- Button (manueller Verwaltungsakt) AP4 — EFZG/Teilentgelt (AngG S 8/EFZG S 2 verifiziert, Gleichlauf 6/8/10/12+4 Wochen; lb-krs-07/08): EFZ-Phasenmaschine models/efzg.py (Kalendertage-Kontingente je Arbeitsjahr, Krank-/AU-Topf getrennt, AU-Urlaubsart ATP_AU), Regeln TEIL_ENTGELT_HALB/-50 % und KRANKENTGELT_PHASE/-100 % je Arbeitstag (Tagesatz aus unreduzierter Basis), fiktive BV-Grundlage S 7 Abs 3 BMSVG im _at_bv_basis_laufen- Hook (50 % Vormonatsentgelt x Bezugs-KT/30), Parameter at_efz_stufen; Krankengeldumlage S 62 ASVG = Falsch-Praemise (kein Baustein); Rechenfaelle docs/rechenfaelle/efzg.md
This commit is contained in:
@@ -9,4 +9,7 @@ from . import account_chart_private
|
||||
from . import account_posting_private
|
||||
from . import mbgm
|
||||
from . import versichertenmeldung
|
||||
from . import fristen_private
|
||||
from . import fristen_private
|
||||
from . import buchungsbeleg
|
||||
from . import wien_dag
|
||||
from . import efzg
|
||||
@@ -213,13 +213,18 @@ class HrPayslip(models.Model):
|
||||
return 0.0
|
||||
basis = self._l10n_at_sv_basis_roh(result_rules)
|
||||
if pflichtig_ab is None or pflichtig_ab <= self.date_from:
|
||||
return basis
|
||||
# AP4: fiktive BG während des Krankengeldbezugs (§ 7 Abs 3
|
||||
# BMSVG) — 50 % des Vormonatsentgelts aliquot nach
|
||||
# Kalendertagen der Halb-/ÖGK-Phase; nur in beitrags-
|
||||
# pflichtigen Läufen (Erstfenster-Teilmonate: dokumentierter
|
||||
# Randfall).
|
||||
return basis + self._at_bv_fiktiv_krankengeld()
|
||||
if pflichtig_ab > self.date_to:
|
||||
# LZP liegt zur Gänze im beitragsfreien Erstfenster.
|
||||
return 0.0
|
||||
ende = min(self.date_to,
|
||||
tage = (min(self.date_to,
|
||||
self.version_id.contract_date_end or self.date_to)
|
||||
tage = (ende - pflichtig_ab).days + 1
|
||||
- pflichtig_ab).days + 1
|
||||
if tage <= 0:
|
||||
return 0.0
|
||||
return basis * tage / max(self._l10n_at_lohnsteuertage(), 1)
|
||||
|
||||
@@ -103,6 +103,10 @@ L10N_AT_BUCHUNGSMATRIX = {
|
||||
'credit': ('3550',)},
|
||||
'rule_dz': {'debit': ('6662',), 'debit_arbeiter': ('6602',),
|
||||
'credit': ('3542',)},
|
||||
# Wr. DAG („U-Bahn-Steuer“): Aufwand 6663 (l10n_at-Template,
|
||||
# KB lb-lnk-04 verifiziert); Credit 3560 Referenzbuchhaltungs-
|
||||
# Rest (Stadtkasse Wien — Kandidat, fehlend-Warnung des Hooks).
|
||||
'rule_wien_dag': {'debit': ('6663',), 'credit': ('3560',)},
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
# Part of the odoo-at-payroll project. License: LGPL-3.
|
||||
"""Buchungsbeleg der Lohnbuchhaltung (AP2d): Druck der FI-Buchung
|
||||
eines Lohnlaufs — die account.move je Lohnlauf (bzw. je Payslip, wenn
|
||||
nicht gebatcht bucht wird) mit Konto, Belegtext, Soll/Haben und
|
||||
Summen; Kontrollgrößen stimmen mit dem Monatsjournal überein
|
||||
(GP6-Matrix, KFS/BW 6)."""
|
||||
from odoo import models
|
||||
|
||||
|
||||
class L10nAtPayrollBuchungsbeleg(models.AbstractModel):
|
||||
_name = 'report.l10n_at_hr_payroll_private.report_buchungsbeleg'
|
||||
_description = 'Buchungsbeleg Lohnlauf (AP2d)'
|
||||
|
||||
def _get_report_values(self, docids, data=None):
|
||||
runs = self.env['hr.payslip.run'].browse(docids)
|
||||
laeufe = []
|
||||
for run in runs:
|
||||
# Batch-Buchung hängt am Lauf, sonst je Payslip-Move
|
||||
# (hr_payroll_account schreibt move_id je Slip bzw. je Run).
|
||||
moves = (run.move_id + run.slip_ids.mapped('move_id')) \
|
||||
.filtered(lambda m: m.state != 'cancel')
|
||||
zeilen = moves.line_ids.filtered(
|
||||
lambda l: l.debit or l.credit).sorted(
|
||||
key=lambda l: (l.account_id.code or '', l.id))
|
||||
laeufe.append({
|
||||
'run': run,
|
||||
'moves': moves,
|
||||
'zeilen': zeilen,
|
||||
'summe_soll': sum(zeilen.mapped('debit')),
|
||||
'summe_haben': sum(zeilen.mapped('credit')),
|
||||
})
|
||||
return {'docs': runs, 'laeufe': laeufe}
|
||||
@@ -0,0 +1,270 @@
|
||||
# Part of the odoo-at-payroll project. License: LGPL-3.
|
||||
"""EFZG/Teilentgelt (AP4) — Entgeltfortzahlung im Krankenstand.
|
||||
|
||||
Rechtsgrundlagen (verifiziert 2026-09-12):
|
||||
- **AngG § 8 Abs 1/2/2a** und **EFZG § 2 Abs 1/3, § 2 Abs 5**
|
||||
(Gleichlauf seit 1. 7. 2018, BGBl I 153/2017; EFZG RIS GesNr
|
||||
10008308, AngG lokal — Stufen 6/8/10/12 Wochen je vollendeten
|
||||
Dienstjahren <1 / 1-14 / 15-24 / ≥25, zuzüglich je 4 Wochen halbes
|
||||
Entgelt; wiederholte Verhinderungen im selben Arbeitsjahr
|
||||
zusammengerechnet; AU/BK-Sonderkontingent 8/10 Wochen ohne
|
||||
Halbsatz, je Anlassfall, unabhängig vom Krankheits-Topf).
|
||||
- **lb-krs-08/02/07** (Wissensbasis, Layer 2): Arbeitsjahrprinzip,
|
||||
Kalendertage als Zähleinheit, Stufensprung im laufenden Stand
|
||||
verlängert; SV: volle EFZ beitragspflichtig, EFZ < 50 %
|
||||
beitragsfrei (hier: Halbsatz = 50 % → pflichtig); Lohnsteuer:
|
||||
laufender Bezug; **BMSVG § 7 Abs 3**: während des Krankengeldbezugs
|
||||
(Halb- und ÖGK-Phase) fiktive BG = 50 % des Entgelts des
|
||||
Kalendermonats vor dem Versicherungsfall, SZ ausgenommen.
|
||||
- Krankengeld selbst zahlt die ÖGK (läuft nicht durch den
|
||||
Dienstgeber-Payslip; lb-krs-02) — der Payroll-Anteil ist die
|
||||
Entgelt-Reduktion des Dienstgebers, die fiktive BV-Grundlage und
|
||||
später die Arbeits-/Entgeltbestätigung (AP8).
|
||||
- „Krankengeldumlage § 62 ASVG" ist eine Falsch-Prämise
|
||||
(RECHTSQUELLEN-Privat 2026-09-10) — kein Umlage-Baustein.
|
||||
|
||||
Zähleinheiten: Kontingente in **Kalendertagen** (lb-krs-08: Praxis),
|
||||
bezogen aus den validierten Abwesenheiten (hr.leave der AT-Urlaubsarten
|
||||
Krankenstand/Arbeitsunfall); die **Entgelt-Minderung** je
|
||||
Arbeitstag aus dem Arbeitseintragssatz (Tagesatz = laufende
|
||||
Monatsbasis ÷ Arbeitstage des Monats — Entgeltausfallsprinzip).
|
||||
Feiertage im Krankenstand werden vom Work-Entry-Modell automatisch
|
||||
nicht als Krankenstandstage erzeugt (Kalender) — die 100 %-
|
||||
Feiertagsentgelt-Regel lb-krs-08 bleibt damit gewahrt.
|
||||
"""
|
||||
from datetime import date, timedelta
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
# WET-Codes der Abwesenheits-Töpfe (AP5-Seed).
|
||||
_KRANK_WET = 'ATP_KRANK'
|
||||
_AU_WET = 'ATP_AU'
|
||||
|
||||
|
||||
class HrPayslip(models.Model):
|
||||
_inherit = 'hr.payslip'
|
||||
|
||||
l10n_at_efz_info = fields.Char(
|
||||
string='AT EFZ-Status (Krankenstand)', compute='_compute_efz_info',
|
||||
help='Phasenstatus des Krankenstands im Abrechnungszeitraum: '
|
||||
'volle/halbe Entgeltfortzahlung, ÖGK-Krankengeld-Phase, '
|
||||
'Kontingente und Stufe (AngG § 8 / EFZG § 2).')
|
||||
|
||||
def _compute_efz_info(self):
|
||||
for slip in self:
|
||||
efz = slip._at_efz_status()
|
||||
if not efz:
|
||||
slip.l10n_at_efz_info = ''
|
||||
continue
|
||||
slip.l10n_at_efz_info = (
|
||||
'Stufe %d (%d+%d Wo) · voll %d AT · halb %d AT · '
|
||||
'ÖGK %d AT' % (
|
||||
efz['stufe_wochen'], efz['stufe_wochen'],
|
||||
efz['halb_wochen'], len(efz['voll_arbeitstage']),
|
||||
len(efz['halb_arbeitstage']),
|
||||
len(efz['oegk_arbeitstage'])))
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# EFZ-Phasenbestimmung
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _at_efz_leaves(self):
|
||||
"""Validierte Krankenstand-/AU-Abwesenheiten des AN als
|
||||
(start, ende, ist_au)-Kalenderspannen."""
|
||||
self.ensure_one()
|
||||
typen = self.env['hr.leave.type'].search([
|
||||
('work_entry_type_id.code', 'in',
|
||||
(_KRANK_WET, _AU_WET))])
|
||||
leaves = self.env['hr.leave'].search([
|
||||
('employee_id', '=', self.employee_id.id),
|
||||
('holiday_status_id', 'in', typen.ids),
|
||||
('state', 'in', ('validate', 'validate1')),
|
||||
])
|
||||
return [
|
||||
(leave.request_date_from, leave.request_date_to,
|
||||
leave.holiday_status_id.work_entry_type_id.code == _AU_WET)
|
||||
for leave in leaves
|
||||
if leave.request_date_from and leave.request_date_to]
|
||||
|
||||
def _at_efz_arbeitsjahr_start(self):
|
||||
"""Beginn des Arbeitsjahres, das die Abrechnungsperiode
|
||||
überwiegend enthält (Eintrittsdatum-Rhythmus, lb-krs-08)."""
|
||||
self.ensure_one()
|
||||
beginn = self.version_id.contract_date_start
|
||||
if not beginn:
|
||||
return None
|
||||
jahre = beginn.year
|
||||
while True:
|
||||
aj_start = date(jahre, beginn.month, beginn.day) \
|
||||
if not (beginn.month == 2 and beginn.day == 29) \
|
||||
else date(jahre, 3, 1)
|
||||
aj_naechstes = date(jahre + 1, aj_start.month, aj_start.day)
|
||||
if aj_start <= self.date_from < aj_naechstes:
|
||||
return aj_start
|
||||
jahre += 1
|
||||
if jahre > beginn.year + 80:
|
||||
return None
|
||||
|
||||
def _at_efz_status(self):
|
||||
"""EFZ-Phasen je Abrechnungsperiode: {'stufe_wochen',
|
||||
'halb_wochen', 'voll_arbeitstage', 'halb_arbeitstage',
|
||||
'oegk_arbeitstage', 'oegk_kalendertage', 'episode_beginn',
|
||||
'info'} — None ohne Krankenstand/AU im Arbeitsjahr.
|
||||
|
||||
Kalendertage-Maschine: Die Krankheits- und AU-Tage des
|
||||
Arbeitsjahres werden in Kalenderreihenfolge den Phasen
|
||||
zugeteilt (voll bis Stufen-Kontingent, halb zuzüglich
|
||||
4 Wochen, danach ÖGK); AU ausschließlich voll (kein
|
||||
Halbsatz, § 8 Abs 2a AngG / § 2 Abs 5 EFZG). Ein
|
||||
Stufensprung im laufenden Stand verlängert das Kontingent
|
||||
(Stufe je Tag neu bewertet, lb-krs-08)."""
|
||||
self.ensure_one()
|
||||
version = self.version_id
|
||||
aj_start = self._at_efz_arbeitsjahr_start()
|
||||
if not aj_start:
|
||||
return None
|
||||
endtag = max(self.date_to, aj_start)
|
||||
krank_tage, au_tage = {}, {}
|
||||
episode_start = None
|
||||
for von, bis, ist_au in self._at_efz_leaves():
|
||||
tag = von
|
||||
while tag <= bis:
|
||||
if aj_start <= tag <= endtag:
|
||||
topf = au_tage if ist_au else krank_tage
|
||||
topf[tag] = True
|
||||
tag += timedelta(days=1)
|
||||
if not krank_tage and not au_tage:
|
||||
return None
|
||||
params = self.env['hr.rule.parameter']._get_parameter_from_code(
|
||||
'at_efz_stufen', self.date_from, raise_if_not_found=False) or {}
|
||||
stufen = sorted(
|
||||
(int(jahre), int(wochen))
|
||||
for jahre, wochen in params.get('stufen', []))
|
||||
halb_wochen = int(params.get('halb_wochen', 4))
|
||||
au_wochen = int(params.get('au_wochen', 8))
|
||||
au2_wochen = int(params.get('au2_wochen', 10))
|
||||
au_ab_jahre = int(params.get('au_ab_jahre', 15))
|
||||
|
||||
def stufe_fuer(dienstjahre, tag_ist_au):
|
||||
"""Anspruchs-Wochen am Stichtag (vollendete Jahre)."""
|
||||
wochen = stufen[0][1] if stufen else 6
|
||||
for jahre, stufe in stufen:
|
||||
if dienstjahre >= jahre:
|
||||
wochen = stufe
|
||||
if tag_ist_au:
|
||||
wochen = au2_wochen if dienstjahre >= au_ab_jahre \
|
||||
else au_wochen
|
||||
return wochen
|
||||
|
||||
beginn = version.contract_date_start
|
||||
phasen = {}
|
||||
verbraucht_krank_voll = verbraucht_krank_halb = 0
|
||||
verbraucht_au = 0
|
||||
for tag in sorted(set(krank_tage) | set(au_tage)):
|
||||
dienstjahre = (tag.year - beginn.year
|
||||
- ((tag.month, tag.day) < (beginn.month,
|
||||
beginn.day)))
|
||||
if tag in au_tage:
|
||||
if verbraucht_au < stufe_fuer(dienstjahre, True) * 7:
|
||||
phasen[tag] = 'voll'
|
||||
else:
|
||||
phasen[tag] = 'oegk'
|
||||
verbraucht_au += 1
|
||||
continue
|
||||
wochen = stufe_fuer(dienstjahre, False)
|
||||
if verbraucht_krank_voll < wochen * 7:
|
||||
phasen[tag] = 'voll'
|
||||
verbraucht_krank_voll += 1
|
||||
elif verbraucht_krank_halb < halb_wochen * 7:
|
||||
phasen[tag] = 'halb'
|
||||
verbraucht_krank_halb += 1
|
||||
else:
|
||||
phasen[tag] = 'oegk'
|
||||
# Arbeitstage je Phase im Abrechnungszeitraum (Work Entries)
|
||||
eintraege = self.env['hr.work.entry'].search([
|
||||
('employee_id', '=', self.employee_id.id),
|
||||
('date', '>=', self.date_from),
|
||||
('date', '<=', self.date_to),
|
||||
('state', '!=', 'cancelled'),
|
||||
('work_entry_type_id.code', 'in', (_KRANK_WET, _AU_WET)),
|
||||
])
|
||||
voll, halb, oegk = [], [], []
|
||||
for eintrag in eintraege:
|
||||
phase = phasen.get(eintrag.date)
|
||||
if phase == 'voll':
|
||||
voll.append(eintrag.date)
|
||||
elif phase == 'halb':
|
||||
halb.append(eintrag.date)
|
||||
elif phase == 'oegk':
|
||||
oegk.append(eintrag.date)
|
||||
# Episode-Beginn für die fiktive BV (§ 7 Abs 3 BMSVG):
|
||||
# frühester zusammenhängender Verhinderungsblock (Lücke = neue
|
||||
# Episode), dessen Phase die Halb-/ÖGK-Phase erreicht.
|
||||
episode_beginn = None
|
||||
for tag in sorted(phasen):
|
||||
if phasen[tag] in ('halb', 'oegk'):
|
||||
block = tag
|
||||
while (block - timedelta(days=1)) in phasen:
|
||||
block = block - timedelta(days=1)
|
||||
episode_beginn = block
|
||||
break
|
||||
dienstjahre = (self.date_from.year - beginn.year
|
||||
- ((self.date_from.month, self.date_from.day)
|
||||
< (beginn.month, beginn.day)))
|
||||
return {
|
||||
'stufe_wochen': stufe_fuer(dienstjahre, False),
|
||||
'halb_wochen': halb_wochen,
|
||||
'voll_arbeitstage': sorted(set(voll)),
|
||||
'halb_arbeitstage': sorted(set(halb)),
|
||||
'oegk_arbeitstage': sorted(set(oegk)),
|
||||
# Kalendertage der Phasen IM ABRECHNUNGSZEITRAUM — Basis
|
||||
# der fiktiven-BV-Aliquote (monatsscharf).
|
||||
'oegk_kalendertage': sorted(
|
||||
tag for tag in phasen
|
||||
if self.date_from <= tag <= self.date_to
|
||||
and phasen[tag] == 'oegk'),
|
||||
'halb_kalendertage': sorted(
|
||||
tag for tag in phasen
|
||||
if self.date_from <= tag <= self.date_to
|
||||
and phasen[tag] == 'halb'),
|
||||
'episode_beginn': episode_beginn,
|
||||
}
|
||||
|
||||
def _at_efz_tagesatz(self, result_rules=None):
|
||||
"""Voller Tages-Entgeltsatz des Monats (Entgeltausfalls-
|
||||
prinzip): laufende Monatsbasis ohne die eigenen
|
||||
Teilentgelt-Reduktionen ÷ Arbeitstage des Kalendermonats."""
|
||||
self.ensure_one()
|
||||
basis = self._l10n_at_sv_basis_roh(result_rules)
|
||||
for code in ('TEIL_ENTGELT_HALB', 'KRANKENTGELT_PHASE'):
|
||||
if result_rules is not None and code in result_rules \
|
||||
and result_rules[code]['total']:
|
||||
basis -= result_rules[code]['total']
|
||||
arbeitstage = self._at_arbeitstage(self.date_from, self.date_to)
|
||||
return basis / max(arbeitstage, 1)
|
||||
|
||||
def _at_bv_fiktiv_krankengeld(self):
|
||||
"""Fiktive BV-Grundlage während des Krankengeldbezugs
|
||||
(§ 7 Abs 3 BMSVG, lb-krs-07): 50 % des Entgelts des
|
||||
Kalendermonats vor dem Versicherungsfall (SZ ausgenommen),
|
||||
aliquot nach Kalendertagen des Bezugs (Halb- + ÖGK-Phase,
|
||||
30-Tage-Konvention). Bezug = letzter validierter Lauf vor
|
||||
Episode-Beginn; ohne solchen: der laufende Monat (kurze
|
||||
Dienstverhältnisse, lb-krs-02)."""
|
||||
self.ensure_one()
|
||||
efz = self._at_efz_status()
|
||||
if not efz or not (efz['halb_kalendertage']
|
||||
or efz['oegk_kalendertage']):
|
||||
return 0.0
|
||||
tage = len(efz['halb_kalendertage']) \
|
||||
+ len(efz['oegk_kalendertage'])
|
||||
vormonat = None
|
||||
if efz['episode_beginn']:
|
||||
vormonat = self.env['hr.payslip'].search([
|
||||
('employee_id', '=', self.employee_id.id),
|
||||
('state', 'in', ('validated', 'paid')),
|
||||
('date_to', '<', efz['episode_beginn']),
|
||||
], order='date_from desc', limit=1)
|
||||
basis_monat = vormonat._l10n_at_sv_basis_roh() if vormonat \
|
||||
else self._l10n_at_sv_basis_roh()
|
||||
return 0.5 * basis_monat * tage / 30.0
|
||||
@@ -3,11 +3,20 @@
|
||||
hr.employee — Muster ``hr_version_bgld.py`` des GemBG-Moduls.
|
||||
|
||||
Die Stufenlogik der Referenz-KVs läuft über Verwendungsgruppenjahre
|
||||
(VWGJ, Biennal-/Triennalsprünge, § 17 der Rahmen-KVs); die Vorrückung
|
||||
selbst ist GP2-Thema (Wizard analog GemBG), hier werden die
|
||||
Vertragsfelder und der Katalog-Lookup bereitgestellt.
|
||||
(VWGJ, Biennal-/Triennalsprünge, § 17 der Rahmen-KVs — verifiziert
|
||||
SI-2203: Abs 3 Einreihung, Abs 4 Anzahl der angerechneten VWGJ durch
|
||||
die Firmenleitung mit Dienstzettel-Mitteilung, Abs 5 Vorrückung am
|
||||
Ersten des Erreichungsmonats). AP9 ergänzt die Vordienstzeiten:
|
||||
Die Firma erkennt Vordienstzeiten in Monaten an (Dienstzettel),
|
||||
daraus leitet der Einstufungs-Vorschlag die Erfahrungsstufe ab
|
||||
(``vwgj_ab`` der Stufenzeilen) und den nächsten Vorrückungstermin.
|
||||
Freigabe bewusst manuell (action_einstufung_uebernehmen).
|
||||
"""
|
||||
from odoo import fields, models
|
||||
from datetime import date
|
||||
|
||||
from dateutil.relativedelta import relativedelta
|
||||
|
||||
from odoo import api, fields, models
|
||||
|
||||
|
||||
class HrVersion(models.Model):
|
||||
@@ -31,6 +40,35 @@ class HrVersion(models.Model):
|
||||
help="Individuell vereinbartes Monatsgehalt; überschreibt das "
|
||||
"Kataloggehalt der Gruppe/Stufe (Günstigkeitsvergleich, "
|
||||
"z. B. § 18 Anrechnung der Rahmen-KVs).")
|
||||
l10n_at_vordienstzeit_monate = fields.Integer(
|
||||
string='AT Angerechnete Vordienstzeiten (Monate)',
|
||||
groups="hr_payroll.group_hr_payroll_user", tracking=True,
|
||||
help="Von der Firmenleitung anerkannte Vordienstzeiten in "
|
||||
"Monaten (§ 17 Abs 4 Rahmen-KV: Anzahl der angerechneten "
|
||||
"Verwendungsgruppenjahre — Dienstzettel-Mitteilungspflicht). "
|
||||
"Fließen in den Einstufungs-Vorschlag und den nächsten "
|
||||
"Vorrückungstermin ein. AP9.")
|
||||
l10n_at_vwgj_monate = fields.Integer(
|
||||
string='AT VWGJ gesamt (Monate)',
|
||||
compute='_compute_l10n_at_vwgj',
|
||||
groups="hr_payroll.group_hr_payroll_user",
|
||||
help="Verwendungsgruppenjahre-Stand heute: Dienstzeit-Monate "
|
||||
"seit Vertragsbeginn zuzüglich angerechnener "
|
||||
"Vordienstzeiten.")
|
||||
l10n_at_vwgj_stufe_vorschlag = fields.Integer(
|
||||
string='AT Erfahrungsstufen-Vorschlag (VWGJ)',
|
||||
compute='_compute_l10n_at_vwgj',
|
||||
groups="hr_payroll.group_hr_payroll_user",
|
||||
help="Vorgeschlagene Erfahrungsstufe aus den vwgj_ab-Schwellen "
|
||||
"der Gruppen-Tabelle (nach N VWGJ); 0 = Schwellen "
|
||||
"nicht gepflegt → manuelle Einstufung maßgeblich.")
|
||||
l10n_at_naechste_vorrueckung = fields.Date(
|
||||
string='AT Nächste VWGJ-Vorrückung',
|
||||
compute='_compute_l10n_at_vwgj',
|
||||
groups="hr_payroll.group_hr_payroll_user",
|
||||
help="§ 17 Abs 5 Rahmen-KV: Erster des Monats, in dem die "
|
||||
"nächste VWGJ-Schwelle erreicht wird (Vertragsbeginn + "
|
||||
"Schwelle − Vordienstzeiten).")
|
||||
currency_id = fields.Many2one(
|
||||
'res.currency', related='company_id.currency_id')
|
||||
l10n_at_personalgruppe = fields.Selection(
|
||||
@@ -50,10 +88,73 @@ class HrVersion(models.Model):
|
||||
self.ensure_one()
|
||||
return self.kv_gruppe_id._get_gehalt(self.erfahrungsstufe, datum)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# AP9: Vordienstzeiten → VWGJ-Einstufungsvorschlag (§ 17 Rahmen-KV)
|
||||
# ------------------------------------------------------------------
|
||||
@api.depends('kv_gruppe_id', 'contract_date_start',
|
||||
'l10n_at_vordienstzeit_monate', 'erfahrungsstufe')
|
||||
def _compute_l10n_at_vwgj(self):
|
||||
"""VWGJ-Stand heute (Dienstzeit + Vordienstzeiten), Stufen-
|
||||
Vorschlag aus den vwgj_ab-Schwellen der Gruppe und der nächste
|
||||
Vorrückungstermin (Erster des Erreichungsmonats, § 17 Abs 5)."""
|
||||
heute = fields.Date.context_today(self)
|
||||
for version in self:
|
||||
version.l10n_at_vwgj_monate = 0
|
||||
version.l10n_at_vwgj_stufe_vorschlag = 0
|
||||
version.l10n_at_naechste_vorrueckung = False
|
||||
if not (version.kv_gruppe_id
|
||||
and version.contract_date_start):
|
||||
continue
|
||||
delta = relativedelta(heute, version.contract_date_start)
|
||||
dienstzeit_monate = delta.years * 12 + delta.months
|
||||
version.l10n_at_vwgj_monate = max(
|
||||
dienstzeit_monate
|
||||
+ (version.l10n_at_vordienstzeit_monate or 0), 0)
|
||||
jahre = version.l10n_at_vwgj_monate // 12
|
||||
version.l10n_at_vwgj_stufe_vorschlag = \
|
||||
version.kv_gruppe_id._get_stufe_fuer_vwgj(jahre, heute)
|
||||
naechste = version.kv_gruppe_id._get_naechste_vwgj(
|
||||
jahre, heute)
|
||||
if naechste:
|
||||
erreichung = version.contract_date_start \
|
||||
+ relativedelta(
|
||||
months=naechste * 12
|
||||
- (version.l10n_at_vordienstzeit_monate or 0))
|
||||
# § 17 Abs 5: Erster des Monats, in dem die erhöhte
|
||||
# Anzahl der VWGJ erreicht wird.
|
||||
version.l10n_at_naechste_vorrueckung = erreichung \
|
||||
.replace(day=1)
|
||||
|
||||
def action_einstufung_uebernehmen(self):
|
||||
"""Vorschlag als Erfahrungsstufe übernehmen — bewusst ein
|
||||
expliziter Verwaltungsakt (§ 17 Abs 4: Firmenleitung, mit
|
||||
Dienstzettel bekanntzugeben); kein automatisches Schreiben."""
|
||||
for version in self:
|
||||
if version.l10n_at_vwgj_stufe_vorschlag:
|
||||
version.erfahrungsstufe = \
|
||||
version.l10n_at_vwgj_stufe_vorschlag
|
||||
|
||||
|
||||
class HrEmployee(models.Model):
|
||||
_inherit = 'hr.employee'
|
||||
|
||||
l10n_at_vordienstzeit_monate = fields.Integer(
|
||||
related="version_id.l10n_at_vordienstzeit_monate", inherited=True,
|
||||
readonly=False, groups="hr_payroll.group_hr_payroll_user")
|
||||
l10n_at_vwgj_monate = fields.Integer(
|
||||
related="version_id.l10n_at_vwgj_monate", inherited=True,
|
||||
groups="hr_payroll.group_hr_payroll_user")
|
||||
l10n_at_vwgj_stufe_vorschlag = fields.Integer(
|
||||
related="version_id.l10n_at_vwgj_stufe_vorschlag", inherited=True,
|
||||
groups="hr_payroll.group_hr_payroll_user")
|
||||
l10n_at_naechste_vorrueckung = fields.Date(
|
||||
related="version_id.l10n_at_naechste_vorrueckung", inherited=True,
|
||||
groups="hr_payroll.group_hr_payroll_user")
|
||||
|
||||
def action_einstufung_uebernehmen(self):
|
||||
"""Delegation an die Version (Button am Mitarbeiterformular)."""
|
||||
self.mapped('version_id').action_einstufung_uebernehmen()
|
||||
|
||||
kv_id = fields.Many2one(
|
||||
related="version_id.kv_id", inherited=True,
|
||||
readonly=False, groups="hr_payroll.group_hr_payroll_user")
|
||||
|
||||
@@ -176,6 +176,42 @@ class L10nAtPayrollKvGruppe(models.Model):
|
||||
('gueltig_ab', '<=', datum)],
|
||||
order='gueltig_ab desc', limit=1)
|
||||
|
||||
def _get_stufe_fuer_vwgj(self, vwgj_jahre, datum=None):
|
||||
"""Erfahrungsstufe für vollendete Verwendungsgruppenjahre
|
||||
(VWGJ) zum Stichtag — AP9. Basis: vwgj_ab der Stufenzeilen
|
||||
(„nach N VWGJ"; 0 = ab Beginn). Gibt die höchste Stufe mit
|
||||
vwgj_ab <= vwgj_jahre zurück; 0, wenn die Gruppe keine
|
||||
gepflegten Schwellen hat (manuelle Einstufung maßgeblich).
|
||||
Rahmen-KV § 17 Abs 3-5 (Biennal-/Triennalsprünge, Vorrückung
|
||||
am Ersten des Erreichungsmonats)."""
|
||||
self.ensure_one()
|
||||
datum = datum or fields.Date.context_today(self)
|
||||
# -1 = Schwelle nicht gepflegt (ORM liest Integer-NULL als 0 —
|
||||
# deshalb Konvention -1 statt NULL; Domain > -1 filtert beides).
|
||||
zeilen = self.env['l10n.at.payroll.kv.gruppe.stufe'].search(
|
||||
[('gruppe_id', '=', self.id),
|
||||
('gueltig_ab', '<=', datum),
|
||||
('vwgj_ab', '>', -1)])
|
||||
beste = zeilen.filtered(lambda z: z.vwgj_ab <= vwgj_jahre)
|
||||
if not beste:
|
||||
return 0
|
||||
return max(beste.mapped('stufe'))
|
||||
|
||||
def _get_naechste_vwgj(self, vwgj_jahre, datum=None):
|
||||
"""Nächste VWGJ-Schwelle oberhalb der erreichten Jahre (für
|
||||
den Vorrückungstermin, § 17 Abs 5) — None an der Höchststufe."""
|
||||
self.ensure_one()
|
||||
datum = datum or fields.Date.context_today(self)
|
||||
zeilen = self.env['l10n.at.payroll.kv.gruppe.stufe'].search(
|
||||
[('gruppe_id', '=', self.id),
|
||||
('gueltig_ab', '<=', datum),
|
||||
('vwgj_ab', '>', -1)])
|
||||
schwellen = sorted(set(zeilen.mapped('vwgj_ab')))
|
||||
for schwelle in schwellen:
|
||||
if schwelle > vwgj_jahre:
|
||||
return schwelle
|
||||
return None
|
||||
|
||||
def _max_stufe(self, datum=None):
|
||||
"""Höchste Stufe mit gültigem Monatsgehalt zum Stichtag."""
|
||||
self.ensure_one()
|
||||
@@ -202,6 +238,13 @@ class L10nAtPayrollKvGruppeStufe(models.Model):
|
||||
bezeichnung = fields.Char(
|
||||
help="Stufen-Label der KV-Tabelle, z. B. 'im 1. u 2. VWGJ' "
|
||||
"oder 'nach 15 VWGJ'.")
|
||||
vwgj_ab = fields.Integer(
|
||||
string='Ab (vollendete) VWGJ', index=True, default=-1,
|
||||
help="Vollendete Verwendungsgruppenjahre, ab denen die Stufe "
|
||||
"gebührt — aus dem Stufen-Label (§ 17 Abs 3-5 "
|
||||
"Rahmen-KV: Biennal-/Triennalsprünge). 0 = ab Beginn; "
|
||||
"-1 = Schwelle nicht gepflegt → kein Einstufungs-"
|
||||
"Vorschlag (manuelle Einstufung maßgeblich). AP9.")
|
||||
gehalt = fields.Monetary(currency_field='currency_id')
|
||||
currency_id = fields.Many2one(
|
||||
'res.currency', default=lambda self: self.env.company.currency_id)
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
# Part of the odoo-at-payroll project. License: LGPL-3.
|
||||
"""Wiener Dienstgeberabgabe („U-Bahn-Steuer") — Wr. DAG, RIS LrW
|
||||
Gesetzesnummer 20000133, geltende Fassung 12. 09. 2026 (verifiziert,
|
||||
Quelle .firecrawl/ris/wien/WDGAG_*.html; § 9: die Einzahlungen fließen
|
||||
der Stadt Wien zu und sind zur Errichtung einer Untergrundbahn zu
|
||||
verwenden):
|
||||
|
||||
- **§ 5:** 2 € je Dienstnehmer:in und je angefangene Woche eines
|
||||
bestehenden Dienstverhältnisses; **§ 2:** Beschäftigungsort Wien
|
||||
(feste Arbeitsstätte, sonst Wohnsitz des Dienstnehmers; Bau-
|
||||
ausführungen > 6 Monate).
|
||||
- **§ 3 Befreiungen:** Gebietskörperschaften; Überschreiten des
|
||||
55. Lebensjahres; Behinderte/Opferfürsorge/BEinstG; Lehrverhältnisse
|
||||
(BAG); ≤ 10 Wochenstunden; Hausbesorger; gesetzliche Mutter-
|
||||
schaftsverbote und Karenzurlaub auf gesetzlichem Anspruch;
|
||||
Präsenzdienst/Zivildienst.
|
||||
- **§ 6:** Selbstberechnung — Vormonatsschuld bis 15. des Folgemonats
|
||||
an die Stadtkasse; Jahreserklärung bis 31. 3. inklusive befreiter
|
||||
Dienstverhältnisse (Fristenkalender-Arten wien_dag/wien_dag_jahres).
|
||||
|
||||
Wochenlogik (Praxis-Annahme, im Parallellauf gegen die Magistrats-
|
||||
Praxis zu verifizieren): „angefangene Woche" = Kalenderwoche, deren
|
||||
Montag im Abrechnungsmonat liegt; abgabepflichtig, wenn das Dienst-
|
||||
verhältnis an mindestens einem Tag der Woche besteht. Karenz-Wochen
|
||||
(validierte ATP_KARENZ-Work Entries) sind befreit. Nicht abgebildet
|
||||
(bis AP 7): Mutterschaftsverbote, Präsenzdienst/Zivildienst, Haus-
|
||||
besorger, Behinderten-Status — manuelle Prüfung, Rest dokumentiert.
|
||||
"""
|
||||
from datetime import timedelta
|
||||
|
||||
from dateutil.relativedelta import relativedelta
|
||||
|
||||
from odoo import models
|
||||
|
||||
|
||||
class HrPayslip(models.Model):
|
||||
_inherit = 'hr.payslip'
|
||||
|
||||
def _at_wien_dag(self):
|
||||
"""Wr. DAG je Beitragsmonat: {'wochen', 'satz', 'betrag',
|
||||
'info'} — None, wenn die Company nicht erfasst ist; 0-Wochen
|
||||
bei Befreiung. Kopfpauschale, nicht lohnabhängig (§ 5)."""
|
||||
self.ensure_one()
|
||||
if not self.company_id.l10n_at_wien_dag:
|
||||
return None
|
||||
version = self.version_id
|
||||
employee = self.employee_id
|
||||
monat_start = self.date_from
|
||||
monat_ende = self.date_to
|
||||
info = []
|
||||
# Befreiungen § 3 (monatsweise Bewertung)
|
||||
if version.l10n_at_lehrling:
|
||||
return {'wochen': 0, 'satz': 0.0, 'betrag': 0.0,
|
||||
'info': 'Befreit: Lehrverhältnis (§ 3 lit d).'}
|
||||
if employee.birthday:
|
||||
vollendet55 = employee.birthday + relativedelta(years=55)
|
||||
if vollendet55 <= monat_ende:
|
||||
return {'wochen': 0, 'satz': 0.0, 'betrag': 0.0,
|
||||
'info': 'Befreit: 55. Lebensjahr überschritten '
|
||||
'(§ 3 lit b, %s).' % vollendet55}
|
||||
kalender = version.resource_calendar_id
|
||||
wochenstunden = sum(
|
||||
(att.hour_to or 0.0) - (att.hour_from or 0.0)
|
||||
for att in kalender.attendance_ids) \
|
||||
if kalender else 0.0
|
||||
if wochenstunden <= 10.0:
|
||||
return {'wochen': 0, 'satz': 0.0, 'betrag': 0.0,
|
||||
'info': 'Befreit: ≤ 10 Wochenstunden (§ 3 lit e).'}
|
||||
# Dienstverhältnis-Bestand im Monat (§ 1)
|
||||
von = max(monat_start,
|
||||
version.contract_date_start or monat_start)
|
||||
bis = min(monat_ende,
|
||||
version.contract_date_end or monat_ende)
|
||||
if von > bis:
|
||||
return {'wochen': 0, 'satz': 0.0, 'betrag': 0.0,
|
||||
'info': 'Kein bestehendes Dienstverhältnis im '
|
||||
'Abrechnungsmonat.'}
|
||||
# Karenz-Tage des Monats (§ 3 lit g — Rest via WET ATP_KARENZ)
|
||||
karenz_tage = set()
|
||||
if self.env['hr.work.entry']._fields.get('leave_id'):
|
||||
eintraege = self.env['hr.work.entry'].search([
|
||||
('employee_id', '=', employee.id),
|
||||
('date', '>=', monat_start),
|
||||
('date', '<=', monat_ende),
|
||||
('state', '!=', 'cancelled'),
|
||||
('work_entry_type_id.code', '=', 'ATP_KARENZ'),
|
||||
])
|
||||
karenz_tage = {e.date for e in eintraege}
|
||||
# Wochen mit Montag im Monat; abgabepflichtig, wenn das
|
||||
# Dienstverhältnis an mindestens einem Tag der Woche besteht
|
||||
# (Schnittmenge [DV] ∩ [Woche] nicht leer).
|
||||
wochen = 0
|
||||
tag = monat_start + timedelta(
|
||||
days=(7 - monat_start.weekday()) % 7)
|
||||
while tag <= monat_ende:
|
||||
woche_ende = tag + timedelta(days=6)
|
||||
if von <= woche_ende and bis >= tag:
|
||||
karenz_in_woche = any(
|
||||
tag + timedelta(days=i) in karenz_tage
|
||||
for i in range((woche_ende - tag).days + 1)
|
||||
if tag + timedelta(days=i) <= monat_ende)
|
||||
if karenz_in_woche:
|
||||
info.append('Karenz-Woche ab %s befreit (§ 3 lit g)'
|
||||
% tag)
|
||||
else:
|
||||
wochen += 1
|
||||
tag += timedelta(days=7)
|
||||
satz = float(self.env['hr.rule.parameter']
|
||||
._get_parameter_from_code(
|
||||
'at_wien_dag_satz', monat_start,
|
||||
raise_if_not_found=False) or 0.0)
|
||||
return {'wochen': wochen, 'satz': satz,
|
||||
'betrag': wochen * satz, 'info': '; '.join(info)}
|
||||
Reference in New Issue
Block a user