[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:
2026-09-12 19:58:53 +02:00
parent ea14c48cca
commit c843701d1a
10 changed files with 820 additions and 5 deletions
@@ -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='112; 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']