# 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 + LST_ABF_FREI.""" 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', 'LST_ABF_FREI'): 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