mirror of
http://100.103.83.12:3003/fegger/odoo-at-payroll.git
synced 2026-09-17 16:56:42 +00:00
ec33dfacf2
AP17 — Lohnnachweis L17, Finanz-Mitteilungen § 109a/§ 109b, GfB-Jahreszahlung:
- L17 (DM-ORG E.24, BEST LF, SART A1/1100, Version 28): je
Lohnzahlungszeitraum je DN aus den GP3-Kennzahlen-Flags; Prüfkatalog-
Semantik (KZ 350 = Brutto gesamt, 351/352/354 Teilbeträge) und formale
Prüfungen (350-Formel, 357/347-Caps, aliquotierte 354/HOPA-Caps);
ODL-PDF-Extraktion der Kapitel E.24/E.16/E.26 (txt feldversetzt)
- Finanz-Mitteilungen § 109a (W1, E.16) und § 109b (B1, E.26): Modelle
je Leistungserbringer/Auftragnehmer, Schwellen als hr.rule.parameter
(450/900 — § 109a-VO; 100.000 — § 109b Abs 4 lit a, RIS NOR40263383),
AL31/32/33, LKZ1-7, UID, bis 3 maßgebliche natürliche Personen,
KORR K/S; Unter-Schwellen-Zeilen entfallen mit Protokollhinweis
- mBGM: geringfügig Beschäftigte (BSGR B010/B030) — T01 § 53a-UV-
Pauschale + V01 BMSVG 1,53 %, kein T02 (GfB-SZ beitragsfrei,
§ 49 Abs 3); JAGB-Pakettrennung für die jährliche Abrechnung
(§ 58 Abs 8 ASVG iVm § 6 Abs 2a BMSVG, DM-ORG D.52) mit
2,5 vH-Zuschlag (Z04) in der GfB-Jahresabrechnung (Fälligkeit
15. 1. Folgejahr)
- Fristenkalender: L17- und § 109b-Fristarten (Ende Februar;
§ 109a/§ 109b EStG und § 58 Abs 8 ASVG RIS-verifiziert)
AP18 — Schwerarbeit/NSchG (Beschaffungsgate erledigt, RIS):
- NSchG (RIS 10008502, BGBl 1981/354) Art XI Abs 3 (2 vH Stammnorm,
3,80 % via Sistierung) und Abs 6 (Nachtschwerarbeitsmonat: 6-Tage-
Kriterium, Rückblick-Fenster 12/18, Randmonat); ASchV (20004642)
§ 1/§ 4/§ 5; DM-ORG E.22.2-Erstellvorschriften (Z 1 analog NSchG,
Z 2/4/5 ≥ 15 Tage, Z 3/GfB nicht melden)
- Kern: Schwerarbeitsmeldung (BEST SM, SART 65/66, Länge 800,
Tätigkeitsblöcke 26 × 10) mit Altersfilter- und Erstellvorschrift-
Prüfungen
- Privat: Nachtschwerarbeitsmonat-Zähler (WET ATP_NACHTSCHWER,
Art-XI-Abs-6-Fenster inkl. Randmonat-Proxy) als stored Flag am Lauf
AP19 — Status- und Verhinderungs-Sonderfälle:
- hr.version-Status (Organ ≤/≥ 25 %, lb-vst-03): Organ ≥ 25 % — kein
SV-Verhältnis (SVDN/SVDG 0 via SV-Satz-Override, mBGM-Ausschluss,
LSt läuft weiter); Ferialpraktikanten-Flag (dokumentativ — BMSVG § 6
nennt keinen Ausschluss, Entgelt-/SV-Logik bleibt normal)
- Präsenz-/Ausbildungs-/Zivildienst-Fiktion (§ 7 Abs 1/2 BMSVG):
fiktive BV-Bemessungsgrundlage 435,90 € (KBGG § 3 Abs 1 idF vor
BGBl I 53/2016, lb-vor-04) aliquotiert auf ATP_PRD_TAGE
- Dienstverhinderungs-Sonderfälle bleiben dokumentiert offen
(lb-dvh-01/04, KB-❓); Feiertags-EFZ ist durch das AP4-WET-Modell
abgedeckt
Tests: 21 neue (L17-Positionen/Prüfkatalog, W1/B1-Schwellen, GfB-mBGM/
JAGB/Jahresabrechnung, NSchM-Fenster/Randmonat, SM-Fixsatz, Organ-Sperre,
PRD-Fiktion); Suite 252/252 grün auf frischer -i-DB (85 Kern, 18 GemBG,
213 Privat, 10 Dokumente). Dokumentierte Reste: DZR-36-Fenster,
Randmonat-Proxy und UV-Pauschale-vs-mBGM im Parallellauf (GP8),
fallweise mBGM (G3/G4), 2027er-Schwellen (C7).
710 lines
34 KiB
Python
710 lines
34 KiB
Python
# Part of the odoo-at-payroll project. License: LGPL-3.
|
||
"""Monatliche Beitragsgrundlagenmeldung mBGM/JASV (GP7/AP5) — DM-ORG
|
||
42.7.0, Kapitel E.32 (BEST ``MB``, mBGM-Version 02; PDF-verifizierte
|
||
Extraktion 2026-09-12).
|
||
|
||
Hierarchie: Paket (SART ``PS`` Selbstabrechner, Länge 305) → mBGM
|
||
(SART ``G1``, 326) → Tarifblock (SART ``T1``, 42) → Verrechnungsbasis
|
||
(``BS``, 33) → Verrechnungsposition (``V1``, 42).
|
||
|
||
Fachliche Grundlagen:
|
||
- **KEIN DN/DG- oder Zweig-Split**: die Standardposition ``T01``
|
||
trägt den GESAMT-Prozentsatz des Tarifsystems. Quelle ist der
|
||
TASY-Katalog (AP0, Variante b): Beschäftigtengruppe + Ergänzungen
|
||
(ERGB am Vertrag) + Dienstgebergruppe (Company) → Tarifgruppe →
|
||
Tarifset; fehlt der Katalog, greift der Hand-Parameter
|
||
``at_mbgm_tarif`` als Fallback (Werte aus dem ÖGK-Handbuch,
|
||
`.additional_docs/oegk/`). Fehlende Sätze brechen den Lauf
|
||
listenweise ab (keine Erfindung von Tarifen).
|
||
- **BSGR** (DM-ORG D.48/TASY): B001 Arbeiter, B002 Angestellte,
|
||
B045 Arbeiterlehrlinge, B044 Angestelltenlehrlinge, B010 gering-
|
||
fällige Arbeiter, B030 geringfügige Angestellte — TASY-Bezeichner
|
||
verifiziert (B044 = „Ang. Lg.“, B045 = „Arb. Lg.“).
|
||
- **ERGB** (D.49): Ergänzungen am Vertrag (z. B. E01 Nachtschwer-
|
||
arbeit, E02 Schlechtwetterentschädigung) werden im Tarifblock
|
||
belegt und steuern die Tarifgruppen-Auflösung im Katalog; das
|
||
Company-Flag ``l10n_at_nschab`` bleibt Fallback ohne Katalog.
|
||
- **Abschläge als eigene Verrechnungspositionen** (D.60, verifiziert):
|
||
A01/A02/A03 = Minderung AV auf 2/1/0 % (§ 2a AMPFG, DN-seitig —
|
||
Abschlag = AB × (2,95 − Zielsatz) %), A09 = UV-Entfall ab 60
|
||
(1,10 %), A10 = AV+IE-Entfall Pensionsanspruch (6,00 %), A15 =
|
||
PV-Halbierung Bonusphase (11,40 %). A02/A10 schließen einander aus
|
||
(Konkurrenz a4: Entfall vor Minderung — wie der GP4-Kern rechnet).
|
||
- **BV (BMSVG)**: eigene Verrechnungsbasis ``BV`` (ungedeckelt, GP5)
|
||
+ Position ``V01`` (1,53 %); SZ-Basis ``SZ`` (§-54-gedeckelt) +
|
||
Position ``T02`` mit dem SZ-Gesamtsatz.
|
||
- **RSUM aus Basis × Satz** (mBGM-eigene Tarifierung); Konsistenz
|
||
gegen die GP4-Payslip-Summen als Prüfwarnung (Toleranz 0,10 €).
|
||
- **Fehlanzeige**: Paket ohne mBGM-Sätze (ANZM 0) für Beitragsmonate
|
||
ohne meldbare Daten (GP5-Deferral; DM-ORG-Form ⚠ Parallellauf
|
||
verifizieren). **Storno**: R1-Sätze mit REFU + identischem BZRM
|
||
(VSUM = gestornierte VSUM) für den Nachtragslauf; Nachreichung als
|
||
neue Deklaration (GP4-Deferral Aufrollung).
|
||
- Fallweise Beschäftigte (G3/G4 mit tagesgenauen Tarifblöcken) und
|
||
Geringfügige ohne Tarifparameter: dokumentierter Rest — der Lauf
|
||
bricht listenweise ab, bis `at_mbgm_tarif` gepflegt ist.
|
||
|
||
Dateibasiert (Prämisse P7): Generierung + Prüfprotokoll; Übertragung
|
||
beim Mandanten. Fristen: Selbstabrechner 15., Vorschreibebetriebe 7.
|
||
des Folgemonats (lb-mel-08) — Fristenkalender GP8."""
|
||
from datetime import date, timedelta
|
||
import base64
|
||
|
||
from odoo import api, fields, models
|
||
from odoo.exceptions import UserError
|
||
|
||
from odoo.addons.l10n_at_hr_payroll.models.elda import (
|
||
L10N_AT_ELDA_IDTEIL,
|
||
L10nAtEldaBestand,
|
||
L10nAtEldaDatei,
|
||
L10nAtEldaSatz,
|
||
l10n_at_euro_cent,
|
||
)
|
||
|
||
from odoo.addons.l10n_at_hr_payroll.models.lohnzettel_l16 import \
|
||
l10n_at_namen
|
||
|
||
# Paket (E.32, Selbstabrechner PS) — nach IDTEIL, Länge 305.
|
||
MBGM_PAKET = [
|
||
(2, 'REFP', 21, 40, 'a'),
|
||
(3, 'BKNR', 61, 10, 'a'),
|
||
(4, 'DGNA', 71, 70, 'a'),
|
||
(5, 'MPKE', 141, 30, 'a'),
|
||
(6, 'JAGB', 171, 1, 'a'),
|
||
(7, 'DTEL', 172, 50, 'a'),
|
||
(8, 'MAIL', 222, 60, 'a'),
|
||
(9, 'BZRM', 282, 6, 'n'), # MMJJJJ
|
||
(10, 'GSVZ', 288, 1, 'a'),
|
||
(11, 'GSUM', 289, 11, 'n'), # Σ VSUM (Cent)
|
||
(12, 'ANZM', 300, 6, 'n'), # Anzahl mBGM (G/R-Sätze)
|
||
]
|
||
|
||
# mBGM-Satz (E.32, G1 Selbstabrechner) — nach IDTEIL, Länge 326.
|
||
MBGM_SATZ = [
|
||
(2, 'REFW', 21, 40, 'a'),
|
||
(3, 'REFU', 61, 40, 'a'),
|
||
(4, 'REFV', 101, 40, 'a'),
|
||
(5, 'VSNR', 141, 10, 'n'),
|
||
(6, 'FANA', 151, 70, 'a'),
|
||
(7, 'VONA', 221, 70, 'a'),
|
||
(8, 'VSUM', 291, 11, 'n'), # Σ RSUM (Cent)
|
||
(9, 'VERG', 302, 1, 'a'), # 1 = SV+BV mit Versicherungszeit
|
||
(10, 'INF1', 303, 12, 'a'),
|
||
(11, 'INF2', 315, 12, 'a'),
|
||
]
|
||
|
||
# Tarifblock (E.32, T1) — nach IDTEIL, Länge 42.
|
||
MBGM_TARIFBLOCK = [
|
||
(2, 'BSGR', 21, 4, 'a'), # Beschäftigtengruppe (SV-Tarifsystem)
|
||
(3, 'ERGB', 25, 15, 'a'), # 5 × 3-stellige Ergänzungen
|
||
(4, 'VVON', 40, 2, 'n'), # Beginn der Verrechnung (Tag)
|
||
(5, 'KEUE', 42, 1, 'a'), # enthält KE/UE
|
||
]
|
||
|
||
# Verrechnungsbasis (E.32, BS) — nach IDTEIL, Länge 33.
|
||
MBGM_BASIS = [
|
||
(2, 'VBTY', 21, 2, 'a'), # AB/SZ/BV/UU …
|
||
(3, 'VBBT', 23, 11, 'n'), # Cent
|
||
]
|
||
|
||
# Verrechnungsposition (E.32, V1) — nach IDTEIL, Länge 42.
|
||
MBGM_POSITION = [
|
||
(2, 'VPTY', 21, 3, 'a'), # T01/T02/V01/A01…A15/Z01 …
|
||
(3, 'VPVZ', 24, 1, 'a'), # Vorzeichen Prozentsatz
|
||
(4, 'VPTA', 25, 6, 'n'), # Prozentsatz ×1000 (3 Nachkommastellen)
|
||
(5, 'RSVZ', 31, 1, 'a'), # Vorzeichen RSUM
|
||
(6, 'RSUM', 32, 11, 'n'), # Cent
|
||
]
|
||
|
||
|
||
class L10nAtPayrollMbgm(models.Model):
|
||
_name = 'l10n.at.payroll.mbgm'
|
||
_description = 'Monatliche Beitragsgrundlagenmeldung (mBGM/JASV)'
|
||
_inherit = ['mail.thread']
|
||
_order = 'beitragsmonat desc, id desc'
|
||
|
||
company_id = fields.Many2one(
|
||
'res.company', required=True,
|
||
default=lambda self: self.env.company)
|
||
beitragsmonat = fields.Date(
|
||
required=True, string='Beitragsmonat',
|
||
help='Erster Tag des Beitragsmonats (MMJJJJ).')
|
||
art = fields.Selection(
|
||
[('MB', 'mBGM (regulär)'),
|
||
('ST', 'Storno (R-Sätze, iVm REFU)'),
|
||
('FZ', 'Fehlanzeige (Paket ohne mBGM-Sätze)')],
|
||
required=True, default='MB', tracking=True)
|
||
storno_id = fields.Many2one(
|
||
'l10n.at.payroll.mbgm', string='Stornierte Deklaration',
|
||
help='Art ST: Referenz auf die ursprüngliche Deklaration; '
|
||
'REFU der R-Sätze = REFW der stornierten mBGM je AN.')
|
||
line_ids = fields.One2many(
|
||
'l10n.at.payroll.mbgm.line', 'mbgm_id', string='mBGM-Sätze')
|
||
datei = fields.Binary(attachment=True, copy=False)
|
||
datei_name = fields.Char(copy=False)
|
||
protokoll = fields.Text(string='Prüfprotokoll')
|
||
state = fields.Selection(
|
||
[('draft', 'Entwurf'), ('done', 'Erzeugt')], default='draft',
|
||
required=True, tracking=True)
|
||
|
||
_sql_constraints = [(
|
||
'mbgm_company_monat_art_uniq',
|
||
'unique(company_id, beitragsmonat, art)',
|
||
'Für diesen Beitragsmonat existiert bereits eine mBGM-'
|
||
'Deklaration dieser Art.')]
|
||
|
||
def _l10n_at_slips(self):
|
||
self.ensure_one()
|
||
erster = self.beitragsmonat
|
||
letzter = (date(erster.year, erster.month, 28)
|
||
+ timedelta(days=4)).replace(day=1) \
|
||
- timedelta(days=1)
|
||
return self.env['hr.payslip'].search([
|
||
('company_id', '=', self.company_id.id),
|
||
('date_from', '>=', erster),
|
||
('date_from', '<=', letzter),
|
||
('state', 'in', ('validated', 'paid')),
|
||
])
|
||
|
||
def _l10n_at_slip(self, employee):
|
||
"""Der (eindeutige) Lauf des AN im Beitragsmonat."""
|
||
self.ensure_one()
|
||
erster = self.beitragsmonat
|
||
letzter = (date(erster.year, erster.month, 28)
|
||
+ timedelta(days=4)).replace(day=1) \
|
||
- timedelta(days=1)
|
||
return self.env['hr.payslip'].search([
|
||
('company_id', '=', self.company_id.id),
|
||
('employee_id', '=', employee.id),
|
||
('date_from', '>=', erster),
|
||
('date_from', '<=', letzter),
|
||
('state', 'in', ('validated', 'paid'))], limit=1)
|
||
|
||
def _l10n_at_bsgr(self, version, slip):
|
||
"""Beschäftigtengruppe (SV-Tarifsystem, TASY-Bezeichner):
|
||
B001 Arbeiter, B002 Angestellte; Lehrlinge B045
|
||
(Arbeiterlehrlinge) / B044 (Angestelltenlehrlinge);
|
||
geringfügig/fallweise B010 (Arbeiter) / B030 (Angestellte).
|
||
Ohne Default-Parameter → Lauf bricht ab, bis der Katalog
|
||
importiert oder ``at_mbgm_tarif`` gepflegt ist (Tarifsystem-
|
||
Werte, keine Erfindung)."""
|
||
personalgruppe = getattr(
|
||
version, 'l10n_at_personalgruppe', '')
|
||
if version.l10n_at_lehrling:
|
||
return 'B045' if personalgruppe == 'arbeiter' else 'B044'
|
||
if slip.l10n_at_geringfuegig or version.l10n_at_fallweise:
|
||
return 'B010' if personalgruppe == 'arbeiter' else 'B030'
|
||
return 'B001' if personalgruppe == 'arbeiter' else 'B002'
|
||
|
||
def _l10n_at_tarif(self, bsgr, version=None):
|
||
"""T01/T02-Gesamtsätze je BSGR — Katalog vor Hand-Parameter.
|
||
|
||
1. TASY-Katalog: Tarifgruppe aus BSGR + ERGB am Vertrag +
|
||
Dienstgebergruppe der Company am Stichtag; Sätze = Summe
|
||
der Tarifset-Komponenten je Verrechnungsbasis (AB/SZ).
|
||
2. Fallback: ``at_mbgm_tarif``-Parameter (BSGR-Schlüssel).
|
||
|
||
Rückgabe ``{'laufend', 'sz', 'quelle'}`` oder None → Lauf
|
||
bricht listenweise ab.
|
||
"""
|
||
self.ensure_one()
|
||
ergaenzungen = version.l10n_at_ergaenzung_ids.mapped('code') \
|
||
if version is not None else []
|
||
dgg = self.company_id.l10n_at_dienstgebergruppe or 'NBALG'
|
||
tarifgruppe = self.env[
|
||
'l10n.at.payroll.tasy.tarifgruppe']._l10n_at_find(
|
||
bsgr, ergaenzungen, dgg, self.beitragsmonat)
|
||
if tarifgruppe:
|
||
ab = tarifgruppe._l10n_at_saetze('AB', self.beitragsmonat)
|
||
sz = tarifgruppe._l10n_at_saetze('SZ', self.beitragsmonat)
|
||
if ab is None or sz is None:
|
||
return None
|
||
return {'laufend': ab['gesamt'], 'sz': sz['gesamt'],
|
||
'quelle': 'katalog'}
|
||
tarif = self.env['hr.rule.parameter']._get_parameter_from_code(
|
||
'at_mbgm_tarif', self.beitragsmonat,
|
||
raise_if_not_found=False) or {}
|
||
eintrag = tarif.get(bsgr)
|
||
if eintrag:
|
||
return {'laufend': eintrag['laufend'], 'sz': eintrag['sz'],
|
||
'quelle': 'parameter'}
|
||
return None
|
||
|
||
def _l10n_at_positionen(self, slip, tarif):
|
||
"""Verrechnungspositionen je Tarifblock: T01 (Gesamtsatz,
|
||
NSchAB-Zweig via Company-Flag) + Abschläge aus den GP4-
|
||
Stichtagsflags; T02 auf SZ; V01 auf BV-Basis. Rückgabe:
|
||
[(vpty, vpta, rsum_cent, basis_typ), …] — RSUM aus Basis ×
|
||
Satz (mBGM-Tarifierung); Abschläge negativ (RSVZ '-')."""
|
||
version = slip.version_id
|
||
ab = sum(basis for _m, basis
|
||
in slip._l10n_at_sv_basis_laufen_monate())
|
||
# NSchAB über das Company-Flag nur im Parameter-Fallback —
|
||
# im Katalog-Pfad steckt der NB-Zweig über die ERGB-E01-
|
||
# Tarifgruppe (Dienstgebergruppe NBNSA) bereits im T01.
|
||
nschab = 3.80 if (self.company_id.l10n_at_nschab
|
||
and tarif.get('quelle') != 'katalog'
|
||
and not version.l10n_at_lehrling
|
||
and getattr(version, 'l10n_at_personalgruppe',
|
||
'') == 'arbeiter') else 0.0
|
||
satz_laufen = float(tarif['laufend']) + nschab
|
||
positionen = [('T01', satz_laufen, ab * satz_laufen / 100.0,
|
||
'AB')]
|
||
# §-2a-AMPFG: Minderung nur des DN-AV (2,95 %), als eigene
|
||
# Abschlagsposition auf JEDER betroffenen Verrechnungsbasis
|
||
# (AB und SZ — der Abschlag senkt den Gesamtsatz je Basis;
|
||
# DN-seitig, GP4-Konvention). Codes: 0 % → A03, 1 % → A02,
|
||
# 2 % → A01 (D.60, verifiziert).
|
||
minderung = 0.0
|
||
monat = self.beitragsmonat
|
||
if not (version.l10n_at_alv_entfall_ab
|
||
and monat >= version.l10n_at_alv_entfall_ab):
|
||
staffel = self.env['hr.rule.parameter'] \
|
||
._get_parameter_from_code(
|
||
'at_sv_av_staffel_ampfg', monat,
|
||
raise_if_not_found=False) or {}
|
||
for _von, bis, dn_satz in staffel.get('tiers', []):
|
||
if ab and ab <= float(bis):
|
||
minderung = 2.95 - float(dn_satz)
|
||
break
|
||
if minderung > 0:
|
||
code = ('A03' if minderung >= 2.95
|
||
else 'A02' if abs(minderung - 1.95) < 0.01
|
||
else 'A01')
|
||
positionen.append(
|
||
(code, 0.0, -ab * minderung / 100.0, 'AB'))
|
||
else:
|
||
# A10 = Entfall AV+IE (DN-AV + DG-AV + DG-IESG).
|
||
positionen.append(
|
||
('A10', 0.0, -ab * 6.00 / 100.0, 'AB'))
|
||
if version.l10n_at_uv_entfall_ab \
|
||
and monat >= version.l10n_at_uv_entfall_ab:
|
||
positionen.append(('A09', 0.0, -ab * 1.10 / 100.0, 'AB'))
|
||
if version.l10n_at_pv_bonusphase_ab \
|
||
and monat >= version.l10n_at_pv_bonusphase_ab:
|
||
positionen.append(('A15', 0.0, -ab * 11.40 / 100.0, 'AB'))
|
||
sz_basis = slip._l10n_at_svdn_basis_sz()
|
||
if sz_basis > 0:
|
||
satz_sz = float(tarif['sz'])
|
||
positionen.append(
|
||
('T02', satz_sz, sz_basis * satz_sz / 100.0, 'SZ'))
|
||
if minderung > 0:
|
||
code = ('A03' if minderung >= 2.95
|
||
else 'A02' if abs(minderung - 1.95) < 0.01
|
||
else 'A01')
|
||
positionen.append(
|
||
(code, 0.0, -sz_basis * minderung / 100.0, 'SZ'))
|
||
bv_basis = (slip._at_bv_basis_laufen()
|
||
+ slip._at_bv_basis_sonst())
|
||
if bv_basis > 0:
|
||
positionen.append(('V01', 1.53, bv_basis * 0.0153, 'BV'))
|
||
return positionen
|
||
|
||
def _l10n_at_gfb_positionen(self, slip):
|
||
"""AP17: Verrechnungspositionen für geringfügig Beschäftigte
|
||
(BSGR B010/B030): T01 = § 53a-UV-Pauschalbeitrag (Engine:
|
||
``_l10n_at_svdg_uv_geringfuegig`` — 2026 6,50 €/Monat, abgeleitet
|
||
aus aBG 591,29 × 1,1 %; § 51 Abs 2 ASVG), V01 = BMSVG-Beitrag
|
||
1,53 % (Abfertigungsbeitrag § 5 Abs 2 BMSVG, Engine-Position
|
||
BV). Keine AV-/IE-Anteile, keine Abschläge (A01–A03/A09/A10/
|
||
A15), kein T02 — GfB-SZ sind beitragsfrei (§ 49 Abs 3;
|
||
lb-bes-06)."""
|
||
self.ensure_one()
|
||
uv_pauschale = slip._l10n_at_svdg_uv_geringfuegig()
|
||
positionen = []
|
||
if uv_pauschale:
|
||
positionen.append(('T01', 0.0, uv_pauschale, 'AB'))
|
||
bv_basis = (slip._at_bv_basis_laufen()
|
||
+ slip._at_bv_basis_sonst())
|
||
if bv_basis > 0:
|
||
positionen.append(('V01', 1.53, bv_basis * 0.0153, 'BV'))
|
||
return positionen
|
||
|
||
def _l10n_at_bv_jahresabrechnung_gfb(self, jahr):
|
||
"""Jährliche Abrechnung GfB (§ 58 Abs 8 ASVG iVm § 6 Abs 2a
|
||
BMSVG): Summe der JAGB-mBGM-Beiträge des Kalenderjahres und
|
||
der 2,5 vH-Zuschlag (Z04 „BV-Zuschlag bei jährlicher Zahlung“,
|
||
D.60) — Fälligkeit bis 15. Jänner des Folgejahres."""
|
||
self.ensure_one()
|
||
deklarationen = self.search([
|
||
('company_id', '=', self.company_id.id),
|
||
('beitragsmonat', '>=', date(jahr, 1, 1)),
|
||
('beitragsmonat', '<=', date(jahr, 12, 31)),
|
||
('art', '=', 'MB'),
|
||
('state', '=', 'done'),
|
||
])
|
||
summe = sum(
|
||
line.vsum
|
||
for deklaration in deklarationen
|
||
for line in deklaration.line_ids.filtered('jagb'))
|
||
zuschlag = round(summe * 0.025, 2)
|
||
return {'summe': summe, 'zuschlag': zuschlag,
|
||
'gesamt': round(summe + zuschlag, 2)}
|
||
|
||
def action_erzeugen(self):
|
||
for deklaration in self:
|
||
fehler = []
|
||
warnungen = []
|
||
company = deklaration.company_id
|
||
bknr = company.l10n_at_beitragskontonummer
|
||
if not bknr:
|
||
fehler.append('Beitragskontonummer (BKNR) fehlt.')
|
||
vstr = (bknr or '')[:2]
|
||
deklaration.line_ids.unlink()
|
||
# AP17: mBGM-Pakete — 'normal' (JAGB 'N') und — bei
|
||
# jährlicher Abrechnung GfB (§ 58 Abs 8 ASVG iVm § 6
|
||
# Abs 2a BMSVG) — ein separates JAGB-Paket (E.32.2.2.1/D.52)
|
||
# für die GfB-Beitragsgrundlagen je Kalendermonat.
|
||
paket_gruppen = {
|
||
'normal': {'saetze': [], 'summe': 0.0, 'anzahl': 0},
|
||
'jagb': {'saetze': [], 'summe': 0.0, 'anzahl': 0},
|
||
}
|
||
jagb_flag = company.l10n_at_gfb_jaehrliche_abrechnung
|
||
if deklaration.art == 'ST':
|
||
jagb_flag = False
|
||
if deklaration.art != 'FZ':
|
||
slips = deklaration._l10n_at_slips()
|
||
nach_an = {}
|
||
for slip in slips:
|
||
nach_an.setdefault(slip.employee_id, []).append(slip)
|
||
for employee, employee_slips in nach_an.items():
|
||
version = employee.version_id
|
||
if not (version and version.ssnid):
|
||
fehler.append(
|
||
'Mitarbeiter %s ohne Versicherungsnummer.'
|
||
% employee.name)
|
||
continue
|
||
if getattr(version, 'l10n_at_status', 'an') \
|
||
== 'organ_mehr':
|
||
warnungen.append(
|
||
'Mitarbeiter %s: Organ ≥ 25 %% (kein SV-'
|
||
'Verhältnis, AP19) — keine mBGM; LSt/Lohnzettel '
|
||
'unverändert.' % employee.name)
|
||
continue
|
||
if len(employee_slips) > 1:
|
||
fehler.append(
|
||
'Mitarbeiter %s mit mehreren Läufen im '
|
||
'Beitragsmonat — mBGM je Lauf ist ein '
|
||
'dokumentierter Rest.' % employee.name)
|
||
continue
|
||
slip = employee_slips[0]
|
||
if deklaration.art == 'ST':
|
||
# Storno: R1-Satz je stornierter mBGM mit
|
||
# REFU = REFW des Originals, VSUM wie Original
|
||
# (E.32.2.8 — ohne T-/BS-/V-Sätze).
|
||
original = deklaration.storno_id.line_ids \
|
||
.filtered(lambda l: l.employee_id == employee)
|
||
if not original:
|
||
fehler.append(
|
||
'Storno %s: keine stornierte mBGM '
|
||
'am Mitarbeiter gefunden.' % employee.name)
|
||
continue
|
||
refw = 'MBGM-ST-%d-%s-%d' % (
|
||
company.id,
|
||
deklaration.beitragsmonat.strftime('%Y%m'),
|
||
employee.id)
|
||
storno_satz = L10nAtEldaSatz(
|
||
L10N_AT_ELDA_IDTEIL + MBGM_SATZ, 326,
|
||
bezeichnung='Storno %s' % employee.name)
|
||
storno_satz.setze('SART', 'R1')
|
||
storno_satz.setze('REFW', refw)
|
||
storno_satz.setze('REFU', original.refw)
|
||
storno_satz.setze('VSNR', version.ssnid)
|
||
storno_satz.setze('VSUM', abs(original.vsum),
|
||
cent=True)
|
||
paket_gruppen['normal']['saetze'].append(
|
||
storno_satz)
|
||
deklaration.env['l10n.at.payroll.mbgm.line']\
|
||
.create({
|
||
'mbgm_id': deklaration.id,
|
||
'employee_id': employee.id,
|
||
'versicherungsnummer': version.ssnid,
|
||
'bsgr': original.bsgr,
|
||
'refw': refw,
|
||
'vsum': original.vsum,
|
||
})
|
||
paket_gruppen['normal']['summe'] += original.vsum
|
||
paket_gruppen['normal']['anzahl'] += 1
|
||
continue
|
||
bsgr = deklaration._l10n_at_bsgr(version, slip)
|
||
if slip.l10n_at_geringfuegig:
|
||
# GfB (AP17): eigenes Beitragsset — § 53a-UV-
|
||
# Pauschale (T01) + BMSVG 1,53 % (V01), keine
|
||
# AV-/IE-/Abschlagspositionen, kein T02 (GfB-SZ
|
||
# beitragsfrei, § 49 Abs 3).
|
||
positionen = deklaration._l10n_at_gfb_positionen(
|
||
slip)
|
||
if not positionen:
|
||
fehler.append(
|
||
'Mitarbeiter %s: GfB-Beitragssätze fehlen '
|
||
'(§-53a-Parameter am Beitragsmonat).'
|
||
% employee.name)
|
||
continue
|
||
else:
|
||
tarif = deklaration._l10n_at_tarif(bsgr, version)
|
||
if not tarif:
|
||
fehler.append(
|
||
'Mitarbeiter %s: kein Tarifsatz für BSGR '
|
||
'%s (weder TASY-Katalog noch Parameter '
|
||
'at_mbgm_tarif am Beitragsmonat).'
|
||
% (employee.name, bsgr))
|
||
continue
|
||
positionen = deklaration._l10n_at_positionen(
|
||
slip, tarif)
|
||
if version.l10n_at_fallweise:
|
||
fehler.append(
|
||
'Mitarbeiter %s: fallweise mBGM (G3/G4 mit '
|
||
'tagesgenauen Tarifblöcken T2/T5) ist ein '
|
||
'dokumentierter Rest (BSGR %s).'
|
||
% (employee.name, bsgr))
|
||
continue
|
||
vsum = sum(rsum for _c, _s, rsum, _b in positionen)
|
||
# Konsistenz gegen die GP4-Payslip-Summen.
|
||
if slip.l10n_at_geringfuegig:
|
||
ist = (slip._l10n_at_svdg_uv_geringfuegig()
|
||
+ slip._at_bv_beitrag())
|
||
else:
|
||
ist = (slip._l10n_at_svdn_gesamt()
|
||
+ slip._l10n_at_svdg_gesamt()
|
||
+ slip._l10n_at_svdn_sz()
|
||
+ sum(
|
||
slip._l10n_at_svdg_sz(k)
|
||
for k in ('kv', 'pv', 'av', 'uv', 'iesg'))
|
||
+ slip._at_bv_beitrag())
|
||
if abs(vsum - ist) > 0.10:
|
||
warnungen.append(
|
||
'Mitarbeiter %s: mBGM-VSUM %.2f weicht um '
|
||
'%.2f von den GP4-Beitragssummen ab '
|
||
'(Tarif-/Rundungsdifferenz prüfen).'
|
||
% (employee.name, vsum, vsum - ist))
|
||
gruppe = (
|
||
'jagb' if (slip.l10n_at_geringfuegig and jagb_flag)
|
||
else 'normal')
|
||
deklaration.env['l10n.at.payroll.mbgm.line'].create({
|
||
'mbgm_id': deklaration.id,
|
||
'employee_id': employee.id,
|
||
'versicherungsnummer': version.ssnid,
|
||
'bsgr': bsgr,
|
||
'refw': 'MBGM-%d-%s-%d' % (
|
||
company.id,
|
||
deklaration.beitragsmonat.strftime('%Y%m'),
|
||
employee.id),
|
||
'vsum': vsum,
|
||
'jagb': gruppe == 'jagb',
|
||
})
|
||
paket_gruppen[gruppe]['saetze'].extend(
|
||
deklaration._l10n_at_mbgm_datensaetze(
|
||
employee, version, bsgr, positionen, vsum))
|
||
paket_gruppen[gruppe]['summe'] += vsum
|
||
paket_gruppen[gruppe]['anzahl'] += 1
|
||
datensaetze = []
|
||
for schluessel in ('normal', 'jagb'):
|
||
gruppe = paket_gruppen[schluessel]
|
||
if not gruppe['anzahl'] and not (
|
||
deklaration.art == 'FZ'
|
||
and schluessel == 'normal'):
|
||
continue
|
||
paket = L10nAtEldaSatz(
|
||
L10N_AT_ELDA_IDTEIL + MBGM_PAKET, 326,
|
||
bezeichnung='mBGM-Paket %s' % schluessel)
|
||
paket.setze('SART', 'PS')
|
||
paket.setze('VSTR', vstr)
|
||
paket.setze('REFP', 'MBGM-%d-%s%s' % (
|
||
company.id,
|
||
deklaration.beitragsmonat.strftime('%Y%m'),
|
||
'-JAGB' if schluessel == 'jagb' else ''))
|
||
paket.setze('BKNR', bknr)
|
||
paket.setze('DGNA', company.name[:70])
|
||
paket.setze('JAGB', 'J' if schluessel == 'jagb' else 'N')
|
||
paket.setze('BZRM', deklaration.beitragsmonat.strftime(
|
||
'%m%Y'))
|
||
paket.setze('GSVZ',
|
||
'+' if gruppe['summe'] >= 0 else '-')
|
||
paket.setze('GSUM', abs(gruppe['summe']), cent=True)
|
||
paket.setze('ANZM', gruppe['anzahl'])
|
||
datensaetze.append(paket)
|
||
datensaetze.extend(gruppe['saetze'])
|
||
if jagb_flag and paket_gruppen['jagb']['summe']:
|
||
warnungen.append(
|
||
'JAGB-Paket: jährliche Abrechnung der GfB-Beiträge '
|
||
'(§ 58 Abs 8 ASVG iVm § 6 Abs 2a BMSVG) — Fälligkeit '
|
||
'bis 15. Jänner des Folgejahres; der jährliche BV-'
|
||
'Beitragsbetrag ist um 2,5 %% Zuschlag zu erhöhen '
|
||
'(Summe mBGM-Jahr: siehe Protokoll der '
|
||
'Dezember-Abrechnung).')
|
||
if jagb_flag and deklaration.beitragsmonat.month == 12 \
|
||
and paket_gruppen['jagb']['summe']:
|
||
jahresabrechnung = deklaration\
|
||
._l10n_at_bv_jahresabrechnung_gfb(
|
||
deklaration.beitragsmonat.year)
|
||
warnungen.append(
|
||
'GfB-Jahresabrechnung %d: Beiträge %.2f € + '
|
||
'2,5 %% Zuschlag %.2f € (Z04) = %.2f €, fällig '
|
||
'bis 15. 1. %d.' % (
|
||
deklaration.beitragsmonat.year,
|
||
jahresabrechnung['summe'],
|
||
jahresabrechnung['zuschlag'],
|
||
jahresabrechnung['gesamt'],
|
||
deklaration.beitragsmonat.year + 1))
|
||
if fehler:
|
||
deklaration.protokoll = 'FEHLER:\n%s\nWARNUNGEN:\n%s' % (
|
||
'\n'.join(fehler), '\n'.join(warnungen) or '—')
|
||
raise UserError('mBGM-Lauf mit Fehlern abgebrochen:\n'
|
||
+ '\n'.join(fehler))
|
||
if datensaetze:
|
||
bestand = L10nAtEldaBestand(
|
||
'MB', vstr, '02',
|
||
hersteller={'name': 'odoo-at-payroll', 'kfz': 'A',
|
||
'plz': company.partner_id.zip,
|
||
'ort': company.partner_id.city,
|
||
'strasse': company.partner_id.street},
|
||
datensaetze=datensaetze)
|
||
datei = L10nAtEldaDatei([bestand])
|
||
deklaration.write({
|
||
'datei': base64.b64encode(datei.render()),
|
||
'datei_name': 'mbgm_%s.txt' % (
|
||
deklaration.beitragsmonat.strftime('%Y%m'),),
|
||
})
|
||
deklaration.write({
|
||
'protokoll': 'WARNUNGEN:\n%s' % (
|
||
'\n'.join(warnungen) or '—'),
|
||
'state': 'done',
|
||
})
|
||
return True
|
||
|
||
def _l10n_at_mbgm_datensaetze(self, employee, version, bsgr,
|
||
positionen, vsum):
|
||
"""G1 + Tarifblock + je Basis (AB/SZ/BV) die Positionen als
|
||
eigene Sätze (BS/V folgen hierarchisch dem Tarifblock)."""
|
||
self.ensure_one()
|
||
slip = self._l10n_at_slip(employee)
|
||
mbgm_satz = L10nAtEldaSatz(
|
||
L10N_AT_ELDA_IDTEIL + MBGM_SATZ, 326,
|
||
bezeichnung='mBGM %s' % employee.name)
|
||
mbgm_satz.setze('SART', 'G1')
|
||
mbgm_satz.setze('REFW', 'MBGM-%d-%s-%d' % (
|
||
self.company_id.id,
|
||
self.beitragsmonat.strftime('%Y%m'), employee.id))
|
||
mbgm_satz.setze('VSNR', version.ssnid)
|
||
familienname, vorname = l10n_at_namen(employee)
|
||
mbgm_satz.setze('FANA', familienname[:70])
|
||
mbgm_satz.setze('VONA', (vorname or '')[:70])
|
||
mbgm_satz.setze('VSUM', abs(vsum), cent=True)
|
||
mbgm_satz.setze('VERG', '1')
|
||
tarifblock = L10nAtEldaSatz(
|
||
L10N_AT_ELDA_IDTEIL + MBGM_TARIFBLOCK, 326,
|
||
bezeichnung='Tarifblock %s' % employee.name)
|
||
tarifblock.setze('SART', 'T1')
|
||
tarifblock.setze('BSGR', bsgr)
|
||
# ERGB (DM-ORG D.49): bis zu 5 Ergänzungen à 3 Stellen —
|
||
# Grundstellung blank, wenn keine gepflegt (Katalog-
|
||
# Normalfall ohne Ergänzung).
|
||
ergaenzungen = version.l10n_at_ergaenzung_ids.mapped('code')[:5]
|
||
if ergaenzungen:
|
||
tarifblock.setze('ERGB', ''.join(ergaenzungen))
|
||
tarifblock.setze('VVON', '1')
|
||
saetze = [mbgm_satz, tarifblock]
|
||
for code, satz_prozent, rsum, basis_typ in positionen:
|
||
basis = L10nAtEldaSatz(
|
||
L10N_AT_ELDA_IDTEIL + MBGM_BASIS, 326,
|
||
bezeichnung='Basis %s %s' % (basis_typ,
|
||
employee.name))
|
||
basis.setze('SART', 'BS')
|
||
basis.setze('VBTY', basis_typ)
|
||
basis_wert = abs(rsum) * 100.0 / satz_prozent \
|
||
if satz_prozent else 0.0
|
||
if basis_typ == 'AB':
|
||
basis_wert = sum(
|
||
b for _m, b in
|
||
slip._l10n_at_sv_basis_laufen_monate())
|
||
elif basis_typ == 'SZ':
|
||
basis_wert = slip._l10n_at_svdn_basis_sz()
|
||
else:
|
||
basis_wert = slip._at_bv_basis_laufen() \
|
||
+ slip._at_bv_basis_sonst()
|
||
basis.setze('VBBT', basis_wert, cent=True)
|
||
position = L10nAtEldaSatz(
|
||
L10N_AT_ELDA_IDTEIL + MBGM_POSITION, 326,
|
||
bezeichnung='Position %s %s' % (code, employee.name))
|
||
position.setze('SART', 'V1')
|
||
position.setze('VPTY', code)
|
||
position.setze('VPTA', satz_prozent, skala=3)
|
||
position.setze('RSVZ', '-' if rsum < 0 else '+')
|
||
position.setze('RSUM', abs(rsum), cent=True)
|
||
saetze.extend([basis, position])
|
||
return saetze
|
||
|
||
|
||
class ResCompany(models.Model):
|
||
_inherit = 'res.company'
|
||
|
||
l10n_at_gfb_jaehrliche_abrechnung = fields.Boolean(
|
||
string='AT GfB: jährliche Abrechnung der Beiträge (JAGB)',
|
||
help='Vereinbarung nach § 58 Abs 8 ASVG iVm § 6 Abs 2a BMSVG: '
|
||
'die Abfertigungs-/BV-Beiträge aus geringfügigen '
|
||
'Beschäftigungsverhältnissen werden jährlich verrechnet. '
|
||
'mBGM-seitig je Kalendermonat ein eigenes Paket mit '
|
||
'JAGB = „J“ (DM-ORG D.52/E.32.2.2.1); Fälligkeit bis '
|
||
'15. Jänner des Folgejahres (unterjähriges AV-Ende: '
|
||
'bis 15. des Folgemonats). Bei jährlicher Zahlungsweise '
|
||
'sind zusätzlich 2,5 vH vom Beitrag zu leisten. '
|
||
'Änderung der Zahlungsweise nur zum Jahresende und dem '
|
||
'KV-Träger vor dem Beitragszeitraum zu melden.')
|
||
|
||
l10n_at_nschab = fields.Boolean(
|
||
string='AT Nachtschwerarbeitsbeitrags-Betrieb (mBGM-Fallback)',
|
||
help='Betrieb mit Nachtschwerarbeit (ÖGK-Tarifsystem '
|
||
'„Normalbetriebe mit Nachtschwerarbeit“, Dienstgebergruppe '
|
||
'NBNSA): ohne TASY-Katalog erhalten Arbeiter (BSGR B001) '
|
||
'im mBGM-T01 den um NSchAB 3,80 % erhöhten Gesamtsatz. '
|
||
'Mit Katalog stattdessen die Ergänzung E01 am Vertrag '
|
||
'setzen — der NB-Zweig ist dann Teil der E01-Tarifgruppe.')
|
||
l10n_at_dienstgebergruppe = fields.Char(
|
||
string='AT Dienstgebergruppe (SV-Tarifsystem)',
|
||
default='NBALG',
|
||
help='Code der Dienstgebergruppe des TASY-Tarifsystems, z. B. '
|
||
'NBALG „Normalbetriebe — Allgemeiner Teil“, NBNSA '
|
||
'(Nachtschwerarbeit), NBBAU (Schlechtwetter), HAUSH '
|
||
'(Haushalte), DGONO (ohne Niederlassung). Bestimmt mit '
|
||
'BSGR + Ergänzungen die Tarifgruppe der mBGM-Tarifierung '
|
||
'(AP0). Vollständige Liste: Tarifsystem-Katalog-Menü '
|
||
'nach dem TASY-Import.')
|
||
|
||
|
||
class HrVersion(models.Model):
|
||
_inherit = 'hr.version'
|
||
|
||
l10n_at_ergaenzung_ids = fields.Many2many(
|
||
'l10n.at.payroll.tasy.ergaenzung',
|
||
'l10n_at_hr_payroll_version_ergaenzung_rel',
|
||
'version_id', 'ergaenzung_id',
|
||
string='AT Ergänzungen (ERGB, SV-Tarifsystem)',
|
||
groups="hr_payroll.group_hr_payroll_user",
|
||
help='Ergänzungen zur Beschäftigtengruppe (DM-ORG D.49) für die '
|
||
'mBGM-Tarifierung — gemeinsam mit BSGR und Dienstgebergruppe '
|
||
'bilden sie die Tarifgruppe. Z. B. E01 Nachtschwerarbeit, '
|
||
'E02 Schlechtwetterentschädigung. Katalog nach dem TASY-'
|
||
'Import; ohne Eintrag gilt der Normalfall ohne Ergänzung.')
|
||
|
||
|
||
class L10nAtPayrollMbgmLine(models.Model):
|
||
_name = 'l10n.at.payroll.mbgm.line'
|
||
_description = 'mBGM — Satzdaten je AN'
|
||
_order = 'employee_name'
|
||
mbgm_id = fields.Many2one(
|
||
'l10n.at.payroll.mbgm', required=True, ondelete='cascade',
|
||
index=True)
|
||
employee_id = fields.Many2one('hr.employee', required=True)
|
||
employee_name = fields.Char(related='employee_id.name', store=True,
|
||
readonly=True)
|
||
versicherungsnummer = fields.Char()
|
||
bsgr = fields.Char(string='Beschäftigtengruppe')
|
||
jagb = fields.Boolean(
|
||
string='JAGB-Paket (jährliche Abrechnung GfB)',
|
||
help='Zeile gehört zum mBGM-Paket für die jährliche Abrechnung '
|
||
'geringfügig Beschäftigter (DM-ORG D.52).')
|
||
refw = fields.Char(string='Referenzwert (REFW)')
|
||
vsum = fields.Monetary(
|
||
string='VSUM', currency_field='currency_id')
|
||
currency_id = fields.Many2one(
|
||
'res.currency', related='mbgm_id.company_id.currency_id') |