# 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