Files
odoo-at-payroll/addons/l10n_at_hr_payroll_private/models/aeb.py
T
fegger c3b9d6eb8e [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
2026-09-12 23:52:11 +02:00

561 lines
22 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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