Files
odoo-at-payroll/addons/l10n_at_hr_payroll/models/lohnzettel_l16.py
T
fegger c474a39466 [IMP] l10n_at_hr_payroll: GP7 ELDA-Fixsatz-Framework, L16 + Arbeitsstättenmeldung, Kind-Modell, Belege (AP5/AP6)
ELDA-Fixsatz-Framework (models/elda.py) nach DM-ORG 42.7.0 Kapitel C.1/C.2+E.1-E.3: Fixlängen-Sätze ohne Feldtrennzeichen (a/a-n linksbündig, Grundstellung blank; n rechtsbündig, Grundstellung 0, führende Nullen, KEINE Interpunktion — Beträge EURO-Cent kaufmännisch gerundet, Prozentsätze x1000 mit 3 Nachkommastellen), Encoding ISO-8859-15 (Standard-Encoding für Fixlängen), optionaler Dateiheader (DHKZ 2x blank + DHVN 01 + ENCD), Vorlaufsatz SART 00 (BEST/VSTR/VERS) + Schlusssatz SART 99 mit SANZ; SANR lückenlos ab 1 je Datenbestand; CRLF als Satztrenner (Praxis-Annahme, DM-ORG normiert keines — Parallellauf verifizieren).

Lohnzettel Finanz L16 (models/lohnzettel_l16.py): Informationssatz I1 (SART I1, Länge 1.100, Strukturversion 03) + Mitteilungssatz L1 (SART L1, Länge 3.500, Lohnzettelversion 28 — zwingend ab 01.03.2026) mit vollständiger, PDF-verifizierter Feldtabelle (~210 Felder inkl. Kinderblock 15x123 ab Pos. 1501; DM-ORG 42.7.0 E.13/E.14). KZ-245-Formel: KZ 210 = Bruttobezüge gem. § 25 GESAMT (DM-ORG-Feldbeschreibung) — die Formel 210-215-220-230-243 ergibt die laufende Steuerbemessung, per Konstruktion konsistent mit der GP3-§-66-Kette (Ableitung aus l10n_at_bemessung/steuerbefreiung_68/s67_abs und Regelcodes; bewusst KEIN paralleles KZ-Klassifikationsfeld). Arbeitsstättenmeldung § 34 Abs 6 ASVG: SART 45 (Länge 300, Version 02, BEST AD/VSTR ST Statistik Austria) je AN am 31.12. aus dem Dienstort-Partner (GKZ manuell, Warnung wenn fehlt) — im selben Jahreslauf (Nutzer-Entscheidung 5.1).

Kind-Datenmodell (models/kind.py — l10n.at.payroll.kind): Stammdaten-Wartung des Familienbonus Plus (Nutzer-Entscheidung 5.3: FB+ rechnet NICHT im Lohnsteuerkern — bei Unterschreiten der Steuer auf 0 erfolgt die Geltendmachung über die Arbeitnehmerveranlagung); Felder für den L16-Kinderblock (Familienname/Vorname/Geburtsdatum/VSNR/Staatsangehörigkeit/FB+-Monate voll/halb, Alterstaffel 166,68/58,34).

Stammdaten (models/meldewesen.py): Beitragskontonummer BKNR + Lohnsteuernummer (res.company), Gemeindekennziffer GKZ (res.partner), VSNR-Formatprüfung LLLPTTMMJJ auf hr.version.ssnid (Prüfzifferalgorithmus = dokumentierter Rest, SVS-Quelle beschaffen), eSV-Abmeldegrund AGRD-Mapping auf hr.departure.reason (Standard 00); fallweise-Beschäftigungs-Flag auf hr.version (§ 33 Abs 1a Z 1 ASVG).

Belege (AP6): Lohnzettel AT (ir.actions.report auf hr.payslip, AT-Layout mit Bezügen/DN-Abzügen/DG-Anteilen/Netto), Monatsdienstliste (mBGM-Kontrollschicht je AN) + Monatsjournal (je Lohnart Summen je Lauf) auf hr.payslip.run als QWeb (models/monatsreports.py + report/belege_reports.xml; account.report wird in keinem HR-Modul genutzt).

Tests: test_elda_l16 (4 — Feldformatierung/Cent-Beträge/skala, Bestands-Assembly SANR/SANZ/CRLF/ISO-8859-15/Dateiheader, L16-Lauf mit positionsexakten Assertions aus der November-Fixture, Arbeitsstätten-Satz). 19.0.4.0.0.
2026-09-12 17:30:59 +02:00

774 lines
34 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.
"""Lohnzettel Finanz (L16) und Arbeitsstättenmeldung (GP7/AP5) —
Jahresdeklaration nach DM-ORG 42.7.0 (lokal `.firecrawl/ris/elda/`).
- **L16** (BEST `LF`, zuständiger Träger VSTR `94` Bundesrechenzentrum):
Informationssatz I1 (SART `I1`, Länge 1100, Strukturversion 03) +
Mitteilungssatz L1 (SART `L1`, Länge 3500, Lohnzettelversion 28 —
zwingend ab 01.03.2026). KZ-Struktur nach lb-lvr-04 (✅
RECHTSQUELLEN-Privat Abschn. 10): **KZ 245 = 210 215 220 230
243**; die KZ-Werte sind aus den Kern-Flags (`l10n_at_bemessung`,
`steuerbefreiung_68`, `s67_abs`) und Regelcodes ableitbar — bewusst
KEIN paralleles KZ-Klassifikationsfeld (Widerspruchsrisiko zur
GP3-Steuerlogik).
- **Arbeitsstättenmeldung** § 34 Abs 6 ASVG (BEST `AD`, VSTR `ST`
Statistik Austria): Satzart 45 (Länge 300, Version 02) je AN mit der
Adresse der Arbeitsstätte am 31. 12. — läuft im selben Jahreslauf
(Nutzer-Entscheidung 5.1; Frist gemeinsam Ende Februar).
- Familienbonus Plus: **keine** Personalverrechnungs-Komponente
(Unterschreiten der Steuer auf 0 → Arbeitnehmerveranlagung;
Nutzer-Entscheidung 2026-09-12); die FB+-Felder des L1
(BFABO/KFABO/VFABO/FABO + Kinderblock) werden aus der
Stammdaten-Wartung am Kind-Modell (`l10n.at.payroll.kind`) befüllt.
- Dateibasiert (Prämisse P7): Generierung + formale Prüfung als
Prüfprotokoll; Übertragung/ELDA-Konto beim Mandanten.
Satzstrukturen: PDF-verifizierte Extraktion 2026-09-12 (DM-ORG 42.7.0,
Kapitel E.13/E.14/E.20; Positions-Ketten lückenlos geprüft)."""
from datetime import date, datetime
import base64
import logging
from odoo import api, fields, models
from odoo.exceptions import UserError
from .elda import (
L10N_AT_ELDA_IDTEIL,
L10nAtEldaBestand,
L10nAtEldaDatei,
L10nAtEldaSatz,
)
_logger = logging.getLogger(__name__)
def l10n_at_namen(employee):
"""(Familienname, Vorname) aus dem Odoo-Gesamtnamen: Split am
letzten Leerzeichen (österreichische Schreibweise „Vorname
Nachname“; mehrgliedrige Familiennamen werden als ein Feld
geführt — eSV FANA/VONA-Verifikation im Parallellauf)."""
name = (employee.name or '').strip()
if ' ' in name:
vorname, familienname = name.rsplit(' ', 1)
return familienname, vorname
return name, ''
# ------------------------------------------------------------------
# Informationssatz I1 (E.13, S. 219220; Länge 1100) — Datenteil
# ------------------------------------------------------------------
L16_INFORMATIONSSATZ = [
(2, 'FSART', 21, 1, 'a'), # I
(3, 'CLEAR', 22, 6, 'a'), # Clearingstelle (RADAUS/OESTAT)
(4, 'CLDVR', 28, 7, 'a'),
(6, 'STNRL', 35, 9, 'n'), # Steuernummer Lohnverrechnungsstelle
(7, 'DVRNL', 44, 7, 'n'),
(9, 'STNRA', 51, 9, 'n'), # Steuernummer Arbeitgeber
(10, 'DVRNA', 60, 7, 'n'),
(11, 'ARTD', 67, 2, 'a'), # LZ = Lohnzetteldaten
(12, 'DTUE', 69, 8, 'n'), # JJJJMMTT
(13, 'ZTUE', 77, 6, 'n'), # HHMMSS
(14, 'STVE', 83, 2, 'n'), # Strukturversion 03
(15, 'ANAM', 85, 37, 'a'), # AG-Name
(16, 'ATIT', 122, 20, 'a'),
(17, 'AADR', 142, 37, 'a'),
(18, 'ALKZ', 179, 3, 'a'),
(19, 'APLZ', 182, 10, 'a'),
(20, 'AORT', 192, 30, 'a'),
(21, 'INTE', 222, 11, 'a'),
(22, 'JAHR', 233, 4, 'n'), # Mitteilungszeitraum JJJJ
(23, 'GESA', 237, 7, 'n'), # Gesamtanzahl Mitteilungen im Jahr
(24, 'ANZA', 244, 7, 'n'), # Anzahl der aktuellen Übermittlung
(25, 'ATEL', 251, 16, 'a'),
(26, 'AFAX', 267, 16, 'a'),
(27, 'RESE', 283, 818, 'a'),
]
# ------------------------------------------------------------------
# Mitteilungssatz L1 (E.14, S. 223233; Länge 3500) — Datenteil inkl.
# RESE-Grundstellungsfeldern (n-Felder Grundstellung 0!)
# ------------------------------------------------------------------
L16_MITTEILUNGSSATZ = [
(2, 'FSART', 21, 1, 'a'), # L
(3, 'CLADR', 22, 6, 'a'),
(4, 'CLDVR', 28, 7, 'a'),
(6, 'STNRL', 35, 9, 'n'),
(7, 'DVRNL', 44, 7, 'n'),
(8, 'ARTU', 51, 1, 'a'), # Art der Übermittlung (Prüfkatalog)
(9, 'DTUE', 52, 8, 'n'), # JJJJMMTT, wie I1
(10, 'ZTUE', 60, 6, 'n'), # wie I1
(11, 'REFN', 66, 30, 'a'), # je AG+Jahr+DV eindeutig
(12, 'ARTL', 96, 2, 'n'), # Art des Lohnzettels (D.31)
(13, 'FEHL', 98, 20, 'a'), # Clearingstelle
(14, 'STAT', 118, 1, 'a'), # Clearingstelle
(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, 'SOZS', 132, 1, 'n'), # soziale Stellung (D.33)
(20, 'AVLN', 133, 4, 'n'), # VSNR LLLP
(21, 'AGBD', 137, 6, 'n'), # TTMMJJ
(22, 'ANAM', 143, 37, 'a'),
(23, 'ATIT', 180, 20, 'a'),
(24, 'AADR', 200, 37, 'a'),
(25, 'ALKZ', 237, 3, 'a'),
(26, 'APLZ', 240, 10, 'a'),
(27, 'AORT', 250, 30, 'a'),
(28, 'GESW', 280, 1, 'a'),
(29, 'GESM', 281, 1, 'a'),
(30, 'GESD', 282, 1, 'a'),
(31, 'VOLL', 283, 1, 'a'), # 'J' überwiegend Vollzeit
(32, 'TEIL', 284, 1, 'a'), # 'J' überwiegend Teilzeit
(33, 'AVAB', 285, 1, 'a'), # Alleinerzieherabsetzbetrag
(34, 'PVLN', 286, 4, 'n'), # Partner-VSNR LLLP (AVB)
(35, 'PGBD', 290, 6, 'n'), # TTMMJJ (AVB)
(36, 'AEAB', 296, 1, 'a'), # Alleinverdienerabsetzbetrag
(37, 'V210', 297, 1, 'a'),
(38, 'B210', 298, 10, 'n'), # KZ 210 § 25 (Cent)
(39, 'V215', 308, 1, 'a'),
(40, 'B215', 309, 10, 'n'), # KZ 215 § 68-steuerfrei
(41, 'V220', 319, 1, 'a'),
(42, 'B220', 320, 10, 'n'), # KZ 220 § 67 Abs 1/2/5 (inkl. 620-€)
(43, 'VIEB', 330, 1, 'a'),
(44, 'BIEB', 331, 10, 'n'), # insgesamt einbehaltene Beiträge
(45, 'V225', 341, 1, 'a'),
(46, 'B225', 342, 10, 'n'), # KZ 225 SV zu KZ 220
(47, 'V226', 352, 1, 'a'),
(48, 'B226', 353, 10, 'n'), # KZ 226 SV zu § 67 Abs 38
(49, 'V230', 363, 1, 'a'),
(50, 'B230', 364, 10, 'n'), # KZ 230 laufende SV/AK/WF
(51, 'RESE51', 374, 1, 'a'),
(52, 'RESE52', 375, 10, 'n'),
(53, 'VAUS', 385, 1, 'a'),
(54, 'BAUS', 386, 10, 'n'),
(55, 'VPEN', 396, 1, 'a'),
(56, 'BPEN', 397, 10, 'n'),
(57, 'VEFB', 407, 1, 'a'),
(58, 'BEFB', 408, 10, 'n'),
(59, 'VSTF', 418, 1, 'a'),
(60, 'BSTF', 419, 10, 'n'),
(61, 'VSSB', 429, 1, 'a'),
(62, 'BSSB', 430, 10, 'n'),
(63, 'V243', 440, 1, 'a'),
(64, 'B243', 441, 10, 'n'), # KZ 243 sonstige steuerfreie
(65, 'V245', 451, 1, 'a'),
(66, 'B245', 452, 10, 'n'), # KZ 245 steuerpflichtige Bezüge
(67, 'VIEL', 462, 1, 'a'),
(68, 'BIEL', 463, 10, 'n'), # insgesamt einbehaltene Lohnsteuer
(69, 'VABL', 473, 1, 'a'),
(70, 'BABL', 474, 10, 'n'), # abzügliche Lohnsteuer (Fixsätze)
(71, 'V260', 484, 1, 'a'),
(72, 'B260', 485, 10, 'n'), # KZ 260 anrechenbare Lohnsteuer
(73, 'VSOB', 495, 1, 'a'),
(74, 'BSOB', 496, 10, 'n'),
(75, 'VFB1', 506, 1, 'a'),
(76, 'BFB1', 507, 10, 'n'),
(77, 'RESE77', 517, 10, 'a'),
(78, 'VFB2', 527, 1, 'a'),
(79, 'BFB2', 528, 10, 'n'),
(80, 'VAUF', 538, 1, 'a'),
(81, 'BAUF', 539, 10, 'n'),
(82, 'VFB3', 549, 1, 'a'),
(83, 'BFB3', 550, 10, 'n'),
(84, 'VNEB', 560, 1, 'a'),
(85, 'BNEB', 561, 10, 'n'),
(86, 'PVON', 571, 2, 'n'),
(87, 'PBIS', 573, 2, 'n'),
(88, 'VPFG', 575, 1, 'a'),
(89, 'BPFG', 576, 10, 'n'),
(90, 'VSFB', 586, 1, 'a'),
(91, 'BSFB', 587, 10, 'n'),
(92, 'RESE92', 597, 1, 'a'),
(93, 'RESE93', 598, 10, 'n'),
(94, 'RESE94', 608, 1, 'a'),
(95, 'RESE95', 609, 10, 'n'),
(96, 'RESE96', 619, 1, 'a'),
(97, 'RESE97', 620, 10, 'n'),
(98, 'RESE98', 630, 1, 'a'),
(99, 'RESE99', 631, 8, 'n'),
(101, 'STNRA', 639, 9, 'n'), # auszahlende Stelle (Pensionen)
(102, 'DVRNA', 648, 7, 'n'),
(103, 'ANME', 655, 1, 'a'), # Unterbrechung LZZR
(104, 'KORR', 656, 1, 'a'), # 'K' berichtigter Lohnzettel
(105, 'VUEB', 657, 1, 'a'),
(106, 'BUEB', 658, 10, 'n'),
(107, 'INTE', 668, 11, 'a'),
(108, 'WOBD', 679, 12, 'a'), # Ordnungsbegriff DG (Personalnummer)
(109, 'ZOBD', 691, 15, 'a'),
(110, 'ANZK', 706, 2, 'n'), # Anzahl Kinder § 106
(111, 'RESE111', 708, 1, 'a'),
(112, 'RESE112', 709, 1, 'a'),
(113, 'RESE113', 710, 1, 'a'),
(114, 'RESE114', 711, 1, 'a'),
(115, 'RESE115', 712, 1, 'a'),
(116, 'RESE116', 713, 1, 'a'),
(117, 'RESE117', 714, 1, 'a'),
(118, 'RESE118', 715, 1, 'a'),
(119, 'RESE119', 716, 1, 'a'),
(120, 'RESE120', 717, 1, 'a'),
(121, 'RESE121', 718, 1, 'a'),
(122, 'RESE122', 719, 1, 'a'),
(123, 'VAPK', 720, 1, 'a'),
(124, 'BAPK', 721, 10, 'n'),
(125, 'EPAB', 731, 1, 'a'),
(126, 'VENT', 732, 1, 'a'),
(127, 'ENTW', 733, 10, 'n'),
(128, 'VPRE', 743, 1, 'a'),
(129, 'PREI', 744, 10, 'n'),
(130, 'WERK', 754, 2, 'n'),
(131, 'VPEND', 756, 1, 'a'),
(132, 'BPEND', 757, 10, 'n'),
(134, 'UEBMO', 767, 2, 'n'),
(135, 'RESE135', 769, 1, 'a'),
(136, 'RESE136', 770, 10, 'n'),
(137, 'LDKZA', 780, 3, 'a'),
(138, 'VWBKB', 783, 1, 'a'),
(139, 'WBKB', 784, 10, 'n'),
(140, 'RESE140', 794, 1, 'a'),
(141, 'RESE141', 795, 10, 'a'),
(142, 'ERVAB', 805, 1, 'a'),
(143, 'BFABO', 806, 1, 'a'), # Familienbonus Plus 'J'
(144, 'KFABO', 807, 2, 'n'), # Anzahl Kinder FB+
(145, 'VFABO', 809, 1, 'a'),
(146, 'FABO', 810, 10, 'n'), # FB+-Betrag
(147, 'HOTA', 820, 3, 'n'), # Telearbeitstage
(148, 'VHOPA', 823, 1, 'a'),
(149, 'HOPA', 824, 10, 'n'), # Homeoffice-Pauschale § 26 Z 9 lit a
(150, 'VKOUN', 834, 1, 'a'),
(151, 'KOUN', 835, 10, 'n'),
(152, 'VMAGB', 845, 1, 'a'),
(153, 'MAGB', 846, 10, 'n'),
(154, 'FLABZ', 856, 1, 'a'), # freiwilliger Lohnsteuerabzug § 47
(155, 'AUEZG', 857, 1, 'a'),
(156, 'VTEPR', 858, 1, 'a'),
(157, 'TEPR', 859, 10, 'n'),
(158, 'VMAPR', 869, 1, 'a'),
(159, 'MAPR', 870, 10, 'n'),
(160, 'STUM', 880, 4, 'n'),
(161, 'STUMJ', 884, 4, 'n'),
(162, 'ZF673', 888, 1, 'a'),
(163, 'BDOZ', 889, 1, 'a'),
(164, 'VFSVB', 890, 1, 'a'),
(165, 'FSVB', 891, 10, 'n'),
(166, 'PLST', 901, 1, 'a'),
(167, 'VKAKL', 902, 1, 'a'),
(168, 'KAKL', 903, 10, 'n'),
(169, 'VZUKB', 913, 1, 'a'),
(170, 'ZUKB', 914, 10, 'n'),
(171, 'GEBD', 924, 8, 'n'),
(172, 'GEBP', 932, 8, 'n'),
(173, 'AKZKB', 940, 2, 'n'),
(174, 'RESE174', 942, 59, 'a'),
(175, 'REFN1', 1001, 30, 'a'),
(176, 'RESE176', 1031, 19, 'a'),
(177, 'BSBKFZ', 1050, 10, 'n'),
(178, 'BSBWR', 1060, 10, 'n'),
(179, 'BSBSB', 1070, 10, 'n'),
(180, 'SB681', 1080, 10, 'n'),
(181, 'SB682', 1090, 10, 'n'),
(182, 'BZUKG', 1100, 10, 'n'),
(183, 'MAKBG', 1110, 10, 'n'),
(184, 'MABST', 1120, 10, 'n'),
(185, 'ZUCSG', 1130, 10, 'n'),
(186, 'GUTSC', 1140, 10, 'n'),
(187, 'MARAB', 1150, 10, 'n'),
(188, 'SBK00', 1160, 1, 'a'),
(189, 'SBK15', 1161, 1, 'a'),
(190, 'SBK20', 1162, 1, 'a'),
(191, 'SBKDW', 1163, 1, 'a'),
(192, 'AKKFZ', 1164, 10, 'n'),
(193, 'KEAKFZ', 1174, 10, 'n'),
(194, 'AGLAG', 1184, 10, 'n'),
(195, 'RESE195', 1194, 307, 'a'),
(210, 'RESE210', 3346, 155, 'a'),
]
# Kinderblock (E.14, Felder 196209): je Kind 123 Stellen ab Pos. 1501
# (Block 1 = 15011623 … Block 15 = 32233345; Positionen aus den
# Feldlängen arithmetisch abgeleitet, PDF-verifiziert).
_KINDER_FELDER = [
(196, 'KFAM', 0, 30, 'a'), # Familienname
(197, 'KVON', 30, 30, 'a'), # Vorname
(198, 'KSTAAT', 60, 3, 'a'), # Staatsangehörigkeit
(199, 'KSTWE', 63, 1, 'a'), # 2. Staatsangehörigkeit
(200, 'KVSNR', 64, 10, 'n'), # VSNR des Kindes
(201, 'KGEBD', 74, 8, 'n'), # TTMMJJJJ
(202, 'KAFBZ', 82, 1, 'a'), # Kennzeichen FB+
(203, 'KAPFB', 83, 1, 'a'),
(204, 'KAUHZ', 84, 1, 'a'),
(205, 'KBGFP', 85, 2, 'n'), # FB+ ab Monat
(206, 'KEGFP', 87, 2, 'n'), # FB+ bis Monat
(207, 'KBHFP', 89, 2, 'n'), # halber FB+ ab Monat
(208, 'KEHFP', 91, 2, 'n'), # halber FB+ bis Monat
(209, 'RESEK', 93, 30, 'a'),
]
def _kinderblock_struktur():
felder = []
for kind_nr in range(1, 16):
basis = 1501 + (kind_nr - 1) * 123
for nr, name, offset, laenge, typ in _KINDER_FELDER:
felder.append(
(nr, '%s_%d' % (name, kind_nr), basis + offset,
laenge, typ))
return felder
L16_MITTEILUNGSSATZ += _kinderblock_struktur()
# ------------------------------------------------------------------
# Adresse der Arbeitsstätte § 34 Abs 6 ASVG (E.20; SART 45, Länge 300,
# Version 02; BEST AD, VSTR ST)
# ------------------------------------------------------------------
ARBEITSSTAETTE_SATZ = [
(2, 'KVTR', 21, 2, 'a'), # KV-Träger (aus BKNR-Präfix)
(3, 'BKNR', 23, 10, 'a'),
(4, 'VSNR', 33, 10, 'n'),
(5, 'BZVM', 43, 2, 'n'), # Beitragszeit von MM
(6, 'BZBM', 45, 2, 'n'), # Beitragszeit bis MM
(7, 'BZJJ', 47, 4, 'n'), # Beitragsjahr
(8, 'FAMS', 51, 2, 'a'), # Finanzamtsnummer meldende Stelle
(9, 'STNR', 53, 9, 'n'), # Steuernummer DG
(10, 'ASTR', 62, 50, 'a'), # Straße der Arbeitsstätte
(11, 'ANRV', 112, 5, 'a'), # Hausnummer von
(12, 'ANRB', 117, 5, 'a'), # Hausnummer bis
(13, 'ASTG', 122, 5, 'a'), # Stiege
(14, 'ATOP', 127, 5, 'a'), # Tür/Top
(15, 'APLZ', 132, 9, 'a'), # PLZ der Arbeitsstätte
(16, 'AORT', 141, 40, 'a'), # Ort
(17, 'AGKZ', 181, 5, 'n'), # Gemeindekennziffer
(18, 'APOL', 186, 50, 'a'), # Politische Gemeinde/Staat
(19, 'AUDT', 236, 8, 'n'), # TTMMJJJJ
(20, 'WOBD', 244, 12, 'a'),
(21, 'ZOBD', 256, 15, 'a'),
(22, 'REFN', 271, 30, 'a'),
]
class L10nAtPayrollL16Line(models.Model):
_name = 'l10n.at.payroll.l16.line'
_description = 'Lohnzettel Finanz — Mitteilungssatz-Daten je AN'
_order = 'employee_name'
l16_id = fields.Many2one(
'l10n.at.payroll.l16', 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)
versicherungsnummer = fields.Char()
soziale_stellung = fields.Char(help='08 (D.33)')
belz = fields.Char(string='LZP ab (TTMM)')
enlz = fields.Char(string='LZP bis (TTMM)')
kz210 = fields.Monetary(currency_field='currency_id')
kz215 = fields.Monetary(currency_field='currency_id')
kz220 = fields.Monetary(currency_field='currency_id')
kz225 = fields.Monetary(currency_field='currency_id')
kz226 = fields.Monetary(currency_field='currency_id')
kz230 = fields.Monetary(currency_field='currency_id')
kz243 = fields.Monetary(currency_field='currency_id')
kz245 = fields.Monetary(currency_field='currency_id')
lohnsteuer = fields.Monetary(currency_field='currency_id',
string='KZ 260 anrechenbare LSt')
beitraege = fields.Monetary(currency_field='currency_id',
string='Einbehaltene Beiträge (VIEB)')
fab_betrag = fields.Monetary(
currency_field='currency_id', string='Familienbonus Plus')
fab_kinder = fields.Integer(string='Anzahl Kinder FB+')
refn = fields.Char()
currency_id = fields.Many2one(
'res.currency', related='l16_id.company_id.currency_id')
def _kz_salden(self, slips):
"""KZ-Summen je AN aus validierten Läufen. **KZ 210 = Brutto-
bezüge gem. § 25 gesamt** (laufende UND sonstige §-67-Bezüge,
ohne § 26/§ 3 Abs 1 Z 16b — DM-ORG E.14 Feldbeschreibung), denn
KZ 245 = 210 215 220 230 243 ist die laufende Steuer-
bemessung: Gesamtbrutto §-68-Freie sonstige Bezüge
laufende SV-DN. 215 aus `_l10n_at_freibetraege_68` des
GP3-Kerns (Monats-Caps 400/600 und ÜStd 15/170 eingerechnet);
220 = sonstige § 67 Abs 1/2/5 ohne Abs-3/6; 225 = SV-DN auf
KZ 220; 230 = laufende SV-DN + AK + WF; VIEB = 230 + 225 +
226; 260/BIEL = LST_LFD + LST_SZ."""
self.ensure_one()
salden = dict.fromkeys(
('kz210', 'kz215', 'kz220', 'kz225', 'kz226', 'kz230',
'kz243', 'kz245', 'lohnsteuer', 'beitraege'), 0.0)
for slip in slips:
for line in slip.line_ids:
rule = line.salary_rule_id
if not line.total:
continue
if rule.l10n_at_bemessung == 'laufend':
salden['kz210'] += line.total
elif (rule.l10n_at_bemessung == 'sonstig'
and rule.l10n_at_s67_abs == 'keine'):
salden['kz210'] += line.total
salden['kz220'] += line.total
elif line.code == 'SVDN_SZ':
salden['kz225'] += line.total
elif line.code in ('SVDN_KV', 'SVDN_PV', 'SVDN_AV',
'SVDN_AK', 'SVDN_WF'):
salden['kz230'] += line.total
elif line.code in ('LST_LFD', 'LST_SZ'):
salden['lohnsteuer'] += line.total
freibe = slip._l10n_at_freibetraege_68()
salden['kz215'] += freibe['szfn'] + freibe['ue_std']
salden['beitraege'] = salden['kz230'] + salden['kz225'] \
+ salden['kz226']
salden['kz245'] = (salden['kz210'] - salden['kz215']
- salden['kz220'] - salden['kz230']
- salden['kz243'])
return salden
def _soziale_stellung(self, employee):
"""D.33: 1 Lehrling, 2 Arbeiter, 3 Angestellte (0 sonstige) —
MVP: Stand des aktuellen Vertrags."""
version = employee.version_id
if version and getattr(version, 'l10n_at_lehrling', False):
return '1'
if version and getattr(
version, 'l10n_at_personalgruppe', None) == 'arbeiter':
return '2'
return '3'
class L10nAtPayrollL16(models.Model):
_name = 'l10n.at.payroll.l16'
_description = 'Lohnzettel Finanz (L16) & Arbeitsstättenmeldung'
_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='Lohnzahlungszeitraum')
art = fields.Selection(
[('01', 'Lohnzettel (Jahresbeleg)'),
('18', 'Berichtigter Lohnzettel (binnen 2 Wochen)')],
required=True, default='01', tracking=True)
line_ids = fields.One2many(
'l10n.at.payroll.l16.line', 'l16_id', string='Mitteilungssätze')
datei_l16 = fields.Binary(
attachment=True, string='L16-Datei', copy=False)
datei_l16_name = fields.Char(copy=False)
datei_arbeitsstaette = fields.Binary(
attachment=True, string='Datei Arbeitsstättenmeldung',
copy=False)
datei_arbeitsstaette_name = fields.Char(copy=False)
protokoll = fields.Text(
string='Prüfprotokoll',
help='Formale Prüfung (Satzlängen, KZ-245-Formel, VSNR-Format, '
'Pflicht-Stammdaten) und Annahmen des Laufes; die '
'Fachprüfung bleibt der Clearingstelle (Mitteilungsfile, '
'GP8-Rest).')
state = fields.Selection(
[('draft', 'Entwurf'), ('done', 'Erzeugt')], default='draft',
required=True, tracking=True)
_sql_constraints = [(
'l16_company_jahr_art_uniq', 'unique(company_id, jahr, art)',
'Für dieses Jahr existiert bereits eine L16-Deklaration dieser '
'Art.')]
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')),
])
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):
"""V-Feld: blank bei 0, sonst '+'/'-' (L1-Feldregeln)."""
if not betrag:
return None
return '+' if betrag > 0 else '-'
def _l10n_at_l1_satz(self, line, stnr, heute, jetzt):
self.ensure_one()
employee = line.employee_id
version = employee.version_id
satz = L10nAtEldaSatz(
L10N_AT_ELDA_IDTEIL + L16_MITTEILUNGSSATZ, 3500,
bezeichnung='L1 %s' % employee.name)
satz.setze('SART', 'L1')
satz.setze('VSTR', '94')
satz.setze('FSART', 'L')
satz.setze('STNRL', stnr)
satz.setze('STNRA', stnr)
satz.setze_datum('DTUE', heute, 'JJJJMMTT')
satz.setze_zeit('ZTUE', jetzt)
satz.setze('REFN', line.refn)
satz.setze('ARTL', self.art)
if self.art == '18':
satz.setze('KORR', 'K')
satz.setze('FIND', 'J')
satz.setze('BELZ', line.belz)
satz.setze('ENLZ', line.enlz)
satz.setze('JALZ', self.jahr)
satz.setze('SOZS', line.soziale_stellung)
satz.setze('AVLN', (line.versicherungsnummer or '')[:4])
satz.setze_datum('AGBD', employee.birthday, 'TTMMJJ')
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 '')
if version and version.sex == 'female':
satz.setze('GESW', 'J')
elif version and version.sex == 'male':
satz.setze('GESM', 'J')
elif version and version.sex:
satz.setze('GESD', 'J')
vollzeit = bool(version and version.work_time_rate >= 1.0)
satz.setze('VOLL', 'J' if vollzeit else 'N')
satz.setze('TEIL', 'N' if vollzeit else 'J')
if version and version.l10n_at_familienabsetzbetrag == 'avab':
satz.setze('AVAB', 'J')
elif version \
and version.l10n_at_familienabsetzbetrag == 'avb':
satz.setze('AEAB', 'J')
for betrag, vfeld, bfeld in (
(line.kz210, 'V210', 'B210'),
(line.kz215, 'V215', 'B215'),
(line.kz220, 'V220', 'B220'),
(line.kz225, 'V225', 'B225'),
(line.kz226, 'V226', 'B226'),
(line.kz230, 'V230', 'B230'),
(line.kz243, 'V243', 'B243'),
(line.kz245, 'V245', 'B245'),
(line.beitraege, 'VIEB', 'BIEB'),
(line.lohnsteuer, 'VIEL', 'BIEL'),
(line.lohnsteuer, 'V260', 'B260')):
satz.setze(vfeld, self._vorzeichen(betrag))
satz.setze(bfeld, abs(betrag) if betrag else 0, cent=True)
satz.setze('WOBD', 'EMP-%d' % employee.id)
if version and version.children:
satz.setze('ANZK', version.children)
# Familienbonus Plus aus der Kind-Stammdaten-Wartung (keine
# Personalverrechnungs-Komponente — Nutzer-Entscheidung).
if line.fab_kinder:
satz.setze('BFABO', 'J')
satz.setze('KFABO', line.fab_kinder)
satz.setze('VFABO', self._vorzeichen(line.fab_betrag))
satz.setze('FABO', abs(line.fab_betrag) if line.fab_betrag
else 0, cent=True)
kinder = self.env['l10n.at.payroll.kind'].search([
('employee_id', '=', employee.id),
('fab_plus', '=', True)])
for nummer, kind in enumerate(kinder[:15], start=1):
satz.setze('KFAM_%d' % nummer,
(kind.familienname or '')[:30])
satz.setze('KVON_%d' % nummer, (kind.vorname or '')[:30])
satz.setze('KSTAAT_%d' % nummer, kind.staatsangehoerigkeit)
satz.setze('KSTWE_%d' % nummer,
kind.staatsangehoerigkeit_2)
satz.setze('KVSNR_%d' % nummer, kind.versicherungsnummer)
satz.setze_datum('KGEBD_%d' % nummer, kind.geburtstag,
'TTMMJJJJ')
satz.setze('KAFBZ_%d' % nummer, 'J')
satz.setze('KBGFP_%d' % nummer, kind.fab_monat_von)
satz.setze('KEGFP_%d' % nummer, kind.fab_monat_bis)
satz.setze('KBHFP_%d' % nummer, kind.fab_halb_monat_von)
satz.setze('KEHFP_%d' % nummer, kind.fab_halb_monat_bis)
return satz
def _l10n_at_l16_bestand(self, erstellungsdatum=None):
self.ensure_one()
heute = erstellungsdatum or date.today()
jetzt = datetime.now()
partner = self.company_id.partner_id
stnr = self.company_id.l10n_at_lohnsteuernummer
informationssatz = L10nAtEldaSatz(
L10N_AT_ELDA_IDTEIL + L16_INFORMATIONSSATZ, 1100,
bezeichnung='L16-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', self.jahr)
informationssatz.setze('GESA', len(self.line_ids))
informationssatz.setze('ANZA', len(self.line_ids))
datensaetze = [informationssatz] + [
self._l10n_at_l1_satz(line, stnr, heute, jetzt)
for line in self.line_ids]
return L10nAtEldaBestand(
'LF', '94', '28', hersteller=self._l10n_at_hersteller(),
datensaetze=datensaetze)
def _l10n_at_arbeitsstaette_bestand(self, erstellungsdatum=None):
"""§ 34 Abs 6 ASVG je AN mit mBGM-Bezug: Adresse der
Arbeitsstätte am 31. 12. (Dienstort-Partner + GKZ)."""
self.ensure_one()
heute = erstellungsdatum or date.today()
bknr = self.company_id.l10n_at_beitragskontonummer
stnr = self.company_id.l10n_at_lohnsteuernummer
datensaetze = []
warnungen = []
for line in self.line_ids:
employee = line.employee_id
dienstort = employee.address_id
if not dienstort or not dienstort.l10n_at_gemeindekennziffer:
warnungen.append(
'Arbeitsstätte %s: Dienstort-Partner bzw. '
'Gemeindekennziffer fehlt (res.partner.'
'l10n_at_gemeindekennziffer am Dienstort-Partner '
'pflegen).' % employee.name)
satz = L10nAtEldaSatz(
L10N_AT_ELDA_IDTEIL + ARBEITSSTAETTE_SATZ, 300,
bezeichnung='Arbeitsstätte %s' % employee.name)
satz.setze('SART', '45')
satz.setze('VSTR', 'ST')
satz.setze('KVTR', (bknr or '')[:2])
satz.setze('BKNR', bknr)
satz.setze('VSNR', line.versicherungsnummer)
satz.setze('BZVM', (line.belz or '01')[:2])
satz.setze('BZBM', (line.enlz or '12')[:2])
satz.setze('BZJJ', self.jahr)
satz.setze('FAMS', (stnr or '')[:2])
satz.setze('STNR', stnr)
satz.setze('ASTR', (dienstort.street if dienstort else '')
[:50])
satz.setze('APLZ', dienstort.zip if dienstort else '')
satz.setze('AORT', (dienstort.city if dienstort else '')[:40])
satz.setze('AGKZ',
dienstort.l10n_at_gemeindekennziffer
if dienstort else None)
satz.setze('APOL', (dienstort.city if dienstort else '')[:50])
satz.setze_datum('AUDT', heute, 'TTMMJJJJ')
satz.setze('WOBD', 'EMP-%d' % employee.id)
datensaetze.append(satz)
if not datensaetze:
return None, warnungen
return L10nAtEldaBestand(
'AD', 'ST', '02', hersteller=self._l10n_at_hersteller(),
datensaetze=datensaetze), warnungen
def action_erzeugen(self):
"""Lauf: KZ-Daten je AN aus validierten Läufen sammeln, formale
Prüfung (KZ-245-Formel, Pflicht-Stammdaten), Dateien erzeugen.
Fehler brechen ab (UserError, DmfA-Muster listenweise);
Warnungen landen im Prüfprotokoll."""
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, 9-stellig).')
if not company.l10n_at_beitragskontonummer:
fehler.append('Beitragskontonummer der Company fehlt '
'(l10n_at_beitragskontonummer, BKNR).')
slips = deklaration._l10n_at_slips()
nach_mitarbeiter = {}
for slip in slips:
nach_mitarbeiter.setdefault(slip.employee_id, []) \
.append(slip)
deklaration.line_ids.unlink()
zeilen_werte = []
for employee, employee_slips in nach_mitarbeiter.items():
version = employee.version_id
if not (version and version.ssnid):
fehler.append(
'Mitarbeiter %s ohne Versicherungsnummer '
'(hr.version.ssnid).' % employee.name)
continue
if not employee.birthday:
fehler.append(
'Mitarbeiter %s ohne Geburtsdatum.' % employee.name)
continue
line = deklaration.env['l10n.at.payroll.l16.line'].new({
'employee_id': employee.id})
salden = line._kz_salden(employee_slips)
erste = min(s.date_from for s in employee_slips)
letzte = max(s.date_to for s in employee_slips)
kinder = deklaration.env['l10n.at.payroll.kind'].search([
('employee_id', '=', employee.id),
('fab_plus', '=', True)])
zeilen_werte.append({
'employee_id': employee.id,
'versicherungsnummer': version.ssnid,
'soziale_stellung':
line._soziale_stellung(employee),
'belz': erste.strftime('%d%m'),
'enlz': letzte.strftime('%d%m'),
'refn': 'L16-%d-%d-%d' % (company.id,
deklaration.jahr,
employee.id),
'fab_kinder': len(kinder),
'fab_betrag': sum(
kind._l10n_at_fab_betrag(deklaration.jahr)
for kind in kinder),
**salden,
})
for werte in zeilen_werte:
deklaration.env['l10n.at.payroll.l16.line'].create(
dict(werte, l16_id=deklaration.id))
for line in deklaration.line_ids:
formel = (line.kz210 - line.kz215 - line.kz220
- line.kz230 - line.kz243)
if abs(formel - line.kz245) > 0.02:
fehler.append(
'%s: KZ-245-Formel verletzt (%.2f%.2f).'
% (line.employee_name, formel, line.kz245))
if line.kz245 < 0:
warnungen.append(
'%s: KZ 245 negativ (Bezüge übersteigen die '
'Abzüge) — FinOnl-Konsistenzprüfung prüft.'
% line.employee_name)
if fehler:
deklaration.protokoll = 'FEHLER:\n%s\nWARNUNGEN:\n%s' % (
'\n'.join(fehler), '\n'.join(warnungen) or '')
raise UserError('L16-Lauf mit Fehlern abgebrochen:\n'
+ '\n'.join(fehler))
if deklaration.line_ids:
datei = L10nAtEldaDatei(
[deklaration._l10n_at_l16_bestand()])
deklaration.write({
'datei_l16': base64.b64encode(
datei.render()),
'datei_l16_name': 'l16_%d_%s.txt' % (
deklaration.jahr,
deklaration.company_id.l10n_at_beitragskontonummer
or deklaration.company_id.id),
})
bestand, ast_warn = \
deklaration._l10n_at_arbeitsstaette_bestand()
warnungen.extend(ast_warn)
if bestand is not None:
datei = L10nAtEldaDatei([bestand])
deklaration.write({
'datei_arbeitsstaette': base64.b64encode(
datei.render()),
'datei_arbeitsstaette_name':
'arbeitsstaette_%d.txt' % deklaration.jahr,
})
deklaration.protokoll = 'WARNUNGEN:\n%s' % (
'\n'.join(warnungen) or '')
deklaration.state = 'done'
return True