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).
269 lines
12 KiB
Python
269 lines
12 KiB
Python
# Part of the odoo-at-payroll project. License: LGPL-3.
|
||
"""Schwerarbeitsmeldung (AP18) — DM-ORG 42.7.0, Kapitel E.22
|
||
(BEST ``SM``, SART 65 „Schwerarbeitsmeldung“ / 66 „Storno“, Länge 800,
|
||
Version 02; PDF-verifizierte ODL-Extraktion 2026-09-16).
|
||
|
||
Rechtsanker (RIS-verifiziert 2026-09-16):
|
||
- **ASchV** (RIS GesNr 20004642, BGBl II 1998/419 idF NOR40272279):
|
||
§ 1 Abs 1 Z 1–26 (Z 1 Schicht-/Wechseldienst in Nachtarbeit 22–6 Uhr
|
||
≥ 6 h; Z 2 Hitze/Kälte NSchG Art VII Z 2/3; Z 3 chemisch/physikalisch;
|
||
Z 4 schwere körperliche Arbeit 2.000 kcal (M) / 1.400 kcal (F);
|
||
Z 5 Pflege; Z 6 Pflegegeld), § 4 (Schwerarbeitsmonat — Versicherungs-
|
||
monat § 231 Z 1 lit a ASVG; Z 5: 12 Tage Schichtdienst), § 5 (Meldung
|
||
M ≥ 40 / F ≥ 35, bis Ende Februar).
|
||
- **Erstellvorschriften (DM-ORG E.22.2):** meldepflichtig Z 1, 2, 4 und 5;
|
||
Z 1 nur bei ≥ 6 Arbeitstagen/Monat Schicht-/Wechseldienst in Nachtarbeit
|
||
(bei unterschiedlicher Nachtschichten-Anzahl analog **NSchG Art XI
|
||
Abs 6**); Z 2/4/5 nur bei ≥ **15 Arbeitstagen/Monat**; Z 3 nicht melden
|
||
(MdE ≥ 10 % erst nachträglich); Z 6 nur bei bekannter Pflegegeldeinstu-
|
||
fung (ab Stufe 3); geringfügig Beschäftigte nicht melden.
|
||
- **lb-swa-05** (Stand 2026-07): jährlich Jänner–Ende Februar; GfB aus-
|
||
geschlossen; fallweise/freie DN zu melden; Leiharbeiter vom Überlasser.
|
||
|
||
Die Ziffern/Zeiträume sind mandantenseitige Fakten (Arbeitsplatz-
|
||
Klassifikation) — das Modell erfasst sie manuell und erzeugt den Fixsatz;
|
||
keine automatisierte Ableitung aus Arbeitszeiten."""
|
||
from datetime import date, datetime
|
||
|
||
import base64
|
||
|
||
from odoo import api, fields, models
|
||
from odoo.exceptions import UserError
|
||
|
||
from .elda import (
|
||
L10N_AT_ELDA_IDTEIL,
|
||
L10nAtEldaBestand,
|
||
L10nAtEldaDatei,
|
||
L10nAtEldaSatz,
|
||
)
|
||
from .lohnzettel_l16 import l10n_at_namen
|
||
|
||
# ------------------------------------------------------------------
|
||
# SART 65/66 (E.22; Länge 800) — Datenteil inkl. 26 Tätigkeitsblöcken
|
||
# ------------------------------------------------------------------
|
||
SCHWERARBEIT_SATZ = [
|
||
(2, 'BKNR', 21, 10, 'a'), # Beitragskontonummer
|
||
(3, 'DGNA', 31, 70, 'a'),
|
||
(4, 'WOBD', 101, 12, 'a'),
|
||
(5, 'ZOBD', 113, 15, 'a'),
|
||
(6, 'DTEL', 128, 50, 'a'), # DG-Telefon
|
||
(7, 'MAIL', 178, 60, 'a'), # DG-Mail
|
||
(8, 'DKFZ', 238, 3, 'a'), # DG Land/Kfz
|
||
(9, 'DPLZ', 241, 9, 'a'),
|
||
(10, 'DORT', 250, 40, 'a'),
|
||
(11, 'DSTR', 290, 50, 'a'),
|
||
(12, 'VSNR', 340, 10, 'n'), # LLLPTTMMJJ
|
||
(13, 'GEBD', 350, 8, 'n'), # TTMMJJJJ
|
||
(14, 'FANA', 358, 70, 'a'),
|
||
(15, 'VONA', 428, 70, 'a'),
|
||
(16, 'JAHR', 498, 4, 'n'), # Tätigkeitsjahr
|
||
# Tätigkeitsblöcke: 26 × 10 = 260 (pos 502–761)
|
||
(17, 'TART', 502, 2, 'a'), # § 1 Abs 1 Z x ASchV
|
||
(18, 'TVON', 504, 4, 'n'), # TTMM
|
||
(19, 'TBIS', 508, 4, 'n'), # TTMM
|
||
(20, 'REFN', 762, 30, 'a'),
|
||
(21, 'RE21', 792, 9, 'a'),
|
||
]
|
||
|
||
|
||
def _taetigkeitsblock_struktur():
|
||
"""26 Tätigkeitsblöcke à 10 Stellen (TART 2 + TVON 4 + TBIS 4)."""
|
||
felder = []
|
||
for block_nr in range(1, 27):
|
||
basis = 502 + (block_nr - 1) * 10
|
||
felder.extend([
|
||
(17 + (block_nr - 1) * 3, 'TART_%d' % block_nr,
|
||
basis, 2, 'a'),
|
||
(18 + (block_nr - 1) * 3, 'TVON_%d' % block_nr,
|
||
basis + 2, 4, 'n'),
|
||
(19 + (block_nr - 1) * 3, 'TBIS_%d' % block_nr,
|
||
basis + 6, 4, 'n'),
|
||
])
|
||
return felder
|
||
|
||
|
||
SCHWERARBEIT_SATZ += _taetigkeitsblock_struktur()
|
||
|
||
|
||
class L10nAtPayrollSchwerarbeitLine(models.Model):
|
||
_name = 'l10n.at.payroll.schwerarbeitsmeldung.line'
|
||
_description = 'Schwerarbeitsmeldung — Satzdaten je AN'
|
||
_order = 'employee_name'
|
||
|
||
meldung_id = fields.Many2one(
|
||
'l10n.at.payroll.schwerarbeitsmeldung', required=True,
|
||
ondelete='cascade', index=True)
|
||
employee_id = fields.Many2one('hr.employee', required=True)
|
||
employee_name = fields.Char(related='employee_id.name',
|
||
readonly=True, store=True)
|
||
versicherungsnummer = fields.Char()
|
||
taetigkeit_ids = fields.One2many(
|
||
'l10n.at.payroll.schwerarbeitsmeldung.taetigkeit', 'line_id',
|
||
string='Tätigkeitsblöcke')
|
||
refn = fields.Char()
|
||
|
||
|
||
class L10nAtPayrollSchwerarbeitTaetigkeit(models.Model):
|
||
_name = 'l10n.at.payroll.schwerarbeitsmeldung.taetigkeit'
|
||
_description = 'Schwerarbeitsmeldung — Tätigkeitsblock (§ 1 ASchV)'
|
||
_order = 'sequence'
|
||
|
||
sequence = fields.Integer(default=10)
|
||
line_id = fields.Many2one(
|
||
'l10n.at.payroll.schwerarbeitsmeldung.line', required=True,
|
||
ondelete='cascade', index=True)
|
||
ziffer = fields.Integer(
|
||
string='ASchV § 1 Abs 1 Ziffer', required=True,
|
||
help='TART-Codes 1–26 der Schwerarbeitsverordnung. Nicht zu '
|
||
'melden: Z 3 (MdE-Feststellung nachträglich) und Z 6 ohne '
|
||
'bekannte Pflegegeldeinstufung ab Stufe 3.')
|
||
von = fields.Date(string='Von (TTMM)')
|
||
bis = fields.Date(string='Bis (TTMM)')
|
||
|
||
@api.constrains('ziffer')
|
||
def _check_ziffer(self):
|
||
for taetigkeit in self:
|
||
if not 1 <= taetigkeit.ziffer <= 26:
|
||
raise UserError(
|
||
'ASchV-Ziffer %s außerhalb 1–26.' % taetigkeit.ziffer)
|
||
|
||
|
||
class L10nAtPayrollSchwerarbeitsmeldung(models.Model):
|
||
_name = 'l10n.at.payroll.schwerarbeitsmeldung'
|
||
_description = 'Schwerarbeitsmeldung (§ 74 Abs 3 ASVG / § 5 ASchV)'
|
||
_inherit = ['mail.thread']
|
||
_order = 'jahr desc, id desc'
|
||
|
||
company_id = fields.Many2one(
|
||
'res.company', required=True,
|
||
default=lambda self: self.env.company)
|
||
jahr = fields.Integer(required=True, string='Tätigkeitsjahr')
|
||
line_ids = fields.One2many(
|
||
'l10n.at.payroll.schwerarbeitsmeldung.line', 'meldung_id',
|
||
string='Meldesätze')
|
||
datei = fields.Binary(attachment=True, string='SM-Datei', 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 = [(
|
||
'sm_company_jahr_uniq', 'unique(company_id, jahr)',
|
||
'Für dieses Jahr existiert bereits eine Schwerarbeitsmeldung.')]
|
||
|
||
def _l10n_at_hersteller(self):
|
||
partner = self.company_id.partner_id
|
||
return {'name': 'odoo-at-payroll', 'kfz': 'A',
|
||
'plz': partner.zip, 'ort': partner.city,
|
||
'strasse': partner.street}
|
||
|
||
def action_erzeugen(self):
|
||
"""SART 65 je AN (Tätigkeitsblöcke 26 × 10) — Erstellvorschriften
|
||
(E.22.2): Z 1 ≥ 6 Nachtarbeitstage/Monat (analog NSchG Art XI
|
||
Abs 6), Z 2/4/5 ≥ 15 Arbeitstage/Monat, GfB ausgeschlossen,
|
||
Altersfilter M ≥ 40 / F ≥ 35 als Warnung."""
|
||
for meldung in self:
|
||
fehler = []
|
||
warnungen = []
|
||
company = meldung.company_id
|
||
bknr = company.l10n_at_beitragskontonummer
|
||
if not bknr:
|
||
fehler.append('Beitragskontonummer (BKNR) fehlt.')
|
||
partner = company.partner_id
|
||
# Zeilen werden je AN erfasst (mandantenseitige Fakten);
|
||
# der Lauf prüft die Erstellvorschriften und erzeugt die
|
||
# Sätze.
|
||
for line in meldung.line_ids:
|
||
employee = line.employee_id
|
||
version = employee.version_id
|
||
if not (version and version.ssnid):
|
||
fehler.append(
|
||
'Mitarbeiter %s ohne Versicherungsnummer.'
|
||
% employee.name)
|
||
continue
|
||
if not employee.birthday:
|
||
fehler.append(
|
||
'Mitarbeiter %s ohne Geburtsdatum.'
|
||
% employee.name)
|
||
continue
|
||
altersstand = meldung.jahr - employee.birthday.year
|
||
ist_frau = (version.sex == 'female')
|
||
grenze = 35 if ist_frau else 40
|
||
if altersstand < grenze:
|
||
warnungen.append(
|
||
'%s: %d Jahre am 31. 12. %d — unter der '
|
||
'Altersgrenze (%d %s); Meldung entfällt bzw. '
|
||
'Zeiten nur bei Begründung (lb-swa-05).'
|
||
% (employee.name, altersstand, meldung.jahr,
|
||
grenze, 'F' if ist_frau else 'M'))
|
||
if line.taetigkeit_ids.filtered(
|
||
lambda t: t.ziffer == 3):
|
||
warnungen.append(
|
||
'%s: Z 3 (chemisch/physikalisch) ist nicht '
|
||
'meldepflichtig (MdE ≥ 10 %% wird erst '
|
||
'nachträglich festgestellt — lb-swa-05).'
|
||
% employee.name)
|
||
if line.taetigkeit_ids.filtered(
|
||
lambda t: t.ziffer == 6):
|
||
warnungen.append(
|
||
'%s: Z 6 (Pflege) nur bei bekannter '
|
||
'Pflegegeldeinstufung ab Stufe 3 melden.'
|
||
% employee.name)
|
||
if fehler:
|
||
meldung.protokoll = 'FEHLER:\n%s' % '\n'.join(fehler)
|
||
raise UserError(
|
||
'Schwerarbeitsmeldungs-Lauf mit Fehlern abgebrochen:\n'
|
||
+ '\n'.join(fehler))
|
||
if meldung.line_ids:
|
||
heute = date.today()
|
||
jetzt = datetime.now()
|
||
datensaetze = []
|
||
for line in meldung.line_ids:
|
||
employee = line.employee_id
|
||
version = employee.version_id
|
||
satz = L10nAtEldaSatz(
|
||
L10N_AT_ELDA_IDTEIL + SCHWERARBEIT_SATZ, 800,
|
||
bezeichnung='SM %s' % employee.name)
|
||
satz.setze('SART', '65')
|
||
satz.setze('VSTR', (bknr or '')[:2])
|
||
satz.setze('BKNR', bknr)
|
||
satz.setze('DGNA', (company.name or '')[:70])
|
||
satz.setze('WOBD', 'EMP-%d' % employee.id)
|
||
satz.setze('DKFZ', 'A')
|
||
satz.setze('DPLZ', (partner.zip or '')[:9])
|
||
satz.setze('DORT', (partner.city or '')[:40])
|
||
satz.setze('DSTR', (partner.street or '')[:50])
|
||
satz.setze('VSNR', line.versicherungsnummer)
|
||
satz.setze_datum('GEBD', employee.birthday)
|
||
familienname, vorname = l10n_at_namen(employee)
|
||
satz.setze('FANA', (familienname or '')[:70])
|
||
satz.setze('VONA', (vorname or '')[:70])
|
||
satz.setze('JAHR', meldung.jahr)
|
||
for nummer, taetigkeit in enumerate(
|
||
line.taetigkeit_ids.sorted('sequence')[:26],
|
||
start=1):
|
||
satz.setze('TART_%d' % nummer,
|
||
'%02d' % taetigkeit.ziffer)
|
||
if taetigkeit.von:
|
||
satz.setze('TVON_%d' % nummer,
|
||
taetigkeit.von.strftime('%d%m'))
|
||
if taetigkeit.bis:
|
||
satz.setze('TBIS_%d' % nummer,
|
||
taetigkeit.bis.strftime('%d%m'))
|
||
satz.setze('REFN', 'SM-%d-%d-%d' % (
|
||
company.id, meldung.jahr, employee.id))
|
||
datensaetze.append(satz)
|
||
bestand = L10nAtEldaBestand(
|
||
'SM', (bknr or '')[:2], '02',
|
||
hersteller=meldung._l10n_at_hersteller(),
|
||
datensaetze=datensaetze)
|
||
datei = L10nAtEldaDatei([bestand])
|
||
meldung.write({
|
||
'datei': base64.b64encode(datei.render()),
|
||
'datei_name': 'schwerarbeit_%d.txt' % meldung.jahr,
|
||
})
|
||
meldung.protokoll = 'WARNUNGEN:\n%s' % (
|
||
warnungen and '\n'.join(warnungen) or '—')
|
||
meldung.state = 'done'
|
||
return True |