[IMP] l10n_at_hr_payroll(_private): Meldewesen-Reste (AP17) und Status-Sonderfälle (AP18/AP19)

AP17 — Lohnnachweis L17, Finanz-Mitteilungen § 109a/§ 109b, GfB-Jahreszahlung:
- L17 (DM-ORG E.24, BEST LF, SART A1/1100, Version 28): je
  Lohnzahlungszeitraum je DN aus den GP3-Kennzahlen-Flags; Prüfkatalog-
  Semantik (KZ 350 = Brutto gesamt, 351/352/354 Teilbeträge) und formale
  Prüfungen (350-Formel, 357/347-Caps, aliquotierte 354/HOPA-Caps);
  ODL-PDF-Extraktion der Kapitel E.24/E.16/E.26 (txt feldversetzt)
- Finanz-Mitteilungen § 109a (W1, E.16) und § 109b (B1, E.26): Modelle
  je Leistungserbringer/Auftragnehmer, Schwellen als hr.rule.parameter
  (450/900 — § 109a-VO; 100.000 — § 109b Abs 4 lit a, RIS NOR40263383),
  AL31/32/33, LKZ1-7, UID, bis 3 maßgebliche natürliche Personen,
  KORR K/S; Unter-Schwellen-Zeilen entfallen mit Protokollhinweis
- mBGM: geringfügig Beschäftigte (BSGR B010/B030) — T01 § 53a-UV-
  Pauschale + V01 BMSVG 1,53 %, kein T02 (GfB-SZ beitragsfrei,
  § 49 Abs 3); JAGB-Pakettrennung für die jährliche Abrechnung
  (§ 58 Abs 8 ASVG iVm § 6 Abs 2a BMSVG, DM-ORG D.52) mit
  2,5 vH-Zuschlag (Z04) in der GfB-Jahresabrechnung (Fälligkeit
  15. 1. Folgejahr)
- Fristenkalender: L17- und § 109b-Fristarten (Ende Februar;
  § 109a/§ 109b EStG und § 58 Abs 8 ASVG RIS-verifiziert)

AP18 — Schwerarbeit/NSchG (Beschaffungsgate erledigt, RIS):
- NSchG (RIS 10008502, BGBl 1981/354) Art XI Abs 3 (2 vH Stammnorm,
  3,80 % via Sistierung) und Abs 6 (Nachtschwerarbeitsmonat: 6-Tage-
  Kriterium, Rückblick-Fenster 12/18, Randmonat); ASchV (20004642)
  § 1/§ 4/§ 5; DM-ORG E.22.2-Erstellvorschriften (Z 1 analog NSchG,
  Z 2/4/5 ≥ 15 Tage, Z 3/GfB nicht melden)
- Kern: Schwerarbeitsmeldung (BEST SM, SART 65/66, Länge 800,
  Tätigkeitsblöcke 26 × 10) mit Altersfilter- und Erstellvorschrift-
  Prüfungen
- Privat: Nachtschwerarbeitsmonat-Zähler (WET ATP_NACHTSCHWER,
  Art-XI-Abs-6-Fenster inkl. Randmonat-Proxy) als stored Flag am Lauf

AP19 — Status- und Verhinderungs-Sonderfälle:
- hr.version-Status (Organ ≤/≥ 25 %, lb-vst-03): Organ ≥ 25 % — kein
  SV-Verhältnis (SVDN/SVDG 0 via SV-Satz-Override, mBGM-Ausschluss,
  LSt läuft weiter); Ferialpraktikanten-Flag (dokumentativ — BMSVG § 6
  nennt keinen Ausschluss, Entgelt-/SV-Logik bleibt normal)
- Präsenz-/Ausbildungs-/Zivildienst-Fiktion (§ 7 Abs 1/2 BMSVG):
  fiktive BV-Bemessungsgrundlage 435,90 € (KBGG § 3 Abs 1 idF vor
  BGBl I 53/2016, lb-vor-04) aliquotiert auf ATP_PRD_TAGE
- Dienstverhinderungs-Sonderfälle bleiben dokumentiert offen
  (lb-dvh-01/04, KB-); Feiertags-EFZ ist durch das AP4-WET-Modell
  abgedeckt

Tests: 21 neue (L17-Positionen/Prüfkatalog, W1/B1-Schwellen, GfB-mBGM/
JAGB/Jahresabrechnung, NSchM-Fenster/Randmonat, SM-Fixsatz, Organ-Sperre,
PRD-Fiktion); Suite 252/252 grün auf frischer -i-DB (85 Kern, 18 GemBG,
213 Privat, 10 Dokumente). Dokumentierte Reste: DZR-36-Fenster,
Randmonat-Proxy und UV-Pauschale-vs-mBGM im Parallellauf (GP8),
fallweise mBGM (G3/G4), 2027er-Schwellen (C7).
This commit is contained in:
2026-09-16 19:12:08 +02:00
parent 1df5fb19c4
commit ec33dfacf2
27 changed files with 2736 additions and 64 deletions
@@ -0,0 +1,464 @@
# Part of the odoo-at-payroll project. License: LGPL-3.
"""Lohnzettel Finanz — Lohnnachweis/Lohnbescheinigung L17 (AP17) —
DM-ORG 42.7.0, Kapitel E.24 (BEST ``LF``, VSTR ``94``, Satzart ``A1``,
Länge 1100, Version 28 — zwingender Einsatz ab 01.03.2026, fachliche
Gültigkeit für Zeiträume ab 01.01.2025; PDF-verifizierte ODL-Extraktion
2026-09-16 aus `.firecrawl/ris/elda/DM-ORG_*v42.7.0*.pdf`).
Der A1-Satz ist je **Lohnzahlungszeitraum** je Dienstnehmer aufzubauen
(D.32: BELZ/ENLZ/JALZ; REFN je AG+Jahr+Dienstverhältnis eindeutig und
auch bei Zeiträumsänderung unverändert). Kennzahlen-Semantik laut
Prüfkatalog L17 (Stand 12/2025, lokal `.firecrawl/ris/elda/`):
**KZ 350 = Bruttobezüge GESAMT**; die Teil-KZs 351 (Zuwendungen neben
Arbeitslohn), 352 (Abfertigungen/Abfindungen), 354 (normale ÜStd-
Zuschläge), 356/738 (Pensionsabfindungen), UEB, 394, KUR, ALT, SOP,
AUS, ZAKV, MAGB sind **Teilmengen** von 350 (Prüfkatalog-Formel:
Feld 28 < (Feld 30 + 32 + 34 + 36 + 38 + 50 + 72 + 74 + 76 + 80 + 82
+ 95 + 100) = Fehler). KZ 738/739 sind nur für Jahre bis 2017 zulässig
(Pensionsabfindung Pensionskasse).
KZ-Mapping aus den validierten Läufen (GP3-Kern-Flags, Muster
`lohnzettel_l16.py`):
- 350: laufende Bezüge + alle sonstigen §-67-Bezüge (gesamtes §-25-Brutto
des LZP inkl. 351/352/354)
- 351: sonstige Bezüge ohne Abs-3/6-Markierung (Sechstel-Route —
Sonderzahlungen und Zuwendungen)
- 352: sonstige Bezüge mit ``l10n_at_s67_abs`` = abs3/abs6
(Abfertigung Alt / freiwillige Abfertigung / Abfindungen)
- 354: § 68-freie Überstundenzuschläge (``_l10n_at_freibetraege_68``)
- 357: einbehaltene SV-Beiträge des laufenden Arbeitslohns
(SVDN_KV/PV/AV/AK/WF)
- 347: einbehaltene SV-Beiträge für Zuwendungen (SVDN_SZ)
- nicht automatisiert (dokumentierter Rest, Grundstellung 0 + Protokoll-
Hinweis): 356/738 Pensionsabfindungen, 736/737/739 SV-Zerlegung auf
Abfindungs-KZs, BUEB, 358, BSPE, BARB, WERK, UEBMO, ANZAT, TSTAAT,
TEPR/MAPR/ZAKV/ANKV/MAGB, KOUN, 394, KUR, ALT, SOP, AUS.
Formale Prüfungen nach Prüfkatalog L17: KZ-350-Formel, 357 ≤ 40 % × 350,
347 ≤ 25 % × 351, 354 ≤ 2.400 € (+1 %) je LZP aliquotiert, HOPA ≤ 3 € ×
HOTA und ≤ 300 € je LZP, DIEN ≤ 99, WERK/UEBMO ≤ 12, ANZAT ≤ 366,
HOTA ≤ LZP-Tage."""
from datetime import date, datetime
import base64
from odoo import api, fields, models
from odoo.exceptions import UserError
from .elda import (
L10N_AT_ELDA_IDTEIL,
L10nAtEldaBestand,
L10nAtEldaDatei,
L10nAtEldaSatz,
)
from .lohnzettel_l16 import L16_INFORMATIONSSATZ
# ------------------------------------------------------------------
# Mitteilungssatz A1 (E.24; Länge 1100) — Datenteil
# ------------------------------------------------------------------
L17_SATZ = [
(2, 'FSART', 21, 1, 'a'), # A
(3, 'CLADR', 22, 6, 'a'),
(4, 'CLDVR', 28, 7, 'a'),
(6, 'STNRL', 35, 9, 'n'), # Steuernummer Lohnverrechnungsstelle
(7, 'DVRNL', 44, 7, 'n'),
(8, 'ARTU', 51, 1, 'a'),
(9, 'DTUE', 52, 8, 'n'), # JJJJMMTT, wie I1
(10, 'ZTUE', 60, 6, 'n'),
(11, 'REFN', 66, 30, 'a'), # je AG+Jahr+DV eindeutig
(12, 'RE12', 96, 2, 'a'),
(13, 'FEHL', 98, 20, 'a'),
(14, 'STAT', 118, 1, 'a'),
(15, 'FIND', 119, 1, 'a'), # 'J' = vom Hersteller für richtig
(16, 'BELZ', 120, 4, 'n'), # Beginn LZP TTMM
(17, 'ENLZ', 124, 4, 'n'), # Ende LZP TTMM
(18, 'JALZ', 128, 4, 'n'), # Jahr LZP
(19, 'PENS', 132, 1, 'a'), # J/N
(20, 'AVSN', 133, 10, 'n'), # LLLPTTMMJJ
(21, 'ANAM', 143, 37, 'a'),
(22, 'ATIT', 180, 20, 'a'),
(23, 'AADR', 200, 37, 'a'),
(24, 'ALKZ', 237, 3, 'a'),
(25, 'APLZ', 240, 10, 'a'),
(26, 'AORT', 250, 30, 'a'),
(27, 'V350', 280, 1, 'a'),
(28, 'B350', 281, 10, 'n'), # Bruttobezüge gesamt (KZ 350)
(29, 'V351', 291, 1, 'a'),
(30, 'B351', 292, 10, 'n'), # Zuwendungen neben Arbeitslohn
(31, 'V352', 302, 1, 'a'),
(32, 'B352', 303, 10, 'n'), # Abfertigungen und Abfindungen
(33, 'V354', 313, 1, 'a'),
(34, 'B354', 314, 10, 'n'), # (normale) ÜStd-Zuschläge
(35, 'V356', 324, 1, 'a'),
(36, 'B356', 325, 10, 'n'), # Pensionsabfindung Arbeitgeber
(37, 'V738', 335, 1, 'a'),
(38, 'B738', 336, 10, 'n'), # Pensionsabfindung Pensionskasse
(39, 'V357', 346, 1, 'a'),
(40, 'B357', 347, 10, 'n'), # SV-Beiträge laufender Arbeitslohn
(41, 'V347', 357, 1, 'a'),
(42, 'B347', 358, 10, 'n'), # SV-Beiträge für Zuwendungen
(43, 'V736', 368, 1, 'a'),
(44, 'B736', 369, 10, 'n'), # SV-Beiträge für KZ 352
(45, 'V737', 379, 1, 'a'),
(46, 'B737', 380, 10, 'n'), # SV-Beiträge für KZ 356
(47, 'V739', 390, 1, 'a'),
(48, 'B739', 391, 10, 'n'), # SV-Beiträge für KZ 738
(49, 'VUEB', 401, 1, 'a'),
(50, 'BUEB', 402, 10, 'n'), # übrige Bezüge außerhalb Österreichs
(51, 'RE51', 412, 1, 'a'),
(52, 'RE52', 413, 10, 'a'),
(53, 'RE53', 423, 1, 'a'),
(54, 'RE54', 424, 10, 'a'),
(55, 'V358', 434, 1, 'a'),
(56, 'B358', 435, 10, 'n'), # anrechenbare ausländische Steuer
(57, 'VSPE', 445, 1, 'a'),
(58, 'BSPE', 446, 10, 'n'), # Spesensätze außerhalb KZ 350
(59, 'VARB', 456, 1, 'a'),
(60, 'BARB', 457, 10, 'n'), # AG-Beiträge an Pensionskassen
(61, 'DIEN', 467, 2, 'n'), # Dienstzeit (Jahre) bei KZ 352
(62, 'STDT', 469, 8, 'n'), # Sterbedatum JJJJMMTT
(63, 'OELD', 477, 20, 'a'), # Ordnungsbegriff ELDA
(64, 'KORR', 497, 1, 'a'), # 'K'/'S'
(65, 'INTE', 498, 11, 'a'),
(66, 'WOBD', 509, 12, 'a'),
(67, 'ZOBD', 521, 15, 'a'),
(68, 'WAHR', 536, 3, 'a'), # Währung
(69, 'MUEB', 539, 2, 'n'), # Monate überwiegende Nachtarbeit
(70, 'V394', 541, 1, 'a'),
(71, 'B394', 542, 10, 'n'), # Sonn-/Feiertags-/Nachtzuschläge
(72, 'VKUR', 552, 1, 'a'),
(73, 'BKUR', 553, 10, 'n'), # Kurzarbeit
(74, 'VALT', 563, 1, 'a'),
(75, 'BALT', 564, 10, 'n'), # Altersteilzeit
(76, 'RE76', 574, 1, 'a'),
(77, 'RE77', 575, 10, 'n'),
(78, 'VSOP', 585, 1, 'a'),
(79, 'BSOP', 586, 10, 'n'), # Sozialplanzahlungen
(80, 'VAUS', 596, 1, 'a'),
(81, 'BAUS', 597, 10, 'n'), # begünstigte Auslandstätigkeit
(82, 'WERK', 607, 2, 'n'), # Werkverkehr (Monate)
(83, 'UEBMO', 609, 2, 'n'), # Überlassung Kfz (Monate)
(84, 'ANZAT', 611, 3, 'n'), # Arbeitstage begünstigte Auslandstätigk.
(85, 'HOTA', 614, 3, 'n'), # Telearbeitstage (Homeoffice)
(86, 'VHOPA', 617, 1, 'a'),
(87, 'HOPA', 618, 10, 'n'), # Homeoffice-Pauschale (§ 26 Z 9 lit. a)
(88, 'VKOUN', 628, 1, 'a'),
(89, 'KOUN', 629, 10, 'n'), # Kostenübernahme § 26 Z 5 lit. b
(90, 'TSTAAT', 639, 3, 'a'), # Tätigkeitsstaat
(91, 'VTEPR', 642, 1, 'a'),
(92, 'TEPR', 643, 10, 'n'), # Teuerungsprämie (§ 124b Z 408)
(93, 'VMAPR', 653, 1, 'a'),
(94, 'MAPR', 654, 10, 'n'), # Mitarbeiterprämie (§ 124b Z 478)
(95, 'VZAKV', 664, 1, 'a'),
(96, 'ZAKV', 665, 10, 'n'), # AG-Zuschuss freiw. KV
(97, 'ANKV', 675, 1, 'a'), # (freiw.) gesetzlich krankenversichert
(98, 'VMAGB', 676, 1, 'a'),
(99, 'MAGB', 677, 10, 'n'), # Mitarbeitergewinnbeteiligung
(100, 'RE100', 687, 314, 'a'),
(101, 'REFN2', 1001, 30, 'a'), # Softwarehersteller-REFN
(102, 'RE102', 1031, 70, 'a'),
]
class L10nAtPayrollL17Line(models.Model):
_name = 'l10n.at.payroll.l17.line'
_description = 'L17 — Lohnnachweis je Dienstnehmer und LZP'
_order = 'employee_name, date_from'
l17_id = fields.Many2one(
'l10n.at.payroll.l17', required=True, ondelete='cascade',
index=True)
employee_id = fields.Many2one('hr.employee', required=True)
employee_name = fields.Char(related='employee_id.name',
readonly=True, store=True)
date_from = fields.Date(string='LZP von', required=True)
date_to = fields.Date(string='LZP bis', required=True)
versicherungsnummer = fields.Char()
kz350 = fields.Monetary(currency_field='currency_id')
kz351 = fields.Monetary(currency_field='currency_id')
kz352 = fields.Monetary(currency_field='currency_id')
kz354 = fields.Monetary(currency_field='currency_id')
kz357 = fields.Monetary(currency_field='currency_id')
kz347 = fields.Monetary(currency_field='currency_id')
hota = fields.Integer(string='Homeoffice-Tage')
hopa = fields.Monetary(currency_field='currency_id')
mueb = fields.Integer(string='Monate überwiegende Nachtarbeit')
refn = fields.Char()
currency_id = fields.Many2one(
'res.currency', related='l17_id.company_id.currency_id')
def _l10n_at_pruefungen(self, fehler, warnungen):
"""Formale Prüfungen nach Prüfkatalog L17 (Stand 12/2025)."""
self.ensure_one()
lzp_tage = max(1, (self.date_to - self.date_from).days + 1)
teilsumme = self.kz351 + self.kz352 + self.kz354
if self.kz350 + 0.02 < teilsumme:
fehler.append(
'%s (%s): KZ 350 < 351+352+354 (Prüfkatalog-Formel).'
% (self.employee_name, self.date_from))
if self.kz357 > self.kz350 * 0.4 + 0.02:
fehler.append(
'%s (%s): KZ 357 > 40 %% von KZ 350 (Prüfkatalog).'
% (self.employee_name, self.date_from))
if self.kz351 and self.kz347 > self.kz351 * 0.25 + 0.02:
fehler.append(
'%s (%s): KZ 347 > 25 %% von KZ 351 (Prüfkatalog).'
% (self.employee_name, self.date_from))
if self.kz354 > 2400.0 * lzp_tage / 365.0 * 1.01 + 0.02:
warnungen.append(
'%s (%s): KZ 354 übersteigt den aliquotierten '
'Prüfkatalog-Cap 2.400 € je Jahr (+1 %% Toleranz).'
% (self.employee_name, self.date_from))
if self.hota > lzp_tage:
fehler.append(
'%s (%s): HOTA > LZP-Tage.' % (
self.employee_name, self.date_from))
if self.hopa > self.hota * 3.0 + 0.02:
fehler.append(
'%s (%s): HOPA > 3 € × HOTA.' % (
self.employee_name, self.date_from))
if self.hopa > 300.0 * lzp_tage / 365.0 * 1.01 + 0.02:
warnungen.append(
'%s (%s): HOPA übersteigt den aliquotierten '
'Prüfkatalog-Cap 300 € je Jahr.' % (
self.employee_name, self.date_from))
class L10nAtPayrollL17(models.Model):
_name = 'l10n.at.payroll.l17'
_description = 'L17 — Lohnnachweis/Lohnbescheinigung (Finanz)'
_inherit = ['mail.thread']
_order = 'jahr desc, id desc'
company_id = fields.Many2one(
'res.company', required=True,
default=lambda self: self.env.company)
jahr = fields.Integer(required=True, string='Jahr')
line_ids = fields.One2many(
'l10n.at.payroll.l17.line', 'l17_id', string='A1-Sätze')
datei = fields.Binary(attachment=True, string='L17-Datei', copy=False)
datei_name = fields.Char(copy=False)
protokoll = fields.Text(
string='Prüfprotokoll',
help='Formale Prüfung nach Prüfkatalog L17 (Stand 12/2025) und '
'dokumentierte Reste der KZ-Belegung.')
state = fields.Selection(
[('draft', 'Entwurf'), ('done', 'Erzeugt')], default='draft',
required=True, tracking=True)
_sql_constraints = [(
'l17_company_jahr_uniq', 'unique(company_id, jahr)',
'Für dieses Jahr existiert bereits eine L17-Deklaration.')]
def _l10n_at_slips(self):
self.ensure_one()
return self.env['hr.payslip'].search([
('company_id', '=', self.company_id.id),
('date_from', '>=', date(self.jahr, 1, 1)),
('date_from', '<=', date(self.jahr, 12, 31)),
('state', 'in', ('validated', 'paid')),
], order='date_from, id')
def _l10n_at_hersteller(self):
partner = self.company_id.partner_id
return {'name': 'odoo-at-payroll', 'kfz': 'A',
'plz': partner.zip, 'ort': partner.city,
'strasse': partner.street}
@api.model
def _vorzeichen(self, betrag):
if not betrag:
return None
return '+' if betrag > 0 else '-'
def _l10n_at_kz_salden(self, slip):
"""(salden, hota) je Lohnzahlungszeitraum aus dem Lauf —
Kennzahlen-Mapping siehe Modul-Doku (GP3-Flags, Muster
`_kz_salden` der L16-Zeile)."""
self.ensure_one()
salden = dict.fromkeys(
('kz350', 'kz351', 'kz352', 'kz354', 'kz357', 'kz347',
'hopa'), 0.0)
hota = 0
for line in slip.line_ids:
rule = line.salary_rule_id
if not line.total:
continue
if rule.l10n_at_bemessung == 'laufend':
salden['kz350'] += line.total
if line.code == 'SB_HOMEOFFICE':
salden['hopa'] += line.total
elif rule.l10n_at_bemessung == 'sonstig':
salden['kz350'] += line.total
if rule.l10n_at_s67_abs in ('abs3', 'abs6'):
salden['kz352'] += line.total
else:
salden['kz351'] += line.total
elif line.code == 'SVDN_SZ':
salden['kz347'] += line.total
elif line.code in ('SVDN_KV', 'SVDN_PV', 'SVDN_AV',
'SVDN_AK', 'SVDN_WF'):
salden['kz357'] += line.total
freibe = slip._l10n_at_freibetraege_68()
salden['kz354'] = freibe['ue_std']
for eingabe in slip.input_line_ids:
if eingabe.input_type_id.code == 'ATP_SB_HOMEOFFICE':
hota += int(eingabe.quantity or 0)
return salden, hota
def _l10n_at_a1_satz(self, line, stnr, heute, jetzt):
self.ensure_one()
employee = line.employee_id
version = employee.version_id
satz = L10nAtEldaSatz(
L10N_AT_ELDA_IDTEIL + L17_SATZ, 1100,
bezeichnung='A1 %s %s' % (employee.name, line.date_from))
satz.setze('SART', 'A1')
satz.setze('VSTR', '94')
satz.setze('FSART', 'A')
satz.setze('STNRL', stnr)
satz.setze_datum('DTUE', heute, 'JJJJMMTT')
satz.setze_zeit('ZTUE', jetzt)
satz.setze('REFN', line.refn)
satz.setze('FIND', 'J')
satz.setze('BELZ', line.date_from.strftime('%d%m'))
satz.setze('ENLZ', line.date_to.strftime('%d%m'))
satz.setze('JALZ', self.jahr)
satz.setze('PENS', 'N')
satz.setze('AVSN', line.versicherungsnummer)
satz.setze('ANAM', (employee.name or '')[:37])
satz.setze('AADR', (version.private_street or '')[:37]
if version else '')
satz.setze('ALKZ', (version.private_country_id.code
if version and version.private_country_id
else 'AUT'))
satz.setze('APLZ', version.private_zip if version else '')
satz.setze('AORT', (version.private_city or '')[:30]
if version else '')
for betrag, vfeld, bfeld, negativ in (
(line.kz350, 'V350', 'B350', False),
(line.kz351, 'V351', 'B351', False),
(line.kz352, 'V352', 'B352', True),
(line.kz354, 'V354', 'B354', True),
(line.kz357, 'V357', 'B357', False),
(line.kz347, 'V347', 'B347', True)):
if betrag < 0 and not negativ:
raise UserError(
'%s (%s): negativer Betrag in %s — das Vorzeichenfeld '
'zulässigerweise "-" nur nach DM-ORG-Feldbeschreibung.'
% (employee.name, line.date_from, vfeld))
satz.setze(vfeld, self._vorzeichen(betrag))
satz.setze(bfeld, abs(betrag) if betrag else 0, cent=True)
if line.mueb:
satz.setze('MUEB', line.mueb)
if line.hota:
satz.setze('HOTA', line.hota)
if line.hopa:
satz.setze('VHOPA', self._vorzeichen(line.hopa))
satz.setze('HOPA', abs(line.hopa), cent=True)
satz.setze('WOBD', 'EMP-%d' % employee.id)
satz.setze('REFN2', line.refn)
return satz
def action_erzeugen(self):
"""A1 je (Dienstnehmer, Lohnzahlungszeitraum) aus validierten
Läufen; formale Prüfungen nach Prüfkatalog L17; Fehler brechen
ab (UserError, DmfA-Muster), Warnungen ins Protokoll."""
for deklaration in self:
fehler = []
warnungen = []
company = deklaration.company_id
if not company.l10n_at_lohnsteuernummer:
fehler.append(
'Lohnsteuernummer der Company fehlt '
'(l10n_at_lohnsteuernummer).')
deklaration.line_ids.unlink()
for slip in deklaration._l10n_at_slips():
version = slip.version_id or slip.employee_id.version_id
if not (version and version.ssnid):
fehler.append(
'Mitarbeiter %s ohne Versicherungsnummer '
'(hr.version.ssnid).' % slip.employee_id.name)
continue
if not slip.employee_id.birthday:
fehler.append(
'Mitarbeiter %s ohne Geburtsdatum.'
% slip.employee_id.name)
continue
salden, hota = deklaration._l10n_at_kz_salden(slip)
refn = 'L17-%d-%d-%d-%s' % (
company.id, deklaration.jahr, slip.employee_id.id,
slip.date_from.strftime('%Y%m%d'))
line = deklaration.env['l10n.at.payroll.l17.line'].create({
'l17_id': deklaration.id,
'employee_id': slip.employee_id.id,
'date_from': slip.date_from,
'date_to': slip.date_to,
'versicherungsnummer': version.ssnid,
'kz350': salden['kz350'],
'kz351': salden['kz351'],
'kz352': salden['kz352'],
'kz354': salden['kz354'],
'kz357': salden['kz357'],
'kz347': salden['kz347'],
'hota': hota,
'hopa': salden['hopa'],
'mueb': 0,
'refn': refn,
})
line._l10n_at_pruefungen(fehler, warnungen)
if deklaration.line_ids:
for line in deklaration.line_ids:
if line.kz352:
warnungen.append(
'%s: KZ 352 belegt — die SV-Zerlegung auf '
'KZ 736 bleibt dokumentierter Rest (v1 = 0); '
'Prüfkatalog-Grenze 40 %% × KZ 352 prüfen.'
% line.employee_name)
if fehler:
deklaration.protokoll = 'FEHLER:\n%s\nWARNUNGEN:\n%s' % (
'\n'.join(fehler), '\n'.join(warnungen) or '')
raise UserError('L17-Lauf mit Fehlern abgebrochen:\n'
+ '\n'.join(fehler))
if deklaration.line_ids:
heute = date.today()
jetzt = datetime.now()
partner = company.partner_id
stnr = company.l10n_at_lohnsteuernummer
informationssatz = L10nAtEldaSatz(
L10N_AT_ELDA_IDTEIL + L16_INFORMATIONSSATZ, 1100,
bezeichnung='L17-Informationssatz')
informationssatz.setze('SART', 'I1')
informationssatz.setze('VSTR', '94')
informationssatz.setze('FSART', 'I')
informationssatz.setze('CLEAR', 'RADAUS')
informationssatz.setze('STNRL', stnr)
informationssatz.setze('STNRA', stnr)
informationssatz.setze('ARTD', 'LZ')
informationssatz.setze_datum('DTUE', heute, 'JJJJMMTT')
informationssatz.setze_zeit('ZTUE', jetzt)
informationssatz.setze('STVE', '03')
informationssatz.setze('ANAM', (partner.name or '')[:37])
informationssatz.setze('AADR', (partner.street or '')[:37])
informationssatz.setze('ALKZ', 'AUT')
informationssatz.setze('APLZ', partner.zip)
informationssatz.setze('AORT', (partner.city or '')[:30])
informationssatz.setze('JAHR', deklaration.jahr)
informationssatz.setze('GESA', len(deklaration.line_ids))
informationssatz.setze('ANZA', len(deklaration.line_ids))
datensaetze = [informationssatz] + [
deklaration._l10n_at_a1_satz(line, stnr, heute, jetzt)
for line in deklaration.line_ids]
bestand = L10nAtEldaBestand(
'LF', '94', '28', hersteller=deklaration
._l10n_at_hersteller(), datensaetze=datensaetze)
datei = L10nAtEldaDatei([bestand])
deklaration.write({
'datei': base64.b64encode(datei.render()),
'datei_name': 'l17_%d_%s.txt' % (
deklaration.jahr, stnr),
})
deklaration.protokoll = 'WARNUNGEN:\n%s' % (
'\n'.join(warnungen) or '')
deklaration.state = 'done'
return True