Files
odoo-at-payroll/addons/l10n_at_hr_payroll/models/monatsreports.py
T
fegger 3c85cc6790 [IMP] l10n_at_hr_payroll: TASY-Tarifsystem-Katalog, AGRD-Auswahl, DG-Kosten-/Lohnkonten-Reports, Wr.-DAG-Grundlagen (AP0-AP3)
AP0 Variante b — TASY-Tarifsystem-Katalog:
- Neue Katalogmodelle (BSGR/ERGB/Dienstgebergruppen/Tarifgruppen/
  Tarifsets/Tarife/Zuordnungen/Zu-Abschläge inkl. Steuerung),
  models/tasy_katalog.py — Auflösung Tarifgruppe (BSGR+ERGB+DGG)
  -> Sätze je Verrechnungsbasis
- Import-Wizard l10n.at.payroll.tasy.katalog.wizard (alle Träger,
  sektionsscharf, idempotent), Views/Menüs, Security
- Realexport-Test gegen TASY-LSWH-Export (B001/B002 = 39,05/37,55 %)

AP1 — eSV-Abmeldegründe (DM-ORG D.22):
- l10n_at_agrd Char -> vollständige Selection (33 Codes),
  Seed der Standard-Abgangsursachen (noupdate, hr-Defaults vorverbunden),
  Form-/Listen-Erbe

AP2a-c — Auswertungen:
- Monatsjournal: eigener Dienstgeberanteile-Block (ATP_DG vorher
  still ausgefiltert); Fix: models/monatsreports.py war nie in
  models/__init__.py importiert (GP7-Reports waren toter Code)
- Neuer Report Dienstgeber-Gesamtkosten (Brutto + LNK aufgeschlüsselt/
  kumuliert je AN + Laufsummen, Spalten FLAF/KommSt/DZ/BV/Wr. DAG)
- Jahreslohnkonto je DN + Betriebsjahreslohnkonto (Jahresbericht-
  Wizard, Monatsmatrix je Lohnart), report/jahresreports.xml

AP3 — Wr. Dienstgeberabgabe (RIS LrW GesNr 20000133 verifiziert):
- Company-Flag l10n_at_wien_dag (Kern), Fristenarten wien_dag/
  wien_dag_jahres (cron-geflaggt, 15./31. 3.), Company-View-Feld,
  Wr.-DAG-Spalte im DG-Kosten-Report
2026-09-12 23:17:00 +02:00

288 lines
12 KiB
Python

# Part of the odoo-at-payroll project. License: LGPL-3.
"""Monatsreports und Jahresberichte der AT-Lohnverrechnung.
- **Monatsdienstliste** (GP7/AP6): je AN die mBGM-Kontrollgrößen —
Abstimmung Abrechnung ↔ SV-Meldung.
- **Monatsjournal** (GP7/AP6, AP2a erweitert): je Lohnart die Summe
über den Lauf, gegliedert nach Bezügen/Abzügen (Bemessung ≠
neutral) und — seit AP2a — **eigenem Block Dienstgeberanteile**
(Kategorie ``ATP_DG``: SV-DG je Zweig, FLAF-DB, KommSt, DZ, BV —
zuvor wurden die neutralen DG-Regeln still ausgefiltert) mit
Kontrollsummen gegen die GP6-Buchung.
- **Dienstgeber-Gesamtkosten** (AP2a): je AN Bruttolohn und die
Lohnnebenkosten aufgeschlüsselt (SV-DG laufend/SZ, FLAF-DB,
KommSt, DZ, BV, weitere DG-Positionen) und kumuliert
(Σ Lohnnebenkosten, Gesamtkosten = Brutto + LNK) mit Laufsummen.
- **Jahreslohnkonto je Dienstnehmer:in** (AP2b) und
**Betriebsjahreslohnkonto** (AP2c): Monatsmatrix je Lohnart aus
``hr.payslip.line`` — Datenbereitstellung über den Wizard
``l10n.at.payroll.jahresbericht.wizard``.
Die Templates bleiben reine Darstellung (QWeb); alle Zahlen werden
hier vorberechnet (read_group-Muster des Contribution Register).
"""
from datetime import date
from odoo import api, fields, models
class L10nAtPayrollMonatsReports(models.AbstractModel):
_name = 'report.l10n_at_hr_payroll.report_monatsdienstliste'
_description = 'Monatsdienstliste (GP7/AP6)'
def _get_report_values(self, docids, data=None):
runs = self.env['hr.payslip.run'].browse(docids)
zeilen = []
for run in runs:
for slip in run.slip_ids.filtered(
lambda p: p.state not in ('draft', 'cancel')):
version = slip.version_id
basen = slip._l10n_at_sv_basis_laufen_monate()
sv_dn = sum(
slip._l10n_at_svdn(k) for k in
('kv', 'pv', 'av', 'ak', 'wf'))
sv_dg = slip._l10n_at_svdg_gesamt()
sz_dn = slip._l10n_at_svdn_sz()
sz_basis = slip._l10n_at_svdn_basis_sz()
zeilen.append({
'slip': slip,
'versicherungsnummer': version.ssnid if version else '',
'personalgruppe': getattr(
version, 'l10n_at_personalgruppe', '')
if version else '',
'geringfuegig': slip.l10n_at_geringfuegig,
'basis_laufen': sum(
b for _m, b in basen),
'sz_basis': sz_basis,
'sv_dn': sv_dn,
'sv_dg': sv_dg,
'sv_dn_sz': sz_dn,
'lohnsteuer': sum(
slip.line_ids.filtered(
lambda l: l.code in ('LST_LFD', 'LST_SZ')
).mapped('total')),
'netto': sum(
slip.line_ids.filtered(
lambda l: l.code == 'NET').mapped('total')),
'brutto': sum(
slip.line_ids.filtered(
lambda l: l.salary_rule_id.l10n_at_bemessung
!= 'neutral').mapped('total')),
})
return {'docs': runs, 'zeilen': zeilen}
class L10nAtPayrollMonatsjournal(models.AbstractModel):
_name = 'report.l10n_at_hr_payroll.report_monatsjournal'
_description = 'Monatsjournal (GP7/AP6, AP2a)'
def _get_report_values(self, docids, data=None):
runs = self.env['hr.payslip.run'].browse(docids)
zeilen = []
for run in runs:
gruppen = {}
dg_gruppen = {}
for slip in run.slip_ids.filtered(
lambda p: p.state not in ('draft', 'cancel')):
for line in slip.line_ids:
regel = line.salary_rule_id
if not line.total:
continue
kategorie = regel.category_id.code or ''
schluessel = (regel.code, regel.name,
regel.l10n_at_bemessung)
if kategorie == 'ATP_DG':
# AP2a: Dienstgeberanteile eigener Block —
# die neutralen DG-Regeln (SVDG_*, FLAFDB,
# KOMMST, DZ, BVG) nicht mehr ausfiltern.
dg_gruppen[schluessel] = \
dg_gruppen.get(schluessel, 0.0) + line.total
elif regel.l10n_at_bemessung != 'neutral':
gruppen[schluessel] = \
gruppen.get(schluessel, 0.0) + line.total
zeilen.append({
'run': run,
'arten': [
{'code': code, 'name': name, 'bemessung': bemessung,
'summe': summe}
for (code, name, bemessung), summe
in sorted(gruppen.items())],
'dg_arten': [
{'code': code, 'name': name, 'bemessung': bemessung,
'summe': summe}
for (code, name, bemessung), summe
in sorted(dg_gruppen.items())],
'summe_arten': sum(gruppen.values()),
'summe_dg': sum(dg_gruppen.values()),
})
return {'docs': runs, 'journal': zeilen}
class L10nAtPayrollDgKosten(models.AbstractModel):
_name = 'report.l10n_at_hr_payroll.report_dg_kosten'
_description = 'Dienstgeber-Gesamtkosten (AP2a)'
# Festspalten des Reports; weitere DG-Positionen (z. B. §-53a-UV
# der Geringfügigen) laufen unter 'weitere'.
_SPALTEN = ('FLAFDB', 'KOMMST', 'DZ', 'BVG', 'WIEN_DAG')
def _get_report_values(self, docids, data=None):
runs = self.env['hr.payslip.run'].browse(docids)
zeilen = []
summen = {
'brutto': 0.0, 'svdg': 0.0, 'dg_summe': 0.0,
'gesamtkosten': 0.0,
'spalten': {code: 0.0 for code in self._SPALTEN},
'weitere': 0.0,
}
for run in runs:
for slip in run.slip_ids.filtered(
lambda p: p.state not in ('draft', 'cancel')):
brutto = sum(
slip.line_ids.filtered(
lambda l: l.total
and l.salary_rule_id.category_id.code
== 'ATP_BRUTTO'
and l.salary_rule_id.l10n_at_bemessung != 'neutral'
).mapped('total'))
dg_komponenten = {}
for line in slip.line_ids:
regel = line.salary_rule_id
if not line.total \
or regel.category_id.code != 'ATP_DG':
continue
dg_komponenten[regel.code] = \
dg_komponenten.get(regel.code, 0.0) + line.total
svdg = (slip._l10n_at_svdg_gesamt()
+ sum(slip._l10n_at_svdg_sz(k) for k in
('kv', 'pv', 'av', 'uv', 'iesg')))
spalten = {code: dg_komponenten.get(code, 0.0)
for code in self._SPALTEN}
dg_summe = sum(dg_komponenten.values())
weitere = dg_summe - svdg - sum(spalten.values())
zeilen.append({
'slip': slip,
'brutto': brutto,
'svdg': svdg,
'spalten': spalten,
'weitere': max(weitere, 0.0),
'dg_summe': dg_summe,
'gesamtkosten': brutto + dg_summe,
})
summen['brutto'] += brutto
summen['svdg'] += svdg
summen['dg_summe'] += dg_summe
summen['gesamtkosten'] += brutto + dg_summe
summen['weitere'] += max(weitere, 0.0)
for code in self._SPALTEN:
summen['spalten'][code] += spalten[code]
return {'docs': runs, 'zeilen': zeilen, 'summen': summen}
class L10nAtPayrollJahresberichtWizard(models.TransientModel):
"""Druck-Wrapper für Jahreslohnkonto (je DN) und
Betriebsjahreslohnkonto (Summen je Lohnart) — AP2b/AP2c."""
_name = 'l10n.at.payroll.jahresbericht.wizard'
_description = 'Jahresberichte: Lohnkonten drucken'
jahr = fields.Integer(
required=True,
default=lambda self: fields.Date.context_today(self).year)
art = fields.Selection(
[('dn', 'Jahreslohnkonto je Dienstnehmer:in'),
('betrieb', 'Betriebsjahreslohnkonto (Summen je Lohnart)')],
required=True, default='dn')
company_id = fields.Many2one(
'res.company', required=True,
default=lambda self: self.env.company)
employee_ids = fields.Many2many(
'hr.employee', string='Dienstnehmer:innen',
help='Leer = alle Dienstnehmer:innen des Unternehmens.')
def action_drucken(self):
self.ensure_one()
if self.art == 'dn':
employees = self.employee_ids or self.env[
'hr.employee'].search(
[('company_id', '=', self.company_id.id)])
report = self.env.ref(
'l10n_at_hr_payroll.action_report_jahreslohnkonto')
return report.report_action(
employees, data={'jahr': self.jahr})
report = self.env.ref(
'l10n_at_hr_payroll.action_report_betriebslohnkonto')
return report.report_action(
self.company_id, data={'jahr': self.jahr})
@api.model
def _l10n_at_lohnkonto_zeilen(self, jahr, company_id,
employee_id=None):
"""Aggregierte Lohnartzeilen des Jahres: [(code, name,
bemessung, kategorie, [Jan..Dez], total), …] — sortiert nach
Kategorie und Code."""
domain = [
('company_id', '=', company_id),
('date_from', '>=', date(jahr, 1, 1)),
('date_from', '<=', date(jahr, 12, 31)),
('slip_id.state', 'not in', ('draft', 'cancel')),
]
if employee_id:
domain.append(('employee_id', '=', employee_id))
zeilen = {}
for line in self.env['hr.payslip.line'].search(domain):
regel = line.salary_rule_id
schluessel = (regel.code, regel.name,
regel.l10n_at_bemessung,
regel.category_id.code or '')
eintrag = zeilen.setdefault(
schluessel, {'monate': [0.0] * 12, 'total': 0.0})
eintrag['monate'][line.date_from.month - 1] += line.total
eintrag['total'] += line.total
result = []
for (code, name, bemessung, kategorie), eintrag in sorted(
zeilen.items()):
result.append({
'code': code, 'name': name, 'bemessung': bemessung,
'kategorie': kategorie,
'monate': eintrag['monate'], 'total': eintrag['total'],
})
return result
class L10nAtPayrollJahreslohnkonto(models.AbstractModel):
_name = 'report.l10n_at_hr_payroll.report_jahreslohnkonto'
_description = 'Jahreslohnkonto je Dienstnehmer:in (AP2b)'
def _get_report_values(self, docids, data=None):
jahr = (data or {}).get('jahr') \
or fields.Date.context_today(self).year
employees = self.env['hr.employee'].browse(docids)
berichte = []
for employee in employees:
berichte.append({
'employee': employee,
'zeilen': self.env[
'l10n.at.payroll.jahresbericht.wizard'
]._l10n_at_lohnkonto_zeilen(
jahr, employee.company_id.id, employee.id),
})
return {'docs': employees, 'jahr': jahr, 'berichte': berichte}
class L10nAtPayrollBetriebslohnkonto(models.AbstractModel):
_name = 'report.l10n_at_hr_payroll.report_betriebslohnkonto'
_description = 'Betriebsjahreslohnkonto (AP2c)'
def _get_report_values(self, docids, data=None):
jahr = (data or {}).get('jahr') \
or fields.Date.context_today(self).year
companies = self.env['res.company'].browse(docids)
berichte = []
for company in companies:
berichte.append({
'company': company,
'zeilen': self.env[
'l10n.at.payroll.jahresbericht.wizard'
]._l10n_at_lohnkonto_zeilen(jahr, company.id),
})
return {'docs': companies, 'jahr': jahr, 'berichte': berichte}