34e6750fe3
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
580 lines
27 KiB
Python
580 lines
27 KiB
Python
# 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') |