[IMP] l10n_at_hr_payroll_private: Arbeits- und Entgeltbestätigungen BEST AK/AW — SART 70/71 Kranken-/75/76 Wochengeld (AP8)
Satzstrukturen PDF-verifiziert aus DM-ORG 42.7.0 (ODL, Pages 493-505; Positionsketten lückenlos): G.10 AEB-Krankengeld v12 (SART 70/71, BEST AK, 111 Felder, Satzlänge 1392) und G.11 AEB-Wochengeld v10 (SART 75/76, BEST AW, 64 Felder, 1048). Die .txt-Extraktion des DM-ORG ist unzuverlässig (Feldversatz). - Neues Modell l10n.at.payroll.aeb (models/aeb.py): Episode (verhinderung_von/bis), KZKU, Storno über storno_id (E.10.2.1: nur Fehler/BMGL-Änderung), REFN/Datei/Prüfprotokoll; Erzeugung auf dem ELDA-Bestands-Framework (Vorlauf/Schluss/Satzlängen- selbstkontrolle) - EFZ-Refaktur: Phasen-Karte nach hr.version verschoben (_l10n_at_efz_arbeitsjahr_start/_l10n_at_efz_phasen/ _l10n_at_efz_stufe_am) — Payslip-Status (AP4) und AEB teilen die Maschine; _at_efz_status-Verhalten durch Tests gepinnt - Fachdaten Krankengeld: AEFZ (Anspruch in Wochen), JAGU=1 (Arbeitsjahr), TAGU=2 (Kalendertage), Teilentgelt TPR/TVO/TBI je Phase, Vorerkrankungen ANV/ANB vor der Episode, VENT (Voll-Phase-Ende), Bezugsblöcke BVO/BBI/BBE + BSUM aus den vollen Monatsläufen vor der Episode, SZKZ datengetrieben - Fachdaten Wochengeld: AVER (Ø-Netto der drei Referenzmonate, § 162 Abs 3 ASVG — AP7-Logik), AVON/ABIS, ANGV=K (kein Fortbezug), AGRD-Validierung nach D.22 (SART 75/76 nur 02/03/04/05/06/10/22/24; unzulässige geleert + Protokoll) - Fund: L10nAtEldaSatz.setze(None) übernimmt die Grundstellung nicht — Felder nur per Leerstring aktiv leeren - 4 Tests: SART 70 mit 76-Tage-Episode (AEFZ 06, TPR1 50, TVO1 13.10.2026, VENT 12.10.2026, Bezugsblock), SART 75 mit AVER/AVON/ABIS, Storno 76 (REFN -ST, Kerndaten des Originals), AGRD-Validierung
This commit is contained in:
@@ -2,6 +2,60 @@
|
||||
|
||||
## Current focus
|
||||
|
||||
**AP 8 Arbeits-/Entgeltbestätigungen umgesetzt — 152/152 Tests
|
||||
grün — damit ist der AP0–AP10-Nutzer-Katalog komplett** (2026-09-12,
|
||||
AEB-Session; siehe auch Parallel-Session-Notiz unten):
|
||||
|
||||
- **Satzstrukturen PDF-verifiziert** (DM-ORG 42.7.0 lokal, ODL
|
||||
Pages 493-505): **G.10** AEB-Krankengeld v12 (SART 70/71, BEST
|
||||
AK, 111 Felder, Satzlänge 1392), **G.11** AEB-Wochengeld v10
|
||||
(SART 75/76, BEST AW, 64 Felder, 1048) — Positionsketten
|
||||
lückenlos geprüft; die .txt-Extraktion des DM-ORG ist
|
||||
UNZUVERLÄSSIG (Feldversatz) — PDF-Extraktion verbindlich.
|
||||
- **Model** `l10n.at.payroll.aeb` (models/aeb.py): art, Episode
|
||||
(verhinderung_von/bis), KZKU, Storno via storno_id (E.10.2.1:
|
||||
nur Fehler/BMGL-Änderung), REFN/Datei/Prüfprotokoll; ELDA-
|
||||
Bestände AK/AW. Fachdaten: Ident/Wohnort (version.private_*),
|
||||
Bank (primary_bank_account_id), EFZ-Anspruch AEFZ/JAGU=1/TAGU=2,
|
||||
Teilentgelt TPR/TVO/TBI, Vorerkrankungen ANV/ANB, VENT,
|
||||
Bezugsblöcke BVO/BBI/BBE+BSUM; Wochengeld: AVER (Ø-Netto
|
||||
3 Referenzmonate § 162 Abs 3), AVON/ABIS, ANGV=K,
|
||||
**AGRD-Validierung D.22** (SART 75/76 nur 02/03/04/05/06/10/22/24
|
||||
— unzulässige geleert + Protokoll).
|
||||
- **EFZ-Refaktur:** Phasen-Karte nach **hr.version** verschoben
|
||||
(`_l10n_at_efz_arbeitsjahr_start`/`_l10n_at_efz_phasen`/
|
||||
`_l10n_at_efz_stufe_am`) — Payslip-Status und AEB teilen die
|
||||
Maschine; AP4-Tests unverändert grün. Fund: `setze(None)`
|
||||
übernimmt Grundstellung NICHT — löschen nur über Leerstring.
|
||||
- 4 Tests (SART 70 mit 76-Tage-Episode: AEFZ 06, TPR1 50,
|
||||
TVO1 13.10., VENT 12.10.; SART 75 mit AVER; Storno 76;
|
||||
AGRD-Validierung). Version **19.0.9.4.0**.
|
||||
- **Offen (MVP-Reste):** Prüfkatalog-Verifikation (Parallellauf,
|
||||
P7), KE/UE-Zeilen, EIN/APFT-Feiertage, VOAB/VOBI, Prämien-/
|
||||
Provisionsausweis (seit-2025-Felder). **Abstimmung mit der
|
||||
Parallel-Session: PLAN-Restluecken AP11+ unabhängig startbar,
|
||||
AEB nicht mehr als Restlücke.**
|
||||
|
||||
**Gap-Reevaluierung + Restlücken-Plan AP11 ff. freigegeben und
|
||||
versioniert** (2026-09-12): Cluster-Abgleich Wissensbasis (601
|
||||
Einträge/69 Cluster) gegen den AP0–AP10-Stand neu aufgezogen —
|
||||
die Vorphase hat die große Gap-Liste weitgehend geschlossen
|
||||
(EFZG AP4, Wochengeld AP7, Wr. DAG AP3, BSGR/ERGB AP0, Reports
|
||||
AP2, Abwesenheits-WETs AP5, VWGJ AP9, GemBG-Fixes AP10).
|
||||
Verblieben: Pfändung, Sachbezüge, Reisekosten/Pendler, End-
|
||||
abrechnungs-Zyklus (Urlaubsersatzleistung/Vergleich/Aufrechnung),
|
||||
Abfertigung Alt, Karenz-Reste, L17/§ 109a, Schwerarbeitsmeldung,
|
||||
Status-/Verhinderungs-Sonderfälle; Optionen ATZ/Kurzarbeit/
|
||||
Beteiligung/Entsendung. Plan **freigegeben (Nutzer)** und persistiert:
|
||||
`personalverrechnung/PLAN-Restluecken-AP11.md` — Pakete AP11
|
||||
Pfändung · AP12 Sachbezüge · AP13 Reise/Pendler · AP14 End-
|
||||
abrechnung · AP15 Abf Alt · AP16 Karenz-Reste (Scope-Gate 8.13) ·
|
||||
AP17 Meldewesen-Reste · AP18 Schwerarbeit (Beschaffungsgate) ·
|
||||
AP19 Status-Sonderfälle; Σ AP11–AP19 ≈ 90–142 PT; je Paket RIS-
|
||||
Gate vor Bau (Werte nur aus Quelle). **AP8 (AEB) läuft parallel in
|
||||
eigener Session** — Plan führt es nur als Restabgleich; keine
|
||||
Doppelumsetzung, Paket-Starts mit der Parallel-Session abstimmen.
|
||||
|
||||
**AP 7 Mutterschutz/Wochengeld umgesetzt — 148/148 Tests grün**
|
||||
(2026-09-12):
|
||||
|
||||
@@ -1446,6 +1500,12 @@ B17-Inhalt/B18; Kleinverifikationen s. KB-KONFLIKTE „Nächste Schritte“).
|
||||
|
||||
## Files that matter right now
|
||||
|
||||
- `personalverrechnung/PLAN-Restluecken-AP11.md` — **freigegebener**
|
||||
Ausbauplan Restlücken Wissensbasis↔Modul (AP11 Pfändung, AP12
|
||||
Sachbezüge, AP13 Reise/Pendler, AP14 Endabrechnung, AP15 Abf Alt,
|
||||
AP16 Karenz-Reste, AP17 L17/§ 109a, AP18 Schwerarbeit, AP19
|
||||
Status-Sonderfälle, AP20+ Optionen; AP8 parallel; Reihenfolge,
|
||||
Gates, PT-Summen; Status je Paket nachziehen)
|
||||
- `.firecrawl/ris/` — Primärquellen-Downloads (19 RIS-Gesetzestexte,
|
||||
LStR-2002-XML, DM-ORG 42.7.0 + Prüfkataloge + XSDs, SV-Werte 2026;
|
||||
Verzeichnis `_README.md` mit GesNr und Kurztitel-Fallen; lokal,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Part of the odoo-at-payroll project. License: LGPL-3.
|
||||
{
|
||||
'name': 'Österreich - Privatwirtschaft Personalverrechnung (General-AT)',
|
||||
'version': '19.0.9.3.0',
|
||||
'version': '19.0.9.4.0',
|
||||
'author': 'Florian Egger',
|
||||
'website': 'https://gem360.at',
|
||||
'category': 'Human Resources/Payroll',
|
||||
@@ -110,6 +110,7 @@ gem360-Brücke).
|
||||
'data/salary_rules_msch_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/rule_parameters_mbgm_private.xml',
|
||||
'data/rule_parameters_wien_private.xml',
|
||||
@@ -121,6 +122,7 @@ gem360-Brücke).
|
||||
'views/meldewesen_views.xml',
|
||||
'views/vertragsfelder_views.xml',
|
||||
'views/mitarbeiter_views.xml',
|
||||
'views/aeb_views.xml',
|
||||
'report/gp8_reports.xml',
|
||||
'report/buchungsbeleg_report.xml',
|
||||
'report/bv_report.xml',
|
||||
|
||||
@@ -13,4 +13,5 @@ from . import fristen_private
|
||||
from . import buchungsbeleg
|
||||
from . import wien_dag
|
||||
from . import efzg
|
||||
from . import mutterschutz
|
||||
from . import mutterschutz
|
||||
from . import aeb
|
||||
@@ -0,0 +1,561 @@
|
||||
# Part of the odoo-at-payroll project. License: LGPL-3.
|
||||
"""AP8: Arbeits- und Entgeltbestätigungen (AEB) — ELDA-Bestände
|
||||
``AK`` (Krankengeld, SART 70/71, Version 12) und ``AW`` (Wochengeld,
|
||||
SART 75/76, Version 10), DM-ORG 42.7.0 Kapitel E.10/E.11 + G.10/G.11
|
||||
(Satzstrukturen PDF-verifiziert aus der lokalen DM-ORG-Extraktion
|
||||
2026-09-12; Positionsketten lückenlos geprüft: Krankengeld 111 Felder
|
||||
bis Stelle 1392, Wochengeld 64 Felder bis Stelle 1048).
|
||||
|
||||
Fachliche Rollen (lb-krs-02 / lb-msf-03): Krankengeld und Wochengeld
|
||||
zahlt die KV; die AEB ist die Bemessungsdaten-Lieferung des
|
||||
Dienstgebers nach Ausschöpfung des vollen EFZ-Anspruchs (Krankengeld)
|
||||
bzw. bei Mutterschutz-Frist (Wochengeld; AGRD dort nur 02/03/04/05/06/
|
||||
10/22/24, D.22). Storno (E.10.2.1) nur bei Fehler oder Änderung der
|
||||
Bemessungsgrundlage — sonst Korrektur per Neumeldung.
|
||||
|
||||
Datenquellen: EFZ-Phasen-Karte (``hr.version._l10n_at_efz_phasen``,
|
||||
AP4/AP8-Refaktur) für Anspruch (AEFZ/JAGU/TAGU), Teilentgelt-Phasen
|
||||
(TPR/TVO/TBI), Vorerkrankungen (ANV/ANB) und volles-Entgelt-Ende
|
||||
(VENT); Payslip-Historie für die Bezugsblöcke (BVO/BBI/BBE) bzw. den
|
||||
Wochengeld-Arbeitsverdienst (AVER = Ø-Netto der drei Referenzmonate,
|
||||
§ 162 Abs 3 ASVG — AP7-Logik).
|
||||
|
||||
Grundstellung ohne Modell (dokumentierter MVP-Rest): Kündigungs-
|
||||
entschädigung/Urlaubsersatzleistung, Einarbeitungs-/Feiertagsfelder,
|
||||
Vordienstzeiten (§ 2 Abs 3 EFZG), Prämien-/Provisionsausweis —
|
||||
Prüfkatalog-Verifikation im Parallellauf (Prämisse P7)."""
|
||||
import base64
|
||||
from datetime import timedelta
|
||||
|
||||
from odoo import 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
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# G.10 Arbeits- und Entgeltbestätigung für Krankengeld – Version 12
|
||||
# (SART 70/71) — nach IDTEIL, Satzlänge 1392.
|
||||
# ------------------------------------------------------------------
|
||||
AEB_KRANKENGELD_SATZ = [
|
||||
(2, 'BKNR', 21, 10, 'a'),
|
||||
(3, 'DGNA', 31, 70, 'a'),
|
||||
(4, 'DTEL', 101, 50, 'a'),
|
||||
(5, 'WOBD', 151, 12, 'a'),
|
||||
(6, 'ZOBD', 163, 15, 'a'),
|
||||
(7, 'VSNR', 178, 10, 'n'),
|
||||
(8, 'GEBD', 188, 8, 'n'),
|
||||
(9, 'FANA', 196, 70, 'a'),
|
||||
(10, 'VONA', 266, 70, 'a'),
|
||||
(11, 'AKGR', 336, 30, 'a'),
|
||||
(12, 'WFKZ', 366, 3, 'a'),
|
||||
(13, 'PLZL', 369, 9, 'a'),
|
||||
(14, 'WORT', 378, 40, 'a'),
|
||||
(15, 'STRA', 418, 50, 'a'),
|
||||
(16, 'BEAB', 468, 8, 'n'),
|
||||
(17, 'KABE', 476, 1, 'n'),
|
||||
(18, 'TAET', 477, 30, 'a'),
|
||||
(19, 'BTAG', 507, 1, 'n'),
|
||||
(20, 'TATU', 508, 1, 'a'),
|
||||
(21, 'LTAG', 509, 8, 'n'),
|
||||
(22, 'GRUN', 517, 30, 'a'),
|
||||
(23, 'KZKU', 547, 1, 'a'),
|
||||
(24, 'ARFT', 548, 7, 'a'),
|
||||
(25, 'ARTX', 555, 25, 'a'),
|
||||
(26, 'BLOE', 580, 1, 'a'),
|
||||
(27, 'EBSV', 581, 8, 'n'),
|
||||
(28, 'IBAN', 589, 34, 'a'),
|
||||
(29, 'BIC', 623, 11, 'a'),
|
||||
(30, 'BVO1', 634, 8, 'n'),
|
||||
(31, 'BBI1', 642, 8, 'n'),
|
||||
(32, 'BBE1', 650, 9, 'n'),
|
||||
(33, 'BVO2', 659, 8, 'n'),
|
||||
(34, 'BBI2', 667, 8, 'n'),
|
||||
(35, 'BBE2', 675, 9, 'n'),
|
||||
(36, 'BVO3', 684, 8, 'n'),
|
||||
(37, 'BBI3', 692, 8, 'n'),
|
||||
(38, 'BBE3', 700, 9, 'n'),
|
||||
(39, 'BVO4', 709, 8, 'n'),
|
||||
(40, 'BBI4', 717, 8, 'n'),
|
||||
(41, 'BBE4', 725, 9, 'n'),
|
||||
(42, 'BSUM', 734, 9, 'n'),
|
||||
(43, 'SZKZ', 743, 1, 'a'),
|
||||
(44, 'SBGB', 744, 1, 'a'),
|
||||
(45, 'SBZT', 745, 24, 'a'),
|
||||
(46, 'KEAB', 769, 8, 'n'),
|
||||
(47, 'KEBI', 777, 8, 'n'),
|
||||
(48, 'UEAB', 785, 8, 'n'),
|
||||
(49, 'UEBI', 793, 8, 'n'),
|
||||
(50, 'VENT', 801, 8, 'n'),
|
||||
(51, 'AEFZ', 809, 2, 'n'),
|
||||
(52, 'JAGU', 811, 1, 'n'),
|
||||
(53, 'TAGU', 812, 1, 'n'),
|
||||
(54, 'TPR1', 813, 2, 'n'),
|
||||
(55, 'TVO1', 815, 8, 'n'),
|
||||
(56, 'TBI1', 823, 8, 'n'),
|
||||
(57, 'TPR2', 831, 2, 'n'),
|
||||
(58, 'TVO2', 833, 8, 'n'),
|
||||
(59, 'TBI2', 841, 8, 'n'),
|
||||
(60, 'ANV1', 849, 8, 'n'),
|
||||
(61, 'ANB1', 857, 8, 'n'),
|
||||
(62, 'ANV2', 865, 8, 'n'),
|
||||
(63, 'ANB2', 873, 8, 'n'),
|
||||
(64, 'ANV3', 881, 8, 'n'),
|
||||
(65, 'ANB3', 889, 8, 'n'),
|
||||
(66, 'ANV4', 897, 8, 'n'),
|
||||
(67, 'ANB4', 905, 8, 'n'),
|
||||
(68, 'ANV5', 913, 8, 'n'),
|
||||
(69, 'ANB5', 921, 8, 'n'),
|
||||
(70, 'ANV6', 929, 8, 'n'),
|
||||
(71, 'ANB6', 937, 8, 'n'),
|
||||
(72, 'FAN2', 945, 70, 'a'),
|
||||
(73, 'VON2', 1015, 70, 'a'),
|
||||
(74, 'AKG2', 1085, 30, 'a'),
|
||||
(75, 'AGRD', 1115, 2, 'a'),
|
||||
(76, 'VOAB', 1117, 8, 'n'),
|
||||
(77, 'VOBI', 1125, 8, 'n'),
|
||||
(78, 'FEAB', 1133, 8, 'n'),
|
||||
(79, 'FEBI', 1141, 8, 'n'),
|
||||
(80, 'EIN1', 1149, 8, 'n'),
|
||||
(81, 'EIN2', 1157, 8, 'n'),
|
||||
(82, 'EIN3', 1165, 8, 'n'),
|
||||
(83, 'EIN4', 1173, 8, 'n'),
|
||||
(84, 'EIN5', 1181, 8, 'n'),
|
||||
(85, 'EIN6', 1189, 8, 'n'),
|
||||
(86, 'REFN', 1197, 30, 'a'),
|
||||
(87, 'APFT1', 1227, 8, 'n'),
|
||||
(88, 'APFT2', 1235, 8, 'n'),
|
||||
(89, 'APFT3', 1243, 8, 'n'),
|
||||
(90, 'APFT4', 1251, 8, 'n'),
|
||||
(91, 'APFT5', 1259, 8, 'n'),
|
||||
(92, 'APFT6', 1267, 8, 'n'),
|
||||
(93, 'APFT7', 1275, 8, 'n'),
|
||||
(94, 'APFT8', 1283, 8, 'n'),
|
||||
(95, 'APFT9', 1291, 8, 'n'),
|
||||
(96, 'APFT10', 1299, 8, 'n'),
|
||||
(97, 'ANV7', 1307, 8, 'n'),
|
||||
(98, 'ANB7', 1315, 8, 'n'),
|
||||
(99, 'ANV8', 1323, 8, 'n'),
|
||||
(100, 'ANB8', 1331, 8, 'n'),
|
||||
(101, 'ANV9', 1339, 8, 'n'),
|
||||
(102, 'ANB9', 1347, 8, 'n'),
|
||||
(103, 'ANV10', 1355, 8, 'n'),
|
||||
(104, 'ANB10', 1363, 8, 'n'),
|
||||
(105, 'GEGC', 1371, 2, 'a'),
|
||||
(106, 'GEGT', 1373, 8, 'a'),
|
||||
(107, 'ARLO', 1381, 1, 'a'),
|
||||
(108, 'SZUM', 1382, 1, 'a'),
|
||||
(109, 'SBUM', 1383, 1, 'a'),
|
||||
(110, 'SBKZ', 1384, 1, 'a'),
|
||||
(111, 'ALBV', 1385, 8, 'n'),
|
||||
]
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# G.11 Arbeits- und Entgeltbestätigung für Wochengeld – Version 10
|
||||
# (SART 75/76) — nach IDTEIL, Satzlänge 1048.
|
||||
# ------------------------------------------------------------------
|
||||
AEB_WOCHENGELD_SATZ = [
|
||||
(2, 'BKNR', 21, 10, 'a'),
|
||||
(3, 'DGNA', 31, 70, 'a'),
|
||||
(4, 'DTEL', 101, 50, 'a'),
|
||||
(5, 'WOBD', 151, 12, 'a'),
|
||||
(6, 'ZOBD', 163, 15, 'a'),
|
||||
(7, 'VSNR', 178, 10, 'n'),
|
||||
(8, 'GEBD', 188, 8, 'n'),
|
||||
(9, 'FANA', 196, 70, 'a'),
|
||||
(10, 'VONA', 266, 70, 'a'),
|
||||
(11, 'AKGR', 336, 30, 'a'),
|
||||
(12, 'WKFZ', 366, 3, 'a'),
|
||||
(13, 'PLZL', 369, 9, 'a'),
|
||||
(14, 'WORT', 378, 40, 'a'),
|
||||
(15, 'STRA', 418, 50, 'a'),
|
||||
(16, 'BEAB', 468, 8, 'n'),
|
||||
(17, 'KABE', 476, 1, 'n'),
|
||||
(18, 'TAET', 477, 30, 'a'),
|
||||
(19, 'BTAG', 507, 1, 'n'),
|
||||
(20, 'TATU', 508, 1, 'a'),
|
||||
(21, 'LTAG', 509, 8, 'n'),
|
||||
(22, 'GRUN', 517, 30, 'a'),
|
||||
(23, 'BLOE', 547, 1, 'a'),
|
||||
(24, 'AGRD', 548, 2, 'a'),
|
||||
(25, 'EBSV', 550, 8, 'n'),
|
||||
(26, 'RESE', 558, 8, 'a'),
|
||||
(27, 'PRAB', 566, 8, 'n'),
|
||||
(28, 'URLV', 574, 8, 'n'),
|
||||
(29, 'URLB', 582, 8, 'n'),
|
||||
(30, 'IBAN', 590, 34, 'a'),
|
||||
(31, 'BIC', 624, 11, 'a'),
|
||||
(32, 'AVER', 635, 9, 'n'),
|
||||
(33, 'AVON', 644, 8, 'n'),
|
||||
(34, 'ABIS', 652, 8, 'n'),
|
||||
(35, 'SBZT', 660, 24, 'a'),
|
||||
(36, 'UNV1', 684, 8, 'n'),
|
||||
(37, 'UNB1', 692, 8, 'n'),
|
||||
(38, 'UNV2', 700, 8, 'n'),
|
||||
(39, 'UNB2', 708, 8, 'n'),
|
||||
(40, 'UNV3', 716, 8, 'n'),
|
||||
(41, 'UNB3', 724, 8, 'n'),
|
||||
(42, 'UNV4', 732, 8, 'n'),
|
||||
(43, 'UNB4', 740, 8, 'n'),
|
||||
(44, 'SZKZ', 748, 1, 'a'),
|
||||
(45, 'ANGV', 749, 1, 'a'),
|
||||
(46, 'AHEB', 750, 8, 'n'),
|
||||
(47, 'AMEB', 758, 8, 'n'),
|
||||
(48, 'FAN2', 766, 70, 'a'),
|
||||
(49, 'VON2', 836, 70, 'a'),
|
||||
(50, 'AKG2', 906, 30, 'a'),
|
||||
(51, 'SZMO', 936, 4, 'n'),
|
||||
(52, 'SZWO', 940, 4, 'n'),
|
||||
(53, 'SBZW', 944, 1, 'a'),
|
||||
(54, 'SBZE', 945, 1, 'a'),
|
||||
(55, 'UNV5', 946, 8, 'n'),
|
||||
(56, 'UNB5', 954, 8, 'n'),
|
||||
(57, 'UNV6', 962, 8, 'n'),
|
||||
(58, 'UNB6', 970, 8, 'n'),
|
||||
(59, 'UNV7', 978, 8, 'n'),
|
||||
(60, 'UNB7', 986, 8, 'n'),
|
||||
(61, 'UNV8', 994, 8, 'n'),
|
||||
(62, 'UNB8', 1002, 8, 'n'),
|
||||
(63, 'AVER1', 1010, 9, 'n'),
|
||||
(64, 'REFN', 1019, 30, 'a'),
|
||||
]
|
||||
|
||||
# SART 75/76: zulaessige Abmeldegruende (DM-ORG D.22).
|
||||
_WOCHENGELD_AGRD = ('02', '03', '04', '05', '06', '10', '22', '24')
|
||||
|
||||
|
||||
def _monats_laenge(monat):
|
||||
"""Letzter Kalendertag des Monats."""
|
||||
naechster = (monat.replace(day=28) + timedelta(days=4)).replace(
|
||||
day=1)
|
||||
return (naechster - timedelta(days=1)).day
|
||||
|
||||
|
||||
class L10nAtPayrollAeb(models.Model):
|
||||
_name = 'l10n.at.payroll.aeb'
|
||||
_description = 'Arbeits- und Entgeltbestätigung (Krankengeld/Wochengeld)'
|
||||
_inherit = ['mail.thread']
|
||||
_order = 'id desc'
|
||||
|
||||
company_id = fields.Many2one(
|
||||
'res.company', required=True,
|
||||
default=lambda self: self.env.company, index=True)
|
||||
art = fields.Selection(
|
||||
[('krankengeld', 'Krankengeld (BEST AK, SART 70/71)'),
|
||||
('wochengeld', 'Wochengeld (BEST AW, SART 75/76)')],
|
||||
required=True, tracking=True)
|
||||
employee_id = fields.Many2one('hr.employee', required=True)
|
||||
kzku = fields.Selection(
|
||||
[('1', 'Krankheit/Unglücksfall'),
|
||||
('2', 'Arbeitsunfall/Berufskrankheit')],
|
||||
string='Kennzeichen (KZKU)', default='1')
|
||||
verhinderung_von = fields.Date(
|
||||
string='Verhinderung ab (Episode)', required=True,
|
||||
help='Beginn der zusammenhängenden Arbeitsunfähigkeit '
|
||||
'(Krankengeld) bzw. der Mutterschutz-Frist (Wochengeld) — '
|
||||
'Bezugszeitraum der Bestätigung.')
|
||||
verhinderung_bis = fields.Date(
|
||||
string='Verhinderung bis (bekannt)',
|
||||
help='Letzter bekannter Verhinderungstag; leer = laufend.')
|
||||
storno_id = fields.Many2one(
|
||||
'l10n.at.payroll.aeb', string='Stornierte Bestätigung',
|
||||
help='Storno (SART 71/76) — nur bei Fehler oder Änderung der '
|
||||
'Bemessungsgrundlage (E.10.2.1); sonst Korrektur per '
|
||||
'Neumeldung (SART 70/75).')
|
||||
refn = fields.Char(string='Referenznummer (REFN)', readonly=True,
|
||||
copy=False)
|
||||
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)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Datenaufbereitung
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _aeb_version(self):
|
||||
return self.employee_id.version_id
|
||||
|
||||
def _aeb_referenz_slips(self, bis, monate):
|
||||
"""Die bis zu ``monate`` jüngsten vollständigen validierten
|
||||
Monatsläufe vor ``bis`` (älteste zuerst)."""
|
||||
self.ensure_one()
|
||||
slips = self.env['hr.payslip'].search([
|
||||
('employee_id', '=', self.employee_id.id),
|
||||
('state', 'in', ('validated', 'paid')),
|
||||
('date_to', '<', bis),
|
||||
], order='date_from desc', limit=monate).filtered(
|
||||
lambda s: s.date_from.day == 1
|
||||
and s.date_to.day == _monats_laenge(s.date_from))
|
||||
return slips.sorted('date_from')
|
||||
|
||||
def _aeb_monats_brutto(self, slip):
|
||||
"""Monats-Bezug (Brutto inkl. SZ-Anteil) eines Laufs."""
|
||||
return slip._l10n_at_brutto('laufend') \
|
||||
+ slip._l10n_at_brutto('sonstig')
|
||||
|
||||
def _aeb_szkz(self):
|
||||
"""SZKZ: Anspruch auf Sonderzahlung — datengetrieben aus den
|
||||
sonstigen (§-67-)Bezügen irgendeines validierten Laufs des
|
||||
AN (B13/14-Struktur des Referenz-KVs; die reinen
|
||||
Referenzmonate können sz-frei sein)."""
|
||||
self.ensure_one()
|
||||
slips = self.env['hr.payslip'].search([
|
||||
('employee_id', '=', self.employee_id.id),
|
||||
('state', 'in', ('validated', 'paid')),
|
||||
], limit=24)
|
||||
return 'J' if any(s._l10n_at_brutto('sonstig') for s in slips) \
|
||||
else 'N'
|
||||
|
||||
def _aeb_wochenplan(self):
|
||||
"""ARFT: 'A' je Arbeitstag, 'F' sonst (Montag–Sonntag)."""
|
||||
self.ensure_one()
|
||||
kalender = self._aeb_version().resource_calendar_id
|
||||
tage = {int(att.dayofweek)
|
||||
for att in kalender.attendance_ids} \
|
||||
if kalender else {0, 1, 2, 3, 4}
|
||||
return ''.join('A' if d in tage else 'F' for d in range(7))
|
||||
|
||||
def _aeb_ident_setzen(self, satz, wfkz_feld):
|
||||
"""Identifikations- und Beschäftigungsfelder beider
|
||||
Satzarten. ``wfkz_feld``: 'WFKZ' (Krankengeld) bzw. 'WKFZ'
|
||||
(Wochengeld) — die Feldnamen weichen ab (G.10/G.11)."""
|
||||
self.ensure_one()
|
||||
version = self._aeb_version()
|
||||
company = self.company_id
|
||||
employee = self.employee_id
|
||||
familienname, vorname = l10n_at_namen(employee)
|
||||
satz.setze('BKNR', company.l10n_at_beitragskontonummer)
|
||||
satz.setze('DGNA', (company.name or '')[:70])
|
||||
satz.setze('DTEL', (company.partner_id.phone or '')[:50])
|
||||
satz.setze('VSNR', version.ssnid)
|
||||
satz.setze_datum('GEBD', employee.birthday)
|
||||
satz.setze('FANA', familienname[:70])
|
||||
satz.setze('VONA', (vorname or '')[:70])
|
||||
satz.setze_datum('BEAB', version.contract_date_start)
|
||||
satz.setze('KABE', '4' if version.l10n_at_lehrling else (
|
||||
'1' if getattr(version, 'l10n_at_personalgruppe', '')
|
||||
== 'arbeiter' else '2'))
|
||||
satz.setze('TAET', (employee.job_id.name or '')[:30])
|
||||
plan = self._aeb_wochenplan()
|
||||
satz.setze('BTAG', str(plan.count('A')))
|
||||
satz.setze('TATU', str(min(plan.count('A'), 6)))
|
||||
# Wohnort (D.12): A = Österreich
|
||||
satz.setze(wfkz_feld, 'A')
|
||||
satz.setze('PLZL', (version.private_zip or '')[:9])
|
||||
satz.setze('WORT', (version.private_city or '')[:40])
|
||||
satz.setze('STRA', (version.private_street or '')[:50])
|
||||
if version.contract_date_end:
|
||||
satz.setze('BLOE', 'G')
|
||||
satz.setze_datum('EBSV', version.contract_date_end)
|
||||
agrd = getattr(version.departure_reason_id, 'l10n_at_agrd',
|
||||
False)
|
||||
if agrd:
|
||||
satz.setze('AGRD', agrd)
|
||||
satz.setze_datum('LTAG', version.contract_date_end)
|
||||
else:
|
||||
satz.setze('BLOE', 'N')
|
||||
bank = employee.primary_bank_account_id
|
||||
if bank:
|
||||
satz.setze('IBAN', (bank.acc_number or '')[:34])
|
||||
if bank.bank_id:
|
||||
satz.setze('BIC', (bank.bank_id.bic or '')[:11])
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Fachdaten
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _aeb_fuelle_krankengeld(self, satz, fehler):
|
||||
"""SART 70: EFZ-Anspruch (AEFZ/JAGU/TAGU), Teilentgelt-Phase,
|
||||
Vorerkrankungen, Bezugsblöcke, VENT (Phasen-Karte AP4)."""
|
||||
self.ensure_one()
|
||||
version = self._aeb_version()
|
||||
episode_von = self.verhinderung_von
|
||||
aj_start = version._l10n_at_efz_arbeitsjahr_start(episode_von)
|
||||
if not aj_start:
|
||||
fehler.append('Kein Vertragsbeginn — EFZ-Daten leer.')
|
||||
return
|
||||
endtag = self.verhinderung_bis or fields.Date.context_today(self)
|
||||
kontext = version._l10n_at_efz_phasen(
|
||||
aj_start, max(endtag, episode_von))
|
||||
phasen = kontext['phasen']
|
||||
satz.setze('KZKU', self.kzku or '1')
|
||||
satz.setze('ARFT', self._aeb_wochenplan())
|
||||
wochen = version._l10n_at_efz_stufe_am(episode_von)
|
||||
if wochen:
|
||||
satz.setze('AEFZ', '%02d' % wochen)
|
||||
satz.setze('JAGU', '1') # Arbeitsjahr (lb-krs-08)
|
||||
satz.setze('TAGU', '2') # Kalendertage
|
||||
satz.setze('SBKZ', 'N') # kein Sachbezug modelliert
|
||||
|
||||
def bloecke(phase=None, filter_ab=None):
|
||||
"""Zusammenhängende Kalendertag-Blöcke einer Phase."""
|
||||
ergebnis = []
|
||||
vorher = None
|
||||
for tag in sorted(phasen):
|
||||
if filter_ab is not None and tag >= filter_ab:
|
||||
break
|
||||
if phase is not None and phasen[tag] != phase:
|
||||
vorher = None
|
||||
continue
|
||||
if vorher is not None and (tag - vorher).days == 1:
|
||||
ergebnis[-1][1] = tag
|
||||
else:
|
||||
ergebnis.append([tag, tag])
|
||||
vorher = tag
|
||||
return ergebnis
|
||||
|
||||
# VENT: Ende der zusammenhängenden Voll-Phase ab Episode.
|
||||
voll = bloecke(phase='voll')
|
||||
voll_der_episode = [b for b in voll if b[0] >= episode_von]
|
||||
if voll_der_episode:
|
||||
satz.setze_datum('VENT', voll_der_episode[0][1])
|
||||
# Teilentgelt: bis zu zwei Halb-Phasen (TPR/TVO/TBI).
|
||||
for index, (von, bis) in enumerate(bloecke(phase='halb')[:2]):
|
||||
satz.setze('TPR%d' % (index + 1), 50)
|
||||
satz.setze_datum('TVO%d' % (index + 1), von)
|
||||
satz.setze_datum('TBI%d' % (index + 1), bis)
|
||||
# Vorerkrankungen (ANV/ANB): Blöcke vor der Episode
|
||||
# (vorangehende Arbeitsunfähigkeiten, E.10.2.7).
|
||||
for index, (von, bis) in enumerate(bloecke(filter_ab=episode_von)[:10]):
|
||||
satz.setze_datum('ANV%d' % (index + 1), von)
|
||||
satz.setze_datum('ANB%d' % (index + 1), bis)
|
||||
# Bezugsblöcke: bis zu 4 volle Monatsläufe vor Episode.
|
||||
slips = self._aeb_referenz_slips(episode_von, 4)[:4]
|
||||
summe = 0.0
|
||||
for index, slip in enumerate(slips):
|
||||
betrag = self._aeb_monats_brutto(slip)
|
||||
summe += betrag
|
||||
satz.setze_datum('BVO%d' % (index + 1), slip.date_from)
|
||||
satz.setze_datum('BBI%d' % (index + 1), slip.date_to)
|
||||
satz.setze('BBE%d' % (index + 1), betrag, cent=True)
|
||||
if summe:
|
||||
satz.setze('BSUM', summe, cent=True)
|
||||
satz.setze('SZKZ', self._aeb_szkz())
|
||||
|
||||
def _aeb_fuelle_wochengeld(self, satz, fehler):
|
||||
"""SART 75: Arbeitsverdienst netto der letzten drei
|
||||
Kalendermonate vor der Frist (§ 162 Abs 3 ASVG — AP7-Logik),
|
||||
Fortbezug, AGRD-Kontrolle (D.22)."""
|
||||
self.ensure_one()
|
||||
von = self.verhinderung_von
|
||||
referenz = self._aeb_referenz_slips(von, 3)
|
||||
if referenz:
|
||||
netto = sum(sum(s.line_ids.filtered(
|
||||
lambda l: l.code == 'NET').mapped('total'))
|
||||
for s in referenz)
|
||||
satz.setze('AVER', netto, cent=True)
|
||||
satz.setze_datum('AVON', referenz[0].date_from)
|
||||
satz.setze_datum('ABIS', referenz[-1].date_to)
|
||||
else:
|
||||
fehler.append(
|
||||
'Keine vollständigen Referenzläufe für den '
|
||||
'Arbeitsverdienst (§ 162 Abs 3 ASVG).')
|
||||
satz.setze('SZKZ', self._aeb_szkz())
|
||||
satz.setze('ANGV', 'K') # kein Fortbezug des Entgelts (AP7)
|
||||
agrd = satz.wert('AGRD')
|
||||
if agrd and agrd not in _WOCHENGELD_AGRD:
|
||||
fehler.append(
|
||||
'Abmeldegrund %s ist für SART 75/76 unzulässig '
|
||||
'(D.22: %s) — Feld geleert.' % (
|
||||
agrd, ', '.join(_WOCHENGELD_AGRD)))
|
||||
# setze(None) übernähme die Grundstellung nicht — Feld
|
||||
# aktiv mit Blank leeren.
|
||||
satz.setze('AGRD', '')
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Dateierzeugung
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def action_erzeugen(self):
|
||||
import datetime as datetime_modul
|
||||
for aeb in self:
|
||||
fehler = []
|
||||
company = aeb.company_id
|
||||
bknr = company.l10n_at_beitragskontonummer
|
||||
if not bknr:
|
||||
raise UserError('Beitragskontonummer (BKNR) fehlt.')
|
||||
version = aeb._aeb_version()
|
||||
if not version.ssnid:
|
||||
raise UserError(
|
||||
'Mitarbeiter %s ohne Versicherungsnummer.'
|
||||
% aeb.employee_id.name)
|
||||
storno = bool(aeb.storno_id)
|
||||
if aeb.art == 'krankengeld':
|
||||
satzart = '71' if storno else '70'
|
||||
struktur = L10N_AT_ELDA_IDTEIL + AEB_KRANKENGELD_SATZ
|
||||
laenge, bestand, vers = 1392, 'AK', '12'
|
||||
praefix, wfkz_feld = 'AEBK', 'WFKZ'
|
||||
else:
|
||||
satzart = '76' if storno else '75'
|
||||
struktur = L10N_AT_ELDA_IDTEIL + AEB_WOCHENGELD_SATZ
|
||||
laenge, bestand, vers = 1048, 'AW', '10'
|
||||
praefix, wfkz_feld = 'AEBW', 'WKFZ'
|
||||
satz = L10nAtEldaSatz(
|
||||
struktur, laenge,
|
||||
bezeichnung='AEB %s' % aeb.employee_id.name)
|
||||
satz.setze('SART', satzart)
|
||||
stichtag = aeb.verhinderung_bis \
|
||||
or datetime_modul.date.today()
|
||||
aeb._aeb_ident_setzen(satz, wfkz_feld)
|
||||
if storno:
|
||||
# Storno (E.10.2.1): Kerndaten der stornierten
|
||||
# Bestätigung übernehmen, REFN unten neu setzen.
|
||||
original = aeb.storno_id
|
||||
satz.setze('VSNR',
|
||||
original._aeb_version().ssnid)
|
||||
satz.setze_datum('GEBD', original.employee_id.birthday)
|
||||
familienname, vorname = l10n_at_namen(
|
||||
original.employee_id)
|
||||
satz.setze('FANA', familienname[:70])
|
||||
satz.setze('VONA', (vorname or '')[:70])
|
||||
satz.setze_datum('BEAB',
|
||||
original._aeb_version()
|
||||
.contract_date_start)
|
||||
elif aeb.art == 'krankengeld':
|
||||
aeb._aeb_fuelle_krankengeld(satz, fehler)
|
||||
else:
|
||||
aeb._aeb_fuelle_wochengeld(satz, fehler)
|
||||
if not aeb.refn:
|
||||
aeb.refn = '%s-%d-%s-%d%s' % (
|
||||
praefix, company.id,
|
||||
aeb.verhinderung_von.strftime('%Y%m%d'),
|
||||
aeb.employee_id.id, '-ST' if storno else '')
|
||||
satz.setze('REFN', aeb.refn[:30])
|
||||
vstr = (bknr or '')[:2]
|
||||
bestands_objekt = L10nAtEldaBestand(
|
||||
bestand, vstr, vers,
|
||||
hersteller={'name': 'odoo-at-payroll', 'kfz': 'A',
|
||||
'plz': company.partner_id.zip,
|
||||
'ort': company.partner_id.city,
|
||||
'strasse': company.partner_id.street},
|
||||
datensaetze=[satz])
|
||||
datei = L10nAtEldaDatei([bestands_objekt])
|
||||
roh = datei.render()
|
||||
# Selbstkontrolle: Datensatz-Satzlaenge (nach Header,
|
||||
# Vorlauf; vor Schluss).
|
||||
datensaetze = roh.decode('ISO-8859-15') \
|
||||
.strip('\r\n').split('\r\n')
|
||||
if len(datensaetze[2]) != laenge:
|
||||
fehler.append(
|
||||
'Satzlängenkontrolle: %d != %d'
|
||||
% (len(datensaetze[2]), laenge))
|
||||
aeb.write({
|
||||
'datei': base64.b64encode(roh).decode(),
|
||||
'datei_name': '%s_%s.txt' % (
|
||||
praefix.lower(),
|
||||
aeb.verhinderung_von.strftime('%Y%m%d')),
|
||||
'protokoll': 'WARNUNGEN:\n%s' % (
|
||||
'\n'.join(fehler) or '—'),
|
||||
'state': 'done',
|
||||
})
|
||||
return True
|
||||
@@ -41,6 +41,135 @@ _KRANK_WET = 'ATP_KRANK'
|
||||
_AU_WET = 'ATP_AU'
|
||||
|
||||
|
||||
class HrVersion(models.Model):
|
||||
_inherit = 'hr.version'
|
||||
|
||||
def _l10n_at_efz_arbeitsjahr_start(self, stichtag):
|
||||
"""Beginn des Arbeitsjahres, das den Stichtag enthält
|
||||
(Eintrittsdatum-Rhythmus, lb-krs-08). None ohne
|
||||
Vertragsbeginn."""
|
||||
self.ensure_one()
|
||||
beginn = self.contract_date_start
|
||||
if not beginn:
|
||||
return None
|
||||
jahre = beginn.year
|
||||
while True:
|
||||
aj_start = date(jahre, beginn.month, beginn.day) \
|
||||
if not (beginn.month == 2 and beginn.day == 29) \
|
||||
else date(jahre, 3, 1)
|
||||
aj_naechstes = date(jahre + 1, aj_start.month, aj_start.day)
|
||||
if aj_start <= stichtag < aj_naechstes:
|
||||
return aj_start
|
||||
jahre += 1
|
||||
if jahre > beginn.year + 80:
|
||||
return None
|
||||
|
||||
def _l10n_at_efz_phasen(self, aj_start, endtag, stichtag=None):
|
||||
"""Komplette EFZ-Phasen-Karte des Arbeitsjahres über alle
|
||||
Krank-/AU-Kalendertage bis ``endtag``: {datum: 'voll'/'halb'/
|
||||
'oegk'} plus Kontext-Parameter — gemeinsame Basis der
|
||||
Payslip-Auswertung (AP4) und der Arbeits- und
|
||||
Entgeltbestätigung (AP8).
|
||||
|
||||
Stufen je Tag (Stufensprung verlängert, lb-krs-08),
|
||||
Krank-/AU-Topf getrennt (AU ohne Halbsatz, § 8 Abs 2a AngG /
|
||||
§ 2 Abs 5 EFZG)."""
|
||||
self.ensure_one()
|
||||
stichtag = stichtag or fields.Date.context_today(self)
|
||||
params = self.env['hr.rule.parameter']._get_parameter_from_code(
|
||||
'at_efz_stufen', stichtag, raise_if_not_found=False) or {}
|
||||
stufen = sorted(
|
||||
(int(jahre), int(wochen))
|
||||
for jahre, wochen in params.get('stufen', []))
|
||||
halb_wochen = int(params.get('halb_wochen', 4))
|
||||
au_wochen = int(params.get('au_wochen', 8))
|
||||
au2_wochen = int(params.get('au2_wochen', 10))
|
||||
au_ab_jahre = int(params.get('au_ab_jahre', 15))
|
||||
typen = self.env['hr.leave.type'].search([
|
||||
('work_entry_type_id.code', 'in', (_KRANK_WET, _AU_WET))])
|
||||
leaves = self.env['hr.leave'].search([
|
||||
('employee_id', '=', self.employee_id.id),
|
||||
('holiday_status_id', 'in', typen.ids),
|
||||
('state', 'in', ('validate', 'validate1')),
|
||||
])
|
||||
krank_tage, au_tage = {}, {}
|
||||
for leave in leaves:
|
||||
if not (leave.request_date_from and leave.request_date_to):
|
||||
continue
|
||||
tag = leave.request_date_from
|
||||
while tag <= leave.request_date_to:
|
||||
if aj_start <= tag <= endtag:
|
||||
topf = au_tage if leave.holiday_status_id \
|
||||
.work_entry_type_id.code == _AU_WET else krank_tage
|
||||
topf[tag] = True
|
||||
tag += timedelta(days=1)
|
||||
beginn = self.contract_date_start
|
||||
|
||||
def stufe_fuer(dienstjahre, tag_ist_au):
|
||||
wochen = stufen[0][1] if stufen else 6
|
||||
for jahre, stufe in stufen:
|
||||
if dienstjahre >= jahre:
|
||||
wochen = stufe
|
||||
if tag_ist_au:
|
||||
wochen = au2_wochen if dienstjahre >= au_ab_jahre \
|
||||
else au_wochen
|
||||
return wochen
|
||||
|
||||
phasen = {}
|
||||
verbraucht_krank_voll = verbraucht_krank_halb = 0
|
||||
verbraucht_au = 0
|
||||
for tag in sorted(set(krank_tage) | set(au_tage)):
|
||||
dienstjahre = (tag.year - beginn.year
|
||||
- ((tag.month, tag.day) < (beginn.month,
|
||||
beginn.day)))
|
||||
if tag in au_tage:
|
||||
if verbraucht_au < stufe_fuer(dienstjahre, True) * 7:
|
||||
phasen[tag] = 'voll'
|
||||
else:
|
||||
phasen[tag] = 'oegk'
|
||||
verbraucht_au += 1
|
||||
continue
|
||||
wochen = stufe_fuer(dienstjahre, False)
|
||||
if verbraucht_krank_voll < wochen * 7:
|
||||
phasen[tag] = 'voll'
|
||||
verbraucht_krank_voll += 1
|
||||
elif verbraucht_krank_halb < halb_wochen * 7:
|
||||
phasen[tag] = 'halb'
|
||||
verbraucht_krank_halb += 1
|
||||
else:
|
||||
phasen[tag] = 'oegk'
|
||||
return {
|
||||
'phasen': phasen,
|
||||
'stufen': stufen,
|
||||
'halb_wochen': halb_wochen,
|
||||
'au_wochen': au_wochen,
|
||||
'au2_wochen': au2_wochen,
|
||||
'au_ab_jahre': au_ab_jahre,
|
||||
}
|
||||
|
||||
def _l10n_at_efz_stufe_am(self, stichtag, ist_au=False):
|
||||
"""EFZ-Stufen-Wochen am Stichtag (vollendete Dienstjahre)."""
|
||||
self.ensure_one()
|
||||
aj_start = self._l10n_at_efz_arbeitsjahr_start(stichtag)
|
||||
if not aj_start:
|
||||
return None
|
||||
kontext = self._l10n_at_efz_phasen(aj_start, stichtag)
|
||||
beginn = self.contract_date_start
|
||||
dienstjahre = (stichtag.year - beginn.year
|
||||
- ((stichtag.month, stichtag.day)
|
||||
< (beginn.month, beginn.day)))
|
||||
stufen = kontext['stufen']
|
||||
wochen = stufen[0][1] if stufen else 6
|
||||
for jahre, stufe in stufen:
|
||||
if dienstjahre >= jahre:
|
||||
wochen = stufe
|
||||
if ist_au:
|
||||
wochen = kontext['au2_wochen'] \
|
||||
if dienstjahre >= kontext['au_ab_jahre'] \
|
||||
else kontext['au_wochen']
|
||||
return wochen
|
||||
|
||||
|
||||
class HrPayslip(models.Model):
|
||||
_inherit = 'hr.payslip'
|
||||
|
||||
@@ -65,121 +194,31 @@ class HrPayslip(models.Model):
|
||||
len(efz['oegk_arbeitstage'])))
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# EFZ-Phasenbestimmung
|
||||
# EFZ-Phasenbestimmung (Phasen-Karte: hr.version, AP8-Refaktur)
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _at_efz_leaves(self):
|
||||
"""Validierte Krankenstand-/AU-Abwesenheiten des AN als
|
||||
(start, ende, ist_au)-Kalenderspannen."""
|
||||
self.ensure_one()
|
||||
typen = self.env['hr.leave.type'].search([
|
||||
('work_entry_type_id.code', 'in',
|
||||
(_KRANK_WET, _AU_WET))])
|
||||
leaves = self.env['hr.leave'].search([
|
||||
('employee_id', '=', self.employee_id.id),
|
||||
('holiday_status_id', 'in', typen.ids),
|
||||
('state', 'in', ('validate', 'validate1')),
|
||||
])
|
||||
return [
|
||||
(leave.request_date_from, leave.request_date_to,
|
||||
leave.holiday_status_id.work_entry_type_id.code == _AU_WET)
|
||||
for leave in leaves
|
||||
if leave.request_date_from and leave.request_date_to]
|
||||
|
||||
def _at_efz_arbeitsjahr_start(self):
|
||||
"""Beginn des Arbeitsjahres, das die Abrechnungsperiode
|
||||
überwiegend enthält (Eintrittsdatum-Rhythmus, lb-krs-08)."""
|
||||
self.ensure_one()
|
||||
beginn = self.version_id.contract_date_start
|
||||
if not beginn:
|
||||
return None
|
||||
jahre = beginn.year
|
||||
while True:
|
||||
aj_start = date(jahre, beginn.month, beginn.day) \
|
||||
if not (beginn.month == 2 and beginn.day == 29) \
|
||||
else date(jahre, 3, 1)
|
||||
aj_naechstes = date(jahre + 1, aj_start.month, aj_start.day)
|
||||
if aj_start <= self.date_from < aj_naechstes:
|
||||
return aj_start
|
||||
jahre += 1
|
||||
if jahre > beginn.year + 80:
|
||||
return None
|
||||
|
||||
def _at_efz_status(self):
|
||||
"""EFZ-Phasen je Abrechnungsperiode: {'stufe_wochen',
|
||||
'halb_wochen', 'voll_arbeitstage', 'halb_arbeitstage',
|
||||
'oegk_arbeitstage', 'oegk_kalendertage', 'episode_beginn',
|
||||
'info'} — None ohne Krankenstand/AU im Arbeitsjahr.
|
||||
'oegk_arbeitstage', 'oegk_kalendertage', 'episode_beginn'}
|
||||
— None ohne Krankenstand/AU im Arbeitsjahr.
|
||||
|
||||
Kalendertage-Maschine: Die Krankheits- und AU-Tage des
|
||||
Arbeitsjahres werden in Kalenderreihenfolge den Phasen
|
||||
zugeteilt (voll bis Stufen-Kontingent, halb zuzüglich
|
||||
4 Wochen, danach ÖGK); AU ausschließlich voll (kein
|
||||
Halbsatz, § 8 Abs 2a AngG / § 2 Abs 5 EFZG). Ein
|
||||
Stufensprung im laufenden Stand verlängert das Kontingent
|
||||
(Stufe je Tag neu bewertet, lb-krs-08)."""
|
||||
Die Phasen-Karte (Kalendertage, Stufen je Tag, Krank-/AU-Topf
|
||||
getrennt, § 8 Abs 2a AngG / § 2 Abs 5 EFZG) liefert
|
||||
``hr.version._l10n_at_efz_phasen`` — gemeinsame Basis mit der
|
||||
Arbeits- und Entgeltbestätigung (AP8)."""
|
||||
self.ensure_one()
|
||||
version = self.version_id
|
||||
aj_start = self._at_efz_arbeitsjahr_start()
|
||||
aj_start = version._l10n_at_efz_arbeitsjahr_start(self.date_from)
|
||||
if not aj_start:
|
||||
return None
|
||||
endtag = max(self.date_to, aj_start)
|
||||
krank_tage, au_tage = {}, {}
|
||||
episode_start = None
|
||||
for von, bis, ist_au in self._at_efz_leaves():
|
||||
tag = von
|
||||
while tag <= bis:
|
||||
if aj_start <= tag <= endtag:
|
||||
topf = au_tage if ist_au else krank_tage
|
||||
topf[tag] = True
|
||||
tag += timedelta(days=1)
|
||||
if not krank_tage and not au_tage:
|
||||
kontext = version._l10n_at_efz_phasen(aj_start, endtag,
|
||||
stichtag=self.date_from)
|
||||
phasen = kontext['phasen']
|
||||
if not phasen:
|
||||
return None
|
||||
params = self.env['hr.rule.parameter']._get_parameter_from_code(
|
||||
'at_efz_stufen', self.date_from, raise_if_not_found=False) or {}
|
||||
stufen = sorted(
|
||||
(int(jahre), int(wochen))
|
||||
for jahre, wochen in params.get('stufen', []))
|
||||
halb_wochen = int(params.get('halb_wochen', 4))
|
||||
au_wochen = int(params.get('au_wochen', 8))
|
||||
au2_wochen = int(params.get('au2_wochen', 10))
|
||||
au_ab_jahre = int(params.get('au_ab_jahre', 15))
|
||||
|
||||
def stufe_fuer(dienstjahre, tag_ist_au):
|
||||
"""Anspruchs-Wochen am Stichtag (vollendete Jahre)."""
|
||||
wochen = stufen[0][1] if stufen else 6
|
||||
for jahre, stufe in stufen:
|
||||
if dienstjahre >= jahre:
|
||||
wochen = stufe
|
||||
if tag_ist_au:
|
||||
wochen = au2_wochen if dienstjahre >= au_ab_jahre \
|
||||
else au_wochen
|
||||
return wochen
|
||||
|
||||
beginn = version.contract_date_start
|
||||
phasen = {}
|
||||
verbraucht_krank_voll = verbraucht_krank_halb = 0
|
||||
verbraucht_au = 0
|
||||
for tag in sorted(set(krank_tage) | set(au_tage)):
|
||||
dienstjahre = (tag.year - beginn.year
|
||||
- ((tag.month, tag.day) < (beginn.month,
|
||||
beginn.day)))
|
||||
if tag in au_tage:
|
||||
if verbraucht_au < stufe_fuer(dienstjahre, True) * 7:
|
||||
phasen[tag] = 'voll'
|
||||
else:
|
||||
phasen[tag] = 'oegk'
|
||||
verbraucht_au += 1
|
||||
continue
|
||||
wochen = stufe_fuer(dienstjahre, False)
|
||||
if verbraucht_krank_voll < wochen * 7:
|
||||
phasen[tag] = 'voll'
|
||||
verbraucht_krank_voll += 1
|
||||
elif verbraucht_krank_halb < halb_wochen * 7:
|
||||
phasen[tag] = 'halb'
|
||||
verbraucht_krank_halb += 1
|
||||
else:
|
||||
phasen[tag] = 'oegk'
|
||||
halb_wochen = kontext['halb_wochen']
|
||||
# Arbeitstage je Phase im Abrechnungszeitraum (Work Entries)
|
||||
eintraege = self.env['hr.work.entry'].search([
|
||||
('employee_id', '=', self.employee_id.id),
|
||||
@@ -208,11 +247,8 @@ class HrPayslip(models.Model):
|
||||
block = block - timedelta(days=1)
|
||||
episode_beginn = block
|
||||
break
|
||||
dienstjahre = (self.date_from.year - beginn.year
|
||||
- ((self.date_from.month, self.date_from.day)
|
||||
< (beginn.month, beginn.day)))
|
||||
return {
|
||||
'stufe_wochen': stufe_fuer(dienstjahre, False),
|
||||
'stufe_wochen': version._l10n_at_efz_stufe_am(self.date_from),
|
||||
'halb_wochen': halb_wochen,
|
||||
'voll_arbeitstage': sorted(set(voll)),
|
||||
'halb_arbeitstage': sorted(set(halb)),
|
||||
|
||||
@@ -16,4 +16,6 @@ access_l10n_at_payroll_mbgm_user,L10n AT Payroll mBGM User,model_l10n_at_payroll
|
||||
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
|
||||
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
|
||||
access_l10n_at_payroll_aeb_user,L10n AT Payroll AEB User,model_l10n_at_payroll_aeb,hr_payroll.group_hr_payroll_user,1,1,1,0
|
||||
access_l10n_at_payroll_aeb_manager,L10n AT Payroll AEB Manager,model_l10n_at_payroll_aeb,hr_payroll.group_hr_payroll_manager,1,1,1,1
|
||||
|
||||
|
@@ -11,3 +11,4 @@ from . import test_wien_dag_private
|
||||
from . import test_einstufung_private
|
||||
from . import test_efzg_private
|
||||
from . import test_mutterschutz_private
|
||||
from . import test_aeb_private
|
||||
|
||||
@@ -0,0 +1,233 @@
|
||||
# Part of the odoo-at-payroll project. License: LGPL-3.
|
||||
"""AP8: Arbeits- und Entgeltbestätigungen — BEST AK (Krankengeld,
|
||||
SART 70/71, Version 12) und BEST AW (Wochengeld, SART 75/76,
|
||||
Version 10). Satzlängen 1392/1048 gegen die G.10/G.11-Kette
|
||||
(PDF-verifiziert), Fachwerte gegen die AP4/AP7-Rechenfälle."""
|
||||
import base64
|
||||
import datetime
|
||||
|
||||
from odoo import SUPERUSER_ID
|
||||
from odoo.tests import TransactionCase, tagged
|
||||
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
class AebPrivateTest(TransactionCase):
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.company = self.env.company
|
||||
self.company.write({'l10n_at_beitragskontonummer': '1412345678'})
|
||||
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 AEB 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
|
||||
self.krankenstand_typ = self.env.ref(
|
||||
'l10n_at_hr_payroll_private.leave_type_at_krankenstand')
|
||||
self.msch_typ = self.env.ref(
|
||||
'l10n_at_hr_payroll_private.leave_type_at_mutterschutz')
|
||||
|
||||
def _employee(self, eintritt):
|
||||
employee = self.env['hr.employee'].create({
|
||||
'name': 'AEB Test', 'birthday': '1990-05-15',
|
||||
'sex': 'female'})
|
||||
version = self.env['hr.version'].search(
|
||||
[('employee_id', '=', employee.id)], limit=1)
|
||||
version.write({
|
||||
'contract_date_start': eintritt,
|
||||
'kv_id': self.kv2203.id,
|
||||
'kv_gruppe_id': self.gruppe_ii.id,
|
||||
'erfahrungsstufe': 1,
|
||||
'ssnid': '1234150590',
|
||||
'resource_calendar_id': self.calendar.id,
|
||||
})
|
||||
version.generate_work_entries(
|
||||
datetime.date(2026, 8, 1), datetime.date(2026, 12, 31))
|
||||
return employee
|
||||
|
||||
def _slip(self, employee, monat):
|
||||
erster = datetime.date(2026, 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': 'AEB %d/2026' % monat,
|
||||
'employee_id': employee.id,
|
||||
'struct_id': self.structure.id,
|
||||
'date_from': erster,
|
||||
'date_to': letzter,
|
||||
})
|
||||
slip.compute_sheet()
|
||||
slip.action_payslip_done()
|
||||
return slip
|
||||
|
||||
def _leave(self, employee, typ, von, bis):
|
||||
leave = self.env['hr.leave'].create({
|
||||
'name': '%s %s' % (typ.name, von),
|
||||
'employee_id': employee.id,
|
||||
'holiday_status_id': typ.id,
|
||||
'request_date_from': von,
|
||||
'request_date_to': bis,
|
||||
})
|
||||
leave.with_user(SUPERUSER_ID)._action_validate()
|
||||
|
||||
def _datei_zeilen(self, aeb):
|
||||
roh = base64.b64decode(aeb.datei).decode('ISO-8859-15')
|
||||
return roh.strip('\r\n').split('\r\n')
|
||||
|
||||
def test_01_aeb_krankengeld(self):
|
||||
"""BEST AK / SART 70: Satzlänge 1392, Identifikation, EFZ-
|
||||
Fachdaten (AEFZ 06, JAGU/TAGU, TPR1 50, VENT, Bezugsblöcke)
|
||||
aus der AP4-Episode 1. 9.–15. 11. 2026."""
|
||||
employee = self._employee(datetime.date(2026, 7, 1))
|
||||
referenz = [self._slip(employee, m) for m in (8,)]
|
||||
# 76-Tage-Episode (EFZ erschöpft — 6 Wo voll bis 12. 10.,
|
||||
# 4 Wo halb 13. 10.–9. 11., ÖGK ab 10. 11.) wie AP4-RF-1.
|
||||
self._leave(employee, self.krankenstand_typ,
|
||||
datetime.date(2026, 9, 1),
|
||||
datetime.date(2026, 11, 15))
|
||||
self._slip(employee, 11)
|
||||
aeb = self.env['l10n.at.payroll.aeb'].create({
|
||||
'company_id': self.company.id,
|
||||
'art': 'krankengeld',
|
||||
'employee_id': employee.id,
|
||||
'verhinderung_von': datetime.date(2026, 9, 1),
|
||||
'verhinderung_bis': datetime.date(2026, 11, 15),
|
||||
})
|
||||
aeb.action_erzeugen()
|
||||
self.assertEqual(aeb.state, 'done')
|
||||
zeilen = self._datei_zeilen(aeb)
|
||||
self.assertEqual(zeilen[0][0:4], ' 01') # Dateiheader
|
||||
self.assertEqual(zeilen[1][0:2], '00') # Vorlauf
|
||||
self.assertEqual(zeilen[2][0:2], '70') # SART 70
|
||||
self.assertEqual(len(zeilen[2]), 1392)
|
||||
self.assertEqual(zeilen[-1][0:2], '99') # Schluss
|
||||
satz = zeilen[2]
|
||||
# Identifikation (Slices 0-basiert = Pos − 1)
|
||||
self.assertEqual(satz[20:30], '1412345678') # BKNR
|
||||
self.assertEqual(satz[177:187], '1234150590') # VSNR
|
||||
self.assertEqual(satz[187:195], '15051990') # GEBD
|
||||
self.assertEqual(satz[467:475], '01072026') # BEAB
|
||||
self.assertEqual(satz[475:476], '2') # KABE Angestellte
|
||||
self.assertEqual(satz[546:547], '1') # KZKU Krankheit
|
||||
self.assertEqual(satz[547:554], 'AAAAAFF') # ARFT
|
||||
self.assertEqual(satz[579:580], 'N') # BLOE
|
||||
# EFZ-Fachdaten (1. Arbeitsjahr: 6 Wochen)
|
||||
self.assertEqual(satz[808:810], '06') # AEFZ
|
||||
self.assertEqual(satz[810:811], '1') # JAGU Arbeitsjahr
|
||||
self.assertEqual(satz[811:812], '2') # TAGU Kalendertage
|
||||
self.assertEqual(satz[812:814], '50') # TPR1
|
||||
self.assertEqual(satz[814:822], '13102026') # TVO1 (13. 10.)
|
||||
self.assertEqual(satz[822:830], '09112026') # TBI1 (9. 11.)
|
||||
self.assertEqual(satz[800:808], '12102026') # VENT (12. 10.)
|
||||
# Bezugsblock: August (voller Monat vor der Episode 1. 9.)
|
||||
self.assertEqual(satz[633:641], '01082026') # BVO1
|
||||
self.assertEqual(satz[649:658], '%09d'
|
||||
% round(2059.41 * 100)) # BBE1
|
||||
self.assertEqual(satz[733:742], '%09d'
|
||||
% round(2059.41 * 100)) # BSUM
|
||||
# Keine Vorerkrankungen vor der Episode (ANV1 Grundstellung 0)
|
||||
self.assertEqual(satz[848:856], '0' * 8)
|
||||
|
||||
def test_02_aeb_wochengeld(self):
|
||||
"""BEST AW / SART 75: Satzlänge 1048, AVER = Ø-Netto der drei
|
||||
Referenzmonate (§ 162 Abs 3 ASVG), ANGV K."""
|
||||
employee = self._employee(datetime.date(2026, 7, 1))
|
||||
referenz = [self._slip(employee, m) for m in (8, 9, 10)]
|
||||
netto = sum(sum(s.line_ids.filtered(
|
||||
lambda l: l.code == 'NET').mapped('total'))
|
||||
for s in referenz)
|
||||
self._leave(employee, self.msch_typ,
|
||||
datetime.date(2026, 11, 1),
|
||||
datetime.date(2026, 11, 15))
|
||||
self._slip(employee, 11)
|
||||
aeb = self.env['l10n.at.payroll.aeb'].create({
|
||||
'company_id': self.company.id,
|
||||
'art': 'wochengeld',
|
||||
'employee_id': employee.id,
|
||||
'verhinderung_von': datetime.date(2026, 11, 1),
|
||||
'verhinderung_bis': datetime.date(2026, 11, 15),
|
||||
})
|
||||
aeb.action_erzeugen()
|
||||
zeilen = self._datei_zeilen(aeb)
|
||||
self.assertEqual(zeilen[2][0:2], '75')
|
||||
self.assertEqual(len(zeilen[2]), 1048)
|
||||
satz = zeilen[2]
|
||||
self.assertEqual(satz[365:368], 'A ') # WKFZ Österreich
|
||||
self.assertEqual(satz[634:643], '%09d' % round(netto * 100))
|
||||
self.assertEqual(satz[643:651], '01082026') # AVON
|
||||
self.assertEqual(satz[651:659], '31102026') # ABIS
|
||||
self.assertEqual(satz[747:748], 'J') # SZKZ (Nov-B13)
|
||||
self.assertEqual(satz[748:749], 'K') # ANGV kein Fortbezug
|
||||
self.assertEqual(satz[557:565], ' ' * 8) # RESE: a-Grundstellung Blank
|
||||
|
||||
def test_03_storno(self):
|
||||
"""SART 71: Storno übernimmt die Kerndaten der stornierten
|
||||
Bestätigung (VSNR/GEBD/BEAB) und trägt '-ST' in der REFN."""
|
||||
employee = self._employee(datetime.date(2026, 7, 1))
|
||||
[self._slip(employee, m) for m in (8, 9, 10)]
|
||||
self._leave(employee, self.msch_typ,
|
||||
datetime.date(2026, 11, 1),
|
||||
datetime.date(2026, 11, 15))
|
||||
original = self.env['l10n.at.payroll.aeb'].create({
|
||||
'company_id': self.company.id,
|
||||
'art': 'wochengeld',
|
||||
'employee_id': employee.id,
|
||||
'verhinderung_von': datetime.date(2026, 11, 1),
|
||||
})
|
||||
original.action_erzeugen()
|
||||
storno = self.env['l10n.at.payroll.aeb'].create({
|
||||
'company_id': self.company.id,
|
||||
'art': 'wochengeld',
|
||||
'employee_id': employee.id,
|
||||
'verhinderung_von': datetime.date(2026, 11, 1),
|
||||
'storno_id': original.id,
|
||||
})
|
||||
storno.action_erzeugen()
|
||||
zeilen = self._datei_zeilen(storno)
|
||||
self.assertEqual(zeilen[2][0:2], '76')
|
||||
self.assertEqual(len(zeilen[2]), 1048)
|
||||
satz = zeilen[2]
|
||||
self.assertEqual(satz[177:187], '1234150590') # VSNR des Originals
|
||||
self.assertTrue(storno.refn.endswith('-ST'))
|
||||
self.assertEqual(satz[1018:1048].strip(), storno.refn)
|
||||
|
||||
def test_04_wochengeld_agrd_validierung(self):
|
||||
"""Unzulässiger AGRD bei SART 75 wird geleert und protokolliert
|
||||
(D.22: nur 02/03/04/05/06/10/22/24)."""
|
||||
employee = self._employee(datetime.date(2026, 7, 1))
|
||||
[self._slip(employee, m) for m in (8, 9, 10)]
|
||||
self._leave(employee, self.msch_typ,
|
||||
datetime.date(2026, 11, 1),
|
||||
datetime.date(2026, 11, 15))
|
||||
self._slip(employee, 11)
|
||||
# Abmeldegrund 16 (Pensionierung) — für SART 75 unzulässig
|
||||
grund = self.env['hr.departure.reason'].search([
|
||||
('l10n_at_agrd', '=', '16')], limit=1)
|
||||
employee.version_id.write({
|
||||
'contract_date_end': datetime.date(2026, 11, 30),
|
||||
'departure_reason_id': grund.id,
|
||||
})
|
||||
aeb = self.env['l10n.at.payroll.aeb'].create({
|
||||
'company_id': self.company.id,
|
||||
'art': 'wochengeld',
|
||||
'employee_id': employee.id,
|
||||
'verhinderung_von': datetime.date(2026, 11, 1),
|
||||
})
|
||||
aeb.action_erzeugen()
|
||||
satz = self._datei_zeilen(aeb)[2]
|
||||
self.assertEqual(satz[546:547], 'G') # BLOE gelöst
|
||||
self.assertEqual(satz[547:549].strip(), '') # AGRD geleert
|
||||
self.assertIn('unzulässig', aeb.protokoll)
|
||||
@@ -0,0 +1,79 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<!-- ============================================================ -->
|
||||
<!-- AP8: Arbeits- und Entgeltbestätigungen (AEB) — ELDA-Bestände -->
|
||||
<!-- AK (Krankengeld SART 70/71) und AW (Wochengeld SART 75/76). -->
|
||||
<!-- ============================================================ -->
|
||||
|
||||
<record id="aeb_list_view" model="ir.ui.view">
|
||||
<field name="name">l10n.at.payroll.aeb.list</field>
|
||||
<field name="model">l10n.at.payroll.aeb</field>
|
||||
<field name="arch" type="xml">
|
||||
<list string="Arbeits- und Entgeltbestätigungen">
|
||||
<field name="art"/>
|
||||
<field name="employee_id"/>
|
||||
<field name="verhinderung_von"/>
|
||||
<field name="verhinderung_bis" optional="show"/>
|
||||
<field name="storno_id" optional="hide"/>
|
||||
<field name="refn" optional="hide"/>
|
||||
<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="aeb_form_view" model="ir.ui.view">
|
||||
<field name="name">l10n.at.payroll.aeb.form</field>
|
||||
<field name="model">l10n.at.payroll.aeb</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Arbeits- und Entgeltbestätigung">
|
||||
<header>
|
||||
<button name="action_erzeugen" type="object"
|
||||
string="Datei erzeugen" class="btn-primary"
|
||||
invisible="state == 'done'"/>
|
||||
<field name="state" widget="statusbar"
|
||||
statusbar_visible="draft,done"/>
|
||||
</header>
|
||||
<sheet>
|
||||
<group>
|
||||
<group>
|
||||
<field name="art"/>
|
||||
<field name="employee_id"/>
|
||||
<field name="kzku"
|
||||
invisible="art != 'krankengeld'"/>
|
||||
<field name="verhinderung_von"/>
|
||||
<field name="verhinderung_bis"/>
|
||||
<field name="storno_id"
|
||||
options="{'no_create': True}"/>
|
||||
<field name="refn" readonly="1"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="company_id"
|
||||
groups="base.group_multi_company"/>
|
||||
<field name="datei" filename="datei_name"/>
|
||||
<field name="datei_name" invisible="1"/>
|
||||
</group>
|
||||
</group>
|
||||
<notebook>
|
||||
<page string="Prüfprotokoll" name="protokoll">
|
||||
<field name="protokoll"/>
|
||||
</page>
|
||||
</notebook>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_aeb" model="ir.actions.act_window">
|
||||
<field name="name">Arbeits- und Entgeltbestätigungen (AEB)</field>
|
||||
<field name="res_model">l10n.at.payroll.aeb</field>
|
||||
<field name="view_mode">list,form</field>
|
||||
</record>
|
||||
|
||||
<menuitem id="menu_l10n_at_payroll_aeb"
|
||||
name="Arbeits- und Entgeltbestätigungen"
|
||||
parent="l10n_at_hr_payroll.menu_l10n_at_payroll_meldewesen"
|
||||
action="action_aeb" sequence="20"/>
|
||||
</odoo>
|
||||
Reference in New Issue
Block a user