[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).
This commit is contained in:
2026-09-16 19:12:08 +02:00
parent 1df5fb19c4
commit ec33dfacf2
27 changed files with 2736 additions and 64 deletions
+189 -59
View File
@@ -300,6 +300,47 @@ class L10nAtPayrollMbgm(models.Model):
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 = []
@@ -310,9 +351,17 @@ class L10nAtPayrollMbgm(models.Model):
fehler.append('Beitragskontonummer (BKNR) fehlt.')
vstr = (bknr or '')[:2]
deklaration.line_ids.unlink()
datensaetze = []
summe_gesamt = 0
anzahl = 0
# 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 = {}
@@ -325,6 +374,13 @@ class L10nAtPayrollMbgm(models.Model):
'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 '
@@ -356,7 +412,8 @@ class L10nAtPayrollMbgm(models.Model):
storno_satz.setze('VSNR', version.ssnid)
storno_satz.setze('VSUM', abs(original.vsum),
cent=True)
datensaetze.append(storno_satz)
paket_gruppen['normal']['saetze'].append(
storno_satz)
deklaration.env['l10n.at.payroll.mbgm.line']\
.create({
'mbgm_id': deklaration.id,
@@ -366,43 +423,63 @@ class L10nAtPayrollMbgm(models.Model):
'refw': refw,
'vsum': original.vsum,
})
summe_gesamt += original.vsum
anzahl += 1
paket_gruppen['normal']['summe'] += original.vsum
paket_gruppen['normal']['anzahl'] += 1
continue
bsgr = deklaration._l10n_at_bsgr(version, slip)
tarif = deklaration._l10n_at_tarif(bsgr, version)
if not tarif:
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: kein Tarifsatz für BSGR '
'%s (weder TASY-Katalog noch Parameter '
'at_mbgm_tarif am Beitragsmonat).'
'Mitarbeiter %s: fallweise mBGM (G3/G4 mit '
'tagesgenauen Tarifblöcken T2/T5) ist ein '
'dokumentierter Rest (BSGR %s).'
% (employee.name, bsgr))
continue
if slip.l10n_at_geringfuegig \
or version.l10n_at_fallweise:
fehler.append(
'Mitarbeiter %s: geringfügige/fallweise '
'Beschäftigung in der mBGM ist ein '
'dokumentierter Rest (BSGR %s, Tarifsystem '
'verifizieren).' % (employee.name, bsgr))
continue
positionen = deklaration._l10n_at_positionen(
slip, tarif)
vsum = sum(rsum for _c, _s, rsum, _b in positionen)
# Konsistenz gegen die GP4-Payslip-Summen.
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 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,
@@ -413,46 +490,82 @@ class L10nAtPayrollMbgm(models.Model):
deklaration.beitragsmonat.strftime('%Y%m'),
employee.id),
'vsum': vsum,
'jagb': gruppe == 'jagb',
})
datensaetze.extend(
paket_gruppen[gruppe]['saetze'].extend(
deklaration._l10n_at_mbgm_datensaetze(
employee, version, bsgr, positionen, vsum))
summe_gesamt += vsum
anzahl += 1
paket = L10nAtEldaSatz(
L10N_AT_ELDA_IDTEIL + MBGM_PAKET, 326,
bezeichnung='mBGM-Paket')
paket.setze('SART', 'PS')
paket.setze('VSTR', vstr)
paket.setze('REFP', 'MBGM-%d-%s' % (
company.id,
deklaration.beitragsmonat.strftime('%Y%m')))
paket.setze('BKNR', bknr)
paket.setze('DGNA', company.name[:70])
paket.setze('JAGB', 'N')
paket.setze('BZRM', deklaration.beitragsmonat.strftime(
'%m%Y'))
paket.setze('GSVZ', '+' if summe_gesamt >= 0 else '-')
paket.setze('GSUM', abs(summe_gesamt), cent=True)
paket.setze('ANZM', anzahl)
datensaetze.insert(0, paket)
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))
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])
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({
'datei': base64.b64encode(datei.render()),
'datei_name': 'mbgm_%s.txt' % (
deklaration.beitragsmonat.strftime('%Y%m'),),
'protokoll': 'WARNUNGEN:\n%s' % (
'\n'.join(warnungen) or ''),
'state': 'done',
@@ -525,6 +638,19 @@ class L10nAtPayrollMbgm(models.Model):
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 '
@@ -573,6 +699,10 @@ class L10nAtPayrollMbgmLine(models.Model):
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')