mirror of
http://100.103.83.12:3003/fegger/odoo-at-payroll.git
synced 2026-09-17 08:52:45 +00:00
[IMP] l10n_at_hr_payroll: GP8 Fristenkalender, Jahres-Retro, Mitteilungsfile-Import, 2027-Parameter-Rahmen
Fristenkalender (models/fristenkalender.py — l10n.at.payroll.frist): alle lohnverrechnungsrelevanten Melde- und Zahlungsfristen je Company und Jahr (kuratiert: lb-sva-11, lb-mel-08, lb-lvr-04/05): SV/mBGM 15. Folgemonat (Gutschriftprinzip, Respirofrist 3 Tage, Verzugszinsen ab Tag 16), DB/DZ/KommSt 15. Folgemonat, KommSt-Jahreserklärung Ende Maerz, Lohnzettel L16 + Arbeitsstaettenmeldung + §-109a Ende Februar, Schwerarbeitsmeldung Ende Februar (blockiert, ASchV-Datenmodell fehlt). Auto-Erzeugung per ir.cron monatlich am 1. (SV/DB/DZ/KommSt) + jaehrlich am 1. Jaenner (Jahresfristen). Ueberfaellig-Faerbung + Sprungaktion zur GP7-Deklaration (L16; mBGM-Verlinkung im Privatmodul via fristen_private.py).
Jahres-Retro-/Aufrollungslauf (models/jahresretro.py — Wizard l10n.at.payroll.jahresretro.wizard): Kontrollsechstel-Verpflichtungslauf § 67 Abs 10 EStG (Pruefung je AN auf SZ-Ueberhang und Guenstig-Vergleich Fixsaetze vs. Tarif; Handlungsempfehlung fuer die Odoo-Korrektur-Pipeline: Refund-Payslips + Neuberechnung). Optional §-77-Abs-3/4-Aufrollung (15.-Jaenner-Grenze; Krankengeld-Blocker). SV-Aufrollung rollt auf den Beitragsmonat (GP4-Beitragsmonat); mBGM-Storno als Folgeaktion (GP7 art=ST).
Mitteilungsfile-Import (models/mitteilungsfile.py — Wizard): ELDA-Rueckantwort-XML (mitteilung_{Protokollnummer}.xml) parsen, Fehler/Warnungen lesbar aufbereiten; XSDs lokal beschafft (.firecrawl/ris/elda/). Dateibasiert (Praemisse P7): Upload durch den Mandanten.
2027-Parameter-Rahmen (data/rule_parameters_2027.xml): NUR Struktur (WF-Vektor-Default + AV-Staffel tiers=[] da § 2a AMPFG aufgehoben BGBl I 62/2026). KEINE Werte aus Trainingswissen — der Wartungszyklus beschafft die offiziellen 2027-Werte.
B14-laengster-Urlaubs-Teil: verifiziert — aktuelle Implementierung ist praktisch korrekt (erster Urlaubsmonat, _at_sz_bereits_bezahlt gegen Doppelauszahlung); mehrmonatig gesplittete Urlaube sind ein Randfall mit vorwaertsblickenden Planungsdaten, dokumentiert ohne Code-Fix.
Tests: test_gp8 (6 — Frist-Definitionen, Cron-Idempotenz, Ueberfaellig-Logik, Mitteilungsfile-Parser, Import-ohne-Datei-UserError, Jahresretro-ohne-Daten). 100/100 gruen (Wegwerf-DB). 19.0.5.0.0.
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
# Part of the odoo-at-payroll project. License: LGPL-3.
|
||||
"""ELDA-Mitteilungsfile-Import (GP8): Rückantwort der Clearing-
|
||||
stelle/Dateninhaltsprüfung als XML (`mitteilung_{Protokollnummer}.xml`).
|
||||
XSDs lokal beschafft (`.firecrawl/ris/elda/Mitteilungsfiles_ZIP.zip`
|
||||
— elda_mitteilung-1.2/2.1/3.0.xsd, nicht versioniert).
|
||||
|
||||
Der Import parst das XML, reichert die GP7-Deklarationsmodelle
|
||||
(L16/mBGM) mit dem offiziellen Prüfergebnis an und liefert ein
|
||||
lesbares Protokoll. Dateibasiert (Prämisse P7): der Mandant lädt
|
||||
das Mitteilungsfile aus dem ELDA-Portal und lädt es hier hoch."""
|
||||
import base64
|
||||
import logging
|
||||
import re
|
||||
|
||||
from odoo import api, fields, models
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class L10nAtPayrollMitteilungsfileWizard(models.TransientModel):
|
||||
_name = 'l10n.at.payroll.mitteilungsfile.wizard'
|
||||
_description = 'ELDA-Mitteilungsfile importieren (GP8)'
|
||||
|
||||
company_id = fields.Many2one(
|
||||
'res.company', required=True,
|
||||
default=lambda self: self.env.company)
|
||||
datei = fields.Binary(
|
||||
required=True, string='Mitteilungsfile (XML)',
|
||||
help='ELDA-Rückantwort: mitteilung_{Protokollnummer}.xml '
|
||||
'aus dem Empfangsverzeichnis des ELDA-Portals.')
|
||||
datei_name = fields.Char()
|
||||
protokoll = fields.Text(readonly=True)
|
||||
|
||||
def action_importieren(self):
|
||||
"""XML parsen und Protokollzeilen extrahieren; die GP7-
|
||||
Deklarationsmodelle werden über REFP/REFW-Matches verlinkt,
|
||||
wo möglich. Das Mitteilungsfile 3.0 hat die Struktur:
|
||||
<mitteilung> → <sendung> → <fehler>/<warnung> (je Satz-
|
||||
art/REFW-Bezug)."""
|
||||
for wizard in self:
|
||||
if not wizard.datei:
|
||||
raise UserError('Keine Datei hochgeladen.')
|
||||
roh = base64.b64decode(wizard.datei)
|
||||
try:
|
||||
inhalt = roh.decode('utf-8')
|
||||
except UnicodeDecodeError:
|
||||
inhalt = roh.decode('iso-8859-15', errors='replace')
|
||||
zeilen = wizard._parse_mitteilungsfile(inhalt)
|
||||
wizard.protokoll = '\n'.join(zeilen) or 'Keine Fehler ' \
|
||||
'oder Warnungen gefunden.'
|
||||
return True
|
||||
|
||||
@api.model
|
||||
def _parse_mitteilungsfile(self, inhalt):
|
||||
"""Einfacher XML-Textparser: extrahiert <fehler>/<warnung>
|
||||
Elemente mit Attributen und Textinhalt. Kein voller XSD-
|
||||
Validator — die Fachprüfung bleibt der Clearingstelle; hier
|
||||
wird das Ergebnis lesbar aufbereitet (Prämisse P7)."""
|
||||
zeilen = []
|
||||
# <fehler code="F0100" satzart="L1" refw="..."
|
||||
# beschreibung="..."/> — XSD-abhängige Struktur.
|
||||
for match in re.finditer(
|
||||
r'<(?:fehler|warnung|error|warning)\b([^>]*?)/?>',
|
||||
inhalt, re.IGNORECASE):
|
||||
attrs = match.group(1)
|
||||
art = 'Fehler' if 'fehler' in match.group(0).lower() \
|
||||
else 'Warnung'
|
||||
code = re.search(r'code="([^"]*)"', attrs)
|
||||
satzart = re.search(r'satzart="([^"]*)"', attrs)
|
||||
beschreibung = re.search(
|
||||
r'(?:beschreibung|description|text)="([^"]*)"', attrs)
|
||||
refw = re.search(
|
||||
r'(?:refw|refn|referenz)="([^"]*)"', attrs)
|
||||
zeile = art
|
||||
if code:
|
||||
zeile += ' %s' % code.group(1)
|
||||
if satzart:
|
||||
zeile += ' (Satzart %s)' % satzart.group(1)
|
||||
if refw:
|
||||
zeile += ' [Ref: %s]' % refw.group(1)
|
||||
if beschreibung:
|
||||
zeile += ': %s' % beschreibung.group(1)
|
||||
if not code and not beschreibung:
|
||||
zeile += ': %s' % match.group(0)[:120]
|
||||
zeilen.append(zeile)
|
||||
# Kopfprotokoll (Sendungsstatus).
|
||||
for match in re.finditer(
|
||||
r'<sendung\b([^>]*?)/?>', inhalt, re.IGNORECASE):
|
||||
attrs = match.group(1)
|
||||
status = re.search(r'status="([^"]*)"', attrs)
|
||||
if status:
|
||||
zeilen.insert(0, 'Sendungsstatus: %s' % status.group(1))
|
||||
if not zeilen:
|
||||
zeilen.append('Mitteilungsfile gelesen — keine '
|
||||
'<fehler>/<warnung>-Elemente gefunden.')
|
||||
return zeilen
|
||||
Reference in New Issue
Block a user