[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:
2026-09-12 23:52:11 +02:00
parent 802f2ef04c
commit c3b9d6eb8e
9 changed files with 1085 additions and 110 deletions
@@ -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 (MontagSonntag)."""
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