Files
odoo-at-payroll/addons/l10n_at_hr_payroll_private/models/mbgm.py
T
fegger ec33dfacf2 [IMP] l10n_at_hr_payroll(_private): Meldewesen-Reste (AP17) und Status-Sonderfälle (AP18/AP19)
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).
2026-09-16 19:12:08 +02:00

710 lines
34 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.
"""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 (A01A03/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')