[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.
This commit is contained in:
@@ -1,17 +1,18 @@
|
||||
# Part of the odoo-at-payroll project. License: LGPL-3.
|
||||
{
|
||||
'name': 'Österreich - Personalverrechnung Kern (AT Payroll Core)',
|
||||
'version': '19.0.3.1.0',
|
||||
'version': '19.0.4.0.0',
|
||||
'author': 'ixsol',
|
||||
'website': 'https://gem360.at',
|
||||
'category': 'Human Resources/Payroll',
|
||||
'version': '19.0.4.0.0',
|
||||
'description': """
|
||||
Österreichischer Personalverrechnungs-Kern
|
||||
==========================================
|
||||
|
||||
Sektor-neutraler Bundes-Kern der österreichischen Personalverrechnung
|
||||
auf der Odoo-19 ``hr_payroll``-Engine. Wird konsumiert von
|
||||
``l10n_at_gemeinde_payroll`` (Bgld. GemBG) und später vom General-AT-
|
||||
``l10n_at_gemeinde_payroll`` (Bgld. GemBG) und vom General-AT-
|
||||
Produkt (``l10n_at_hr_payroll_private``). Grundlagen: ``personalverrechnung/``
|
||||
(PLAN-payroll-bgld.md, RECHTSQUELLEN-Bgld.md, PLAN-repo-split.md).
|
||||
|
||||
@@ -32,8 +33,27 @@ Produkt (``l10n_at_hr_payroll_private``). Grundlagen: ``personalverrechnung/``
|
||||
``rule_parameters_sv_2026.xml``; BV-Beitrag Abfertigung Neu
|
||||
(BMSVG § 6 Abs 1, ``at_sv_bv_dg``) als Parameter für das
|
||||
General-AT-Modul (GP5);
|
||||
- **Meldewesen/Belege (AP5/AP6, GP7):** ELDA-Fixsatz-Framework
|
||||
(``models/elda.py`` — Vorlauf/Schluss, SANR/SANZ, Cent-Formatter,
|
||||
ISO-8859-15), **Lohnzettel Finanz L16 + Arbeitsstättenmeldung**
|
||||
(``models/lohnzettel_l16.py`` — I1/L1 nach DM-ORG 42.7.0,
|
||||
Lohnzettelversion 28, KZ-245-Formel-Prüfung, BEST LF/94 bzw. AD/ST),
|
||||
Kind-Datenmodell für die FB+-Wartung (``models/kind.py``),
|
||||
Meldewesen-Stammdaten (BKNR/Lohnsteuernummer, GKZ am Partner,
|
||||
VSNR-Formatprüfung, eSV-Abmeldegrund-Mapping,
|
||||
``models/meldewesen.py``), Belege Lohnzettel AT/Monatsdienstliste/
|
||||
Monatsjournal (QWeb, ``models/monatsreports.py``);
|
||||
- deklarative TASY-Mapping-Tabelle und Import-Wizard (Preview,
|
||||
Sprungwarnung > 10 %, explizite Bestätigung, versionierte Werte);
|
||||
- **Meldewesen/Belege (AP5/AP6, GP7):** ELDA-Fixsatz-Framework
|
||||
(``models/elda.py``), Lohnzettel Finanz L16 + Arbeitsstättenmeldung
|
||||
(``models/lohnzettel_l16.py``, DM-ORG 42.7.0, PDF-verifizierte
|
||||
Satzstrukturen), Kind-Datenmodell für die FB+-Wartung
|
||||
(``models/kind.py`` — Nutzer-Entscheidung: FB+ rechnet NICHT im
|
||||
Lohnsteuerkern, Arbeitnehmerveranlagung), Meldewesen-Stammdaten
|
||||
(BKNR/Lohnsteuernummer, GKZ am Partner, VSNR-Formatprüfung, eSV-
|
||||
Abmeldegrund-Mapping), Belege Lohnzettel AT/Monatsdienstliste/
|
||||
Monatsjournal als QWeb (``report/belege_reports.xml``);
|
||||
- folgen nach Freigabe der Bgld.-APs: Meldewesen (L16/FinanzOnline,
|
||||
eSV/ELDA, AP5), Belege (AP6).
|
||||
|
||||
@@ -48,6 +68,8 @@ Kein VRV-Bezug: die Gemeinde-Buchhaltung (Anlage 3b, Ansatz/MVAG) läuft
|
||||
'data/rule_parameters_sv_2026.xml',
|
||||
'data/tasy_mapping.xml',
|
||||
'views/tasy_views.xml',
|
||||
'views/meldewesen_views.xml',
|
||||
'report/belege_reports.xml',
|
||||
],
|
||||
'countries': ['at'],
|
||||
'license': 'LGPL-3',
|
||||
|
||||
@@ -2,4 +2,7 @@
|
||||
from . import tasy
|
||||
from . import lohnsteuer
|
||||
from . import sozialversicherung
|
||||
from . import sozialversicherung
|
||||
from . import elda
|
||||
from . import meldewesen
|
||||
from . import kind
|
||||
from . import lohnzettel_l16
|
||||
@@ -0,0 +1,256 @@
|
||||
# Part of the odoo-at-payroll project. License: LGPL-3.
|
||||
"""ELDA-Fixsatz-Framework (GP7/AP5) — „Datenaustausch mit Dienstgebern"
|
||||
(DM-ORG 42.7.0, 07/2026; lokal `.firecrawl/ris/elda/`, unversioniert).
|
||||
|
||||
Dateikonventionen (DM-ORG Kapitel C.1/C.2, E.1–E.3 — verifiziert 2026-09-12):
|
||||
- Datei = optionaler Dateiheader (DHKZ 2×blank + DHVN 01 + ENCD) + mindestens
|
||||
ein Datenbestand; Datenbestand = Vorlaufsatz (SART 00, Feld VERS = Version
|
||||
der nachfolgenden Satzstrukturen) + Datensätze + Schlusssatz (SART 99,
|
||||
SANZ = Satzanzahl inkl. Vorlauf und Schluss).
|
||||
- Fixlängen-Sätze ohne Feldtrennzeichen; alle Sätze eines Bestands nehmen
|
||||
die maximale Satzlänge des Bestands an (RESE-Auffüllung). Sätze werden
|
||||
zeilenweise mit CRLF getrennt (die DM-ORG normiert KEIN explizites
|
||||
Satztrennzeichen — CRLF ist die Praxis der ELDA-Kanäle; Annahme im
|
||||
Parallellauf gegen den Mandanten-ELDA-Zugang verifizieren).
|
||||
- ``a``/``a/n``: linksbündig, Grundstellung blank; ``n``: rechtsbündig,
|
||||
Grundstellung 0, führende Nullen, KEINE Interpunktion (Beträge als
|
||||
EURO-Cent, kaufmännisch gerundet; Prozentsätze mit impliziten
|
||||
Nachkommastellen via ``skala``).
|
||||
- Encoding Fixlängen-Dateien: ISO-8859-15 (Standard-Encoding; der
|
||||
Dateiheader deklariert ENCD). Vorzeichen gehören in eigene V-Felder
|
||||
(RSVZ/Vxxx), ``n``-Felder sind nie negativ.
|
||||
"""
|
||||
from datetime import date, datetime
|
||||
from decimal import ROUND_HALF_UP, Decimal
|
||||
|
||||
L10N_AT_ELDA_ENCODING = 'ISO-8859-15'
|
||||
L10N_AT_ELDA_ZEILENTRENNER = '\r\n'
|
||||
|
||||
# Identifikationsteil (E.1, S. 174) — 20 Stellen, alle Satzarten gleich;
|
||||
# alle Angaben zwingend.
|
||||
L10N_AT_ELDA_IDTEIL = [
|
||||
(1, 'SART', 1, 2, 'a'), # Satzart
|
||||
(2, 'SANR', 3, 7, 'n'), # laufende Satznummer je Bestand, ab 1
|
||||
(3, 'UVST', 10, 2, 'a'), # datenübernehmender Träger (ED = ELDA)
|
||||
(4, 'OBUS', 12, 7, 'n'), # Ordnungsbegriff der übermittelnden Stelle
|
||||
(5, 'VSTR', 19, 2, 'a'), # zuständiger Träger (D.4)
|
||||
]
|
||||
|
||||
# Vorlaufsatz-Datenteil (E.2, S. 175–177, Version 08) — folgt dem IDTEIL;
|
||||
# zwingend außer HTEL/SOID/VNMF; Satzlänge = maximale Satzlänge des Bestands.
|
||||
L10N_AT_ELDA_VORLAUF = [
|
||||
(2, 'PROJ', 21, 2, 'a'), # DM; TM = Testdaten
|
||||
(3, 'BEST', 23, 2, 'a'), # Verarbeitung (MB/VR/LF/AD/MA …, E.2)
|
||||
(4, 'DTNR', 25, 6, 'a'), # Datenträgernummer (TP-Übermittlung: frei)
|
||||
(5, 'EDAT', 31, 8, 'n'), # TTMMJJJJ
|
||||
(6, 'EZEI', 39, 6, 'n'), # HHMMSS
|
||||
(7, 'HRST', 45, 45, 'a'), # Herstellername
|
||||
(8, 'HKFZ', 90, 3, 'a'), # Internationales Kfz-Kennzeichen (A)
|
||||
(9, 'HPLZ', 93, 7, 'a'),
|
||||
(10, 'HORT', 100, 20, 'a'),
|
||||
(11, 'HSTR', 120, 30, 'a'),
|
||||
(12, 'VERS', 150, 2, 'n'), # Version der Satzstrukturen (D.26)
|
||||
(13, 'HTEL', 152, 20, 'a'),
|
||||
(14, 'SOID', 172, 70, 'a'),
|
||||
(15, 'VNMF', 242, 5, 'a'), # Mitteilungsfile-Version
|
||||
]
|
||||
|
||||
# Schlusssatz-Datenteil (E.3, S. 178, Version 01) — nach IDTEIL;
|
||||
# SANZ zwingend, ELNR/HEMA optional.
|
||||
L10N_AT_ELDA_SCHLUSS = [
|
||||
(2, 'SANZ', 21, 6, 'n'), # Satzanzahl inkl. Vorlauf- und Schlusssatz
|
||||
(3, 'ELNR', 27, 6, 'n'),
|
||||
(4, 'HEMA', 33, 60, 'a'),
|
||||
]
|
||||
|
||||
|
||||
def l10n_at_euro_cent(betrag):
|
||||
"""EURO-Betrag → Cent (kaufmännisch gerundet; DM-ORG: keine
|
||||
Interpunktion)."""
|
||||
return int((Decimal(str(betrag)) * 100).quantize(
|
||||
Decimal('1'), rounding=ROUND_HALF_UP))
|
||||
|
||||
|
||||
class L10nAtEldaSatz:
|
||||
"""Ein Fixlängen-Satz: Felddaten (Nr, Name, Pos, Länge, Typ), zwingende
|
||||
Grundstellung je Typ, Rendering auf die Bestands-Satzlänge. Kein ORM —
|
||||
reine Bau-Hilfe der Meldungsgeneratoren (DmfA-Muster: Datensatz-Baum im
|
||||
Python-Teil, Validierung/Prüfprotokoll darüber)."""
|
||||
|
||||
def __init__(self, struktur, satzlaenge, bezeichnung=''):
|
||||
self.struktur = list(struktur)
|
||||
self.satzlaenge = satzlaenge
|
||||
self.bezeichnung = bezeichnung
|
||||
self._felder = {f[1]: f for f in self.struktur}
|
||||
self._werte = {}
|
||||
if self.struktur:
|
||||
letzte = max(f[2] + f[3] - 1 for f in self.struktur)
|
||||
if letzte > satzlaenge:
|
||||
raise ValueError(
|
||||
'Struktur %s endet bei Position %d — Satzlänge %d '
|
||||
'überschritten' % (bezeichnung, letzte, satzlaenge))
|
||||
|
||||
def setze(self, name, wert, cent=False, skala=0):
|
||||
"""Feld setzen; ``wert`` None übernimmt die Grundstellung.
|
||||
``cent``: EURO-Betrag → Cent; ``skala``: implizite Nachkommastellen
|
||||
(z. B. Prozentsatz mit 3 Nachkommastellen → skala=3)."""
|
||||
if wert is None:
|
||||
return self
|
||||
nr, fname, pos, laenge, typ = self._felder[name]
|
||||
if cent:
|
||||
wert = l10n_at_euro_cent(wert)
|
||||
if isinstance(wert, float):
|
||||
wert = int((Decimal(str(wert)) * (10 ** skala)).quantize(
|
||||
Decimal('1'), rounding=ROUND_HALF_UP))
|
||||
elif isinstance(wert, int):
|
||||
wert = wert * (10 ** skala)
|
||||
roh = str(wert)
|
||||
if typ == 'n':
|
||||
if not roh.isdigit():
|
||||
raise ValueError('Feld %s (%s): %r ist nicht numerisch' % (
|
||||
name, self.bezeichnung, roh))
|
||||
roh = roh.rjust(laenge, '0')
|
||||
else:
|
||||
roh = roh.ljust(laenge, ' ')
|
||||
if len(roh) > laenge:
|
||||
raise ValueError('Feld %s (%s): %r länger als %d' % (
|
||||
name, self.bezeichnung, roh, laenge))
|
||||
self._werte[name] = roh
|
||||
return self
|
||||
|
||||
def setze_datum(self, name, wert, fmt='TTMMJJJJ'):
|
||||
"""Datum in DM-ORG-Feldformat (Vorlaufsatz EDAT = TTMMJJJJ,
|
||||
L16-Übermittlungsfelder = JJJJMMTT, LZP/AGBD = TTMMJJ)."""
|
||||
if wert is None:
|
||||
return self
|
||||
if isinstance(wert, str):
|
||||
wert = datetime.strptime(wert, '%Y-%m-%d').date()
|
||||
formate = {'TTMMJJJJ': '%d%m%Y', 'JJJJMMTT': '%Y%m%d',
|
||||
'TTMMJJ': '%d%m%y'}
|
||||
if fmt not in formate:
|
||||
raise ValueError('Unbekanntes Datumsformat %s' % fmt)
|
||||
return self.setze(name, wert.strftime(formate[fmt]))
|
||||
|
||||
def setze_zeit(self, name, wert):
|
||||
if wert is None:
|
||||
return self
|
||||
return self.setze(name, wert.strftime('%H%M%S'))
|
||||
|
||||
def ist_gesetzt(self, name):
|
||||
return name in self._werte
|
||||
|
||||
def wert(self, name):
|
||||
return self._werte.get(name)
|
||||
|
||||
def render(self):
|
||||
"""Satz in Bestands-Satzlänge; unbelegte Felder in Grundstellung
|
||||
(``a`` blank / ``n`` 0), Positionen nach dem letzten Feld blank."""
|
||||
puffer = [' '] * self.satzlaenge
|
||||
for nr, name, pos, laenge, typ in self.struktur:
|
||||
roh = self._werte.get(name)
|
||||
if roh is None:
|
||||
roh = '0' * laenge if typ == 'n' else ' ' * laenge
|
||||
puffer[pos - 1:pos - 1 + laenge] = roh
|
||||
return ''.join(puffer)
|
||||
|
||||
|
||||
class L10nAtEldaBestand:
|
||||
"""Datenbestand: Vorlaufsatz + Datensätze + Schlusssatz zu EINER
|
||||
Verarbeitung (BEST) für EINEN zuständigen Träger (VSTR). SANR zählt
|
||||
lückenlos ab 1, SANZ inklusive Vorlauf/Schluss (Prüfvorschrift C.1.2);
|
||||
alle Sätze nehmen die maximale Satzlänge der Datensätze an."""
|
||||
|
||||
def __init__(self, best, vstr, vers, hersteller=None, test=False,
|
||||
obus='0000000', datensaetze=None):
|
||||
self.best = best
|
||||
self.vstr = vstr
|
||||
self.vers = vers
|
||||
self.hersteller = hersteller or {}
|
||||
self.projekt = 'TM' if test else 'DM'
|
||||
self.obus = obus
|
||||
self.datensaetze = datensaetze or []
|
||||
|
||||
def ergaenze(self, satz):
|
||||
self.datensaetze.append(satz)
|
||||
return satz
|
||||
|
||||
def render(self, erstellungsdatum=None):
|
||||
"""Zeilen des Bestands (ohne Zeilentrenner)."""
|
||||
if not self.datensaetze:
|
||||
raise ValueError('Datenbestand %s ohne Datensätze' % self.best)
|
||||
heute = erstellungsdatum or date.today()
|
||||
satzlaenge = max(s.satzlaenge for s in self.datensaetze)
|
||||
# Der Vorlaufsatz nimmt die Satzlänge des Bestands an — aber
|
||||
# nie kürzer als seine eigene Struktur (DM-ORG E.2: Vorlauf
|
||||
# trägt alle Herstellfelder; reale Bestände sind ≥ 246).
|
||||
satzlaenge = max(
|
||||
satzlaenge,
|
||||
max(f[2] + f[3] - 1 for f in
|
||||
(L10N_AT_ELDA_IDTEIL + L10N_AT_ELDA_VORLAUF)))
|
||||
zeilen = []
|
||||
vorlauf_struktur = [
|
||||
f for f in (L10N_AT_ELDA_IDTEIL + L10N_AT_ELDA_VORLAUF)
|
||||
if f[2] + f[3] - 1 <= satzlaenge]
|
||||
vorlauf = L10nAtEldaSatz(
|
||||
vorlauf_struktur, satzlaenge,
|
||||
bezeichnung='Vorlaufsatz %s' % self.best)
|
||||
vorlauf.setze('SART', '00')
|
||||
vorlauf.setze('SANR', 1)
|
||||
vorlauf.setze('UVST', 'ED')
|
||||
vorlauf.setze('OBUS', self.obus)
|
||||
vorlauf.setze('VSTR', self.vstr)
|
||||
vorlauf.setze('PROJ', self.projekt)
|
||||
vorlauf.setze('BEST', self.best)
|
||||
vorlauf.setze_datum('EDAT', heute, 'TTMMJJJJ')
|
||||
vorlauf.setze_zeit('EZEI', datetime.now())
|
||||
vorlauf.setze('HRST', (self.hersteller.get('name') or '')[:45])
|
||||
vorlauf.setze('HKFZ', self.hersteller.get('kfz', 'A'))
|
||||
vorlauf.setze('HPLZ', self.hersteller.get('plz'))
|
||||
vorlauf.setze('HORT', self.hersteller.get('ort'))
|
||||
vorlauf.setze('HSTR', self.hersteller.get('strasse'))
|
||||
vorlauf.setze('VERS', self.vers)
|
||||
vorlauf.setze('VNMF', '3.0')
|
||||
zeilen.append(vorlauf.render())
|
||||
for nummer, satz in enumerate(self.datensaetze, start=2):
|
||||
# Datensatz-Generatoren setzen ihre SART selbst; der Bestand
|
||||
# ergänzt Trägerangaben (falls offen) und die laufende
|
||||
# Satznummer (C.1.2: lückenlos ab 1).
|
||||
if not satz.ist_gesetzt('UVST'):
|
||||
satz.setze('UVST', 'ED')
|
||||
if not satz.ist_gesetzt('OBUS'):
|
||||
satz.setze('OBUS', self.obus)
|
||||
if not satz.ist_gesetzt('VSTR'):
|
||||
satz.setze('VSTR', self.vstr)
|
||||
satz._werte['SANR'] = str(nummer).rjust(7, '0')
|
||||
zeilen.append(satz.render())
|
||||
schluss = L10nAtEldaSatz(
|
||||
L10N_AT_ELDA_IDTEIL + L10N_AT_ELDA_SCHLUSS, satzlaenge,
|
||||
bezeichnung='Schlusssatz %s' % self.best)
|
||||
schluss.setze('SART', '99')
|
||||
schluss.setze('SANR', len(self.datensaetze) + 2)
|
||||
schluss.setze('UVST', 'ED')
|
||||
schluss.setze('OBUS', self.obus)
|
||||
schluss.setze('VSTR', self.vstr)
|
||||
schluss.setze('SANZ', len(self.datensaetze) + 2)
|
||||
zeilen.append(schluss.render())
|
||||
return zeilen
|
||||
|
||||
|
||||
class L10nAtEldaDatei:
|
||||
"""ELDA-Datei: Dateiheader (ENCD-Standard ISO-8859-15) + Datenbestände;
|
||||
Rendering in Bytes (Encoding + CRLF-Zeilentrenner, letzte Zeile mit
|
||||
Abschluss-Trenner)."""
|
||||
|
||||
def __init__(self, bestaende=None, header=True):
|
||||
self.bestaende = bestaende or []
|
||||
self.header = header
|
||||
|
||||
def render(self):
|
||||
zeilen = []
|
||||
if self.header:
|
||||
zeilen.append(' 01' + L10N_AT_ELDA_ENCODING.ljust(50))
|
||||
for bestand in self.bestaende:
|
||||
zeilen.extend(bestand.render())
|
||||
roh = L10N_AT_ELDA_ZEILENTRENNER.join(zeilen) \
|
||||
+ L10N_AT_ELDA_ZEILENTRENNER
|
||||
return roh.encode(L10N_AT_ELDA_ENCODING)
|
||||
@@ -0,0 +1,104 @@
|
||||
# Part of the odoo-at-payroll project. License: LGPL-3.
|
||||
"""Kind-Datenmodell (GP7): Kinder je Mitarbeiter für die Wartung des
|
||||
Familienbonus Plus und die Befüllung des L16-Kinderblocks
|
||||
(Lohnzettel-Finanz-Mitteilungssatz, DM-ORG 42.7.0 E.14, Felder
|
||||
Nr. 196–209: je Kind 123 Stellen — KFAM/KVON/KSTAAT/KSTWE/KVSNR/
|
||||
KGEBD/KAFBZ/KAPFB/KAUHZ/KBGFP/KEGFP/KBHFP/KEHFP).
|
||||
|
||||
Nutzer-Entscheidung 2026-09-12: die FB+-Steuerwirkung selbst ist KEIN
|
||||
Bestandteil der Personalverrechnung (bei Unterschreiten der Steuer auf 0
|
||||
erfolgt die Geltendmachung über die Arbeitnehmerveranlagung) — das Modell
|
||||
dient der Stammdaten-Wartung (Anspruch, Monate, halber Bonus) und der
|
||||
L16-Meldung. Monatsfelder im Format MM (Kinderblock 2n); die
|
||||
Betragsermittlung (FABO) rechnet der L16-Generator aus den gepflegten
|
||||
Monaten und der Altersstaffel (§ 33 Abs 5 EStG: bis 18. Geburtstag
|
||||
166,68 €/Monat, darüber 58,34 €/Monat solange Familienbeihilfe)."""
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class HrEmployee(models.Model):
|
||||
_inherit = 'hr.employee'
|
||||
|
||||
l10n_at_payroll_kind_ids = fields.One2many(
|
||||
'l10n.at.payroll.kind', 'employee_id',
|
||||
string='Kinder (Familienbonus Plus)',
|
||||
groups="hr_payroll.group_hr_payroll_user")
|
||||
|
||||
|
||||
class L10nAtPayrollKind(models.Model):
|
||||
_name = 'l10n.at.payroll.kind'
|
||||
_description = 'Kind (Familienbonus Plus / Lohnzettel L16)'
|
||||
_order = 'geburtstag, id'
|
||||
|
||||
employee_id = fields.Many2one(
|
||||
'hr.employee', string='Mitarbeiter:in', required=True,
|
||||
index=True, ondelete='cascade')
|
||||
familienname = fields.Char(
|
||||
string='Familienname (KFAM)', required=True)
|
||||
vorname = fields.Char(string='Vorname (KVON)')
|
||||
geburtstag = fields.Date(
|
||||
string='Geburtsdatum (KGEBD)', required=True,
|
||||
help='Maßgeblich für die FB+-Staffel (bis zum 18. Geburtstag '
|
||||
'166,68 €/Monat, danach 58,34 €/Monat solange '
|
||||
'Familienbeihilfe) und die Altersfilter (z. B. '
|
||||
'Schwerarbeitsmeldung 40 M/35 F).')
|
||||
versicherungsnummer = fields.Char(
|
||||
string='Versicherungsnummer (KVSNR)',
|
||||
help='10-stellig, LLLPTTMMJJ; nur wenn zugeteilt.')
|
||||
staatsangehoerigkeit = fields.Char(
|
||||
string='Staatsangehörigkeit (KSTAAT, 3-stelliger Code)')
|
||||
staatsangehoerigkeit_2 = fields.Char(
|
||||
string='2. Staatsangehörigkeit (KSTWE, 1-stelliger Code)')
|
||||
# ------------------------------------------------------------------
|
||||
# Familienbonus Plus — Wartung (L16-Kinderblock)
|
||||
# ------------------------------------------------------------------
|
||||
fab_plus = fields.Boolean(
|
||||
string='Familienbonus Plus geltend gemacht',
|
||||
help='L16 KFABO (Anzahl) und FABO (Betrag) ergeben sich aus den '
|
||||
'Kindern mit gesetztem FB+-Kennzeichen.')
|
||||
fab_monat_von = fields.Integer(
|
||||
string='FB+ ab Monat (KBGFP)',
|
||||
help='Monat (1–12), ab dem der volle Familienbonus Plus für '
|
||||
'dieses Kind geltend gemacht wurde (L16 KBGFP).')
|
||||
fab_monat_bis = fields.Integer(
|
||||
string='FB+ bis Monat (KEGFP)',
|
||||
help='Letzter Monat (1–12) des vollen Familienbonus Plus (L16 '
|
||||
'KEGFP).')
|
||||
fab_halb_monat_von = fields.Integer(
|
||||
string='Halber FB+ ab Monat (KBHFP)',
|
||||
help='Monat (1–12), ab dem nur mehr der halbe Familienbonus Plus '
|
||||
'geltend gemacht wurde (Aufteilung zwischen den '
|
||||
'Elternteilen; L16 KBHFP).')
|
||||
fab_halb_monat_bis = fields.Integer(
|
||||
string='Halber FB+ bis Monat (KEHFP)',
|
||||
help='Letzter Monat (1–12) des halben Familienbonus Plus (L16 '
|
||||
'KEHFP).')
|
||||
|
||||
def _l10n_at_fab_monate(self, jahr):
|
||||
"""[(Monat, Anteil 1.0/0.5), …] des FB+ im Lohnzetteljahr aus den
|
||||
gepflegten Monatsfeldern (voll + halb, Überschneidung: der
|
||||
spätere Eintrag gewinnt)."""
|
||||
self.ensure_one()
|
||||
monate = {}
|
||||
von, bis = self.fab_monat_von or 0, self.fab_monat_bis or 0
|
||||
if von and bis and 1 <= von <= bis <= 12:
|
||||
for monat in range(von, bis + 1):
|
||||
monate[monat] = 1.0
|
||||
hvon, hbis = (self.fab_halb_monat_von or 0,
|
||||
self.fab_halb_monat_bis or 0)
|
||||
if hvon and hbis and 1 <= hvon <= hbis <= 12:
|
||||
for monat in range(hvon, hbis + 1):
|
||||
monate[monat] = 0.5
|
||||
return sorted(monate.items())
|
||||
|
||||
def _l10n_at_fab_betrag(self, jahr, voll=166.68, vermindert=58.34):
|
||||
"""FB+-Jahresbetrag je Kind aus Monaten × Monatsrate; die Rate
|
||||
wechselt mit dem 18. Geburtstag (monatliche Staffel: Monate vor
|
||||
dem Geburtsmonat + 18 Jahre voller Satz, danach verminderter
|
||||
Satz — § 33 Abs 5 EStG, solange Familienbeihilfe)."""
|
||||
self.ensure_one()
|
||||
if not self.geburtstag or not self.fab_plus:
|
||||
return 0.0
|
||||
umschalt = self.geburtstag.month
|
||||
return sum(anteil * (vermindert if monat >= umschalt else voll)
|
||||
for monat, anteil in self._l10n_at_fab_monate(jahr))
|
||||
@@ -0,0 +1,774 @@
|
||||
# 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."""
|
||||
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
|
||||
@@ -0,0 +1,96 @@
|
||||
# Part of the odoo-at-payroll project. License: LGPL-3.
|
||||
"""Meldewesen-Stammdaten (GP7/AP5): ELDA-/FinOnl-Identifikation der
|
||||
Dienstgeberin und der Arbeitsstätten.
|
||||
|
||||
- ``res.company``: Beitragskontonummer (BKNR — ELDA-Feld, Präfix =
|
||||
KV-Träger) und Lohnsteuernummer (9-stellig: 2 Stellen Finanzamt + 7
|
||||
Stellen Nummer; L16-I1 STNRA/STNRL, Arbeitsstättenmeldung STNR).
|
||||
- ``res.partner``: österreichische Gemeindekennziffer (AGKZ) für die
|
||||
Arbeitsstättenmeldung nach § 34 Abs 6 ASVG — Odoo hält keine GKZ, das
|
||||
Feld ist mandantenseitig zu pflegen (Warnung des L16-Laufs, wenn es am
|
||||
Dienstort-Partner fehlt).
|
||||
- ``hr.version``: Kennzeichen fallweise Beschäftigung (§ 33 Abs 1a
|
||||
Z 1 ASVG — tagesgenaue vereinfachte Anmeldung vor Arbeitsantritt,
|
||||
DM-ORG BEST MA) und VSNR-Formatprüfung (10-stellig LLLPTTMMJJ;
|
||||
Prüfzifferalgorithmus der SVS ist nicht beschafft → nur Format/
|
||||
Plausibilität, Rest dokumentiert).
|
||||
- ``hr.departure.reason``: Mapping auf den eSV-Abmeldegrund (AGRD,
|
||||
DM-ORG D.22; Standard ``00`` sonstiger Grund mit Ende des
|
||||
Beschäftigungsverhältnisses)."""
|
||||
from odoo import api, fields, models
|
||||
|
||||
from odoo.exceptions import ValidationError
|
||||
|
||||
|
||||
class ResCompany(models.Model):
|
||||
_inherit = 'res.company'
|
||||
|
||||
l10n_at_beitragskontonummer = fields.Char(
|
||||
string='AT Beitragskontonummer (BKNR)',
|
||||
help='eSV-Beitragskontonummer des Dienstgebers (10 Stellen;\n'
|
||||
'die ersten Stellen codieren den KV-Träger). Pflichtfeld\n'
|
||||
'jeder ELDA-Meldung (mBGM-Paket BKNR, Versichertenmeldung,\n'
|
||||
'Arbeitsstättenmeldung, fallweise Anmeldung).')
|
||||
l10n_at_lohnsteuernummer = fields.Char(
|
||||
string='AT Lohnsteuernummer',
|
||||
help='9-stellig: 2 Stellen Finanzamtsnummer + 7 Stellen\n'
|
||||
'Steuernummer (Lohnzettel Finanz I1 STNRA/STNRL;\n'
|
||||
'Arbeitsstättenmeldung STNR).')
|
||||
|
||||
|
||||
class ResPartner(models.Model):
|
||||
_inherit = 'res.partner'
|
||||
|
||||
l10n_at_gemeindekennziffer = fields.Char(
|
||||
string='AT Gemeindekennziffer (GKZ)',
|
||||
help='5-stellige österreichische Gemeindekennzahl für die\n'
|
||||
'Arbeitsstättenmeldung nach § 34 Abs 6 ASVG (Feld AGKZ).\n'
|
||||
'Am Dienstort-Partner des Mitarbeiters pflegen.')
|
||||
|
||||
|
||||
class HrVersion(models.Model):
|
||||
_inherit = 'hr.version'
|
||||
|
||||
l10n_at_fallweise = fields.Boolean(
|
||||
string='AT Fallweise Beschäftigung',
|
||||
groups="hr_payroll.group_hr_payroll_user", tracking=True,
|
||||
help='Fallweise Beschäftigung (§ 33 Abs 1a Z 1 ASVG): je\n'
|
||||
'Beschäftigungstag ist die vereinfachte Anmeldung vor\n'
|
||||
'Arbeitsantritt zu übermitteln (ELDA BEST MA, Satzarten\n'
|
||||
'32/33); An-/Abmeldung laufen über die mBGM (G3/G4).')
|
||||
|
||||
@api.constrains('ssnid')
|
||||
def _l10n_at_vsnr_format(self):
|
||||
"""VSNR-Formatprüfung LLLPTTMMJJ (10 Stellen, Ziffern, plausibles
|
||||
Geburtsdatum TTMMJJ an den Stellen 5–10). Der
|
||||
Prüfzifferalgorithmus der SVS ist nicht beschafft → nur
|
||||
Format/Plausibilität (Rest dokumentiert; die Meldungsgeneratoren
|
||||
listen ungültige VSNR listenweise als UserError)."""
|
||||
for version in self:
|
||||
if not version.ssnid:
|
||||
continue
|
||||
roh = version.ssnid.strip()
|
||||
if len(roh) != 10 or not roh.isdigit():
|
||||
raise ValidationError(
|
||||
'Versicherungsnummer %r: 10 Ziffern im Format LLLPTTMMJJ '
|
||||
'erwartet.' % version.ssnid)
|
||||
try:
|
||||
tag = int(roh[4:6])
|
||||
monat = int(roh[6:8])
|
||||
if not 1 <= tag <= 31 or not 1 <= monat <= 12:
|
||||
raise ValueError()
|
||||
except ValueError:
|
||||
raise ValidationError(
|
||||
'Versicherungsnummer %r enthält kein plausibles '
|
||||
'Geburtsdatum (TTMMJJ an den Stellen 5–10).'
|
||||
% version.ssnid)
|
||||
|
||||
|
||||
class HrDepartureReason(models.Model):
|
||||
_inherit = 'hr.departure.reason'
|
||||
|
||||
l10n_at_agrd = fields.Char(
|
||||
string='AT eSV-Abmeldegrund (AGRD)',
|
||||
help='2-stelliger Abmeldegrund-Code der Versichertenmeldung '
|
||||
'(DM-ORG D.22); Standard 00 „Sonstiger Grund mit Ende des '
|
||||
'Beschäftigungsverhältnisses“.')
|
||||
@@ -0,0 +1,90 @@
|
||||
# Part of the odoo-at-payroll project. License: LGPL-3.
|
||||
"""Monatsdienstliste und Monatsjournal (GP7/AP6): QWeb-Datenlieferanten
|
||||
auf dem Lohnlauf (hr.payslip.run) — voraberechnete Aggregation über
|
||||
`hr.payslip.line` (read_group-Muster des Contribution Register), damit
|
||||
die Templates reine Darstellung bleiben.
|
||||
|
||||
- **Monatsdienstliste**: je AN eine Zeile mit den mBGM-Kontrollgrößen
|
||||
(Versicherungstage/Beitragsgrundlagen je Beitragsmonat, DN- und
|
||||
DG-Beiträge, DG-Anteile) — Abstimmung Abrechnung ↔ SV-Meldung.
|
||||
- **Monatsjournal**: je Lohnart die Summe über den Lauf (Brutto,
|
||||
Abzüge, DG-Anteile, Netto) mit Kontrollsummen gegen die
|
||||
GP6-Buchung."""
|
||||
from odoo import models
|
||||
|
||||
|
||||
class L10nAtPayrollMonatsReports(models.AbstractModel):
|
||||
_name = 'report.l10n_at_hr_payroll.report_monatsdienstliste'
|
||||
_description = 'Monatsdienstliste (GP7/AP6)'
|
||||
|
||||
def _get_report_values(self, docids, data=None):
|
||||
runs = self.env['hr.payslip.run'].browse(docids)
|
||||
zeilen = []
|
||||
for run in runs:
|
||||
for slip in run.slip_ids.filtered(
|
||||
lambda p: p.state not in ('draft', 'cancel')):
|
||||
version = slip.version_id
|
||||
basen = slip._l10n_at_sv_basis_laufen_monate()
|
||||
sv_dn = sum(
|
||||
slip._l10n_at_svdn(k) for k in
|
||||
('kv', 'pv', 'av', 'ak', 'wf'))
|
||||
sv_dg = slip._l10n_at_svdg_gesamt()
|
||||
sz_dn = slip._l10n_at_svdn_sz()
|
||||
sz_basis = slip._l10n_at_svdn_basis_sz()
|
||||
zeilen.append({
|
||||
'slip': slip,
|
||||
'versicherungsnummer': version.ssnid if version else '',
|
||||
'personalgruppe': getattr(
|
||||
version, 'l10n_at_personalgruppe', '')
|
||||
if version else '',
|
||||
'geringfuegig': slip.l10n_at_geringfuegig,
|
||||
'basis_laufen': sum(
|
||||
b for _m, b in basen),
|
||||
'sz_basis': sz_basis,
|
||||
'sv_dn': sv_dn,
|
||||
'sv_dg': sv_dg,
|
||||
'sv_dn_sz': sz_dn,
|
||||
'lohnsteuer': sum(
|
||||
slip.line_ids.filtered(
|
||||
lambda l: l.code in ('LST_LFD', 'LST_SZ')
|
||||
).mapped('total')),
|
||||
'netto': sum(
|
||||
slip.line_ids.filtered(
|
||||
lambda l: l.code == 'NET').mapped('total')),
|
||||
'brutto': sum(
|
||||
slip.line_ids.filtered(
|
||||
lambda l: l.salary_rule_id.l10n_at_bemessung
|
||||
!= 'neutral').mapped('total')),
|
||||
})
|
||||
return {'docs': runs, 'zeilen': zeilen}
|
||||
|
||||
|
||||
class L10nAtPayrollMonatsjournal(models.AbstractModel):
|
||||
_name = 'report.l10n_at_hr_payroll.report_monatsjournal'
|
||||
_description = 'Monatsjournal (GP7/AP6)'
|
||||
|
||||
def _get_report_values(self, docids, data=None):
|
||||
runs = self.env['hr.payslip.run'].browse(docids)
|
||||
zeilen = []
|
||||
for run in runs:
|
||||
gruppen = {}
|
||||
for slip in run.slip_ids.filtered(
|
||||
lambda p: p.state not in ('draft', 'cancel')):
|
||||
for line in slip.line_ids:
|
||||
regel = line.salary_rule_id
|
||||
if not line.total or regel.l10n_at_bemessung \
|
||||
== 'neutral':
|
||||
continue
|
||||
schluessel = (regel.code, regel.name,
|
||||
regel.l10n_at_bemessung)
|
||||
eintrag = gruppen.setdefault(schluessel, 0.0)
|
||||
gruppen[schluessel] = eintrag + line.total
|
||||
zeilen.append({
|
||||
'run': run,
|
||||
'arten': [
|
||||
{'code': code, 'name': name, 'bemessung': bemessung,
|
||||
'summe': summe}
|
||||
for (code, name, bemessung), summe
|
||||
in sorted(gruppen.items())],
|
||||
})
|
||||
return {'docs': runs, 'journal': zeilen}
|
||||
@@ -0,0 +1,203 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<!-- ============================================================ -->
|
||||
<!-- GP7/AP6 Belege: Lohnzettel AT (am Lohnzettel/Payslip), -->
|
||||
<!-- Monatsdienstliste + Monatsjournal (am Lohnlauf). -->
|
||||
<!-- ============================================================ -->
|
||||
|
||||
<record id="action_report_lohnzettel_at" model="ir.actions.report">
|
||||
<field name="name">Lohnzettel (AT)</field>
|
||||
<field name="model">hr.payslip</field>
|
||||
<field name="report_type">qweb-pdf</field>
|
||||
<field name="report_name">l10n_at_hr_payroll.report_lohnzettel_at</field>
|
||||
<field name="report_file">l10n_at_hr_payroll.report_lohnzettel_at</field>
|
||||
<field name="paperformat_id" ref="hr_payroll.paperformat_euro_light"/>
|
||||
<field name="binding_model_id" ref="hr_payroll.model_hr_payslip"/>
|
||||
<field name="binding_type">report</field>
|
||||
</record>
|
||||
|
||||
<record id="action_report_monatsdienstliste" model="ir.actions.report">
|
||||
<field name="name">Monatsdienstliste (SV-Kontrolle)</field>
|
||||
<field name="model">hr.payslip.run</field>
|
||||
<field name="report_type">qweb-pdf</field>
|
||||
<field name="report_name">l10n_at_hr_payroll.report_monatsdienstliste</field>
|
||||
<field name="report_file">l10n_at_hr_payroll.report_monatsdienstliste</field>
|
||||
<field name="paperformat_id" ref="hr_payroll.paperformat_euro_light"/>
|
||||
<field name="binding_model_id" ref="hr_payroll.model_hr_payslip_run"/>
|
||||
<field name="binding_type">report</field>
|
||||
</record>
|
||||
|
||||
<record id="action_report_monatsjournal" model="ir.actions.report">
|
||||
<field name="name">Monatsjournal</field>
|
||||
<field name="model">hr.payslip.run</field>
|
||||
<field name="report_type">qweb-pdf</field>
|
||||
<field name="report_name">l10n_at_hr_payroll.report_monatsjournal</field>
|
||||
<field name="report_file">l10n_at_hr_payroll.report_monatsjournal</field>
|
||||
<field name="paperformat_id" ref="hr_payroll.paperformat_euro_light"/>
|
||||
<field name="binding_model_id" ref="hr_payroll.model_hr_payslip_run"/>
|
||||
<field name="binding_type">report</field>
|
||||
</record>
|
||||
|
||||
<!-- Lohnzettel AT: AT-Layout — Lohnarten nach Kategorie, DN-Abzüge,
|
||||
DG-Anteile (SV-DG, FLAF, KommSt, DZ, BV), Netto. -->
|
||||
<template id="report_lohnzettel_at">
|
||||
<t t-call="web.html_container">
|
||||
<t t-foreach="docs" t-as="slip">
|
||||
<t t-call="web.external_layout">
|
||||
<div class="page">
|
||||
<h2>Lohnzettel</h2>
|
||||
<table class="table table-sm">
|
||||
<tr>
|
||||
<td><strong>Mitarbeiter:in</strong></td>
|
||||
<td><t t-out="slip.employee_id.name"/></td>
|
||||
<td><strong>Versicherungsnummer</strong></td>
|
||||
<td><t t-out="slip.version_id.ssnid or ''"/></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Abrechnungsperiode</strong></td>
|
||||
<td><t t-out="slip.date_from"/> – <t t-out="slip.date_to"/></td>
|
||||
<td><strong>Personalgruppe</strong></td>
|
||||
<td><t t-out="slip.version_id.l10n_at_personalgruppe or ''"/></td>
|
||||
</tr>
|
||||
</table>
|
||||
<h3>Bezüge</h3>
|
||||
<table class="table table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Lohnart</th>
|
||||
<th class="text-end">Betrag</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr t-foreach="slip.line_ids.filtered(lambda l: l.total and l.salary_rule_id.l10n_at_bemessung != 'neutral' and l.salary_rule_id.category_id.code == 'ATP_BRUTTO')" t-as="line">
|
||||
<td><t t-out="line.name"/></td>
|
||||
<td class="text-end">
|
||||
<span t-out="line.total" t-options='{"widget": "monetary", "display_currency": slip.currency_id}'/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Brutto</strong></td>
|
||||
<td class="text-end">
|
||||
<strong><span t-out="sum(slip.line_ids.filtered(lambda l: l.total and l.salary_rule_id.l10n_at_bemessung != 'neutral' and l.salary_rule_id.category_id.code == 'ATP_BRUTTO').mapped('total'))" t-options='{"widget": "monetary", "display_currency": slip.currency_id}'/></strong>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h3>Abzüge (Dienstnehmer:in)</h3>
|
||||
<table class="table table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Abzug</th>
|
||||
<th class="text-end">Betrag</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr t-foreach="slip.line_ids.filtered(lambda l: l.total and l.salary_rule_id.category_id.code == 'ATP_ABZUG')" t-as="line">
|
||||
<td><t t-out="line.name"/></td>
|
||||
<td class="text-end">
|
||||
<span t-out="-line.total" t-options='{"widget": "monetary", "display_currency": slip.currency_id}'/>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h3>Dienstgeberanteile</h3>
|
||||
<table class="table table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>DG-Komponente</th>
|
||||
<th class="text-end">Betrag</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr t-foreach="slip.line_ids.filtered(lambda l: l.total and l.salary_rule_id.category_id.code == 'ATP_DG')" t-as="line">
|
||||
<td><t t-out="line.name"/></td>
|
||||
<td class="text-end">
|
||||
<span t-out="line.total" t-options='{"widget": "monetary", "display_currency": slip.currency_id}'/>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p class="text-end h4">
|
||||
<strong>Netto:</strong>
|
||||
<span t-out="sum(slip.line_ids.filtered(lambda l: l.code == 'NET').mapped('total'))" t-options='{"widget": "monetary", "display_currency": slip.currency_id}'/>
|
||||
</p>
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
</t>
|
||||
</template>
|
||||
|
||||
<!-- Monatsdienstliste: je AN die mBGM-Kontrollgrößen -->
|
||||
<template id="report_monatsdienstliste">
|
||||
<t t-call="web.html_container">
|
||||
<t t-foreach="docs" t-as="run">
|
||||
<t t-call="web.external_layout">
|
||||
<div class="page">
|
||||
<h2>Monatsdienstliste — <t t-out="run.name"/></h2>
|
||||
<table class="table table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Mitarbeiter:in</th>
|
||||
<th>VSNR</th>
|
||||
<th>Gruppe</th>
|
||||
<th class="text-end">BG laufend</th>
|
||||
<th class="text-end">BG SZ</th>
|
||||
<th class="text-end">SV-DN</th>
|
||||
<th class="text-end">SV-DN SZ</th>
|
||||
<th class="text-end">SV-DG</th>
|
||||
<th class="text-end">Lohnsteuer</th>
|
||||
<th class="text-end">Netto</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr t-foreach="zeilen" t-as="z">
|
||||
<td><t t-out="z['slip'].employee_id.name"/></td>
|
||||
<td><t t-out="z['versicherungsnummer']"/></td>
|
||||
<td><t t-out="z['personalgruppe']"/><t t-if="z['geringfuegig']" t-out=" (geringf.)"/></td>
|
||||
<td class="text-end"><span t-out="z['basis_laufen']" t-options='{"widget": "monetary", "display_currency": run.company_id.currency_id}'/></td>
|
||||
<td class="text-end"><span t-out="z['sz_basis']" t-options='{"widget": "monetary", "display_currency": run.company_id.currency_id}'/></td>
|
||||
<td class="text-end"><span t-out="z['sv_dn']" t-options='{"widget": "monetary", "display_currency": run.company_id.currency_id}'/></td>
|
||||
<td class="text-end"><span t-out="z['sv_dn_sz']" t-options='{"widget": "monetary", "display_currency": run.company_id.currency_id}'/></td>
|
||||
<td class="text-end"><span t-out="z['sv_dg']" t-options='{"widget": "monetary", "display_currency": run.company_id.currency_id}'/></td>
|
||||
<td class="text-end"><span t-out="z['lohnsteuer']" t-options='{"widget": "monetary", "display_currency": run.company_id.currency_id}'/></td>
|
||||
<td class="text-end"><span t-out="z['netto']" t-options='{"widget": "monetary", "display_currency": run.company_id.currency_id}'/></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
</t>
|
||||
</template>
|
||||
|
||||
<!-- Monatsjournal: je Lohnart die Summe über den Lauf -->
|
||||
<template id="report_monatsjournal">
|
||||
<t t-call="web.html_container">
|
||||
<t t-foreach="journal" t-as="j">
|
||||
<t t-call="web.external_layout">
|
||||
<div class="page">
|
||||
<h2>Monatsjournal — <t t-out="j['run'].name"/></h2>
|
||||
<table class="table table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Code</th>
|
||||
<th>Lohnart</th>
|
||||
<th>Bemessung</th>
|
||||
<th class="text-end">Summe</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr t-foreach="j['arten']" t-as="art">
|
||||
<td><t t-out="art['code']"/></td>
|
||||
<td><t t-out="art['name']"/></td>
|
||||
<td><t t-out="art['bemessung']"/></td>
|
||||
<td class="text-end"><span t-out="art['summe']" t-options="{"widget": "monetary", "display_currency": j['run'].company_id.currency_id}"/></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
</t>
|
||||
</template>
|
||||
</odoo>
|
||||
@@ -2,4 +2,9 @@ id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
access_l10n_at_payroll_tasy_mapping_user,L10n AT Payroll TASY-Mapping User,model_l10n_at_payroll_tasy_mapping,hr_payroll.group_hr_payroll_user,1,0,0,0
|
||||
access_l10n_at_payroll_tasy_mapping_manager,L10n AT Payroll TASY-Mapping Manager,model_l10n_at_payroll_tasy_mapping,hr_payroll.group_hr_payroll_manager,1,1,1,1
|
||||
access_l10n_at_payroll_tasy_import_wizard,L10n AT Payroll TASY-Import Wizard,model_l10n_at_payroll_tasy_import_wizard,hr_payroll.group_hr_payroll_user,1,1,1,1
|
||||
access_l10n_at_payroll_tasy_import_line,L10n AT Payroll TASY-Import Line,model_l10n_at_payroll_tasy_import_line,hr_payroll.group_hr_payroll_user,1,1,1,1
|
||||
access_l10n_at_payroll_tasy_import_line,L10n AT Payroll TASY-Import Line,model_l10n_at_payroll_tasy_import_line,hr_payroll.group_hr_payroll_user,1,1,1,1
|
||||
access_l10n_at_payroll_l16_user,L10n AT Payroll L16 User,model_l10n_at_payroll_l16,hr_payroll.group_hr_payroll_user,1,1,1,0
|
||||
access_l10n_at_payroll_l16_manager,L10n AT Payroll L16 Manager,model_l10n_at_payroll_l16,hr_payroll.group_hr_payroll_manager,1,1,1,1
|
||||
access_l10n_at_payroll_l16_line_user,L10n AT Payroll L16-Line User,model_l10n_at_payroll_l16_line,hr_payroll.group_hr_payroll_user,1,1,1,1
|
||||
access_l10n_at_payroll_kind_user,L10n AT Payroll Kind User,model_l10n_at_payroll_kind,hr_payroll.group_hr_payroll_user,1,1,1,1
|
||||
access_l10n_at_payroll_kind_manager,L10n AT Payroll Kind Manager,model_l10n_at_payroll_kind,hr_payroll.group_hr_payroll_manager,1,1,1,1
|
||||
|
@@ -1,4 +1,5 @@
|
||||
# Part of the odoo-at-payroll project. License: LGPL-3.
|
||||
from . import test_tasy_import
|
||||
from . import test_lohnsteuer
|
||||
from . import test_sozialversicherung
|
||||
from . import test_sozialversicherung
|
||||
from . import test_elda_l16
|
||||
@@ -0,0 +1,239 @@
|
||||
# Part of the odoo-at-payroll project. License: LGPL-3.
|
||||
"""GP7 Kern: ELDA-Fixsatz-Framework (DM-ORG 42.7.0 Konventionen) und
|
||||
Lohnzettel-Finanz-Generator (I1/L1, BEST LF/94) — Positionsexakte
|
||||
Satz-Assertions, CRLF/ISO-8859-15-Encoding, SANR/SANZ-Kette,
|
||||
KZ-245-Formel-Konsistenz aus GP3-Daten."""
|
||||
import base64
|
||||
import datetime
|
||||
|
||||
from odoo.tests import TransactionCase, tagged
|
||||
|
||||
from odoo.addons.l10n_at_hr_payroll.models.elda import (
|
||||
L10N_AT_ELDA_IDTEIL,
|
||||
L10nAtEldaBestand,
|
||||
L10nAtEldaDatei,
|
||||
L10nAtEldaSatz,
|
||||
l10n_at_euro_cent,
|
||||
)
|
||||
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
class EldaFrameworkTest(TransactionCase):
|
||||
|
||||
def test_01_feldformatierung(self):
|
||||
"""a/n: linksbündig blank-gefüllt; n: rechtsbündig mit führenden
|
||||
Nullen, keine Interpunktion; Cent kaufmännisch; skala=3 für
|
||||
Prozentsätze (39,30 % → 039300)."""
|
||||
satz = L10nAtEldaSatz(
|
||||
[(1, 'A', 1, 5, 'a'), (2, 'N', 6, 4, 'n'),
|
||||
(3, 'B', 10, 10, 'n')], 20)
|
||||
satz.setze('A', 'AB')
|
||||
satz.setze('N', 42)
|
||||
self.assertEqual(satz.render()[:5], 'AB ')
|
||||
self.assertEqual(satz.render()[5:9], '0042')
|
||||
# Unbelegtes n-Feld: Grundstellung 0.
|
||||
self.assertEqual(satz.render()[9:19], '0' * 10)
|
||||
self.assertEqual(l10n_at_euro_cent(1234.567), 123457)
|
||||
self.assertEqual(l10n_at_euro_cent(0.005), 1)
|
||||
prozent = L10nAtEldaSatz(
|
||||
[(1, 'VPTA', 1, 6, 'n')], 6)
|
||||
prozent.setze('VPTA', 39.30, skala=3)
|
||||
self.assertEqual(prozent.render(), '039300')
|
||||
|
||||
def test_02_bestand_datei(self):
|
||||
"""Vorlaufsatz SART 00 mit BEST/VSTR/VERS, SANR lückenlos ab 1,
|
||||
SANZ inklusive Vorlauf/Schluss, CRLF-getrennte Zeilen in
|
||||
ISO-8859-15, optionaler Dateiheader ENCD."""
|
||||
satz1 = L10nAtEldaSatz(
|
||||
L10N_AT_ELDA_IDTEIL + [(2, 'X', 21, 4, 'a')], 24,
|
||||
bezeichnung='Test')
|
||||
satz1.setze('SART', '45')
|
||||
satz1.setze('VSTR', 'ST')
|
||||
satz1.setze('X', 'TEST')
|
||||
satz2 = L10nAtEldaSatz(
|
||||
L10N_AT_ELDA_IDTEIL + [(2, 'Y', 21, 4, 'n')], 24,
|
||||
bezeichnung='Test')
|
||||
satz2.setze('SART', '45')
|
||||
satz2.setze('VSTR', 'ST')
|
||||
satz2.setze('Y', 7)
|
||||
bestand = L10nAtEldaBestand(
|
||||
'AD', 'ST', '02',
|
||||
hersteller={'name': 'Test', 'kfz': 'A'},
|
||||
datensaetze=[satz1, satz2])
|
||||
zeilen = bestand.render(
|
||||
erstellungsdatum=datetime.date(2026, 1, 15))
|
||||
vorlauf = zeilen[0]
|
||||
self.assertEqual(len(zeilen), 4)
|
||||
self.assertEqual(vorlauf[18:20], 'ST') # VSTR (Pos. 19)
|
||||
self.assertEqual(vorlauf[22:24], 'AD') # BEST
|
||||
self.assertEqual(vorlauf[30:38], '15012026') # EDAT TTMMJJJJ
|
||||
self.assertEqual(vorlauf[149:151], '02') # VERS
|
||||
self.assertEqual(zeilen[1][0:2], '45')
|
||||
self.assertEqual(zeilen[1][2:9], '0000002') # SANR
|
||||
self.assertEqual(zeilen[3][20:26], '000004') # SANZ
|
||||
datei = L10nAtEldaDatei([bestand])
|
||||
roh = datei.render()
|
||||
self.assertIn(b'\r\n', roh)
|
||||
erste = roh.split(b'\r\n')[0]
|
||||
self.assertEqual(erste[:4], b' 01')
|
||||
self.assertIn(b'ISO-8859-15', erste)
|
||||
# Umlaut in ISO-8859-15 codiert.
|
||||
satz3 = L10nAtEldaSatz(
|
||||
L10N_AT_ELDA_IDTEIL + [(2, 'Z', 21, 10, 'a')], 30,
|
||||
bezeichnung='Umlaut')
|
||||
satz3.setze('SART', '45')
|
||||
satz3.setze('VSTR', 'ST')
|
||||
satz3.setze('Z', 'Für')
|
||||
self.assertIn('Für'.encode('ISO-8859-15'),
|
||||
satz3.render().encode('ISO-8859-15'))
|
||||
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
class L16GeneratorTest(TransactionCase):
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.company = self.env.company
|
||||
self.company.write({
|
||||
'l10n_at_lohnsteuernummer': '123456789',
|
||||
'l10n_at_beitragskontonummer': '1412345678',
|
||||
})
|
||||
self.structure = self.env.ref(
|
||||
'l10n_at_hr_payroll_private.structure_at_privat')
|
||||
self.kv2203 = self.env.ref(
|
||||
'l10n_at_hr_payroll_private.kv_si_2203')
|
||||
self.gruppe_ii = self.env.ref(
|
||||
'l10n_at_hr_payroll_private.kv2203_grp_ii')
|
||||
self.calendar = self.env['resource.calendar'].create({
|
||||
'name': 'AT Test 40h',
|
||||
'tz': 'Europe/Vienna',
|
||||
'hours_per_day': 8,
|
||||
'attendance_ids': [
|
||||
(0, 0, {'name': 'Tag %d' % wd, 'dayofweek': str(wd),
|
||||
'hour_from': 8.0, 'hour_to': 16.0})
|
||||
for wd in range(5)
|
||||
],
|
||||
})
|
||||
self.company.resource_calendar_id = self.calendar
|
||||
employee = self.env['hr.employee'].create({
|
||||
'name': 'L16 Test', 'birthday': '1990-05-15'})
|
||||
version = self.env['hr.version'].search(
|
||||
[('employee_id', '=', employee.id)], limit=1)
|
||||
version.write({
|
||||
'contract_date_start': datetime.date(2020, 1, 1),
|
||||
'ssnid': '1234150590',
|
||||
'sex': 'female',
|
||||
'kv_id': self.kv2203.id,
|
||||
'kv_gruppe_id': self.gruppe_ii.id,
|
||||
'erfahrungsstufe': 1,
|
||||
'resource_calendar_id': self.calendar.id,
|
||||
})
|
||||
self.employee = employee
|
||||
self.version = version
|
||||
|
||||
def _slip(self, monat, jahr=2026, confirm=True):
|
||||
erster = datetime.date(jahr, monat, 1)
|
||||
letzter = (erster.replace(day=28) + datetime.timedelta(days=4)) \
|
||||
.replace(day=1) - datetime.timedelta(days=1)
|
||||
slip = self.env['hr.payslip'].create({
|
||||
'name': 'L16 %d/%d' % (monat, jahr),
|
||||
'employee_id': self.employee.id,
|
||||
'struct_id': self.structure.id,
|
||||
'date_from': erster,
|
||||
'date_to': letzter,
|
||||
})
|
||||
slip.compute_sheet()
|
||||
if confirm:
|
||||
slip.action_payslip_done()
|
||||
return slip
|
||||
|
||||
def test_01_l16_lauf_und_positionen(self):
|
||||
"""Jahreslauf: KZ-Salden aus GP3/GP4-Flags, L1-Record mit
|
||||
exakten Positionen (Satzlänge 3500, REFN/ARTL/KZ-V/B-Paare,
|
||||
SOZS/AVLN/AGBD), I1 (1100, STNRA/JAHR/GESA/ANZA), Datei mit
|
||||
LF-Bestand VSTR 94, KZ-245-Konsistenz per Konstruktion."""
|
||||
self._slip(11) # November mit B13
|
||||
deklaration = self.env['l10n.at.payroll.l16'].create({
|
||||
'company_id': self.company.id,
|
||||
'jahr': 2026,
|
||||
'art': '01',
|
||||
})
|
||||
deklaration.action_erzeugen()
|
||||
self.assertEqual(deklaration.state, 'done')
|
||||
self.assertEqual(len(deklaration.line_ids), 1)
|
||||
line = deklaration.line_ids
|
||||
# November: KZ 210 = KVGEO + B13 = 4.118,82 (§ 25 gesamt);
|
||||
# KZ 220 = B13 = 2.059,41; KZ 245 = laufende Bemessung.
|
||||
self.assertAlmostEqual(line.kz210, 4118.82, places=2)
|
||||
self.assertAlmostEqual(line.kz220, 2059.41, places=2)
|
||||
# KZ-245-Formel: 210 − 215 − 220 − 230 − 243
|
||||
erwartet = (line.kz210 - line.kz215 - line.kz220
|
||||
- line.kz230 - line.kz243)
|
||||
self.assertAlmostEqual(line.kz245, erwartet, places=1)
|
||||
# Datei: I1 (1100) + L1 (3500) + Vorlauf/Schluss (auf 3500
|
||||
# aufgefüllt), LF-Bestand, VSTR 94, Lohnzettelversion 28.
|
||||
roh = base64.b64decode(deklaration.datei_l16).decode(
|
||||
'ISO-8859-15')
|
||||
zeilen = roh.strip('\r\n').split('\r\n')
|
||||
# Dateiheader + Vorlauf + I1 + L1 + Schluss.
|
||||
self.assertEqual(len(zeilen), 5)
|
||||
header, vorlauf, i1, l1, schluss = zeilen
|
||||
self.assertEqual(header[:4], ' 01')
|
||||
self.assertIn('ISO-8859-15', header)
|
||||
self.assertEqual(len(i1), 1100)
|
||||
self.assertEqual(len(l1), 3500)
|
||||
self.assertEqual(vorlauf[0:2], '00')
|
||||
self.assertEqual(vorlauf[18:20], '94')
|
||||
self.assertEqual(vorlauf[22:24], 'LF')
|
||||
self.assertEqual(vorlauf[149:151], '28')
|
||||
self.assertEqual(i1[18:20], '94')
|
||||
self.assertEqual(i1[20:21], 'I')
|
||||
self.assertEqual(i1[34:43], '123456789') # STNRL
|
||||
self.assertEqual(i1[50:59], '123456789') # STNRA
|
||||
self.assertEqual(i1[82:84], '03') # STVE
|
||||
self.assertEqual(i1[232:236], '2026') # JAHR
|
||||
self.assertEqual(i1[236:243], '0000001') # GESA
|
||||
self.assertEqual(l1[18:20], '94')
|
||||
self.assertEqual(l1[20:21], 'L')
|
||||
self.assertEqual(l1[95:97], '01') # ARTL
|
||||
self.assertEqual(l1[119:123], '0111') # BELZ TTMM
|
||||
self.assertEqual(l1[123:127], '3011') # ENLZ
|
||||
self.assertEqual(l1[127:131], '2026') # JALZ
|
||||
self.assertEqual(l1[131:132], '3') # SOZS Angestellte
|
||||
self.assertEqual(l1[132:136], '1234') # AVLN LLLP
|
||||
self.assertEqual(l1[136:142], '150590') # AGBD TTMMJJ
|
||||
# KZ-Felder: V/B-Paare (1-based-Positionen → Index pos−1).
|
||||
self.assertEqual(l1[296:297], '+') # V210 (Pos. 297)
|
||||
self.assertEqual(l1[297:307], '0000411882') # B210
|
||||
self.assertEqual(l1[318:319], '+') # V220 (Pos. 319)
|
||||
self.assertEqual(l1[319:329], '0000205941') # B220
|
||||
self.assertEqual(l1[450:451], '+') # V245 (Pos. 451)
|
||||
b245 = '%010d' % int(round(line.kz245 * 100))
|
||||
self.assertEqual(l1[451:461], b245)
|
||||
self.assertEqual(schluss[0:2], '99')
|
||||
self.assertEqual(schluss[20:26], '000004') # SANZ
|
||||
|
||||
def test_02_arbeitsstaette(self):
|
||||
"""§ 34 Abs 6 ASVG: je AN ein Satz 45 (Länge 300) mit BKNR,
|
||||
VSNR, STNR und GKZ aus dem Dienstort-Partner; Warnung, wenn
|
||||
die GKZ fehlt."""
|
||||
self._slip(1)
|
||||
deklaration = self.env['l10n.at.payroll.l16'].create({
|
||||
'company_id': self.company.id,
|
||||
'jahr': 2026,
|
||||
'art': '01',
|
||||
})
|
||||
deklaration.action_erzeugen()
|
||||
self.assertTrue(deklaration.datei_arbeitsstaette)
|
||||
roh = base64.b64decode(
|
||||
deklaration.datei_arbeitsstaette).decode('ISO-8859-15')
|
||||
zeilen = roh.strip('\r\n').split('\r\n')
|
||||
vorlauf, satz45 = zeilen[1], zeilen[2]
|
||||
self.assertEqual(vorlauf[18:20], 'ST')
|
||||
self.assertEqual(vorlauf[22:24], 'AD')
|
||||
self.assertEqual(satz45[0:2], '45')
|
||||
self.assertEqual(len(satz45), 300)
|
||||
self.assertEqual(satz45[22:32], '1412345678') # BKNR
|
||||
self.assertEqual(satz45[32:42], '1234150590') # VSNR
|
||||
self.assertEqual(satz45[52:61], '123456789') # STNR
|
||||
self.assertIn('Gemeindekennziffer', deklaration.protokoll)
|
||||
@@ -0,0 +1,127 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<!-- ============================================================ -->
|
||||
<!-- GP7 Meldewesen: L16-/Arbeitsstätten-Deklaration + Kind-Stamm -->
|
||||
<!-- (FB+-Wartung am Mitarbeiter). -->
|
||||
<!-- ============================================================ -->
|
||||
|
||||
<record id="l16_list_view" model="ir.ui.view">
|
||||
<field name="name">l10n.at.payroll.l16.list</field>
|
||||
<field name="model">l10n.at.payroll.l16</field>
|
||||
<field name="arch" type="xml">
|
||||
<list string="Lohnzettel Finanz (L16)">
|
||||
<field name="jahr"/>
|
||||
<field name="art"/>
|
||||
<field name="company_id" groups="base.group_multi_company"/>
|
||||
<field name="state" widget="badge"
|
||||
decoration-success="state == 'done'"
|
||||
decoration-info="state == 'draft'"/>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="l16_form_view" model="ir.ui.view">
|
||||
<field name="name">l10n.at.payroll.l16.form</field>
|
||||
<field name="model">l10n.at.payroll.l16</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Lohnzettel Finanz (L16) & Arbeitsstättenmeldung">
|
||||
<header>
|
||||
<button name="action_erzeugen" type="object"
|
||||
string="Lauf erzeugen (Dateien)"
|
||||
class="btn-primary"
|
||||
invisible="state == 'done'"/>
|
||||
<field name="state" widget="statusbar"
|
||||
statusbar_visible="draft,done"/>
|
||||
</header>
|
||||
<sheet>
|
||||
<group>
|
||||
<group>
|
||||
<field name="jahr"/>
|
||||
<field name="art"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="company_id"
|
||||
groups="base.group_multi_company"/>
|
||||
</group>
|
||||
</group>
|
||||
<notebook>
|
||||
<page string="Mitteilungssätze" name="linien">
|
||||
<field name="line_ids">
|
||||
<list editable="bottom">
|
||||
<field name="employee_name"/>
|
||||
<field name="versicherungsnummer"/>
|
||||
<field name="soziale_stellung"/>
|
||||
<field name="kz210" optional="show"/>
|
||||
<field name="kz215" optional="hide"/>
|
||||
<field name="kz220" optional="show"/>
|
||||
<field name="kz230" optional="hide"/>
|
||||
<field name="kz245" optional="show"/>
|
||||
<field name="lohnsteuer"/>
|
||||
<field name="fab_betrag"
|
||||
string="FB+"/>
|
||||
</list>
|
||||
</field>
|
||||
</page>
|
||||
<page string="Dateien" name="dateien">
|
||||
<group>
|
||||
<field name="datei_l16"
|
||||
filename="datei_l16_name"/>
|
||||
<field name="datei_l16_name" invisible="1"/>
|
||||
<field name="datei_arbeitsstaette"
|
||||
filename="datei_arbeitsstaette_name"/>
|
||||
<field name="datei_arbeitsstaette_name"
|
||||
invisible="1"/>
|
||||
</group>
|
||||
</page>
|
||||
<page string="Prüfprotokoll" name="protokoll">
|
||||
<field name="protokoll"/>
|
||||
</page>
|
||||
</notebook>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_l16" model="ir.actions.act_window">
|
||||
<field name="name">Lohnzettel Finanz (L16)</field>
|
||||
<field name="res_model">l10n.at.payroll.l16</field>
|
||||
<field name="view_mode">list,form</field>
|
||||
</record>
|
||||
|
||||
<menuitem id="menu_l10n_at_payroll_meldewesen"
|
||||
name="Meldewesen (ELDA/FinOnl)"
|
||||
parent="hr_work_entry_enterprise.menu_hr_payroll_root"
|
||||
sequence="97"/>
|
||||
|
||||
<menuitem id="menu_l10n_at_payroll_l16"
|
||||
name="Lohnzettel Finanz (L16)"
|
||||
parent="menu_l10n_at_payroll_meldewesen"
|
||||
action="action_l16" sequence="10"/>
|
||||
|
||||
<!-- Kind-Stammdaten (FB+-Wartung) als Seite am Mitarbeiterformular -->
|
||||
<record id="hr_employee_view_form_kind" model="ir.ui.view">
|
||||
<field name="name">hr.employee.form.l10n_at_payroll_kind</field>
|
||||
<field name="model">hr.employee</field>
|
||||
<field name="inherit_id" ref="hr.view_employee_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<notebook position="inside">
|
||||
<page string="Kinder (FB+)" name="l10n_at_payroll_kinder"
|
||||
groups="hr_payroll.group_hr_payroll_user">
|
||||
<field name="l10n_at_payroll_kind_ids">
|
||||
<list editable="bottom">
|
||||
<field name="familienname"/>
|
||||
<field name="vorname"/>
|
||||
<field name="geburtstag"/>
|
||||
<field name="versicherungsnummer"/>
|
||||
<field name="fab_plus" widget="boolean_toggle"/>
|
||||
<field name="fab_monat_von"/>
|
||||
<field name="fab_monat_bis"/>
|
||||
<field name="fab_halb_monat_von"/>
|
||||
<field name="fab_halb_monat_bis"/>
|
||||
</list>
|
||||
</field>
|
||||
</page>
|
||||
</notebook>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
Reference in New Issue
Block a user