[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
@@ -73,12 +73,15 @@ Kein VRV-Bezug: die Gemeinde-Buchhaltung (Anlage 3b, Ansatz/MVAG) läuft
'data/rule_parameters_lst_2026.xml',
'data/rule_parameters_sv_2026.xml',
'data/rule_parameters_2027.xml',
'data/rule_parameters_109.xml',
'data/tasy_mapping.xml',
'data/hr_departure_reason_seed.xml',
'views/tasy_views.xml',
'views/tasy_katalog_views.xml',
'views/company_views.xml',
'views/meldewesen_views.xml',
'views/meldewesen_finanz_views.xml',
'views/schwerarbeitsmeldung_views.xml',
'views/fristen_views.xml',
'report/belege_reports.xml',
'report/jahresreports.xml',
@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- ============================================================ -->
<!-- AP17: Schwellen der Finanz-Mitteilungen (§ 109a/§ 109b -->
<!-- EStG). § 109a-VO: 450 € je Leistung / 900 € kumulativ je -->
<!-- Leistungserbringer (Fristenkalender/§-109a-VO). § 109b -->
<!-- Abs 4 lit a: 100.000 € je Leistungserbringer und -->
<!-- Kalenderjahr (RIS NOR40263383, verifiziert 2026-09-16). -->
<!-- ============================================================ -->
<record id="hr_rule_parameter_at_m109_schwellen" model="hr.rule.parameter">
<field name="name">AT § 109a/§ 109b EStG Schwellen</field>
<field name="code">at_m109_schwellen</field>
<field name="country_id" ref="base.at"/>
</record>
<record id="hr_rule_parameter_value_at_m109_schwellen" model="hr.rule.parameter.value">
<field name="rule_parameter_id" ref="hr_rule_parameter_at_m109_schwellen"/>
<field name="date_from">2026-01-01</field>
<field name="parameter_value">{'m109a_leistung': 450.0, 'm109a_kumulativ': 900.0, 'm109b_jahr': 100000.0}</field>
</record>
</odoo>
@@ -7,6 +7,9 @@ from . import elda
from . import meldewesen
from . import kind
from . import lohnzettel_l16
from . import lohnzettel_l17
from . import mitteilung_109
from . import schwerarbeitsmeldung
from . import fristenkalender
from . import jahresretro
from . import mitteilungsfile
@@ -92,7 +92,38 @@ FRISTART_DEFINITIONEN = {
'hilfetext': 'Meldung der Honorare außerhalb von '
'Dienstverhältnissen bis Ende Februar via '
'ELDA (8 Tätigkeitsgruppen; kumulativ ≤ 900 € '
'und ≤ 450 € je Leistung → entfällt).',
'und ≤ 450 € je Leistung → entfällt). '
'Fixsatz: l10n.at.payroll.mitteilung109 '
'(AP17).',
},
'mitteilung_109b': {
'name': '§ 109b-EStG-Meldung (Auslandszahlungen)',
'monatlich': False,
'monat': 2,
'tag_im_monat': 28,
'action': None,
'hilfetext': 'Mitteilung über Zahlungen ins Ausland '
'(§ 109b EStG: §-22-Tätigkeiten, Vermittlung, '
'Beratung im Inland) elektronisch bis Ende '
'Februar des Folgejahres; amtlicher Vordruck '
'bis Ende Jänner (§ 109b Abs 6, RIS '
'NOR40263383); entfällt unter der '
'100.000-€-Schwelle bzw. bei Steuerabzug '
'§ 99/KSt-Vergleich (Abs 4). Fixsatz: '
'l10n.at.payroll.mitteilung109 (AP17).',
},
'l17': {
'name': 'Lohnnachweis/Lohnbescheinigung L17 (A1)',
'monatlich': False,
'monat': 2,
'tag_im_monat': 28,
'action': 'l16',
'hilfetext': 'Lohnzettel Finanz je Lohnzahlungszeitraum '
'(DM-ORG E.24, Satzart A1, Version 28 — '
'zwingender Einsatz ab 01.03.2026, fachliche '
'Gültigkeit für Zeiträume ab 01.01.2025); '
'Übermittlung im Jahreslauf mit der L16 '
'(Ende Februar).',
},
'schwerarbeit': {
'name': 'Schwerarbeitsmeldung (ASchV § 5)',
@@ -224,7 +255,7 @@ class L10nAtPayrollFrist(models.Model):
# Jahresfristen nur am 1. Jänner (für das Vorjahr).
if heute.month == 1:
for art in ('l16', 'arbeitsstaette', 'paragraph_109a',
'schwerarbeit'):
'mitteilung_109b', 'l17', 'schwerarbeit'):
monat = FRISTART_DEFINITIONEN[art]['monat']
tag = FRISTART_DEFINITIONEN[art]['tag_im_monat']
jahr_deadline = heute.year
@@ -0,0 +1,464 @@
# Part of the odoo-at-payroll project. License: LGPL-3.
"""Lohnzettel Finanz — Lohnnachweis/Lohnbescheinigung L17 (AP17) —
DM-ORG 42.7.0, Kapitel E.24 (BEST ``LF``, VSTR ``94``, Satzart ``A1``,
Länge 1100, Version 28 — zwingender Einsatz ab 01.03.2026, fachliche
Gültigkeit für Zeiträume ab 01.01.2025; PDF-verifizierte ODL-Extraktion
2026-09-16 aus `.firecrawl/ris/elda/DM-ORG_*v42.7.0*.pdf`).
Der A1-Satz ist je **Lohnzahlungszeitraum** je Dienstnehmer aufzubauen
(D.32: BELZ/ENLZ/JALZ; REFN je AG+Jahr+Dienstverhältnis eindeutig und
auch bei Zeiträumsänderung unverändert). Kennzahlen-Semantik laut
Prüfkatalog L17 (Stand 12/2025, lokal `.firecrawl/ris/elda/`):
**KZ 350 = Bruttobezüge GESAMT**; die Teil-KZs 351 (Zuwendungen neben
Arbeitslohn), 352 (Abfertigungen/Abfindungen), 354 (normale ÜStd-
Zuschläge), 356/738 (Pensionsabfindungen), UEB, 394, KUR, ALT, SOP,
AUS, ZAKV, MAGB sind **Teilmengen** von 350 (Prüfkatalog-Formel:
Feld 28 < (Feld 30 + 32 + 34 + 36 + 38 + 50 + 72 + 74 + 76 + 80 + 82
+ 95 + 100) = Fehler). KZ 738/739 sind nur für Jahre bis 2017 zulässig
(Pensionsabfindung Pensionskasse).
KZ-Mapping aus den validierten Läufen (GP3-Kern-Flags, Muster
`lohnzettel_l16.py`):
- 350: laufende Bezüge + alle sonstigen §-67-Bezüge (gesamtes §-25-Brutto
des LZP inkl. 351/352/354)
- 351: sonstige Bezüge ohne Abs-3/6-Markierung (Sechstel-Route —
Sonderzahlungen und Zuwendungen)
- 352: sonstige Bezüge mit ``l10n_at_s67_abs`` = abs3/abs6
(Abfertigung Alt / freiwillige Abfertigung / Abfindungen)
- 354: § 68-freie Überstundenzuschläge (``_l10n_at_freibetraege_68``)
- 357: einbehaltene SV-Beiträge des laufenden Arbeitslohns
(SVDN_KV/PV/AV/AK/WF)
- 347: einbehaltene SV-Beiträge für Zuwendungen (SVDN_SZ)
- nicht automatisiert (dokumentierter Rest, Grundstellung 0 + Protokoll-
Hinweis): 356/738 Pensionsabfindungen, 736/737/739 SV-Zerlegung auf
Abfindungs-KZs, BUEB, 358, BSPE, BARB, WERK, UEBMO, ANZAT, TSTAAT,
TEPR/MAPR/ZAKV/ANKV/MAGB, KOUN, 394, KUR, ALT, SOP, AUS.
Formale Prüfungen nach Prüfkatalog L17: KZ-350-Formel, 357 ≤ 40 % × 350,
347 ≤ 25 % × 351, 354 ≤ 2.400 € (+1 %) je LZP aliquotiert, HOPA ≤ 3 € ×
HOTA und ≤ 300 € je LZP, DIEN ≤ 99, WERK/UEBMO ≤ 12, ANZAT ≤ 366,
HOTA ≤ LZP-Tage."""
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 L16_INFORMATIONSSATZ
# ------------------------------------------------------------------
# Mitteilungssatz A1 (E.24; Länge 1100) — Datenteil
# ------------------------------------------------------------------
L17_SATZ = [
(2, 'FSART', 21, 1, 'a'), # A
(3, 'CLADR', 22, 6, 'a'),
(4, 'CLDVR', 28, 7, 'a'),
(6, 'STNRL', 35, 9, 'n'), # Steuernummer Lohnverrechnungsstelle
(7, 'DVRNL', 44, 7, 'n'),
(8, 'ARTU', 51, 1, 'a'),
(9, 'DTUE', 52, 8, 'n'), # JJJJMMTT, wie I1
(10, 'ZTUE', 60, 6, 'n'),
(11, 'REFN', 66, 30, 'a'), # je AG+Jahr+DV eindeutig
(12, 'RE12', 96, 2, 'a'),
(13, 'FEHL', 98, 20, 'a'),
(14, 'STAT', 118, 1, 'a'),
(15, 'FIND', 119, 1, 'a'), # 'J' = vom Hersteller für richtig
(16, 'BELZ', 120, 4, 'n'), # Beginn LZP TTMM
(17, 'ENLZ', 124, 4, 'n'), # Ende LZP TTMM
(18, 'JALZ', 128, 4, 'n'), # Jahr LZP
(19, 'PENS', 132, 1, 'a'), # J/N
(20, 'AVSN', 133, 10, 'n'), # LLLPTTMMJJ
(21, 'ANAM', 143, 37, 'a'),
(22, 'ATIT', 180, 20, 'a'),
(23, 'AADR', 200, 37, 'a'),
(24, 'ALKZ', 237, 3, 'a'),
(25, 'APLZ', 240, 10, 'a'),
(26, 'AORT', 250, 30, 'a'),
(27, 'V350', 280, 1, 'a'),
(28, 'B350', 281, 10, 'n'), # Bruttobezüge gesamt (KZ 350)
(29, 'V351', 291, 1, 'a'),
(30, 'B351', 292, 10, 'n'), # Zuwendungen neben Arbeitslohn
(31, 'V352', 302, 1, 'a'),
(32, 'B352', 303, 10, 'n'), # Abfertigungen und Abfindungen
(33, 'V354', 313, 1, 'a'),
(34, 'B354', 314, 10, 'n'), # (normale) ÜStd-Zuschläge
(35, 'V356', 324, 1, 'a'),
(36, 'B356', 325, 10, 'n'), # Pensionsabfindung Arbeitgeber
(37, 'V738', 335, 1, 'a'),
(38, 'B738', 336, 10, 'n'), # Pensionsabfindung Pensionskasse
(39, 'V357', 346, 1, 'a'),
(40, 'B357', 347, 10, 'n'), # SV-Beiträge laufender Arbeitslohn
(41, 'V347', 357, 1, 'a'),
(42, 'B347', 358, 10, 'n'), # SV-Beiträge für Zuwendungen
(43, 'V736', 368, 1, 'a'),
(44, 'B736', 369, 10, 'n'), # SV-Beiträge für KZ 352
(45, 'V737', 379, 1, 'a'),
(46, 'B737', 380, 10, 'n'), # SV-Beiträge für KZ 356
(47, 'V739', 390, 1, 'a'),
(48, 'B739', 391, 10, 'n'), # SV-Beiträge für KZ 738
(49, 'VUEB', 401, 1, 'a'),
(50, 'BUEB', 402, 10, 'n'), # übrige Bezüge außerhalb Österreichs
(51, 'RE51', 412, 1, 'a'),
(52, 'RE52', 413, 10, 'a'),
(53, 'RE53', 423, 1, 'a'),
(54, 'RE54', 424, 10, 'a'),
(55, 'V358', 434, 1, 'a'),
(56, 'B358', 435, 10, 'n'), # anrechenbare ausländische Steuer
(57, 'VSPE', 445, 1, 'a'),
(58, 'BSPE', 446, 10, 'n'), # Spesensätze außerhalb KZ 350
(59, 'VARB', 456, 1, 'a'),
(60, 'BARB', 457, 10, 'n'), # AG-Beiträge an Pensionskassen
(61, 'DIEN', 467, 2, 'n'), # Dienstzeit (Jahre) bei KZ 352
(62, 'STDT', 469, 8, 'n'), # Sterbedatum JJJJMMTT
(63, 'OELD', 477, 20, 'a'), # Ordnungsbegriff ELDA
(64, 'KORR', 497, 1, 'a'), # 'K'/'S'
(65, 'INTE', 498, 11, 'a'),
(66, 'WOBD', 509, 12, 'a'),
(67, 'ZOBD', 521, 15, 'a'),
(68, 'WAHR', 536, 3, 'a'), # Währung
(69, 'MUEB', 539, 2, 'n'), # Monate überwiegende Nachtarbeit
(70, 'V394', 541, 1, 'a'),
(71, 'B394', 542, 10, 'n'), # Sonn-/Feiertags-/Nachtzuschläge
(72, 'VKUR', 552, 1, 'a'),
(73, 'BKUR', 553, 10, 'n'), # Kurzarbeit
(74, 'VALT', 563, 1, 'a'),
(75, 'BALT', 564, 10, 'n'), # Altersteilzeit
(76, 'RE76', 574, 1, 'a'),
(77, 'RE77', 575, 10, 'n'),
(78, 'VSOP', 585, 1, 'a'),
(79, 'BSOP', 586, 10, 'n'), # Sozialplanzahlungen
(80, 'VAUS', 596, 1, 'a'),
(81, 'BAUS', 597, 10, 'n'), # begünstigte Auslandstätigkeit
(82, 'WERK', 607, 2, 'n'), # Werkverkehr (Monate)
(83, 'UEBMO', 609, 2, 'n'), # Überlassung Kfz (Monate)
(84, 'ANZAT', 611, 3, 'n'), # Arbeitstage begünstigte Auslandstätigk.
(85, 'HOTA', 614, 3, 'n'), # Telearbeitstage (Homeoffice)
(86, 'VHOPA', 617, 1, 'a'),
(87, 'HOPA', 618, 10, 'n'), # Homeoffice-Pauschale (§ 26 Z 9 lit. a)
(88, 'VKOUN', 628, 1, 'a'),
(89, 'KOUN', 629, 10, 'n'), # Kostenübernahme § 26 Z 5 lit. b
(90, 'TSTAAT', 639, 3, 'a'), # Tätigkeitsstaat
(91, 'VTEPR', 642, 1, 'a'),
(92, 'TEPR', 643, 10, 'n'), # Teuerungsprämie (§ 124b Z 408)
(93, 'VMAPR', 653, 1, 'a'),
(94, 'MAPR', 654, 10, 'n'), # Mitarbeiterprämie (§ 124b Z 478)
(95, 'VZAKV', 664, 1, 'a'),
(96, 'ZAKV', 665, 10, 'n'), # AG-Zuschuss freiw. KV
(97, 'ANKV', 675, 1, 'a'), # (freiw.) gesetzlich krankenversichert
(98, 'VMAGB', 676, 1, 'a'),
(99, 'MAGB', 677, 10, 'n'), # Mitarbeitergewinnbeteiligung
(100, 'RE100', 687, 314, 'a'),
(101, 'REFN2', 1001, 30, 'a'), # Softwarehersteller-REFN
(102, 'RE102', 1031, 70, 'a'),
]
class L10nAtPayrollL17Line(models.Model):
_name = 'l10n.at.payroll.l17.line'
_description = 'L17 — Lohnnachweis je Dienstnehmer und LZP'
_order = 'employee_name, date_from'
l17_id = fields.Many2one(
'l10n.at.payroll.l17', 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)
date_from = fields.Date(string='LZP von', required=True)
date_to = fields.Date(string='LZP bis', required=True)
versicherungsnummer = fields.Char()
kz350 = fields.Monetary(currency_field='currency_id')
kz351 = fields.Monetary(currency_field='currency_id')
kz352 = fields.Monetary(currency_field='currency_id')
kz354 = fields.Monetary(currency_field='currency_id')
kz357 = fields.Monetary(currency_field='currency_id')
kz347 = fields.Monetary(currency_field='currency_id')
hota = fields.Integer(string='Homeoffice-Tage')
hopa = fields.Monetary(currency_field='currency_id')
mueb = fields.Integer(string='Monate überwiegende Nachtarbeit')
refn = fields.Char()
currency_id = fields.Many2one(
'res.currency', related='l17_id.company_id.currency_id')
def _l10n_at_pruefungen(self, fehler, warnungen):
"""Formale Prüfungen nach Prüfkatalog L17 (Stand 12/2025)."""
self.ensure_one()
lzp_tage = max(1, (self.date_to - self.date_from).days + 1)
teilsumme = self.kz351 + self.kz352 + self.kz354
if self.kz350 + 0.02 < teilsumme:
fehler.append(
'%s (%s): KZ 350 < 351+352+354 (Prüfkatalog-Formel).'
% (self.employee_name, self.date_from))
if self.kz357 > self.kz350 * 0.4 + 0.02:
fehler.append(
'%s (%s): KZ 357 > 40 %% von KZ 350 (Prüfkatalog).'
% (self.employee_name, self.date_from))
if self.kz351 and self.kz347 > self.kz351 * 0.25 + 0.02:
fehler.append(
'%s (%s): KZ 347 > 25 %% von KZ 351 (Prüfkatalog).'
% (self.employee_name, self.date_from))
if self.kz354 > 2400.0 * lzp_tage / 365.0 * 1.01 + 0.02:
warnungen.append(
'%s (%s): KZ 354 übersteigt den aliquotierten '
'Prüfkatalog-Cap 2.400 € je Jahr (+1 %% Toleranz).'
% (self.employee_name, self.date_from))
if self.hota > lzp_tage:
fehler.append(
'%s (%s): HOTA > LZP-Tage.' % (
self.employee_name, self.date_from))
if self.hopa > self.hota * 3.0 + 0.02:
fehler.append(
'%s (%s): HOPA > 3 € × HOTA.' % (
self.employee_name, self.date_from))
if self.hopa > 300.0 * lzp_tage / 365.0 * 1.01 + 0.02:
warnungen.append(
'%s (%s): HOPA übersteigt den aliquotierten '
'Prüfkatalog-Cap 300 € je Jahr.' % (
self.employee_name, self.date_from))
class L10nAtPayrollL17(models.Model):
_name = 'l10n.at.payroll.l17'
_description = 'L17 — Lohnnachweis/Lohnbescheinigung (Finanz)'
_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='Jahr')
line_ids = fields.One2many(
'l10n.at.payroll.l17.line', 'l17_id', string='A1-Sätze')
datei = fields.Binary(attachment=True, string='L17-Datei', copy=False)
datei_name = fields.Char(copy=False)
protokoll = fields.Text(
string='Prüfprotokoll',
help='Formale Prüfung nach Prüfkatalog L17 (Stand 12/2025) und '
'dokumentierte Reste der KZ-Belegung.')
state = fields.Selection(
[('draft', 'Entwurf'), ('done', 'Erzeugt')], default='draft',
required=True, tracking=True)
_sql_constraints = [(
'l17_company_jahr_uniq', 'unique(company_id, jahr)',
'Für dieses Jahr existiert bereits eine L17-Deklaration.')]
def _l10n_at_slips(self):
self.ensure_one()
return self.env['hr.payslip'].search([
('company_id', '=', self.company_id.id),
('date_from', '>=', date(self.jahr, 1, 1)),
('date_from', '<=', date(self.jahr, 12, 31)),
('state', 'in', ('validated', 'paid')),
], order='date_from, id')
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}
@api.model
def _vorzeichen(self, betrag):
if not betrag:
return None
return '+' if betrag > 0 else '-'
def _l10n_at_kz_salden(self, slip):
"""(salden, hota) je Lohnzahlungszeitraum aus dem Lauf —
Kennzahlen-Mapping siehe Modul-Doku (GP3-Flags, Muster
`_kz_salden` der L16-Zeile)."""
self.ensure_one()
salden = dict.fromkeys(
('kz350', 'kz351', 'kz352', 'kz354', 'kz357', 'kz347',
'hopa'), 0.0)
hota = 0
for line in slip.line_ids:
rule = line.salary_rule_id
if not line.total:
continue
if rule.l10n_at_bemessung == 'laufend':
salden['kz350'] += line.total
if line.code == 'SB_HOMEOFFICE':
salden['hopa'] += line.total
elif rule.l10n_at_bemessung == 'sonstig':
salden['kz350'] += line.total
if rule.l10n_at_s67_abs in ('abs3', 'abs6'):
salden['kz352'] += line.total
else:
salden['kz351'] += line.total
elif line.code == 'SVDN_SZ':
salden['kz347'] += line.total
elif line.code in ('SVDN_KV', 'SVDN_PV', 'SVDN_AV',
'SVDN_AK', 'SVDN_WF'):
salden['kz357'] += line.total
freibe = slip._l10n_at_freibetraege_68()
salden['kz354'] = freibe['ue_std']
for eingabe in slip.input_line_ids:
if eingabe.input_type_id.code == 'ATP_SB_HOMEOFFICE':
hota += int(eingabe.quantity or 0)
return salden, hota
def _l10n_at_a1_satz(self, line, stnr, heute, jetzt):
self.ensure_one()
employee = line.employee_id
version = employee.version_id
satz = L10nAtEldaSatz(
L10N_AT_ELDA_IDTEIL + L17_SATZ, 1100,
bezeichnung='A1 %s %s' % (employee.name, line.date_from))
satz.setze('SART', 'A1')
satz.setze('VSTR', '94')
satz.setze('FSART', 'A')
satz.setze('STNRL', stnr)
satz.setze_datum('DTUE', heute, 'JJJJMMTT')
satz.setze_zeit('ZTUE', jetzt)
satz.setze('REFN', line.refn)
satz.setze('FIND', 'J')
satz.setze('BELZ', line.date_from.strftime('%d%m'))
satz.setze('ENLZ', line.date_to.strftime('%d%m'))
satz.setze('JALZ', self.jahr)
satz.setze('PENS', 'N')
satz.setze('AVSN', line.versicherungsnummer)
satz.setze('ANAM', (employee.name or '')[:37])
satz.setze('AADR', (version.private_street or '')[:37]
if version else '')
satz.setze('ALKZ', (version.private_country_id.code
if version and version.private_country_id
else 'AUT'))
satz.setze('APLZ', version.private_zip if version else '')
satz.setze('AORT', (version.private_city or '')[:30]
if version else '')
for betrag, vfeld, bfeld, negativ in (
(line.kz350, 'V350', 'B350', False),
(line.kz351, 'V351', 'B351', False),
(line.kz352, 'V352', 'B352', True),
(line.kz354, 'V354', 'B354', True),
(line.kz357, 'V357', 'B357', False),
(line.kz347, 'V347', 'B347', True)):
if betrag < 0 and not negativ:
raise UserError(
'%s (%s): negativer Betrag in %s — das Vorzeichenfeld '
'zulässigerweise "-" nur nach DM-ORG-Feldbeschreibung.'
% (employee.name, line.date_from, vfeld))
satz.setze(vfeld, self._vorzeichen(betrag))
satz.setze(bfeld, abs(betrag) if betrag else 0, cent=True)
if line.mueb:
satz.setze('MUEB', line.mueb)
if line.hota:
satz.setze('HOTA', line.hota)
if line.hopa:
satz.setze('VHOPA', self._vorzeichen(line.hopa))
satz.setze('HOPA', abs(line.hopa), cent=True)
satz.setze('WOBD', 'EMP-%d' % employee.id)
satz.setze('REFN2', line.refn)
return satz
def action_erzeugen(self):
"""A1 je (Dienstnehmer, Lohnzahlungszeitraum) aus validierten
Läufen; formale Prüfungen nach Prüfkatalog L17; Fehler brechen
ab (UserError, DmfA-Muster), Warnungen ins Protokoll."""
for deklaration in self:
fehler = []
warnungen = []
company = deklaration.company_id
if not company.l10n_at_lohnsteuernummer:
fehler.append(
'Lohnsteuernummer der Company fehlt '
'(l10n_at_lohnsteuernummer).')
deklaration.line_ids.unlink()
for slip in deklaration._l10n_at_slips():
version = slip.version_id or slip.employee_id.version_id
if not (version and version.ssnid):
fehler.append(
'Mitarbeiter %s ohne Versicherungsnummer '
'(hr.version.ssnid).' % slip.employee_id.name)
continue
if not slip.employee_id.birthday:
fehler.append(
'Mitarbeiter %s ohne Geburtsdatum.'
% slip.employee_id.name)
continue
salden, hota = deklaration._l10n_at_kz_salden(slip)
refn = 'L17-%d-%d-%d-%s' % (
company.id, deklaration.jahr, slip.employee_id.id,
slip.date_from.strftime('%Y%m%d'))
line = deklaration.env['l10n.at.payroll.l17.line'].create({
'l17_id': deklaration.id,
'employee_id': slip.employee_id.id,
'date_from': slip.date_from,
'date_to': slip.date_to,
'versicherungsnummer': version.ssnid,
'kz350': salden['kz350'],
'kz351': salden['kz351'],
'kz352': salden['kz352'],
'kz354': salden['kz354'],
'kz357': salden['kz357'],
'kz347': salden['kz347'],
'hota': hota,
'hopa': salden['hopa'],
'mueb': 0,
'refn': refn,
})
line._l10n_at_pruefungen(fehler, warnungen)
if deklaration.line_ids:
for line in deklaration.line_ids:
if line.kz352:
warnungen.append(
'%s: KZ 352 belegt — die SV-Zerlegung auf '
'KZ 736 bleibt dokumentierter Rest (v1 = 0); '
'Prüfkatalog-Grenze 40 %% × KZ 352 prüfen.'
% line.employee_name)
if fehler:
deklaration.protokoll = 'FEHLER:\n%s\nWARNUNGEN:\n%s' % (
'\n'.join(fehler), '\n'.join(warnungen) or '')
raise UserError('L17-Lauf mit Fehlern abgebrochen:\n'
+ '\n'.join(fehler))
if deklaration.line_ids:
heute = date.today()
jetzt = datetime.now()
partner = company.partner_id
stnr = company.l10n_at_lohnsteuernummer
informationssatz = L10nAtEldaSatz(
L10N_AT_ELDA_IDTEIL + L16_INFORMATIONSSATZ, 1100,
bezeichnung='L17-Informationssatz')
informationssatz.setze('SART', 'I1')
informationssatz.setze('VSTR', '94')
informationssatz.setze('FSART', 'I')
informationssatz.setze('CLEAR', 'RADAUS')
informationssatz.setze('STNRL', stnr)
informationssatz.setze('STNRA', stnr)
informationssatz.setze('ARTD', 'LZ')
informationssatz.setze_datum('DTUE', heute, 'JJJJMMTT')
informationssatz.setze_zeit('ZTUE', jetzt)
informationssatz.setze('STVE', '03')
informationssatz.setze('ANAM', (partner.name or '')[:37])
informationssatz.setze('AADR', (partner.street or '')[:37])
informationssatz.setze('ALKZ', 'AUT')
informationssatz.setze('APLZ', partner.zip)
informationssatz.setze('AORT', (partner.city or '')[:30])
informationssatz.setze('JAHR', deklaration.jahr)
informationssatz.setze('GESA', len(deklaration.line_ids))
informationssatz.setze('ANZA', len(deklaration.line_ids))
datensaetze = [informationssatz] + [
deklaration._l10n_at_a1_satz(line, stnr, heute, jetzt)
for line in deklaration.line_ids]
bestand = L10nAtEldaBestand(
'LF', '94', '28', hersteller=deklaration
._l10n_at_hersteller(), datensaetze=datensaetze)
datei = L10nAtEldaDatei([bestand])
deklaration.write({
'datei': base64.b64encode(datei.render()),
'datei_name': 'l17_%d_%s.txt' % (
deklaration.jahr, stnr),
})
deklaration.protokoll = 'WARNUNGEN:\n%s' % (
'\n'.join(warnungen) or '')
deklaration.state = 'done'
return True
@@ -0,0 +1,573 @@
# Part of the odoo-at-payroll project. License: LGPL-3.
"""Finanz-Mitteilungen § 109a/§ 109b EStG (AP17) — DM-ORG 42.7.0,
Kapitel E.16 (Satzart ``W1``, Code „W“, Länge 1100, Version 28) und
E.26 (Satzart ``B1``, Code „B“, Länge 1100, Version 28); PDF-verifizierte
ODL-Extraktion 2026-09-16. Beide Mitteilungssätze laufen im BEST ``LF`` /
VSTR ``94`` mit dem Informationssatz I1 (ARTD „LZ“, D.4).
Rechtsanker (RIS-verifiziert 2026-09-16, `.additional_docs/ris_normtexte`
-Regime: NormDokument.wxe-Downloads):
- **§ 109a EStG** (NOR40263382, Fassung 20.07.2024): Mitteilungen über
Leistungen von (Sub-)Unternehmern/PVG — Name/Anschrift/SVN bzw. StNr,
Art der Leistung (D.34: 8 Tätigkeitsgruppen), Kalenderjahr, Entgelt +
Umsatzsteuer. Entgelt = ohne USt einschließlich Kostenersätze und
Sachbezüge; bei freien Dienstnehmern inkl. DN-SV-Anteil und
BMV-Kassen-Zahlungen (B270/B271).
- **§ 109b EStG** (NOR40263383, Fassung 20.07.2024): Zahlungen INS
AUSLAND für Leistungen § 22 (AL31), Vermittlung (AL32), kaufm./techn.
Beratung im Inland (AL33). Angaben je Leistungserbringer (incl.
Länderkennung(en) der Zielländer, UID) sowie je Personengemeinschaft/
Körperschaft die im Inland maßgeblich auftretende natürliche Person
(bis 3 NP-Blöcke). Mitteilung unterbleibt, wenn a) die Kalenderjahres-
Zahlungen desselben Leistungserbringers 100.000 € nicht übersteigen,
b) ein Steuerabzug § 99 zu erfolgen hat, c) die ausländische KSt um
nicht mehr als 10 %-Punkte unter der österreichischen KSt liegt
(Abs 4; a) automatisiert, b)/c) als Fallauswahl).
- **Fristen** (Fristenkalender): § 109a — Ende Februar (Fristart
`paragraph_109a`); § 109b Abs 6 — elektronisch bis Ende Februar des
Folgejahres (amtlicher Vordruck bis Ende Jänner).
Schwellen als `hr.rule.parameter` `at_m109_schwellen` (versioniert):
m109a_leistung 450 €, m109a_kumulativ 900 € (je Leistungserbringer,
Kalenderjahr — Fristenkalender/§-109a-VO), m109b_jahr 100.000 €.
Storno: KORR „S“ (W1/B1) — E.16.1/E.26.1 zulassen beide Varianten
(Beträge auf 0 oder KORR „S“)."""
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 L16_INFORMATIONSSATZ
# ------------------------------------------------------------------
# W1 — Mitteilungen gemäß § 109a EStG (E.16; Länge 1100) — Datenteil
# ------------------------------------------------------------------
MITTEILUNG_W1_SATZ = [
(2, 'FSART', 21, 1, 'a'), # W
(3, 'CLADR', 22, 6, 'a'),
(4, 'CLDVR', 28, 7, 'a'),
(6, 'STNRL', 35, 9, 'n'),
(7, 'DVRNL', 44, 7, 'n'),
(8, 'ARTU', 51, 1, 'a'),
(9, 'DTUE', 52, 8, 'n'), # JJJJMMTT, wie I1
(10, 'ZTUE', 60, 6, 'n'),
(11, 'REFN', 66, 30, 'a'), # je AG+Jahr+Auftragsverhältnis eindeutig
(12, 'INTE', 96, 2, 'a'),
(13, 'FEHL', 98, 20, 'a'),
(14, 'STAT', 118, 1, 'a'),
(15, 'FIND', 119, 1, 'a'),
(16, 'JAHR', 120, 4, 'n'),
(17, 'AVSN', 124, 10, 'n'), # Auftragnehmer VN LLLPTTMMJJ
(19, 'ASTN', 134, 9, 'n'), # Auftragnehmer Steuernummer
(20, 'ANAM', 143, 37, 'a'),
(21, 'ATIT', 180, 20, 'a'),
(22, 'AADR', 200, 37, 'a'),
(23, 'ALKZ', 237, 3, 'a'),
(24, 'APLZ', 240, 10, 'a'),
(25, 'AORT', 250, 30, 'a'),
(26, 'ARTE', 280, 2, 'n'), # Art der erbrachten Leistung (D.34)
(27, 'V341', 282, 1, 'a'),
(28, 'B341', 283, 10, 'n'), # Entgelt (KZ 341)
(29, 'V344', 293, 1, 'a'),
(30, 'B344', 294, 10, 'n'), # Umsatzsteuer (KZ 344)
(31, 'FANRA', 304, 2, 'n'), # Finanzamtsnummer Auftraggeber
(32, 'STNRA', 306, 7, 'n'), # Steuernummer Auftraggeber
(33, 'DVRNA', 313, 7, 'n'), # DVR-Nummer Auftraggeber
(34, 'TELN', 320, 20, 'a'),
(35, 'FAXN', 340, 20, 'a'),
(36, 'KORR', 360, 1, 'a'), # K/S
(37, 'WOBD', 361, 12, 'a'),
(38, 'ZOBD', 373, 15, 'a'),
(39, 'V270', 388, 1, 'a'),
(40, 'B270', 389, 10, 'n'), # DN-Anteil SV (KZ 270)
(41, 'V271', 399, 1, 'a'),
(42, 'B271', 400, 10, 'n'), # eingezahlter BMV-Kasse-Beitrag
(43, 'RE43', 410, 591, 'a'),
(44, 'REFN2', 1001, 30, 'a'),
(45, 'RE45', 1031, 70, 'a'),
]
# ------------------------------------------------------------------
# B1 — Mitteilungen gemäß § 109b EStG (E.26; Länge 1100) — Datenteil
# ------------------------------------------------------------------
MITTEILUNG_B1_SATZ = [
(2, 'FSART', 21, 1, 'a'), # B
(3, 'CLADR', 22, 6, 'a'),
(4, 'CLDVR', 28, 7, 'a'),
(6, 'STNRL', 35, 9, 'n'),
(7, 'DVRNL', 44, 7, 'n'),
(8, 'ARTU', 51, 1, 'a'),
(9, 'DTUE', 52, 8, 'n'), # JJJJMMTT, wie I1
(10, 'ZTUE', 60, 6, 'n'),
(11, 'REFN', 66, 30, 'a'), # je AG+Jahr+Auftragsverhältnis eindeutig
(12, 'INTE', 96, 2, 'a'),
(13, 'FEHL', 98, 20, 'a'),
(14, 'STAT', 118, 1, 'a'),
(15, 'FIND', 119, 1, 'a'),
(16, 'JAHR', 120, 4, 'n'),
(17, 'LVSN', 124, 10, 'n'), # Leistungserbringer VN LLLPTTMMJJ
(19, 'LSTN', 134, 9, 'n'), # Leistungserbringer Steuernummer
(20, 'LNAM', 143, 37, 'a'),
(21, 'RE21', 180, 20, 'a'),
(22, 'LADR', 200, 37, 'a'),
(23, 'LLKZ', 237, 3, 'a'),
(24, 'LPLZ', 240, 10, 'a'),
(25, 'LORT', 250, 30, 'a'),
(26, 'RE26', 280, 2, 'a'),
(27, 'V499', 282, 1, 'a'),
(28, 'B499', 283, 10, 'n'), # Zahlungen inkl. USt (KZ 499)
(29, 'V344', 293, 1, 'a'),
(30, 'B344', 294, 10, 'n'), # darin enthaltene USt (KZ 344)
(32, 'STNRA', 304, 9, 'n'), # Steuernummer Auftraggeber
(33, 'RE33', 313, 7, 'a'),
(34, 'TELN', 320, 20, 'a'),
(35, 'FAXN', 340, 20, 'a'),
(36, 'KORR', 360, 1, 'a'), # K/S
(37, 'AL31', 361, 1, 'a'), # § 109b Abs 2 Z 1 (§-22-Tätigkeiten)
(38, 'AL32', 362, 1, 'a'), # Z 2 Vermittlungsleistungen
(39, 'AL33', 363, 1, 'a'), # Z 3 kaufm./techn. Beratung
(40, 'LKZ1', 364, 3, 'a'),
(41, 'LKZ2', 367, 3, 'a'),
(42, 'LKZ3', 370, 3, 'a'),
(43, 'LKZ4', 373, 3, 'a'),
(44, 'LKZ5', 376, 3, 'a'),
(45, 'LKZ6', 379, 3, 'a'),
(46, 'LKZ7', 382, 3, 'a'),
(47, 'UIDL', 385, 15, 'a'), # UID Leistungserbringer
(48, 'UIDA', 400, 15, 'a'), # UID Auftraggeber
(49, 'VSNRA', 415, 10, 'n'), # VN Auftraggeber
(50, 'NPVS1', 425, 10, 'n'), # NP-Block 1 (maßgebliche Person)
(51, 'NPFA1', 435, 2, 'n'),
(52, 'NPST1', 437, 7, 'n'),
(53, 'NPNA1', 444, 30, 'a'),
(54, 'NPAD1', 474, 30, 'a'),
(55, 'NPLK1', 504, 3, 'a'),
(56, 'NPPL1', 507, 10, 'a'),
(57, 'NPOR1', 517, 30, 'a'),
(58, 'NPUI1', 547, 15, 'a'),
(59, 'NPVS2', 562, 10, 'n'),
(60, 'NPFA2', 572, 2, 'n'),
(61, 'NPST2', 574, 7, 'n'),
(62, 'NPNA2', 581, 30, 'a'),
(63, 'NPAD2', 611, 30, 'a'),
(64, 'NPLK2', 641, 3, 'a'),
(65, 'NPPL2', 644, 10, 'a'),
(66, 'NPOR2', 654, 30, 'a'),
(67, 'NPUI2', 684, 15, 'a'),
(68, 'NPVS3', 699, 10, 'n'),
(69, 'NPFA3', 709, 2, 'n'),
(70, 'NPST3', 711, 7, 'n'),
(71, 'NPNA3', 718, 30, 'a'),
(72, 'NPAD3', 748, 30, 'a'),
(73, 'NPLK3', 778, 3, 'a'),
(74, 'NPPL3', 781, 10, 'a'),
(75, 'NPOR3', 791, 30, 'a'),
(76, 'NPUI3', 821, 15, 'a'),
(77, 'WOBD', 836, 12, 'a'),
(78, 'ZOBD', 848, 15, 'a'),
(79, 'RE79', 863, 123, 'a'),
(80, 'STAT', 986, 5, 'a'),
(81, 'NMEL', 991, 7, 'n'),
(82, 'RE82', 998, 3, 'a'),
(83, 'REFN2', 1001, 30, 'a'),
(84, 'RE84', 1031, 70, 'a'),
]
# W1/B1 gemeinsame Vorzeichen-/Betragsfelder je Zeile.
_BETRAEGE_W1 = (('entgelt', 'V341', 'B341', True),
('umsatzsteuer', 'V344', 'B344', True),
('dn_sv_anteil', 'V270', 'B270', True),
('bmv_beitrag', 'V271', 'B271', True))
_BETRAEGE_B1 = (('entgelt', 'V499', 'B499', True),
('umsatzsteuer', 'V344', 'B344', True))
class L10nAtPayrollMitteilung109Np(models.Model):
_name = 'l10n.at.payroll.mitteilung109.np'
_description = '§ 109b — maßgeblich auftretende natürliche Person'
_order = 'sequence'
sequence = fields.Integer(default=10)
line_id = fields.Many2one(
'l10n.at.payroll.mitteilung109.line', required=True,
ondelete='cascade', index=True)
versicherungsnummer = fields.Char(string='VN (LLLPTTMMJJ)')
finanzamtsnummer = fields.Char(size=2)
steuernummer = fields.Char(size=7)
name = fields.Char(string='Name (Familienname Vorname)')
adresse = fields.Char()
landeskennung = fields.Char(size=3)
plz = fields.Char(size=10)
ort = fields.Char()
uid = fields.Char(string='UID-Nummer')
class L10nAtPayrollMitteilung109Line(models.Model):
_name = 'l10n.at.payroll.mitteilung109.line'
_description = 'Finanz-Mitteilung je Auftraggeber/Auftragnehmer'
_order = 'name_partner'
deklaration_id = fields.Many2one(
'l10n.at.payroll.mitteilung109', required=True,
ondelete='cascade', index=True)
name_partner = fields.Char(
string='Auftragnehmer/Leistungserbringer', required=True)
versicherungsnummer = fields.Char(
string='VN (LLLPTTMMJJ)',
help='D.6-Format; statt VN ist das Geburtsdatenformat ohne '
'VN-Prüfziffer zulässig (Storno-Regel der Kapitel E.16/E.26).')
steuernummer = fields.Char(string='Steuernummer (7, Prüfziffer an 7)')
uid = fields.Char(string='UID-Nummer')
name = fields.Char(string='Familienname und Vorname')
adresse = fields.Char()
landeskennung = fields.Char(size=3, default='AUT')
plz = fields.Char(size=10)
ort = fields.Char(size=30)
entgelt = fields.Monetary(
currency_field='currency_id',
help='W1: Entgelt ohne USt inkl. Kostenersätze und Sachbezüge '
'(bei freien Dienstnehmern inkl. DN-SV-Anteil und BMV-Kasse). '
'B1: Zahlungen einschließlich Umsatzsteuer (KZ 499).')
umsatzsteuer = fields.Monetary(
currency_field='currency_id',
help='W1: in Rechnung gestellte USt (KZ 344); B1: darin '
'enthaltene USt (KZ 344).')
dn_sv_anteil = fields.Monetary(
currency_field='currency_id',
string='DN-Anteil SV (KZ 270, W1)')
bmv_beitrag = fields.Monetary(
currency_field='currency_id',
string='BMV-Kasse-Beitrag (KZ 271, W1)')
art_leistung = fields.Selection(
[(code, label) for code, label in (
('01', 'Aufsichtsrat/Verwaltungsrat'),
('02', 'Bausparkassen-/Versicherungsvertreter'),
('03', 'Stiftungsvorstand'),
('04', 'Vortragende/Lehrende/Unterrichtende'),
('05', 'Kolporteure/Zeitungszusteller'),
('06', 'Privatgeschäftsvermittler'),
('07', 'Funktionäre öff.-rechtl. Körperschaften'),
('08', 'sonstige Leistungen (freier DV, § 4 Abs 4 ASVG)'))],
string='Art der erbrachten Leistung (D.34)')
al31 = fields.Boolean(
string='§ 109b Abs 2 Z 1 (§ 22-Tätigkeit im Inland)')
al32 = fields.Boolean(string='§ 109b Abs 2 Z 2 (Vermittlung)')
al33 = fields.Boolean(string='§ 109b Abs 2 Z 3 (Beratung im Inland)')
ziellaender = fields.Char(
string='Länderkennungen der Zielländer',
help='Bis zu 7 internationale Länderkennungen à 3 Stellen, '
'komma-separiert (z. B. DE,CH).')
ausnahme_grund = fields.Selection(
[('keine', 'Mitteilungspflicht'),
('steuerabzug', 'Steuerabzug gem. § 99 EStG (Abs 4)'),
('kst_gleich', 'Ausländische KSt nicht > 10 %-Punkte unter '
'österreichischer KSt (Abs 4)')],
string='B1-Ausnahme (§ 109b Abs 4)',
default='keine',
help='Die 100.000-€-Schwelle prüft der Lauf automatisch; '
'Steuerabzug und KSt-Vergleich sind Sachentscheidungen '
'als Auswahlfelder.')
refn = fields.Char()
gemeldet = fields.Boolean(
string='Meldepflichtig (Schwellenprüfung)',
default=True, readonly=True,
help='Wird vom Lauf gesetzt: Zeilen unter den Schwellen bzw. mit '
'B1-Ausnahme entfallen aus der Datei.')
korr = fields.Selection(
[('keine', 'Neue Mitteilung'), ('K', 'Berichtigung'),
('S', 'Storno')], default='keine', string='Korrekturindikation')
np_ids = fields.One2many(
'l10n.at.payroll.mitteilung109.np', 'line_id',
string='Maßgebliche natürliche Personen (max. 3)')
currency_id = fields.Many2one(
'res.currency',
related='deklaration_id.company_id.currency_id')
class L10nAtPayrollMitteilung109(models.Model):
_name = 'l10n.at.payroll.mitteilung109'
_description = 'Finanz-Mitteilungen § 109a/§ 109b EStG'
_inherit = ['mail.thread']
_order = 'jahr desc, art, id desc'
company_id = fields.Many2one(
'res.company', required=True,
default=lambda self: self.env.company)
jahr = fields.Integer(required=True, string='Kalenderjahr')
art = fields.Selection(
[('w1', '§ 109a — Werkverträge (W1)'),
('b1', '§ 109b — Auslandszahlungen (B1)')],
required=True, string='Mitteilungsart')
line_ids = fields.One2many(
'l10n.at.payroll.mitteilung109.line', 'deklaration_id',
string='Mitteilungssätze')
datei = fields.Binary(attachment=True, string='LF-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 = [(
'm109_company_jahr_art_uniq', 'unique(company_id, jahr, art)',
'Für dieses Jahr existiert bereits eine Deklaration dieser Art.')]
def _l10n_at_schwellen(self):
self.ensure_one()
return self.env['hr.rule.parameter']._get_parameter_from_code(
'at_m109_schwellen', date(self.jahr, 1, 1),
raise_if_not_found=False) or {}
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 _l10n_at_satz(self, line, stnr, heute, jetzt):
"""W1/B1-Mitteilungssatz je Zeile; IDTEIL (SART W1/B1, VSTR 94)."""
self.ensure_one()
if self.art == 'w1':
struktur, sart, fsart = MITTEILUNG_W1_SATZ, 'W1', 'W'
else:
struktur, sart, fsart = MITTEILUNG_B1_SATZ, 'B1', 'B'
satz = L10nAtEldaSatz(
L10N_AT_ELDA_IDTEIL + struktur, 1100,
bezeichnung='%s %s' % (sart, line.name_partner))
satz.setze('SART', sart)
satz.setze('VSTR', '94')
satz.setze('FSART', fsart)
satz.setze('STNRL', stnr)
satz.setze_datum('DTUE', heute, 'JJJJMMTT')
satz.setze_zeit('ZTUE', jetzt)
satz.setze('REFN', line.refn)
satz.setze('FIND', 'J')
satz.setze('JAHR', self.jahr)
if self.art == 'w1':
satz.setze('AVSN', line.versicherungsnummer)
satz.setze('ASTN', line.steuernummer)
satz.setze('ANAM', (line.name or '')[:37])
satz.setze('AADR', (line.adresse or '')[:37])
satz.setze('ALKZ', (line.landeskennung or 'AUT')[:3])
satz.setze('APLZ', (line.plz or '')[:10])
satz.setze('AORT', (line.ort or '')[:30])
satz.setze('ARTE', line.art_leistung)
for feld, vfeld, bfeld, _neg in _BETRAEGE_W1:
betrag = getattr(line, feld)
satz.setze(vfeld, self._vorzeichen(betrag))
satz.setze(bfeld, abs(betrag) if betrag else 0, cent=True)
satz.setze('FANRA', (stnr or '')[:2])
satz.setze('STNRA', (stnr or '')[2:9])
satz.setze('WOBD', 'LE-%d' % line.id)
else:
satz.setze('LVSN', line.versicherungsnummer)
satz.setze('LSTN', line.steuernummer)
satz.setze('LNAM', (line.name or '')[:37])
satz.setze('LADR', (line.adresse or '')[:37])
satz.setze('LLKZ', (line.landeskennung or 'AUT')[:3])
satz.setze('LPLZ', (line.plz or '')[:10])
satz.setze('LORT', (line.ort or '')[:30])
for feld, vfeld, bfeld, _neg in _BETRAEGE_W1:
if feld == 'umsatzsteuer':
satz.setze(vfeld, self._vorzeichen(line.umsatzsteuer))
satz.setze(bfeld, abs(line.umsatzsteuer)
if line.umsatzsteuer else 0, cent=True)
elif feld == 'entgelt':
satz.setze('V499', self._vorzeichen(line.entgelt))
satz.setze('B499', abs(line.entgelt) if line.entgelt
else 0, cent=True)
satz.setze('AL31', 'J' if line.al31 else 'N')
satz.setze('AL32', 'J' if line.al32 else 'N')
satz.setze('AL33', 'J' if line.al33 else 'N')
laender = [k.strip().upper() for k in (
line.ziellaender or '').split(',') if k.strip()][:7]
if laender:
for idx, laender_code in enumerate(laender, start=1):
satz.setze('LKZ%d' % idx, laender_code)
satz.setze('UIDL', (line.uid or '')[:15])
satz.setze('UIDA', (self.company_id.vat or '')[:15])
satz.setze('STNRA', stnr)
satz.setze('WOBD', 'LE-%d' % line.id)
for nummer, np_zeile in enumerate(
line.np_ids.sorted('sequence')[:3], start=1):
satz.setze('NPVS%d' % nummer, np_zeile.versicherungsnummer)
satz.setze('NPFA%d' % nummer, np_zeile.finanzamtsnummer)
satz.setze('NPST%d' % nummer, np_zeile.steuernummer)
satz.setze('NPNA%d' % nummer, (np_zeile.name or '')[:30])
satz.setze('NPAD%d' % nummer, (np_zeile.adresse or '')[:30])
satz.setze('NPLK%d' % nummer,
(np_zeile.landeskennung or '')[:3])
satz.setze('NPPL%d' % nummer, (np_zeile.plz or '')[:10])
satz.setze('NPOR%d' % nummer, (np_zeile.ort or '')[:30])
satz.setze('NPUI%d' % nummer, (np_zeile.uid or '')[:15])
satz.setze('REFN2', line.refn)
return satz
@api.model
def _vorzeichen(self, betrag):
if not betrag:
return None
return '+' if betrag > 0 else '-'
@api.model
def _vorzeichen(self, betrag):
if not betrag:
return None
return '+' if betrag > 0 else '-'
def action_erzeugen(self):
"""W1/B1-Mitteilungssätze je Zeile bauen; Schwellen/Ausnahmen
prüfen (§ 109a-VO 450/900 € je Leistungserbringer; § 109b Abs 4
100.000 € je Leistungserbringer und Kalenderjahr); unter-
schwellige Zeilen bleiben mit ``gemeldet = False`` (Protokoll-
Hinweis) und entfallen aus der Datei. LF-Bestand: I1 (ARTD 'LZ')
+ die meldepflichtigen Sätze."""
for deklaration in self:
fehler = []
warnungen = []
company = deklaration.company_id
stnr = company.l10n_at_lohnsteuernummer
if not stnr:
fehler.append('Lohnsteuernummer der Company fehlt.')
if deklaration.art == 'w1' and len(stnr or '') < 9:
fehler.append(
'FANRA/STNRA erfordern eine 9-stellige '
'Lohnsteuernummer (2 FA + 7 StNr).')
schwellen = deklaration._l10n_at_schwellen()
if not schwellen:
fehler.append(
'Parameter at_m109_schwellen fehlt (Werte nur aus '
'Quelle — RIS/VO-Verifikation vor Belegung).')
if fehler:
deklaration.protokoll = 'FEHLER:\n%s' % (
'\n'.join(fehler))
raise UserError('Mitteilungs-Lauf mit Fehlern abgebrochen:\n'
+ '\n'.join(fehler))
# Schwellenprüfung je Leistungserbringer (Kalenderjahr).
if deklaration.art == 'w1':
je_leistung = float(schwellen.get('m109a_leistung', 0.0))
kumulativ_cap = float(
schwellen.get('m109a_kumulativ', 0.0))
else:
je_leistung = None
kumulativ_cap = float(
schwellen.get('m109b_jahr', 0.0))
nach_le = {}
for line in deklaration.line_ids:
schluessel = (line.name_partner, line.steuernummer or '',
line.versicherungsnummer or '')
nach_le.setdefault(schluessel, []).append(line)
for schluessel, zeilen in nach_le.items():
summe = sum(z.entgelt for z in zeilen)
for line in zeilen:
gemeldet = True
if deklaration.art == 'w1':
unter_schwellen = (
(line.entgelt or 0) <= je_leistung
and summe <= kumulativ_cap)
if unter_schwellen:
gemeldet = False
warnungen.append(
'%s: Entgelt %.2f unter den §-109a-'
'Schwellen (je Leistung ≤ %.2f, kumulativ '
'je LE ≤ %.2f) — keine Meldung.' % (
line.name_partner, line.entgelt or 0,
je_leistung, kumulativ_cap))
else:
if line.ausnahme_grund != 'keine':
gemeldet = False
warnungen.append(
'%s: Ausnahme nach § 109b Abs 4 '
'(%s) — keine Mitteilung.' % (
line.name_partner,
line.ausnahme_grund))
elif summe <= kumulativ_cap:
gemeldet = False
warnungen.append(
'%s: Kalenderjahres-Zahlungen %.2f '
'übersteigen 100.000 € nicht '
'(§ 109b Abs 4 lit a) — keine '
'Mitteilung.' % (
line.name_partner, summe))
elif not (line.al31 or line.al32 or line.al33):
fehler.append(
'%s: B1 ohne Art der Leistung (AL31/32/33).'
% line.name_partner)
elif not (line.ziellaender or '').strip():
fehler.append(
'%s: B1 ohne Zielländer-Länderkennung.'
% line.name_partner)
line.gemeldet = gemeldet
if deklaration.art == 'b1':
for line in deklaration.line_ids:
np_anzahl = len(line.np_ids)
ist_koerperschaft = bool(
line.steuernummer and not line.versicherungsnummer)
if ist_koerperschaft and not np_anzahl:
warnungen.append(
'%s: Personenvereinigung/Körperschaft als '
'Leistungserbringer ohne maßgeblich '
'auftretende natürliche Person '
'(§ 109b Abs 3 Z 2) prüfen.'
% line.name_partner)
if np_anzahl > 3:
fehler.append(
'%s: mehr als 3 maßgebliche natürliche '
'Personen (DM-ORG: 3 Blöcke).' % (
line.name_partner))
if fehler:
deklaration.protokoll = 'FEHLER:\n%s\nWARNUNGEN:\n%s' % (
'\n'.join(fehler), '\n'.join(warnungen) or '')
raise UserError('Mitteilungs-Lauf mit Fehlern '
'abgebrochen:\n' + '\n'.join(fehler))
gemeldete = deklaration.line_ids.filtered('gemeldet')
if gemeldete:
heute = date.today()
jetzt = datetime.now()
partner = company.partner_id
informationssatz = L10nAtEldaSatz(
L10N_AT_ELDA_IDTEIL + L16_INFORMATIONSSATZ, 1100,
bezeichnung='Mitteilungs-Informationssatz')
informationssatz.setze('SART', 'I1')
informationssatz.setze('VSTR', '94')
informationssatz.setze('FSART', 'I')
informationssatz.setze('CLEAR', 'RADAUS')
informationssatz.setze('STNRL', stnr)
informationssatz.setze('STNRA', stnr)
informationssatz.setze('ARTD', 'LZ')
informationssatz.setze_datum('DTUE', heute, 'JJJJMMTT')
informationssatz.setze_zeit('ZTUE', jetzt)
informationssatz.setze('STVE', '03')
informationssatz.setze('ANAM', (partner.name or '')[:37])
informationssatz.setze('AADR', (partner.street or '')[:37])
informationssatz.setze('ALKZ', 'AUT')
informationssatz.setze('APLZ', partner.zip)
informationssatz.setze('AORT', (partner.city or '')[:30])
informationssatz.setze('JAHR', deklaration.jahr)
informationssatz.setze('GESA', len(gemeldete))
informationssatz.setze('ANZA', len(gemeldete))
datensaetze = [informationssatz] + [
deklaration._l10n_at_satz(line, stnr, heute, jetzt)
for line in gemeldete]
bestand = L10nAtEldaBestand(
'LF', '94', '28', hersteller=deklaration
._l10n_at_hersteller(), datensaetze=datensaetze)
datei = L10nAtEldaDatei([bestand])
deklaration.write({
'datei': base64.b64encode(datei.render()),
'datei_name': 'mitteilung109_%s_%d_%s.txt' % (
deklaration.art, deklaration.jahr, stnr),
})
deklaration.protokoll = 'WARNUNGEN:\n%s' % (
'\n'.join(warnungen) or '')
deklaration.state = 'done'
return True
@@ -0,0 +1,269 @@
# 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 126 (Z 1 Schicht-/Wechseldienst in Nachtarbeit 226 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ännerEnde 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 502761)
(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 126 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 126.' % 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
@@ -32,4 +32,15 @@ access_l10n_at_payroll_tasy_zuord_manager,L10n AT Payroll TASY-Zuordnung Manager
access_l10n_at_payroll_tasy_zuabschlag_user,L10n AT Payroll TASY-Zuabschlag User,model_l10n_at_payroll_tasy_zuabschlag,hr_payroll.group_hr_payroll_user,1,0,0,0
access_l10n_at_payroll_tasy_zuabschlag_manager,L10n AT Payroll TASY-Zuabschlag Manager,model_l10n_at_payroll_tasy_zuabschlag,hr_payroll.group_hr_payroll_manager,1,1,1,1
access_l10n_at_payroll_tasy_zuabschlag_steuerung_user,L10n AT Payroll TASY-Zuabschlag-Steuerung User,model_l10n_at_payroll_tasy_zuabschlag_steuerung,hr_payroll.group_hr_payroll_user,1,0,0,0
access_l10n_at_payroll_tasy_zuabschlag_steuerung_manager,L10n AT Payroll TASY-Zuabschlag-Steuerung Manager,model_l10n_at_payroll_tasy_zuabschlag_steuerung,hr_payroll.group_hr_payroll_manager,1,1,1,1
access_l10n_at_payroll_tasy_zuabschlag_steuerung_manager,L10n AT Payroll TASY-Zuabschlag-Steuerung Manager,model_l10n_at_payroll_tasy_zuabschlag_steuerung,hr_payroll.group_hr_payroll_manager,1,1,1,1
access_l10n_at_payroll_l17_user,L10n AT Payroll L17 User,model_l10n_at_payroll_l17,hr_payroll.group_hr_payroll_user,1,1,1,0
access_l10n_at_payroll_l17_manager,L10n AT Payroll L17 Manager,model_l10n_at_payroll_l17,hr_payroll.group_hr_payroll_manager,1,1,1,1
access_l10n_at_payroll_l17_line_user,L10n AT Payroll L17-Line User,model_l10n_at_payroll_l17_line,hr_payroll.group_hr_payroll_user,1,1,1,1
access_l10n_at_payroll_m109_user,L10n AT Payroll Mitteilung109 User,model_l10n_at_payroll_mitteilung109,hr_payroll.group_hr_payroll_user,1,1,1,0
access_l10n_at_payroll_m109_manager,L10n AT Payroll Mitteilung109 Manager,model_l10n_at_payroll_mitteilung109,hr_payroll.group_hr_payroll_manager,1,1,1,1
access_l10n_at_payroll_m109_line_user,L10n AT Payroll Mitteilung109-Line User,model_l10n_at_payroll_mitteilung109_line,hr_payroll.group_hr_payroll_user,1,1,1,1
access_l10n_at_payroll_m109_np_user,L10n AT Payroll Mitteilung109-NP User,model_l10n_at_payroll_mitteilung109_np,hr_payroll.group_hr_payroll_user,1,1,1,1
access_l10n_at_payroll_sm_user,L10n AT Payroll SM User,model_l10n_at_payroll_schwerarbeitsmeldung,hr_payroll.group_hr_payroll_user,1,1,1,0
access_l10n_at_payroll_sm_manager,L10n AT Payroll SM Manager,model_l10n_at_payroll_schwerarbeitsmeldung,hr_payroll.group_hr_payroll_manager,1,1,1,1
access_l10n_at_payroll_sm_line_user,L10n AT Payroll SM-Line User,model_l10n_at_payroll_schwerarbeitsmeldung_line,hr_payroll.group_hr_payroll_user,1,1,1,1
access_l10n_at_payroll_sm_taetigkeit_user,L10n AT Payroll SM-Taetigkeit User,model_l10n_at_payroll_schwerarbeitsmeldung_taetigkeit,hr_payroll.group_hr_payroll_user,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
32 access_l10n_at_payroll_tasy_zuabschlag_user L10n AT Payroll TASY-Zuabschlag User model_l10n_at_payroll_tasy_zuabschlag hr_payroll.group_hr_payroll_user 1 0 0 0
33 access_l10n_at_payroll_tasy_zuabschlag_manager L10n AT Payroll TASY-Zuabschlag Manager model_l10n_at_payroll_tasy_zuabschlag hr_payroll.group_hr_payroll_manager 1 1 1 1
34 access_l10n_at_payroll_tasy_zuabschlag_steuerung_user L10n AT Payroll TASY-Zuabschlag-Steuerung User model_l10n_at_payroll_tasy_zuabschlag_steuerung hr_payroll.group_hr_payroll_user 1 0 0 0
35 access_l10n_at_payroll_tasy_zuabschlag_steuerung_manager L10n AT Payroll TASY-Zuabschlag-Steuerung Manager model_l10n_at_payroll_tasy_zuabschlag_steuerung hr_payroll.group_hr_payroll_manager 1 1 1 1
36 access_l10n_at_payroll_l17_user L10n AT Payroll L17 User model_l10n_at_payroll_l17 hr_payroll.group_hr_payroll_user 1 1 1 0
37 access_l10n_at_payroll_l17_manager L10n AT Payroll L17 Manager model_l10n_at_payroll_l17 hr_payroll.group_hr_payroll_manager 1 1 1 1
38 access_l10n_at_payroll_l17_line_user L10n AT Payroll L17-Line User model_l10n_at_payroll_l17_line hr_payroll.group_hr_payroll_user 1 1 1 1
39 access_l10n_at_payroll_m109_user L10n AT Payroll Mitteilung109 User model_l10n_at_payroll_mitteilung109 hr_payroll.group_hr_payroll_user 1 1 1 0
40 access_l10n_at_payroll_m109_manager L10n AT Payroll Mitteilung109 Manager model_l10n_at_payroll_mitteilung109 hr_payroll.group_hr_payroll_manager 1 1 1 1
41 access_l10n_at_payroll_m109_line_user L10n AT Payroll Mitteilung109-Line User model_l10n_at_payroll_mitteilung109_line hr_payroll.group_hr_payroll_user 1 1 1 1
42 access_l10n_at_payroll_m109_np_user L10n AT Payroll Mitteilung109-NP User model_l10n_at_payroll_mitteilung109_np hr_payroll.group_hr_payroll_user 1 1 1 1
43 access_l10n_at_payroll_sm_user L10n AT Payroll SM User model_l10n_at_payroll_schwerarbeitsmeldung hr_payroll.group_hr_payroll_user 1 1 1 0
44 access_l10n_at_payroll_sm_manager L10n AT Payroll SM Manager model_l10n_at_payroll_schwerarbeitsmeldung hr_payroll.group_hr_payroll_manager 1 1 1 1
45 access_l10n_at_payroll_sm_line_user L10n AT Payroll SM-Line User model_l10n_at_payroll_schwerarbeitsmeldung_line hr_payroll.group_hr_payroll_user 1 1 1 1
46 access_l10n_at_payroll_sm_taetigkeit_user L10n AT Payroll SM-Taetigkeit User model_l10n_at_payroll_schwerarbeitsmeldung_taetigkeit hr_payroll.group_hr_payroll_user 1 1 1 1
@@ -4,4 +4,5 @@ from . import test_tasy_katalog
from . import test_lohnsteuer
from . import test_sozialversicherung
from . import test_elda_l16
from . import test_elda_l17
from . import test_gp8
@@ -0,0 +1,216 @@
# Part of the odoo-at-payroll project. License: LGPL-3.
"""AP17: L17 (A1) und Finanz-Mitteilungen § 109a (W1)/§ 109b (B1) —
Positionsexakte Satz-Assertions (ODL-Extraktion 2026-09-16, 1-basierte
DM-ORG-Positionen → 0-basierte String-Indizes), Prüfkatalog-Logik
(350-Formel, 357/347-Caps, 354-Cap aliquotiert) und Schwellen der
Mitteilungsläufe."""
import base64
import datetime
import types
from odoo.tests import TransactionCase, tagged
from odoo.addons.l10n_at_hr_payroll.models.elda import (
L10N_AT_ELDA_IDTEIL,
L10nAtEldaSatz,
)
from odoo.addons.l10n_at_hr_payroll.models.lohnzettel_l17 import L17_SATZ
@tagged('post_install', '-at_install')
class L17SatzTest(TransactionCase):
def test_01_a1_satz_laenge_positionen(self):
"""A1: Länge 1100; FSART 'A' an Position 21; BELZ 120; JALZ 128;
B350 an 281 (10 n, Cent); HOPA an 618; Satzlänge exakt."""
satz = L10nAtEldaSatz(
L10N_AT_ELDA_IDTEIL + L17_SATZ, 1100,
bezeichnung='A1 Test')
satz.setze('SART', 'A1')
satz.setze('VSTR', '94')
satz.setze('FSART', 'A')
satz.setze('PENS', 'N')
satz.setze('AVSN', '1234150590')
satz.setze('BELZ', '0101')
satz.setze('ENLZ', '0131')
satz.setze('JALZ', 2026)
satz.setze('V350', '+')
satz.setze('B350', 1234.56, cent=True)
satz.setze('HOTA', 21)
satz.setze('VHOPA', '+')
satz.setze('HOPA', 63.0, cent=True)
roh = satz.render()
self.assertEqual(len(roh), 1100)
self.assertEqual(roh[20], 'A') # FSART pos 21
self.assertEqual(roh[119:123], '0101') # BELZ pos 120
self.assertEqual(roh[127:131], '2026') # JALZ pos 128
self.assertEqual(roh[131], 'N') # PENS pos 132
self.assertEqual(roh[132:142], '1234150590') # AVSN pos 133
self.assertEqual(roh[279], '+') # V350 pos 280
self.assertEqual(roh[280:290], '0000123456') # B350 pos 281
self.assertEqual(roh[616], '+') # VHOPA pos 617
self.assertEqual(roh[617:627], '0000006300') # HOPA pos 618
def test_02_kz_salden_mapping(self):
"""GP3-Flags → L17-KZs: laufend → 350; sonstig ohne Abs-3/6 →
350+351; abs3/abs6 → 352; SVDN_* → 357; SVDN_SZ → 347; § 68-
Freibetrag ÜStd → 354; Homeoffice-Regel → HOPA."""
deklaration = self.env['l10n.at.payroll.l17'].create({
'company_id': self.env.company.id, 'jahr': 2026})
laufend = types.SimpleNamespace(
l10n_at_bemessung='laufend', l10n_at_s67_abs='keine')
sz = types.SimpleNamespace(
l10n_at_bemessung='sonstig', l10n_at_s67_abs='keine')
abf = types.SimpleNamespace(
l10n_at_bemessung='sonstig', l10n_at_s67_abs='abs3')
neutral = types.SimpleNamespace(
l10n_at_bemessung='neutral', l10n_at_s67_abs='keine')
slip = types.SimpleNamespace(
line_ids=[
types.SimpleNamespace(salary_rule_id=laufend,
code='GEHALT', total=3000.0),
types.SimpleNamespace(salary_rule_id=sz,
code='SZ13', total=3000.0),
types.SimpleNamespace(salary_rule_id=abf,
code='ABF_ALT', total=6000.0),
types.SimpleNamespace(salary_rule_id=neutral,
code='SVDN_KV', total=100.0),
types.SimpleNamespace(salary_rule_id=neutral,
code='SVDN_SZ', total=50.0),
types.SimpleNamespace(salary_rule_id=laufend,
code='SB_HOMEOFFICE', total=63.0),
],
input_line_ids=[],
_l10n_at_freibetraege_68=lambda: {'szfn': 0.0,
'ue_std': 60.0},
)
salden, hota = deklaration._l10n_at_kz_salden(slip)
self.assertEqual(salden['kz350'], 12063.0)
self.assertEqual(salden['kz351'], 3000.0)
self.assertEqual(salden['kz352'], 6000.0)
self.assertEqual(salden['kz354'], 60.0)
self.assertEqual(salden['kz357'], 100.0)
self.assertEqual(salden['kz347'], 50.0)
self.assertEqual(salden['hopa'], 63.0)
def test_03_l17_pruefungen_pruefkatalog(self):
deklaration = self.env['l10n.at.payroll.l17'].create({
'company_id': self.env.company.id, 'jahr': 2026})
employee = self.env['hr.employee'].create({
'name': 'L17 Prüfung', 'birthday': '1990-01-01'})
line = self.env['l10n.at.payroll.l17.line'].create({
'l17_id': deklaration.id,
'employee_id': employee.id,
'date_from': datetime.date(2026, 1, 1),
'date_to': datetime.date(2026, 1, 31),
})
fehler, warnungen = [], []
line.write({'kz350': 5000.0, 'kz351': 3000.0, 'kz352': 2500.0})
line._l10n_at_pruefungen(fehler, warnungen)
self.assertTrue(any('KZ 350' in f for f in fehler))
fehler.clear()
line.write({'kz350': 12000.0, 'kz351': 3000.0, 'kz352': 0.0,
'kz357': 4900.0})
line._l10n_at_pruefungen(fehler, warnungen)
self.assertTrue(any('KZ 357' in f for f in fehler))
fehler.clear()
line.write({'kz357': 0.0, 'kz354': 2500.0})
line._l10n_at_pruefungen(fehler, warnungen)
self.assertTrue(any('KZ 354' in w for w in warnungen))
@tagged('post_install', '-at_install')
class Mitteilung109Test(TransactionCase):
def setUp(self):
super().setUp()
self.company = self.env.company
self.company.write({'l10n_at_lohnsteuernummer': '123456789'})
def _deklaration(self, art, entgelt):
deklaration = self.env['l10n.at.payroll.mitteilung109'].create({
'company_id': self.company.id, 'jahr': 2026, 'art': art})
werte = {
'name_partner': 'Testpartner',
'name': 'Testwerk',
'steuernummer': '8765432',
'entgelt': entgelt,
}
if art == 'w1':
werte.update({'art_leistung': '04', 'adresse': 'Werkstraße 1',
'plz': '1010', 'ort': 'Wien'})
else:
werte.update({'al31': True, 'ziellaender': 'DE,CH',
'adresse': 'Werkstraße 1', 'plz': '1010',
'ort': 'Wien'})
deklaration.line_ids = [(0, 0, werte)]
return deklaration
def test_04_w1_satz_und_positionen(self):
deklaration = self._deklaration('w1', 1200.0)
deklaration.action_erzeugen()
line = deklaration.line_ids[0]
self.assertTrue(line.gemeldet)
daten = base64.b64decode(deklaration.datei).decode('ISO-8859-15')
w1_satz = daten.splitlines()[3]
self.assertEqual(len(w1_satz), 1100)
self.assertEqual(w1_satz[:2], 'W1')
self.assertEqual(w1_satz[20], 'W') # FSART pos 21
self.assertEqual(w1_satz[281], '+') # V341 pos 282
self.assertEqual(w1_satz[282:292], '0000120000') # B341 pos 283
self.assertEqual(w1_satz[303:305], '12') # FANRA pos 304
self.assertEqual(w1_satz[305:312], '3456789') # STNRA pos 306
def test_05_w1_schwelle_untermeldung(self):
deklaration = self._deklaration('w1', 400.0)
deklaration.action_erzeugen()
line = deklaration.line_ids[0]
self.assertFalse(line.gemeldet)
self.assertIn('Schwellen', deklaration.protokoll)
self.assertFalse(deklaration.datei)
def test_05b_w1_900_eur_kumulativ_grenze(self):
"""450 € je Leistung, aber 1.000 € kumulativ je LE →
meldepflichtig trotz je-Leistung-Unterschreitung."""
deklaration = self._deklaration('w1', 450.0)
deklaration.line_ids = [
(0, 0, {'name_partner': 'Testpartner', 'name': 'Testwerk',
'steuernummer': '8765432', 'art_leistung': '04',
'entgelt': 550.0}),
]
deklaration.action_erzeugen()
self.assertTrue(deklaration.line_ids[0].gemeldet)
def test_06_b1_satz_schwellen_ausnahme(self):
deklaration = self._deklaration('b1', 120000.0)
deklaration.action_erzeugen()
line = deklaration.line_ids[0]
self.assertTrue(line.gemeldet)
daten = base64.b64decode(deklaration.datei).decode('ISO-8859-15')
b1_satz = daten.splitlines()[3]
self.assertEqual(len(b1_satz), 1100)
self.assertEqual(b1_satz[:2], 'B1')
self.assertEqual(b1_satz[20], 'B') # FSART pos 21
self.assertEqual(b1_satz[281], '+') # V499 pos 282
self.assertEqual(b1_satz[282:292], '0012000000') # B499 pos 283
self.assertEqual(b1_satz[360], 'J') # AL31 pos 361
self.assertEqual(b1_satz[361], 'N') # AL32 pos 362
self.assertEqual(b1_satz[363:366], 'DE ') # LKZ1 pos 364 (3 a)
self.assertEqual(b1_satz[366:369], 'CH ') # LKZ2 pos 367 (3 a)
# Unter der 100.000-€-Schwelle: keine Meldung (§ 109b Abs 4).
klein = self._deklaration('b1', 50000.0)
klein.action_erzeugen()
self.assertFalse(klein.line_ids[0].gemeldet)
# Ausnahme Steuerabzug § 99: keine Meldung trotz Betrags.
abzug = self._deklaration('b1', 150000.0)
abzug.line_ids[0].ausnahme_grund = 'steuerabzug'
abzug.action_erzeugen()
self.assertFalse(abzug.line_ids[0].gemeldet)
def test_07_fristenkalender_ap17(self):
from odoo.addons.l10n_at_hr_payroll.models.fristenkalender \
import FRISTART_DEFINITIONEN
for art in ('l17', 'mitteilung_109b'):
self.assertIn(art, FRISTART_DEFINITIONEN)
self.assertEqual(
FRISTART_DEFINITIONEN[art]['monat'], 2)
@@ -0,0 +1,227 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- ============================================================ -->
<!-- AP17: Lohnnachweis/Lohnbescheinigung L17 (A1) und Finanz- -->
<!-- Mitteilungen § 109a (W1)/§ 109b (B1). Menü unter dem -->
<!-- Meldewesen (ELDA/FinOnl). -->
<!-- ============================================================ -->
<record id="l17_list_view" model="ir.ui.view">
<field name="name">l10n.at.payroll.l17.list</field>
<field name="model">l10n.at.payroll.l17</field>
<field name="arch" type="xml">
<list string="Lohnnachweis/Lohnbescheinigung (L17)">
<field name="jahr"/>
<field name="company_id" groups="base.group_multi_company"/>
<field name="state" widget="badge"
decoration-success="state == 'done'"
decoration-info="state == 'draft'"/>
</list>
</field>
</record>
<record id="l17_form_view" model="ir.ui.view">
<field name="name">l10n.at.payroll.l17.form</field>
<field name="model">l10n.at.payroll.l17</field>
<field name="arch" type="xml">
<form string="L17 — Lohnnachweis/Lohnbescheinigung">
<header>
<button name="action_erzeugen" type="object"
string="Lauf erzeugen (A1 je LZP)"
class="btn-primary"
invisible="state == 'done'"/>
<field name="state" widget="statusbar"
statusbar_visible="draft,done"/>
</header>
<sheet>
<group>
<group>
<field name="jahr"/>
</group>
<group>
<field name="company_id"
groups="base.group_multi_company"/>
</group>
</group>
<notebook>
<page string="A1-Sätze (je LZP)" name="linien">
<field name="line_ids">
<list editable="bottom">
<field name="employee_name"/>
<field name="date_from"/>
<field name="date_to"/>
<field name="versicherungsnummer"/>
<field name="kz350"/>
<field name="kz351" optional="show"/>
<field name="kz352" optional="show"/>
<field name="kz354" optional="hide"/>
<field name="kz357" optional="show"/>
<field name="kz347" optional="hide"/>
<field name="hota" optional="hide"/>
<field name="hopa" optional="hide"/>
</list>
</field>
</page>
<page string="Datei" name="datei">
<group>
<field name="datei" filename="datei_name"/>
<field name="datei_name" invisible="1"/>
</group>
</page>
<page string="Prüfprotokoll" name="protokoll">
<field name="protokoll"/>
</page>
</notebook>
</sheet>
</form>
</field>
</record>
<record id="action_l17" model="ir.actions.act_window">
<field name="name">Lohnnachweis L17</field>
<field name="res_model">l10n.at.payroll.l17</field>
<field name="view_mode">list,form</field>
</record>
<menuitem id="menu_l10n_at_payroll_l17"
name="Lohnnachweis L17 (A1)"
parent="menu_l10n_at_payroll_meldewesen"
action="action_l17" sequence="12"/>
<record id="mitteilung109_list_view" model="ir.ui.view">
<field name="name">l10n.at.payroll.mitteilung109.list</field>
<field name="model">l10n.at.payroll.mitteilung109</field>
<field name="arch" type="xml">
<list string="Finanz-Mitteilungen § 109a/§ 109b">
<field name="jahr"/>
<field name="art"/>
<field name="company_id" groups="base.group_multi_company"/>
<field name="state" widget="badge"
decoration-success="state == 'done'"
decoration-info="state == 'draft'"/>
</list>
</field>
</record>
<record id="mitteilung109_form_view" model="ir.ui.view">
<field name="name">l10n.at.payroll.mitteilung109.form</field>
<field name="model">l10n.at.payroll.mitteilung109</field>
<field name="arch" type="xml">
<form string="Finanz-Mitteilungen § 109a/§ 109b">
<header>
<button name="action_erzeugen" type="object"
string="Lauf erzeugen (Schwellen, Datei)"
class="btn-primary"
invisible="state == 'done'"/>
<field name="state" widget="statusbar"
statusbar_visible="draft,done"/>
</header>
<sheet>
<group>
<group>
<field name="jahr"/>
<field name="art"/>
</group>
<group>
<field name="company_id"
groups="base.group_multi_company"/>
</group>
</group>
<notebook>
<page string="Mitteilungssätze" name="linien">
<field name="line_ids" context="{'default_art': art}">
<list editable="bottom"
open_form_view="1">
<field name="name_partner"/>
<field name="name"/>
<field name="versicherungsnummer"/>
<field name="steuernummer"
optional="show"/>
<field name="entgelt"/>
<field name="umsatzsteuer"
optional="show"/>
<field name="gemeldet"/>
</list>
</field>
</page>
<page string="Datei" name="datei">
<group>
<field name="datei" filename="datei_name"/>
<field name="datei_name" invisible="1"/>
</group>
</page>
<page string="Prüfprotokoll" name="protokoll">
<field name="protokoll"/>
</page>
</notebook>
</sheet>
</form>
</field>
</record>
<record id="mitteilung109_line_form_view" model="ir.ui.view">
<field name="name">l10n.at.payroll.mitteilung109.line.form</field>
<field name="model">l10n.at.payroll.mitteilung109.line</field>
<field name="arch" type="xml">
<form string="Finanz-Mitteilung (§ 109a/§ 109b)">
<sheet>
<group>
<group>
<field name="name_partner"/>
<field name="name"/>
<field name="versicherungsnummer"/>
<field name="steuernummer"/>
<field name="uid"/>
</group>
<group>
<field name="adresse"/>
<field name="landeskennung"/>
<field name="plz"/>
<field name="ort"/>
</group>
</group>
<group>
<group>
<field name="entgelt"/>
<field name="umsatzsteuer"/>
<field name="dn_sv_anteil"/>
<field name="bmv_beitrag"/>
</group>
<group>
<field name="art_leistung"/>
<field name="al31"/>
<field name="al32"/>
<field name="al33"/>
<field name="ziellaender"/>
<field name="ausnahme_grund"/>
<field name="korr"/>
<field name="gemeldet"/>
</group>
</group>
<group string="Maßgeblich auftretende natürliche Personen (max. 3)">
<field name="np_ids">
<list editable="bottom">
<field name="sequence" widget="handle"/>
<field name="name"/>
<field name="versicherungsnummer"/>
<field name="steuernummer"/>
<field name="uid" optional="hide"/>
</list>
</field>
</group>
</sheet>
</form>
</field>
</record>
<record id="action_mitteilung109" model="ir.actions.act_window">
<field name="name">Finanz-Mitteilungen § 109a/§ 109b</field>
<field name="res_model">l10n.at.payroll.mitteilung109</field>
<field name="view_mode">list,form</field>
</record>
<menuitem id="menu_l10n_at_payroll_m109"
name="Mitteilungen § 109a/§ 109b"
parent="menu_l10n_at_payroll_meldewesen"
action="action_mitteilung109" sequence="14"/>
</odoo>
@@ -0,0 +1,104 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- AP18: Schwerarbeitsmeldung (ASchV § 5 / § 74 Abs 3 ASVG; -->
<!-- DM-ORG E.22, BEST SM, SART 65/66). -->
<record id="sm_list_view" model="ir.ui.view">
<field name="name">l10n.at.payroll.schwerarbeitsmeldung.list</field>
<field name="model">l10n.at.payroll.schwerarbeitsmeldung</field>
<field name="arch" type="xml">
<list string="Schwerarbeitsmeldung (ASchV)">
<field name="jahr"/>
<field name="company_id" groups="base.group_multi_company"/>
<field name="state" widget="badge"
decoration-success="state == 'done'"
decoration-info="state == 'draft'"/>
</list>
</field>
</record>
<record id="sm_form_view" model="ir.ui.view">
<field name="name">l10n.at.payroll.schwerarbeitsmeldung.form</field>
<field name="model">l10n.at.payroll.schwerarbeitsmeldung</field>
<field name="arch" type="xml">
<form string="Schwerarbeitsmeldung (§ 5 ASchV)">
<header>
<button name="action_erzeugen" type="object"
string="Lauf erzeugen (SART 65)"
class="btn-primary"
invisible="state == 'done'"/>
<field name="state" widget="statusbar"
statusbar_visible="draft,done"/>
</header>
<sheet>
<group>
<group>
<field name="jahr"/>
</group>
<group>
<field name="company_id"
groups="base.group_multi_company"/>
</group>
</group>
<notebook>
<page string="Meldesätze" name="linien">
<field name="line_ids">
<list editable="bottom"
open_form_view="1">
<field name="employee_name"/>
<field name="versicherungsnummer"/>
</list>
</field>
</page>
<page string="Datei" name="datei">
<group>
<field name="datei" filename="datei_name"/>
<field name="datei_name" invisible="1"/>
</group>
</page>
<page string="Prüfprotokoll" name="protokoll">
<field name="protokoll"/>
</page>
</notebook>
</sheet>
</form>
</field>
</record>
<record id="sm_line_form_view" model="ir.ui.view">
<field name="name">l10n.at.payroll.schwerarbeitsmeldung.line.form</field>
<field name="model">l10n.at.payroll.schwerarbeitsmeldung.line</field>
<field name="arch" type="xml">
<form string="Schwerarbeitsmeldung je AN">
<sheet>
<group>
<group>
<field name="employee_id"/>
<field name="versicherungsnummer"/>
</group>
</group>
<group string="Tätigkeitsblöcke (ASchV § 1 Ziffer, von/bis)">
<field name="taetigkeit_ids">
<list editable="bottom">
<field name="sequence" widget="handle"/>
<field name="ziffer"/>
<field name="von"/>
<field name="bis"/>
</list>
</field>
</group>
</sheet>
</form>
</field>
</record>
<record id="action_schwerarbeitsmeldung" model="ir.actions.act_window">
<field name="name">Schwerarbeitsmeldung</field>
<field name="res_model">l10n.at.payroll.schwerarbeitsmeldung</field>
<field name="view_mode">list,form</field>
</record>
<menuitem id="menu_l10n_at_payroll_sm"
name="Schwerarbeitsmeldung (SM)"
parent="menu_l10n_at_payroll_meldewesen"
action="action_schwerarbeitsmeldung" sequence="16"/>
</odoo>