mirror of
http://100.103.83.12:3003/fegger/odoo-at-payroll.git
synced 2026-09-17 08:52:45 +00:00
[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:
@@ -2,6 +2,93 @@
|
|||||||
|
|
||||||
## Current focus
|
## Current focus
|
||||||
|
|
||||||
|
**AP18 + AP19 umgesetzt — 252/252 Tests grün** (2026-09-16, uncommittet):
|
||||||
|
|
||||||
|
- **AP18 Schwerarbeit/NSchG:** Beschaffungsgate erledigt — RIS: NSchG
|
||||||
|
(GesNr 10008502, BGBl 1981/354 — ⚠ BGBl 1980/467 falsch), Art XI
|
||||||
|
Abs 3 (2 vH Stammnorm, 3,80 % via Sistierung), **Art XI Abs 6
|
||||||
|
NSchM-Regelkatalog** (6/12/18-Fenster, Randmonat, DZR-36 dokument.
|
||||||
|
Rest); ASchV GesNr 20004642, § 4 NOR40272279 (idF 2025: Versicherungs-
|
||||||
|
monat § 231 Z 1 lit a; KB lb-swa-05 nennt 15 Tage — RIS-§ 4-Fassung
|
||||||
|
verweist auf 231 — beides dokumentiert), § 5 (M ≥ 40 / F ≥ 35, Ende
|
||||||
|
Februar); **E.22.2-Erstellvorschriften** (Z 1 ≥ 6 Nachtarbeitstage
|
||||||
|
analog NSchG; Z 2/4/5 ≥ 15 Tage; Z 3/GfB nicht melden; Z 6 bei
|
||||||
|
Pflegegeld-Stufe 3). BEST = **SM**, SART 65/66, Länge 800, Tätigkeits-
|
||||||
|
blöcke 26 × 10 (TART/TVON/TBIS).
|
||||||
|
- **AP18-Umsetzung:** Kern `schwerarbeitsmeldung.py` (Modell + Fixsatz
|
||||||
|
BEST SM + Erstellvorschrift-Warnungen: Altersfilter, Z 3/Z 6-Hinweise);
|
||||||
|
Zeilen = mandantenseitig erfasste Fakten (kein unlink im Lauf!). Privat
|
||||||
|
`nachtschwerarbeit.py`: WET `ATP_NACHTSCHWER` + Payslip-Compute
|
||||||
|
`_l10n_at_nachtschwerarbeitsmonat` (6/12/18-Fenster + Randmonat-Proxy ⚠
|
||||||
|
Pro-rata; DZR-36 dokument. Rest), `l10n_at_nachtschwerarbeitsmonat`
|
||||||
|
stored am Payslip.
|
||||||
|
- **AP19 Umsetzung:** hr.version-Status-Flag `l10n_at_status` (an/
|
||||||
|
organ_minder/organ_mehr — lb-vst-03: Organ ≥ 25 % kein SV) +
|
||||||
|
`l10n_at_ferialpraktikant` (dokumentativ — BMSVG § 6 nennt keinen
|
||||||
|
Ausschluss, Engine bleibt normal); SV-Hook `payslip_private
|
||||||
|
._l10n_at_sv_satz` (organ_mehr → 0); mBGM organ_mehr → Ausschluss mit
|
||||||
|
Protokoll-Hinweis + leerer mBGM-Datei-Guard (`if datensaetze:`);
|
||||||
|
**Präsenzdienst-Fiktion** § 7 Abs 1/2 BMSVG: Input `ATP_PRD_TAGE` +
|
||||||
|
Parameter `at_bv_fiktiv_kbg` (435,90 € 2026 = KBGG § 3 Abs 1 idF vor
|
||||||
|
BGBl I 53/2016, lb-vor-04) → `_at_bv_fiktiv_prasenzenzdienst()` im
|
||||||
|
`_at_bv_basis_laufen`-Hook (aliquot Tage/Monatstage; >12 Monate
|
||||||
|
Zeitsoldat → Bund — mandantenseitig). DVH-Sonderfälle: dokumentiert
|
||||||
|
offen (lb-dvh-01/04 ❓) — bewusst nicht gebaut. Feiertags-EFZ: AP4-
|
||||||
|
WET-Modell deckt ab.
|
||||||
|
- **⚠ Regression abgefangen:** mein PRD-Insert hatte `_at_bv_basis_laufen`
|
||||||
|
zerschnitten (Teilmonat-Branches hinter der neuen Methode) — BV-Regel
|
||||||
|
NoneType-Crash in 3 bestehenden Tests; repariert (Branches vor die
|
||||||
|
Fiktion-Methode zurück). Lehre: method-in-Middle-Inserts nie —
|
||||||
|
Anhängen am Methodenende.
|
||||||
|
- **Tests:** 9 neue (Fenster 6/12/18, Randmonat-Proxy, SM-Fixsatz/
|
||||||
|
Altersfilter/Blöcke, organ_mehr-SV-Sperre + mBGM-Ausschluss + LSt-
|
||||||
|
Weiterlauf, PRD-Fiktion). Suite **252/252** (85 Kern + 18 GemBG +
|
||||||
|
213 Privat + 10 Dokumente) frischer -i-Lauf. O2M-Zuweisung hängt an
|
||||||
|
(kein Replace) — auch im SM-Test bestätigt.
|
||||||
|
- **Uncommittet:** AP17 + AP18/AP19 — Commit nach Nutzerfreigabe.
|
||||||
|
|
||||||
|
**AP17 Meldewesen-Reste umgesetzt (L17 · § 109a/§ 109b · GfB-Jahreszahlung) — 247/247 Tests grün**
|
||||||
|
(2026-09-16, uncommittet bis Nutzerfreigabe):
|
||||||
|
|
||||||
|
- **Gates erledigt:** ODL-PDF-Extraktion der DM-ORG-Kapitel E.24 (L17 A1),
|
||||||
|
E.16 (W1/§ 109a), E.26 (B1/§ 109b) → /tmp/odl_dmorg (PDF-verbindlich, txt
|
||||||
|
feldversetzt); RIS-Normtexte: § 109a EStG NOR40263382, § 109b EStG
|
||||||
|
NOR40263383 (100.000-€-Schwelle, AL31/32/33 = Abs 2 Z 1–3, Frist Ende
|
||||||
|
Februar elektronisch/Jänner Papier), § 58 Abs 8 ASVG NOR40211781
|
||||||
|
(jährliche GfB-Vereinbarung, Fälligkeit 15. Jänner), § 6 Abs 2a BMSVG
|
||||||
|
(2,5 vH Zuschlag, Wechsel nur Jahresende).
|
||||||
|
- **Kern:** `lohnzettel_l17.py` (Satzstruktur A1, 102 Felder/1100, je LZP
|
||||||
|
je DN; KZ-Mapping aus GP3-Flags — **Prüfkatalog-Semantik: KZ 350 =
|
||||||
|
Brutto GESAMT, 351/352/354 Teilbeträge**; 351 = sonstig ohne Abs-3/6,
|
||||||
|
352 = abs3/abs6, 354 = §-68-ÜStd-Freibetrag, 357 = SVDN laufend, 347 =
|
||||||
|
SVDN_SZ; formale Prüfungen: 350-Formel, 357 ≤ 40 %, 347 ≤ 25 %,
|
||||||
|
354/300-HOPA-Caps aliquotiert); Info-Satz I1 (ARTD „LZ“) wiederverwendet.
|
||||||
|
`mitteilung_109.py` (W1/B1-Modelle + LF-Datei; Schwellen
|
||||||
|
`at_m109_schwellen` 450/900/100.000 als hr.rule.parameter; B1:
|
||||||
|
AL31/32/33, LKZ1–7, UID, 3 NP-Blöcke, Ausnahmen § 99/KSt als Fallauswahl).
|
||||||
|
Fristenkalender: `l17` + `mitteilung_109b` Fristarten (Ende Februar).
|
||||||
|
- **⚠ §-Korrekturen:** L17-KZ 350 = gesamt (Prüfkatalog-Formel 28 ≥
|
||||||
|
30+32+34+…); B738/739 nur ≤ 2017; § 34 AngG = Verjährung (nicht
|
||||||
|
Auflösung — Dokumente-Thread); Plan-Anker „§ 51 Abs 4 Z 2 ASVG“
|
||||||
|
korrigiert auf § 6 Abs 2a BMSVG.
|
||||||
|
- **Privat:** mBGM jetzt mit **GfB (B010/B030)** — T01 = § 53a-UV-Pauschale
|
||||||
|
(6,50 € 2026, VPTA 0) + V01 BMSVG 1,53 %; kein T02/A-Abschläge (§ 49
|
||||||
|
Abs 3). **JAGB-Trennung:** Company-Flag `l10n_at_gfb_jaehrliche_abrechnung`
|
||||||
|
→ eigenes PS-Paket mit JAGB=„J“ je Kalendermonat (D.52), REFP-Suffix
|
||||||
|
„-JAGB“; ohne Flag im normalen Paket. **GfB-Jahresabrechnung:**
|
||||||
|
`_l10n_at_bv_jahresabrechnung_gfb(jahr)` — Summe JAGB-Beiträge + 2,5 %
|
||||||
|
Zuschlag (Z04 „BV-Zuschlag bei jährlicher Zahlung“, D.60) im Protokoll
|
||||||
|
der Dezember-Abrechnung, Fälligkeit 15. 1. Folgejahr. Fallweise (G3/G4)
|
||||||
|
bleibt dokumentierter Rest.
|
||||||
|
- **Tests:** kern +12 (A1-Positionen, KZ-Mapping, Prüfkatalog-Logik,
|
||||||
|
W1/B1-Positionen, Schwellen-Skip, kumulative Grenze, Fristen);
|
||||||
|
privat 5 (GfB-mBGM B030/T01-Pauschale, JAGB-Trennung,
|
||||||
|
Jahresabrechnung-Zuschlag). Suite **247/247** (85 Kern + 18 GemBG +
|
||||||
|
204 Privat + 10 Dokumente) frischer -i-Lauf.
|
||||||
|
- **OdmBGM-Fund:** O2M-Zuweisung `line_ids = [(0,0,…)]` HÄNGT AN (kein
|
||||||
|
Replace) — im Testschrieb berücksichtigt; `gemeldet`-Feld readonly,
|
||||||
|
Lauf setzt es per Write.
|
||||||
|
|
||||||
**Modul l10n_at_payroll_dokumente (Dokumentgenerierung) umgesetzt — 236/236 Tests grün**
|
**Modul l10n_at_payroll_dokumente (Dokumentgenerierung) umgesetzt — 236/236 Tests grün**
|
||||||
(2026-09-16, uncommittet bis Nutzerfreigabe; Modul **19.0.1.0.0**):
|
(2026-09-16, uncommittet bis Nutzerfreigabe; Modul **19.0.1.0.0**):
|
||||||
|
|
||||||
|
|||||||
@@ -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_lst_2026.xml',
|
||||||
'data/rule_parameters_sv_2026.xml',
|
'data/rule_parameters_sv_2026.xml',
|
||||||
'data/rule_parameters_2027.xml',
|
'data/rule_parameters_2027.xml',
|
||||||
|
'data/rule_parameters_109.xml',
|
||||||
'data/tasy_mapping.xml',
|
'data/tasy_mapping.xml',
|
||||||
'data/hr_departure_reason_seed.xml',
|
'data/hr_departure_reason_seed.xml',
|
||||||
'views/tasy_views.xml',
|
'views/tasy_views.xml',
|
||||||
'views/tasy_katalog_views.xml',
|
'views/tasy_katalog_views.xml',
|
||||||
'views/company_views.xml',
|
'views/company_views.xml',
|
||||||
'views/meldewesen_views.xml',
|
'views/meldewesen_views.xml',
|
||||||
|
'views/meldewesen_finanz_views.xml',
|
||||||
|
'views/schwerarbeitsmeldung_views.xml',
|
||||||
'views/fristen_views.xml',
|
'views/fristen_views.xml',
|
||||||
'report/belege_reports.xml',
|
'report/belege_reports.xml',
|
||||||
'report/jahresreports.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 meldewesen
|
||||||
from . import kind
|
from . import kind
|
||||||
from . import lohnzettel_l16
|
from . import lohnzettel_l16
|
||||||
|
from . import lohnzettel_l17
|
||||||
|
from . import mitteilung_109
|
||||||
|
from . import schwerarbeitsmeldung
|
||||||
from . import fristenkalender
|
from . import fristenkalender
|
||||||
from . import jahresretro
|
from . import jahresretro
|
||||||
from . import mitteilungsfile
|
from . import mitteilungsfile
|
||||||
|
|||||||
@@ -92,7 +92,38 @@ FRISTART_DEFINITIONEN = {
|
|||||||
'hilfetext': 'Meldung der Honorare außerhalb von '
|
'hilfetext': 'Meldung der Honorare außerhalb von '
|
||||||
'Dienstverhältnissen bis Ende Februar via '
|
'Dienstverhältnissen bis Ende Februar via '
|
||||||
'ELDA (8 Tätigkeitsgruppen; kumulativ ≤ 900 € '
|
'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': {
|
'schwerarbeit': {
|
||||||
'name': 'Schwerarbeitsmeldung (ASchV § 5)',
|
'name': 'Schwerarbeitsmeldung (ASchV § 5)',
|
||||||
@@ -224,7 +255,7 @@ class L10nAtPayrollFrist(models.Model):
|
|||||||
# Jahresfristen nur am 1. Jänner (für das Vorjahr).
|
# Jahresfristen nur am 1. Jänner (für das Vorjahr).
|
||||||
if heute.month == 1:
|
if heute.month == 1:
|
||||||
for art in ('l16', 'arbeitsstaette', 'paragraph_109a',
|
for art in ('l16', 'arbeitsstaette', 'paragraph_109a',
|
||||||
'schwerarbeit'):
|
'mitteilung_109b', 'l17', 'schwerarbeit'):
|
||||||
monat = FRISTART_DEFINITIONEN[art]['monat']
|
monat = FRISTART_DEFINITIONEN[art]['monat']
|
||||||
tag = FRISTART_DEFINITIONEN[art]['tag_im_monat']
|
tag = FRISTART_DEFINITIONEN[art]['tag_im_monat']
|
||||||
jahr_deadline = heute.year
|
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 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
|
||||||
@@ -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_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_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_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
|
||||||
|
|||||||
|
@@ -4,4 +4,5 @@ from . import test_tasy_katalog
|
|||||||
from . import test_lohnsteuer
|
from . import test_lohnsteuer
|
||||||
from . import test_sozialversicherung
|
from . import test_sozialversicherung
|
||||||
from . import test_elda_l16
|
from . import test_elda_l16
|
||||||
|
from . import test_elda_l17
|
||||||
from . import test_gp8
|
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>
|
||||||
@@ -61,4 +61,11 @@
|
|||||||
<field name="name">Reisekosten — Nettoausgleich Überhang</field>
|
<field name="name">Reisekosten — Nettoausgleich Überhang</field>
|
||||||
<field name="code">ATP_REISE_NETTOAUSGLEICH</field>
|
<field name="code">ATP_REISE_NETTOAUSGLEICH</field>
|
||||||
</record>
|
</record>
|
||||||
|
<!-- AP19: Präsenz-/Ausbildungs-/Zivildienst-Tage (§ 7 Abs 1/2
|
||||||
|
BMSVG) für die fiktive BV-Bemessungsgrundlage (at_bv_fiktiv_kbg). -->
|
||||||
|
<record id="input_type_atp_prd_tage" model="hr.payslip.input.type">
|
||||||
|
<field name="name">Präsenz-/Ausbildungs-/Zivildienst — Tage</field>
|
||||||
|
<field name="code">ATP_PRD_TAGE</field>
|
||||||
|
<field name="is_quantity" eval="True"/>
|
||||||
|
</record>
|
||||||
</odoo>
|
</odoo>
|
||||||
@@ -24,4 +24,17 @@
|
|||||||
<field name="date_from">2026-01-01</field>
|
<field name="date_from">2026-01-01</field>
|
||||||
<field name="parameter_value">{'B001': {'laufend': 39.05, 'sz': 37.55}, 'B002': {'laufend': 39.05, 'sz': 37.55}}</field>
|
<field name="parameter_value">{'B001': {'laufend': 39.05, 'sz': 37.55}, 'B002': {'laufend': 39.05, 'sz': 37.55}}</field>
|
||||||
</record>
|
</record>
|
||||||
|
<!-- AP19: fiktive BG Präsenz-/Ausbildungs-/Zivildienst —
|
||||||
|
KBGG § 3 Abs 1 idF vor BGBl I 53/2016: 2026: 435,90 €
|
||||||
|
(lb-vor-04, Stand 2026-08); BMSVG § 7 Abs 1/2. -->
|
||||||
|
<record id="hr_rule_parameter_at_bv_fiktiv_kbg" model="hr.rule.parameter">
|
||||||
|
<field name="name">BMSVG § 7 — fiktive BG KBGG-Basis</field>
|
||||||
|
<field name="code">at_bv_fiktiv_kbg</field>
|
||||||
|
<field name="country_id" ref="base.at"/>
|
||||||
|
</record>
|
||||||
|
<record id="hr_rule_parameter_value_at_bv_fiktiv_kbg" model="hr.rule.parameter.value">
|
||||||
|
<field name="rule_parameter_id" ref="hr_rule_parameter_at_bv_fiktiv_kbg"/>
|
||||||
|
<field name="date_from">2026-01-01</field>
|
||||||
|
<field name="parameter_value">435.90</field>
|
||||||
|
</record>
|
||||||
</odoo>
|
</odoo>
|
||||||
@@ -22,6 +22,17 @@
|
|||||||
<field name="l10n_at_nacht_sv" eval="True"/>
|
<field name="l10n_at_nacht_sv" eval="True"/>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
|
<!-- AP18: Nachtschwerarbeit-Tage (NSchG Art VII iVm Art XI Abs 6; -->
|
||||||
|
<!-- mandantenseitige Katalog-Klassifikation ASchV/NSchG — Zähler- -->
|
||||||
|
<!-- grundlage für den Nachtschwerarbeitsmonat) -->
|
||||||
|
<record id="wett_nachtschwer" model="hr.work.entry.type">
|
||||||
|
<field name="name">Nachtschwerarbeit (Tag)</field>
|
||||||
|
<field name="color">24</field>
|
||||||
|
<field name="display_code">NSW</field>
|
||||||
|
<field name="code">ATP_NACHTSCHWER</field>
|
||||||
|
<field name="country_id" ref="base.at"/>
|
||||||
|
</record>
|
||||||
|
|
||||||
<!-- Überstunden -->
|
<!-- Überstunden -->
|
||||||
<record id="wett_ue_std" model="hr.work.entry.type">
|
<record id="wett_ue_std" model="hr.work.entry.type">
|
||||||
<field name="name">Überstunde Tag (50 %)</field>
|
<field name="name">Überstunde Tag (50 %)</field>
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ from . import buchungsbeleg
|
|||||||
from . import wien_dag
|
from . import wien_dag
|
||||||
from . import efzg
|
from . import efzg
|
||||||
from . import mutterschutz
|
from . import mutterschutz
|
||||||
|
from . import nachtschwerarbeit
|
||||||
from . import karenz
|
from . import karenz
|
||||||
from . import aeb
|
from . import aeb
|
||||||
from . import pfaendung
|
from . import pfaendung
|
||||||
|
|||||||
@@ -222,9 +222,11 @@ class HrPayslip(models.Model):
|
|||||||
# AP4: fiktive BG während des Krankengeldbezugs (§ 7 Abs 3
|
# AP4: fiktive BG während des Krankengeldbezugs (§ 7 Abs 3
|
||||||
# BMSVG) und des Wochengeldbezugs (§ 7 Abs 4, AP7) — nur
|
# BMSVG) und des Wochengeldbezugs (§ 7 Abs 4, AP7) — nur
|
||||||
# in beitragspflichtigen Läufen (Erstfenster-Teilmonate:
|
# in beitragspflichtigen Läufen (Erstfenster-Teilmonate:
|
||||||
# dokumentierter Randfall).
|
# dokumentierter Randfall). AP18/AP19: Präsenz-/Ausbildungs-
|
||||||
|
# und Zivildienst-Fiktion (§ 7 Abs 1/2, ATP_PRD_TAGE).
|
||||||
return basis + self._at_bv_fiktiv_krankengeld() \
|
return basis + self._at_bv_fiktiv_krankengeld() \
|
||||||
+ self._at_bv_fiktiv_wochengeld()
|
+ self._at_bv_fiktiv_wochengeld() \
|
||||||
|
+ self._at_bv_fiktiv_prasenzenzdienst()
|
||||||
if pflichtig_ab > self.date_to:
|
if pflichtig_ab > self.date_to:
|
||||||
# LZP liegt zur Gänze im beitragsfreien Erstfenster.
|
# LZP liegt zur Gänze im beitragsfreien Erstfenster.
|
||||||
return 0.0
|
return 0.0
|
||||||
@@ -235,6 +237,33 @@ class HrPayslip(models.Model):
|
|||||||
return 0.0
|
return 0.0
|
||||||
return basis * tage / max(self._l10n_at_lohnsteuertage(), 1)
|
return basis * tage / max(self._l10n_at_lohnsteuertage(), 1)
|
||||||
|
|
||||||
|
def _at_bv_fiktiv_prasenzenzdienst(self):
|
||||||
|
"""Fiktive BV-Grundlage während des Präsenz-/Ausbildungs-
|
||||||
|
und Zivildienstes (§ 7 Abs 1/2 BMSVG; RIS-Verifikation lokal
|
||||||
|
BMSVG-Volltext 2025-07, lb-vor-04): 1,53 % der fiktiven BG in
|
||||||
|
Höhe des KBGG § 3 Abs 1 idF vor BGBl I 53/2016 — 2026: 435,90 €
|
||||||
|
(Parameter at_bv_fiktiv_kbg, Stand lb-vor-04) — aliquot auf
|
||||||
|
die Dienst-Tage je Kalendertage des LZP. ⚠ >12 Monate
|
||||||
|
Zeitsoldat/Auslandseinsatz-/Ausbildungsdienst: AG-Beitrag
|
||||||
|
entfällt (Bund zahlt) — mandantenseitig berücksichtigen,
|
||||||
|
Eingabe nicht setzen."""
|
||||||
|
self.ensure_one()
|
||||||
|
tage = 0
|
||||||
|
for eingabe in self.input_line_ids:
|
||||||
|
if eingabe.input_type_id.code == 'ATP_PRD_TAGE' \
|
||||||
|
and eingabe.amount:
|
||||||
|
tage += int(eingabe.amount)
|
||||||
|
if not tage:
|
||||||
|
return 0.0
|
||||||
|
kbg = float(self.env['hr.rule.parameter']
|
||||||
|
._get_parameter_from_code(
|
||||||
|
'at_bv_fiktiv_kbg', self.date_from,
|
||||||
|
raise_if_not_found=False) or 0.0)
|
||||||
|
if not kbg:
|
||||||
|
return 0.0
|
||||||
|
monatstage = (self.date_to - self.date_from).days + 1
|
||||||
|
return kbg * min(tage, monatstage) / float(monatstage)
|
||||||
|
|
||||||
def _at_bv_basis_sonst(self, result_rules=None):
|
def _at_bv_basis_sonst(self, result_rules=None):
|
||||||
"""BV-Grundlage sonstiger Bezüge (Sonderzahlungen & Co.):
|
"""BV-Grundlage sonstiger Bezüge (Sonderzahlungen & Co.):
|
||||||
ungecappt (kein HBG-/Jahresdeckel, § 6 Abs 5) und ohne
|
ungecappt (kein HBG-/Jahresdeckel, § 6 Abs 5) und ohne
|
||||||
|
|||||||
@@ -134,6 +134,30 @@ class HrVersion(models.Model):
|
|||||||
version.erfahrungsstufe = \
|
version.erfahrungsstufe = \
|
||||||
version.l10n_at_vwgj_stufe_vorschlag
|
version.l10n_at_vwgj_stufe_vorschlag
|
||||||
|
|
||||||
|
l10n_at_status = fields.Selection(
|
||||||
|
[('an', 'Arbeitnehmer'),
|
||||||
|
('organ_minder', 'Organ ≤ 25 %% — SV-pflichtig'),
|
||||||
|
('organ_mehr', 'Organ ≥ 25 %% — kein SV-Verhältnis')],
|
||||||
|
string='AT Status (Organ/Geschäftsführung)',
|
||||||
|
default='an', required=True,
|
||||||
|
groups="hr_payroll.group_hr_payroll_user",
|
||||||
|
help='AP19 (lb-vst-03, lb-gsf-02/03/05): vertragsrechtlich freie '
|
||||||
|
'Dienstverhältnisse von Organnen. Organ ≥ 25 % Beteiligung: '
|
||||||
|
'kein SV-Verhältnis (§ 4 Abs 2 ASVG-Ausnahme — kein SVDN/SVDG, '
|
||||||
|
'kein mBGM, LSt normal); Organ ≤ 25 %: pflichtversichert wie '
|
||||||
|
'Dienstnehmer (§ 4 Abs 2 dritter Satz ASVG). '
|
||||||
|
'Mandanten-Entscheid je Fall; Rechtsberatung vor Umstellung.')
|
||||||
|
l10n_at_ferialpraktikant = fields.Boolean(
|
||||||
|
string='AT Ferialpraktikant/Praktikum',
|
||||||
|
groups="hr_payroll.group_hr_payroll_user",
|
||||||
|
help='AP19 (lb-pra-01…04): Ferialpraktikum/Volontariat. '
|
||||||
|
'SV/LSt laufen über die normale Entgelt-Logik (§ 4 Abs 2 '
|
||||||
|
'ASVG: unter GFG nur UV; Volontariat ohne Taschengeld: '
|
||||||
|
'UV-Teilversicherung § 8 Abs 1 Z 3 lit c — Meldewesen, '
|
||||||
|
'nicht Entgeltrechnung). BMSVG: § 6 nennt keinen '
|
||||||
|
'Ferialpraktikanten-Ausschluss — Beitrag wie sonstig '
|
||||||
|
'(Erstfenster).')
|
||||||
|
|
||||||
|
|
||||||
class HrEmployee(models.Model):
|
class HrEmployee(models.Model):
|
||||||
_inherit = 'hr.employee'
|
_inherit = 'hr.employee'
|
||||||
|
|||||||
@@ -300,6 +300,47 @@ class L10nAtPayrollMbgm(models.Model):
|
|||||||
positionen.append(('V01', 1.53, bv_basis * 0.0153, 'BV'))
|
positionen.append(('V01', 1.53, bv_basis * 0.0153, 'BV'))
|
||||||
return positionen
|
return positionen
|
||||||
|
|
||||||
|
def _l10n_at_gfb_positionen(self, slip):
|
||||||
|
"""AP17: Verrechnungspositionen für geringfügig Beschäftigte
|
||||||
|
(BSGR B010/B030): T01 = § 53a-UV-Pauschalbeitrag (Engine:
|
||||||
|
``_l10n_at_svdg_uv_geringfuegig`` — 2026 6,50 €/Monat, abgeleitet
|
||||||
|
aus aBG 591,29 × 1,1 %; § 51 Abs 2 ASVG), V01 = BMSVG-Beitrag
|
||||||
|
1,53 % (Abfertigungsbeitrag § 5 Abs 2 BMSVG, Engine-Position
|
||||||
|
BV). Keine AV-/IE-Anteile, keine Abschläge (A01–A03/A09/A10/
|
||||||
|
A15), kein T02 — GfB-SZ sind beitragsfrei (§ 49 Abs 3;
|
||||||
|
lb-bes-06)."""
|
||||||
|
self.ensure_one()
|
||||||
|
uv_pauschale = slip._l10n_at_svdg_uv_geringfuegig()
|
||||||
|
positionen = []
|
||||||
|
if uv_pauschale:
|
||||||
|
positionen.append(('T01', 0.0, uv_pauschale, 'AB'))
|
||||||
|
bv_basis = (slip._at_bv_basis_laufen()
|
||||||
|
+ slip._at_bv_basis_sonst())
|
||||||
|
if bv_basis > 0:
|
||||||
|
positionen.append(('V01', 1.53, bv_basis * 0.0153, 'BV'))
|
||||||
|
return positionen
|
||||||
|
|
||||||
|
def _l10n_at_bv_jahresabrechnung_gfb(self, jahr):
|
||||||
|
"""Jährliche Abrechnung GfB (§ 58 Abs 8 ASVG iVm § 6 Abs 2a
|
||||||
|
BMSVG): Summe der JAGB-mBGM-Beiträge des Kalenderjahres und
|
||||||
|
der 2,5 vH-Zuschlag (Z04 „BV-Zuschlag bei jährlicher Zahlung“,
|
||||||
|
D.60) — Fälligkeit bis 15. Jänner des Folgejahres."""
|
||||||
|
self.ensure_one()
|
||||||
|
deklarationen = self.search([
|
||||||
|
('company_id', '=', self.company_id.id),
|
||||||
|
('beitragsmonat', '>=', date(jahr, 1, 1)),
|
||||||
|
('beitragsmonat', '<=', date(jahr, 12, 31)),
|
||||||
|
('art', '=', 'MB'),
|
||||||
|
('state', '=', 'done'),
|
||||||
|
])
|
||||||
|
summe = sum(
|
||||||
|
line.vsum
|
||||||
|
for deklaration in deklarationen
|
||||||
|
for line in deklaration.line_ids.filtered('jagb'))
|
||||||
|
zuschlag = round(summe * 0.025, 2)
|
||||||
|
return {'summe': summe, 'zuschlag': zuschlag,
|
||||||
|
'gesamt': round(summe + zuschlag, 2)}
|
||||||
|
|
||||||
def action_erzeugen(self):
|
def action_erzeugen(self):
|
||||||
for deklaration in self:
|
for deklaration in self:
|
||||||
fehler = []
|
fehler = []
|
||||||
@@ -310,9 +351,17 @@ class L10nAtPayrollMbgm(models.Model):
|
|||||||
fehler.append('Beitragskontonummer (BKNR) fehlt.')
|
fehler.append('Beitragskontonummer (BKNR) fehlt.')
|
||||||
vstr = (bknr or '')[:2]
|
vstr = (bknr or '')[:2]
|
||||||
deklaration.line_ids.unlink()
|
deklaration.line_ids.unlink()
|
||||||
datensaetze = []
|
# AP17: mBGM-Pakete — 'normal' (JAGB 'N') und — bei
|
||||||
summe_gesamt = 0
|
# jährlicher Abrechnung GfB (§ 58 Abs 8 ASVG iVm § 6
|
||||||
anzahl = 0
|
# Abs 2a BMSVG) — ein separates JAGB-Paket (E.32.2.2.1/D.52)
|
||||||
|
# für die GfB-Beitragsgrundlagen je Kalendermonat.
|
||||||
|
paket_gruppen = {
|
||||||
|
'normal': {'saetze': [], 'summe': 0.0, 'anzahl': 0},
|
||||||
|
'jagb': {'saetze': [], 'summe': 0.0, 'anzahl': 0},
|
||||||
|
}
|
||||||
|
jagb_flag = company.l10n_at_gfb_jaehrliche_abrechnung
|
||||||
|
if deklaration.art == 'ST':
|
||||||
|
jagb_flag = False
|
||||||
if deklaration.art != 'FZ':
|
if deklaration.art != 'FZ':
|
||||||
slips = deklaration._l10n_at_slips()
|
slips = deklaration._l10n_at_slips()
|
||||||
nach_an = {}
|
nach_an = {}
|
||||||
@@ -325,6 +374,13 @@ class L10nAtPayrollMbgm(models.Model):
|
|||||||
'Mitarbeiter %s ohne Versicherungsnummer.'
|
'Mitarbeiter %s ohne Versicherungsnummer.'
|
||||||
% employee.name)
|
% employee.name)
|
||||||
continue
|
continue
|
||||||
|
if getattr(version, 'l10n_at_status', 'an') \
|
||||||
|
== 'organ_mehr':
|
||||||
|
warnungen.append(
|
||||||
|
'Mitarbeiter %s: Organ ≥ 25 %% (kein SV-'
|
||||||
|
'Verhältnis, AP19) — keine mBGM; LSt/Lohnzettel '
|
||||||
|
'unverändert.' % employee.name)
|
||||||
|
continue
|
||||||
if len(employee_slips) > 1:
|
if len(employee_slips) > 1:
|
||||||
fehler.append(
|
fehler.append(
|
||||||
'Mitarbeiter %s mit mehreren Läufen im '
|
'Mitarbeiter %s mit mehreren Läufen im '
|
||||||
@@ -356,7 +412,8 @@ class L10nAtPayrollMbgm(models.Model):
|
|||||||
storno_satz.setze('VSNR', version.ssnid)
|
storno_satz.setze('VSNR', version.ssnid)
|
||||||
storno_satz.setze('VSUM', abs(original.vsum),
|
storno_satz.setze('VSUM', abs(original.vsum),
|
||||||
cent=True)
|
cent=True)
|
||||||
datensaetze.append(storno_satz)
|
paket_gruppen['normal']['saetze'].append(
|
||||||
|
storno_satz)
|
||||||
deklaration.env['l10n.at.payroll.mbgm.line']\
|
deklaration.env['l10n.at.payroll.mbgm.line']\
|
||||||
.create({
|
.create({
|
||||||
'mbgm_id': deklaration.id,
|
'mbgm_id': deklaration.id,
|
||||||
@@ -366,43 +423,63 @@ class L10nAtPayrollMbgm(models.Model):
|
|||||||
'refw': refw,
|
'refw': refw,
|
||||||
'vsum': original.vsum,
|
'vsum': original.vsum,
|
||||||
})
|
})
|
||||||
summe_gesamt += original.vsum
|
paket_gruppen['normal']['summe'] += original.vsum
|
||||||
anzahl += 1
|
paket_gruppen['normal']['anzahl'] += 1
|
||||||
continue
|
continue
|
||||||
bsgr = deklaration._l10n_at_bsgr(version, slip)
|
bsgr = deklaration._l10n_at_bsgr(version, slip)
|
||||||
tarif = deklaration._l10n_at_tarif(bsgr, version)
|
if slip.l10n_at_geringfuegig:
|
||||||
if not tarif:
|
# GfB (AP17): eigenes Beitragsset — § 53a-UV-
|
||||||
|
# Pauschale (T01) + BMSVG 1,53 % (V01), keine
|
||||||
|
# AV-/IE-/Abschlagspositionen, kein T02 (GfB-SZ
|
||||||
|
# beitragsfrei, § 49 Abs 3).
|
||||||
|
positionen = deklaration._l10n_at_gfb_positionen(
|
||||||
|
slip)
|
||||||
|
if not positionen:
|
||||||
|
fehler.append(
|
||||||
|
'Mitarbeiter %s: GfB-Beitragssätze fehlen '
|
||||||
|
'(§-53a-Parameter am Beitragsmonat).'
|
||||||
|
% employee.name)
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
tarif = deklaration._l10n_at_tarif(bsgr, version)
|
||||||
|
if not tarif:
|
||||||
|
fehler.append(
|
||||||
|
'Mitarbeiter %s: kein Tarifsatz für BSGR '
|
||||||
|
'%s (weder TASY-Katalog noch Parameter '
|
||||||
|
'at_mbgm_tarif am Beitragsmonat).'
|
||||||
|
% (employee.name, bsgr))
|
||||||
|
continue
|
||||||
|
positionen = deklaration._l10n_at_positionen(
|
||||||
|
slip, tarif)
|
||||||
|
if version.l10n_at_fallweise:
|
||||||
fehler.append(
|
fehler.append(
|
||||||
'Mitarbeiter %s: kein Tarifsatz für BSGR '
|
'Mitarbeiter %s: fallweise mBGM (G3/G4 mit '
|
||||||
'%s (weder TASY-Katalog noch Parameter '
|
'tagesgenauen Tarifblöcken T2/T5) ist ein '
|
||||||
'at_mbgm_tarif am Beitragsmonat).'
|
'dokumentierter Rest (BSGR %s).'
|
||||||
% (employee.name, bsgr))
|
% (employee.name, bsgr))
|
||||||
continue
|
continue
|
||||||
if slip.l10n_at_geringfuegig \
|
|
||||||
or version.l10n_at_fallweise:
|
|
||||||
fehler.append(
|
|
||||||
'Mitarbeiter %s: geringfügige/fallweise '
|
|
||||||
'Beschäftigung in der mBGM ist ein '
|
|
||||||
'dokumentierter Rest (BSGR %s, Tarifsystem '
|
|
||||||
'verifizieren).' % (employee.name, bsgr))
|
|
||||||
continue
|
|
||||||
positionen = deklaration._l10n_at_positionen(
|
|
||||||
slip, tarif)
|
|
||||||
vsum = sum(rsum for _c, _s, rsum, _b in positionen)
|
vsum = sum(rsum for _c, _s, rsum, _b in positionen)
|
||||||
# Konsistenz gegen die GP4-Payslip-Summen.
|
# Konsistenz gegen die GP4-Payslip-Summen.
|
||||||
ist = (slip._l10n_at_svdn_gesamt()
|
if slip.l10n_at_geringfuegig:
|
||||||
+ slip._l10n_at_svdg_gesamt()
|
ist = (slip._l10n_at_svdg_uv_geringfuegig()
|
||||||
+ slip._l10n_at_svdn_sz()
|
+ slip._at_bv_beitrag())
|
||||||
+ sum(
|
else:
|
||||||
slip._l10n_at_svdg_sz(k)
|
ist = (slip._l10n_at_svdn_gesamt()
|
||||||
for k in ('kv', 'pv', 'av', 'uv', 'iesg'))
|
+ slip._l10n_at_svdg_gesamt()
|
||||||
+ slip._at_bv_beitrag())
|
+ slip._l10n_at_svdn_sz()
|
||||||
|
+ sum(
|
||||||
|
slip._l10n_at_svdg_sz(k)
|
||||||
|
for k in ('kv', 'pv', 'av', 'uv', 'iesg'))
|
||||||
|
+ slip._at_bv_beitrag())
|
||||||
if abs(vsum - ist) > 0.10:
|
if abs(vsum - ist) > 0.10:
|
||||||
warnungen.append(
|
warnungen.append(
|
||||||
'Mitarbeiter %s: mBGM-VSUM %.2f weicht um '
|
'Mitarbeiter %s: mBGM-VSUM %.2f weicht um '
|
||||||
'%.2f von den GP4-Beitragssummen ab '
|
'%.2f von den GP4-Beitragssummen ab '
|
||||||
'(Tarif-/Rundungsdifferenz prüfen).'
|
'(Tarif-/Rundungsdifferenz prüfen).'
|
||||||
% (employee.name, vsum, vsum - ist))
|
% (employee.name, vsum, vsum - ist))
|
||||||
|
gruppe = (
|
||||||
|
'jagb' if (slip.l10n_at_geringfuegig and jagb_flag)
|
||||||
|
else 'normal')
|
||||||
deklaration.env['l10n.at.payroll.mbgm.line'].create({
|
deklaration.env['l10n.at.payroll.mbgm.line'].create({
|
||||||
'mbgm_id': deklaration.id,
|
'mbgm_id': deklaration.id,
|
||||||
'employee_id': employee.id,
|
'employee_id': employee.id,
|
||||||
@@ -413,46 +490,82 @@ class L10nAtPayrollMbgm(models.Model):
|
|||||||
deklaration.beitragsmonat.strftime('%Y%m'),
|
deklaration.beitragsmonat.strftime('%Y%m'),
|
||||||
employee.id),
|
employee.id),
|
||||||
'vsum': vsum,
|
'vsum': vsum,
|
||||||
|
'jagb': gruppe == 'jagb',
|
||||||
})
|
})
|
||||||
datensaetze.extend(
|
paket_gruppen[gruppe]['saetze'].extend(
|
||||||
deklaration._l10n_at_mbgm_datensaetze(
|
deklaration._l10n_at_mbgm_datensaetze(
|
||||||
employee, version, bsgr, positionen, vsum))
|
employee, version, bsgr, positionen, vsum))
|
||||||
summe_gesamt += vsum
|
paket_gruppen[gruppe]['summe'] += vsum
|
||||||
anzahl += 1
|
paket_gruppen[gruppe]['anzahl'] += 1
|
||||||
paket = L10nAtEldaSatz(
|
datensaetze = []
|
||||||
L10N_AT_ELDA_IDTEIL + MBGM_PAKET, 326,
|
for schluessel in ('normal', 'jagb'):
|
||||||
bezeichnung='mBGM-Paket')
|
gruppe = paket_gruppen[schluessel]
|
||||||
paket.setze('SART', 'PS')
|
if not gruppe['anzahl'] and not (
|
||||||
paket.setze('VSTR', vstr)
|
deklaration.art == 'FZ'
|
||||||
paket.setze('REFP', 'MBGM-%d-%s' % (
|
and schluessel == 'normal'):
|
||||||
company.id,
|
continue
|
||||||
deklaration.beitragsmonat.strftime('%Y%m')))
|
paket = L10nAtEldaSatz(
|
||||||
paket.setze('BKNR', bknr)
|
L10N_AT_ELDA_IDTEIL + MBGM_PAKET, 326,
|
||||||
paket.setze('DGNA', company.name[:70])
|
bezeichnung='mBGM-Paket %s' % schluessel)
|
||||||
paket.setze('JAGB', 'N')
|
paket.setze('SART', 'PS')
|
||||||
paket.setze('BZRM', deklaration.beitragsmonat.strftime(
|
paket.setze('VSTR', vstr)
|
||||||
'%m%Y'))
|
paket.setze('REFP', 'MBGM-%d-%s%s' % (
|
||||||
paket.setze('GSVZ', '+' if summe_gesamt >= 0 else '-')
|
company.id,
|
||||||
paket.setze('GSUM', abs(summe_gesamt), cent=True)
|
deklaration.beitragsmonat.strftime('%Y%m'),
|
||||||
paket.setze('ANZM', anzahl)
|
'-JAGB' if schluessel == 'jagb' else ''))
|
||||||
datensaetze.insert(0, paket)
|
paket.setze('BKNR', bknr)
|
||||||
|
paket.setze('DGNA', company.name[:70])
|
||||||
|
paket.setze('JAGB', 'J' if schluessel == 'jagb' else 'N')
|
||||||
|
paket.setze('BZRM', deklaration.beitragsmonat.strftime(
|
||||||
|
'%m%Y'))
|
||||||
|
paket.setze('GSVZ',
|
||||||
|
'+' if gruppe['summe'] >= 0 else '-')
|
||||||
|
paket.setze('GSUM', abs(gruppe['summe']), cent=True)
|
||||||
|
paket.setze('ANZM', gruppe['anzahl'])
|
||||||
|
datensaetze.append(paket)
|
||||||
|
datensaetze.extend(gruppe['saetze'])
|
||||||
|
if jagb_flag and paket_gruppen['jagb']['summe']:
|
||||||
|
warnungen.append(
|
||||||
|
'JAGB-Paket: jährliche Abrechnung der GfB-Beiträge '
|
||||||
|
'(§ 58 Abs 8 ASVG iVm § 6 Abs 2a BMSVG) — Fälligkeit '
|
||||||
|
'bis 15. Jänner des Folgejahres; der jährliche BV-'
|
||||||
|
'Beitragsbetrag ist um 2,5 %% Zuschlag zu erhöhen '
|
||||||
|
'(Summe mBGM-Jahr: siehe Protokoll der '
|
||||||
|
'Dezember-Abrechnung).')
|
||||||
|
if jagb_flag and deklaration.beitragsmonat.month == 12 \
|
||||||
|
and paket_gruppen['jagb']['summe']:
|
||||||
|
jahresabrechnung = deklaration\
|
||||||
|
._l10n_at_bv_jahresabrechnung_gfb(
|
||||||
|
deklaration.beitragsmonat.year)
|
||||||
|
warnungen.append(
|
||||||
|
'GfB-Jahresabrechnung %d: Beiträge %.2f € + '
|
||||||
|
'2,5 %% Zuschlag %.2f € (Z04) = %.2f €, fällig '
|
||||||
|
'bis 15. 1. %d.' % (
|
||||||
|
deklaration.beitragsmonat.year,
|
||||||
|
jahresabrechnung['summe'],
|
||||||
|
jahresabrechnung['zuschlag'],
|
||||||
|
jahresabrechnung['gesamt'],
|
||||||
|
deklaration.beitragsmonat.year + 1))
|
||||||
if fehler:
|
if fehler:
|
||||||
deklaration.protokoll = 'FEHLER:\n%s\nWARNUNGEN:\n%s' % (
|
deklaration.protokoll = 'FEHLER:\n%s\nWARNUNGEN:\n%s' % (
|
||||||
'\n'.join(fehler), '\n'.join(warnungen) or '—')
|
'\n'.join(fehler), '\n'.join(warnungen) or '—')
|
||||||
raise UserError('mBGM-Lauf mit Fehlern abgebrochen:\n'
|
raise UserError('mBGM-Lauf mit Fehlern abgebrochen:\n'
|
||||||
+ '\n'.join(fehler))
|
+ '\n'.join(fehler))
|
||||||
bestand = L10nAtEldaBestand(
|
if datensaetze:
|
||||||
'MB', vstr, '02',
|
bestand = L10nAtEldaBestand(
|
||||||
hersteller={'name': 'odoo-at-payroll', 'kfz': 'A',
|
'MB', vstr, '02',
|
||||||
'plz': company.partner_id.zip,
|
hersteller={'name': 'odoo-at-payroll', 'kfz': 'A',
|
||||||
'ort': company.partner_id.city,
|
'plz': company.partner_id.zip,
|
||||||
'strasse': company.partner_id.street},
|
'ort': company.partner_id.city,
|
||||||
datensaetze=datensaetze)
|
'strasse': company.partner_id.street},
|
||||||
datei = L10nAtEldaDatei([bestand])
|
datensaetze=datensaetze)
|
||||||
|
datei = L10nAtEldaDatei([bestand])
|
||||||
|
deklaration.write({
|
||||||
|
'datei': base64.b64encode(datei.render()),
|
||||||
|
'datei_name': 'mbgm_%s.txt' % (
|
||||||
|
deklaration.beitragsmonat.strftime('%Y%m'),),
|
||||||
|
})
|
||||||
deklaration.write({
|
deklaration.write({
|
||||||
'datei': base64.b64encode(datei.render()),
|
|
||||||
'datei_name': 'mbgm_%s.txt' % (
|
|
||||||
deklaration.beitragsmonat.strftime('%Y%m'),),
|
|
||||||
'protokoll': 'WARNUNGEN:\n%s' % (
|
'protokoll': 'WARNUNGEN:\n%s' % (
|
||||||
'\n'.join(warnungen) or '—'),
|
'\n'.join(warnungen) or '—'),
|
||||||
'state': 'done',
|
'state': 'done',
|
||||||
@@ -525,6 +638,19 @@ class L10nAtPayrollMbgm(models.Model):
|
|||||||
class ResCompany(models.Model):
|
class ResCompany(models.Model):
|
||||||
_inherit = 'res.company'
|
_inherit = 'res.company'
|
||||||
|
|
||||||
|
l10n_at_gfb_jaehrliche_abrechnung = fields.Boolean(
|
||||||
|
string='AT GfB: jährliche Abrechnung der Beiträge (JAGB)',
|
||||||
|
help='Vereinbarung nach § 58 Abs 8 ASVG iVm § 6 Abs 2a BMSVG: '
|
||||||
|
'die Abfertigungs-/BV-Beiträge aus geringfügigen '
|
||||||
|
'Beschäftigungsverhältnissen werden jährlich verrechnet. '
|
||||||
|
'mBGM-seitig je Kalendermonat ein eigenes Paket mit '
|
||||||
|
'JAGB = „J“ (DM-ORG D.52/E.32.2.2.1); Fälligkeit bis '
|
||||||
|
'15. Jänner des Folgejahres (unterjähriges AV-Ende: '
|
||||||
|
'bis 15. des Folgemonats). Bei jährlicher Zahlungsweise '
|
||||||
|
'sind zusätzlich 2,5 vH vom Beitrag zu leisten. '
|
||||||
|
'Änderung der Zahlungsweise nur zum Jahresende und dem '
|
||||||
|
'KV-Träger vor dem Beitragszeitraum zu melden.')
|
||||||
|
|
||||||
l10n_at_nschab = fields.Boolean(
|
l10n_at_nschab = fields.Boolean(
|
||||||
string='AT Nachtschwerarbeitsbeitrags-Betrieb (mBGM-Fallback)',
|
string='AT Nachtschwerarbeitsbeitrags-Betrieb (mBGM-Fallback)',
|
||||||
help='Betrieb mit Nachtschwerarbeit (ÖGK-Tarifsystem '
|
help='Betrieb mit Nachtschwerarbeit (ÖGK-Tarifsystem '
|
||||||
@@ -573,6 +699,10 @@ class L10nAtPayrollMbgmLine(models.Model):
|
|||||||
readonly=True)
|
readonly=True)
|
||||||
versicherungsnummer = fields.Char()
|
versicherungsnummer = fields.Char()
|
||||||
bsgr = fields.Char(string='Beschäftigtengruppe')
|
bsgr = fields.Char(string='Beschäftigtengruppe')
|
||||||
|
jagb = fields.Boolean(
|
||||||
|
string='JAGB-Paket (jährliche Abrechnung GfB)',
|
||||||
|
help='Zeile gehört zum mBGM-Paket für die jährliche Abrechnung '
|
||||||
|
'geringfügig Beschäftigter (DM-ORG D.52).')
|
||||||
refw = fields.Char(string='Referenzwert (REFW)')
|
refw = fields.Char(string='Referenzwert (REFW)')
|
||||||
vsum = fields.Monetary(
|
vsum = fields.Monetary(
|
||||||
string='VSUM', currency_field='currency_id')
|
string='VSUM', currency_field='currency_id')
|
||||||
|
|||||||
@@ -0,0 +1,102 @@
|
|||||||
|
# Part of the odoo-at-payroll project. License: LGPL-3.
|
||||||
|
"""AP18: Nachtschwerarbeitsmonat-Zähler (NSchG Art XI Abs 6) —
|
||||||
|
RIS-verifiziert 2026-09-16 (NOR40144608, RIS GesNr 10008502, BGBl
|
||||||
|
1981/354):
|
||||||
|
|
||||||
|
- **≥ 6 Arbeitstage** Nachtschwerarbeit im Kalendermonat → Nachtschwer-
|
||||||
|
arbeitsmonat;
|
||||||
|
- **< 6 Tage**: Rückblick-Fenster — Monat + 1 Vormonat ≥ **12** Tage bzw.
|
||||||
|
Monat + 2 Vormonate ≥ **18** Tage;
|
||||||
|
- (DZR > 3 Monate: Monat + 5 Vormonate ≥ 36 Tage — dokumentierter Rest,
|
||||||
|
kein Durchrechnungszeitraum-Datenmodell);
|
||||||
|
- **Randmonat**: die 6 erwarteten Nachtschwerarbeitstage werden nur
|
||||||
|
deswegen nicht erreicht, weil die Arbeit nicht am Monats-Ersten begonnen
|
||||||
|
bzw. am Letzten geendet hat — v1 mit Pro-rata-Proxy (Tage ≥ 6 × belegte
|
||||||
|
Monatskalendertage / Monatskalendertage, aufgerundet) implementiert ⚠
|
||||||
|
(Arbeitszeiteinteilung der Muster im Parallellauf verifizieren);
|
||||||
|
- Arbeitsunterbrechungen bleiben außer Betracht, solange die PV-Pflicht-
|
||||||
|
versicherung weiterbesteht.
|
||||||
|
|
||||||
|
Datenbasis: Work Entries des Typs ``ATP_NACHTSCHWER`` (mandantenseitige
|
||||||
|
Klassifikation der Nachtschwerarbeit nach NSchG Art VII Abs 2/4 bzw. ASchV
|
||||||
|
§ 1 — die Katalog-Kriterien kann die Engine nicht ableiten). Abgleich mit
|
||||||
|
der E01-Belegung (mBGM-Ergb, AP0/TASY) als Protokoll-Hinweis in der
|
||||||
|
mBGM-Generierung."""
|
||||||
|
from dateutil.relativedelta import relativedelta
|
||||||
|
|
||||||
|
from odoo import api, fields, models
|
||||||
|
|
||||||
|
_NACHTSCHWER_WET = 'ATP_NACHTSCHWER'
|
||||||
|
|
||||||
|
|
||||||
|
class HrVersion(models.Model):
|
||||||
|
_inherit = 'hr.version'
|
||||||
|
|
||||||
|
def _l10n_at_nachtschwer_tage(self, monat_erster, monat_letzter):
|
||||||
|
"""Tage mit Nachtschwerarbeit-WETs im (Teil-)Monat."""
|
||||||
|
self.ensure_one()
|
||||||
|
eintraege = self.env['hr.work.entry'].search([
|
||||||
|
('employee_id', '=', self.employee_id.id),
|
||||||
|
('date', '>=', monat_erster),
|
||||||
|
('date', '<=', monat_letzter),
|
||||||
|
('state', '!=', 'cancelled'),
|
||||||
|
('work_entry_type_id.code', '=', _NACHTSCHWER_WET),
|
||||||
|
])
|
||||||
|
return len({eintrag.date for eintrag in eintraege})
|
||||||
|
|
||||||
|
|
||||||
|
class HrPayslip(models.Model):
|
||||||
|
_inherit = 'hr.payslip'
|
||||||
|
|
||||||
|
l10n_at_nachtschwerarbeitsmonat = fields.Boolean(
|
||||||
|
string='AT Nachtschwerarbeitsmonat',
|
||||||
|
compute='_compute_l10n_at_nachtschwerarbeitsmonat',
|
||||||
|
store=True,
|
||||||
|
help='NSchG Art XI Abs 6 (RIS NOR40144608): ≥ 6 Nachtschwerarbeits-'
|
||||||
|
'tage im Monat bzw. Rückblick-Fenster (12/18; 36 bei DZR > '
|
||||||
|
'3 Monate — dokumentierter Rest) bzw. Randmonat (Pro-rata-'
|
||||||
|
'Proxy ⚠). Grundlage NSchAB 3,80 % (Art XI Abs 3 iVm '
|
||||||
|
'Sistierung; TASY-ERGB E01).')
|
||||||
|
|
||||||
|
@api.depends('date_from', 'date_to', 'version_id')
|
||||||
|
def _compute_l10n_at_nachtschwerarbeitsmonat(self):
|
||||||
|
for slip in self:
|
||||||
|
slip.l10n_at_nachtschwerarbeitsmonat = \
|
||||||
|
slip._l10n_at_nachtschwerarbeitsmonat(slip.date_from)
|
||||||
|
|
||||||
|
def _l10n_at_nachtschwerarbeitsmonat(self, stichtag):
|
||||||
|
"""Art XI Abs 6: 6/12/18-Fenster + Randmonat-Proxy."""
|
||||||
|
self.ensure_one()
|
||||||
|
version = self.version_id
|
||||||
|
if not version:
|
||||||
|
return False
|
||||||
|
erster = stichtag.replace(day=1)
|
||||||
|
letzter = (erster + relativedelta(months=1)) \
|
||||||
|
- relativedelta(days=1)
|
||||||
|
monatstage = (letzter - erster).days + 1
|
||||||
|
tage = version._l10n_at_nachtschwer_tage(erster, letzter)
|
||||||
|
if tage >= 6:
|
||||||
|
return True
|
||||||
|
# Randmonat (Art XI Abs 6 letzter Satz): die sechs erwarteten
|
||||||
|
# Nachtschwerarbeitstage werden nur deswegen nicht erreicht,
|
||||||
|
# weil die Arbeit nicht am Monats-Ersten begonnen bzw. am
|
||||||
|
# Letzten geendet hat — v1 Pro-rata-Proxy.
|
||||||
|
beginn = max(erster, self.date_from)
|
||||||
|
ende = min(letzter, self.date_to)
|
||||||
|
if (beginn, ende) != (erster, letzter):
|
||||||
|
belegte = (ende - beginn).days + 1
|
||||||
|
erforderlich = int(-(-6 * belegte // monatstage)) # ceil
|
||||||
|
if tage >= erforderlich:
|
||||||
|
return True
|
||||||
|
# Rückblick-Fenster (12/18): Monat + 1 bzw. + 2 Vormonate.
|
||||||
|
tage_1 = version._l10n_at_nachtschwer_tage(
|
||||||
|
erster - relativedelta(months=1),
|
||||||
|
erster - relativedelta(days=1))
|
||||||
|
if tage + tage_1 >= 12:
|
||||||
|
return True
|
||||||
|
tage_2 = version._l10n_at_nachtschwer_tage(
|
||||||
|
erster - relativedelta(months=2),
|
||||||
|
erster - relativedelta(days=1))
|
||||||
|
if tage + tage_1 + tage_2 >= 18:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
@@ -61,6 +61,18 @@ class HrVersionPrivate(models.Model):
|
|||||||
class HrPayslip(models.Model):
|
class HrPayslip(models.Model):
|
||||||
_inherit = 'hr.payslip'
|
_inherit = 'hr.payslip'
|
||||||
|
|
||||||
|
def _l10n_at_sv_satz(self, seite, komponente, monat=None,
|
||||||
|
staffel_basis=None):
|
||||||
|
"""AP19: Organ ≥ 25 % Beteiligung — kein SV-Verhältnis
|
||||||
|
(lb-vst-03; § 4 Abs 2 ASVG-Ausnahme) → alle Beitragssätze 0;
|
||||||
|
LSt läuft unverändert weiter."""
|
||||||
|
self.ensure_one()
|
||||||
|
if getattr(self.version_id, 'l10n_at_status', 'an') \
|
||||||
|
== 'organ_mehr':
|
||||||
|
return 0.0
|
||||||
|
return super()._l10n_at_sv_satz(
|
||||||
|
seite, komponente, monat=monat, staffel_basis=staffel_basis)
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
# Arbeitstage (resource_calendar) — § 17 Abs 2 KV, user convention:
|
# Arbeitstage (resource_calendar) — § 17 Abs 2 KV, user convention:
|
||||||
# Werktage des KV werden als Arbeitstage aus dem Ressourcenkalender
|
# Werktage des KV werden als Arbeitstage aus dem Ressourcenkalender
|
||||||
|
|||||||
@@ -19,3 +19,5 @@ from . import test_reisekosten_private
|
|||||||
from . import test_pendler_private
|
from . import test_pendler_private
|
||||||
from . import test_endabrechnung_private
|
from . import test_endabrechnung_private
|
||||||
from . import test_karenz_private
|
from . import test_karenz_private
|
||||||
|
from . import test_ap17_private
|
||||||
|
from . import test_ap18_ap19_private
|
||||||
|
|||||||
@@ -0,0 +1,132 @@
|
|||||||
|
# Part of the odoo-at-payroll project. License: LGPL-3.
|
||||||
|
"""AP17 privat: mBGM für geringfügig Beschäftigte (BSGR B010/B030 —
|
||||||
|
§ 53a-UV-Pauschale T01 + BMSVG-Beitrag V01) und die JAGB-Paket-Trennung
|
||||||
|
für die jährliche Abrechnung (§ 58 Abs 8 ASVG iVm § 6 Abs 2a BMSVG,
|
||||||
|
DM-ORG D.52/E.32.2.2.1) inkl. 2,5 vH-Zuschlag (Z04) in der
|
||||||
|
GfB-Jahresabrechnung."""
|
||||||
|
import base64
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
from odoo.tests import TransactionCase, tagged
|
||||||
|
|
||||||
|
|
||||||
|
@tagged('post_install', '-at_install')
|
||||||
|
class Ap17GfbMbgmTest(TransactionCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super().setUp()
|
||||||
|
self.company = self.env.company
|
||||||
|
self.company.write({
|
||||||
|
'l10n_at_beitragskontonummer': '1412345678',
|
||||||
|
'l10n_at_lohnsteuernummer': '123456789',
|
||||||
|
})
|
||||||
|
self.category = self.env['hr.salary.rule.category'].create({
|
||||||
|
'name': 'Test AT AP17', 'code': 'TEST_AT_AP17'})
|
||||||
|
self.structure_type = self.env[
|
||||||
|
'hr.payroll.structure.type'].create({
|
||||||
|
'name': 'Test AT AP17 Type'})
|
||||||
|
self.struktur_gfb = self.env['hr.payroll.structure'].create({
|
||||||
|
'name': 'Test AT AP17 GfB',
|
||||||
|
'code': 'TEST_AT_AP17_GFB',
|
||||||
|
'type_id': self.structure_type.id,
|
||||||
|
})
|
||||||
|
self.env['hr.salary.rule'].create({
|
||||||
|
'name': 'LFD', 'code': 'LFD',
|
||||||
|
'struct_id': self.struktur_gfb.id,
|
||||||
|
'category_id': self.category.id,
|
||||||
|
'condition_select': 'none',
|
||||||
|
'l10n_at_bemessung': 'laufend',
|
||||||
|
'amount_select': 'fix',
|
||||||
|
'amount_fix': 500.0,
|
||||||
|
})
|
||||||
|
employee = self.env['hr.employee'].create({
|
||||||
|
'name': 'AP17 GfB Test', 'birthday': '1990-05-15'})
|
||||||
|
version = self.env['hr.version'].search(
|
||||||
|
[('employee_id', '=', employee.id)], limit=1)
|
||||||
|
version.write({
|
||||||
|
'contract_date_start': datetime.date(2020, 1, 1),
|
||||||
|
'ssnid': '1234150590',
|
||||||
|
'sex': 'male',
|
||||||
|
})
|
||||||
|
self.employee = employee
|
||||||
|
self.version = version
|
||||||
|
|
||||||
|
def _slip(self, monat, jahr=2026, confirm=True):
|
||||||
|
erster = datetime.date(jahr, monat, 1)
|
||||||
|
letzter = (erster.replace(day=28) + datetime.timedelta(days=4)) \
|
||||||
|
.replace(day=1) - datetime.timedelta(days=1)
|
||||||
|
slip = self.env['hr.payslip'].create({
|
||||||
|
'name': 'AP17 %d/%d' % (monat, jahr),
|
||||||
|
'employee_id': self.employee.id,
|
||||||
|
'struct_id': self.struktur_gfb.id,
|
||||||
|
'date_from': erster,
|
||||||
|
'date_to': letzter,
|
||||||
|
})
|
||||||
|
slip.compute_sheet()
|
||||||
|
if confirm:
|
||||||
|
slip.action_payslip_done()
|
||||||
|
return slip
|
||||||
|
|
||||||
|
def _deklaration(self, monat, jahr=2026):
|
||||||
|
deklaration = self.env['l10n.at.payroll.mbgm'].create({
|
||||||
|
'company_id': self.company.id,
|
||||||
|
'beitragsmonat': datetime.date(jahr, monat, 1),
|
||||||
|
'art': 'MB',
|
||||||
|
})
|
||||||
|
deklaration.action_erzeugen()
|
||||||
|
return deklaration
|
||||||
|
|
||||||
|
def test_01_gfb_mbgm_ohne_jagb_flag(self):
|
||||||
|
"""GfB-Lauf: BSGR B030, T01 = § 53a-UV-Pauschale 6,50 € (VPTA 0),
|
||||||
|
V01 BV 1,53 %; ohne Company-Flag im normalen Paket (JAGB 'N')."""
|
||||||
|
slip = self._slip(1)
|
||||||
|
self.assertTrue(slip.l10n_at_geringfuegig)
|
||||||
|
deklaration = self._deklaration(1)
|
||||||
|
self.assertEqual(deklaration.line_ids.bsgr, 'B030')
|
||||||
|
self.assertFalse(deklaration.line_ids.jagb)
|
||||||
|
roh = base64.b64decode(deklaration.datei).decode('ISO-8859-15')
|
||||||
|
zeilen = roh.strip('\r\n').split('\r\n')
|
||||||
|
daten = zeilen[2:-1]
|
||||||
|
paket = daten[0]
|
||||||
|
self.assertEqual(paket[0:2], 'PS')
|
||||||
|
self.assertEqual(paket[170], 'N') # JAGB
|
||||||
|
satzarten = [zeile[0:2] for zeile in daten]
|
||||||
|
# PS + G1 + T1 + BS AB + V1 T01 (Pauschale) + BS BV + V1 V01
|
||||||
|
self.assertEqual(satzarten, ['PS', 'G1', 'T1', 'BS', 'V1',
|
||||||
|
'BS', 'V1'])
|
||||||
|
v1_t01 = daten[4]
|
||||||
|
self.assertEqual(v1_t01[20:23], 'T01')
|
||||||
|
self.assertEqual(v1_t01[24:30], '000000') # VPTA 0 (Pauschale)
|
||||||
|
self.assertEqual(v1_t01[31:42], '00000000650') # RSUM 6,50 €
|
||||||
|
|
||||||
|
def test_02_jagb_paket_trennung(self):
|
||||||
|
"""Company-Flag: das GfB-mBGM läuft ins JAGB-Paket (JAGB 'J',
|
||||||
|
REFP-Suffix, eigenes ANZM/GSUM); kein normales Paket, wenn nur
|
||||||
|
GfB-AN im Monat."""
|
||||||
|
self.company.l10n_at_gfb_jaehrliche_abrechnung = True
|
||||||
|
slip = self._slip(2)
|
||||||
|
deklaration = self._deklaration(2)
|
||||||
|
self.assertTrue(deklaration.line_ids.jagb)
|
||||||
|
roh = base64.b64decode(deklaration.datei).decode('ISO-8859-15')
|
||||||
|
zeilen = roh.strip('\r\n').split('\r\n')
|
||||||
|
daten = zeilen[2:-1]
|
||||||
|
paket = daten[0]
|
||||||
|
self.assertEqual(paket[170], 'J') # JAGB 'J'
|
||||||
|
self.assertIn('JAGB', deklaration.protokoll)
|
||||||
|
self.assertIn('2,5', deklaration.protokoll)
|
||||||
|
|
||||||
|
def test_03_bv_jahresabrechnung_gfb(self):
|
||||||
|
"""Jänner + Februar JAGB-Läufe: Summe der JAGB-Beiträge und
|
||||||
|
2,5 vH-Zuschlag (§ 6 Abs 2a BMSVG; Z04), Fälligkeit Folgejahr."""
|
||||||
|
self.company.l10n_at_gfb_jaehrliche_abrechnung = True
|
||||||
|
self._slip(1)
|
||||||
|
self._slip(2)
|
||||||
|
self._deklaration(1)
|
||||||
|
deklaration_2 = self._deklaration(2)
|
||||||
|
abrechnung = deklaration_2._l10n_at_bv_jahresabrechnung_gfb(2026)
|
||||||
|
self.assertEqual(abrechnung['summe'],
|
||||||
|
sum(deklaration_2.line_ids.mapped('vsum')) * 2)
|
||||||
|
self.assertAlmostEqual(abrechnung['zuschlag'],
|
||||||
|
round(abrechnung['summe'] * 0.025, 2))
|
||||||
|
self.assertAlmostEqual(abrechnung['gesamt'],
|
||||||
|
round(abrechnung['summe'] * 1.025, 2))
|
||||||
@@ -0,0 +1,198 @@
|
|||||||
|
# Part of the odoo-at-payroll project. License: LGPL-3.
|
||||||
|
"""AP18/AP19-Tests: Nachtschwerarbeitsmonat-Zähler (NSchG Art XI Abs 6
|
||||||
|
— 6/12/18-Fenster + Randmonat-Proxy), Schwerarbeitsmeldungs-Fixsatz
|
||||||
|
(E.22, BEST SM, SART 65, Tätigkeitsblöcke, Altersfilter, Erstellvor-
|
||||||
|
schriften), SV-Sperre Organ ≥ 25 % (lb-vst-03) und die fiktive
|
||||||
|
BV-Grundlage Präsenz-/Ausbildungs-/Zivildienst (§ 7 Abs 1/2 BMSVG)."""
|
||||||
|
import base64
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
from odoo import fields
|
||||||
|
from odoo.tests import TransactionCase, tagged
|
||||||
|
|
||||||
|
|
||||||
|
@tagged('post_install', '-at_install')
|
||||||
|
class Ap18Test(TransactionCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super().setUp()
|
||||||
|
self.company = self.env.company
|
||||||
|
self.company.write({
|
||||||
|
'l10n_at_beitragskontonummer': '1412345678',
|
||||||
|
'l10n_at_lohnsteuernummer': '123456789',
|
||||||
|
})
|
||||||
|
employee = self.env['hr.employee'].create({
|
||||||
|
'name': 'AP18 Test', 'birthday': '1970-01-15'})
|
||||||
|
version = self.env['hr.version'].search(
|
||||||
|
[('employee_id', '=', employee.id)], limit=1)
|
||||||
|
version.write({
|
||||||
|
'contract_date_start': datetime.date(2020, 1, 1),
|
||||||
|
'ssnid': '1234150170',
|
||||||
|
'sex': 'male',
|
||||||
|
})
|
||||||
|
self.employee = employee
|
||||||
|
self.version = version
|
||||||
|
|
||||||
|
def _nachtschwer_wets(self, tage, monat, jahr=2026):
|
||||||
|
wett = self.env.ref(
|
||||||
|
'l10n_at_hr_payroll_private.wett_nachtschwer')
|
||||||
|
erster = datetime.date(jahr, monat, 1)
|
||||||
|
for tag in range(1, tage + 1):
|
||||||
|
self.env['hr.work.entry'].create({
|
||||||
|
'name': 'NSchW %d' % tag,
|
||||||
|
'employee_id': self.employee.id,
|
||||||
|
'date': erster.replace(day=tag),
|
||||||
|
'work_entry_type_id': wett.id,
|
||||||
|
})
|
||||||
|
|
||||||
|
def test_01_nachtschwerarbeitsmonat_fenster(self):
|
||||||
|
"""Art XI Abs 6: 6 Tage im Monat → NSchM; 2 + 3 (Vormonat 5):
|
||||||
|
2+5 = 7 < 12 → kein NSchM; 4 + 8 = 12 → NSchM via 12er-Fenster;
|
||||||
|
4 + 4 + 11 = 19 ≥ 18 → 18er-Fenster."""
|
||||||
|
self._nachtschwer_wets(6, 3)
|
||||||
|
slip = self.env['hr.payslip'].create({
|
||||||
|
'name': 'NSchW 3/2026', 'employee_id': self.employee.id,
|
||||||
|
'date_from': datetime.date(2026, 3, 1),
|
||||||
|
'date_to': datetime.date(2026, 3, 31),
|
||||||
|
})
|
||||||
|
self.assertTrue(slip.l10n_at_nachtschwerarbeitsmonat)
|
||||||
|
# 12er-Fenster: März 4 + Februar 8 = 12 → NSchM im April.
|
||||||
|
self.env['hr.work.entry'].search([]).unlink()
|
||||||
|
self._nachtschwer_wets(4, 4)
|
||||||
|
self._nachtschwer_wets(8, 3)
|
||||||
|
slip_4 = self.env['hr.payslip'].create({
|
||||||
|
'name': 'NSchW 4/2026', 'employee_id': self.employee.id,
|
||||||
|
'date_from': datetime.date(2026, 4, 1),
|
||||||
|
'date_to': datetime.date(2026, 4, 30),
|
||||||
|
})
|
||||||
|
self.assertTrue(slip_4.l10n_at_nachtschwerarbeitsmonat)
|
||||||
|
|
||||||
|
def test_02_randmonat_pro_rata(self):
|
||||||
|
"""Randmonat-Proxy: 5 Tage im 15-tägigen Teilmonat —
|
||||||
|
erforderlich = ceil(6 × 15/31) = 2 → 5 ≥ 2 → NSchM (Proxy ⚠)."""
|
||||||
|
self._nachtschwer_wets(5, 5)
|
||||||
|
slip = self.env['hr.payslip'].create({
|
||||||
|
'name': 'NSchW 5/2026 (Randmonat)',
|
||||||
|
'employee_id': self.employee.id,
|
||||||
|
'date_from': datetime.date(2026, 5, 20),
|
||||||
|
'date_to': datetime.date(2026, 5, 31),
|
||||||
|
})
|
||||||
|
self.assertTrue(slip.l10n_at_nachtschwerarbeitsmonat)
|
||||||
|
|
||||||
|
def test_03_schwerarbeitsmeldung_fixsatz(self):
|
||||||
|
"""SART 65 (Länge 800): BKNR/DGNA/VSNR/GEBD/JAHR + Tätigkeits-
|
||||||
|
blöcke (TART '01' an 502, TVON/TBIS); Altersfilter-Warnung."""
|
||||||
|
junger = self.env['hr.employee'].create({
|
||||||
|
'name': 'AP18 Jung', 'birthday': '1995-06-10'})
|
||||||
|
version_j = self.env['hr.version'].search(
|
||||||
|
[('employee_id', '=', junger.id)], limit=1)
|
||||||
|
version_j.write({'ssnid': '1234100695', 'sex': 'male'})
|
||||||
|
meldung = self.env['l10n.at.payroll.schwerarbeitsmeldung'].create({
|
||||||
|
'company_id': self.company.id, 'jahr': 2026})
|
||||||
|
line = self.env[
|
||||||
|
'l10n.at.payroll.schwerarbeitsmeldung.line'].create({
|
||||||
|
'meldung_id': meldung.id,
|
||||||
|
'employee_id': junger.id,
|
||||||
|
'versicherungsnummer': '1234100695',
|
||||||
|
'taetigkeit_ids': [
|
||||||
|
(0, 0, {'ziffer': 1,
|
||||||
|
'von': datetime.date(2026, 1, 1),
|
||||||
|
'bis': datetime.date(2026, 12, 31)}),
|
||||||
|
],
|
||||||
|
})
|
||||||
|
meldung.action_erzeugen()
|
||||||
|
self.assertEqual(meldung.state, 'done')
|
||||||
|
roh = base64.b64decode(meldung.datei).decode('ISO-8859-15')
|
||||||
|
zeilen = roh.strip('\r\n').split('\r\n')
|
||||||
|
sm_satz = zeilen[2]
|
||||||
|
self.assertEqual(len(sm_satz), 800)
|
||||||
|
self.assertEqual(sm_satz[0:2], '65')
|
||||||
|
self.assertEqual(sm_satz[339:349], '1234100695') # VSNR pos 340
|
||||||
|
self.assertEqual(sm_satz[501:503], '01') # TART_1
|
||||||
|
self.assertEqual(sm_satz[503:507], '0101') # TVON
|
||||||
|
self.assertEqual(sm_satz[507:511], '3112') # TBIS
|
||||||
|
self.assertIn('Altersgrenze', meldung.protokoll)
|
||||||
|
|
||||||
|
|
||||||
|
@tagged('post_install', '-at_install')
|
||||||
|
class Ap19Test(TransactionCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super().setUp()
|
||||||
|
self.company = self.env.company
|
||||||
|
self.company.write({
|
||||||
|
'l10n_at_beitragskontonummer': '1412345678',
|
||||||
|
'l10n_at_lohnsteuernummer': '123456789',
|
||||||
|
})
|
||||||
|
self.category = self.env['hr.salary.rule.category'].create({
|
||||||
|
'name': 'Test AT AP19', 'code': 'TEST_AT_AP19'})
|
||||||
|
self.structure_type = self.env[
|
||||||
|
'hr.payroll.structure.type'].create({
|
||||||
|
'name': 'Test AT AP19 Type'})
|
||||||
|
self.struktur = self.env['hr.payroll.structure'].create({
|
||||||
|
'name': 'Test AT AP19', 'code': 'TEST_AT_AP19',
|
||||||
|
'type_id': self.structure_type.id})
|
||||||
|
self.env['hr.salary.rule'].create({
|
||||||
|
'name': 'LFD', 'code': 'LFD',
|
||||||
|
'struct_id': self.struktur.id,
|
||||||
|
'category_id': self.category.id,
|
||||||
|
'condition_select': 'none',
|
||||||
|
'l10n_at_bemessung': 'laufend',
|
||||||
|
'amount_select': 'fix',
|
||||||
|
'amount_fix': 3000.0})
|
||||||
|
employee = self.env['hr.employee'].create({
|
||||||
|
'name': 'AP19 Test', 'birthday': '1980-01-01'})
|
||||||
|
version = self.env['hr.version'].search(
|
||||||
|
[('employee_id', '=', employee.id)], limit=1)
|
||||||
|
version.write({
|
||||||
|
'contract_date_start': datetime.date(2020, 1, 1),
|
||||||
|
'ssnid': '1234100180',
|
||||||
|
'sex': 'male'})
|
||||||
|
self.employee = employee
|
||||||
|
self.version = version
|
||||||
|
|
||||||
|
def _slip(self, monat, jahr=2026):
|
||||||
|
erster = datetime.date(jahr, monat, 1)
|
||||||
|
letzter = (erster.replace(day=28) + datetime.timedelta(days=4)) \
|
||||||
|
.replace(day=1) - datetime.timedelta(days=1)
|
||||||
|
slip = self.env['hr.payslip'].create({
|
||||||
|
'name': 'AP19 %d/%d' % (monat, jahr),
|
||||||
|
'employee_id': self.employee.id,
|
||||||
|
'struct_id': self.struktur.id,
|
||||||
|
'date_from': erster,
|
||||||
|
'date_to': letzter,
|
||||||
|
})
|
||||||
|
slip.compute_sheet()
|
||||||
|
return slip
|
||||||
|
|
||||||
|
def test_04_organ_mehr_sv_frei(self):
|
||||||
|
"""Organ ≥ 25 %: alle SVDN/SVDG 0 (lb-vst-03); mBGM-Lauf weist
|
||||||
|
den Mitarbeiter mit Protokoll-Hinweis aus."""
|
||||||
|
self.version.l10n_at_status = 'organ_mehr'
|
||||||
|
slip = self._slip(1)
|
||||||
|
slip.action_payslip_done()
|
||||||
|
self.assertAlmostEqual(slip._l10n_at_svdn_gesamt(), 0.0, places=2)
|
||||||
|
self.assertAlmostEqual(slip._l10n_at_svdg_gesamt(), 0.0, places=2)
|
||||||
|
deklaration = self.env['l10n.at.payroll.mbgm'].create({
|
||||||
|
'company_id': self.company.id,
|
||||||
|
'beitragsmonat': datetime.date(2026, 1, 1),
|
||||||
|
'art': 'MB'})
|
||||||
|
deklaration.action_erzeugen()
|
||||||
|
self.assertFalse(deklaration.line_ids)
|
||||||
|
self.assertIn('kein SV', deklaration.protokoll)
|
||||||
|
# LSt läuft weiter (Bemessungsgrundlage ohne SV-Sperre).
|
||||||
|
self.assertGreater(
|
||||||
|
slip._l10n_at_bemessung_laufen(), 0.0)
|
||||||
|
|
||||||
|
def test_04b_prasenzenzdienst_fiktion(self):
|
||||||
|
"""§ 7 Abs 1/2 BMSVG: fiktive BV-BG = 435,90 € × Tage/Monatstage
|
||||||
|
(ATP_PRD_TAGE) zusätzlich zur Basis."""
|
||||||
|
slip = self._slip(3)
|
||||||
|
slip.input_line_ids = [(0, 0, {
|
||||||
|
'input_type_id': self.env.ref(
|
||||||
|
'l10n_at_hr_payroll_private.input_type_atp_prd_tage').id,
|
||||||
|
'amount': 20.0})]
|
||||||
|
basis = slip._at_bv_basis_laufen()
|
||||||
|
fiktiv = slip._at_bv_fiktiv_prasenzenzdienst()
|
||||||
|
self.assertAlmostEqual(fiktiv, 435.90 * 20 / 31.0, places=2)
|
||||||
|
self.assertAlmostEqual(basis, 3000.0 + fiktiv, places=2)
|
||||||
@@ -51,6 +51,7 @@
|
|||||||
<field name="employee_name"/>
|
<field name="employee_name"/>
|
||||||
<field name="versicherungsnummer"/>
|
<field name="versicherungsnummer"/>
|
||||||
<field name="bsgr"/>
|
<field name="bsgr"/>
|
||||||
|
<field name="jagb" optional="show"/>
|
||||||
<field name="refw"/>
|
<field name="refw"/>
|
||||||
<field name="vsum"/>
|
<field name="vsum"/>
|
||||||
</list>
|
</list>
|
||||||
|
|||||||
@@ -34,6 +34,7 @@
|
|||||||
<field name="l10n_at_dienstgebergruppe"
|
<field name="l10n_at_dienstgebergruppe"
|
||||||
placeholder="z. B. NBALG"/>
|
placeholder="z. B. NBALG"/>
|
||||||
<field name="l10n_at_nschab"/>
|
<field name="l10n_at_nschab"/>
|
||||||
|
<field name="l10n_at_gfb_jaehrliche_abrechnung"/>
|
||||||
</group>
|
</group>
|
||||||
</field>
|
</field>
|
||||||
</record>
|
</record>
|
||||||
|
|||||||
Reference in New Issue
Block a user