Files
odoo-at-payroll/addons/l10n_at_hr_payroll/models/mitteilung_109.py
T
fegger ec33dfacf2 [IMP] l10n_at_hr_payroll(_private): Meldewesen-Reste (AP17) und Status-Sonderfälle (AP18/AP19)
AP17 — Lohnnachweis L17, Finanz-Mitteilungen § 109a/§ 109b, GfB-Jahreszahlung:
- L17 (DM-ORG E.24, BEST LF, SART A1/1100, Version 28): je
  Lohnzahlungszeitraum je DN aus den GP3-Kennzahlen-Flags; Prüfkatalog-
  Semantik (KZ 350 = Brutto gesamt, 351/352/354 Teilbeträge) und formale
  Prüfungen (350-Formel, 357/347-Caps, aliquotierte 354/HOPA-Caps);
  ODL-PDF-Extraktion der Kapitel E.24/E.16/E.26 (txt feldversetzt)
- Finanz-Mitteilungen § 109a (W1, E.16) und § 109b (B1, E.26): Modelle
  je Leistungserbringer/Auftragnehmer, Schwellen als hr.rule.parameter
  (450/900 — § 109a-VO; 100.000 — § 109b Abs 4 lit a, RIS NOR40263383),
  AL31/32/33, LKZ1-7, UID, bis 3 maßgebliche natürliche Personen,
  KORR K/S; Unter-Schwellen-Zeilen entfallen mit Protokollhinweis
- mBGM: geringfügig Beschäftigte (BSGR B010/B030) — T01 § 53a-UV-
  Pauschale + V01 BMSVG 1,53 %, kein T02 (GfB-SZ beitragsfrei,
  § 49 Abs 3); JAGB-Pakettrennung für die jährliche Abrechnung
  (§ 58 Abs 8 ASVG iVm § 6 Abs 2a BMSVG, DM-ORG D.52) mit
  2,5 vH-Zuschlag (Z04) in der GfB-Jahresabrechnung (Fälligkeit
  15. 1. Folgejahr)
- Fristenkalender: L17- und § 109b-Fristarten (Ende Februar;
  § 109a/§ 109b EStG und § 58 Abs 8 ASVG RIS-verifiziert)

AP18 — Schwerarbeit/NSchG (Beschaffungsgate erledigt, RIS):
- NSchG (RIS 10008502, BGBl 1981/354) Art XI Abs 3 (2 vH Stammnorm,
  3,80 % via Sistierung) und Abs 6 (Nachtschwerarbeitsmonat: 6-Tage-
  Kriterium, Rückblick-Fenster 12/18, Randmonat); ASchV (20004642)
  § 1/§ 4/§ 5; DM-ORG E.22.2-Erstellvorschriften (Z 1 analog NSchG,
  Z 2/4/5 ≥ 15 Tage, Z 3/GfB nicht melden)
- Kern: Schwerarbeitsmeldung (BEST SM, SART 65/66, Länge 800,
  Tätigkeitsblöcke 26 × 10) mit Altersfilter- und Erstellvorschrift-
  Prüfungen
- Privat: Nachtschwerarbeitsmonat-Zähler (WET ATP_NACHTSCHWER,
  Art-XI-Abs-6-Fenster inkl. Randmonat-Proxy) als stored Flag am Lauf

AP19 — Status- und Verhinderungs-Sonderfälle:
- hr.version-Status (Organ ≤/≥ 25 %, lb-vst-03): Organ ≥ 25 % — kein
  SV-Verhältnis (SVDN/SVDG 0 via SV-Satz-Override, mBGM-Ausschluss,
  LSt läuft weiter); Ferialpraktikanten-Flag (dokumentativ — BMSVG § 6
  nennt keinen Ausschluss, Entgelt-/SV-Logik bleibt normal)
- Präsenz-/Ausbildungs-/Zivildienst-Fiktion (§ 7 Abs 1/2 BMSVG):
  fiktive BV-Bemessungsgrundlage 435,90 € (KBGG § 3 Abs 1 idF vor
  BGBl I 53/2016, lb-vor-04) aliquotiert auf ATP_PRD_TAGE
- Dienstverhinderungs-Sonderfälle bleiben dokumentiert offen
  (lb-dvh-01/04, KB-); Feiertags-EFZ ist durch das AP4-WET-Modell
  abgedeckt

Tests: 21 neue (L17-Positionen/Prüfkatalog, W1/B1-Schwellen, GfB-mBGM/
JAGB/Jahresabrechnung, NSchM-Fenster/Randmonat, SM-Fixsatz, Organ-Sperre,
PRD-Fiktion); Suite 252/252 grün auf frischer -i-DB (85 Kern, 18 GemBG,
213 Privat, 10 Dokumente). Dokumentierte Reste: DZR-36-Fenster,
Randmonat-Proxy und UV-Pauschale-vs-mBGM im Parallellauf (GP8),
fallweise mBGM (G3/G4), 2027er-Schwellen (C7).
2026-09-16 19:12:08 +02:00

573 lines
26 KiB
Python

# Part of the odoo-at-payroll project. License: LGPL-3.
"""Finanz-Mitteilungen § 109a/§ 109b EStG (AP17) — DM-ORG 42.7.0,
Kapitel E.16 (Satzart ``W1``, Code „W“, Länge 1100, Version 28) und
E.26 (Satzart ``B1``, Code „B“, Länge 1100, Version 28); PDF-verifizierte
ODL-Extraktion 2026-09-16. Beide Mitteilungssätze laufen im BEST ``LF`` /
VSTR ``94`` mit dem Informationssatz I1 (ARTD „LZ“, D.4).
Rechtsanker (RIS-verifiziert 2026-09-16, `.additional_docs/ris_normtexte`
-Regime: NormDokument.wxe-Downloads):
- **§ 109a EStG** (NOR40263382, Fassung 20.07.2024): Mitteilungen über
Leistungen von (Sub-)Unternehmern/PVG — Name/Anschrift/SVN bzw. StNr,
Art der Leistung (D.34: 8 Tätigkeitsgruppen), Kalenderjahr, Entgelt +
Umsatzsteuer. Entgelt = ohne USt einschließlich Kostenersätze und
Sachbezüge; bei freien Dienstnehmern inkl. DN-SV-Anteil und
BMV-Kassen-Zahlungen (B270/B271).
- **§ 109b EStG** (NOR40263383, Fassung 20.07.2024): Zahlungen INS
AUSLAND für Leistungen § 22 (AL31), Vermittlung (AL32), kaufm./techn.
Beratung im Inland (AL33). Angaben je Leistungserbringer (incl.
Länderkennung(en) der Zielländer, UID) sowie je Personengemeinschaft/
Körperschaft die im Inland maßgeblich auftretende natürliche Person
(bis 3 NP-Blöcke). Mitteilung unterbleibt, wenn a) die Kalenderjahres-
Zahlungen desselben Leistungserbringers 100.000 € nicht übersteigen,
b) ein Steuerabzug § 99 zu erfolgen hat, c) die ausländische KSt um
nicht mehr als 10 %-Punkte unter der österreichischen KSt liegt
(Abs 4; a) automatisiert, b)/c) als Fallauswahl).
- **Fristen** (Fristenkalender): § 109a — Ende Februar (Fristart
`paragraph_109a`); § 109b Abs 6 — elektronisch bis Ende Februar des
Folgejahres (amtlicher Vordruck bis Ende Jänner).
Schwellen als `hr.rule.parameter` `at_m109_schwellen` (versioniert):
m109a_leistung 450 €, m109a_kumulativ 900 € (je Leistungserbringer,
Kalenderjahr — Fristenkalender/§-109a-VO), m109b_jahr 100.000 €.
Storno: KORR „S“ (W1/B1) — E.16.1/E.26.1 zulassen beide Varianten
(Beträge auf 0 oder KORR „S“)."""
from datetime import date, datetime
import base64
from odoo import api, fields, models
from odoo.exceptions import UserError
from .elda import (
L10N_AT_ELDA_IDTEIL,
L10nAtEldaBestand,
L10nAtEldaDatei,
L10nAtEldaSatz,
)
from .lohnzettel_l16 import L16_INFORMATIONSSATZ
# ------------------------------------------------------------------
# W1 — Mitteilungen gemäß § 109a EStG (E.16; Länge 1100) — Datenteil
# ------------------------------------------------------------------
MITTEILUNG_W1_SATZ = [
(2, 'FSART', 21, 1, 'a'), # W
(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'),
(9, 'DTUE', 52, 8, 'n'), # JJJJMMTT, wie I1
(10, 'ZTUE', 60, 6, 'n'),
(11, 'REFN', 66, 30, 'a'), # je AG+Jahr+Auftragsverhältnis eindeutig
(12, 'INTE', 96, 2, 'a'),
(13, 'FEHL', 98, 20, 'a'),
(14, 'STAT', 118, 1, 'a'),
(15, 'FIND', 119, 1, 'a'),
(16, 'JAHR', 120, 4, 'n'),
(17, 'AVSN', 124, 10, 'n'), # Auftragnehmer VN LLLPTTMMJJ
(19, 'ASTN', 134, 9, 'n'), # Auftragnehmer Steuernummer
(20, 'ANAM', 143, 37, 'a'),
(21, 'ATIT', 180, 20, 'a'),
(22, 'AADR', 200, 37, 'a'),
(23, 'ALKZ', 237, 3, 'a'),
(24, 'APLZ', 240, 10, 'a'),
(25, 'AORT', 250, 30, 'a'),
(26, 'ARTE', 280, 2, 'n'), # Art der erbrachten Leistung (D.34)
(27, 'V341', 282, 1, 'a'),
(28, 'B341', 283, 10, 'n'), # Entgelt (KZ 341)
(29, 'V344', 293, 1, 'a'),
(30, 'B344', 294, 10, 'n'), # Umsatzsteuer (KZ 344)
(31, 'FANRA', 304, 2, 'n'), # Finanzamtsnummer Auftraggeber
(32, 'STNRA', 306, 7, 'n'), # Steuernummer Auftraggeber
(33, 'DVRNA', 313, 7, 'n'), # DVR-Nummer Auftraggeber
(34, 'TELN', 320, 20, 'a'),
(35, 'FAXN', 340, 20, 'a'),
(36, 'KORR', 360, 1, 'a'), # K/S
(37, 'WOBD', 361, 12, 'a'),
(38, 'ZOBD', 373, 15, 'a'),
(39, 'V270', 388, 1, 'a'),
(40, 'B270', 389, 10, 'n'), # DN-Anteil SV (KZ 270)
(41, 'V271', 399, 1, 'a'),
(42, 'B271', 400, 10, 'n'), # eingezahlter BMV-Kasse-Beitrag
(43, 'RE43', 410, 591, 'a'),
(44, 'REFN2', 1001, 30, 'a'),
(45, 'RE45', 1031, 70, 'a'),
]
# ------------------------------------------------------------------
# B1 — Mitteilungen gemäß § 109b EStG (E.26; Länge 1100) — Datenteil
# ------------------------------------------------------------------
MITTEILUNG_B1_SATZ = [
(2, 'FSART', 21, 1, 'a'), # B
(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'),
(9, 'DTUE', 52, 8, 'n'), # JJJJMMTT, wie I1
(10, 'ZTUE', 60, 6, 'n'),
(11, 'REFN', 66, 30, 'a'), # je AG+Jahr+Auftragsverhältnis eindeutig
(12, 'INTE', 96, 2, 'a'),
(13, 'FEHL', 98, 20, 'a'),
(14, 'STAT', 118, 1, 'a'),
(15, 'FIND', 119, 1, 'a'),
(16, 'JAHR', 120, 4, 'n'),
(17, 'LVSN', 124, 10, 'n'), # Leistungserbringer VN LLLPTTMMJJ
(19, 'LSTN', 134, 9, 'n'), # Leistungserbringer Steuernummer
(20, 'LNAM', 143, 37, 'a'),
(21, 'RE21', 180, 20, 'a'),
(22, 'LADR', 200, 37, 'a'),
(23, 'LLKZ', 237, 3, 'a'),
(24, 'LPLZ', 240, 10, 'a'),
(25, 'LORT', 250, 30, 'a'),
(26, 'RE26', 280, 2, 'a'),
(27, 'V499', 282, 1, 'a'),
(28, 'B499', 283, 10, 'n'), # Zahlungen inkl. USt (KZ 499)
(29, 'V344', 293, 1, 'a'),
(30, 'B344', 294, 10, 'n'), # darin enthaltene USt (KZ 344)
(32, 'STNRA', 304, 9, 'n'), # Steuernummer Auftraggeber
(33, 'RE33', 313, 7, 'a'),
(34, 'TELN', 320, 20, 'a'),
(35, 'FAXN', 340, 20, 'a'),
(36, 'KORR', 360, 1, 'a'), # K/S
(37, 'AL31', 361, 1, 'a'), # § 109b Abs 2 Z 1 (§-22-Tätigkeiten)
(38, 'AL32', 362, 1, 'a'), # Z 2 Vermittlungsleistungen
(39, 'AL33', 363, 1, 'a'), # Z 3 kaufm./techn. Beratung
(40, 'LKZ1', 364, 3, 'a'),
(41, 'LKZ2', 367, 3, 'a'),
(42, 'LKZ3', 370, 3, 'a'),
(43, 'LKZ4', 373, 3, 'a'),
(44, 'LKZ5', 376, 3, 'a'),
(45, 'LKZ6', 379, 3, 'a'),
(46, 'LKZ7', 382, 3, 'a'),
(47, 'UIDL', 385, 15, 'a'), # UID Leistungserbringer
(48, 'UIDA', 400, 15, 'a'), # UID Auftraggeber
(49, 'VSNRA', 415, 10, 'n'), # VN Auftraggeber
(50, 'NPVS1', 425, 10, 'n'), # NP-Block 1 (maßgebliche Person)
(51, 'NPFA1', 435, 2, 'n'),
(52, 'NPST1', 437, 7, 'n'),
(53, 'NPNA1', 444, 30, 'a'),
(54, 'NPAD1', 474, 30, 'a'),
(55, 'NPLK1', 504, 3, 'a'),
(56, 'NPPL1', 507, 10, 'a'),
(57, 'NPOR1', 517, 30, 'a'),
(58, 'NPUI1', 547, 15, 'a'),
(59, 'NPVS2', 562, 10, 'n'),
(60, 'NPFA2', 572, 2, 'n'),
(61, 'NPST2', 574, 7, 'n'),
(62, 'NPNA2', 581, 30, 'a'),
(63, 'NPAD2', 611, 30, 'a'),
(64, 'NPLK2', 641, 3, 'a'),
(65, 'NPPL2', 644, 10, 'a'),
(66, 'NPOR2', 654, 30, 'a'),
(67, 'NPUI2', 684, 15, 'a'),
(68, 'NPVS3', 699, 10, 'n'),
(69, 'NPFA3', 709, 2, 'n'),
(70, 'NPST3', 711, 7, 'n'),
(71, 'NPNA3', 718, 30, 'a'),
(72, 'NPAD3', 748, 30, 'a'),
(73, 'NPLK3', 778, 3, 'a'),
(74, 'NPPL3', 781, 10, 'a'),
(75, 'NPOR3', 791, 30, 'a'),
(76, 'NPUI3', 821, 15, 'a'),
(77, 'WOBD', 836, 12, 'a'),
(78, 'ZOBD', 848, 15, 'a'),
(79, 'RE79', 863, 123, 'a'),
(80, 'STAT', 986, 5, 'a'),
(81, 'NMEL', 991, 7, 'n'),
(82, 'RE82', 998, 3, 'a'),
(83, 'REFN2', 1001, 30, 'a'),
(84, 'RE84', 1031, 70, 'a'),
]
# W1/B1 gemeinsame Vorzeichen-/Betragsfelder je Zeile.
_BETRAEGE_W1 = (('entgelt', 'V341', 'B341', True),
('umsatzsteuer', 'V344', 'B344', True),
('dn_sv_anteil', 'V270', 'B270', True),
('bmv_beitrag', 'V271', 'B271', True))
_BETRAEGE_B1 = (('entgelt', 'V499', 'B499', True),
('umsatzsteuer', 'V344', 'B344', True))
class L10nAtPayrollMitteilung109Np(models.Model):
_name = 'l10n.at.payroll.mitteilung109.np'
_description = '§ 109b — maßgeblich auftretende natürliche Person'
_order = 'sequence'
sequence = fields.Integer(default=10)
line_id = fields.Many2one(
'l10n.at.payroll.mitteilung109.line', required=True,
ondelete='cascade', index=True)
versicherungsnummer = fields.Char(string='VN (LLLPTTMMJJ)')
finanzamtsnummer = fields.Char(size=2)
steuernummer = fields.Char(size=7)
name = fields.Char(string='Name (Familienname Vorname)')
adresse = fields.Char()
landeskennung = fields.Char(size=3)
plz = fields.Char(size=10)
ort = fields.Char()
uid = fields.Char(string='UID-Nummer')
class L10nAtPayrollMitteilung109Line(models.Model):
_name = 'l10n.at.payroll.mitteilung109.line'
_description = 'Finanz-Mitteilung je Auftraggeber/Auftragnehmer'
_order = 'name_partner'
deklaration_id = fields.Many2one(
'l10n.at.payroll.mitteilung109', required=True,
ondelete='cascade', index=True)
name_partner = fields.Char(
string='Auftragnehmer/Leistungserbringer', required=True)
versicherungsnummer = fields.Char(
string='VN (LLLPTTMMJJ)',
help='D.6-Format; statt VN ist das Geburtsdatenformat ohne '
'VN-Prüfziffer zulässig (Storno-Regel der Kapitel E.16/E.26).')
steuernummer = fields.Char(string='Steuernummer (7, Prüfziffer an 7)')
uid = fields.Char(string='UID-Nummer')
name = fields.Char(string='Familienname und Vorname')
adresse = fields.Char()
landeskennung = fields.Char(size=3, default='AUT')
plz = fields.Char(size=10)
ort = fields.Char(size=30)
entgelt = fields.Monetary(
currency_field='currency_id',
help='W1: Entgelt ohne USt inkl. Kostenersätze und Sachbezüge '
'(bei freien Dienstnehmern inkl. DN-SV-Anteil und BMV-Kasse). '
'B1: Zahlungen einschließlich Umsatzsteuer (KZ 499).')
umsatzsteuer = fields.Monetary(
currency_field='currency_id',
help='W1: in Rechnung gestellte USt (KZ 344); B1: darin '
'enthaltene USt (KZ 344).')
dn_sv_anteil = fields.Monetary(
currency_field='currency_id',
string='DN-Anteil SV (KZ 270, W1)')
bmv_beitrag = fields.Monetary(
currency_field='currency_id',
string='BMV-Kasse-Beitrag (KZ 271, W1)')
art_leistung = fields.Selection(
[(code, label) for code, label in (
('01', 'Aufsichtsrat/Verwaltungsrat'),
('02', 'Bausparkassen-/Versicherungsvertreter'),
('03', 'Stiftungsvorstand'),
('04', 'Vortragende/Lehrende/Unterrichtende'),
('05', 'Kolporteure/Zeitungszusteller'),
('06', 'Privatgeschäftsvermittler'),
('07', 'Funktionäre öff.-rechtl. Körperschaften'),
('08', 'sonstige Leistungen (freier DV, § 4 Abs 4 ASVG)'))],
string='Art der erbrachten Leistung (D.34)')
al31 = fields.Boolean(
string='§ 109b Abs 2 Z 1 (§ 22-Tätigkeit im Inland)')
al32 = fields.Boolean(string='§ 109b Abs 2 Z 2 (Vermittlung)')
al33 = fields.Boolean(string='§ 109b Abs 2 Z 3 (Beratung im Inland)')
ziellaender = fields.Char(
string='Länderkennungen der Zielländer',
help='Bis zu 7 internationale Länderkennungen à 3 Stellen, '
'komma-separiert (z. B. DE,CH).')
ausnahme_grund = fields.Selection(
[('keine', 'Mitteilungspflicht'),
('steuerabzug', 'Steuerabzug gem. § 99 EStG (Abs 4)'),
('kst_gleich', 'Ausländische KSt nicht > 10 %-Punkte unter '
'österreichischer KSt (Abs 4)')],
string='B1-Ausnahme (§ 109b Abs 4)',
default='keine',
help='Die 100.000-€-Schwelle prüft der Lauf automatisch; '
'Steuerabzug und KSt-Vergleich sind Sachentscheidungen '
'als Auswahlfelder.')
refn = fields.Char()
gemeldet = fields.Boolean(
string='Meldepflichtig (Schwellenprüfung)',
default=True, readonly=True,
help='Wird vom Lauf gesetzt: Zeilen unter den Schwellen bzw. mit '
'B1-Ausnahme entfallen aus der Datei.')
korr = fields.Selection(
[('keine', 'Neue Mitteilung'), ('K', 'Berichtigung'),
('S', 'Storno')], default='keine', string='Korrekturindikation')
np_ids = fields.One2many(
'l10n.at.payroll.mitteilung109.np', 'line_id',
string='Maßgebliche natürliche Personen (max. 3)')
currency_id = fields.Many2one(
'res.currency',
related='deklaration_id.company_id.currency_id')
class L10nAtPayrollMitteilung109(models.Model):
_name = 'l10n.at.payroll.mitteilung109'
_description = 'Finanz-Mitteilungen § 109a/§ 109b EStG'
_inherit = ['mail.thread']
_order = 'jahr desc, art, id desc'
company_id = fields.Many2one(
'res.company', required=True,
default=lambda self: self.env.company)
jahr = fields.Integer(required=True, string='Kalenderjahr')
art = fields.Selection(
[('w1', '§ 109a — Werkverträge (W1)'),
('b1', '§ 109b — Auslandszahlungen (B1)')],
required=True, string='Mitteilungsart')
line_ids = fields.One2many(
'l10n.at.payroll.mitteilung109.line', 'deklaration_id',
string='Mitteilungssätze')
datei = fields.Binary(attachment=True, string='LF-Datei', copy=False)
datei_name = fields.Char(copy=False)
protokoll = fields.Text(string='Prüfprotokoll')
state = fields.Selection(
[('draft', 'Entwurf'), ('done', 'Erzeugt')], default='draft',
required=True, tracking=True)
_sql_constraints = [(
'm109_company_jahr_art_uniq', 'unique(company_id, jahr, art)',
'Für dieses Jahr existiert bereits eine Deklaration dieser Art.')]
def _l10n_at_schwellen(self):
self.ensure_one()
return self.env['hr.rule.parameter']._get_parameter_from_code(
'at_m109_schwellen', date(self.jahr, 1, 1),
raise_if_not_found=False) or {}
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}
def _l10n_at_satz(self, line, stnr, heute, jetzt):
"""W1/B1-Mitteilungssatz je Zeile; IDTEIL (SART W1/B1, VSTR 94)."""
self.ensure_one()
if self.art == 'w1':
struktur, sart, fsart = MITTEILUNG_W1_SATZ, 'W1', 'W'
else:
struktur, sart, fsart = MITTEILUNG_B1_SATZ, 'B1', 'B'
satz = L10nAtEldaSatz(
L10N_AT_ELDA_IDTEIL + struktur, 1100,
bezeichnung='%s %s' % (sart, line.name_partner))
satz.setze('SART', sart)
satz.setze('VSTR', '94')
satz.setze('FSART', fsart)
satz.setze('STNRL', stnr)
satz.setze_datum('DTUE', heute, 'JJJJMMTT')
satz.setze_zeit('ZTUE', jetzt)
satz.setze('REFN', line.refn)
satz.setze('FIND', 'J')
satz.setze('JAHR', self.jahr)
if self.art == 'w1':
satz.setze('AVSN', line.versicherungsnummer)
satz.setze('ASTN', line.steuernummer)
satz.setze('ANAM', (line.name or '')[:37])
satz.setze('AADR', (line.adresse or '')[:37])
satz.setze('ALKZ', (line.landeskennung or 'AUT')[:3])
satz.setze('APLZ', (line.plz or '')[:10])
satz.setze('AORT', (line.ort or '')[:30])
satz.setze('ARTE', line.art_leistung)
for feld, vfeld, bfeld, _neg in _BETRAEGE_W1:
betrag = getattr(line, feld)
satz.setze(vfeld, self._vorzeichen(betrag))
satz.setze(bfeld, abs(betrag) if betrag else 0, cent=True)
satz.setze('FANRA', (stnr or '')[:2])
satz.setze('STNRA', (stnr or '')[2:9])
satz.setze('WOBD', 'LE-%d' % line.id)
else:
satz.setze('LVSN', line.versicherungsnummer)
satz.setze('LSTN', line.steuernummer)
satz.setze('LNAM', (line.name or '')[:37])
satz.setze('LADR', (line.adresse or '')[:37])
satz.setze('LLKZ', (line.landeskennung or 'AUT')[:3])
satz.setze('LPLZ', (line.plz or '')[:10])
satz.setze('LORT', (line.ort or '')[:30])
for feld, vfeld, bfeld, _neg in _BETRAEGE_W1:
if feld == 'umsatzsteuer':
satz.setze(vfeld, self._vorzeichen(line.umsatzsteuer))
satz.setze(bfeld, abs(line.umsatzsteuer)
if line.umsatzsteuer else 0, cent=True)
elif feld == 'entgelt':
satz.setze('V499', self._vorzeichen(line.entgelt))
satz.setze('B499', abs(line.entgelt) if line.entgelt
else 0, cent=True)
satz.setze('AL31', 'J' if line.al31 else 'N')
satz.setze('AL32', 'J' if line.al32 else 'N')
satz.setze('AL33', 'J' if line.al33 else 'N')
laender = [k.strip().upper() for k in (
line.ziellaender or '').split(',') if k.strip()][:7]
if laender:
for idx, laender_code in enumerate(laender, start=1):
satz.setze('LKZ%d' % idx, laender_code)
satz.setze('UIDL', (line.uid or '')[:15])
satz.setze('UIDA', (self.company_id.vat or '')[:15])
satz.setze('STNRA', stnr)
satz.setze('WOBD', 'LE-%d' % line.id)
for nummer, np_zeile in enumerate(
line.np_ids.sorted('sequence')[:3], start=1):
satz.setze('NPVS%d' % nummer, np_zeile.versicherungsnummer)
satz.setze('NPFA%d' % nummer, np_zeile.finanzamtsnummer)
satz.setze('NPST%d' % nummer, np_zeile.steuernummer)
satz.setze('NPNA%d' % nummer, (np_zeile.name or '')[:30])
satz.setze('NPAD%d' % nummer, (np_zeile.adresse or '')[:30])
satz.setze('NPLK%d' % nummer,
(np_zeile.landeskennung or '')[:3])
satz.setze('NPPL%d' % nummer, (np_zeile.plz or '')[:10])
satz.setze('NPOR%d' % nummer, (np_zeile.ort or '')[:30])
satz.setze('NPUI%d' % nummer, (np_zeile.uid or '')[:15])
satz.setze('REFN2', line.refn)
return satz
@api.model
def _vorzeichen(self, betrag):
if not betrag:
return None
return '+' if betrag > 0 else '-'
@api.model
def _vorzeichen(self, betrag):
if not betrag:
return None
return '+' if betrag > 0 else '-'
def action_erzeugen(self):
"""W1/B1-Mitteilungssätze je Zeile bauen; Schwellen/Ausnahmen
prüfen (§ 109a-VO 450/900 € je Leistungserbringer; § 109b Abs 4
100.000 € je Leistungserbringer und Kalenderjahr); unter-
schwellige Zeilen bleiben mit ``gemeldet = False`` (Protokoll-
Hinweis) und entfallen aus der Datei. LF-Bestand: I1 (ARTD 'LZ')
+ die meldepflichtigen Sätze."""
for deklaration in self:
fehler = []
warnungen = []
company = deklaration.company_id
stnr = company.l10n_at_lohnsteuernummer
if not stnr:
fehler.append('Lohnsteuernummer der Company fehlt.')
if deklaration.art == 'w1' and len(stnr or '') < 9:
fehler.append(
'FANRA/STNRA erfordern eine 9-stellige '
'Lohnsteuernummer (2 FA + 7 StNr).')
schwellen = deklaration._l10n_at_schwellen()
if not schwellen:
fehler.append(
'Parameter at_m109_schwellen fehlt (Werte nur aus '
'Quelle — RIS/VO-Verifikation vor Belegung).')
if fehler:
deklaration.protokoll = 'FEHLER:\n%s' % (
'\n'.join(fehler))
raise UserError('Mitteilungs-Lauf mit Fehlern abgebrochen:\n'
+ '\n'.join(fehler))
# Schwellenprüfung je Leistungserbringer (Kalenderjahr).
if deklaration.art == 'w1':
je_leistung = float(schwellen.get('m109a_leistung', 0.0))
kumulativ_cap = float(
schwellen.get('m109a_kumulativ', 0.0))
else:
je_leistung = None
kumulativ_cap = float(
schwellen.get('m109b_jahr', 0.0))
nach_le = {}
for line in deklaration.line_ids:
schluessel = (line.name_partner, line.steuernummer or '',
line.versicherungsnummer or '')
nach_le.setdefault(schluessel, []).append(line)
for schluessel, zeilen in nach_le.items():
summe = sum(z.entgelt for z in zeilen)
for line in zeilen:
gemeldet = True
if deklaration.art == 'w1':
unter_schwellen = (
(line.entgelt or 0) <= je_leistung
and summe <= kumulativ_cap)
if unter_schwellen:
gemeldet = False
warnungen.append(
'%s: Entgelt %.2f unter den §-109a-'
'Schwellen (je Leistung ≤ %.2f, kumulativ '
'je LE ≤ %.2f) — keine Meldung.' % (
line.name_partner, line.entgelt or 0,
je_leistung, kumulativ_cap))
else:
if line.ausnahme_grund != 'keine':
gemeldet = False
warnungen.append(
'%s: Ausnahme nach § 109b Abs 4 '
'(%s) — keine Mitteilung.' % (
line.name_partner,
line.ausnahme_grund))
elif summe <= kumulativ_cap:
gemeldet = False
warnungen.append(
'%s: Kalenderjahres-Zahlungen %.2f '
'übersteigen 100.000 € nicht '
'(§ 109b Abs 4 lit a) — keine '
'Mitteilung.' % (
line.name_partner, summe))
elif not (line.al31 or line.al32 or line.al33):
fehler.append(
'%s: B1 ohne Art der Leistung (AL31/32/33).'
% line.name_partner)
elif not (line.ziellaender or '').strip():
fehler.append(
'%s: B1 ohne Zielländer-Länderkennung.'
% line.name_partner)
line.gemeldet = gemeldet
if deklaration.art == 'b1':
for line in deklaration.line_ids:
np_anzahl = len(line.np_ids)
ist_koerperschaft = bool(
line.steuernummer and not line.versicherungsnummer)
if ist_koerperschaft and not np_anzahl:
warnungen.append(
'%s: Personenvereinigung/Körperschaft als '
'Leistungserbringer ohne maßgeblich '
'auftretende natürliche Person '
'(§ 109b Abs 3 Z 2) prüfen.'
% line.name_partner)
if np_anzahl > 3:
fehler.append(
'%s: mehr als 3 maßgebliche natürliche '
'Personen (DM-ORG: 3 Blöcke).' % (
line.name_partner))
if fehler:
deklaration.protokoll = 'FEHLER:\n%s\nWARNUNGEN:\n%s' % (
'\n'.join(fehler), '\n'.join(warnungen) or '')
raise UserError('Mitteilungs-Lauf mit Fehlern '
'abgebrochen:\n' + '\n'.join(fehler))
gemeldete = deklaration.line_ids.filtered('gemeldet')
if gemeldete:
heute = date.today()
jetzt = datetime.now()
partner = company.partner_id
informationssatz = L10nAtEldaSatz(
L10N_AT_ELDA_IDTEIL + L16_INFORMATIONSSATZ, 1100,
bezeichnung='Mitteilungs-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', deklaration.jahr)
informationssatz.setze('GESA', len(gemeldete))
informationssatz.setze('ANZA', len(gemeldete))
datensaetze = [informationssatz] + [
deklaration._l10n_at_satz(line, stnr, heute, jetzt)
for line in gemeldete]
bestand = L10nAtEldaBestand(
'LF', '94', '28', hersteller=deklaration
._l10n_at_hersteller(), datensaetze=datensaetze)
datei = L10nAtEldaDatei([bestand])
deklaration.write({
'datei': base64.b64encode(datei.render()),
'datei_name': 'mitteilung109_%s_%d_%s.txt' % (
deklaration.art, deklaration.jahr, stnr),
})
deklaration.protokoll = 'WARNUNGEN:\n%s' % (
'\n'.join(warnungen) or '')
deklaration.state = 'done'
return True