[IMP] l10n_at_hr_payroll: GP7 ELDA-Fixsatz-Framework, L16 + Arbeitsstättenmeldung, Kind-Modell, Belege (AP5/AP6)

ELDA-Fixsatz-Framework (models/elda.py) nach DM-ORG 42.7.0 Kapitel C.1/C.2+E.1-E.3: Fixlängen-Sätze ohne Feldtrennzeichen (a/a-n linksbündig, Grundstellung blank; n rechtsbündig, Grundstellung 0, führende Nullen, KEINE Interpunktion — Beträge EURO-Cent kaufmännisch gerundet, Prozentsätze x1000 mit 3 Nachkommastellen), Encoding ISO-8859-15 (Standard-Encoding für Fixlängen), optionaler Dateiheader (DHKZ 2x blank + DHVN 01 + ENCD), Vorlaufsatz SART 00 (BEST/VSTR/VERS) + Schlusssatz SART 99 mit SANZ; SANR lückenlos ab 1 je Datenbestand; CRLF als Satztrenner (Praxis-Annahme, DM-ORG normiert keines — Parallellauf verifizieren).

Lohnzettel Finanz L16 (models/lohnzettel_l16.py): Informationssatz I1 (SART I1, Länge 1.100, Strukturversion 03) + Mitteilungssatz L1 (SART L1, Länge 3.500, Lohnzettelversion 28 — zwingend ab 01.03.2026) mit vollständiger, PDF-verifizierter Feldtabelle (~210 Felder inkl. Kinderblock 15x123 ab Pos. 1501; DM-ORG 42.7.0 E.13/E.14). KZ-245-Formel: KZ 210 = Bruttobezüge gem. § 25 GESAMT (DM-ORG-Feldbeschreibung) — die Formel 210-215-220-230-243 ergibt die laufende Steuerbemessung, per Konstruktion konsistent mit der GP3-§-66-Kette (Ableitung aus l10n_at_bemessung/steuerbefreiung_68/s67_abs und Regelcodes; bewusst KEIN paralleles KZ-Klassifikationsfeld). Arbeitsstättenmeldung § 34 Abs 6 ASVG: SART 45 (Länge 300, Version 02, BEST AD/VSTR ST Statistik Austria) je AN am 31.12. aus dem Dienstort-Partner (GKZ manuell, Warnung wenn fehlt) — im selben Jahreslauf (Nutzer-Entscheidung 5.1).

Kind-Datenmodell (models/kind.py — l10n.at.payroll.kind): Stammdaten-Wartung des Familienbonus Plus (Nutzer-Entscheidung 5.3: FB+ rechnet NICHT im Lohnsteuerkern — bei Unterschreiten der Steuer auf 0 erfolgt die Geltendmachung über die Arbeitnehmerveranlagung); Felder für den L16-Kinderblock (Familienname/Vorname/Geburtsdatum/VSNR/Staatsangehörigkeit/FB+-Monate voll/halb, Alterstaffel 166,68/58,34).

Stammdaten (models/meldewesen.py): Beitragskontonummer BKNR + Lohnsteuernummer (res.company), Gemeindekennziffer GKZ (res.partner), VSNR-Formatprüfung LLLPTTMMJJ auf hr.version.ssnid (Prüfzifferalgorithmus = dokumentierter Rest, SVS-Quelle beschaffen), eSV-Abmeldegrund AGRD-Mapping auf hr.departure.reason (Standard 00); fallweise-Beschäftigungs-Flag auf hr.version (§ 33 Abs 1a Z 1 ASVG).

Belege (AP6): Lohnzettel AT (ir.actions.report auf hr.payslip, AT-Layout mit Bezügen/DN-Abzügen/DG-Anteilen/Netto), Monatsdienstliste (mBGM-Kontrollschicht je AN) + Monatsjournal (je Lohnart Summen je Lauf) auf hr.payslip.run als QWeb (models/monatsreports.py + report/belege_reports.xml; account.report wird in keinem HR-Modul genutzt).

Tests: test_elda_l16 (4 — Feldformatierung/Cent-Beträge/skala, Bestands-Assembly SANR/SANZ/CRLF/ISO-8859-15/Dateiheader, L16-Lauf mit positionsexakten Assertions aus der November-Fixture, Arbeitsstätten-Satz). 19.0.4.0.0.
This commit is contained in:
2026-09-12 17:30:59 +02:00
parent 529ac30cdb
commit c474a39466
12 changed files with 1925 additions and 5 deletions
@@ -0,0 +1,203 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- ============================================================ -->
<!-- GP7/AP6 Belege: Lohnzettel AT (am Lohnzettel/Payslip), -->
<!-- Monatsdienstliste + Monatsjournal (am Lohnlauf). -->
<!-- ============================================================ -->
<record id="action_report_lohnzettel_at" model="ir.actions.report">
<field name="name">Lohnzettel (AT)</field>
<field name="model">hr.payslip</field>
<field name="report_type">qweb-pdf</field>
<field name="report_name">l10n_at_hr_payroll.report_lohnzettel_at</field>
<field name="report_file">l10n_at_hr_payroll.report_lohnzettel_at</field>
<field name="paperformat_id" ref="hr_payroll.paperformat_euro_light"/>
<field name="binding_model_id" ref="hr_payroll.model_hr_payslip"/>
<field name="binding_type">report</field>
</record>
<record id="action_report_monatsdienstliste" model="ir.actions.report">
<field name="name">Monatsdienstliste (SV-Kontrolle)</field>
<field name="model">hr.payslip.run</field>
<field name="report_type">qweb-pdf</field>
<field name="report_name">l10n_at_hr_payroll.report_monatsdienstliste</field>
<field name="report_file">l10n_at_hr_payroll.report_monatsdienstliste</field>
<field name="paperformat_id" ref="hr_payroll.paperformat_euro_light"/>
<field name="binding_model_id" ref="hr_payroll.model_hr_payslip_run"/>
<field name="binding_type">report</field>
</record>
<record id="action_report_monatsjournal" model="ir.actions.report">
<field name="name">Monatsjournal</field>
<field name="model">hr.payslip.run</field>
<field name="report_type">qweb-pdf</field>
<field name="report_name">l10n_at_hr_payroll.report_monatsjournal</field>
<field name="report_file">l10n_at_hr_payroll.report_monatsjournal</field>
<field name="paperformat_id" ref="hr_payroll.paperformat_euro_light"/>
<field name="binding_model_id" ref="hr_payroll.model_hr_payslip_run"/>
<field name="binding_type">report</field>
</record>
<!-- Lohnzettel AT: AT-Layout — Lohnarten nach Kategorie, DN-Abzüge,
DG-Anteile (SV-DG, FLAF, KommSt, DZ, BV), Netto. -->
<template id="report_lohnzettel_at">
<t t-call="web.html_container">
<t t-foreach="docs" t-as="slip">
<t t-call="web.external_layout">
<div class="page">
<h2>Lohnzettel</h2>
<table class="table table-sm">
<tr>
<td><strong>Mitarbeiter:in</strong></td>
<td><t t-out="slip.employee_id.name"/></td>
<td><strong>Versicherungsnummer</strong></td>
<td><t t-out="slip.version_id.ssnid or ''"/></td>
</tr>
<tr>
<td><strong>Abrechnungsperiode</strong></td>
<td><t t-out="slip.date_from"/> <t t-out="slip.date_to"/></td>
<td><strong>Personalgruppe</strong></td>
<td><t t-out="slip.version_id.l10n_at_personalgruppe or ''"/></td>
</tr>
</table>
<h3>Bezüge</h3>
<table class="table table-sm">
<thead>
<tr>
<th>Lohnart</th>
<th class="text-end">Betrag</th>
</tr>
</thead>
<tbody>
<tr t-foreach="slip.line_ids.filtered(lambda l: l.total and l.salary_rule_id.l10n_at_bemessung != 'neutral' and l.salary_rule_id.category_id.code == 'ATP_BRUTTO')" t-as="line">
<td><t t-out="line.name"/></td>
<td class="text-end">
<span t-out="line.total" t-options='{"widget": "monetary", "display_currency": slip.currency_id}'/>
</td>
</tr>
<tr>
<td><strong>Brutto</strong></td>
<td class="text-end">
<strong><span t-out="sum(slip.line_ids.filtered(lambda l: l.total and l.salary_rule_id.l10n_at_bemessung != 'neutral' and l.salary_rule_id.category_id.code == 'ATP_BRUTTO').mapped('total'))" t-options='{"widget": "monetary", "display_currency": slip.currency_id}'/></strong>
</td>
</tr>
</tbody>
</table>
<h3>Abzüge (Dienstnehmer:in)</h3>
<table class="table table-sm">
<thead>
<tr>
<th>Abzug</th>
<th class="text-end">Betrag</th>
</tr>
</thead>
<tbody>
<tr t-foreach="slip.line_ids.filtered(lambda l: l.total and l.salary_rule_id.category_id.code == 'ATP_ABZUG')" t-as="line">
<td><t t-out="line.name"/></td>
<td class="text-end">
<span t-out="-line.total" t-options='{"widget": "monetary", "display_currency": slip.currency_id}'/>
</td>
</tr>
</tbody>
</table>
<h3>Dienstgeberanteile</h3>
<table class="table table-sm">
<thead>
<tr>
<th>DG-Komponente</th>
<th class="text-end">Betrag</th>
</tr>
</thead>
<tbody>
<tr t-foreach="slip.line_ids.filtered(lambda l: l.total and l.salary_rule_id.category_id.code == 'ATP_DG')" t-as="line">
<td><t t-out="line.name"/></td>
<td class="text-end">
<span t-out="line.total" t-options='{"widget": "monetary", "display_currency": slip.currency_id}'/>
</td>
</tr>
</tbody>
</table>
<p class="text-end h4">
<strong>Netto:</strong>
<span t-out="sum(slip.line_ids.filtered(lambda l: l.code == 'NET').mapped('total'))" t-options='{"widget": "monetary", "display_currency": slip.currency_id}'/>
</p>
</div>
</t>
</t>
</t>
</template>
<!-- Monatsdienstliste: je AN die mBGM-Kontrollgrößen -->
<template id="report_monatsdienstliste">
<t t-call="web.html_container">
<t t-foreach="docs" t-as="run">
<t t-call="web.external_layout">
<div class="page">
<h2>Monatsdienstliste — <t t-out="run.name"/></h2>
<table class="table table-sm">
<thead>
<tr>
<th>Mitarbeiter:in</th>
<th>VSNR</th>
<th>Gruppe</th>
<th class="text-end">BG laufend</th>
<th class="text-end">BG SZ</th>
<th class="text-end">SV-DN</th>
<th class="text-end">SV-DN SZ</th>
<th class="text-end">SV-DG</th>
<th class="text-end">Lohnsteuer</th>
<th class="text-end">Netto</th>
</tr>
</thead>
<tbody>
<tr t-foreach="zeilen" t-as="z">
<td><t t-out="z['slip'].employee_id.name"/></td>
<td><t t-out="z['versicherungsnummer']"/></td>
<td><t t-out="z['personalgruppe']"/><t t-if="z['geringfuegig']" t-out=" (geringf.)"/></td>
<td class="text-end"><span t-out="z['basis_laufen']" t-options='{"widget": "monetary", "display_currency": run.company_id.currency_id}'/></td>
<td class="text-end"><span t-out="z['sz_basis']" t-options='{"widget": "monetary", "display_currency": run.company_id.currency_id}'/></td>
<td class="text-end"><span t-out="z['sv_dn']" t-options='{"widget": "monetary", "display_currency": run.company_id.currency_id}'/></td>
<td class="text-end"><span t-out="z['sv_dn_sz']" t-options='{"widget": "monetary", "display_currency": run.company_id.currency_id}'/></td>
<td class="text-end"><span t-out="z['sv_dg']" t-options='{"widget": "monetary", "display_currency": run.company_id.currency_id}'/></td>
<td class="text-end"><span t-out="z['lohnsteuer']" t-options='{"widget": "monetary", "display_currency": run.company_id.currency_id}'/></td>
<td class="text-end"><span t-out="z['netto']" t-options='{"widget": "monetary", "display_currency": run.company_id.currency_id}'/></td>
</tr>
</tbody>
</table>
</div>
</t>
</t>
</t>
</template>
<!-- Monatsjournal: je Lohnart die Summe über den Lauf -->
<template id="report_monatsjournal">
<t t-call="web.html_container">
<t t-foreach="journal" t-as="j">
<t t-call="web.external_layout">
<div class="page">
<h2>Monatsjournal — <t t-out="j['run'].name"/></h2>
<table class="table table-sm">
<thead>
<tr>
<th>Code</th>
<th>Lohnart</th>
<th>Bemessung</th>
<th class="text-end">Summe</th>
</tr>
</thead>
<tbody>
<tr t-foreach="j['arten']" t-as="art">
<td><t t-out="art['code']"/></td>
<td><t t-out="art['name']"/></td>
<td><t t-out="art['bemessung']"/></td>
<td class="text-end"><span t-out="art['summe']" t-options="{&quot;widget&quot;: &quot;monetary&quot;, &quot;display_currency&quot;: j['run'].company_id.currency_id}"/></td>
</tr>
</tbody>
</table>
</div>
</t>
</t>
</t>
</template>
</odoo>