# 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