9add58fe83
Pay the pre-2003 AngG severance from the end-of-employment run as its own neutral benefit line: free of SV (§ 49 Abs 3 Z 7 ASVG) and payroll levies, outside the annual sixth, and collected into the § 291d EO termination pot net of its own wage tax. Taxation follows the binding better-of rule of § 67 Abs 3 EStG: multiple-of-method (tariff wage tax on the running monthly wage × the statutory multiple of the payout) versus the flat 6 % rate, applied mandatorily in favour of the employee. The tariff wage tax is captured as a separately confirmed value instead of deriving it from gross pay through the § 62 engine. The wizard enforces § 23a Abs 3 AngG (parental exit needs five uninterrupted years), blocks BMSVG-new contracts except frozen claims and validates AGRD 02/06 against the claim type. The LNK basis subtracts § 67 Abs 3/6 rules only when they actually run in the tax base, the L16 generator reports LST_ABF in key 260 outside 210/220, and result_rules access in the garnishment paths avoids DefaultDictPayroll materialisation. Verified against the original statute texts in .ris/ (AngG §§ 23/23a, EStG § 67 Abs 3). Version 19.0.13.0.0 (private), 19.0.9.0.0 (core)
774 lines
34 KiB
Python
774 lines
34 KiB
Python
# 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. 219–220; 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. 223–233; 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 3–8
|
||
(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 196–209): je Kind 123 Stellen ab Pos. 1501
|
||
# (Block 1 = 1501–1623 … Block 15 = 3223–3345; 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='0–8 (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 + LST_ABF."""
|
||
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', 'LST_ABF'):
|
||
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 |