[IMP] l10n_at_hr_payroll_private: GP7 mBGM/JASV + Versichertenmeldung + fallweise Anmeldung auf dem ELDA-Framework
mBGM/JASV (models/mbgm.py — DM-ORG 42.7.0 E.32, BEST MB, Version 02): Paket PS (BKNR/DGNA/BZRM/GSUM/ANZM) → mBGM G1 (REFW/VSNR/FANA/VONA/VSUM/VERG=1) → Tarifblock T1 (BSGR/VVON) → Verrechnungsbasis BS (VBTY/VBBT) → Verrechnungsposition V1 (VPTY/VPVZ/VPTA/RSVZ/RSUM). KEIN DN/DG-Zweig-Split: T01 = GESAMT-Prozentsatz des Tarifsystems je Beschäftigtengruppe (Parameter at_mbgm_tarif, normativ: laufend 39,05 % = KV 7,65 + PV 22,80 + AV 5,90 + UV 1,10 + WF 1,00 + IESG 0,10; SZ 37,55 % = DN KV 3,87 + PV 10,25 + AV 2,95 + DG 20,48). Abschläge als EIGENE Verrechnungspositionen je Basis: A03/A02/A01 = Minderung AV auf 0/1/2 % (§ 2a AMPFG, nur DN-AV, je Verrechnungsbasis AB UND SZ), A09 = UV-Entfall 60, A10 = AV+IE-Entfall Pensionsanspruch (Konkurrenz a4: sperrt Staffelabschlag), A15 = PV-Halbierung Bonusphase — konsistent mit der GP4-§-2a-DN-seitig-Konvention. BV (BMSVG): eigene Verrechnungsbasis BV (ungedeckelt, GP5) + Position V01 1,53 %. RSUM = Basis x Satz (mBGM-Tarifierung), Konsistenz-Check gegen GP4-Payslip-Beitragssummen ±0,10 € als Prüfwarnung. Storno: R1-Sätze mit REFU/VSUM wie Original. Fehlanzeige: Paket ohne mBGM-Sätze (ANZM 0). Geringfügige/fallweise ohne Parametrisierung: listenweiser Abbruch (keine Tarif-Erfindung; BSGR B044/B045/B010/B030 aus dem SV-Tarifsystem pflegen). Versichertenmeldung (models/versichertenmeldung.py — DM-ORG E.29, BEST VR, Version 03, Satzlänge 772): M3 vor Arbeitsantritt / M4 (7 Kalendertage ab PV-Ende) mit deklarativer Event-Erkennung über die Meldungs-Registry (l10n.at.payroll.vr.event) — idempotente Läufe, Dimona-auto-Idee ohne write-diff-Hacks. Felder: BKNR/VSNR/GEBD/FANA/VONA (Namenssplit am letzten Leerzeichen), ADAT/EBSV, BBER 01-04 aus l10n_at_personalgruppe/lehrling, GERF (Kataloggehalt vs. GFG), FRDV, BVAB/BVJN aus dem GP5-BMSVG-Flag, VWAZ (Stunden x100, neu ab 01.01.2026), AGRD aus dem hr.departure.reason-Mapping (Standard 00). Fallweise Anmeldung (BEST MA, SART 32, Version 07, Länge 450): MVP je Anmeldetag am Vertragsbeginn; tagesgenaue Einsatztage via Work-Entry-Auswertung = dokumentierter Rest. NSchAB-Branche als Company-Flag (l10n_at_nschab): B001-T01 + 3,80 % (Schlechtwetterentschädigung, ÖGK-Tarifsystem Normalbetriebe mit Schlechtwetterentschädigung). Tests: test_meldewesen_private (2 — mBGM-Satzhierarchie mit A03 je Basis aus November-Fixture + GSUM-Konsistenz; VR M3/M4 mit Registry-Idempotenz und AGRD). Rechenfälle docs/rechenfaelle/meldewesen.md. 19.0.7.0.0.
This commit is contained in:
@@ -117,19 +117,37 @@ Auszahlung über `hr_payroll_account_iso20022`; kein VRV/MVAG
|
|||||||
|
|
||||||
Rechenfälle `docs/rechenfaelle/buchhaltung.md`.
|
Rechenfälle `docs/rechenfaelle/buchhaltung.md`.
|
||||||
|
|
||||||
**Offen (GP7+):** Meldewesen/Belege (GP7), Jahreswechsel (GP8);
|
**GP7 Meldewesen/Belege (umgesetzt 2026-09-12):** auf dem Kern-
|
||||||
All-in-UI-Bericht (GP8); B14 längster-Urlaubs-Teil-Verfeinerung;
|
Framework (ELDA-Fixlängen-Dateien, ISO-8859-15, CRLF-Annahme;
|
||||||
DZ-WKO-Verifikation (8.11) vor Wien-Mandanten; §-7-BMSVG-Fiktivfälle
|
L16 + Arbeitsstättenmeldung + Belege Lohnzettel AT/Monatsdienstliste/
|
||||||
(Präsenzdienst/Kranken-/Wochengeld) als dokumentierte Option
|
Monatsjournal im Kern): **mBGM/JASV** (Paket/mBGM/Tarifblock/Verrech-
|
||||||
(Rechenfälle-Datei); Arbeiter-Sub-Konten (Option A-Rest) mit der
|
nungsbasis/-position, T01-Gesamtsatz aus ``at_mbgm_tarif`` — normativ
|
||||||
Referenzbuchhaltung fixieren (GP0-Rest).
|
39,05 % laufend / 37,55 % SZ; Abschläge A01/A02/A03 § 2a AMPFG **je
|
||||||
|
Basis AB und SZ**, A09/A10/A15 aus GP4-Stichtagen; BV-Basis + V01 aus
|
||||||
|
GP5; Storno-R-Sätze mit REFU; Fehlanzeige als Null-Paket; Gering-
|
||||||
|
fährige/fallweise ohne Tarifparameter = listenweiser Laufabbruch);
|
||||||
|
**Versichertenmeldung** M3/M4 (deklarative Event-Erkennung über die
|
||||||
|
Meldungs-Registry — idempotent, kein write-diff; BBER 01–04, GERF,
|
||||||
|
FRDV, BVAB/BVJN aus GP5, VWAZ; AGRD-Mapping am Abgangsgrund) und
|
||||||
|
**fallweise Anmeldung** (BEST MA, SART 32; MVP je Anmeldetag);
|
||||||
|
NSchAB-Branche als Company-Flag (B001-T01 + 3,80 %).
|
||||||
|
|
||||||
|
Rechenfälle `docs/rechenfaelle/meldewesen.md`.
|
||||||
|
|
||||||
|
**Offen (GP8+):** Jahreswechsel (GP8); All-in-UI-Bericht (GP8);
|
||||||
|
B14 längster-Urlaubs-Teil-Verfeinerung; DZ-WKO-Verifikation (8.11)
|
||||||
|
vor Wien-Mandanten; §-7-BMSVG-Fiktivfälle (Option); Arbeiter-Sub-
|
||||||
|
Konten-Endfixierung (Referenzbuchhaltung); Schwerarbeitsmeldung
|
||||||
|
(nach ASchV-Datenmodell-Bau, DM-ORG E.22-Struktur liegt bei);
|
||||||
|
§ 109a (separater Baustein); Mitteilungsfile-Import (GP8);
|
||||||
|
geringfügig/fallweise-Tarifparameter pflegen (at_mbgm_tarif).
|
||||||
|
|
||||||
Tests: `test_kv_katalog` + `test_entgelt_private` (GP2) +
|
Tests: `test_kv_katalog` + `test_entgelt_private` (GP2) +
|
||||||
`test_lst_private` (GP3) + `test_sv_private` (GP4) +
|
`test_lst_private` (GP3) + `test_sv_private` (GP4) +
|
||||||
`test_bv_private` (GP5) + `test_account_private` (GP6) —
|
`test_bv_private` (GP5) + `test_account_private` (GP6) +
|
||||||
|
`test_elda_l16` (GP7 Kern) + `test_meldewesen_private` (GP7) —
|
||||||
`.venv/bin/python -m odoo -d <testdb> -i l10n_at_hr_payroll,l10n_at_hr_payroll_private
|
`.venv/bin/python -m odoo -d <testdb> -i l10n_at_hr_payroll,l10n_at_hr_payroll_private
|
||||||
--test-tags /l10n_at_hr_payroll,/l10n_at_hr_payroll_private --test-enable --stop-after-init
|
--test-tags /l10n_at_hr_payroll,/l10n_at_hr_payroll_private --test-enable --stop-after-init
|
||||||
--addons-path=odoo_19.0+e.20260910/odoo-19.0+e.20260910/odoo/addons,addons`
|
--addons-path=odoo_19.0+e.20260910/odoo-19.0+e.20260910/odoo/addons,addons`
|
||||||
(Lokal grün am 2026-09-12 auf Wegwerf-DB, frischer `-i`-Lauf:
|
(Lokal grün am 2026-09-12 auf Wegwerf-DB, frischer `-i`-Lauf:
|
||||||
**88/88 Tests** — NET-Rundungscents bucht Odoo als „Adjustment Entry"
|
**94/94 Tests**.)
|
||||||
auf das Journal-Default.)
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
# Part of the odoo-at-payroll project. License: LGPL-3.
|
# Part of the odoo-at-payroll project. License: LGPL-3.
|
||||||
{
|
{
|
||||||
'name': 'Österreich - Privatwirtschaft Personalverrechnung (General-AT)',
|
'name': 'Österreich - Privatwirtschaft Personalverrechnung (General-AT)',
|
||||||
'version': '19.0.6.0.0',
|
'version': '19.0.7.0.0',
|
||||||
'author': 'ixsol / gem360',
|
'author': 'ixsol / gem360',
|
||||||
'website': 'https://gem360.at',
|
'website': 'https://gem360.at',
|
||||||
'category': 'Human Resources/Payroll',
|
'category': 'Human Resources/Payroll',
|
||||||
@@ -65,6 +65,18 @@ Grundlagen: ``personalverrechnung/IMPLEMENTIERUNGSPLAN-Privat.md`` und
|
|||||||
(Zahlungsregistrierung/SEPA via ``hr_payroll_account_iso20022``).
|
(Zahlungsregistrierung/SEPA via ``hr_payroll_account_iso20022``).
|
||||||
Auszahlung über ``hr_payroll_account_iso20022``; kein VRV/MVAG
|
Auszahlung über ``hr_payroll_account_iso20022``; kein VRV/MVAG
|
||||||
(gem360-Brücke).
|
(gem360-Brücke).
|
||||||
|
- **GP7 Meldewesen (dieser Stand):** mBGM/JASV (BEST MB, DM-ORG
|
||||||
|
42.7.0: Paket/mBGM/Tarifblock/Verrechnungsbasis/-position mit
|
||||||
|
GESAMT-Prozentsatz je BSGR aus ``at_mbgm_tarif``, Abschläge
|
||||||
|
A01/A02/A03 §-2a-AMPFG, A09 UV-60, A10 AV+IE-Entfall, A15 PV-
|
||||||
|
Halbierung; BV-Basis + V01 aus GP5; Storno-R-Sätze; Fehlanzeige)
|
||||||
|
+ Versichertenmeldung (BEST VR: M3/M4 mit deklarativer Event-
|
||||||
|
Erkennung über die Meldungs-Registry, AGRD-Mapping am
|
||||||
|
Abgangsgrund, VWAZ, BBER, GERF; fallweise Anmeldung BEST MA
|
||||||
|
SART 32) — auf dem GP7-Kern-Framework
|
||||||
|
(l10n_at_hr_payroll/models/elda.py; L16 + Arbeitsstättenmeldung
|
||||||
|
+ Belege siehe Kern-Modul). NSchAB-Branche als Company-Flag
|
||||||
|
(B001-T01 + 3,80 %).
|
||||||
- **Seed D2 (Referenz-KVs R1):** SI-2203 „Handwerk und Gewerbe" +
|
- **Seed D2 (Referenz-KVs R1):** SI-2203 „Handwerk und Gewerbe" +
|
||||||
SI-2748 „Metallgewerbe" (Gehaltstabellen ab 1. 1. 2026, kuratiert aus
|
SI-2748 „Metallgewerbe" (Gehaltstabellen ab 1. 1. 2026, kuratiert aus
|
||||||
den KV-Library-Volltexten).
|
den KV-Library-Volltexten).
|
||||||
@@ -94,9 +106,11 @@ gem360-Brücke).
|
|||||||
'data/salary_rules_sv_private.xml',
|
'data/salary_rules_sv_private.xml',
|
||||||
'data/salary_rules_bvg_private.xml',
|
'data/salary_rules_bvg_private.xml',
|
||||||
'data/hr_payroll_account_private.xml',
|
'data/hr_payroll_account_private.xml',
|
||||||
|
'data/rule_parameters_mbgm_private.xml',
|
||||||
'data/bv_kasse_seed.xml',
|
'data/bv_kasse_seed.xml',
|
||||||
'views/kv_views.xml',
|
'views/kv_views.xml',
|
||||||
'views/bv_views.xml',
|
'views/bv_views.xml',
|
||||||
|
'views/meldewesen_views.xml',
|
||||||
'report/bv_report.xml',
|
'report/bv_report.xml',
|
||||||
],
|
],
|
||||||
'countries': ['at'],
|
'countries': ['at'],
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<!-- GP7: mBGM-Tarifsätze (Standardposition T01/T02 = GESAMT- -->
|
||||||
|
<!-- Prozentsatz je Beschäftigtengruppe aus dem ÖGK- -->
|
||||||
|
<!-- Beitragstarifsystem „Normalbetriebe Allgemeiner Teil", -->
|
||||||
|
<!-- gültig ab 01.07.2026 (.additional_docs/oegk/, Primär- -->
|
||||||
|
<!-- quelle GP4). NORMATIVE Sätze (Abschläge wie A03 laufen als -->
|
||||||
|
<!-- eigene Verrechnungspositionen): laufend = DN 18,07 + DG -->
|
||||||
|
<!-- 20,98 = 39,30 − AK/… exakt: KV 7,65 + PV 22,80 + AV 5,90 + -->
|
||||||
|
<!-- UV 1,10 + WF 1,00 + IESG 0,10 = 39,05 %; SZ = KV 3,87 + -->
|
||||||
|
<!-- PV 10,25 + AV 2,95 (DN; § 67 Abs 12) + DG 20,48 (KV 3,78 + -->
|
||||||
|
<!-- PV 12,55 + AV 2,95 + UV 1,10 + IESG 0,10) = 37,55 %. -->
|
||||||
|
<!-- NSchAB-Betriebe: + 3,80 % auf B001 laufend über das Company- -->
|
||||||
|
<!-- Flag l10n_at_nschab. Lehrlinge (B044/B045) und Geringfügige -->
|
||||||
|
<!-- (B010/B030) bewusst NICHT geseedet — Werte aus dem SV- -->
|
||||||
|
<!-- Tarifsystem pflegen, sobai solche Beschäftigten vorkommen. -->
|
||||||
|
<record id="hr_rule_parameter_at_mbgm_tarif" model="hr.rule.parameter">
|
||||||
|
<field name="name">mBGM-Tarifsätze je Beschäftigtengruppe (SV-Tarifsystem)</field>
|
||||||
|
<field name="code">at_mbgm_tarif</field>
|
||||||
|
<field name="country_id" ref="base.at"/>
|
||||||
|
</record>
|
||||||
|
<record id="hr_rule_parameter_value_at_mbgm_tarif_2026" model="hr.rule.parameter.value">
|
||||||
|
<field name="rule_parameter_id" ref="hr_rule_parameter_at_mbgm_tarif"/>
|
||||||
|
<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>
|
||||||
|
</record>
|
||||||
|
</odoo>
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
# Rechenfälle Meldewesen/Belege (GP7) — ELDA/eSV, L16, Belege
|
||||||
|
|
||||||
|
**Quellen:** DM-ORG 42.7.0 „Datenaustausch mit Dienstgebern", 42.
|
||||||
|
Ergänzung 07/2026 (`.firecrawl/ris/elda/` — Volltext-PDF + pdftotext;
|
||||||
|
Satzstrukturen PDF-verifiziert extrahiert 2026-09-12, Positions-
|
||||||
|
Ketten lückenlos); Pruefkatalog L16 ab 2026 (Stand 12/2025, xlsx);
|
||||||
|
KB lb-lvr-04/05 (L16), lb-mel-08 (Fristen), lb-bes-02/03 (fallweise),
|
||||||
|
lb-sva-14 (SZ-Sätze), lb-swa-05 (Schwerarbeit, out); Nutzer-
|
||||||
|
Entscheidungen 5.1–5.4 (Arbeitsstättenmeldung in GP7; Mitteilungsfile-
|
||||||
|
Import Rest; **FB+ = keine Personalverrechnung** — bei Steuer < 0
|
||||||
|
Arbeitnehmerveranlagung, **Kind-Modell für die FABO+-Wartung**; alles
|
||||||
|
in einem GP7). Umsetzung: Kern `l10n_at_hr_payroll` 19.0.4.0.0
|
||||||
|
(`models/elda.py`, `lohnzettel_l16.py`, `kind.py`, `meldewesen.py`,
|
||||||
|
`monatsreports.py`, `report/belege_reports.xml`), privat
|
||||||
|
19.0.7.0.0 (`models/mbgm.py`, `versichertenmeldung.py`,
|
||||||
|
`data/rule_parameters_mbgm_private.xml`). Tests: `test_elda_l16`,
|
||||||
|
`test_meldewesen_private`.
|
||||||
|
|
||||||
|
## Systematik (GP7, verifiziert)
|
||||||
|
|
||||||
|
- **ELDA-Fixlängen-Dateien** (C.1/C.2, E.1–E.3): Datei = optionaler
|
||||||
|
Dateiheader (DHKZ 2×blank, DHVN 01, ENCD **ISO-8859-15**) + Daten-
|
||||||
|
bestand (Vorlaufsatz SART 00 mit BEST/VSTR/VERS → Datensätze →
|
||||||
|
Schlusssatz SART 99 mit SANZ inkl. Vorlauf/Schluss); a/n linksbündig
|
||||||
|
blank-gefüllt, n rechtsbündig null-gefüllt, **Cent ohne
|
||||||
|
Interpunktion**, Prozentsätze ×1000 (3 Nachkommastellen), SANR
|
||||||
|
lückenlos ab 1. **CRLF als Satztrenner = Praxis-Annahme** (die
|
||||||
|
DM-ORG normiert kein Trennzeichen — Parallellauf verifizieren).
|
||||||
|
- **mBGM/JASV** (BEST `MB`, Version 02; E.32): Hierarchie Paket PS
|
||||||
|
(BKNR/DGNA/BZRM/GSUM/ANZM) → mBGM G1 (REFW/VSNR/FANA/VONA/VSUM/
|
||||||
|
VERG=1) → Tarifblock T1 (BSGR/VVON) → Verrechnungsbasis BS (VBTY/
|
||||||
|
VBBT) → Verrechnungsposition V1 (VPTY/VPVZ/VPTA/RSVZ/RSUM).
|
||||||
|
**KEIN DN/DG-Zweig-Split**: T01 = GESAMT-Prozentsatz des Tarifsystems
|
||||||
|
(Parameter `at_mbgm_tarif`, normativ: laufend **39,05 %** =
|
||||||
|
KV 7,65 + PV 22,80 + AV 5,90 + UV 1,10 + WF 1,00 + IESG 0,10; SZ
|
||||||
|
**37,55 %** = DN KV 3,87 + PV 10,25 + AV 2,95 + DG 20,48). Abschläge
|
||||||
|
als EIGENE Positionen je Basis: **A01/A02/A03** = Minderung AV auf
|
||||||
|
2/1/0 % (§ 2a AMPFG — nur DN-AV, **je Verrechnungsbasis AB UND SZ**;
|
||||||
|
A03 = −2,95 % der Basis, RSVZ −), A09 = UV-Entfall 60 (−1,10 %), A10
|
||||||
|
= AV+IE-Entfall (−6,00 %), A15 = PV-Halbierung (−11,40 %);
|
||||||
|
Konkurrenz a4: Entfall vor Minderung (A10 sperrt A0x). **BV**:
|
||||||
|
Basis `BV` (GP5, ungedeckelt) + `V01` 1,53 %. RSUM = Basis × Satz
|
||||||
|
(mBGM-Tarifierung); Konsistenz gegen GP4-Payslip-Summen als
|
||||||
|
Prüfwarnung (Toleranz 0,10 €). Storno: R-Satz je stornierter mBGM
|
||||||
|
(REFU, VSUM wie Original, ohne T/BS/V). Fehlanzeige: Paket ohne
|
||||||
|
mBGM-Sätze (ANZM 0). Fallweise/Geringfügige ohne Parametrisierung:
|
||||||
|
Lauf bricht listenweise ab (BSGR B044/B045/B010/B030 —
|
||||||
|
Tarifsystem-Werte pflegen, keine Erfindung).
|
||||||
|
- **L16 Lohnzettel Finanz** (BEST `LF`, VSTR `94` BRZ; E.13/E.14):
|
||||||
|
I1 (Länge 1100, Strukturversion 03) + L1 (Länge **3500**,
|
||||||
|
Lohnzettelversion 28, zwingend ab 01.03.2026). **KZ 210 =
|
||||||
|
Bruttobezüge gem. § 25 GESAMT** (laufende + sonstige §-67-Bezüge,
|
||||||
|
DM-ORG-Feldbeschreibung!) — deshalb ist **KZ 245 = 210 − 215 − 220
|
||||||
|
− 230 − 243** die laufende Steuerbemessung und per Konstruktion mit
|
||||||
|
der GP3-§-66-Kette konsistent. KZ-Ableitung aus den Kern-Flags
|
||||||
|
(kein paralleles Klassifikationsfeld): 210 = laufend+sonstig § 67
|
||||||
|
(ohne Abs-3/6), 215 = `_l10n_at_freibetraege_68` (§-68-Caps je LZP),
|
||||||
|
220 = sonstig ohne Abs-3/6 (inkl. 620-€-Freibetrag vor SV), 225 =
|
||||||
|
SVDN_SZ, 230 = laufende SVDN+AK+WF, 243 = 0 (nicht im Inventar),
|
||||||
|
260/BIEL = LST_LFD + LST_SZ. FB+-Felder (BFABO/KFABO/VFABO/FABO +
|
||||||
|
Kinderblock 15×123 ab Pos. 1501) aus der **Kind-Stammdaten-Wartung**
|
||||||
|
(Nutzer-Entscheidung: FB+ rechnet nicht im Lohnsteuerkern — ANV).
|
||||||
|
REFN je AG+Jahr+DV eindeutig; berichtigter Lohnzettel: ARTL 18 +
|
||||||
|
KORR K + FIND J.
|
||||||
|
- **Arbeitsstättenmeldung** § 34 Abs 6 ASVG (BEST `AD`, VSTR `ST`
|
||||||
|
Statistik Austria; E.20, SART 45, Länge 300): je AN Adresse am
|
||||||
|
31. 12. aus dem Dienstort-Partner; **GKZ** (Gemeindekennziffer) als
|
||||||
|
manuelles Partner-Feld — Warnung, wenn fehlt.
|
||||||
|
- **Versichertenmeldung** (BEST `VR`, Version 03, Länge 772; E.29):
|
||||||
|
M3 vor Arbeitsantritt (BKNR/VSNR/GEBD/FANA/VONA/ADAT/BBER 01–04/
|
||||||
|
GERF/FRDV/BVAB/BVJN aus GP5/VWAZ Stunden×100), M4 7 Tage (ADAT +
|
||||||
|
EBSV, AGRD aus dem hr.departure.reason-Mapping, Standard 00),
|
||||||
|
M6 nur Ausnahmefälle; **deklarative Event-Erkennung** über die
|
||||||
|
Meldungs-Registry (keine write-diff-Hacks; idempotente Läufe).
|
||||||
|
**Fallweise** (BEST `MA`, SART 32, Länge 450, Version 07): je
|
||||||
|
Beschäftigungstag vor Arbeitsantritt; MVP: je AN/Vertragsbeginn
|
||||||
|
ein Anmeldetag (tagesgenaue Einsatztage via Work-Entries = Rest).
|
||||||
|
- **Belege (AP6)**: Lohnzettel AT (ir.actions.report auf hr.payslip,
|
||||||
|
AT-Layout mit Bezügen/Abzügen/**DG-Anteilen**/Netto; per Structure
|
||||||
|
über `report_id`), Monatsdienstliste (mBGM-Kontrollschicht je AN:
|
||||||
|
BG laufend/SZ, SV-DN/SZ, SV-DG, LSt, Netto) + Monatsjournal (je
|
||||||
|
Lohnart Summen je Lauf) auf `hr.payslip.run` (QWeb, AbstractModel).
|
||||||
|
Kein HR-Modul nutzt `account.report` — QWeb ist der framework-
|
||||||
|
gerechte Weg (Contribution-Register-Muster).
|
||||||
|
|
||||||
|
## Rechenfälle (Fixtures)
|
||||||
|
|
||||||
|
| RF | Konstellation | Erwartung |
|
||||||
|
|---|---|---|
|
||||||
|
| RF-ELDA-1 | Feld-Formatter | a/n linksbündig blank; n rechtsbündig 0-gefüllt; Cent kaufmännisch (0,005 → 1); Prozentsatz skala 3 (39,30 → 039300) |
|
||||||
|
| RF-ELDA-2 | Bestand/Datei | Vorlauf SART 00 (VSTR/BEST/EDAT TTMMJJJJ/VERS), SANR lückenlos, SANZ inkl. Rahmen, CRLF, Dateiheader ` 01ISO-8859-15…`, Umlaute ISO-8859-15-codiert |
|
||||||
|
| RF-L16-1 | November mit B13 (Jahr 2026) | KZ 210 = 4.118,82 (§ 25 gesamt!), KZ 220 = 2.059,41, KZ 245 = laufende Bemessung > 0; I1 (1.100) mit STNRA/JAHR/GESA; L1 (3.500) exakte Positionen: REFN/ARTL 01/BELZ 0111/SOZS 3/AVLN 1234/AGBD 150590/B210 0000411882; Vorlauf VSTR 94, BEST LF, VERS 28; SANZ 4 |
|
||||||
|
| RF-L16-2 | Arbeitsstätte | Satz 45 (300): BKNR/VSNR/STNR; VSTR ST, BEST AD; Warnung GKZ fehlt |
|
||||||
|
| RF-MBGM-1 | November mit B13, Angestellte (B002) | PS (BKNR/BZRM 112026/ANZM 1/GSUM = Σ VSUM), G1 (VSNR), T1 BSGR B002, Positionen T01 039050, **A03 je Basis** (AB + SZ, RSVZ −), T02 037550, V01; GSUM ≈ GP4-Beitragssummen ± 0,10 € |
|
||||||
|
| RF-VR-1 | Vertrag ab 2020, Ende 15.02.2026 | M3 (ADAT 01012020, BBER 02, GERF N, FRDV N, VWAZ 4000, BVJN J); Registry verhindert Doppelanmeldung; M4 (ADAT 15022026, EBSV, AGRD 00) |
|
||||||
|
|
||||||
|
## Offene Punkte / bewusst nicht in GP7
|
||||||
|
|
||||||
|
- **Schwerarbeitsmeldung** (DM-ORG E.22 Satzstruktur existiert!):
|
||||||
|
blockiert — keine ASchV-Work-Entry-Typen/Zähler im Modul, ASchV in
|
||||||
|
RQ-Privat ❓ nicht erschlossen. Erst nach Datenmodell-Bau.
|
||||||
|
- **§ 109a** (W1-Satz): separater Baustein (Honorare außerhalb der
|
||||||
|
Lohnverrechnung); Mitteilungs-XSDs/Prüfkatalog beschafft.
|
||||||
|
- **L17** (A1-Satz): keine AG-Pflicht. **Mitteilungsfile-Import**
|
||||||
|
(Rückantwort-Fehler ins Deklarationsmodell): GP8-Rest (5.2).
|
||||||
|
- Geringfügigen-mBGM (B010/B030-Sätze, Z04-Jahreszahlung + 2,5 %):
|
||||||
|
parametrisieren, sobai vorkommend; mBGM je Lauf bei Mehrfachläufen
|
||||||
|
im Monat; M6/Änderung; FB+-Lohnsteuermechanik (ANV-Sache);
|
||||||
|
VSNR-Prüfziffer-Algorithmus (SVS-Quelle beschaffen);
|
||||||
|
tagesgenaue fallweise-Einsatztage (Work-Entry-Auswertung).
|
||||||
|
- Referenzbuchhaltung/Parallellauf: CRLF-Satztrenner, ELDA-Kanäle
|
||||||
|
(Sendemodul https/ftps 40/220 MB), Fristenkalender (GP8).
|
||||||
@@ -6,4 +6,6 @@ from . import bv_kasse
|
|||||||
from . import abfertigung_private
|
from . import abfertigung_private
|
||||||
from . import bv_report
|
from . import bv_report
|
||||||
from . import account_chart_private
|
from . import account_chart_private
|
||||||
from . import account_posting_private
|
from . import account_posting_private
|
||||||
|
from . import mbgm
|
||||||
|
from . import versichertenmeldung
|
||||||
@@ -0,0 +1,506 @@
|
|||||||
|
# Part of the odoo-at-payroll project. License: LGPL-3.
|
||||||
|
"""Monatliche Beitragsgrundlagenmeldung mBGM/JASV (GP7/AP5) — DM-ORG
|
||||||
|
42.7.0, Kapitel E.32 (BEST ``MB``, mBGM-Version 02; PDF-verifizierte
|
||||||
|
Extraktion 2026-09-12).
|
||||||
|
|
||||||
|
Hierarchie: Paket (SART ``PS`` Selbstabrechner, Länge 305) → mBGM
|
||||||
|
(SART ``G1``, 326) → Tarifblock (SART ``T1``, 42) → Verrechnungsbasis
|
||||||
|
(``BS``, 33) → Verrechnungsposition (``V1``, 42).
|
||||||
|
|
||||||
|
Fachliche Grundlagen:
|
||||||
|
- **KEIN DN/DG- oder Zweig-Split**: die Standardposition ``T01``
|
||||||
|
trägt den GESAMT-Prozentsatz des Tarifsystems (Parameter
|
||||||
|
``at_mbgm_tarif`` — BSGR-spezifisch; Werte aus dem ÖGK-
|
||||||
|
Beitragstarifsystem, `.additional_docs/oegk/`, jeweils
|
||||||
|
parametrisiert zu pflegen). Fehlende BSGR-Sätze brechen den Lauf
|
||||||
|
listenweise ab (keine Erfindung von Tarifen).
|
||||||
|
- **Abschläge als eigene Verrechnungspositionen** (D.60, verifiziert):
|
||||||
|
A01/A02/A03 = Minderung AV auf 2/1/0 % (§ 2a AMPFG, DN-seitig —
|
||||||
|
Abschlag = AB × (2,95 − Zielsatz) %), A09 = UV-Entfall ab 60
|
||||||
|
(1,10 %), A10 = AV+IE-Entfall Pensionsanspruch (6,00 %), A15 =
|
||||||
|
PV-Halbierung Bonusphase (11,40 %). A02/A10 schließen einander aus
|
||||||
|
(Konkurrenz a4: Entfall vor Minderung — wie der GP4-Kern rechnet).
|
||||||
|
- **BV (BMSVG)**: eigene Verrechnungsbasis ``BV`` (ungedeckelt, GP5)
|
||||||
|
+ Position ``V01`` (1,53 %); SZ-Basis ``SZ`` (§-54-gedeckelt) +
|
||||||
|
Position ``T02`` mit dem SZ-Gesamtsatz.
|
||||||
|
- **RSUM aus Basis × Satz** (mBGM-eigene Tarifierung); Konsistenz
|
||||||
|
gegen die GP4-Payslip-Summen als Prüfwarnung (Toleranz 0,10 €).
|
||||||
|
- **Fehlanzeige**: Paket ohne mBGM-Sätze (ANZM 0) für Beitragsmonate
|
||||||
|
ohne meldbare Daten (GP5-Deferral; DM-ORG-Form ⚠ Parallellauf
|
||||||
|
verifizieren). **Storno**: R1-Sätze mit REFU + identischem BZRM
|
||||||
|
(VSUM = gestornierte VSUM) für den Nachtragslauf; Nachreichung als
|
||||||
|
neue Deklaration (GP4-Deferral Aufrollung).
|
||||||
|
- Fallweise Beschäftigte (G3/G4 mit tagesgenauen Tarifblöcken) und
|
||||||
|
Geringfügige ohne Tarifparameter: dokumentierter Rest — der Lauf
|
||||||
|
bricht listenweise ab, bis `at_mbgm_tarif` gepflegt ist.
|
||||||
|
|
||||||
|
Dateibasiert (Prämisse P7): Generierung + Prüfprotokoll; Übertragung
|
||||||
|
beim Mandanten. Fristen: Selbstabrechner 15., Vorschreibebetriebe 7.
|
||||||
|
des Folgemonats (lb-mel-08) — Fristenkalender GP8."""
|
||||||
|
from datetime import date, timedelta
|
||||||
|
import base64
|
||||||
|
|
||||||
|
from odoo import api, fields, models
|
||||||
|
from odoo.exceptions import UserError
|
||||||
|
|
||||||
|
from odoo.addons.l10n_at_hr_payroll.models.elda import (
|
||||||
|
L10N_AT_ELDA_IDTEIL,
|
||||||
|
L10nAtEldaBestand,
|
||||||
|
L10nAtEldaDatei,
|
||||||
|
L10nAtEldaSatz,
|
||||||
|
l10n_at_euro_cent,
|
||||||
|
)
|
||||||
|
|
||||||
|
from odoo.addons.l10n_at_hr_payroll.models.lohnzettel_l16 import \
|
||||||
|
l10n_at_namen
|
||||||
|
|
||||||
|
# Paket (E.32, Selbstabrechner PS) — nach IDTEIL, Länge 305.
|
||||||
|
MBGM_PAKET = [
|
||||||
|
(2, 'REFP', 21, 40, 'a'),
|
||||||
|
(3, 'BKNR', 61, 10, 'a'),
|
||||||
|
(4, 'DGNA', 71, 70, 'a'),
|
||||||
|
(5, 'MPKE', 141, 30, 'a'),
|
||||||
|
(6, 'JAGB', 171, 1, 'a'),
|
||||||
|
(7, 'DTEL', 172, 50, 'a'),
|
||||||
|
(8, 'MAIL', 222, 60, 'a'),
|
||||||
|
(9, 'BZRM', 282, 6, 'n'), # MMJJJJ
|
||||||
|
(10, 'GSVZ', 288, 1, 'a'),
|
||||||
|
(11, 'GSUM', 289, 11, 'n'), # Σ VSUM (Cent)
|
||||||
|
(12, 'ANZM', 300, 6, 'n'), # Anzahl mBGM (G/R-Sätze)
|
||||||
|
]
|
||||||
|
|
||||||
|
# mBGM-Satz (E.32, G1 Selbstabrechner) — nach IDTEIL, Länge 326.
|
||||||
|
MBGM_SATZ = [
|
||||||
|
(2, 'REFW', 21, 40, 'a'),
|
||||||
|
(3, 'REFU', 61, 40, 'a'),
|
||||||
|
(4, 'REFV', 101, 40, 'a'),
|
||||||
|
(5, 'VSNR', 141, 10, 'n'),
|
||||||
|
(6, 'FANA', 151, 70, 'a'),
|
||||||
|
(7, 'VONA', 221, 70, 'a'),
|
||||||
|
(8, 'VSUM', 291, 11, 'n'), # Σ RSUM (Cent)
|
||||||
|
(9, 'VERG', 302, 1, 'a'), # 1 = SV+BV mit Versicherungszeit
|
||||||
|
(10, 'INF1', 303, 12, 'a'),
|
||||||
|
(11, 'INF2', 315, 12, 'a'),
|
||||||
|
]
|
||||||
|
|
||||||
|
# Tarifblock (E.32, T1) — nach IDTEIL, Länge 42.
|
||||||
|
MBGM_TARIFBLOCK = [
|
||||||
|
(2, 'BSGR', 21, 4, 'a'), # Beschäftigtengruppe (SV-Tarifsystem)
|
||||||
|
(3, 'ERGB', 25, 15, 'a'), # 5 × 3-stellige Ergänzungen
|
||||||
|
(4, 'VVON', 40, 2, 'n'), # Beginn der Verrechnung (Tag)
|
||||||
|
(5, 'KEUE', 42, 1, 'a'), # enthält KE/UE
|
||||||
|
]
|
||||||
|
|
||||||
|
# Verrechnungsbasis (E.32, BS) — nach IDTEIL, Länge 33.
|
||||||
|
MBGM_BASIS = [
|
||||||
|
(2, 'VBTY', 21, 2, 'a'), # AB/SZ/BV/UU …
|
||||||
|
(3, 'VBBT', 23, 11, 'n'), # Cent
|
||||||
|
]
|
||||||
|
|
||||||
|
# Verrechnungsposition (E.32, V1) — nach IDTEIL, Länge 42.
|
||||||
|
MBGM_POSITION = [
|
||||||
|
(2, 'VPTY', 21, 3, 'a'), # T01/T02/V01/A01…A15/Z01 …
|
||||||
|
(3, 'VPVZ', 24, 1, 'a'), # Vorzeichen Prozentsatz
|
||||||
|
(4, 'VPTA', 25, 6, 'n'), # Prozentsatz ×1000 (3 Nachkommastellen)
|
||||||
|
(5, 'RSVZ', 31, 1, 'a'), # Vorzeichen RSUM
|
||||||
|
(6, 'RSUM', 32, 11, 'n'), # Cent
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class L10nAtPayrollMbgm(models.Model):
|
||||||
|
_name = 'l10n.at.payroll.mbgm'
|
||||||
|
_description = 'Monatliche Beitragsgrundlagenmeldung (mBGM/JASV)'
|
||||||
|
_inherit = ['mail.thread']
|
||||||
|
_order = 'beitragsmonat desc, id desc'
|
||||||
|
|
||||||
|
company_id = fields.Many2one(
|
||||||
|
'res.company', required=True,
|
||||||
|
default=lambda self: self.env.company)
|
||||||
|
beitragsmonat = fields.Date(
|
||||||
|
required=True, string='Beitragsmonat',
|
||||||
|
help='Erster Tag des Beitragsmonats (MMJJJJ).')
|
||||||
|
art = fields.Selection(
|
||||||
|
[('MB', 'mBGM (regulär)'),
|
||||||
|
('ST', 'Storno (R-Sätze, iVm REFU)'),
|
||||||
|
('FZ', 'Fehlanzeige (Paket ohne mBGM-Sätze)')],
|
||||||
|
required=True, default='MB', tracking=True)
|
||||||
|
storno_id = fields.Many2one(
|
||||||
|
'l10n.at.payroll.mbgm', string='Stornierte Deklaration',
|
||||||
|
help='Art ST: Referenz auf die ursprüngliche Deklaration; '
|
||||||
|
'REFU der R-Sätze = REFW der stornierten mBGM je AN.')
|
||||||
|
line_ids = fields.One2many(
|
||||||
|
'l10n.at.payroll.mbgm.line', 'mbgm_id', string='mBGM-Sätze')
|
||||||
|
datei = fields.Binary(attachment=True, 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 = [(
|
||||||
|
'mbgm_company_monat_art_uniq',
|
||||||
|
'unique(company_id, beitragsmonat, art)',
|
||||||
|
'Für diesen Beitragsmonat existiert bereits eine mBGM-'
|
||||||
|
'Deklaration dieser Art.')]
|
||||||
|
|
||||||
|
def _l10n_at_slips(self):
|
||||||
|
self.ensure_one()
|
||||||
|
erster = self.beitragsmonat
|
||||||
|
letzter = (date(erster.year, erster.month, 28)
|
||||||
|
+ timedelta(days=4)).replace(day=1) \
|
||||||
|
- timedelta(days=1)
|
||||||
|
return self.env['hr.payslip'].search([
|
||||||
|
('company_id', '=', self.company_id.id),
|
||||||
|
('date_from', '>=', erster),
|
||||||
|
('date_from', '<=', letzter),
|
||||||
|
('state', 'in', ('validated', 'paid')),
|
||||||
|
])
|
||||||
|
|
||||||
|
def _l10n_at_slip(self, employee):
|
||||||
|
"""Der (eindeutige) Lauf des AN im Beitragsmonat."""
|
||||||
|
self.ensure_one()
|
||||||
|
erster = self.beitragsmonat
|
||||||
|
letzter = (date(erster.year, erster.month, 28)
|
||||||
|
+ timedelta(days=4)).replace(day=1) \
|
||||||
|
- timedelta(days=1)
|
||||||
|
return self.env['hr.payslip'].search([
|
||||||
|
('company_id', '=', self.company_id.id),
|
||||||
|
('employee_id', '=', employee.id),
|
||||||
|
('date_from', '>=', erster),
|
||||||
|
('date_from', '<=', letzter),
|
||||||
|
('state', 'in', ('validated', 'paid'))], limit=1)
|
||||||
|
|
||||||
|
def _l10n_at_bsgr(self, version, slip):
|
||||||
|
"""Beschäftigtengruppe (SV-Tarifsystem): B001 Arbeiter, B002
|
||||||
|
Angestellte; Lehrlinge B044/B045 und geringfügig/fallweise
|
||||||
|
(B010/B030) ohne Default-Parameter → Lauf bricht ab, bis
|
||||||
|
`at_mbgm_tarif` gepflegt ist (Tarifsystem-Werte, keine
|
||||||
|
Erfindung)."""
|
||||||
|
if version.l10n_at_lehrling:
|
||||||
|
return 'B044' if getattr(
|
||||||
|
version, 'l10n_at_personalgruppe', '') == 'arbeiter' \
|
||||||
|
else 'B045'
|
||||||
|
if slip.l10n_at_geringfuegig or version.l10n_at_fallweise:
|
||||||
|
return 'B030'
|
||||||
|
return 'B001' if getattr(
|
||||||
|
version, 'l10n_at_personalgruppe', '') == 'arbeiter' \
|
||||||
|
else 'B002'
|
||||||
|
|
||||||
|
def _l10n_at_tarif(self, bsgr):
|
||||||
|
self.ensure_one()
|
||||||
|
tarif = self.env['hr.rule.parameter']._get_parameter_from_code(
|
||||||
|
'at_mbgm_tarif', self.beitragsmonat,
|
||||||
|
raise_if_not_found=False) or {}
|
||||||
|
return tarif.get(bsgr)
|
||||||
|
|
||||||
|
def _l10n_at_positionen(self, slip, tarif):
|
||||||
|
"""Verrechnungspositionen je Tarifblock: T01 (Gesamtsatz,
|
||||||
|
NSchAB-Zweig via Company-Flag) + Abschläge aus den GP4-
|
||||||
|
Stichtagsflags; T02 auf SZ; V01 auf BV-Basis. Rückgabe:
|
||||||
|
[(vpty, vpta, rsum_cent, basis_typ), …] — RSUM aus Basis ×
|
||||||
|
Satz (mBGM-Tarifierung); Abschläge negativ (RSVZ '-')."""
|
||||||
|
version = slip.version_id
|
||||||
|
ab = sum(basis for _m, basis
|
||||||
|
in slip._l10n_at_sv_basis_laufen_monate())
|
||||||
|
nschab = 3.80 if (self.company_id.l10n_at_nschab
|
||||||
|
and not version.l10n_at_lehrling
|
||||||
|
and getattr(version, 'l10n_at_personalgruppe',
|
||||||
|
'') == 'arbeiter') else 0.0
|
||||||
|
satz_laufen = float(tarif['laufend']) + nschab
|
||||||
|
positionen = [('T01', satz_laufen, ab * satz_laufen / 100.0,
|
||||||
|
'AB')]
|
||||||
|
# §-2a-AMPFG: Minderung nur des DN-AV (2,95 %), als eigene
|
||||||
|
# Abschlagsposition auf JEDER betroffenen Verrechnungsbasis
|
||||||
|
# (AB und SZ — der Abschlag senkt den Gesamtsatz je Basis;
|
||||||
|
# DN-seitig, GP4-Konvention). Codes: 0 % → A03, 1 % → A02,
|
||||||
|
# 2 % → A01 (D.60, verifiziert).
|
||||||
|
minderung = 0.0
|
||||||
|
monat = self.beitragsmonat
|
||||||
|
if not (version.l10n_at_alv_entfall_ab
|
||||||
|
and monat >= version.l10n_at_alv_entfall_ab):
|
||||||
|
staffel = self.env['hr.rule.parameter'] \
|
||||||
|
._get_parameter_from_code(
|
||||||
|
'at_sv_av_staffel_ampfg', monat,
|
||||||
|
raise_if_not_found=False) or {}
|
||||||
|
for _von, bis, dn_satz in staffel.get('tiers', []):
|
||||||
|
if ab and ab <= float(bis):
|
||||||
|
minderung = 2.95 - float(dn_satz)
|
||||||
|
break
|
||||||
|
if minderung > 0:
|
||||||
|
code = ('A03' if minderung >= 2.95
|
||||||
|
else 'A02' if abs(minderung - 1.95) < 0.01
|
||||||
|
else 'A01')
|
||||||
|
positionen.append(
|
||||||
|
(code, 0.0, -ab * minderung / 100.0, 'AB'))
|
||||||
|
else:
|
||||||
|
# A10 = Entfall AV+IE (DN-AV + DG-AV + DG-IESG).
|
||||||
|
positionen.append(
|
||||||
|
('A10', 0.0, -ab * 6.00 / 100.0, 'AB'))
|
||||||
|
if version.l10n_at_uv_entfall_ab \
|
||||||
|
and monat >= version.l10n_at_uv_entfall_ab:
|
||||||
|
positionen.append(('A09', 0.0, -ab * 1.10 / 100.0, 'AB'))
|
||||||
|
if version.l10n_at_pv_bonusphase_ab \
|
||||||
|
and monat >= version.l10n_at_pv_bonusphase_ab:
|
||||||
|
positionen.append(('A15', 0.0, -ab * 11.40 / 100.0, 'AB'))
|
||||||
|
sz_basis = slip._l10n_at_svdn_basis_sz()
|
||||||
|
if sz_basis > 0:
|
||||||
|
satz_sz = float(tarif['sz'])
|
||||||
|
positionen.append(
|
||||||
|
('T02', satz_sz, sz_basis * satz_sz / 100.0, 'SZ'))
|
||||||
|
if minderung > 0:
|
||||||
|
code = ('A03' if minderung >= 2.95
|
||||||
|
else 'A02' if abs(minderung - 1.95) < 0.01
|
||||||
|
else 'A01')
|
||||||
|
positionen.append(
|
||||||
|
(code, 0.0, -sz_basis * minderung / 100.0, 'SZ'))
|
||||||
|
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 action_erzeugen(self):
|
||||||
|
for deklaration in self:
|
||||||
|
fehler = []
|
||||||
|
warnungen = []
|
||||||
|
company = deklaration.company_id
|
||||||
|
bknr = company.l10n_at_beitragskontonummer
|
||||||
|
if not bknr:
|
||||||
|
fehler.append('Beitragskontonummer (BKNR) fehlt.')
|
||||||
|
vstr = (bknr or '')[:2]
|
||||||
|
deklaration.line_ids.unlink()
|
||||||
|
datensaetze = []
|
||||||
|
summe_gesamt = 0
|
||||||
|
anzahl = 0
|
||||||
|
if deklaration.art != 'FZ':
|
||||||
|
slips = deklaration._l10n_at_slips()
|
||||||
|
nach_an = {}
|
||||||
|
for slip in slips:
|
||||||
|
nach_an.setdefault(slip.employee_id, []).append(slip)
|
||||||
|
for employee, employee_slips in nach_an.items():
|
||||||
|
version = employee.version_id
|
||||||
|
if not (version and version.ssnid):
|
||||||
|
fehler.append(
|
||||||
|
'Mitarbeiter %s ohne Versicherungsnummer.'
|
||||||
|
% employee.name)
|
||||||
|
continue
|
||||||
|
if len(employee_slips) > 1:
|
||||||
|
fehler.append(
|
||||||
|
'Mitarbeiter %s mit mehreren Läufen im '
|
||||||
|
'Beitragsmonat — mBGM je Lauf ist ein '
|
||||||
|
'dokumentierter Rest.' % employee.name)
|
||||||
|
continue
|
||||||
|
slip = employee_slips[0]
|
||||||
|
if deklaration.art == 'ST':
|
||||||
|
# Storno: R1-Satz je stornierter mBGM mit
|
||||||
|
# REFU = REFW des Originals, VSUM wie Original
|
||||||
|
# (E.32.2.8 — ohne T-/BS-/V-Sätze).
|
||||||
|
original = deklaration.storno_id.line_ids \
|
||||||
|
.filtered(lambda l: l.employee_id == employee)
|
||||||
|
if not original:
|
||||||
|
fehler.append(
|
||||||
|
'Storno %s: keine stornierte mBGM '
|
||||||
|
'am Mitarbeiter gefunden.' % employee.name)
|
||||||
|
continue
|
||||||
|
refw = 'MBGM-ST-%d-%s-%d' % (
|
||||||
|
company.id,
|
||||||
|
deklaration.beitragsmonat.strftime('%Y%m'),
|
||||||
|
employee.id)
|
||||||
|
storno_satz = L10nAtEldaSatz(
|
||||||
|
L10N_AT_ELDA_IDTEIL + MBGM_SATZ, 326,
|
||||||
|
bezeichnung='Storno %s' % employee.name)
|
||||||
|
storno_satz.setze('SART', 'R1')
|
||||||
|
storno_satz.setze('REFW', refw)
|
||||||
|
storno_satz.setze('REFU', original.refw)
|
||||||
|
storno_satz.setze('VSNR', version.ssnid)
|
||||||
|
storno_satz.setze('VSUM', abs(original.vsum),
|
||||||
|
cent=True)
|
||||||
|
datensaetze.append(storno_satz)
|
||||||
|
deklaration.env['l10n.at.payroll.mbgm.line']\
|
||||||
|
.create({
|
||||||
|
'mbgm_id': deklaration.id,
|
||||||
|
'employee_id': employee.id,
|
||||||
|
'versicherungsnummer': version.ssnid,
|
||||||
|
'bsgr': original.bsgr,
|
||||||
|
'refw': refw,
|
||||||
|
'vsum': original.vsum,
|
||||||
|
})
|
||||||
|
summe_gesamt += original.vsum
|
||||||
|
anzahl += 1
|
||||||
|
continue
|
||||||
|
bsgr = deklaration._l10n_at_bsgr(version, slip)
|
||||||
|
tarif = deklaration._l10n_at_tarif(bsgr)
|
||||||
|
if not tarif:
|
||||||
|
fehler.append(
|
||||||
|
'Mitarbeiter %s: kein Tarifsatz für BSGR '
|
||||||
|
'%s parametrisiert (at_mbgm_tarif, Werte '
|
||||||
|
'aus dem SV-Tarifsystem pflegen).'
|
||||||
|
% (employee.name, bsgr))
|
||||||
|
continue
|
||||||
|
if slip.l10n_at_geringfuegig \
|
||||||
|
or version.l10n_at_fallweise:
|
||||||
|
fehler.append(
|
||||||
|
'Mitarbeiter %s: geringfügige/fallweise '
|
||||||
|
'Beschäftigung in der mBGM ist ein '
|
||||||
|
'dokumentierter Rest (BSGR %s, Tarifsystem '
|
||||||
|
'verifizieren).' % (employee.name, bsgr))
|
||||||
|
continue
|
||||||
|
positionen = deklaration._l10n_at_positionen(
|
||||||
|
slip, tarif)
|
||||||
|
vsum = sum(rsum for _c, _s, rsum, _b in positionen)
|
||||||
|
# Konsistenz gegen die GP4-Payslip-Summen.
|
||||||
|
ist = (slip._l10n_at_svdn_gesamt()
|
||||||
|
+ slip._l10n_at_svdg_gesamt()
|
||||||
|
+ slip._l10n_at_svdn_sz()
|
||||||
|
+ sum(
|
||||||
|
slip._l10n_at_svdg_sz(k)
|
||||||
|
for k in ('kv', 'pv', 'av', 'uv', 'iesg'))
|
||||||
|
+ slip._at_bv_beitrag())
|
||||||
|
if abs(vsum - ist) > 0.10:
|
||||||
|
warnungen.append(
|
||||||
|
'Mitarbeiter %s: mBGM-VSUM %.2f weicht um '
|
||||||
|
'%.2f von den GP4-Beitragssummen ab '
|
||||||
|
'(Tarif-/Rundungsdifferenz prüfen).'
|
||||||
|
% (employee.name, vsum, vsum - ist))
|
||||||
|
deklaration.env['l10n.at.payroll.mbgm.line'].create({
|
||||||
|
'mbgm_id': deklaration.id,
|
||||||
|
'employee_id': employee.id,
|
||||||
|
'versicherungsnummer': version.ssnid,
|
||||||
|
'bsgr': bsgr,
|
||||||
|
'refw': 'MBGM-%d-%s-%d' % (
|
||||||
|
company.id,
|
||||||
|
deklaration.beitragsmonat.strftime('%Y%m'),
|
||||||
|
employee.id),
|
||||||
|
'vsum': vsum,
|
||||||
|
})
|
||||||
|
datensaetze.extend(
|
||||||
|
deklaration._l10n_at_mbgm_datensaetze(
|
||||||
|
employee, version, bsgr, positionen, vsum))
|
||||||
|
summe_gesamt += vsum
|
||||||
|
anzahl += 1
|
||||||
|
paket = L10nAtEldaSatz(
|
||||||
|
L10N_AT_ELDA_IDTEIL + MBGM_PAKET, 326,
|
||||||
|
bezeichnung='mBGM-Paket')
|
||||||
|
paket.setze('SART', 'PS')
|
||||||
|
paket.setze('VSTR', vstr)
|
||||||
|
paket.setze('REFP', 'MBGM-%d-%s' % (
|
||||||
|
company.id,
|
||||||
|
deklaration.beitragsmonat.strftime('%Y%m')))
|
||||||
|
paket.setze('BKNR', bknr)
|
||||||
|
paket.setze('DGNA', company.name[:70])
|
||||||
|
paket.setze('JAGB', 'N')
|
||||||
|
paket.setze('BZRM', deklaration.beitragsmonat.strftime(
|
||||||
|
'%m%Y'))
|
||||||
|
paket.setze('GSVZ', '+' if summe_gesamt >= 0 else '-')
|
||||||
|
paket.setze('GSUM', abs(summe_gesamt), cent=True)
|
||||||
|
paket.setze('ANZM', anzahl)
|
||||||
|
datensaetze.insert(0, paket)
|
||||||
|
if fehler:
|
||||||
|
deklaration.protokoll = 'FEHLER:\n%s\nWARNUNGEN:\n%s' % (
|
||||||
|
'\n'.join(fehler), '\n'.join(warnungen) or '—')
|
||||||
|
raise UserError('mBGM-Lauf mit Fehlern abgebrochen:\n'
|
||||||
|
+ '\n'.join(fehler))
|
||||||
|
bestand = L10nAtEldaBestand(
|
||||||
|
'MB', vstr, '02',
|
||||||
|
hersteller={'name': 'odoo-at-payroll', 'kfz': 'A',
|
||||||
|
'plz': company.partner_id.zip,
|
||||||
|
'ort': company.partner_id.city,
|
||||||
|
'strasse': company.partner_id.street},
|
||||||
|
datensaetze=datensaetze)
|
||||||
|
datei = L10nAtEldaDatei([bestand])
|
||||||
|
deklaration.write({
|
||||||
|
'datei': base64.b64encode(datei.render()),
|
||||||
|
'datei_name': 'mbgm_%s.txt' % (
|
||||||
|
deklaration.beitragsmonat.strftime('%Y%m'),),
|
||||||
|
'protokoll': 'WARNUNGEN:\n%s' % (
|
||||||
|
'\n'.join(warnungen) or '—'),
|
||||||
|
'state': 'done',
|
||||||
|
})
|
||||||
|
return True
|
||||||
|
|
||||||
|
def _l10n_at_mbgm_datensaetze(self, employee, version, bsgr,
|
||||||
|
positionen, vsum):
|
||||||
|
"""G1 + Tarifblock + je Basis (AB/SZ/BV) die Positionen als
|
||||||
|
eigene Sätze (BS/V folgen hierarchisch dem Tarifblock)."""
|
||||||
|
self.ensure_one()
|
||||||
|
slip = self._l10n_at_slip(employee)
|
||||||
|
mbgm_satz = L10nAtEldaSatz(
|
||||||
|
L10N_AT_ELDA_IDTEIL + MBGM_SATZ, 326,
|
||||||
|
bezeichnung='mBGM %s' % employee.name)
|
||||||
|
mbgm_satz.setze('SART', 'G1')
|
||||||
|
mbgm_satz.setze('REFW', 'MBGM-%d-%s-%d' % (
|
||||||
|
self.company_id.id,
|
||||||
|
self.beitragsmonat.strftime('%Y%m'), employee.id))
|
||||||
|
mbgm_satz.setze('VSNR', version.ssnid)
|
||||||
|
familienname, vorname = l10n_at_namen(employee)
|
||||||
|
mbgm_satz.setze('FANA', familienname[:70])
|
||||||
|
mbgm_satz.setze('VONA', (vorname or '')[:70])
|
||||||
|
mbgm_satz.setze('VSUM', abs(vsum), cent=True)
|
||||||
|
mbgm_satz.setze('VERG', '1')
|
||||||
|
tarifblock = L10nAtEldaSatz(
|
||||||
|
L10N_AT_ELDA_IDTEIL + MBGM_TARIFBLOCK, 326,
|
||||||
|
bezeichnung='Tarifblock %s' % employee.name)
|
||||||
|
tarifblock.setze('SART', 'T1')
|
||||||
|
tarifblock.setze('BSGR', bsgr)
|
||||||
|
tarifblock.setze('VVON', '1')
|
||||||
|
saetze = [mbgm_satz, tarifblock]
|
||||||
|
for code, satz_prozent, rsum, basis_typ in positionen:
|
||||||
|
basis = L10nAtEldaSatz(
|
||||||
|
L10N_AT_ELDA_IDTEIL + MBGM_BASIS, 326,
|
||||||
|
bezeichnung='Basis %s %s' % (basis_typ,
|
||||||
|
employee.name))
|
||||||
|
basis.setze('SART', 'BS')
|
||||||
|
basis.setze('VBTY', basis_typ)
|
||||||
|
basis_wert = abs(rsum) * 100.0 / satz_prozent \
|
||||||
|
if satz_prozent else 0.0
|
||||||
|
if basis_typ == 'AB':
|
||||||
|
basis_wert = sum(
|
||||||
|
b for _m, b in
|
||||||
|
slip._l10n_at_sv_basis_laufen_monate())
|
||||||
|
elif basis_typ == 'SZ':
|
||||||
|
basis_wert = slip._l10n_at_svdn_basis_sz()
|
||||||
|
else:
|
||||||
|
basis_wert = slip._at_bv_basis_laufen() \
|
||||||
|
+ slip._at_bv_basis_sonst()
|
||||||
|
basis.setze('VBBT', basis_wert, cent=True)
|
||||||
|
position = L10nAtEldaSatz(
|
||||||
|
L10N_AT_ELDA_IDTEIL + MBGM_POSITION, 326,
|
||||||
|
bezeichnung='Position %s %s' % (code, employee.name))
|
||||||
|
position.setze('SART', 'V1')
|
||||||
|
position.setze('VPTY', code)
|
||||||
|
position.setze('VPTA', satz_prozent, skala=3)
|
||||||
|
position.setze('RSVZ', '-' if rsum < 0 else '+')
|
||||||
|
position.setze('RSUM', abs(rsum), cent=True)
|
||||||
|
saetze.extend([basis, position])
|
||||||
|
return saetze
|
||||||
|
|
||||||
|
|
||||||
|
class ResCompany(models.Model):
|
||||||
|
_inherit = 'res.company'
|
||||||
|
|
||||||
|
l10n_at_nschab = fields.Boolean(
|
||||||
|
string='AT Schlechtwetterentschädigungsbeitrag-Branche',
|
||||||
|
help='Betrieb mit Schlechtwetterentschädigung (ÖGK-'
|
||||||
|
'Tarifsystem „Normalbetriebe mit '
|
||||||
|
'Schlechtwetterentschädigung“): Arbeiter (BSGR B001) '
|
||||||
|
'erhalten im mBGM-T01 den um NSchAB 3,80 % erhöhten '
|
||||||
|
'Gesamtsatz.')
|
||||||
|
|
||||||
|
|
||||||
|
class L10nAtPayrollMbgmLine(models.Model):
|
||||||
|
_name = 'l10n.at.payroll.mbgm.line'
|
||||||
|
_description = 'mBGM — Satzdaten je AN'
|
||||||
|
_order = 'employee_name'
|
||||||
|
mbgm_id = fields.Many2one(
|
||||||
|
'l10n.at.payroll.mbgm', required=True, ondelete='cascade',
|
||||||
|
index=True)
|
||||||
|
employee_id = fields.Many2one('hr.employee', required=True)
|
||||||
|
employee_name = fields.Char(related='employee_id.name', store=True,
|
||||||
|
readonly=True)
|
||||||
|
versicherungsnummer = fields.Char()
|
||||||
|
bsgr = fields.Char(string='Beschäftigtengruppe')
|
||||||
|
refw = fields.Char(string='Referenzwert (REFW)')
|
||||||
|
vsum = fields.Monetary(
|
||||||
|
string='VSUM', currency_field='currency_id')
|
||||||
|
currency_id = fields.Many2one(
|
||||||
|
'res.currency', related='mbgm_id.company_id.currency_id')
|
||||||
@@ -0,0 +1,406 @@
|
|||||||
|
# Part of the odoo-at-payroll project. License: LGPL-3.
|
||||||
|
"""Versichertenmeldung reduziert (GP7/AP5) — DM-ORG 42.7.0 Kapitel
|
||||||
|
E.29 (BEST ``VR``, Version 03, Satzlänge 772; PDF-verifizierte
|
||||||
|
Feldtabelle 2026-09-12): Anmeldung M3 (vor Arbeitsantritt), Abmeldung
|
||||||
|
M4 (7 Kalendertage ab PV-Ende), Änderungsmeldung M6 nur für
|
||||||
|
Ausnahmefälle (Inhaltsänderungen laufen über die mBGM; lb-mel-08).
|
||||||
|
Fallweise Anmeldung (BEST ``MA``, SART 32/33, Länge 450): je
|
||||||
|
Beschäftigungstag vor Arbeitsantritt (§ 33 Abs 1a Z 1 ASVG).
|
||||||
|
|
||||||
|
Event-Modell deklarativ (Dimona-auto-Muster, ohne write-diff):
|
||||||
|
offene Anmeldungen = älteste Version eines Beschäftigungsspannen-
|
||||||
|
Beginns ohne M3-Eintrag in der Meldungs-Registry; offene Abmeldungen
|
||||||
|
= beendete Versionen ohne M4-Eintrag. Die Registry
|
||||||
|
(``l10n.at.payroll.vr.event``) protokolliert gesendete Meldungen
|
||||||
|
(REFW/ADAT), damit erneute Läufe idempotent bleiben.
|
||||||
|
|
||||||
|
Felder: BBER 01 Arbeiter/02 Angestellte(r)/03–04 Lehrling (D.39),
|
||||||
|
GERF geringfügig (J/N), FRDV freies Dienstverhältnis, ADAT An-/
|
||||||
|
Abmeldedatum, EBSV arbeitsrechtliches Ende vs ADAT (UEL/KE-Verlänger
|
||||||
|
ung der PV sind nicht modelliert → ADAT = contract_date_end),
|
||||||
|
AGRD eSV-Abmeldegrund (Mapping am hr.departure.reason, Standard 00),
|
||||||
|
BVAB/BVJN aus dem GP5-BMSVG-Flag, VWAZ vereinbarte Wochenarbeitszeit
|
||||||
|
(Stunden ×100, neu für Meldedaten ab 01.01.2026).
|
||||||
|
|
||||||
|
Dateibasiert (Prämisse P7); Bestätigungen/Übertragung beim Mandanten."""
|
||||||
|
from datetime import date
|
||||||
|
import base64
|
||||||
|
|
||||||
|
from odoo import api, fields, models
|
||||||
|
from odoo.exceptions import UserError
|
||||||
|
|
||||||
|
from odoo.addons.l10n_at_hr_payroll.models.elda import (
|
||||||
|
L10N_AT_ELDA_IDTEIL,
|
||||||
|
L10nAtEldaBestand,
|
||||||
|
L10nAtEldaDatei,
|
||||||
|
L10nAtEldaSatz,
|
||||||
|
)
|
||||||
|
|
||||||
|
from odoo.addons.l10n_at_hr_payroll.models.lohnzettel_l16 import \
|
||||||
|
l10n_at_namen
|
||||||
|
|
||||||
|
# Versichertenmeldung (E.29, Version 03) — nach IDTEIL, Länge 772.
|
||||||
|
VR_SATZ = [
|
||||||
|
(2, 'REFW', 21, 40, 'a'),
|
||||||
|
(3, 'REFU', 61, 40, 'a'),
|
||||||
|
(4, 'BKNR', 101, 10, 'a'),
|
||||||
|
(5, 'DGNA', 111, 70, 'a'),
|
||||||
|
(6, 'DTEL', 181, 50, 'a'),
|
||||||
|
(7, 'MAIL', 231, 60, 'a'),
|
||||||
|
(8, 'INF1', 291, 12, 'a'),
|
||||||
|
(9, 'INF2', 303, 12, 'a'),
|
||||||
|
(10, 'VSNR', 315, 10, 'n'),
|
||||||
|
(11, 'GEBD', 325, 8, 'n'), # TTMMJJJJ
|
||||||
|
(12, 'REFV', 333, 40, 'a'),
|
||||||
|
(13, 'FANA', 373, 70, 'a'),
|
||||||
|
(14, 'VONA', 443, 70, 'a'),
|
||||||
|
(15, 'ADAT', 513, 8, 'n'), # TTMMJJJJ
|
||||||
|
(16, 'BDAT', 521, 8, 'n'),
|
||||||
|
(17, 'RDAT', 529, 8, 'n'),
|
||||||
|
(18, 'BBER', 537, 2, 'a'), # Beschäftigungsbereich (D.39)
|
||||||
|
(19, 'GERF', 539, 1, 'a'), # geringfügig J/N
|
||||||
|
(20, 'FRDV', 540, 1, 'a'), # freier Dienstvertrag J/N
|
||||||
|
(21, 'EBSV', 541, 8, 'n'), # Ende des Beschäftigungsverhältnisses
|
||||||
|
(22, 'AGRD', 549, 2, 'a'), # Abmeldegrund (D.22)
|
||||||
|
(23, 'SAGR', 551, 20, 'a'),
|
||||||
|
(24, 'KEAB', 571, 8, 'n'),
|
||||||
|
(25, 'KEBI', 579, 8, 'n'),
|
||||||
|
(26, 'UEAB', 587, 8, 'n'),
|
||||||
|
(27, 'UEBI', 595, 8, 'n'),
|
||||||
|
(28, 'BVAB', 603, 8, 'n'), # BV-Beitrag ab
|
||||||
|
(29, 'BVEN', 611, 8, 'n'),
|
||||||
|
(30, 'BVJN', 619, 1, 'a'), # Betriebliche Vorsorge J/N
|
||||||
|
(31, 'UMDA', 620, 8, 'n'),
|
||||||
|
(32, 'RUMD', 628, 8, 'n'),
|
||||||
|
(33, 'SOUM', 636, 1, 'a'),
|
||||||
|
(34, 'ZTUM', 637, 2, 'a'),
|
||||||
|
(35, 'ZKUM', 639, 10, 'a'),
|
||||||
|
(36, 'RWUM', 649, 40, 'a'),
|
||||||
|
(37, 'RUUM', 689, 40, 'a'),
|
||||||
|
(38, 'BKUM', 729, 40, 'a'),
|
||||||
|
(39, 'VWAZ', 769, 4, 'n'), # Stunden ×100
|
||||||
|
]
|
||||||
|
|
||||||
|
# Fallweise Anmeldung (E.18, Version 07) — nach IDTEIL, Länge 450.
|
||||||
|
FALLWEISE_SATZ = [
|
||||||
|
(2, 'BKNR', 21, 10, 'a'),
|
||||||
|
(3, 'DGNA', 31, 70, 'a'),
|
||||||
|
(4, 'VSNR', 101, 10, 'n'),
|
||||||
|
(5, 'GEBD', 111, 8, 'n'),
|
||||||
|
(6, 'FANA', 119, 70, 'a'),
|
||||||
|
(7, 'VONA', 189, 70, 'a'),
|
||||||
|
(8, 'GESL', 259, 1, 'n'), # 1 männlich/2 weiblich …
|
||||||
|
(9, 'ADAT', 260, 8, 'n'), # Tag der fallweisen Beschäftigung
|
||||||
|
(10, 'BKFZ', 268, 3, 'a'),
|
||||||
|
(11, 'BPLZ', 271, 9, 'a'),
|
||||||
|
(12, 'BORT', 280, 40, 'a'),
|
||||||
|
(14, 'WOBD', 321, 12, 'a'),
|
||||||
|
(15, 'ZOBD', 333, 15, 'a'),
|
||||||
|
(17, 'REFN', 410, 30, 'a'),
|
||||||
|
(18, 'VTAZ', 440, 4, 'n'), # Stunden ×100
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class L10nAtPayrollVrEvent(models.Model):
|
||||||
|
"""Meldungs-Registry: je gesendeter Meldung ein Eintrag (Art,
|
||||||
|
ADAT, REFW) — Grundlage für die deklarative Event-Erkennung der
|
||||||
|
offenen An-/Abmeldungen."""
|
||||||
|
_name = 'l10n.at.payroll.vr.event'
|
||||||
|
_description = 'Versichertenmeldung — gesendete Meldungen'
|
||||||
|
_order = 'adatum, id'
|
||||||
|
|
||||||
|
company_id = fields.Many2one('res.company', required=True,
|
||||||
|
default=lambda self: self.env.company)
|
||||||
|
employee_id = fields.Many2one('hr.employee', required=True,
|
||||||
|
index=True, ondelete='cascade')
|
||||||
|
version_id = fields.Many2one('hr.version', required=True,
|
||||||
|
ondelete='cascade')
|
||||||
|
art = fields.Selection(
|
||||||
|
[('M3', 'Anmeldung'), ('M4', 'Abmeldung'), ('M6', 'Änderung'),
|
||||||
|
('MA', 'Fallweise Anmeldung')],
|
||||||
|
required=True)
|
||||||
|
adatum = fields.Date(required=True)
|
||||||
|
refw = fields.Char(required=True)
|
||||||
|
|
||||||
|
|
||||||
|
class L10nAtPayrollVr(models.Model):
|
||||||
|
_name = 'l10n.at.payroll.vr'
|
||||||
|
_description = 'Versichertenmeldung (An-/Abmeldung) — ELDA-Lauf'
|
||||||
|
_inherit = ['mail.thread']
|
||||||
|
_order = 'id desc'
|
||||||
|
|
||||||
|
company_id = fields.Many2one(
|
||||||
|
'res.company', required=True,
|
||||||
|
default=lambda self: self.env.company)
|
||||||
|
stichtag = fields.Date(
|
||||||
|
default=fields.Date.today,
|
||||||
|
help='Anmeldungen bis zu diesem Tag (M3 ist eigentlich VOR '
|
||||||
|
'Arbeitsantritt zu übermitteln — der Lauf meldet '
|
||||||
|
'beginnende Beschäftigungsverhältnisse).')
|
||||||
|
nur_fallweise = fields.Boolean(
|
||||||
|
string='Nur fallweise Anmeldungen (BEST MA)',
|
||||||
|
help='Erzeugt ausschließlich Anmeldungen fallweise '
|
||||||
|
'Beschäftigter (je Beschäftigungstag, SART 32).')
|
||||||
|
datei = fields.Binary(attachment=True, 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)
|
||||||
|
event_ids = fields.Many2many(
|
||||||
|
'l10n.at.payroll.vr.event', string='Gesendete Meldungen',
|
||||||
|
copy=False)
|
||||||
|
|
||||||
|
def _l10n_at_offene_anmeldungen(self):
|
||||||
|
"""Älteste aktive Version je AN ohne M3-Eintrag: Beginn eines
|
||||||
|
Beschäftigungsverhältnisses (früheste contract_date_start)."""
|
||||||
|
self.ensure_one()
|
||||||
|
offene = []
|
||||||
|
versionen = self.env['hr.version'].search([
|
||||||
|
('company_id', '=', self.company_id.id),
|
||||||
|
('contract_date_start', '!=', False),
|
||||||
|
('active', '=', True),
|
||||||
|
]).with_company(self.company_id)
|
||||||
|
nach_an = {}
|
||||||
|
for version in versionen:
|
||||||
|
nach_an.setdefault(version.employee_id, version)
|
||||||
|
if version.contract_date_start \
|
||||||
|
< nach_an[version.employee_id].contract_date_start:
|
||||||
|
nach_an[version.employee_id] = version
|
||||||
|
registry = self.env['l10n.at.payroll.vr.event'].search([
|
||||||
|
('company_id', '=', self.company_id.id),
|
||||||
|
('art', 'in', ('M3', 'MA')),
|
||||||
|
])
|
||||||
|
gesehen = {(e.employee_id.id, e.adatum) for e in registry}
|
||||||
|
for employee, version in nach_an.items():
|
||||||
|
if (employee.id, version.contract_date_start) in gesehen:
|
||||||
|
continue
|
||||||
|
if version.contract_date_start > self.stichtag:
|
||||||
|
continue
|
||||||
|
offene.append(version)
|
||||||
|
return sorted(offene, key=lambda v: v.contract_date_start)
|
||||||
|
|
||||||
|
def _l10n_at_offene_abmeldungen(self):
|
||||||
|
self.ensure_one()
|
||||||
|
abmeldungen = []
|
||||||
|
versionen = self.env['hr.version'].search([
|
||||||
|
('company_id', '=', self.company_id.id),
|
||||||
|
('contract_date_end', '!=', False),
|
||||||
|
]).with_company(self.company_id)
|
||||||
|
registry = self.env['l10n.at.payroll.vr.event'].search([
|
||||||
|
('company_id', '=', self.company_id.id),
|
||||||
|
('art', '=', 'M4'),
|
||||||
|
])
|
||||||
|
gesehen = {(e.employee_id.id, e.adatum) for e in registry}
|
||||||
|
for version in versionen:
|
||||||
|
if (version.employee_id.id, version.contract_date_end) \
|
||||||
|
in gesehen:
|
||||||
|
continue
|
||||||
|
abmeldungen.append(version)
|
||||||
|
return sorted(abmeldungen, key=lambda v: v.contract_date_end)
|
||||||
|
|
||||||
|
def _l10n_at_bber(self, version):
|
||||||
|
if version.l10n_at_lehrling:
|
||||||
|
return '03' if getattr(
|
||||||
|
version, 'l10n_at_personalgruppe', '') == 'arbeiter' \
|
||||||
|
else '04'
|
||||||
|
return '01' if getattr(
|
||||||
|
version, 'l10n_at_personalgruppe', '') == 'arbeiter' \
|
||||||
|
else '02'
|
||||||
|
|
||||||
|
def _l10n_at_geringfuegig(self, version):
|
||||||
|
"""Vorausschauende Geringfügigkeits-Einschätzung zur Anmeldung:
|
||||||
|
Kataloggehalt/Überzahlung gegen die GFG (§ 5 Abs 2 ASVG); die
|
||||||
|
endgültige Bewertung läuft monatlich über den GP4-Kern."""
|
||||||
|
gfg = float(self.env['hr.rule.parameter']
|
||||||
|
._get_parameter_from_code(
|
||||||
|
'at_sv_gfg_monatlich', version.contract_date_start,
|
||||||
|
raise_if_not_found=False) or 0.0)
|
||||||
|
gehalt = version.wage \
|
||||||
|
or (version.kv_gruppe_id._get_gehalt(
|
||||||
|
version.erfahrungsstufe, version.contract_date_start)
|
||||||
|
if getattr(version, 'kv_gruppe_id', False) else 0.0)
|
||||||
|
ueberzahlung = getattr(version, 'ueberzahlung', 0.0) or 0.0
|
||||||
|
if ueberzahlung:
|
||||||
|
gehalt = ueberzahlung
|
||||||
|
return bool(gfg and gehalt and gehalt <= gfg)
|
||||||
|
|
||||||
|
def action_erzeugen(self):
|
||||||
|
for lauf in self:
|
||||||
|
fehler = []
|
||||||
|
company = lauf.company_id
|
||||||
|
bknr = company.l10n_at_beitragskontonummer
|
||||||
|
if not bknr:
|
||||||
|
raise UserError('Beitragskontonummer (BKNR) fehlt.')
|
||||||
|
datensaetze = []
|
||||||
|
ereignisse = []
|
||||||
|
vstr = bknr[:2]
|
||||||
|
if lauf.nur_fallweise:
|
||||||
|
versionen = self.env['hr.version'].search([
|
||||||
|
('company_id', '=', company.id),
|
||||||
|
('l10n_at_fallweise', '=', True),
|
||||||
|
('active', '=', True),
|
||||||
|
])
|
||||||
|
registry = self.env['l10n.at.payroll.vr.event'].search([
|
||||||
|
('company_id', '=', company.id),
|
||||||
|
('art', '=', 'MA'),
|
||||||
|
])
|
||||||
|
gesehen = {(e.employee_id.id, e.adatum)
|
||||||
|
for e in registry}
|
||||||
|
for version in versionen:
|
||||||
|
if not version.ssnid:
|
||||||
|
fehler.append('%s ohne Versicherungsnummer.'
|
||||||
|
% version.employee_id.name)
|
||||||
|
continue
|
||||||
|
# MVP: je AN ein Anmeldetag (der Vertragsbeginn);
|
||||||
|
# tagesgenaue Einsatztage = documented Rest
|
||||||
|
# (Work-Entry-Auswertung).
|
||||||
|
adat = version.contract_date_start
|
||||||
|
if not adat or (version.employee_id.id, adat) \
|
||||||
|
in gesehen:
|
||||||
|
continue
|
||||||
|
satz = L10nAtEldaSatz(
|
||||||
|
L10N_AT_ELDA_IDTEIL + FALLWEISE_SATZ, 450,
|
||||||
|
bezeichnung='Fallweise %s'
|
||||||
|
% version.employee_id.name)
|
||||||
|
satz.setze('SART', '32')
|
||||||
|
satz.setze('VSTR', vstr)
|
||||||
|
satz.setze('BKNR', bknr)
|
||||||
|
satz.setze('DGNA', company.name[:70])
|
||||||
|
satz.setze('VSNR', version.ssnid)
|
||||||
|
satz.setze_datum('GEBD', version.employee_id.birthday)
|
||||||
|
satz.setze('FANA', (version.private_last_name
|
||||||
|
or '')[:70])
|
||||||
|
satz.setze('VONA', (version.private_first_name
|
||||||
|
or '')[:70])
|
||||||
|
satz.setze('GESL', '2'
|
||||||
|
if version.sex == 'female' else '1')
|
||||||
|
satz.setze_datum('ADAT', adat)
|
||||||
|
kalender = version.resource_calendar_id
|
||||||
|
if kalender:
|
||||||
|
satz.setze('VTAZ', kalender.hours_per_day,
|
||||||
|
skala=2)
|
||||||
|
refn = 'MA-%d-%s-%d' % (
|
||||||
|
company.id, adat.strftime('%Y%m%d'),
|
||||||
|
version.employee_id.id)
|
||||||
|
satz.setze('REFN', refn)
|
||||||
|
datensaetze.append(satz)
|
||||||
|
ereignisse.append((version, 'MA', adat, refn))
|
||||||
|
else:
|
||||||
|
for version in lauf._l10n_at_offene_anmeldungen():
|
||||||
|
if not version.ssnid:
|
||||||
|
fehler.append(
|
||||||
|
'Anmeldung %s ohne Versicherungsnummer.'
|
||||||
|
% version.employee_id.name)
|
||||||
|
continue
|
||||||
|
satz = L10nAtEldaSatz(
|
||||||
|
L10N_AT_ELDA_IDTEIL + VR_SATZ, 772,
|
||||||
|
bezeichnung='M3 %s' % version.employee_id.name)
|
||||||
|
refw = 'M3-%d-%s-%d' % (
|
||||||
|
company.id,
|
||||||
|
version.contract_date_start.strftime('%Y%m%d'),
|
||||||
|
version.employee_id.id)
|
||||||
|
satz.setze('SART', 'M3')
|
||||||
|
satz.setze('VSTR', vstr)
|
||||||
|
satz.setze('REFW', refw)
|
||||||
|
satz.setze('BKNR', bknr)
|
||||||
|
satz.setze('DGNA', company.name[:70])
|
||||||
|
satz.setze('VSNR', version.ssnid)
|
||||||
|
satz.setze_datum(
|
||||||
|
'GEBD', version.employee_id.birthday)
|
||||||
|
familienname, vorname = l10n_at_namen(
|
||||||
|
version.employee_id)
|
||||||
|
satz.setze('FANA', familienname[:70])
|
||||||
|
satz.setze('VONA', (vorname or '')[:70])
|
||||||
|
satz.setze_datum('ADAT', version.contract_date_start)
|
||||||
|
satz.setze('BBER', lauf._l10n_at_bber(version))
|
||||||
|
satz.setze('GERF', 'J' if lauf
|
||||||
|
._l10n_at_geringfuegig(version) else 'N')
|
||||||
|
satz.setze('FRDV', 'N')
|
||||||
|
if getattr(version, 'l10n_at_abfertigung_neu',
|
||||||
|
False):
|
||||||
|
satz.setze('BVAB',
|
||||||
|
version.contract_date_start
|
||||||
|
.strftime('%d%m%Y'))
|
||||||
|
satz.setze('BVJN', 'J')
|
||||||
|
kalender = version.resource_calendar_id
|
||||||
|
if kalender:
|
||||||
|
satz.setze('VWAZ', kalender.hours_per_week,
|
||||||
|
skala=2)
|
||||||
|
datensaetze.append(satz)
|
||||||
|
ereignisse.append((version, 'M3',
|
||||||
|
version.contract_date_start,
|
||||||
|
refw))
|
||||||
|
for version in lauf._l10n_at_offene_abmeldungen():
|
||||||
|
if not version.ssnid:
|
||||||
|
fehler.append(
|
||||||
|
'Abmeldung %s ohne Versicherungsnummer.'
|
||||||
|
% version.employee_id.name)
|
||||||
|
continue
|
||||||
|
satz = L10nAtEldaSatz(
|
||||||
|
L10N_AT_ELDA_IDTEIL + VR_SATZ, 772,
|
||||||
|
bezeichnung='M4 %s' % version.employee_id.name)
|
||||||
|
refw = 'M4-%d-%s-%d' % (
|
||||||
|
company.id,
|
||||||
|
version.contract_date_end.strftime('%Y%m%d'),
|
||||||
|
version.employee_id.id)
|
||||||
|
agrd = getattr(
|
||||||
|
version.departure_reason_id, 'l10n_at_agrd',
|
||||||
|
False) or '00'
|
||||||
|
satz.setze('SART', 'M4')
|
||||||
|
satz.setze('VSTR', vstr)
|
||||||
|
satz.setze('REFW', refw)
|
||||||
|
satz.setze('BKNR', bknr)
|
||||||
|
satz.setze('DGNA', company.name[:70])
|
||||||
|
satz.setze('VSNR', version.ssnid)
|
||||||
|
satz.setze_datum(
|
||||||
|
'GEBD', version.employee_id.birthday)
|
||||||
|
familienname, vorname = l10n_at_namen(
|
||||||
|
version.employee_id)
|
||||||
|
satz.setze('FANA', familienname[:70])
|
||||||
|
satz.setze('VONA', (vorname or '')[:70])
|
||||||
|
satz.setze_datum('ADAT', version.contract_date_end)
|
||||||
|
satz.setze_datum('EBSV', version.contract_date_end)
|
||||||
|
satz.setze('AGRD', agrd)
|
||||||
|
datensaetze.append(satz)
|
||||||
|
ereignisse.append((version, 'M4',
|
||||||
|
version.contract_date_end,
|
||||||
|
refw))
|
||||||
|
if fehler:
|
||||||
|
lauf.protokoll = 'FEHLER:\n' + '\n'.join(fehler)
|
||||||
|
raise UserError('Versichertenmeldung mit Fehlern '
|
||||||
|
'abgebrochen:\n' + '\n'.join(fehler))
|
||||||
|
if not datensaetze:
|
||||||
|
raise UserError('Keine offenen Meldungen zum '
|
||||||
|
'Stichtag %s.' % lauf.stichtag)
|
||||||
|
bestand = L10nAtEldaBestand(
|
||||||
|
'MA' if lauf.nur_fallweise else 'VR', vstr, '07'
|
||||||
|
if lauf.nur_fallweise else '03',
|
||||||
|
hersteller={'name': 'odoo-at-payroll', 'kfz': 'A',
|
||||||
|
'plz': company.partner_id.zip,
|
||||||
|
'ort': company.partner_id.city,
|
||||||
|
'strasse': company.partner_id.street},
|
||||||
|
datensaetze=datensaetze)
|
||||||
|
datei = L10nAtEldaDatei([bestand])
|
||||||
|
ereignis_werte = []
|
||||||
|
for version, art, adatum, refw in ereignisse:
|
||||||
|
ereignis_werte.append({
|
||||||
|
'company_id': company.id,
|
||||||
|
'employee_id': version.employee_id.id,
|
||||||
|
'version_id': version.id,
|
||||||
|
'art': art,
|
||||||
|
'adatum': adatum,
|
||||||
|
'refw': refw,
|
||||||
|
})
|
||||||
|
ereignisse = self.env['l10n.at.payroll.vr.event'].create(
|
||||||
|
ereignis_werte) if ereignis_werte else \
|
||||||
|
self.env['l10n.at.payroll.vr.event']
|
||||||
|
lauf.write({
|
||||||
|
'datei': base64.b64encode(datei.render()),
|
||||||
|
'datei_name': 'versichertenmeldung_%s.txt' % (
|
||||||
|
fields.Date.today().strftime('%Y%m%d'),),
|
||||||
|
'event_ids': [(6, 0, ereignisse.ids)],
|
||||||
|
'state': 'done',
|
||||||
|
'protokoll': 'Meldungen: %d' % len(datensaetze),
|
||||||
|
})
|
||||||
|
return True
|
||||||
@@ -11,4 +11,9 @@ access_l10n_at_payroll_kv_import_wizard,L10n AT Payroll KV-Import Wizard,model_l
|
|||||||
access_l10n_at_payroll_kv_import_line,L10n AT Payroll KV-Import Line,model_l10n_at_payroll_kv_import_line,hr_payroll.group_hr_payroll_user,1,1,1,1
|
access_l10n_at_payroll_kv_import_line,L10n AT Payroll KV-Import Line,model_l10n_at_payroll_kv_import_line,hr_payroll.group_hr_payroll_user,1,1,1,1
|
||||||
access_l10n_at_bv_kasse_user,L10n AT BV-Kasse User,model_l10n_at_bv_kasse,hr_payroll.group_hr_payroll_user,1,0,0,0
|
access_l10n_at_bv_kasse_user,L10n AT BV-Kasse User,model_l10n_at_bv_kasse,hr_payroll.group_hr_payroll_user,1,0,0,0
|
||||||
access_l10n_at_bv_kasse_manager,L10n AT BV-Kasse Manager,model_l10n_at_bv_kasse,hr_payroll.group_hr_payroll_manager,1,1,1,1
|
access_l10n_at_bv_kasse_manager,L10n AT BV-Kasse Manager,model_l10n_at_bv_kasse,hr_payroll.group_hr_payroll_manager,1,1,1,1
|
||||||
access_l10n_at_bv_endabrechnung_wizard,L10n AT BV-Endabrechnung Wizard,model_l10n_at_bv_endabrechnung_wizard,hr_payroll.group_hr_payroll_user,1,1,1,1
|
access_l10n_at_bv_endabrechnung_wizard,L10n AT BV-Endabrechnung Wizard,model_l10n_at_bv_endabrechnung_wizard,hr_payroll.group_hr_payroll_user,1,1,1,1
|
||||||
|
access_l10n_at_payroll_mbgm_user,L10n AT Payroll mBGM User,model_l10n_at_payroll_mbgm,hr_payroll.group_hr_payroll_user,1,1,1,0
|
||||||
|
access_l10n_at_payroll_mbgm_manager,L10n AT Payroll mBGM Manager,model_l10n_at_payroll_mbgm,hr_payroll.group_hr_payroll_manager,1,1,1,1
|
||||||
|
access_l10n_at_payroll_mbgm_line_user,L10n AT Payroll mBGM-Line User,model_l10n_at_payroll_mbgm_line,hr_payroll.group_hr_payroll_user,1,1,1,1
|
||||||
|
access_l10n_at_payroll_vr_user,L10n AT Payroll VR User,model_l10n_at_payroll_vr,hr_payroll.group_hr_payroll_user,1,1,1,1
|
||||||
|
access_l10n_at_payroll_vr_event_user,L10n AT Payroll VR-Event User,model_l10n_at_payroll_vr_event,hr_payroll.group_hr_payroll_user,1,1,1,1
|
||||||
|
@@ -4,4 +4,5 @@ from . import test_entgelt_private
|
|||||||
from . import test_lst_private
|
from . import test_lst_private
|
||||||
from . import test_sv_private
|
from . import test_sv_private
|
||||||
from . import test_bv_private
|
from . import test_bv_private
|
||||||
from . import test_account_private
|
from . import test_account_private
|
||||||
|
from . import test_meldewesen_private
|
||||||
@@ -0,0 +1,183 @@
|
|||||||
|
# Part of the odoo-at-payroll project. License: LGPL-3.
|
||||||
|
"""GP7 privat: mBGM (Paket/mBGM/Tarifblock/Basis/Position-Hierarchie
|
||||||
|
aus GP4-Beitragssummen, T01-Gesamtsatz aus at_mbgm_tarif, Abschläge
|
||||||
|
A01/A02/A03 §-2a-AMPFG, A09/A10/A15, BV-Basis V01) und
|
||||||
|
Versichertenmeldung M3/M4 (deklarative Event-Erkennung,
|
||||||
|
Meldungs-Registry, AGRD)."""
|
||||||
|
import base64
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
from odoo.exceptions import UserError
|
||||||
|
from odoo.tests import TransactionCase, tagged
|
||||||
|
|
||||||
|
|
||||||
|
@tagged('post_install', '-at_install')
|
||||||
|
class MeldewesenPrivateTest(TransactionCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super().setUp()
|
||||||
|
self.company = self.env.company
|
||||||
|
self.company.write({
|
||||||
|
'l10n_at_beitragskontonummer': '1412345678',
|
||||||
|
'l10n_at_lohnsteuernummer': '123456789',
|
||||||
|
})
|
||||||
|
self.structure = self.env.ref(
|
||||||
|
'l10n_at_hr_payroll_private.structure_at_privat')
|
||||||
|
self.kv2203 = self.env.ref(
|
||||||
|
'l10n_at_hr_payroll_private.kv_si_2203')
|
||||||
|
self.gruppe_ii = self.env.ref(
|
||||||
|
'l10n_at_hr_payroll_private.kv2203_grp_ii')
|
||||||
|
self.calendar = self.env['resource.calendar'].create({
|
||||||
|
'name': 'AT Test 40h',
|
||||||
|
'tz': 'Europe/Vienna',
|
||||||
|
'hours_per_day': 8,
|
||||||
|
'attendance_ids': [
|
||||||
|
(0, 0, {'name': 'Tag %d' % wd, 'dayofweek': str(wd),
|
||||||
|
'hour_from': 8.0, 'hour_to': 16.0})
|
||||||
|
for wd in range(5)
|
||||||
|
],
|
||||||
|
})
|
||||||
|
self.company.resource_calendar_id = self.calendar
|
||||||
|
employee = self.env['hr.employee'].create({
|
||||||
|
'name': 'mBGM 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',
|
||||||
|
'kv_id': self.kv2203.id,
|
||||||
|
'kv_gruppe_id': self.gruppe_ii.id,
|
||||||
|
'erfahrungsstufe': 1,
|
||||||
|
'resource_calendar_id': self.calendar.id,
|
||||||
|
})
|
||||||
|
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': 'mBGM %d/%d' % (monat, jahr),
|
||||||
|
'employee_id': self.employee.id,
|
||||||
|
'struct_id': self.structure.id,
|
||||||
|
'date_from': erster,
|
||||||
|
'date_to': letzter,
|
||||||
|
})
|
||||||
|
slip.compute_sheet()
|
||||||
|
if confirm:
|
||||||
|
slip.action_payslip_done()
|
||||||
|
return slip
|
||||||
|
|
||||||
|
def _total(self, slip, code):
|
||||||
|
return sum(slip.line_ids.filtered(
|
||||||
|
lambda l: l.code == code).mapped('total'))
|
||||||
|
|
||||||
|
def test_01_mbgm_november(self):
|
||||||
|
"""November mit B13 (KV-Katalog 2.059,41): Paket PS mit
|
||||||
|
BZRM/GSUM/ANZM, mBGM G1 (VSNR, VSUM), Tarifblock T1 (BSGR
|
||||||
|
B002), Basen AB/SZ/BV mit Positionen T01 (39,30 % Gesamtsatz),
|
||||||
|
T02 (34,60 %), V01 (1,53 %) — Konsistenz gegen die
|
||||||
|
GP4-Beitragssummen."""
|
||||||
|
slip = self._slip(11)
|
||||||
|
deklaration = self.env['l10n.at.payroll.mbgm'].create({
|
||||||
|
'company_id': self.company.id,
|
||||||
|
'beitragsmonat': datetime.date(2026, 11, 1),
|
||||||
|
'art': 'MB',
|
||||||
|
})
|
||||||
|
deklaration.action_erzeugen()
|
||||||
|
self.assertEqual(deklaration.state, 'done')
|
||||||
|
self.assertEqual(len(deklaration.line_ids), 1)
|
||||||
|
self.assertEqual(deklaration.line_ids.bsgr, 'B002')
|
||||||
|
roh = base64.b64decode(deklaration.datei).decode('ISO-8859-15')
|
||||||
|
zeilen = roh.strip('\r\n').split('\r\n')
|
||||||
|
# Dateiheader + Vorlauf + PS + G1 + T1 + 5×(BS+V1)
|
||||||
|
# (T01, A03, T02, A03-SZ, V01 BV) + Schluss: 13 Daten-Sätze
|
||||||
|
# + 3 Rahmen-Zeilen = 16.
|
||||||
|
self.assertEqual(len(zeilen), 16)
|
||||||
|
vorlauf = zeilen[1]
|
||||||
|
self.assertEqual(vorlauf[0:2], '00')
|
||||||
|
self.assertEqual(vorlauf[18:20], '14')
|
||||||
|
self.assertEqual(vorlauf[22:24], 'MB')
|
||||||
|
daten = zeilen[2:-1]
|
||||||
|
paket = daten[0]
|
||||||
|
self.assertEqual(paket[0:2], 'PS')
|
||||||
|
self.assertEqual(paket[60:70], '1412345678') # BKNR
|
||||||
|
self.assertEqual(paket[281:287], '112026') # BZRM MMJJJJ
|
||||||
|
self.assertEqual(paket[299:305], '000001') # ANZM
|
||||||
|
gsum = int(paket[288:299])
|
||||||
|
ist = (slip._l10n_at_svdn_gesamt()
|
||||||
|
+ slip._l10n_at_svdg_gesamt()
|
||||||
|
+ slip._l10n_at_svdn_sz()
|
||||||
|
+ sum(slip._l10n_at_svdg_sz(k) for k in
|
||||||
|
('kv', 'pv', 'av', 'uv', 'iesg'))
|
||||||
|
+ slip._at_bv_beitrag())
|
||||||
|
self.assertLessEqual(abs(gsum / 100.0 - ist), 0.10)
|
||||||
|
satzarten = [zeile[0:2] for zeile in daten]
|
||||||
|
self.assertEqual(satzarten, ['PS', 'G1', 'T1', 'BS',
|
||||||
|
'V1', 'BS', 'V1', 'BS', 'V1',
|
||||||
|
'BS', 'V1', 'BS', 'V1'])
|
||||||
|
g1 = daten[1]
|
||||||
|
self.assertEqual(g1[140:150], '1234150590')
|
||||||
|
t1 = daten[2]
|
||||||
|
self.assertEqual(t1[20:24], 'B002')
|
||||||
|
v1_t01 = daten[4]
|
||||||
|
self.assertEqual(v1_t01[20:23], 'T01')
|
||||||
|
self.assertEqual(v1_t01[24:30], '039050') # 39,05 %
|
||||||
|
# §-2a-AMPFG-Abschlag A03 (BG 2.059,41 ≤ 2.225 → AV 0 %),
|
||||||
|
# auf AB und SZ je als eigene Position.
|
||||||
|
v1_a03 = daten[6]
|
||||||
|
self.assertEqual(v1_a03[20:23], 'A03')
|
||||||
|
self.assertEqual(v1_a03[30:31], '-') # RSVZ negativ
|
||||||
|
v1_t02 = daten[8]
|
||||||
|
self.assertEqual(v1_t02[20:23], 'T02')
|
||||||
|
self.assertEqual(v1_t02[24:30], '037550') # 37,55 %
|
||||||
|
v1_a03_sz = daten[10]
|
||||||
|
self.assertEqual(v1_a03_sz[20:23], 'A03')
|
||||||
|
|
||||||
|
def test_02_vr_an_abmeldung(self):
|
||||||
|
"""M3 (vor Arbeitsantritt) mit BBER/GERF/BVJN/VWAZ und M4
|
||||||
|
(7-Tage-Frist) mit AGRD aus dem Abgangsgrund-Mapping; die
|
||||||
|
Registry verhindert Doppelanmeldungen."""
|
||||||
|
lauf = self.env['l10n.at.payroll.vr'].create({
|
||||||
|
'company_id': self.company.id,
|
||||||
|
'stichtag': datetime.date(2026, 1, 31),
|
||||||
|
})
|
||||||
|
lauf.action_erzeugen()
|
||||||
|
roh = base64.b64decode(lauf.datei).decode('ISO-8859-15')
|
||||||
|
zeilen = roh.strip('\r\n').split('\r\n')
|
||||||
|
m3 = [z for z in zeilen if z[0:2] == 'M3']
|
||||||
|
self.assertEqual(len(m3), 1)
|
||||||
|
self.assertEqual(m3[0][100:110], '1412345678') # BKNR
|
||||||
|
self.assertEqual(m3[0][314:324], '1234150590') # VSNR
|
||||||
|
self.assertEqual(m3[0][324:332], '15051990') # GEBD TTMMJJJJ
|
||||||
|
self.assertEqual(m3[0][512:520], '01012020') # ADAT
|
||||||
|
self.assertEqual(m3[0][536:538], '02') # BBER Angestellte
|
||||||
|
self.assertEqual(m3[0][538:539], 'N') # GERF
|
||||||
|
self.assertEqual(m3[0][539:540], 'N') # FRDV
|
||||||
|
self.assertEqual(m3[0][768:772], '4000') # VWAZ 40 h
|
||||||
|
self.assertEqual(m3[0][618:619], 'J') # BVJN (GP5)
|
||||||
|
# Registry verhindert Doppelanmeldung; Abmeldung nach Ende.
|
||||||
|
zweiter = self.env['l10n.at.payroll.vr'].create({
|
||||||
|
'company_id': self.company.id,
|
||||||
|
'stichtag': datetime.date(2026, 2, 28),
|
||||||
|
})
|
||||||
|
with self.assertRaises(UserError):
|
||||||
|
zweiter.action_erzeugen()
|
||||||
|
self.version.write({
|
||||||
|
'contract_date_end': datetime.date(2026, 2, 15),
|
||||||
|
'departure_reason_id': self.env['hr.departure.reason']
|
||||||
|
.search([], limit=1).id,
|
||||||
|
})
|
||||||
|
dritter = self.env['l10n.at.payroll.vr'].create({
|
||||||
|
'company_id': self.company.id,
|
||||||
|
'stichtag': datetime.date(2026, 3, 31),
|
||||||
|
})
|
||||||
|
dritter.action_erzeugen()
|
||||||
|
roh = base64.b64decode(dritter.datei).decode('ISO-8859-15')
|
||||||
|
m4 = [z for z in roh.strip('\r\n').split('\r\n')
|
||||||
|
if z[0:2] == 'M4']
|
||||||
|
self.assertEqual(len(m4), 1)
|
||||||
|
self.assertEqual(m4[0][512:520], '15022026') # ADAT
|
||||||
|
self.assertEqual(m4[0][548:550], '00') # AGRD Default
|
||||||
@@ -0,0 +1,147 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<!-- ============================================================ -->
|
||||||
|
<!-- GP7 Meldewesen (privat): mBGM + Versichertenmeldung. -->
|
||||||
|
<!-- ============================================================ -->
|
||||||
|
|
||||||
|
<record id="mbgm_list_view" model="ir.ui.view">
|
||||||
|
<field name="name">l10n.at.payroll.mbgm.list</field>
|
||||||
|
<field name="model">l10n.at.payroll.mbgm</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<list string="mBGM (JASV)">
|
||||||
|
<field name="beitragsmonat"/>
|
||||||
|
<field name="art"/>
|
||||||
|
<field name="state" widget="badge"
|
||||||
|
decoration-success="state == 'done'"
|
||||||
|
decoration-info="state == 'draft'"/>
|
||||||
|
</list>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="mbgm_form_view" model="ir.ui.view">
|
||||||
|
<field name="name">l10n.at.payroll.mbgm.form</field>
|
||||||
|
<field name="model">l10n.at.payroll.mbgm</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<form string="Monatliche Beitragsgrundlagenmeldung (mBGM)">
|
||||||
|
<header>
|
||||||
|
<button name="action_erzeugen" type="object"
|
||||||
|
string="Lauf erzeugen (Datei)"
|
||||||
|
class="btn-primary"
|
||||||
|
invisible="state == 'done'"/>
|
||||||
|
<field name="state" widget="statusbar"
|
||||||
|
statusbar_visible="draft,done"/>
|
||||||
|
</header>
|
||||||
|
<sheet>
|
||||||
|
<group>
|
||||||
|
<group>
|
||||||
|
<field name="beitragsmonat"/>
|
||||||
|
<field name="art"/>
|
||||||
|
<field name="storno_id"
|
||||||
|
invisible="art != 'ST'"/>
|
||||||
|
</group>
|
||||||
|
<group>
|
||||||
|
<field name="company_id"
|
||||||
|
groups="base.group_multi_company"/>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
<notebook>
|
||||||
|
<page string="mBGM-Sätze" name="linien">
|
||||||
|
<field name="line_ids">
|
||||||
|
<list editable="bottom">
|
||||||
|
<field name="employee_name"/>
|
||||||
|
<field name="versicherungsnummer"/>
|
||||||
|
<field name="bsgr"/>
|
||||||
|
<field name="refw"/>
|
||||||
|
<field name="vsum"/>
|
||||||
|
</list>
|
||||||
|
</field>
|
||||||
|
</page>
|
||||||
|
<page string="Datei & Protokoll" name="datei">
|
||||||
|
<group>
|
||||||
|
<field name="datei"
|
||||||
|
filename="datei_name"/>
|
||||||
|
<field name="datei_name" invisible="1"/>
|
||||||
|
</group>
|
||||||
|
<field name="protokoll"/>
|
||||||
|
</page>
|
||||||
|
</notebook>
|
||||||
|
</sheet>
|
||||||
|
</form>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="action_mbgm" model="ir.actions.act_window">
|
||||||
|
<field name="name">mBGM (JASV)</field>
|
||||||
|
<field name="res_model">l10n.at.payroll.mbgm</field>
|
||||||
|
<field name="view_mode">list,form</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="vr_form_view" model="ir.ui.view">
|
||||||
|
<field name="name">l10n.at.payroll.vr.form</field>
|
||||||
|
<field name="model">l10n.at.payroll.vr</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<form string="Versichertenmeldung (An-/Abmeldung)">
|
||||||
|
<header>
|
||||||
|
<button name="action_erzeugen" type="object"
|
||||||
|
string="Meldungen erzeugen (Datei)"
|
||||||
|
class="btn-primary"
|
||||||
|
invisible="state == 'done'"/>
|
||||||
|
<field name="state" widget="statusbar"
|
||||||
|
statusbar_visible="draft,done"/>
|
||||||
|
</header>
|
||||||
|
<sheet>
|
||||||
|
<group>
|
||||||
|
<group>
|
||||||
|
<field name="stichtag"/>
|
||||||
|
<field name="nur_fallweise"/>
|
||||||
|
</group>
|
||||||
|
<group>
|
||||||
|
<field name="company_id"
|
||||||
|
groups="base.group_multi_company"/>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
<notebook>
|
||||||
|
<page string="Datei & Protokoll" name="datei">
|
||||||
|
<group>
|
||||||
|
<field name="datei"
|
||||||
|
filename="datei_name"/>
|
||||||
|
<field name="datei_name" invisible="1"/>
|
||||||
|
</group>
|
||||||
|
<field name="protokoll"/>
|
||||||
|
<field name="event_ids">
|
||||||
|
<list>
|
||||||
|
<field name="employee_id"/>
|
||||||
|
<field name="art"/>
|
||||||
|
<field name="adatum"/>
|
||||||
|
<field name="refw"/>
|
||||||
|
</list>
|
||||||
|
</field>
|
||||||
|
</page>
|
||||||
|
</notebook>
|
||||||
|
</sheet>
|
||||||
|
</form>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="action_vr" model="ir.actions.act_window">
|
||||||
|
<field name="name">Versichertenmeldung (An-/Abmeldung)</field>
|
||||||
|
<field name="res_model">l10n.at.payroll.vr</field>
|
||||||
|
<field name="view_mode">form</field>
|
||||||
|
<field name="target">new</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<menuitem id="menu_l10n_at_payroll_private_meldewesen"
|
||||||
|
name="Meldewesen (ELDA/eSV)"
|
||||||
|
parent="l10n_at_hr_payroll_private.menu_l10n_at_payroll_private_root"
|
||||||
|
sequence="70"/>
|
||||||
|
|
||||||
|
<menuitem id="menu_l10n_at_payroll_private_mbgm"
|
||||||
|
name="mBGM (JASV)"
|
||||||
|
parent="menu_l10n_at_payroll_private_meldewesen"
|
||||||
|
action="action_mbgm" sequence="10"/>
|
||||||
|
|
||||||
|
<menuitem id="menu_l10n_at_payroll_private_vr"
|
||||||
|
name="Versichertenmeldung"
|
||||||
|
parent="menu_l10n_at_payroll_private_meldewesen"
|
||||||
|
action="action_vr" sequence="20"/>
|
||||||
|
</odoo>
|
||||||
Reference in New Issue
Block a user