Files
odoo-at-payroll/addons/l10n_at_hr_payroll_private/models/mbgm.py
T
fegger 34e6750fe3 [IMP] l10n_at_hr_payroll_private: mBGM liest TASY-Katalog (BSGR/ERGB), AT-Abwesenheiten mit hr_holidays (AP0+AP1+AP5)
AP0 — mBGM-Tarifierung aus dem TASY-Katalog:
- Katalog vor Hand-Parameter at_mbgm_tarif (quelle katalog/parameter);
  Company-Feld l10n_at_dienstgebergruppe (Default NBALG),
  Vertrags-m2m l10n_at_ergaenzung_ids; ERGB wird im T1 real belegt
- BSGR-Korrektur nach TASY-Bezeichnern: Lehrlinge Arbeiter->B045 /
  Angestellte->B044 (invertiert bisher), geringfuegig/fallweise
  B010/B030 je Personalgruppe
- NSchAB-Feld: Falschbezeichnung Schlechtwetter -> Nachtschwerarbeit
  (3,80 % = NB-Zweig DG ab 2020); Flag nur noch Parameter-Fallback
- Tests: Katalog schlaegt manipulierten Parameter, ERGB-Emission,
  BSGR-Mapping

AP1 — M4-Test auf explizit ungemappte Abgangsursache (AGRD 00-Fallback)

AP5 — hr_holidays/hr_work_entry_holidays als Pflicht-Dependence:
- AT-WETs ATP_URLAUB/ATP_KRANK/ATP_KARENZ (is_leave) + AT-Urlaubsarten
  (UrlG, Krankenstand mit Krankmeldungspflicht, unbezahlte Karenz)
  nach Odoo-Lokalisierungsmuster; ATP_AU (Arbeitsunfall) folgt mit AP4
- Test: validierter Krankenstand erzeugt Work Entries mit AT-WET
2026-09-12 23:17:38 +02:00

580 lines
27 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Part of the odoo-at-payroll project. License: LGPL-3.
"""Monatliche Beitragsgrundlagenmeldung mBGM/JASV (GP7/AP5) — DM-ORG
42.7.0, Kapitel E.32 (BEST ``MB``, mBGM-Version 02; PDF-verifizierte
Extraktion 2026-09-12).
Hierarchie: Paket (SART ``PS`` Selbstabrechner, Länge 305) → mBGM
(SART ``G1``, 326) → Tarifblock (SART ``T1``, 42) → Verrechnungsbasis
(``BS``, 33) → Verrechnungsposition (``V1``, 42).
Fachliche Grundlagen:
- **KEIN DN/DG- oder Zweig-Split**: die Standardposition ``T01``
trägt den GESAMT-Prozentsatz des Tarifsystems. Quelle ist der
TASY-Katalog (AP0, Variante b): Beschäftigtengruppe + Ergänzungen
(ERGB am Vertrag) + Dienstgebergruppe (Company) → Tarifgruppe →
Tarifset; fehlt der Katalog, greift der Hand-Parameter
``at_mbgm_tarif`` als Fallback (Werte aus dem ÖGK-Handbuch,
`.additional_docs/oegk/`). Fehlende Sätze brechen den Lauf
listenweise ab (keine Erfindung von Tarifen).
- **BSGR** (DM-ORG D.48/TASY): B001 Arbeiter, B002 Angestellte,
B045 Arbeiterlehrlinge, B044 Angestelltenlehrlinge, B010 gering-
fällige Arbeiter, B030 geringfügige Angestellte — TASY-Bezeichner
verifiziert (B044 = „Ang. Lg.“, B045 = „Arb. Lg.“).
- **ERGB** (D.49): Ergänzungen am Vertrag (z. B. E01 Nachtschwer-
arbeit, E02 Schlechtwetterentschädigung) werden im Tarifblock
belegt und steuern die Tarifgruppen-Auflösung im Katalog; das
Company-Flag ``l10n_at_nschab`` bleibt Fallback ohne Katalog.
- **Abschläge als eigene Verrechnungspositionen** (D.60, verifiziert):
A01/A02/A03 = Minderung AV auf 2/1/0 % (§ 2a AMPFG, DN-seitig —
Abschlag = AB × (2,95 Zielsatz) %), A09 = UV-Entfall ab 60
(1,10 %), A10 = AV+IE-Entfall Pensionsanspruch (6,00 %), A15 =
PV-Halbierung Bonusphase (11,40 %). A02/A10 schließen einander aus
(Konkurrenz a4: Entfall vor Minderung — wie der GP4-Kern rechnet).
- **BV (BMSVG)**: eigene Verrechnungsbasis ``BV`` (ungedeckelt, GP5)
+ Position ``V01`` (1,53 %); SZ-Basis ``SZ`` (§-54-gedeckelt) +
Position ``T02`` mit dem SZ-Gesamtsatz.
- **RSUM aus Basis × Satz** (mBGM-eigene Tarifierung); Konsistenz
gegen die GP4-Payslip-Summen als Prüfwarnung (Toleranz 0,10 €).
- **Fehlanzeige**: Paket ohne mBGM-Sätze (ANZM 0) für Beitragsmonate
ohne meldbare Daten (GP5-Deferral; DM-ORG-Form ⚠ Parallellauf
verifizieren). **Storno**: R1-Sätze mit REFU + identischem BZRM
(VSUM = gestornierte VSUM) für den Nachtragslauf; Nachreichung als
neue Deklaration (GP4-Deferral Aufrollung).
- Fallweise Beschäftigte (G3/G4 mit tagesgenauen Tarifblöcken) und
Geringfügige ohne Tarifparameter: dokumentierter Rest — der Lauf
bricht listenweise ab, bis `at_mbgm_tarif` gepflegt ist.
Dateibasiert (Prämisse P7): Generierung + Prüfprotokoll; Übertragung
beim Mandanten. Fristen: Selbstabrechner 15., Vorschreibebetriebe 7.
des Folgemonats (lb-mel-08) — Fristenkalender GP8."""
from datetime import date, timedelta
import base64
from odoo import api, fields, models
from odoo.exceptions import UserError
from odoo.addons.l10n_at_hr_payroll.models.elda import (
L10N_AT_ELDA_IDTEIL,
L10nAtEldaBestand,
L10nAtEldaDatei,
L10nAtEldaSatz,
l10n_at_euro_cent,
)
from odoo.addons.l10n_at_hr_payroll.models.lohnzettel_l16 import \
l10n_at_namen
# Paket (E.32, Selbstabrechner PS) — nach IDTEIL, Länge 305.
MBGM_PAKET = [
(2, 'REFP', 21, 40, 'a'),
(3, 'BKNR', 61, 10, 'a'),
(4, 'DGNA', 71, 70, 'a'),
(5, 'MPKE', 141, 30, 'a'),
(6, 'JAGB', 171, 1, 'a'),
(7, 'DTEL', 172, 50, 'a'),
(8, 'MAIL', 222, 60, 'a'),
(9, 'BZRM', 282, 6, 'n'), # MMJJJJ
(10, 'GSVZ', 288, 1, 'a'),
(11, 'GSUM', 289, 11, 'n'), # Σ VSUM (Cent)
(12, 'ANZM', 300, 6, 'n'), # Anzahl mBGM (G/R-Sätze)
]
# mBGM-Satz (E.32, G1 Selbstabrechner) — nach IDTEIL, Länge 326.
MBGM_SATZ = [
(2, 'REFW', 21, 40, 'a'),
(3, 'REFU', 61, 40, 'a'),
(4, 'REFV', 101, 40, 'a'),
(5, 'VSNR', 141, 10, 'n'),
(6, 'FANA', 151, 70, 'a'),
(7, 'VONA', 221, 70, 'a'),
(8, 'VSUM', 291, 11, 'n'), # Σ RSUM (Cent)
(9, 'VERG', 302, 1, 'a'), # 1 = SV+BV mit Versicherungszeit
(10, 'INF1', 303, 12, 'a'),
(11, 'INF2', 315, 12, 'a'),
]
# Tarifblock (E.32, T1) — nach IDTEIL, Länge 42.
MBGM_TARIFBLOCK = [
(2, 'BSGR', 21, 4, 'a'), # Beschäftigtengruppe (SV-Tarifsystem)
(3, 'ERGB', 25, 15, 'a'), # 5 × 3-stellige Ergänzungen
(4, 'VVON', 40, 2, 'n'), # Beginn der Verrechnung (Tag)
(5, 'KEUE', 42, 1, 'a'), # enthält KE/UE
]
# Verrechnungsbasis (E.32, BS) — nach IDTEIL, Länge 33.
MBGM_BASIS = [
(2, 'VBTY', 21, 2, 'a'), # AB/SZ/BV/UU …
(3, 'VBBT', 23, 11, 'n'), # Cent
]
# Verrechnungsposition (E.32, V1) — nach IDTEIL, Länge 42.
MBGM_POSITION = [
(2, 'VPTY', 21, 3, 'a'), # T01/T02/V01/A01…A15/Z01 …
(3, 'VPVZ', 24, 1, 'a'), # Vorzeichen Prozentsatz
(4, 'VPTA', 25, 6, 'n'), # Prozentsatz ×1000 (3 Nachkommastellen)
(5, 'RSVZ', 31, 1, 'a'), # Vorzeichen RSUM
(6, 'RSUM', 32, 11, 'n'), # Cent
]
class L10nAtPayrollMbgm(models.Model):
_name = 'l10n.at.payroll.mbgm'
_description = 'Monatliche Beitragsgrundlagenmeldung (mBGM/JASV)'
_inherit = ['mail.thread']
_order = 'beitragsmonat desc, id desc'
company_id = fields.Many2one(
'res.company', required=True,
default=lambda self: self.env.company)
beitragsmonat = fields.Date(
required=True, string='Beitragsmonat',
help='Erster Tag des Beitragsmonats (MMJJJJ).')
art = fields.Selection(
[('MB', 'mBGM (regulär)'),
('ST', 'Storno (R-Sätze, iVm REFU)'),
('FZ', 'Fehlanzeige (Paket ohne mBGM-Sätze)')],
required=True, default='MB', tracking=True)
storno_id = fields.Many2one(
'l10n.at.payroll.mbgm', string='Stornierte Deklaration',
help='Art ST: Referenz auf die ursprüngliche Deklaration; '
'REFU der R-Sätze = REFW der stornierten mBGM je AN.')
line_ids = fields.One2many(
'l10n.at.payroll.mbgm.line', 'mbgm_id', string='mBGM-Sätze')
datei = fields.Binary(attachment=True, 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 = [(
'mbgm_company_monat_art_uniq',
'unique(company_id, beitragsmonat, art)',
'Für diesen Beitragsmonat existiert bereits eine mBGM-'
'Deklaration dieser Art.')]
def _l10n_at_slips(self):
self.ensure_one()
erster = self.beitragsmonat
letzter = (date(erster.year, erster.month, 28)
+ timedelta(days=4)).replace(day=1) \
- timedelta(days=1)
return self.env['hr.payslip'].search([
('company_id', '=', self.company_id.id),
('date_from', '>=', erster),
('date_from', '<=', letzter),
('state', 'in', ('validated', 'paid')),
])
def _l10n_at_slip(self, employee):
"""Der (eindeutige) Lauf des AN im Beitragsmonat."""
self.ensure_one()
erster = self.beitragsmonat
letzter = (date(erster.year, erster.month, 28)
+ timedelta(days=4)).replace(day=1) \
- timedelta(days=1)
return self.env['hr.payslip'].search([
('company_id', '=', self.company_id.id),
('employee_id', '=', employee.id),
('date_from', '>=', erster),
('date_from', '<=', letzter),
('state', 'in', ('validated', 'paid'))], limit=1)
def _l10n_at_bsgr(self, version, slip):
"""Beschäftigtengruppe (SV-Tarifsystem, TASY-Bezeichner):
B001 Arbeiter, B002 Angestellte; Lehrlinge B045
(Arbeiterlehrlinge) / B044 (Angestelltenlehrlinge);
geringfügig/fallweise B010 (Arbeiter) / B030 (Angestellte).
Ohne Default-Parameter → Lauf bricht ab, bis der Katalog
importiert oder ``at_mbgm_tarif`` gepflegt ist (Tarifsystem-
Werte, keine Erfindung)."""
personalgruppe = getattr(
version, 'l10n_at_personalgruppe', '')
if version.l10n_at_lehrling:
return 'B045' if personalgruppe == 'arbeiter' else 'B044'
if slip.l10n_at_geringfuegig or version.l10n_at_fallweise:
return 'B010' if personalgruppe == 'arbeiter' else 'B030'
return 'B001' if personalgruppe == 'arbeiter' else 'B002'
def _l10n_at_tarif(self, bsgr, version=None):
"""T01/T02-Gesamtsätze je BSGR — Katalog vor Hand-Parameter.
1. TASY-Katalog: Tarifgruppe aus BSGR + ERGB am Vertrag +
Dienstgebergruppe der Company am Stichtag; Sätze = Summe
der Tarifset-Komponenten je Verrechnungsbasis (AB/SZ).
2. Fallback: ``at_mbgm_tarif``-Parameter (BSGR-Schlüssel).
Rückgabe ``{'laufend', 'sz', 'quelle'}`` oder None → Lauf
bricht listenweise ab.
"""
self.ensure_one()
ergaenzungen = version.l10n_at_ergaenzung_ids.mapped('code') \
if version is not None else []
dgg = self.company_id.l10n_at_dienstgebergruppe or 'NBALG'
tarifgruppe = self.env[
'l10n.at.payroll.tasy.tarifgruppe']._l10n_at_find(
bsgr, ergaenzungen, dgg, self.beitragsmonat)
if tarifgruppe:
ab = tarifgruppe._l10n_at_saetze('AB', self.beitragsmonat)
sz = tarifgruppe._l10n_at_saetze('SZ', self.beitragsmonat)
if ab is None or sz is None:
return None
return {'laufend': ab['gesamt'], 'sz': sz['gesamt'],
'quelle': 'katalog'}
tarif = self.env['hr.rule.parameter']._get_parameter_from_code(
'at_mbgm_tarif', self.beitragsmonat,
raise_if_not_found=False) or {}
eintrag = tarif.get(bsgr)
if eintrag:
return {'laufend': eintrag['laufend'], 'sz': eintrag['sz'],
'quelle': 'parameter'}
return None
def _l10n_at_positionen(self, slip, tarif):
"""Verrechnungspositionen je Tarifblock: T01 (Gesamtsatz,
NSchAB-Zweig via Company-Flag) + Abschläge aus den GP4-
Stichtagsflags; T02 auf SZ; V01 auf BV-Basis. Rückgabe:
[(vpty, vpta, rsum_cent, basis_typ), …] — RSUM aus Basis ×
Satz (mBGM-Tarifierung); Abschläge negativ (RSVZ '-')."""
version = slip.version_id
ab = sum(basis for _m, basis
in slip._l10n_at_sv_basis_laufen_monate())
# NSchAB über das Company-Flag nur im Parameter-Fallback —
# im Katalog-Pfad steckt der NB-Zweig über die ERGB-E01-
# Tarifgruppe (Dienstgebergruppe NBNSA) bereits im T01.
nschab = 3.80 if (self.company_id.l10n_at_nschab
and tarif.get('quelle') != 'katalog'
and not version.l10n_at_lehrling
and getattr(version, 'l10n_at_personalgruppe',
'') == 'arbeiter') else 0.0
satz_laufen = float(tarif['laufend']) + nschab
positionen = [('T01', satz_laufen, ab * satz_laufen / 100.0,
'AB')]
# §-2a-AMPFG: Minderung nur des DN-AV (2,95 %), als eigene
# Abschlagsposition auf JEDER betroffenen Verrechnungsbasis
# (AB und SZ — der Abschlag senkt den Gesamtsatz je Basis;
# DN-seitig, GP4-Konvention). Codes: 0 % → A03, 1 % → A02,
# 2 % → A01 (D.60, verifiziert).
minderung = 0.0
monat = self.beitragsmonat
if not (version.l10n_at_alv_entfall_ab
and monat >= version.l10n_at_alv_entfall_ab):
staffel = self.env['hr.rule.parameter'] \
._get_parameter_from_code(
'at_sv_av_staffel_ampfg', monat,
raise_if_not_found=False) or {}
for _von, bis, dn_satz in staffel.get('tiers', []):
if ab and ab <= float(bis):
minderung = 2.95 - float(dn_satz)
break
if minderung > 0:
code = ('A03' if minderung >= 2.95
else 'A02' if abs(minderung - 1.95) < 0.01
else 'A01')
positionen.append(
(code, 0.0, -ab * minderung / 100.0, 'AB'))
else:
# A10 = Entfall AV+IE (DN-AV + DG-AV + DG-IESG).
positionen.append(
('A10', 0.0, -ab * 6.00 / 100.0, 'AB'))
if version.l10n_at_uv_entfall_ab \
and monat >= version.l10n_at_uv_entfall_ab:
positionen.append(('A09', 0.0, -ab * 1.10 / 100.0, 'AB'))
if version.l10n_at_pv_bonusphase_ab \
and monat >= version.l10n_at_pv_bonusphase_ab:
positionen.append(('A15', 0.0, -ab * 11.40 / 100.0, 'AB'))
sz_basis = slip._l10n_at_svdn_basis_sz()
if sz_basis > 0:
satz_sz = float(tarif['sz'])
positionen.append(
('T02', satz_sz, sz_basis * satz_sz / 100.0, 'SZ'))
if minderung > 0:
code = ('A03' if minderung >= 2.95
else 'A02' if abs(minderung - 1.95) < 0.01
else 'A01')
positionen.append(
(code, 0.0, -sz_basis * minderung / 100.0, 'SZ'))
bv_basis = (slip._at_bv_basis_laufen()
+ slip._at_bv_basis_sonst())
if bv_basis > 0:
positionen.append(('V01', 1.53, bv_basis * 0.0153, 'BV'))
return positionen
def action_erzeugen(self):
for deklaration in self:
fehler = []
warnungen = []
company = deklaration.company_id
bknr = company.l10n_at_beitragskontonummer
if not bknr:
fehler.append('Beitragskontonummer (BKNR) fehlt.')
vstr = (bknr or '')[:2]
deklaration.line_ids.unlink()
datensaetze = []
summe_gesamt = 0
anzahl = 0
if deklaration.art != 'FZ':
slips = deklaration._l10n_at_slips()
nach_an = {}
for slip in slips:
nach_an.setdefault(slip.employee_id, []).append(slip)
for employee, employee_slips in nach_an.items():
version = employee.version_id
if not (version and version.ssnid):
fehler.append(
'Mitarbeiter %s ohne Versicherungsnummer.'
% employee.name)
continue
if len(employee_slips) > 1:
fehler.append(
'Mitarbeiter %s mit mehreren Läufen im '
'Beitragsmonat — mBGM je Lauf ist ein '
'dokumentierter Rest.' % employee.name)
continue
slip = employee_slips[0]
if deklaration.art == 'ST':
# Storno: R1-Satz je stornierter mBGM mit
# REFU = REFW des Originals, VSUM wie Original
# (E.32.2.8 — ohne T-/BS-/V-Sätze).
original = deklaration.storno_id.line_ids \
.filtered(lambda l: l.employee_id == employee)
if not original:
fehler.append(
'Storno %s: keine stornierte mBGM '
'am Mitarbeiter gefunden.' % employee.name)
continue
refw = 'MBGM-ST-%d-%s-%d' % (
company.id,
deklaration.beitragsmonat.strftime('%Y%m'),
employee.id)
storno_satz = L10nAtEldaSatz(
L10N_AT_ELDA_IDTEIL + MBGM_SATZ, 326,
bezeichnung='Storno %s' % employee.name)
storno_satz.setze('SART', 'R1')
storno_satz.setze('REFW', refw)
storno_satz.setze('REFU', original.refw)
storno_satz.setze('VSNR', version.ssnid)
storno_satz.setze('VSUM', abs(original.vsum),
cent=True)
datensaetze.append(storno_satz)
deklaration.env['l10n.at.payroll.mbgm.line']\
.create({
'mbgm_id': deklaration.id,
'employee_id': employee.id,
'versicherungsnummer': version.ssnid,
'bsgr': original.bsgr,
'refw': refw,
'vsum': original.vsum,
})
summe_gesamt += original.vsum
anzahl += 1
continue
bsgr = deklaration._l10n_at_bsgr(version, slip)
tarif = deklaration._l10n_at_tarif(bsgr, version)
if not tarif:
fehler.append(
'Mitarbeiter %s: kein Tarifsatz für BSGR '
'%s (weder TASY-Katalog noch Parameter '
'at_mbgm_tarif am Beitragsmonat).'
% (employee.name, bsgr))
continue
if slip.l10n_at_geringfuegig \
or version.l10n_at_fallweise:
fehler.append(
'Mitarbeiter %s: geringfügige/fallweise '
'Beschäftigung in der mBGM ist ein '
'dokumentierter Rest (BSGR %s, Tarifsystem '
'verifizieren).' % (employee.name, bsgr))
continue
positionen = deklaration._l10n_at_positionen(
slip, tarif)
vsum = sum(rsum for _c, _s, rsum, _b in positionen)
# Konsistenz gegen die GP4-Payslip-Summen.
ist = (slip._l10n_at_svdn_gesamt()
+ slip._l10n_at_svdg_gesamt()
+ slip._l10n_at_svdn_sz()
+ sum(
slip._l10n_at_svdg_sz(k)
for k in ('kv', 'pv', 'av', 'uv', 'iesg'))
+ slip._at_bv_beitrag())
if abs(vsum - ist) > 0.10:
warnungen.append(
'Mitarbeiter %s: mBGM-VSUM %.2f weicht um '
'%.2f von den GP4-Beitragssummen ab '
'(Tarif-/Rundungsdifferenz prüfen).'
% (employee.name, vsum, vsum - ist))
deklaration.env['l10n.at.payroll.mbgm.line'].create({
'mbgm_id': deklaration.id,
'employee_id': employee.id,
'versicherungsnummer': version.ssnid,
'bsgr': bsgr,
'refw': 'MBGM-%d-%s-%d' % (
company.id,
deklaration.beitragsmonat.strftime('%Y%m'),
employee.id),
'vsum': vsum,
})
datensaetze.extend(
deklaration._l10n_at_mbgm_datensaetze(
employee, version, bsgr, positionen, vsum))
summe_gesamt += vsum
anzahl += 1
paket = L10nAtEldaSatz(
L10N_AT_ELDA_IDTEIL + MBGM_PAKET, 326,
bezeichnung='mBGM-Paket')
paket.setze('SART', 'PS')
paket.setze('VSTR', vstr)
paket.setze('REFP', 'MBGM-%d-%s' % (
company.id,
deklaration.beitragsmonat.strftime('%Y%m')))
paket.setze('BKNR', bknr)
paket.setze('DGNA', company.name[:70])
paket.setze('JAGB', 'N')
paket.setze('BZRM', deklaration.beitragsmonat.strftime(
'%m%Y'))
paket.setze('GSVZ', '+' if summe_gesamt >= 0 else '-')
paket.setze('GSUM', abs(summe_gesamt), cent=True)
paket.setze('ANZM', anzahl)
datensaetze.insert(0, paket)
if fehler:
deklaration.protokoll = 'FEHLER:\n%s\nWARNUNGEN:\n%s' % (
'\n'.join(fehler), '\n'.join(warnungen) or '')
raise UserError('mBGM-Lauf mit Fehlern abgebrochen:\n'
+ '\n'.join(fehler))
bestand = L10nAtEldaBestand(
'MB', vstr, '02',
hersteller={'name': 'odoo-at-payroll', 'kfz': 'A',
'plz': company.partner_id.zip,
'ort': company.partner_id.city,
'strasse': company.partner_id.street},
datensaetze=datensaetze)
datei = L10nAtEldaDatei([bestand])
deklaration.write({
'datei': base64.b64encode(datei.render()),
'datei_name': 'mbgm_%s.txt' % (
deklaration.beitragsmonat.strftime('%Y%m'),),
'protokoll': 'WARNUNGEN:\n%s' % (
'\n'.join(warnungen) or ''),
'state': 'done',
})
return True
def _l10n_at_mbgm_datensaetze(self, employee, version, bsgr,
positionen, vsum):
"""G1 + Tarifblock + je Basis (AB/SZ/BV) die Positionen als
eigene Sätze (BS/V folgen hierarchisch dem Tarifblock)."""
self.ensure_one()
slip = self._l10n_at_slip(employee)
mbgm_satz = L10nAtEldaSatz(
L10N_AT_ELDA_IDTEIL + MBGM_SATZ, 326,
bezeichnung='mBGM %s' % employee.name)
mbgm_satz.setze('SART', 'G1')
mbgm_satz.setze('REFW', 'MBGM-%d-%s-%d' % (
self.company_id.id,
self.beitragsmonat.strftime('%Y%m'), employee.id))
mbgm_satz.setze('VSNR', version.ssnid)
familienname, vorname = l10n_at_namen(employee)
mbgm_satz.setze('FANA', familienname[:70])
mbgm_satz.setze('VONA', (vorname or '')[:70])
mbgm_satz.setze('VSUM', abs(vsum), cent=True)
mbgm_satz.setze('VERG', '1')
tarifblock = L10nAtEldaSatz(
L10N_AT_ELDA_IDTEIL + MBGM_TARIFBLOCK, 326,
bezeichnung='Tarifblock %s' % employee.name)
tarifblock.setze('SART', 'T1')
tarifblock.setze('BSGR', bsgr)
# ERGB (DM-ORG D.49): bis zu 5 Ergänzungen à 3 Stellen —
# Grundstellung blank, wenn keine gepflegt (Katalog-
# Normalfall ohne Ergänzung).
ergaenzungen = version.l10n_at_ergaenzung_ids.mapped('code')[:5]
if ergaenzungen:
tarifblock.setze('ERGB', ''.join(ergaenzungen))
tarifblock.setze('VVON', '1')
saetze = [mbgm_satz, tarifblock]
for code, satz_prozent, rsum, basis_typ in positionen:
basis = L10nAtEldaSatz(
L10N_AT_ELDA_IDTEIL + MBGM_BASIS, 326,
bezeichnung='Basis %s %s' % (basis_typ,
employee.name))
basis.setze('SART', 'BS')
basis.setze('VBTY', basis_typ)
basis_wert = abs(rsum) * 100.0 / satz_prozent \
if satz_prozent else 0.0
if basis_typ == 'AB':
basis_wert = sum(
b for _m, b in
slip._l10n_at_sv_basis_laufen_monate())
elif basis_typ == 'SZ':
basis_wert = slip._l10n_at_svdn_basis_sz()
else:
basis_wert = slip._at_bv_basis_laufen() \
+ slip._at_bv_basis_sonst()
basis.setze('VBBT', basis_wert, cent=True)
position = L10nAtEldaSatz(
L10N_AT_ELDA_IDTEIL + MBGM_POSITION, 326,
bezeichnung='Position %s %s' % (code, employee.name))
position.setze('SART', 'V1')
position.setze('VPTY', code)
position.setze('VPTA', satz_prozent, skala=3)
position.setze('RSVZ', '-' if rsum < 0 else '+')
position.setze('RSUM', abs(rsum), cent=True)
saetze.extend([basis, position])
return saetze
class ResCompany(models.Model):
_inherit = 'res.company'
l10n_at_nschab = fields.Boolean(
string='AT Nachtschwerarbeitsbeitrags-Betrieb (mBGM-Fallback)',
help='Betrieb mit Nachtschwerarbeit (ÖGK-Tarifsystem '
'„Normalbetriebe mit Nachtschwerarbeit“, Dienstgebergruppe '
'NBNSA): ohne TASY-Katalog erhalten Arbeiter (BSGR B001) '
'im mBGM-T01 den um NSchAB 3,80 % erhöhten Gesamtsatz. '
'Mit Katalog stattdessen die Ergänzung E01 am Vertrag '
'setzen — der NB-Zweig ist dann Teil der E01-Tarifgruppe.')
l10n_at_dienstgebergruppe = fields.Char(
string='AT Dienstgebergruppe (SV-Tarifsystem)',
default='NBALG',
help='Code der Dienstgebergruppe des TASY-Tarifsystems, z. B. '
'NBALG „Normalbetriebe — Allgemeiner Teil“, NBNSA '
'(Nachtschwerarbeit), NBBAU (Schlechtwetter), HAUSH '
'(Haushalte), DGONO (ohne Niederlassung). Bestimmt mit '
'BSGR + Ergänzungen die Tarifgruppe der mBGM-Tarifierung '
'(AP0). Vollständige Liste: Tarifsystem-Katalog-Menü '
'nach dem TASY-Import.')
class HrVersion(models.Model):
_inherit = 'hr.version'
l10n_at_ergaenzung_ids = fields.Many2many(
'l10n.at.payroll.tasy.ergaenzung',
'l10n_at_hr_payroll_version_ergaenzung_rel',
'version_id', 'ergaenzung_id',
string='AT Ergänzungen (ERGB, SV-Tarifsystem)',
groups="hr_payroll.group_hr_payroll_user",
help='Ergänzungen zur Beschäftigtengruppe (DM-ORG D.49) für die '
'mBGM-Tarifierung — gemeinsam mit BSGR und Dienstgebergruppe '
'bilden sie die Tarifgruppe. Z. B. E01 Nachtschwerarbeit, '
'E02 Schlechtwetterentschädigung. Katalog nach dem TASY-'
'Import; ohne Eintrag gilt der Normalfall ohne Ergänzung.')
class L10nAtPayrollMbgmLine(models.Model):
_name = 'l10n.at.payroll.mbgm.line'
_description = 'mBGM — Satzdaten je AN'
_order = 'employee_name'
mbgm_id = fields.Many2one(
'l10n.at.payroll.mbgm', required=True, ondelete='cascade',
index=True)
employee_id = fields.Many2one('hr.employee', required=True)
employee_name = fields.Char(related='employee_id.name', store=True,
readonly=True)
versicherungsnummer = fields.Char()
bsgr = fields.Char(string='Beschäftigtengruppe')
refw = fields.Char(string='Referenzwert (REFW)')
vsum = fields.Monetary(
string='VSUM', currency_field='currency_id')
currency_id = fields.Many2one(
'res.currency', related='mbgm_id.company_id.currency_id')