# 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). ' 'Fixsatz: l10n.at.payroll.mitteilung109 ' '(AP17).', }, 'mitteilung_109b': { 'name': '§ 109b-EStG-Meldung (Auslandszahlungen)', 'monatlich': False, 'monat': 2, 'tag_im_monat': 28, 'action': None, 'hilfetext': 'Mitteilung über Zahlungen ins Ausland ' '(§ 109b EStG: §-22-Tätigkeiten, Vermittlung, ' 'Beratung im Inland) elektronisch bis Ende ' 'Februar des Folgejahres; amtlicher Vordruck ' 'bis Ende Jänner (§ 109b Abs 6, RIS ' 'NOR40263383); entfällt unter der ' '100.000-€-Schwelle bzw. bei Steuerabzug ' '§ 99/KSt-Vergleich (Abs 4). Fixsatz: ' 'l10n.at.payroll.mitteilung109 (AP17).', }, 'l17': { 'name': 'Lohnnachweis/Lohnbescheinigung L17 (A1)', 'monatlich': False, 'monat': 2, 'tag_im_monat': 28, 'action': 'l16', 'hilfetext': 'Lohnzettel Finanz je Lohnzahlungszeitraum ' '(DM-ORG E.24, Satzart A1, Version 28 — ' 'zwingender Einsatz ab 01.03.2026, fachliche ' 'Gültigkeit für Zeiträume ab 01.01.2025); ' 'Übermittlung im Jahreslauf mit der L16 ' '(Ende Februar).', }, '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).', }, 'wien_dag': { 'name': 'Wr. Dienstgeberabgabe entrichten (Stadtkasse)', 'monatlich': True, 'tag': 15, 'action': None, 'wien_dag': True, 'hilfetext': 'Wr. DAG („U-Bahn-Steuer“, GesNr 20000133): ' 'Selbstberechnungsabgabe — die im Vormonat ' 'entstandene Abgabenschuld bis zum 15. des ' 'Folgemonats an die Stadtkasse (Magistrat) ' 'entrichten (§ 6 Abs 1). Nur Firmen mit Flag ' 'l10n_at_wien_dag (Beschäftigungsort Wien).', }, 'wien_dag_jahres': { 'name': 'Wr. DAG-Jahreserklärung (Magistrat)', 'monatlich': False, 'monat': 3, 'tag_im_monat': 31, 'action': None, 'wien_dag': True, 'hilfetext': 'Bis 31. März die im Vorjahr entstandene ' 'Abgabenschuld beim Magistrat schriftlich ' 'erklären; darin auch die befreiten ' 'Dienstverhältnisse angeben (§ 6 Abs 2 Wr. ' 'DAG). Nur Firmen mit Flag l10n_at_wien_dag.', }, } 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']) monatliche_arten = ['sv_mbgm', 'db_dz_kommst'] if company.l10n_at_wien_dag: monatliche_arten.append('wien_dag') for art in monatliche_arten: 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', 'mitteilung_109b', 'l17', '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)) # Wr. DAG-Jahreserklärung (31. 3., § 6 Abs 2). if company.l10n_at_wien_dag: self._sicherstellen( company, 'wien_dag_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']