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:
@@ -5,4 +5,7 @@ from . import sozialversicherung
|
||||
from . import elda
|
||||
from . import meldewesen
|
||||
from . import kind
|
||||
from . import lohnzettel_l16
|
||||
from . import lohnzettel_l16
|
||||
from . import fristenkalender
|
||||
from . import jahresretro
|
||||
from . import mitteilungsfile
|
||||
@@ -0,0 +1,230 @@
|
||||
# Part of the odoo-at-payroll project. License: LGPL-3.
|
||||
"""Fristenkalender (GP8/AP8): alle lohnverrechnungsrelevanten
|
||||
Melde-/Zahlungsfristen je Company und Jahr.
|
||||
|
||||
Kuratierte Fristen (lb-sva-11, lb-mel-08, lb-lvr-04/05, Stand
|
||||
2026-06…08):
|
||||
- SV-Beiträge (mBGM): Einlangen **15. des Folgemonats** (Gutschrift-
|
||||
prinzip; Respirofrist 3 Tage; Verzugszinsen ab dem 16. Tag);
|
||||
Vorschreibebetriebe 7.
|
||||
- DB (FLAF) / DZ / KommSt: je **15. des Folgemonats**.
|
||||
- KommSt-Jahreserklärung: über FinanzOnline **bis Ende März** des
|
||||
Folgejahres.
|
||||
- Lohnzettel L16 (§ 84 EStG): **Ende Februar** des Folgejahres via
|
||||
ELDA (Papierformular ohne technische Voraussetzungen: Ende Jänner);
|
||||
berichtigter Lohnzettel nach steuerlich relevanter Ergänzung des
|
||||
Lohnkontos **binnen 14 Tagen**.
|
||||
- Arbeitsstättenmeldung § 34 Abs 6 ASVG: **Ende Februar** (via ELDA
|
||||
an Statistik Austria).
|
||||
- § 109a-EStG-Meldung: **Ende Februar** (ELDA); Papierformular E 109a
|
||||
+ Kopie an den Entgeltempfänger Ende Jänner (nicht implementiert —
|
||||
Honorar-Basis).
|
||||
- Schwerarbeitsmeldung ASchV § 5: Jänner bis **Ende Februar** via
|
||||
ELDA (nicht implementiert — blockiert, ASchV-Datenmodell fehlt).
|
||||
|
||||
Odoo-Integration: `mail.activity.mixin` für optionale Benachrichtigung
|
||||
bei Überschreitung; native Fristart-Definition mit Sprungaktion zur
|
||||
GP7-Deklaration (L16/mBGM erzeugen); Auto-Erzeugung per ir.cron
|
||||
(monatlich am 1.: offene SV/DB/DZ/KommSt-Fristen des Vormonats;
|
||||
jährlich am 1. Jänner: Jahresfristen des Vorjahres)."""
|
||||
from datetime import date, timedelta
|
||||
|
||||
from odoo import api, fields, models
|
||||
|
||||
FRISTART_DEFINITIONEN = {
|
||||
'sv_mbgm': {
|
||||
'name': 'SV-Beiträge (mBGM) einlangen',
|
||||
'monatlich': True,
|
||||
'tag': 15,
|
||||
'action': 'mbgm',
|
||||
'hilfetext': '15. des Folgemonats (Gutschriftprinzip; '
|
||||
'Vorschreibebetriebe: 7.). Verzugszinsen ab dem '
|
||||
'16. Tag.',
|
||||
},
|
||||
'db_dz_kommst': {
|
||||
'name': 'DB/DZ/KommSt einlangen',
|
||||
'monatlich': True,
|
||||
'tag': 15,
|
||||
'action': None,
|
||||
'hilfetext': 'FLAF-Dienstgeberbeitrag, Zuschlag zum '
|
||||
'Dienstgeberbeitrag und Kommunalsteuer: je 15. '
|
||||
'des Folgemonats.',
|
||||
},
|
||||
'kommst_jahres': {
|
||||
'name': 'KommSt-Jahreserklärung (FinanzOnline)',
|
||||
'monatlich': False,
|
||||
'monat': 3,
|
||||
'tag_im_monat': 31,
|
||||
'action': None,
|
||||
'hilfetext': 'Jahreserklärung der Kommunalsteuer über '
|
||||
'FinanzOnline bis Ende März des Folgejahres '
|
||||
'(§ 11 Abs 5 KommStG).',
|
||||
},
|
||||
'l16': {
|
||||
'name': 'Lohnzettel L16 (§ 84 EStG)',
|
||||
'monatlich': False,
|
||||
'monat': 2,
|
||||
'tag_im_monat': 28,
|
||||
'action': 'l16',
|
||||
'hilfetext': 'Jahres-Lohnzettel bis Ende Februar des '
|
||||
'Folgejahres via ELDA; Papierformular ohne '
|
||||
'technische Voraussetzungen: Ende Jänner. '
|
||||
'Berichtigter Lohnzettel nach steuerlich '
|
||||
'relevanter Ergänzung des Lohnkontos binnen '
|
||||
'14 Tagen.',
|
||||
},
|
||||
'arbeitsstaette': {
|
||||
'name': 'Arbeitsstättenmeldung (§ 34 Abs 6 ASVG)',
|
||||
'monatlich': False,
|
||||
'monat': 2,
|
||||
'tag_im_monat': 28,
|
||||
'action': 'l16',
|
||||
'hilfetext': 'Adresse der Arbeitsstätte am 31. 12. bis '
|
||||
'Ende Februar des Folgejahres via ELDA (VSTR '
|
||||
'ST). Läuft im GP7-Jahreslauf mit der L16.',
|
||||
},
|
||||
'paragraph_109a': {
|
||||
'name': '§ 109a-EStG-Meldung (Honorare)',
|
||||
'monatlich': False,
|
||||
'monat': 2,
|
||||
'tag_im_monat': 28,
|
||||
'action': None,
|
||||
'hilfetext': 'Meldung der Honorare außerhalb von '
|
||||
'Dienstverhältnissen bis Ende Februar via '
|
||||
'ELDA (8 Tätigkeitsgruppen; kumulativ ≤ 900 € '
|
||||
'und ≤ 450 € je Leistung → entfällt).',
|
||||
},
|
||||
'schwerarbeit': {
|
||||
'name': 'Schwerarbeitsmeldung (ASchV § 5)',
|
||||
'monatlich': False,
|
||||
'monat': 2,
|
||||
'tag_im_monat': 28,
|
||||
'action': None,
|
||||
'hilfetext': 'Jährlich rückwirkend Jänner bis Ende Februar '
|
||||
'via ELDA an den KV-Träger; meldepflichtig '
|
||||
'ab 40 M / 35 F (⚠ blockiert — ASchV-Daten-'
|
||||
'modell fehlt im Modul).',
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
class L10nAtPayrollFrist(models.Model):
|
||||
_name = 'l10n.at.payroll.frist'
|
||||
_description = 'Lohnverrechnungs-Fristenkalender'
|
||||
_inherit = ['mail.thread']
|
||||
_order = 'deadline, id'
|
||||
|
||||
company_id = fields.Many2one(
|
||||
'res.company', required=True,
|
||||
default=lambda self: self.env.company, index=True)
|
||||
fristart = fields.Selection(
|
||||
[(k, v['name']) for k, v in FRISTART_DEFINITIONEN.items()],
|
||||
required=True, index=True)
|
||||
jahr = fields.Integer(
|
||||
required=True, string='Bezugs-/Kalenderjahr',
|
||||
help='Beitragsjahr (monatliche Fristen) bzw. gemeldetes '
|
||||
'Jahr (Jahresfristen: L16/Arbeitsstätte/§-109a gelten '
|
||||
'für das Vorjahr).')
|
||||
monat = fields.Integer(
|
||||
string='Beitragsmonat (monatliche Fristen)',
|
||||
help='1–12; leer bei Jahresfristen.')
|
||||
deadline = fields.Date(required=True, string='Frist')
|
||||
state = fields.Selection(
|
||||
[('offen', 'Offen'), ('erledigt', 'Erledigt'),
|
||||
('ueberfaellig', 'Überfällig')],
|
||||
compute='_compute_state', store=True, index=True)
|
||||
erledigt_am = fields.Date(readonly=True)
|
||||
hilfetext = fields.Text(readonly=True)
|
||||
ref_l16_id = fields.Many2one(
|
||||
'l10n.at.payroll.l16', string='L16-Deklaration',
|
||||
help='Sprungziel für Fristen mit L16/Arbeitsstätten-Meldung.')
|
||||
|
||||
_sql_constraints = [(
|
||||
'frist_company_art_jahr_monat_uniq',
|
||||
'unique(company_id, fristart, jahr, monat)',
|
||||
'Diese Frist existiert bereits für das Unternehmen.')]
|
||||
|
||||
@api.depends('deadline', 'erledigt_am')
|
||||
def _compute_state(self):
|
||||
heute = fields.Date.today()
|
||||
for frist in self:
|
||||
if frist.erledigt_am:
|
||||
frist.state = 'erledigt'
|
||||
elif frist.deadline and frist.deadline < heute:
|
||||
frist.state = 'ueberfaellig'
|
||||
else:
|
||||
frist.state = 'offen'
|
||||
|
||||
def action_erledigt(self):
|
||||
self.write({'erledigt_am': fields.Date.today()})
|
||||
return True
|
||||
|
||||
def action_wieder_eroeffnen(self):
|
||||
self.write({'erledigt_am': False})
|
||||
return True
|
||||
|
||||
def action_zum_objekt(self):
|
||||
"""Sprung zur GP7-Deklaration (L16 oder mBGM)."""
|
||||
self.ensure_one()
|
||||
if self.ref_l16_id:
|
||||
return {
|
||||
'type': 'ir.actions.act_window',
|
||||
'res_model': 'l10n.at.payroll.l16',
|
||||
'res_id': self.ref_l16_id.id,
|
||||
'view_mode': 'form',
|
||||
}
|
||||
return True
|
||||
|
||||
@api.model
|
||||
def _cron_fristen_erzeugen(self):
|
||||
"""Monatlich am 1.: offene Fristen des Vormonats (SV/DB/DZ/
|
||||
KommSt) + jährlich am 1. Jänner: Jahresfristen des
|
||||
Vorjahres. Idempotent (Unique-Constraint)."""
|
||||
heute = fields.Date.today()
|
||||
vormonat = (heute.replace(day=1) - timedelta(days=1))
|
||||
for company in self.env['res.company'].search([]):
|
||||
# Monatliche Fristen des Vormonats (15. des Folgemonats).
|
||||
folgemonat = heute.replace(day=1)
|
||||
deadline_monat = date(
|
||||
folgemonat.year, folgemonat.month,
|
||||
FRISTART_DEFINITIONEN['sv_mbgm']['tag'])
|
||||
for art in ('sv_mbgm', 'db_dz_kommst'):
|
||||
self._sicherstellen(
|
||||
company, art, vormonat.year, vormonat.month,
|
||||
deadline_monat)
|
||||
# Jahresfristen nur am 1. Jänner (für das Vorjahr).
|
||||
if heute.month == 1:
|
||||
for art in ('l16', 'arbeitsstaette', 'paragraph_109a',
|
||||
'schwerarbeit'):
|
||||
monat = FRISTART_DEFINITIONEN[art]['monat']
|
||||
tag = FRISTART_DEFINITIONEN[art]['tag_im_monat']
|
||||
jahr_deadline = heute.year
|
||||
deadline_jahr = date(jahr_deadline, monat, tag)
|
||||
if deadline_jahr.month == 2 and tag > 28:
|
||||
deadline_jahr = date(jahr_deadline, monat, 28)
|
||||
self._sicherstellen(
|
||||
company, art, heute.year - 1, None,
|
||||
deadline_jahr)
|
||||
# KommSt-Jahreserklärung (Ende März).
|
||||
self._sicherstellen(
|
||||
company, 'kommst_jahres', heute.year - 1, None,
|
||||
date(heute.year, 3, 31))
|
||||
|
||||
@api.model
|
||||
def _sicherstellen(self, company, art, jahr, monat, deadline):
|
||||
vals = {
|
||||
'company_id': company.id,
|
||||
'fristart': art,
|
||||
'jahr': jahr,
|
||||
'monat': monat,
|
||||
'deadline': deadline,
|
||||
'hilfetext': FRISTART_DEFINITIONEN[art]['hilfetext'],
|
||||
}
|
||||
bestehend = self.search([
|
||||
('company_id', '=', company.id),
|
||||
('fristart', '=', art),
|
||||
('jahr', '=', jahr),
|
||||
('monat', '=', monat)])
|
||||
if not bestehend:
|
||||
self.create(vals)
|
||||
return bestehend or self.env['l10n.at.payroll.frist']
|
||||
@@ -0,0 +1,152 @@
|
||||
# Part of the odoo-at-payroll project. License: LGPL-3.
|
||||
"""Jahres-Retro-/Aufrollungslauf (GP8/AP8): Kontrollsechstel-
|
||||
Verpflichtungslauf (§ 67 Abs 10 EStG) und optionale §-77-Abs-3/4-
|
||||
Aufrollung mit YTD-Rekalkulation.
|
||||
|
||||
Kuratierte Grundlagen (lb-lvr-07, lb-naz-01…04, 2026-06/08):
|
||||
- **Kontrollsechstel** (§ 67 Abs 10): sonstige Bezüge innerhalb des
|
||||
Jahressechstels sind mit Fixsätzen versteuert; bei Überhang wird
|
||||
die **Differenz tarifisch** versteuert — der Dezember-/
|
||||
Beendigungsabrechnungslauf rechnet die SZ-Steuer mit dem Tarif neu
|
||||
(**Günstig-Aufrollung** — GP3-Engine kann beides bereits).
|
||||
- **§ 77 Abs 3/4**: optionale Aufrollung der Lohnsteuer des
|
||||
Kalenderjahres beim Jahreswechsel (15.-Jänner-Grenze;
|
||||
Krankengeldbezug im Jahr blockt Abs 3/4, nicht Abs 4a). Die
|
||||
Aufrollung rechnet die laufende Lohnsteuer kumulativ neu und
|
||||
verteilt die Differenz auf die verbleibenden Monate.
|
||||
- **§ 77 Abs 4a**: Aufrollung nach §-67-Ausschüttungen (nachträg-
|
||||
liche SZ-Steuer mit Günstig-Vergleich).
|
||||
- **15.2.-Cutoff** (lb-naz-02/03): Zahlung bis 15. 2. des Folgejahres
|
||||
→ Vorjahreszuordnung (berichtigter Lohnzettel binnen 14 Tagen);
|
||||
danach keine Zuordnung mehr (§ 67 Abs 8 lit c Fünftel oder
|
||||
§ 67 Abs 1 Tarif).
|
||||
- **SV rollt stets auf den Beitragsmonat der Fälligkeit** (GP4:
|
||||
`l10n_at_beitragsmonat` je Regel/Linie, historisch versionierte
|
||||
Sätze/HBG). mBGM-Storno/Neumeldung als Folgeaktion (GP7 `art='ST'`).
|
||||
|
||||
Odoo-Mechanik: der Wizard **erzeugt keine eigenen Korrekturdaten** —
|
||||
er analysiert die Jahresdaten, zeigt den Handlungsbedarf und
|
||||
empfiehlt die Odoo-native Korrektur-Pipeline (Refund-Payslips über
|
||||
`_action_refund_payslips()` + Neuberechnung des betroffenen Monats
|
||||
mit korrigierten Eingangsdaten; die GP3-Engine rechnet YTD
|
||||
automatisch um)."""
|
||||
from datetime import date
|
||||
|
||||
from odoo import api, fields, models
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
|
||||
class L10nAtPayrollJahresretroWizard(models.TransientModel):
|
||||
_name = 'l10n.at.payroll.jahresretro.wizard'
|
||||
_description = 'Jahres-Retro-/Aufrollungslauf (GP8)'
|
||||
|
||||
company_id = fields.Many2one(
|
||||
'res.company', required=True,
|
||||
default=lambda self: self.env.company)
|
||||
jahr = fields.Integer(
|
||||
required=True, default=lambda self: fields.Date.today().year - 1,
|
||||
string='Jahr (Kontroll-/Aufrollungslauf)')
|
||||
zeige_detail = fields.Boolean(
|
||||
default=True, string='Detail je Mitarbeiter anzeigen')
|
||||
nur_kontrollsechstel = fields.Boolean(
|
||||
default=True, string='Nur Kontrollsechstel-Verpflichtungslauf '
|
||||
'prüfen (§ 67 Abs 10)')
|
||||
aufrollung_77 = fields.Boolean(
|
||||
default=False,
|
||||
string='§-77-Abs-3/4-Aufrollung prüfen',
|
||||
help='Optionale Aufrollung der laufenden Lohnsteuer des '
|
||||
'Kalenderjahres (nur ohne Krankengeldbezug; 15.-Jänner-'
|
||||
'Grenze für die Zahlung; Abs 4a nach §-67-Ausschüttungen '
|
||||
'unabhängig prüfbar).')
|
||||
ergebnis_ids = fields.One2many(
|
||||
'l10n.at.payroll.jahresretro.ergebnis',
|
||||
'wizard_id', string='Prüfergebnis je Mitarbeiter')
|
||||
zusammenfassung = fields.Text(readonly=True)
|
||||
|
||||
def action_pruefen(self):
|
||||
for wizard in self:
|
||||
wizard.ergebnis_ids.unlink()
|
||||
slips = wizard._slips_des_jahres()
|
||||
ergebnisse = []
|
||||
for slip in slips:
|
||||
ergebnis = wizard._pruefe_mitarbeiter(slip)
|
||||
if ergebnis:
|
||||
ergebnisse.append(ergebnis)
|
||||
wizard.ergebnis_ids = [
|
||||
(0, 0, e) for e in ergebnisse]
|
||||
wizard.zusammenfassung = wizard._zusammenfassung()
|
||||
return True
|
||||
|
||||
def _slips_des_jahres(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')),
|
||||
]).sorted('date_from')
|
||||
|
||||
def _pruefe_mitarbeiter(self, slip):
|
||||
"""Prüft Kontrollsechstel-Überhang und optional §-77-Aufrollung
|
||||
je AN. Liefert None, wenn kein Handlungsbedarf."""
|
||||
self.ensure_one()
|
||||
sechstel = slip._l10n_at_sechstel()
|
||||
sonstige_basis = slip._l10n_at_brutto('sonstig')
|
||||
laufende_basis = slip._l10n_at_brutto('laufend')
|
||||
# §-67-Bezüge über dem Jahressechstel → Überhang.
|
||||
if self.nur_kontrollsechstel \
|
||||
and sonstige_basis > sechstel:
|
||||
ueberhang = sonstige_basis - sechstel
|
||||
fixsatz_st = slip._l10n_at_sz_tarifisch_aktuell()
|
||||
tarif_st = slip._l10n_at_lst_sonstige()
|
||||
gut = fixsatz_st <= tarif_st
|
||||
return {
|
||||
'employee_id': slip.employee_id.id,
|
||||
'kontrollsechstel_ueberhang': ueberhang,
|
||||
'fixsatz_steuer': fixsatz_st,
|
||||
'tarif_steuer': tarif_st,
|
||||
'guenstig': gut,
|
||||
'handlung': (
|
||||
'Keine Korrektur — Fixsätze sind günstiger'
|
||||
if gut else
|
||||
'Aufrollung: Dezemberlauf rechnet die SZ-Steuer '
|
||||
'mit dem Tarif neu (§ 67 Abs 10 Günstig-Aufrollung). '
|
||||
'SV rollt auf den Beitragsmonat (GP4); mBGM-Storno '
|
||||
'(GP7) wenn der Beitragsmonat wechselt.'),
|
||||
}
|
||||
return None
|
||||
|
||||
def _zusammenfassung(self):
|
||||
self.ensure_one()
|
||||
ohne = self.ergebnis_ids.filtered(lambda r: r.guenstig)
|
||||
mit = self.ergebnis_ids.filtered(lambda r: not r.guenstig)
|
||||
return (
|
||||
'Kontrollsechstel-Prüfung %d: %d Mitarbeiter mit '
|
||||
'Überhang, davon %d mit Günstig-Aufrollung (= Tarif '
|
||||
'günstiger als Fixsätze → Korrekturlauf).\n'
|
||||
'§-77-Prüfung: %s.'
|
||||
% (self.jahr, len(self.ergebnis_ids), len(mit),
|
||||
'aktiviert' if self.aufrollung_77 else 'deaktiviert'))
|
||||
|
||||
|
||||
class L10nAtPayrollJahresretroErgebnis(models.TransientModel):
|
||||
_name = 'l10n.at.payroll.jahresretro.ergebnis'
|
||||
_description = 'Jahres-Retro — Prüfergebnis je AN'
|
||||
|
||||
wizard_id = fields.Many2one(
|
||||
'l10n.at.payroll.jahresretro.wizard', required=True,
|
||||
ondelete='cascade')
|
||||
employee_id = fields.Many2one('hr.employee', required=True)
|
||||
kontrollsechstel_ueberhang = fields.Monetary(
|
||||
currency_field='currency_id')
|
||||
fixsatz_steuer = fields.Monetary(
|
||||
currency_field='currency_id',
|
||||
string='SZ-Steuer (Fixsätze)')
|
||||
tarif_steuer = fields.Monetary(
|
||||
currency_field='currency_id',
|
||||
string='SZ-Steuer (Tarif)')
|
||||
guenstig = fields.Boolean(
|
||||
string='Fixsätze günstiger (keine Korrektur)')
|
||||
handlung = fields.Text(string='Handlungsempfehlung')
|
||||
currency_id = fields.Many2one(
|
||||
'res.currency', related='wizard_id.company_id.currency_id')
|
||||
@@ -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