Compare commits

72 Commits

Author SHA1 Message Date
fegger a3ef14ec40 [IMP] odoo-at-payroll: Memory — Merge nach main vollzogen 2026-09-17 14:23:55 +02:00
fegger 9ba625f059 [IMP] odoo-at-payroll: Memory — Push-Status und Folgeschritte des pv-agent-Merges 2026-09-17 14:21:53 +02:00
fegger 8d9ce2f2d4 [IMP] pv-agent: rj-Publikationsfreigabe dokumentiert (D15-Punkt geschlossen) 2026-09-17 14:18:28 +02:00
fegger 67330a241f [FIX] pv-agent: Repo-Pfade nach Merge — KV-Library im selben Repo, Map regeneriert, planung.md und .gitignore aktualisiert 2026-09-17 14:06:15 +02:00
fegger 09f25533cb [IMP] odoo-at-payroll: pv-agent-Subtree in AGENTS, README und Memory dokumentiert 2026-09-17 14:06:15 +02:00
fegger f027e93666 [IMP] pv-agent: konsolidiere .agents — pv-rag-agent-Skill ans Repo-Root, Legacy-Dubletten entfernt 2026-09-17 14:06:15 +02:00
fegger 9f120a4075 [ADD] pv-agent: merge Wissensbasis-Agent (RAG-Service, Wissensbasis, Tools) per filter-repo Remap 2026-09-17 13:55:59 +02:00
fegger a848c07e96 [ADD] l10n_at_payroll_agent: PV-agent review bridge for payout plausibility
PV-agent bridge (D25/M4.1): Odoo stays the system of record; the
standalone, KB-bound knowledge service (pv-agent) reviews plausibility
and returns evidence-backed answers (mode=review, agent API v1). No
back path: the agent never calls Odoo and receives no personal data —
only the minimized, schema-bound review context.

- context builder: projects a computed draft payslip with an
  ATP_PRAMIE input (planned one-off payout) onto the review context
  (facts + computation): gross wage (hr.version.wage), applying
  collective agreement (version.kv_id) and the draft's DG/LST lines
  (SVDG_*/LST_*/FLAFDB/KOMMST/DZ/BVG/WIEN_DAG)
- client (stdlib urllib, no extra dependency): POST /v1/ask with
  X-Request-ID, bearer key from company settings (pv_agent_url /
  pv_agent_api_key, group_hr_payroll_user); neutral UserErrors,
  technical details only in the server log
- review wizard on the draft payslip: agent answer (evidence-backed),
  structured verdict (plausible / implausible / not checkable) and
  checks in the dialog
- security: wizard bound to hr_payroll.group_hr_payroll_user; agent
  service key stored on res.company, never logged; 5 tests
2026-09-17 13:50:18 +02:00
fegger 26a20e83b2 docs: record m4.1 odoo module scaffold 2026-09-17 00:38:59 +02:00
fegger f233cb706c feat(agent): add odoo review mode with plausibility verdict 2026-09-17 00:29:03 +02:00
fegger 30a25a4601 feat(tools): map kv library variants to kb ids 2026-09-17 00:06:36 +02:00
fegger e1a6e9e0b2 docs: plan odoo agent integration (D25) 2026-09-17 00:02:44 +02:00
fegger 5090f07880 feat(agent): support cost questions and thinking fallback 2026-09-17 00:02:38 +02:00
fegger af8bfcaa6a feat(agent): log answer comments 2026-09-17 00:02:31 +02:00
fegger b42c326d7f feat(agent): add index bootstrap and answer feedback 2026-09-16 22:40:06 +02:00
fegger 4f5107a46a feat(agent): add test frontend and Docker stack 2026-09-16 21:42:05 +02:00
fegger c8b55fbd38 feat(agent): harden API-first service 2026-09-16 21:12:47 +02:00
fegger aa637270b7 docs: record final ollama evaluation 2026-09-16 20:09:05 +02:00
fegger e4726e6cfd docs: record runtime evaluation decision 2026-09-16 19:29:55 +02:00
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
fegger 7e1d466ab7 fix(agent): stabilize decision support grounding 2026-09-16 12:15:42 +02:00
fegger 1df5fb19c4 [ADD] l10n_at_payroll_dokumente: generate documents from templates
Document generation module: templates with own text (structure reference
and source stand documented per template, no licensed full text in the
repo), fill fields resolved from hr data (company, employee, hr.version
stand) plus wizard input, preview rendering and PDF/HTML attachment via
the generic report layout.

- templates: pilot set for the general-AT track formulated as own text
  after the KB muster structure (lb-bes-09, lb-bnd-55, lb-bnd-52/62,
  lb-end-35), legal status per template marked as draft
- rendering via convert_inline_template_to_qweb + ir.qweb (t-out
  auto-escapes values) — deliberately not mail.render.mixin inline
  engine, whose expression whitelist blocks werte[...] placeholders for
  non-template-editors
- wizard resolves hr.version by date_version at the reference date;
  PDF falls back to HTML attachment without wkhtmltopdf
- security: ir.model.access (hr user read / manager write) + company
  rule; 8 tests, suite 236/236 green on a fresh -i database run
2026-09-16 10:36:49 +02:00
fegger efe76e6b3d [FIX] personalverrechnung: record Ausbildungskostenrückersatz gap in AP14 plan
Renewed knowledge-base comparison (2026-09-16, Batch 10) surfaced a new
settlement component not covered by the 2026-09-12 gap analysis:
Ausbildungskostenrückersatz at early termination (lb-end-27/28). Added to
the AP14 scope, anchors and gap list; RIS verification of the legal basis
gates implementation, scope gate at the pilot client.
2026-09-16 10:36:42 +02:00
fegger 31ed539941 feat(agent): tune retrieval for decision support 2026-09-16 09:34:29 +02:00
fegger 27f3b4b7f9 [ADD] personalverrechnung: Lexis360 knowledge base (Batch 10)
Zehnter Lexis 360-Intake (64 Briefings aus den Sektionsordnern
beispiele / muster / uebersichten_checklisten, Quell-Staende
2020-12 bis 2026-08), erweitert das Korpus auf 665 kuratierte
Eintraege in 69 Clustern. Dasselbe Werk, dieselbe Breadcrumb-Form
— die Pipeline globt jetzt die drei Subordner; die neue
no-author-Breadcrumb-Variante (Muster-Exporte
"Kapitel > Thema · <Monat> <Jahr>") defaultet auf "Lexis Redaktion".

- 13 bestehende Cluster erweitert (keine neuen):
  beendigungsarten bnd 44→66 (Muster: Kuendigung/Aenderung/
  Aufloesung/Entlassung/Fristablauf/Befristung/Verwarnung/
  Dienstfreistellung, 13 Checklisten, Anspruchsuebersicht Tod),
  endabrechnung end 25→34 (Beispiele: Zeitschulden,
  Viertel-/Zwoelftelregelung, Voll-Ueberttritt,
  Gleitzeitguthaben, UEL; Checklisten: Ausbildungskosten,
  Kuendigungsentschaedigung; Muster: Vergleich, Vordienstzeiten),
  uberstunden ues 4→9, telearbeit tel 6→11,
  beendigungsphase-sonstiges bso 10→16, arbeitszeitgrenzen azg 4→9,
  arbeitszeitmodelle azm 7→10, entsendung grz 8→11,
  beschaftigungsformen bes 8→10, sonderzahlungen son 5→6,
  schwerarbeit swa 5→6, arbeitskrafteuberlassung aug 2→3,
  urlaub url 16→17
- alte Quell-Staende (2020-12 bis 2025-12) mit "⚠ Alter Stand"
  in den Eintraegen markiert
- Pipeline: BATCH_LEXIS 10, SOURCES["lexis"].pdf_subdirs,
  BC_LINE_NOAUTHOR_RE + Slug-Kollisionsguard in extract_lexis()
- RUNBOOK (Batch-10-Paragraf) und Agent-Memory aktualisiert;
  --registry und --check beide gruen (635/635, 0 problems)
2026-09-16 08:52:47 +02:00
fegger 16f6591e36 docs: record rechtsprechung eval baseline 2026-09-15 16:42:49 +02:00
fegger 39d80d371e chore: consolidate local source directories 2026-09-15 15:58:22 +02:00
fegger 209a64dc53 feat(kb): ingest rechtsprechung corpus 2026-09-15 15:27:28 +02:00
fegger d53e1e64a1 [IMP] l10n_at_hr_payroll_private: add unpaid care/parental leave family (AP16)
Close the parental/care-leave block after the pilot-client scope release (8.13): six unpaid full-release kinds join the generic parental-leave work entry — Bildungskarenz (AVRAG § 11, two to twelve months by agreement with the AMS-financed Weiterbildungsbeihilfe), Familienhospizkarenz for terminal accompaniment (AVRAG § 14a, three to six months), accompaniment of seriously ill children (§ 14b, five to nine months), children's rehabilitation leave (§ 14e, four weeks per year), Pflegekarenz (§ 14c, one to three months, undivided) and the Papa-Monat (VKG § 1a, one month, creditable toward seniority under § 1a Abs 7 VKG iVm § 15f MSchG while Bildungs-/Pflegekarenz is not). All are unpaid at the employer — the external bodies pay the replacement income (AK, AMS, Bundesamt, KV-Träger) — so the new negative KARENZ_ENTGELT rule stops the employer wage per working day through the shared Tagesatz helper whose exclusion list now covers EFZG, Mutterschutz and Karenz corrections. The Versichertenmeldung run emits M4 deregistrations at the end of the pay entitlement (the day before the release begins) and M3 re-registrations at the return date — omitted when the employment itself ends — with the eSV Karenzart strings logged as protocol information pending the special DM-ORG formats (GP7 track). The fictive BMSVG basis gains the § 7 Abs 4 Z 1–3 bridge: when a new maternity prohibition follows a parental-leave stretch directly or after employment of less than three months, the reference months fall before the preceding prohibition, and a re-founded employment within three months of the ended karenziert employment uses the single last full calendar month; contributions during the KBG period are FLAF-financed (§ 7 Abs 5) and during hospice/care leave Bund-financed (§ 7 Abs 6) — no employer rule, verified by test. Pflegeteilzeit and comparable reduced-hour arrangements pay the BV contribution on the pre-reduction wage via a new version field consumed by the BMSVG basis (§ 6 Abs 4). The endabrechnung wizard gains the § 23 Abs 8 AngG full-time re-rating preview (monthly salary ÷ work_time_rate, confirmable) with § 23a Abs 4a and UEL § 10 Abs 4 UrlG guidance. The Mutterschutz status now detects the § 163 ASVG Sonderwochengeld case (prohibition inside a Karenz — the KV-Träger pays the increased sickness benefit) and warns on § 166 ASVG suspension (full for >50 % continued pay, half at 50 %, activity and Pflegekarenzgeld hints). Verified against the original statute text in .ris/ (AVRAG §§ 11/11a/14a–14e, VKG § 1a, MSchG § 15f, BMSVG § 7 Abs 4–6, ASVG §§ 163/166) and the knowledge-base sources lb-kar/elt/kzs/pap/kbg; the vacation proration of leave stretches (§ 15f Abs 2 MSchG on the hr_holidays allocation side), the eSV special formats for the Karenz art block, the full Sonderwochengeld computation and the leave-overlap policy decision remain documented open points.

Version 19.0.17.0.0 (private)
2026-09-15 14:25:48 +02:00
fegger 1f4b4ce933 length-Retry: abgeschnittene Antworten sind die q-024-Ursache, kein Thinking (D14)
- Diagnose: q-024-Flakiness war KEIN Think-Ghost (thinking-Feld leer,
  keine Tags), sondern num_predict=1024 — lange belegte Antworten
  brachen bei done_reason=length ab (3/3 Sondenlaeufe), Zitationen
  wurden unvollstaendig, CITE_RE matchte partielle IDs -> Verletzung ->
  Regenerierungs-Eskalation -> UNCERTAIN.
- Fix: num_predict 1024 -> 2048; OllamaClient.chat_full() liefert
  (content, done_reason); chat_with_length_retry() wiederholt bei
  length einmal mit 2x Budget (technischer Retry, kein Regel-
  Regenerierungszaehler) - im Antwort-, Map- und Regenerierungspfad.
- Voll-Eval (46 Fragen): Zitier-Praezision 100 %, Verweigerung korrekt
  100 % (46/46) - erstmals alle M3-Gates erfuellt; erwartete Quelle
  92,7 %; Latenz mean 39,6 s / p95 78 s (vollstaendige statt
  abgeschnittener Antworten). q-024: 4/4 stabil.
- Tests 59 -> 60. Report lokal data/eval-qwen38-lengthfix.json.
2026-09-15 14:14:27 +02:00
fegger 850e99df20 [IMP] l10n_at_hr_payroll_private: add premiums at termination under § 16 AngG (AP14-B)
Close the premium-at-termination gap of the end-of-employment cycle: an aliquote periodic premium (annual, sales, bonus, 15th payment) whose entitlement period outlasts the termination is covered by § 16 AngG — the pro-rata amount follows the ratio of the elapsed service to the entitlement period, is due for employees regardless of the termination ground (loss clauses void, 9 ObA 82/13v), while performance-linked premiums for workers may validly hinge on non-fault grounds and premiums tied to a specific service (e.g. Bilanzgeld) are not aliquote without the service (lb-end-18). Two routes: PRAM_END (bemessung sonstig) covers premiums paid with the end-of-employment run — a special payment inside the annual sixth at the flat 6 % rate with the 620 EUR free amount and 2 615 EUR free threshold checked by the core, SV as special-payment contributions (KV/PV/AV without AK/WF) against the monthly HBG and the annual SZ cap, fully payroll-levy liable and BV-contributory at the uncapped 1.53 % rate (§ 6 Abs 1 iVm Abs 5 BMSVG). PRAM_END_TARIF (bemessung laufend plus the core l10n_at_s67_abs6_ueberhang flag) covers premiums due at a pre-agreed maturity because their amount only becomes determinable after the entitlement period ends (e.g. balance-sheet premiums): taxed like running pay with the payment-month tariff without touching the annual sixth (§ 67 Abs 10, not a § 67 Abs 8 lit c case per 98/14/0009 = ARD 5483/14/2004) — the SV contribution month rolls back into the termination year via a new payslip field consumed by an extended branch of the existing SV-basis-by-Beitragsmonat override (historical caps and rates, E-MVB 044-01-00-006), while the LSt stays in the payment month. The wizard section previews the § 16 pro-rata amount from the full premium, entitlement period and elapsed service, keeps the payable amount confirmable (KB agreement), requires the service/claim-basis confirmation, enforces the contribution month for the deferred case, and lands the memo on the payslip for the separate Lohnzettel workflow (LStR Rz 911a); the existing manual PRAM rule is untouched. The account matrix posts both rules to 6230. Verified against the original statute text in .ris/ (AngG § 16, BMSVG § 6 Abs 5) and the knowledge-base source lb-end-18; the full SZ roll-forward (storno plus recomputation against the termination year's SZ annual cap) and the mBGM storno/re-transmission remain open in the GP7 payroll-reporting track, and the BV attribution of the deferred premium remains in the payment month as a documented approximation.

Version 19.0.16.0.0 (private)
2026-09-15 13:33:49 +02:00
fegger 472b19a4b2 [IMP] l10n_at_hr_payroll_private: add settlement payments under § 67 Abs 8 lit a (AP15-C)
Complete the termination sequence with the settlement-sum path: comparison sums from court or out-of-court settlements, judgment/decision arrears and dismissal-objection payments are split between the statutory-severance and voluntary-severance rules and the settlement remainder. The remainder (VERGLEICH, bemessung neutral, sv_art laufend) reuses the lit-a/b fifth rule: after deducting the § 62 employee-rate contributions, one fifth stays tax-free capped at one fifth of nine times the monthly HBG (12 474 EUR 2026), and the remaining four fifths run with the payment-month tariff without touching the annual sixth. A new rule flag l10n_at_vergleich_bv marks the part paid for periods with a BV-Kasse entitlement: up to the parameterised cap at_lst_67_abs8_vergleich_bv (7 500 EUR 2026; § 67 Abs 8 is § 33a/§ 124b indexed) it is taxed at the flat 6 % rate with the contributions allocated pro rata (LStR Rz 1102b), while the excess falls back into the fifth rule. Both settlement rules stay payroll-levy liable (§ 41 Abs 4 lit b FLAG exempts only § 67 Abs 3 and 6) and SV-liable in the payment month, with per-Beitragsmonat roll-forward and insurance-period extension remaining documented manual options (lb-end-22, E-MVB 011-02-00-001). Identifiable severance components keep running through the existing statutory and voluntary severance machinery; a settlement expressly agreed as statutory severance is taxed in full under § 67 Abs 3 (VwGH 2013/13/0001 = ARD 6490/20/2016), and pension buy-outs (lit e) and social-plan payments (lit f) are out of scope. The wizard section requires confirmation of the settlement's itemisation and contested-claim status plus the BV entitlement, enforces the BV cap with the excess captured as remainder, and previews the free fifth, tariff basis and 6 % tax; the settlement memo lands on the payslip for the separate Lohnzettel workflow (LStR Rz 911a). The account matrix posts both settlement rules to the provisional 6200/6000 (workers 6000) and the new tax rule to 3540. Verified against the original statute text in .ris/ (EStG § 67 Abs 8 lit a) and the knowledge-base sources lb-end-21/22; KZ positioning of the lit-a tariff parts and their § 291d pot allocation remain documented verification points against LStR Rz 1087 ff.

Version 19.0.15.0.0 (private)
2026-09-15 12:38:19 +02:00
fegger 4b86bcb790 [IMP] l10n_at_hr_payroll_private: add voluntary severance under § 67 Abs 6 (AP15-B)
Split the voluntary severance into its privileged part and a tariff remainder on the exit run: the privileged part combines the quarter rule (1/4 of the last twelve months' running pay, capped at nine times the monthly HBG) with the twelfth rule (2/12 to 12/12 by proven service, capped at n × 3 × HBG), reduced per Z 3 by severance already received and the statutory claim due at this termination, all taxed at the flat 6 % rate. The excess runs as its own rule flagged l10n_at_s67_abs6_ueberhang: taxed with the running tariff of the payment month but excluded from the annual sixth, since Z 5 exempts only the Z 1/Z 2 amounts and the excess is a special payment, not running pay. The twelve-month basis aggregates validated prior runs plus own lines, excluding the flagged excess and § 26 Z 4 travel overhang via a dedicated rule flag. Free of SV (§ 49 Abs 3 Z 7 ASVG), payroll levies regardless of the 6 %/tarif split (§ 41 Abs 4 lit b FLAG, § 122 Abs 8 WKG, § 5 Abs 2 lit b KommStG) and BV contributions; the former BV-Anwartschaft exclusion Z 7 is repealed (BGBl I 118/2015). The wizard proposes the rolling basis and the statutory claim, previews Z 1/Z 2/reduction/privileged/excess/tax and requires explicit confirmation against claim-conversion risk. The § 291d EO termination pot nets both severance wage-tax rules; key 260 now includes LST_ABF_FREI. Verified against the original statute text in .ris/ (EStG § 67 Abs 6) and the knowledge-base sources lb-end-12/13/15; KZ positioning of Abs-6 amounts and the excess's § 291d pot allocation remain documented verification points against LStR Rz 1087 ff.

Version 19.0.14.0.0 (private), 19.0.10.0.0 (core)
2026-09-15 11:34:09 +02:00
fegger 23a5ef74dc Stufe 2: Antworttyp-Routing — Survey Map-Reduce + Rueckfrage-Regel (D13, M6)
- Planer liefert jetzt type: survey|specific. Survey-Fragen (Uebersicht
  ueber viele Dokumente) laufen ueber Map-Reduce: Retrieval auf
  survey_blocks=16 erweitert, ein Map-Call destilliert JE Block als
  Stichpunkte mit seiner KB-ID (MAP_SYSTEM_PROMPT), ein Reduce-Call
  synthetisiert die Endantwort. Grounding unveraendert: Zitier-Validierung
  strikt ueber die Retrieved-Union; leerer Map-Output -> Fallback auf
  Einzelantwort.
- Systemprompt-Regel 9: haengt die Antwort wesentlich von nicht genanntem
  Kontext ab (Branche, Bundesland, Zeitraum), belegte allgemeine Aussage
  plus EINE Rueckfrage statt Verweigerung (API-first; Odoo-Chat kann die
  Rueckfrage als Follow-up nutzen).
- Ergebnis: q-029 (WIKU-Survey, bisher hartnaeckigste Fehlverweigerung)
  geheilt - Teilantwort mit 5 belegten Heften; q-015 antwortet mit
  expliziter KV-Abhaengigkeit + Rueckfrage statt Branchen-Noise.
- Eval (46 Fragen): Zitier-Praezision 97,8 %, Verweigerung 97,8 % (Gate
  erfuellt), erwartete Quelle 90,2 %, Latenz mean 34,2 s (Map-Reduce nur
  bei Survey-Fragen, ~95 s). Report lokal data/eval-qwen38-stage2.json.
- Tests 57 -> 59 (Survey-Integration, Typ-Parsing).
2026-09-15 11:16:53 +02:00
fegger 9add58fe83 [IMP] l10n_at_hr_payroll_private: add statutory old-system severance (AP15-A)
Pay the pre-2003 AngG severance from the end-of-employment run as its own neutral benefit line: free of SV (§ 49 Abs 3 Z 7 ASVG) and payroll levies, outside the annual sixth, and collected into the § 291d EO termination pot net of its own wage tax. Taxation follows the binding better-of rule of § 67 Abs 3 EStG: multiple-of-method (tariff wage tax on the running monthly wage × the statutory multiple of the payout) versus the flat 6 % rate, applied mandatorily in favour of the employee. The tariff wage tax is captured as a separately confirmed value instead of deriving it from gross pay through the § 62 engine. The wizard enforces § 23a Abs 3 AngG (parental exit needs five uninterrupted years), blocks BMSVG-new contracts except frozen claims and validates AGRD 02/06 against the claim type. The LNK basis subtracts § 67 Abs 3/6 rules only when they actually run in the tax base, the L16 generator reports LST_ABF in key 260 outside 210/220, and result_rules access in the garnishment paths avoids DefaultDictPayroll materialisation. Verified against the original statute texts in .ris/ (AngG §§ 23/23a, EStG § 67 Abs 3).

Version 19.0.13.0.0 (private), 19.0.9.0.0 (core)
2026-09-15 11:01:55 +02:00
fegger aa7adfd464 Stufe 1: komplexe Fragen via Query-Planer, Per-Query-Slots, Scope-Filter (D12, M6)
- Query-Planer (agent/query_planner.py): Heuristik-Gate (Jahr, Vergleichs-/
  Aggregationsmarker, Laenge) entscheidet ueber einen kleinen LLM-Call,
  der komplexe Fragen in 1-3 Sub-Queries zerlegt (JSON, temp 0; Fehler ->
  Original als Einzel-Query). Je Sub-Query optional stand_year und scope.
- Multi-Query-Retrieval: je Sub-Query BM25+Dense mit RRF-Summe;
  Per-Query-Slots (2 je Sub-Query) sichern jeden Frageaspekt im Kontext
  (sonst dominieren Eintraege, die in mehreren Sub-Queries mittelgut
  matchen — q-113-Befund). Scope-Filter: 'gesetz' (nur Lexis/WIKU/RIS)
  und 'kv' (nur Branchen-KV) mit Fallback auf unscoped bei leerem
  Ergebnis. Temporal-Intent: stand_year + temporal_boost (default 0,
  FTS-jahr-Tag-Signal reichte).
- Grounding unveraendert: eine Retrieved-Menge (Union), eine Antwort,
  Post-Validierung ueber die Union, Verweigerungspflicht unveraendert.
- Goldset 42 -> 46: q-110 (Temporal 2023; 2024er-Lohnordnung existiert
  im Korpus nicht - Mantelvertrag ohne Lohntabelle, korrekt verweigert),
  q-111 (2025), q-112 (Abfertigung-Vergleich), q-113 (Gesetz+KV).
- Eval (46 Fragen): Zitier-Praezision 97,8 %, Verweigerung 97,8 %
  (Gate >94,3 % erfuellt), erwartete Quelle 90,2 %, Latenz mean 33,5 s.
- Tests 50 -> 57. Reports lokal: data/eval-qwen38-stage1*.json.
  API-first festgehalten (D13-Vorbereitung): Odoo bleibt duenner Client;
  Lohndaten-Zugriff in M4 erfordert Privacy-Neubewertung.
2026-09-15 09:58:25 +02:00
fegger 4687cd4792 Tuning: Prompt-Budget + cross_ref-Erweiterung heilt Fehlverweigerungen (D11)
- Ursache der 4 Fehlverweigerungen nach KV/RIS-Erweiterung: lange
  KV-Chunks ueberlieferten num_ctx=16384 (bis 62,7 KB Prompt) — Ollama
  trunciert den Systemprompt vorn, das Modell verliert die Zitierregeln
  ('Block-N'-Zitate statt IDs -> Regenerierung -> Verweigerung).
- Fix: num_ctx 32768; trim_results (max_context_chars=90.000, niedrig
  gerankte Bloecke ganz weglassen statt truncieren, Mindestbestand 6);
  cross_ref_expand 3 -> 6 (Top-3 sind oft Branchen-KV-Bloecke mit leeren
  cross_refs - kuratierte Nachbarn kamen sonst nie nach).
- Eval: alle 4 Faelle geheilt, 10/10 Bestaetigungslaeufe OK.
  Retrieval Recall@8 0,851 -> 0,946 (cross_ref-Extras bringen
  Expected-IDs nach). Antworten: Zitier-Praezision 97,6 %, Verweigerung
  97,6 % (>94,3 %-Gate), erwartete Quelle 83,8 -> 91,9 %, Latenz mean 33 s.
- Tests 49 -> 50 (trim_results); Reports lokal (data/eval-*).
2026-09-15 08:31:02 +02:00
fegger 7fb569b7cf Retrieval-Kalibrierung nach Korpusverdopplung (D10) + Goldset-Erweiterung
- Hybrid-Fusion um dense_weight erweitert; kalibriert per Goldset-Sweep:
  dense_weight=2.0 (BM25 durch KV-S-Titel-Matches inflationiert),
  rrf_k=20, candidate_pool=150 -> Recall@8 0,851 -> 0,923 (>0,9),
  Hit-Rate 0,973, MRR 0,667. ENV: PV_DENSE_WEIGHT/PV_RRF_K/
  PV_CANDIDATE_POOL.
- CITE_RE um kv|ris erweitert (Post-Validierung deckt neue ID-Raeume).
- Goldset: +6 KV/RIS-Fragen (q-101-106) + Branchen-Refusal r-005;
  q-021 auf lb-kar-04 rekalibriert (Top-1, deckt Beginn/Dauer voll -
  dokumentiert im Note).
- Antwortmodus-Eval (qwen3.8:27b, 42 Fragen): Zitier-Praezision 95,2 %,
  Verweigerung 90,5 % - unter den M3-Gates, Tuning folgt
  (Report data/eval-qwen38-kvris.json, lokal).
- Docs: agent/README.md Baseline, planung.md Umsetzungsstand,
  .agents/MEMORY.md (D9/D10, offene Punkte).
2026-09-15 07:45:04 +02:00
fegger 97aa3c1c82 KV/RIS-Erweiterung: 614 WKO-KV-Dokumente + 59 RIS-Gesetze (D9)
- Korpus 601 -> 1274 Layer-2-Eintraege: kv-kvt-001...614 (ein Cluster
  kollektivvertraege, Branche als Tag) und ris-<cluster-prefix>-nn auf
  der bestehenden Cluster-Map + 7 neue Cluster (zvr/avr/agg/lst/abo/
  nso/kvt); LAW_MAP dokumentiert die 59 Gesetz-Zuordnungen.
- kv/ris-Eintraege sind quellentreu generiert (D9) - Gesetze sind
  amtliche Werke, KV-Lohntabellen zahlenexakt; Tool-Output in
  tools/ (ingest_sources.py, build_registry.py, kb_common.py),
  eingefrorene ID-Kataloge tools/catalogs/*.json (nur Metadaten).
- kb.json/INDEX.md regeneriert (1274 Eintraege, 76 Cluster);
  agent/kb.py: ID-Raeume kv|ris, source akzeptiert html-only.
- Tests 41 -> 49 (Konverter, LAW_MAP-Abdeckung, neue ID-Raeume,
  Korpus-Integrationszahl).
2026-09-15 07:44:56 +02:00
fegger b3e8e47cdd Prompt v2 + Kontext-Section-Prioritaet: Fehlverweigerungen behoben (D8)
Diagnose: q-008 bekam die 'Verweise'-Navigationssektion von lb-end-03 als
Kontext (BM25-Langennormalisierung bevorzugt duenne Chunks) -> inhaltlose
Bloecke -> korrekte Verweigerung nach Regel 4. q-031 enthaelt eine falsche
Praemisse (kein 'Mindestlohngesetz' in Oesterreich; Wissensbasis sagt:
kein gesetzlich betraglich festgelegtes Mindestentgelt).

Fixes: (a) retrieve.py _representative_chunk - pro Eintrag beste Inhalts-
sektion (Zusammenfassung > Kernwerte > Rechtsgrundlagen > Payroll >
sonstige > Verweise), Section-Swap aus dem Index falls nur 'Verweise'
rangiert; (b) generate.py Prompt v2 - Regel 4 erlaubt Teilantworten,
Regel 8 verlangt Prämisse-Korrektur mit Muster-Beispiel.

Bestaetigungslauf qwen3.8:27b (35 Fragen): Zitier-Praezision 100 % (8
Regenerierungen, alle geheilt), Verweigerung korrekt 94,3 %, erwartete
Quelle 83,9 % (v1 80,6 %), mean 34 s. q-008 + q-031 behoben; verbleibender
bekannter Fall q-029 (breite Survey-Frage, sicherer Fehlermodus).
Retrieval-Metriken unveraendert (Recall@8 0,952). 41 Tests gruen.
2026-09-15 00:22:41 +02:00
fegger 9e1d2fd2b0 Bake-off ergaenzt: muse-glimmer:latest (Rang 5 von 6)
Nach User-Wunsch im selben Protokoll gemessen (35 Goldset-Fragen):
100% Zitier-Praezision bei nur 1 Regenerierung (diszipliniertestes
Modell), aber 8/35 falsche Verweigerungen (Ueberverweigerung teils trotz
vorhandener Zitate) und 37,5s mean - schlaegt qwen3.8:27b in keiner
Kennzahl. D7 unveraendert: qwen3.8:27b bleibt fixiert.
2026-09-14 23:55:45 +02:00
fegger 263bbd2cac M3 Bake-off abgeschlossen: qwen3.8:27b als Antwortmodell fixiert (D7)
Alle 5 Kandidaten auf dem Goldset (35 Fragen, Antwortmodus) gemessen.
Protokoll Zitier-Praezision > Verweigerungskorrektheit > Latenz:

  qwen3.8:27b   100% / 94,3% / 80,6%  32s  -> Sieger, fixiert
  gemma4:26b    100% / 91,4% / 67,7%  7,9s -> Latenz-Kandidat (4x schneller)
  gemma4:12B    94,3% / 91,4% / 77,4%  21s
  qwen3.6:27B   94,3% / 85,7% / 80,6%  43s
  mistral-small3.1:24b 100% / 71,4% / 58,1%  20s

mistral-Deutsch-Hypothese praktisch widerlegt (71,4% Verweigerungs-
korrektheit). q-008/q-031 verweigern beide Finalisten -> Prompt-/
Retrieval-Tuning, nicht modellspezifisch. Tabelle in planung.md 13,
README und MEMORY aktualisiert.
2026-09-14 23:30:36 +02:00
fegger 4fa98b7e82 Bake-off M3: qwen3.8 vs qwen3.6 gemessen; Topologie korrigiert (Remote-GPU-Maschine)
Zwischenstand Bake-off (Antwortmodus, 35 Goldset-Fragen):
qwen3.8:27b 100% Zitier-Praezision / 94,3% Verweigerung korrekt / 32s;
qwen3.6:27B 94,3% / 85,7% / 43s (10 Regenerierungen) - klar schwächer.

Topologie korrigiert: 100.103.83.12:11435 ist die Remote-GPU-Maschine
(Tailscale, R9700), NICHT dieselbe wie die Dev-Maschine (deren localhost:
11434 ist ein eigener, fast leerer Ollama; URL-Nicht-auf-11434-Korrigieren
in Skill/README/Config präzisiert). bge-m3 auf der GPU-Maschine gepullt.

gemma4:26b-Lauf brach ab: auf der GPU-Maschine sterben seit dem Crash
alle llama-server-Loads (API 500 'exit status 1'), auch bge-m3. Neustart
dort noetig: sudo systemctl restart ollama. Danach mistral-small3.1:24b
und gemma4:12B ausstehen.
2026-09-14 22:42:54 +02:00
fegger 50e62509e6 Ollama-URL korrigiert (100.103.83.12:11435) und M1/M2 gegen Echt-System validiert
Der Host betreibt zwei Ollama-Instanzen: 11434 (fast leer, 0.16.2) und
11435 (Ziel-Instanz, 0.32.13, Modell-Zoo) — Port nicht mehr auf 11434
'korrigieren' (Dokumentation + Skill + Config-Default angepasst).

Validierung gegen das echte System: bge-m3 per API gepullt, Hybrid-Index
(3005 Chunks, 100 % eingebettet, 144 s), M1-Akzeptanz erreicht (Recall@8
0,952 > 0,9; Hit-Rate 0,968). Antwortmodus-Eval mit qwen3.8:27b (Thinking
verifiziert aus): Zitier-Präzision 100 % (4 Regenerierungen), Verweigerung
korrekt 94,3 %, Latenz mean 32 s. ATZ-Konfliktfall korrekt beide Werte
mit Warnung. generate.py: sources enthaelt jetzt nur zitierte Quellen;
Systemprompt ohne redundante Quellenzeile.

Bake-off-Feld (M3) bereits installiert: qwen3.6:27B, gemma4:26b,
mistral-small3.1:24b, gemma4:12B.
2026-09-14 22:09:09 +02:00
fegger cf16426f5f M1+M2: RAG-Pipeline mit verbindlichem Grounding
agent/-Paket: Ingest (601 Layer-2-Eintraege -> 3005 Chunks, FTS5-BM25 +
Vektoren-Cache), Hybrid-Retrieval (RRF, Stand-Boost, cross_ref-Erweiterung),
Ollama-Client (embed/chat, think-Flag-Fallback, kurzes Connect-Budget),
Systemprompt mit Zitierpflicht, Post-Validierung (zitierte IDs gemaess
Retrieved-Set, 1x Regenerierung, dann Verweigerung), FastAPI (/ask, /health,
/reindex), CLI, Goldset (31 Fragen, IDs gegen kb.json verifiziert, inkl.
ATZ-Konfliktfall + 4 Verweigerungsfaelle), Eval-Suite, Test-Chat.

41 Offline-Tests gruen. Baseline BM25-only: Hit-Rate 0,871 / Recall@8 0,855 /
MRR 0,476. Hybrid-Messung, Antwortmodus-Eval und Modell-Bake-off (M3) auf
dem Host ausstaendig (Ollama aus der Zed-Sandbox nicht erreichbar).

MEMORY.md und planung.md Umsetzungsstand aktualisiert.
2026-09-14 16:53:04 +02:00
fegger b5ca81abab Planung und Skills für den Wissensbasis-RAG-Agenten
planung.md: Architektur (schlanker RAG-Service, SQLite-Index, Hybrid-Retrieval),
verbindliche Grounding-Regeln, Modell-Bake-off M3 (qwen3.8:27b, qwen3:32b,
gemma3:27b, mistral-small3.2:24b, qwen3:14b als Latenz-Untergrenze),
Meilensteine M1-M4 und Odoo-Integrationsoptionen.

.agents: neuer Skill pv-rag-agent (verbindliche Regeln für die Implementierung)
sowie bestehende Projekt-Skills (agent-memory, wissensbasis,
odoo19-development, opendataloader-pdf).
2026-09-14 16:34:13 +02:00
fegger 9d141eed9a Initialimport: Wissensbasis Layer 2 (601 kuratierte Einträge)
571 Lexis-Briefings (lb-*) + 30 WIKU-Publikationen (wk-*) in 69 Clustern,
Frontmatter-Schema als Single Source of Truth; kb.json + INDEX.md generiert.
.gitignore für lizenzierte/lokale Corpora (.lexis360, .wiku, .firecrawl, .ris)
sowie künftige RAG-Artefakte (data/).
2026-09-14 16:34:07 +02:00
fegger ee94f4664f [IMP] l10n_at_hr_payroll_private: add end-of-employment payroll cycle (AP14-A)
Pay the untaken vacation, dismissal compensation, time balances and open advances from one exit run. The vacation replacement splits natively into a running and a special-payment component per § 67 Abs 8 lit d, only the running part extends the mandatory insurance under § 11 Abs 2 ASVG with sequential windows (dismissal compensation first) and daily-value ceilings. Dismissal compensation follows the one-fifth rule of § 67 Abs 8 lit b without raising the annual sixth, stays fully social-insurable by allocation over the notice period and fully subject to payroll taxes. Advances are offset against net pay only (§ 293 Abs 3 EO) and time credits carry the 50 % statutory surcharge without § 68 relief. Verified against the original statute texts in .ris/; § 67 Abs 6 lump-sum payments are deliberately deferred to AP15.
2026-09-14 16:08:44 +02:00
fegger 9ff977e0d4 [IMP] l10n_at_hr_payroll_private: add Pendlerpauschale, Pendlereuro and job ticket data (AP13-B)
Model commuter declarations as effective-dated L 34 records so monthly changes, L 34/Pendlerrechner evidence and historical payslip reproduction stay auditable. PP enters as an aliquoted § 66 freibetrag of the running tax base while PE joins the annual absetzbetraege, matching the cumulative engine's rollup. Ticket handling follows § 26 Z 5: employer ticket costs reduce PP pro rata (lit i), Werkverkehr is data-driven via the residual route and salary conversion does not block PP/PE. Also register the AP13-A tests that were accidentally left out of the previous commit.
2026-09-14 14:47:23 +02:00
fegger 3f307a0688 [IMP] l10n_at_hr_payroll_private: add domestic travel expenses (AP13-A)
Use Odoo's payroll expense reconciliation flow so documented employee-paid travel expenses remain linked to their payslip and payable settlement. Classify only the excess over the verified domestic tax-free limits as running remuneration, avoiding both unpaid expense reimbursements and double payment.
2026-09-14 13:42:06 +02:00
fegger 91ac9e5fda [IMP] various: record AP12 completion and ignore RIS cache 2026-09-14 13:20:38 +02:00
fegger 8efea77b6c [IMP] l10n_at_hr_payroll_private: Sachbezüge (AP12) 2026-09-14 12:22:15 +02:00
fegger 018554b215 [IMP] l10n_at_hr_payroll_private: Lohnpfändung — Existenzminimum § 291a EO, DN-Abzug, Kostenersatz, DSE (AP11)
Pfändungs-/Exekutionsmaschinerie nach EO §§ 291/291a/292/301/302/
292h für den Privatwirtschafts-DN (Plan:
personalverrechnung/PLAN-Restluecken-AP11.md §AP11, MVP + Kostenersatz):

- Neues Modell l10n.at.payroll.pfaendung (models/pfaendung.py):
  Gläubiger, Vollstreckungsträger, Pfändungsart, Zustellungsdatum,
  Forderungshöhe, Status aktiv/gesetzt/geloest, Verwalter
  (§ 301 Abs 1 Z 6), 4-Wochen-Zahlungssperre (§ 304),
  Kostenersatz-Variante (§ 292h), DSE-/Auszahlungsfristen (computed),
  Rang nach Einlangedatum, Constraint Löse-/Zustellungsdatum;
  hr.version: l10n_at_unterhaltspflichten (0–5) und
  l10n_at_sz_anspruch (§ 291a Abs 2 Z 1/Z 2); hr.salary.rule:
  Flags l10n_at_pfandung_unpfandbar (AP12-Schnittstelle) und
  l10n_at_pfandung_beendigung (AP14/15-Schnittstelle)
- Engine (hr.payslip): Existenzminimum-Maschine § 291a EO je Topf
  (laufend/sz13/sz14/beendigung), Pfändungsberechnungsgrundlage
  § 291 EO (laufend = Bezüge − unpfändbar − SV-DN − LSt),
  Masse, Kostenersatz § 292h (2 %/max 8 € + 1 %/max 4 €, nie vom
  Existenzminimum), Regeln PFAENDUNG (seq 75) / PFAENDUNG_SZ (seq 76);
  result_rules enthält ungerundete Rule-Summen — Lesezugänge
  runden auf Cents (Zeilenbeträge), sonst 1-Cent-Drift
- Parameter at_eo_azr (2025: 1.273,99 / 2026: 1.308,39 €) und
  at_eo_kostenersatz; DSE-Report (qweb-pdf auf hr.employee),
  Views/Menu unter Payroll-Wurzel, Security (payroll-Groups +
  Company-ir.rule)
- Rechenfälle docs/rechenfaelle/pfaendung.md (RF-PFA-1…7):
  lb-pfa-03 exakt verifiziert (33.840 € → EX 16.684 € /
  pfändbar 17.156 €); 103/103 Tests grün
- Offene Reste dokumentiert: Unterhaltsexekution 75 %/Koinzidenz,
  Vorschüsse § 290c, Sachbezüge (AP12), Beendigungs-Regeln (AP14/15),
  2027-AZR-Werte

Version 19.0.9.5.0
2026-09-13 02:33:26 +02:00
fegger c3b9d6eb8e [IMP] l10n_at_hr_payroll_private: Arbeits- und Entgeltbestätigungen BEST AK/AW — SART 70/71 Kranken-/75/76 Wochengeld (AP8)
Satzstrukturen PDF-verifiziert aus DM-ORG 42.7.0 (ODL, Pages
493-505; Positionsketten lückenlos): G.10 AEB-Krankengeld v12
(SART 70/71, BEST AK, 111 Felder, Satzlänge 1392) und G.11
AEB-Wochengeld v10 (SART 75/76, BEST AW, 64 Felder, 1048).
Die .txt-Extraktion des DM-ORG ist unzuverlässig (Feldversatz).

- Neues Modell l10n.at.payroll.aeb (models/aeb.py): Episode
  (verhinderung_von/bis), KZKU, Storno über storno_id (E.10.2.1:
  nur Fehler/BMGL-Änderung), REFN/Datei/Prüfprotokoll; Erzeugung
  auf dem ELDA-Bestands-Framework (Vorlauf/Schluss/Satzlängen-
  selbstkontrolle)
- EFZ-Refaktur: Phasen-Karte nach hr.version verschoben
  (_l10n_at_efz_arbeitsjahr_start/_l10n_at_efz_phasen/
  _l10n_at_efz_stufe_am) — Payslip-Status (AP4) und AEB teilen die
  Maschine; _at_efz_status-Verhalten durch Tests gepinnt
- Fachdaten Krankengeld: AEFZ (Anspruch in Wochen), JAGU=1
  (Arbeitsjahr), TAGU=2 (Kalendertage), Teilentgelt TPR/TVO/TBI
  je Phase, Vorerkrankungen ANV/ANB vor der Episode, VENT
  (Voll-Phase-Ende), Bezugsblöcke BVO/BBI/BBE + BSUM aus den
  vollen Monatsläufen vor der Episode, SZKZ datengetrieben
- Fachdaten Wochengeld: AVER (Ø-Netto der drei Referenzmonate,
  § 162 Abs 3 ASVG — AP7-Logik), AVON/ABIS, ANGV=K (kein
  Fortbezug), AGRD-Validierung nach D.22 (SART 75/76 nur
  02/03/04/05/06/10/22/24; unzulässige geleert + Protokoll)
- Fund: L10nAtEldaSatz.setze(None) übernimmt die Grundstellung
  nicht — Felder nur per Leerstring aktiv leeren
- 4 Tests: SART 70 mit 76-Tage-Episode (AEFZ 06, TPR1 50,
  TVO1 13.10.2026, VENT 12.10.2026, Bezugsblock), SART 75 mit
  AVER/AVON/ABIS, Storno 76 (REFN -ST, Kerndaten des Originals),
  AGRD-Validierung
2026-09-12 23:52:11 +02:00
fegger 802f2ef04c [IMP] l10n_at_gemeinde_payroll: Autor im Manifest korrigiert (Florian Egger) 2026-09-12 23:29:08 +02:00
fegger e20de9d543 [IMP] l10n_at_hr_payroll: Autor im Manifest korrigiert (Florian Egger) 2026-09-12 23:29:08 +02:00
fegger e444c1b2c6 [IMP] l10n_at_hr_payroll_private: Autor im Manifest korrigiert (Florian Egger) 2026-09-12 23:28:25 +02:00
fegger 11954f41d7 [IMP] odoo-at-payroll: wissensbasis lb-krs-08 Stufenverifikation ; AP7-Agent-Memory
- lb-krs-08 (Krankenstand-EFZ): die offene ⚠-Marke 'EFZG/AngG-
  Stufenabsätze vor Implementierung gegen RIS verifizieren' ist mit
  der AP4-Umsetzung erledigt — -Eintrag mit Fundstellen
  (EFZG GesNr 10008308 § 2 Abs 1; AngG § 8 Abs 1/2/2a, Stand 2026-09);
  lohnsteuerliche Einordnung laufender Bezug bleibt ⚠ (ohne §-Zitat)
- MEMORY.md: AP7-Status (Wochengeld = KV-Leistung, Rollenklärung
  gegenüber der Plan-Annahme) und offene Reste
2026-09-12 23:27:29 +02:00
fegger 290b432b73 [IMP] l10n_at_hr_payroll_private: Mutterschutz/Wochengeld — Entgelt-Stopp, volle §-162-Abs-3-Berechnung, fiktive BV (AP7)
Rollenklärung (lb-msf-03, ASVG § 162 Abs 1): Wochengeld ist eine
KV-Leistung — die ÖGK zahlt; der Dienstgeber stoppt das Entgelt in
den Beschäftigungsfristen (MSchG §§ 3/5), liefert die Bemessungsdaten
(Arbeits-/Entgeltbestätigung folgt mit AP8) und fuehrt die Abfertigung
Neu auf fiktiver Grundlage.

- models/mutterschutz.py: _at_msch_status (Kalendertage/Arbeitstage/
  Episode aus der neuen Urlaubsart 'Mutterschutz-Frist', WET ATP_MSCH),
  _at_wochengeld — volle Wochengeld-Rechnung § 162 Abs 3 ASVG:
  Ø-Netto der drei Referenzmonate (letzte 13 Wochen) x (1 + satzungs-
  SZ-Prozentsatz 14/17/21 %, Parameter at_msch_sz_prozentsatz,
  vor Erstverwendung mandantenseitig zu verifizieren) ÷ Kalender-
  tage; Fallback kurzes DV (lb-msf-03)
- Regel MSCH_ENTGELT: Entgelt-Stopp je Arbeitstag der Frist (negativ,
  Brutto; Tagesatz-Helper mit EFZG geteilt)
- Fiktive BV-Grundlage § 7 Abs 4 BMSVG (RIS verifiziert):
  Monatsentgelt-Ø der letzten drei Kalendermonate vor dem Versicherungs-
  fall der Mutterschaft inkl. anteiliger Sonderzahlungen x Bezugs-KT/30,
  im _at_bv_basis_laufen-Hook neben der Krankengeld-Fiktion (AP4)
- Rechenfaelle docs/rechenfaelle/mutterschutz.md; 4 Tests
  (Entgelt-Stopp + fiktive BV, §-162-Abs-3-Arithmetik, Wirkungslosigkeit
  ohne Frist, kurzes-DV-Fallback); Optionen dokumentiert:
  Sonderwochengeld § 163, Ruhen § 166, Karenz-Referenzmonat § 7 Abs 4 Z 1-3
2026-09-12 23:27:20 +02:00
fegger 0abc1383d1 [FIX] l10n_at_gemeinde_payroll: Modul erstmals installierbar — 9 Odoo-19-Latenzfehler; BDA-Vordienstzeiten-Vorschlag (AP10+AP9)
AP10 — Latenzfehler des nie installierten Vorlaufs:
- regular_pay auf hr.payroll.structure entfernt (Feld existiert nicht)
- is_paid/round_days/round_days_type von WET-Records entfernt
- Falscher XML-ID-Praefix payroll_structure_bgld. -> Modul-Referenzen
- amount_select 'python' -> 'code' (Selection)
- import in Regelcode entfernt (safe_eval verbietet IMPORT_NAME)
- rules['ME'].total -> result_rules['ME']['total'] (Odoo 19 befuellt
  rules nie; SZQ-Sonderzahlung rechnete gegen Null)
- categories.X -> categories['X'] (NETTO-Regel)
- work_time_rate /100 entfernt (Odoo 19 speichert Bruchteil 0..1)
- nie gelaufene Tests: Payslip-name-Pflichtfeld, Stichtag vor
  Katalog-Gueltigkeit (Wizard-Skip korrekt), date_start ist Compute
  aus contract_date_start

AP9 — BDA-Vordienstzeiten (GemBG S 67 Abs 1-3, RIS LrB GesNr
20001002 verifiziert): Feld vordienstzeiten_monate (Help dokumentiert
Anrechnungskatalog inkl. Obergrenzen), Compute bda_vorschlag
(Vertragsbeginn - Monate), Uebernahme-Button als Verwaltungsakt,
View-Erweiterung
2026-09-12 23:18:07 +02:00
fegger 9b4221fcef [IMP] l10n_at_hr_payroll_private: Wr. DAG, Buchungsbeleg, VWGJ-Einstufung, EFZG-Teilentgelt mit fiktiver BV (AP2d+AP3+AP9+AP4)
AP3 — Wiener Dienstgeberabgabe (U-Bahn-Steuer, RIS LrW GesNr
20000133 verifiziert): Regel WIEN_DAG (Kopfpauschale 2 Euro je DN
und angefangene Woche, Wochenlogik Montag-im-Monat, Befreiungen
Lehrling/55+/<=10 WStd/Karenz-Woche), Company-Flag-Verwertung,
Parameter at_wien_dag_satz, FI 6663/3560 (Referenz-Rest)

AP2d — Buchungsbeleg-Report am Lohnlauf (Konto/Belegtext/Soll/Haben
der FI-Buchung, balancierte Summen; test_account_private-Erweiterung)

AP9 — Vordienstzeiten -> Einstufung: Stufen-Schwelle vwgj_ab an
l10n.at.payroll.kv.gruppe.stufe (aus Labels 'nach N VWGJ'; -1 =
nicht gepflegt, ORM liest Integer-NULL als 0), CSV-Seed,
_get_stufe_fuer_vwgj/_get_naechste_vwgj, Vertragsfeld
l10n_at_vordienstzeit_monate + VWGJ-Vorschlag + Vorrueckungstermin
(S 17 Abs 5 Rahmen-KV, Erster des Erreichungsmonats) + Uebernahme-
Button (manueller Verwaltungsakt)

AP4 — EFZG/Teilentgelt (AngG S 8/EFZG S 2 verifiziert, Gleichlauf
6/8/10/12+4 Wochen; lb-krs-07/08): EFZ-Phasenmaschine models/efzg.py
(Kalendertage-Kontingente je Arbeitsjahr, Krank-/AU-Topf getrennt,
AU-Urlaubsart ATP_AU), Regeln TEIL_ENTGELT_HALB/-50 % und
KRANKENTGELT_PHASE/-100 % je Arbeitstag (Tagesatz aus unreduzierter
Basis), fiktive BV-Grundlage S 7 Abs 3 BMSVG im _at_bv_basis_laufen-
Hook (50 % Vormonatsentgelt x Bezugs-KT/30), Parameter at_efz_stufen;
Krankengeldumlage S 62 ASVG = Falsch-Praemise (kein Baustein);
Rechenfaelle docs/rechenfaelle/efzg.md
2026-09-12 23:17:57 +02:00
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
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
fegger 3c5cc3aded [IMP] odoo-at-payroll: GP9 abgeschlossen — AP9 final, GP1-GP9 komplett, Projekt M8 beendet
CHANGELOG-GP9-Eintrag; MEMORY-Handoff (GP1-GP9 komplett, M8 final, AP10 optional).
2026-09-12 20:44:22 +02:00
fegger 8b9dbf7612 [IMP] l10n_at_hr_payroll_private: GP9 Mitarbeiter-UI, NSchAB-Company-View, Multi-Company-Rules, Demo-Datensatz, Benutzerhandbuch (AP9)
Mitarbeiter-UI (views/mitarbeiter_views.xml): All-in-Felder (l10n_at_ue_pauschale_monatlich, l10n_at_grundlohn_ausweis, § 2g AVRAG) als Gruppe am Mitarbeiterformular; NSchAB-Company-Flag am Lohnverrechnungs-Reiter (l10n_at_nschab — Feld nur im Privatmodul).

Multi-Company-Record-Rules (security/security_rules.xml): ir.rule für l10n.at.payroll.mbgm, .mbgm.line, .vr, .vr.event.

Demo-Datensatz (data/demo.xml, noupdate): 3 AN (Angestellte Vollzeit, Arbeiter Teilzeit, Lehrling) mit Verträgen, KV-Einstufung, VSNR, BMSVG-Flag; Demo-Company mit BKNR/Lohnsteuernummer/Bundesland; nur im Demo-Modus aktiv.

Benutzerhandbuch (docs/benutzerhandbuch.md): Quick-Start deutsch — Stammdaten, Monatsabrechnung, Meldewesen (mBGM/Versichertenmeldung/L16), Belege drucken, Fristenkalender, Jahreswechsel, BV-Endabrechnung.

19.0.8.1.0. 100/100 grün (Wegwerf-DB); odoo_dev auf GP9 aktualisiert.
2026-09-12 20:43:55 +02:00
fegger 447a880063 [IMP] l10n_at_hr_payroll: GP9 Company-Stammdaten-UI, Multi-Company-Record-Rules (AP9)
Company-Stammdaten-UI (views/company_views.xml): res.company-Formular-Inherit mit Reiter "Lohnverrechnung (AT)" — BKNR (10-stellig, Pflicht für alle Meldewesen-Läufe), Lohnsteuernummer (9-stellig), Bundesland (DZ-Satz), WKO-Mitglied (DZ-Pflicht), WF-Wien. Sichtbar nur mit hr_payroll.group_hr_payroll_user.

Multi-Company-Record-Rules (security/security_rules.xml): ir.rule-Records für l10n.at.payroll.l16, .l16.line, .kind, .frist — jede:r Payroll-User sieht nur die Daten der Companies, auf die sie/er Zugriff hat (company_ids aus ir.rule-Standard-Mechanik; Muster: hr_payroll-Engine).

19.0.5.1.0. 100/100 grün (Wegwerf-DB).
2026-09-12 20:43:44 +02:00
fegger 2f55bfb21a [IMP] odoo-at-payroll: GP8-Bau dokumentiert — M8 erreicht, GP1-GP8 komplett
CHANGELOG-GP8-Eintrag; Plan-Abschnitt-10-Update (Fristenkalender, Kontrollsechstel-Wizard, Mitteilungsfile-Import, 2027-Parameter-Rahmen, Vertragsstichtage-UI, All-in/DAG-Reports, B14-Verifikation); MEMORY-Handoff (100/100, odoo_dev auf GP8, GP9/AP9 als optionaler Folgeauftrag); README-Update (GP1-GP8 komplett).
2026-09-12 20:00:27 +02:00
fegger 57522b5be3 [IMP] l10n_at_hr_payroll_private: GP8 Vertragsstichtage-UI, All-in-Deckungsrechnung, DAG-Ausweis, Frist-mBGM-Verlinkung
Vertragsstichtage-UI (views/vertragsfelder_views.xml): GP4-Felder (ALV-Entfall, UV-Entfall, PV-Bonusphase, LNK-frei) + Status (Lehrling, fallweise, Familienabsetzbetrag, ueberw. Nachtarbeit) als gruppierte Payroll-Seite am Mitarbeiterformular (Muster: KV-Einstufung + BV-Gruppe).

All-in-Deckungsrechnung (report/gp8_reports.xml — QWeb auf hr.payslip): GP2-_at_allin_deckung als Ausweis (Pauschale vs. Bezuege, Rest = Über-/Unterdeckung; § 2g AVRAG). DAG-Ausweis (QWeb auf hr.payslip.run): § 1 DAG je Monat — Lohnsumme der Geringfuegigen, Schwelle (1,5 × GFG), Abgabe 19,4 % (GP4 _l10n_at_dag).

Frist-mBGM-Verlinkung (models/fristen_private.py): ref_mbgm_id-Feld am Fristenkalender (Kernmodell kennt mBGM nicht) + action_zum_objekt-Super-Chain; _l10n_at_verlinke_deklarationen setzt Sprungziele fuer offene L16- und mBGM-Fristen.

19.0.8.0.0. Tests: GP8-Kernsuite 6 (100/100 gesamt).
2026-09-12 19:59:03 +02:00
fegger c843701d1a [IMP] l10n_at_hr_payroll: GP8 Fristenkalender, Jahres-Retro, Mitteilungsfile-Import, 2027-Parameter-Rahmen
Fristenkalender (models/fristenkalender.py — l10n.at.payroll.frist): alle lohnverrechnungsrelevanten Melde- und Zahlungsfristen je Company und Jahr (kuratiert: lb-sva-11, lb-mel-08, lb-lvr-04/05): SV/mBGM 15. Folgemonat (Gutschriftprinzip, Respirofrist 3 Tage, Verzugszinsen ab Tag 16), DB/DZ/KommSt 15. Folgemonat, KommSt-Jahreserklärung Ende Maerz, Lohnzettel L16 + Arbeitsstaettenmeldung + §-109a Ende Februar, Schwerarbeitsmeldung Ende Februar (blockiert, ASchV-Datenmodell fehlt). Auto-Erzeugung per ir.cron monatlich am 1. (SV/DB/DZ/KommSt) + jaehrlich am 1. Jaenner (Jahresfristen). Ueberfaellig-Faerbung + Sprungaktion zur GP7-Deklaration (L16; mBGM-Verlinkung im Privatmodul via fristen_private.py).

Jahres-Retro-/Aufrollungslauf (models/jahresretro.py — Wizard l10n.at.payroll.jahresretro.wizard): Kontrollsechstel-Verpflichtungslauf § 67 Abs 10 EStG (Pruefung je AN auf SZ-Ueberhang und Guenstig-Vergleich Fixsaetze vs. Tarif; Handlungsempfehlung fuer die Odoo-Korrektur-Pipeline: Refund-Payslips + Neuberechnung). Optional §-77-Abs-3/4-Aufrollung (15.-Jaenner-Grenze; Krankengeld-Blocker). SV-Aufrollung rollt auf den Beitragsmonat (GP4-Beitragsmonat); mBGM-Storno als Folgeaktion (GP7 art=ST).

Mitteilungsfile-Import (models/mitteilungsfile.py — Wizard): ELDA-Rueckantwort-XML (mitteilung_{Protokollnummer}.xml) parsen, Fehler/Warnungen lesbar aufbereiten; XSDs lokal beschafft (.firecrawl/ris/elda/). Dateibasiert (Praemisse P7): Upload durch den Mandanten.

2027-Parameter-Rahmen (data/rule_parameters_2027.xml): NUR Struktur (WF-Vektor-Default + AV-Staffel tiers=[] da § 2a AMPFG aufgehoben BGBl I 62/2026). KEINE Werte aus Trainingswissen — der Wartungszyklus beschafft die offiziellen 2027-Werte.

B14-laengster-Urlaubs-Teil: verifiziert — aktuelle Implementierung ist praktisch korrekt (erster Urlaubsmonat, _at_sz_bereits_bezahlt gegen Doppelauszahlung); mehrmonatig gesplittete Urlaube sind ein Randfall mit vorwaertsblickenden Planungsdaten, dokumentiert ohne Code-Fix.

Tests: test_gp8 (6 — Frist-Definitionen, Cron-Idempotenz, Ueberfaellig-Logik, Mitteilungsfile-Parser, Import-ohne-Datei-UserError, Jahresretro-ohne-Daten). 100/100 gruen (Wegwerf-DB). 19.0.5.0.0.
2026-09-12 19:58:53 +02:00
fegger ea14c48cca [IMP] odoo-at-payroll: GP7-Bau, DM-ORG-42.7.0-Verifikationen und Nutzer-Entscheidungen 5.1-5.4 dokumentiert
CHANGELOG-GP7-Eintrag; Plan-Abschnitt-10-Update (ELDA-Fixsatz-Konventionen, KZ-210-§-25-Gesamtbrutto-Fund, mBGM-Abschläge je Basis, Nutzer-Entscheidungen: Arbeitsstätte in GP7, Mitteilungsfile-Import GP8, FB+ nicht im Lohnsteuerkern/ANV + Kind-Modell für FABO+-Wartung); MEMORY-Handoff (94/94, Wegwerf-DB, GP8 als nächster Bauabschnitt, offene Reste: odoo_dev-Update, geringfügig-mBGM-Parametrisierung, CRLF/ELDA-Kanal-Verifikation, Referenzbuchhaltungs-Endfixierung, Schwerarbeit nach ASchV-Datenmodell-Bau).
2026-09-12 17:31:21 +02:00
fegger 14940ac31a [IMP] l10n_at_hr_payroll_private: GP7 mBGM/JASV + Versichertenmeldung + fallweise Anmeldung auf dem ELDA-Framework
mBGM/JASV (models/mbgm.py — DM-ORG 42.7.0 E.32, BEST MB, Version 02): Paket PS (BKNR/DGNA/BZRM/GSUM/ANZM) → mBGM G1 (REFW/VSNR/FANA/VONA/VSUM/VERG=1) → Tarifblock T1 (BSGR/VVON) → Verrechnungsbasis BS (VBTY/VBBT) → Verrechnungsposition V1 (VPTY/VPVZ/VPTA/RSVZ/RSUM). KEIN DN/DG-Zweig-Split: T01 = GESAMT-Prozentsatz des Tarifsystems je Beschäftigtengruppe (Parameter at_mbgm_tarif, normativ: laufend 39,05 % = KV 7,65 + PV 22,80 + AV 5,90 + UV 1,10 + WF 1,00 + IESG 0,10; SZ 37,55 % = DN KV 3,87 + PV 10,25 + AV 2,95 + DG 20,48). Abschläge als EIGENE Verrechnungspositionen je Basis: A03/A02/A01 = Minderung AV auf 0/1/2 % (§ 2a AMPFG, nur DN-AV, je Verrechnungsbasis AB UND SZ), A09 = UV-Entfall 60, A10 = AV+IE-Entfall Pensionsanspruch (Konkurrenz a4: sperrt Staffelabschlag), A15 = PV-Halbierung Bonusphase — konsistent mit der GP4-§-2a-DN-seitig-Konvention. BV (BMSVG): eigene Verrechnungsbasis BV (ungedeckelt, GP5) + Position V01 1,53 %. RSUM = Basis x Satz (mBGM-Tarifierung), Konsistenz-Check gegen GP4-Payslip-Beitragssummen ±0,10 € als Prüfwarnung. Storno: R1-Sätze mit REFU/VSUM wie Original. Fehlanzeige: Paket ohne mBGM-Sätze (ANZM 0). Geringfügige/fallweise ohne Parametrisierung: listenweiser Abbruch (keine Tarif-Erfindung; BSGR B044/B045/B010/B030 aus dem SV-Tarifsystem pflegen).

Versichertenmeldung (models/versichertenmeldung.py — DM-ORG E.29, BEST VR, Version 03, Satzlänge 772): M3 vor Arbeitsantritt / M4 (7 Kalendertage ab PV-Ende) mit deklarativer Event-Erkennung über die Meldungs-Registry (l10n.at.payroll.vr.event) — idempotente Läufe, Dimona-auto-Idee ohne write-diff-Hacks. Felder: BKNR/VSNR/GEBD/FANA/VONA (Namenssplit am letzten Leerzeichen), ADAT/EBSV, BBER 01-04 aus l10n_at_personalgruppe/lehrling, GERF (Kataloggehalt vs. GFG), FRDV, BVAB/BVJN aus dem GP5-BMSVG-Flag, VWAZ (Stunden x100, neu ab 01.01.2026), AGRD aus dem hr.departure.reason-Mapping (Standard 00). Fallweise Anmeldung (BEST MA, SART 32, Version 07, Länge 450): MVP je Anmeldetag am Vertragsbeginn; tagesgenaue Einsatztage via Work-Entry-Auswertung = dokumentierter Rest.

NSchAB-Branche als Company-Flag (l10n_at_nschab): B001-T01 + 3,80 % (Schlechtwetterentschädigung, ÖGK-Tarifsystem Normalbetriebe mit Schlechtwetterentschädigung).

Tests: test_meldewesen_private (2 — mBGM-Satzhierarchie mit A03 je Basis aus November-Fixture + GSUM-Konsistenz; VR M3/M4 mit Registry-Idempotenz und AGRD). Rechenfälle docs/rechenfaelle/meldewesen.md. 19.0.7.0.0.
2026-09-12 17:31:12 +02:00
fegger c474a39466 [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.
2026-09-12 17:30:59 +02:00
1919 changed files with 437563 additions and 316 deletions
+1063 -7
View File
File diff suppressed because it is too large Load Diff
+187
View File
@@ -0,0 +1,187 @@
---
name: pv-rag-agent
description: |
Build and maintain the local RAG agent for Austrian payroll
(Personalverrechnung) that answers strictly from the curated
Wissensbasis (pv-agent/wissensbasis/, Layer 2, 1622 entries). Covers the RAG
service pipeline (ingest, hybrid retrieval, generation, grounding,
eval), Ollama integration (server, models, VRAM budget), the binding
grounding and citation rules, the model bake-off protocol, and the
Odoo Enterprise integration (Phase B, Modul
l10n_at_payroll_agent). Use for any work on the pv-agent/agent/
package, retrieval quality, prompts, the goldset/eval suite, Ollama
model choice, or the Odoo bridge module. Combine with
.agents/skills/wissensbasis/SKILL.md whenever Wissensbasis content changes.
disable-model-invocation: false
---
# PV RAG Agent (Wissensbasis-Copilot)
Implementation skill for the payroll knowledge agent planned in
`pv-agent/planung.md`. That file holds the full plan (architecture, milestones
M1M4, decision points); this skill records the rules a thread must
respect while implementing it.
Repo-Kontext (seit 2026-09-17): der Agent lebt als `pv-agent/`-Subtree
im odoo-at-payroll-Repo; Service-Dokumentation unter `pv-agent/docs/`,
Betrieb via compose aus `pv-agent/` auf dem GPU-Host.
## Scope & applicability
Use this skill for all work on:
- the RAG service (`pv-agent/agent/` package: ingest, retrieve, generate, api,
cli, eval) and its index (`pv-agent/data/index.db`);
- prompts, grounding checks, citation formatting, refusal behaviour;
- Ollama model configuration, embedding/reranker setup, server
connectivity;
- the eval goldset and any retrieval/prompt/model change;
- the Odoo bridge module `l10n_at_payroll_agent` (Phase B, in
`addons/`);
When work touches the Wissensbasis itself (new batches, frontmatter,
curation), also apply `.agents/skills/wissensbasis/SKILL.md`. When work
touches Odoo code, also apply
`.agents/skills/odoo19-development/SKILL.md`.
## System context (fixed facts)
- **Ollama server:** `http://100.103.83.12:11435` — a **remote GPU
machine** in the Tailscale network (development environment, Radeon
AI Pro R9700) holding the model zoo: qwen3.8:27b, bge-m3, bake-off
candidates. Do **not** "correct" the URL to localhost or port 11434 —
a local Ollama on the dev machine's 11434 is a different, near-empty
instance. Verify with `curl http://100.103.83.12:11435/api/tags`.
Shell access for restarts/logs exists only on that machine.
- **GPU:** AMD Radeon AI Pro R9700, 32 GB — keep the total resident
budget (answer model + embeddings + KV cache) under ~28 GB.
- **Models (provisional until bake-off, see protocol below):**
- answer model: `qwen3.8:27b` (Q4, ~18 GB, 256K context) — newest
Qwen generation (verified on the Ollama library 2026-09);
**thinking is on by default** — disable per request for RAG
latency (library documents per-request disabling plus
`reasoning_effort` / `preserve_thinking`; verify the exact Ollama
API option when implementing); vision exists but stays unused;
- fallback / known quantity: `qwen3:32b` (Q4_K_M, ~20 GB), thinking
mode **off**;
- further bake-off candidates: `gemma3:27b`, `mistral-small3.2:24b`
(24B, ~15 GB, 128K context; European vendor, expected strong
German — user hypothesis, verify in the bake-off; no thinking
mode), `qwen3:14b` (latency floor), `qwen3:30b-a3b` (MoE,
throughput);
- embeddings: `bge-m3` via `/api/embed` (multilingual, German);
- optional reranker: `bge-reranker-v2-m3` — verify the installed
Ollama version's rerank API **before** building on it; the design
must work without reranking (fallback: hybrid score only).
- **Corpus:** Layer 2 only — `pv-agent/wissensbasis/dokumente/*.md`,
1 622 entries,
frontmatter is the single source of truth; `pv-agent/wissensbasis/kb.json`
is its generated,
validated projection.
## Binding grounding constraints
These are the product's core promise — never weaken them:
1. **Answers only from the retrieved Layer-2 context.** No training
knowledge, no web search, no tools, no browsing hooks. The pipeline
has no outbound path besides Ollama — keep it that way.
2. **Citation duty:** every factual statement carries its KB ID
(e.g. `[lb-atz-07]`); every value carries its Stand
(`(Stand YYYY-MM)`), mirroring the Wissensbasis curation convention.
3. **Post-validation:** every ID cited in an answer must be in the
retrieved set. On violation: one regeneration with a stricter
instruction, then refuse or mark the answer as uncertain. Never ship
an answer that fails this check.
4. **Refusal duty:** if retrieval is empty or weak, say so ("Dazu
enthält die Wissensbasis keine Aussage") and optionally name related
clusters. Never fill gaps from prior knowledge.
5. **Corpus conflicts:** present **both** values with ⚠ and IDs (e.g.
ATZ replacement quota 28,5 % vs 27,5 %, lb-atz-07 vs lb-atz-09/12);
never resolve silently — same rule as curation convention 3.
6. **§ discipline:** cite norms only as the source names them
(`legal_bases`); no § completion from training knowledge.
7. **Layer 1 stays out of prompts** (`.lexis360/`, `.wiku/` are
licensed). Layer-2 text is curated own-words content and safe.
Layer-1 provisioning for deeper quotes is an open point (see
`wissensbasis/README.md`) — do not decide it ad hoc.
8. **Privacy:** the agent is a knowledge assistant. No employee or
payroll data flows into prompts — only the question and Layer-2
text.
## Architecture decisions (do not redesign without user approval)
- Lean custom pipeline, **no LangChain/LlamaIndex** (1 600+ docs, full
control over grounding beats framework convenience).
- One SQLite file `pv-agent/data/index.db` (gitignored): FTS5 (BM25) + dense
vectors + metadata columns. No external vector DB.
- Chunking: H2 sections per entry; `## Kernwerte & Fristen` tables
become their own chunks (numeric questions); parent-child — retrieve
on section, provide section + metadata header as context.
- Hybrid retrieval: BM25 + dense (RRF fusion), optional reranker,
metadata filters (`topic`, `stand` recency), `cross_refs` expansion
of top hits; 812 context blocks, each with a metadata header
(ID · Titel · Stand · topic · Werk).
- FastAPI surface: `POST /ask`, `GET /health`, `POST /reindex`;
`pv-agent/agent/cli.py` for the dev loop; minimal static web UI for demos.
- German normalization for FTS5: umlaut folding at ingest time
(ä→ae or ä→a — pick once, stay consistent; ASCII slugs follow the
Wissensbasis convention: umlauts dropped, ß→ss).
## Ingestion rules
- Parse Layer-2 frontmatter directly from the `.md` files; treat
`kb.json` as a consistency gate (entry counts and ID sets must
match — mismatch aborts the ingest with a clear error).
- Incremental embeddings: cache vectors keyed by content hash; a
reindex only embeds new/changed chunks.
- After each new Wissensbasis batch (workflow in
`.agents/skills/wissensbasis/SKILL.md`): run `POST /reindex`, then run the
eval suite.
## Validation gates
- **Goldset** `pv-agent/agent/eval/goldset.yaml`: 3050 questions with expected
IDs, including conflict cases (ATZ quotas) and 35 out-of-KB
questions that must be refused.
- **Metrics** (run before merging any retrieval/prompt/model change):
retrieval recall@8 (target > 0.9), citation precision (target 100 %),
refusal correctness, end-to-end latency.
- **Unit tests** (`pv-agent/tests/`): ingest schema validation, normalization,
post-validation behaviour (hallucinated ID → regeneration → refuse),
conflict rendering.
- Never validate with values from training knowledge — use the
Wissensbasis and Layer-1 spot checks instead.
## Model change protocol (bake-off, M3)
The answer model is only changed via a documented bake-off on the
goldset: `qwen3.8:27b` vs. `qwen3:32b` vs. `gemma3:27b` vs.
`mistral-small3.2:24b` (plus `qwen3:14b` as latency floor; temperature
~0.1, thinking off where the model has a thinking mode). Decision criteria: citation precision first, then refusal
correctness, then latency. Record the outcome like other project
decisions (D1/D2 style) in `pv-agent/planung.md` and `.agents/MEMORY.md`
(workflow: `.agents/skills/agent-memory/SKILL.md`). A faster model may only
win if citation precision is equal.
## Odoo Enterprise integration (Phase B)
- **Option A (planned default):** thin custom module with an OWL chat
panel; service URL via `ir.config_parameter`; role-based access.
The RAG service remains the single source of truth for grounding and
citations. No retrieval/grounding logic in Odoo.
- **Option B (to verify first):** Odoo 19's native LLM modules with
Ollama as an OpenAI-compatible provider. **Never assume module
names, models, fields or endpoints** — verify against the actual
Odoo 19 source before planning Option B in detail
(`.agents/skills/odoo19-development/SKILL.md`).
- Phase A runs independently of this decision; do not couple the
service API to Odoo specifics.
## Hygiene
- `data/index.db`, `data/`, logs and caches: gitignored.
- `.lexis360/`, `.wiku/`, `.firecrawl/`, `.ris/` stay unversioned
(licensed / local). Never commit them.
- Configuration via environment variables (`agent/config.py`): Ollama
URL, model names, port — no hardcoded hosts in business code.
+1
View File
@@ -4,6 +4,7 @@ __pycache__/
.lexis360/ .lexis360/
.wiku/ .wiku/
.additional_docs/ .additional_docs/
.ris/
odoo_19.0+e.20260910/ odoo_19.0+e.20260910/
.venv/ .venv/
.zed-hours/ .zed-hours/
+25
View File
@@ -35,6 +35,9 @@ no skill applies.
- Any Wissensbasis work (batch intake, curation, `kb.json`, the sources - Any Wissensbasis work (batch intake, curation, `kb.json`, the sources
`.lexis360/` and `.wiku/`): `.lexis360/` and `.wiku/`):
`wissensbasis/SKILL.md` `wissensbasis/SKILL.md`
- Wissensbasis-Agent/RAG-Service (pv-agent/ — Service-Code, Retrieval,
Prompts, Goldset/Eval, Ollama-Modelle, Review-Brücke):
`pv-rag-agent/SKILL.md`
- Any PDF extraction (text/Markdown/JSON from PDFs, scanned-PDF OCR, - Any PDF extraction (text/Markdown/JSON from PDFs, scanned-PDF OCR,
tables) — e.g. Wissensbasis sources, legal PDFs, ad-hoc extraction and tables) — e.g. Wissensbasis sources, legal PDFs, ad-hoc extraction and
spot-checks: spot-checks:
@@ -133,6 +136,28 @@ Before considering the task complete, review the resulting changes for:
- unnecessary custom code; - unnecessary custom code;
- regression risk. - regression risk.
## RAG-Service (pv-agent/)
Seit dem Repo-Merge 2026-09-17 lebt der Wissensbasis-Agent als
`pv-agent/`-Subtree in diesem Repo (History erhalten; Plan:
`pv-agent/planung.md`, Standalone-Memory-Archiv: `pv-agent/.agents/MEMORY.md`).
- Service: FastAPI unter `pv-agent/agent/`, Betrieb als Docker-Stack
auf dem GPU-Host (compose aus `pv-agent/`, Doku: `pv-agent/docs/DOCKER.md`);
Ollama auf `http://100.103.83.12:11435`.
- Wissensbasis: `pv-agent/wissensbasis/` (Layer 2, kanonischer
RAG-Korpus; `personalverrechnung/wissensbasis/` bleibt vorerst als
zweite Kopie bestehen — Zusammenlegung als separater Schritt).
- Vertrags-/Grounding-Regeln: `pv-agent/docs/API.md` und
`.agents/skills/pv-rag-agent/SKILL.md` (zitatpflichtige Antworten,
Post-Validierung, Verweigerung, Eval-Gates vor jedem Retrieval-,
Prompt- oder Modell-Change).
- Odoo-Brücke: `addons/l10n_at_payroll_agent` (Review-Modus gegen
`/v1/ask` `mode=review`; Feature-Flag `PV_REVIEW_MODE` im Agenten,
default aus).
- Tests: pytest offline aus `pv-agent/` (`python -m pytest tests`);
Odoo-Seite der Brücke läuft in der regulären Odoo-Test-Session.
## Verify before assuming ## Verify before assuming
Never invent or assume the existence of an API. Never invent or assume the existence of an API.
+15
View File
@@ -10,12 +10,27 @@ seit 2026-09-09 — Überführung aus `gem360-git` per History-Import; siehe
|---|---| |---|---|
| `l10n_at_hr_payroll` | **Bundes-Kern** (sektor-neutral): SV-Werte 2026 als `hr.rule.parameter`-Seed, TASY-Import-Wizard; folgen: Lohnsteuer (AP3), Meldewesen L16/eSV (AP5), Belege (AP6) | | `l10n_at_hr_payroll` | **Bundes-Kern** (sektor-neutral): SV-Werte 2026 als `hr.rule.parameter`-Seed, TASY-Import-Wizard; folgen: Lohnsteuer (AP3), Meldewesen L16/eSV (AP5), Belege (AP6) |
| `l10n_at_gemeinde_payroll` | **Bgld. GemBG 2014**: Katalogmodell (AP1), Entgelt-Engine (AP2); AP3+ gemäß `personalverrechnung/IMPLEMENTIERUNGSPLAN-Bgld.md` | | `l10n_at_gemeinde_payroll` | **Bgld. GemBG 2014**: Katalogmodell (AP1), Entgelt-Engine (AP2); AP3+ gemäß `personalverrechnung/IMPLEMENTIERUNGSPLAN-Bgld.md` |
| `l10n_at_payroll_dokumente` | **Dokumente aus Vorlagen**: Dokumentgenerierung (Dienstvertrag, Kündigung, Bescheinigungen …) aus hr-Stammdaten; eigene Texte, Struktur-Referenz (kb_ref) dokumentiert; Wizard mit Auto-/Manuellfeldern, Vorschau, PDF/HTML-Ablage |
| `l10n_at_hr_payroll_private` | **General-AT** (KV-basierte Privatwirtschaft) — Gerüst; Implementierungsplan freigegeben ([`IMPLEMENTIERUNGSPLAN-Privat.md`](personalverrechnung/IMPLEMENTIERUNGSPLAN-Privat.md)), Umsetzung ab M1 (GP0) | | `l10n_at_hr_payroll_private` | **General-AT** (KV-basierte Privatwirtschaft) — Gerüst; Implementierungsplan freigegeben ([`IMPLEMENTIERUNGSPLAN-Privat.md`](personalverrechnung/IMPLEMENTIERUNGSPLAN-Privat.md)), Umsetzung ab M1 (GP0) |
| `l10n_at_payroll_agent` | **Agenten-Brücke** (M4.1): Plausibilitätsprüfung einer geplanten Auszahlung — serverseitiger Client ruft den pv-agent-Service per `/v1/ask` `mode=review` auf; depends `l10n_at_hr_payroll_private` |
VRV-Buchung für Gemeinden (Anlage 3b, Ansatz/MVAG, EHH/FHH) läuft über VRV-Buchung für Gemeinden (Anlage 3b, Ansatz/MVAG, EHH/FHH) läuft über
die Brücke `l10n_at_gemeinde_payroll_vrv` im **gem360-Repo** (AP7). die Brücke `l10n_at_gemeinde_payroll_vrv` im **gem360-Repo** (AP7).
Abhängigkeitsrichtung: gem360 → dieses Repo, **nie** umgekehrt. Abhängigkeitsrichtung: gem360 → dieses Repo, **nie** umgekehrt.
## Wissensbasis-Agent (pv-agent/)
Seit 2026-09-17 Teil dieses Repos (Subtree `pv-agent/`, History aus dem
eigenständigen Repo erhalten): lokaler RAG-Agent für die österreichische
Personalverrechnung, der ausschließlich aus der kuratierten Wissensbasis
(`pv-agent/wissensbasis/`, Layer 2) antwortet. Der Service (FastAPI)
läuft als Docker-Stack auf dem GPU-Host; Odoo konsumiert ihn über
`addons/l10n_at_payroll_agent` (Plausibilitätsprüfung einer geplanten
Auszahlung via `/v1/ask` `mode=review`). Plan, Betrieb und Regeln:
`pv-agent/planung.md`, `pv-agent/docs/DOCKER.md`, `pv-agent/docs/API.md`
und `.agents/skills/pv-rag-agent/SKILL.md`.
## Entwicklung ## Entwicklung
- Odoo 19: **Quellen-Referenz ist `odoo_19.0+e.20260910/odoo-19.0+e.20260910/`** - Odoo 19: **Quellen-Referenz ist `odoo_19.0+e.20260910/odoo-19.0+e.20260910/`**
@@ -1,8 +1,8 @@
# Part of the odoo-at-payroll project. License: LGPL-3. # Part of the odoo-at-payroll project. License: LGPL-3.
{ {
'name': 'Österreich - Gemeinde-Personalverrechnung (Bgld. GemBG)', 'name': 'Österreich - Gemeinde-Personalverrechnung (Bgld. GemBG)',
'version': '19.0.1.4.0', 'version': '19.0.1.6.0',
'author': 'ixsol / gem360', 'author': 'Florian Egger',
'website': 'https://gem360.at', 'website': 'https://gem360.at',
'category': 'Human Resources/Payroll', 'category': 'Human Resources/Payroll',
'description': """ 'description': """
@@ -16,7 +16,6 @@
<field name="name">Gemeindebedienstete (Bgld. GemBG 2014)</field> <field name="name">Gemeindebedienstete (Bgld. GemBG 2014)</field>
<field name="code">GEMBG.BGLD</field> <field name="code">GEMBG.BGLD</field>
<field name="type_id" ref="structure_type_gembg_bgld"/> <field name="type_id" ref="structure_type_gembg_bgld"/>
<field name="regular_pay" eval="True"/>
</record> </record>
<!-- ============================================================ --> <!-- ============================================================ -->
@@ -42,47 +41,32 @@
<!-- ============================================================ --> <!-- ============================================================ -->
<!-- Work-Entry-Types für GemBG-Komponenten --> <!-- Work-Entry-Types für GemBG-Komponenten -->
<!-- ============================================================ --> <!-- ============================================================ -->
<!-- Odoo 19: hr.work.entry.type hat weder is_paid noch
round_days/round_days_type mehr (Latenzfehler des Bgld-
Vorlaufs, entfernt). -->
<record id="work_entry_type_ueberstunden_tag" model="hr.work.entry.type"> <record id="work_entry_type_ueberstunden_tag" model="hr.work.entry.type">
<field name="name">Überstunden (tagsüber, § 76 GemBG)</field> <field name="name">Überstunden (tagsüber, § 76 GemBG)</field>
<field name="code">GEMBG_UEB_TAG</field> <field name="code">GEMBG_UEB_TAG</field>
<field name="round_days">NO</field>
<field name="round_days_type">DOWN</field>
<field name="is_paid" eval="True"/>
</record> </record>
<record id="work_entry_type_ueberstunden_nacht" model="hr.work.entry.type"> <record id="work_entry_type_ueberstunden_nacht" model="hr.work.entry.type">
<field name="name">Überstunden (Nachtzeit 22-6 Uhr, § 76 GemBG)</field> <field name="name">Überstunden (Nachtzeit 22-6 Uhr, § 76 GemBG)</field>
<field name="code">GEMBG_UEB_NACHT</field> <field name="code">GEMBG_UEB_NACHT</field>
<field name="round_days">NO</field>
<field name="round_days_type">DOWN</field>
<field name="is_paid" eval="True"/>
</record> </record>
<record id="work_entry_type_mehrdienst" model="hr.work.entry.type"> <record id="work_entry_type_mehrdienst" model="hr.work.entry.type">
<field name="name">Mehrdienstleistung (§ 76 GemBG)</field> <field name="name">Mehrdienstleistung (§ 76 GemBG)</field>
<field name="code">GEMBG_MEHR</field> <field name="code">GEMBG_MEHR</field>
<field name="round_days">NO</field>
<field name="round_days_type">DOWN</field>
<field name="is_paid" eval="True"/>
</record> </record>
<record id="work_entry_type_journaldienst" model="hr.work.entry.type"> <record id="work_entry_type_journaldienst" model="hr.work.entry.type">
<field name="name">Journaldienst (§ 79 GemBG)</field> <field name="name">Journaldienst (§ 79 GemBG)</field>
<field name="code">GEMBG_JOURNAL</field> <field name="code">GEMBG_JOURNAL</field>
<field name="round_days">NO</field>
<field name="round_days_type">DOWN</field>
<field name="is_paid" eval="True"/>
</record> </record>
<record id="work_entry_type_bereitschaft" model="hr.work.entry.type"> <record id="work_entry_type_bereitschaft" model="hr.work.entry.type">
<field name="name">Bereitschaftsdienst (§ 80 GemBG)</field> <field name="name">Bereitschaftsdienst (§ 80 GemBG)</field>
<field name="code">GEMBG_BEREIT</field> <field name="code">GEMBG_BEREIT</field>
<field name="round_days">NO</field>
<field name="round_days_type">DOWN</field>
<field name="is_paid" eval="True"/>
</record> </record>
<record id="work_entry_type_sonn_feiertag" model="hr.work.entry.type"> <record id="work_entry_type_sonn_feiertag" model="hr.work.entry.type">
<field name="name">Sonn-/Feiertagsdienst (§ 78 GemBG)</field> <field name="name">Sonn-/Feiertagsdienst (§ 78 GemBG)</field>
<field name="code">GEMBG_SONNFEI</field> <field name="code">GEMBG_SONNFEI</field>
<field name="round_days">NO</field>
<field name="round_days_type">DOWN</field>
<field name="is_paid" eval="True"/>
</record> </record>
<!-- ============================================================ --> <!-- ============================================================ -->
@@ -7,11 +7,11 @@
<field name="name">Monatsentgelt</field> <field name="name">Monatsentgelt</field>
<field name="code">ME</field> <field name="code">ME</field>
<field name="sequence" eval="1"/> <field name="sequence" eval="1"/>
<field name="category_id" ref="payroll_structure_bgld.category_gembg_brutto"/> <field name="category_id" ref="category_gembg_brutto"/>
<field name="struct_id" ref="payroll_structure_bgld.structure_gembg_bgld"/> <field name="struct_id" ref="structure_gembg_bgld"/>
<field name="condition_select">python</field> <field name="condition_select">python</field>
<field name="condition_python">result = bool(employee.entlohnungsgruppe_id and employee.entlohnungsstufe)</field> <field name="condition_python">result = bool(employee.entlohnungsgruppe_id and employee.entlohnungsstufe)</field>
<field name="amount_select">python</field> <field name="amount_select">code</field>
<field name="amount_python_compute"><![CDATA[ <field name="amount_python_compute"><![CDATA[
# Monatsentgelt nach Bgld. GemBG 2014 (IVa. Hauptstück §§ 133 ff). # Monatsentgelt nach Bgld. GemBG 2014 (IVa. Hauptstück §§ 133 ff).
# Katalog-Lookup: Entlohnungsgruppe + Stufe + Stichtag. # Katalog-Lookup: Entlohnungsgruppe + Stufe + Stichtag.
@@ -24,9 +24,10 @@ else:
# Ausbildungsphase: 5 % Abzug (§ 133h GemBG) # Ausbildungsphase: 5 % Abzug (§ 133h GemBG)
if employee.ausbildungsphase: if employee.ausbildungsphase:
base *= 0.95 base *= 0.95
# Teilzeit: anteilig über work_time_rate (§ 59 GemBG) # Teilzeit: anteilig über work_time_rate (§ 59 GemBG) — Odoo 19
work_rate = employee.work_time_rate or 100.0 # speichert work_time_rate als Bruchteil 0..1 (nicht Prozent).
result = base * work_rate / 100.0 work_rate = employee.work_time_rate or 1.0
result = base * work_rate
]]></field> ]]></field>
</record> </record>
@@ -37,8 +38,8 @@ else:
<field name="name">Kinderzulage (§ 61 GemBG)</field> <field name="name">Kinderzulage (§ 61 GemBG)</field>
<field name="code">KIZU</field> <field name="code">KIZU</field>
<field name="sequence" eval="2"/> <field name="sequence" eval="2"/>
<field name="category_id" ref="payroll_structure_bgld.category_gembg_brutto"/> <field name="category_id" ref="category_gembg_brutto"/>
<field name="struct_id" ref="payroll_structure_bgld.structure_gembg_bgld"/> <field name="struct_id" ref="structure_gembg_bgld"/>
<field name="condition_select">python</field> <field name="condition_select">python</field>
<field name="condition_python">result = employee.kinder_anzahl > 0</field> <field name="condition_python">result = employee.kinder_anzahl > 0</field>
<field name="amount_select">code</field> <field name="amount_select">code</field>
@@ -58,16 +59,16 @@ result = employee.kinder_anzahl * kiz_satz
<field name="name">Funktionszulage (§ 62 GemBG)</field> <field name="name">Funktionszulage (§ 62 GemBG)</field>
<field name="code">FUZU</field> <field name="code">FUZU</field>
<field name="sequence" eval="3"/> <field name="sequence" eval="3"/>
<field name="category_id" ref="payroll_structure_bgld.category_gembg_brutto"/> <field name="category_id" ref="category_gembg_brutto"/>
<field name="struct_id" ref="payroll_structure_bgld.structure_gembg_bgld"/> <field name="struct_id" ref="structure_gembg_bgld"/>
<field name="condition_select">python</field> <field name="condition_select">python</field>
<field name="condition_python">result = employee.funktionszulage > 0</field> <field name="condition_python">result = employee.funktionszulage > 0</field>
<field name="amount_select">code</field> <field name="amount_select">code</field>
<field name="amount_python_compute"><![CDATA[ <field name="amount_python_compute"><![CDATA[
# Funktionszulage § 62 GemBG — Satz am Dienstvertrag (hr.version). # Funktionszulage § 62 GemBG — Satz am Dienstvertrag (hr.version).
# Bei Teilzeit anteilig (wie Monatsentgelt). # Bei Teilzeit anteilig (wie Monatsentgelt); work_time_rate = Bruchteil.
work_rate = employee.work_time_rate or 100.0 work_rate = employee.work_time_rate or 1.0
result = employee.funktionszulage * work_rate / 100.0 result = employee.funktionszulage * work_rate
]]></field> ]]></field>
</record> </record>
@@ -78,8 +79,8 @@ result = employee.funktionszulage * work_rate / 100.0
<field name="name">Überstundenvergütung tagsüber (§ 76 GemBG)</field> <field name="name">Überstundenvergütung tagsüber (§ 76 GemBG)</field>
<field name="code">UEZ_TAG</field> <field name="code">UEZ_TAG</field>
<field name="sequence" eval="10"/> <field name="sequence" eval="10"/>
<field name="category_id" ref="payroll_structure_bgld.category_gembg_brutto"/> <field name="category_id" ref="category_gembg_brutto"/>
<field name="struct_id" ref="payroll_structure_bgld.structure_gembg_bgld"/> <field name="struct_id" ref="structure_gembg_bgld"/>
<field name="condition_select">python</field> <field name="condition_select">python</field>
<field name="condition_python">result = 'GEMBG_UEB_TAG' in worked_days and worked_days['GEMBG_UEB_TAG'].number_of_hours > 0</field> <field name="condition_python">result = 'GEMBG_UEB_TAG' in worked_days and worked_days['GEMBG_UEB_TAG'].number_of_hours > 0</field>
<field name="amount_select">code</field> <field name="amount_select">code</field>
@@ -100,8 +101,8 @@ else:
<field name="name">Überstundenvergütung Nachtzeit (§ 76 GemBG)</field> <field name="name">Überstundenvergütung Nachtzeit (§ 76 GemBG)</field>
<field name="code">UEZ_NACHT</field> <field name="code">UEZ_NACHT</field>
<field name="sequence" eval="11"/> <field name="sequence" eval="11"/>
<field name="category_id" ref="payroll_structure_bgld.category_gembg_brutto"/> <field name="category_id" ref="category_gembg_brutto"/>
<field name="struct_id" ref="payroll_structure_bgld.structure_gembg_bgld"/> <field name="struct_id" ref="structure_gembg_bgld"/>
<field name="condition_select">python</field> <field name="condition_select">python</field>
<field name="condition_python">result = 'GEMBG_UEB_NACHT' in worked_days and worked_days['GEMBG_UEB_NACHT'].number_of_hours > 0</field> <field name="condition_python">result = 'GEMBG_UEB_NACHT' in worked_days and worked_days['GEMBG_UEB_NACHT'].number_of_hours > 0</field>
<field name="amount_select">code</field> <field name="amount_select">code</field>
@@ -125,8 +126,8 @@ else:
<field name="name">Journaldienstzulage (§ 79 GemBG)</field> <field name="name">Journaldienstzulage (§ 79 GemBG)</field>
<field name="code">JZU</field> <field name="code">JZU</field>
<field name="sequence" eval="12"/> <field name="sequence" eval="12"/>
<field name="category_id" ref="payroll_structure_bgld.category_gembg_brutto"/> <field name="category_id" ref="category_gembg_brutto"/>
<field name="struct_id" ref="payroll_structure_bgld.structure_gembg_bgld"/> <field name="struct_id" ref="structure_gembg_bgld"/>
<field name="condition_select">python</field> <field name="condition_select">python</field>
<field name="condition_python">result = 'GEMBG_JOURNAL' in worked_days and worked_days['GEMBG_JOURNAL'].number_of_hours > 0</field> <field name="condition_python">result = 'GEMBG_JOURNAL' in worked_days and worked_days['GEMBG_JOURNAL'].number_of_hours > 0</field>
<field name="amount_select">code</field> <field name="amount_select">code</field>
@@ -146,8 +147,8 @@ result = tage * satz
<field name="name">Sonn-/Feiertagszulage (§ 78 GemBG)</field> <field name="name">Sonn-/Feiertagszulage (§ 78 GemBG)</field>
<field name="code">SFZU</field> <field name="code">SFZU</field>
<field name="sequence" eval="13"/> <field name="sequence" eval="13"/>
<field name="category_id" ref="payroll_structure_bgld.category_gembg_brutto"/> <field name="category_id" ref="category_gembg_brutto"/>
<field name="struct_id" ref="payroll_structure_bgld.structure_gembg_bgld"/> <field name="struct_id" ref="structure_gembg_bgld"/>
<field name="condition_select">python</field> <field name="condition_select">python</field>
<field name="condition_python">result = 'GEMBG_SONNFEI' in worked_days and worked_days['GEMBG_SONNFEI'].number_of_hours > 0</field> <field name="condition_python">result = 'GEMBG_SONNFEI' in worked_days and worked_days['GEMBG_SONNFEI'].number_of_hours > 0</field>
<field name="amount_select">code</field> <field name="amount_select">code</field>
@@ -174,8 +175,8 @@ else:
<field name="name">Bereitschaftsentschädigung (§ 80 GemBG)</field> <field name="name">Bereitschaftsentschädigung (§ 80 GemBG)</field>
<field name="code">BERB</field> <field name="code">BERB</field>
<field name="sequence" eval="14"/> <field name="sequence" eval="14"/>
<field name="category_id" ref="payroll_structure_bgld.category_gembg_brutto"/> <field name="category_id" ref="category_gembg_brutto"/>
<field name="struct_id" ref="payroll_structure_bgld.structure_gembg_bgld"/> <field name="struct_id" ref="structure_gembg_bgld"/>
<field name="condition_select">python</field> <field name="condition_select">python</field>
<field name="condition_python">result = 'GEMBG_BEREIT' in worked_days and worked_days['GEMBG_BEREIT'].number_of_hours > 0</field> <field name="condition_python">result = 'GEMBG_BEREIT' in worked_days and worked_days['GEMBG_BEREIT'].number_of_hours > 0</field>
<field name="amount_select">code</field> <field name="amount_select">code</field>
@@ -195,8 +196,8 @@ result = stunden * satz
<field name="name">Erschwerniszulage (§ 83 GemBG)</field> <field name="name">Erschwerniszulage (§ 83 GemBG)</field>
<field name="code">ERZU</field> <field name="code">ERZU</field>
<field name="sequence" eval="15"/> <field name="sequence" eval="15"/>
<field name="category_id" ref="payroll_structure_bgld.category_gembg_brutto"/> <field name="category_id" ref="category_gembg_brutto"/>
<field name="struct_id" ref="payroll_structure_bgld.structure_gembg_bgld"/> <field name="struct_id" ref="structure_gembg_bgld"/>
<field name="condition_select">python</field> <field name="condition_select">python</field>
<field name="condition_python">result = 'ERZU_INPUT' in inputs and inputs['ERZU_INPUT'].amount > 0</field> <field name="condition_python">result = 'ERZU_INPUT' in inputs and inputs['ERZU_INPUT'].amount > 0</field>
<field name="amount_select">code</field> <field name="amount_select">code</field>
@@ -212,8 +213,8 @@ result = inputs['ERZU_INPUT'].amount
<field name="name">Jubiläumszuwendung (§ 89 GemBG)</field> <field name="name">Jubiläumszuwendung (§ 89 GemBG)</field>
<field name="code">JUB</field> <field name="code">JUB</field>
<field name="sequence" eval="16"/> <field name="sequence" eval="16"/>
<field name="category_id" ref="payroll_structure_bgld.category_gembg_brutto"/> <field name="category_id" ref="category_gembg_brutto"/>
<field name="struct_id" ref="payroll_structure_bgld.structure_gembg_bgld"/> <field name="struct_id" ref="structure_gembg_bgld"/>
<field name="condition_select">python</field> <field name="condition_select">python</field>
<field name="condition_python">result = 'JUB_INPUT' in inputs and inputs['JUB_INPUT'].amount > 0</field> <field name="condition_python">result = 'JUB_INPUT' in inputs and inputs['JUB_INPUT'].amount > 0</field>
<field name="amount_select">code</field> <field name="amount_select">code</field>
@@ -229,8 +230,8 @@ result = inputs['JUB_INPUT'].amount
<field name="name">Fahrtkostenzuschuss (§ 88 GemBG)</field> <field name="name">Fahrtkostenzuschuss (§ 88 GemBG)</field>
<field name="code">FKZ</field> <field name="code">FKZ</field>
<field name="sequence" eval="17"/> <field name="sequence" eval="17"/>
<field name="category_id" ref="payroll_structure_bgld.category_gembg_brutto"/> <field name="category_id" ref="category_gembg_brutto"/>
<field name="struct_id" ref="payroll_structure_bgld.structure_gembg_bgld"/> <field name="struct_id" ref="structure_gembg_bgld"/>
<field name="condition_select">python</field> <field name="condition_select">python</field>
<field name="condition_python">result = 'FKZ_INPUT' in inputs and inputs['FKZ_INPUT'].amount > 0</field> <field name="condition_python">result = 'FKZ_INPUT' in inputs and inputs['FKZ_INPUT'].amount > 0</field>
<field name="amount_select">code</field> <field name="amount_select">code</field>
@@ -246,8 +247,8 @@ result = inputs['FKZ_INPUT'].amount
<field name="name">Sonderzahlung (quartalsweise, § 133n GemBG)</field> <field name="name">Sonderzahlung (quartalsweise, § 133n GemBG)</field>
<field name="code">SZQ</field> <field name="code">SZQ</field>
<field name="sequence" eval="30"/> <field name="sequence" eval="30"/>
<field name="category_id" ref="payroll_structure_bgld.category_gembg_brutto"/> <field name="category_id" ref="category_gembg_brutto"/>
<field name="struct_id" ref="payroll_structure_bgld.structure_gembg_bgld"/> <field name="struct_id" ref="structure_gembg_bgld"/>
<field name="condition_select">python</field> <field name="condition_select">python</field>
<field name="condition_python"><![CDATA[ <field name="condition_python"><![CDATA[
# § 133n GemBG: 50 % des Monatsentgelts (und der Kinderzulage) # § 133n GemBG: 50 % des Monatsentgelts (und der Kinderzulage)
@@ -255,7 +256,6 @@ result = inputs['FKZ_INPUT'].amount
# Quartalsende (März, Juni, September, Dezember). # Quartalsende (März, Juni, September, Dezember).
# Der Monat der Auszahlung ist der Monat, für den das Entgelt # Der Monat der Auszahlung ist der Monat, für den das Entgelt
# gebührt (§ 56 Abs 2 GemBG: 15.3., 15.6., 15.9., 15.12.). # gebührt (§ 56 Abs 2 GemBG: 15.3., 15.6., 15.9., 15.12.).
import datetime
month = payslip.date_to.month month = payslip.date_to.month
result = month in (3, 6, 9, 12) result = month in (3, 6, 9, 12)
]]></field> ]]></field>
@@ -266,8 +266,9 @@ result = month in (3, 6, 9, 12)
# Kinderzulage), die ihnen für den Monat der Auszahlung zustehen"). # Kinderzulage), die ihnen für den Monat der Auszahlung zustehen").
# Bei unterbrochenem Vierteljahr: nur der entsprechende Teil # Bei unterbrochenem Vierteljahr: nur der entsprechende Teil
# (R1: Vollzeit-Rechnung; Anteil folgt mit der Zeit-Erfassung). # (R1: Vollzeit-Rechnung; Anteil folgt mit der Zeit-Erfassung).
me_total = rules['ME'].total or 0 # Odoo 19 befült result_rules (nicht rules) mit den Regelsummen.
kizu_total = rules['KIZU'].total or 0 me_total = result_rules['ME']['total'] or 0
kizu_total = result_rules['KIZU']['total'] or 0
result = (me_total + kizu_total) * 0.5 result = (me_total + kizu_total) * 0.5
]]></field> ]]></field>
</record> </record>
@@ -279,12 +280,12 @@ result = (me_total + kizu_total) * 0.5
<field name="name">Netto</field> <field name="name">Netto</field>
<field name="code">NETTO</field> <field name="code">NETTO</field>
<field name="sequence" eval="100"/> <field name="sequence" eval="100"/>
<field name="category_id" ref="payroll_structure_bgld.category_gembg_netto"/> <field name="category_id" ref="category_gembg_netto"/>
<field name="struct_id" ref="payroll_structure_bgld.structure_gembg_bgld"/> <field name="struct_id" ref="structure_gembg_bgld"/>
<field name="condition_select">none</field> <field name="condition_select">none</field>
<field name="amount_select">code</field> <field name="amount_select">code</field>
<field name="amount_python_compute"><![CDATA[ <field name="amount_python_compute"><![CDATA[
result = categories.GEMBG_BRUTTO - categories.GEMBG_ABZUG result = categories['GEMBG_BRUTTO'] - categories['GEMBG_ABZUG']
]]></field> ]]></field>
</record> </record>
</odoo> </odoo>
@@ -24,12 +24,55 @@ class HrVersion(models.Model):
besoldungsdienstalter = fields.Date( besoldungsdienstalter = fields.Date(
string='Besoldungsdienstalter (ab)', string='Besoldungsdienstalter (ab)',
groups="hr_payroll.group_hr_payroll_user", tracking=True) groups="hr_payroll.group_hr_payroll_user", tracking=True)
vordienstzeiten_monate = fields.Integer(
string='Anerkannte Vordienstzeiten (Monate)',
groups="hr_payroll.group_hr_payroll_user", tracking=True,
help='AP9 — Bgld. GemBG § 67: Das Besoldungsdienstalter umfasst '
'die im Dienstverhältnis verbrachten wirksamen Zeiten '
'ZUSAMMEN mit den anrechenbaren Vordienstzeiten '
'(Abs 1). Anzurechnen sind u. a. Zeiten in einem '
'Dienstverhältnis zu einer Gebietskörperschaft/einem '
'Gemeindeverband (EWR/Türkei/Schweiz) sowie EU- und '
'zwischenstaatliche Dienstverhältnisse (Abs 2 Z 1 f); '
'Grundwehr-/Ausbildungsdienst zusammen max. 6 Monate, '
'Zivil-/Ersatzdienst max. 9 Monate (Abs 2 Z 4); über '
'die Pflichtzeiten hinaus einschlägige Berufstätigkeit '
'oder ein Verwaltungspraktikum max. 10 Jahre (Abs 3). '
'Doppelanrechnung desselben Zeitraums unzulässig. '
'Hier: die vom Bescheid anerkannten Monate gesamt.')
bda_vorschlag = fields.Date(
string='BDA-Vorschlag (Vordienstzeiten)',
compute='_compute_bda_vorschlag',
groups="hr_payroll.group_hr_payroll_user",
help='Vorschlag: Vertragsbeginn zurückverlegt um die '
'anerkannten Vordienstzeiten (§ 67 Abs 1). Übernahme '
'bewusst manuell — Besoldungsdienstalter ist ein '
'dokumentationspflichtiger Verwaltungsakt (s. Vorrückungs-'
'Wizard).')
vorrueckungstermin = fields.Date( vorrueckungstermin = fields.Date(
compute='_compute_vorrueckungstermin', store=True, readonly=False, compute='_compute_vorrueckungstermin', store=True, readonly=False,
groups="hr_payroll.group_hr_payroll_user", tracking=True, groups="hr_payroll.group_hr_payroll_user", tracking=True,
help="Vorschlag nach § 66 Abs 2 GemBG (BDA + Vorrückungszeitraum); " help="Vorschlag nach § 66 Abs 2 GemBG (BDA + Vorrückungszeitraum); "
"bei Überleitungsfällen manuell korrigierbar.") "bei Überleitungsfällen manuell korrigierbar.")
@api.depends('contract_date_start', 'vordienstzeiten_monate')
def _compute_bda_vorschlag(self):
for version in self:
if not (version.contract_date_start
and version.vordienstzeiten_monate):
version.bda_vorschlag = False
continue
version.bda_vorschlag = version.contract_date_start \
- relativedelta(months=version.vordienstzeiten_monate)
def action_bda_uebernehmen(self):
"""BDA-Vorschlag als Besoldungsdienstalter übernehmen — ein
expliziter Verwaltungsakt, kein stilles Schreiben im Lohnlauf
(Muster: Vorrückungs-Wizard § 66 Abs 2)."""
for version in self:
if version.bda_vorschlag:
version.besoldungsdienstalter = version.bda_vorschlag
@api.depends('entlohnungsgruppe_id', 'besoldungsdienstalter') @api.depends('entlohnungsgruppe_id', 'besoldungsdienstalter')
def _compute_vorrueckungstermin(self): def _compute_vorrueckungstermin(self):
for version in self: for version in self:
@@ -67,6 +110,16 @@ class HrEmployee(models.Model):
besoldungsdienstalter = fields.Date( besoldungsdienstalter = fields.Date(
related="version_id.besoldungsdienstalter", inherited=True, related="version_id.besoldungsdienstalter", inherited=True,
readonly=False, groups="hr_payroll.group_hr_payroll_user") readonly=False, groups="hr_payroll.group_hr_payroll_user")
vordienstzeiten_monate = fields.Integer(
related="version_id.vordienstzeiten_monate", inherited=True,
readonly=False, groups="hr_payroll.group_hr_payroll_user")
bda_vorschlag = fields.Date(
related="version_id.bda_vorschlag", inherited=True,
groups="hr_payroll.group_hr_payroll_user")
vorrueckungstermin = fields.Date( vorrueckungstermin = fields.Date(
related="version_id.vorrueckungstermin", inherited=True, related="version_id.vorrueckungstermin", inherited=True,
readonly=False, groups="hr_payroll.group_hr_payroll_user") readonly=False, groups="hr_payroll.group_hr_payroll_user")
def action_bda_uebernehmen(self):
"""Delegation an die Version (Button am Mitarbeiterformular)."""
self.mapped('version_id').action_bda_uebernehmen()
@@ -44,6 +44,7 @@ class EntgeltTest(TransactionCase):
slip_month = month or 7 slip_month = month or 7
slip = self.env['hr.payslip'].create({ slip = self.env['hr.payslip'].create({
'name': 'GemBG-Test %d/2026' % slip_month,
'employee_id': employee.id, 'employee_id': employee.id,
'struct_id': self.structure.id, 'struct_id': self.structure.id,
'date_from': date(2026, slip_month, 1), 'date_from': date(2026, slip_month, 1),
@@ -1,6 +1,8 @@
# Part of the gem360 onboarding. See LICENSE / project docs for details. # Part of the gem360 onboarding. See LICENSE / project docs for details.
from datetime import date from datetime import date
from freezegun import freeze_time
from odoo.tests import TransactionCase, tagged from odoo.tests import TransactionCase, tagged
@@ -53,9 +55,14 @@ class KatalogTest(TransactionCase):
if not version: if not version:
version = self.env['hr.version'].create({ version = self.env['hr.version'].create({
'employee_id': employee.id, 'employee_id': employee.id,
'date_start': date(2020, 1, 1), 'contract_date_start': date(2020, 1, 1),
}) })
version.write({ version.write({
# contract_date_start ins Suchfenster des Vorrückungs-
# Wizards rücken — hr.version.date_start ist ein nicht-
# gespeicherter Compute daraus (nie gelaufener Test,
# Odoo-19-Latenzkorrektur).
'contract_date_start': date(2014, 1, 1),
'entlohnungsgruppe_id': 'entlohnungsgruppe_id':
self.env.ref('l10n_at_gemeinde_payroll.bgld_grp_av_av1').id, self.env.ref('l10n_at_gemeinde_payroll.bgld_grp_av_av1').id,
'entlohnungsstufe': stufe, 'entlohnungsstufe': stufe,
@@ -76,10 +83,13 @@ class KatalogTest(TransactionCase):
"""Wizard erhöht die Stufe fälliger Versionen und überspringt """Wizard erhöht die Stufe fälliger Versionen und überspringt
die Höchststufe.""" die Höchststufe."""
version = self._version_mit_katalog(date(2014, 3, 15), stufe=3) version = self._version_mit_katalog(date(2014, 3, 15), stufe=3)
version.vorrueckungstermin = date(2024, 4, 1) # Stichtag ab Katalog-Gültigkeit (Seed 2026-07-01) — vor dem
max_stufe = version.entlohnungsgruppe_id._max_stufe(date(2024, 4, 1)) # 1. 7. 2026 gibt es keine Katalogwerte, der Wizard überspringt
# korrekt (nie gelaufener Test, Odoo-19-Latenzkorrektur).
version.vorrueckungstermin = date(2028, 4, 1)
max_stufe = version.entlohnungsgruppe_id._max_stufe(date(2028, 4, 1))
wizard = self.env['gemeinde.payroll.vorrueckung.wizard'].create({ wizard = self.env['gemeinde.payroll.vorrueckung.wizard'].create({
'stichtag': date(2024, 4, 1), 'stichtag': date(2028, 4, 1),
}) })
self.assertIn(version, wizard.version_ids) self.assertIn(version, wizard.version_ids)
wizard.action_apply() wizard.action_apply()
@@ -87,9 +97,29 @@ class KatalogTest(TransactionCase):
version_top = self._version_mit_katalog(date(2014, 3, 15), version_top = self._version_mit_katalog(date(2014, 3, 15),
stufe=max_stufe) stufe=max_stufe)
version_top.vorrueckungstermin = date(2024, 4, 1) version_top.vorrueckungstermin = date(2028, 4, 1)
wizard2 = self.env['gemeinde.payroll.vorrueckung.wizard'].create({ wizard2 = self.env['gemeinde.payroll.vorrueckung.wizard'].create({
'stichtag': date(2024, 4, 1), 'stichtag': date(2028, 4, 1),
}) })
wizard2.action_apply() wizard2.action_apply()
self.assertEqual(version_top.entlohnungsstufe, max_stufe) self.assertEqual(version_top.entlohnungsstufe, max_stufe)
def test_06_bda_vorschlag_aus_vordienstzeiten(self):
"""AP9 — § 67 Abs 1 GemBG: BDA-Vorschlag = Vertragsbeginn
zurückverlegt um die anerkannten Vordienstzeiten; Übernahme
setzt das BDA und den Vorrückungstermin neu (§ 66 Abs 2)."""
version = self._version_mit_katalog(date(2014, 3, 15))
version.write({'contract_date_start': date(2026, 7, 1)})
self.assertFalse(version.bda_vorschlag)
version.vordienstzeiten_monate = 48
self.assertEqual(version.bda_vorschlag, date(2022, 7, 1))
# Ohne Übernahme bleibt das gepflegte BDA (Setup) unangetastet.
self.assertEqual(version.besoldungsdienstalter, date(2014, 3, 15))
with freeze_time('2026-09-12'):
version.action_bda_uebernehmen()
self.assertEqual(version.besoldungsdienstalter, date(2022, 7, 1))
# Vorrückungstermin nach § 66 Abs 2: Die 4-Jahres-Vollendung
# (1. 7. 2026) ist am 12. 9. 2026 bereits verstrichen → der
# Termin der NÄCHSTFÄLLIGEN Vorrückung: BDA + 8 Jahre =
# 1. 7. 2030 → 1. 8. 2030.
self.assertEqual(version.vorrueckungstermin, date(2030, 8, 1))
@@ -10,8 +10,15 @@
groups="hr_payroll.group_hr_payroll_user"> groups="hr_payroll.group_hr_payroll_user">
<field name="entlohnungsgruppe_id"/> <field name="entlohnungsgruppe_id"/>
<field name="entlohnungsstufe" invisible="not entlohnungsgruppe_id"/> <field name="entlohnungsstufe" invisible="not entlohnungsgruppe_id"/>
<field name="vordienstzeiten_monate" invisible="not entlohnungsgruppe_id"/>
<field name="bda_vorschlag" invisible="not entlohnungsgruppe_id"/>
<field name="besoldungsdienstalter" invisible="not entlohnungsgruppe_id"/> <field name="besoldungsdienstalter" invisible="not entlohnungsgruppe_id"/>
<field name="vorrueckungstermin" invisible="not entlohnungsgruppe_id"/> <field name="vorrueckungstermin" invisible="not entlohnungsgruppe_id"/>
<button name="action_bda_uebernehmen"
string="BDA-Vorschlag übernehmen" type="object"
class="btn-secondary"
invisible="not entlohnungsgruppe_id or not bda_vorschlag"
groups="hr_payroll.group_hr_payroll_manager"/>
</group> </group>
<group name="gembg_zulagen" string="GemBG Zulagen" <group name="gembg_zulagen" string="GemBG Zulagen"
groups="hr_payroll.group_hr_payroll_user"> groups="hr_payroll.group_hr_payroll_user">
+40 -3
View File
@@ -1,8 +1,8 @@
# Part of the odoo-at-payroll project. License: LGPL-3. # Part of the odoo-at-payroll project. License: LGPL-3.
{ {
'name': 'Österreich - Personalverrechnung Kern (AT Payroll Core)', 'name': 'Österreich - Personalverrechnung Kern (AT Payroll Core)',
'version': '19.0.3.1.0', 'version': '19.0.10.0.0',
'author': 'ixsol', 'author': 'Florian Egger',
'website': 'https://gem360.at', 'website': 'https://gem360.at',
'category': 'Human Resources/Payroll', 'category': 'Human Resources/Payroll',
'description': """ 'description': """
@@ -11,7 +11,7 @@
Sektor-neutraler Bundes-Kern der österreichischen Personalverrechnung Sektor-neutraler Bundes-Kern der österreichischen Personalverrechnung
auf der Odoo-19 ``hr_payroll``-Engine. Wird konsumiert von auf der Odoo-19 ``hr_payroll``-Engine. Wird konsumiert von
``l10n_at_gemeinde_payroll`` (Bgld. GemBG) und später vom General-AT- ``l10n_at_gemeinde_payroll`` (Bgld. GemBG) und vom General-AT-
Produkt (``l10n_at_hr_payroll_private``). Grundlagen: ``personalverrechnung/`` Produkt (``l10n_at_hr_payroll_private``). Grundlagen: ``personalverrechnung/``
(PLAN-payroll-bgld.md, RECHTSQUELLEN-Bgld.md, PLAN-repo-split.md). (PLAN-payroll-bgld.md, RECHTSQUELLEN-Bgld.md, PLAN-repo-split.md).
@@ -32,8 +32,33 @@ Produkt (``l10n_at_hr_payroll_private``). Grundlagen: ``personalverrechnung/``
``rule_parameters_sv_2026.xml``; BV-Beitrag Abfertigung Neu ``rule_parameters_sv_2026.xml``; BV-Beitrag Abfertigung Neu
(BMSVG § 6 Abs 1, ``at_sv_bv_dg``) als Parameter für das (BMSVG § 6 Abs 1, ``at_sv_bv_dg``) als Parameter für das
General-AT-Modul (GP5); General-AT-Modul (GP5);
- **Meldewesen/Belege (AP5/AP6, GP7):** ELDA-Fixsatz-Framework
(``models/elda.py`` — Vorlauf/Schluss, SANR/SANZ, Cent-Formatter,
ISO-8859-15), **Lohnzettel Finanz L16 + Arbeitsstättenmeldung**
(``models/lohnzettel_l16.py`` — I1/L1 nach DM-ORG 42.7.0,
Lohnzettelversion 28, KZ-245-Formel-Prüfung, BEST LF/94 bzw. AD/ST),
Kind-Datenmodell für die FB+-Wartung (``models/kind.py``),
Meldewesen-Stammdaten (BKNR/Lohnsteuernummer, GKZ am Partner,
VSNR-Formatprüfung, eSV-Abmeldegrund-Mapping,
``models/meldewesen.py``), Belege Lohnzettel AT/Monatsdienstliste/
Monatsjournal (QWeb, ``models/monatsreports.py``);
- **GP8:** Fristenkalender (``models/fristenkalender.py`` — Cron,
Auto-Erzeugung, Sprung zur GP7-Deklaration), Jahres-Retro-/
Aufrollungslauf (``models/jahresretro.py`` — Kontrollsechstel
§ 67 Abs 10, §-77-Abs-3/4-Option), Mitteilungsfile-Import
(``models/mitteilungsfile.py`` — ELDA-Rückantwort-XML),
2027-Parameter-Struktur (nur Rahmen);
- deklarative TASY-Mapping-Tabelle und Import-Wizard (Preview, - deklarative TASY-Mapping-Tabelle und Import-Wizard (Preview,
Sprungwarnung > 10 %, explizite Bestätigung, versionierte Werte); Sprungwarnung > 10 %, explizite Bestätigung, versionierte Werte);
- **Meldewesen/Belege (AP5/AP6, GP7):** ELDA-Fixsatz-Framework
(``models/elda.py``), Lohnzettel Finanz L16 + Arbeitsstättenmeldung
(``models/lohnzettel_l16.py``, DM-ORG 42.7.0, PDF-verifizierte
Satzstrukturen), Kind-Datenmodell für die FB+-Wartung
(``models/kind.py`` — Nutzer-Entscheidung: FB+ rechnet NICHT im
Lohnsteuerkern, Arbeitnehmerveranlagung), Meldewesen-Stammdaten
(BKNR/Lohnsteuernummer, GKZ am Partner, VSNR-Formatprüfung, eSV-
Abmeldegrund-Mapping), Belege Lohnzettel AT/Monatsdienstliste/
Monatsjournal als QWeb (``report/belege_reports.xml``);
- folgen nach Freigabe der Bgld.-APs: Meldewesen (L16/FinanzOnline, - folgen nach Freigabe der Bgld.-APs: Meldewesen (L16/FinanzOnline,
eSV/ELDA, AP5), Belege (AP6). eSV/ELDA, AP5), Belege (AP6).
@@ -43,11 +68,23 @@ Kein VRV-Bezug: die Gemeinde-Buchhaltung (Anlage 3b, Ansatz/MVAG) läuft
'depends': ['hr_payroll'], 'depends': ['hr_payroll'],
'data': [ 'data': [
'security/ir.model.access.csv', 'security/ir.model.access.csv',
'security/security_rules.xml',
'data/rule_parameters_2026.xml', 'data/rule_parameters_2026.xml',
'data/rule_parameters_lst_2026.xml', 'data/rule_parameters_lst_2026.xml',
'data/rule_parameters_sv_2026.xml', 'data/rule_parameters_sv_2026.xml',
'data/rule_parameters_2027.xml',
'data/rule_parameters_109.xml',
'data/tasy_mapping.xml', 'data/tasy_mapping.xml',
'data/hr_departure_reason_seed.xml',
'views/tasy_views.xml', 'views/tasy_views.xml',
'views/tasy_katalog_views.xml',
'views/company_views.xml',
'views/meldewesen_views.xml',
'views/meldewesen_finanz_views.xml',
'views/schwerarbeitsmeldung_views.xml',
'views/fristen_views.xml',
'report/belege_reports.xml',
'report/jahresreports.xml',
], ],
'countries': ['at'], 'countries': ['at'],
'license': 'LGPL-3', 'license': 'LGPL-3',
@@ -0,0 +1,68 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo noupdate="1">
<!-- ============================================================ -->
<!-- AP1: eSV-Abmeldegründe (AGRD, DM-ORG D.22) an den -->
<!-- Abgangsursachen. -->
<!-- -->
<!-- Die Odoo-Standardursachen (hr.departure_fired/resigned/ -->
<!-- retired) werden vorverbunden statt dupliziert; die übrigen -->
<!-- AT-typischen Ursachen kommen hinzu. noupdate: mandantenseitig -->
<!-- geänderte oder ergänzte Ursachen bleiben unberührt. -->
<!-- ============================================================ -->
<record id="hr.departure_fired" model="hr.departure.reason">
<field name="l10n_at_agrd">01</field>
</record>
<record id="hr.departure_resigned" model="hr.departure.reason">
<field name="l10n_at_agrd">02</field>
</record>
<record id="hr.departure_retired" model="hr.departure.reason">
<field name="l10n_at_agrd">16</field>
</record>
<record id="departure_reason_einvernehmlich"
model="hr.departure.reason">
<field name="name">Einvernehmliche Lösung</field>
<field name="country_id" ref="base.at"/>
<field name="sequence">40</field>
<field name="l10n_at_agrd">03</field>
</record>
<record id="departure_reason_zeitablauf" model="hr.departure.reason">
<field name="name">Zeitablauf</field>
<field name="country_id" ref="base.at"/>
<field name="sequence">41</field>
<field name="l10n_at_agrd">04</field>
</record>
<record id="departure_reason_berechtigter_austritt"
model="hr.departure.reason">
<field name="name">Berechtigter vorzeitiger Austritt</field>
<field name="country_id" ref="base.at"/>
<field name="sequence">42</field>
<field name="l10n_at_agrd">05</field>
</record>
<record id="departure_reason_fristlose" model="hr.departure.reason">
<field name="name">Fristlose Entlassung</field>
<field name="country_id" ref="base.at"/>
<field name="sequence">43</field>
<field name="l10n_at_agrd">06</field>
</record>
<record id="departure_reason_karenz" model="hr.departure.reason">
<field name="name">Karenz nach MSchG / VKG</field>
<field name="country_id" ref="base.at"/>
<field name="sequence">44</field>
<field name="l10n_at_agrd">07</field>
</record>
<record id="departure_reason_tod" model="hr.departure.reason">
<field name="name">Tod des/der Dienstnehmers:in</field>
<field name="country_id" ref="base.at"/>
<field name="sequence">45</field>
<field name="l10n_at_agrd">13</field>
</record>
<record id="departure_reason_sonstiges_ende"
model="hr.departure.reason">
<field name="name">Sonstiger Grund mit Ende des Dienstverhältnisses</field>
<field name="country_id" ref="base.at"/>
<field name="sequence">46</field>
<field name="l10n_at_agrd">00</field>
</record>
</odoo>
@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- ============================================================ -->
<!-- AP17: Schwellen der Finanz-Mitteilungen (§ 109a/§ 109b -->
<!-- EStG). § 109a-VO: 450 € je Leistung / 900 € kumulativ je -->
<!-- Leistungserbringer (Fristenkalender/§-109a-VO). § 109b -->
<!-- Abs 4 lit a: 100.000 € je Leistungserbringer und -->
<!-- Kalenderjahr (RIS NOR40263383, verifiziert 2026-09-16). -->
<!-- ============================================================ -->
<record id="hr_rule_parameter_at_m109_schwellen" model="hr.rule.parameter">
<field name="name">AT § 109a/§ 109b EStG Schwellen</field>
<field name="code">at_m109_schwellen</field>
<field name="country_id" ref="base.at"/>
</record>
<record id="hr_rule_parameter_value_at_m109_schwellen" model="hr.rule.parameter.value">
<field name="rule_parameter_id" ref="hr_rule_parameter_at_m109_schwellen"/>
<field name="date_from">2026-01-01</field>
<field name="parameter_value">{'m109a_leistung': 450.0, 'm109a_kumulativ': 900.0, 'm109b_jahr': 100000.0}</field>
</record>
</odoo>
@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- GP8: 2027-Parameter-Platzhalter (NUR Struktur, keine Werte!). -->
<!-- Der Wartungszyklus (RUNBOOK) beschafft die offiziellen 2027- -->
<!-- Werte (SV-Werte PDF, Lohnsteuer-Tarif, KV-Gehaltstabellen) -->
<!-- und pflegt diese Zeilen nach — GP8 liefert nur den Rahmen. -->
<!-- KEINE Werte aus Trainingswissen! -->
<record id="hr_rule_parameter_value_at_sv_wf_vektor_2027" model="hr.rule.parameter.value">
<field name="rule_parameter_id" ref="hr_rule_parameter_at_sv_wf_vektor"/>
<field name="date_from">2027-01-01</field>
<field name="parameter_value">{'default': {'dn': 0.5, 'dg': 0.5}}</field>
</record>
<!-- AV-Staffel 2027: § 2a AMPFG aufgehoben (BGBl I 62/2026) → -->
<!-- tiers=[] (voller DN-Satz) — das ist bereits 2026 verifiziert -->
<!-- und kein Trainingswert. -->
<record id="hr_rule_parameter_value_at_sv_av_staffel_2027" model="hr.rule.parameter.value">
<field name="rule_parameter_id" ref="hr_rule_parameter_at_sv_av_staffel_ampfg"/>
<field name="date_from">2027-01-01</field>
<field name="parameter_value">{'tiers': [], 'dn_voll': 2.95}</field>
</record>
</odoo>
@@ -103,4 +103,21 @@
<field name="date_from">2027-01-01</field> <field name="date_from">2027-01-01</field>
<field name="parameter_value">{'stunden': 10.0, 'betrag': 120.0}</field> <field name="parameter_value">{'stunden': 10.0, 'betrag': 120.0}</field>
</record> </record>
<!-- Steuerfreie Sachbezüge § 3/§ 26 EStG (Freibeträge, Jahres- -->
<!-- Kumulative je DN; lb-sac-17/07/02, lb-ges-02). Mahlzeiten -->
<!-- 8 €/AT (Z 17), Mitarbeiterrabatte 1.000 €/J (Z 21), -->
<!-- Betriebsveranstaltungen 365 €/J (Z 14), Sachzuwendungen/ -->
<!-- Jubiläum/Geschenke 186 €/J (Z 14), Homeoffice 3 €/Tag max 100 -->
<!-- Tage/J (Z 9). Grenzen als Jahresversionen führen. -->
<record id="hr_rule_parameter_at_st_grenzen" model="hr.rule.parameter">
<field name="name">Steuerfreie Sachbezüge § 3/§ 26 EStG (Grenzen)</field>
<field name="code">at_st_grenzen</field>
<field name="country_id" ref="base.at"/>
</record>
<record id="hr_rule_parameter_value_at_st_grenzen" model="hr.rule.parameter.value">
<field name="rule_parameter_id" ref="hr_rule_parameter_at_st_grenzen"/>
<field name="date_from">2026-01-01</field>
<field name="parameter_value">{'mahlzeit_tag': 8.0, 'rabatt_jahr': 1000.0, 'betriebsveranstaltung_jahr': 365.0, 'jubilaeum_jahr': 186.0, 'homeoffice_tag': 3.0, 'homeoffice_tage': 100}</field>
</record>
</odoo> </odoo>
+12 -1
View File
@@ -1,5 +1,16 @@
# Part of the odoo-at-payroll project. License: LGPL-3. # Part of the odoo-at-payroll project. License: LGPL-3.
from . import tasy from . import tasy
from . import tasy_katalog
from . import lohnsteuer from . import lohnsteuer
from . import sozialversicherung from . import sozialversicherung
from . import sozialversicherung from . import elda
from . import meldewesen
from . import kind
from . import lohnzettel_l16
from . import lohnzettel_l17
from . import mitteilung_109
from . import schwerarbeitsmeldung
from . import fristenkalender
from . import jahresretro
from . import mitteilungsfile
from . import monatsreports
+256
View File
@@ -0,0 +1,256 @@
# Part of the odoo-at-payroll project. License: LGPL-3.
"""ELDA-Fixsatz-Framework (GP7/AP5) — „Datenaustausch mit Dienstgebern"
(DM-ORG 42.7.0, 07/2026; lokal `.firecrawl/ris/elda/`, unversioniert).
Dateikonventionen (DM-ORG Kapitel C.1/C.2, E.1E.3 verifiziert 2026-09-12):
- Datei = optionaler Dateiheader (DHKZ 2×blank + DHVN 01 + ENCD) + mindestens
ein Datenbestand; Datenbestand = Vorlaufsatz (SART 00, Feld VERS = Version
der nachfolgenden Satzstrukturen) + Datensätze + Schlusssatz (SART 99,
SANZ = Satzanzahl inkl. Vorlauf und Schluss).
- Fixlängen-Sätze ohne Feldtrennzeichen; alle Sätze eines Bestands nehmen
die maximale Satzlänge des Bestands an (RESE-Auffüllung). Sätze werden
zeilenweise mit CRLF getrennt (die DM-ORG normiert KEIN explizites
Satztrennzeichen CRLF ist die Praxis der ELDA-Kanäle; Annahme im
Parallellauf gegen den Mandanten-ELDA-Zugang verifizieren).
- ``a``/``a/n``: linksbündig, Grundstellung blank; ``n``: rechtsbündig,
Grundstellung 0, führende Nullen, KEINE Interpunktion (Beträge als
EURO-Cent, kaufmännisch gerundet; Prozentsätze mit impliziten
Nachkommastellen via ``skala``).
- Encoding Fixlängen-Dateien: ISO-8859-15 (Standard-Encoding; der
Dateiheader deklariert ENCD). Vorzeichen gehören in eigene V-Felder
(RSVZ/Vxxx), ``n``-Felder sind nie negativ.
"""
from datetime import date, datetime
from decimal import ROUND_HALF_UP, Decimal
L10N_AT_ELDA_ENCODING = 'ISO-8859-15'
L10N_AT_ELDA_ZEILENTRENNER = '\r\n'
# Identifikationsteil (E.1, S. 174) — 20 Stellen, alle Satzarten gleich;
# alle Angaben zwingend.
L10N_AT_ELDA_IDTEIL = [
(1, 'SART', 1, 2, 'a'), # Satzart
(2, 'SANR', 3, 7, 'n'), # laufende Satznummer je Bestand, ab 1
(3, 'UVST', 10, 2, 'a'), # datenübernehmender Träger (ED = ELDA)
(4, 'OBUS', 12, 7, 'n'), # Ordnungsbegriff der übermittelnden Stelle
(5, 'VSTR', 19, 2, 'a'), # zuständiger Träger (D.4)
]
# Vorlaufsatz-Datenteil (E.2, S. 175177, Version 08) — folgt dem IDTEIL;
# zwingend außer HTEL/SOID/VNMF; Satzlänge = maximale Satzlänge des Bestands.
L10N_AT_ELDA_VORLAUF = [
(2, 'PROJ', 21, 2, 'a'), # DM; TM = Testdaten
(3, 'BEST', 23, 2, 'a'), # Verarbeitung (MB/VR/LF/AD/MA …, E.2)
(4, 'DTNR', 25, 6, 'a'), # Datenträgernummer (TP-Übermittlung: frei)
(5, 'EDAT', 31, 8, 'n'), # TTMMJJJJ
(6, 'EZEI', 39, 6, 'n'), # HHMMSS
(7, 'HRST', 45, 45, 'a'), # Herstellername
(8, 'HKFZ', 90, 3, 'a'), # Internationales Kfz-Kennzeichen (A)
(9, 'HPLZ', 93, 7, 'a'),
(10, 'HORT', 100, 20, 'a'),
(11, 'HSTR', 120, 30, 'a'),
(12, 'VERS', 150, 2, 'n'), # Version der Satzstrukturen (D.26)
(13, 'HTEL', 152, 20, 'a'),
(14, 'SOID', 172, 70, 'a'),
(15, 'VNMF', 242, 5, 'a'), # Mitteilungsfile-Version
]
# Schlusssatz-Datenteil (E.3, S. 178, Version 01) — nach IDTEIL;
# SANZ zwingend, ELNR/HEMA optional.
L10N_AT_ELDA_SCHLUSS = [
(2, 'SANZ', 21, 6, 'n'), # Satzanzahl inkl. Vorlauf- und Schlusssatz
(3, 'ELNR', 27, 6, 'n'),
(4, 'HEMA', 33, 60, 'a'),
]
def l10n_at_euro_cent(betrag):
"""EURO-Betrag → Cent (kaufmännisch gerundet; DM-ORG: keine
Interpunktion)."""
return int((Decimal(str(betrag)) * 100).quantize(
Decimal('1'), rounding=ROUND_HALF_UP))
class L10nAtEldaSatz:
"""Ein Fixlängen-Satz: Felddaten (Nr, Name, Pos, Länge, Typ), zwingende
Grundstellung je Typ, Rendering auf die Bestands-Satzlänge. Kein ORM
reine Bau-Hilfe der Meldungsgeneratoren (DmfA-Muster: Datensatz-Baum im
Python-Teil, Validierung/Prüfprotokoll darüber)."""
def __init__(self, struktur, satzlaenge, bezeichnung=''):
self.struktur = list(struktur)
self.satzlaenge = satzlaenge
self.bezeichnung = bezeichnung
self._felder = {f[1]: f for f in self.struktur}
self._werte = {}
if self.struktur:
letzte = max(f[2] + f[3] - 1 for f in self.struktur)
if letzte > satzlaenge:
raise ValueError(
'Struktur %s endet bei Position %d — Satzlänge %d '
'überschritten' % (bezeichnung, letzte, satzlaenge))
def setze(self, name, wert, cent=False, skala=0):
"""Feld setzen; ``wert`` None übernimmt die Grundstellung.
``cent``: EURO-Betrag Cent; ``skala``: implizite Nachkommastellen
(z. B. Prozentsatz mit 3 Nachkommastellen skala=3)."""
if wert is None:
return self
nr, fname, pos, laenge, typ = self._felder[name]
if cent:
wert = l10n_at_euro_cent(wert)
if isinstance(wert, float):
wert = int((Decimal(str(wert)) * (10 ** skala)).quantize(
Decimal('1'), rounding=ROUND_HALF_UP))
elif isinstance(wert, int):
wert = wert * (10 ** skala)
roh = str(wert)
if typ == 'n':
if not roh.isdigit():
raise ValueError('Feld %s (%s): %r ist nicht numerisch' % (
name, self.bezeichnung, roh))
roh = roh.rjust(laenge, '0')
else:
roh = roh.ljust(laenge, ' ')
if len(roh) > laenge:
raise ValueError('Feld %s (%s): %r länger als %d' % (
name, self.bezeichnung, roh, laenge))
self._werte[name] = roh
return self
def setze_datum(self, name, wert, fmt='TTMMJJJJ'):
"""Datum in DM-ORG-Feldformat (Vorlaufsatz EDAT = TTMMJJJJ,
L16-Übermittlungsfelder = JJJJMMTT, LZP/AGBD = TTMMJJ)."""
if wert is None:
return self
if isinstance(wert, str):
wert = datetime.strptime(wert, '%Y-%m-%d').date()
formate = {'TTMMJJJJ': '%d%m%Y', 'JJJJMMTT': '%Y%m%d',
'TTMMJJ': '%d%m%y'}
if fmt not in formate:
raise ValueError('Unbekanntes Datumsformat %s' % fmt)
return self.setze(name, wert.strftime(formate[fmt]))
def setze_zeit(self, name, wert):
if wert is None:
return self
return self.setze(name, wert.strftime('%H%M%S'))
def ist_gesetzt(self, name):
return name in self._werte
def wert(self, name):
return self._werte.get(name)
def render(self):
"""Satz in Bestands-Satzlänge; unbelegte Felder in Grundstellung
(``a`` blank / ``n`` 0), Positionen nach dem letzten Feld blank."""
puffer = [' '] * self.satzlaenge
for nr, name, pos, laenge, typ in self.struktur:
roh = self._werte.get(name)
if roh is None:
roh = '0' * laenge if typ == 'n' else ' ' * laenge
puffer[pos - 1:pos - 1 + laenge] = roh
return ''.join(puffer)
class L10nAtEldaBestand:
"""Datenbestand: Vorlaufsatz + Datensätze + Schlusssatz zu EINER
Verarbeitung (BEST) für EINEN zuständigen Träger (VSTR). SANR zählt
lückenlos ab 1, SANZ inklusive Vorlauf/Schluss (Prüfvorschrift C.1.2);
alle Sätze nehmen die maximale Satzlänge der Datensätze an."""
def __init__(self, best, vstr, vers, hersteller=None, test=False,
obus='0000000', datensaetze=None):
self.best = best
self.vstr = vstr
self.vers = vers
self.hersteller = hersteller or {}
self.projekt = 'TM' if test else 'DM'
self.obus = obus
self.datensaetze = datensaetze or []
def ergaenze(self, satz):
self.datensaetze.append(satz)
return satz
def render(self, erstellungsdatum=None):
"""Zeilen des Bestands (ohne Zeilentrenner)."""
if not self.datensaetze:
raise ValueError('Datenbestand %s ohne Datensätze' % self.best)
heute = erstellungsdatum or date.today()
satzlaenge = max(s.satzlaenge for s in self.datensaetze)
# Der Vorlaufsatz nimmt die Satzlänge des Bestands an — aber
# nie kürzer als seine eigene Struktur (DM-ORG E.2: Vorlauf
# trägt alle Herstellfelder; reale Bestände sind ≥ 246).
satzlaenge = max(
satzlaenge,
max(f[2] + f[3] - 1 for f in
(L10N_AT_ELDA_IDTEIL + L10N_AT_ELDA_VORLAUF)))
zeilen = []
vorlauf_struktur = [
f for f in (L10N_AT_ELDA_IDTEIL + L10N_AT_ELDA_VORLAUF)
if f[2] + f[3] - 1 <= satzlaenge]
vorlauf = L10nAtEldaSatz(
vorlauf_struktur, satzlaenge,
bezeichnung='Vorlaufsatz %s' % self.best)
vorlauf.setze('SART', '00')
vorlauf.setze('SANR', 1)
vorlauf.setze('UVST', 'ED')
vorlauf.setze('OBUS', self.obus)
vorlauf.setze('VSTR', self.vstr)
vorlauf.setze('PROJ', self.projekt)
vorlauf.setze('BEST', self.best)
vorlauf.setze_datum('EDAT', heute, 'TTMMJJJJ')
vorlauf.setze_zeit('EZEI', datetime.now())
vorlauf.setze('HRST', (self.hersteller.get('name') or '')[:45])
vorlauf.setze('HKFZ', self.hersteller.get('kfz', 'A'))
vorlauf.setze('HPLZ', self.hersteller.get('plz'))
vorlauf.setze('HORT', self.hersteller.get('ort'))
vorlauf.setze('HSTR', self.hersteller.get('strasse'))
vorlauf.setze('VERS', self.vers)
vorlauf.setze('VNMF', '3.0')
zeilen.append(vorlauf.render())
for nummer, satz in enumerate(self.datensaetze, start=2):
# Datensatz-Generatoren setzen ihre SART selbst; der Bestand
# ergänzt Trägerangaben (falls offen) und die laufende
# Satznummer (C.1.2: lückenlos ab 1).
if not satz.ist_gesetzt('UVST'):
satz.setze('UVST', 'ED')
if not satz.ist_gesetzt('OBUS'):
satz.setze('OBUS', self.obus)
if not satz.ist_gesetzt('VSTR'):
satz.setze('VSTR', self.vstr)
satz._werte['SANR'] = str(nummer).rjust(7, '0')
zeilen.append(satz.render())
schluss = L10nAtEldaSatz(
L10N_AT_ELDA_IDTEIL + L10N_AT_ELDA_SCHLUSS, satzlaenge,
bezeichnung='Schlusssatz %s' % self.best)
schluss.setze('SART', '99')
schluss.setze('SANR', len(self.datensaetze) + 2)
schluss.setze('UVST', 'ED')
schluss.setze('OBUS', self.obus)
schluss.setze('VSTR', self.vstr)
schluss.setze('SANZ', len(self.datensaetze) + 2)
zeilen.append(schluss.render())
return zeilen
class L10nAtEldaDatei:
"""ELDA-Datei: Dateiheader (ENCD-Standard ISO-8859-15) + Datenbestände;
Rendering in Bytes (Encoding + CRLF-Zeilentrenner, letzte Zeile mit
Abschluss-Trenner)."""
def __init__(self, bestaende=None, header=True):
self.bestaende = bestaende or []
self.header = header
def render(self):
zeilen = []
if self.header:
zeilen.append(' 01' + L10N_AT_ELDA_ENCODING.ljust(50))
for bestand in self.bestaende:
zeilen.extend(bestand.render())
roh = L10N_AT_ELDA_ZEILENTRENNER.join(zeilen) \
+ L10N_AT_ELDA_ZEILENTRENNER
return roh.encode(L10N_AT_ELDA_ENCODING)
@@ -0,0 +1,295 @@
# Part of the odoo-at-payroll project. License: LGPL-3.
"""Fristenkalender (GP8/AP8): alle lohnverrechnungsrelevanten
Melde-/Zahlungsfristen je Company und Jahr.
Kuratierte Fristen (lb-sva-11, lb-mel-08, lb-lvr-04/05, Stand
2026-0608):
- SV-Beiträge (mBGM): Einlangen **15. des Folgemonats** (Gutschrift-
prinzip; Respirofrist 3 Tage; Verzugszinsen ab dem 16. Tag);
Vorschreibebetriebe 7.
- DB (FLAF) / DZ / KommSt: je **15. des Folgemonats**.
- KommSt-Jahreserklärung: über FinanzOnline **bis Ende März** des
Folgejahres.
- Lohnzettel L16 (§ 84 EStG): **Ende Februar** des Folgejahres via
ELDA (Papierformular ohne technische Voraussetzungen: Ende Jänner);
berichtigter Lohnzettel nach steuerlich relevanter Ergänzung des
Lohnkontos **binnen 14 Tagen**.
- Arbeitsstättenmeldung § 34 Abs 6 ASVG: **Ende Februar** (via ELDA
an Statistik Austria).
- § 109a-EStG-Meldung: **Ende Februar** (ELDA); Papierformular E 109a
+ Kopie an den Entgeltempfänger Ende Jänner (nicht implementiert
Honorar-Basis).
- Schwerarbeitsmeldung ASchV § 5: Jänner bis **Ende Februar** via
ELDA (nicht implementiert blockiert, ASchV-Datenmodell fehlt).
Odoo-Integration: `mail.activity.mixin` für optionale Benachrichtigung
bei Überschreitung; native Fristart-Definition mit Sprungaktion zur
GP7-Deklaration (L16/mBGM erzeugen); Auto-Erzeugung per ir.cron
(monatlich am 1.: offene SV/DB/DZ/KommSt-Fristen des Vormonats;
jährlich am 1. Jänner: Jahresfristen des Vorjahres)."""
from datetime import date, timedelta
from odoo import api, fields, models
FRISTART_DEFINITIONEN = {
'sv_mbgm': {
'name': 'SV-Beiträge (mBGM) einlangen',
'monatlich': True,
'tag': 15,
'action': 'mbgm',
'hilfetext': '15. des Folgemonats (Gutschriftprinzip; '
'Vorschreibebetriebe: 7.). Verzugszinsen ab dem '
'16. Tag.',
},
'db_dz_kommst': {
'name': 'DB/DZ/KommSt einlangen',
'monatlich': True,
'tag': 15,
'action': None,
'hilfetext': 'FLAF-Dienstgeberbeitrag, Zuschlag zum '
'Dienstgeberbeitrag und Kommunalsteuer: je 15. '
'des Folgemonats.',
},
'kommst_jahres': {
'name': 'KommSt-Jahreserklärung (FinanzOnline)',
'monatlich': False,
'monat': 3,
'tag_im_monat': 31,
'action': None,
'hilfetext': 'Jahreserklärung der Kommunalsteuer über '
'FinanzOnline bis Ende März des Folgejahres '
'(§ 11 Abs 5 KommStG).',
},
'l16': {
'name': 'Lohnzettel L16 (§ 84 EStG)',
'monatlich': False,
'monat': 2,
'tag_im_monat': 28,
'action': 'l16',
'hilfetext': 'Jahres-Lohnzettel bis Ende Februar des '
'Folgejahres via ELDA; Papierformular ohne '
'technische Voraussetzungen: Ende Jänner. '
'Berichtigter Lohnzettel nach steuerlich '
'relevanter Ergänzung des Lohnkontos binnen '
'14 Tagen.',
},
'arbeitsstaette': {
'name': 'Arbeitsstättenmeldung (§ 34 Abs 6 ASVG)',
'monatlich': False,
'monat': 2,
'tag_im_monat': 28,
'action': 'l16',
'hilfetext': 'Adresse der Arbeitsstätte am 31. 12. bis '
'Ende Februar des Folgejahres via ELDA (VSTR '
'ST). Läuft im GP7-Jahreslauf mit der L16.',
},
'paragraph_109a': {
'name': '§ 109a-EStG-Meldung (Honorare)',
'monatlich': False,
'monat': 2,
'tag_im_monat': 28,
'action': None,
'hilfetext': 'Meldung der Honorare außerhalb von '
'Dienstverhältnissen bis Ende Februar via '
'ELDA (8 Tätigkeitsgruppen; kumulativ ≤ 900 € '
'und ≤ 450 € je Leistung → entfällt). '
'Fixsatz: l10n.at.payroll.mitteilung109 '
'(AP17).',
},
'mitteilung_109b': {
'name': '§ 109b-EStG-Meldung (Auslandszahlungen)',
'monatlich': False,
'monat': 2,
'tag_im_monat': 28,
'action': None,
'hilfetext': 'Mitteilung über Zahlungen ins Ausland '
'(§ 109b EStG: §-22-Tätigkeiten, Vermittlung, '
'Beratung im Inland) elektronisch bis Ende '
'Februar des Folgejahres; amtlicher Vordruck '
'bis Ende Jänner (§ 109b Abs 6, RIS '
'NOR40263383); entfällt unter der '
'100.000-€-Schwelle bzw. bei Steuerabzug '
'§ 99/KSt-Vergleich (Abs 4). Fixsatz: '
'l10n.at.payroll.mitteilung109 (AP17).',
},
'l17': {
'name': 'Lohnnachweis/Lohnbescheinigung L17 (A1)',
'monatlich': False,
'monat': 2,
'tag_im_monat': 28,
'action': 'l16',
'hilfetext': 'Lohnzettel Finanz je Lohnzahlungszeitraum '
'(DM-ORG E.24, Satzart A1, Version 28 — '
'zwingender Einsatz ab 01.03.2026, fachliche '
'Gültigkeit für Zeiträume ab 01.01.2025); '
'Übermittlung im Jahreslauf mit der L16 '
'(Ende Februar).',
},
'schwerarbeit': {
'name': 'Schwerarbeitsmeldung (ASchV § 5)',
'monatlich': False,
'monat': 2,
'tag_im_monat': 28,
'action': None,
'hilfetext': 'Jährlich rückwirkend Jänner bis Ende Februar '
'via ELDA an den KV-Träger; meldepflichtig '
'ab 40 M / 35 F (⚠ blockiert — ASchV-Daten-'
'modell fehlt im Modul).',
},
'wien_dag': {
'name': 'Wr. Dienstgeberabgabe entrichten (Stadtkasse)',
'monatlich': True,
'tag': 15,
'action': None,
'wien_dag': True,
'hilfetext': 'Wr. DAG („U-Bahn-Steuer“, GesNr 20000133): '
'Selbstberechnungsabgabe — die im Vormonat '
'entstandene Abgabenschuld bis zum 15. des '
'Folgemonats an die Stadtkasse (Magistrat) '
'entrichten (§ 6 Abs 1). Nur Firmen mit Flag '
'l10n_at_wien_dag (Beschäftigungsort Wien).',
},
'wien_dag_jahres': {
'name': 'Wr. DAG-Jahreserklärung (Magistrat)',
'monatlich': False,
'monat': 3,
'tag_im_monat': 31,
'action': None,
'wien_dag': True,
'hilfetext': 'Bis 31. März die im Vorjahr entstandene '
'Abgabenschuld beim Magistrat schriftlich '
'erklären; darin auch die befreiten '
'Dienstverhältnisse angeben (§ 6 Abs 2 Wr. '
'DAG). Nur Firmen mit Flag l10n_at_wien_dag.',
},
}
class L10nAtPayrollFrist(models.Model):
_name = 'l10n.at.payroll.frist'
_description = 'Lohnverrechnungs-Fristenkalender'
_inherit = ['mail.thread']
_order = 'deadline, id'
company_id = fields.Many2one(
'res.company', required=True,
default=lambda self: self.env.company, index=True)
fristart = fields.Selection(
[(k, v['name']) for k, v in FRISTART_DEFINITIONEN.items()],
required=True, index=True)
jahr = fields.Integer(
required=True, string='Bezugs-/Kalenderjahr',
help='Beitragsjahr (monatliche Fristen) bzw. gemeldetes '
'Jahr (Jahresfristen: L16/Arbeitsstätte/§-109a gelten '
'für das Vorjahr).')
monat = fields.Integer(
string='Beitragsmonat (monatliche Fristen)',
help='112; leer bei Jahresfristen.')
deadline = fields.Date(required=True, string='Frist')
state = fields.Selection(
[('offen', 'Offen'), ('erledigt', 'Erledigt'),
('ueberfaellig', 'Überfällig')],
compute='_compute_state', store=True, index=True)
erledigt_am = fields.Date(readonly=True)
hilfetext = fields.Text(readonly=True)
ref_l16_id = fields.Many2one(
'l10n.at.payroll.l16', string='L16-Deklaration',
help='Sprungziel für Fristen mit L16/Arbeitsstätten-Meldung.')
_sql_constraints = [(
'frist_company_art_jahr_monat_uniq',
'unique(company_id, fristart, jahr, monat)',
'Diese Frist existiert bereits für das Unternehmen.')]
@api.depends('deadline', 'erledigt_am')
def _compute_state(self):
heute = fields.Date.today()
for frist in self:
if frist.erledigt_am:
frist.state = 'erledigt'
elif frist.deadline and frist.deadline < heute:
frist.state = 'ueberfaellig'
else:
frist.state = 'offen'
def action_erledigt(self):
self.write({'erledigt_am': fields.Date.today()})
return True
def action_wieder_eroeffnen(self):
self.write({'erledigt_am': False})
return True
def action_zum_objekt(self):
"""Sprung zur GP7-Deklaration (L16 oder mBGM)."""
self.ensure_one()
if self.ref_l16_id:
return {
'type': 'ir.actions.act_window',
'res_model': 'l10n.at.payroll.l16',
'res_id': self.ref_l16_id.id,
'view_mode': 'form',
}
return True
@api.model
def _cron_fristen_erzeugen(self):
"""Monatlich am 1.: offene Fristen des Vormonats (SV/DB/DZ/
KommSt) + jährlich am 1. Jänner: Jahresfristen des
Vorjahres. Idempotent (Unique-Constraint)."""
heute = fields.Date.today()
vormonat = (heute.replace(day=1) - timedelta(days=1))
for company in self.env['res.company'].search([]):
# Monatliche Fristen des Vormonats (15. des Folgemonats).
folgemonat = heute.replace(day=1)
deadline_monat = date(
folgemonat.year, folgemonat.month,
FRISTART_DEFINITIONEN['sv_mbgm']['tag'])
monatliche_arten = ['sv_mbgm', 'db_dz_kommst']
if company.l10n_at_wien_dag:
monatliche_arten.append('wien_dag')
for art in monatliche_arten:
self._sicherstellen(
company, art, vormonat.year, vormonat.month,
deadline_monat)
# Jahresfristen nur am 1. Jänner (für das Vorjahr).
if heute.month == 1:
for art in ('l16', 'arbeitsstaette', 'paragraph_109a',
'mitteilung_109b', 'l17', 'schwerarbeit'):
monat = FRISTART_DEFINITIONEN[art]['monat']
tag = FRISTART_DEFINITIONEN[art]['tag_im_monat']
jahr_deadline = heute.year
deadline_jahr = date(jahr_deadline, monat, tag)
if deadline_jahr.month == 2 and tag > 28:
deadline_jahr = date(jahr_deadline, monat, 28)
self._sicherstellen(
company, art, heute.year - 1, None,
deadline_jahr)
# KommSt-Jahreserklärung (Ende März).
self._sicherstellen(
company, 'kommst_jahres', heute.year - 1, None,
date(heute.year, 3, 31))
# Wr. DAG-Jahreserklärung (31. 3., § 6 Abs 2).
if company.l10n_at_wien_dag:
self._sicherstellen(
company, 'wien_dag_jahres', heute.year - 1,
None, date(heute.year, 3, 31))
@api.model
def _sicherstellen(self, company, art, jahr, monat, deadline):
vals = {
'company_id': company.id,
'fristart': art,
'jahr': jahr,
'monat': monat,
'deadline': deadline,
'hilfetext': FRISTART_DEFINITIONEN[art]['hilfetext'],
}
bestehend = self.search([
('company_id', '=', company.id),
('fristart', '=', art),
('jahr', '=', jahr),
('monat', '=', monat)])
if not bestehend:
self.create(vals)
return bestehend or self.env['l10n.at.payroll.frist']
@@ -0,0 +1,152 @@
# Part of the odoo-at-payroll project. License: LGPL-3.
"""Jahres-Retro-/Aufrollungslauf (GP8/AP8): Kontrollsechstel-
Verpflichtungslauf (§ 67 Abs 10 EStG) und optionale §-77-Abs-3/4-
Aufrollung mit YTD-Rekalkulation.
Kuratierte Grundlagen (lb-lvr-07, lb-naz-0104, 2026-06/08):
- **Kontrollsechstel** (§ 67 Abs 10): sonstige Bezüge innerhalb des
Jahressechstels sind mit Fixsätzen versteuert; bei Überhang wird
die **Differenz tarifisch** versteuert der Dezember-/
Beendigungsabrechnungslauf rechnet die SZ-Steuer mit dem Tarif neu
(**Günstig-Aufrollung** GP3-Engine kann beides bereits).
- **§ 77 Abs 3/4**: optionale Aufrollung der Lohnsteuer des
Kalenderjahres beim Jahreswechsel (15.-Jänner-Grenze;
Krankengeldbezug im Jahr blockt Abs 3/4, nicht Abs 4a). Die
Aufrollung rechnet die laufende Lohnsteuer kumulativ neu und
verteilt die Differenz auf die verbleibenden Monate.
- **§ 77 Abs 4a**: Aufrollung nach §-67-Ausschüttungen (nachträg-
liche SZ-Steuer mit Günstig-Vergleich).
- **15.2.-Cutoff** (lb-naz-02/03): Zahlung bis 15. 2. des Folgejahres
Vorjahreszuordnung (berichtigter Lohnzettel binnen 14 Tagen);
danach keine Zuordnung mehr (§ 67 Abs 8 lit c Fünftel oder
§ 67 Abs 1 Tarif).
- **SV rollt stets auf den Beitragsmonat der Fälligkeit** (GP4:
`l10n_at_beitragsmonat` je Regel/Linie, historisch versionierte
Sätze/HBG). mBGM-Storno/Neumeldung als Folgeaktion (GP7 `art='ST'`).
Odoo-Mechanik: der Wizard **erzeugt keine eigenen Korrekturdaten**
er analysiert die Jahresdaten, zeigt den Handlungsbedarf und
empfiehlt die Odoo-native Korrektur-Pipeline (Refund-Payslips über
`_action_refund_payslips()` + Neuberechnung des betroffenen Monats
mit korrigierten Eingangsdaten; die GP3-Engine rechnet YTD
automatisch um)."""
from datetime import date
from odoo import api, fields, models
from odoo.exceptions import UserError
class L10nAtPayrollJahresretroWizard(models.TransientModel):
_name = 'l10n.at.payroll.jahresretro.wizard'
_description = 'Jahres-Retro-/Aufrollungslauf (GP8)'
company_id = fields.Many2one(
'res.company', required=True,
default=lambda self: self.env.company)
jahr = fields.Integer(
required=True, default=lambda self: fields.Date.today().year - 1,
string='Jahr (Kontroll-/Aufrollungslauf)')
zeige_detail = fields.Boolean(
default=True, string='Detail je Mitarbeiter anzeigen')
nur_kontrollsechstel = fields.Boolean(
default=True, string='Nur Kontrollsechstel-Verpflichtungslauf '
'prüfen (§ 67 Abs 10)')
aufrollung_77 = fields.Boolean(
default=False,
string='§-77-Abs-3/4-Aufrollung prüfen',
help='Optionale Aufrollung der laufenden Lohnsteuer des '
'Kalenderjahres (nur ohne Krankengeldbezug; 15.-Jänner-'
'Grenze für die Zahlung; Abs 4a nach §-67-Ausschüttungen '
'unabhängig prüfbar).')
ergebnis_ids = fields.One2many(
'l10n.at.payroll.jahresretro.ergebnis',
'wizard_id', string='Prüfergebnis je Mitarbeiter')
zusammenfassung = fields.Text(readonly=True)
def action_pruefen(self):
for wizard in self:
wizard.ergebnis_ids.unlink()
slips = wizard._slips_des_jahres()
ergebnisse = []
for slip in slips:
ergebnis = wizard._pruefe_mitarbeiter(slip)
if ergebnis:
ergebnisse.append(ergebnis)
wizard.ergebnis_ids = [
(0, 0, e) for e in ergebnisse]
wizard.zusammenfassung = wizard._zusammenfassung()
return True
def _slips_des_jahres(self):
self.ensure_one()
return self.env['hr.payslip'].search([
('company_id', '=', self.company_id.id),
('date_from', '>=', date(self.jahr, 1, 1)),
('date_from', '<=', date(self.jahr, 12, 31)),
('state', 'in', ('validated', 'paid')),
]).sorted('date_from')
def _pruefe_mitarbeiter(self, slip):
"""Prüft Kontrollsechstel-Überhang und optional §-77-Aufrollung
je AN. Liefert None, wenn kein Handlungsbedarf."""
self.ensure_one()
sechstel = slip._l10n_at_sechstel()
sonstige_basis = slip._l10n_at_brutto('sonstig')
laufende_basis = slip._l10n_at_brutto('laufend')
# §-67-Bezüge über dem Jahressechstel → Überhang.
if self.nur_kontrollsechstel \
and sonstige_basis > sechstel:
ueberhang = sonstige_basis - sechstel
fixsatz_st = slip._l10n_at_sz_tarifisch_aktuell()
tarif_st = slip._l10n_at_lst_sonstige()
gut = fixsatz_st <= tarif_st
return {
'employee_id': slip.employee_id.id,
'kontrollsechstel_ueberhang': ueberhang,
'fixsatz_steuer': fixsatz_st,
'tarif_steuer': tarif_st,
'guenstig': gut,
'handlung': (
'Keine Korrektur — Fixsätze sind günstiger'
if gut else
'Aufrollung: Dezemberlauf rechnet die SZ-Steuer '
'mit dem Tarif neu (§ 67 Abs 10 Günstig-Aufrollung). '
'SV rollt auf den Beitragsmonat (GP4); mBGM-Storno '
'(GP7) wenn der Beitragsmonat wechselt.'),
}
return None
def _zusammenfassung(self):
self.ensure_one()
ohne = self.ergebnis_ids.filtered(lambda r: r.guenstig)
mit = self.ergebnis_ids.filtered(lambda r: not r.guenstig)
return (
'Kontrollsechstel-Prüfung %d: %d Mitarbeiter mit '
'Überhang, davon %d mit Günstig-Aufrollung (= Tarif '
'günstiger als Fixsätze → Korrekturlauf).\n'
'§-77-Prüfung: %s.'
% (self.jahr, len(self.ergebnis_ids), len(mit),
'aktiviert' if self.aufrollung_77 else 'deaktiviert'))
class L10nAtPayrollJahresretroErgebnis(models.TransientModel):
_name = 'l10n.at.payroll.jahresretro.ergebnis'
_description = 'Jahres-Retro — Prüfergebnis je AN'
wizard_id = fields.Many2one(
'l10n.at.payroll.jahresretro.wizard', required=True,
ondelete='cascade')
employee_id = fields.Many2one('hr.employee', required=True)
kontrollsechstel_ueberhang = fields.Monetary(
currency_field='currency_id')
fixsatz_steuer = fields.Monetary(
currency_field='currency_id',
string='SZ-Steuer (Fixsätze)')
tarif_steuer = fields.Monetary(
currency_field='currency_id',
string='SZ-Steuer (Tarif)')
guenstig = fields.Boolean(
string='Fixsätze günstiger (keine Korrektur)')
handlung = fields.Text(string='Handlungsempfehlung')
currency_id = fields.Many2one(
'res.currency', related='wizard_id.company_id.currency_id')
+104
View File
@@ -0,0 +1,104 @@
# Part of the odoo-at-payroll project. License: LGPL-3.
"""Kind-Datenmodell (GP7): Kinder je Mitarbeiter für die Wartung des
Familienbonus Plus und die Befüllung des L16-Kinderblocks
(Lohnzettel-Finanz-Mitteilungssatz, DM-ORG 42.7.0 E.14, Felder
Nr. 196209: je Kind 123 Stellen KFAM/KVON/KSTAAT/KSTWE/KVSNR/
KGEBD/KAFBZ/KAPFB/KAUHZ/KBGFP/KEGFP/KBHFP/KEHFP).
Nutzer-Entscheidung 2026-09-12: die FB+-Steuerwirkung selbst ist KEIN
Bestandteil der Personalverrechnung (bei Unterschreiten der Steuer auf 0
erfolgt die Geltendmachung über die Arbeitnehmerveranlagung) das Modell
dient der Stammdaten-Wartung (Anspruch, Monate, halber Bonus) und der
L16-Meldung. Monatsfelder im Format MM (Kinderblock 2n); die
Betragsermittlung (FABO) rechnet der L16-Generator aus den gepflegten
Monaten und der Altersstaffel (§ 33 Abs 5 EStG: bis 18. Geburtstag
166,68 /Monat, darüber 58,34 /Monat solange Familienbeihilfe)."""
from odoo import fields, models
class HrEmployee(models.Model):
_inherit = 'hr.employee'
l10n_at_payroll_kind_ids = fields.One2many(
'l10n.at.payroll.kind', 'employee_id',
string='Kinder (Familienbonus Plus)',
groups="hr_payroll.group_hr_payroll_user")
class L10nAtPayrollKind(models.Model):
_name = 'l10n.at.payroll.kind'
_description = 'Kind (Familienbonus Plus / Lohnzettel L16)'
_order = 'geburtstag, id'
employee_id = fields.Many2one(
'hr.employee', string='Mitarbeiter:in', required=True,
index=True, ondelete='cascade')
familienname = fields.Char(
string='Familienname (KFAM)', required=True)
vorname = fields.Char(string='Vorname (KVON)')
geburtstag = fields.Date(
string='Geburtsdatum (KGEBD)', required=True,
help='Maßgeblich für die FB+-Staffel (bis zum 18. Geburtstag '
'166,68 €/Monat, danach 58,34 €/Monat solange '
'Familienbeihilfe) und die Altersfilter (z. B. '
'Schwerarbeitsmeldung 40 M/35 F).')
versicherungsnummer = fields.Char(
string='Versicherungsnummer (KVSNR)',
help='10-stellig, LLLPTTMMJJ; nur wenn zugeteilt.')
staatsangehoerigkeit = fields.Char(
string='Staatsangehörigkeit (KSTAAT, 3-stelliger Code)')
staatsangehoerigkeit_2 = fields.Char(
string='2. Staatsangehörigkeit (KSTWE, 1-stelliger Code)')
# ------------------------------------------------------------------
# Familienbonus Plus — Wartung (L16-Kinderblock)
# ------------------------------------------------------------------
fab_plus = fields.Boolean(
string='Familienbonus Plus geltend gemacht',
help='L16 KFABO (Anzahl) und FABO (Betrag) ergeben sich aus den '
'Kindern mit gesetztem FB+-Kennzeichen.')
fab_monat_von = fields.Integer(
string='FB+ ab Monat (KBGFP)',
help='Monat (112), ab dem der volle Familienbonus Plus für '
'dieses Kind geltend gemacht wurde (L16 KBGFP).')
fab_monat_bis = fields.Integer(
string='FB+ bis Monat (KEGFP)',
help='Letzter Monat (112) des vollen Familienbonus Plus (L16 '
'KEGFP).')
fab_halb_monat_von = fields.Integer(
string='Halber FB+ ab Monat (KBHFP)',
help='Monat (112), ab dem nur mehr der halbe Familienbonus Plus '
'geltend gemacht wurde (Aufteilung zwischen den '
'Elternteilen; L16 KBHFP).')
fab_halb_monat_bis = fields.Integer(
string='Halber FB+ bis Monat (KEHFP)',
help='Letzter Monat (112) des halben Familienbonus Plus (L16 '
'KEHFP).')
def _l10n_at_fab_monate(self, jahr):
"""[(Monat, Anteil 1.0/0.5), …] des FB+ im Lohnzetteljahr aus den
gepflegten Monatsfeldern (voll + halb, Überschneidung: der
spätere Eintrag gewinnt)."""
self.ensure_one()
monate = {}
von, bis = self.fab_monat_von or 0, self.fab_monat_bis or 0
if von and bis and 1 <= von <= bis <= 12:
for monat in range(von, bis + 1):
monate[monat] = 1.0
hvon, hbis = (self.fab_halb_monat_von or 0,
self.fab_halb_monat_bis or 0)
if hvon and hbis and 1 <= hvon <= hbis <= 12:
for monat in range(hvon, hbis + 1):
monate[monat] = 0.5
return sorted(monate.items())
def _l10n_at_fab_betrag(self, jahr, voll=166.68, vermindert=58.34):
"""FB+-Jahresbetrag je Kind aus Monaten × Monatsrate; die Rate
wechselt mit dem 18. Geburtstag (monatliche Staffel: Monate vor
dem Geburtsmonat + 18 Jahre voller Satz, danach verminderter
Satz § 33 Abs 5 EStG, solange Familienbeihilfe)."""
self.ensure_one()
if not self.geburtstag or not self.fab_plus:
return 0.0
umschalt = self.geburtstag.month
return sum(anteil * (vermindert if monat >= umschalt else voll)
for monat, anteil in self._l10n_at_fab_monate(jahr))
+130 -9
View File
@@ -52,6 +52,33 @@ class HrSalaryRule(models.Model):
help='Anteil des Zeilenbetrags, der Zuschlag (und damit ' help='Anteil des Zeilenbetrags, der Zuschlag (und damit '
'§-68-potentiell steuerfrei) ist — z. B. 50 %-Regel mit ' '§-68-potentiell steuerfrei) ist — z. B. 50 %-Regel mit '
'Grundvergütung: 0,5/1,5 = 33,33 %.') 'Grundvergütung: 0,5/1,5 = 33,33 %.')
l10n_at_st_frei = fields.Selection(
[('keine', 'Keine § 3/§ 26-Steuerfreiheit'),
('mahlzeit', 'Mahlzeiten-Gutschein (§ 3 Abs 1 Z 17; 8 €/AT)'),
('rabatt', 'Mitarbeiterrabatt (§ 3 Abs 1 Z 21; 1.000 €/J)'),
('betriebsveranstaltung',
'Betriebsveranstaltung (§ 3 Abs 1 Z 14; 365 €/J)'),
('jubilaeum',
'Sachzuwendung/Jubiläum/Geschenk (§ 3 Abs 1 Z 14; 186 €/J)'),
('homeoffice',
'Homeoffice-Pauschale (§ 26 Z 9; 3 €/Tag, max 100 Tage/J)')],
string='AT § 3/§ 26 (steuerfrei)',
default='keine',
help='Steuerfreie Bezugsbestandteile (Freibeträge): der '
'steuerfreie Anteil wird je Bezugsmonat von der LSt-'
'Bemessungsgrundlage und von der LNK-Bemessungsgrundlage '
'abgezogen; Grenzen als Jahres-Kumulative je Dienstnehmer '
'(at_st_grenzen). Nur der übersteigende Teil ist '
'steuerpflichtig. Menge = Arbeitstage (Z 17/Z 9).')
l10n_at_s67_abs6_ueberhang = fields.Boolean(
string='AT § 67 Abs 6 Z 6 — Überhang',
help='Übersteigender Teil einer freiwilligen Abfertigung/Abfindung '
'(§ 67 Abs 6 Z 6 EStG): wird wie ein laufender Bezug zum '
'Tarif des Zuflussmonats versteuert, erhöht aber das '
'Jahressechstel nicht — Abs 2 ist nur auf die mit 6 % '
'versteuerten Z-1/Z-2-Beträge nicht anzuwendbar (Z 5), der '
'Überhang ist ein sonstiger Bezug, kein laufender. LNK-frei '
'über § 67 Abs 6 (lb-end-12), SV-frei über § 49 Abs 3 Z 7.')
class HrVersion(models.Model): class HrVersion(models.Model):
@@ -197,6 +224,76 @@ class HrPayslip(models.Model):
ergebnis['ue_std'] = min(max(betrag_ue, 0.0), float(ue['betrag'])) ergebnis['ue_std'] = min(max(betrag_ue, 0.0), float(ue['betrag']))
return ergebnis return ergebnis
# ------------------------------------------------------------------
# § 3/§ 26 — Steuerfreiheit (Buckets je Regel-Flag, versionierte
# Caps; Jahres-Kumulative — Spieglung der §-49-SV-Seite)
# ------------------------------------------------------------------
def _l10n_at_st_frei_ytd(self, kategorie, menge=False):
"""Jahresverbrauch einer §-3/§-26-Kategorie aus früheren
validierten Läufen (refundbewusst über _l10n_at_ytd_slaeufe)."""
self.ensure_one()
total = 0.0
for slip in self._l10n_at_ytd_slaeufe():
linien = slip.line_ids.filtered(
lambda l: l.salary_rule_id.l10n_at_st_frei == kategorie)
if menge:
total += sum(linien.mapped('quantity'))
else:
total += sum(linien.mapped('total'))
return total
def _l10n_at_st_frei_freie_laufen(self, result_rules=None):
"""Steuerfreie Anteile laufender Bezüge dieses LZP: je Kategorie
Tages-/Jahresgrenzen (at_st_grenzen) minus Jahresverbrauch;
Freibeträge nur der übersteigende Teil steuerpflichtig
(lb-sac-07/17, lb-ges-02, lb-sac-02)."""
self.ensure_one()
grenzen = self.env['hr.rule.parameter']._get_parameter_from_code(
'at_st_grenzen', self.date_to, raise_if_not_found=False)
if not grenzen:
return 0.0
frei_gesamt = 0.0
for rule in self.struct_id.rule_ids:
kategorie = rule.l10n_at_st_frei
if (not kategorie or kategorie == 'keine'
or rule.l10n_at_bemessung != 'laufend'):
continue
werte = None
if result_rules is not None:
if rule.code in result_rules:
werte = result_rules[rule.code]
else:
linie = self.line_ids.filtered(lambda l: l.code == rule.code)
if linie:
werte = {'total': sum(linie.mapped('total')),
'quantity': sum(linie.mapped('quantity'))}
if not werte:
continue
total = werte['total']
menge = werte.get('quantity', 0.0)
if kategorie == 'mahlzeit':
frei = min(
total, float(grenzen.get('mahlzeit_tag')) * menge)
elif kategorie == 'homeoffice':
rest_tage = max(
float(grenzen.get('homeoffice_tage'))
- self._l10n_at_st_frei_ytd('homeoffice', menge=True),
0.0)
frei = min(
total,
float(grenzen.get('homeoffice_tag'))
* min(menge, rest_tage))
else:
pool = {'rabatt': 'rabatt_jahr',
'betriebsveranstaltung': 'betriebsveranstaltung_jahr',
'jubilaeum': 'jubilaeum_jahr'}.get(kategorie)
rest = max(
float(grenzen.get(pool))
- self._l10n_at_st_frei_ytd(kategorie), 0.0)
frei = min(total, rest)
frei_gesamt += max(frei, 0.0)
return frei_gesamt
# ------------------------------------------------------------------ # ------------------------------------------------------------------
# DN-SV (Bemessungsbestandteil § 62) — Basis/Sätze/Geringfügigkeit/ # DN-SV (Bemessungsbestandteil § 62) — Basis/Sätze/Geringfügigkeit/
# §-49-Kumulative/DG-Seite: models/sozialversicherung.py (GP4) # §-49-Kumulative/DG-Seite: models/sozialversicherung.py (GP4)
@@ -217,11 +314,11 @@ class HrPayslip(models.Model):
"""SV-BG sonstige Bezüge: HBG monatlich und SZ-Jahreshöchstbetrag """SV-BG sonstige Bezüge: HBG monatlich und SZ-Jahreshöchstbetrag
(13.860) minus YTD-SZ-Basen (Anspruchsprinzip-Vorlauf GP4).""" (13.860) minus YTD-SZ-Basen (Anspruchsprinzip-Vorlauf GP4)."""
self.ensure_one() self.ensure_one()
sz = max(self._l10n_at_brutto('sonstig', result_rules), 0.0) sz = max(self._l10n_at_sv_brutto('sonstig', result_rules), 0.0)
hbg = float(self._l10n_at_param('at_sv_hbg_monatlich')) hbg = float(self._l10n_at_param('at_sv_hbg_monatlich'))
jahreshbg = float(self._l10n_at_param('at_sv_hbg_sz_jaehrlich')) jahreshbg = float(self._l10n_at_param('at_sv_hbg_sz_jaehrlich'))
for slip in self._l10n_at_ytd_slaeufe(): for slip in self._l10n_at_ytd_slaeufe():
jahreshbg -= max(slip._l10n_at_brutto('sonstig'), 0.0) jahreshbg -= max(slip._l10n_at_sv_brutto('sonstig'), 0.0)
return min(sz, hbg, max(jahreshbg, 0.0)) return min(sz, hbg, max(jahreshbg, 0.0))
def _l10n_at_svdn_sz(self, result_rules=None): def _l10n_at_svdn_sz(self, result_rules=None):
@@ -242,14 +339,16 @@ class HrPayslip(models.Model):
# ------------------------------------------------------------------ # ------------------------------------------------------------------
def _l10n_at_bemessung_laufen(self, result_rules=None): def _l10n_at_bemessung_laufen(self, result_rules=None):
"""LSt-Bemessung laufend: Arbeitslohn SV-DN (§ 62-Kette, """LSt-Bemessung laufend: Arbeitslohn SV-DN (§ 62-Kette,
inkl. AK/WF) steuerfreie Bezüge § 68. Der volle Arbeitslohn inkl. AK/WF) steuerfreie Bezüge § 68 steuerfreie Sachbezüge
ist lohnsteuerlich unbegrenzt (keine HBG-Cap für die LSt; § 3/§ 26 (Freibeträge, Jahres-Kumulative). Der volle
die Cap gilt nur für die SV-Sätze).""" Arbeitslohn ist lohnsteuerlich unbegrenzt (keine HBG-Cap für
die LSt; die Cap gilt nur für die SV-Sätze)."""
self.ensure_one() self.ensure_one()
freibe = self._l10n_at_freibetraege_68(result_rules) freibe = self._l10n_at_freibetraege_68(result_rules)
return (self._l10n_at_brutto('laufend', result_rules) return (self._l10n_at_brutto('laufend', result_rules)
- self._l10n_at_svdn_gesamt(result_rules) - self._l10n_at_svdn_gesamt(result_rules)
- freibe['szfn'] - freibe['ue_std']) - freibe['szfn'] - freibe['ue_std']
- self._l10n_at_st_frei_freie_laufen(result_rules))
def _l10n_at_sz_bemessung(self, result_rules=None): def _l10n_at_sz_bemessung(self, result_rules=None):
"""LSt-Bemessung sonstiger Bezug: Bezug SV-DN (Abs 12); """LSt-Bemessung sonstiger Bezug: Bezug SV-DN (Abs 12);
@@ -261,11 +360,32 @@ class HrPayslip(models.Model):
def _l10n_at_brutto_laufen_ytd(self, result_rules=None): def _l10n_at_brutto_laufen_ytd(self, result_rules=None):
"""Kumulierte laufende Bruttobezüge des Jahres (Sechstel-Basis """Kumulierte laufende Bruttobezüge des Jahres (Sechstel-Basis
ist Brutto, § 67 Abs 2 inkl. Auszahlungsmonat-Laufendem).""" ist Brutto, § 67 Abs 2 inkl. Auszahlungsmonat-Laufendem).
Der §-67-Abs-6-Überhang (Flag) läuft tarifisch im Zuflussmonat,
ist aber ein sonstiger Bezug und erhöht das Jahressechstel
nicht."""
self.ensure_one() self.ensure_one()
total = self._l10n_at_brutto('laufend', result_rules) total = self._l10n_at_brutto('laufend', result_rules) \
- self._l10n_at_abs6_ueberhang_total(result_rules)
for slip in self._l10n_at_ytd_slaeufe(): for slip in self._l10n_at_ytd_slaeufe():
total += slip._l10n_at_brutto('laufend') total += slip._l10n_at_brutto('laufend') \
- slip._l10n_at_abs6_ueberhang_total()
return total
def _l10n_at_abs6_ueberhang_total(self, result_rules=None):
"""Summe der als §-67-Abs-6-Z-6-Überhang gekennzeichneten
Regel-Zeilen (tarifisch im Zuflussmonat, ohne Sechstel-Effekt)."""
self.ensure_one()
total = 0.0
for rule in self.struct_id.rule_ids:
if not rule.l10n_at_s67_abs6_ueberhang:
continue
if result_rules is not None:
if rule.code in result_rules:
total += result_rules[rule.code]['total']
else:
total += sum(self.line_ids.filtered(
lambda l, c=rule.code: l.code == c).mapped('total'))
return total return total
def _l10n_at_sechstel(self, result_rules=None): def _l10n_at_sechstel(self, result_rules=None):
@@ -310,6 +430,7 @@ class HrPayslip(models.Model):
# ------------------------------------------------------------------ # ------------------------------------------------------------------
# § 66 Monatslohnsteuer — kumulativ mit Rollung # § 66 Monatslohnsteuer — kumulativ mit Rollung
# ------------------------------------------------------------------ # ------------------------------------------------------------------
def _l10n_at_lst_laufend(self, result_rules=None): def _l10n_at_lst_laufend(self, result_rules=None):
"""LSt auf laufende Bezüge (§ 66): kumulative Hochrechnung """LSt auf laufende Bezüge (§ 66): kumulative Hochrechnung
(360-Tage-Jahr) minus bereits einbehaltene LSt Vormonats- (360-Tage-Jahr) minus bereits einbehaltene LSt Vormonats-
@@ -0,0 +1,775 @@
# Part of the odoo-at-payroll project. License: LGPL-3.
"""Lohnzettel Finanz (L16) und Arbeitsstättenmeldung (GP7/AP5) —
Jahresdeklaration nach DM-ORG 42.7.0 (lokal `.firecrawl/ris/elda/`).
- **L16** (BEST `LF`, zuständiger Träger VSTR `94` Bundesrechenzentrum):
Informationssatz I1 (SART `I1`, Länge 1100, Strukturversion 03) +
Mitteilungssatz L1 (SART `L1`, Länge 3500, Lohnzettelversion 28
zwingend ab 01.03.2026). KZ-Struktur nach lb-lvr-04 (
RECHTSQUELLEN-Privat Abschn. 10): **KZ 245 = 210 215 220 230
243**; die KZ-Werte sind aus den Kern-Flags (`l10n_at_bemessung`,
`steuerbefreiung_68`, `s67_abs`) und Regelcodes ableitbar bewusst
KEIN paralleles KZ-Klassifikationsfeld (Widerspruchsrisiko zur
GP3-Steuerlogik).
- **Arbeitsstättenmeldung** § 34 Abs 6 ASVG (BEST `AD`, VSTR `ST`
Statistik Austria): Satzart 45 (Länge 300, Version 02) je AN mit der
Adresse der Arbeitsstätte am 31. 12. läuft im selben Jahreslauf
(Nutzer-Entscheidung 5.1; Frist gemeinsam Ende Februar).
- Familienbonus Plus: **keine** Personalverrechnungs-Komponente
(Unterschreiten der Steuer auf 0 Arbeitnehmerveranlagung;
Nutzer-Entscheidung 2026-09-12); die FB+-Felder des L1
(BFABO/KFABO/VFABO/FABO + Kinderblock) werden aus der
Stammdaten-Wartung am Kind-Modell (`l10n.at.payroll.kind`) befüllt.
- Dateibasiert (Prämisse P7): Generierung + formale Prüfung als
Prüfprotokoll; Übertragung/ELDA-Konto beim Mandanten.
Satzstrukturen: PDF-verifizierte Extraktion 2026-09-12 (DM-ORG 42.7.0,
Kapitel E.13/E.14/E.20; Positions-Ketten lückenlos geprüft)."""
from datetime import date, datetime
import base64
import logging
from odoo import api, fields, models
from odoo.exceptions import UserError
from .elda import (
L10N_AT_ELDA_IDTEIL,
L10nAtEldaBestand,
L10nAtEldaDatei,
L10nAtEldaSatz,
)
_logger = logging.getLogger(__name__)
def l10n_at_namen(employee):
"""(Familienname, Vorname) aus dem Odoo-Gesamtnamen: Split am
letzten Leerzeichen (österreichische Schreibweise Vorname
Nachname; mehrgliedrige Familiennamen werden als ein Feld
geführt eSV FANA/VONA-Verifikation im Parallellauf)."""
name = (employee.name or '').strip()
if ' ' in name:
vorname, familienname = name.rsplit(' ', 1)
return familienname, vorname
return name, ''
# ------------------------------------------------------------------
# Informationssatz I1 (E.13, S. 219220; Länge 1100) — Datenteil
# ------------------------------------------------------------------
L16_INFORMATIONSSATZ = [
(2, 'FSART', 21, 1, 'a'), # I
(3, 'CLEAR', 22, 6, 'a'), # Clearingstelle (RADAUS/OESTAT)
(4, 'CLDVR', 28, 7, 'a'),
(6, 'STNRL', 35, 9, 'n'), # Steuernummer Lohnverrechnungsstelle
(7, 'DVRNL', 44, 7, 'n'),
(9, 'STNRA', 51, 9, 'n'), # Steuernummer Arbeitgeber
(10, 'DVRNA', 60, 7, 'n'),
(11, 'ARTD', 67, 2, 'a'), # LZ = Lohnzetteldaten
(12, 'DTUE', 69, 8, 'n'), # JJJJMMTT
(13, 'ZTUE', 77, 6, 'n'), # HHMMSS
(14, 'STVE', 83, 2, 'n'), # Strukturversion 03
(15, 'ANAM', 85, 37, 'a'), # AG-Name
(16, 'ATIT', 122, 20, 'a'),
(17, 'AADR', 142, 37, 'a'),
(18, 'ALKZ', 179, 3, 'a'),
(19, 'APLZ', 182, 10, 'a'),
(20, 'AORT', 192, 30, 'a'),
(21, 'INTE', 222, 11, 'a'),
(22, 'JAHR', 233, 4, 'n'), # Mitteilungszeitraum JJJJ
(23, 'GESA', 237, 7, 'n'), # Gesamtanzahl Mitteilungen im Jahr
(24, 'ANZA', 244, 7, 'n'), # Anzahl der aktuellen Übermittlung
(25, 'ATEL', 251, 16, 'a'),
(26, 'AFAX', 267, 16, 'a'),
(27, 'RESE', 283, 818, 'a'),
]
# ------------------------------------------------------------------
# Mitteilungssatz L1 (E.14, S. 223233; Länge 3500) — Datenteil inkl.
# RESE-Grundstellungsfeldern (n-Felder Grundstellung 0!)
# ------------------------------------------------------------------
L16_MITTEILUNGSSATZ = [
(2, 'FSART', 21, 1, 'a'), # L
(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'), # Art der Übermittlung (Prüfkatalog)
(9, 'DTUE', 52, 8, 'n'), # JJJJMMTT, wie I1
(10, 'ZTUE', 60, 6, 'n'), # wie I1
(11, 'REFN', 66, 30, 'a'), # je AG+Jahr+DV eindeutig
(12, 'ARTL', 96, 2, 'n'), # Art des Lohnzettels (D.31)
(13, 'FEHL', 98, 20, 'a'), # Clearingstelle
(14, 'STAT', 118, 1, 'a'), # Clearingstelle
(15, 'FIND', 119, 1, 'a'), # 'J' = vom Hersteller für richtig
(16, 'BELZ', 120, 4, 'n'), # Beginn LZP TTMM
(17, 'ENLZ', 124, 4, 'n'), # Ende LZP TTMM
(18, 'JALZ', 128, 4, 'n'), # Jahr LZP
(19, 'SOZS', 132, 1, 'n'), # soziale Stellung (D.33)
(20, 'AVLN', 133, 4, 'n'), # VSNR LLLP
(21, 'AGBD', 137, 6, 'n'), # TTMMJJ
(22, 'ANAM', 143, 37, 'a'),
(23, 'ATIT', 180, 20, 'a'),
(24, 'AADR', 200, 37, 'a'),
(25, 'ALKZ', 237, 3, 'a'),
(26, 'APLZ', 240, 10, 'a'),
(27, 'AORT', 250, 30, 'a'),
(28, 'GESW', 280, 1, 'a'),
(29, 'GESM', 281, 1, 'a'),
(30, 'GESD', 282, 1, 'a'),
(31, 'VOLL', 283, 1, 'a'), # 'J' überwiegend Vollzeit
(32, 'TEIL', 284, 1, 'a'), # 'J' überwiegend Teilzeit
(33, 'AVAB', 285, 1, 'a'), # Alleinerzieherabsetzbetrag
(34, 'PVLN', 286, 4, 'n'), # Partner-VSNR LLLP (AVB)
(35, 'PGBD', 290, 6, 'n'), # TTMMJJ (AVB)
(36, 'AEAB', 296, 1, 'a'), # Alleinverdienerabsetzbetrag
(37, 'V210', 297, 1, 'a'),
(38, 'B210', 298, 10, 'n'), # KZ 210 § 25 (Cent)
(39, 'V215', 308, 1, 'a'),
(40, 'B215', 309, 10, 'n'), # KZ 215 § 68-steuerfrei
(41, 'V220', 319, 1, 'a'),
(42, 'B220', 320, 10, 'n'), # KZ 220 § 67 Abs 1/2/5 (inkl. 620-€)
(43, 'VIEB', 330, 1, 'a'),
(44, 'BIEB', 331, 10, 'n'), # insgesamt einbehaltene Beiträge
(45, 'V225', 341, 1, 'a'),
(46, 'B225', 342, 10, 'n'), # KZ 225 SV zu KZ 220
(47, 'V226', 352, 1, 'a'),
(48, 'B226', 353, 10, 'n'), # KZ 226 SV zu § 67 Abs 38
(49, 'V230', 363, 1, 'a'),
(50, 'B230', 364, 10, 'n'), # KZ 230 laufende SV/AK/WF
(51, 'RESE51', 374, 1, 'a'),
(52, 'RESE52', 375, 10, 'n'),
(53, 'VAUS', 385, 1, 'a'),
(54, 'BAUS', 386, 10, 'n'),
(55, 'VPEN', 396, 1, 'a'),
(56, 'BPEN', 397, 10, 'n'),
(57, 'VEFB', 407, 1, 'a'),
(58, 'BEFB', 408, 10, 'n'),
(59, 'VSTF', 418, 1, 'a'),
(60, 'BSTF', 419, 10, 'n'),
(61, 'VSSB', 429, 1, 'a'),
(62, 'BSSB', 430, 10, 'n'),
(63, 'V243', 440, 1, 'a'),
(64, 'B243', 441, 10, 'n'), # KZ 243 sonstige steuerfreie
(65, 'V245', 451, 1, 'a'),
(66, 'B245', 452, 10, 'n'), # KZ 245 steuerpflichtige Bezüge
(67, 'VIEL', 462, 1, 'a'),
(68, 'BIEL', 463, 10, 'n'), # insgesamt einbehaltene Lohnsteuer
(69, 'VABL', 473, 1, 'a'),
(70, 'BABL', 474, 10, 'n'), # abzügliche Lohnsteuer (Fixsätze)
(71, 'V260', 484, 1, 'a'),
(72, 'B260', 485, 10, 'n'), # KZ 260 anrechenbare Lohnsteuer
(73, 'VSOB', 495, 1, 'a'),
(74, 'BSOB', 496, 10, 'n'),
(75, 'VFB1', 506, 1, 'a'),
(76, 'BFB1', 507, 10, 'n'),
(77, 'RESE77', 517, 10, 'a'),
(78, 'VFB2', 527, 1, 'a'),
(79, 'BFB2', 528, 10, 'n'),
(80, 'VAUF', 538, 1, 'a'),
(81, 'BAUF', 539, 10, 'n'),
(82, 'VFB3', 549, 1, 'a'),
(83, 'BFB3', 550, 10, 'n'),
(84, 'VNEB', 560, 1, 'a'),
(85, 'BNEB', 561, 10, 'n'),
(86, 'PVON', 571, 2, 'n'),
(87, 'PBIS', 573, 2, 'n'),
(88, 'VPFG', 575, 1, 'a'),
(89, 'BPFG', 576, 10, 'n'),
(90, 'VSFB', 586, 1, 'a'),
(91, 'BSFB', 587, 10, 'n'),
(92, 'RESE92', 597, 1, 'a'),
(93, 'RESE93', 598, 10, 'n'),
(94, 'RESE94', 608, 1, 'a'),
(95, 'RESE95', 609, 10, 'n'),
(96, 'RESE96', 619, 1, 'a'),
(97, 'RESE97', 620, 10, 'n'),
(98, 'RESE98', 630, 1, 'a'),
(99, 'RESE99', 631, 8, 'n'),
(101, 'STNRA', 639, 9, 'n'), # auszahlende Stelle (Pensionen)
(102, 'DVRNA', 648, 7, 'n'),
(103, 'ANME', 655, 1, 'a'), # Unterbrechung LZZR
(104, 'KORR', 656, 1, 'a'), # 'K' berichtigter Lohnzettel
(105, 'VUEB', 657, 1, 'a'),
(106, 'BUEB', 658, 10, 'n'),
(107, 'INTE', 668, 11, 'a'),
(108, 'WOBD', 679, 12, 'a'), # Ordnungsbegriff DG (Personalnummer)
(109, 'ZOBD', 691, 15, 'a'),
(110, 'ANZK', 706, 2, 'n'), # Anzahl Kinder § 106
(111, 'RESE111', 708, 1, 'a'),
(112, 'RESE112', 709, 1, 'a'),
(113, 'RESE113', 710, 1, 'a'),
(114, 'RESE114', 711, 1, 'a'),
(115, 'RESE115', 712, 1, 'a'),
(116, 'RESE116', 713, 1, 'a'),
(117, 'RESE117', 714, 1, 'a'),
(118, 'RESE118', 715, 1, 'a'),
(119, 'RESE119', 716, 1, 'a'),
(120, 'RESE120', 717, 1, 'a'),
(121, 'RESE121', 718, 1, 'a'),
(122, 'RESE122', 719, 1, 'a'),
(123, 'VAPK', 720, 1, 'a'),
(124, 'BAPK', 721, 10, 'n'),
(125, 'EPAB', 731, 1, 'a'),
(126, 'VENT', 732, 1, 'a'),
(127, 'ENTW', 733, 10, 'n'),
(128, 'VPRE', 743, 1, 'a'),
(129, 'PREI', 744, 10, 'n'),
(130, 'WERK', 754, 2, 'n'),
(131, 'VPEND', 756, 1, 'a'),
(132, 'BPEND', 757, 10, 'n'),
(134, 'UEBMO', 767, 2, 'n'),
(135, 'RESE135', 769, 1, 'a'),
(136, 'RESE136', 770, 10, 'n'),
(137, 'LDKZA', 780, 3, 'a'),
(138, 'VWBKB', 783, 1, 'a'),
(139, 'WBKB', 784, 10, 'n'),
(140, 'RESE140', 794, 1, 'a'),
(141, 'RESE141', 795, 10, 'a'),
(142, 'ERVAB', 805, 1, 'a'),
(143, 'BFABO', 806, 1, 'a'), # Familienbonus Plus 'J'
(144, 'KFABO', 807, 2, 'n'), # Anzahl Kinder FB+
(145, 'VFABO', 809, 1, 'a'),
(146, 'FABO', 810, 10, 'n'), # FB+-Betrag
(147, 'HOTA', 820, 3, 'n'), # Telearbeitstage
(148, 'VHOPA', 823, 1, 'a'),
(149, 'HOPA', 824, 10, 'n'), # Homeoffice-Pauschale § 26 Z 9 lit a
(150, 'VKOUN', 834, 1, 'a'),
(151, 'KOUN', 835, 10, 'n'),
(152, 'VMAGB', 845, 1, 'a'),
(153, 'MAGB', 846, 10, 'n'),
(154, 'FLABZ', 856, 1, 'a'), # freiwilliger Lohnsteuerabzug § 47
(155, 'AUEZG', 857, 1, 'a'),
(156, 'VTEPR', 858, 1, 'a'),
(157, 'TEPR', 859, 10, 'n'),
(158, 'VMAPR', 869, 1, 'a'),
(159, 'MAPR', 870, 10, 'n'),
(160, 'STUM', 880, 4, 'n'),
(161, 'STUMJ', 884, 4, 'n'),
(162, 'ZF673', 888, 1, 'a'),
(163, 'BDOZ', 889, 1, 'a'),
(164, 'VFSVB', 890, 1, 'a'),
(165, 'FSVB', 891, 10, 'n'),
(166, 'PLST', 901, 1, 'a'),
(167, 'VKAKL', 902, 1, 'a'),
(168, 'KAKL', 903, 10, 'n'),
(169, 'VZUKB', 913, 1, 'a'),
(170, 'ZUKB', 914, 10, 'n'),
(171, 'GEBD', 924, 8, 'n'),
(172, 'GEBP', 932, 8, 'n'),
(173, 'AKZKB', 940, 2, 'n'),
(174, 'RESE174', 942, 59, 'a'),
(175, 'REFN1', 1001, 30, 'a'),
(176, 'RESE176', 1031, 19, 'a'),
(177, 'BSBKFZ', 1050, 10, 'n'),
(178, 'BSBWR', 1060, 10, 'n'),
(179, 'BSBSB', 1070, 10, 'n'),
(180, 'SB681', 1080, 10, 'n'),
(181, 'SB682', 1090, 10, 'n'),
(182, 'BZUKG', 1100, 10, 'n'),
(183, 'MAKBG', 1110, 10, 'n'),
(184, 'MABST', 1120, 10, 'n'),
(185, 'ZUCSG', 1130, 10, 'n'),
(186, 'GUTSC', 1140, 10, 'n'),
(187, 'MARAB', 1150, 10, 'n'),
(188, 'SBK00', 1160, 1, 'a'),
(189, 'SBK15', 1161, 1, 'a'),
(190, 'SBK20', 1162, 1, 'a'),
(191, 'SBKDW', 1163, 1, 'a'),
(192, 'AKKFZ', 1164, 10, 'n'),
(193, 'KEAKFZ', 1174, 10, 'n'),
(194, 'AGLAG', 1184, 10, 'n'),
(195, 'RESE195', 1194, 307, 'a'),
(210, 'RESE210', 3346, 155, 'a'),
]
# Kinderblock (E.14, Felder 196209): je Kind 123 Stellen ab Pos. 1501
# (Block 1 = 15011623 … Block 15 = 32233345; Positionen aus den
# Feldlängen arithmetisch abgeleitet, PDF-verifiziert).
_KINDER_FELDER = [
(196, 'KFAM', 0, 30, 'a'), # Familienname
(197, 'KVON', 30, 30, 'a'), # Vorname
(198, 'KSTAAT', 60, 3, 'a'), # Staatsangehörigkeit
(199, 'KSTWE', 63, 1, 'a'), # 2. Staatsangehörigkeit
(200, 'KVSNR', 64, 10, 'n'), # VSNR des Kindes
(201, 'KGEBD', 74, 8, 'n'), # TTMMJJJJ
(202, 'KAFBZ', 82, 1, 'a'), # Kennzeichen FB+
(203, 'KAPFB', 83, 1, 'a'),
(204, 'KAUHZ', 84, 1, 'a'),
(205, 'KBGFP', 85, 2, 'n'), # FB+ ab Monat
(206, 'KEGFP', 87, 2, 'n'), # FB+ bis Monat
(207, 'KBHFP', 89, 2, 'n'), # halber FB+ ab Monat
(208, 'KEHFP', 91, 2, 'n'), # halber FB+ bis Monat
(209, 'RESEK', 93, 30, 'a'),
]
def _kinderblock_struktur():
felder = []
for kind_nr in range(1, 16):
basis = 1501 + (kind_nr - 1) * 123
for nr, name, offset, laenge, typ in _KINDER_FELDER:
felder.append(
(nr, '%s_%d' % (name, kind_nr), basis + offset,
laenge, typ))
return felder
L16_MITTEILUNGSSATZ += _kinderblock_struktur()
# ------------------------------------------------------------------
# Adresse der Arbeitsstätte § 34 Abs 6 ASVG (E.20; SART 45, Länge 300,
# Version 02; BEST AD, VSTR ST)
# ------------------------------------------------------------------
ARBEITSSTAETTE_SATZ = [
(2, 'KVTR', 21, 2, 'a'), # KV-Träger (aus BKNR-Präfix)
(3, 'BKNR', 23, 10, 'a'),
(4, 'VSNR', 33, 10, 'n'),
(5, 'BZVM', 43, 2, 'n'), # Beitragszeit von MM
(6, 'BZBM', 45, 2, 'n'), # Beitragszeit bis MM
(7, 'BZJJ', 47, 4, 'n'), # Beitragsjahr
(8, 'FAMS', 51, 2, 'a'), # Finanzamtsnummer meldende Stelle
(9, 'STNR', 53, 9, 'n'), # Steuernummer DG
(10, 'ASTR', 62, 50, 'a'), # Straße der Arbeitsstätte
(11, 'ANRV', 112, 5, 'a'), # Hausnummer von
(12, 'ANRB', 117, 5, 'a'), # Hausnummer bis
(13, 'ASTG', 122, 5, 'a'), # Stiege
(14, 'ATOP', 127, 5, 'a'), # Tür/Top
(15, 'APLZ', 132, 9, 'a'), # PLZ der Arbeitsstätte
(16, 'AORT', 141, 40, 'a'), # Ort
(17, 'AGKZ', 181, 5, 'n'), # Gemeindekennziffer
(18, 'APOL', 186, 50, 'a'), # Politische Gemeinde/Staat
(19, 'AUDT', 236, 8, 'n'), # TTMMJJJJ
(20, 'WOBD', 244, 12, 'a'),
(21, 'ZOBD', 256, 15, 'a'),
(22, 'REFN', 271, 30, 'a'),
]
class L10nAtPayrollL16Line(models.Model):
_name = 'l10n.at.payroll.l16.line'
_description = 'Lohnzettel Finanz — Mitteilungssatz-Daten je AN'
_order = 'employee_name'
l16_id = fields.Many2one(
'l10n.at.payroll.l16', required=True, ondelete='cascade',
index=True)
employee_id = fields.Many2one('hr.employee', required=True)
employee_name = fields.Char(related='employee_id.name',
readonly=True, store=True)
versicherungsnummer = fields.Char()
soziale_stellung = fields.Char(help='08 (D.33)')
belz = fields.Char(string='LZP ab (TTMM)')
enlz = fields.Char(string='LZP bis (TTMM)')
kz210 = fields.Monetary(currency_field='currency_id')
kz215 = fields.Monetary(currency_field='currency_id')
kz220 = fields.Monetary(currency_field='currency_id')
kz225 = fields.Monetary(currency_field='currency_id')
kz226 = fields.Monetary(currency_field='currency_id')
kz230 = fields.Monetary(currency_field='currency_id')
kz243 = fields.Monetary(currency_field='currency_id')
kz245 = fields.Monetary(currency_field='currency_id')
lohnsteuer = fields.Monetary(currency_field='currency_id',
string='KZ 260 anrechenbare LSt')
beitraege = fields.Monetary(currency_field='currency_id',
string='Einbehaltene Beiträge (VIEB)')
fab_betrag = fields.Monetary(
currency_field='currency_id', string='Familienbonus Plus')
fab_kinder = fields.Integer(string='Anzahl Kinder FB+')
refn = fields.Char()
currency_id = fields.Many2one(
'res.currency', related='l16_id.company_id.currency_id')
def _kz_salden(self, slips):
"""KZ-Summen je AN aus validierten Läufen. **KZ 210 = Brutto-
bezüge gem. § 25 gesamt** (laufende UND sonstige §-67-Bezüge,
ohne § 26/§ 3 Abs 1 Z 16b DM-ORG E.14 Feldbeschreibung), denn
KZ 245 = 210 215 220 230 243 ist die laufende Steuer-
bemessung: Gesamtbrutto §-68-Freie sonstige Bezüge
laufende SV-DN. 215 aus `_l10n_at_freibetraege_68` des
GP3-Kerns (Monats-Caps 400/600 und ÜStd 15/170 eingerechnet);
220 = sonstige § 67 Abs 1/2/5 ohne Abs-3/6; 225 = SV-DN auf
KZ 220; 230 = laufende SV-DN + AK + WF; VIEB = 230 + 225 +
226; 260/BIEL = LST_LFD + LST_SZ + LST_ABF + LST_ABF_FREI."""
self.ensure_one()
salden = dict.fromkeys(
('kz210', 'kz215', 'kz220', 'kz225', 'kz226', 'kz230',
'kz243', 'kz245', 'lohnsteuer', 'beitraege'), 0.0)
for slip in slips:
for line in slip.line_ids:
rule = line.salary_rule_id
if not line.total:
continue
if rule.l10n_at_bemessung == 'laufend':
salden['kz210'] += line.total
elif (rule.l10n_at_bemessung == 'sonstig'
and rule.l10n_at_s67_abs == 'keine'):
salden['kz210'] += line.total
salden['kz220'] += line.total
elif line.code == 'SVDN_SZ':
salden['kz225'] += line.total
elif line.code in ('SVDN_KV', 'SVDN_PV', 'SVDN_AV',
'SVDN_AK', 'SVDN_WF'):
salden['kz230'] += line.total
elif line.code in ('LST_LFD', 'LST_SZ', 'LST_ABF',
'LST_ABF_FREI'):
salden['lohnsteuer'] += line.total
freibe = slip._l10n_at_freibetraege_68()
salden['kz215'] += freibe['szfn'] + freibe['ue_std']
salden['beitraege'] = salden['kz230'] + salden['kz225'] \
+ salden['kz226']
salden['kz245'] = (salden['kz210'] - salden['kz215']
- salden['kz220'] - salden['kz230']
- salden['kz243'])
return salden
def _soziale_stellung(self, employee):
"""D.33: 1 Lehrling, 2 Arbeiter, 3 Angestellte (0 sonstige) —
MVP: Stand des aktuellen Vertrags."""
version = employee.version_id
if version and getattr(version, 'l10n_at_lehrling', False):
return '1'
if version and getattr(
version, 'l10n_at_personalgruppe', None) == 'arbeiter':
return '2'
return '3'
class L10nAtPayrollL16(models.Model):
_name = 'l10n.at.payroll.l16'
_description = 'Lohnzettel Finanz (L16) & Arbeitsstättenmeldung'
_inherit = ['mail.thread']
_order = 'jahr desc, id desc'
company_id = fields.Many2one(
'res.company', required=True,
default=lambda self: self.env.company)
jahr = fields.Integer(required=True, string='Lohnzahlungszeitraum')
art = fields.Selection(
[('01', 'Lohnzettel (Jahresbeleg)'),
('18', 'Berichtigter Lohnzettel (binnen 2 Wochen)')],
required=True, default='01', tracking=True)
line_ids = fields.One2many(
'l10n.at.payroll.l16.line', 'l16_id', string='Mitteilungssätze')
datei_l16 = fields.Binary(
attachment=True, string='L16-Datei', copy=False)
datei_l16_name = fields.Char(copy=False)
datei_arbeitsstaette = fields.Binary(
attachment=True, string='Datei Arbeitsstättenmeldung',
copy=False)
datei_arbeitsstaette_name = fields.Char(copy=False)
protokoll = fields.Text(
string='Prüfprotokoll',
help='Formale Prüfung (Satzlängen, KZ-245-Formel, VSNR-Format, '
'Pflicht-Stammdaten) und Annahmen des Laufes; die '
'Fachprüfung bleibt der Clearingstelle (Mitteilungsfile, '
'GP8-Rest).')
state = fields.Selection(
[('draft', 'Entwurf'), ('done', 'Erzeugt')], default='draft',
required=True, tracking=True)
_sql_constraints = [(
'l16_company_jahr_art_uniq', 'unique(company_id, jahr, art)',
'Für dieses Jahr existiert bereits eine L16-Deklaration dieser '
'Art.')]
def _l10n_at_slips(self):
self.ensure_one()
return self.env['hr.payslip'].search([
('company_id', '=', self.company_id.id),
('date_from', '>=', date(self.jahr, 1, 1)),
('date_from', '<=', date(self.jahr, 12, 31)),
('state', 'in', ('validated', 'paid')),
])
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}
@api.model
def _vorzeichen(self, betrag):
"""V-Feld: blank bei 0, sonst '+'/'-' (L1-Feldregeln)."""
if not betrag:
return None
return '+' if betrag > 0 else '-'
def _l10n_at_l1_satz(self, line, stnr, heute, jetzt):
self.ensure_one()
employee = line.employee_id
version = employee.version_id
satz = L10nAtEldaSatz(
L10N_AT_ELDA_IDTEIL + L16_MITTEILUNGSSATZ, 3500,
bezeichnung='L1 %s' % employee.name)
satz.setze('SART', 'L1')
satz.setze('VSTR', '94')
satz.setze('FSART', 'L')
satz.setze('STNRL', stnr)
satz.setze('STNRA', stnr)
satz.setze_datum('DTUE', heute, 'JJJJMMTT')
satz.setze_zeit('ZTUE', jetzt)
satz.setze('REFN', line.refn)
satz.setze('ARTL', self.art)
if self.art == '18':
satz.setze('KORR', 'K')
satz.setze('FIND', 'J')
satz.setze('BELZ', line.belz)
satz.setze('ENLZ', line.enlz)
satz.setze('JALZ', self.jahr)
satz.setze('SOZS', line.soziale_stellung)
satz.setze('AVLN', (line.versicherungsnummer or '')[:4])
satz.setze_datum('AGBD', employee.birthday, 'TTMMJJ')
satz.setze('ANAM', (employee.name or '')[:37])
satz.setze('AADR', (version.private_street or '')[:37]
if version else '')
satz.setze('ALKZ', (version.private_country_id.code
if version and version.private_country_id
else 'AUT'))
satz.setze('APLZ', version.private_zip if version else '')
satz.setze('AORT', (version.private_city or '')[:30]
if version else '')
if version and version.sex == 'female':
satz.setze('GESW', 'J')
elif version and version.sex == 'male':
satz.setze('GESM', 'J')
elif version and version.sex:
satz.setze('GESD', 'J')
vollzeit = bool(version and version.work_time_rate >= 1.0)
satz.setze('VOLL', 'J' if vollzeit else 'N')
satz.setze('TEIL', 'N' if vollzeit else 'J')
if version and version.l10n_at_familienabsetzbetrag == 'avab':
satz.setze('AVAB', 'J')
elif version \
and version.l10n_at_familienabsetzbetrag == 'avb':
satz.setze('AEAB', 'J')
for betrag, vfeld, bfeld in (
(line.kz210, 'V210', 'B210'),
(line.kz215, 'V215', 'B215'),
(line.kz220, 'V220', 'B220'),
(line.kz225, 'V225', 'B225'),
(line.kz226, 'V226', 'B226'),
(line.kz230, 'V230', 'B230'),
(line.kz243, 'V243', 'B243'),
(line.kz245, 'V245', 'B245'),
(line.beitraege, 'VIEB', 'BIEB'),
(line.lohnsteuer, 'VIEL', 'BIEL'),
(line.lohnsteuer, 'V260', 'B260')):
satz.setze(vfeld, self._vorzeichen(betrag))
satz.setze(bfeld, abs(betrag) if betrag else 0, cent=True)
satz.setze('WOBD', 'EMP-%d' % employee.id)
if version and version.children:
satz.setze('ANZK', version.children)
# Familienbonus Plus aus der Kind-Stammdaten-Wartung (keine
# Personalverrechnungs-Komponente — Nutzer-Entscheidung).
if line.fab_kinder:
satz.setze('BFABO', 'J')
satz.setze('KFABO', line.fab_kinder)
satz.setze('VFABO', self._vorzeichen(line.fab_betrag))
satz.setze('FABO', abs(line.fab_betrag) if line.fab_betrag
else 0, cent=True)
kinder = self.env['l10n.at.payroll.kind'].search([
('employee_id', '=', employee.id),
('fab_plus', '=', True)])
for nummer, kind in enumerate(kinder[:15], start=1):
satz.setze('KFAM_%d' % nummer,
(kind.familienname or '')[:30])
satz.setze('KVON_%d' % nummer, (kind.vorname or '')[:30])
satz.setze('KSTAAT_%d' % nummer, kind.staatsangehoerigkeit)
satz.setze('KSTWE_%d' % nummer,
kind.staatsangehoerigkeit_2)
satz.setze('KVSNR_%d' % nummer, kind.versicherungsnummer)
satz.setze_datum('KGEBD_%d' % nummer, kind.geburtstag,
'TTMMJJJJ')
satz.setze('KAFBZ_%d' % nummer, 'J')
satz.setze('KBGFP_%d' % nummer, kind.fab_monat_von)
satz.setze('KEGFP_%d' % nummer, kind.fab_monat_bis)
satz.setze('KBHFP_%d' % nummer, kind.fab_halb_monat_von)
satz.setze('KEHFP_%d' % nummer, kind.fab_halb_monat_bis)
return satz
def _l10n_at_l16_bestand(self, erstellungsdatum=None):
self.ensure_one()
heute = erstellungsdatum or date.today()
jetzt = datetime.now()
partner = self.company_id.partner_id
stnr = self.company_id.l10n_at_lohnsteuernummer
informationssatz = L10nAtEldaSatz(
L10N_AT_ELDA_IDTEIL + L16_INFORMATIONSSATZ, 1100,
bezeichnung='L16-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', self.jahr)
informationssatz.setze('GESA', len(self.line_ids))
informationssatz.setze('ANZA', len(self.line_ids))
datensaetze = [informationssatz] + [
self._l10n_at_l1_satz(line, stnr, heute, jetzt)
for line in self.line_ids]
return L10nAtEldaBestand(
'LF', '94', '28', hersteller=self._l10n_at_hersteller(),
datensaetze=datensaetze)
def _l10n_at_arbeitsstaette_bestand(self, erstellungsdatum=None):
"""§ 34 Abs 6 ASVG je AN mit mBGM-Bezug: Adresse der
Arbeitsstätte am 31. 12. (Dienstort-Partner + GKZ)."""
self.ensure_one()
heute = erstellungsdatum or date.today()
bknr = self.company_id.l10n_at_beitragskontonummer
stnr = self.company_id.l10n_at_lohnsteuernummer
datensaetze = []
warnungen = []
for line in self.line_ids:
employee = line.employee_id
dienstort = employee.address_id
if not dienstort or not dienstort.l10n_at_gemeindekennziffer:
warnungen.append(
'Arbeitsstätte %s: Dienstort-Partner bzw. '
'Gemeindekennziffer fehlt (res.partner.'
'l10n_at_gemeindekennziffer am Dienstort-Partner '
'pflegen).' % employee.name)
satz = L10nAtEldaSatz(
L10N_AT_ELDA_IDTEIL + ARBEITSSTAETTE_SATZ, 300,
bezeichnung='Arbeitsstätte %s' % employee.name)
satz.setze('SART', '45')
satz.setze('VSTR', 'ST')
satz.setze('KVTR', (bknr or '')[:2])
satz.setze('BKNR', bknr)
satz.setze('VSNR', line.versicherungsnummer)
satz.setze('BZVM', (line.belz or '01')[:2])
satz.setze('BZBM', (line.enlz or '12')[:2])
satz.setze('BZJJ', self.jahr)
satz.setze('FAMS', (stnr or '')[:2])
satz.setze('STNR', stnr)
satz.setze('ASTR', (dienstort.street if dienstort else '')
[:50])
satz.setze('APLZ', dienstort.zip if dienstort else '')
satz.setze('AORT', (dienstort.city if dienstort else '')[:40])
satz.setze('AGKZ',
dienstort.l10n_at_gemeindekennziffer
if dienstort else None)
satz.setze('APOL', (dienstort.city if dienstort else '')[:50])
satz.setze_datum('AUDT', heute, 'TTMMJJJJ')
satz.setze('WOBD', 'EMP-%d' % employee.id)
datensaetze.append(satz)
if not datensaetze:
return None, warnungen
return L10nAtEldaBestand(
'AD', 'ST', '02', hersteller=self._l10n_at_hersteller(),
datensaetze=datensaetze), warnungen
def action_erzeugen(self):
"""Lauf: KZ-Daten je AN aus validierten Läufen sammeln, formale
Prüfung (KZ-245-Formel, Pflicht-Stammdaten), Dateien erzeugen.
Fehler brechen ab (UserError, DmfA-Muster listenweise);
Warnungen landen im Prüfprotokoll."""
for deklaration in self:
fehler = []
warnungen = []
company = deklaration.company_id
if not company.l10n_at_lohnsteuernummer:
fehler.append('Lohnsteuernummer der Company fehlt '
'(l10n_at_lohnsteuernummer, 9-stellig).')
if not company.l10n_at_beitragskontonummer:
fehler.append('Beitragskontonummer der Company fehlt '
'(l10n_at_beitragskontonummer, BKNR).')
slips = deklaration._l10n_at_slips()
nach_mitarbeiter = {}
for slip in slips:
nach_mitarbeiter.setdefault(slip.employee_id, []) \
.append(slip)
deklaration.line_ids.unlink()
zeilen_werte = []
for employee, employee_slips in nach_mitarbeiter.items():
version = employee.version_id
if not (version and version.ssnid):
fehler.append(
'Mitarbeiter %s ohne Versicherungsnummer '
'(hr.version.ssnid).' % employee.name)
continue
if not employee.birthday:
fehler.append(
'Mitarbeiter %s ohne Geburtsdatum.' % employee.name)
continue
line = deklaration.env['l10n.at.payroll.l16.line'].new({
'employee_id': employee.id})
salden = line._kz_salden(employee_slips)
erste = min(s.date_from for s in employee_slips)
letzte = max(s.date_to for s in employee_slips)
kinder = deklaration.env['l10n.at.payroll.kind'].search([
('employee_id', '=', employee.id),
('fab_plus', '=', True)])
zeilen_werte.append({
'employee_id': employee.id,
'versicherungsnummer': version.ssnid,
'soziale_stellung':
line._soziale_stellung(employee),
'belz': erste.strftime('%d%m'),
'enlz': letzte.strftime('%d%m'),
'refn': 'L16-%d-%d-%d' % (company.id,
deklaration.jahr,
employee.id),
'fab_kinder': len(kinder),
'fab_betrag': sum(
kind._l10n_at_fab_betrag(deklaration.jahr)
for kind in kinder),
**salden,
})
for werte in zeilen_werte:
deklaration.env['l10n.at.payroll.l16.line'].create(
dict(werte, l16_id=deklaration.id))
for line in deklaration.line_ids:
formel = (line.kz210 - line.kz215 - line.kz220
- line.kz230 - line.kz243)
if abs(formel - line.kz245) > 0.02:
fehler.append(
'%s: KZ-245-Formel verletzt (%.2f%.2f).'
% (line.employee_name, formel, line.kz245))
if line.kz245 < 0:
warnungen.append(
'%s: KZ 245 negativ (Bezüge übersteigen die '
'Abzüge) — FinOnl-Konsistenzprüfung prüft.'
% line.employee_name)
if fehler:
deklaration.protokoll = 'FEHLER:\n%s\nWARNUNGEN:\n%s' % (
'\n'.join(fehler), '\n'.join(warnungen) or '')
raise UserError('L16-Lauf mit Fehlern abgebrochen:\n'
+ '\n'.join(fehler))
if deklaration.line_ids:
datei = L10nAtEldaDatei(
[deklaration._l10n_at_l16_bestand()])
deklaration.write({
'datei_l16': base64.b64encode(
datei.render()),
'datei_l16_name': 'l16_%d_%s.txt' % (
deklaration.jahr,
deklaration.company_id.l10n_at_beitragskontonummer
or deklaration.company_id.id),
})
bestand, ast_warn = \
deklaration._l10n_at_arbeitsstaette_bestand()
warnungen.extend(ast_warn)
if bestand is not None:
datei = L10nAtEldaDatei([bestand])
deklaration.write({
'datei_arbeitsstaette': base64.b64encode(
datei.render()),
'datei_arbeitsstaette_name':
'arbeitsstaette_%d.txt' % deklaration.jahr,
})
deklaration.protokoll = 'WARNUNGEN:\n%s' % (
'\n'.join(warnungen) or '')
deklaration.state = 'done'
return True
@@ -0,0 +1,464 @@
# Part of the odoo-at-payroll project. License: LGPL-3.
"""Lohnzettel Finanz — Lohnnachweis/Lohnbescheinigung L17 (AP17) —
DM-ORG 42.7.0, Kapitel E.24 (BEST ``LF``, VSTR ``94``, Satzart ``A1``,
Länge 1100, Version 28 zwingender Einsatz ab 01.03.2026, fachliche
Gültigkeit für Zeiträume ab 01.01.2025; PDF-verifizierte ODL-Extraktion
2026-09-16 aus `.firecrawl/ris/elda/DM-ORG_*v42.7.0*.pdf`).
Der A1-Satz ist je **Lohnzahlungszeitraum** je Dienstnehmer aufzubauen
(D.32: BELZ/ENLZ/JALZ; REFN je AG+Jahr+Dienstverhältnis eindeutig und
auch bei Zeiträumsänderung unverändert). Kennzahlen-Semantik laut
Prüfkatalog L17 (Stand 12/2025, lokal `.firecrawl/ris/elda/`):
**KZ 350 = Bruttobezüge GESAMT**; die Teil-KZs 351 (Zuwendungen neben
Arbeitslohn), 352 (Abfertigungen/Abfindungen), 354 (normale ÜStd-
Zuschläge), 356/738 (Pensionsabfindungen), UEB, 394, KUR, ALT, SOP,
AUS, ZAKV, MAGB sind **Teilmengen** von 350 (Prüfkatalog-Formel:
Feld 28 < (Feld 30 + 32 + 34 + 36 + 38 + 50 + 72 + 74 + 76 + 80 + 82
+ 95 + 100) = Fehler). KZ 738/739 sind nur für Jahre bis 2017 zulässig
(Pensionsabfindung Pensionskasse).
KZ-Mapping aus den validierten Läufen (GP3-Kern-Flags, Muster
`lohnzettel_l16.py`):
- 350: laufende Bezüge + alle sonstigen §-67-Bezüge (gesamtes §-25-Brutto
des LZP inkl. 351/352/354)
- 351: sonstige Bezüge ohne Abs-3/6-Markierung (Sechstel-Route
Sonderzahlungen und Zuwendungen)
- 352: sonstige Bezüge mit ``l10n_at_s67_abs`` = abs3/abs6
(Abfertigung Alt / freiwillige Abfertigung / Abfindungen)
- 354: § 68-freie Überstundenzuschläge (``_l10n_at_freibetraege_68``)
- 357: einbehaltene SV-Beiträge des laufenden Arbeitslohns
(SVDN_KV/PV/AV/AK/WF)
- 347: einbehaltene SV-Beiträge für Zuwendungen (SVDN_SZ)
- nicht automatisiert (dokumentierter Rest, Grundstellung 0 + Protokoll-
Hinweis): 356/738 Pensionsabfindungen, 736/737/739 SV-Zerlegung auf
Abfindungs-KZs, BUEB, 358, BSPE, BARB, WERK, UEBMO, ANZAT, TSTAAT,
TEPR/MAPR/ZAKV/ANKV/MAGB, KOUN, 394, KUR, ALT, SOP, AUS.
Formale Prüfungen nach Prüfkatalog L17: KZ-350-Formel, 357 40 % × 350,
347 25 % × 351, 354 2.400 (+1 %) je LZP aliquotiert, HOPA 3 ×
HOTA und 300 je LZP, DIEN 99, WERK/UEBMO 12, ANZAT 366,
HOTA LZP-Tage."""
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
# ------------------------------------------------------------------
# Mitteilungssatz A1 (E.24; Länge 1100) — Datenteil
# ------------------------------------------------------------------
L17_SATZ = [
(2, 'FSART', 21, 1, 'a'), # A
(3, 'CLADR', 22, 6, 'a'),
(4, 'CLDVR', 28, 7, 'a'),
(6, 'STNRL', 35, 9, 'n'), # Steuernummer Lohnverrechnungsstelle
(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+DV eindeutig
(12, 'RE12', 96, 2, 'a'),
(13, 'FEHL', 98, 20, 'a'),
(14, 'STAT', 118, 1, 'a'),
(15, 'FIND', 119, 1, 'a'), # 'J' = vom Hersteller für richtig
(16, 'BELZ', 120, 4, 'n'), # Beginn LZP TTMM
(17, 'ENLZ', 124, 4, 'n'), # Ende LZP TTMM
(18, 'JALZ', 128, 4, 'n'), # Jahr LZP
(19, 'PENS', 132, 1, 'a'), # J/N
(20, 'AVSN', 133, 10, 'n'), # LLLPTTMMJJ
(21, 'ANAM', 143, 37, 'a'),
(22, 'ATIT', 180, 20, 'a'),
(23, 'AADR', 200, 37, 'a'),
(24, 'ALKZ', 237, 3, 'a'),
(25, 'APLZ', 240, 10, 'a'),
(26, 'AORT', 250, 30, 'a'),
(27, 'V350', 280, 1, 'a'),
(28, 'B350', 281, 10, 'n'), # Bruttobezüge gesamt (KZ 350)
(29, 'V351', 291, 1, 'a'),
(30, 'B351', 292, 10, 'n'), # Zuwendungen neben Arbeitslohn
(31, 'V352', 302, 1, 'a'),
(32, 'B352', 303, 10, 'n'), # Abfertigungen und Abfindungen
(33, 'V354', 313, 1, 'a'),
(34, 'B354', 314, 10, 'n'), # (normale) ÜStd-Zuschläge
(35, 'V356', 324, 1, 'a'),
(36, 'B356', 325, 10, 'n'), # Pensionsabfindung Arbeitgeber
(37, 'V738', 335, 1, 'a'),
(38, 'B738', 336, 10, 'n'), # Pensionsabfindung Pensionskasse
(39, 'V357', 346, 1, 'a'),
(40, 'B357', 347, 10, 'n'), # SV-Beiträge laufender Arbeitslohn
(41, 'V347', 357, 1, 'a'),
(42, 'B347', 358, 10, 'n'), # SV-Beiträge für Zuwendungen
(43, 'V736', 368, 1, 'a'),
(44, 'B736', 369, 10, 'n'), # SV-Beiträge für KZ 352
(45, 'V737', 379, 1, 'a'),
(46, 'B737', 380, 10, 'n'), # SV-Beiträge für KZ 356
(47, 'V739', 390, 1, 'a'),
(48, 'B739', 391, 10, 'n'), # SV-Beiträge für KZ 738
(49, 'VUEB', 401, 1, 'a'),
(50, 'BUEB', 402, 10, 'n'), # übrige Bezüge außerhalb Österreichs
(51, 'RE51', 412, 1, 'a'),
(52, 'RE52', 413, 10, 'a'),
(53, 'RE53', 423, 1, 'a'),
(54, 'RE54', 424, 10, 'a'),
(55, 'V358', 434, 1, 'a'),
(56, 'B358', 435, 10, 'n'), # anrechenbare ausländische Steuer
(57, 'VSPE', 445, 1, 'a'),
(58, 'BSPE', 446, 10, 'n'), # Spesensätze außerhalb KZ 350
(59, 'VARB', 456, 1, 'a'),
(60, 'BARB', 457, 10, 'n'), # AG-Beiträge an Pensionskassen
(61, 'DIEN', 467, 2, 'n'), # Dienstzeit (Jahre) bei KZ 352
(62, 'STDT', 469, 8, 'n'), # Sterbedatum JJJJMMTT
(63, 'OELD', 477, 20, 'a'), # Ordnungsbegriff ELDA
(64, 'KORR', 497, 1, 'a'), # 'K'/'S'
(65, 'INTE', 498, 11, 'a'),
(66, 'WOBD', 509, 12, 'a'),
(67, 'ZOBD', 521, 15, 'a'),
(68, 'WAHR', 536, 3, 'a'), # Währung
(69, 'MUEB', 539, 2, 'n'), # Monate überwiegende Nachtarbeit
(70, 'V394', 541, 1, 'a'),
(71, 'B394', 542, 10, 'n'), # Sonn-/Feiertags-/Nachtzuschläge
(72, 'VKUR', 552, 1, 'a'),
(73, 'BKUR', 553, 10, 'n'), # Kurzarbeit
(74, 'VALT', 563, 1, 'a'),
(75, 'BALT', 564, 10, 'n'), # Altersteilzeit
(76, 'RE76', 574, 1, 'a'),
(77, 'RE77', 575, 10, 'n'),
(78, 'VSOP', 585, 1, 'a'),
(79, 'BSOP', 586, 10, 'n'), # Sozialplanzahlungen
(80, 'VAUS', 596, 1, 'a'),
(81, 'BAUS', 597, 10, 'n'), # begünstigte Auslandstätigkeit
(82, 'WERK', 607, 2, 'n'), # Werkverkehr (Monate)
(83, 'UEBMO', 609, 2, 'n'), # Überlassung Kfz (Monate)
(84, 'ANZAT', 611, 3, 'n'), # Arbeitstage begünstigte Auslandstätigk.
(85, 'HOTA', 614, 3, 'n'), # Telearbeitstage (Homeoffice)
(86, 'VHOPA', 617, 1, 'a'),
(87, 'HOPA', 618, 10, 'n'), # Homeoffice-Pauschale (§ 26 Z 9 lit. a)
(88, 'VKOUN', 628, 1, 'a'),
(89, 'KOUN', 629, 10, 'n'), # Kostenübernahme § 26 Z 5 lit. b
(90, 'TSTAAT', 639, 3, 'a'), # Tätigkeitsstaat
(91, 'VTEPR', 642, 1, 'a'),
(92, 'TEPR', 643, 10, 'n'), # Teuerungsprämie (§ 124b Z 408)
(93, 'VMAPR', 653, 1, 'a'),
(94, 'MAPR', 654, 10, 'n'), # Mitarbeiterprämie (§ 124b Z 478)
(95, 'VZAKV', 664, 1, 'a'),
(96, 'ZAKV', 665, 10, 'n'), # AG-Zuschuss freiw. KV
(97, 'ANKV', 675, 1, 'a'), # (freiw.) gesetzlich krankenversichert
(98, 'VMAGB', 676, 1, 'a'),
(99, 'MAGB', 677, 10, 'n'), # Mitarbeitergewinnbeteiligung
(100, 'RE100', 687, 314, 'a'),
(101, 'REFN2', 1001, 30, 'a'), # Softwarehersteller-REFN
(102, 'RE102', 1031, 70, 'a'),
]
class L10nAtPayrollL17Line(models.Model):
_name = 'l10n.at.payroll.l17.line'
_description = 'L17 — Lohnnachweis je Dienstnehmer und LZP'
_order = 'employee_name, date_from'
l17_id = fields.Many2one(
'l10n.at.payroll.l17', required=True, ondelete='cascade',
index=True)
employee_id = fields.Many2one('hr.employee', required=True)
employee_name = fields.Char(related='employee_id.name',
readonly=True, store=True)
date_from = fields.Date(string='LZP von', required=True)
date_to = fields.Date(string='LZP bis', required=True)
versicherungsnummer = fields.Char()
kz350 = fields.Monetary(currency_field='currency_id')
kz351 = fields.Monetary(currency_field='currency_id')
kz352 = fields.Monetary(currency_field='currency_id')
kz354 = fields.Monetary(currency_field='currency_id')
kz357 = fields.Monetary(currency_field='currency_id')
kz347 = fields.Monetary(currency_field='currency_id')
hota = fields.Integer(string='Homeoffice-Tage')
hopa = fields.Monetary(currency_field='currency_id')
mueb = fields.Integer(string='Monate überwiegende Nachtarbeit')
refn = fields.Char()
currency_id = fields.Many2one(
'res.currency', related='l17_id.company_id.currency_id')
def _l10n_at_pruefungen(self, fehler, warnungen):
"""Formale Prüfungen nach Prüfkatalog L17 (Stand 12/2025)."""
self.ensure_one()
lzp_tage = max(1, (self.date_to - self.date_from).days + 1)
teilsumme = self.kz351 + self.kz352 + self.kz354
if self.kz350 + 0.02 < teilsumme:
fehler.append(
'%s (%s): KZ 350 < 351+352+354 (Prüfkatalog-Formel).'
% (self.employee_name, self.date_from))
if self.kz357 > self.kz350 * 0.4 + 0.02:
fehler.append(
'%s (%s): KZ 357 > 40 %% von KZ 350 (Prüfkatalog).'
% (self.employee_name, self.date_from))
if self.kz351 and self.kz347 > self.kz351 * 0.25 + 0.02:
fehler.append(
'%s (%s): KZ 347 > 25 %% von KZ 351 (Prüfkatalog).'
% (self.employee_name, self.date_from))
if self.kz354 > 2400.0 * lzp_tage / 365.0 * 1.01 + 0.02:
warnungen.append(
'%s (%s): KZ 354 übersteigt den aliquotierten '
'Prüfkatalog-Cap 2.400 € je Jahr (+1 %% Toleranz).'
% (self.employee_name, self.date_from))
if self.hota > lzp_tage:
fehler.append(
'%s (%s): HOTA > LZP-Tage.' % (
self.employee_name, self.date_from))
if self.hopa > self.hota * 3.0 + 0.02:
fehler.append(
'%s (%s): HOPA > 3 € × HOTA.' % (
self.employee_name, self.date_from))
if self.hopa > 300.0 * lzp_tage / 365.0 * 1.01 + 0.02:
warnungen.append(
'%s (%s): HOPA übersteigt den aliquotierten '
'Prüfkatalog-Cap 300 € je Jahr.' % (
self.employee_name, self.date_from))
class L10nAtPayrollL17(models.Model):
_name = 'l10n.at.payroll.l17'
_description = 'L17 — Lohnnachweis/Lohnbescheinigung (Finanz)'
_inherit = ['mail.thread']
_order = 'jahr desc, id desc'
company_id = fields.Many2one(
'res.company', required=True,
default=lambda self: self.env.company)
jahr = fields.Integer(required=True, string='Jahr')
line_ids = fields.One2many(
'l10n.at.payroll.l17.line', 'l17_id', string='A1-Sätze')
datei = fields.Binary(attachment=True, string='L17-Datei', copy=False)
datei_name = fields.Char(copy=False)
protokoll = fields.Text(
string='Prüfprotokoll',
help='Formale Prüfung nach Prüfkatalog L17 (Stand 12/2025) und '
'dokumentierte Reste der KZ-Belegung.')
state = fields.Selection(
[('draft', 'Entwurf'), ('done', 'Erzeugt')], default='draft',
required=True, tracking=True)
_sql_constraints = [(
'l17_company_jahr_uniq', 'unique(company_id, jahr)',
'Für dieses Jahr existiert bereits eine L17-Deklaration.')]
def _l10n_at_slips(self):
self.ensure_one()
return self.env['hr.payslip'].search([
('company_id', '=', self.company_id.id),
('date_from', '>=', date(self.jahr, 1, 1)),
('date_from', '<=', date(self.jahr, 12, 31)),
('state', 'in', ('validated', 'paid')),
], order='date_from, id')
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}
@api.model
def _vorzeichen(self, betrag):
if not betrag:
return None
return '+' if betrag > 0 else '-'
def _l10n_at_kz_salden(self, slip):
"""(salden, hota) je Lohnzahlungszeitraum aus dem Lauf —
Kennzahlen-Mapping siehe Modul-Doku (GP3-Flags, Muster
`_kz_salden` der L16-Zeile)."""
self.ensure_one()
salden = dict.fromkeys(
('kz350', 'kz351', 'kz352', 'kz354', 'kz357', 'kz347',
'hopa'), 0.0)
hota = 0
for line in slip.line_ids:
rule = line.salary_rule_id
if not line.total:
continue
if rule.l10n_at_bemessung == 'laufend':
salden['kz350'] += line.total
if line.code == 'SB_HOMEOFFICE':
salden['hopa'] += line.total
elif rule.l10n_at_bemessung == 'sonstig':
salden['kz350'] += line.total
if rule.l10n_at_s67_abs in ('abs3', 'abs6'):
salden['kz352'] += line.total
else:
salden['kz351'] += line.total
elif line.code == 'SVDN_SZ':
salden['kz347'] += line.total
elif line.code in ('SVDN_KV', 'SVDN_PV', 'SVDN_AV',
'SVDN_AK', 'SVDN_WF'):
salden['kz357'] += line.total
freibe = slip._l10n_at_freibetraege_68()
salden['kz354'] = freibe['ue_std']
for eingabe in slip.input_line_ids:
if eingabe.input_type_id.code == 'ATP_SB_HOMEOFFICE':
hota += int(eingabe.quantity or 0)
return salden, hota
def _l10n_at_a1_satz(self, line, stnr, heute, jetzt):
self.ensure_one()
employee = line.employee_id
version = employee.version_id
satz = L10nAtEldaSatz(
L10N_AT_ELDA_IDTEIL + L17_SATZ, 1100,
bezeichnung='A1 %s %s' % (employee.name, line.date_from))
satz.setze('SART', 'A1')
satz.setze('VSTR', '94')
satz.setze('FSART', 'A')
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('BELZ', line.date_from.strftime('%d%m'))
satz.setze('ENLZ', line.date_to.strftime('%d%m'))
satz.setze('JALZ', self.jahr)
satz.setze('PENS', 'N')
satz.setze('AVSN', line.versicherungsnummer)
satz.setze('ANAM', (employee.name or '')[:37])
satz.setze('AADR', (version.private_street or '')[:37]
if version else '')
satz.setze('ALKZ', (version.private_country_id.code
if version and version.private_country_id
else 'AUT'))
satz.setze('APLZ', version.private_zip if version else '')
satz.setze('AORT', (version.private_city or '')[:30]
if version else '')
for betrag, vfeld, bfeld, negativ in (
(line.kz350, 'V350', 'B350', False),
(line.kz351, 'V351', 'B351', False),
(line.kz352, 'V352', 'B352', True),
(line.kz354, 'V354', 'B354', True),
(line.kz357, 'V357', 'B357', False),
(line.kz347, 'V347', 'B347', True)):
if betrag < 0 and not negativ:
raise UserError(
'%s (%s): negativer Betrag in %s — das Vorzeichenfeld '
'zulässigerweise "-" nur nach DM-ORG-Feldbeschreibung.'
% (employee.name, line.date_from, vfeld))
satz.setze(vfeld, self._vorzeichen(betrag))
satz.setze(bfeld, abs(betrag) if betrag else 0, cent=True)
if line.mueb:
satz.setze('MUEB', line.mueb)
if line.hota:
satz.setze('HOTA', line.hota)
if line.hopa:
satz.setze('VHOPA', self._vorzeichen(line.hopa))
satz.setze('HOPA', abs(line.hopa), cent=True)
satz.setze('WOBD', 'EMP-%d' % employee.id)
satz.setze('REFN2', line.refn)
return satz
def action_erzeugen(self):
"""A1 je (Dienstnehmer, Lohnzahlungszeitraum) aus validierten
Läufen; formale Prüfungen nach Prüfkatalog L17; Fehler brechen
ab (UserError, DmfA-Muster), Warnungen ins Protokoll."""
for deklaration in self:
fehler = []
warnungen = []
company = deklaration.company_id
if not company.l10n_at_lohnsteuernummer:
fehler.append(
'Lohnsteuernummer der Company fehlt '
'(l10n_at_lohnsteuernummer).')
deklaration.line_ids.unlink()
for slip in deklaration._l10n_at_slips():
version = slip.version_id or slip.employee_id.version_id
if not (version and version.ssnid):
fehler.append(
'Mitarbeiter %s ohne Versicherungsnummer '
'(hr.version.ssnid).' % slip.employee_id.name)
continue
if not slip.employee_id.birthday:
fehler.append(
'Mitarbeiter %s ohne Geburtsdatum.'
% slip.employee_id.name)
continue
salden, hota = deklaration._l10n_at_kz_salden(slip)
refn = 'L17-%d-%d-%d-%s' % (
company.id, deklaration.jahr, slip.employee_id.id,
slip.date_from.strftime('%Y%m%d'))
line = deklaration.env['l10n.at.payroll.l17.line'].create({
'l17_id': deklaration.id,
'employee_id': slip.employee_id.id,
'date_from': slip.date_from,
'date_to': slip.date_to,
'versicherungsnummer': version.ssnid,
'kz350': salden['kz350'],
'kz351': salden['kz351'],
'kz352': salden['kz352'],
'kz354': salden['kz354'],
'kz357': salden['kz357'],
'kz347': salden['kz347'],
'hota': hota,
'hopa': salden['hopa'],
'mueb': 0,
'refn': refn,
})
line._l10n_at_pruefungen(fehler, warnungen)
if deklaration.line_ids:
for line in deklaration.line_ids:
if line.kz352:
warnungen.append(
'%s: KZ 352 belegt — die SV-Zerlegung auf '
'KZ 736 bleibt dokumentierter Rest (v1 = 0); '
'Prüfkatalog-Grenze 40 %% × KZ 352 prüfen.'
% line.employee_name)
if fehler:
deklaration.protokoll = 'FEHLER:\n%s\nWARNUNGEN:\n%s' % (
'\n'.join(fehler), '\n'.join(warnungen) or '')
raise UserError('L17-Lauf mit Fehlern abgebrochen:\n'
+ '\n'.join(fehler))
if deklaration.line_ids:
heute = date.today()
jetzt = datetime.now()
partner = company.partner_id
stnr = company.l10n_at_lohnsteuernummer
informationssatz = L10nAtEldaSatz(
L10N_AT_ELDA_IDTEIL + L16_INFORMATIONSSATZ, 1100,
bezeichnung='L17-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(deklaration.line_ids))
informationssatz.setze('ANZA', len(deklaration.line_ids))
datensaetze = [informationssatz] + [
deklaration._l10n_at_a1_satz(line, stnr, heute, jetzt)
for line in deklaration.line_ids]
bestand = L10nAtEldaBestand(
'LF', '94', '28', hersteller=deklaration
._l10n_at_hersteller(), datensaetze=datensaetze)
datei = L10nAtEldaDatei([bestand])
deklaration.write({
'datei': base64.b64encode(datei.render()),
'datei_name': 'l17_%d_%s.txt' % (
deklaration.jahr, stnr),
})
deklaration.protokoll = 'WARNUNGEN:\n%s' % (
'\n'.join(warnungen) or '')
deklaration.state = 'done'
return True
@@ -0,0 +1,151 @@
# Part of the odoo-at-payroll project. License: LGPL-3.
"""Meldewesen-Stammdaten (GP7/AP5): ELDA-/FinOnl-Identifikation der
Dienstgeberin und der Arbeitsstätten.
- ``res.company``: Beitragskontonummer (BKNR ELDA-Feld, Präfix =
KV-Träger) und Lohnsteuernummer (9-stellig: 2 Stellen Finanzamt + 7
Stellen Nummer; L16-I1 STNRA/STNRL, Arbeitsstättenmeldung STNR).
- ``res.partner``: österreichische Gemeindekennziffer (AGKZ) für die
Arbeitsstättenmeldung nach § 34 Abs 6 ASVG Odoo hält keine GKZ, das
Feld ist mandantenseitig zu pflegen (Warnung des L16-Laufs, wenn es am
Dienstort-Partner fehlt).
- ``hr.version``: Kennzeichen fallweise Beschäftigung (§ 33 Abs 1a
Z 1 ASVG tagesgenaue vereinfachte Anmeldung vor Arbeitsantritt,
DM-ORG BEST MA) und VSNR-Formatprüfung (10-stellig LLLPTTMMJJ;
Prüfzifferalgorithmus der SVS ist nicht beschafft nur Format/
Plausibilität, Rest dokumentiert).
- ``hr.departure.reason``: Mapping auf den eSV-Abmeldegrund (AGRD,
DM-ORG D.22 vollständige Code-Liste als Selection, Standard
``00`` sonstiger Grund mit Ende des Beschäftigungsverhältnisses;
die üblichen Abgangsursachen sind im Seed vorverbunden,
noupdate mandantenseitig ergänzte Ursachen bleiben unberührt)."""
from odoo import api, fields, models
from odoo.exceptions import ValidationError
# eSV-Abmeldegründe, DM-ORG 42.7.0 Kapitel D.22 (AGRD Abmeldegrund,
# Code) — verifiziert gegen .firecrawl/ris/elda/DM-ORG_v42.7.0.txt
# (42. Ergänzung 07/2026). Codes 26/28 existieren nicht (Lücken im
# Katalog), Code 00 = sonstiger Grund mit Ende des DV.
L10N_AT_AGRD = [
('01', '01 — Kündigung durch den Dienstgeber'),
('02', '02 — Kündigung durch den Dienstnehmer'),
('03', '03 — Einvernehmliche Lösung'),
('04', '04 — Zeitablauf'),
('05', '05 — Berechtigter vorzeitiger Austritt'),
('06', '06 — Fristlose Entlassung'),
('07', '07 — Karenz nach MSchG / VKG'),
('08', '08 — Präsenzdienstleistung im Bundesheer'),
('09', '09 — Zivildienst'),
('10', '10 — Pragmatisierung'),
('11', '11 — Länger als 1 Monat währender unbezahlter Urlaub'),
('12', '12 — Ummeldung'),
('13', '13 — Tod des Dienstnehmers'),
('14', '14 — Änderung der SV-Pflicht'),
('15', '15 — Truppenübung'),
('16', '16 — Pensionierung'),
('17', '17 — Ende freier Dienstvertrag (§ 4 Abs 4 ASVG)'),
('18', '18 — Enthebung von der Gerichtspraxis'),
('19', '19 — Unterbrechung der Gerichtspraxis'),
('20', '20 — Entlassung aus der Bundesbetreuung'),
('21', '21 — Kündigung durch DN während Elternteilzeit (MSchG/VKG)'),
('22', '22 — Unberechtigter vorzeitiger Austritt'),
('23', '23 — Bildungskarenz gemäß § 11 AVRAG'),
('24', '24 — Vorzeitiger Austritt gem. § 25 IO durch Dienstnehmer'),
('25', '25 — Kündigung gem. § 25 IO durch Insolvenzverwalter:in'),
('27', '27 — Kündigung gem. § 25 IO durch Dienstgeber'),
('29', '29 — SV-Ende Beschäftigung aufrecht'),
('30', '30 — Lösung in der Probezeit durch Dienstgeber'),
('31', '31 — Pflegekarenz (interner SV-Abmeldegrund)'),
('32', '32 — Bildungskarenz gemäß § 12 AVRAG'),
('33', '33 — Familienhospizkarenz (interner SV-Abmeldegrund)'),
('34', '34 — Lösung in der Probezeit durch Dienstnehmer'),
('00', '00 — Sonstiger Grund mit Ende des DV'),
]
class ResCompany(models.Model):
_inherit = 'res.company'
l10n_at_beitragskontonummer = fields.Char(
string='AT Beitragskontonummer (BKNR)',
help='eSV-Beitragskontonummer des Dienstgebers (10 Stellen;\n'
'die ersten Stellen codieren den KV-Träger). Pflichtfeld\n'
'jeder ELDA-Meldung (mBGM-Paket BKNR, Versichertenmeldung,\n'
'Arbeitsstättenmeldung, fallweise Anmeldung).')
l10n_at_lohnsteuernummer = fields.Char(
string='AT Lohnsteuernummer',
help='9-stellig: 2 Stellen Finanzamtsnummer + 7 Stellen\n'
'Steuernummer (Lohnzettel Finanz I1 STNRA/STNRL;\n'
'Arbeitsstättenmeldung STNR).')
l10n_at_wien_dag = fields.Boolean(
string='AT Beschäftigungsort Wien (Wr. DAG)',
help='Wiener Dienstgeberabgabe („U-Bahn-Steuer“, Wr. DAG,\n'
'RIS LrW GesNr 20000133): 2 € je Dienstnehmer:in und\n'
'angefangene Woche je Bestehen eines Dienstverhältnisses\n'
'mit Beschäftigungsort Wien (§§ 1, 2, 5); Befreiungen § 3\n'
'(z. B. Überschreiten des 55. Lebensjahres, Lehrlinge,\n'
'≤ 10 Wochenstunden, Mutterschaftsverbote/Karenz,\n'
'Präsenzdienst, Gebietskörperschaften); Selbstberechnung\n'
'15. des Folgemonats, Jahreserklärung bis 31. 3. (§ 6).')
class ResPartner(models.Model):
_inherit = 'res.partner'
l10n_at_gemeindekennziffer = fields.Char(
string='AT Gemeindekennziffer (GKZ)',
help='5-stellige österreichische Gemeindekennzahl für die\n'
'Arbeitsstättenmeldung nach § 34 Abs 6 ASVG (Feld AGKZ).\n'
'Am Dienstort-Partner des Mitarbeiters pflegen.')
class HrVersion(models.Model):
_inherit = 'hr.version'
l10n_at_fallweise = fields.Boolean(
string='AT Fallweise Beschäftigung',
groups="hr_payroll.group_hr_payroll_user", tracking=True,
help='Fallweise Beschäftigung (§ 33 Abs 1a Z 1 ASVG): je\n'
'Beschäftigungstag ist die vereinfachte Anmeldung vor\n'
'Arbeitsantritt zu übermitteln (ELDA BEST MA, Satzarten\n'
'32/33); An-/Abmeldung laufen über die mBGM (G3/G4).')
@api.constrains('ssnid')
def _l10n_at_vsnr_format(self):
"""VSNR-Formatprüfung LLLPTTMMJJ (10 Stellen, Ziffern, plausibles
Geburtsdatum TTMMJJ an den Stellen 510). Der
Prüfzifferalgorithmus der SVS ist nicht beschafft nur
Format/Plausibilität (Rest dokumentiert; die Meldungsgeneratoren
listen ungültige VSNR listenweise als UserError)."""
for version in self:
if not version.ssnid:
continue
roh = version.ssnid.strip()
if len(roh) != 10 or not roh.isdigit():
raise ValidationError(
'Versicherungsnummer %r: 10 Ziffern im Format LLLPTTMMJJ '
'erwartet.' % version.ssnid)
try:
tag = int(roh[4:6])
monat = int(roh[6:8])
if not 1 <= tag <= 31 or not 1 <= monat <= 12:
raise ValueError()
except ValueError:
raise ValidationError(
'Versicherungsnummer %r enthält kein plausibles '
'Geburtsdatum (TTMMJJ an den Stellen 510).'
% version.ssnid)
class HrDepartureReason(models.Model):
_inherit = 'hr.departure.reason'
l10n_at_agrd = fields.Selection(
L10N_AT_AGRD,
string='AT eSV-Abmeldegrund (AGRD)',
help='2-stelliger Abmeldegrund-Code der Versichertenmeldung '
'(DM-ORG D.22); Standard 00 „Sonstiger Grund mit Ende des '
'Beschäftigungsverhältnisses“. Die Abmeldung M4 liest das '
'Mapping dieser Abgangsursache.')
@@ -0,0 +1,573 @@
# 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
@@ -0,0 +1,97 @@
# Part of the odoo-at-payroll project. License: LGPL-3.
"""ELDA-Mitteilungsfile-Import (GP8): Rückantwort der Clearing-
stelle/Dateninhaltsprüfung als XML (`mitteilung_{Protokollnummer}.xml`).
XSDs lokal beschafft (`.firecrawl/ris/elda/Mitteilungsfiles_ZIP.zip`
elda_mitteilung-1.2/2.1/3.0.xsd, nicht versioniert).
Der Import parst das XML, reichert die GP7-Deklarationsmodelle
(L16/mBGM) mit dem offiziellen Prüfergebnis an und liefert ein
lesbares Protokoll. Dateibasiert (Prämisse P7): der Mandant lädt
das Mitteilungsfile aus dem ELDA-Portal und lädt es hier hoch."""
import base64
import logging
import re
from odoo import api, fields, models
from odoo.exceptions import UserError
_logger = logging.getLogger(__name__)
class L10nAtPayrollMitteilungsfileWizard(models.TransientModel):
_name = 'l10n.at.payroll.mitteilungsfile.wizard'
_description = 'ELDA-Mitteilungsfile importieren (GP8)'
company_id = fields.Many2one(
'res.company', required=True,
default=lambda self: self.env.company)
datei = fields.Binary(
required=True, string='Mitteilungsfile (XML)',
help='ELDA-Rückantwort: mitteilung_{Protokollnummer}.xml '
'aus dem Empfangsverzeichnis des ELDA-Portals.')
datei_name = fields.Char()
protokoll = fields.Text(readonly=True)
def action_importieren(self):
"""XML parsen und Protokollzeilen extrahieren; die GP7-
Deklarationsmodelle werden über REFP/REFW-Matches verlinkt,
wo möglich. Das Mitteilungsfile 3.0 hat die Struktur:
<mitteilung> <sendung> <fehler>/<warnung> (je Satz-
art/REFW-Bezug)."""
for wizard in self:
if not wizard.datei:
raise UserError('Keine Datei hochgeladen.')
roh = base64.b64decode(wizard.datei)
try:
inhalt = roh.decode('utf-8')
except UnicodeDecodeError:
inhalt = roh.decode('iso-8859-15', errors='replace')
zeilen = wizard._parse_mitteilungsfile(inhalt)
wizard.protokoll = '\n'.join(zeilen) or 'Keine Fehler ' \
'oder Warnungen gefunden.'
return True
@api.model
def _parse_mitteilungsfile(self, inhalt):
"""Einfacher XML-Textparser: extrahiert <fehler>/<warnung>
Elemente mit Attributen und Textinhalt. Kein voller XSD-
Validator die Fachprüfung bleibt der Clearingstelle; hier
wird das Ergebnis lesbar aufbereitet (Prämisse P7)."""
zeilen = []
# <fehler code="F0100" satzart="L1" refw="..."
# beschreibung="..."/> — XSD-abhängige Struktur.
for match in re.finditer(
r'<(?:fehler|warnung|error|warning)\b([^>]*?)/?>',
inhalt, re.IGNORECASE):
attrs = match.group(1)
art = 'Fehler' if 'fehler' in match.group(0).lower() \
else 'Warnung'
code = re.search(r'code="([^"]*)"', attrs)
satzart = re.search(r'satzart="([^"]*)"', attrs)
beschreibung = re.search(
r'(?:beschreibung|description|text)="([^"]*)"', attrs)
refw = re.search(
r'(?:refw|refn|referenz)="([^"]*)"', attrs)
zeile = art
if code:
zeile += ' %s' % code.group(1)
if satzart:
zeile += ' (Satzart %s)' % satzart.group(1)
if refw:
zeile += ' [Ref: %s]' % refw.group(1)
if beschreibung:
zeile += ': %s' % beschreibung.group(1)
if not code and not beschreibung:
zeile += ': %s' % match.group(0)[:120]
zeilen.append(zeile)
# Kopfprotokoll (Sendungsstatus).
for match in re.finditer(
r'<sendung\b([^>]*?)/?>', inhalt, re.IGNORECASE):
attrs = match.group(1)
status = re.search(r'status="([^"]*)"', attrs)
if status:
zeilen.insert(0, 'Sendungsstatus: %s' % status.group(1))
if not zeilen:
zeilen.append('Mitteilungsfile gelesen — keine '
'<fehler>/<warnung>-Elemente gefunden.')
return zeilen
@@ -0,0 +1,288 @@
# 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}
@@ -0,0 +1,269 @@
# Part of the odoo-at-payroll project. License: LGPL-3.
"""Schwerarbeitsmeldung (AP18) — DM-ORG 42.7.0, Kapitel E.22
(BEST ``SM``, SART 65 Schwerarbeitsmeldung / 66 Storno, Länge 800,
Version 02; PDF-verifizierte ODL-Extraktion 2026-09-16).
Rechtsanker (RIS-verifiziert 2026-09-16):
- **ASchV** (RIS GesNr 20004642, BGBl II 1998/419 idF NOR40272279):
§ 1 Abs 1 Z 126 (Z 1 Schicht-/Wechseldienst in Nachtarbeit 226 Uhr
6 h; Z 2 Hitze/Kälte NSchG Art VII Z 2/3; Z 3 chemisch/physikalisch;
Z 4 schwere körperliche Arbeit 2.000 kcal (M) / 1.400 kcal (F);
Z 5 Pflege; Z 6 Pflegegeld), § 4 (Schwerarbeitsmonat Versicherungs-
monat § 231 Z 1 lit a ASVG; Z 5: 12 Tage Schichtdienst), § 5 (Meldung
M 40 / F 35, bis Ende Februar).
- **Erstellvorschriften (DM-ORG E.22.2):** meldepflichtig Z 1, 2, 4 und 5;
Z 1 nur bei 6 Arbeitstagen/Monat Schicht-/Wechseldienst in Nachtarbeit
(bei unterschiedlicher Nachtschichten-Anzahl analog **NSchG Art XI
Abs 6**); Z 2/4/5 nur bei **15 Arbeitstagen/Monat**; Z 3 nicht melden
(MdE 10 % erst nachträglich); Z 6 nur bei bekannter Pflegegeldeinstu-
fung (ab Stufe 3); geringfügig Beschäftigte nicht melden.
- **lb-swa-05** (Stand 2026-07): jährlich JännerEnde Februar; GfB aus-
geschlossen; fallweise/freie DN zu melden; Leiharbeiter vom Überlasser.
Die Ziffern/Zeiträume sind mandantenseitige Fakten (Arbeitsplatz-
Klassifikation) das Modell erfasst sie manuell und erzeugt den Fixsatz;
keine automatisierte Ableitung aus Arbeitszeiten."""
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 l10n_at_namen
# ------------------------------------------------------------------
# SART 65/66 (E.22; Länge 800) — Datenteil inkl. 26 Tätigkeitsblöcken
# ------------------------------------------------------------------
SCHWERARBEIT_SATZ = [
(2, 'BKNR', 21, 10, 'a'), # Beitragskontonummer
(3, 'DGNA', 31, 70, 'a'),
(4, 'WOBD', 101, 12, 'a'),
(5, 'ZOBD', 113, 15, 'a'),
(6, 'DTEL', 128, 50, 'a'), # DG-Telefon
(7, 'MAIL', 178, 60, 'a'), # DG-Mail
(8, 'DKFZ', 238, 3, 'a'), # DG Land/Kfz
(9, 'DPLZ', 241, 9, 'a'),
(10, 'DORT', 250, 40, 'a'),
(11, 'DSTR', 290, 50, 'a'),
(12, 'VSNR', 340, 10, 'n'), # LLLPTTMMJJ
(13, 'GEBD', 350, 8, 'n'), # TTMMJJJJ
(14, 'FANA', 358, 70, 'a'),
(15, 'VONA', 428, 70, 'a'),
(16, 'JAHR', 498, 4, 'n'), # Tätigkeitsjahr
# Tätigkeitsblöcke: 26 × 10 = 260 (pos 502761)
(17, 'TART', 502, 2, 'a'), # § 1 Abs 1 Z x ASchV
(18, 'TVON', 504, 4, 'n'), # TTMM
(19, 'TBIS', 508, 4, 'n'), # TTMM
(20, 'REFN', 762, 30, 'a'),
(21, 'RE21', 792, 9, 'a'),
]
def _taetigkeitsblock_struktur():
"""26 Tätigkeitsblöcke à 10 Stellen (TART 2 + TVON 4 + TBIS 4)."""
felder = []
for block_nr in range(1, 27):
basis = 502 + (block_nr - 1) * 10
felder.extend([
(17 + (block_nr - 1) * 3, 'TART_%d' % block_nr,
basis, 2, 'a'),
(18 + (block_nr - 1) * 3, 'TVON_%d' % block_nr,
basis + 2, 4, 'n'),
(19 + (block_nr - 1) * 3, 'TBIS_%d' % block_nr,
basis + 6, 4, 'n'),
])
return felder
SCHWERARBEIT_SATZ += _taetigkeitsblock_struktur()
class L10nAtPayrollSchwerarbeitLine(models.Model):
_name = 'l10n.at.payroll.schwerarbeitsmeldung.line'
_description = 'Schwerarbeitsmeldung — Satzdaten je AN'
_order = 'employee_name'
meldung_id = fields.Many2one(
'l10n.at.payroll.schwerarbeitsmeldung', required=True,
ondelete='cascade', index=True)
employee_id = fields.Many2one('hr.employee', required=True)
employee_name = fields.Char(related='employee_id.name',
readonly=True, store=True)
versicherungsnummer = fields.Char()
taetigkeit_ids = fields.One2many(
'l10n.at.payroll.schwerarbeitsmeldung.taetigkeit', 'line_id',
string='Tätigkeitsblöcke')
refn = fields.Char()
class L10nAtPayrollSchwerarbeitTaetigkeit(models.Model):
_name = 'l10n.at.payroll.schwerarbeitsmeldung.taetigkeit'
_description = 'Schwerarbeitsmeldung — Tätigkeitsblock (§ 1 ASchV)'
_order = 'sequence'
sequence = fields.Integer(default=10)
line_id = fields.Many2one(
'l10n.at.payroll.schwerarbeitsmeldung.line', required=True,
ondelete='cascade', index=True)
ziffer = fields.Integer(
string='ASchV § 1 Abs 1 Ziffer', required=True,
help='TART-Codes 126 der Schwerarbeitsverordnung. Nicht zu '
'melden: Z 3 (MdE-Feststellung nachträglich) und Z 6 ohne '
'bekannte Pflegegeldeinstufung ab Stufe 3.')
von = fields.Date(string='Von (TTMM)')
bis = fields.Date(string='Bis (TTMM)')
@api.constrains('ziffer')
def _check_ziffer(self):
for taetigkeit in self:
if not 1 <= taetigkeit.ziffer <= 26:
raise UserError(
'ASchV-Ziffer %s außerhalb 126.' % taetigkeit.ziffer)
class L10nAtPayrollSchwerarbeitsmeldung(models.Model):
_name = 'l10n.at.payroll.schwerarbeitsmeldung'
_description = 'Schwerarbeitsmeldung (§ 74 Abs 3 ASVG / § 5 ASchV)'
_inherit = ['mail.thread']
_order = 'jahr desc, id desc'
company_id = fields.Many2one(
'res.company', required=True,
default=lambda self: self.env.company)
jahr = fields.Integer(required=True, string='Tätigkeitsjahr')
line_ids = fields.One2many(
'l10n.at.payroll.schwerarbeitsmeldung.line', 'meldung_id',
string='Meldesätze')
datei = fields.Binary(attachment=True, string='SM-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 = [(
'sm_company_jahr_uniq', 'unique(company_id, jahr)',
'Für dieses Jahr existiert bereits eine Schwerarbeitsmeldung.')]
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 action_erzeugen(self):
"""SART 65 je AN (Tätigkeitsblöcke 26 × 10) — Erstellvorschriften
(E.22.2): Z 1 6 Nachtarbeitstage/Monat (analog NSchG Art XI
Abs 6), Z 2/4/5 15 Arbeitstage/Monat, GfB ausgeschlossen,
Altersfilter M 40 / F 35 als Warnung."""
for meldung in self:
fehler = []
warnungen = []
company = meldung.company_id
bknr = company.l10n_at_beitragskontonummer
if not bknr:
fehler.append('Beitragskontonummer (BKNR) fehlt.')
partner = company.partner_id
# Zeilen werden je AN erfasst (mandantenseitige Fakten);
# der Lauf prüft die Erstellvorschriften und erzeugt die
# Sätze.
for line in meldung.line_ids:
employee = line.employee_id
version = employee.version_id
if not (version and version.ssnid):
fehler.append(
'Mitarbeiter %s ohne Versicherungsnummer.'
% employee.name)
continue
if not employee.birthday:
fehler.append(
'Mitarbeiter %s ohne Geburtsdatum.'
% employee.name)
continue
altersstand = meldung.jahr - employee.birthday.year
ist_frau = (version.sex == 'female')
grenze = 35 if ist_frau else 40
if altersstand < grenze:
warnungen.append(
'%s: %d Jahre am 31. 12. %d — unter der '
'Altersgrenze (%d %s); Meldung entfällt bzw. '
'Zeiten nur bei Begründung (lb-swa-05).'
% (employee.name, altersstand, meldung.jahr,
grenze, 'F' if ist_frau else 'M'))
if line.taetigkeit_ids.filtered(
lambda t: t.ziffer == 3):
warnungen.append(
'%s: Z 3 (chemisch/physikalisch) ist nicht '
'meldepflichtig (MdE ≥ 10 %% wird erst '
'nachträglich festgestellt — lb-swa-05).'
% employee.name)
if line.taetigkeit_ids.filtered(
lambda t: t.ziffer == 6):
warnungen.append(
'%s: Z 6 (Pflege) nur bei bekannter '
'Pflegegeldeinstufung ab Stufe 3 melden.'
% employee.name)
if fehler:
meldung.protokoll = 'FEHLER:\n%s' % '\n'.join(fehler)
raise UserError(
'Schwerarbeitsmeldungs-Lauf mit Fehlern abgebrochen:\n'
+ '\n'.join(fehler))
if meldung.line_ids:
heute = date.today()
jetzt = datetime.now()
datensaetze = []
for line in meldung.line_ids:
employee = line.employee_id
version = employee.version_id
satz = L10nAtEldaSatz(
L10N_AT_ELDA_IDTEIL + SCHWERARBEIT_SATZ, 800,
bezeichnung='SM %s' % employee.name)
satz.setze('SART', '65')
satz.setze('VSTR', (bknr or '')[:2])
satz.setze('BKNR', bknr)
satz.setze('DGNA', (company.name or '')[:70])
satz.setze('WOBD', 'EMP-%d' % employee.id)
satz.setze('DKFZ', 'A')
satz.setze('DPLZ', (partner.zip or '')[:9])
satz.setze('DORT', (partner.city or '')[:40])
satz.setze('DSTR', (partner.street or '')[:50])
satz.setze('VSNR', line.versicherungsnummer)
satz.setze_datum('GEBD', employee.birthday)
familienname, vorname = l10n_at_namen(employee)
satz.setze('FANA', (familienname or '')[:70])
satz.setze('VONA', (vorname or '')[:70])
satz.setze('JAHR', meldung.jahr)
for nummer, taetigkeit in enumerate(
line.taetigkeit_ids.sorted('sequence')[:26],
start=1):
satz.setze('TART_%d' % nummer,
'%02d' % taetigkeit.ziffer)
if taetigkeit.von:
satz.setze('TVON_%d' % nummer,
taetigkeit.von.strftime('%d%m'))
if taetigkeit.bis:
satz.setze('TBIS_%d' % nummer,
taetigkeit.bis.strftime('%d%m'))
satz.setze('REFN', 'SM-%d-%d-%d' % (
company.id, meldung.jahr, employee.id))
datensaetze.append(satz)
bestand = L10nAtEldaBestand(
'SM', (bknr or '')[:2], '02',
hersteller=meldung._l10n_at_hersteller(),
datensaetze=datensaetze)
datei = L10nAtEldaDatei([bestand])
meldung.write({
'datei': base64.b64encode(datei.render()),
'datei_name': 'schwerarbeit_%d.txt' % meldung.jahr,
})
meldung.protokoll = 'WARNUNGEN:\n%s' % (
warnungen and '\n'.join(warnungen) or '')
meldung.state = 'done'
return True
@@ -37,6 +37,18 @@ from odoo.tools.safe_eval import safe_eval
class HrSalaryRule(models.Model): class HrSalaryRule(models.Model):
_inherit = 'hr.salary.rule' _inherit = 'hr.salary.rule'
l10n_at_sv_art = fields.Selection(
[('folgt', 'Folgt AT LSt-Bemessungsart'),
('laufend', 'Laufender SV-Bezug'),
('sonstig', 'Sonstiger SV-Bezug'),
('keine', 'Kein SV-Bezug')],
string='AT SV-Bemessungsart',
default='folgt',
help='Steuert ausschließlich die SV-Zuordnung. Standard „Folgt“ '
'übernimmt die AT LSt-Bemessungsart und ist damit '
'rückwärtskompatibel. Abweichung für Drittlohn oder '
'abweichende LSt-/SV-Zuordnung, z. B. Bonusmeilen oder '
'Hardware-Verkauf.')
l10n_at_sv49_frei = fields.Selection( l10n_at_sv49_frei = fields.Selection(
[('keine', 'Keine §-49-Abs-3-Befreiung'), [('keine', 'Keine §-49-Abs-3-Befreiung'),
('mahlzeit', 'Mahlzeiten-Gutschein (§ 49 Abs 3 Z 12; 8 €/AT)'), ('mahlzeit', 'Mahlzeiten-Gutschein (§ 49 Abs 3 Z 12; 8 €/AT)'),
@@ -254,7 +266,7 @@ class HrPayslip(models.Model):
for rule in self.struct_id.rule_ids: for rule in self.struct_id.rule_ids:
kategorie = rule.l10n_at_sv49_frei kategorie = rule.l10n_at_sv49_frei
if (not kategorie or kategorie == 'keine' if (not kategorie or kategorie == 'keine'
or rule.l10n_at_bemessung != 'laufend'): or self._l10n_at_sv_art(rule) != 'laufend'):
continue continue
werte = None werte = None
if result_rules is not None: if result_rules is not None:
@@ -297,11 +309,33 @@ class HrPayslip(models.Model):
# ------------------------------------------------------------------ # ------------------------------------------------------------------
# Laufende Beitragsgrundlage — je Beitragsmonat (Anspruchsprinzip) # Laufende Beitragsgrundlage — je Beitragsmonat (Anspruchsprinzip)
# ------------------------------------------------------------------ # ------------------------------------------------------------------
def _l10n_at_sv_art(self, rule):
"""Effektive SV-Art einer Regel; Standard folgt der LSt-Art."""
self.ensure_one()
return (rule.l10n_at_bemessung if rule.l10n_at_sv_art == 'folgt'
else rule.l10n_at_sv_art)
def _l10n_at_sv_brutto(self, art, result_rules=None):
"""SV-Brutto je Art; abweichende SV-Zuordnung bleibt von der
LSt-Bemessungsart getrennt (Drittlohn/Sonderfälle)."""
self.ensure_one()
total = 0.0
for rule in self.struct_id.rule_ids:
if self._l10n_at_sv_art(rule) != art:
continue
if result_rules is not None:
if rule.code in result_rules:
total += result_rules[rule.code].get('total', 0.0)
else:
total += sum(self.line_ids.filtered(
lambda l: l.code == rule.code).mapped('total'))
return total
def _l10n_at_sv_basis_roh(self, result_rules=None): def _l10n_at_sv_basis_roh(self, result_rules=None):
"""Laufender Arbeitslohn minus beitragsfreie §-49-Anteile """Laufendes SV-Brutto minus beitragsfreie §-49-Anteile
(vor HBG-Cap; Geringfügigkeits-/Staffel-Basis).""" (vor HBG-Cap; Geringfügigkeits-/Staffel-Basis)."""
self.ensure_one() self.ensure_one()
roh = self._l10n_at_brutto('laufend', result_rules) \ roh = self._l10n_at_sv_brutto('laufend', result_rules) \
- self._l10n_at_sv49_freie_laufen(result_rules) - self._l10n_at_sv49_freie_laufen(result_rules)
return max(roh, 0.0) return max(roh, 0.0)
@@ -312,7 +346,7 @@ class HrPayslip(models.Model):
self.ensure_one() self.ensure_one()
gruppen = {} gruppen = {}
for rule in self.struct_id.rule_ids: for rule in self.struct_id.rule_ids:
if rule.l10n_at_bemessung != 'laufend': if self._l10n_at_sv_art(rule) != 'laufend':
continue continue
werte = None werte = None
if result_rules is not None: if result_rules is not None:
+415 -9
View File
@@ -1,4 +1,17 @@
# Part of the odoo-at-payroll project. License: LGPL-3. # Part of the odoo-at-payroll project. License: LGPL-3.
"""TASY-LSWH-Import (SV-Tarifsystem, sozialversicherung.at/tarifsystem).
Zwei Stufen:
1. ``l10n.at.payroll.tasy.import.wizard`` SV-Werte als
hr.rule.parameter (Mapping-gesteuert, Vorschau, historische
Werte bleiben erhalten; GP4).
2. ``l10n.at.payroll.tasy.katalog.wizard`` Vollständiger
Tarifsystem-Katalog (AP0, Variante b): BSGR/ERGB/Dienstgeber-
gruppen/Tarifgruppen/Tarifsets/Tarife/Zu-/Abschläge, alle
Träger, versioniert über von/bis. Die mBGM-Tarifierung liest
daraus; der Hand-Parameter ``at_mbgm_tarif`` bleibt Fallback.
"""
from lxml import etree from lxml import etree
import base64 import base64
@@ -7,6 +20,19 @@ from odoo import _, api, fields, models
from odoo.exceptions import UserError from odoo.exceptions import UserError
def _l10n_at_tasy_root(file_field):
"""TASY-XML robust laden: rohe XML-Bytes ODER base64."""
parser = etree.XMLParser(resolve_entities=False, no_network=True)
data = file_field or b''
if isinstance(data, str):
data = data.encode()
# Binary kann XML-Bytes ODER base64 (str/Bytes) liefern:
# dekodieren, wenn kein '<' am Anfang steht.
if not data.lstrip().startswith(b'<'):
data = base64.b64decode(data)
return etree.fromstring(data, parser)
class L10nAtPayrollTasyMapping(models.Model): class L10nAtPayrollTasyMapping(models.Model):
"""Mapping TASY-Code/-Zweig -> SV-Regelparameter (hr.rule.parameter). """Mapping TASY-Code/-Zweig -> SV-Regelparameter (hr.rule.parameter).
@@ -102,15 +128,7 @@ class L10nAtPayrollTasyImportWizard(models.TransientModel):
def _tasy_root(self): def _tasy_root(self):
self.ensure_one() self.ensure_one()
parser = etree.XMLParser(resolve_entities=False, no_network=True) return _l10n_at_tasy_root(self.file)
data = self.file or b''
if isinstance(data, str):
data = data.encode()
# Binary kann XML-Bytes ODER base64 (str/Bytes) liefern:
# dekodieren, wenn kein '<' am Anfang steht.
if not data.lstrip().startswith(b'<'):
data = base64.b64decode(data)
return etree.fromstring(data, parser)
@staticmethod @staticmethod
def _tasy_text(node, tag): def _tasy_text(node, tag):
@@ -299,4 +317,392 @@ class L10nAtPayrollTasyImportWizard(models.TransientModel):
count=applied), count=applied),
'next': {'type': 'ir.actions.act_window_close'}, 'next': {'type': 'ir.actions.act_window_close'},
}, },
}
class L10nAtPayrollTasyKatalogWizard(models.TransientModel):
"""Lädt den vollständigen Tarifsystem-Katalog aus dem
TASY-LSWH-Export (AP0, Variante b) alle Trägergruppen,
versioniert über von/bis-Datum. Idempotent: wiederholter Import
derselben Datei aktualisiert statt zu duplizieren; historische
Gültigkeitsfenster bleiben erhalten (Jahreswechsel = neuer
Import, keine Löschung).
Die Sätze selbst werden NICHT hieraus in hr.rule.parameter
geschrieben die mBGM-Tarifierung liest den Katalog direkt
(models/tasy_katalog.py); nur die Zweig-Split-Werte für den
GP4-Kern laufen über den Wert-Import-Wizard.
"""
_name = 'l10n.at.payroll.tasy.katalog.wizard'
_description = 'SV-Tarifsystem-Katalog importieren (TASY)'
file = fields.Binary(required=True, string='TASY-Export (XML)')
filename = fields.Char()
stichtag = fields.Date(
readonly=True,
help="Gültigkeitstichtag des Exports (nur Dokumentation — "
"die Katalogzeilen tragen eigene von/bis-Fenster).")
protocol = fields.Text(readonly=True)
@staticmethod
def _tasy_text(node, tag):
child = node.find(tag)
return (child.text or '').strip() if child is not None else ''
@staticmethod
def _tasy_texts(node, tag):
"""Alle Kindelemente dieses Tags — TASY nutzt wiederholte
Referenz-Elemente (z. B. <tarife>xmlId</tarife>)."""
return [child.text.strip() for child in node.findall(tag)
if child.text and child.text.strip()]
@staticmethod
def _tasy_bool(text):
return (text or '').strip().lower() in ('true', '1', 'j', 'ja',
'yes', 'y')
def _tasy_root(self):
self.ensure_one()
return _l10n_at_tasy_root(self.file)
@staticmethod
def _section(root, tag):
"""Sektionsscharfe Iteration — root.iter() erfasste auch die
Referenz-Elemente (z. B. <tarifgruppe>xmlId</tarifgruppe> in
Zuordnungszeilen); die Sektionen enthalten nur echte Zeilen."""
section = root.find(tag)
return list(section) if section is not None else []
# ------------------------------------------------------------------
# Upsert-Helfer
# ------------------------------------------------------------------
def _upsert_code_katalog(self, model, nodes, protocol, label,
extra_vals=None):
"""Katalogzeilen mit (Trägergruppe, Code, von) als Schlüssel."""
created = updated = 0
for node in nodes:
code = self._tasy_text(node, 'code')
von = self._tasy_text(node, 'vonDatum')
if not code or not von:
continue
vals = {
'code': code,
'bezeichnung': self._tasy_text(node, 'bezeichnung'),
'kurzbezeichnung': self._tasy_text(
node, 'kurzbezeichnung'),
'beschreibung': self._tasy_text(node, 'beschreibung'),
'traegergruppe': self._tasy_text(node, 'traegergruppe'),
'von_datum': fields.Date.to_date(von),
'bis_datum': fields.Date.to_date(
self._tasy_text(node, 'bisDatum') or '2999-12-31'),
'tasy_xml_id': node.get('xmlId'),
}
vals.update(extra_vals(node) if extra_vals else {})
rec = self.env[model].search([
('traegergruppe', '=', vals['traegergruppe']),
('code', '=', code),
('von_datum', '=', vals['von_datum'])], limit=1)
if rec:
rec.write(vals)
updated += 1
else:
self.env[model].create(vals)
created += 1
protocol.append('%s: %d neu, %d aktualisiert'
% (label, created, updated))
def _upsert_tarif(self, nodes, protocol):
"""Tarife ohne Code — Identität ist der Inhalt (Zweig, Sätze,
Gültigkeit); xmlId zuerst, dann Inhaltsvergleich."""
created = updated = 0
for node in nodes:
zweig = self._tasy_text(node, 'tarifZweig')
von = self._tasy_text(node, 'vonDatum')
if not zweig or not von:
continue
vals = {
'zweig': zweig,
'dn': float(self._tasy_text(
node, 'dienstnehmerAnteil') or 0.0),
'dg': float(self._tasy_text(
node, 'dienstgeberAnteil') or 0.0),
'traegergruppe': self._tasy_text(node, 'traegergruppe'),
'von_datum': fields.Date.to_date(von),
'bis_datum': fields.Date.to_date(
self._tasy_text(node, 'bisDatum') or '2999-12-31'),
'tasy_xml_id': node.get('xmlId'),
}
rec = self.env['l10n.at.payroll.tasy.tarif'].search([
('zweig', '=', zweig),
('traegergruppe', '=', vals['traegergruppe']),
('von_datum', '=', vals['von_datum'])]).filtered(
lambda t, v=vals: abs(t.dn - v['dn']) < 1e-9
and abs(t.dg - v['dg']) < 1e-9)[:1]
if rec:
rec.write(vals)
updated += 1
else:
self.env['l10n.at.payroll.tasy.tarif'].create(vals)
created += 1
protocol.append('Tarife: %d neu, %d aktualisiert'
% (created, updated))
def _upsert_tarifset(self, nodes, tarif_map, protocol):
"""Tarifsets — Schlüssel (Träger, Kurzbezeichnung, von);
Tarif-Verknüpfungen werden je Import neu gesetzt."""
created = updated = 0
for node in nodes:
von = self._tasy_text(node, 'vonDatum')
kurz = self._tasy_text(node, 'kurzbezeichnung')
if not von:
continue
vals = {
'bezeichnung': self._tasy_text(node, 'bezeichnung'),
'kurzbezeichnung': kurz,
'traegergruppe': self._tasy_text(node, 'traegergruppe'),
'von_datum': fields.Date.to_date(von),
'bis_datum': fields.Date.to_date(
self._tasy_text(node, 'bisDatum') or '2999-12-31'),
'tasy_xml_id': node.get('xmlId'),
}
rec = self.env['l10n.at.payroll.tasy.tarifset'].search([
('traegergruppe', '=', vals['traegergruppe']),
('kurzbezeichnung', '=', kurz),
('von_datum', '=', vals['von_datum'])], limit=1)
tarife = [tarif_map[ref] for ref in self._tasy_texts(
node, 'tarife') if ref in tarif_map]
vals['tarif_ids'] = [(6, 0, [t.id for t in tarife])]
if rec:
rec.write(vals)
updated += 1
else:
rec = self.env['l10n.at.payroll.tasy.tarifset'].create(
vals)
created += 1
protocol.append('Tarifsets: %d neu, %d aktualisiert'
% (created, updated))
# ------------------------------------------------------------------
# Import
# ------------------------------------------------------------------
def action_import(self):
self.ensure_one()
root = self._tasy_root()
stichtag = (root.findtext('gueltigkeitstichtag') or '').strip()
if not stichtag:
raise UserError(_('Ungültiger TASY-Export: '
'Gültigkeitstichtag fehlt.'))
self.stichtag = fields.Date.to_date(stichtag)
protocol = ['Stichtag des Exports: %s' % stichtag]
# 1) Code-Kataloge: BSGR, Ergänzungen, Dienstgebergruppen,
# Zu-/Abschläge (inkl. Steuerung).
self._upsert_code_katalog(
'l10n.at.payroll.tasy.bsgr',
self._section(root, 'beschaeftigtengruppen'), protocol,
'BSGR')
self._upsert_code_katalog(
'l10n.at.payroll.tasy.ergaenzung',
self._section(root, 'ergaenzungen'), protocol,
'Ergänzungen')
self._upsert_code_katalog(
'l10n.at.payroll.tasy.dienstgebergruppe',
self._section(root, 'dienstgebergruppen'), protocol,
'Dienstgebergruppen')
bsgr_map = {b.tasy_xml_id: b for b in self.env[
'l10n.at.payroll.tasy.bsgr'].search([])}
erg_map = {e.tasy_xml_id: e for e in self.env[
'l10n.at.payroll.tasy.ergaenzung'].search([])}
dgg_map = {d.tasy_xml_id: d for d in self.env[
'l10n.at.payroll.tasy.dienstgebergruppe'].search([])}
zuabschlag_map = {}
zuabschlag_nodes = self._section(root, 'zuAbschlaege')
self._upsert_code_katalog(
'l10n.at.payroll.tasy.zuabschlag',
zuabschlag_nodes, protocol, 'Zu-/Abschläge',
extra_vals=lambda n: {
'zuabschlagkz': self._tasy_text(n, 'zuabschlagkz'),
'versicherungsrelevant': self._tasy_bool(
self._tasy_text(n, 'versicherungsRelevant')),
})
for za in self.env['l10n.at.payroll.tasy.zuabschlag'].search([]):
zuabschlag_map[za.tasy_xml_id] = za
steuerung_created = 0
steuerung_obj = self.env[
'l10n.at.payroll.tasy.zuabschlag.steuerung']
for node in self._section(root, 'zuAbschlaegeSteuerung'):
zuabschlag = zuabschlag_map.get(
self._tasy_text(node, 'zuAbschlag'))
von = self._tasy_text(node, 'vonDatum')
if not zuabschlag or not von:
continue
vals = {
'zuabschlag_id': zuabschlag.id,
'altersgrenze_min_jahr': float(
self._tasy_text(node, 'altersgrenzeMinJahr') or 0.0),
'altersgrenze_soll_jahr': float(
self._tasy_text(node, 'altersgrenzeSollJahr') or 0.0),
'geboren_bis': fields.Date.to_date(
self._tasy_text(node, 'geborenBis'))
if self._tasy_text(node, 'geborenBis') else False,
'geschlecht': self._tasy_text(node, 'geschlecht'),
'traegergruppe': self._tasy_text(node, 'traegergruppe'),
'von_datum': fields.Date.to_date(von),
'bis_datum': fields.Date.to_date(
self._tasy_text(node, 'bisDatum') or '2999-12-31'),
'tasy_xml_id': node.get('xmlId'),
}
if not steuerung_obj.search([
('tasy_xml_id', '=', vals['tasy_xml_id'])], limit=1):
steuerung_obj.create(vals)
steuerung_created += 1
protocol.append('Zu-/Abschlag-Steuerungen: %d neu'
% steuerung_created)
# 2) Tarife, dann Tarifgruppen (m2m auf die Kataloge),
# dann Tarifsets (m2m auf die Tarife).
self._upsert_tarif(self._section(root, 'tarife'), protocol)
tarif_map = {t.tasy_xml_id: t for t in self.env[
'l10n.at.payroll.tasy.tarif'].search([])}
tarifgruppe_obj = self.env['l10n.at.payroll.tasy.tarifgruppe']
created = updated = 0
for node in self._section(root, 'tarifgruppen'):
code = self._tasy_text(node, 'code')
von = self._tasy_text(node, 'vonDatum')
if not code or not von:
continue
vals = {
'code': code,
'bezeichnung': self._tasy_text(node, 'bezeichnung'),
'traegergruppe': self._tasy_text(node, 'traegergruppe'),
'von_datum': fields.Date.to_date(von),
'bis_datum': fields.Date.to_date(
self._tasy_text(node, 'bisDatum') or '2999-12-31'),
'tasy_xml_id': node.get('xmlId'),
'fallweise_zulaessig': self._tasy_bool(
self._tasy_text(node,
'fallweiseBeschaeftigungZulaessig')),
'bv_zulaessig': self._tasy_bool(
self._tasy_text(node,
'betrieblicheVorsorgeZulaessig')),
}
rec = tarifgruppe_obj.search([
('traegergruppe', '=', vals['traegergruppe']),
('code', '=', code),
('von_datum', '=', vals['von_datum'])], limit=1)
if not rec:
rec = tarifgruppe_obj.create(vals)
created += 1
else:
rec.write(vals)
updated += 1
rec.write({
'bsgr_ids': [(6, 0, [
bsgr_map[r].id for r in self._tasy_texts(
node, 'beschaeftigtengruppen')
if r in bsgr_map])],
'ergaenzung_ids': [(6, 0, [
erg_map[r].id for r in self._tasy_texts(
node, 'ergaenzungen') if r in erg_map])],
'dienstgebergruppe_ids': [(6, 0, [
dgg_map[r].id for r in self._tasy_texts(
node, 'dienstgeberGruppen')
if r in dgg_map])],
})
protocol.append('Tarifgruppen: %d neu, %d aktualisiert'
% (created, updated))
self._upsert_tarifset(self._section(root, 'tarifsets'),
tarif_map, protocol)
tarifset_map = {s.tasy_xml_id: s for s in self.env[
'l10n.at.payroll.tasy.tarifset'].search([])}
tarifgruppe_map = {t.tasy_xml_id: t for t in tarifgruppe_obj
.search([])}
# 3) Verrechnungsbasis-/Positionstypen + Kombinationen
# auflösen, dann tarifgruppeSetZuord importieren.
basis_typ = {b.get('xmlId'): self._tasy_text(b, 'code')
for b in self._section(root, 'verrechBasisTypen')}
pos_typ = {p.get('xmlId'): self._tasy_text(p, 'code')
for p in self._section(root, 'verrechPosTypen')}
kombinationen = {}
for k in self._section(root, 'verrechBasisPositionKombinationen'):
kombinationen[k.get('xmlId')] = (
basis_typ.get(self._tasy_text(k, 'verrechnungBasisTyp'),
''),
pos_typ.get(self._tasy_text(k, 'verrechnungPosTyp'), ''),
)
zuord_obj = self.env['l10n.at.payroll.tasy.tarifset.zuord']
created = updated = 0
for node in self._section(root, 'tarifgruppeSetZuordnungen'):
tarifgruppe = tarifgruppe_map.get(
self._tasy_text(node, 'tarifgruppe'))
tarifset = tarifset_map.get(self._tasy_text(node, 'tarifset'))
von = self._tasy_text(node, 'vonDatum')
if not tarifgruppe or not tarifset or not von:
continue
basis, pos = kombinationen.get(
self._tasy_text(node, 'verrechBasPosKombination'),
('', ''))
vals = {
'tarifgruppe_id': tarifgruppe.id,
'tarifset_id': tarifset.id,
'basis_typ': basis,
'pos_typ': pos,
'traegergruppe': self._tasy_text(node, 'traegergruppe'),
'von_datum': fields.Date.to_date(von),
'bis_datum': fields.Date.to_date(
self._tasy_text(node, 'bisDatum') or '2999-12-31'),
'tasy_xml_id': node.get('xmlId'),
}
rec = zuord_obj.search([
('tarifgruppe_id', '=', tarifgruppe.id),
('tarifset_id', '=', tarifset.id),
('basis_typ', '=', basis),
('pos_typ', '=', pos),
('von_datum', '=', vals['von_datum'])], limit=1)
if rec:
rec.write(vals)
updated += 1
else:
zuord_obj.create(vals)
created += 1
protocol.append('Tarifgruppe-Tarifset-Zuordnungen: '
'%d neu, %d aktualisiert' % (created, updated))
# 4) Zu-/Abschlag-Zuordnungen (m2m auf Tarifsets/Tarifgruppen).
verknuepft = 0
for node in self._section(root, 'zuAbschlagTarifsetZuordnungen'):
zuabschlag = zuabschlag_map.get(
self._tasy_text(node, 'zuAbschlag'))
if not zuabschlag:
continue
tarifset = tarifset_map.get(
self._tasy_text(node, 'tarifsetByTarifsetId'))
tarifgruppen = [
tarifgruppe_map[r] for r in self._tasy_texts(
node, 'tarifgruppe') if r in tarifgruppe_map]
if tarifset:
zuabschlag.tarifset_ids = [(4, tarifset.id)]
for tarifgruppe in tarifgruppen:
zuabschlag.tarifgruppe_ids = [(4, tarifgruppe.id)]
verknuepft += 1
protocol.append('Zu-/Abschlag-Zuordnungen: %d verknüpft'
% verknuepft)
self.protocol = '\n'.join(protocol)
return {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'type': 'success',
'message': _('Tarifsystem-Katalog importiert.'),
'next': {'type': 'ir.actions.act_window_close'},
},
} }
@@ -0,0 +1,300 @@
# Part of the odoo-at-payroll project. License: LGPL-3.
"""SV-Tarifsystem-Katalog (AP0, Variante b): Beschäftigtengruppen
(BSGR), Ergänzungen (ERGB), Dienstgebergruppen, Tarifgruppen,
Tarifsets/Tarife und Zu-/Abschläge geladen aus dem TASY-LSWH-Export
(sozialversicherung.at/tarifsystem; DM-ORG D.48/D.49: Tarifgruppe =
BSGR + ERGB).
Der Katalog wird zur Laufzeit über den Import-Wizard
(``l10n.at.payroll.tasy.katalog.wizard``, models/tasy.py) gepflegt
alle Trägergruppen, versioniert über von/bis-Datum; kein XML-Seed
(keine erfundenen Werte). Die mBGM-Tarifierung des Privatmoduls liest
die Sätze von hier (Katalog vor Hand-Parameter ``at_mbgm_tarif``).
"""
from odoo import api, fields, models
class L10nAtPayrollTasyBsgr(models.Model):
_name = 'l10n.at.payroll.tasy.bsgr'
_description = 'SV-Tarifsystem: Beschäftigtengruppe (BSGR)'
_order = 'code, von_datum'
code = fields.Char(required=True, index=True)
bezeichnung = fields.Char()
kurzbezeichnung = fields.Char()
beschreibung = fields.Char()
traegergruppe = fields.Char(index=True)
von_datum = fields.Date(string='Gültig ab', required=True)
bis_datum = fields.Date(string='Gültig bis', required=True)
tasy_xml_id = fields.Char(index=True)
_unique_bsgr = models.Constraint(
'unique (traegergruppe, code, von_datum)',
"Je Trägergruppe, Code und Gültig-ab gibt es nur eine "
"Beschäftigtengruppe.")
class L10nAtPayrollTasyErgaenzung(models.Model):
_name = 'l10n.at.payroll.tasy.ergaenzung'
_description = 'SV-Tarifsystem: Ergänzung zur BSGR (ERGB)'
_order = 'code, von_datum'
code = fields.Char(required=True, index=True)
bezeichnung = fields.Char()
kurzbezeichnung = fields.Char()
beschreibung = fields.Char()
traegergruppe = fields.Char(index=True)
von_datum = fields.Date(string='Gültig ab', required=True)
bis_datum = fields.Date(string='Gültig bis', required=True)
tasy_xml_id = fields.Char(index=True)
_unique_ergaenzung = models.Constraint(
'unique (traegergruppe, code, von_datum)',
"Je Trägergruppe, Code und Gültig-ab gibt es nur eine "
"Ergänzung.")
class L10nAtPayrollTasyDienstgebergruppe(models.Model):
_name = 'l10n.at.payroll.tasy.dienstgebergruppe'
_description = 'SV-Tarifsystem: Dienstgebergruppe'
_order = 'code, von_datum'
code = fields.Char(required=True, index=True)
bezeichnung = fields.Char()
kurzbezeichnung = fields.Char()
beschreibung = fields.Char()
traegergruppe = fields.Char(index=True)
von_datum = fields.Date(string='Gültig ab', required=True)
bis_datum = fields.Date(string='Gültig bis', required=True)
tasy_xml_id = fields.Char(index=True)
_unique_dgg = models.Constraint(
'unique (traegergruppe, code, von_datum)',
"Je Trägergruppe, Code und Gültig-ab gibt es nur eine "
"Dienstgebergruppe.")
class L10nAtPayrollTasyTarif(models.Model):
"""Einzelner Tarifsatz (Zweig × DN/DG-Anteil × Gültigkeit).
Kein natürlicher Code im Export Identität ist der Inhalt
(Zweig, Sätze, Gültigkeit); der Katalog-Import vereinheitlicht
über diese Inhaltsmerkmale statt über die wechselnden xmlIds.
"""
_name = 'l10n.at.payroll.tasy.tarif'
_description = 'SV-Tarifsystem: Tarif (Zweig-Satz)'
_order = 'zweig, von_datum'
zweig = fields.Char(required=True, index=True)
dn = fields.Float(string='DN-Anteil (%)', digits=(16, 3))
dg = fields.Float(string='DG-Anteil (%)', digits=(16, 3))
traegergruppe = fields.Char(index=True)
von_datum = fields.Date(string='Gültig ab', required=True)
bis_datum = fields.Date(string='Gültig bis', required=True)
tasy_xml_id = fields.Char(index=True)
tarifset_ids = fields.Many2many(
'l10n.at.payroll.tasy.tarifset',
'l10n_at_payroll_tasy_tarif_tarifset_rel',
'tarif_id', 'tarifset_id', string='Tarifsets')
class L10nAtPayrollTasyTarifset(models.Model):
_name = 'l10n.at.payroll.tasy.tarifset'
_description = 'SV-Tarifsystem: Tarifset'
_order = 'kurzbezeichnung, von_datum'
bezeichnung = fields.Char()
kurzbezeichnung = fields.Char()
traegergruppe = fields.Char(index=True)
von_datum = fields.Date(string='Gültig ab', required=True)
bis_datum = fields.Date(string='Gültig bis', required=True)
tasy_xml_id = fields.Char(index=True)
tarif_ids = fields.Many2many(
'l10n.at.payroll.tasy.tarif',
'l10n_at_payroll_tasy_tarif_tarifset_rel',
'tarifset_id', 'tarif_id', string='Tarife')
_unique_tarifset = models.Constraint(
'unique (traegergruppe, kurzbezeichnung, von_datum)',
"Je Trägergruppe, Kurzbezeichnung und Gültig-ab gibt es nur "
"ein Tarifset.")
class L10nAtPayrollTasyTarifgruppe(models.Model):
"""Tarifgruppe = BSGR (+ Ergänzungen) × Dienstgebergruppen.
Auflösung: ``_l10n_at_find`` (BSGR + ERGB-Menge + DGG am Stichtag)
``_l10n_at_saetze`` (Sätze je Verrechnungsbasis aus dem
zugeordneten Tarifset).
"""
_name = 'l10n.at.payroll.tasy.tarifgruppe'
_description = 'SV-Tarifsystem: Tarifgruppe'
_order = 'code, von_datum'
code = fields.Char(required=True, index=True)
bezeichnung = fields.Char()
traegergruppe = fields.Char(index=True)
von_datum = fields.Date(string='Gültig ab', required=True)
bis_datum = fields.Date(string='Gültig bis', required=True)
tasy_xml_id = fields.Char(index=True)
bsgr_ids = fields.Many2many(
'l10n.at.payroll.tasy.bsgr',
'l10n_at_payroll_tasy_tarifgruppe_bsgr_rel',
'tarifgruppe_id', 'bsgr_id', string='Beschäftigtengruppen')
ergaenzung_ids = fields.Many2many(
'l10n.at.payroll.tasy.ergaenzung',
'l10n_at_payroll_tasy_tarifgruppe_ergaenzung_rel',
'tarifgruppe_id', 'ergaenzung_id', string='Ergänzungen (ERGB)')
dienstgebergruppe_ids = fields.Many2many(
'l10n.at.payroll.tasy.dienstgebergruppe',
'l10n_at_payroll_tasy_tarifgruppe_dgg_rel',
'tarifgruppe_id', 'dienstgebergruppe_id',
string='Dienstgebergruppen')
fallweise_zulaessig = fields.Boolean(string='Fallweise zulässig')
bv_zulaessig = fields.Boolean(
string='Betriebliche Vorsorge zulässig')
zuord_ids = fields.One2many(
'l10n.at.payroll.tasy.tarifset.zuord', 'tarifgruppe_id',
string='Tarifset-Zuordnungen')
_unique_tarifgruppe = models.Constraint(
'unique (traegergruppe, code, von_datum)',
"Je Trägergruppe, Code und Gültig-ab gibt es nur eine "
"Tarifgruppe.")
@api.model
def _l10n_at_find(self, bsgr_code, ergaenzung_codes, dgg_code,
stichtag):
"""Tarifgruppe zu BSGR + ERGB-Menge + DGG am Stichtag.
Die ERGB-Menge muss exakt passen (leere Menge = Normalfall
ohne Ergänzung) DM-ORG D.48/D.49. Liefert leere Recordset,
wenn keine Zeile existiert (Aufrufer entscheidet über
Fallback/Abbruch).
"""
ergaenzung_codes = set(ergaenzung_codes or ())
kandidaten = self.search([
('bsgr_ids.code', '=', bsgr_code),
('von_datum', '<=', stichtag),
('bis_datum', '>=', stichtag),
])
if dgg_code:
kandidaten = kandidaten.filtered(
lambda t: dgg_code
in t.dienstgebergruppe_ids.mapped('code'))
for tarifgruppe in kandidaten:
if {e.code for e in tarifgruppe.ergaenzung_ids} \
== ergaenzung_codes:
return tarifgruppe
return self.env['l10n.at.payroll.tasy.tarifgruppe']
def _l10n_at_saetze(self, basis_typ, stichtag):
"""Sätze je Verrechnungsbasis (AB/SZ/BV/UU …) am Stichtag.
Rückgabe ``None`` ohne gültige Tarifset-Zuordnung, sonst
``{'dn': x, 'dg': y, 'gesamt': x + y, 'komponenten': tarif}``.
Je Zweig zählt der späteste gültige Tarif (Schutz gegen
duplette Import-Zeilen).
"""
self.ensure_one()
zuord = self.zuord_ids.filtered(
lambda z: z.basis_typ == basis_typ
and z.von_datum <= stichtag <= z.bis_datum)
if not zuord:
return None
zuord = zuord.sorted(key=lambda z: z.von_datum)[-1]
tarife = zuord.tarifset_id.tarif_ids.filtered(
lambda t: t.von_datum <= stichtag <= t.bis_datum)
komponenten = []
gesehene_zweige = set()
for tarif in tarife.sorted(key=lambda t: t.von_datum,
reverse=True):
if tarif.zweig in gesehene_zweige:
continue
gesehene_zweige.add(tarif.zweig)
komponenten.append(tarif)
dn = sum(t.dn for t in komponenten)
dg = sum(t.dg for t in komponenten)
return {'dn': dn, 'dg': dg, 'gesamt': dn + dg,
'komponenten': komponenten}
class L10nAtPayrollTasyTarifsetZuord(models.Model):
"""tarifgruppeSetZuord: Tarifgruppe × Tarifset × Basis/Position."""
_name = 'l10n.at.payroll.tasy.tarifset.zuord'
_description = 'SV-Tarifsystem: Tarifgruppe-Tarifset-Zuordnung'
_order = 'tarifgruppe_id, basis_typ, von_datum'
tarifgruppe_id = fields.Many2one(
'l10n.at.payroll.tasy.tarifgruppe', required=True, index=True,
ondelete='cascade')
tarifset_id = fields.Many2one(
'l10n.at.payroll.tasy.tarifset', required=True, index=True,
ondelete='cascade')
basis_typ = fields.Char(string='Verrechnungsbasis', index=True)
pos_typ = fields.Char(string='Verrechnungsposition', index=True)
traegergruppe = fields.Char(index=True)
von_datum = fields.Date(string='Gültig ab', required=True)
bis_datum = fields.Date(string='Gültig bis', required=True)
tasy_xml_id = fields.Char(index=True)
_unique_zuord = models.Constraint(
'unique (tarifgruppe_id, tarifset_id, basis_typ, pos_typ, '
'von_datum)',
"Diese Tarifgruppe-Tarifset-Zuordnung existiert bereits.")
class L10nAtPayrollTasyZuabschlag(models.Model):
_name = 'l10n.at.payroll.tasy.zuabschlag'
_description = 'SV-Tarifsystem: Zu-/Abschlag'
_order = 'code, von_datum'
code = fields.Char(required=True, index=True)
bezeichnung = fields.Char()
kurzbezeichnung = fields.Char()
beschreibung = fields.Char()
zuabschlagkz = fields.Char(string='Zu-/Abschlag-Kennzeichen')
versicherungsrelevant = fields.Boolean()
traegergruppe = fields.Char(index=True)
von_datum = fields.Date(string='Gültig ab', required=True)
bis_datum = fields.Date(string='Gültig bis', required=True)
tasy_xml_id = fields.Char(index=True)
steuerung_ids = fields.One2many(
'l10n.at.payroll.tasy.zuabschlag.steuerung', 'zuabschlag_id',
string='Steuerung (Alters-/Einkommensgrenzen)')
tarifset_ids = fields.Many2many(
'l10n.at.payroll.tasy.tarifset',
'l10n_at_payroll_tasy_zuabschlag_tarifset_rel',
'zuabschlag_id', 'tarifset_id', string='Tarifsets')
tarifgruppe_ids = fields.Many2many(
'l10n.at.payroll.tasy.tarifgruppe',
'l10n_at_payroll_tasy_zuabschlag_tarifgruppe_rel',
'zuabschlag_id', 'tarifgruppe_id', string='Tarifgruppen')
_unique_zuabschlag = models.Constraint(
'unique (traegergruppe, code, von_datum)',
"Je Trägergruppe, Code und Gültig-ab gibt es nur einen "
"Zu-/Abschlag.")
class L10nAtPayrollTasyZuabschlagSteuerung(models.Model):
"""Steuerung eines Zu-/Abschlags (Altersgrenzen, Geschlecht,
Geburtsjahrgang) informativ; die mBGM-Abschlagslogik rechnet
aus den GP4-Stichtagsflags, nicht aus dieser Tabelle."""
_name = 'l10n.at.payroll.tasy.zuabschlag.steuerung'
_description = 'SV-Tarifsystem: Zu-/Abschlag-Steuerung'
_order = 'zuabschlag_id, von_datum'
zuabschlag_id = fields.Many2one(
'l10n.at.payroll.tasy.zuabschlag', required=True, index=True,
ondelete='cascade')
altersgrenze_min_jahr = fields.Float(
string='Altersgrenze min. (Jahre)', digits=(16, 1))
altersgrenze_soll_jahr = fields.Float(
string='Altersgrenze soll (Jahre)', digits=(16, 1))
geboren_bis = fields.Date(string='Geboren bis')
geschlecht = fields.Char()
traegergruppe = fields.Char(index=True)
von_datum = fields.Date(string='Gültig ab', required=True)
bis_datum = fields.Date(string='Gültig bis', required=True)
tasy_xml_id = fields.Char(index=True)
@@ -0,0 +1,302 @@
<?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>
<record id="action_report_dg_kosten" model="ir.actions.report">
<field name="name">Dienstgeber-Gesamtkosten</field>
<field name="model">hr.payslip.run</field>
<field name="report_type">qweb-pdf</field>
<field name="report_name">l10n_at_hr_payroll.report_dg_kosten</field>
<field name="report_file">l10n_at_hr_payroll.report_dg_kosten</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 — Bezüge/ -->
<!-- Abzüge und (seit AP2a) eigener Block Dienstgeberanteile. -->
<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>
<h4>Bezüge &amp; Abzüge</h4>
<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>
<tr>
<td colspan="3"><strong>Summe Bezüge &amp; Abzüge</strong></td>
<td class="text-end"><strong><span t-out="j['summe_arten']" t-options="{&quot;widget&quot;: &quot;monetary&quot;, &quot;display_currency&quot;: j['run'].company_id.currency_id}"/></strong></td>
</tr>
</tbody>
</table>
<h4>Dienstgeberanteile (Lohnnebenkosten)</h4>
<table class="table table-sm">
<thead>
<tr>
<th>Code</th>
<th>Position</th>
<th>Bemessung</th>
<th class="text-end">Summe</th>
</tr>
</thead>
<tbody>
<tr t-foreach="j['dg_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>
<tr>
<td colspan="3"><strong>Summe Dienstgeberanteile</strong></td>
<td class="text-end"><strong><span t-out="j['summe_dg']" t-options="{&quot;widget&quot;: &quot;monetary&quot;, &quot;display_currency&quot;: j['run'].company_id.currency_id}"/></strong></td>
</tr>
</tbody>
</table>
</div>
</t>
</t>
</t>
</template>
<!-- Dienstgeber-Gesamtkosten: Brutto + Lohnnebenkosten je AN, -->
<!-- aufgeschlüsselt und kumuliert (AP2a). -->
<template id="report_dg_kosten">
<t t-call="web.html_container">
<t t-foreach="docs" t-as="run">
<t t-call="web.external_layout">
<div class="page">
<h2>Dienstgeber-Gesamtkosten — <t t-out="run.name"/></h2>
<table class="table table-sm">
<thead>
<tr>
<th>Mitarbeiter:in</th>
<th class="text-end">Bruttolohn</th>
<th class="text-end">SV-DG (laufend+SZ)</th>
<th class="text-end">FLAF-DB</th>
<th class="text-end">KommSt</th>
<th class="text-end">DZ (WKO)</th>
<th class="text-end">BV</th>
<th class="text-end">Wr. DAG</th>
<th class="text-end">Weitere DG</th>
<th class="text-end">Σ Lohnnebenkosten</th>
<th class="text-end">Gesamtkosten</th>
</tr>
</thead>
<tbody>
<tr t-foreach="zeilen" t-as="z">
<td><t t-out="z['slip'].employee_id.name"/></td>
<td class="text-end"><span t-out="z['brutto']" t-options="{&quot;widget&quot;: &quot;monetary&quot;, &quot;display_currency&quot;: run.company_id.currency_id}"/></td>
<td class="text-end"><span t-out="z['svdg']" t-options="{&quot;widget&quot;: &quot;monetary&quot;, &quot;display_currency&quot;: run.company_id.currency_id}"/></td>
<td class="text-end"><span t-out="z['spalten']['FLAFDB']" t-options="{&quot;widget&quot;: &quot;monetary&quot;, &quot;display_currency&quot;: run.company_id.currency_id}"/></td>
<td class="text-end"><span t-out="z['spalten']['KOMMST']" t-options="{&quot;widget&quot;: &quot;monetary&quot;, &quot;display_currency&quot;: run.company_id.currency_id}"/></td>
<td class="text-end"><span t-out="z['spalten']['DZ']" t-options="{&quot;widget&quot;: &quot;monetary&quot;, &quot;display_currency&quot;: run.company_id.currency_id}"/></td>
<td class="text-end"><span t-out="z['spalten']['BVG']" t-options="{&quot;widget&quot;: &quot;monetary&quot;, &quot;display_currency&quot;: run.company_id.currency_id}"/></td>
<td class="text-end"><span t-out="z['spalten']['WIEN_DAG']" t-options="{&quot;widget&quot;: &quot;monetary&quot;, &quot;display_currency&quot;: run.company_id.currency_id}"/></td>
<td class="text-end"><span t-out="z['weitere']" t-options="{&quot;widget&quot;: &quot;monetary&quot;, &quot;display_currency&quot;: run.company_id.currency_id}"/></td>
<td class="text-end"><span t-out="z['dg_summe']" t-options="{&quot;widget&quot;: &quot;monetary&quot;, &quot;display_currency&quot;: run.company_id.currency_id}"/></td>
<td class="text-end"><strong><span t-out="z['gesamtkosten']" t-options="{&quot;widget&quot;: &quot;monetary&quot;, &quot;display_currency&quot;: run.company_id.currency_id}"/></strong></td>
</tr>
<tr>
<td><strong>Summe Lauf</strong></td>
<td class="text-end"><strong><span t-out="summen['brutto']" t-options="{&quot;widget&quot;: &quot;monetary&quot;, &quot;display_currency&quot;: run.company_id.currency_id}"/></strong></td>
<td class="text-end"><strong><span t-out="summen['svdg']" t-options="{&quot;widget&quot;: &quot;monetary&quot;, &quot;display_currency&quot;: run.company_id.currency_id}"/></strong></td>
<td class="text-end"><strong><span t-out="summen['spalten']['FLAFDB']" t-options="{&quot;widget&quot;: &quot;monetary&quot;, &quot;display_currency&quot;: run.company_id.currency_id}"/></strong></td>
<td class="text-end"><strong><span t-out="summen['spalten']['KOMMST']" t-options="{&quot;widget&quot;: &quot;monetary&quot;, &quot;display_currency&quot;: run.company_id.currency_id}"/></strong></td>
<td class="text-end"><strong><span t-out="summen['spalten']['DZ']" t-options="{&quot;widget&quot;: &quot;monetary&quot;, &quot;display_currency&quot;: run.company_id.currency_id}"/></strong></td>
<td class="text-end"><strong><span t-out="summen['spalten']['BVG']" t-options="{&quot;widget&quot;: &quot;monetary&quot;, &quot;display_currency&quot;: run.company_id.currency_id}"/></strong></td>
<td class="text-end"><strong><span t-out="summen['spalten']['WIEN_DAG']" t-options="{&quot;widget&quot;: &quot;monetary&quot;, &quot;display_currency&quot;: run.company_id.currency_id}"/></strong></td>
<td class="text-end"><strong><span t-out="summen['weitere']" t-options="{&quot;widget&quot;: &quot;monetary&quot;, &quot;display_currency&quot;: run.company_id.currency_id}"/></strong></td>
<td class="text-end"><strong><span t-out="summen['dg_summe']" t-options="{&quot;widget&quot;: &quot;monetary&quot;, &quot;display_currency&quot;: run.company_id.currency_id}"/></strong></td>
<td class="text-end"><strong><span t-out="summen['gesamtkosten']" t-options="{&quot;widget&quot;: &quot;monetary&quot;, &quot;display_currency&quot;: run.company_id.currency_id}"/></strong></td>
</tr>
</tbody>
</table>
</div>
</t>
</t>
</t>
</template>
</odoo>
@@ -0,0 +1,138 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- ============================================================ -->
<!-- AP2b/AP2c: Jahreslohnkonto je Dienstnehmer:in und -->
<!-- Betriebsjahreslohnkonto — Monatsmatrix je Lohnart; Druck -->
<!-- über den Wizard l10n.at.payroll.jahresbericht.wizard. -->
<!-- ============================================================ -->
<record id="action_report_jahreslohnkonto" model="ir.actions.report">
<field name="name">Jahreslohnkonto (Dienstnehmer:in)</field>
<field name="model">hr.employee</field>
<field name="report_type">qweb-pdf</field>
<field name="report_name">l10n_at_hr_payroll.report_jahreslohnkonto</field>
<field name="report_file">l10n_at_hr_payroll.report_jahreslohnkonto</field>
<field name="paperformat_id" ref="hr_payroll.paperformat_euro_light"/>
</record>
<record id="action_report_betriebslohnkonto" model="ir.actions.report">
<field name="name">Betriebsjahreslohnkonto</field>
<field name="model">res.company</field>
<field name="report_type">qweb-pdf</field>
<field name="report_name">l10n_at_hr_payroll.report_betriebslohnkonto</field>
<field name="report_file">l10n_at_hr_payroll.report_betriebslohnkonto</field>
<field name="paperformat_id" ref="hr_payroll.paperformat_euro_light"/>
</record>
<template id="report_jahreslohnkonto">
<t t-call="web.html_container">
<t t-foreach="berichte" t-as="b">
<t t-call="web.external_layout">
<div class="page">
<h2>Jahreslohnkonto <t t-out="jahr"/><t t-out="b['employee'].name"/></h2>
<table class="table table-sm">
<thead>
<tr>
<th>Code</th>
<th>Lohnart</th>
<th>Kategorie</th>
<th class="text-end" t-foreach="range(1, 13)" t-as="monat">
<t t-out="monat"/>
</th>
<th class="text-end">Jahr</th>
</tr>
</thead>
<tbody>
<tr t-foreach="b['zeilen']" t-as="zeile">
<td><t t-out="zeile['code']"/></td>
<td><t t-out="zeile['name']"/></td>
<td><t t-out="zeile['kategorie']"/></td>
<td class="text-end" t-foreach="zeile['monate']" t-as="wert">
<span t-out="wert" t-options='{"widget": "monetary", "display_currency": b["employee"].company_id.currency_id}'/>
</td>
<td class="text-end"><strong><span t-out="zeile['total']" t-options='{"widget": "monetary", "display_currency": b["employee"].company_id.currency_id}'/></strong></td>
</tr>
</tbody>
</table>
</div>
</t>
</t>
</t>
</template>
<template id="report_betriebslohnkonto">
<t t-call="web.html_container">
<t t-foreach="berichte" t-as="b">
<t t-call="web.external_layout">
<div class="page">
<h2>Betriebsjahreslohnkonto <t t-out="jahr"/><t t-out="b['company'].name"/></h2>
<table class="table table-sm">
<thead>
<tr>
<th>Code</th>
<th>Lohnart</th>
<th>Kategorie</th>
<th class="text-end" t-foreach="range(1, 13)" t-as="monat">
<t t-out="monat"/>
</th>
<th class="text-end">Jahr</th>
</tr>
</thead>
<tbody>
<tr t-foreach="b['zeilen']" t-as="zeile">
<td><t t-out="zeile['code']"/></td>
<td><t t-out="zeile['name']"/></td>
<td><t t-out="zeile['kategorie']"/></td>
<td class="text-end" t-foreach="zeile['monate']" t-as="wert">
<span t-out="wert" t-options='{"widget": "monetary", "display_currency": b["company"].currency_id}'/>
</td>
<td class="text-end"><strong><span t-out="zeile['total']" t-options='{"widget": "monetary", "display_currency": b["company"].currency_id}'/></strong></td>
</tr>
</tbody>
</table>
</div>
</t>
</t>
</t>
</template>
<!-- Druck-Wizard -->
<record id="jahresbericht_wizard_form_view" model="ir.ui.view">
<field name="name">l10n.at.payroll.jahresbericht.wizard.form</field>
<field name="model">l10n.at.payroll.jahresbericht.wizard</field>
<field name="arch" type="xml">
<form string="Jahresberichte drucken">
<group>
<group>
<field name="jahr"/>
<field name="art"/>
</group>
<group>
<field name="company_id"
groups="base.group_multi_company"/>
<field name="employee_ids" widget="many2many_tags"
invisible="art != 'dn'"/>
</group>
</group>
<footer>
<button string="Drucken" name="action_drucken"
type="object" class="btn-primary"/>
<button string="Abbrechen" class="btn-secondary"
special="cancel"/>
</footer>
</form>
</field>
</record>
<record id="action_jahresbericht_wizard" model="ir.actions.act_window">
<field name="name">Jahresberichte (Lohnkonten)</field>
<field name="res_model">l10n.at.payroll.jahresbericht.wizard</field>
<field name="view_mode">form</field>
<field name="target">new</field>
</record>
<menuitem id="menu_l10n_at_payroll_jahresberichte"
name="Jahresberichte (Lohnkonten)"
parent="menu_l10n_at_payroll_root"
action="action_jahresbericht_wizard" sequence="80"/>
</odoo>
@@ -2,4 +2,45 @@ id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_l10n_at_payroll_tasy_mapping_user,L10n AT Payroll TASY-Mapping User,model_l10n_at_payroll_tasy_mapping,hr_payroll.group_hr_payroll_user,1,0,0,0 access_l10n_at_payroll_tasy_mapping_user,L10n AT Payroll TASY-Mapping User,model_l10n_at_payroll_tasy_mapping,hr_payroll.group_hr_payroll_user,1,0,0,0
access_l10n_at_payroll_tasy_mapping_manager,L10n AT Payroll TASY-Mapping Manager,model_l10n_at_payroll_tasy_mapping,hr_payroll.group_hr_payroll_manager,1,1,1,1 access_l10n_at_payroll_tasy_mapping_manager,L10n AT Payroll TASY-Mapping Manager,model_l10n_at_payroll_tasy_mapping,hr_payroll.group_hr_payroll_manager,1,1,1,1
access_l10n_at_payroll_tasy_import_wizard,L10n AT Payroll TASY-Import Wizard,model_l10n_at_payroll_tasy_import_wizard,hr_payroll.group_hr_payroll_user,1,1,1,1 access_l10n_at_payroll_tasy_import_wizard,L10n AT Payroll TASY-Import Wizard,model_l10n_at_payroll_tasy_import_wizard,hr_payroll.group_hr_payroll_user,1,1,1,1
access_l10n_at_payroll_tasy_import_line,L10n AT Payroll TASY-Import Line,model_l10n_at_payroll_tasy_import_line,hr_payroll.group_hr_payroll_user,1,1,1,1 access_l10n_at_payroll_tasy_import_line,L10n AT Payroll TASY-Import Line,model_l10n_at_payroll_tasy_import_line,hr_payroll.group_hr_payroll_user,1,1,1,1
access_l10n_at_payroll_l16_user,L10n AT Payroll L16 User,model_l10n_at_payroll_l16,hr_payroll.group_hr_payroll_user,1,1,1,0
access_l10n_at_payroll_l16_manager,L10n AT Payroll L16 Manager,model_l10n_at_payroll_l16,hr_payroll.group_hr_payroll_manager,1,1,1,1
access_l10n_at_payroll_l16_line_user,L10n AT Payroll L16-Line User,model_l10n_at_payroll_l16_line,hr_payroll.group_hr_payroll_user,1,1,1,1
access_l10n_at_payroll_kind_user,L10n AT Payroll Kind User,model_l10n_at_payroll_kind,hr_payroll.group_hr_payroll_user,1,1,1,1
access_l10n_at_payroll_kind_manager,L10n AT Payroll Kind Manager,model_l10n_at_payroll_kind,hr_payroll.group_hr_payroll_manager,1,1,1,1
access_l10n_at_payroll_frist_user,L10n AT Payroll Frist User,model_l10n_at_payroll_frist,hr_payroll.group_hr_payroll_user,1,1,1,1
access_l10n_at_payroll_frist_manager,L10n AT Payroll Frist Manager,model_l10n_at_payroll_frist,hr_payroll.group_hr_payroll_manager,1,1,1,1
access_l10n_at_payroll_jahresretro_wizard,L10n AT Payroll Jahresretro Wizard,model_l10n_at_payroll_jahresretro_wizard,hr_payroll.group_hr_payroll_user,1,1,1,1
access_l10n_at_payroll_jahresretro_ergebnis,L10n AT Payroll Jahresretro Ergebnis,model_l10n_at_payroll_jahresretro_ergebnis,hr_payroll.group_hr_payroll_user,1,1,1,1
access_l10n_at_payroll_mitteilungsfile_wizard,L10n AT Payroll Mitteilungsfile Wizard,model_l10n_at_payroll_mitteilungsfile_wizard,hr_payroll.group_hr_payroll_user,1,1,1,1
access_l10n_at_payroll_jahresbericht_wizard,L10n AT Payroll Jahresbericht Wizard,model_l10n_at_payroll_jahresbericht_wizard,hr_payroll.group_hr_payroll_user,1,1,1,1
access_l10n_at_payroll_tasy_katalog_wizard,L10n AT Payroll TASY-Katalog Wizard,model_l10n_at_payroll_tasy_katalog_wizard,hr_payroll.group_hr_payroll_user,1,1,1,1
access_l10n_at_payroll_tasy_bsgr_user,L10n AT Payroll TASY-BSGR User,model_l10n_at_payroll_tasy_bsgr,hr_payroll.group_hr_payroll_user,1,0,0,0
access_l10n_at_payroll_tasy_bsgr_manager,L10n AT Payroll TASY-BSGR Manager,model_l10n_at_payroll_tasy_bsgr,hr_payroll.group_hr_payroll_manager,1,1,1,1
access_l10n_at_payroll_tasy_ergaenzung_user,L10n AT Payroll TASY-Ergänzung User,model_l10n_at_payroll_tasy_ergaenzung,hr_payroll.group_hr_payroll_user,1,0,0,0
access_l10n_at_payroll_tasy_ergaenzung_manager,L10n AT Payroll TASY-Ergänzung Manager,model_l10n_at_payroll_tasy_ergaenzung,hr_payroll.group_hr_payroll_manager,1,1,1,1
access_l10n_at_payroll_tasy_dgg_user,L10n AT Payroll TASY-Dienstgebergruppe User,model_l10n_at_payroll_tasy_dienstgebergruppe,hr_payroll.group_hr_payroll_user,1,0,0,0
access_l10n_at_payroll_tasy_dgg_manager,L10n AT Payroll TASY-Dienstgebergruppe Manager,model_l10n_at_payroll_tasy_dienstgebergruppe,hr_payroll.group_hr_payroll_manager,1,1,1,1
access_l10n_at_payroll_tasy_tarifgruppe_user,L10n AT Payroll TASY-Tarifgruppe User,model_l10n_at_payroll_tasy_tarifgruppe,hr_payroll.group_hr_payroll_user,1,0,0,0
access_l10n_at_payroll_tasy_tarifgruppe_manager,L10n AT Payroll TASY-Tarifgruppe Manager,model_l10n_at_payroll_tasy_tarifgruppe,hr_payroll.group_hr_payroll_manager,1,1,1,1
access_l10n_at_payroll_tasy_tarifset_user,L10n AT Payroll TASY-Tarifset User,model_l10n_at_payroll_tasy_tarifset,hr_payroll.group_hr_payroll_user,1,0,0,0
access_l10n_at_payroll_tasy_tarifset_manager,L10n AT Payroll TASY-Tarifset Manager,model_l10n_at_payroll_tasy_tarifset,hr_payroll.group_hr_payroll_manager,1,1,1,1
access_l10n_at_payroll_tasy_tarif_user,L10n AT Payroll TASY-Tarif User,model_l10n_at_payroll_tasy_tarif,hr_payroll.group_hr_payroll_user,1,0,0,0
access_l10n_at_payroll_tasy_tarif_manager,L10n AT Payroll TASY-Tarif Manager,model_l10n_at_payroll_tasy_tarif,hr_payroll.group_hr_payroll_manager,1,1,1,1
access_l10n_at_payroll_tasy_zuord_user,L10n AT Payroll TASY-Zuordnung User,model_l10n_at_payroll_tasy_tarifset_zuord,hr_payroll.group_hr_payroll_user,1,0,0,0
access_l10n_at_payroll_tasy_zuord_manager,L10n AT Payroll TASY-Zuordnung Manager,model_l10n_at_payroll_tasy_tarifset_zuord,hr_payroll.group_hr_payroll_manager,1,1,1,1
access_l10n_at_payroll_tasy_zuabschlag_user,L10n AT Payroll TASY-Zuabschlag User,model_l10n_at_payroll_tasy_zuabschlag,hr_payroll.group_hr_payroll_user,1,0,0,0
access_l10n_at_payroll_tasy_zuabschlag_manager,L10n AT Payroll TASY-Zuabschlag Manager,model_l10n_at_payroll_tasy_zuabschlag,hr_payroll.group_hr_payroll_manager,1,1,1,1
access_l10n_at_payroll_tasy_zuabschlag_steuerung_user,L10n AT Payroll TASY-Zuabschlag-Steuerung User,model_l10n_at_payroll_tasy_zuabschlag_steuerung,hr_payroll.group_hr_payroll_user,1,0,0,0
access_l10n_at_payroll_tasy_zuabschlag_steuerung_manager,L10n AT Payroll TASY-Zuabschlag-Steuerung Manager,model_l10n_at_payroll_tasy_zuabschlag_steuerung,hr_payroll.group_hr_payroll_manager,1,1,1,1
access_l10n_at_payroll_l17_user,L10n AT Payroll L17 User,model_l10n_at_payroll_l17,hr_payroll.group_hr_payroll_user,1,1,1,0
access_l10n_at_payroll_l17_manager,L10n AT Payroll L17 Manager,model_l10n_at_payroll_l17,hr_payroll.group_hr_payroll_manager,1,1,1,1
access_l10n_at_payroll_l17_line_user,L10n AT Payroll L17-Line User,model_l10n_at_payroll_l17_line,hr_payroll.group_hr_payroll_user,1,1,1,1
access_l10n_at_payroll_m109_user,L10n AT Payroll Mitteilung109 User,model_l10n_at_payroll_mitteilung109,hr_payroll.group_hr_payroll_user,1,1,1,0
access_l10n_at_payroll_m109_manager,L10n AT Payroll Mitteilung109 Manager,model_l10n_at_payroll_mitteilung109,hr_payroll.group_hr_payroll_manager,1,1,1,1
access_l10n_at_payroll_m109_line_user,L10n AT Payroll Mitteilung109-Line User,model_l10n_at_payroll_mitteilung109_line,hr_payroll.group_hr_payroll_user,1,1,1,1
access_l10n_at_payroll_m109_np_user,L10n AT Payroll Mitteilung109-NP User,model_l10n_at_payroll_mitteilung109_np,hr_payroll.group_hr_payroll_user,1,1,1,1
access_l10n_at_payroll_sm_user,L10n AT Payroll SM User,model_l10n_at_payroll_schwerarbeitsmeldung,hr_payroll.group_hr_payroll_user,1,1,1,0
access_l10n_at_payroll_sm_manager,L10n AT Payroll SM Manager,model_l10n_at_payroll_schwerarbeitsmeldung,hr_payroll.group_hr_payroll_manager,1,1,1,1
access_l10n_at_payroll_sm_line_user,L10n AT Payroll SM-Line User,model_l10n_at_payroll_schwerarbeitsmeldung_line,hr_payroll.group_hr_payroll_user,1,1,1,1
access_l10n_at_payroll_sm_taetigkeit_user,L10n AT Payroll SM-Taetigkeit User,model_l10n_at_payroll_schwerarbeitsmeldung_taetigkeit,hr_payroll.group_hr_payroll_user,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_l10n_at_payroll_tasy_mapping_user L10n AT Payroll TASY-Mapping User model_l10n_at_payroll_tasy_mapping hr_payroll.group_hr_payroll_user 1 0 0 0
3 access_l10n_at_payroll_tasy_mapping_manager L10n AT Payroll TASY-Mapping Manager model_l10n_at_payroll_tasy_mapping hr_payroll.group_hr_payroll_manager 1 1 1 1
4 access_l10n_at_payroll_tasy_import_wizard L10n AT Payroll TASY-Import Wizard model_l10n_at_payroll_tasy_import_wizard hr_payroll.group_hr_payroll_user 1 1 1 1
5 access_l10n_at_payroll_tasy_import_line L10n AT Payroll TASY-Import Line model_l10n_at_payroll_tasy_import_line hr_payroll.group_hr_payroll_user 1 1 1 1
6 access_l10n_at_payroll_l16_user L10n AT Payroll L16 User model_l10n_at_payroll_l16 hr_payroll.group_hr_payroll_user 1 1 1 0
7 access_l10n_at_payroll_l16_manager L10n AT Payroll L16 Manager model_l10n_at_payroll_l16 hr_payroll.group_hr_payroll_manager 1 1 1 1
8 access_l10n_at_payroll_l16_line_user L10n AT Payroll L16-Line User model_l10n_at_payroll_l16_line hr_payroll.group_hr_payroll_user 1 1 1 1
9 access_l10n_at_payroll_kind_user L10n AT Payroll Kind User model_l10n_at_payroll_kind hr_payroll.group_hr_payroll_user 1 1 1 1
10 access_l10n_at_payroll_kind_manager L10n AT Payroll Kind Manager model_l10n_at_payroll_kind hr_payroll.group_hr_payroll_manager 1 1 1 1
11 access_l10n_at_payroll_frist_user L10n AT Payroll Frist User model_l10n_at_payroll_frist hr_payroll.group_hr_payroll_user 1 1 1 1
12 access_l10n_at_payroll_frist_manager L10n AT Payroll Frist Manager model_l10n_at_payroll_frist hr_payroll.group_hr_payroll_manager 1 1 1 1
13 access_l10n_at_payroll_jahresretro_wizard L10n AT Payroll Jahresretro Wizard model_l10n_at_payroll_jahresretro_wizard hr_payroll.group_hr_payroll_user 1 1 1 1
14 access_l10n_at_payroll_jahresretro_ergebnis L10n AT Payroll Jahresretro Ergebnis model_l10n_at_payroll_jahresretro_ergebnis hr_payroll.group_hr_payroll_user 1 1 1 1
15 access_l10n_at_payroll_mitteilungsfile_wizard L10n AT Payroll Mitteilungsfile Wizard model_l10n_at_payroll_mitteilungsfile_wizard hr_payroll.group_hr_payroll_user 1 1 1 1
16 access_l10n_at_payroll_jahresbericht_wizard L10n AT Payroll Jahresbericht Wizard model_l10n_at_payroll_jahresbericht_wizard hr_payroll.group_hr_payroll_user 1 1 1 1
17 access_l10n_at_payroll_tasy_katalog_wizard L10n AT Payroll TASY-Katalog Wizard model_l10n_at_payroll_tasy_katalog_wizard hr_payroll.group_hr_payroll_user 1 1 1 1
18 access_l10n_at_payroll_tasy_bsgr_user L10n AT Payroll TASY-BSGR User model_l10n_at_payroll_tasy_bsgr hr_payroll.group_hr_payroll_user 1 0 0 0
19 access_l10n_at_payroll_tasy_bsgr_manager L10n AT Payroll TASY-BSGR Manager model_l10n_at_payroll_tasy_bsgr hr_payroll.group_hr_payroll_manager 1 1 1 1
20 access_l10n_at_payroll_tasy_ergaenzung_user L10n AT Payroll TASY-Ergänzung User model_l10n_at_payroll_tasy_ergaenzung hr_payroll.group_hr_payroll_user 1 0 0 0
21 access_l10n_at_payroll_tasy_ergaenzung_manager L10n AT Payroll TASY-Ergänzung Manager model_l10n_at_payroll_tasy_ergaenzung hr_payroll.group_hr_payroll_manager 1 1 1 1
22 access_l10n_at_payroll_tasy_dgg_user L10n AT Payroll TASY-Dienstgebergruppe User model_l10n_at_payroll_tasy_dienstgebergruppe hr_payroll.group_hr_payroll_user 1 0 0 0
23 access_l10n_at_payroll_tasy_dgg_manager L10n AT Payroll TASY-Dienstgebergruppe Manager model_l10n_at_payroll_tasy_dienstgebergruppe hr_payroll.group_hr_payroll_manager 1 1 1 1
24 access_l10n_at_payroll_tasy_tarifgruppe_user L10n AT Payroll TASY-Tarifgruppe User model_l10n_at_payroll_tasy_tarifgruppe hr_payroll.group_hr_payroll_user 1 0 0 0
25 access_l10n_at_payroll_tasy_tarifgruppe_manager L10n AT Payroll TASY-Tarifgruppe Manager model_l10n_at_payroll_tasy_tarifgruppe hr_payroll.group_hr_payroll_manager 1 1 1 1
26 access_l10n_at_payroll_tasy_tarifset_user L10n AT Payroll TASY-Tarifset User model_l10n_at_payroll_tasy_tarifset hr_payroll.group_hr_payroll_user 1 0 0 0
27 access_l10n_at_payroll_tasy_tarifset_manager L10n AT Payroll TASY-Tarifset Manager model_l10n_at_payroll_tasy_tarifset hr_payroll.group_hr_payroll_manager 1 1 1 1
28 access_l10n_at_payroll_tasy_tarif_user L10n AT Payroll TASY-Tarif User model_l10n_at_payroll_tasy_tarif hr_payroll.group_hr_payroll_user 1 0 0 0
29 access_l10n_at_payroll_tasy_tarif_manager L10n AT Payroll TASY-Tarif Manager model_l10n_at_payroll_tasy_tarif hr_payroll.group_hr_payroll_manager 1 1 1 1
30 access_l10n_at_payroll_tasy_zuord_user L10n AT Payroll TASY-Zuordnung User model_l10n_at_payroll_tasy_tarifset_zuord hr_payroll.group_hr_payroll_user 1 0 0 0
31 access_l10n_at_payroll_tasy_zuord_manager L10n AT Payroll TASY-Zuordnung Manager model_l10n_at_payroll_tasy_tarifset_zuord hr_payroll.group_hr_payroll_manager 1 1 1 1
32 access_l10n_at_payroll_tasy_zuabschlag_user L10n AT Payroll TASY-Zuabschlag User model_l10n_at_payroll_tasy_zuabschlag hr_payroll.group_hr_payroll_user 1 0 0 0
33 access_l10n_at_payroll_tasy_zuabschlag_manager L10n AT Payroll TASY-Zuabschlag Manager model_l10n_at_payroll_tasy_zuabschlag hr_payroll.group_hr_payroll_manager 1 1 1 1
34 access_l10n_at_payroll_tasy_zuabschlag_steuerung_user L10n AT Payroll TASY-Zuabschlag-Steuerung User model_l10n_at_payroll_tasy_zuabschlag_steuerung hr_payroll.group_hr_payroll_user 1 0 0 0
35 access_l10n_at_payroll_tasy_zuabschlag_steuerung_manager L10n AT Payroll TASY-Zuabschlag-Steuerung Manager model_l10n_at_payroll_tasy_zuabschlag_steuerung hr_payroll.group_hr_payroll_manager 1 1 1 1
36 access_l10n_at_payroll_l17_user L10n AT Payroll L17 User model_l10n_at_payroll_l17 hr_payroll.group_hr_payroll_user 1 1 1 0
37 access_l10n_at_payroll_l17_manager L10n AT Payroll L17 Manager model_l10n_at_payroll_l17 hr_payroll.group_hr_payroll_manager 1 1 1 1
38 access_l10n_at_payroll_l17_line_user L10n AT Payroll L17-Line User model_l10n_at_payroll_l17_line hr_payroll.group_hr_payroll_user 1 1 1 1
39 access_l10n_at_payroll_m109_user L10n AT Payroll Mitteilung109 User model_l10n_at_payroll_mitteilung109 hr_payroll.group_hr_payroll_user 1 1 1 0
40 access_l10n_at_payroll_m109_manager L10n AT Payroll Mitteilung109 Manager model_l10n_at_payroll_mitteilung109 hr_payroll.group_hr_payroll_manager 1 1 1 1
41 access_l10n_at_payroll_m109_line_user L10n AT Payroll Mitteilung109-Line User model_l10n_at_payroll_mitteilung109_line hr_payroll.group_hr_payroll_user 1 1 1 1
42 access_l10n_at_payroll_m109_np_user L10n AT Payroll Mitteilung109-NP User model_l10n_at_payroll_mitteilung109_np hr_payroll.group_hr_payroll_user 1 1 1 1
43 access_l10n_at_payroll_sm_user L10n AT Payroll SM User model_l10n_at_payroll_schwerarbeitsmeldung hr_payroll.group_hr_payroll_user 1 1 1 0
44 access_l10n_at_payroll_sm_manager L10n AT Payroll SM Manager model_l10n_at_payroll_schwerarbeitsmeldung hr_payroll.group_hr_payroll_manager 1 1 1 1
45 access_l10n_at_payroll_sm_line_user L10n AT Payroll SM-Line User model_l10n_at_payroll_schwerarbeitsmeldung_line hr_payroll.group_hr_payroll_user 1 1 1 1
46 access_l10n_at_payroll_sm_taetigkeit_user L10n AT Payroll SM-Taetigkeit User model_l10n_at_payroll_schwerarbeitsmeldung_taetigkeit hr_payroll.group_hr_payroll_user 1 1 1 1
@@ -0,0 +1,43 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- ============================================================ -->
<!-- GP9/AP9: Multi-Company-Record-Rules für alle AT-Payroll- -->
<!-- Datenmodelle (Muster: hr_payroll-Engine). Jede:r Payroll- -->
--> User sieht nur die Daten der Companies, auf die sie/er Zugriff
<!-- hat (company_ids aus ir.rule-Standard-Mechanik). -->
<!-- ============================================================ -->
<!-- Lohnzettel Finanz (L16) + Zeilen -->
<record id="rule_l16_company" model="ir.rule">
<field name="name">L16: eigene Company</field>
<field name="model_id" ref="model_l10n_at_payroll_l16"/>
<field name="domain_force">[('company_id', 'in', company_ids)]</field>
<field name="groups"
eval="[(4, ref('hr_payroll.group_hr_payroll_user'))]"/>
</record>
<record id="rule_l16_line_company" model="ir.rule">
<field name="name">L16-Zeile: eigene Company</field>
<field name="model_id" ref="model_l10n_at_payroll_l16_line"/>
<field name="domain_force">[('l16_id.company_id', 'in', company_ids)]</field>
<field name="groups"
eval="[(4, ref('hr_payroll.group_hr_payroll_user'))]"/>
</record>
<!-- Kind (FB+-Wartung) -->
<record id="rule_kind_company" model="ir.rule">
<field name="name">Kind: eigene Company</field>
<field name="model_id" ref="model_l10n_at_payroll_kind"/>
<field name="domain_force">[('employee_id.company_id', 'in', company_ids)]</field>
<field name="groups"
eval="[(4, ref('hr_payroll.group_hr_payroll_user'))]"/>
</record>
<!-- Fristenkalender -->
<record id="rule_frist_company" model="ir.rule">
<field name="name">Frist: eigene Company</field>
<field name="model_id" ref="model_l10n_at_payroll_frist"/>
<field name="domain_force">[('company_id', 'in', company_ids)]</field>
<field name="groups"
eval="[(4, ref('hr_payroll.group_hr_payroll_user'))]"/>
</record>
</odoo>
+5 -1
View File
@@ -1,4 +1,8 @@
# Part of the odoo-at-payroll project. License: LGPL-3. # Part of the odoo-at-payroll project. License: LGPL-3.
from . import test_tasy_import from . import test_tasy_import
from . import test_tasy_katalog
from . import test_lohnsteuer from . import test_lohnsteuer
from . import test_sozialversicherung from . import test_sozialversicherung
from . import test_elda_l16
from . import test_elda_l17
from . import test_gp8
@@ -0,0 +1,274 @@
# Part of the odoo-at-payroll project. License: LGPL-3.
"""GP7 Kern: ELDA-Fixsatz-Framework (DM-ORG 42.7.0 Konventionen) und
Lohnzettel-Finanz-Generator (I1/L1, BEST LF/94) Positionsexakte
Satz-Assertions, CRLF/ISO-8859-15-Encoding, SANR/SANZ-Kette,
KZ-245-Formel-Konsistenz aus GP3-Daten."""
import base64
import datetime
import types
from odoo.tests import TransactionCase, tagged
from odoo.addons.l10n_at_hr_payroll.models.elda import (
L10N_AT_ELDA_IDTEIL,
L10nAtEldaBestand,
L10nAtEldaDatei,
L10nAtEldaSatz,
l10n_at_euro_cent,
)
@tagged('post_install', '-at_install')
class EldaFrameworkTest(TransactionCase):
def test_01_feldformatierung(self):
"""a/n: linksbündig blank-gefüllt; n: rechtsbündig mit führenden
Nullen, keine Interpunktion; Cent kaufmännisch; skala=3 für
Prozentsätze (39,30 % 039300)."""
satz = L10nAtEldaSatz(
[(1, 'A', 1, 5, 'a'), (2, 'N', 6, 4, 'n'),
(3, 'B', 10, 10, 'n')], 20)
satz.setze('A', 'AB')
satz.setze('N', 42)
self.assertEqual(satz.render()[:5], 'AB ')
self.assertEqual(satz.render()[5:9], '0042')
# Unbelegtes n-Feld: Grundstellung 0.
self.assertEqual(satz.render()[9:19], '0' * 10)
self.assertEqual(l10n_at_euro_cent(1234.567), 123457)
self.assertEqual(l10n_at_euro_cent(0.005), 1)
prozent = L10nAtEldaSatz(
[(1, 'VPTA', 1, 6, 'n')], 6)
prozent.setze('VPTA', 39.30, skala=3)
self.assertEqual(prozent.render(), '039300')
def test_02_bestand_datei(self):
"""Vorlaufsatz SART 00 mit BEST/VSTR/VERS, SANR lückenlos ab 1,
SANZ inklusive Vorlauf/Schluss, CRLF-getrennte Zeilen in
ISO-8859-15, optionaler Dateiheader ENCD."""
satz1 = L10nAtEldaSatz(
L10N_AT_ELDA_IDTEIL + [(2, 'X', 21, 4, 'a')], 24,
bezeichnung='Test')
satz1.setze('SART', '45')
satz1.setze('VSTR', 'ST')
satz1.setze('X', 'TEST')
satz2 = L10nAtEldaSatz(
L10N_AT_ELDA_IDTEIL + [(2, 'Y', 21, 4, 'n')], 24,
bezeichnung='Test')
satz2.setze('SART', '45')
satz2.setze('VSTR', 'ST')
satz2.setze('Y', 7)
bestand = L10nAtEldaBestand(
'AD', 'ST', '02',
hersteller={'name': 'Test', 'kfz': 'A'},
datensaetze=[satz1, satz2])
zeilen = bestand.render(
erstellungsdatum=datetime.date(2026, 1, 15))
vorlauf = zeilen[0]
self.assertEqual(len(zeilen), 4)
self.assertEqual(vorlauf[18:20], 'ST') # VSTR (Pos. 19)
self.assertEqual(vorlauf[22:24], 'AD') # BEST
self.assertEqual(vorlauf[30:38], '15012026') # EDAT TTMMJJJJ
self.assertEqual(vorlauf[149:151], '02') # VERS
self.assertEqual(zeilen[1][0:2], '45')
self.assertEqual(zeilen[1][2:9], '0000002') # SANR
self.assertEqual(zeilen[3][20:26], '000004') # SANZ
datei = L10nAtEldaDatei([bestand])
roh = datei.render()
self.assertIn(b'\r\n', roh)
erste = roh.split(b'\r\n')[0]
self.assertEqual(erste[:4], b' 01')
self.assertIn(b'ISO-8859-15', erste)
# Umlaut in ISO-8859-15 codiert.
satz3 = L10nAtEldaSatz(
L10N_AT_ELDA_IDTEIL + [(2, 'Z', 21, 10, 'a')], 30,
bezeichnung='Umlaut')
satz3.setze('SART', '45')
satz3.setze('VSTR', 'ST')
satz3.setze('Z', 'Für')
self.assertIn('Für'.encode('ISO-8859-15'),
satz3.render().encode('ISO-8859-15'))
@tagged('post_install', '-at_install')
class L16GeneratorTest(TransactionCase):
def setUp(self):
super().setUp()
self.company = self.env.company
self.company.write({
'l10n_at_lohnsteuernummer': '123456789',
'l10n_at_beitragskontonummer': '1412345678',
})
self.structure = self.env.ref(
'l10n_at_hr_payroll_private.structure_at_privat')
self.kv2203 = self.env.ref(
'l10n_at_hr_payroll_private.kv_si_2203')
self.gruppe_ii = self.env.ref(
'l10n_at_hr_payroll_private.kv2203_grp_ii')
self.calendar = self.env['resource.calendar'].create({
'name': 'AT Test 40h',
'tz': 'Europe/Vienna',
'hours_per_day': 8,
'attendance_ids': [
(0, 0, {'name': 'Tag %d' % wd, 'dayofweek': str(wd),
'hour_from': 8.0, 'hour_to': 16.0})
for wd in range(5)
],
})
self.company.resource_calendar_id = self.calendar
employee = self.env['hr.employee'].create({
'name': 'L16 Test', 'birthday': '1990-05-15'})
version = self.env['hr.version'].search(
[('employee_id', '=', employee.id)], limit=1)
version.write({
'contract_date_start': datetime.date(2020, 1, 1),
'ssnid': '1234150590',
'sex': 'female',
'kv_id': self.kv2203.id,
'kv_gruppe_id': self.gruppe_ii.id,
'erfahrungsstufe': 1,
'resource_calendar_id': self.calendar.id,
})
self.employee = employee
self.version = version
def _slip(self, monat, jahr=2026, confirm=True):
erster = datetime.date(jahr, monat, 1)
letzter = (erster.replace(day=28) + datetime.timedelta(days=4)) \
.replace(day=1) - datetime.timedelta(days=1)
slip = self.env['hr.payslip'].create({
'name': 'L16 %d/%d' % (monat, jahr),
'employee_id': self.employee.id,
'struct_id': self.structure.id,
'date_from': erster,
'date_to': letzter,
})
slip.compute_sheet()
if confirm:
slip.action_payslip_done()
return slip
def test_01_l16_lauf_und_positionen(self):
"""Jahreslauf: KZ-Salden aus GP3/GP4-Flags, L1-Record mit
exakten Positionen (Satzlänge 3500, REFN/ARTL/KZ-V/B-Paare,
SOZS/AVLN/AGBD), I1 (1100, STNRA/JAHR/GESA/ANZA), Datei mit
LF-Bestand VSTR 94, KZ-245-Konsistenz per Konstruktion."""
self._slip(11) # November mit B13
deklaration = self.env['l10n.at.payroll.l16'].create({
'company_id': self.company.id,
'jahr': 2026,
'art': '01',
})
deklaration.action_erzeugen()
self.assertEqual(deklaration.state, 'done')
self.assertEqual(len(deklaration.line_ids), 1)
line = deklaration.line_ids
# November: KZ 210 = KVGEO + B13 = 4.118,82 (§ 25 gesamt);
# KZ 220 = B13 = 2.059,41; KZ 245 = laufende Bemessung.
self.assertAlmostEqual(line.kz210, 4118.82, places=2)
self.assertAlmostEqual(line.kz220, 2059.41, places=2)
# KZ-245-Formel: 210 215 220 230 243
erwartet = (line.kz210 - line.kz215 - line.kz220
- line.kz230 - line.kz243)
self.assertAlmostEqual(line.kz245, erwartet, places=1)
# Datei: I1 (1100) + L1 (3500) + Vorlauf/Schluss (auf 3500
# aufgefüllt), LF-Bestand, VSTR 94, Lohnzettelversion 28.
roh = base64.b64decode(deklaration.datei_l16).decode(
'ISO-8859-15')
zeilen = roh.strip('\r\n').split('\r\n')
# Dateiheader + Vorlauf + I1 + L1 + Schluss.
self.assertEqual(len(zeilen), 5)
header, vorlauf, i1, l1, schluss = zeilen
self.assertEqual(header[:4], ' 01')
self.assertIn('ISO-8859-15', header)
self.assertEqual(len(i1), 1100)
self.assertEqual(len(l1), 3500)
self.assertEqual(vorlauf[0:2], '00')
self.assertEqual(vorlauf[18:20], '94')
self.assertEqual(vorlauf[22:24], 'LF')
self.assertEqual(vorlauf[149:151], '28')
self.assertEqual(i1[18:20], '94')
self.assertEqual(i1[20:21], 'I')
self.assertEqual(i1[34:43], '123456789') # STNRL
self.assertEqual(i1[50:59], '123456789') # STNRA
self.assertEqual(i1[82:84], '03') # STVE
self.assertEqual(i1[232:236], '2026') # JAHR
self.assertEqual(i1[236:243], '0000001') # GESA
self.assertEqual(l1[18:20], '94')
self.assertEqual(l1[20:21], 'L')
self.assertEqual(l1[95:97], '01') # ARTL
self.assertEqual(l1[119:123], '0111') # BELZ TTMM
self.assertEqual(l1[123:127], '3011') # ENLZ
self.assertEqual(l1[127:131], '2026') # JALZ
self.assertEqual(l1[131:132], '3') # SOZS Angestellte
self.assertEqual(l1[132:136], '1234') # AVLN LLLP
self.assertEqual(l1[136:142], '150590') # AGBD TTMMJJ
# KZ-Felder: V/B-Paare (1-based-Positionen → Index pos1).
self.assertEqual(l1[296:297], '+') # V210 (Pos. 297)
self.assertEqual(l1[297:307], '0000411882') # B210
self.assertEqual(l1[318:319], '+') # V220 (Pos. 319)
self.assertEqual(l1[319:329], '0000205941') # B220
self.assertEqual(l1[450:451], '+') # V245 (Pos. 451)
b245 = '%010d' % int(round(line.kz245 * 100))
self.assertEqual(l1[451:461], b245)
self.assertEqual(schluss[0:2], '99')
self.assertEqual(schluss[20:26], '000004') # SANZ
def test_01b_abfertigung_lohnsteuer_salden(self):
"""Abs.-3-Bezug bleibt außerhalb KZ 210/220; LST_ABF zählt zu 260."""
deklaration = self.env['l10n.at.payroll.l16'].create({
'company_id': self.company.id,
'jahr': 2026,
'art': '01',
})
l16_line = self.env['l10n.at.payroll.l16.line'].create({
'l16_id': deklaration.id,
'employee_id': self.employee.id,
})
abs3_rule = types.SimpleNamespace(
l10n_at_bemessung='sonstig', l10n_at_s67_abs='abs3')
lst_rule = types.SimpleNamespace(
l10n_at_bemessung='neutral', l10n_at_s67_abs='keine')
slip = types.SimpleNamespace(
line_ids=[
types.SimpleNamespace(
salary_rule_id=abs3_rule, code='ABF_ALT', total=12000.0),
types.SimpleNamespace(
salary_rule_id=lst_rule, code='LST_ABF', total=720.0),
types.SimpleNamespace(
salary_rule_id=lst_rule, code='LST_ABF_FREI',
total=300.0),
],
_l10n_at_freibetraege_68=lambda: {'szfn': 0.0, 'ue_std': 0.0},
)
salden = l16_line._kz_salden([slip])
self.assertEqual(salden['kz210'], 0.0)
self.assertEqual(salden['kz220'], 0.0)
self.assertEqual(salden['lohnsteuer'], 1020.0)
def test_02_arbeitsstaette(self):
"""§ 34 Abs 6 ASVG: je AN ein Satz 45 (Länge 300) mit BKNR,
VSNR, STNR und GKZ aus dem Dienstort-Partner; Warnung, wenn
die GKZ fehlt."""
self._slip(1)
deklaration = self.env['l10n.at.payroll.l16'].create({
'company_id': self.company.id,
'jahr': 2026,
'art': '01',
})
deklaration.action_erzeugen()
self.assertTrue(deklaration.datei_arbeitsstaette)
roh = base64.b64decode(
deklaration.datei_arbeitsstaette).decode('ISO-8859-15')
zeilen = roh.strip('\r\n').split('\r\n')
vorlauf, satz45 = zeilen[1], zeilen[2]
self.assertEqual(vorlauf[18:20], 'ST')
self.assertEqual(vorlauf[22:24], 'AD')
self.assertEqual(satz45[0:2], '45')
self.assertEqual(len(satz45), 300)
self.assertEqual(satz45[22:32], '1412345678') # BKNR
self.assertEqual(satz45[32:42], '1234150590') # VSNR
self.assertEqual(satz45[52:61], '123456789') # STNR
self.assertIn('Gemeindekennziffer', deklaration.protokoll)
@@ -0,0 +1,216 @@
# Part of the odoo-at-payroll project. License: LGPL-3.
"""AP17: L17 (A1) und Finanz-Mitteilungen § 109a (W1)/§ 109b (B1) —
Positionsexakte Satz-Assertions (ODL-Extraktion 2026-09-16, 1-basierte
DM-ORG-Positionen 0-basierte String-Indizes), Prüfkatalog-Logik
(350-Formel, 357/347-Caps, 354-Cap aliquotiert) und Schwellen der
Mitteilungsläufe."""
import base64
import datetime
import types
from odoo.tests import TransactionCase, tagged
from odoo.addons.l10n_at_hr_payroll.models.elda import (
L10N_AT_ELDA_IDTEIL,
L10nAtEldaSatz,
)
from odoo.addons.l10n_at_hr_payroll.models.lohnzettel_l17 import L17_SATZ
@tagged('post_install', '-at_install')
class L17SatzTest(TransactionCase):
def test_01_a1_satz_laenge_positionen(self):
"""A1: Länge 1100; FSART 'A' an Position 21; BELZ 120; JALZ 128;
B350 an 281 (10 n, Cent); HOPA an 618; Satzlänge exakt."""
satz = L10nAtEldaSatz(
L10N_AT_ELDA_IDTEIL + L17_SATZ, 1100,
bezeichnung='A1 Test')
satz.setze('SART', 'A1')
satz.setze('VSTR', '94')
satz.setze('FSART', 'A')
satz.setze('PENS', 'N')
satz.setze('AVSN', '1234150590')
satz.setze('BELZ', '0101')
satz.setze('ENLZ', '0131')
satz.setze('JALZ', 2026)
satz.setze('V350', '+')
satz.setze('B350', 1234.56, cent=True)
satz.setze('HOTA', 21)
satz.setze('VHOPA', '+')
satz.setze('HOPA', 63.0, cent=True)
roh = satz.render()
self.assertEqual(len(roh), 1100)
self.assertEqual(roh[20], 'A') # FSART pos 21
self.assertEqual(roh[119:123], '0101') # BELZ pos 120
self.assertEqual(roh[127:131], '2026') # JALZ pos 128
self.assertEqual(roh[131], 'N') # PENS pos 132
self.assertEqual(roh[132:142], '1234150590') # AVSN pos 133
self.assertEqual(roh[279], '+') # V350 pos 280
self.assertEqual(roh[280:290], '0000123456') # B350 pos 281
self.assertEqual(roh[616], '+') # VHOPA pos 617
self.assertEqual(roh[617:627], '0000006300') # HOPA pos 618
def test_02_kz_salden_mapping(self):
"""GP3-Flags → L17-KZs: laufend → 350; sonstig ohne Abs-3/6 →
350+351; abs3/abs6 352; SVDN_* 357; SVDN_SZ 347; § 68-
Freibetrag ÜStd 354; Homeoffice-Regel HOPA."""
deklaration = self.env['l10n.at.payroll.l17'].create({
'company_id': self.env.company.id, 'jahr': 2026})
laufend = types.SimpleNamespace(
l10n_at_bemessung='laufend', l10n_at_s67_abs='keine')
sz = types.SimpleNamespace(
l10n_at_bemessung='sonstig', l10n_at_s67_abs='keine')
abf = types.SimpleNamespace(
l10n_at_bemessung='sonstig', l10n_at_s67_abs='abs3')
neutral = types.SimpleNamespace(
l10n_at_bemessung='neutral', l10n_at_s67_abs='keine')
slip = types.SimpleNamespace(
line_ids=[
types.SimpleNamespace(salary_rule_id=laufend,
code='GEHALT', total=3000.0),
types.SimpleNamespace(salary_rule_id=sz,
code='SZ13', total=3000.0),
types.SimpleNamespace(salary_rule_id=abf,
code='ABF_ALT', total=6000.0),
types.SimpleNamespace(salary_rule_id=neutral,
code='SVDN_KV', total=100.0),
types.SimpleNamespace(salary_rule_id=neutral,
code='SVDN_SZ', total=50.0),
types.SimpleNamespace(salary_rule_id=laufend,
code='SB_HOMEOFFICE', total=63.0),
],
input_line_ids=[],
_l10n_at_freibetraege_68=lambda: {'szfn': 0.0,
'ue_std': 60.0},
)
salden, hota = deklaration._l10n_at_kz_salden(slip)
self.assertEqual(salden['kz350'], 12063.0)
self.assertEqual(salden['kz351'], 3000.0)
self.assertEqual(salden['kz352'], 6000.0)
self.assertEqual(salden['kz354'], 60.0)
self.assertEqual(salden['kz357'], 100.0)
self.assertEqual(salden['kz347'], 50.0)
self.assertEqual(salden['hopa'], 63.0)
def test_03_l17_pruefungen_pruefkatalog(self):
deklaration = self.env['l10n.at.payroll.l17'].create({
'company_id': self.env.company.id, 'jahr': 2026})
employee = self.env['hr.employee'].create({
'name': 'L17 Prüfung', 'birthday': '1990-01-01'})
line = self.env['l10n.at.payroll.l17.line'].create({
'l17_id': deklaration.id,
'employee_id': employee.id,
'date_from': datetime.date(2026, 1, 1),
'date_to': datetime.date(2026, 1, 31),
})
fehler, warnungen = [], []
line.write({'kz350': 5000.0, 'kz351': 3000.0, 'kz352': 2500.0})
line._l10n_at_pruefungen(fehler, warnungen)
self.assertTrue(any('KZ 350' in f for f in fehler))
fehler.clear()
line.write({'kz350': 12000.0, 'kz351': 3000.0, 'kz352': 0.0,
'kz357': 4900.0})
line._l10n_at_pruefungen(fehler, warnungen)
self.assertTrue(any('KZ 357' in f for f in fehler))
fehler.clear()
line.write({'kz357': 0.0, 'kz354': 2500.0})
line._l10n_at_pruefungen(fehler, warnungen)
self.assertTrue(any('KZ 354' in w for w in warnungen))
@tagged('post_install', '-at_install')
class Mitteilung109Test(TransactionCase):
def setUp(self):
super().setUp()
self.company = self.env.company
self.company.write({'l10n_at_lohnsteuernummer': '123456789'})
def _deklaration(self, art, entgelt):
deklaration = self.env['l10n.at.payroll.mitteilung109'].create({
'company_id': self.company.id, 'jahr': 2026, 'art': art})
werte = {
'name_partner': 'Testpartner',
'name': 'Testwerk',
'steuernummer': '8765432',
'entgelt': entgelt,
}
if art == 'w1':
werte.update({'art_leistung': '04', 'adresse': 'Werkstraße 1',
'plz': '1010', 'ort': 'Wien'})
else:
werte.update({'al31': True, 'ziellaender': 'DE,CH',
'adresse': 'Werkstraße 1', 'plz': '1010',
'ort': 'Wien'})
deklaration.line_ids = [(0, 0, werte)]
return deklaration
def test_04_w1_satz_und_positionen(self):
deklaration = self._deklaration('w1', 1200.0)
deklaration.action_erzeugen()
line = deklaration.line_ids[0]
self.assertTrue(line.gemeldet)
daten = base64.b64decode(deklaration.datei).decode('ISO-8859-15')
w1_satz = daten.splitlines()[3]
self.assertEqual(len(w1_satz), 1100)
self.assertEqual(w1_satz[:2], 'W1')
self.assertEqual(w1_satz[20], 'W') # FSART pos 21
self.assertEqual(w1_satz[281], '+') # V341 pos 282
self.assertEqual(w1_satz[282:292], '0000120000') # B341 pos 283
self.assertEqual(w1_satz[303:305], '12') # FANRA pos 304
self.assertEqual(w1_satz[305:312], '3456789') # STNRA pos 306
def test_05_w1_schwelle_untermeldung(self):
deklaration = self._deklaration('w1', 400.0)
deklaration.action_erzeugen()
line = deklaration.line_ids[0]
self.assertFalse(line.gemeldet)
self.assertIn('Schwellen', deklaration.protokoll)
self.assertFalse(deklaration.datei)
def test_05b_w1_900_eur_kumulativ_grenze(self):
"""450 € je Leistung, aber 1.000 € kumulativ je LE →
meldepflichtig trotz je-Leistung-Unterschreitung."""
deklaration = self._deklaration('w1', 450.0)
deklaration.line_ids = [
(0, 0, {'name_partner': 'Testpartner', 'name': 'Testwerk',
'steuernummer': '8765432', 'art_leistung': '04',
'entgelt': 550.0}),
]
deklaration.action_erzeugen()
self.assertTrue(deklaration.line_ids[0].gemeldet)
def test_06_b1_satz_schwellen_ausnahme(self):
deklaration = self._deklaration('b1', 120000.0)
deklaration.action_erzeugen()
line = deklaration.line_ids[0]
self.assertTrue(line.gemeldet)
daten = base64.b64decode(deklaration.datei).decode('ISO-8859-15')
b1_satz = daten.splitlines()[3]
self.assertEqual(len(b1_satz), 1100)
self.assertEqual(b1_satz[:2], 'B1')
self.assertEqual(b1_satz[20], 'B') # FSART pos 21
self.assertEqual(b1_satz[281], '+') # V499 pos 282
self.assertEqual(b1_satz[282:292], '0012000000') # B499 pos 283
self.assertEqual(b1_satz[360], 'J') # AL31 pos 361
self.assertEqual(b1_satz[361], 'N') # AL32 pos 362
self.assertEqual(b1_satz[363:366], 'DE ') # LKZ1 pos 364 (3 a)
self.assertEqual(b1_satz[366:369], 'CH ') # LKZ2 pos 367 (3 a)
# Unter der 100.000-€-Schwelle: keine Meldung (§ 109b Abs 4).
klein = self._deklaration('b1', 50000.0)
klein.action_erzeugen()
self.assertFalse(klein.line_ids[0].gemeldet)
# Ausnahme Steuerabzug § 99: keine Meldung trotz Betrags.
abzug = self._deklaration('b1', 150000.0)
abzug.line_ids[0].ausnahme_grund = 'steuerabzug'
abzug.action_erzeugen()
self.assertFalse(abzug.line_ids[0].gemeldet)
def test_07_fristenkalender_ap17(self):
from odoo.addons.l10n_at_hr_payroll.models.fristenkalender \
import FRISTART_DEFINITIONEN
for art in ('l17', 'mitteilung_109b'):
self.assertIn(art, FRISTART_DEFINITIONEN)
self.assertEqual(
FRISTART_DEFINITIONEN[art]['monat'], 2)
+107
View File
@@ -0,0 +1,107 @@
# Part of the odoo-at-payroll project. License: LGPL-3.
"""GP8 Kern: Fristenkalender (Auto-Erzeugung, Frist-Typen, Überfällig-
Logik), Jahres-Retro (Kontrollsechstel-Prüfung), Mitteilungsfile-Parser."""
from datetime import date
from odoo.tests import TransactionCase, tagged
from odoo.addons.l10n_at_hr_payroll.models.fristenkalender import (
FRISTART_DEFINITIONEN,
)
@tagged('post_install', '-at_install')
class FristenTest(TransactionCase):
def test_01_frist_definitionen(self):
"""Alle Fristarten sind definiert mit Name/Deadline-Logik."""
for art, definition in FRISTART_DEFINITIONEN.items():
self.assertIn('name', definition)
self.assertIn('hilfetext', definition)
self.assertTrue(
'monatlich' in definition or 'monat' in definition,
'Fristart %s ohne Fälligkeits-Logik' % art)
def test_02_cron_erzeugt_fristen(self):
"""Cron erzeugt monatliche SV/DB-Fristen idempotent + jährliche
Fristen am 1. Jänner."""
frist = self.env['l10n.at.payroll.frist']
company = self.env.company
# Manueller Cron-Lauf (heute ist ein Tag im Jahr).
frist._cron_fristen_erzeugen()
# Monatliche Fristen existieren für den Vormonat.
heute = date.today()
vormonat = (heute.replace(day=1) - __import__('datetime')
.timedelta(days=1))
monatliche = frist.search([
('company_id', '=', company.id),
('fristart', 'in', ('sv_mbgm', 'db_dz_kommst')),
('jahr', '=', vormonat.year),
('monat', '=', vormonat.month)])
self.assertEqual(len(monatliche), 2)
# Idempotenz: zweiter Lauf erzeugt keine Duplikate.
frist._cron_fristen_erzeugen()
monatliche2 = frist.search([
('company_id', '=', company.id),
('fristart', 'in', ('sv_mbgm', 'db_dz_kommst')),
('jahr', '=', vormonat.year),
('monat', '=', vormonat.month)])
self.assertEqual(len(monatliche2), 2)
def test_03_state_ueberfaellig(self):
"""Überfällige Fristen markieren sich nach Deadline-
Überschreitung; action_erledigt setzt erledigt."""
frist = self.env['l10n.at.payroll.frist'].create({
'company_id': self.env.company.id,
'fristart': 'l16',
'jahr': 2025,
'monat': 0,
'deadline': '2026-01-01',
})
self.assertEqual(frist.state, 'ueberfaellig')
frist.action_erledigt()
self.assertEqual(frist.state, 'erledigt')
self.assertTrue(frist.erledigt_am)
@tagged('post_install', '-at_install')
class MitteilungsfileTest(TransactionCase):
def test_01_parse_mitteilungsfile(self):
"""XML-Parser extrahiert Fehler-/Warnung-Elemente."""
wizard = self.env['l10n.at.payroll.mitteilungsfile.wizard']
xml = """<?xml version="1.0"?>
<mitteilung>
<sendung status="ok"/>
<fehler code="F0100" satzart="L1"
beschreibung="STEUERNUMMER fehlt"/>
<warnung code="W0102" satzart="G1"
beschreibung="Beitragssatz-Differenz"/>
</mitteilung>"""
zeilen = wizard._parse_mitteilungsfile(xml)
self.assertIn('Sendungsstatus: ok', zeilen)
self.assertTrue(any('F0100' in z for z in zeilen))
self.assertTrue(any('W0102' in z for z in zeilen))
self.assertTrue(any('STEUERNUMMER' in z for z in zeilen))
def test_02_import_ohne_datei(self):
"""Import ohne Datei löst UserError."""
from odoo.exceptions import UserError
wizard = self.env['l10n.at.payroll.mitteilungsfile.wizard'] \
.create({'company_id': self.env.company.id})
with self.assertRaises(UserError):
wizard.action_importieren()
@tagged('post_install', '-at_install')
class JahresretroTest(TransactionCase):
def test_01_wizard_ohne_daten(self):
"""Wizard ohne Läufe im Jahr: leeres Ergebnis, keine Fehler."""
wizard = self.env['l10n.at.payroll.jahresretro.wizard'].create({
'company_id': self.env.company.id,
'jahr': 1999, # weit in der Vergangenheit
})
wizard.action_pruefen()
self.assertFalse(wizard.ergebnis_ids)
self.assertIn('1999', wizard.zusammenfassung)
@@ -27,7 +27,8 @@ class LohnsteuerTest(TransactionCase):
}) })
def _regel(self, code, betrag, bemessung='laufend', flag68='keine', def _regel(self, code, betrag, bemessung='laufend', flag68='keine',
anteil=0.0, quantity='1.0', python=None): anteil=0.0, quantity='1.0', python=None, st_frei='keine',
sv_art='folgt'):
vals = { vals = {
'name': code, 'name': code,
'code': code, 'code': code,
@@ -37,6 +38,8 @@ class LohnsteuerTest(TransactionCase):
'l10n_at_bemessung': bemessung, 'l10n_at_bemessung': bemessung,
'l10n_at_steuerbefreiung_68': flag68, 'l10n_at_steuerbefreiung_68': flag68,
'l10n_at_zuschlagsanteil': anteil, 'l10n_at_zuschlagsanteil': anteil,
'l10n_at_st_frei': st_frei,
'l10n_at_sv_art': sv_art,
} }
if python: if python:
vals.update({'amount_select': 'code', vals.update({'amount_select': 'code',
@@ -105,6 +108,7 @@ class LohnsteuerTest(TransactionCase):
slip._l10n_at_absetzbetraege(20000.0), slip._l10n_at_absetzbetraege(20000.0),
496.0 + 828.0 + 273.0, places=2) 496.0 + 828.0 + 273.0, places=2)
# ------------------------------------------------------------------ # ------------------------------------------------------------------
# Lohnsteuertage (user decision: 30 je vollem Monat) # Lohnsteuertage (user decision: 30 je vollem Monat)
# ------------------------------------------------------------------ # ------------------------------------------------------------------
@@ -121,6 +125,27 @@ class LohnsteuerTest(TransactionCase):
'contract_date_end': date(2026, 1, 10)}) 'contract_date_end': date(2026, 1, 10)})
self.assertEqual(slip._l10n_at_lohnsteuertage(), 10) self.assertEqual(slip._l10n_at_lohnsteuertage(), 10)
# ------------------------------------------------------------------
# § 67 Abs 6 Z 6 — Überhang (tarifisch, ohne Sechstel-Effekt)
# ------------------------------------------------------------------
def test_20_abs6_ueberhang_ohne_sechstel(self):
"""Der gekennzeichnete Überhang läuft tarifisch in der laufenden
Bemessung, erhöht aber das Jahressechstel nicht."""
employee = self._employee()
slip_ohne = self._slip(employee, 1)
regel = self._regel('T_ABS6_UEB', 1000.0, sv_art='keine')
regel.l10n_at_s67_abs6_ueberhang = True
slip_mit = self._slip(employee, 2)
self.assertAlmostEqual(
slip_mit._l10n_at_abs6_ueberhang_total(), 1000.0, places=2)
self.assertAlmostEqual(
slip_mit._l10n_at_bemessung_laufen()
- slip_ohne._l10n_at_bemessung_laufen(), 1000.0, places=2)
self.assertAlmostEqual(
slip_mit._l10n_at_sechstel() - slip_ohne._l10n_at_sechstel(),
0.0, places=2)
# ------------------------------------------------------------------ # ------------------------------------------------------------------
# § 68 Freibeträge (Buckets über Regel-Flags) # § 68 Freibeträge (Buckets über Regel-Flags)
# ------------------------------------------------------------------ # ------------------------------------------------------------------
@@ -282,4 +307,114 @@ class LohnsteuerTest(TransactionCase):
self.assertAlmostEqual( self.assertAlmostEqual(
slip._l10n_at_sz_tarifisch_aktuell(), sz_bem - 4000.0, places=2) slip._l10n_at_sz_tarifisch_aktuell(), sz_bem - 4000.0, places=2)
self.assertAlmostEqual( self.assertAlmostEqual(
slip._l10n_at_lst_sonstige(), (4000.0 - 620.0) * 0.06, places=1) slip._l10n_at_lst_sonstige(), (4000.0 - 620.0) * 0.06, places=1)
# ------------------------------------------------------------------
# § 3/§ 26 — steuerfreie Sachbezüge (Freibeträge, Jahres-Kumulative)
# ------------------------------------------------------------------
def test_13_st_frei_mahlzeit(self):
"""Mahlzeiten-Gutschein 11 €/AT × 10 AT = 110 €: 80 € steuerfrei
(8 /AT, § 3 Abs 1 Z 17), 30 steuerpflichtig; LSt-Bemessung
um 80 reduziert (lb-sac-17)."""
employee = self._employee()
self._regel('LFD', 2000.0)
self._regel('MAH', 11.0, quantity='10.0', st_frei='mahlzeit')
slip = self._slip(employee, 1)
self.assertAlmostEqual(
slip._l10n_at_st_frei_freie_laufen(), 80.0, places=2)
svdn = slip._l10n_at_svdn_gesamt()
self.assertAlmostEqual(
slip._l10n_at_bemessung_laufen(),
2000.0 + 110.0 - svdn - 80.0, places=2)
def test_14_st_frei_jahrespools(self):
"""Jahres-Kumulative: Betriebsveranstaltung 365 €/J (Freibetrag)
und Sachzuwendung/Jubiläum 186 /J als separate Pools
(§ 3 Abs 1 Z 14; lb-sac-07, lb-ges-02)."""
employee = self._employee()
self._regel('LFD', 2000.0)
self._regel('BV', 200.0, st_frei='betriebsveranstaltung')
slip_a = self._slip(employee, 1, confirm=True)
self.assertAlmostEqual(
slip_a._l10n_at_st_frei_freie_laufen(), 200.0, places=2)
# Februar: 165 € Rest (365 200) → 165 frei, 35 steuerpflichtig
slip_b = self._slip(employee, 2, confirm=True)
self.assertAlmostEqual(
slip_b._l10n_at_st_frei_freie_laufen(), 165.0, places=2)
# März: Pool verbraucht → 0 frei, 200 steuerpflichtig
slip_c = self._slip(employee, 3)
self.assertAlmostEqual(
slip_c._l10n_at_st_frei_freie_laufen(), 0.0, places=2)
# Sachzuwendung separater Pool: 300 € → 186 frei, 114 pflichtig
self._regel('GSC', 300.0, st_frei='jubilaeum')
slip_d = self._slip(employee, 4)
self.assertAlmostEqual(
slip_d._l10n_at_st_frei_freie_laufen(), 186.0, places=2)
def test_15_st_frei_homeoffice(self):
"""Homeoffice 3 €/Tag, max 100 Tage/Jahr (§ 26 Z 9): 70 Tage im
Jänner (210 frei), Februar nur 30 Tage frei (90), 40 Tage × 3 =
120 steuerpflichtig."""
employee = self._employee()
self._regel('LFD', 2000.0)
self._regel('HO', 3.0, quantity='70.0', st_frei='homeoffice')
slip_a = self._slip(employee, 1, confirm=True)
self.assertAlmostEqual(
slip_a._l10n_at_st_frei_freie_laufen(), 210.0, places=2)
# Februar: 30 Tage Rest (100 70) → 90 frei, 120 steuerpflichtig
slip_b = self._slip(employee, 2)
self.assertAlmostEqual(
slip_b._l10n_at_st_frei_freie_laufen(), 90.0, places=2)
def test_16_st_frei_neutral(self):
"""Ohne st_frei-Flag keine Wirkung auf die LSt-Bemessung
(Regression)."""
employee = self._employee()
self._regel('LFD', 2000.0)
slip = self._slip(employee, 1)
self.assertAlmostEqual(slip._l10n_at_st_frei_freie_laufen(), 0.0)
svdn = slip._l10n_at_svdn_gesamt()
self.assertAlmostEqual(
slip._l10n_at_bemessung_laufen(), 2000.0 - svdn, places=2)
def test_17_sv_art_entkoppelt_lst_art(self):
"""SV-Art folgt standardmäßig der LSt-Art; Drittlohn kann nur
SV-laufend sein, Hardware-Verkauf LSt-sonstig/SV-laufend."""
employee = self._employee()
self._regel('LFD', 100.0)
self._regel('DRITT', 300.0, bemessung='neutral', sv_art='laufend')
self._regel('HW', 200.0, bemessung='sonstig', sv_art='laufend')
slip = self._slip(employee, 1)
self.assertAlmostEqual(
slip._l10n_at_sv_brutto('laufend'), 600.0, places=2)
self.assertAlmostEqual(
slip._l10n_at_sv_brutto('sonstig'), 0.0, places=2)
self.assertAlmostEqual(slip._l10n_at_svdn_basis_sz(), 0.0, places=2)
self.assertAlmostEqual(
slip._l10n_at_brutto('laufend'), 100.0, places=2)
self.assertAlmostEqual(
slip._l10n_at_brutto('sonstig'), 200.0, places=2)
def test_18_sv_art_folgt_ist_neutral(self):
"""Der Default 'folgt' erhält bestehende laufende und sonstige
SV-Zuordnungen unverändert."""
employee = self._employee()
self._regel('LFD', 100.0)
self._regel('SZ', 200.0, bemessung='sonstig')
slip = self._slip(employee, 1)
self.assertAlmostEqual(
slip._l10n_at_sv_brutto('laufend'), 100.0, places=2)
self.assertAlmostEqual(
slip._l10n_at_sv_brutto('sonstig'), 200.0, places=2)
def test_19_sv_art_keine_nur_lst(self):
"""SV-Art 'keine' schließt eine Regel ausschließlich aus der
SV aus; die deklarierte LSt-Bemessung bleibt unverändert."""
employee = self._employee()
self._regel('LFD', 100.0)
self._regel('NUR_LST', 300.0, sv_art='keine')
slip = self._slip(employee, 1)
self.assertAlmostEqual(
slip._l10n_at_sv_brutto('laufend'), 100.0, places=2)
self.assertAlmostEqual(
slip._l10n_at_brutto('laufend'), 400.0, places=2)
@@ -0,0 +1,422 @@
# Part of the odoo-at-payroll project. License: LGPL-3.
"""AP0 (Variante b): TASY-Katalog-Import und Tarifgruppen-Auflösung.
Fixture-Slice mit den normativen Normalbetriebe-Werten 2026 (AB 39,05 %,
SZ 37,55 % verifiziert gegen den echten TASY-LSWH-Export 2026-07-01)
plus E01-Ergänzung (NB-Zweig 3,80 % DG, Dienstgebergruppe NBNSA).
"""
import base64
from datetime import date
from odoo.tests import TransactionCase, tagged
TASY_KATALOG_FIXTURE = """<?xml version="1.0" encoding="UTF-8"?>
<tarifssystemlsh>
<gueltigkeitstichtag>2026-07-01</gueltigkeitstichtag>
<beschaeftigtengruppen>
<beschaeftigtengruppe xmlId="G001">
<code>B001</code>
<bezeichnung>Arbeiter</bezeichnung>
<kurzbezeichnung>Arb.</kurzbezeichnung>
<traegergruppe>OEGK</traegergruppe>
<vonDatum>2019-01-01</vonDatum>
<bisDatum>2999-12-31</bisDatum>
</beschaeftigtengruppe>
<beschaeftigtengruppe xmlId="G002">
<code>B002</code>
<bezeichnung>Angestellte</bezeichnung>
<kurzbezeichnung>Ang.</kurzbezeichnung>
<traegergruppe>OEGK</traegergruppe>
<vonDatum>2019-01-01</vonDatum>
<bisDatum>2999-12-31</bisDatum>
</beschaeftigtengruppe>
<beschaeftigtengruppe xmlId="G044">
<code>B044</code>
<bezeichnung>Angestelltenlehrlinge</bezeichnung>
<kurzbezeichnung>Ang. Lg.</kurzbezeichnung>
<traegergruppe>OEGK</traegergruppe>
<vonDatum>2019-01-01</vonDatum>
<bisDatum>2999-12-31</bisDatum>
</beschaeftigtengruppe>
<beschaeftigtengruppe xmlId="G045">
<code>B045</code>
<bezeichnung>Arbeiterlehrlinge</bezeichnung>
<kurzbezeichnung>Arb. Lg.</kurzbezeichnung>
<traegergruppe>OEGK</traegergruppe>
<vonDatum>2019-01-01</vonDatum>
<bisDatum>2999-12-31</bisDatum>
</beschaeftigtengruppe>
<beschaeftigtengruppe xmlId="G010">
<code>B010</code>
<bezeichnung>Geringfügig beschäftigte Arbeiter</bezeichnung>
<kurzbezeichnung>Gfg. Arb.</kurzbezeichnung>
<traegergruppe>OEGK</traegergruppe>
<vonDatum>2019-01-01</vonDatum>
<bisDatum>2999-12-31</bisDatum>
</beschaeftigtengruppe>
</beschaeftigtengruppen>
<ergaenzungen>
<ergaenzung xmlId="E01X">
<code>E01</code>
<bezeichnung>Nachtschwerarbeitsbeitrag</bezeichnung>
<kurzbezeichnung>NSchAB</kurzbezeichnung>
<traegergruppe>OEGK</traegergruppe>
<vonDatum>2019-01-01</vonDatum>
<bisDatum>2999-12-31</bisDatum>
</ergaenzung>
</ergaenzungen>
<dienstgebergruppen>
<dienstgebergruppe xmlId="NBAL">
<code>NBALG</code>
<bezeichnung>Normalbetriebe - Allgemeiner Teil</bezeichnung>
<kurzbezeichnung>NBALG</kurzbezeichnung>
<traegergruppe>OEGK</traegergruppe>
<vonDatum>2019-01-01</vonDatum>
<bisDatum>2999-12-31</bisDatum>
</dienstgebergruppe>
<dienstgebergruppe xmlId="NBNS">
<code>NBNSA</code>
<bezeichnung>Normalbetriebe mit Nachtschwerarbeit</bezeichnung>
<kurzbezeichnung>NBNSA</kurzbezeichnung>
<traegergruppe>OEGK</traegergruppe>
<vonDatum>2019-01-01</vonDatum>
<bisDatum>2999-12-31</bisDatum>
</dienstgebergruppe>
</dienstgebergruppen>
<tarifgruppen>
<tarifgruppe xmlId="T001">
<beschaeftigtengruppen>G001</beschaeftigtengruppen>
<dienstgeberGruppen>NBAL</dienstgeberGruppen>
<code>B001</code>
<bezeichnung>Arbeiter</bezeichnung>
<traegergruppe>OEGK</traegergruppe>
<vonDatum>2019-01-01</vonDatum>
<bisDatum>2999-12-31</bisDatum>
<fallweiseBeschaeftigungZulaessig>true</fallweiseBeschaeftigungZulaessig>
<betrieblicheVorsorgeZulaessig>true</betrieblicheVorsorgeZulaessig>
</tarifgruppe>
<tarifgruppe xmlId="T002">
<beschaeftigtengruppen>G002</beschaeftigtengruppen>
<dienstgeberGruppen>NBAL</dienstgeberGruppen>
<code>B002</code>
<bezeichnung>Angestellte</bezeichnung>
<traegergruppe>OEGK</traegergruppe>
<vonDatum>2019-01-01</vonDatum>
<bisDatum>2999-12-31</bisDatum>
<fallweiseBeschaeftigungZulaessig>false</fallweiseBeschaeftigungZulaessig>
<betrieblicheVorsorgeZulaessig>true</betrieblicheVorsorgeZulaessig>
</tarifgruppe>
<tarifgruppe xmlId="T002E01">
<beschaeftigtengruppen>G002</beschaeftigtengruppen>
<ergaenzungen>E01X</ergaenzungen>
<dienstgeberGruppen>NBNS</dienstgeberGruppen>
<code>B002E01</code>
<bezeichnung>Angestellte/Nachtschwerarbeitsbeitrag</bezeichnung>
<traegergruppe>OEGK</traegergruppe>
<vonDatum>2019-01-01</vonDatum>
<bisDatum>2999-12-31</bisDatum>
<betrieblicheVorsorgeZulaessig>true</betrieblicheVorsorgeZulaessig>
</tarifgruppe>
<tarifgruppe xmlId="T045">
<beschaeftigtengruppen>G045</beschaeftigtengruppen>
<dienstgeberGruppen>NBAL</dienstgeberGruppen>
<code>B045</code>
<bezeichnung>Arbeiterlehrlinge</bezeichnung>
<traegergruppe>OEGK</traegergruppe>
<vonDatum>2019-01-01</vonDatum>
<bisDatum>2999-12-31</bisDatum>
</tarifgruppe>
</tarifgruppen>
<tarife>
<tarif xmlId="R001">
<tarifZweig>KV</tarifZweig>
<dienstnehmerAnteil>3.87</dienstnehmerAnteil>
<dienstgeberAnteil>3.78</dienstgeberAnteil>
<traegergruppe>OEGK</traegergruppe>
<vonDatum>2019-01-01</vonDatum>
<bisDatum>2999-12-31</bisDatum>
</tarif>
<tarif xmlId="R002">
<tarifZweig>PV</tarifZweig>
<dienstnehmerAnteil>10.25</dienstnehmerAnteil>
<dienstgeberAnteil>12.55</dienstgeberAnteil>
<traegergruppe>OEGK</traegergruppe>
<vonDatum>2019-01-01</vonDatum>
<bisDatum>2999-12-31</bisDatum>
</tarif>
<tarif xmlId="R003">
<tarifZweig>AV</tarifZweig>
<dienstnehmerAnteil>2.95</dienstnehmerAnteil>
<dienstgeberAnteil>2.95</dienstgeberAnteil>
<traegergruppe>OEGK</traegergruppe>
<vonDatum>2019-01-01</vonDatum>
<bisDatum>2999-12-31</bisDatum>
</tarif>
<tarif xmlId="R004">
<tarifZweig>UV</tarifZweig>
<dienstnehmerAnteil>0</dienstnehmerAnteil>
<dienstgeberAnteil>1.1</dienstgeberAnteil>
<traegergruppe>OEGK</traegergruppe>
<vonDatum>2019-01-01</vonDatum>
<bisDatum>2999-12-31</bisDatum>
</tarif>
<tarif xmlId="R005">
<tarifZweig>WF</tarifZweig>
<dienstnehmerAnteil>0.5</dienstnehmerAnteil>
<dienstgeberAnteil>0.5</dienstgeberAnteil>
<traegergruppe>OEGK</traegergruppe>
<vonDatum>2019-01-01</vonDatum>
<bisDatum>2999-12-31</bisDatum>
</tarif>
<tarif xmlId="R006">
<tarifZweig>AK</tarifZweig>
<dienstnehmerAnteil>0.5</dienstnehmerAnteil>
<dienstgeberAnteil>0</dienstgeberAnteil>
<traegergruppe>OEGK</traegergruppe>
<vonDatum>2019-01-01</vonDatum>
<bisDatum>2999-12-31</bisDatum>
</tarif>
<tarif xmlId="R007">
<tarifZweig>IE</tarifZweig>
<dienstnehmerAnteil>0</dienstnehmerAnteil>
<dienstgeberAnteil>0.1</dienstgeberAnteil>
<traegergruppe>OEGK</traegergruppe>
<vonDatum>2019-01-01</vonDatum>
<bisDatum>2999-12-31</bisDatum>
</tarif>
<tarif xmlId="R008">
<tarifZweig>NB</tarifZweig>
<dienstnehmerAnteil>0</dienstnehmerAnteil>
<dienstgeberAnteil>3.8</dienstgeberAnteil>
<traegergruppe>OEGK</traegergruppe>
<vonDatum>2020-01-01</vonDatum>
<bisDatum>2999-12-31</bisDatum>
</tarif>
</tarife>
<tarifsets>
<tarifset xmlId="S001">
<bezeichnung>40/AK/AV/IE/KV/PV/UV/WF</bezeichnung>
<kurzbezeichnung>40/AK/AV/IE/KV/PV/UV/WF</kurzbezeichnung>
<traegergruppe>OEGK</traegergruppe>
<vonDatum>2019-01-01</vonDatum>
<bisDatum>2999-12-31</bisDatum>
<tarife>R001</tarife>
<tarife>R002</tarife>
<tarife>R003</tarife>
<tarife>R004</tarife>
<tarife>R005</tarife>
<tarife>R006</tarife>
<tarife>R007</tarife>
</tarifset>
<tarifset xmlId="S002">
<bezeichnung>47/AV/IE/KV/PV/UV</bezeichnung>
<kurzbezeichnung>47/AV/IE/KV/PV/UV</kurzbezeichnung>
<traegergruppe>OEGK</traegergruppe>
<vonDatum>2019-01-01</vonDatum>
<bisDatum>2999-12-31</bisDatum>
<tarife>R001</tarife>
<tarife>R002</tarife>
<tarife>R003</tarife>
<tarife>R004</tarife>
<tarife>R007</tarife>
</tarifset>
<tarifset xmlId="S003">
<bezeichnung>40/AK/AV/IE/KV/PV/UV/WF/NB</bezeichnung>
<kurzbezeichnung>40/AK/AV/IE/KV/PV/UV/WF/NB</kurzbezeichnung>
<traegergruppe>OEGK</traegergruppe>
<vonDatum>2019-01-01</vonDatum>
<bisDatum>2999-12-31</bisDatum>
<tarife>R001</tarife>
<tarife>R002</tarife>
<tarife>R003</tarife>
<tarife>R004</tarife>
<tarife>R005</tarife>
<tarife>R006</tarife>
<tarife>R007</tarife>
<tarife>R008</tarife>
</tarifset>
</tarifsets>
<verrechBasisTypen>
<verrechBasisTyp xmlId="VBTAB"><code>AB</code><bezeichnung>Arbeitsbezug</bezeichnung><traegergruppe>OEGK</traegergruppe><vonDatum>2019-01-01</vonDatum><bisDatum>2999-12-31</bisDatum></verrechBasisTyp>
<verrechBasisTyp xmlId="VBTSZ"><code>SZ</code><bezeichnung>Sonderzahlung</bezeichnung><traegergruppe>OEGK</traegergruppe><vonDatum>2019-01-01</vonDatum><bisDatum>2999-12-31</bisDatum></verrechBasisTyp>
</verrechBasisTypen>
<verrechPosTypen>
<verrechPosTyp xmlId="VPT01"><code>T01</code><bezeichnung>Gesamtsatz laufend</bezeichnung><traegergruppe>OEGK</traegergruppe><vonDatum>2019-01-01</vonDatum><bisDatum>2999-12-31</bisDatum></verrechPosTyp>
<verrechPosTyp xmlId="VPT02"><code>T02</code><bezeichnung>Gesamtsatz Sonderzahlung</bezeichnung><traegergruppe>OEGK</traegergruppe><vonDatum>2019-01-01</vonDatum><bisDatum>2999-12-31</bisDatum></verrechPosTyp>
</verrechPosTypen>
<verrechBasisPositionKombinationen>
<verrechBasisPositionKombination xmlId="K01"><verrechnungBasisTyp>VBTAB</verrechnungBasisTyp><verrechnungPosTyp>VPT01</verrechnungPosTyp><traegergruppe>OEGK</traegergruppe><vonDatum>2019-01-01</vonDatum><bisDatum>2999-12-31</bisDatum></verrechBasisPositionKombination>
<verrechBasisPositionKombination xmlId="K02"><verrechnungBasisTyp>VBTSZ</verrechnungBasisTyp><verrechnungPosTyp>VPT02</verrechnungPosTyp><traegergruppe>OEGK</traegergruppe><vonDatum>2019-01-01</vonDatum><bisDatum>2999-12-31</bisDatum></verrechBasisPositionKombination>
</verrechBasisPositionKombinationen>
<tarifgruppeSetZuordnungen>
<tarifgruppeSetZuord xmlId="Z001"><tarifgruppe>T001</tarifgruppe><tarifset>S001</tarifset><verrechBasPosKombination>K01</verrechBasPosKombination><traegergruppe>OEGK</traegergruppe><vonDatum>2019-01-01</vonDatum><bisDatum>2999-12-31</bisDatum></tarifgruppeSetZuord>
<tarifgruppeSetZuord xmlId="Z002"><tarifgruppe>T001</tarifgruppe><tarifset>S002</tarifset><verrechBasPosKombination>K02</verrechBasPosKombination><traegergruppe>OEGK</traegergruppe><vonDatum>2019-01-01</vonDatum><bisDatum>2999-12-31</bisDatum></tarifgruppeSetZuord>
<tarifgruppeSetZuord xmlId="Z003"><tarifgruppe>T002</tarifgruppe><tarifset>S001</tarifset><verrechBasPosKombination>K01</verrechBasPosKombination><traegergruppe>OEGK</traegergruppe><vonDatum>2019-01-01</vonDatum><bisDatum>2999-12-31</bisDatum></tarifgruppeSetZuord>
<tarifgruppeSetZuord xmlId="Z004"><tarifgruppe>T002</tarifgruppe><tarifset>S002</tarifset><verrechBasPosKombination>K02</verrechBasPosKombination><traegergruppe>OEGK</traegergruppe><vonDatum>2019-01-01</vonDatum><bisDatum>2999-12-31</bisDatum></tarifgruppeSetZuord>
<tarifgruppeSetZuord xmlId="Z005"><tarifgruppe>T002E01</tarifgruppe><tarifset>S003</tarifset><verrechBasPosKombination>K01</verrechBasPosKombination><traegergruppe>OEGK</traegergruppe><vonDatum>2019-01-01</vonDatum><bisDatum>2999-12-31</bisDatum></tarifgruppeSetZuord>
<tarifgruppeSetZuord xmlId="Z006"><tarifgruppe>T002E01</tarifgruppe><tarifset>S002</tarifset><verrechBasPosKombination>K02</verrechBasPosKombination><traegergruppe>OEGK</traegergruppe><vonDatum>2019-01-01</vonDatum><bisDatum>2999-12-31</bisDatum></tarifgruppeSetZuord>
<tarifgruppeSetZuord xmlId="Z007"><tarifgruppe>T045</tarifgruppe><tarifset>S001</tarifset><verrechBasPosKombination>K01</verrechBasPosKombination><traegergruppe>OEGK</traegergruppe><vonDatum>2019-01-01</vonDatum><bisDatum>2999-12-31</bisDatum></tarifgruppeSetZuord>
<tarifgruppeSetZuord xmlId="Z008"><tarifgruppe>T045</tarifgruppe><tarifset>S002</tarifset><verrechBasPosKombination>K02</verrechBasPosKombination><traegergruppe>OEGK</traegergruppe><vonDatum>2019-01-01</vonDatum><bisDatum>2999-12-31</bisDatum></tarifgruppeSetZuord>
</tarifgruppeSetZuordnungen>
<zuAbschlaege>
<zuAbschlag xmlId="A09X">
<code>A09</code>
<bezeichnung>Entfall UV 60. Lj</bezeichnung>
<kurzbezeichnung>UV 60</kurzbezeichnung>
<zuabschlagkz>A</zuabschlagkz>
<versicherungsRelevant>false</versicherungsRelevant>
<traegergruppe>OEGK</traegergruppe>
<vonDatum>2019-01-01</vonDatum>
<bisDatum>2999-12-31</bisDatum>
</zuAbschlag>
</zuAbschlaege>
<zuAbschlaegeSteuerung>
<zuAbschlagSteuerung xmlId="A09S1">
<zuAbschlag>A09X</zuAbschlag>
<altersgrenzeMinJahr>60</altersgrenzeMinJahr>
<traegergruppe>OEGK</traegergruppe>
<vonDatum>2019-01-01</vonDatum>
<bisDatum>2999-12-31</bisDatum>
</zuAbschlagSteuerung>
</zuAbschlaegeSteuerung>
<zuAbschlagTarifsetZuordnungen>
<zuAbschlagTarifsetZuord xmlId="AZ01">
<zuAbschlag>A09X</zuAbschlag>
<tarifsetByTarifsetId>S001</tarifsetByTarifsetId>
<tarifgruppe>T001</tarifgruppe>
<tarifgruppe>T002</tarifgruppe>
<traegergruppe>OEGK</traegergruppe>
<vonDatum>2019-01-01</vonDatum>
<bisDatum>2999-12-31</bisDatum>
</zuAbschlagTarifsetZuord>
</zuAbschlagTarifsetZuordnungen>
</tarifssystemlsh>
"""
@tagged('post_install', '-at_install')
class TasyKatalogTest(TransactionCase):
"""Katalog-Import (Fixture + Realexport) und Tarifgruppen-Auflösung."""
def _import(self, raw=None):
import os
if raw is None:
path = os.path.join(os.path.dirname(__file__), *[
'..', '..', '..', 'personalverrechnung', 'quellen',
'TASY-LSWH-Export-20260324.xml'])
if not os.path.exists(path):
self.skipTest('TASY-Quelldatei nicht im Workingtree')
with open(path, 'rb') as fh:
raw = fh.read()
wizard = self.env['l10n.at.payroll.tasy.katalog.wizard'].create({
'file': base64.b64encode(raw).decode(),
'filename': 'fixture.xml',
})
wizard.action_import()
return wizard
def test_01_import_fixture_und_aufloesung(self):
"""Import erzeugt den Katalog; B001/B002 lösen zu den
normativen Normalbetriebe-Sätzen auf (AB 39,05, SZ 37,55)."""
self._import(TASY_KATALOG_FIXTURE.encode())
self.assertEqual(
self.env['l10n.at.payroll.tasy.bsgr'].search_count([]), 5)
self.assertEqual(
self.env['l10n.at.payroll.tasy.ergaenzung'].search_count([]),
1)
self.assertEqual(
self.env['l10n.at.payroll.tasy.dienstgebergruppe']
.search_count([]), 2)
self.assertEqual(
self.env['l10n.at.payroll.tasy.tarifgruppe'].search_count([]),
4)
self.assertEqual(
self.env['l10n.at.payroll.tasy.tarifset'].search_count([]), 3)
self.assertEqual(
self.env['l10n.at.payroll.tasy.tarifset.zuord']
.search_count([]), 8)
stichtag = date(2026, 7, 1)
for code in ('B001', 'B002'):
tarifgruppe = self.env[
'l10n.at.payroll.tasy.tarifgruppe']._l10n_at_find(
code, [], 'NBALG', stichtag)
self.assertTrue(tarifgruppe, code)
ab = tarifgruppe._l10n_at_saetze('AB', stichtag)
sz = tarifgruppe._l10n_at_saetze('SZ', stichtag)
self.assertAlmostEqual(ab['gesamt'], 39.05, places=2)
self.assertAlmostEqual(ab['dn'], 18.07, places=2)
self.assertAlmostEqual(ab['dg'], 20.98, places=2)
self.assertAlmostEqual(sz['gesamt'], 37.55, places=2)
def test_02_import_idempotent(self):
"""Erneuter Import derselben Datei dupliziert nichts."""
self._import(TASY_KATALOG_FIXTURE.encode())
self._import(TASY_KATALOG_FIXTURE.encode())
self.assertEqual(
self.env['l10n.at.payroll.tasy.tarifgruppe'].search_count([]),
4)
self.assertEqual(
self.env['l10n.at.payroll.tasy.tarif'].search_count([]), 8)
self.assertEqual(
self.env['l10n.at.payroll.tasy.tarifset.zuord']
.search_count([]), 8)
def test_03_ergaenzung_e01(self):
"""B002 + E01 + NBNSA → T01 inkl. NB-Zweig 3,80 % DG
(42,85 %); ohne Ergänzung unter NBNSA keine Tarifgruppe."""
self._import(TASY_KATALOG_FIXTURE.encode())
stichtag = date(2026, 7, 1)
find = self.env['l10n.at.payroll.tasy.tarifgruppe']._l10n_at_find
# B002 + E01 unter NBALG existiert nicht (E01 gehört zu NBNSA)
self.assertFalse(find('B002', ['E01'], 'NBALG', stichtag))
tarifgruppe = find('B002', ['E01'], 'NBNSA', stichtag)
self.assertTrue(tarifgruppe)
ab = tarifgruppe._l10n_at_saetze('AB', stichtag)
self.assertAlmostEqual(ab['gesamt'], 42.85, places=2)
zweige = {t.zweig for t in ab['komponenten']}
self.assertIn('NB', zweige)
# SZ ohne NB (Sonderzahlungen tragen keinen NSchAB)
sz = tarifgruppe._l10n_at_saetze('SZ', stichtag)
self.assertAlmostEqual(sz['gesamt'], 37.55, places=2)
def test_04_lehrlings_bsg(self):
"""B045 (Arbeiterlehrlinge) ist auflösbar — die
Lehrlings-BSGR-Korrektur beruht auf diesen Bezeichnern."""
self._import(TASY_KATALOG_FIXTURE.encode())
stichtag = date(2026, 7, 1)
b044 = self.env['l10n.at.payroll.tasy.bsgr'].search([
('code', '=', 'B044')])
b045 = self.env['l10n.at.payroll.tasy.bsgr'].search([
('code', '=', 'B045')])
self.assertEqual(b044.bezeichnung, 'Angestelltenlehrlinge')
self.assertEqual(b045.bezeichnung, 'Arbeiterlehrlinge')
tarifgruppe = self.env[
'l10n.at.payroll.tasy.tarifgruppe']._l10n_at_find(
'B045', [], 'NBALG', stichtag)
self.assertTrue(tarifgruppe)
def test_05_real_export(self):
"""Echter TASY-LSWH-Export: Katalog-Volumen und die
normativen 2026er Sätze aus der Katalogauflösung."""
self._import()
stichtag = date(2026, 7, 1)
find = self.env['l10n.at.payroll.tasy.tarifgruppe']._l10n_at_find
for code in ('B001', 'B002'):
tarifgruppe = find(code, [], 'NBALG', stichtag)
self.assertTrue(tarifgruppe, code)
ab = tarifgruppe._l10n_at_saetze('AB', stichtag)
sz = tarifgruppe._l10n_at_saetze('SZ', stichtag)
self.assertAlmostEqual(ab['gesamt'], 39.05, places=2)
self.assertAlmostEqual(sz['gesamt'], 37.55, places=2)
# Lehrlings-BSGR-Bezeichner (Korrektur-Grundlage)
self.assertEqual(self.env['l10n.at.payroll.tasy.bsgr'].search([
('code', '=', 'B044')]).bezeichnung, 'Angestelltenlehrlinge')
self.assertEqual(self.env['l10n.at.payroll.tasy.bsgr'].search([
('code', '=', 'B045')]).bezeichnung, 'Arbeiterlehrlinge')
# Geringfügige je Personalgruppe
self.assertEqual(self.env['l10n.at.payroll.tasy.bsgr'].search([
('code', '=', 'B010')]).bezeichnung,
'Geringfügig beschäftigte Arbeiter')
# Ergänzungen sind im Katalog
self.assertTrue(self.env['l10n.at.payroll.tasy.ergaenzung'].search([
('code', '=', 'E01')]))
@@ -0,0 +1,39 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- ============================================================ -->
<!-- GP9/AP9: Company-Stammdaten-UI für die AT-Lohnverrechnung. -->
<!-- BKNR und Lohnsteuernummer sind Pflicht für alle Meldewesen- -->
<!-- Läufe (L16, mBGM, Versichertenmeldung); Bundesland und Flags -->
<!-- steuern die Beitragssätze (DZ-Satz je BL, WF-Wien, NSchAB). -->
<!-- ============================================================ -->
<record id="res_company_view_form_l10n_at" model="ir.ui.view">
<field name="name">res.company.form.l10n.at</field>
<field name="model">res.company</field>
<field name="inherit_id" ref="base.view_company_form"/>
<field name="arch" type="xml">
<xpath expr="//notebook" position="inside">
<page string="Lohnverrechnung (AT)"
name="l10n_at_payroll"
groups="hr_payroll.group_hr_payroll_user">
<group>
<group string="Meldewesen (ELDA/eSV)"
name="l10n_at_meldewesen">
<field name="l10n_at_beitragskontonummer"
placeholder="z. B. 1412345678"/>
<field name="l10n_at_lohnsteuernummer"
placeholder="9-stellig (2 FA + 7 StNr)"/>
</group>
<group string="Beitragssätze &amp; Abgaben"
name="l10n_at_abgaben">
<field name="l10n_at_bundesland"/>
<field name="l10n_at_wk_mitglied"/>
<field name="l10n_at_wf_wien"/>
<field name="l10n_at_wien_dag"/>
</group>
</group>
</page>
</xpath>
</field>
</record>
</odoo>
@@ -0,0 +1,191 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- ============================================================ -->
<!-- GP8: Fristenkalender + Jahres-Retro + Mitteilungsfile-Import. -->
<!-- ============================================================ -->
<record id="frist_list_view" model="ir.ui.view">
<field name="name">l10n.at.payroll.frist.list</field>
<field name="model">l10n.at.payroll.frist</field>
<field name="arch" type="xml">
<list string="Fristenkalender" default_order="deadline">
<field name="deadline"/>
<field name="fristart"/>
<field name="jahr"/>
<field name="monat" optional="hide"/>
<field name="company_id" groups="base.group_multi_company"/>
<field name="state" widget="badge"
decoration-success="state == 'erledigt'"
decoration-warning="state == 'offen'"
decoration-danger="state == 'ueberfaellig'"/>
<button name="action_erledigt" type="object"
icon="fa-check" string="Erledigt"
invisible="state == 'erledigt'"/>
<button name="action_zum_objekt" type="object"
icon="fa-arrow-right" string="Zur Meldung"/>
</list>
</field>
</record>
<record id="frist_form_view" model="ir.ui.view">
<field name="name">l10n.at.payroll.frist.form</field>
<field name="model">l10n.at.payroll.frist</field>
<field name="arch" type="xml">
<form string="Frist">
<sheet>
<group>
<group>
<field name="fristart"/>
<field name="jahr"/>
<field name="monat"/>
<field name="deadline"/>
</group>
<group>
<field name="company_id"
groups="base.group_multi_company"/>
<field name="state" widget="badge"/>
<field name="erledigt_am"/>
</group>
</group>
<notebook>
<page string="Hinweis" name="hinweis">
<field name="hilfetext"/>
</page>
<page string="Meldungsobjekt" name="objekt">
<group>
<field name="ref_l16_id"/>
</group>
</page>
</notebook>
</sheet>
<header>
<button name="action_erledigt" type="object"
string="Als erledigt markieren"
invisible="state == 'erledigt'"/>
<button name="action_wieder_eroeffnen" type="object"
string="Wieder eröffnen"
invisible="state != 'erledigt'"/>
</header>
</form>
</field>
</record>
<record id="action_frist" model="ir.actions.act_window">
<field name="name">Fristenkalender</field>
<field name="res_model">l10n.at.payroll.frist</field>
<field name="view_mode">list,form</field>
<field name="context">{'search_default_state': 'offen'}</field>
</record>
<!-- Jahres-Retro-Wizard -->
<record id="jahresretro_wizard_form_view" model="ir.ui.view">
<field name="name">l10n.at.payroll.jahresretro.wizard.form</field>
<field name="model">l10n.at.payroll.jahresretro.wizard</field>
<field name="arch" type="xml">
<form string="Jahres-Retro-/Aufrollungslauf">
<group>
<group>
<field name="jahr"/>
<field name="company_id"
groups="base.group_multi_company"/>
</group>
<group>
<field name="nur_kontrollsechstel"/>
<field name="aufrollung_77"/>
<field name="zeige_detail"/>
</group>
</group>
<footer>
<button name="action_pruefen" type="object"
string="Jahresdaten prüfen" class="btn-primary"/>
<button string="Schließen" class="btn-secondary"
special="cancel"/>
</footer>
<notebook invisible="not zeige_detail">
<page string="Prüfergebnis" name="ergebnis">
<field name="ergebnis_ids">
<list create="0" delete="0">
<field name="employee_id"/>
<field name="kontrollsechstel_ueberhang"/>
<field name="fixsatz_steuer"/>
<field name="tarif_steuer"/>
<field name="guenstig" widget="badge"
decoration-success="guenstig"
decoration-danger="not guenstig"/>
<field name="handlung"/>
</list>
</field>
</page>
<page string="Zusammenfassung" name="zusammenfassung">
<field name="zusammenfassung"/>
</page>
</notebook>
</form>
</field>
</record>
<record id="action_jahresretro_wizard" model="ir.actions.act_window">
<field name="name">Jahres-Retro-/Aufrollungslauf</field>
<field name="res_model">l10n.at.payroll.jahresretro.wizard</field>
<field name="view_mode">form</field>
<field name="target">new</field>
</record>
<!-- Mitteilungsfile-Import -->
<record id="mitteilungsfile_wizard_form_view" model="ir.ui.view">
<field name="name">l10n.at.payroll.mitteilungsfile.wizard.form</field>
<field name="model">l10n.at.payroll.mitteilungsfile.wizard</field>
<field name="arch" type="xml">
<form string="ELDA-Mitteilungsfile importieren">
<group>
<field name="company_id"
groups="base.group_multi_company"/>
<field name="datei" filename="datei_name"/>
<field name="datei_name" invisible="1"/>
</group>
<footer>
<button name="action_importieren" type="object"
string="Importieren" class="btn-primary"/>
<button string="Schließen" class="btn-secondary"
special="cancel"/>
</footer>
<field name="protokoll" invisible="not protokoll"/>
</form>
</field>
</record>
<record id="action_mitteilungsfile_wizard" model="ir.actions.act_window">
<field name="name">ELDA-Mitteilungsfile importieren</field>
<field name="res_model">l10n.at.payroll.mitteilungsfile.wizard</field>
<field name="view_mode">form</field>
<field name="target">new</field>
</record>
<!-- Cron: Fristen monatlich/jährlich erzeugen -->
<record id="ir_cron_fristen_erzeugen" model="ir.cron">
<field name="name">L10n AT Payroll: Fristenkalender pflegen</field>
<field name="model_id" ref="model_l10n_at_payroll_frist"/>
<field name="state">code</field>
<field name="code">model._cron_fristen_erzeugen()</field>
<field name="interval_number">1</field>
<field name="interval_type">months</field>
<field name="nextcall">2026-10-01 02:00:00</field>
<field name="active" eval="True"/>
</record>
<!-- Menüs -->
<menuitem id="menu_l10n_at_payroll_fristen"
name="Fristenkalender"
parent="menu_l10n_at_payroll_meldewesen"
action="action_frist" sequence="5"/>
<menuitem id="menu_l10n_at_payroll_jahresretro"
name="Jahres-Retro-/Aufrollungslauf"
parent="menu_l10n_at_payroll_meldewesen"
action="action_jahresretro_wizard" sequence="20"/>
<menuitem id="menu_l10n_at_payroll_mitteilungsfile"
name="Mitteilungsfile-Import"
parent="menu_l10n_at_payroll_meldewesen"
action="action_mitteilungsfile_wizard" sequence="30"/>
</odoo>
@@ -0,0 +1,227 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- ============================================================ -->
<!-- AP17: Lohnnachweis/Lohnbescheinigung L17 (A1) und Finanz- -->
<!-- Mitteilungen § 109a (W1)/§ 109b (B1). Menü unter dem -->
<!-- Meldewesen (ELDA/FinOnl). -->
<!-- ============================================================ -->
<record id="l17_list_view" model="ir.ui.view">
<field name="name">l10n.at.payroll.l17.list</field>
<field name="model">l10n.at.payroll.l17</field>
<field name="arch" type="xml">
<list string="Lohnnachweis/Lohnbescheinigung (L17)">
<field name="jahr"/>
<field name="company_id" groups="base.group_multi_company"/>
<field name="state" widget="badge"
decoration-success="state == 'done'"
decoration-info="state == 'draft'"/>
</list>
</field>
</record>
<record id="l17_form_view" model="ir.ui.view">
<field name="name">l10n.at.payroll.l17.form</field>
<field name="model">l10n.at.payroll.l17</field>
<field name="arch" type="xml">
<form string="L17 — Lohnnachweis/Lohnbescheinigung">
<header>
<button name="action_erzeugen" type="object"
string="Lauf erzeugen (A1 je LZP)"
class="btn-primary"
invisible="state == 'done'"/>
<field name="state" widget="statusbar"
statusbar_visible="draft,done"/>
</header>
<sheet>
<group>
<group>
<field name="jahr"/>
</group>
<group>
<field name="company_id"
groups="base.group_multi_company"/>
</group>
</group>
<notebook>
<page string="A1-Sätze (je LZP)" name="linien">
<field name="line_ids">
<list editable="bottom">
<field name="employee_name"/>
<field name="date_from"/>
<field name="date_to"/>
<field name="versicherungsnummer"/>
<field name="kz350"/>
<field name="kz351" optional="show"/>
<field name="kz352" optional="show"/>
<field name="kz354" optional="hide"/>
<field name="kz357" optional="show"/>
<field name="kz347" optional="hide"/>
<field name="hota" optional="hide"/>
<field name="hopa" optional="hide"/>
</list>
</field>
</page>
<page string="Datei" name="datei">
<group>
<field name="datei" filename="datei_name"/>
<field name="datei_name" invisible="1"/>
</group>
</page>
<page string="Prüfprotokoll" name="protokoll">
<field name="protokoll"/>
</page>
</notebook>
</sheet>
</form>
</field>
</record>
<record id="action_l17" model="ir.actions.act_window">
<field name="name">Lohnnachweis L17</field>
<field name="res_model">l10n.at.payroll.l17</field>
<field name="view_mode">list,form</field>
</record>
<menuitem id="menu_l10n_at_payroll_l17"
name="Lohnnachweis L17 (A1)"
parent="menu_l10n_at_payroll_meldewesen"
action="action_l17" sequence="12"/>
<record id="mitteilung109_list_view" model="ir.ui.view">
<field name="name">l10n.at.payroll.mitteilung109.list</field>
<field name="model">l10n.at.payroll.mitteilung109</field>
<field name="arch" type="xml">
<list string="Finanz-Mitteilungen § 109a/§ 109b">
<field name="jahr"/>
<field name="art"/>
<field name="company_id" groups="base.group_multi_company"/>
<field name="state" widget="badge"
decoration-success="state == 'done'"
decoration-info="state == 'draft'"/>
</list>
</field>
</record>
<record id="mitteilung109_form_view" model="ir.ui.view">
<field name="name">l10n.at.payroll.mitteilung109.form</field>
<field name="model">l10n.at.payroll.mitteilung109</field>
<field name="arch" type="xml">
<form string="Finanz-Mitteilungen § 109a/§ 109b">
<header>
<button name="action_erzeugen" type="object"
string="Lauf erzeugen (Schwellen, Datei)"
class="btn-primary"
invisible="state == 'done'"/>
<field name="state" widget="statusbar"
statusbar_visible="draft,done"/>
</header>
<sheet>
<group>
<group>
<field name="jahr"/>
<field name="art"/>
</group>
<group>
<field name="company_id"
groups="base.group_multi_company"/>
</group>
</group>
<notebook>
<page string="Mitteilungssätze" name="linien">
<field name="line_ids" context="{'default_art': art}">
<list editable="bottom"
open_form_view="1">
<field name="name_partner"/>
<field name="name"/>
<field name="versicherungsnummer"/>
<field name="steuernummer"
optional="show"/>
<field name="entgelt"/>
<field name="umsatzsteuer"
optional="show"/>
<field name="gemeldet"/>
</list>
</field>
</page>
<page string="Datei" name="datei">
<group>
<field name="datei" filename="datei_name"/>
<field name="datei_name" invisible="1"/>
</group>
</page>
<page string="Prüfprotokoll" name="protokoll">
<field name="protokoll"/>
</page>
</notebook>
</sheet>
</form>
</field>
</record>
<record id="mitteilung109_line_form_view" model="ir.ui.view">
<field name="name">l10n.at.payroll.mitteilung109.line.form</field>
<field name="model">l10n.at.payroll.mitteilung109.line</field>
<field name="arch" type="xml">
<form string="Finanz-Mitteilung (§ 109a/§ 109b)">
<sheet>
<group>
<group>
<field name="name_partner"/>
<field name="name"/>
<field name="versicherungsnummer"/>
<field name="steuernummer"/>
<field name="uid"/>
</group>
<group>
<field name="adresse"/>
<field name="landeskennung"/>
<field name="plz"/>
<field name="ort"/>
</group>
</group>
<group>
<group>
<field name="entgelt"/>
<field name="umsatzsteuer"/>
<field name="dn_sv_anteil"/>
<field name="bmv_beitrag"/>
</group>
<group>
<field name="art_leistung"/>
<field name="al31"/>
<field name="al32"/>
<field name="al33"/>
<field name="ziellaender"/>
<field name="ausnahme_grund"/>
<field name="korr"/>
<field name="gemeldet"/>
</group>
</group>
<group string="Maßgeblich auftretende natürliche Personen (max. 3)">
<field name="np_ids">
<list editable="bottom">
<field name="sequence" widget="handle"/>
<field name="name"/>
<field name="versicherungsnummer"/>
<field name="steuernummer"/>
<field name="uid" optional="hide"/>
</list>
</field>
</group>
</sheet>
</form>
</field>
</record>
<record id="action_mitteilung109" model="ir.actions.act_window">
<field name="name">Finanz-Mitteilungen § 109a/§ 109b</field>
<field name="res_model">l10n.at.payroll.mitteilung109</field>
<field name="view_mode">list,form</field>
</record>
<menuitem id="menu_l10n_at_payroll_m109"
name="Mitteilungen § 109a/§ 109b"
parent="menu_l10n_at_payroll_meldewesen"
action="action_mitteilung109" sequence="14"/>
</odoo>
@@ -0,0 +1,149 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- ============================================================ -->
<!-- GP7 Meldewesen: L16-/Arbeitsstätten-Deklaration + Kind-Stamm -->
<!-- (FB+-Wartung am Mitarbeiter). -->
<!-- ============================================================ -->
<record id="l16_list_view" model="ir.ui.view">
<field name="name">l10n.at.payroll.l16.list</field>
<field name="model">l10n.at.payroll.l16</field>
<field name="arch" type="xml">
<list string="Lohnzettel Finanz (L16)">
<field name="jahr"/>
<field name="art"/>
<field name="company_id" groups="base.group_multi_company"/>
<field name="state" widget="badge"
decoration-success="state == 'done'"
decoration-info="state == 'draft'"/>
</list>
</field>
</record>
<record id="l16_form_view" model="ir.ui.view">
<field name="name">l10n.at.payroll.l16.form</field>
<field name="model">l10n.at.payroll.l16</field>
<field name="arch" type="xml">
<form string="Lohnzettel Finanz (L16) &amp; Arbeitsstättenmeldung">
<header>
<button name="action_erzeugen" type="object"
string="Lauf erzeugen (Dateien)"
class="btn-primary"
invisible="state == 'done'"/>
<field name="state" widget="statusbar"
statusbar_visible="draft,done"/>
</header>
<sheet>
<group>
<group>
<field name="jahr"/>
<field name="art"/>
</group>
<group>
<field name="company_id"
groups="base.group_multi_company"/>
</group>
</group>
<notebook>
<page string="Mitteilungssätze" name="linien">
<field name="line_ids">
<list editable="bottom">
<field name="employee_name"/>
<field name="versicherungsnummer"/>
<field name="soziale_stellung"/>
<field name="kz210" optional="show"/>
<field name="kz215" optional="hide"/>
<field name="kz220" optional="show"/>
<field name="kz230" optional="hide"/>
<field name="kz245" optional="show"/>
<field name="lohnsteuer"/>
<field name="fab_betrag"
string="FB+"/>
</list>
</field>
</page>
<page string="Dateien" name="dateien">
<group>
<field name="datei_l16"
filename="datei_l16_name"/>
<field name="datei_l16_name" invisible="1"/>
<field name="datei_arbeitsstaette"
filename="datei_arbeitsstaette_name"/>
<field name="datei_arbeitsstaette_name"
invisible="1"/>
</group>
</page>
<page string="Prüfprotokoll" name="protokoll">
<field name="protokoll"/>
</page>
</notebook>
</sheet>
</form>
</field>
</record>
<record id="action_l16" model="ir.actions.act_window">
<field name="name">Lohnzettel Finanz (L16)</field>
<field name="res_model">l10n.at.payroll.l16</field>
<field name="view_mode">list,form</field>
</record>
<menuitem id="menu_l10n_at_payroll_meldewesen"
name="Meldewesen (ELDA/FinOnl)"
parent="hr_work_entry_enterprise.menu_hr_payroll_root"
sequence="97"/>
<menuitem id="menu_l10n_at_payroll_l16"
name="Lohnzettel Finanz (L16)"
parent="menu_l10n_at_payroll_meldewesen"
action="action_l16" sequence="10"/>
<!-- Kind-Stammdaten (FB+-Wartung) als Seite am Mitarbeiterformular -->
<record id="hr_employee_view_form_kind" model="ir.ui.view">
<field name="name">hr.employee.form.l10n_at_payroll_kind</field>
<field name="model">hr.employee</field>
<field name="inherit_id" ref="hr.view_employee_form"/>
<field name="arch" type="xml">
<notebook position="inside">
<page string="Kinder (FB+)" name="l10n_at_payroll_kinder"
groups="hr_payroll.group_hr_payroll_user">
<field name="l10n_at_payroll_kind_ids">
<list editable="bottom">
<field name="familienname"/>
<field name="vorname"/>
<field name="geburtstag"/>
<field name="versicherungsnummer"/>
<field name="fab_plus" widget="boolean_toggle"/>
<field name="fab_monat_von"/>
<field name="fab_monat_bis"/>
<field name="fab_halb_monat_von"/>
<field name="fab_halb_monat_bis"/>
</list>
</field>
</page>
</notebook>
</field>
</record>
<!-- AGRD-Mapping an der Abgangsursache (AP1) -->
<record id="hr_departure_reason_view_form_agrd" model="ir.ui.view">
<field name="name">hr.departure.reason.form.l10n_at_agrd</field>
<field name="model">hr.departure.reason</field>
<field name="inherit_id" ref="hr.hr_departure_reason_view_form"/>
<field name="arch" type="xml">
<field name="name" position="after">
<field name="l10n_at_agrd"/>
</field>
</field>
</record>
<record id="hr_departure_reason_view_list_agrd" model="ir.ui.view">
<field name="name">hr.departure.reason.list.l10n_at_agrd</field>
<field name="model">hr.departure.reason</field>
<field name="inherit_id" ref="hr.hr_departure_reason_view_list"/>
<field name="arch" type="xml">
<field name="name" position="after">
<field name="l10n_at_agrd" optional="show"/>
</field>
</field>
</record>
</odoo>
@@ -0,0 +1,104 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- AP18: Schwerarbeitsmeldung (ASchV § 5 / § 74 Abs 3 ASVG; -->
<!-- DM-ORG E.22, BEST SM, SART 65/66). -->
<record id="sm_list_view" model="ir.ui.view">
<field name="name">l10n.at.payroll.schwerarbeitsmeldung.list</field>
<field name="model">l10n.at.payroll.schwerarbeitsmeldung</field>
<field name="arch" type="xml">
<list string="Schwerarbeitsmeldung (ASchV)">
<field name="jahr"/>
<field name="company_id" groups="base.group_multi_company"/>
<field name="state" widget="badge"
decoration-success="state == 'done'"
decoration-info="state == 'draft'"/>
</list>
</field>
</record>
<record id="sm_form_view" model="ir.ui.view">
<field name="name">l10n.at.payroll.schwerarbeitsmeldung.form</field>
<field name="model">l10n.at.payroll.schwerarbeitsmeldung</field>
<field name="arch" type="xml">
<form string="Schwerarbeitsmeldung (§ 5 ASchV)">
<header>
<button name="action_erzeugen" type="object"
string="Lauf erzeugen (SART 65)"
class="btn-primary"
invisible="state == 'done'"/>
<field name="state" widget="statusbar"
statusbar_visible="draft,done"/>
</header>
<sheet>
<group>
<group>
<field name="jahr"/>
</group>
<group>
<field name="company_id"
groups="base.group_multi_company"/>
</group>
</group>
<notebook>
<page string="Meldesätze" name="linien">
<field name="line_ids">
<list editable="bottom"
open_form_view="1">
<field name="employee_name"/>
<field name="versicherungsnummer"/>
</list>
</field>
</page>
<page string="Datei" name="datei">
<group>
<field name="datei" filename="datei_name"/>
<field name="datei_name" invisible="1"/>
</group>
</page>
<page string="Prüfprotokoll" name="protokoll">
<field name="protokoll"/>
</page>
</notebook>
</sheet>
</form>
</field>
</record>
<record id="sm_line_form_view" model="ir.ui.view">
<field name="name">l10n.at.payroll.schwerarbeitsmeldung.line.form</field>
<field name="model">l10n.at.payroll.schwerarbeitsmeldung.line</field>
<field name="arch" type="xml">
<form string="Schwerarbeitsmeldung je AN">
<sheet>
<group>
<group>
<field name="employee_id"/>
<field name="versicherungsnummer"/>
</group>
</group>
<group string="Tätigkeitsblöcke (ASchV § 1 Ziffer, von/bis)">
<field name="taetigkeit_ids">
<list editable="bottom">
<field name="sequence" widget="handle"/>
<field name="ziffer"/>
<field name="von"/>
<field name="bis"/>
</list>
</field>
</group>
</sheet>
</form>
</field>
</record>
<record id="action_schwerarbeitsmeldung" model="ir.actions.act_window">
<field name="name">Schwerarbeitsmeldung</field>
<field name="res_model">l10n.at.payroll.schwerarbeitsmeldung</field>
<field name="view_mode">list,form</field>
</record>
<menuitem id="menu_l10n_at_payroll_sm"
name="Schwerarbeitsmeldung (SM)"
parent="menu_l10n_at_payroll_meldewesen"
action="action_schwerarbeitsmeldung" sequence="16"/>
</odoo>
@@ -0,0 +1,195 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- ============================================================ -->
<!-- AP0 (Variante b): SV-Tarifsystem-Katalog — BSGR/ERGB/DGG/ -->
<!-- Tarifgruppen/Tarifsets/Zu-/Abschläge aus dem TASY-Import. -->
<!-- ============================================================ -->
<record id="tasy_tarifgruppe_list_view" model="ir.ui.view">
<field name="name">l10n.at.payroll.tasy.tarifgruppe.list</field>
<field name="model">l10n.at.payroll.tasy.tarifgruppe</field>
<field name="arch" type="xml">
<list string="Tarifgruppen">
<field name="code"/>
<field name="bezeichnung"/>
<field name="bsgr_ids" widget="many2many_tags"/>
<field name="ergaenzung_ids" widget="many2many_tags"/>
<field name="dienstgebergruppe_ids" widget="many2many_tags"/>
<field name="von_datum" optional="show"/>
<field name="bis_datum" optional="show"/>
<field name="fallweise_zulaessig" optional="hide"/>
<field name="bv_zulaessig" optional="hide"/>
</list>
</field>
</record>
<record id="tasy_tarifgruppe_form_view" model="ir.ui.view">
<field name="name">l10n.at.payroll.tasy.tarifgruppe.form</field>
<field name="model">l10n.at.payroll.tasy.tarifgruppe</field>
<field name="arch" type="xml">
<form string="Tarifgruppe">
<sheet>
<group>
<group>
<field name="code"/>
<field name="bezeichnung"/>
<field name="traegergruppe"/>
<field name="von_datum"/>
<field name="bis_datum"/>
</group>
<group>
<field name="bsgr_ids" widget="many2many_tags"/>
<field name="ergaenzung_ids"
widget="many2many_tags"/>
<field name="dienstgebergruppe_ids"
widget="many2many_tags"/>
<field name="fallweise_zulaessig"/>
<field name="bv_zulaessig"/>
</group>
</group>
<notebook>
<page string="Tarifset-Zuordnungen"
name="zuordnungen">
<field name="zuord_ids" readonly="1">
<list>
<field name="basis_typ"/>
<field name="pos_typ"/>
<field name="tarifset_id"/>
<field name="von_datum"/>
<field name="bis_datum"/>
</list>
</field>
</page>
</notebook>
</sheet>
</form>
</field>
</record>
<record id="action_tasy_tarifgruppe" model="ir.actions.act_window">
<field name="name">Tarifgruppen (SV-Tarifsystem)</field>
<field name="res_model">l10n.at.payroll.tasy.tarifgruppe</field>
<field name="view_mode">list,form</field>
</record>
<record id="tasy_bsgr_list_view" model="ir.ui.view">
<field name="name">l10n.at.payroll.tasy.bsgr.list</field>
<field name="model">l10n.at.payroll.tasy.bsgr</field>
<field name="arch" type="xml">
<list string="Beschäftigtengruppen">
<field name="code"/>
<field name="kurzbezeichnung"/>
<field name="bezeichnung"/>
<field name="traegergruppe"/>
<field name="von_datum" optional="show"/>
<field name="bis_datum" optional="hide"/>
</list>
</field>
</record>
<record id="action_tasy_bsgr" model="ir.actions.act_window">
<field name="name">Beschäftigtengruppen (BSGR)</field>
<field name="res_model">l10n.at.payroll.tasy.bsgr</field>
<field name="view_mode">list</field>
</record>
<record id="tasy_ergaenzung_list_view" model="ir.ui.view">
<field name="name">l10n.at.payroll.tasy.ergaenzung.list</field>
<field name="model">l10n.at.payroll.tasy.ergaenzung</field>
<field name="arch" type="xml">
<list string="Ergänzungen">
<field name="code"/>
<field name="kurzbezeichnung"/>
<field name="bezeichnung"/>
<field name="traegergruppe"/>
<field name="von_datum" optional="show"/>
<field name="bis_datum" optional="hide"/>
</list>
</field>
</record>
<record id="action_tasy_ergaenzung" model="ir.actions.act_window">
<field name="name">Ergänzungen (ERGB)</field>
<field name="res_model">l10n.at.payroll.tasy.ergaenzung</field>
<field name="view_mode">list</field>
</record>
<record id="tasy_zuabschlag_list_view" model="ir.ui.view">
<field name="name">l10n.at.payroll.tasy.zuabschlag.list</field>
<field name="model">l10n.at.payroll.tasy.zuabschlag</field>
<field name="arch" type="xml">
<list string="Zu-/Abschläge">
<field name="code"/>
<field name="kurzbezeichnung"/>
<field name="bezeichnung"/>
<field name="zuabschlagkz" optional="show"/>
<field name="versicherungsrelevant" optional="show"/>
<field name="von_datum" optional="show"/>
<field name="bis_datum" optional="hide"/>
</list>
</field>
</record>
<record id="action_tasy_zuabschlag" model="ir.actions.act_window">
<field name="name">Zu-/Abschläge</field>
<field name="res_model">l10n.at.payroll.tasy.zuabschlag</field>
<field name="view_mode">list</field>
</record>
<!-- Katalog-Import-Wizard -->
<record id="tasy_katalog_wizard_form_view" model="ir.ui.view">
<field name="name">l10n.at.payroll.tasy.katalog.wizard.form</field>
<field name="model">l10n.at.payroll.tasy.katalog.wizard</field>
<field name="arch" type="xml">
<form string="SV-Tarifsystem-Katalog importieren">
<group>
<field name="file" filename="filename"/>
<field name="filename" invisible="1"/>
<field name="stichtag" readonly="1"/>
</group>
<group string="Protokoll">
<field name="protocol" readonly="1"/>
</group>
<footer>
<button string="Katalog importieren"
name="action_import" type="object"
class="btn-primary"/>
<button string="Abbrechen" class="btn-secondary"
special="cancel"/>
</footer>
</form>
</field>
</record>
<record id="action_tasy_katalog_wizard" model="ir.actions.act_window">
<field name="name">Tarifsystem-Katalog importieren (TASY)</field>
<field name="res_model">l10n.at.payroll.tasy.katalog.wizard</field>
<field name="view_mode">form</field>
<field name="target">new</field>
</record>
<menuitem id="menu_l10n_at_payroll_tasy_tarifgruppen"
name="Tarifsystem: Tarifgruppen"
parent="menu_l10n_at_payroll_root"
action="action_tasy_tarifgruppe" sequence="30"/>
<menuitem id="menu_l10n_at_payroll_tasy_bsgr"
name="Tarifsystem: Beschäftigtengruppen"
parent="menu_l10n_at_payroll_root"
action="action_tasy_bsgr" sequence="40"/>
<menuitem id="menu_l10n_at_payroll_tasy_ergaenzung"
name="Tarifsystem: Ergänzungen"
parent="menu_l10n_at_payroll_root"
action="action_tasy_ergaenzung" sequence="50"/>
<menuitem id="menu_l10n_at_payroll_tasy_zuabschlaege"
name="Tarifsystem: Zu-/Abschläge"
parent="menu_l10n_at_payroll_root"
action="action_tasy_zuabschlag" sequence="60"/>
<menuitem id="menu_l10n_at_payroll_tasy_katalog_import"
name="Tarifsystem-Katalog importieren"
parent="menu_l10n_at_payroll_root"
action="action_tasy_katalog_wizard" sequence="70"/>
</odoo>
+32 -9
View File
@@ -117,19 +117,42 @@ Auszahlung über `hr_payroll_account_iso20022`; kein VRV/MVAG
Rechenfälle `docs/rechenfaelle/buchhaltung.md`. Rechenfälle `docs/rechenfaelle/buchhaltung.md`.
**Offen (GP7+):** Meldewesen/Belege (GP7), Jahreswechsel (GP8); **GP7 Meldewesen/Belege (umgesetzt 2026-09-12):** auf dem Kern-
All-in-UI-Bericht (GP8); B14 längster-Urlaubs-Teil-Verfeinerung; Framework (ELDA-Fixlängen-Dateien, ISO-8859-15, CRLF-Annahme;
DZ-WKO-Verifikation (8.11) vor Wien-Mandanten; §-7-BMSVG-Fiktivfälle L16 + Arbeitsstättenmeldung + Belege Lohnzettel AT/Monatsdienstliste/
(Präsenzdienst/Kranken-/Wochengeld) als dokumentierte Option Monatsjournal im Kern): **mBGM/JASV** (Paket/mBGM/Tarifblock/Verrech-
(Rechenfälle-Datei); Arbeiter-Sub-Konten (Option A-Rest) mit der nungsbasis/-position, T01-Gesamtsatz aus ``at_mbgm_tarif`` — normativ
Referenzbuchhaltung fixieren (GP0-Rest). 39,05 % laufend / 37,55 % SZ; Abschläge A01/A02/A03 § 2a AMPFG **je
Basis AB und SZ**, A09/A10/A15 aus GP4-Stichtagen; BV-Basis + V01 aus
GP5; Storno-R-Sätze mit REFU; Fehlanzeige als Null-Paket; Gering-
fährige/fallweise ohne Tarifparameter = listenweiser Laufabbruch);
**Versichertenmeldung** M3/M4 (deklarative Event-Erkennung über die
Meldungs-Registry — idempotent, kein write-diff; BBER 0104, GERF,
FRDV, BVAB/BVJN aus GP5, VWAZ; AGRD-Mapping am Abgangsgrund) und
**fallweise Anmeldung** (BEST MA, SART 32; MVP je Anmeldetag);
NSchAB-Branche als Company-Flag (B001-T01 + 3,80 %).
Rechenfälle `docs/rechenfaelle/meldewesen.md`.
**GP8 Jahreswechsel/Fristen/Retro (umgesetzt 2026-09-12):**
Fristenkalender (`l10n.at.payroll.frist` mit ir.cron, Auto-
Erzeugung monatlich/jährlich, Überfällig-Färbung, Sprung zur
GP7-Deklaration), Jahres-Retro-Wizard (Kontrollsechstel § 67
Abs 10 mit Günstig-Vergleich, §-77-Abs-3/4-Option), Mitteilungs-
file-Import (ELDA-Rückantwort-XML), Vertragsstichtage-UI,
All-in-Deckungsrechnung + DAG-Ausweis (QWeb), 2027-Parameter-Rahmen.
**GP1GP8 komplett (M8 erreicht).**
**Offen (GP9/AP9):** UI/Security (Rollen), Multi-Company-Test,
Dokumentation, Schulung; geringfähig/fallweise-Tarifparameter.
Tests: `test_kv_katalog` + `test_entgelt_private` (GP2) + Tests: `test_kv_katalog` + `test_entgelt_private` (GP2) +
`test_lst_private` (GP3) + `test_sv_private` (GP4) + `test_lst_private` (GP3) + `test_sv_private` (GP4) +
`test_bv_private` (GP5) + `test_account_private` (GP6) `test_bv_private` (GP5) + `test_account_private` (GP6) +
`test_elda_l16` (GP7 Kern) + `test_meldewesen_private` (GP7) +
`test_gp8` (GP8 Kern) —
`.venv/bin/python -m odoo -d <testdb> -i l10n_at_hr_payroll,l10n_at_hr_payroll_private `.venv/bin/python -m odoo -d <testdb> -i l10n_at_hr_payroll,l10n_at_hr_payroll_private
--test-tags /l10n_at_hr_payroll,/l10n_at_hr_payroll_private --test-enable --stop-after-init --test-tags /l10n_at_hr_payroll,/l10n_at_hr_payroll_private --test-enable --stop-after-init
--addons-path=odoo_19.0+e.20260910/odoo-19.0+e.20260910/odoo/addons,addons` --addons-path=odoo_19.0+e.20260910/odoo-19.0+e.20260910/odoo/addons,addons`
(Lokal grün am 2026-09-12 auf Wegwerf-DB, frischer `-i`-Lauf: (Lokal grün am 2026-09-12 auf Wegwerf-DB, frischer `-i`-Lauf:
**88/88 Tests** — NET-Rundungscents bucht Odoo als „Adjustment Entry" **100/100 Tests**.)
auf das Journal-Default.)
@@ -1,8 +1,8 @@
# Part of the odoo-at-payroll project. License: LGPL-3. # Part of the odoo-at-payroll project. License: LGPL-3.
{ {
'name': 'Österreich - Privatwirtschaft Personalverrechnung (General-AT)', 'name': 'Österreich - Privatwirtschaft Personalverrechnung (General-AT)',
'version': '19.0.6.0.0', 'version': '19.0.17.0.0',
'author': 'ixsol / gem360', 'author': 'Florian Egger',
'website': 'https://gem360.at', 'website': 'https://gem360.at',
'category': 'Human Resources/Payroll', 'category': 'Human Resources/Payroll',
'description': """ 'description': """
@@ -65,6 +65,18 @@ Grundlagen: ``personalverrechnung/IMPLEMENTIERUNGSPLAN-Privat.md`` und
(Zahlungsregistrierung/SEPA via ``hr_payroll_account_iso20022``). (Zahlungsregistrierung/SEPA via ``hr_payroll_account_iso20022``).
Auszahlung über ``hr_payroll_account_iso20022``; kein VRV/MVAG Auszahlung über ``hr_payroll_account_iso20022``; kein VRV/MVAG
(gem360-Brücke). (gem360-Brücke).
- **GP7 Meldewesen (dieser Stand):** mBGM/JASV (BEST MB, DM-ORG
42.7.0: Paket/mBGM/Tarifblock/Verrechnungsbasis/-position mit
GESAMT-Prozentsatz je BSGR aus ``at_mbgm_tarif``, Abschläge
A01/A02/A03 §-2a-AMPFG, A09 UV-60, A10 AV+IE-Entfall, A15 PV-
Halbierung; BV-Basis + V01 aus GP5; Storno-R-Sätze; Fehlanzeige)
+ Versichertenmeldung (BEST VR: M3/M4 mit deklarativer Event-
Erkennung über die Meldungs-Registry, AGRD-Mapping am
Abgangsgrund, VWAZ, BBER, GERF; fallweise Anmeldung BEST MA
SART 32) auf dem GP7-Kern-Framework
(l10n_at_hr_payroll/models/elda.py; L16 + Arbeitsstättenmeldung
+ Belege siehe Kern-Modul). NSchAB-Branche als Company-Flag
(B001-T01 + 3,80 %).
- **Seed D2 (Referenz-KVs R1):** SI-2203 Handwerk und Gewerbe" + - **Seed D2 (Referenz-KVs R1):** SI-2203 Handwerk und Gewerbe" +
SI-2748 Metallgewerbe" (Gehaltstabellen ab 1. 1. 2026, kuratiert aus SI-2748 Metallgewerbe" (Gehaltstabellen ab 1. 1. 2026, kuratiert aus
den KV-Library-Volltexten). den KV-Library-Volltexten).
@@ -79,27 +91,63 @@ Kein VRV-Bezug (Gemeinde-Schiene bleibt ``l10n_at_gemeinde_payroll`` +
gem360-Brücke). gem360-Brücke).
""", """,
'depends': ['l10n_at_hr_payroll', 'hr_payroll_account', 'depends': ['l10n_at_hr_payroll', 'hr_payroll_account',
'l10n_at', 'hr_payroll_account_iso20022'], 'hr_payroll_expense', 'l10n_at', 'hr_payroll_account_iso20022',
'hr_holidays', 'hr_work_entry_holidays'],
'data': [ 'data': [
'security/ir.model.access.csv', 'security/ir.model.access.csv',
'security/security_rules.xml',
'data/kv_seed.xml', 'data/kv_seed.xml',
'data/l10n.at.payroll.kv.gruppe.csv', 'data/l10n.at.payroll.kv.gruppe.csv',
'data/l10n.at.payroll.kv.gruppe.stufe.csv', 'data/l10n.at.payroll.kv.gruppe.stufe.csv',
'data/l10n.at.payroll.kv.wert.csv', 'data/l10n.at.payroll.kv.wert.csv',
'data/structure_private.xml', 'data/structure_private.xml',
'data/work_entry_types_private.xml', 'data/work_entry_types_private.xml',
'data/absence_private.xml',
'data/input_types_private.xml', 'data/input_types_private.xml',
'data/input_types_endabrechnung_private.xml',
'data/salary_rules_private.xml', 'data/salary_rules_private.xml',
'data/salary_rules_lst_private.xml', 'data/salary_rules_lst_private.xml',
'data/salary_rules_efzg_private.xml',
'data/salary_rules_msch_private.xml',
'data/salary_rules_karenz_private.xml',
'data/salary_rules_pfaendung_private.xml',
'data/salary_rules_sb_private.xml',
'data/salary_rules_sb2_private.xml',
'data/salary_rules_reisekosten_private.xml',
'data/salary_rules_endabrechnung_private.xml',
'data/salary_rules_sv_private.xml', 'data/salary_rules_sv_private.xml',
'data/salary_rules_bvg_private.xml', 'data/salary_rules_bvg_private.xml',
'data/hr_payroll_account_private.xml', 'data/hr_payroll_account_private.xml',
'data/rule_parameters_mbgm_private.xml',
'data/rule_parameters_wien_private.xml',
'data/rule_parameters_efzg_private.xml',
'data/rule_parameters_msch_private.xml',
'data/rule_parameters_pfaendung_private.xml',
'data/rule_parameters_sb_private.xml',
'data/rule_parameters_sb2_private.xml',
'data/rule_parameters_reisekosten_private.xml',
'data/rule_parameters_pendler_private.xml',
'data/rule_parameters_endabrechnung_private.xml',
'data/bv_kasse_seed.xml', 'data/bv_kasse_seed.xml',
'views/kv_views.xml', 'views/kv_views.xml',
'views/bv_views.xml', 'views/bv_views.xml',
'views/meldewesen_views.xml',
'views/vertragsfelder_views.xml',
'views/mitarbeiter_views.xml',
'views/aeb_views.xml',
'views/pfaendung_views.xml',
'views/sachbezuege_views.xml',
'views/reisekosten_views.xml',
'views/pendler_views.xml',
'views/endabrechnung_views.xml',
'report/gp8_reports.xml',
'report/buchungsbeleg_report.xml',
'report/bv_report.xml', 'report/bv_report.xml',
'report/pfaendung_dse_report.xml',
], ],
'countries': ['at'], 'demo': ['data/demo.xml'],
'countries': ['at'],
'license': 'LGPL-3', 'license': 'LGPL-3',
'installable': True, 'installable': True,
'auto_install': False, 'auto_install': False,
@@ -0,0 +1,229 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- ============================================================ -->
<!-- AP5: AT-Abwesenheiten — Work-Entry-Types und Urlaubsarten. -->
<!-- -->
<!-- hr_holidays (Time Off) ist Pflicht-Dependence des AT- -->
<!-- Privatprodukts; hr_work_entry_holidays erzeugt aus validierten -->
<!-- Abwesenheiten automatisch Work Entries über das work_entry_ -->
<!-- type_id der Urlaubsart (Odoo-Muster der Lokalisierungen). -->
<!-- Die AT-Arten sind country-gebunden (base.at) und ergänzen die -->
<!-- globalen Standardarten — bewusst additiv, nichts überschrieben. -->
<!-- ============================================================ -->
<!-- Work-Entry-Types -->
<record id="wett_atp_urlaub" model="hr.work.entry.type">
<field name="name">Urlaub (UrlG)</field>
<field name="color">3</field>
<field name="display_code">URL</field>
<field name="code">ATP_URLAUB</field>
<field name="country_id" ref="base.at"/>
<field name="is_leave" eval="True"/>
</record>
<record id="wett_atp_krank" model="hr.work.entry.type">
<field name="name">Krankenstand (EFZG)</field>
<field name="color">2</field>
<field name="display_code">KRK</field>
<field name="code">ATP_KRANK</field>
<field name="country_id" ref="base.at"/>
<field name="is_leave" eval="True"/>
</record>
<!-- AP4: Arbeitsunfall/Berufskrankheit — eigenes EFZ-Sonderkontingent
(§ 8 Abs 2a AngG / § 2 Abs 5 EFZG), getrennter Topf. -->
<record id="wett_atp_au" model="hr.work.entry.type">
<field name="name">Arbeitsunfall/Berufskrankheit (EFZG)</field>
<field name="color">9</field>
<field name="display_code">AU</field>
<field name="code">ATP_AU</field>
<field name="country_id" ref="base.at"/>
<field name="is_leave" eval="True"/>
</record>
<record id="wett_atp_karenz" model="hr.work.entry.type">
<field name="name">Unbezahlte Karenz (MSchG/VKG)</field>
<field name="color">6</field>
<field name="display_code">KAR</field>
<field name="code">ATP_KARENZ</field>
<field name="country_id" ref="base.at"/>
<field name="is_leave" eval="True"/>
</record>
<!-- AP7: Mutterschutz-Fristen (§§ 3/5 MSchG) — Wochengeldbezug, -->
<!-- Entgelt gestoppt; Wochengeld zahlt die ÖGK. -->
<record id="wett_atp_msch" model="hr.work.entry.type">
<field name="name">Mutterschutz-Frist (MSchG §§ 3/5)</field>
<field name="color">8</field>
<field name="display_code">MSF</field>
<field name="code">ATP_MSCH</field>
<field name="country_id" ref="base.at"/>
<field name="is_leave" eval="True"/>
</record>
<!-- Urlaubsarten (hr.leave.type) mit WET-Verknüpfung -->
<record id="leave_type_at_urlaub" model="hr.leave.type">
<field name="name">Urlaub (UrlG)</field>
<field name="requires_allocation" eval="True"/>
<field name="employee_requests" eval="True"/>
<field name="leave_validation_type">both</field>
<field name="allocation_validation_type">hr</field>
<field name="request_unit">day</field>
<field name="company_id" eval="False"/>
<field name="country_id" ref="base.at"/>
<field name="work_entry_type_id"
ref="l10n_at_hr_payroll_private.wett_atp_urlaub"/>
</record>
<record id="leave_type_at_krankenstand" model="hr.leave.type">
<field name="name">Krankenstand</field>
<field name="requires_allocation" eval="False"/>
<field name="leave_validation_type">hr</field>
<field name="request_unit">day</field>
<field name="support_document" eval="True"/>
<field name="company_id" eval="False"/>
<field name="country_id" ref="base.at"/>
<field name="work_entry_type_id"
ref="l10n_at_hr_payroll_private.wett_atp_krank"/>
</record>
<record id="leave_type_at_arbeitsunfall" model="hr.leave.type">
<field name="name">Arbeitsunfall/Berufskrankheit</field>
<field name="requires_allocation" eval="False"/>
<field name="leave_validation_type">hr</field>
<field name="request_unit">day</field>
<field name="support_document" eval="True"/>
<field name="company_id" eval="False"/>
<field name="country_id" ref="base.at"/>
<field name="work_entry_type_id"
ref="l10n_at_hr_payroll_private.wett_atp_au"/>
</record>
<record id="leave_type_at_mutterschutz" model="hr.leave.type">
<field name="name">Mutterschutz-Frist (§§ 3/5 MSchG)</field>
<field name="requires_allocation" eval="False"/>
<field name="leave_validation_type">hr</field>
<field name="request_unit">day</field>
<field name="support_document" eval="True"/>
<field name="company_id" eval="False"/>
<field name="country_id" ref="base.at"/>
<field name="work_entry_type_id"
ref="l10n_at_hr_payroll_private.wett_atp_msch"/>
</record>
<record id="leave_type_at_karenz" model="hr.leave.type">
<field name="name">Unbezahlte Karenz</field>
<field name="requires_allocation" eval="False"/>
<field name="leave_validation_type">hr</field>
<field name="request_unit">day</field>
<field name="company_id" eval="False"/>
<field name="country_id" ref="base.at"/>
<field name="work_entry_type_id"
ref="l10n_at_hr_payroll_private.wett_atp_karenz"/>
</record>
<!-- AP16 (8.13): unbezahlte Karenz-Arten — alle entgeltlos
(Entgelt-Stopp über KARENZ_ENTGELT), je Art eigene eSV-Events;
Dienstzeit: Papamonat anrechnen (§ 1a Abs 7 VKG), Bildungs-/
Pflegekarenz nicht (§ 11 Abs 2, § 14c/14d AVRAG). -->
<record id="wett_atp_bildungskarenz" model="hr.work.entry.type">
<field name="name">Bildungskarenz (§ 11 AVRAG)</field>
<field name="color">4</field>
<field name="display_code">BLK</field>
<field name="code">ATP_BILDUNGSKARENZ</field>
<field name="country_id" ref="base.at"/>
<field name="is_leave" eval="True"/>
</record>
<record id="wett_atp_hospizkarenz" model="hr.work.entry.type">
<field name="name">Familienhospizkarenz — Sterbebegleitung (§ 14a AVRAG)</field>
<field name="color">5</field>
<field name="display_code">HSP</field>
<field name="code">ATP_HOSPIZKARENZ</field>
<field name="country_id" ref="base.at"/>
<field name="is_leave" eval="True"/>
</record>
<record id="wett_atp_kinderkrank" model="hr.work.entry.type">
<field name="name">Begleitung schwerst erkrankter Kinder (§ 14b AVRAG)</field>
<field name="color">7</field>
<field name="display_code">KKB</field>
<field name="code">ATP_KINDERKRANK</field>
<field name="country_id" ref="base.at"/>
<field name="is_leave" eval="True"/>
</record>
<record id="wett_atp_kinderrehab" model="hr.work.entry.type">
<field name="name">Freistellung Kinderrehabilitation (§ 14e AVRAG)</field>
<field name="color">10</field>
<field name="display_code">KRE</field>
<field name="code">ATP_KINDERREHAB</field>
<field name="country_id" ref="base.at"/>
<field name="is_leave" eval="True"/>
</record>
<record id="wett_atp_pflegekarenz" model="hr.work.entry.type">
<field name="name">Pflegekarenz (§ 14c AVRAG)</field>
<field name="color">1</field>
<field name="display_code">PFG</field>
<field name="code">ATP_PFLEGEKARENZ</field>
<field name="country_id" ref="base.at"/>
<field name="is_leave" eval="True"/>
</record>
<record id="wett_atp_papamonat" model="hr.work.entry.type">
<field name="name">Papa-Monat (§ 1a VKG)</field>
<field name="color">0</field>
<field name="display_code">PAP</field>
<field name="code">ATP_PAPAMONAT</field>
<field name="country_id" ref="base.at"/>
<field name="is_leave" eval="True"/>
</record>
<record id="leave_type_at_bildungskarenz" model="hr.leave.type">
<field name="name">Bildungskarenz (§ 11 AVRAG)</field>
<field name="requires_allocation" eval="False"/>
<field name="leave_validation_type">hr</field>
<field name="request_unit">day</field>
<field name="company_id" eval="False"/>
<field name="country_id" ref="base.at"/>
<field name="work_entry_type_id"
ref="l10n_at_hr_payroll_private.wett_atp_bildungskarenz"/>
</record>
<record id="leave_type_at_hospizkarenz" model="hr.leave.type">
<field name="name">Familienhospizkarenz — Sterbebegleitung</field>
<field name="requires_allocation" eval="False"/>
<field name="leave_validation_type">hr</field>
<field name="request_unit">day</field>
<field name="company_id" eval="False"/>
<field name="country_id" ref="base.at"/>
<field name="work_entry_type_id"
ref="l10n_at_hr_payroll_private.wett_atp_hospizkarenz"/>
</record>
<record id="leave_type_at_kinderkrank" model="hr.leave.type">
<field name="name">Begleitung schwerst erkrankter Kinder</field>
<field name="requires_allocation" eval="False"/>
<field name="leave_validation_type">hr</field>
<field name="request_unit">day</field>
<field name="company_id" eval="False"/>
<field name="country_id" ref="base.at"/>
<field name="work_entry_type_id"
ref="l10n_at_hr_payroll_private.wett_atp_kinderkrank"/>
</record>
<record id="leave_type_at_kinderrehab" model="hr.leave.type">
<field name="name">Freistellung Kinderrehabilitation</field>
<field name="requires_allocation" eval="False"/>
<field name="leave_validation_type">hr</field>
<field name="request_unit">day</field>
<field name="company_id" eval="False"/>
<field name="country_id" ref="base.at"/>
<field name="work_entry_type_id"
ref="l10n_at_hr_payroll_private.wett_atp_kinderrehab"/>
</record>
<record id="leave_type_at_pflegekarenz" model="hr.leave.type">
<field name="name">Pflegekarenz (§ 14c AVRAG)</field>
<field name="requires_allocation" eval="False"/>
<field name="leave_validation_type">hr</field>
<field name="request_unit">day</field>
<field name="company_id" eval="False"/>
<field name="country_id" ref="base.at"/>
<field name="work_entry_type_id"
ref="l10n_at_hr_payroll_private.wett_atp_pflegekarenz"/>
</record>
<record id="leave_type_at_papamonat" model="hr.leave.type">
<field name="name">Papa-Monat (§ 1a VKG)</field>
<field name="requires_allocation" eval="False"/>
<field name="leave_validation_type">hr</field>
<field name="request_unit">day</field>
<field name="company_id" eval="False"/>
<field name="country_id" ref="base.at"/>
<field name="work_entry_type_id"
ref="l10n_at_hr_payroll_private.wett_atp_papamonat"/>
</record>
</odoo>
@@ -0,0 +1,91 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- ============================================================ -->
<!-- GP9/AP9: Demo-Datensatz für Schulungszwecke. -->
<!-- 3 AN: Angestellte Vollzeit, Arbeiter Teilzeit, Lehrling. -->
<!-- 3 Monats-Läufe (Nov 2026 mit B13, Dez mit B14, Jän 2027). -->
<!-- Noupdate: Demo-Daten überschreiben keine echten Daten. -->
<!-- Load with: odoo -i l10n_at_hr_payroll_private (Demo-Modus) -->
<!-- ============================================================ -->
<data noupdate="1">
<!-- Company-Stammdaten für Demo -->
<record id="demo_company_at" model="res.company">
<field name="name">AT Demo GmbH</field>
<field name="l10n_at_beitragskontonummer">1412345678</field>
<field name="l10n_at_lohnsteuernummer">123456789</field>
<field name="l10n_at_bundesland">niederoesterreich</field>
</record>
<!-- Kalender -->
<record id="demo_calendar" model="resource.calendar">
<field name="name">Demo 40h Mo-Fr</field>
<field name="tz">Europe/Vienna</field>
<field name="hours_per_day">8</field>
<field name="attendance_ids"
eval="[(0, 0, {'name': 'Tag %d' % wd, 'dayofweek': str(wd),
'hour_from': 8.0, 'hour_to': 16.0}) for wd in range(5)]"/>
</record>
<!-- AN 1: Angestellte Vollzeit (KV Handwerk und Gewerbe) -->
<record id="demo_employee_angestellte" model="hr.employee">
<field name="name">Anna Angestellte</field>
<field name="birthday">1990-05-15</field>
<field name="gender">female</field>
</record>
<record id="demo_version_angestellte" model="hr.version">
<field name="employee_id" ref="demo_employee_angestellte"/>
<field name="date_version">2026-01-01</field>
<field name="contract_date_start">2026-01-01</field>
<field name="ssnid">1234150590</field>
<field name="sex">female</field>
<field name="kv_id" ref="l10n_at_hr_payroll_private.kv_si_2203"/>
<field name="kv_gruppe_id" ref="l10n_at_hr_payroll_private.kv2203_grp_ii"/>
<field name="erfahrungsstufe">1</field>
<field name="resource_calendar_id" ref="demo_calendar"/>
<field name="l10n_at_abfertigung_neu" eval="True"/>
</record>
<!-- AN 2: Arbeiter Teilzeit -->
<record id="demo_employee_arbeiter" model="hr.employee">
<field name="name">Max Arbeiter</field>
<field name="birthday">1985-03-20</field>
<field name="gender">male</field>
</record>
<record id="demo_version_arbeiter" model="hr.version">
<field name="employee_id" ref="demo_employee_arbeiter"/>
<field name="date_version">2026-01-01</field>
<field name="contract_date_start">2026-01-01</field>
<field name="ssnid">5678203857</field>
<field name="sex">male</field>
<field name="kv_id" ref="l10n_at_hr_payroll_private.kv_si_2203"/>
<field name="kv_gruppe_id" ref="l10n_at_hr_payroll_private.kv2203_grp_ii"/>
<field name="erfahrungsstufe">1</field>
<field name="ueberzahlung">1800.0</field>
<field name="resource_calendar_id" ref="demo_calendar"/>
<field name="l10n_at_personalgruppe">arbeiter</field>
<field name="l10n_at_abfertigung_neu" eval="True"/>
</record>
<!-- AN 3: Lehrling -->
<record id="demo_employee_lehrling" model="hr.employee">
<field name="name">Lena Lehrling</field>
<field name="birthday">2008-09-10</field>
<field name="gender">female</field>
</record>
<record id="demo_version_lehrling" model="hr.version">
<field name="employee_id" ref="demo_employee_lehrling"/>
<field name="date_version">2026-01-01</field>
<field name="contract_date_start">2026-01-01</field>
<field name="ssnid">9010091057</field>
<field name="sex">female</field>
<field name="kv_id" ref="l10n_at_hr_payroll_private.kv_si_2203"/>
<field name="kv_gruppe_id" ref="l10n_at_hr_payroll_private.kv2203_grp_ii"/>
<field name="erfahrungsstufe">1</field>
<field name="resource_calendar_id" ref="demo_calendar"/>
<field name="l10n_at_lehrling" eval="True"/>
<field name="l10n_at_abfertigung_neu" eval="True"/>
</record>
</data>
</odoo>
@@ -0,0 +1,58 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- AP14-A: Endabrechnungs-Eingaben. Mengen-Eingaben (Tage/Stunden)
tragen die Menge im amount-Feld (Muster ATP_SB_MAHLZEIT); die
Regeln leiten daraus die Beträge ab. -->
<record id="input_type_atp_uel_tage" model="hr.payslip.input.type">
<field name="name">Urlaubsersatzleistung — offene Urlaubstage</field>
<field name="code">ATP_UEL_TAGE</field>
<field name="is_quantity" eval="True"/>
</record>
<record id="input_type_atp_abgang" model="hr.payslip.input.type">
<field name="name">Abgangsentschädigung (Brutto)</field>
<field name="code">ATP_ABGANG</field>
</record>
<record id="input_type_atp_abgang_ffrist_tage" model="hr.payslip.input.type">
<field name="name">Abgangsentschädigung — Kündigungsfrist (Kalendertage)</field>
<field name="code">ATP_ABGANG_FFRIST_TAGE</field>
<field name="is_quantity" eval="True"/>
</record>
<record id="input_type_atp_zeit_guthaben" model="hr.payslip.input.type">
<field name="name">Zeitguthaben Normalarbeitszeit (Stunden)</field>
<field name="code">ATP_ZEIT_GUTHABEN</field>
<field name="is_quantity" eval="True"/>
</record>
<record id="input_type_atp_zeit_schulden" model="hr.payslip.input.type">
<field name="name">Zeitschulden (Stunden)</field>
<field name="code">ATP_ZEIT_SCHULDEN</field>
<field name="is_quantity" eval="True"/>
</record>
<record id="input_type_atp_abrechnung_vorschuss" model="hr.payslip.input.type">
<field name="name">Aufrechnung offener Vorschuss (Netto)</field>
<field name="code">ATP_ABRECHNUNG_VORSCHUSS</field>
</record>
<record id="input_type_atp_abf_alt" model="hr.payslip.input.type">
<field name="name">Gesetzliche Abfertigung Alt (Brutto)</field>
<field name="code">ATP_ABF_ALT</field>
</record>
<record id="input_type_atp_abf_frei" model="hr.payslip.input.type">
<field name="name">Freiwillige Abfertigung (Brutto)</field>
<field name="code">ATP_ABF_FREI</field>
</record>
<record id="input_type_atp_vergleich" model="hr.payslip.input.type">
<field name="name">Vergleichssumme — Rest (Brutto, Fünftelregelung)</field>
<field name="code">ATP_VERGLEICH</field>
</record>
<record id="input_type_atp_vergleich_bv" model="hr.payslip.input.type">
<field name="name">Vergleichssumme — BV-Anwartschafts-Teil (Brutto)</field>
<field name="code">ATP_VERGLEICH_BV</field>
</record>
<record id="input_type_atp_pram_end" model="hr.payslip.input.type">
<field name="name">Aliquote Prämie bei Beendigung (Brutto, § 16 AngG)</field>
<field name="code">ATP_PRAM_END</field>
</record>
<record id="input_type_atp_pram_end_tarif" model="hr.payslip.input.type">
<field name="name">Später fällige anteilige Prämie (Brutto, § 67 Abs 10)</field>
<field name="code">ATP_PRAM_END_TARIF</field>
</record>
</odoo>
@@ -7,4 +7,65 @@
<field name="name">Prämie/Ausgleich (manuell)</field> <field name="name">Prämie/Ausgleich (manuell)</field>
<field name="code">ATP_PRAMIE</field> <field name="code">ATP_PRAMIE</field>
</record> </record>
<!-- AP12a: steuerfreie Sachbezüge (§ 3/§ 26 EStG, § 49 Abs 3 ASVG) -->
<!-- als manuelle Eingaben. Menge (number_of_days) = Arbeitstage -->
<!-- (Mahlzeiten/Homeoffice), amount = Tages-/Pauschalwert; die -->
<!-- Freibetrags-/Kumulationslogik läuft über die SB_*-Regeln. -->
<record id="input_type_atp_sb_mahlzeit" model="hr.payslip.input.type">
<field name="name">Sachbezug Mahlzeiten — Anz. Gutschein-Arbeitstage (manuell)</field>
<field name="code">ATP_SB_MAHLZEIT</field>
<field name="is_quantity" eval="True"/>
</record>
<record id="input_type_atp_sb_betriebsveranstaltung" model="hr.payslip.input.type">
<field name="name">Sachbezug Betriebsveranstaltung — Gesamtvalue (manuell)</field>
<field name="code">ATP_SB_BETRIEBSVERANSTALTUNG</field>
</record>
<record id="input_type_atp_sb_sachzuwendung" model="hr.payslip.input.type">
<field name="name">Sachbezug Sachzuwendung/Geschenk — Gesamtvalue (manuell)</field>
<field name="code">ATP_SB_SACHZUWENDUNG</field>
</record>
<record id="input_type_atp_sb_rabatt" model="hr.payslip.input.type">
<field name="name">Sachbezug Mitarbeiterrabatt — Gesamtvalue (manuell)</field>
<field name="code">ATP_SB_RABATT</field>
</record>
<record id="input_type_atp_sb_homeoffice" model="hr.payslip.input.type">
<field name="name">Sachbezug Homeoffice — Anz. Tage (manuell)</field>
<field name="code">ATP_SB_HOMEOFFICE</field>
<field name="is_quantity" eval="True"/>
</record>
<!-- AP12b: ereignisbezogene Werte. Amount ist jeweils der bereits
ermittelte Vorteil (tatsächliche Mehrkosten bzw. Buchwert minus
Kaufpreis); Stammdaten steuern Wagen/Wohnung/Parkplatz. -->
<record id="input_type_atp_sb_handy" model="hr.payslip.input.type">
<field name="name">Sachbezug Handy — private Mehrkosten (manuell)</field>
<field name="code">ATP_SB_HANDY</field>
</record>
<record id="input_type_atp_sb_gebraucht" model="hr.payslip.input.type">
<field name="name">Sachbezug Arbeitsmittel-Verkauf unter Buchwert (manuell)</field>
<field name="code">ATP_SB_GEBRAUCHT</field>
</record>
<record id="input_type_atp_sb_bonusmeilen" model="hr.payslip.input.type">
<field name="name">Sachbezug Bonusmeilen — Einlösungswert (manuell)</field>
<field name="code">ATP_SB_BONUSMEILEN</field>
</record>
<!-- AP13-A: Diese Eingaben werden ausschließlich aus den mit dem
Payslip verknüpften hr.expense-Reisekosten erzeugt. -->
<record id="input_type_atp_reise_steuerpflichtig" model="hr.payslip.input.type">
<field name="name">Reisekosten — steuerpflichtiger Überhang</field>
<field name="code">ATP_REISE_STEUERPFLICHTIG</field>
</record>
<record id="input_type_atp_reise_nettoausgleich" model="hr.payslip.input.type">
<field name="name">Reisekosten — Nettoausgleich Überhang</field>
<field name="code">ATP_REISE_NETTOAUSGLEICH</field>
</record>
<!-- AP19: Präsenz-/Ausbildungs-/Zivildienst-Tage (§ 7 Abs 1/2
BMSVG) für die fiktive BV-Bemessungsgrundlage (at_bv_fiktiv_kbg). -->
<record id="input_type_atp_prd_tage" model="hr.payslip.input.type">
<field name="name">Präsenz-/Ausbildungs-/Zivildienst — Tage</field>
<field name="code">ATP_PRD_TAGE</field>
<field name="is_quantity" eval="True"/>
</record>
</odoo> </odoo>
@@ -1,132 +1,132 @@
id,gruppe_id:id,stufe,bezeichnung,gehalt,gueltig_ab id,gruppe_id:id,stufe,bezeichnung,gehalt,gueltig_ab,vwgj_ab
kv2203_st_ii_1,l10n_at_hr_payroll_private.kv2203_grp_ii,1,im 1. u 2. VWGJ,2059.41,2026-01-01 kv2203_st_ii_1,l10n_at_hr_payroll_private.kv2203_grp_ii,1,im 1. u 2. VWGJ,2059.41,2026-01-01,0
kv2203_st_ii_2,l10n_at_hr_payroll_private.kv2203_grp_ii,2,nach 2 VWGJ,2179.68,2026-01-01 kv2203_st_ii_2,l10n_at_hr_payroll_private.kv2203_grp_ii,2,nach 2 VWGJ,2179.68,2026-01-01,2
kv2203_st_ii_3,l10n_at_hr_payroll_private.kv2203_grp_ii,3,nach 4 VWGJ,2315.46,2026-01-01 kv2203_st_ii_3,l10n_at_hr_payroll_private.kv2203_grp_ii,3,nach 4 VWGJ,2315.46,2026-01-01,4
kv2203_st_ii_4,l10n_at_hr_payroll_private.kv2203_grp_ii,4,nach 6 VWGJ,2457.65,2026-01-01 kv2203_st_ii_4,l10n_at_hr_payroll_private.kv2203_grp_ii,4,nach 6 VWGJ,2457.65,2026-01-01,6
kv2203_st_ii_5,l10n_at_hr_payroll_private.kv2203_grp_ii,5,nach 8 VWGJ,2599.83,2026-01-01 kv2203_st_ii_5,l10n_at_hr_payroll_private.kv2203_grp_ii,5,nach 8 VWGJ,2599.83,2026-01-01,8
kv2203_st_ii_6,l10n_at_hr_payroll_private.kv2203_grp_ii,6,nach 10 VWGJ,2742.03,2026-01-01 kv2203_st_ii_6,l10n_at_hr_payroll_private.kv2203_grp_ii,6,nach 10 VWGJ,2742.03,2026-01-01,10
kv2203_st_ii_7,l10n_at_hr_payroll_private.kv2203_grp_ii,7,nach 12 VWGJ,2863.88,2026-01-01 kv2203_st_ii_7,l10n_at_hr_payroll_private.kv2203_grp_ii,7,nach 12 VWGJ,2863.88,2026-01-01,12
kv2203_st_ii_8,l10n_at_hr_payroll_private.kv2203_grp_ii,8,nach 15 VWGJ,3087.28,2026-01-01 kv2203_st_ii_8,l10n_at_hr_payroll_private.kv2203_grp_ii,8,nach 15 VWGJ,3087.28,2026-01-01,15
kv2203_st_iii_1,l10n_at_hr_payroll_private.kv2203_grp_iii,1,im 1. u 2. VWGJ,2528.04,2026-01-01 kv2203_st_iii_1,l10n_at_hr_payroll_private.kv2203_grp_iii,1,im 1. u 2. VWGJ,2528.04,2026-01-01,0
kv2203_st_iii_2,l10n_at_hr_payroll_private.kv2203_grp_iii,2,nach 2 VWGJ,2705.03,2026-01-01 kv2203_st_iii_2,l10n_at_hr_payroll_private.kv2203_grp_iii,2,nach 2 VWGJ,2705.03,2026-01-01,2
kv2203_st_iii_3,l10n_at_hr_payroll_private.kv2203_grp_iii,3,nach 4 VWGJ,2881.97,2026-01-01 kv2203_st_iii_3,l10n_at_hr_payroll_private.kv2203_grp_iii,3,nach 4 VWGJ,2881.97,2026-01-01,4
kv2203_st_iii_4,l10n_at_hr_payroll_private.kv2203_grp_iii,4,nach 6 VWGJ,3058.96,2026-01-01 kv2203_st_iii_4,l10n_at_hr_payroll_private.kv2203_grp_iii,4,nach 6 VWGJ,3058.96,2026-01-01,6
kv2203_st_iii_5,l10n_at_hr_payroll_private.kv2203_grp_iii,5,nach 8 VWGJ,3232.73,2026-01-01 kv2203_st_iii_5,l10n_at_hr_payroll_private.kv2203_grp_iii,5,nach 8 VWGJ,3232.73,2026-01-01,8
kv2203_st_iii_6,l10n_at_hr_payroll_private.kv2203_grp_iii,6,nach 10 VWGJ,3409.53,2026-01-01 kv2203_st_iii_6,l10n_at_hr_payroll_private.kv2203_grp_iii,6,nach 10 VWGJ,3409.53,2026-01-01,10
kv2203_st_iii_7,l10n_at_hr_payroll_private.kv2203_grp_iii,7,nach 12 VWGJ,3561.04,2026-01-01 kv2203_st_iii_7,l10n_at_hr_payroll_private.kv2203_grp_iii,7,nach 12 VWGJ,3561.04,2026-01-01,12
kv2203_st_iii_8,l10n_at_hr_payroll_private.kv2203_grp_iii,8,nach 15 VWGJ,3838.85,2026-01-01 kv2203_st_iii_8,l10n_at_hr_payroll_private.kv2203_grp_iii,8,nach 15 VWGJ,3838.85,2026-01-01,15
kv2203_st_iv_1,l10n_at_hr_payroll_private.kv2203_grp_iv,1,im 1. u 2. VWGJ,3146.27,2026-01-01 kv2203_st_iv_1,l10n_at_hr_payroll_private.kv2203_grp_iv,1,im 1. u 2. VWGJ,3146.27,2026-01-01,0
kv2203_st_iv_2,l10n_at_hr_payroll_private.kv2203_grp_iv,2,nach 2 VWGJ,3366.49,2026-01-01 kv2203_st_iv_2,l10n_at_hr_payroll_private.kv2203_grp_iv,2,nach 2 VWGJ,3366.49,2026-01-01,2
kv2203_st_iv_3,l10n_at_hr_payroll_private.kv2203_grp_iv,3,nach 4 VWGJ,3586.73,2026-01-01 kv2203_st_iv_3,l10n_at_hr_payroll_private.kv2203_grp_iv,3,nach 4 VWGJ,3586.73,2026-01-01,4
kv2203_st_iv_4,l10n_at_hr_payroll_private.kv2203_grp_iv,4,nach 6 VWGJ,3806.97,2026-01-01 kv2203_st_iv_4,l10n_at_hr_payroll_private.kv2203_grp_iv,4,nach 6 VWGJ,3806.97,2026-01-01,6
kv2203_st_iv_5,l10n_at_hr_payroll_private.kv2203_grp_iv,5,nach 8 VWGJ,4027.20,2026-01-01 kv2203_st_iv_5,l10n_at_hr_payroll_private.kv2203_grp_iv,5,nach 8 VWGJ,4027.20,2026-01-01,8
kv2203_st_iv_6,l10n_at_hr_payroll_private.kv2203_grp_iv,6,nach 10 VWGJ,4247.47,2026-01-01 kv2203_st_iv_6,l10n_at_hr_payroll_private.kv2203_grp_iv,6,nach 10 VWGJ,4247.47,2026-01-01,10
kv2203_st_iv_7,l10n_at_hr_payroll_private.kv2203_grp_iv,7,nach 12 VWGJ,4436.22,2026-01-01 kv2203_st_iv_7,l10n_at_hr_payroll_private.kv2203_grp_iv,7,nach 12 VWGJ,4436.22,2026-01-01,12
kv2203_st_iv_8,l10n_at_hr_payroll_private.kv2203_grp_iv,8,nach 15 VWGJ,4782.32,2026-01-01 kv2203_st_iv_8,l10n_at_hr_payroll_private.kv2203_grp_iv,8,nach 15 VWGJ,4782.32,2026-01-01,15
kv2203_st_v_1,l10n_at_hr_payroll_private.kv2203_grp_v,1,im 1. u 2. VWGJ,3919.67,2026-01-01 kv2203_st_v_1,l10n_at_hr_payroll_private.kv2203_grp_v,1,im 1. u 2. VWGJ,3919.67,2026-01-01,0
kv2203_st_v_2,l10n_at_hr_payroll_private.kv2203_grp_v,2,nach 2 VWGJ,4194.02,2026-01-01 kv2203_st_v_2,l10n_at_hr_payroll_private.kv2203_grp_v,2,nach 2 VWGJ,4194.02,2026-01-01,2
kv2203_st_v_3,l10n_at_hr_payroll_private.kv2203_grp_v,3,nach 4 VWGJ,4468.41,2026-01-01 kv2203_st_v_3,l10n_at_hr_payroll_private.kv2203_grp_v,3,nach 4 VWGJ,4468.41,2026-01-01,4
kv2203_st_v_4,l10n_at_hr_payroll_private.kv2203_grp_v,4,nach 6 VWGJ,4742.77,2026-01-01 kv2203_st_v_4,l10n_at_hr_payroll_private.kv2203_grp_v,4,nach 6 VWGJ,4742.77,2026-01-01,6
kv2203_st_v_5,l10n_at_hr_payroll_private.kv2203_grp_v,5,nach 8 VWGJ,5017.15,2026-01-01 kv2203_st_v_5,l10n_at_hr_payroll_private.kv2203_grp_v,5,nach 8 VWGJ,5017.15,2026-01-01,8
kv2203_st_v_6,l10n_at_hr_payroll_private.kv2203_grp_v,6,nach 10 VWGJ,5291.57,2026-01-01 kv2203_st_v_6,l10n_at_hr_payroll_private.kv2203_grp_v,6,nach 10 VWGJ,5291.57,2026-01-01,10
kv2203_st_v_7,l10n_at_hr_payroll_private.kv2203_grp_v,7,nach 12 VWGJ,5526.70,2026-01-01 kv2203_st_v_7,l10n_at_hr_payroll_private.kv2203_grp_v,7,nach 12 VWGJ,5526.70,2026-01-01,12
kv2203_st_v_8,l10n_at_hr_payroll_private.kv2203_grp_v,8,nach 15 VWGJ,5957.86,2026-01-01 kv2203_st_v_8,l10n_at_hr_payroll_private.kv2203_grp_v,8,nach 15 VWGJ,5957.86,2026-01-01,15
kv2203_st_vi_1,l10n_at_hr_payroll_private.kv2203_grp_vi,1,im 1. u 2. VWGJ,5482.40,2026-01-01 kv2203_st_vi_1,l10n_at_hr_payroll_private.kv2203_grp_vi,1,im 1. u 2. VWGJ,5482.40,2026-01-01,0
kv2203_st_vi_2,l10n_at_hr_payroll_private.kv2203_grp_vi,2,nach 2 VWGJ,6167.67,2026-01-01 kv2203_st_vi_2,l10n_at_hr_payroll_private.kv2203_grp_vi,2,nach 2 VWGJ,6167.67,2026-01-01,2
kv2203_st_vi_3,l10n_at_hr_payroll_private.kv2203_grp_vi,3,nach 5 VWGJ,6853.01,2026-01-01 kv2203_st_vi_3,l10n_at_hr_payroll_private.kv2203_grp_vi,3,nach 5 VWGJ,6853.01,2026-01-01,5
kv2203_st_m1_1,l10n_at_hr_payroll_private.kv2203_grp_m1,1,im 1. u 2. VWGJ,2427.72,2026-01-01 kv2203_st_m1_1,l10n_at_hr_payroll_private.kv2203_grp_m1,1,im 1. u 2. VWGJ,2427.72,2026-01-01,0
kv2203_st_m1_2,l10n_at_hr_payroll_private.kv2203_grp_m1,2,nach 2 VWGJ,2427.72,2026-01-01 kv2203_st_m1_2,l10n_at_hr_payroll_private.kv2203_grp_m1,2,nach 2 VWGJ,2427.72,2026-01-01,2
kv2203_st_m1_3,l10n_at_hr_payroll_private.kv2203_grp_m1,3,nach 4 VWGJ,2586.57,2026-01-01 kv2203_st_m1_3,l10n_at_hr_payroll_private.kv2203_grp_m1,3,nach 4 VWGJ,2586.57,2026-01-01,4
kv2203_st_m1_4,l10n_at_hr_payroll_private.kv2203_grp_m1,4,nach 6 VWGJ,2745.39,2026-01-01 kv2203_st_m1_4,l10n_at_hr_payroll_private.kv2203_grp_m1,4,nach 6 VWGJ,2745.39,2026-01-01,6
kv2203_st_m1_5,l10n_at_hr_payroll_private.kv2203_grp_m1,5,nach 8 VWGJ,2904.19,2026-01-01 kv2203_st_m1_5,l10n_at_hr_payroll_private.kv2203_grp_m1,5,nach 8 VWGJ,2904.19,2026-01-01,8
kv2203_st_m1_6,l10n_at_hr_payroll_private.kv2203_grp_m1,6,nach 10 VWGJ,3063.05,2026-01-01 kv2203_st_m1_6,l10n_at_hr_payroll_private.kv2203_grp_m1,6,nach 10 VWGJ,3063.05,2026-01-01,10
kv2203_st_m1_7,l10n_at_hr_payroll_private.kv2203_grp_m1,7,nach 12 VWGJ,3199.16,2026-01-01 kv2203_st_m1_7,l10n_at_hr_payroll_private.kv2203_grp_m1,7,nach 12 VWGJ,3199.16,2026-01-01,12
kv2203_st_m1_8,l10n_at_hr_payroll_private.kv2203_grp_m1,8,nach 15 VWGJ,3448.74,2026-01-01 kv2203_st_m1_8,l10n_at_hr_payroll_private.kv2203_grp_m1,8,nach 15 VWGJ,3448.74,2026-01-01,15
kv2203_st_m2_1,l10n_at_hr_payroll_private.kv2203_grp_m2,1,im 1. u 2. VWGJ,3095.50,2026-01-01 kv2203_st_m2_1,l10n_at_hr_payroll_private.kv2203_grp_m2,1,im 1. u 2. VWGJ,3095.50,2026-01-01,0
kv2203_st_m2_2,l10n_at_hr_payroll_private.kv2203_grp_m2,2,nach 2 VWGJ,3095.50,2026-01-01 kv2203_st_m2_2,l10n_at_hr_payroll_private.kv2203_grp_m2,2,nach 2 VWGJ,3095.50,2026-01-01,2
kv2203_st_m2_3,l10n_at_hr_payroll_private.kv2203_grp_m2,3,nach 4 VWGJ,3297.98,2026-01-01 kv2203_st_m2_3,l10n_at_hr_payroll_private.kv2203_grp_m2,3,nach 4 VWGJ,3297.98,2026-01-01,4
kv2203_st_m2_4,l10n_at_hr_payroll_private.kv2203_grp_m2,4,nach 6 VWGJ,3500.52,2026-01-01 kv2203_st_m2_4,l10n_at_hr_payroll_private.kv2203_grp_m2,4,nach 6 VWGJ,3500.52,2026-01-01,6
kv2203_st_m2_5,l10n_at_hr_payroll_private.kv2203_grp_m2,5,nach 8 VWGJ,3703.01,2026-01-01 kv2203_st_m2_5,l10n_at_hr_payroll_private.kv2203_grp_m2,5,nach 8 VWGJ,3703.01,2026-01-01,8
kv2203_st_m2_6,l10n_at_hr_payroll_private.kv2203_grp_m2,6,nach 10 VWGJ,3905.53,2026-01-01 kv2203_st_m2_6,l10n_at_hr_payroll_private.kv2203_grp_m2,6,nach 10 VWGJ,3905.53,2026-01-01,10
kv2203_st_m2_7,l10n_at_hr_payroll_private.kv2203_grp_m2,7,nach 12 VWGJ,4079.13,2026-01-01 kv2203_st_m2_7,l10n_at_hr_payroll_private.kv2203_grp_m2,7,nach 12 VWGJ,4079.13,2026-01-01,12
kv2203_st_m2_8,l10n_at_hr_payroll_private.kv2203_grp_m2,8,nach 15 VWGJ,4397.37,2026-01-01 kv2203_st_m2_8,l10n_at_hr_payroll_private.kv2203_grp_m2,8,nach 15 VWGJ,4397.37,2026-01-01,15
kv2203_st_m2fs_1,l10n_at_hr_payroll_private.kv2203_grp_m2fs,1,im 1. u 2. VWGJ,3241.68,2026-01-01 kv2203_st_m2fs_1,l10n_at_hr_payroll_private.kv2203_grp_m2fs,1,im 1. u 2. VWGJ,3241.68,2026-01-01,0
kv2203_st_m2fs_2,l10n_at_hr_payroll_private.kv2203_grp_m2fs,2,nach 2 VWGJ,3241.68,2026-01-01 kv2203_st_m2fs_2,l10n_at_hr_payroll_private.kv2203_grp_m2fs,2,nach 2 VWGJ,3241.68,2026-01-01,2
kv2203_st_m2fs_3,l10n_at_hr_payroll_private.kv2203_grp_m2fs,3,nach 4 VWGJ,3453.74,2026-01-01 kv2203_st_m2fs_3,l10n_at_hr_payroll_private.kv2203_grp_m2fs,3,nach 4 VWGJ,3453.74,2026-01-01,4
kv2203_st_m2fs_4,l10n_at_hr_payroll_private.kv2203_grp_m2fs,4,nach 6 VWGJ,3665.79,2026-01-01 kv2203_st_m2fs_4,l10n_at_hr_payroll_private.kv2203_grp_m2fs,4,nach 6 VWGJ,3665.79,2026-01-01,6
kv2203_st_m2fs_5,l10n_at_hr_payroll_private.kv2203_grp_m2fs,5,nach 8 VWGJ,3877.83,2026-01-01 kv2203_st_m2fs_5,l10n_at_hr_payroll_private.kv2203_grp_m2fs,5,nach 8 VWGJ,3877.83,2026-01-01,8
kv2203_st_m2fs_6,l10n_at_hr_payroll_private.kv2203_grp_m2fs,6,nach 10 VWGJ,4089.90,2026-01-01 kv2203_st_m2fs_6,l10n_at_hr_payroll_private.kv2203_grp_m2fs,6,nach 10 VWGJ,4089.90,2026-01-01,10
kv2203_st_m2fs_7,l10n_at_hr_payroll_private.kv2203_grp_m2fs,7,nach 12 VWGJ,4271.68,2026-01-01 kv2203_st_m2fs_7,l10n_at_hr_payroll_private.kv2203_grp_m2fs,7,nach 12 VWGJ,4271.68,2026-01-01,12
kv2203_st_m2fs_8,l10n_at_hr_payroll_private.kv2203_grp_m2fs,8,nach 15 VWGJ,4605.01,2026-01-01 kv2203_st_m2fs_8,l10n_at_hr_payroll_private.kv2203_grp_m2fs,8,nach 15 VWGJ,4605.01,2026-01-01,15
kv2203_st_m3_1,l10n_at_hr_payroll_private.kv2203_grp_m3,1,im 1. u 2. VWGJ,3542.72,2026-01-01 kv2203_st_m3_1,l10n_at_hr_payroll_private.kv2203_grp_m3,1,im 1. u 2. VWGJ,3542.72,2026-01-01,0
kv2203_st_m3_2,l10n_at_hr_payroll_private.kv2203_grp_m3,2,nach 2 VWGJ,3542.72,2026-01-01 kv2203_st_m3_2,l10n_at_hr_payroll_private.kv2203_grp_m3,2,nach 2 VWGJ,3542.72,2026-01-01,2
kv2203_st_m3_3,l10n_at_hr_payroll_private.kv2203_grp_m3,3,nach 4 VWGJ,3774.51,2026-01-01 kv2203_st_m3_3,l10n_at_hr_payroll_private.kv2203_grp_m3,3,nach 4 VWGJ,3774.51,2026-01-01,4
kv2203_st_m3_4,l10n_at_hr_payroll_private.kv2203_grp_m3,4,nach 6 VWGJ,4006.27,2026-01-01 kv2203_st_m3_4,l10n_at_hr_payroll_private.kv2203_grp_m3,4,nach 6 VWGJ,4006.27,2026-01-01,6
kv2203_st_m3_5,l10n_at_hr_payroll_private.kv2203_grp_m3,5,nach 8 VWGJ,4238.05,2026-01-01 kv2203_st_m3_5,l10n_at_hr_payroll_private.kv2203_grp_m3,5,nach 8 VWGJ,4238.05,2026-01-01,8
kv2203_st_m3_6,l10n_at_hr_payroll_private.kv2203_grp_m3,6,nach 10 VWGJ,4469.81,2026-01-01 kv2203_st_m3_6,l10n_at_hr_payroll_private.kv2203_grp_m3,6,nach 10 VWGJ,4469.81,2026-01-01,10
kv2203_st_m3_7,l10n_at_hr_payroll_private.kv2203_grp_m3,7,nach 12 VWGJ,4668.45,2026-01-01 kv2203_st_m3_7,l10n_at_hr_payroll_private.kv2203_grp_m3,7,nach 12 VWGJ,4668.45,2026-01-01,12
kv2203_st_m3_8,l10n_at_hr_payroll_private.kv2203_grp_m3,8,nach 15 VWGJ,5032.68,2026-01-01 kv2203_st_m3_8,l10n_at_hr_payroll_private.kv2203_grp_m3,8,nach 15 VWGJ,5032.68,2026-01-01,15
kv2748_st_i_1,l10n_at_hr_payroll_private.kv2748_grp_i,1,im 1. u 2. VWGJ,1960.54,2026-01-01 kv2748_st_i_1,l10n_at_hr_payroll_private.kv2748_grp_i,1,im 1. u 2. VWGJ,1960.54,2026-01-01,0
kv2748_st_i_2,l10n_at_hr_payroll_private.kv2748_grp_i,2,nach 2 VWGJ,2058.43,2026-01-01 kv2748_st_i_2,l10n_at_hr_payroll_private.kv2748_grp_i,2,nach 2 VWGJ,2058.43,2026-01-01,2
kv2748_st_i_3,l10n_at_hr_payroll_private.kv2748_grp_i,3,nach 4 VWGJ,2190.76,2026-01-01 kv2748_st_i_3,l10n_at_hr_payroll_private.kv2748_grp_i,3,nach 4 VWGJ,2190.76,2026-01-01,4
kv2748_st_i_4,l10n_at_hr_payroll_private.kv2748_grp_i,4,nach 6 VWGJ,2323.12,2026-01-01 kv2748_st_i_4,l10n_at_hr_payroll_private.kv2748_grp_i,4,nach 6 VWGJ,2323.12,2026-01-01,6
kv2748_st_i_5,l10n_at_hr_payroll_private.kv2748_grp_i,5,nach 8 VWGJ,2455.45,2026-01-01 kv2748_st_i_5,l10n_at_hr_payroll_private.kv2748_grp_i,5,nach 8 VWGJ,2455.45,2026-01-01,8
kv2748_st_i_6,l10n_at_hr_payroll_private.kv2748_grp_i,6,nach 10 VWGJ,2588.72,2026-01-01 kv2748_st_i_6,l10n_at_hr_payroll_private.kv2748_grp_i,6,nach 10 VWGJ,2588.72,2026-01-01,10
kv2748_st_i_7,l10n_at_hr_payroll_private.kv2748_grp_i,7,nach 12 VWGJ,2703.75,2026-01-01 kv2748_st_i_7,l10n_at_hr_payroll_private.kv2748_grp_i,7,nach 12 VWGJ,2703.75,2026-01-01,12
kv2748_st_i_8,l10n_at_hr_payroll_private.kv2748_grp_i,8,nach 15 VWGJ,2914.69,2026-01-01 kv2748_st_i_8,l10n_at_hr_payroll_private.kv2748_grp_i,8,nach 15 VWGJ,2914.69,2026-01-01,15
kv2748_st_ii_1,l10n_at_hr_payroll_private.kv2748_grp_ii,1,im 1. u 2. VWGJ,2199.15,2026-01-01 kv2748_st_ii_1,l10n_at_hr_payroll_private.kv2748_grp_ii,1,im 1. u 2. VWGJ,2199.15,2026-01-01,0
kv2748_st_ii_2,l10n_at_hr_payroll_private.kv2748_grp_ii,2,nach 2 VWGJ,2350.62,2026-01-01 kv2748_st_ii_2,l10n_at_hr_payroll_private.kv2748_grp_ii,2,nach 2 VWGJ,2350.62,2026-01-01,2
kv2748_st_ii_3,l10n_at_hr_payroll_private.kv2748_grp_ii,3,nach 4 VWGJ,2502.07,2026-01-01 kv2748_st_ii_3,l10n_at_hr_payroll_private.kv2748_grp_ii,3,nach 4 VWGJ,2502.07,2026-01-01,4
kv2748_st_ii_4,l10n_at_hr_payroll_private.kv2748_grp_ii,4,nach 6 VWGJ,2655.45,2026-01-01 kv2748_st_ii_4,l10n_at_hr_payroll_private.kv2748_grp_ii,4,nach 6 VWGJ,2655.45,2026-01-01,6
kv2748_st_ii_5,l10n_at_hr_payroll_private.kv2748_grp_ii,5,nach 8 VWGJ,2809.08,2026-01-01 kv2748_st_ii_5,l10n_at_hr_payroll_private.kv2748_grp_ii,5,nach 8 VWGJ,2809.08,2026-01-01,8
kv2748_st_ii_6,l10n_at_hr_payroll_private.kv2748_grp_ii,6,nach 10 VWGJ,2962.72,2026-01-01 kv2748_st_ii_6,l10n_at_hr_payroll_private.kv2748_grp_ii,6,nach 10 VWGJ,2962.72,2026-01-01,10
kv2748_st_ii_7,l10n_at_hr_payroll_private.kv2748_grp_ii,7,nach 12 VWGJ,3094.32,2026-01-01 kv2748_st_ii_7,l10n_at_hr_payroll_private.kv2748_grp_ii,7,nach 12 VWGJ,3094.32,2026-01-01,12
kv2748_st_ii_8,l10n_at_hr_payroll_private.kv2748_grp_ii,8,nach 15 VWGJ,3335.74,2026-01-01 kv2748_st_ii_8,l10n_at_hr_payroll_private.kv2748_grp_ii,8,nach 15 VWGJ,3335.74,2026-01-01,15
kv2748_st_iii_1,l10n_at_hr_payroll_private.kv2748_grp_iii,1,im 1. u 2. VWGJ,2727.40,2026-01-01 kv2748_st_iii_1,l10n_at_hr_payroll_private.kv2748_grp_iii,1,im 1. u 2. VWGJ,2727.40,2026-01-01,0
kv2748_st_iii_2,l10n_at_hr_payroll_private.kv2748_grp_iii,2,nach 2 VWGJ,2918.34,2026-01-01 kv2748_st_iii_2,l10n_at_hr_payroll_private.kv2748_grp_iii,2,nach 2 VWGJ,2918.34,2026-01-01,2
kv2748_st_iii_3,l10n_at_hr_payroll_private.kv2748_grp_iii,3,nach 4 VWGJ,3109.21,2026-01-01 kv2748_st_iii_3,l10n_at_hr_payroll_private.kv2748_grp_iii,3,nach 4 VWGJ,3109.21,2026-01-01,4
kv2748_st_iii_4,l10n_at_hr_payroll_private.kv2748_grp_iii,4,nach 6 VWGJ,3300.16,2026-01-01 kv2748_st_iii_4,l10n_at_hr_payroll_private.kv2748_grp_iii,4,nach 6 VWGJ,3300.16,2026-01-01,6
kv2748_st_iii_5,l10n_at_hr_payroll_private.kv2748_grp_iii,5,nach 8 VWGJ,3491.06,2026-01-01 kv2748_st_iii_5,l10n_at_hr_payroll_private.kv2748_grp_iii,5,nach 8 VWGJ,3491.06,2026-01-01,8
kv2748_st_iii_6,l10n_at_hr_payroll_private.kv2748_grp_iii,6,nach 10 VWGJ,3682.00,2026-01-01 kv2748_st_iii_6,l10n_at_hr_payroll_private.kv2748_grp_iii,6,nach 10 VWGJ,3682.00,2026-01-01,10
kv2748_st_iii_7,l10n_at_hr_payroll_private.kv2748_grp_iii,7,nach 12 VWGJ,3845.61,2026-01-01 kv2748_st_iii_7,l10n_at_hr_payroll_private.kv2748_grp_iii,7,nach 12 VWGJ,3845.61,2026-01-01,12
kv2748_st_iii_8,l10n_at_hr_payroll_private.kv2748_grp_iii,8,nach 15 VWGJ,4145.62,2026-01-01 kv2748_st_iii_8,l10n_at_hr_payroll_private.kv2748_grp_iii,8,nach 15 VWGJ,4145.62,2026-01-01,15
kv2748_st_iv_1,l10n_at_hr_payroll_private.kv2748_grp_iv,1,im 1. u 2. VWGJ,3415.01,2026-01-01 kv2748_st_iv_1,l10n_at_hr_payroll_private.kv2748_grp_iv,1,im 1. u 2. VWGJ,3415.01,2026-01-01,0
kv2748_st_iv_2,l10n_at_hr_payroll_private.kv2748_grp_iv,2,nach 2 VWGJ,3654.05,2026-01-01 kv2748_st_iv_2,l10n_at_hr_payroll_private.kv2748_grp_iv,2,nach 2 VWGJ,3654.05,2026-01-01,2
kv2748_st_iv_3,l10n_at_hr_payroll_private.kv2748_grp_iv,3,nach 4 VWGJ,3893.09,2026-01-01 kv2748_st_iv_3,l10n_at_hr_payroll_private.kv2748_grp_iv,3,nach 4 VWGJ,3893.09,2026-01-01,4
kv2748_st_iv_4,l10n_at_hr_payroll_private.kv2748_grp_iv,4,nach 6 VWGJ,4132.15,2026-01-01 kv2748_st_iv_4,l10n_at_hr_payroll_private.kv2748_grp_iv,4,nach 6 VWGJ,4132.15,2026-01-01,6
kv2748_st_iv_5,l10n_at_hr_payroll_private.kv2748_grp_iv,5,nach 8 VWGJ,4371.19,2026-01-01 kv2748_st_iv_5,l10n_at_hr_payroll_private.kv2748_grp_iv,5,nach 8 VWGJ,4371.19,2026-01-01,8
kv2748_st_iv_6,l10n_at_hr_payroll_private.kv2748_grp_iv,6,nach 10 VWGJ,4610.26,2026-01-01 kv2748_st_iv_6,l10n_at_hr_payroll_private.kv2748_grp_iv,6,nach 10 VWGJ,4610.26,2026-01-01,10
kv2748_st_iv_7,l10n_at_hr_payroll_private.kv2748_grp_iv,7,nach 12 VWGJ,4815.15,2026-01-01 kv2748_st_iv_7,l10n_at_hr_payroll_private.kv2748_grp_iv,7,nach 12 VWGJ,4815.15,2026-01-01,12
kv2748_st_iv_8,l10n_at_hr_payroll_private.kv2748_grp_iv,8,nach 15 VWGJ,5190.77,2026-01-01 kv2748_st_iv_8,l10n_at_hr_payroll_private.kv2748_grp_iv,8,nach 15 VWGJ,5190.77,2026-01-01,15
kv2748_st_ivm_1,l10n_at_hr_payroll_private.kv2748_grp_ivm,1,im 1.-4. VWGJ,3415.01,2026-01-01 kv2748_st_ivm_1,l10n_at_hr_payroll_private.kv2748_grp_ivm,1,im 1.-4. VWGJ,3415.01,2026-01-01,0
kv2748_st_ivm_2,l10n_at_hr_payroll_private.kv2748_grp_ivm,2,nach 4 VWGJ,3654.05,2026-01-01 kv2748_st_ivm_2,l10n_at_hr_payroll_private.kv2748_grp_ivm,2,nach 4 VWGJ,3654.05,2026-01-01,4
kv2748_st_ivm_3,l10n_at_hr_payroll_private.kv2748_grp_ivm,3,nach 6 VWGJ,3893.09,2026-01-01 kv2748_st_ivm_3,l10n_at_hr_payroll_private.kv2748_grp_ivm,3,nach 6 VWGJ,3893.09,2026-01-01,6
kv2748_st_ivm_4,l10n_at_hr_payroll_private.kv2748_grp_ivm,4,nach 8 VWGJ,4132.15,2026-01-01 kv2748_st_ivm_4,l10n_at_hr_payroll_private.kv2748_grp_ivm,4,nach 8 VWGJ,4132.15,2026-01-01,8
kv2748_st_ivm_5,l10n_at_hr_payroll_private.kv2748_grp_ivm,5,nach 10 VWGJ,4371.19,2026-01-01 kv2748_st_ivm_5,l10n_at_hr_payroll_private.kv2748_grp_ivm,5,nach 10 VWGJ,4371.19,2026-01-01,10
kv2748_st_ivm_6,l10n_at_hr_payroll_private.kv2748_grp_ivm,6,nach 12 VWGJ,4610.26,2026-01-01 kv2748_st_ivm_6,l10n_at_hr_payroll_private.kv2748_grp_ivm,6,nach 12 VWGJ,4610.26,2026-01-01,12
kv2748_st_ivm_7,l10n_at_hr_payroll_private.kv2748_grp_ivm,7,nach 14 VWGJ,4815.15,2026-01-01 kv2748_st_ivm_7,l10n_at_hr_payroll_private.kv2748_grp_ivm,7,nach 14 VWGJ,4815.15,2026-01-01,14
kv2748_st_ivm_8,l10n_at_hr_payroll_private.kv2748_grp_ivm,8,nach 17 VWGJ,5047.10,2026-01-01 kv2748_st_ivm_8,l10n_at_hr_payroll_private.kv2748_grp_ivm,8,nach 17 VWGJ,5047.10,2026-01-01,17
kv2748_st_v_1,l10n_at_hr_payroll_private.kv2748_grp_v,1,im 1. u 2. VWGJ,4299.70,2026-01-01 kv2748_st_v_1,l10n_at_hr_payroll_private.kv2748_grp_v,1,im 1. u 2. VWGJ,4299.70,2026-01-01,0
kv2748_st_v_2,l10n_at_hr_payroll_private.kv2748_grp_v,2,nach 2 VWGJ,4594.04,2026-01-01 kv2748_st_v_2,l10n_at_hr_payroll_private.kv2748_grp_v,2,nach 2 VWGJ,4594.04,2026-01-01,2
kv2748_st_v_3,l10n_at_hr_payroll_private.kv2748_grp_v,3,nach 4 VWGJ,4888.37,2026-01-01 kv2748_st_v_3,l10n_at_hr_payroll_private.kv2748_grp_v,3,nach 4 VWGJ,4888.37,2026-01-01,4
kv2748_st_v_4,l10n_at_hr_payroll_private.kv2748_grp_v,4,nach 6 VWGJ,5182.71,2026-01-01 kv2748_st_v_4,l10n_at_hr_payroll_private.kv2748_grp_v,4,nach 6 VWGJ,5182.71,2026-01-01,6
kv2748_st_v_5,l10n_at_hr_payroll_private.kv2748_grp_v,5,nach 8 VWGJ,5477.00,2026-01-01 kv2748_st_v_5,l10n_at_hr_payroll_private.kv2748_grp_v,5,nach 8 VWGJ,5477.00,2026-01-01,8
kv2748_st_v_6,l10n_at_hr_payroll_private.kv2748_grp_v,6,nach 10 VWGJ,5771.34,2026-01-01 kv2748_st_v_6,l10n_at_hr_payroll_private.kv2748_grp_v,6,nach 10 VWGJ,5771.34,2026-01-01,10
kv2748_st_v_7,l10n_at_hr_payroll_private.kv2748_grp_v,7,nach 12 VWGJ,6023.65,2026-01-01 kv2748_st_v_7,l10n_at_hr_payroll_private.kv2748_grp_v,7,nach 12 VWGJ,6023.65,2026-01-01,12
kv2748_st_v_8,l10n_at_hr_payroll_private.kv2748_grp_v,8,nach 15 VWGJ,6486.15,2026-01-01 kv2748_st_v_8,l10n_at_hr_payroll_private.kv2748_grp_v,8,nach 15 VWGJ,6486.15,2026-01-01,15
kv2748_st_vom_1,l10n_at_hr_payroll_private.kv2748_grp_vom,1,im 1.-6. VWGJ,4299.70,2026-01-01 kv2748_st_vom_1,l10n_at_hr_payroll_private.kv2748_grp_vom,1,im 1.-6. VWGJ,4299.70,2026-01-01,0
kv2748_st_vom_2,l10n_at_hr_payroll_private.kv2748_grp_vom,2,nach 6 VWGJ,4594.04,2026-01-01 kv2748_st_vom_2,l10n_at_hr_payroll_private.kv2748_grp_vom,2,nach 6 VWGJ,4594.04,2026-01-01,6
kv2748_st_vom_3,l10n_at_hr_payroll_private.kv2748_grp_vom,3,nach 8 VWGJ,4888.37,2026-01-01 kv2748_st_vom_3,l10n_at_hr_payroll_private.kv2748_grp_vom,3,nach 8 VWGJ,4888.37,2026-01-01,8
kv2748_st_vom_4,l10n_at_hr_payroll_private.kv2748_grp_vom,4,nach 10 VWGJ,5182.71,2026-01-01 kv2748_st_vom_4,l10n_at_hr_payroll_private.kv2748_grp_vom,4,nach 10 VWGJ,5182.71,2026-01-01,10
kv2748_st_vom_5,l10n_at_hr_payroll_private.kv2748_grp_vom,5,nach 12 VWGJ,5477.00,2026-01-01 kv2748_st_vom_5,l10n_at_hr_payroll_private.kv2748_grp_vom,5,nach 12 VWGJ,5477.00,2026-01-01,12
kv2748_st_vi_1,l10n_at_hr_payroll_private.kv2748_grp_vi,1,im 1. u 2. VWGJ,6021.80,2026-01-01 kv2748_st_vi_1,l10n_at_hr_payroll_private.kv2748_grp_vi,1,im 1. u 2. VWGJ,6021.80,2026-01-01,0
kv2748_st_vi_2,l10n_at_hr_payroll_private.kv2748_grp_vi,2,nach 2 VWGJ,6762.62,2026-01-01 kv2748_st_vi_2,l10n_at_hr_payroll_private.kv2748_grp_vi,2,nach 2 VWGJ,6762.62,2026-01-01,2
kv2748_st_vi_3,l10n_at_hr_payroll_private.kv2748_grp_vi,3,nach 5 VWGJ,7503.50,2026-01-01 kv2748_st_vi_3,l10n_at_hr_payroll_private.kv2748_grp_vi,3,nach 5 VWGJ,7503.50,2026-01-01,5
kv2748_st_meister_1,l10n_at_hr_payroll_private.kv2748_grp_meister,1,im 1. u 2. VWGJ,4130.76,2026-01-01 kv2748_st_meister_1,l10n_at_hr_payroll_private.kv2748_grp_meister,1,im 1. u 2. VWGJ,4130.76,2026-01-01,0
kv2748_st_meister_2,l10n_at_hr_payroll_private.kv2748_grp_meister,2,nach 2 VWGJ,4130.76,2026-01-01 kv2748_st_meister_2,l10n_at_hr_payroll_private.kv2748_grp_meister,2,nach 2 VWGJ,4130.76,2026-01-01,2
kv2748_st_meister_3,l10n_at_hr_payroll_private.kv2748_grp_meister,3,nach 4 VWGJ,4303.13,2026-01-01 kv2748_st_meister_3,l10n_at_hr_payroll_private.kv2748_grp_meister,3,nach 4 VWGJ,4303.13,2026-01-01,4
kv2748_st_meister_4,l10n_at_hr_payroll_private.kv2748_grp_meister,4,nach 6 VWGJ,4475.48,2026-01-01 kv2748_st_meister_4,l10n_at_hr_payroll_private.kv2748_grp_meister,4,nach 6 VWGJ,4475.48,2026-01-01,6
kv2748_st_meister_5,l10n_at_hr_payroll_private.kv2748_grp_meister,5,nach 8 VWGJ,4647.84,2026-01-01 kv2748_st_meister_5,l10n_at_hr_payroll_private.kv2748_grp_meister,5,nach 8 VWGJ,4647.84,2026-01-01,8
kv2748_st_meister_6,l10n_at_hr_payroll_private.kv2748_grp_meister,6,nach 10 VWGJ,4820.17,2026-01-01 kv2748_st_meister_6,l10n_at_hr_payroll_private.kv2748_grp_meister,6,nach 10 VWGJ,4820.17,2026-01-01,10
kv2748_st_meister_7,l10n_at_hr_payroll_private.kv2748_grp_meister,7,nach 12 VWGJ,4992.53,2026-01-01 kv2748_st_meister_7,l10n_at_hr_payroll_private.kv2748_grp_meister,7,nach 12 VWGJ,4992.53,2026-01-01,12
kv2748_st_meister_8,l10n_at_hr_payroll_private.kv2748_grp_meister,8,nach 15 VWGJ,5270.29,2026-01-01 kv2748_st_meister_8,l10n_at_hr_payroll_private.kv2748_grp_meister,8,nach 15 VWGJ,5270.29,2026-01-01,15
1 id gruppe_id:id stufe bezeichnung gehalt gueltig_ab vwgj_ab
2 kv2203_st_ii_1 l10n_at_hr_payroll_private.kv2203_grp_ii 1 im 1. u 2. VWGJ 2059.41 2026-01-01 0
3 kv2203_st_ii_2 l10n_at_hr_payroll_private.kv2203_grp_ii 2 nach 2 VWGJ 2179.68 2026-01-01 2
4 kv2203_st_ii_3 l10n_at_hr_payroll_private.kv2203_grp_ii 3 nach 4 VWGJ 2315.46 2026-01-01 4
5 kv2203_st_ii_4 l10n_at_hr_payroll_private.kv2203_grp_ii 4 nach 6 VWGJ 2457.65 2026-01-01 6
6 kv2203_st_ii_5 l10n_at_hr_payroll_private.kv2203_grp_ii 5 nach 8 VWGJ 2599.83 2026-01-01 8
7 kv2203_st_ii_6 l10n_at_hr_payroll_private.kv2203_grp_ii 6 nach 10 VWGJ 2742.03 2026-01-01 10
8 kv2203_st_ii_7 l10n_at_hr_payroll_private.kv2203_grp_ii 7 nach 12 VWGJ 2863.88 2026-01-01 12
9 kv2203_st_ii_8 l10n_at_hr_payroll_private.kv2203_grp_ii 8 nach 15 VWGJ 3087.28 2026-01-01 15
10 kv2203_st_iii_1 l10n_at_hr_payroll_private.kv2203_grp_iii 1 im 1. u 2. VWGJ 2528.04 2026-01-01 0
11 kv2203_st_iii_2 l10n_at_hr_payroll_private.kv2203_grp_iii 2 nach 2 VWGJ 2705.03 2026-01-01 2
12 kv2203_st_iii_3 l10n_at_hr_payroll_private.kv2203_grp_iii 3 nach 4 VWGJ 2881.97 2026-01-01 4
13 kv2203_st_iii_4 l10n_at_hr_payroll_private.kv2203_grp_iii 4 nach 6 VWGJ 3058.96 2026-01-01 6
14 kv2203_st_iii_5 l10n_at_hr_payroll_private.kv2203_grp_iii 5 nach 8 VWGJ 3232.73 2026-01-01 8
15 kv2203_st_iii_6 l10n_at_hr_payroll_private.kv2203_grp_iii 6 nach 10 VWGJ 3409.53 2026-01-01 10
16 kv2203_st_iii_7 l10n_at_hr_payroll_private.kv2203_grp_iii 7 nach 12 VWGJ 3561.04 2026-01-01 12
17 kv2203_st_iii_8 l10n_at_hr_payroll_private.kv2203_grp_iii 8 nach 15 VWGJ 3838.85 2026-01-01 15
18 kv2203_st_iv_1 l10n_at_hr_payroll_private.kv2203_grp_iv 1 im 1. u 2. VWGJ 3146.27 2026-01-01 0
19 kv2203_st_iv_2 l10n_at_hr_payroll_private.kv2203_grp_iv 2 nach 2 VWGJ 3366.49 2026-01-01 2
20 kv2203_st_iv_3 l10n_at_hr_payroll_private.kv2203_grp_iv 3 nach 4 VWGJ 3586.73 2026-01-01 4
21 kv2203_st_iv_4 l10n_at_hr_payroll_private.kv2203_grp_iv 4 nach 6 VWGJ 3806.97 2026-01-01 6
22 kv2203_st_iv_5 l10n_at_hr_payroll_private.kv2203_grp_iv 5 nach 8 VWGJ 4027.20 2026-01-01 8
23 kv2203_st_iv_6 l10n_at_hr_payroll_private.kv2203_grp_iv 6 nach 10 VWGJ 4247.47 2026-01-01 10
24 kv2203_st_iv_7 l10n_at_hr_payroll_private.kv2203_grp_iv 7 nach 12 VWGJ 4436.22 2026-01-01 12
25 kv2203_st_iv_8 l10n_at_hr_payroll_private.kv2203_grp_iv 8 nach 15 VWGJ 4782.32 2026-01-01 15
26 kv2203_st_v_1 l10n_at_hr_payroll_private.kv2203_grp_v 1 im 1. u 2. VWGJ 3919.67 2026-01-01 0
27 kv2203_st_v_2 l10n_at_hr_payroll_private.kv2203_grp_v 2 nach 2 VWGJ 4194.02 2026-01-01 2
28 kv2203_st_v_3 l10n_at_hr_payroll_private.kv2203_grp_v 3 nach 4 VWGJ 4468.41 2026-01-01 4
29 kv2203_st_v_4 l10n_at_hr_payroll_private.kv2203_grp_v 4 nach 6 VWGJ 4742.77 2026-01-01 6
30 kv2203_st_v_5 l10n_at_hr_payroll_private.kv2203_grp_v 5 nach 8 VWGJ 5017.15 2026-01-01 8
31 kv2203_st_v_6 l10n_at_hr_payroll_private.kv2203_grp_v 6 nach 10 VWGJ 5291.57 2026-01-01 10
32 kv2203_st_v_7 l10n_at_hr_payroll_private.kv2203_grp_v 7 nach 12 VWGJ 5526.70 2026-01-01 12
33 kv2203_st_v_8 l10n_at_hr_payroll_private.kv2203_grp_v 8 nach 15 VWGJ 5957.86 2026-01-01 15
34 kv2203_st_vi_1 l10n_at_hr_payroll_private.kv2203_grp_vi 1 im 1. u 2. VWGJ 5482.40 2026-01-01 0
35 kv2203_st_vi_2 l10n_at_hr_payroll_private.kv2203_grp_vi 2 nach 2 VWGJ 6167.67 2026-01-01 2
36 kv2203_st_vi_3 l10n_at_hr_payroll_private.kv2203_grp_vi 3 nach 5 VWGJ 6853.01 2026-01-01 5
37 kv2203_st_m1_1 l10n_at_hr_payroll_private.kv2203_grp_m1 1 im 1. u 2. VWGJ 2427.72 2026-01-01 0
38 kv2203_st_m1_2 l10n_at_hr_payroll_private.kv2203_grp_m1 2 nach 2 VWGJ 2427.72 2026-01-01 2
39 kv2203_st_m1_3 l10n_at_hr_payroll_private.kv2203_grp_m1 3 nach 4 VWGJ 2586.57 2026-01-01 4
40 kv2203_st_m1_4 l10n_at_hr_payroll_private.kv2203_grp_m1 4 nach 6 VWGJ 2745.39 2026-01-01 6
41 kv2203_st_m1_5 l10n_at_hr_payroll_private.kv2203_grp_m1 5 nach 8 VWGJ 2904.19 2026-01-01 8
42 kv2203_st_m1_6 l10n_at_hr_payroll_private.kv2203_grp_m1 6 nach 10 VWGJ 3063.05 2026-01-01 10
43 kv2203_st_m1_7 l10n_at_hr_payroll_private.kv2203_grp_m1 7 nach 12 VWGJ 3199.16 2026-01-01 12
44 kv2203_st_m1_8 l10n_at_hr_payroll_private.kv2203_grp_m1 8 nach 15 VWGJ 3448.74 2026-01-01 15
45 kv2203_st_m2_1 l10n_at_hr_payroll_private.kv2203_grp_m2 1 im 1. u 2. VWGJ 3095.50 2026-01-01 0
46 kv2203_st_m2_2 l10n_at_hr_payroll_private.kv2203_grp_m2 2 nach 2 VWGJ 3095.50 2026-01-01 2
47 kv2203_st_m2_3 l10n_at_hr_payroll_private.kv2203_grp_m2 3 nach 4 VWGJ 3297.98 2026-01-01 4
48 kv2203_st_m2_4 l10n_at_hr_payroll_private.kv2203_grp_m2 4 nach 6 VWGJ 3500.52 2026-01-01 6
49 kv2203_st_m2_5 l10n_at_hr_payroll_private.kv2203_grp_m2 5 nach 8 VWGJ 3703.01 2026-01-01 8
50 kv2203_st_m2_6 l10n_at_hr_payroll_private.kv2203_grp_m2 6 nach 10 VWGJ 3905.53 2026-01-01 10
51 kv2203_st_m2_7 l10n_at_hr_payroll_private.kv2203_grp_m2 7 nach 12 VWGJ 4079.13 2026-01-01 12
52 kv2203_st_m2_8 l10n_at_hr_payroll_private.kv2203_grp_m2 8 nach 15 VWGJ 4397.37 2026-01-01 15
53 kv2203_st_m2fs_1 l10n_at_hr_payroll_private.kv2203_grp_m2fs 1 im 1. u 2. VWGJ 3241.68 2026-01-01 0
54 kv2203_st_m2fs_2 l10n_at_hr_payroll_private.kv2203_grp_m2fs 2 nach 2 VWGJ 3241.68 2026-01-01 2
55 kv2203_st_m2fs_3 l10n_at_hr_payroll_private.kv2203_grp_m2fs 3 nach 4 VWGJ 3453.74 2026-01-01 4
56 kv2203_st_m2fs_4 l10n_at_hr_payroll_private.kv2203_grp_m2fs 4 nach 6 VWGJ 3665.79 2026-01-01 6
57 kv2203_st_m2fs_5 l10n_at_hr_payroll_private.kv2203_grp_m2fs 5 nach 8 VWGJ 3877.83 2026-01-01 8
58 kv2203_st_m2fs_6 l10n_at_hr_payroll_private.kv2203_grp_m2fs 6 nach 10 VWGJ 4089.90 2026-01-01 10
59 kv2203_st_m2fs_7 l10n_at_hr_payroll_private.kv2203_grp_m2fs 7 nach 12 VWGJ 4271.68 2026-01-01 12
60 kv2203_st_m2fs_8 l10n_at_hr_payroll_private.kv2203_grp_m2fs 8 nach 15 VWGJ 4605.01 2026-01-01 15
61 kv2203_st_m3_1 l10n_at_hr_payroll_private.kv2203_grp_m3 1 im 1. u 2. VWGJ 3542.72 2026-01-01 0
62 kv2203_st_m3_2 l10n_at_hr_payroll_private.kv2203_grp_m3 2 nach 2 VWGJ 3542.72 2026-01-01 2
63 kv2203_st_m3_3 l10n_at_hr_payroll_private.kv2203_grp_m3 3 nach 4 VWGJ 3774.51 2026-01-01 4
64 kv2203_st_m3_4 l10n_at_hr_payroll_private.kv2203_grp_m3 4 nach 6 VWGJ 4006.27 2026-01-01 6
65 kv2203_st_m3_5 l10n_at_hr_payroll_private.kv2203_grp_m3 5 nach 8 VWGJ 4238.05 2026-01-01 8
66 kv2203_st_m3_6 l10n_at_hr_payroll_private.kv2203_grp_m3 6 nach 10 VWGJ 4469.81 2026-01-01 10
67 kv2203_st_m3_7 l10n_at_hr_payroll_private.kv2203_grp_m3 7 nach 12 VWGJ 4668.45 2026-01-01 12
68 kv2203_st_m3_8 l10n_at_hr_payroll_private.kv2203_grp_m3 8 nach 15 VWGJ 5032.68 2026-01-01 15
69 kv2748_st_i_1 l10n_at_hr_payroll_private.kv2748_grp_i 1 im 1. u 2. VWGJ 1960.54 2026-01-01 0
70 kv2748_st_i_2 l10n_at_hr_payroll_private.kv2748_grp_i 2 nach 2 VWGJ 2058.43 2026-01-01 2
71 kv2748_st_i_3 l10n_at_hr_payroll_private.kv2748_grp_i 3 nach 4 VWGJ 2190.76 2026-01-01 4
72 kv2748_st_i_4 l10n_at_hr_payroll_private.kv2748_grp_i 4 nach 6 VWGJ 2323.12 2026-01-01 6
73 kv2748_st_i_5 l10n_at_hr_payroll_private.kv2748_grp_i 5 nach 8 VWGJ 2455.45 2026-01-01 8
74 kv2748_st_i_6 l10n_at_hr_payroll_private.kv2748_grp_i 6 nach 10 VWGJ 2588.72 2026-01-01 10
75 kv2748_st_i_7 l10n_at_hr_payroll_private.kv2748_grp_i 7 nach 12 VWGJ 2703.75 2026-01-01 12
76 kv2748_st_i_8 l10n_at_hr_payroll_private.kv2748_grp_i 8 nach 15 VWGJ 2914.69 2026-01-01 15
77 kv2748_st_ii_1 l10n_at_hr_payroll_private.kv2748_grp_ii 1 im 1. u 2. VWGJ 2199.15 2026-01-01 0
78 kv2748_st_ii_2 l10n_at_hr_payroll_private.kv2748_grp_ii 2 nach 2 VWGJ 2350.62 2026-01-01 2
79 kv2748_st_ii_3 l10n_at_hr_payroll_private.kv2748_grp_ii 3 nach 4 VWGJ 2502.07 2026-01-01 4
80 kv2748_st_ii_4 l10n_at_hr_payroll_private.kv2748_grp_ii 4 nach 6 VWGJ 2655.45 2026-01-01 6
81 kv2748_st_ii_5 l10n_at_hr_payroll_private.kv2748_grp_ii 5 nach 8 VWGJ 2809.08 2026-01-01 8
82 kv2748_st_ii_6 l10n_at_hr_payroll_private.kv2748_grp_ii 6 nach 10 VWGJ 2962.72 2026-01-01 10
83 kv2748_st_ii_7 l10n_at_hr_payroll_private.kv2748_grp_ii 7 nach 12 VWGJ 3094.32 2026-01-01 12
84 kv2748_st_ii_8 l10n_at_hr_payroll_private.kv2748_grp_ii 8 nach 15 VWGJ 3335.74 2026-01-01 15
85 kv2748_st_iii_1 l10n_at_hr_payroll_private.kv2748_grp_iii 1 im 1. u 2. VWGJ 2727.40 2026-01-01 0
86 kv2748_st_iii_2 l10n_at_hr_payroll_private.kv2748_grp_iii 2 nach 2 VWGJ 2918.34 2026-01-01 2
87 kv2748_st_iii_3 l10n_at_hr_payroll_private.kv2748_grp_iii 3 nach 4 VWGJ 3109.21 2026-01-01 4
88 kv2748_st_iii_4 l10n_at_hr_payroll_private.kv2748_grp_iii 4 nach 6 VWGJ 3300.16 2026-01-01 6
89 kv2748_st_iii_5 l10n_at_hr_payroll_private.kv2748_grp_iii 5 nach 8 VWGJ 3491.06 2026-01-01 8
90 kv2748_st_iii_6 l10n_at_hr_payroll_private.kv2748_grp_iii 6 nach 10 VWGJ 3682.00 2026-01-01 10
91 kv2748_st_iii_7 l10n_at_hr_payroll_private.kv2748_grp_iii 7 nach 12 VWGJ 3845.61 2026-01-01 12
92 kv2748_st_iii_8 l10n_at_hr_payroll_private.kv2748_grp_iii 8 nach 15 VWGJ 4145.62 2026-01-01 15
93 kv2748_st_iv_1 l10n_at_hr_payroll_private.kv2748_grp_iv 1 im 1. u 2. VWGJ 3415.01 2026-01-01 0
94 kv2748_st_iv_2 l10n_at_hr_payroll_private.kv2748_grp_iv 2 nach 2 VWGJ 3654.05 2026-01-01 2
95 kv2748_st_iv_3 l10n_at_hr_payroll_private.kv2748_grp_iv 3 nach 4 VWGJ 3893.09 2026-01-01 4
96 kv2748_st_iv_4 l10n_at_hr_payroll_private.kv2748_grp_iv 4 nach 6 VWGJ 4132.15 2026-01-01 6
97 kv2748_st_iv_5 l10n_at_hr_payroll_private.kv2748_grp_iv 5 nach 8 VWGJ 4371.19 2026-01-01 8
98 kv2748_st_iv_6 l10n_at_hr_payroll_private.kv2748_grp_iv 6 nach 10 VWGJ 4610.26 2026-01-01 10
99 kv2748_st_iv_7 l10n_at_hr_payroll_private.kv2748_grp_iv 7 nach 12 VWGJ 4815.15 2026-01-01 12
100 kv2748_st_iv_8 l10n_at_hr_payroll_private.kv2748_grp_iv 8 nach 15 VWGJ 5190.77 2026-01-01 15
101 kv2748_st_ivm_1 l10n_at_hr_payroll_private.kv2748_grp_ivm 1 im 1.-4. VWGJ 3415.01 2026-01-01 0
102 kv2748_st_ivm_2 l10n_at_hr_payroll_private.kv2748_grp_ivm 2 nach 4 VWGJ 3654.05 2026-01-01 4
103 kv2748_st_ivm_3 l10n_at_hr_payroll_private.kv2748_grp_ivm 3 nach 6 VWGJ 3893.09 2026-01-01 6
104 kv2748_st_ivm_4 l10n_at_hr_payroll_private.kv2748_grp_ivm 4 nach 8 VWGJ 4132.15 2026-01-01 8
105 kv2748_st_ivm_5 l10n_at_hr_payroll_private.kv2748_grp_ivm 5 nach 10 VWGJ 4371.19 2026-01-01 10
106 kv2748_st_ivm_6 l10n_at_hr_payroll_private.kv2748_grp_ivm 6 nach 12 VWGJ 4610.26 2026-01-01 12
107 kv2748_st_ivm_7 l10n_at_hr_payroll_private.kv2748_grp_ivm 7 nach 14 VWGJ 4815.15 2026-01-01 14
108 kv2748_st_ivm_8 l10n_at_hr_payroll_private.kv2748_grp_ivm 8 nach 17 VWGJ 5047.10 2026-01-01 17
109 kv2748_st_v_1 l10n_at_hr_payroll_private.kv2748_grp_v 1 im 1. u 2. VWGJ 4299.70 2026-01-01 0
110 kv2748_st_v_2 l10n_at_hr_payroll_private.kv2748_grp_v 2 nach 2 VWGJ 4594.04 2026-01-01 2
111 kv2748_st_v_3 l10n_at_hr_payroll_private.kv2748_grp_v 3 nach 4 VWGJ 4888.37 2026-01-01 4
112 kv2748_st_v_4 l10n_at_hr_payroll_private.kv2748_grp_v 4 nach 6 VWGJ 5182.71 2026-01-01 6
113 kv2748_st_v_5 l10n_at_hr_payroll_private.kv2748_grp_v 5 nach 8 VWGJ 5477.00 2026-01-01 8
114 kv2748_st_v_6 l10n_at_hr_payroll_private.kv2748_grp_v 6 nach 10 VWGJ 5771.34 2026-01-01 10
115 kv2748_st_v_7 l10n_at_hr_payroll_private.kv2748_grp_v 7 nach 12 VWGJ 6023.65 2026-01-01 12
116 kv2748_st_v_8 l10n_at_hr_payroll_private.kv2748_grp_v 8 nach 15 VWGJ 6486.15 2026-01-01 15
117 kv2748_st_vom_1 l10n_at_hr_payroll_private.kv2748_grp_vom 1 im 1.-6. VWGJ 4299.70 2026-01-01 0
118 kv2748_st_vom_2 l10n_at_hr_payroll_private.kv2748_grp_vom 2 nach 6 VWGJ 4594.04 2026-01-01 6
119 kv2748_st_vom_3 l10n_at_hr_payroll_private.kv2748_grp_vom 3 nach 8 VWGJ 4888.37 2026-01-01 8
120 kv2748_st_vom_4 l10n_at_hr_payroll_private.kv2748_grp_vom 4 nach 10 VWGJ 5182.71 2026-01-01 10
121 kv2748_st_vom_5 l10n_at_hr_payroll_private.kv2748_grp_vom 5 nach 12 VWGJ 5477.00 2026-01-01 12
122 kv2748_st_vi_1 l10n_at_hr_payroll_private.kv2748_grp_vi 1 im 1. u 2. VWGJ 6021.80 2026-01-01 0
123 kv2748_st_vi_2 l10n_at_hr_payroll_private.kv2748_grp_vi 2 nach 2 VWGJ 6762.62 2026-01-01 2
124 kv2748_st_vi_3 l10n_at_hr_payroll_private.kv2748_grp_vi 3 nach 5 VWGJ 7503.50 2026-01-01 5
125 kv2748_st_meister_1 l10n_at_hr_payroll_private.kv2748_grp_meister 1 im 1. u 2. VWGJ 4130.76 2026-01-01 0
126 kv2748_st_meister_2 l10n_at_hr_payroll_private.kv2748_grp_meister 2 nach 2 VWGJ 4130.76 2026-01-01 2
127 kv2748_st_meister_3 l10n_at_hr_payroll_private.kv2748_grp_meister 3 nach 4 VWGJ 4303.13 2026-01-01 4
128 kv2748_st_meister_4 l10n_at_hr_payroll_private.kv2748_grp_meister 4 nach 6 VWGJ 4475.48 2026-01-01 6
129 kv2748_st_meister_5 l10n_at_hr_payroll_private.kv2748_grp_meister 5 nach 8 VWGJ 4647.84 2026-01-01 8
130 kv2748_st_meister_6 l10n_at_hr_payroll_private.kv2748_grp_meister 6 nach 10 VWGJ 4820.17 2026-01-01 10
131 kv2748_st_meister_7 l10n_at_hr_payroll_private.kv2748_grp_meister 7 nach 12 VWGJ 4992.53 2026-01-01 12
132 kv2748_st_meister_8 l10n_at_hr_payroll_private.kv2748_grp_meister 8 nach 15 VWGJ 5270.29 2026-01-01 15
@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- AP4: EFZ-Stufen — AngG § 8 Abs 1 / EFZG § 2 Abs 1 (Gleichlauf -->
<!-- BGBl I 153/2017; EFZG RIS GesNr 10008308, AngG lokal — beide -->
<!-- verifiziert 2026-09-12): 6/8/10/12 Wochen je vollendete -->
<!-- Dienstjahre 0/1/15/25; zuzüglich je 4 Wochen halbes Entgelt. -->
<!-- AU/BK-Sonderkontingent § 8 Abs 2a AngG / § 2 Abs 5 EFZG: -->
<!-- 8 Wochen, ab 15 Dienstjahren 10 Wochen — ohne Halbsatz, je -->
<!-- Anlassfall. KV/BV-Kalenderjahr-Umstellung (§ 8 Abs 9 AngG) = -->
<!-- dokumentierte Option, hier nicht abgebildet. -->
<record id="hr_rule_parameter_at_efz_stufen" model="hr.rule.parameter">
<field name="name">EFZ-Stufen Krankenstand (AngG § 8 / EFZG § 2)</field>
<field name="code">at_efz_stufen</field>
<field name="country_id" ref="base.at"/>
</record>
<record id="hr_rule_parameter_value_at_efz_stufen"
model="hr.rule.parameter.value">
<field name="rule_parameter_id"
ref="hr_rule_parameter_at_efz_stufen"/>
<field name="date_from">2018-07-01</field>
<field name="parameter_value">{'stufen': [[0, 6], [1, 8], [15, 10], [25, 12]], 'halb_wochen': 4, 'au_wochen': 8, 'au2_wochen': 10, 'au_ab_jahre': 15}</field>
</record>
</odoo>
@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- AP14-A: Endabrechnungs-Parameter (lb-url-16 Tagewert-Divisoren;
§-10-Aliquotierung läuft über die Urlaubstage-Eingabe, der
Fünftel-Deckel dynamisch aus 9 × at_sv_hbg_monatlich / 5). -->
<record id="hr_rule_parameter_at_endabrechnung" model="hr.rule.parameter">
<field name="name">Endabrechnung — UEL-Tagewert-Divisoren</field>
<field name="code">at_endabrechnung</field>
<field name="country_id" ref="base.at"/>
</record>
<record id="hr_rule_parameter_value_at_endabrechnung"
model="hr.rule.parameter.value">
<field name="rule_parameter_id" ref="hr_rule_parameter_at_endabrechnung"/>
<field name="date_from">2026-01-01</field>
<field name="parameter_value">{'divisor_werktage': 26.0, 'divisor_arbeitstage': 22.0, 'abfertigung_alt_staffel': [[36, 2.0], [60, 3.0], [120, 4.0], [180, 6.0], [240, 9.0], [300, 12.0]], 'abfertigung_steuersatz': 6.0}</field>
</record>
<!-- AP15-C: §-67-Abs-8-lit-a-BV-Deckel; § 67 Abs 8 unterliegt der
Inflationsanpassung (§ 33a/§ 124b EStG), daher Parameter. -->
<record id="hr_rule_parameter_at_lst_67_abs8_vergleich_bv" model="hr.rule.parameter">
<field name="name">§ 67 Abs 8 lit a — Vergleich BV-Anwartschafts-Teil Deckel</field>
<field name="code">at_lst_67_abs8_vergleich_bv</field>
<field name="country_id" ref="base.at"/>
</record>
<record id="hr_rule_parameter_value_at_lst_67_abs8_vergleich_bv" model="hr.rule.parameter.value">
<field name="rule_parameter_id" ref="hr_rule_parameter_at_lst_67_abs8_vergleich_bv"/>
<field name="date_from">2026-01-01</field>
<field name="parameter_value">7500.0</field>
</record>
</odoo>
@@ -0,0 +1,40 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- GP7: mBGM-Tarifsätze (Standardposition T01/T02 = GESAMT- -->
<!-- Prozentsatz je Beschäftigtengruppe aus dem ÖGK- -->
<!-- Beitragstarifsystem „Normalbetriebe Allgemeiner Teil", -->
<!-- gültig ab 01.07.2026 (.additional_docs/oegk/, Primär- -->
<!-- quelle GP4). NORMATIVE Sätze (Abschläge wie A03 laufen als -->
<!-- eigene Verrechnungspositionen): laufend = DN 18,07 + DG -->
<!-- 20,98 = 39,30 AK/… exakt: KV 7,65 + PV 22,80 + AV 5,90 + -->
<!-- UV 1,10 + WF 1,00 + IESG 0,10 = 39,05 %; SZ = KV 3,87 + -->
<!-- PV 10,25 + AV 2,95 (DN; § 67 Abs 12) + DG 20,48 (KV 3,78 + -->
<!-- PV 12,55 + AV 2,95 + UV 1,10 + IESG 0,10) = 37,55 %. -->
<!-- NSchAB-Betriebe: + 3,80 % auf B001 laufend über das Company- -->
<!-- Flag l10n_at_nschab. Lehrlinge (B044/B045) und Geringfügige -->
<!-- (B010/B030) bewusst NICHT geseedet — Werte aus dem SV- -->
<!-- Tarifsystem pflegen, sobai solche Beschäftigten vorkommen. -->
<record id="hr_rule_parameter_at_mbgm_tarif" model="hr.rule.parameter">
<field name="name">mBGM-Tarifsätze je Beschäftigtengruppe (SV-Tarifsystem)</field>
<field name="code">at_mbgm_tarif</field>
<field name="country_id" ref="base.at"/>
</record>
<record id="hr_rule_parameter_value_at_mbgm_tarif_2026" model="hr.rule.parameter.value">
<field name="rule_parameter_id" ref="hr_rule_parameter_at_mbgm_tarif"/>
<field name="date_from">2026-01-01</field>
<field name="parameter_value">{'B001': {'laufend': 39.05, 'sz': 37.55}, 'B002': {'laufend': 39.05, 'sz': 37.55}}</field>
</record>
<!-- AP19: fiktive BG Präsenz-/Ausbildungs-/Zivildienst —
KBGG § 3 Abs 1 idF vor BGBl I 53/2016: 2026: 435,90 €
(lb-vor-04, Stand 2026-08); BMSVG § 7 Abs 1/2. -->
<record id="hr_rule_parameter_at_bv_fiktiv_kbg" model="hr.rule.parameter">
<field name="name">BMSVG § 7 — fiktive BG KBGG-Basis</field>
<field name="code">at_bv_fiktiv_kbg</field>
<field name="country_id" ref="base.at"/>
</record>
<record id="hr_rule_parameter_value_at_bv_fiktiv_kbg" model="hr.rule.parameter.value">
<field name="rule_parameter_id" ref="hr_rule_parameter_at_bv_fiktiv_kbg"/>
<field name="date_from">2026-01-01</field>
<field name="parameter_value">435.90</field>
</record>
</odoo>
@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- AP7: Satzungsmäßiger Sonderzahlungs-Prozentsatz des -->
<!-- Wochengeldes (§ 162 Abs 3 ASVG; lb-msf-03, Stand 2026-07: -->
<!-- 14 % / 17 % / 21 % je nach Ausmaß der Sonderzahlungen — -->
<!-- ⚠ satzungsabhängig, vor Erstverwendung mandantenseitig gegen -->
<!-- die Satzung des KV-Trägers verifizieren; hier 17 % als -->
<!-- mittlerer Wert, kein verifizierter Mandantenwert). -->
<record id="hr_rule_parameter_at_msch_sz_prozentsatz" model="hr.rule.parameter">
<field name="name">Wochengeld-Sonderzahlungsprozentsatz (§ 162 Abs 3 ASVG)</field>
<field name="code">at_msch_sz_prozentsatz</field>
<field name="country_id" ref="base.at"/>
</record>
<record id="hr_rule_parameter_value_at_msch_sz_prozentsatz"
model="hr.rule.parameter.value">
<field name="rule_parameter_id"
ref="hr_rule_parameter_at_msch_sz_prozentsatz"/>
<field name="date_from">2026-01-01</field>
<field name="parameter_value">17.0</field>
</record>
</odoo>
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- AP13-B: verifizierte Pendlerwerte 2026 (lb-pen-01/02/04/09, -->
<!-- Budgetbegleitgesetz 2025). Staffel: nächsthöhere Stufe erst bei -->
<!-- MEHR als dem unteren km-Wert (klein: 20/41/61, gross 2/21/41/61 -->
<!-- — km ganzzahlig aus dem Pendlerrechner/L 34). -->
<record id="hr_rule_parameter_at_pendler" model="hr.rule.parameter">
<field name="name">Pendlerpauschale / Pendlereuro</field>
<field name="code">at_pendler</field>
<field name="country_id" ref="base.at"/>
</record>
<record id="hr_rule_parameter_value_at_pendler_2026"
model="hr.rule.parameter.value">
<field name="rule_parameter_id" ref="hr_rule_parameter_at_pendler"/>
<field name="date_from">2026-01-01</field>
<field name="parameter_value">{'klein': [[20, 696.0], [41, 1356.0], [61, 2016.0]], 'gross': [[2, 372.0], [21, 1476.0], [41, 2568.0], [61, 3672.0]], 'pendlereuro_km_jahr': 6.0}</field>
</record>
</odoo>
@@ -0,0 +1,46 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- ============================================================ -->
<!-- AP11: Lohnpfändung — AZR-Werte (ASVG § 293 Abs 1 lit a -->
<!-- sublit bb) versioniert je Gültigkeitsjahr. Die EO-§-291a- -->
<!-- Grundbeträge (allg./erhöht/Unterhalt) und die HöBG werden -->
<!-- in der Engine aus dem AZR abgeleitet (KB-KONFLIKTE A9: -->
<!-- 2025 1.273,99 € BGBl II 417/2024; 2026 1.308,39 € BGBl II -->
<!-- 263/2025). Jährliche Anpassung über die Anm.-Reihe nach- -->
<!-- führen (Wartung, s. RUNBOOK). -->
<!-- ============================================================ -->
<record id="hr_rule_parameter_at_eo_azr" model="hr.rule.parameter">
<field name="name">EO Pfändung — Ausgleichszulagenrichtsatz (AZR)</field>
<field name="code">at_eo_azr</field>
<field name="country_id" ref="base.at"/>
</record>
<record id="hr_rule_parameter_value_at_eo_azr_2025"
model="hr.rule.parameter.value">
<field name="rule_parameter_id" ref="hr_rule_parameter_at_eo_azr"/>
<field name="date_from">2025-01-01</field>
<field name="parameter_value">{'azr': 1273.99}</field>
</record>
<record id="hr_rule_parameter_value_at_eo_azr_2026"
model="hr.rule.parameter.value">
<field name="rule_parameter_id" ref="hr_rule_parameter_at_eo_azr"/>
<field name="date_from">2026-01-01</field>
<field name="parameter_value">{'azr': 1308.39}</field>
</record>
<!-- Kostenersatz für Pfändungsdurchführung (§ 292h EO) und
Drittschuldnererklärung (§ 302 EO) — pauschaliert, lb-pfa-09.
USt auf den §-302-Kostenersatz: keine (VwGH 2011/15/0181). -->
<record id="hr_rule_parameter_at_eo_kostenersatz"
model="hr.rule.parameter">
<field name="name">EO Pfändung — Kostenersatz (§ 292h/§ 302)</field>
<field name="code">at_eo_kostenersatz</field>
<field name="country_id" ref="base.at"/>
</record>
<record id="hr_rule_parameter_value_at_eo_kostenersatz"
model="hr.rule.parameter.value">
<field name="rule_parameter_id"
ref="hr_rule_parameter_at_eo_kostenersatz"/>
<field name="date_from">2017-01-01</field>
<field name="parameter_value">{'erste_rate': 2.0, 'erste_max': 8.0, 'weitere_rate': 1.0, 'weitere_max': 4.0, 'dse_normal': 35.0, 'dse_ausnahme': 25.0}</field>
</record>
</odoo>
@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- AP13-A: verified domestic § 26 Z 4 / RGV reference values.
Parameters are deliberately date-versioned; foreign rates and
KV-specific § 3 Abs 1 Z 16b rates are outside this first scope. -->
<record id="hr_rule_parameter_at_reise_inland" model="hr.rule.parameter">
<field name="name">Reisekosten Inland</field>
<field name="code">at_reise_inland</field>
<field name="country_id" ref="base.at"/>
</record>
<record id="hr_rule_parameter_value_at_reise_inland_2026"
model="hr.rule.parameter.value">
<field name="rule_parameter_id" ref="hr_rule_parameter_at_reise_inland"/>
<field name="date_from">2026-01-01</field>
<field name="parameter_value">{'taggeld_tag': 30.0, 'naechtigung_pauschale': 17.0}</field>
</record>
<record id="hr_rule_parameter_at_km_geld" model="hr.rule.parameter">
<field name="name">Amtliches Kilometergeld</field>
<field name="code">at_km_geld</field>
<field name="country_id" ref="base.at"/>
</record>
<record id="hr_rule_parameter_value_at_km_geld_2026"
model="hr.rule.parameter.value">
<field name="rule_parameter_id" ref="hr_rule_parameter_at_km_geld"/>
<field name="date_from">2026-01-01</field>
<field name="parameter_value">{'pkw': 0.50, 'motorrad': 0.25, 'fahrrad': 0.25, 'dienstfahrrad': 0.0, 'fuss': 0.25, 'mitfahrer': 0.15, 'km_jahr_max': 30000.0, 'niedriger_satz_jahr_max': 12600.0}</field>
</record>
</odoo>
@@ -0,0 +1,45 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- AP12b: Sachbezugswerte-VO — Jahresversionen, keine Literale in
den Regeln. Rechtsanker: lb-sac-03/04/09/18. -->
<record id="hr_rule_parameter_at_sb_wagen" model="hr.rule.parameter">
<field name="name">Sachbezug Dienstwagen — Sätze und Höchstwerte</field>
<field name="code">at_sb_wagen</field>
<field name="country_id" ref="base.at"/>
</record>
<record id="hr_rule_parameter_value_at_sb_wagen" model="hr.rule.parameter.value">
<field name="rule_parameter_id" ref="hr_rule_parameter_at_sb_wagen"/>
<field name="date_from">2026-01-01</field>
<field name="parameter_value">{'voll_hoch_satz': 2.0, 'voll_hoch_cap': 960.0, 'voll_niedrig_satz': 1.5, 'voll_niedrig_cap': 720.0, 'halb_hoch_satz': 1.0, 'halb_hoch_cap': 480.0, 'halb_niedrig_satz': 0.75, 'halb_niedrig_cap': 360.0}</field>
</record>
<record id="hr_rule_parameter_at_sb_wagen_co2" model="hr.rule.parameter">
<field name="name">Sachbezug Dienstwagen — CO₂-Grenzwerte</field>
<field name="code">at_sb_wagen_co2</field>
<field name="country_id" ref="base.at"/>
</record>
<record id="hr_rule_parameter_value_at_sb_wagen_co2" model="hr.rule.parameter.value">
<field name="rule_parameter_id" ref="hr_rule_parameter_at_sb_wagen_co2"/>
<field name="date_from">2026-01-01</field>
<field name="parameter_value">{'vor_2016': 130.0, '2017': 127.0, '2018': 124.0, '2019': 121.0, '2020': 141.0, '2021': 138.0, '2022': 135.0, '2023': 132.0, '2024': 129.0, 'ab_2025': 126.0}</field>
</record>
<record id="hr_rule_parameter_at_sb_wohnung" model="hr.rule.parameter">
<field name="name">Sachbezug Dienstwohnung — m²-Werte und Abschläge</field>
<field name="code">at_sb_wohnung</field>
<field name="country_id" ref="base.at"/>
</record>
<record id="hr_rule_parameter_value_at_sb_wohnung" model="hr.rule.parameter.value">
<field name="rule_parameter_id" ref="hr_rule_parameter_at_sb_wohnung"/>
<field name="date_from">2026-01-01</field>
<field name="parameter_value">{'burgenland': 6.09, 'kaernten': 7.81, 'niederoesterreich': 6.85, 'oberoesterreich': 7.23, 'salzburg': 9.22, 'steiermark': 9.21, 'tirol': 8.14, 'vorarlberg': 10.25, 'wien': 6.67, 'nicht_norm_abschlag': 0.30, 'besorger_abschlag': 0.35, 'kombiniert_abschlag': 0.555, 'klein_frei_m2': 35.0, 'klein_abschlag_bis_m2': 45.0, 'klein_abschlag': 0.35}</field>
</record>
<record id="hr_rule_parameter_at_sb_parkplatz" model="hr.rule.parameter">
<field name="name">Sachbezug Parkplatz — Monatswert</field>
<field name="code">at_sb_parkplatz</field>
<field name="country_id" ref="base.at"/>
</record>
<record id="hr_rule_parameter_value_at_sb_parkplatz" model="hr.rule.parameter.value">
<field name="rule_parameter_id" ref="hr_rule_parameter_at_sb_parkplatz"/>
<field name="date_from">2026-01-01</field>
<field name="parameter_value">14.53</field>
</record>
</odoo>
@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- ============================================================ -->
<!-- AP12a: Sachbezüge — tagesbasierte Werte (tatsächlicher -->
<!-- Wert pro Arbeitstag). Mahlzeiten: tatsächlicher Bonwert -->
<!-- (Default = 8 €/AT = Freibetrag, d. h. vollständig frei; -->
<!-- Company setzt den tatsächlichen Bonwert, z. B. 11,0 — der -->
<!-- Überhang (Wert8)×Tage ist steuer-/beitragspflichtig, -->
<!-- lb-sac-17). Homeoffice: gesetzlich fix 3 €/AT (lb-tel-04/05, -->
<!-- § 26 Z 9 EStG). Freibetrags-Caps selbst liegen im Kern -->
<!-- (at_st_grenzen / at_sv49_grenzen). -->
<!-- ============================================================ -->
<record id="hr_rule_parameter_at_sb_werte" model="hr.rule.parameter">
<field name="name">Sachbezüge — tagesbasierte Werte</field>
<field name="code">at_sb_werte</field>
<field name="country_id" ref="base.at"/>
</record>
<record id="hr_rule_parameter_value_at_sb_werte"
model="hr.rule.parameter.value">
<field name="rule_parameter_id" ref="hr_rule_parameter_at_sb_werte"/>
<field name="date_from">2026-01-01</field>
<field name="parameter_value">{'mahlzeit_wert': 8.0, 'homeoffice_wert': 3.0}</field>
</record>
</odoo>
@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- Wr. Dienstgeberabgabe („U-Bahn-Steuer“): § 5 — 2 € je -->
<!-- Dienstnehmer:in und angefangene Woche. Quelle: RIS LrW, -->
<!-- Gesetzesnummer 20000133, geltende Fassung 12. 09. 2026. -->
<!-- Rückerstattungsgrenzen § 7 (218,02 / 2.180,19 €, +20/+10 %) -->
<!-- sind Jahreswerte der Erklärung — kein Abrechnungsparameter. -->
<record id="hr_rule_parameter_at_wien_dag_satz" model="hr.rule.parameter">
<field name="name">Wiener Dienstgeberabgabe je Woche (§ 5 Wr. DAG)</field>
<field name="code">at_wien_dag_satz</field>
<field name="country_id" ref="base.at"/>
</record>
<record id="hr_rule_parameter_value_at_wien_dag_satz"
model="hr.rule.parameter.value">
<field name="rule_parameter_id"
ref="hr_rule_parameter_at_wien_dag_satz"/>
<field name="date_from">2012-06-01</field>
<field name="parameter_value">2.0</field>
</record>
</odoo>
@@ -0,0 +1,57 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- ============================================================ -->
<!-- AP4 EFZG/Teilentgelt: Reduktion des Dienstgeber-Entgelts in -->
<!-- der halben Phase (50 %) und der ÖGK-Krankengeld-Phase (100 %) -->
<!-- — Entgeltausfallsprinzip je Arbeitstag (AngG § 8 / EFZG § 2; -->
<!-- SV/Lohnsteuer laufen über die reduzierte laufende Basis, -->
<!-- lb-krs-07). Reihenfolge nach allen laufenden Bezügen (PRAM -->
<!-- seq 40), vor B13/SZ. -->
<!-- ============================================================ -->
<record id="rule_teil_entgelt_halb" model="hr.salary.rule">
<field name="name">Teilentgelt Krankenstand (halbes Entgelt, 4 Wochen)</field>
<field name="code">TEIL_ENTGELT_HALB</field>
<field name="sequence" eval="45"/>
<field name="category_id" ref="l10n_at_hr_payroll_private.category_atp_brutto"/>
<field name="struct_id" ref="l10n_at_hr_payroll_private.structure_at_privat"/>
<field name="condition_select">python</field>
<field name="condition_python"><![CDATA[
efz = payslip._at_efz_status()
result = bool(efz and efz['halb_arbeitstage'])
]]></field>
<field name="amount_select">code</field>
<field name="l10n_at_bemessung">laufend</field>
<field name="amount_python_compute"><![CDATA[
# Halbes Entgelt für die Halb-Phase-Arbeitstage (AngG § 8 Abs 1:
# je weitere vier Wochen das halbe Entgelt): Minderung um 50 % des
# vollen Tagesentgelts (Tagesatz ohne Teilentgelt-Reduktionen).
efz = payslip._at_efz_status()
result = -payslip._at_efz_tagesatz(result_rules) * 0.5 \
* len(efz['halb_arbeitstage'])
]]></field>
</record>
<record id="rule_krankentgelt_phase" model="hr.salary.rule">
<field name="name">Krankengeld-Phase (ÖGK; kein Dienstgeber-Entgelt)</field>
<field name="code">KRANKENTGELT_PHASE</field>
<field name="sequence" eval="46"/>
<field name="category_id" ref="l10n_at_hr_payroll_private.category_atp_brutto"/>
<field name="struct_id" ref="l10n_at_hr_payroll_private.structure_at_privat"/>
<field name="condition_select">python</field>
<field name="condition_python"><![CDATA[
efz = payslip._at_efz_status()
result = bool(efz and efz['oegk_arbeitstage'])
]]></field>
<field name="amount_select">code</field>
<field name="l10n_at_bemessung">laufend</field>
<field name="amount_python_compute"><![CDATA[
# ÖGK-Krankengeld-Phase: erschöpfter EFZ-Anspruch — der Dienstgeber
# schuldet kein Entgelt (Krankengeld zahlt die ÖGK, lb-krs-02);
# die fiktive BV-Grundlage läuft separat (§ 7 Abs 3 BMSVG).
efz = payslip._at_efz_status()
result = -payslip._at_efz_tagesatz(result_rules) \
* len(efz['oegk_arbeitstage'])
]]></field>
</record>
</odoo>
@@ -0,0 +1,280 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- ============================================================ -->
<!-- AP14-A: Endabrechnungs-Regeln. -->
<!-- UEL: § 67 Abs 8 lit d — laufender Anteil als laufender -->
<!-- Bezug (Tarif + Sechstel nativ), SZ-Anteil als sonstiger Bezug -->
<!-- (6 %-Band, Freibetrag/Freigrenze im Kern). SV: nur der -->
<!-- laufende Anteil verlängert die Pflichtversicherung (§ 11 -->
<!-- Abs 2 ASVG, Tage-Umrechnung lb-url-15). -->
<!-- Abgang: § 67 Abs 8 lit b — neutral (kein Sechstel), LSt über -->
<!-- Fünftel-Override; SV-pflichtig mit Umlegung auf die -->
<!-- Kündigungsfrist; LNK-pflichtig (FLAG § 41 Abs 4 nennt nur -->
<!-- § 67 Abs 3 und 6). -->
<!-- Vergleich: § 67 Abs 8 lit a (AP15-C) — Rest über Fünftel- -->
<!-- Override, BV-Anwartschafts-Teil bis 7.500 € mit 6 %; beide -->
<!-- SV-pflichtig (Zuflussmonat; Aufrollung je Beitragsmonat bzw. -->
<!-- PV-Verlängerung siehe lb-end-22) und LNK-pflichtig. -->
<!-- Prämie: § 16 AngG-Aliquot bei Beendigung (AP14-B, lb-end-18) — -->
<!-- mit Endabrechnung sonstiger Bezug (Sechstel, SZ-SV); später -->
<!-- fällig über § 67 Abs 10 (tarifisch im Zuflussmonat ohne -->
<!-- Sechstel, Kern-Flag) mit Rollung des SV-Beitragsmonats ins -->
<!-- Austrittsjahr (Wizard-Write auf die Linie; mBGM-Storno GP7). -->
<!-- ============================================================ -->
<!-- Urlaubsersatzleistung — laufender Anteil (§ 67 Abs 8 lit d). -->
<record id="rule_url_ersatz_lfd" model="hr.salary.rule">
<field name="name">Urlaubsersatzleistung — laufender Anteil (§ 67 Abs 8 lit d)</field>
<field name="code">URL_ERSATZ_LFD</field>
<field name="sequence" eval="25"/>
<field name="category_id" ref="l10n_at_hr_payroll_private.category_atp_brutto"/>
<field name="struct_id" ref="l10n_at_hr_payroll_private.structure_at_privat"/>
<field name="condition_select">python</field>
<field name="condition_python">result = 'ATP_UEL_TAGE' in inputs and inputs['ATP_UEL_TAGE'].amount</field>
<field name="l10n_at_bemessung">laufend</field>
<field name="l10n_at_sv_verlaengerung">urlaubsersatz</field>
<field name="amount_select">code</field>
<field name="amount_python_compute"><![CDATA[
# § 10 UrlG (lb-url-16): Tageswert = Monatsentgelt / Divisor
# (26 Werktage / 22 Arbeitstage); keine Rundung auf ganze Tage.
result = payslip._at_kv_gehalt_basis() / payslip._at_uel_divisor()
result_qty = inputs['ATP_UEL_TAGE'].amount
]]></field>
</record>
<!-- Urlaubsersatzleistung — Sonderzahlungsanteil (§ 67 Abs 8 lit d). -->
<record id="rule_url_ersatz_sz" model="hr.salary.rule">
<field name="name">Urlaubsersatzleistung — Sonderzahlungsanteil (§ 67 Abs 8 lit d)</field>
<field name="code">URL_ERSATZ_SZ</field>
<field name="sequence" eval="26"/>
<field name="category_id" ref="l10n_at_hr_payroll_private.category_atp_brutto"/>
<field name="struct_id" ref="l10n_at_hr_payroll_private.structure_at_privat"/>
<field name="condition_select">python</field>
<field name="condition_python">result = 'ATP_UEL_TAGE' in inputs and inputs['ATP_UEL_TAGE'].amount</field>
<field name="l10n_at_bemessung">sonstig</field>
<field name="amount_select">code</field>
<field name="amount_python_compute"><![CDATA[
# SZ-Anteil der Urlaubsentgelt-Basis (lb-url-16): 2 Sonderzahlungen
# à Monatsentgelt pro Jahr → je Monat 2 × SZ-Basis/12, als Tageswert
# (auch bei bereits überaliquot bezahlten SZ).
result = payslip._at_uel_sz_tageswert()
result_qty = inputs['ATP_UEL_TAGE'].amount
]]></field>
</record>
<!-- Abgangsentschädigung (§ 67 Abs 8 lit b — Kündigungsentschädigung). -->
<record id="rule_abgang" model="hr.salary.rule">
<field name="name">Abgangsentschädigung (§ 67 Abs 8 lit b)</field>
<field name="code">ABGANG</field>
<field name="sequence" eval="29"/>
<field name="category_id" ref="l10n_at_hr_payroll_private.category_atp_brutto"/>
<field name="struct_id" ref="l10n_at_hr_payroll_private.structure_at_privat"/>
<field name="condition_select">python</field>
<field name="condition_python">result = 'ATP_ABGANG' in inputs and inputs['ATP_ABGANG'].amount</field>
<field name="l10n_at_bemessung">neutral</field>
<field name="l10n_at_sv_art">laufend</field>
<field name="l10n_at_s67_fuenftel" eval="True"/>
<field name="l10n_at_sv_verlaengerung">abgang</field>
<field name="amount_select">code</field>
<field name="amount_python_compute"><![CDATA[
result = inputs['ATP_ABGANG'].amount
]]></field>
</record>
<!-- Gesetzliche Abfertigung Alt (§§ 23/23a AngG, § 67 Abs 3 EStG). -->
<record id="rule_abf_alt" model="hr.salary.rule">
<field name="name">Gesetzliche Abfertigung Alt (§ 67 Abs 3)</field>
<field name="code">ABF_ALT</field>
<field name="sequence" eval="30"/>
<field name="category_id" ref="l10n_at_hr_payroll_private.category_atp_brutto"/>
<field name="struct_id" ref="l10n_at_hr_payroll_private.structure_at_privat"/>
<field name="condition_select">python</field>
<field name="condition_python">result = 'ATP_ABF_ALT' in inputs and inputs['ATP_ABF_ALT'].amount</field>
<field name="l10n_at_bemessung">neutral</field>
<field name="l10n_at_sv_art">keine</field>
<field name="l10n_at_s67_abs">abs3</field>
<field name="l10n_at_pfandung_beendigung" eval="True"/>
<field name="amount_select">code</field>
<field name="amount_python_compute"><![CDATA[
result = inputs['ATP_ABF_ALT'].amount
]]></field>
</record>
<!-- Freiwillige Abfertigung (§ 67 Abs 6 EStG): begünstigter Teil
(Viertel-/Zwölftelregelung mit 6 %) und tarifischer Überhang
(Z 6) als getrennte Zeilen. -->
<record id="rule_abf_frei" model="hr.salary.rule">
<field name="name">Freiwillige Abfertigung — begünstigter Teil (§ 67 Abs 6)</field>
<field name="code">ABF_FREI</field>
<field name="sequence" eval="31"/>
<field name="category_id" ref="l10n_at_hr_payroll_private.category_atp_brutto"/>
<field name="struct_id" ref="l10n_at_hr_payroll_private.structure_at_privat"/>
<field name="condition_select">python</field>
<field name="condition_python">result = 'ATP_ABF_FREI' in inputs and inputs['ATP_ABF_FREI'].amount</field>
<field name="l10n_at_bemessung">neutral</field>
<field name="l10n_at_sv_art">keine</field>
<field name="l10n_at_s67_abs">abs6</field>
<field name="l10n_at_pfandung_beendigung" eval="True"/>
<field name="amount_select">code</field>
<field name="amount_python_compute"><![CDATA[
result = payslip._at_abf_frei_beguenstigt(result_rules)
]]></field>
</record>
<record id="rule_abf_frei_tarif" model="hr.salary.rule">
<field name="name">Freiwillige Abfertigung — Überhang (§ 67 Abs 6 Z 6)</field>
<field name="code">ABF_FREI_TARIF</field>
<field name="sequence" eval="32"/>
<field name="category_id" ref="l10n_at_hr_payroll_private.category_atp_brutto"/>
<field name="struct_id" ref="l10n_at_hr_payroll_private.structure_at_privat"/>
<field name="condition_select">python</field>
<field name="condition_python">result = payslip._at_abf_frei_ueberhang(result_rules) > 0</field>
<field name="l10n_at_bemessung">laufend</field>
<field name="l10n_at_sv_art">keine</field>
<field name="l10n_at_s67_abs">abs6</field>
<field name="l10n_at_s67_abs6_ueberhang" eval="True"/>
<field name="amount_select">code</field>
<field name="amount_python_compute"><![CDATA[
result = payslip._at_abf_frei_ueberhang(result_rules)
]]></field>
</record>
<!-- Vergleichssumme — Rest (§ 67 Abs 8 lit a, Fünftelregelung). -->
<record id="rule_vergleich" model="hr.salary.rule">
<field name="name">Vergleichssumme — Rest (§ 67 Abs 8 lit a)</field>
<field name="code">VERGLEICH</field>
<field name="sequence" eval="33"/>
<field name="category_id" ref="l10n_at_hr_payroll_private.category_atp_brutto"/>
<field name="struct_id" ref="l10n_at_hr_payroll_private.structure_at_privat"/>
<field name="condition_select">python</field>
<field name="condition_python">result = 'ATP_VERGLEICH' in inputs and inputs['ATP_VERGLEICH'].amount</field>
<field name="l10n_at_bemessung">neutral</field>
<field name="l10n_at_sv_art">laufend</field>
<field name="l10n_at_s67_fuenftel" eval="True"/>
<field name="amount_select">code</field>
<field name="amount_python_compute"><![CDATA[
result = inputs['ATP_VERGLEICH'].amount
]]></field>
</record>
<!-- Vergleichssumme — BV-Anwartschafts-Teil (§ 67 Abs 8 lit a
letzter Satz): bis 7.500 € mit 6 % (LST_VERGLEICH_BV), Überhang
fällt in die Fünftelregelung zurück. Erkennbare Abfertigungs-
anteile werden getrennt als ABF_ALT/ABF_FREI erfasst. -->
<record id="rule_vergleich_bv" model="hr.salary.rule">
<field name="name">Vergleichssumme — BV-Anwartschafts-Teil (§ 67 Abs 8 lit a)</field>
<field name="code">VERGLEICH_BV</field>
<field name="sequence" eval="34"/>
<field name="category_id" ref="l10n_at_hr_payroll_private.category_atp_brutto"/>
<field name="struct_id" ref="l10n_at_hr_payroll_private.structure_at_privat"/>
<field name="condition_select">python</field>
<field name="condition_python">result = 'ATP_VERGLEICH_BV' in inputs and inputs['ATP_VERGLEICH_BV'].amount</field>
<field name="l10n_at_bemessung">neutral</field>
<field name="l10n_at_sv_art">laufend</field>
<field name="l10n_at_vergleich_bv" eval="True"/>
<field name="amount_select">code</field>
<field name="amount_python_compute"><![CDATA[
result = inputs['ATP_VERGLEICH_BV'].amount
]]></field>
</record>
<!-- Aliquote Prämie/Sonderzahlung bei Beendigung (§ 16 AngG):
mit der Endabrechnung ausgezahlt — sonstiger Bezug im
Jahressechstel (6 %, Freibetrag/Freigrenze), SZ-SV, voll
LNK- und BV-pflichtig (lb-end-18). -->
<record id="rule_pram_end" model="hr.salary.rule">
<field name="name">Aliquote Prämie bei Beendigung (§ 16 AngG)</field>
<field name="code">PRAM_END</field>
<field name="sequence" eval="35"/>
<field name="category_id" ref="l10n_at_hr_payroll_private.category_atp_brutto"/>
<field name="struct_id" ref="l10n_at_hr_payroll_private.structure_at_privat"/>
<field name="condition_select">python</field>
<field name="condition_python">result = 'ATP_PRAM_END' in inputs and inputs['ATP_PRAM_END'].amount</field>
<field name="l10n_at_bemessung">sonstig</field>
<field name="amount_select">code</field>
<field name="amount_python_compute"><![CDATA[
result = inputs['ATP_PRAM_END'].amount
]]></field>
</record>
<!-- Später fällige anteilige Prämie (§ 67 Abs 10): die Höhe ist
erst nach Ablauf der Anspruchsperiode ermittelbar und wird zum
vereinbarten Fälligkeitszeitpunkt ausgezahlt — wie ein laufender
Bezug zum Tarif des Zuflussmonats, OHNE Sechstel-Erhöhung
(Kern-Flag), gesonderter Lohnzettel (LStR Rz 911a); der
SV-Beitragsmonat wird per Rollung ins Austrittsjahr auf die
Linie geschrieben (E-MVB 044-01-00-006). -->
<record id="rule_pram_end_tarif" model="hr.salary.rule">
<field name="name">Später fällige anteilige Prämie (§ 67 Abs 10)</field>
<field name="code">PRAM_END_TARIF</field>
<field name="sequence" eval="36"/>
<field name="category_id" ref="l10n_at_hr_payroll_private.category_atp_brutto"/>
<field name="struct_id" ref="l10n_at_hr_payroll_private.structure_at_privat"/>
<field name="condition_select">python</field>
<field name="condition_python">result = 'ATP_PRAM_END_TARIF' in inputs and inputs['ATP_PRAM_END_TARIF'].amount</field>
<field name="l10n_at_bemessung">laufend</field>
<field name="l10n_at_s67_abs6_ueberhang" eval="True"/>
<field name="amount_select">code</field>
<field name="amount_python_compute"><![CDATA[
result = inputs['ATP_PRAM_END_TARIF'].amount
]]></field>
</record>
<!-- Zeitguthaben an Normalarbeitszeit (§ 19e Abs 2 AZG: + 50 %). -->
<record id="rule_zeit_guthaben" model="hr.salary.rule">
<field name="name">Zeitguthaben Normalarbeitszeit + 50 % (§ 19e Abs 2 AZG)</field>
<field name="code">ZEIT_GUTHABEN</field>
<field name="sequence" eval="27"/>
<field name="category_id" ref="l10n_at_hr_payroll_private.category_atp_brutto"/>
<field name="struct_id" ref="l10n_at_hr_payroll_private.structure_at_privat"/>
<field name="condition_select">python</field>
<field name="condition_python">result = 'ATP_ZEIT_GUTHABEN' in inputs and inputs['ATP_ZEIT_GUTHABEN'].amount</field>
<field name="l10n_at_bemessung">laufend</field>
<field name="amount_select">code</field>
<field name="amount_python_compute"><![CDATA[
# § 19e Abs 2 AZG: Guthaben an Normalarbeitszeit + 50 % Zuschlag,
# bemessen mit der Normalstunde (lb-end-10); der Zuschlag ist KEIN
# Überstundenzuschlag iSd § 68 EStG (kein Freibetrags-Flag).
result = payslip._at_grundstunde_norm() * 1.5
result_qty = inputs['ATP_ZEIT_GUTHABEN'].amount
]]></field>
</record>
<!-- Zeitschulden (Abrechnung mit der aktuellen Normalstunde). -->
<record id="rule_zeit_schulden" model="hr.salary.rule">
<field name="name">Zeitschulden-Abrechnung (negative Zeile)</field>
<field name="code">ZEIT_SCHULDEN</field>
<field name="sequence" eval="28"/>
<field name="category_id" ref="l10n_at_hr_payroll_private.category_atp_brutto"/>
<field name="struct_id" ref="l10n_at_hr_payroll_private.structure_at_privat"/>
<field name="condition_select">python</field>
<field name="condition_python">result = 'ATP_ZEIT_SCHULDEN' in inputs and inputs['ATP_ZEIT_SCHULDEN'].amount</field>
<field name="l10n_at_bemessung">laufend</field>
<field name="amount_select">code</field>
<field name="amount_python_compute"><![CDATA[
# Abrechnung offener Zeitschulden mit der aktuellen Normalstunde
# (lb-end-08); negative laufende Bezugszeile.
result = -payslip._at_grundstunde_norm()
result_qty = inputs['ATP_ZEIT_SCHULDEN'].amount
]]></field>
</record>
<!-- Aufrechnung offener Vorschüsse: Rückverrechnung vom Netto
NACH SV und Lohnsteuer (§ 293 Abs 3 EO, lb-end-06) — neutral,
ohne SV-/LSt-/LNK-Wirkung. -->
<record id="rule_end_abrechnung_vorschuss" model="hr.salary.rule">
<field name="name">Aufrechnung offener Vorschuss (Netto)</field>
<field name="code">END_ABRECHNUNG_VORSCHUSS</field>
<field name="sequence" eval="80"/>
<field name="category_id" ref="l10n_at_hr_payroll_private.category_atp_abzug"/>
<field name="struct_id" ref="l10n_at_hr_payroll_private.structure_at_privat"/>
<field name="condition_select">python</field>
<field name="condition_python">result = 'ATP_ABRECHNUNG_VORSCHUSS' in inputs and inputs['ATP_ABRECHNUNG_VORSCHUSS'].amount</field>
<field name="l10n_at_bemessung">neutral</field>
<field name="l10n_at_sv_art">keine</field>
<field name="amount_select">code</field>
<field name="amount_python_compute"><![CDATA[
result = inputs['ATP_ABRECHNUNG_VORSCHUSS'].amount
]]></field>
</record>
</odoo>
@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- ============================================================ -->
<!-- AP16 (8.13): Karenz-Entgelt-Stopp — entgeltlose Voll- -->
<!-- Freistellungen (AVRAG §§ 11/14a/14b/14e/14c, VKG § 1a): -->
<!-- das Karenzgeld/Pflegekarenzgeld zahlen AK/Bund (nicht der -->
<!-- AG), der Dienstgeber schuldet für die Freistellungs-Tage -->
<!-- kein Entgelt. Negative Brutto-Zeile je Arbeitstag aus dem -->
<!-- Arbeitseintragssatz (Muster MSCH_ENTGELT, AP7); der Tagesatz -->
<!-- teilt die Ausschlüsse mit EFZG/MSCH (_at_efz_tagesatz). -->
<!-- ============================================================ -->
<record id="rule_karenz_entgelt" model="hr.salary.rule">
<field name="name">Karenz-Freistellung (kein Dienstgeber-Entgelt; Leistungen extern)</field>
<field name="code">KARENZ_ENTGELT</field>
<field name="sequence" eval="48"/>
<field name="category_id" ref="l10n_at_hr_payroll_private.category_atp_brutto"/>
<field name="struct_id" ref="l10n_at_hr_payroll_private.structure_at_privat"/>
<field name="condition_select">python</field>
<field name="condition_python"><![CDATA[
result = payslip._at_karenz_entgelt_tage() > 0
]]></field>
<field name="amount_select">code</field>
<field name="l10n_at_bemessung">laufend</field>
<field name="amount_python_compute"><![CDATA[
# Freistellung je Arbeitstag: volle Tages-Entgelt-Reduktion
# (Tagesatz teilt die Ausschlüsse mit EFZG/MSCH/KARENZ).
result = -payslip._at_efz_tagesatz(result_rules) \
* payslip._at_karenz_entgelt_tage()
]]></field>
</record>
</odoo>
@@ -104,4 +104,49 @@ result = payslip._l10n_at_lst_laufend(result_rules)
result = payslip._l10n_at_lst_sonstige(result_rules) result = payslip._l10n_at_lst_sonstige(result_rules)
]]></field> ]]></field>
</record> </record>
<record id="rule_lst_abf" model="hr.salary.rule">
<field name="name">Lohnsteuer Abfertigung Alt (§ 67 Abs 3)</field>
<field name="code">LST_ABF</field>
<field name="sequence" eval="72"/>
<field name="category_id" ref="l10n_at_hr_payroll_private.category_atp_abzug"/>
<field name="struct_id" ref="l10n_at_hr_payroll_private.structure_at_privat"/>
<field name="condition_select">python</field>
<field name="condition_python">result = payslip._at_abf_alt_lohnsteuer(result_rules) > 0</field>
<field name="amount_select">code</field>
<field name="l10n_at_bemessung">neutral</field>
<field name="l10n_at_sv_art">keine</field>
<field name="amount_python_compute"><![CDATA[
result = payslip._at_abf_alt_lohnsteuer(result_rules)
]]></field>
</record>
<record id="rule_lst_abf_frei" model="hr.salary.rule">
<field name="name">Lohnsteuer freiwillige Abfertigung (§ 67 Abs 6)</field>
<field name="code">LST_ABF_FREI</field>
<field name="sequence" eval="73"/>
<field name="category_id" ref="l10n_at_hr_payroll_private.category_atp_abzug"/>
<field name="struct_id" ref="l10n_at_hr_payroll_private.structure_at_privat"/>
<field name="condition_select">python</field>
<field name="condition_python">result = payslip._at_abf_frei_lst_6(result_rules) > 0</field>
<field name="amount_select">code</field>
<field name="l10n_at_bemessung">neutral</field>
<field name="l10n_at_sv_art">keine</field>
<field name="amount_python_compute"><![CDATA[
result = payslip._at_abf_frei_lst_6(result_rules)
]]></field>
</record>
<record id="rule_lst_vergleich_bv" model="hr.salary.rule">
<field name="name">Lohnsteuer Vergleich — BV-Anwartschafts-Teil (§ 67 Abs 8 lit a)</field>
<field name="code">LST_VERGLEICH_BV</field>
<field name="sequence" eval="74"/>
<field name="category_id" ref="l10n_at_hr_payroll_private.category_atp_abzug"/>
<field name="struct_id" ref="l10n_at_hr_payroll_private.structure_at_privat"/>
<field name="condition_select">python</field>
<field name="condition_python">result = payslip._at_vergleich_bv_lst_6(result_rules) > 0</field>
<field name="amount_select">code</field>
<field name="l10n_at_bemessung">neutral</field>
<field name="l10n_at_sv_art">keine</field>
<field name="amount_python_compute"><![CDATA[
result = payslip._at_vergleich_bv_lst_6(result_rules)
]]></field>
</record>
</odoo> </odoo>
@@ -0,0 +1,34 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- ============================================================ -->
<!-- AP7: Mutterschutz — Entgelt-Stopp während der Beschäftigungs- -->
<!-- verbote (MSchG §§ 3/5): Das Wochengeld zahlt die ÖGK (§ 162 -->
<!-- Abs 1 ASVG), der Dienstgeber schuldet für die Verbots-Tage -->
<!-- kein Entgelt (lb-msf-03). Negative Brutto-Zeile je Arbeitstag -->
<!-- aus dem Arbeitseintragssatz (Muster KRANKENTGELT_PHASE); die -->
<!-- fiktive BV-Grundlage läuft separat (§ 7 Abs 4 BMSVG). -->
<!-- ============================================================ -->
<record id="rule_msch_entgelt" model="hr.salary.rule">
<field name="name">Mutterschutz-Frist (kein Dienstgeber-Entgelt; Wochengeld zahlt ÖGK)</field>
<field name="code">MSCH_ENTGELT</field>
<field name="sequence" eval="47"/>
<field name="category_id" ref="l10n_at_hr_payroll_private.category_atp_brutto"/>
<field name="struct_id" ref="l10n_at_hr_payroll_private.structure_at_privat"/>
<field name="condition_select">python</field>
<field name="condition_python"><![CDATA[
msch = payslip._at_msch_status()
result = bool(msch and msch['arbeitstage'])
]]></field>
<field name="amount_select">code</field>
<field name="l10n_at_bemessung">laufend</field>
<field name="amount_python_compute"><![CDATA[
# Beschäftigungsverbot je Arbeitstag: volle Tages-Entgelt-
# Reduktion (Tagesatz ohne Teilentgelt-Reduktionen — Helper
# teilt die Ausschlüsse mit EFZG und MSCH).
msch = payslip._at_msch_status()
result = -payslip._at_efz_tagesatz(result_rules) \
* len(msch['arbeitstage'])
]]></field>
</record>
</odoo>
@@ -0,0 +1,78 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- ============================================================ -->
<!-- AP11: Lohnpfändung — DN-Abzug (Einbehalt) in ATP_ABZUG. -->
<!-- Reihenfolge: nach SV-DN (seq 6065) und LSt (seq 7071), -->
<!-- vor NET (seq 100) — die Pfändungsberechnungsgrundlage -->
<!-- (§ 291 EO) ist das Netto nach SV-DN/LSt (Bemessung 'neutral' -->
<!-- → kein SV-/LSt-Effekt). PFAENDUNG = laufender Topf; -->
<!-- PFAENDUNG_SZ = 13./14.-Bezug (eigener Berechnungskreis, -->
<!-- lb-pfa-05). Kostenersatz (§ 292h/§ 302) ist Teil des -->
<!-- pfändbaren Betrags (Verteilposition, keine extra Abzugs- -->
<!-- zeile — sonst Doppelabzug; s. _at_pfaendung_kostenersatz). -->
<!-- ============================================================ -->
<record id="rule_pfaendung" model="hr.salary.rule">
<field name="name">Lohnpfändung — Einbehalt (laufender Bezug, § 291a EO)</field>
<field name="code">PFAENDUNG</field>
<field name="sequence" eval="75"/>
<field name="category_id" ref="l10n_at_hr_payroll_private.category_atp_abzug"/>
<field name="struct_id" ref="l10n_at_hr_payroll_private.structure_at_privat"/>
<field name="condition_select">python</field>
<field name="condition_python"><![CDATA[
# Einbehalt nur bei aktiver Pfändung und pfändbarem laufendem Bezug.
result = bool(payslip.employee_id._at_pfaendungen_aktiv(payslip.date_to)) \
and payslip._at_pfaendung_masse(result_rules, 'laufend') > 0
]]></field>
<field name="amount_select">code</field>
<field name="l10n_at_bemessung">neutral</field>
<field name="amount_python_compute"><![CDATA[
# Laufender Topf: Berechnungsgrundlage (Netto nach SV-DN/LSt, ohne
# unpfändbare Bezugsarten) minus Existenzminimum (§ 291a EO).
result = payslip._at_pfaendung_masse(result_rules, 'laufend')
]]></field>
</record>
<record id="rule_pfaendung_sz" model="hr.salary.rule">
<field name="name">Lohnpfändung — 13./14. Bezug (eigener Topf, § 291a EO)</field>
<field name="code">PFAENDUNG_SZ</field>
<field name="sequence" eval="76"/>
<field name="category_id" ref="l10n_at_hr_payroll_private.category_atp_abzug"/>
<field name="struct_id" ref="l10n_at_hr_payroll_private.structure_at_privat"/>
<field name="condition_select">python</field>
<field name="condition_python"><![CDATA[
# 13./14.-Bezug als eigener Pfändungsberechnungskreis (lb-pfa-05).
masse = (payslip._at_pfaendung_masse(result_rules, 'sz13')
+ payslip._at_pfaendung_masse(result_rules, 'sz14'))
result = bool(payslip.employee_id._at_pfaendungen_aktiv(payslip.date_to)) \
and masse > 0
]]></field>
<field name="amount_select">code</field>
<field name="l10n_at_bemessung">neutral</field>
<field name="amount_python_compute"><![CDATA[
# 13. und 14. Bezug je eigener Existenzminimum (allg. Grundbetrag,
# SZ = § 290b → nicht erhöht).
result = (payslip._at_pfaendung_masse(result_rules, 'sz13')
+ payslip._at_pfaendung_masse(result_rules, 'sz14'))
]]></field>
</record>
<record id="rule_pfaendung_end" model="hr.salary.rule">
<field name="name">Lohnpfändung — Beendigungsansprüche (§ 291d EO)</field>
<field name="code">PFAENDUNG_END</field>
<field name="sequence" eval="77"/>
<field name="category_id" ref="l10n_at_hr_payroll_private.category_atp_abzug"/>
<field name="struct_id" ref="l10n_at_hr_payroll_private.structure_at_privat"/>
<field name="condition_select">python</field>
<field name="condition_python"><![CDATA[
result = bool(payslip.employee_id._at_pfaendungen_aktiv(payslip.date_to)) \
and payslip._at_pfaendung_masse(result_rules, 'beendigung') > 0
]]></field>
<field name="amount_select">code</field>
<field name="l10n_at_bemessung">neutral</field>
<field name="l10n_at_sv_art">keine</field>
<field name="amount_python_compute"><![CDATA[
result = payslip._at_pfaendung_masse(result_rules, 'beendigung')
]]></field>
</record>
</odoo>
@@ -0,0 +1,57 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- The standard hr_payroll_expense link/reconciliation requires this
exact EXPENSES rule and a payable debit account. Its neutral AT
classification reimburses documented expenses without adding them to
LSt/SV/LNK bases. -->
<record id="rule_expenses" model="hr.salary.rule">
<field name="name">Spesenersatz (über Reisekostenabrechnung)</field>
<field name="code">EXPENSES</field>
<field name="sequence" eval="43"/>
<field name="category_id" ref="l10n_at_hr_payroll_private.category_atp_brutto"/>
<field name="struct_id" ref="l10n_at_hr_payroll_private.structure_at_privat"/>
<field name="condition_select">python</field>
<field name="condition_python">result = 'EXPENSES' in inputs and inputs['EXPENSES'].amount</field>
<field name="l10n_at_bemessung">neutral</field>
<field name="l10n_at_sv_art">keine</field>
<field name="amount_select">code</field>
<field name="amount_python_compute"><![CDATA[
result = inputs['EXPENSES'].amount
]]></field>
</record>
<!-- Only the amount over the documented § 26 limit is ordinary
remuneration. EXPENSES still refunds the actual expense once;
REISE_NETTOAUSGLEICH prevents a second reimbursement through this
classification line while taxes remain withheld. -->
<record id="rule_reise_steuerpflichtig" model="hr.salary.rule">
<field name="name">Reisekosten — steuerpflichtiger Überhang</field>
<field name="code">REISE_STEUERPFLICHTIG</field>
<field name="sequence" eval="44"/>
<field name="category_id" ref="l10n_at_hr_payroll_private.category_atp_brutto"/>
<field name="struct_id" ref="l10n_at_hr_payroll_private.structure_at_privat"/>
<field name="condition_select">python</field>
<field name="condition_python">result = 'ATP_REISE_STEUERPFLICHTIG' in inputs and inputs['ATP_REISE_STEUERPFLICHTIG'].amount</field>
<field name="l10n_at_bemessung">laufend</field>
<field name="l10n_at_s26_z4_reise" eval="True"/>
<field name="amount_select">code</field>
<field name="amount_python_compute"><![CDATA[
result = inputs['ATP_REISE_STEUERPFLICHTIG'].amount
]]></field>
</record>
<record id="rule_reise_nettoausgleich" model="hr.salary.rule">
<field name="name">Reisekosten — Nettoausgleich Überhang</field>
<field name="code">REISE_NETTOAUSGLEICH</field>
<field name="sequence" eval="45"/>
<field name="category_id" ref="l10n_at_hr_payroll_private.category_atp_abzug"/>
<field name="struct_id" ref="l10n_at_hr_payroll_private.structure_at_privat"/>
<field name="condition_select">python</field>
<field name="condition_python">result = 'ATP_REISE_NETTOAUSGLEICH' in inputs and inputs['ATP_REISE_NETTOAUSGLEICH'].amount</field>
<field name="l10n_at_bemessung">neutral</field>
<field name="l10n_at_sv_art">keine</field>
<field name="amount_select">code</field>
<field name="amount_python_compute"><![CDATA[
result = inputs['ATP_REISE_NETTOAUSGLEICH'].amount
]]></field>
</record>
</odoo>
@@ -0,0 +1,81 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- AP12b: wertgebundene Sachbezüge. Wagen/Wohnung/Parkplatz sind
Version-Stammdaten; Handy/Verkauf/Bonusmeilen Ereignis-Eingaben.
Die Regeln stehen vor B13/B14 (50ff.) und SV/LSt (60ff.). -->
<record id="rule_sb_parkplatz" model="hr.salary.rule">
<field name="name">Sachbezug Parkplatz am Dienstort</field>
<field name="code">SB_PARKPLATZ</field>
<field name="sequence" eval="53"/>
<field name="category_id" ref="l10n_at_hr_payroll_private.category_atp_brutto"/>
<field name="struct_id" ref="l10n_at_hr_payroll_private.structure_at_privat"/>
<field name="condition_select">python</field>
<field name="condition_python">result = payslip.version_id._l10n_at_sb_parkplatz_wert(payslip.date_to) &gt; 0</field>
<field name="amount_select">code</field>
<field name="l10n_at_bemessung">laufend</field>
<field name="amount_python_compute">result = payslip.version_id._l10n_at_sb_parkplatz_wert(payslip.date_to)</field>
</record>
<record id="rule_sb_wohnung" model="hr.salary.rule">
<field name="name">Sachbezug Dienstwohnung</field>
<field name="code">SB_WOHNUNG</field>
<field name="sequence" eval="54"/>
<field name="category_id" ref="l10n_at_hr_payroll_private.category_atp_brutto"/>
<field name="struct_id" ref="l10n_at_hr_payroll_private.structure_at_privat"/>
<field name="condition_select">python</field>
<field name="condition_python">result = payslip.version_id._l10n_at_sb_wohnung_wert(payslip.date_to) &gt; 0</field>
<field name="amount_select">code</field>
<field name="l10n_at_bemessung">laufend</field>
<field name="amount_python_compute">result = payslip.version_id._l10n_at_sb_wohnung_wert(payslip.date_to)</field>
</record>
<record id="rule_sb_wagen" model="hr.salary.rule">
<field name="name">Sachbezug Dienstwagen Privatnutzung</field>
<field name="code">SB_WAGEN</field>
<field name="sequence" eval="55"/>
<field name="category_id" ref="l10n_at_hr_payroll_private.category_atp_brutto"/>
<field name="struct_id" ref="l10n_at_hr_payroll_private.structure_at_privat"/>
<field name="condition_select">python</field>
<field name="condition_python">result = payslip.version_id._l10n_at_sb_wagen_wert(payslip.date_to) &gt; 0</field>
<field name="amount_select">code</field>
<field name="l10n_at_bemessung">laufend</field>
<field name="amount_python_compute">result = payslip.version_id._l10n_at_sb_wagen_wert(payslip.date_to)</field>
</record>
<record id="rule_sb_gebraucht" model="hr.salary.rule">
<field name="name">Sachbezug Arbeitsmittel-Verkauf unter Buchwert</field>
<field name="code">SB_GEBRAUCHT</field>
<field name="sequence" eval="56"/>
<field name="category_id" ref="l10n_at_hr_payroll_private.category_atp_brutto"/>
<field name="struct_id" ref="l10n_at_hr_payroll_private.structure_at_privat"/>
<field name="condition_select">python</field>
<field name="condition_python">result = 'ATP_SB_GEBRAUCHT' in inputs and inputs['ATP_SB_GEBRAUCHT'].amount</field>
<field name="amount_select">code</field>
<field name="l10n_at_bemessung">sonstig</field>
<field name="l10n_at_sv_art">laufend</field>
<field name="amount_python_compute">result = inputs['ATP_SB_GEBRAUCHT'].amount</field>
</record>
<record id="rule_sb_bonusmeilen" model="hr.salary.rule">
<field name="name">Sachbezug Bonusmeilen (Drittlohn, nur SV)</field>
<field name="code">SB_BONUSMEILEN</field>
<field name="sequence" eval="57"/>
<!-- Drittlohn erhöht nicht den Auszahlungsnettoanspruch. -->
<field name="category_id" ref="l10n_at_hr_payroll_private.category_atp_dg"/>
<field name="struct_id" ref="l10n_at_hr_payroll_private.structure_at_privat"/>
<field name="condition_select">python</field>
<field name="condition_python">result = 'ATP_SB_BONUSMEILEN' in inputs and inputs['ATP_SB_BONUSMEILEN'].amount</field>
<field name="amount_select">code</field>
<field name="l10n_at_bemessung">neutral</field>
<field name="l10n_at_sv_art">laufend</field>
<field name="amount_python_compute">result = inputs['ATP_SB_BONUSMEILEN'].amount</field>
</record>
<record id="rule_sb_handy" model="hr.salary.rule">
<field name="name">Sachbezug Handy — private Mehrkosten</field>
<field name="code">SB_HANDY</field>
<field name="sequence" eval="58"/>
<field name="category_id" ref="l10n_at_hr_payroll_private.category_atp_brutto"/>
<field name="struct_id" ref="l10n_at_hr_payroll_private.structure_at_privat"/>
<field name="condition_select">python</field>
<field name="condition_python">result = payslip.version_id.l10n_at_sb_handy_profil == 'ueberwiegend_privat' and 'ATP_SB_HANDY' in inputs and inputs['ATP_SB_HANDY'].amount</field>
<field name="amount_select">code</field>
<field name="l10n_at_bemessung">laufend</field>
<field name="amount_python_compute">result = inputs['ATP_SB_HANDY'].amount</field>
</record>
</odoo>
@@ -0,0 +1,129 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- ============================================================ -->
<!-- AP12a: steuerfreie Sachbezüge — Freibeträge nach -->
<!-- § 3/§ 26 EStG, beitragsfrei § 49 Abs 3 ASVG, lohnneben- -->
<!-- kostenfrei (lb-sac-17/07/02, lb-ges-02). -->
<!-- Input-Semantik: -->
<!-- - tagesbasiert (Mahlzeiten/Homeoffice): amount = Anzahl -->
<!-- Arbeitstage; der Tageswert kommt aus dem versionierten -->
<!-- Parameter at_sb_werte (Kern-Mechanik: total = Wert×Tage, -->
<!-- Freibetrag = min(total, Cap×Tage)). -->
<!-- - Pool-basiert (BV/Rabatt/Geschenk): amount = Gesamtvalue -->
<!-- des Monats; Freibetrag = min(total, Jahrespool YTD). -->
<!-- Die Freibetrags-/Jahreskumulationslogik läuft im Kern über -->
<!-- l10n_at_sv49_frei (SV) und l10n_at_st_frei (LSt/LNK). -->
<!-- Nur der übersteigende Teil ist beitrags-/steuerpflichtig -->
<!-- (Freibeträge, keine Freigrenzen). -->
<!-- ============================================================ -->
<record id="rule_sb_mahlzeit" model="hr.salary.rule">
<field name="name">Sachbezug Mahlzeiten-Gutschein (manuell)</field>
<field name="code">SB_MAHLZEIT</field>
<field name="sequence" eval="41"/>
<field name="category_id" ref="l10n_at_hr_payroll_private.category_atp_brutto"/>
<field name="struct_id" ref="l10n_at_hr_payroll_private.structure_at_privat"/>
<field name="condition_select">python</field>
<field name="condition_python">result = 'ATP_SB_MAHLZEIT' in inputs and inputs['ATP_SB_MAHLZEIT'].amount</field>
<field name="amount_select">code</field>
<field name="l10n_at_bemessung">laufend</field>
<field name="l10n_at_sv49_frei">mahlzeit</field>
<field name="l10n_at_st_frei">mahlzeit</field>
<field name="amount_python_compute"><![CDATA[
# § 3 Abs 1 Z 17 EStG / § 49 Abs 3 Z 12 ASVG: Gutschein bis 8 €/
# Arbeitstag frei (Kern-Cap at_st_grenzen.mahlzeit_tag). Input =
# Anz. Arbeitstage; Tageswert = tatsächlicher Bonwert (at_sb_werte).
# total = Wert × Tage (Engine: amount×qty); Überhang (Wert8)×Tage
# pflichtig (lb-sac-17).
tage = inputs['ATP_SB_MAHLZEIT'].amount
wert = float(payslip._l10n_at_param('at_sb_werte')['mahlzeit_wert'])
result = wert
result_qty = tage
]]></field>
</record>
<record id="rule_sb_betriebsveranstaltung" model="hr.salary.rule">
<field name="name">Sachbezug Betriebsveranstaltung (manuell)</field>
<field name="code">SB_BETRIEBSVERANSTALTUNG</field>
<field name="sequence" eval="42"/>
<field name="category_id" ref="l10n_at_hr_payroll_private.category_atp_brutto"/>
<field name="struct_id" ref="l10n_at_hr_payroll_private.structure_at_privat"/>
<field name="condition_select">python</field>
<field name="condition_python">result = 'ATP_SB_BETRIEBSVERANSTALTUNG' in inputs and inputs['ATP_SB_BETRIEBSVERANSTALTUNG'].amount</field>
<field name="amount_select">code</field>
<field name="l10n_at_bemessung">laufend</field>
<field name="l10n_at_sv49_frei">betriebsveranstaltung</field>
<field name="l10n_at_st_frei">betriebsveranstaltung</field>
<field name="amount_python_compute"><![CDATA[
# § 3 Abs 1 Z 14 EStG / § 49 Abs 3 Z 17 ASVG: Teilnahme bis 365 €/
# AN/Kalenderjahr frei (Jahres-Kumulative je DN); Überhang
# lohnsteuer-/beitragspflichtig (Freibetrag, lb-sac-07).
result = inputs['ATP_SB_BETRIEBSVERANSTALTUNG'].amount
]]></field>
</record>
<record id="rule_sb_sachzuwendung" model="hr.salary.rule">
<field name="name">Sachbezug Sachzuwendung/Jubiläum/Geschenk (manuell)</field>
<field name="code">SB_SACHZUWENDUNG</field>
<field name="sequence" eval="43"/>
<field name="category_id" ref="l10n_at_hr_payroll_private.category_atp_brutto"/>
<field name="struct_id" ref="l10n_at_hr_payroll_private.structure_at_privat"/>
<field name="condition_select">python</field>
<field name="condition_python">result = 'ATP_SB_SACHZUWENDUNG' in inputs and inputs['ATP_SB_SACHZUWENDUNG'].amount</field>
<field name="amount_select">code</field>
<field name="l10n_at_bemessung">laufend</field>
<field name="l10n_at_sv49_frei">jubilaeum</field>
<field name="l10n_at_st_frei">jubilaeum</field>
<field name="amount_python_compute"><![CDATA[
# § 3 Abs 1 Z 14 EStG / § 49 Abs 3 Z 17 ASVG: Sachzuwendungen
# (Jubiläums-/Weihnachtsgeschenke) bis 186 €/AN/Kalenderjahr frei —
# eigener Pool neben der Betriebsveranstaltung (lb-ges-02, lb-sac-07);
# Barzuwendungen immer pflichtig (Regel nur für Sachwerte).
result = inputs['ATP_SB_SACHZUWENDUNG'].amount
]]></field>
</record>
<record id="rule_sb_rabatt" model="hr.salary.rule">
<field name="name">Sachbezug Mitarbeiterrabatt (manuell)</field>
<field name="code">SB_RABATT</field>
<field name="sequence" eval="44"/>
<field name="category_id" ref="l10n_at_hr_payroll_private.category_atp_brutto"/>
<field name="struct_id" ref="l10n_at_hr_payroll_private.structure_at_privat"/>
<field name="condition_select">python</field>
<field name="condition_python">result = 'ATP_SB_RABATT' in inputs and inputs['ATP_SB_RABATT'].amount</field>
<field name="amount_select">code</field>
<field name="l10n_at_bemessung">laufend</field>
<field name="l10n_at_sv49_frei">rabatt</field>
<field name="l10n_at_st_frei">rabatt</field>
<field name="amount_python_compute"><![CDATA[
# § 3 Abs 1 Z 21 EStG / § 49 Abs 3 Z 29 ASVG: Rabatte über der
# 20 %-Freigrenze bis 1.000 €/AN/Jahr frei (Jahres-Kumulative);
# darüber pflichtig (lb-sac-02). Aufzeichnungspflicht des
# Arbeitgebers bleibt außen (Lohnkonto-Prüffeld).
result = inputs['ATP_SB_RABATT'].amount
]]></field>
</record>
<record id="rule_sb_homeoffice" model="hr.salary.rule">
<field name="name">Sachbezug Homeoffice-Pauschale (manuell)</field>
<field name="code">SB_HOMEOFFICE</field>
<field name="sequence" eval="48"/>
<field name="category_id" ref="l10n_at_hr_payroll_private.category_atp_brutto"/>
<field name="struct_id" ref="l10n_at_hr_payroll_private.structure_at_privat"/>
<field name="condition_select">python</field>
<field name="condition_python">result = 'ATP_SB_HOMEOFFICE' in inputs and inputs['ATP_SB_HOMEOFFICE'].amount</field>
<field name="amount_select">code</field>
<field name="l10n_at_bemessung">laufend</field>
<field name="l10n_at_sv49_frei">homeoffice</field>
<field name="l10n_at_st_frei">homeoffice</field>
<field name="amount_python_compute"><![CDATA[
# § 26 Z 9 EStG / § 49 Abs 3 Z 31 ASVG: Homeoffice-Pauschale 3 €/
# Arbeitstag, max 100 Tage/Kalenderjahr (Jahres-Kumulative). Input =
# Anz. Tage; Tageswert = 3 € (at_sb_werte.homeoffice_wert).
tage = inputs['ATP_SB_HOMEOFFICE'].amount
wert = float(payslip._l10n_at_param('at_sb_werte')['homeoffice_wert'])
result = wert
result_qty = tage
]]></field>
</record>
</odoo>
@@ -250,4 +250,26 @@ result = payslip._at_kommst(result_rules)
result = payslip._at_dz(result_rules) result = payslip._at_dz(result_rules)
]]></field> ]]></field>
</record> </record>
<!-- Wiener Dienstgeberabgabe („U-Bahn-Steuer“, Wr. DAG § 5): -->
<!-- Kopfpauschale 2 € je DN/angefangene Woche, Company-Flag -->
<!-- l10n_at_wien_dag; Befreiungen/Wochenlogik s. models/wien_dag.py -->
<record id="rule_wien_dag" model="hr.salary.rule">
<field name="name">Wiener Dienstgeberabgabe (Wr. DAG)</field>
<field name="code">WIEN_DAG</field>
<field name="sequence" eval="95"/>
<field name="category_id" ref="l10n_at_hr_payroll_private.category_atp_dg"/>
<field name="struct_id" ref="l10n_at_hr_payroll_private.structure_at_privat"/>
<field name="condition_select">python</field>
<field name="condition_python"><![CDATA[
ergebnis = payslip._at_wien_dag()
result = bool(ergebnis and ergebnis['betrag'] > 0)
]]></field>
<field name="amount_select">code</field>
<field name="l10n_at_bemessung">neutral</field>
<field name="amount_python_compute"><![CDATA[
ergebnis = payslip._at_wien_dag()
result = ergebnis['betrag']
]]></field>
</record>
</odoo> </odoo>
@@ -22,6 +22,17 @@
<field name="l10n_at_nacht_sv" eval="True"/> <field name="l10n_at_nacht_sv" eval="True"/>
</record> </record>
<!-- AP18: Nachtschwerarbeit-Tage (NSchG Art VII iVm Art XI Abs 6; -->
<!-- mandantenseitige Katalog-Klassifikation ASchV/NSchG — Zähler- -->
<!-- grundlage für den Nachtschwerarbeitsmonat) -->
<record id="wett_nachtschwer" model="hr.work.entry.type">
<field name="name">Nachtschwerarbeit (Tag)</field>
<field name="color">24</field>
<field name="display_code">NSW</field>
<field name="code">ATP_NACHTSCHWER</field>
<field name="country_id" ref="base.at"/>
</record>
<!-- Überstunden --> <!-- Überstunden -->
<record id="wett_ue_std" model="hr.work.entry.type"> <record id="wett_ue_std" model="hr.work.entry.type">
<field name="name">Überstunde Tag (50 %)</field> <field name="name">Überstunde Tag (50 %)</field>
@@ -0,0 +1,199 @@
# Benutzerhandbuch — AT-Payroll (Privatwirtschaft)
## Übersicht
Dieses Modul ermöglicht die vollständige monatliche Lohnverrechnung für
die österreichische Privatwirtschaft nach Kollektivvertrag (KV) auf
Basis von Odoo 19. Es deckt ab:
| Baustein | Funktion |
|---|---|
| Entgelt (GP2) | KV-Gehalt, AZG-Zuschläge (ÜStd, Nacht, So/Fei, Mehrarbeit), Urlaubsentgelt, 13./14. Bezug |
| Lohnsteuer (GP3) | § 66 kumulativ, § 67 Sechstel/Fixsätze, § 68-Steuerfreiheiten |
| SV/DB (GP4) | ÖGK-Beitragssätze, Geringfügigkeit, § 49-Freibeträge, FLAF, KommSt, DZ |
| Abfertigung Neu (GP5) | BMSVG 1,53 %, Anwartschaft, Endabrechnung |
| FI-Buchung (GP6) | UGB-Konten (KFS/BW 6), Journal SLR |
| Meldewesen (GP7) | mBGM/JASV, Versichertenmeldung, Lohnzettel L16, Arbeitsstättenmeldung |
| Belege (GP7) | Lohnzettel AT, Monatsdienstliste, Monatsjournal |
| Jahreswechsel (GP8) | Fristenkalender, Kontrollsechstel, §-77-Aufrollung |
---
## 1. Stammdaten anlegen
### 1.1 Unternehmen (Company)
Öffnen Sie **Einstellungen → Unternehmen** und wählen Sie Ihr
Unternehmen. Im Reiter **„Lohnverrechnung (AT)"** pflegen Sie:
| Feld | Beschreibung | Pflicht |
|---|---|---|
| Beitragskontonummer (BKNR) | 10-stellige eSV-Nummer (z. B. 1412345678) | Ja — ohne sie brechen alle Meldewesen-Läufe ab |
| Lohnsteuernummer | 9-stellig: 2 Stellen Finanzamt + 7 Stellen Steuernummer | Ja — für L16 |
| Bundesland | Betriebsstätten-Bundesland (steuert DZ-Satz) | Empfohlen |
| WKO-Mitglied | Aktivieren, wenn Ihr Unternehmen WKO-Mitglied ist (DZ-Pflicht) | Optional |
### 1.2 Mitarbeiter:in
Öffnen Sie **Personal → Mitarbeiter:innen** und legen Sie eine:n
Mitarbeiter:in an. Im Payroll-Bereich des Formulars:
1. **KV-Einstufung**: Wählen Sie den Kollektivvertrag, die
Verwendungsgruppe und die Erfahrungsstufe
2. **SV-Stichtage & Status** (GP8): Setzen Sie bei Bedarf
Satzausschaltungen (ALV ab 63, UV ab 60, PV-Bonusphase) und
Status-Flags (Lehrling, fallweise, Familienabsetzbetrag)
3. **Abfertigung Neu** (GP5): Standardmäßig aktiviert (BMSVG);
wählen Sie die MV-Kasse und pflegen Sie ggf. Erträge aus dem
Kassen-Auszug
4. **All-in** (§ 2g AVRAG): Bei All-in-Verträgen die
Überstundenpauschale und den ausgewiesenen Grundlohn eintragen
5. **Versicherungsnummer (ssnid)**: 10-stellig, Format LLLPTTMMJJ
— **Pflicht für alle Meldewesen-Läufe**
### 1.3 Versichertenmeldung (Anmeldung)
Bei Eintritt eines:r neuen Mitarbeiter:in:
1. Öffnen Sie **Meldewesen → Versichertenmeldung**
2. Klicken Sie auf **„Meldungen erzeugen (Datei)"**
3. Der Wizard erkennt offene Anmeldungen automatisch (Event-
Erkennung über die Meldungs-Registry) und erzeugt die ELDA-Datei
4. Übertragen Sie die Datei über das ELDA-Portal des Mandanten
---
## 2. Monatsabrechnung
### 2.1 Lohnlauf erstellen
1. Öffnen Sie **Payroll → Lohnläufe** und erstellen Sie einen Lauf
für den Abrechnungsmonat
2. Fügen Sie die Mitarbeiter:innen hinzu (oder nutzen Sie den
Batch-Modus für alle aktiven AN)
3. Klicken Sie auf **„Abrechnen"** — die Engine berechnet:
- KV-Grundentgelt (Katalog oder Überzahlung)
- AZG-Zuschläge (aus Work-Entries/Zeiterfassung)
- Sonderzahlungen B13 (13. Bezug, z. B. November) und B14
(14. Bezug, bei Urlaubsantritt oder Austritt)
- Lohnsteuer (§ 66 kumulativ, § 67 Sechstel/Fixsätze)
- SV-Beiträge (ÖGK DN+DG, historisch versioniert je Beitragsmonat)
- Lohnnebenkosten (FLAF-DB, KommSt, DZ)
- BV-Beitrag (1,53 % Abfertigung Neu)
4. Prüfen Sie das **Netto** und bestätigen Sie den Lauf
### 2.2 FI-Buchung
Nach Bestätigung des Laufes bucht Odoo automatisch auf die
UGB-Konten (KFS/BW 6). Prüfen Sie:
- **Aufwand**: Gehälter (6200/6000), SV-DG (6560/6500), BV (6440),
KommSt (6660), FLAF (6661), DZ (6662)
- **Verbindlichkeiten**: SV (3600), LSt (3540), FLAF (3541),
KommSt (3550), DZ (3542)
- **Netto**: Lohnverbindlichkeit (3850)
---
## 3. Meldewesen (monatlich)
### 3.1 mBGM/JASV (SV-Meldung)
**Frist: 15. des Folgemonats** (Gutschriftprinzip)
1. Öffnen Sie **Meldewesen → mBGM (JASV)**
2. Erstellen Sie einen Eintrag für den Beitragsmonat
3. Klicken Sie auf **„Lauf erzeugen (Datei)"**
4. Der Wizard prüft: BKNR vorhanden, VSNR je AN valide, Tarifsätze
parametrisiert (fehlende BSGR → listenweiser Abbruch)
5. Laden Sie die Datei ins ELDA-Portal hoch
### 3.2 Versichertenmeldung (bei Ein-/Austritt)
- **Anmeldung (M3)**: vor Arbeitsantritt — wird automatisch erkannt
- **Abmeldung (M4)**: innerhalb von 7 Kalendertagen nach
Beschäftigungsende
- **Fallweise (MA)**: je Beschäftigungstag vor Arbeitsantritt
### 3.3 Lohnzettel L16 (jährlich)
**Frist: Ende Februar des Folgejahres** (via ELDA)
1. Öffnen Sie **Meldewesen → Lohnzettel Finanz (L16)**
2. Erstellen Sie einen Eintrag für das Abrechnungsjahr
3. Klicken Sie auf **„Lauf erzeugen (Dateien)"**
4. Es werden **zwei Dateien** erzeugt:
- `l16_<Jahr>_<BKNR>.txt` — der Lohnzettel (BEST LF)
- `arbeitsstaette_<Jahr>.txt` — die Arbeitsstättenmeldung (BEST AD)
5. Beide Dateien werden über ELDA übertragen
---
## 4. Belege drucken
| Beleg | Druckort | Inhalt |
|---|---|---|
| **Lohnzettel (AT)** | Lohnabrechnung → Drucken | Bezüge, Abzüge, DG-Anteile, Netto |
| **Monatsdienstliste** | Lohnlauf → Drucken | mBGM-Kontrollgrößen je AN |
| **Monatsjournal** | Lohnlauf → Drucken | Lohnart-Summen je Lauf |
| **All-in-Deckungsrechnung** | Lohnabrechnung → Drucken | Pauschale vs. Bezüge |
| **DAG-Ausweis** | Lohnlauf → Drucken | § 1 DAG (geringfügig Beschäftigte) |
---
## 5. Fristenkalender
Öffnen Sie **Meldewesen → Fristenkalender**. Der Kalender zeigt:
- **Monatliche Fristen**: SV 15., DB/DZ/KommSt 15. des Folgemonats
- **Jahresfristen**: L16/Arbeitsstätte/§-109a Ende Februar,
KommSt-Jahreserklärung Ende März
Fristen werden automatisch per Cron erzeugt (monatlich am 1.).
Überfällige Fristen werden rot markiert. Klicken Sie auf den
Pfeil-Button, um direkt zur zugehörigen Deklaration zu springen.
---
## 6. Jahreswechsel
### 6.1 KV-Runde
1. Neue KV-Gehaltstabellen über den **KV-Katalog-Import** eingeben
(Paste oder CSV)
2. Neue `gueltig_ab`-Versionen werden automatisch angelegt;
historische Werte bleiben unberührt
### 6.2 LSt/SV-Parameterpflege
1. Neue Jahreswerte in `rule_parameters_<jahr>.xml` nachpflegen
(Beschaffung: „Beitragsrechtliche Werte" von
sozialversicherung.at; LSt-Tarif vom BMF)
2. Beispiel 2027: `data/rule_parameters_2027.xml` enthält
bereits die Struktur
### 6.3 Kontrollsechstel-Prüfung
1. Öffnen Sie **Meldewesen → Jahres-Retro-/Aufrollungslauf**
2. Wählen Sie das Jahr und klicken Sie auf **„Jahresdaten prüfen"**
3. Der Wizard zeigt je AN den §-67-Überhang und ob die
Günstig-Aufrollung (Tarif statt Fixsätze) günstiger wäre
---
## 7. BV-Endabrechnung (Austritt)
Bei Austritt eines:r im BMSVG-Systems verbleibenden AN:
1. Öffnen Sie **Abfertigung Neu → BV-Endabrechnung (Austritt)**
2. Wählen Sie den:die Mitarbeiter:in und die Beendigungsart
3. Der Wizard prüft: 36 Beitragsmonate? § 14 Abs 2-Matrix
(AN-Kündigung, verschuldete Entlassung, unberechtigter Austritt)
4. Zeigt Anwartschaft (Beiträge + Erträge) und Fristen
---
## Support
Bei Fragen wenden Sie sich an Ihren Lohnverrechnungs-Dienstleister.
Technische Dokumentation: `README.md` und `docs/` im Modul.
@@ -0,0 +1,92 @@
# Rechenfälle EFZG/Teilentgelt (AP4)
Modell: `models/efzg.py` (EFZ-Phasenmaschine, fiktive BV),
Regeln `TEIL_ENTGELT_HALB`/`KRANKENTGELT_PHASE`
(`data/salary_rules_efzg_private.xml`), Parameter `at_efz_stufen`.
## Rechtsquellen (verifiziert 2026-09-12)
- **AngG § 8 Abs 1/2/2a** (lokal, RIS) und **EFZG § 2** (RIS GesNr
10008308, geltende Fassung): Stufen 6/8/10/12 Wochen je vollendeten
Dienstjahren 0 / 1 / 15 / 25, zuzüglich je 4 Wochen halbes Entgelt;
Gleichlauf seit 1. 7. 2018 (BGBl I 153/2017).
- **Wissensbasis lb-krs-08** (Arbeitsjahrprinzip, Kalendertage,
Stufensprung verlängert, Feiertage zählen nicht, AU-Sonderkontingent
8/10 Wochen ohne Halbsatz), **lb-krs-07** (SV/Lohnsteuer/BMSVG:
laufender Bezug; fiktive BV = 50 % Vormonatsentgelt ohne SZ,
§ 7 Abs 3 BMSVG; EFZ < 50 % beitragsfrei — Halbsatz 50 % ist
pflichtig), **lb-krs-02** (Krankengeld zahlt ÖGK, nicht der
Payslip).
- „Krankengeldumlage § 62 ASVG" = Falsch-Prämise
(RECHTSQUELLEN-Privat, 2026-09-10 ausgeräumt) — kein Baustein.
## Maschinenmodell
- **Kalendertage-Maschine:** Kontingentverbrauch in Kalendertagen je
Arbeitsjahr (Eintrittsdatum-Rhythmus); Krankheits- und AU-Topf
getrennt (AU nur voll, § 8 Abs 2a AngG / § 2 Abs 5 EFZG).
Stufenbewertung je Tag (Stufensprung im laufenden Stand verlängert).
- **Entgelt-Minderung je Arbeitstag** (Work Entries ATP_KRANK/ATP_AU):
Tagesatz = laufende Monatsbasis (ohne Teilentgelt-Reduktionen) ÷
Arbeitstage des Kalendermonats — Entgeltausfallsprinzip; negative
Brutto-Regeln `TEIL_ENTGELT_HALB` (50 %/AT) und
`KRANKENTGELT_PHASE` (100 %/AT) mit Bemessung `laufend`
SV/Lohnsteuer/LNK folgen automatisch über die reduzierte Basis.
- **Fiktive BV (§ 7 Abs 3 BMSVG):** während des Krankengeldbezugs
(Halb- + ÖGK-Phase) 50 % des Entgelts des Kalendermonats vor dem
Versicherungsfall (Episode = zusammenhängender Block), aliquot nach
Kalendertagen (30-Tage-Konvention), im
`_at_bv_basis_laufen`-Hook — fließt in BVG-Zeile und mBGM-BV-Basis.
## RF-EFZ-1 — Voll/Halb/ÖGK-Übergang (1. Arbeitsjahr, 6+4 Wochen)
Eintritt 1. 7. 2026 (Stufe 6 Wochen), KV-Gruppe II/1
(2.059,41 €), Krankenstand 1. 9.15. 11. 2026 (76 Kalendertage):
- Voll: Kalendertage 142 (1. 9.12. 10.) → September ohne Reduktion;
Oktober Arbeitstage 13.31. 10. (14 AT) → Halbphase.
- Halb: Kalendertage 4370 (13. 10.9. 11.); ÖGK ab 71 (10. 11.).
- November (21 AT): halb 6 AT (2.6., 9. 11.), ÖGK 4 AT
(10.13. 11.); Tagesatz 2.059,41 ÷ 21 = 98,07 € →
`TEIL_ENTGELT_HALB` = 294,21 €, `KRANKENTGELT_PHASE` = 392,28 €.
## RF-EFZ-2 — Kontingent-Zusammenrechnung
Wiederholte Verhinderung im selben Arbeitsjahr verbraucht dasselbe
Kalendertage-Kontingent (§ 8 Abs 2 AngG): 1.9.20.9. (20 KT) +
1.10.20.10. (20 KT) + 25.10.15.11. → KT 4142 voll, ab KT 43 halb
— der dritte Krankenstand startet in der Halbphase.
## RF-EFZ-3 — Stufe 8 Wochen (2.15. Jahr)
Eintritt 1. 7. 2024 (1 vollendetes Jahr): Stufe 8 Wochen — erst
Kalendertag 57 fällt aus der Vollphase.
## RF-EFZ-4 — AU-Sonderkontingent
Nach erschöpftem Krankheits-Kontingent gebührt für Arbeitsunfall/
Berufskrankheit (eigene Urlaubsart, WET `ATP_AU`) das
Sonderkontingent 8 Wochen (ab 15 Dienstjahren 10) vollständig —
unabhängig vom Krankheits-Topf, ohne Halbsatz.
## RF-EFZ-5 — Fiktive BV in der Krankengeldphase
November 2026 (RF-EFZ-1): Halb- + ÖGK-Kalendertage 15 → fiktive BG
= 50 % × Vormonatsbasis (Oktoberslip, 2.059,41 €) × 15/30 = 514,85 €;
BV-Basis = reduziertes laufendes Entgelt (1.372,93 €) + 514,85 €;
BVG = 1,53 % × 1.887,78 € = 28,92 €.
## Offene Punkte / dokumentierte Reste
- **Kalenderjahr-Umstellung** (§ 8 Abs 9 AngG / § 2 Abs 8 EFZG per
KV/BV): Merkmal + Sonderrechnung Umstellungsjahr — Option.
- **ÜStd-/Provisionsdurchschnitte** (13 Wochen / 12 Monate) bei
wechselnden Entgelten — vorbereitete Bezugsbasis folgt mit der
Zeit-Zuschlags-Engine.
- **SZ-Kürzung** bei nicht vollem EFZ (9 ObA 209/00a, „kann"-Regel)
— KV-abhängig, Option.
- **Meldung/Nachweis, Urlaubsunterbrechung § 5 UrlG, Beendigung
§ 5 EFZG** (EFZ über Ende hinaus): prozessuale Regeln —
Fristen-/Hinweismodell mit AP8 (Arbeits- und Entgeltbestätigung).
- Erstfenster-Teilmonate mit Krankengeld-Phase: fiktive BG nur in
beitragspflichtigen Läufen (Randfall, dokumentiert).
@@ -0,0 +1,308 @@
# Endabrechnungs-Zyklus — Rechenfälle AP14-A / AP14-B / AP15-A / AP15-C
Stand: 2026-09. AP14-A bildet die Beendigungs-Abrechnung auf der
privaten Structure ab: Urlaubsersatzleistung (UEL), Abgangsentschädigung,
Aufrechnung offener Vorschüsse und Zeitguthaben/-schulden. Grundlagen
sind am Originalwortlaut verifiziert (`.ris/`, Stand 2026-09-14):
§ 11 Abs 2 ASVG, § 67 Abs 8 lit b und lit d EStG, § 19e Abs 2 AZG,
§ 41 Abs 4 FLAG.
## Rechts- und Systemgrenzen
- **UEL (§ 10 UrlG, § 67 Abs 8 lit d EStG):** Zweikomponenten — der
laufende Anteil läuft als laufender Bezug durch den Tarif des
Austrittsmonats und erhöht das Jahressechstel (nativ über
`bemessung='laufend'`); der Sonderzahlungsanteil läuft als sonstiger
Bezug durch das 6 %-Band (Freibetrag 620 €, Freigrenze 2.615 € im
Kern). Nur der laufende Anteil verlängert die Pflichtversicherung
(§ 11 Abs 2 ASVG). Tagewert-Divisoren 26 (Werktage) / 22 (5-AT); die
SV-Tage-Umrechnung (je volle Woche à N AT + 7N KT, nur ganze
Blöcke) ist Verwaltungspraxis (lb-url-15), nicht Gesetzeswortlaut.
- **Abgangsentschädigung (§ 67 Abs 8 lit b):** nach Abzug der §-62-
Beiträge ein Fünftel steuerfrei, gedeckelt mit einem Fünftel des
Neunfachen der monatlichen HBG (2026: 6.930 × 9/5 = **12.474 €**);
der Rest läuft tarifisch im Zuflussmonat — Abs 2 (Sechstel) ist nicht
anzuwenden. SV-pflichtig mit Umlegung auf den Kündigungsfrist-
Zeitraum; voll lohnnebenkostenpflichtig (FLAG § 41 Abs 4 nennt nur
§ 67 Abs 3 und 6).
- **Sequentielle Verlängerungsfenster:** Gebühren sowohl eine
Kündigungsentschädigung als auch eine UEL, ist zur Bestimmung des
Zeitraumes zunächst die Kündigungsentschädigung und im Anschluss die
Ersatzleistung heranzuziehen (§ 11 Abs 2 ASVG) — die SV-Basen werden
fensterweise auf die Monate verteilt, Standardanteile bleiben auf
die volle monatliche HBG gedeckelt, Verlängerungsanteile auf den
Tageswert (HBG × Tage/30).
- **Aufrechnung offener Vorschüsse (§ 293 Abs 3 EO):** Rückverrechnung
vom Netto nach SV und Lohnsteuer — zulässig auch gegen das
Existenzminimum; keine Abgabenberührung (lb-end-06).
- **Zeitguthaben (§ 19e Abs 2 AZG):** Guthaben an Normalarbeitszeit
+ 50 % Zuschlag, bemessen mit der Normalstunde; der Zuschlag ist
KEIN Überstundenzuschlag iSd § 68 EStG (lb-end-10). Entfall bei
unberechtigtem vorzeitigem Austritt (Eingabe weglassen); KV kann
abweichen.
## RF-END-1 — UEL-Zweikomponenten
Ende 15. 3., 11 offene AT à 120 € (2.640 € Gehalt / 22):
- `URL_ERSATZ_LFD` = 1.320 €: tarifisch im März, Sechstel +5.280 €
(annualisiert 60/15);
- `URL_ERSATZ_SZ` = 220 € (2 × 2.640/12 = 440/Monat → 20/Tag):
6 %-Band;
- SV: 1.320 € auf die Verlängerungsmonate (Tage-Schema), 220 € als
sonstige Beitragsgrundlage gegen Monats- und Jahres-HBGl.
## RF-END-2 — SV-Tage-Umrechnung (§ 11 Abs 2)
- 23 AT (5-AT-Woche) → +4×2 KT = **31 SV-Tage**;
- 12 WT (6-WT-Woche) → +2×1 KT = **14**;
- 6 AT (2-Tage-Teilzeitwoche) → +3×5 KT = **21** (lb-url-15-Beispiel).
- Ende 15. 3., 31 SV-Tage → Verlängerung 16. 3.15. 4.; UEL-lfd-Basis
tageproportional (März 16/31, April 15/31), Tageswert-Deckel je
Verlängerungsmonat.
## RF-END-3 — Abgangsentschädigung (Fünftelregelung)
9.000 € Entschädigung, 42 Tage Kündigungsfrist, DN-Satz s:
- Bemessung 9.000 × (1 s/100); davon 1/5 steuerfrei (≤ 12.474 €);
4/5 tarifisch im Zuflussmonat, ohne Sechstelerhöhung;
- SV: Umlegung 16. 3.26. 4. (Kündigungsfrist zuerst);
- LNK: volle 9.000 € in der Basis (§ 41 Abs 3 FLAG).
## RF-END-4 — Aufrechnung
2.000 € offener Vorschuss: `END_ABRECHNUNG_VORSCHUSS` mindert nur das
Netto; LSt-, SV- und LNK-Basen unverändert.
## RF-END-5 — Zeitguthaben/-schulden
10 h Normalarbeitszeit-Guthaben × 1,5 + 5 h Zeitschulden × (1):
beides laufender Bezug, kein §-68-Freibetrag.
## RF-ABF-1 — Abfertigung Alt, Vervielfachung günstiger
21 Dienstjahre ergeben nach § 23 Abs 1 AngG 9 Monatsentgelte. Bei
1.800 € laufendem Monatsarbeitslohn und 2.100 € arbeitsrechtlicher
Bemessungsgrundlage einschließlich 2/12 Sonderzahlungen:
- Bruttoabfertigung: 2.100 × 9 = **18.900 €**;
- steuerlicher Vervielfacher: 18.900 / 1.800 = **10,5** — nicht 9;
- auf den maßgeblichen laufenden Bezug entfallende Tarif-LSt laut
Ausgangsfall: **101,96 €**;
- Vervielfachungssteuer: 101,96 × 10,5 = **1.070,58 €**;
- Vergleich mit 18.900 × 6 % = **1.134 €**;
- zwingend wird der niedrigere Steuerbetrag angesetzt.
`ABF_ALT` ist SV-, BV-, DB-, DZ- und KommSt-frei, verbraucht kein
Jahressechstel und läuft als Beendigungsanspruch in den §-291d-EO-Topf.
## RF-ABF-2 — 6 % günstiger
Bei hohem laufendem Monatsarbeitslohn kann die vervielfachte Tarif-LSt
den Fixsatz überschreiten. Für 9.333,34 € Abfertigung wird dann zwingend
**560,00 €** Lohnsteuer (6 %) angesetzt.
## RF-ABF-3 — Staffel und Sonderfälle
- 3/5/10/15/20/25 Jahre → 2/3/4/6/9/12 Monatsentgelte;
- Elternaustritt nach § 23a Abs 3/4: erst nach mindestens fünf Jahren,
dann Hälfte, maximal 3 Monatsentgelte;
- Tod nach § 23 Abs 6: Hälfte des normalen Anspruchs;
- normale Arbeitnehmerkündigung, unberechtigter Austritt und
verschuldete Entlassung begründen grundsätzlich keinen Anspruch;
- Abfertigung-Neu-Verträge sind gesperrt, außer ein eingefrorener
Altanspruch wird ausdrücklich nachgewiesen.
Der Wizard schlägt Dienstzeit, Faktor und eine Basis aus laufendem Entgelt
plus aliquoten Sonderzahlungen vor. Die auf den maßgeblichen laufenden
Monatsarbeitslohn entfallende Tarif-LSt wird getrennt bestätigt, weil sie
von der §-62-Bemessung und den persönlichen Absetzbeträgen abhängt und nicht
mit dem Bruttolohn gleichgesetzt werden darf. Wegen regelmäßiger Zulagen,
Durchschnittsbezügen, Lehrzeiten, Teilübertritten und §-23a-Sonderfällen
müssen Anspruch, Basis und Nachweis vor Anlage des Laufs bestätigt werden.
Die geprüften Angaben werden am Payslip gespeichert.
## RF-ABF-4 — Freiwillige Abfertigung, Viertel-/Zwölftelregelung
15 nachgewiesene Dienstjahre (6/12), 12-Monats-Basis 36.000 € (alle
steuerbaren Bezugsteile inklusive steuerfreier Zuschläge und Sachbezüge,
ohne Sonderzahlungen und ohne §-26-Z-4-Reisevergütungen), Brutto
40.000 €:
- Z 1 Viertelregelung: 36.000 / 4 = **9.000 €** (Deckel 9 × HBG =
62.370 € nicht greifend);
- Z 2 Zwölftelregelung: 36.000 × 6/12 = **18.000 €** (Deckel
6 × 3 × HBG = 124.740 € nicht greifend);
- begünstigter Teil: min(40.000, 9.000 + 18.000) = **27.000 €** mit
6 % = **1.620 €** Lohnsteuer (`ABF_FREI`/`LST_ABF_FREI`);
- Überhang **13.000 €** als `ABF_FREI_TARIF`: tarifisch in der
LSt-Bemessung des Zuflussmonats (LST_LFD), ohne Jahressechstel-Erhöhung
(Kern-Flag `l10n_at_s67_abs6_ueberhang`);
- SV-frei (§ 49 Abs 3 Z 7 ASVG), LNK-frei unabhängig von der
Besteuerungsart (§ 41 Abs 4 lit b FLAG, § 122 Abs 8 WKG,
§ 5 Abs 2 lit b KommStG), BV-frei (§ 6 Abs 5 BMSVG); Z 7 (BV-
Anwartschafts-Ausschluss) ist seit BGBl I 118/2015 aufgehoben.
## RF-ABF-5 — Z-3-Kürzung und Deckel
4.000 € bereits erhaltene Abfertigungen plus 9.000 € gesetzlicher
Anspruch am aktuellen Ende kürzen das Z-2-Ausmaß von 18.000 € auf
5.000 €; begünstigt sind 9.000 + 5.000 = 14.000 € von 20.000 € Brutto,
der Rest läuft zum Tarif. Bei einer 12-Monats-Basis von 12.000.000 €
begrenzen die Deckel auf 62.370 € (Z 1) plus 124.740 € (Z 2, 6 × 3 ×
HBG), begünstigt sind also höchstens 187.110 €.
## RF-ABF-6 — Pfändung und Wizard
- `ABF_FREI` (begünstigter Teil) läuft über den §-291d-Beendigungstopf,
vermindert um `LST_ABF` und `LST_ABF_FREI`; der Überhang
(`ABF_FREI_TARIF`) bleibt über die laufende Bemessung pfändbar, seine
Lohnsteuer steckt bereits in `LST_LFD`.
- Der Wizard schlägt die 12-Monats-Basis aus den letzten zwölf
validierten Läufen vor (ohne Überhang- und Reise-Regeln, geminderte
Bezüge nicht automatisch zurückgerechnet) und den gesetzlichen
Anspruch aus der Alt-Abfertigung; Prüfung und Nachweise werden
bestätigt (lückenlose Vordienstzeiten zum Lohnkonto, § 76 EStG,
keine Anspruchsumwandlung, lb-end-15).
## RF-VGL-1 — Vergleichssumme Rest, Fünftelregelung (§ 67 Abs 8 lit a)
Außergerichtlicher Vergleich 9.000 € brutto für nachgeforderte
Mehleistungen (lb-end-21 Beispiel 1; kein erkennbarer Abfertigungs-
anteil, Ansprüche strittig):
- `VERGLEICH` (bemessung neutral, `l10n_at_s67_fuenftel`): nach Abzug
der §-62-DN-Beiträge (Beispiel: 18,07 % → 1.626,30 €) ist 1/5 der
Bemessung steuerfrei (1.474,74 €), Deckel 12.474 € (= 1/5 × 9 ×
HBG) greift hier nicht;
- 4/5 (5.898,96 € im Beispiel) laufen tarifisch im Zuflussmonat
(LST_LFD), ohne Jahressechstel-Erhöhung (Abs 2 nicht anzuwenden);
bei Zahlung Monate nach dem DV-Ende ohne weitere laufende Bezüge
ist die Bemessungsgrundlage nur der Vergleichsbetrag;
- SV-pflichtig im Zuflussmonat (Pauschalvergleich im Zweifel
beitragspflichtig, E-MVB 011-02-00-001); Ansprüche aus der aktiven
DV-Zeit sind je Beitragsmonat aufzurollen (Kern-Feld
`l10n_at_beitragsmonat`), Ansprüche nach DV-Ende verlängern die
Pflichtversicherung (lb-end-22);
- voll LNK-pflichtig (§ 41 Abs 4 lit b FLAG nennt nur Abs 3 und 6).
## RF-VGL-2 — BV-Anwartschafts-Teil (6 %, Deckel 7.500 €)
Vergleichssumme 9.000 €, davon Anteil für Zeiträume mit
BV-Kassen-Anwartschaft (lb-end-21 Beispiel 2):
- erste 7.500 € (`VERGLEICH_BV`): nach anteiligem §-62-Abzug
(83,33 % der Beiträge) mit 6 % = `LST_VERGLEICH_BV`;
- Überhang 1.500 € (16,67 % der Beiträge) fällt in die
Fünftelregelung zurück (1/5 steuerfrei, 4/5 tarifisch);
- SV und LNK zur Gänze pflichtig; Deckel parametrisiert
(`at_lst_67_abs8_vergleich_bv` — § 67 Abs 8 unterliegt der
Inflationsanpassung § 33a/§ 124b EStG);
- beim Übertritt Alt→Neu gilt der 6-%-Teil nur für übertragene
Zeiträume; eingefrorene Zeiträume laufen über die Fünftelregelung
(lb-end-21 Hinweis).
## RF-VGL-3 — Deckelfälle
- Rest mit Bemessungsgrundlage 70.000 € (lb-end-21 Deckelbeispiel;
das ist bereits die §-62-gekürzte Grundlage): Fünftel an der Grenze
12.474 € (9 × HBG/5), Rest tarifisch. Da die Engine erst den
§-62-Abzug ansetzt und DANACH das Fünftel deckelt, greift der Deckel
brutto-abhängig ab ca. 76.200 € (Testfall 100.000 € brutto);
- BV-Teil 10.000 €: begünstigt 7.500 € mit 6 %, Überhang 2.500 € in
die Fünftelregelung.
## RF-VGL-4 — Wizard, Aufteilung und Pfändung
- Erkennbare Abfertigungsanteile werden getrennt erfasst: gesetzliche
Abfertigung über den Abf-Alt-Abschnitt (§ 67 Abs 3; ausdrücklich
vereinbarte Abfertigung unterhalb der Klagsforderung → gesamt § 67
Abs 3, VwGH 2013/13/0001 = ARD 6490/20/2016), freiwillige über den
Abf-Frei-Abschnitt (§ 67 Abs 6); der Rest über den Vergleichs-
abschnitt. Widmungen sind nur anzuerkennen, wenn die Ansprüche
strittig waren (lb-end-22); beim Pauschalvergleich ist der
Verhältnisfaktor Forderung : Vergleichssumme zu dokumentieren
(E-MVB 011-02-00-001); Pensionsabfindungen (lit e) und
Sozialplan-Zahlungen (lit f) gehören nicht in lit a.
- Wizard-Bestätigungspflichten: Aufgliederung/Strittigkeit,
BV-Anwartschaft je betroffenem Zeitraum, BV-Teil ≤ Deckel (über-
steigender Betrag als Rest erfassen). Vergleichs-Nachweis landet
auf dem Lauf (Payslip-Tab) und ist für den gesonderten Lohnzettel
bei Zahlung nach dem Beendigungsmonat aufzubereiten (LStR Rz 911a,
GP7-Rest).
- `VERGLEICH`/`VERGLEICH_BV` laufen — wie `ABGANG` — NICHT über den
§-291d-Beendigungstopf (offener Verifikationspunkt, s. u.); der
Fünftel-Tarifteil ist über die laufende Bemessung pfändbar, seine
Lohnsteuer steckt in `LST_LFD`.
## RF-PRM-1 — Aliquote Prämie, mit Endabrechnung (§ 16 AngG)
3.000 € aliquote Jahres-/Leistungsprämie für 3 der 12 Monate
Anspruchszeitraum (voller Betrag 12.000 € × 3/12, lb-end-18):
- `PRAM_END` (bemessung „sonstig“): sonstiger Bezug innerhalb des
Jahressechstels mit 6 % (`LST_SZ`-Pfad, Freibetrag 620/Freigrenze
2.615 vom Kern geprüft), ohne Sechstel-Erhöhung;
- SZ-SV: KV/PV/AV ohne AK/WF (§ 62 Z 3/Z 5), monatliche HBG und
SZ-Jahreshöchstbetrag (Kern `_l10n_at_svdn_basis_sz`);
- voll LNK-pflichtig (DB/DZ/KommSt, lb-end-18), BV 1,53 % ungecappt
(§ 6 Abs 1 iVm Abs 5 BMSVG);
- § 16: für Angestellte unabhängig vom Beendigungsgrund gebührend;
leistungsabhängige Prämien bei Arbeitern dürfen wirksam an „kein
Verschulden“ gebunden sein; Prämien mit spezifischer Leistungs-
verknüpfung (z. B. Bilanzgeld) sind ohne Tätigkeit nicht aliquot
zu leisten. Wizard bestätigt die Leistungs-/Anspruchsgrundlage.
## RF-PRM-2 — Später fällige Prämie (§ 67 Abs 10) mit SV-Rollung
3.000 € anteilige Prämie für Leistungen im Austrittsjahr 2025, Höhe
erst nach Ablauf der Periode ermittelbar, Fälligkeit und Auszahlung
März 2026 (lb-end-18):
- `PRAM_END_TARIF` (bemessung „laufend“ + Kern-Flag
`l10n_at_s67_abs6_ueberhang`): wie ein laufender Bezug zum Tarif des
Zuflussmonats (LST_LFD), OHNE Sechstel-Erhöhung (§ 67 Abs 10);
- keine Nachzahlung iSd § 67 Abs 8 lit c, weil die Zahlung nicht
später als zum vereinbarten Fälligkeitszeitpunkt erfolgt
(98/14/0009 = ARD 5483/14/2004);
- SV-Rollung: Lauf-Feld `l10n_at_pram_beitragsmonat` ordnet die
Zeile dem Beitragsmonat des Austrittsjahres zu (E-MVB
044-01-00-006) — historische HBG (2025: 6.450 €) und Sätze des
Rollmonats, private Überschreibung von
`_l10n_at_sv_basis_laufen_monate` (Pin); LSt und mBGM-Schnittstelle
(Storno/Neuübermittlung) bleiben im GP7-Track;
- gesonderter Lohnzettel für den Kalendermonat der Zahlung
(LStR Rz 911a); LNK und BV wie RF-PRM-1.
## RF-PRM-3 — Wizard-Aliquot und Validierungen
Der Wizard-Abschnitt schlägt den §-16-Aliquot-Betrag vor (voller
Betrag × zurückgelegte Dienstzeit / Anspruchsperiode), der zahlende
Betrag bleibt bestätigbar (KV-Vereinbarung). Validierungen:
Leistungs-/Anspruchsgrundlage bestätigen, Dienstzeit ≤ Anspruchszeit-
raum, bei späterer Fälligkeit Beitragsmonat der Rollung erforderlich.
## Offene Punkte (Verifikationsreste)
- KZ-Positionierung der Abs-6-Bezüge (außerhalb KZ 210/220, LSt in
KZ 260 — GP3-Konvention wie Abs 3) und die Zuordnung des Z-6-Überhangs
zum §-291d-EO-Topf sind gegen LStR Rz 1087 ff / § 291d EO zu
verifizieren, bevor der MVP freigegeben wird. Dasselbe gilt für die
KZ-Positionierung der lit-a-Tarif-Teile (laufend über LST_LFD) und
die §-291d-Zuordnung von `VERGLEICH`/`VERGLEICH_BV`.
- Geminderte Bezüge in der 12-Monats-Basis: die LStR-Rückrechnungs-
praxis (zwölf Monate voller Bezüge) widerspricht dem VwGH
(Krankengeld 6/7 einbeziehen) — bewusst ohne Automatik, Basis ist
bestätigbar (lb-end-13 ⚠).
- Prämien-Beendigung (§ 16 AngG-Aliquotierung, Rollung) — mit AP14-B
umgesetzt (`PRAM_END`/`PRAM_END_TARIF`, Wizard-Abschnitt,
Beitragsmonat-Rollung auf dem Lauf); offen bleibt die vollständige
SZ-Aufrollung (Storno + Neurechnung gegen die SZ-Jahreshöchst-
betrag des Austrittsjahres) und das mBGM-Storno/Neuübermitteln
(GP7/Meldewesen-Track).
- Meldewesen-Schnittstellen: „Ende des Entgeltanspruches" in mBGM/
Abmeldung (GP7), gesonderter Lohnzettel bei Zahlung außerhalb des
Beendigungsmonats (LStR 2002 Rz 911a).
- Der §-62-Abzug des Fünftelpfades ist über den effektiven DN-Satz
approximiert (HBG-Caps der Verlängerungsmonate laufen im SV-Pfad).
@@ -0,0 +1,113 @@
# Karenz-/Familien-Freistellungen — Rechenfälle AP16 (8.13 freigegeben)
Karenz-Familie im Privatmodul: unbezahlte Voll-Freistellungen (Eltern-
karenz generisch, Bildungs-, Familienhospiz-/Sterbebegleitungs-,
Kinderkranken-Begleitungs-, Kinderrehabilitations- und Pflegekarenz,
Papa-Monat) mit Entgelt-Stopp, eSV-An-/Abmeldungen, fiktiver BV mit
§-7-Abs-4-Brücke, §-6-Abs-4-BV bei Teilzeit, Sonderwochengeld (§ 163
ASVG) und Ruhen (§ 166 ASVG). Grundlagen: KB lb-kar-01…06, lb-elt-
01…06, lb-kzs-01…07, lb-pap-01/02, lb-kbg-06 (Stand 2026-07) und die
verifizierten Originaltexte in `.ris/` (AVRAG, VKG, MSchG, KBGG,
FamZeitbG, BMSVG § 7, ASVG §§ 162/163/166).
## RF-KAR-1 — Entgelt-Stopp je Karenz-Art (test_01)
Bildungskarenz 1.15. 11. 2026 (§ 11 AVRAG: 212 Monate, teilbar ≥ 2,
Rahmenfrist 4 Jahre, kein Rechtsanspruch — Einvernehmen): das
Karenzgeld zahlt die **AK** (nicht der AG) — `KARENZ_ENTGELT` stoppt
das Dienstgeber-Entgelt je Arbeitstag (negatives Tagesatz-Muster von
`MSCH_ENTGELT`, geteilter Tagesatz-Helfer `_at_efz_tagesatz`, der
Ausschlüsse mit EFZG/MSCH teilt). 10 AT → (2.059,41/21) × 10.
Gleiches Muster für Hospizkarenz (§ 14a, 3/6 Monate), Begleitung
schwerst erkrankter Kinder (§ 14b, 5/9 Monate), Kinderrehabilitation
(§ 14e, 4 Wochen/Jahr), Pflegekarenz (§ 14c, 13 Monate, unteilbar)
und Papa-Monat (§ 1a VKG, 1 Monat).
## RF-KAR-2 — eSV-An-/Abmeldungen (vr.event)
- Abmeldung zum **Ende des Entgeltanspruchs** = Tag vor dem Karenz-
beginn; Anmeldung zum ersten Arbeitstag nach dem Karenzende —
entfällt, wenn das DV mit der Karenz endet (normale Vertrags-
Abmeldung).
- **Papa-Monat/Familienzeit tagesgenau:** Abmeldung am Tag **vor**
Beginn der Familienzeit (§ 11 Abs 3 lit a ASVG — unbezahlter Urlaub
hält die Pflichtversicherung nur bis 1 Monat, nicht bei Familien-
zeit; lb-pap-01), Anmeldung vor Arbeitsantritt.
- Karenzart-Strings („Familienhospizkarenz/Pflegekarenz An-/Abmeldung",
lb-kzs-05) laufen als Protokoll-Info; erweiterte eSV-Sonderformate
= GP7-Rest. Idempotenz über die Registry (employee, art, adatum).
## RF-KAR-3 — Fiktive BV mit Karenz-Brücke (§ 7 Abs 4 Z 13 BMSVG)
Mutterschutz-Frist 1.15. 11. 2026 (Verbot 1) → Elternkarenz
16. 11. 202628. 2. 2027 → neuerliches Beschäftigungsverbot 1. 3. 2027
(Z 1, unmittelbar nach der Karenz): die fiktive BV-Basis des
neuerlichen Verbots bemisst sich nach dem **Monatsentgelt des
Kalendermonats vor dem Verbot 1** — Referenzmonate Aug/Sep/Okt 2026
(statt Dez 2026Feb 2027, die karenzbedingt leer wären). Z 2
(Beschäftigung < 3 Monate zwischen Karenz und Verbot): gleicher
Stichtag. Z 3 (neues DV binnen 3 Monaten nach Beendigung des
karenzierten DV mit Karenz): der letzte volle Kalendermonat vor dem
neuerlichen Verbot als EINZELNER Referenzmonat („volles Monatsentgelt").
## RF-KAR-4 — BV-Vor-Entgelt bei Pflegeteilzeit (§ 6 Abs 4 BMSVG)
20-h-Teilzeit (work_time_rate 0,5) mit Vor-Reduzierungs-Entgelt
2.059,41 €: die BV-Basis läuft während der Maßnahme auf dem
Vor-Reduzierungs-Entgelt (§ 6 Abs 4 BMSVG), nicht auf der reduzierten
laufenden Basis (1.029,71 €). Bildungs-/Hospizteilzeit analog über
das Feld `l10n_at_bv_vor_entgelt` am Vertrag.
## RF-KAR-5 — Sonderwochengeld (§ 163 ASVG) und Ruhen (§ 166 ASVG)
- Mutterschutz-Frist **innerhalb** einer Karenz: kein Wochengeld
(§ 162), sondern **Sonderwochengeld** — Zahlung durch die ÖGK
(erhöhtes Krankengeld § 141 Abs 2, Bemessungsgrundlage § 125 aus
dem Arbeitsverdienst vor dem Ende des letzten Entgeltanspruchs,
§-108f-Anpassung); Ausweis im Mutterschutz-Status.
- Ruhen (§ 166 Abs 1): Fortbezug > 50 % (Entgeltfortzahlung 100 %-
Phase) → Ruhen zur Gänze, = 50 % (Halbphase) → hälftig; Zeiten der
Urlaubsersatzleistung/UEL/KE gelten nicht als weitergeleistete
Bezüge; Z 3 Erwerbstätigkeit (in Höhe des Einkommens) und Z 4
Pflegekarenzgeld (in dessen Höhe) = Bezugszeitraum-Daten der ÖGK
(Hinweis).
## RF-KAR-6 — Elternteilzeit: Entgelt und Beendigungs-Basen
- Entgelt: letzter voller Lohn/Gehalt, aliquotiert nach der
herabgesetzten Wochenstundenzahl (lb-elt-02) — maschinell über
`work_time_rate` der Version (Versionskalender ÷ Firmenzalender).
- SZ-Mischberechnung (§ 15j Abs 7 MSchG / § 8b Abs 7 VKG): B13/B14
bemessen je Monat aus dem jeweiligen (reduzierten) Monatsentgelt ×
Dienstzeitfaktor — VZ-/TZ-Anteile im Kalenderjahr ergeben sich
automatisch.
- Endabrechnungs-Wizard: §-23-Abs-8-Hochrechnung (DV während
MSchG-/VKG-Teilzeit: frühere Normalarbeitszeit = Monatsentgelt ÷
work_time_rate, Vorschau); für Arbeitnehmerkündigung § 23a Abs 4a
(5-Jahres-Ø der geleisteten Arbeitszeit ohne Karenz, manuell);
UEL § 10 Abs 4 UrlG (überwiegende Arbeitszeit des Urlaubsjahres).
Dienstzeitanrechnung: Karenzzeiten je Kind in vollem Umfang
(§ 15f Abs 1 MSchG, Geburten ab 1. 8. 2019, max. 22./24. Lebens-
monat); Papa-Monat anrechnen (§ 1a Abs 7 VKG).
## Offene Punkte (AP16-Reste)
- **Urlaubs-Aliquotierung aus Karenz** (§ 15f Abs 2 MSchG: 30 WT/25 AT
: 365 × aktive KT, Teiltage aufrunden) greift in die hr_holidays-
Allokationsseite — eigener Folgeschritt; Urlaubsverjährung
verlängert sich um die Karenz (§ 4 Abs 5 UrlG).
- **Krankheit während Karenz/Mutterschutz** ist als gleichzeitige
Abwesenheit in hr.leave nur mit einer Überlappungs-Policy-
Entscheidung umsetzbar (Odoo sperrt Quere-Überlappungen;
`allow_request_on_top` erfordert time_type „other") —
Mandanten-Entscheid; der Test umgeht mit dem offiziellen
leave_skip_date_check-Bypass.
- Fristen der AG↔AN-Kommunikation (3-/2-Monats-Bekanntgaben,
5/10-Arbeitstage, Begründungspflichten 5 Kalendertage) = Help-Texte;
keine Fristen-Automatik.
- eSV-Sonderformate (Karenzart-Block „Familienhospizkarenz/Pflege-
karenz") und mBGM-Feinschliff = GP7; Sonderwochengeld-Vollrechnung
(§ 141 Abs 2/§ 125/§ 108f) = Anzeige-Stufe, kein Eigenrechenpfad.
- lb-kar-02: Folgen der 13-Wochen-Überschreitung + § 2i-AVRAG-Konflikt
und lb-elt-06 §-8-/§-8a-VKG-Zuordnung — keine Implementierung auf
kontroversen Literaturansichten (dokumentiert ⚠).
@@ -0,0 +1,112 @@
# Rechenfälle Meldewesen/Belege (GP7) — ELDA/eSV, L16, Belege
**Quellen:** DM-ORG 42.7.0 „Datenaustausch mit Dienstgebern", 42.
Ergänzung 07/2026 (`.firecrawl/ris/elda/` — Volltext-PDF + pdftotext;
Satzstrukturen PDF-verifiziert extrahiert 2026-09-12, Positions-
Ketten lückenlos); Pruefkatalog L16 ab 2026 (Stand 12/2025, xlsx);
KB lb-lvr-04/05 (L16), lb-mel-08 (Fristen), lb-bes-02/03 (fallweise),
lb-sva-14 (SZ-Sätze), lb-swa-05 (Schwerarbeit, out); Nutzer-
Entscheidungen 5.15.4 (Arbeitsstättenmeldung in GP7; Mitteilungsfile-
Import Rest; **FB+ = keine Personalverrechnung** — bei Steuer < 0
Arbeitnehmerveranlagung, **Kind-Modell für die FABO+-Wartung**; alles
in einem GP7). Umsetzung: Kern `l10n_at_hr_payroll` 19.0.4.0.0
(`models/elda.py`, `lohnzettel_l16.py`, `kind.py`, `meldewesen.py`,
`monatsreports.py`, `report/belege_reports.xml`), privat
19.0.7.0.0 (`models/mbgm.py`, `versichertenmeldung.py`,
`data/rule_parameters_mbgm_private.xml`). Tests: `test_elda_l16`,
`test_meldewesen_private`.
## Systematik (GP7, verifiziert)
- **ELDA-Fixlängen-Dateien** (C.1/C.2, E.1E.3): Datei = optionaler
Dateiheader (DHKZ 2×blank, DHVN 01, ENCD **ISO-8859-15**) + Daten-
bestand (Vorlaufsatz SART 00 mit BEST/VSTR/VERS → Datensätze →
Schlusssatz SART 99 mit SANZ inkl. Vorlauf/Schluss); a/n linksbündig
blank-gefüllt, n rechtsbündig null-gefüllt, **Cent ohne
Interpunktion**, Prozentsätze ×1000 (3 Nachkommastellen), SANR
lückenlos ab 1. **CRLF als Satztrenner = Praxis-Annahme** (die
DM-ORG normiert kein Trennzeichen — Parallellauf verifizieren).
- **mBGM/JASV** (BEST `MB`, Version 02; E.32): Hierarchie Paket PS
(BKNR/DGNA/BZRM/GSUM/ANZM) → mBGM G1 (REFW/VSNR/FANA/VONA/VSUM/
VERG=1) → Tarifblock T1 (BSGR/VVON) → Verrechnungsbasis BS (VBTY/
VBBT) → Verrechnungsposition V1 (VPTY/VPVZ/VPTA/RSVZ/RSUM).
**KEIN DN/DG-Zweig-Split**: T01 = GESAMT-Prozentsatz des Tarifsystems
(Parameter `at_mbgm_tarif`, normativ: laufend **39,05 %** =
KV 7,65 + PV 22,80 + AV 5,90 + UV 1,10 + WF 1,00 + IESG 0,10; SZ
**37,55 %** = DN KV 3,87 + PV 10,25 + AV 2,95 + DG 20,48). Abschläge
als EIGENE Positionen je Basis: **A01/A02/A03** = Minderung AV auf
2/1/0 % (§ 2a AMPFG — nur DN-AV, **je Verrechnungsbasis AB UND SZ**;
A03 = 2,95 % der Basis, RSVZ ), A09 = UV-Entfall 60 (1,10 %), A10
= AV+IE-Entfall (6,00 %), A15 = PV-Halbierung (11,40 %);
Konkurrenz a4: Entfall vor Minderung (A10 sperrt A0x). **BV**:
Basis `BV` (GP5, ungedeckelt) + `V01` 1,53 %. RSUM = Basis × Satz
(mBGM-Tarifierung); Konsistenz gegen GP4-Payslip-Summen als
Prüfwarnung (Toleranz 0,10 €). Storno: R-Satz je stornierter mBGM
(REFU, VSUM wie Original, ohne T/BS/V). Fehlanzeige: Paket ohne
mBGM-Sätze (ANZM 0). Fallweise/Geringfügige ohne Parametrisierung:
Lauf bricht listenweise ab (BSGR B044/B045/B010/B030 —
Tarifsystem-Werte pflegen, keine Erfindung).
- **L16 Lohnzettel Finanz** (BEST `LF`, VSTR `94` BRZ; E.13/E.14):
I1 (Länge 1100, Strukturversion 03) + L1 (Länge **3500**,
Lohnzettelversion 28, zwingend ab 01.03.2026). **KZ 210 =
Bruttobezüge gem. § 25 GESAMT** (laufende + sonstige §-67-Bezüge,
DM-ORG-Feldbeschreibung!) — deshalb ist **KZ 245 = 210 215 220
230 243** die laufende Steuerbemessung und per Konstruktion mit
der GP3-§-66-Kette konsistent. KZ-Ableitung aus den Kern-Flags
(kein paralleles Klassifikationsfeld): 210 = laufend+sonstig § 67
(ohne Abs-3/6), 215 = `_l10n_at_freibetraege_68` (§-68-Caps je LZP),
220 = sonstig ohne Abs-3/6 (inkl. 620-€-Freibetrag vor SV), 225 =
SVDN_SZ, 230 = laufende SVDN+AK+WF, 243 = 0 (nicht im Inventar),
260/BIEL = LST_LFD + LST_SZ. FB+-Felder (BFABO/KFABO/VFABO/FABO +
Kinderblock 15×123 ab Pos. 1501) aus der **Kind-Stammdaten-Wartung**
(Nutzer-Entscheidung: FB+ rechnet nicht im Lohnsteuerkern — ANV).
REFN je AG+Jahr+DV eindeutig; berichtigter Lohnzettel: ARTL 18 +
KORR K + FIND J.
- **Arbeitsstättenmeldung** § 34 Abs 6 ASVG (BEST `AD`, VSTR `ST`
Statistik Austria; E.20, SART 45, Länge 300): je AN Adresse am
31. 12. aus dem Dienstort-Partner; **GKZ** (Gemeindekennziffer) als
manuelles Partner-Feld — Warnung, wenn fehlt.
- **Versichertenmeldung** (BEST `VR`, Version 03, Länge 772; E.29):
M3 vor Arbeitsantritt (BKNR/VSNR/GEBD/FANA/VONA/ADAT/BBER 0104/
GERF/FRDV/BVAB/BVJN aus GP5/VWAZ Stunden×100), M4 7 Tage (ADAT +
EBSV, AGRD aus dem hr.departure.reason-Mapping, Standard 00),
M6 nur Ausnahmefälle; **deklarative Event-Erkennung** über die
Meldungs-Registry (keine write-diff-Hacks; idempotente Läufe).
**Fallweise** (BEST `MA`, SART 32, Länge 450, Version 07): je
Beschäftigungstag vor Arbeitsantritt; MVP: je AN/Vertragsbeginn
ein Anmeldetag (tagesgenaue Einsatztage via Work-Entries = Rest).
- **Belege (AP6)**: Lohnzettel AT (ir.actions.report auf hr.payslip,
AT-Layout mit Bezügen/Abzügen/**DG-Anteilen**/Netto; per Structure
über `report_id`), Monatsdienstliste (mBGM-Kontrollschicht je AN:
BG laufend/SZ, SV-DN/SZ, SV-DG, LSt, Netto) + Monatsjournal (je
Lohnart Summen je Lauf) auf `hr.payslip.run` (QWeb, AbstractModel).
Kein HR-Modul nutzt `account.report` — QWeb ist der framework-
gerechte Weg (Contribution-Register-Muster).
## Rechenfälle (Fixtures)
| RF | Konstellation | Erwartung |
|---|---|---|
| RF-ELDA-1 | Feld-Formatter | a/n linksbündig blank; n rechtsbündig 0-gefüllt; Cent kaufmännisch (0,005 → 1); Prozentsatz skala 3 (39,30 → 039300) |
| RF-ELDA-2 | Bestand/Datei | Vorlauf SART 00 (VSTR/BEST/EDAT TTMMJJJJ/VERS), SANR lückenlos, SANZ inkl. Rahmen, CRLF, Dateiheader ` 01ISO-8859-15…`, Umlaute ISO-8859-15-codiert |
| RF-L16-1 | November mit B13 (Jahr 2026) | KZ 210 = 4.118,82 (§ 25 gesamt!), KZ 220 = 2.059,41, KZ 245 = laufende Bemessung > 0; I1 (1.100) mit STNRA/JAHR/GESA; L1 (3.500) exakte Positionen: REFN/ARTL 01/BELZ 0111/SOZS 3/AVLN 1234/AGBD 150590/B210 0000411882; Vorlauf VSTR 94, BEST LF, VERS 28; SANZ 4 |
| RF-L16-2 | Arbeitsstätte | Satz 45 (300): BKNR/VSNR/STNR; VSTR ST, BEST AD; Warnung GKZ fehlt |
| RF-MBGM-1 | November mit B13, Angestellte (B002) | PS (BKNR/BZRM 112026/ANZM 1/GSUM = Σ VSUM), G1 (VSNR), T1 BSGR B002, Positionen T01 039050, **A03 je Basis** (AB + SZ, RSVZ ), T02 037550, V01; GSUM ≈ GP4-Beitragssummen ± 0,10 € |
| RF-VR-1 | Vertrag ab 2020, Ende 15.02.2026 | M3 (ADAT 01012020, BBER 02, GERF N, FRDV N, VWAZ 4000, BVJN J); Registry verhindert Doppelanmeldung; M4 (ADAT 15022026, EBSV, AGRD 00) |
## Offene Punkte / bewusst nicht in GP7
- **Schwerarbeitsmeldung** (DM-ORG E.22 Satzstruktur existiert!):
blockiert — keine ASchV-Work-Entry-Typen/Zähler im Modul, ASchV in
RQ-Privat ❓ nicht erschlossen. Erst nach Datenmodell-Bau.
- **§ 109a** (W1-Satz): separater Baustein (Honorare außerhalb der
Lohnverrechnung); Mitteilungs-XSDs/Prüfkatalog beschafft.
- **L17** (A1-Satz): keine AG-Pflicht. **Mitteilungsfile-Import**
(Rückantwort-Fehler ins Deklarationsmodell): GP8-Rest (5.2).
- Geringfügigen-mBGM (B010/B030-Sätze, Z04-Jahreszahlung + 2,5 %):
parametrisieren, sobai vorkommend; mBGM je Lauf bei Mehrfachläufen
im Monat; M6/Änderung; FB+-Lohnsteuermechanik (ANV-Sache);
VSNR-Prüfziffer-Algorithmus (SVS-Quelle beschaffen);
tagesgenaue fallweise-Einsatztage (Work-Entry-Auswertung).
- Referenzbuchhaltung/Parallellauf: CRLF-Satztrenner, ELDA-Kanäle
(Sendemodul https/ftps 40/220 MB), Fristenkalender (GP8).
@@ -0,0 +1,79 @@
# Rechenfälle Mutterschutz/Wochengeld (AP7)
Modell: `models/mutterschutz.py`, Regel `MSCH_ENTGELT`
(`data/salary_rules_msch_private.xml`), Parameter
`at_msch_sz_prozentsatz` (⚠ satzungsabhängig, vor Erstverwendung
verifizieren).
## Rechtsquellen (verifiziert 2026-09-12)
- **MSchG § 3 Abs 1** (Achtwochenfrist vor der voraussichtlichen
Entbindung, ärztliches Zeugnis) und **§ 5 Abs 1** (8 Wochen
Wochenbett; mindestens 12 bei Früh-/Mehrlingsgeburten oder
Kaiserschnitt; Verlängerung um vorzeitig verkürzte Achtwochenfrist,
max. 16 Wochen) — lokal, RIS.
- **ASVG § 162 Abs 1/3/4** (lb-msf-03, Stand 2026-07): Wochengeld =
**Leistung aus der Krankenversicherung**; Bemessung nach dem Ø-
Arbeitsverdienst der letzten 13 Wochen (drei Kalendermonate),
vermindert um Lohnsteuer, SV-Anteil, Kammerumlage, WBF-Beitrag,
Aufwandersätze, SW-Beitrag; erhöht um den satzungsmäßigen
**SZ-Prozentsatz (14/17/21 %)**; je Kalendertag.
- **BMSVG § 7 Abs 4** (RIS, verifiziert): während des Anspruchs auf
(Sonder-)Wochengeld bei aufrechtem Arbeitsverhältnis fiktive
BV-Grundlage = Monatsentgelt aus den letzten **drei
Kalendermonaten vor dem Versicherungsfall der Mutterschaft,
einschließlich anteiliger Sonderzahlungen**; Spezial-Referenzmonat
bei neuerlichem Verbot unmittelbar nach Karenz (Z 13, dort Monat
vor dem der Karenz vorangegangenen Verbot) — dokumentierte Option.
## Rollenverteilung (klärt die frühere Plan-Annahme)
Die ÖGK zahlt das Wochengeld direkt (§ 162 Abs 1 ASVG) — der
Dienstgeber zahlt für die Verbotstage **kein Entgelt**
(`MSCH_ENTGELT`-Reduktion je Arbeitstag), liefert die
Bemessungsdaten (hier vollständig berechnet; AEB mit AP8) und führt
die BV auf fiktiver Grundlage weiter (§ 7 Abs 4 BMSVG).
Eine „Dienstgeber-Wochengeldzahlung" (deutsches
Mutterschaftsgeld-Modell) existiert in Österreich nicht.
## RF-MS-1 — Entgelt-Stopp und fiktive BV
Eintritt 1. 7. 2026, KV-Gruppe II/1 (2.059,41 €), Referenzläufe
Aug/Sep/Okt voll entlohnt; Mutterschutz-Frist 1.15. 11. 2026
(15 Kalendertage, 10 Arbeitstage):
- `MSCH_ENTGELT` = (2.059,41 ÷ 21 AT) × 10 = 980,67 €.
- Fiktive BV-BG (§ 7 Abs 4) = Ø-Monatsentgelt der Monate AugOkt
(je 2.059,41, keine SZ-Zeilen) × 15/30 = 1.029,71 €.
- Wochengeld-Ausweis: Ø-Netto der drei Referenzläufe + 17 %
SZ-Prozentsatz ÷ 92 Kalendertage (31+30+31).
## RF-MS-2 — Wochengeld-Berechnung (§ 162 Abs 3)
Je Kalendertag: (Σ Netto der drei Referenzmonate × (1 + SZ-%)) ÷
Kalendertage der drei Monate. Kurzes Dienstverhältnis: Ø über die
vorhandenen Monate; ohne jede Historie: Netto des laufenden Monats
(lb-msf-03). SZ-Fortzahlung während des Bezugs (vereinbart) wäre
reale Grundlage statt der fiktiven — Option.
## RF-MS-3 — Fristenmodell
Schutzfristen als Urlaubsart „Mutterschutz-Frist" (WET `ATP_MSCH`,
Krankmeldungspflicht-Feld zweckentfremdbar); die Berechnung der
Achtwochenfrist aus dem ärztlichen Zeugnis (§ 3 Abs 2 MSchG) und
Verlängerungen (§ 5 Abs 1) bleibt beim Mandanten.
## Offene Punkte / dokumentierte Reste
- **Sonderwochengeld** (§ 163 ASVG, Günstigkeitsvergleich § 162
Abs 3b; Versicherungsfall ≤ 13 Wochen nach Karenzende bzw.
Teilzeit nach KBG) — Option, braucht Verdienthistorie vor dem
letzten Entgeltanspruch inkl. Valorisierung.
- **Ruhen** (§ 166 ASVG): EFZ > 50 %, Erwerbseinkommen,
Pflegekarenzgeld — Abrechnungs-/Melde-Schnittstelle (AP8-Kontext).
- **SZ-Prozentsatz** 14/17/21 % satzungsabhängig — Parameter vor
Erstverwendung gegen die Satzung des KV-Trägers verifizieren.
- **Karenz-Referenzmonat** (§ 7 Abs 4 Z 13 BMSVG): Folgeschwangerschaft
unmittelbar nach Karenz — Sonder-Bemessungsmonat, Option.
- **AEB SART 75/76** (Arbeits- und Entgeltbestätigung Wochengeld,
seit 2025 inkl. Prämien-/Provisionsfelder netto) — AP8.
@@ -0,0 +1,69 @@
# Pendlerförderung — Rechenfälle AP13-B
Stand: 2026-09. AP13-B umsetzt Pendlerpauschale (PP) und Pendlereuro (PE)
sowie die L-34-Verhältnisdaten inkl. §-26-Z5-Tickets auf der privaten
Structure. Grundlagen: lb-pen-01/02/04/06/08/09 und § 16 Abs 1 Z 6 EStG.
## Rechtsanker
- PP = Freibetrag der laufenden LSt-Bemessungsgrundlage; Staffeln 2026
klein 696/1.356/2.016 € (ab 20/>40/>60 km), groß 372/1.476/2.568/3.672 €
(ab 2/>20/>40/>60 km); nächsthöhere Stufe erst bei **mehr** als dem
unteren km-Wert.
- Fahrten-Stufe: 47 Fahrten → 1/3, 810 → 2/3, ab 11 → voll.
- Untermonatige Beschäftigung: Jahreswert × Stufe / 360 × Lohnsteuertage
des LZP (lb-pen-09).
- PE 2026: 6 €/km/Jahr (Budgetbegleitgesetz 2025), aliquotiert mit der
Fahrten-Stufe, direkter LSt-Absetzbetrag, Fixkopplung zum PP-Anspruch.
- Arbeitgebereigenes Kfz für die Pendelstrecke sperrt PP und PE; ein
arbeitgebereigenes Dienstfahrrad sperrt nicht.
- § 26 Z 5 lit a Werkverkehr: PP nur für die nicht abgedeckte Reststrecke
— datengetrieben über die L-34-Reststrecke.
- § 26 Z 5 lit b Öffi-Ticket: AG-Ticketkosten mindern das PP anteilig
(§ 16 Abs 1 Z 6 lit i); Bezugsumwandlung sperrt PP/PE nicht (Ticket =
steuerpflichtiger laufender Bezug).
- PP/PE erfordern die L 34 EDV mit Pendlerrechner-Ausdruck; Änderungen der
Verhältnisse sind binnen eines Monats zu melden — im Modell als neue
Gültigkeitsperiode (`l10n.at.payroll.pendler.erklaerung`).
## RF-PEN-1 — kleines PP voll, 25 km
PP = 696/12 = 58,00 €/Monat Freibetrag; PE = 25 × 6 = 150 €/Jahr.
Kein SV- und kein LNK-Effekt.
## RF-PEN-2 — Fahrten-Stufe 1/3
20 km, 4 Fahrten: PP = 696 × 1/3 / 12 = 19,33 €; PE = (20 × 6) × 1/3 = 50
€/Jahr.
## RF-PEN-3 — untermonatig (lb-pen-09)
Beginn 25. 4., 5 Pendeltage, 25 km, klein 1/3: PP = 696 × 1/3 / 360 × 6
= **3,87 €**.
## RF-PEN-4 — Öffi-Ticket
AG-Ticketkosten 30 €/Monat bei kleinem PP 58 €: PP = 28 €; PE bleibt
(Fixkopplung zum Restanspruch). Kosten ≥ PP → PP = 0 → PE = 0.
## RF-PEN-5 — Werkverkehr
PP auf der Reststrecke aus der L-34 (Datenpflege); PE folgt der Reststrecke.
Überwiegende Werkverkehr-Beförderung mit voll abgedeckter Strecke →
keine Erklärung mit Reststrecke → kein PP/PE.
## Engine-Bindung
- PP: Abzug in `_l10n_at_bemessung_laufen` (Privat-Override) — wirkt auf
die §-66-Hochrechnung, nicht auf SV/LNK.
- PE: Ergänzung in `_l10n_at_absetzbetraege` — Jahreswert × Stufe, wie der
Verkehrsabsetzbetrag über die kumulative Rollung verbraucht.
- Lohnkonto-Ausweise (zustehender PE, Kfz-Monate, Werkverkehrsmonate) sind
Report-Thema (lb-pen-06) und nicht Bestandteil dieses MVP.
## Offene Punkte (dokumentiert, nicht geblockt)
- Erhöhter Verkehrsabsetzbetrag (853 €) und Pendler-Zuschlag (804 €) sind
einkommensabhängige Jahreskomponenten — Quer-LSt-Rest (siehe Plan).
- L 34-Änderungsmeldung binnen Monatsfrist ist Prozessregel; das Modell
stützt die monatsgenaue Historie, prüft die Frist nicht.
@@ -0,0 +1,111 @@
# Rechenfälle Lohnpfändung (AP11)
Modell: `models/pfaendung.py` (Pfändungsvormerkung + Existenzminimum-
Maschine), `models/pfaendung_report.py` (DSE), Regeln `PFAENDUNG`/
`PFAENDUNG_SZ` (`data/salary_rules_pfaendung_private.xml`), Parameter
`at_eo_azr`/`at_eo_kostenersatz` (`data/rule_parameters_pfaendung_private.xml`).
## Rechtsquellen (verifiziert 2026-09-13)
- **EO § 291a** (Existenzminimum) — Wortlaut
`.additional_docs/Lexis360_291a_eo.pdf` (NOR40233792, idF 1.7.2021):
(1) allg. Grundbetrag = AZR; (2) +1/6 bei keinen §-290b-Leistungen
(erhöhter allg. Grundbetrag), +20 % je Unterhaltsperson (max 5);
(3) 30 % des Mehrbetrags + 10 % je Person (max 5); über dem Vierfachen
des AZR (HöBG) jedenfalls pfändbar; (5) Grundbeträge auf volle Euro,
Höchstteil nach § 291 Abs 2 (20-€-Schritte).
- **EO § 292 Abs 4** (absolutes Geldexistenzminimum = halber allg.
Grundbetrag) — `.additional_docs/Lexis360_292_eo.pdf` (NOR40187912).
- **AZR** am RIS (KB-KONFLIKTE A9, 3. RIS-Runde 2026-09-11): 2025
1.273,99 € (BGBl II 417/2024), 2026 1.308,39 € (BGBl II 263/2025).
- **KB** lb-pfa-01…14 (Stand 2026-07), wk-pfa-01: Einlangen/Rang
(lb-pfa-01/10), Durchführung/§ 304 (lb-pfa-07), Kostenersatz
§ 292h/§ 302 (lb-pfa-09), Beendigungsansprüche § 291d (lb-pfa-03),
Vorschüsse § 290c + abs. EX (lb-pfa-12), Unterhalt § 292f (lb-pfa-11),
Sachbezüge (lb-pfa-14).
## Maschinenmodell (2026, AZR 1.308,39 €)
- **Werte (Parameter `at_eo_azr`):** allg. Grundbetrag **1.308 €** ·
erhöhter (×7/6) **1.526 €** · Unterhaltsgrundbetrag (20 %) **261 €**
(max 5) · HöBG (4×, 20-€-Abrundung) **5.220 €**/Bezugsmonat ·
absolutes Geld-EX (½) **654 €**.
- **Formel** (§ 291a, gegen lb-pfa-03 verifiziert — s. RF-PFA-3):
`grund = (allg|erh) + n·unterhalt`; `hoechst = 5.220 × bezugsmonate`;
`mehr = max(0, min(basis, hoechst) grund)`;
`EX = min(basis, grund + (0,30 + 0,10·n)·mehr)`; `pfändbar = basis EX`.
- **Töpfe:** laufend (Netto nach SV-DN/LSt, ohne unpfändbare Bezüge),
13./14. (eigener Kreis, allg. Grundbetrag), Beendigung (erhöhter
Grundbetrag, HöBG × Bezugsmonate, lb-pfa-03).
- **Bemessungsart** `neutral` (DN-Abzug nach SV/LSt, kein SV-/LSt-Effekt);
Kategorien `ATP_ABZUG`, seq 75/76 (nach LSt 71, vor NET 100).
## RF-PFA-1 — Laufender Topf, 0 vs. 2 Unterhaltspflichten
SZ-Anspruch (erhöht **nicht**), basis (Netto) 2.000 €:
- **n=0:** grund 1.308 · mehr 692 · EX 1.308 + 0,30×692 = **1.515,60 €**
· pfändbar **484,40 €**.
- **n=2:** grund 1.308 + 2×261 = 1.830 · mehr 170 · EX 1.830 + 0,50×170
= **1.915,00 €** · pfändbar **85,00 €**.
## RF-PFA-2 — Höchstberechnungsgrundlage (Cap)
n=0, basis 6.000 € (> HöBG 5.220 €): mehr gedeckelt auf 5.2201.308 =
3.912 · EX = 1.308 + 0,30×3.912 = **2.481,60 €** · pfändbar =
6.000 2.481,60 = **3.518,40 €** (davon 780 € = 6.0005.220 jedenfalls
pfändbar, 2.738,40 € aus dem Bereich bis HöBG).
## RF-PFA-3 — Beendigungstopf (lb-pfa-03, exakt)
Abfertigung „Alt" 6×6.000 € brutto, 6 % LSt → basis **33.840 €**;
n=2; erhöhter Grundbetrag (Beendigung); 6 Bezugsmonate:
- grund = 1.526 + 2×261 = **2.048 €** · HöBG = 5.220×6 = **31.320 €**
- basis > HöBG → mehr = 31.320 2.048 = 29.272
- EX = 2.048 + 0,50×29.272 = **16.684,00 €** ✓ (KB: 16.684 €)
- pfändbar = 33.840 16.684 = **17.156,00 €** ✓ (KB: 17.156 €)
## RF-PFA-4 — SZ-Anspruch vs. erhöhter Grundbetrag
basis 2.000 €, n=0:
- **mit SZ-Anspruch** (allg. 1.308): EX **1.515,60 €**, pfändbar 484,40 €.
- **ohne SZ-Anspruch** (erhöht 1.526): mehr 474 · EX 1.526 + 0,30×474
= **1.668,20 €** · pfändbar **331,80 €** (niedrigerer Bezug →
höherer Schutz).
## RF-PFA-5 — Absolutes Geldexistenzminimum (§ 292 Abs 4)
2026: **654 €** (= ½ × 1.308,39 € abgerundet) — bleibt dem DN bei
Rückverrechnung (Vorschüsse/Darlehen, lb-pfa-12) jedenfalls verbleiben;
bei Unterhaltspfändung 75 % (490,50 €, dokumentierter Rest).
## RF-PFA-6 — Kostenersatz (§ 292h EO)
Pfändbare Masse 1.000 €: erste Zahlung 2 %/max 8 € = **8,00 €** +
weitere 1 %/max 4 € auf (1.0008) = **4,00 €** → Kostenersatz
**12,00 €** (minded nur den an den Gläubiger gehenden Anteil, nie das
EX; USt: keine, VwGH 2011/15/0181).
## RF-PFA-7 — DN-Abzug im Lauf (PFAENDUNG-Regel)
Mitarbeiter mit KV-Entgelt + aktive Pfändung: `PFAENDUNG`-Zeile (laufend
Topf) = berechnungsgrundlage(laufend) Existenzminimum; Netto wird
entsprechend reduziert; `PFAENDUNG_SZ` greift nur im SZ-Monat.
## Offene Punkte / dokumentierte Reste
- **Unterhaltsexekution (75 %) & Koinzidenz/Unterhaltsmasse** (lb-pfa-05):
bei pfändenden Unterhaltsgläubigern niedrigeres EX (75 %, exekutierte
Pflicht zählt nicht mit) + exklusive Unterhaltsmasse — Vollmechanik
(mehrere Pfändungen, Rang, Masse) im Parallellauf verifizieren;
MVP nutzt die Schuldner-Unterhaltspflichten (n) in der Grundform.
- **Vorschüsse/Nachzahlungen** (§ 290c EO, lb-pfa-12): Neutralität +
Aufrollung; Schnittstelle über abs. Geld-EX (RF-PFA-5) vorbereitet.
- **Sachbezugspfändung** (lb-pfa-14): Schnittstelle AP12 (Flag
`l10n_at_pfandung_unpfandbar` / Sachbezugsbasis).
- **Beendigungs-Regel-Anbindung** (AP14/15): Engine + RF-PFA-3 vorhanden,
Regel folgt mit Abfertigung/Urlaubsersatzleistung.
- **2027-Werte** (AZR): bei Jahreswechsel über `at_eo_azr` nachführen
(Wartung, RUNBOOK).
@@ -0,0 +1,60 @@
# Reisekosten — Rechenfälle AP13-A
Stand: 2026-09. AP13-A verarbeitet belegte **inländische**
Reisekosten über Odoos `hr_payroll_expense`-Ablauf. Die Reisekostenabrechnung
wird dem nächsten Payslip zugeordnet; die Standardregel `EXPENSES` trägt den
gesamten Ersatz und die Verbindlichkeitsabstimmung.
## Rechts- und Systemgrenze
- § 26 Z 4 EStG: Inland-Taggeld maximal 30,00 € je 24 Stunden,
bei mehr als drei Stunden 1/12 je angefangener Stunde; Nächtigungspauschale
17,00 € oder belegte tatsächliche Kosten.
- Amtliches Kilometergeld: Pkw/Kombi 0,50 €, Motorrad 0,25 €,
Fahrrad/Fußweg 0,25 €, notwendiger Mitfahrer 0,15 €; insgesamt maximal
30.000 km im Kalenderjahr einschließlich Familienheimfahrten.
- Übersteigende oder nicht dokumentierte Beträge sind laufender Arbeitslohn
und damit LSt-, SV- und lohnnebenkostenpflichtig.
- Auslandssätze, § 3 Abs. 1 Z 16b EStG und die detaillierte Einsatzortdauer
sind nicht Bestandteil von AP13-A.
## RF-REI-1 — Taggeld im Höchstausmaß
Dienstreise 10 Stunden, Auslagenersatz 25,00 €:
- Höchstausmaß: 10 × 30/12 = 25,00 €;
- `EXPENSES`: 25,00 € netto ersetzt;
- keine LSt-, SV- oder LNK-Wirkung.
## RF-REI-2 — Taggeld-Überhang
Dienstreise 10 Stunden, Auslagenersatz 40,00 €:
- steuerfrei: 25,00 €;
- Überhang: 15,00 €;
- `EXPENSES` ersetzt 40,00 €, `REISE_STEUERPFLICHTIG` erhöht die laufende
Bemessungsgrundlage um 15,00 €, `REISE_NETTOAUSGLEICH` verhindert einen
zweiten Ersatz dieser 15,00 €;
- die darauf entfallende DN-SV und LSt vermindern die Nettauszahlung.
## RF-REI-3 — Kilometergeld und Jahresgrenze
500 km mit Pkw, Anspruch 250,00 €:
- 500 × 0,50 € = 250,00 € steuerfrei bei Reisekostenaufzeichnung;
- nach 29.800 bestätigten Jahreskilometern sind nur 200 km bzw. 100,00 €
steuerfrei, 150,00 € sind laufender Arbeitslohn.
## RF-REI-4 — Familienheimfahrt
Eine dokumentierte Heimfahrt am arbeitsfreien Tag wird als eigene Reiseart
geführt und zählt in dieselbe 30.000-km-Jahresgrenze. Eine zweite erstattete
Heimfahrt in derselben Kalenderwoche wird vor Einreichung abgewiesen.
Für denselben arbeitsfreien Tag darf kein steuerfreies Taggeld kumulieren.
## Buchungsanker
`EXPENSES` muss auf ein Verbindlichkeitskonto buchen. Die AT-
Buchungsmatrix ordnet es dem abstimmbaren Lohnverbindlichkeitskonto 3850 zu;
`hr_payroll_expense` gleicht dieses gegen die aus der Spesenabrechnung
entstandene Mitarbeiterverbindlichkeit ab.
@@ -0,0 +1,173 @@
# Rechenfälle steuerfreie Sachbezüge (AP12a)
Kern: Feld `hr.salary_rule.l10n_at_st_frei` + Methoden
`_l10n_at_st_frei_ytd`/`_l10n_at_st_frei_freie_laufen` (Anbindung
`_l10n_at_bemessung_laufen`) in `l10n_at_hr_payroll/models/lohnsteuer.py`,
Parameter `at_st_grenzen` (`l10n_at_hr_payroll/data/rule_parameters_lst_2026.xml`).
Privat: Eingabetypen `ATP_SB_*` (`data/input_types_private.xml`), Regeln
`SB_MAHLZEIT`/`SB_BETRIEBSVERANSTALTUNG`/`SB_SACHZUWENDUNG`/`SB_RABATT`/
`SB_HOMEOFFICE` (`data/salary_rules_sb_private.xml`), Parameter
`at_sb_werte` (`data/rule_parameters_sb_private.xml`), LNK-Anbindung
`_at_lnk_basis` (`models/payslip_private.py`). SV-Seite: bestehendes
`l10n_at_sv49_frei`/`at_sv49_grenzen` (die SB-Regeln setzen beide Flags).
## Rechtsquellen (verifiziert 2026-09-13)
- **Mahlzeiten** (lb-sac-17): § 3 Abs 1 Z 17 EStG / § 49 Abs 3 Z 12 ASVG —
„großer" Essensbon **€ 8,00/Arbeitstag** steuer- und beitragsfrei
(Voraussetzungen: keine Verpflegungszulage u. a., sonst nur € 2,00);
Jahresbemessung 220 Arbeitstage; Übersteigerung pflichtig.
- **Betriebsveranstaltungen** (lb-sac-07, lb-sva-03): § 3 Abs 1 Z 14 EStG /
§ 49 Abs 3 Z 17 ASVG — Teilnahme bis **€ 365,/AN/Kalenderjahr** frei;
keine Barablöse.
- **Sachzuwendungen/Geschenke** (lb-ges-02): § 3 Abs 1 Z 14 EStG —
Weihnachtsgeschenke sowie Dienst-/Firmenjubiläen je bis **€ 186,/Jahr**
(gesonderter Pool neben der BV); Barzuwendungen immer pflichtig.
- **Mitarbeiterrabatte** (lb-sac-02): § 3 Abs 1 Z 21 EStG / § 49 Abs 3
Z 29 ASVG — Rabatte **über 20 %** mit **€-1.000,-Freibetrag/Jahr** je
AN/AG; darunter keine Bezug; darüber abgabenpflichtig (laufend oder
jährlicher Sachbezug).
- **Homeoffice/Telearbeit** (lb-tel-04/05): § 26 Z 9 EStG / § 49 Abs 3
Z 31 ASVG — Telearbeit-Pauschale **€ 3/Tag, max 100 Tage/Jahr**
(= € 300/Jahr) nicht steuerbar; Lohnkonto: Anzahl Telearbeitstage
erfassen.
- Alle Freibeträge: **nur der übersteigende Teil** ist beitrags-/steuer-
pflichtig (Freibeträge, keine Freigrenzen), lohnnebenkostenfrei.
## Maschinenmodell (2026)
- **Eingabe-Semantik** (manuell, `hr.payslip.input.amount`):
- tagesbasiert (Mahlzeiten/Homeoffice): `amount` = **Anz. Arbeitstage**;
Tageswert aus `at_sb_werte` (`mahlzeit_wert` Default 8,0 = tatsächl.
Bonwert, `homeoffice_wert` 3,0 = gesetzlich fix); Regel: `result =
wert`, `result_qty = tage` → Engine: `total = wert × tage`,
`quantity = tage` (für die Cap-Berechnung Cap × Tage).
- poolbasiert (BV/Sachzuwendung/Rabatt): `amount` = **Gesamtwert des
Monats**; Regel: `result = amount` (`quantity` = 1).
- **Freibetrag je Lauf** (Kern, `_l10n_at_st_frei_freie_laufen`,
Spieglung `_l10n_at_sv49_freie_laufen`): tagesbasiert
`frei = min(total, cap × menge)`; Homeoffice zusätzlich
`rest_tage = 100 YTD-Menge`, poolbasiert `frei = min(total,
pool YTD)`. YTD = Summe der Zeilen (Menge bzw. Total) **validierter**
früherer Läufe des Kalenderjahres je DN (`_l10n_at_ytd_slaeufe`).
- **Bemessungen**: LSt-BG laufend = Arbeitslohn SVDN § 68
`st_frei`; LNK-BG = (laufend + im Monat gezahlter § 67) `st_frei`
§ 67 Abs 3/6 → SV-DN sind aus der LSt-BG abzugsfähig, aus der
LNK-BG nicht (Zuflussprinzip, kein SV-Abzug).
- **Caps** (`at_st_grenzen`, `at_sv49_grenzen`, 2026): Mahlzeit 8,0 €/AT ·
Rabatt 1.000 €/J · BV 365 €/J · Jubiläum 186 €/J · Homeoffice
3,0 €/AT, 100 Tage/J.
## RF-SB-1 — Mahlzeiten voll im Freibetrag
10 Gutschein-Arbeitstage à 8,0 € (`mahlzeit_wert`):
- `SB_MAHLZEIT`: total **80,00 €**, quantity 10.
- frei (SV + LSt) = min(80, 8 × 10) = **80,00 €** → SV-BG, LSt-BG und
LNK-BG **unverändert** gegenüber dem Lauf ohne SB (Tests test_01).
## RF-SB-2 — Mahlzeiten mit Überhang (Bonwert 11,00 €)
`at_sb_werte.mahlzeit_wert` = 11,00 (versioniert), 10 AT:
- `SB_MAHLZEIT`: total **110,00 €** (11 × 10), quantity 10.
- frei = min(110, 8 × 10) = **80,00 €** → Überhang **30,00 €**.
- SV-BG +30,00 · LNK-BG +30,00 · LSt-BG +30,00 **abzüglich** der darauf
entfallenden SV-DN (SV abzugsfähig: ΔLSt + ΔSVDN = 30,00 €) (test_02).
## RF-SB-3 — Betriebsveranstaltungen (365-€-Pool)
Jänner 300 € (validiert), Februar 100 €:
- Jänner: frei min(300, 3650) = **300 €** → voll frei.
- Februar: YTD 300 → frei min(100, 365300) = **65 €**, Überhang **35 €**
in SV-/LSt-(netto SVDN)/LNK-Bemessung (test_03).
## RF-SB-4 — Eigene Pools: Sachzuwendung (186) & Rabatt (1.000)
- **Sachzuwendung**: Jänner 100 € (frei), Februar 100 € → frei
186100 = **86 €**, Überhang **14 €** (test_04).
- **Rabatt**: Jänner 700 € (frei, über 20 %), Februar 500 € → frei
1.000700 = **300 €**, Überhang **200 €** (test_04).
- Pools sind unabhängig (BV 365 ≠ Sachzuwendung 186).
## RF-SB-5 — Homeoffice (100-Tage-Limit)
70 Tage (validiert), dann 80 Tage:
- Jänner: 70 × 3 = **210 €** frei (70 ≤ 100 Tage).
- Februar: rest 10070 = 30 Tage → frei 30 × 3 = **90 €**; Zeile
80 × 3 = 240 € → Überhang **150 €** pflichtig (test_05).
## RF-SB-6 — Ohne SB-Eingaben neutral
Keine `ATP_SB_*`-Eingaben → keine `SB_*`-Zeilen,
`_l10n_at_st_frei_freie_laufen()` = `_l10n_at_sv49_freie_laufen()` = 0
→ Bemessungen wie ohne AP12a (test_06).
## AP12b — Wertgebundene Sachbezüge (2026)
Stammdaten liegen auf `hr.version` (ein Fahrzeug/eine Wohnung pro
Dienstverhältnis im MVP), ereignisbezogene Werte auf `ATP_SB_*`-Eingaben.
Die Werte sind vollständig versioniert: `at_sb_wagen`, `at_sb_wagen_co2`,
`at_sb_wohnung`, `at_sb_parkplatz`.
- **Dienstwagen** (lb-sac-03/04, § 4 SBWVO): AK × 2 % (über Grenzwert,
cap 960) bzw. 1,5 % (Grenzwert erfüllt, cap 720); halber SB bei
nachgewiesenen ≤ 500 Privat-km/Monat: 1 %/0,75 % (caps 480/360).
CO₂-Grenzwert folgt dem Erstzulassungsjahr; 0 g/km = kein Ansatz;
laufender Kostenbeitrag mindert nach § 4 Abs 7.
- **Dienstwohnung** (lb-sac-09, § 2 Abs 7a SBWVO): m² × Richtwert je
Bundesland; Normwohnung 30 %, Hausbesorger 35 % (kombiniert 55,5 %);
arbeitsplatznah/nicht Lebensmittelpunkt: ≤ 35 m² = 0, über 3545 m²
35 % während der ersten 12 Überlassungsmonate.
- **Parkplatz** (lb-sac-18, § 4a SBWVO): 14,53 €/Monat minus DN-
Kostenbeitrag, soweit keine dokumentierte Ausnahme vorliegt. AP12b
behandelt ihn als laufenden Bezug; der Quelltext nennt die Kategorie
nicht explizit (⚠ Annahme vor Produktivsetzung gegen LStR/RIS prüfen).
- **Handy** (lb-sac-15): beruflich/fallweise privat = 0; überwiegend
private Mehrkosten sind laufender manueller Bezug. Verkauf unter
Buchwert ist als Eingabe „Buchwert minus Kaufpreis“: SV **laufend**,
LSt **sonstig (§ 67)**, LNK-pflichtig.
- **Bonusmeilen** (lb-sac-08): Einlösungswert ist Drittlohn, ausschließlich
SV-laufend (§ 49 Abs 1 ASVG), kein eigenes LSt-/LNK-Brutto. Die darauf
entfallende DN-SV mindert jedoch gemäß bestehender §-62-Kette die LSt-BG.
## RF-SB-7 — Dienstwagen
AK 36.700 €, EZ 2025, CO₂ 112 g/km (≤ 126): 36.700 × 1,5 % =
**550,50 €**. Halber Sachbezug: **275,25 €**. Bei 130 g/km:
36.700 × 2 % = **734,00 €**. E-Fahrzeug (0 g/km) = **0 €**.
Kostenbeitrag 300 €: 550,50 300 = **250,50 €**.
## RF-SB-8 — Dienstwohnung und Parkplatz
- Wien, 72,4 m²: 72,4 × 6,67 = **482,91 €**.
- Arbeitsplatznahe Kleinwohnung: 30 m² = **0 €**; 40 m² im Monat 1:
40 × 6,67 × 65 % = **173,42 €**.
- Parkplatz: 14,53 € minus 5 € Kostenbeitrag = **9,53 €**.
## RF-SB-9 — Handy und Bonusmeilen
- Private Handy-Mehrkosten 80 € nur bei Profil „überwiegend privat“:
**80 € laufend**; berufliches Profil erzeugt keine Zeile.
- Arbeitsmittel-Verkauf (Buchwert 800 € Preis 100 €): **700 €**;
SV-Basis laufend +700, SV-SZ-Basis 0, LSt-sonstig und LNK +700.
- Bonusmeilen, Einlösungswert 300 €: SV-Basis +300, LNK 0, kein eigenes
LSt-Brutto; Auszahlungsnetto vermindert sich nur um die DN-SV abzüglich
der daraus folgenden LSt-Entlastung.
## Offene Punkte / dokumentierte Reste
- **Dienstwagen:** Mini-Sachbezug (Fahrtenbuch/km-Wert/Jahresvergleich),
Fahrzeugwechsel, Pool/Fahrgemeinschaft, Mehrfachfahrzeuge und einmalige
Kostenbeiträge (lb-sac-04) folgen außerhalb des MVP.
- **Kleinwohnung:** Aufrollung/Nachversteuerung beim Überschreiten der
12-Monats-Frist (§ 67 Abs 10), Mehrnutzer- und Gemeinschaftsflächen-
aufteilung sind noch nicht automatisiert.
- **Jobticket** (SBV § 4b): erst ab AP13.
- **Lohnkonto-Aufzeichnungspflichten** (Mahlzeitenvoraussetzungen
lb-sac-17, Telearbeitstage lb-tel-05, Rabatte > 20 % lb-sac-02 sowie
Nachweise/Erklärungen AP12b): Arbeitgeber-Prüffeld, nicht Engine.
- **2027-Werte**: alle `at_st_grenzen`/`at_sv49_grenzen`, `at_sb_werte`
und AP12b-Parameter am Jahreswechsel nachführen (Wartung/RUNBOOK).
@@ -6,4 +6,20 @@ from . import bv_kasse
from . import abfertigung_private from . import abfertigung_private
from . import bv_report from . import bv_report
from . import account_chart_private from . import account_chart_private
from . import account_posting_private from . import account_posting_private
from . import mbgm
from . import versichertenmeldung
from . import fristen_private
from . import buchungsbeleg
from . import wien_dag
from . import efzg
from . import mutterschutz
from . import nachtschwerarbeit
from . import karenz
from . import aeb
from . import pfaendung
from . import pfaendung_report
from . import sachbezuege_private
from . import reisekosten
from . import pendler
from . import endabrechnung
@@ -206,24 +206,64 @@ class HrPayslip(models.Model):
Erstfenster 30-Tage-Monat (KB-Beispiel 4.500/30 × 17 = Erstfenster 30-Tage-Monat (KB-Beispiel 4.500/30 × 17 =
2.550); Teilmonate über die Lohnsteuertage, um Doppelabzüge 2.550); Teilmonate über die Lohnsteuertage, um Doppelabzüge
zu vermeiden (das Entgelt ist für die Teilzeit bereits zu vermeiden (das Entgelt ist für die Teilzeit bereits
aliquotiert).""" aliquotiert). AP16 (§ 6 Abs 4 BMSVG): während einer
Arbeitszeit-Reduzierung mit gesetztem Feld
l10n_at_bv_vor_entgelt (z. B. Pflegeteilzeit § 14d AVRAG)
läuft der Beitrag auf dem Vor-Reduzierungs-Entgelt."""
self.ensure_one() self.ensure_one()
pflichtig_ab, kurz_av = self._at_bv_fenster() pflichtig_ab, kurz_av = self._at_bv_fenster()
if kurz_av: if kurz_av:
return 0.0 return 0.0
vor_entgelt = self.version_id.l10n_at_bv_vor_entgelt
basis = self._l10n_at_sv_basis_roh(result_rules) basis = self._l10n_at_sv_basis_roh(result_rules)
if vor_entgelt:
basis = max(basis, float(vor_entgelt))
if pflichtig_ab is None or pflichtig_ab <= self.date_from: if pflichtig_ab is None or pflichtig_ab <= self.date_from:
return basis # AP4: fiktive BG während des Krankengeldbezugs (§ 7 Abs 3
# BMSVG) und des Wochengeldbezugs (§ 7 Abs 4, AP7) — nur
# in beitragspflichtigen Läufen (Erstfenster-Teilmonate:
# dokumentierter Randfall). AP18/AP19: Präsenz-/Ausbildungs-
# und Zivildienst-Fiktion (§ 7 Abs 1/2, ATP_PRD_TAGE).
return basis + self._at_bv_fiktiv_krankengeld() \
+ self._at_bv_fiktiv_wochengeld() \
+ self._at_bv_fiktiv_prasenzenzdienst()
if pflichtig_ab > self.date_to: if pflichtig_ab > self.date_to:
# LZP liegt zur Gänze im beitragsfreien Erstfenster. # LZP liegt zur Gänze im beitragsfreien Erstfenster.
return 0.0 return 0.0
ende = min(self.date_to, tage = (min(self.date_to,
self.version_id.contract_date_end or self.date_to) self.version_id.contract_date_end or self.date_to)
tage = (ende - pflichtig_ab).days + 1 - pflichtig_ab).days + 1
if tage <= 0: if tage <= 0:
return 0.0 return 0.0
return basis * tage / max(self._l10n_at_lohnsteuertage(), 1) return basis * tage / max(self._l10n_at_lohnsteuertage(), 1)
def _at_bv_fiktiv_prasenzenzdienst(self):
"""Fiktive BV-Grundlage während des Präsenz-/Ausbildungs-
und Zivildienstes (§ 7 Abs 1/2 BMSVG; RIS-Verifikation lokal
BMSVG-Volltext 2025-07, lb-vor-04): 1,53 % der fiktiven BG in
Höhe des KBGG § 3 Abs 1 idF vor BGBl I 53/2016 2026: 435,90
(Parameter at_bv_fiktiv_kbg, Stand lb-vor-04) aliquot auf
die Dienst-Tage je Kalendertage des LZP. >12 Monate
Zeitsoldat/Auslandseinsatz-/Ausbildungsdienst: AG-Beitrag
entfällt (Bund zahlt) mandantenseitig berücksichtigen,
Eingabe nicht setzen."""
self.ensure_one()
tage = 0
for eingabe in self.input_line_ids:
if eingabe.input_type_id.code == 'ATP_PRD_TAGE' \
and eingabe.amount:
tage += int(eingabe.amount)
if not tage:
return 0.0
kbg = float(self.env['hr.rule.parameter']
._get_parameter_from_code(
'at_bv_fiktiv_kbg', self.date_from,
raise_if_not_found=False) or 0.0)
if not kbg:
return 0.0
monatstage = (self.date_to - self.date_from).days + 1
return kbg * min(tage, monatstage) / float(monatstage)
def _at_bv_basis_sonst(self, result_rules=None): def _at_bv_basis_sonst(self, result_rules=None):
"""BV-Grundlage sonstiger Bezüge (Sonderzahlungen & Co.): """BV-Grundlage sonstiger Bezüge (Sonderzahlungen & Co.):
ungecappt (kein HBG-/Jahresdeckel, § 6 Abs 5) und ohne ungecappt (kein HBG-/Jahresdeckel, § 6 Abs 5) und ohne
@@ -52,6 +52,10 @@ L10N_AT_BUCHUNGSMATRIX = {
'rule_ue_sofei': {'debit': ('6310',)}, 'rule_ue_sofei': {'debit': ('6310',)},
'rule_ue_sofei_nacht': {'debit': ('6310',)}, 'rule_ue_sofei_nacht': {'debit': ('6310',)},
'rule_pram': {'debit': ('6230',)}, 'rule_pram': {'debit': ('6230',)},
# hr_payroll_expense requires a payable debit account on EXPENSES;
# use the reconcilable payroll payable account for its automatic
# expense-to-payslip reconciliation.
'rule_expenses': {'debit': ('3850',)},
'rule_b13': {'debit': ('6240',)}, 'rule_b13': {'debit': ('6240',)},
'rule_b14': {'debit': ('6240',)}, 'rule_b14': {'debit': ('6240',)},
'rule_b14_rueck': {'debit': ('6240',)}, 'rule_b14_rueck': {'debit': ('6240',)},
@@ -64,6 +68,25 @@ L10N_AT_BUCHUNGSMATRIX = {
'rule_svdn_sz': {'credit': ('3600',)}, 'rule_svdn_sz': {'credit': ('3600',)},
'rule_lst_lfd': {'credit': ('3540',)}, 'rule_lst_lfd': {'credit': ('3540',)},
'rule_lst_sz': {'credit': ('3540',)}, 'rule_lst_sz': {'credit': ('3540',)},
'rule_lst_abf': {'credit': ('3540',)},
'rule_lst_abf_frei': {'credit': ('3540',)},
'rule_lst_vergleich_bv': {'credit': ('3540',)},
# Gesetzliche Abfertigung Alt: bis zur Referenzbuchhaltungs-
# Fixierung allgemeiner Gehalts-/Lohnaufwand, company-dependent
# Regelkonto bleibt übersteuerbar.
'rule_abf_alt': {'debit': ('6200',), 'debit_arbeiter': ('6000',)},
# Freiwillige Abfertigung (§ 67 Abs 6): begünstigter Teil und
# tarifischer Überhang — gleiche vorläufige Aufwandskonten.
'rule_abf_frei': {'debit': ('6200',), 'debit_arbeiter': ('6000',)},
'rule_abf_frei_tarif': {'debit': ('6200',), 'debit_arbeiter': ('6000',)},
# Vergleichssumme (§ 67 Abs 8 lit a): Rest und BV-Anwartschafts-
# Teil — gleiche vorläufige Aufwandskonten wie die Abfertigungen.
'rule_vergleich': {'debit': ('6200',), 'debit_arbeiter': ('6000',)},
'rule_vergleich_bv': {'debit': ('6200',), 'debit_arbeiter': ('6000',)},
# Prämie bei Beendigung (§ 16 AngG): mit Endabrechnung und später
# fällig — Prämien-Aufwandskonto wie rule_pram.
'rule_pram_end': {'debit': ('6230',)},
'rule_pram_end_tarif': {'debit': ('6230',)},
# Nettoauszahlung (reconcilable, s. o.) # Nettoauszahlung (reconcilable, s. o.)
'rule_netto': {'credit': ('3850',)}, 'rule_netto': {'credit': ('3850',)},
# DG-Beiträge (650655 SV Arbeiter · 656659 SV Angestellte) # DG-Beiträge (650655 SV Arbeiter · 656659 SV Angestellte)
@@ -103,6 +126,10 @@ L10N_AT_BUCHUNGSMATRIX = {
'credit': ('3550',)}, 'credit': ('3550',)},
'rule_dz': {'debit': ('6662',), 'debit_arbeiter': ('6602',), 'rule_dz': {'debit': ('6662',), 'debit_arbeiter': ('6602',),
'credit': ('3542',)}, 'credit': ('3542',)},
# Wr. DAG („U-Bahn-Steuer“): Aufwand 6663 (l10n_at-Template,
# KB lb-lnk-04 verifiziert); Credit 3560 Referenzbuchhaltungs-
# Rest (Stadtkasse Wien — Kandidat, fehlend-Warnung des Hooks).
'rule_wien_dag': {'debit': ('6663',), 'credit': ('3560',)},
} }
@@ -0,0 +1,561 @@
# Part of the odoo-at-payroll project. License: LGPL-3.
"""AP8: Arbeits- und Entgeltbestätigungen (AEB) — ELDA-Bestände
``AK`` (Krankengeld, SART 70/71, Version 12) und ``AW`` (Wochengeld,
SART 75/76, Version 10), DM-ORG 42.7.0 Kapitel E.10/E.11 + G.10/G.11
(Satzstrukturen PDF-verifiziert aus der lokalen DM-ORG-Extraktion
2026-09-12; Positionsketten lückenlos geprüft: Krankengeld 111 Felder
bis Stelle 1392, Wochengeld 64 Felder bis Stelle 1048).
Fachliche Rollen (lb-krs-02 / lb-msf-03): Krankengeld und Wochengeld
zahlt die KV; die AEB ist die Bemessungsdaten-Lieferung des
Dienstgebers nach Ausschöpfung des vollen EFZ-Anspruchs (Krankengeld)
bzw. bei Mutterschutz-Frist (Wochengeld; AGRD dort nur 02/03/04/05/06/
10/22/24, D.22). Storno (E.10.2.1) nur bei Fehler oder Änderung der
Bemessungsgrundlage sonst Korrektur per Neumeldung.
Datenquellen: EFZ-Phasen-Karte (``hr.version._l10n_at_efz_phasen``,
AP4/AP8-Refaktur) für Anspruch (AEFZ/JAGU/TAGU), Teilentgelt-Phasen
(TPR/TVO/TBI), Vorerkrankungen (ANV/ANB) und volles-Entgelt-Ende
(VENT); Payslip-Historie für die Bezugsblöcke (BVO/BBI/BBE) bzw. den
Wochengeld-Arbeitsverdienst (AVER = Ø-Netto der drei Referenzmonate,
§ 162 Abs 3 ASVG AP7-Logik).
Grundstellung ohne Modell (dokumentierter MVP-Rest): Kündigungs-
entschädigung/Urlaubsersatzleistung, Einarbeitungs-/Feiertagsfelder,
Vordienstzeiten (§ 2 Abs 3 EFZG), Prämien-/Provisionsausweis
Prüfkatalog-Verifikation im Parallellauf (Prämisse P7)."""
import base64
from datetime import timedelta
from odoo import fields, models
from odoo.exceptions import UserError
from odoo.addons.l10n_at_hr_payroll.models.elda import (
L10N_AT_ELDA_IDTEIL,
L10nAtEldaBestand,
L10nAtEldaDatei,
L10nAtEldaSatz,
)
from odoo.addons.l10n_at_hr_payroll.models.lohnzettel_l16 import \
l10n_at_namen
# ------------------------------------------------------------------
# G.10 Arbeits- und Entgeltbestätigung für Krankengeld Version 12
# (SART 70/71) — nach IDTEIL, Satzlänge 1392.
# ------------------------------------------------------------------
AEB_KRANKENGELD_SATZ = [
(2, 'BKNR', 21, 10, 'a'),
(3, 'DGNA', 31, 70, 'a'),
(4, 'DTEL', 101, 50, 'a'),
(5, 'WOBD', 151, 12, 'a'),
(6, 'ZOBD', 163, 15, 'a'),
(7, 'VSNR', 178, 10, 'n'),
(8, 'GEBD', 188, 8, 'n'),
(9, 'FANA', 196, 70, 'a'),
(10, 'VONA', 266, 70, 'a'),
(11, 'AKGR', 336, 30, 'a'),
(12, 'WFKZ', 366, 3, 'a'),
(13, 'PLZL', 369, 9, 'a'),
(14, 'WORT', 378, 40, 'a'),
(15, 'STRA', 418, 50, 'a'),
(16, 'BEAB', 468, 8, 'n'),
(17, 'KABE', 476, 1, 'n'),
(18, 'TAET', 477, 30, 'a'),
(19, 'BTAG', 507, 1, 'n'),
(20, 'TATU', 508, 1, 'a'),
(21, 'LTAG', 509, 8, 'n'),
(22, 'GRUN', 517, 30, 'a'),
(23, 'KZKU', 547, 1, 'a'),
(24, 'ARFT', 548, 7, 'a'),
(25, 'ARTX', 555, 25, 'a'),
(26, 'BLOE', 580, 1, 'a'),
(27, 'EBSV', 581, 8, 'n'),
(28, 'IBAN', 589, 34, 'a'),
(29, 'BIC', 623, 11, 'a'),
(30, 'BVO1', 634, 8, 'n'),
(31, 'BBI1', 642, 8, 'n'),
(32, 'BBE1', 650, 9, 'n'),
(33, 'BVO2', 659, 8, 'n'),
(34, 'BBI2', 667, 8, 'n'),
(35, 'BBE2', 675, 9, 'n'),
(36, 'BVO3', 684, 8, 'n'),
(37, 'BBI3', 692, 8, 'n'),
(38, 'BBE3', 700, 9, 'n'),
(39, 'BVO4', 709, 8, 'n'),
(40, 'BBI4', 717, 8, 'n'),
(41, 'BBE4', 725, 9, 'n'),
(42, 'BSUM', 734, 9, 'n'),
(43, 'SZKZ', 743, 1, 'a'),
(44, 'SBGB', 744, 1, 'a'),
(45, 'SBZT', 745, 24, 'a'),
(46, 'KEAB', 769, 8, 'n'),
(47, 'KEBI', 777, 8, 'n'),
(48, 'UEAB', 785, 8, 'n'),
(49, 'UEBI', 793, 8, 'n'),
(50, 'VENT', 801, 8, 'n'),
(51, 'AEFZ', 809, 2, 'n'),
(52, 'JAGU', 811, 1, 'n'),
(53, 'TAGU', 812, 1, 'n'),
(54, 'TPR1', 813, 2, 'n'),
(55, 'TVO1', 815, 8, 'n'),
(56, 'TBI1', 823, 8, 'n'),
(57, 'TPR2', 831, 2, 'n'),
(58, 'TVO2', 833, 8, 'n'),
(59, 'TBI2', 841, 8, 'n'),
(60, 'ANV1', 849, 8, 'n'),
(61, 'ANB1', 857, 8, 'n'),
(62, 'ANV2', 865, 8, 'n'),
(63, 'ANB2', 873, 8, 'n'),
(64, 'ANV3', 881, 8, 'n'),
(65, 'ANB3', 889, 8, 'n'),
(66, 'ANV4', 897, 8, 'n'),
(67, 'ANB4', 905, 8, 'n'),
(68, 'ANV5', 913, 8, 'n'),
(69, 'ANB5', 921, 8, 'n'),
(70, 'ANV6', 929, 8, 'n'),
(71, 'ANB6', 937, 8, 'n'),
(72, 'FAN2', 945, 70, 'a'),
(73, 'VON2', 1015, 70, 'a'),
(74, 'AKG2', 1085, 30, 'a'),
(75, 'AGRD', 1115, 2, 'a'),
(76, 'VOAB', 1117, 8, 'n'),
(77, 'VOBI', 1125, 8, 'n'),
(78, 'FEAB', 1133, 8, 'n'),
(79, 'FEBI', 1141, 8, 'n'),
(80, 'EIN1', 1149, 8, 'n'),
(81, 'EIN2', 1157, 8, 'n'),
(82, 'EIN3', 1165, 8, 'n'),
(83, 'EIN4', 1173, 8, 'n'),
(84, 'EIN5', 1181, 8, 'n'),
(85, 'EIN6', 1189, 8, 'n'),
(86, 'REFN', 1197, 30, 'a'),
(87, 'APFT1', 1227, 8, 'n'),
(88, 'APFT2', 1235, 8, 'n'),
(89, 'APFT3', 1243, 8, 'n'),
(90, 'APFT4', 1251, 8, 'n'),
(91, 'APFT5', 1259, 8, 'n'),
(92, 'APFT6', 1267, 8, 'n'),
(93, 'APFT7', 1275, 8, 'n'),
(94, 'APFT8', 1283, 8, 'n'),
(95, 'APFT9', 1291, 8, 'n'),
(96, 'APFT10', 1299, 8, 'n'),
(97, 'ANV7', 1307, 8, 'n'),
(98, 'ANB7', 1315, 8, 'n'),
(99, 'ANV8', 1323, 8, 'n'),
(100, 'ANB8', 1331, 8, 'n'),
(101, 'ANV9', 1339, 8, 'n'),
(102, 'ANB9', 1347, 8, 'n'),
(103, 'ANV10', 1355, 8, 'n'),
(104, 'ANB10', 1363, 8, 'n'),
(105, 'GEGC', 1371, 2, 'a'),
(106, 'GEGT', 1373, 8, 'a'),
(107, 'ARLO', 1381, 1, 'a'),
(108, 'SZUM', 1382, 1, 'a'),
(109, 'SBUM', 1383, 1, 'a'),
(110, 'SBKZ', 1384, 1, 'a'),
(111, 'ALBV', 1385, 8, 'n'),
]
# ------------------------------------------------------------------
# G.11 Arbeits- und Entgeltbestätigung für Wochengeld Version 10
# (SART 75/76) — nach IDTEIL, Satzlänge 1048.
# ------------------------------------------------------------------
AEB_WOCHENGELD_SATZ = [
(2, 'BKNR', 21, 10, 'a'),
(3, 'DGNA', 31, 70, 'a'),
(4, 'DTEL', 101, 50, 'a'),
(5, 'WOBD', 151, 12, 'a'),
(6, 'ZOBD', 163, 15, 'a'),
(7, 'VSNR', 178, 10, 'n'),
(8, 'GEBD', 188, 8, 'n'),
(9, 'FANA', 196, 70, 'a'),
(10, 'VONA', 266, 70, 'a'),
(11, 'AKGR', 336, 30, 'a'),
(12, 'WKFZ', 366, 3, 'a'),
(13, 'PLZL', 369, 9, 'a'),
(14, 'WORT', 378, 40, 'a'),
(15, 'STRA', 418, 50, 'a'),
(16, 'BEAB', 468, 8, 'n'),
(17, 'KABE', 476, 1, 'n'),
(18, 'TAET', 477, 30, 'a'),
(19, 'BTAG', 507, 1, 'n'),
(20, 'TATU', 508, 1, 'a'),
(21, 'LTAG', 509, 8, 'n'),
(22, 'GRUN', 517, 30, 'a'),
(23, 'BLOE', 547, 1, 'a'),
(24, 'AGRD', 548, 2, 'a'),
(25, 'EBSV', 550, 8, 'n'),
(26, 'RESE', 558, 8, 'a'),
(27, 'PRAB', 566, 8, 'n'),
(28, 'URLV', 574, 8, 'n'),
(29, 'URLB', 582, 8, 'n'),
(30, 'IBAN', 590, 34, 'a'),
(31, 'BIC', 624, 11, 'a'),
(32, 'AVER', 635, 9, 'n'),
(33, 'AVON', 644, 8, 'n'),
(34, 'ABIS', 652, 8, 'n'),
(35, 'SBZT', 660, 24, 'a'),
(36, 'UNV1', 684, 8, 'n'),
(37, 'UNB1', 692, 8, 'n'),
(38, 'UNV2', 700, 8, 'n'),
(39, 'UNB2', 708, 8, 'n'),
(40, 'UNV3', 716, 8, 'n'),
(41, 'UNB3', 724, 8, 'n'),
(42, 'UNV4', 732, 8, 'n'),
(43, 'UNB4', 740, 8, 'n'),
(44, 'SZKZ', 748, 1, 'a'),
(45, 'ANGV', 749, 1, 'a'),
(46, 'AHEB', 750, 8, 'n'),
(47, 'AMEB', 758, 8, 'n'),
(48, 'FAN2', 766, 70, 'a'),
(49, 'VON2', 836, 70, 'a'),
(50, 'AKG2', 906, 30, 'a'),
(51, 'SZMO', 936, 4, 'n'),
(52, 'SZWO', 940, 4, 'n'),
(53, 'SBZW', 944, 1, 'a'),
(54, 'SBZE', 945, 1, 'a'),
(55, 'UNV5', 946, 8, 'n'),
(56, 'UNB5', 954, 8, 'n'),
(57, 'UNV6', 962, 8, 'n'),
(58, 'UNB6', 970, 8, 'n'),
(59, 'UNV7', 978, 8, 'n'),
(60, 'UNB7', 986, 8, 'n'),
(61, 'UNV8', 994, 8, 'n'),
(62, 'UNB8', 1002, 8, 'n'),
(63, 'AVER1', 1010, 9, 'n'),
(64, 'REFN', 1019, 30, 'a'),
]
# SART 75/76: zulaessige Abmeldegruende (DM-ORG D.22).
_WOCHENGELD_AGRD = ('02', '03', '04', '05', '06', '10', '22', '24')
def _monats_laenge(monat):
"""Letzter Kalendertag des Monats."""
naechster = (monat.replace(day=28) + timedelta(days=4)).replace(
day=1)
return (naechster - timedelta(days=1)).day
class L10nAtPayrollAeb(models.Model):
_name = 'l10n.at.payroll.aeb'
_description = 'Arbeits- und Entgeltbestätigung (Krankengeld/Wochengeld)'
_inherit = ['mail.thread']
_order = 'id desc'
company_id = fields.Many2one(
'res.company', required=True,
default=lambda self: self.env.company, index=True)
art = fields.Selection(
[('krankengeld', 'Krankengeld (BEST AK, SART 70/71)'),
('wochengeld', 'Wochengeld (BEST AW, SART 75/76)')],
required=True, tracking=True)
employee_id = fields.Many2one('hr.employee', required=True)
kzku = fields.Selection(
[('1', 'Krankheit/Unglücksfall'),
('2', 'Arbeitsunfall/Berufskrankheit')],
string='Kennzeichen (KZKU)', default='1')
verhinderung_von = fields.Date(
string='Verhinderung ab (Episode)', required=True,
help='Beginn der zusammenhängenden Arbeitsunfähigkeit '
'(Krankengeld) bzw. der Mutterschutz-Frist (Wochengeld) — '
'Bezugszeitraum der Bestätigung.')
verhinderung_bis = fields.Date(
string='Verhinderung bis (bekannt)',
help='Letzter bekannter Verhinderungstag; leer = laufend.')
storno_id = fields.Many2one(
'l10n.at.payroll.aeb', string='Stornierte Bestätigung',
help='Storno (SART 71/76) — nur bei Fehler oder Änderung der '
'Bemessungsgrundlage (E.10.2.1); sonst Korrektur per '
'Neumeldung (SART 70/75).')
refn = fields.Char(string='Referenznummer (REFN)', readonly=True,
copy=False)
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)
# ------------------------------------------------------------------
# Datenaufbereitung
# ------------------------------------------------------------------
def _aeb_version(self):
return self.employee_id.version_id
def _aeb_referenz_slips(self, bis, monate):
"""Die bis zu ``monate`` jüngsten vollständigen validierten
Monatsläufe vor ``bis`` (älteste zuerst)."""
self.ensure_one()
slips = self.env['hr.payslip'].search([
('employee_id', '=', self.employee_id.id),
('state', 'in', ('validated', 'paid')),
('date_to', '<', bis),
], order='date_from desc', limit=monate).filtered(
lambda s: s.date_from.day == 1
and s.date_to.day == _monats_laenge(s.date_from))
return slips.sorted('date_from')
def _aeb_monats_brutto(self, slip):
"""Monats-Bezug (Brutto inkl. SZ-Anteil) eines Laufs."""
return slip._l10n_at_brutto('laufend') \
+ slip._l10n_at_brutto('sonstig')
def _aeb_szkz(self):
"""SZKZ: Anspruch auf Sonderzahlung — datengetrieben aus den
sonstigen (§-67-)Bezügen irgendeines validierten Laufs des
AN (B13/14-Struktur des Referenz-KVs; die reinen
Referenzmonate können sz-frei sein)."""
self.ensure_one()
slips = self.env['hr.payslip'].search([
('employee_id', '=', self.employee_id.id),
('state', 'in', ('validated', 'paid')),
], limit=24)
return 'J' if any(s._l10n_at_brutto('sonstig') for s in slips) \
else 'N'
def _aeb_wochenplan(self):
"""ARFT: 'A' je Arbeitstag, 'F' sonst (MontagSonntag)."""
self.ensure_one()
kalender = self._aeb_version().resource_calendar_id
tage = {int(att.dayofweek)
for att in kalender.attendance_ids} \
if kalender else {0, 1, 2, 3, 4}
return ''.join('A' if d in tage else 'F' for d in range(7))
def _aeb_ident_setzen(self, satz, wfkz_feld):
"""Identifikations- und Beschäftigungsfelder beider
Satzarten. ``wfkz_feld``: 'WFKZ' (Krankengeld) bzw. 'WKFZ'
(Wochengeld) die Feldnamen weichen ab (G.10/G.11)."""
self.ensure_one()
version = self._aeb_version()
company = self.company_id
employee = self.employee_id
familienname, vorname = l10n_at_namen(employee)
satz.setze('BKNR', company.l10n_at_beitragskontonummer)
satz.setze('DGNA', (company.name or '')[:70])
satz.setze('DTEL', (company.partner_id.phone or '')[:50])
satz.setze('VSNR', version.ssnid)
satz.setze_datum('GEBD', employee.birthday)
satz.setze('FANA', familienname[:70])
satz.setze('VONA', (vorname or '')[:70])
satz.setze_datum('BEAB', version.contract_date_start)
satz.setze('KABE', '4' if version.l10n_at_lehrling else (
'1' if getattr(version, 'l10n_at_personalgruppe', '')
== 'arbeiter' else '2'))
satz.setze('TAET', (employee.job_id.name or '')[:30])
plan = self._aeb_wochenplan()
satz.setze('BTAG', str(plan.count('A')))
satz.setze('TATU', str(min(plan.count('A'), 6)))
# Wohnort (D.12): A = Österreich
satz.setze(wfkz_feld, 'A')
satz.setze('PLZL', (version.private_zip or '')[:9])
satz.setze('WORT', (version.private_city or '')[:40])
satz.setze('STRA', (version.private_street or '')[:50])
if version.contract_date_end:
satz.setze('BLOE', 'G')
satz.setze_datum('EBSV', version.contract_date_end)
agrd = getattr(version.departure_reason_id, 'l10n_at_agrd',
False)
if agrd:
satz.setze('AGRD', agrd)
satz.setze_datum('LTAG', version.contract_date_end)
else:
satz.setze('BLOE', 'N')
bank = employee.primary_bank_account_id
if bank:
satz.setze('IBAN', (bank.acc_number or '')[:34])
if bank.bank_id:
satz.setze('BIC', (bank.bank_id.bic or '')[:11])
# ------------------------------------------------------------------
# Fachdaten
# ------------------------------------------------------------------
def _aeb_fuelle_krankengeld(self, satz, fehler):
"""SART 70: EFZ-Anspruch (AEFZ/JAGU/TAGU), Teilentgelt-Phase,
Vorerkrankungen, Bezugsblöcke, VENT (Phasen-Karte AP4)."""
self.ensure_one()
version = self._aeb_version()
episode_von = self.verhinderung_von
aj_start = version._l10n_at_efz_arbeitsjahr_start(episode_von)
if not aj_start:
fehler.append('Kein Vertragsbeginn — EFZ-Daten leer.')
return
endtag = self.verhinderung_bis or fields.Date.context_today(self)
kontext = version._l10n_at_efz_phasen(
aj_start, max(endtag, episode_von))
phasen = kontext['phasen']
satz.setze('KZKU', self.kzku or '1')
satz.setze('ARFT', self._aeb_wochenplan())
wochen = version._l10n_at_efz_stufe_am(episode_von)
if wochen:
satz.setze('AEFZ', '%02d' % wochen)
satz.setze('JAGU', '1') # Arbeitsjahr (lb-krs-08)
satz.setze('TAGU', '2') # Kalendertage
satz.setze('SBKZ', 'N') # kein Sachbezug modelliert
def bloecke(phase=None, filter_ab=None):
"""Zusammenhängende Kalendertag-Blöcke einer Phase."""
ergebnis = []
vorher = None
for tag in sorted(phasen):
if filter_ab is not None and tag >= filter_ab:
break
if phase is not None and phasen[tag] != phase:
vorher = None
continue
if vorher is not None and (tag - vorher).days == 1:
ergebnis[-1][1] = tag
else:
ergebnis.append([tag, tag])
vorher = tag
return ergebnis
# VENT: Ende der zusammenhängenden Voll-Phase ab Episode.
voll = bloecke(phase='voll')
voll_der_episode = [b for b in voll if b[0] >= episode_von]
if voll_der_episode:
satz.setze_datum('VENT', voll_der_episode[0][1])
# Teilentgelt: bis zu zwei Halb-Phasen (TPR/TVO/TBI).
for index, (von, bis) in enumerate(bloecke(phase='halb')[:2]):
satz.setze('TPR%d' % (index + 1), 50)
satz.setze_datum('TVO%d' % (index + 1), von)
satz.setze_datum('TBI%d' % (index + 1), bis)
# Vorerkrankungen (ANV/ANB): Blöcke vor der Episode
# (vorangehende Arbeitsunfähigkeiten, E.10.2.7).
for index, (von, bis) in enumerate(bloecke(filter_ab=episode_von)[:10]):
satz.setze_datum('ANV%d' % (index + 1), von)
satz.setze_datum('ANB%d' % (index + 1), bis)
# Bezugsblöcke: bis zu 4 volle Monatsläufe vor Episode.
slips = self._aeb_referenz_slips(episode_von, 4)[:4]
summe = 0.0
for index, slip in enumerate(slips):
betrag = self._aeb_monats_brutto(slip)
summe += betrag
satz.setze_datum('BVO%d' % (index + 1), slip.date_from)
satz.setze_datum('BBI%d' % (index + 1), slip.date_to)
satz.setze('BBE%d' % (index + 1), betrag, cent=True)
if summe:
satz.setze('BSUM', summe, cent=True)
satz.setze('SZKZ', self._aeb_szkz())
def _aeb_fuelle_wochengeld(self, satz, fehler):
"""SART 75: Arbeitsverdienst netto der letzten drei
Kalendermonate vor der Frist (§ 162 Abs 3 ASVG AP7-Logik),
Fortbezug, AGRD-Kontrolle (D.22)."""
self.ensure_one()
von = self.verhinderung_von
referenz = self._aeb_referenz_slips(von, 3)
if referenz:
netto = sum(sum(s.line_ids.filtered(
lambda l: l.code == 'NET').mapped('total'))
for s in referenz)
satz.setze('AVER', netto, cent=True)
satz.setze_datum('AVON', referenz[0].date_from)
satz.setze_datum('ABIS', referenz[-1].date_to)
else:
fehler.append(
'Keine vollständigen Referenzläufe für den '
'Arbeitsverdienst (§ 162 Abs 3 ASVG).')
satz.setze('SZKZ', self._aeb_szkz())
satz.setze('ANGV', 'K') # kein Fortbezug des Entgelts (AP7)
agrd = satz.wert('AGRD')
if agrd and agrd not in _WOCHENGELD_AGRD:
fehler.append(
'Abmeldegrund %s ist für SART 75/76 unzulässig '
'(D.22: %s) — Feld geleert.' % (
agrd, ', '.join(_WOCHENGELD_AGRD)))
# setze(None) übernähme die Grundstellung nicht — Feld
# aktiv mit Blank leeren.
satz.setze('AGRD', '')
# ------------------------------------------------------------------
# Dateierzeugung
# ------------------------------------------------------------------
def action_erzeugen(self):
import datetime as datetime_modul
for aeb in self:
fehler = []
company = aeb.company_id
bknr = company.l10n_at_beitragskontonummer
if not bknr:
raise UserError('Beitragskontonummer (BKNR) fehlt.')
version = aeb._aeb_version()
if not version.ssnid:
raise UserError(
'Mitarbeiter %s ohne Versicherungsnummer.'
% aeb.employee_id.name)
storno = bool(aeb.storno_id)
if aeb.art == 'krankengeld':
satzart = '71' if storno else '70'
struktur = L10N_AT_ELDA_IDTEIL + AEB_KRANKENGELD_SATZ
laenge, bestand, vers = 1392, 'AK', '12'
praefix, wfkz_feld = 'AEBK', 'WFKZ'
else:
satzart = '76' if storno else '75'
struktur = L10N_AT_ELDA_IDTEIL + AEB_WOCHENGELD_SATZ
laenge, bestand, vers = 1048, 'AW', '10'
praefix, wfkz_feld = 'AEBW', 'WKFZ'
satz = L10nAtEldaSatz(
struktur, laenge,
bezeichnung='AEB %s' % aeb.employee_id.name)
satz.setze('SART', satzart)
stichtag = aeb.verhinderung_bis \
or datetime_modul.date.today()
aeb._aeb_ident_setzen(satz, wfkz_feld)
if storno:
# Storno (E.10.2.1): Kerndaten der stornierten
# Bestätigung übernehmen, REFN unten neu setzen.
original = aeb.storno_id
satz.setze('VSNR',
original._aeb_version().ssnid)
satz.setze_datum('GEBD', original.employee_id.birthday)
familienname, vorname = l10n_at_namen(
original.employee_id)
satz.setze('FANA', familienname[:70])
satz.setze('VONA', (vorname or '')[:70])
satz.setze_datum('BEAB',
original._aeb_version()
.contract_date_start)
elif aeb.art == 'krankengeld':
aeb._aeb_fuelle_krankengeld(satz, fehler)
else:
aeb._aeb_fuelle_wochengeld(satz, fehler)
if not aeb.refn:
aeb.refn = '%s-%d-%s-%d%s' % (
praefix, company.id,
aeb.verhinderung_von.strftime('%Y%m%d'),
aeb.employee_id.id, '-ST' if storno else '')
satz.setze('REFN', aeb.refn[:30])
vstr = (bknr or '')[:2]
bestands_objekt = L10nAtEldaBestand(
bestand, vstr, vers,
hersteller={'name': 'odoo-at-payroll', 'kfz': 'A',
'plz': company.partner_id.zip,
'ort': company.partner_id.city,
'strasse': company.partner_id.street},
datensaetze=[satz])
datei = L10nAtEldaDatei([bestands_objekt])
roh = datei.render()
# Selbstkontrolle: Datensatz-Satzlaenge (nach Header,
# Vorlauf; vor Schluss).
datensaetze = roh.decode('ISO-8859-15') \
.strip('\r\n').split('\r\n')
if len(datensaetze[2]) != laenge:
fehler.append(
'Satzlängenkontrolle: %d != %d'
% (len(datensaetze[2]), laenge))
aeb.write({
'datei': base64.b64encode(roh).decode(),
'datei_name': '%s_%s.txt' % (
praefix.lower(),
aeb.verhinderung_von.strftime('%Y%m%d')),
'protokoll': 'WARNUNGEN:\n%s' % (
'\n'.join(fehler) or ''),
'state': 'done',
})
return True
@@ -0,0 +1,32 @@
# Part of the odoo-at-payroll project. License: LGPL-3.
"""Buchungsbeleg der Lohnbuchhaltung (AP2d): Druck der FI-Buchung
eines Lohnlaufs die account.move je Lohnlauf (bzw. je Payslip, wenn
nicht gebatcht bucht wird) mit Konto, Belegtext, Soll/Haben und
Summen; Kontrollgrößen stimmen mit dem Monatsjournal überein
(GP6-Matrix, KFS/BW 6)."""
from odoo import models
class L10nAtPayrollBuchungsbeleg(models.AbstractModel):
_name = 'report.l10n_at_hr_payroll_private.report_buchungsbeleg'
_description = 'Buchungsbeleg Lohnlauf (AP2d)'
def _get_report_values(self, docids, data=None):
runs = self.env['hr.payslip.run'].browse(docids)
laeufe = []
for run in runs:
# Batch-Buchung hängt am Lauf, sonst je Payslip-Move
# (hr_payroll_account schreibt move_id je Slip bzw. je Run).
moves = (run.move_id + run.slip_ids.mapped('move_id')) \
.filtered(lambda m: m.state != 'cancel')
zeilen = moves.line_ids.filtered(
lambda l: l.debit or l.credit).sorted(
key=lambda l: (l.account_id.code or '', l.id))
laeufe.append({
'run': run,
'moves': moves,
'zeilen': zeilen,
'summe_soll': sum(zeilen.mapped('debit')),
'summe_haben': sum(zeilen.mapped('credit')),
})
return {'docs': runs, 'laeufe': laeufe}
@@ -0,0 +1,309 @@
# Part of the odoo-at-payroll project. License: LGPL-3.
"""EFZG/Teilentgelt (AP4) — Entgeltfortzahlung im Krankenstand.
Rechtsgrundlagen (verifiziert 2026-09-12):
- **AngG § 8 Abs 1/2/2a** und **EFZG § 2 Abs 1/3, § 2 Abs 5**
(Gleichlauf seit 1. 7. 2018, BGBl I 153/2017; EFZG RIS GesNr
10008308, AngG lokal Stufen 6/8/10/12 Wochen je vollendeten
Dienstjahren <1 / 1-14 / 15-24 / 25, zuzüglich je 4 Wochen halbes
Entgelt; wiederholte Verhinderungen im selben Arbeitsjahr
zusammengerechnet; AU/BK-Sonderkontingent 8/10 Wochen ohne
Halbsatz, je Anlassfall, unabhängig vom Krankheits-Topf).
- **lb-krs-08/02/07** (Wissensbasis, Layer 2): Arbeitsjahrprinzip,
Kalendertage als Zähleinheit, Stufensprung im laufenden Stand
verlängert; SV: volle EFZ beitragspflichtig, EFZ < 50 %
beitragsfrei (hier: Halbsatz = 50 % pflichtig); Lohnsteuer:
laufender Bezug; **BMSVG § 7 Abs 3**: während des Krankengeldbezugs
(Halb- und ÖGK-Phase) fiktive BG = 50 % des Entgelts des
Kalendermonats vor dem Versicherungsfall, SZ ausgenommen.
- Krankengeld selbst zahlt die ÖGK (läuft nicht durch den
Dienstgeber-Payslip; lb-krs-02) der Payroll-Anteil ist die
Entgelt-Reduktion des Dienstgebers, die fiktive BV-Grundlage und
später die Arbeits-/Entgeltbestätigung (AP8).
- Krankengeldumlage § 62 ASVG" ist eine Falsch-Prämise
(RECHTSQUELLEN-Privat 2026-09-10) kein Umlage-Baustein.
Zähleinheiten: Kontingente in **Kalendertagen** (lb-krs-08: Praxis),
bezogen aus den validierten Abwesenheiten (hr.leave der AT-Urlaubsarten
Krankenstand/Arbeitsunfall); die **Entgelt-Minderung** je
Arbeitstag aus dem Arbeitseintragssatz (Tagesatz = laufende
Monatsbasis ÷ Arbeitstage des Monats Entgeltausfallsprinzip).
Feiertage im Krankenstand werden vom Work-Entry-Modell automatisch
nicht als Krankenstandstage erzeugt (Kalender) die 100 %-
Feiertagsentgelt-Regel lb-krs-08 bleibt damit gewahrt.
"""
from datetime import date, timedelta
from odoo import fields, models
# WET-Codes der Abwesenheits-Töpfe (AP5-Seed).
_KRANK_WET = 'ATP_KRANK'
_AU_WET = 'ATP_AU'
class HrVersion(models.Model):
_inherit = 'hr.version'
def _l10n_at_efz_arbeitsjahr_start(self, stichtag):
"""Beginn des Arbeitsjahres, das den Stichtag enthält
(Eintrittsdatum-Rhythmus, lb-krs-08). None ohne
Vertragsbeginn."""
self.ensure_one()
beginn = self.contract_date_start
if not beginn:
return None
jahre = beginn.year
while True:
aj_start = date(jahre, beginn.month, beginn.day) \
if not (beginn.month == 2 and beginn.day == 29) \
else date(jahre, 3, 1)
aj_naechstes = date(jahre + 1, aj_start.month, aj_start.day)
if aj_start <= stichtag < aj_naechstes:
return aj_start
jahre += 1
if jahre > beginn.year + 80:
return None
def _l10n_at_efz_phasen(self, aj_start, endtag, stichtag=None):
"""Komplette EFZ-Phasen-Karte des Arbeitsjahres über alle
Krank-/AU-Kalendertage bis ``endtag``: {datum: 'voll'/'halb'/
'oegk'} plus Kontext-Parameter gemeinsame Basis der
Payslip-Auswertung (AP4) und der Arbeits- und
Entgeltbestätigung (AP8).
Stufen je Tag (Stufensprung verlängert, lb-krs-08),
Krank-/AU-Topf getrennt (AU ohne Halbsatz, § 8 Abs 2a AngG /
§ 2 Abs 5 EFZG)."""
self.ensure_one()
stichtag = stichtag or fields.Date.context_today(self)
params = self.env['hr.rule.parameter']._get_parameter_from_code(
'at_efz_stufen', stichtag, raise_if_not_found=False) or {}
stufen = sorted(
(int(jahre), int(wochen))
for jahre, wochen in params.get('stufen', []))
halb_wochen = int(params.get('halb_wochen', 4))
au_wochen = int(params.get('au_wochen', 8))
au2_wochen = int(params.get('au2_wochen', 10))
au_ab_jahre = int(params.get('au_ab_jahre', 15))
typen = self.env['hr.leave.type'].search([
('work_entry_type_id.code', 'in', (_KRANK_WET, _AU_WET))])
leaves = self.env['hr.leave'].search([
('employee_id', '=', self.employee_id.id),
('holiday_status_id', 'in', typen.ids),
('state', 'in', ('validate', 'validate1')),
])
krank_tage, au_tage = {}, {}
for leave in leaves:
if not (leave.request_date_from and leave.request_date_to):
continue
tag = leave.request_date_from
while tag <= leave.request_date_to:
if aj_start <= tag <= endtag:
topf = au_tage if leave.holiday_status_id \
.work_entry_type_id.code == _AU_WET else krank_tage
topf[tag] = True
tag += timedelta(days=1)
beginn = self.contract_date_start
def stufe_fuer(dienstjahre, tag_ist_au):
wochen = stufen[0][1] if stufen else 6
for jahre, stufe in stufen:
if dienstjahre >= jahre:
wochen = stufe
if tag_ist_au:
wochen = au2_wochen if dienstjahre >= au_ab_jahre \
else au_wochen
return wochen
phasen = {}
verbraucht_krank_voll = verbraucht_krank_halb = 0
verbraucht_au = 0
for tag in sorted(set(krank_tage) | set(au_tage)):
dienstjahre = (tag.year - beginn.year
- ((tag.month, tag.day) < (beginn.month,
beginn.day)))
if tag in au_tage:
if verbraucht_au < stufe_fuer(dienstjahre, True) * 7:
phasen[tag] = 'voll'
else:
phasen[tag] = 'oegk'
verbraucht_au += 1
continue
wochen = stufe_fuer(dienstjahre, False)
if verbraucht_krank_voll < wochen * 7:
phasen[tag] = 'voll'
verbraucht_krank_voll += 1
elif verbraucht_krank_halb < halb_wochen * 7:
phasen[tag] = 'halb'
verbraucht_krank_halb += 1
else:
phasen[tag] = 'oegk'
return {
'phasen': phasen,
'stufen': stufen,
'halb_wochen': halb_wochen,
'au_wochen': au_wochen,
'au2_wochen': au2_wochen,
'au_ab_jahre': au_ab_jahre,
}
def _l10n_at_efz_stufe_am(self, stichtag, ist_au=False):
"""EFZ-Stufen-Wochen am Stichtag (vollendete Dienstjahre)."""
self.ensure_one()
aj_start = self._l10n_at_efz_arbeitsjahr_start(stichtag)
if not aj_start:
return None
kontext = self._l10n_at_efz_phasen(aj_start, stichtag)
beginn = self.contract_date_start
dienstjahre = (stichtag.year - beginn.year
- ((stichtag.month, stichtag.day)
< (beginn.month, beginn.day)))
stufen = kontext['stufen']
wochen = stufen[0][1] if stufen else 6
for jahre, stufe in stufen:
if dienstjahre >= jahre:
wochen = stufe
if ist_au:
wochen = kontext['au2_wochen'] \
if dienstjahre >= kontext['au_ab_jahre'] \
else kontext['au_wochen']
return wochen
class HrPayslip(models.Model):
_inherit = 'hr.payslip'
l10n_at_efz_info = fields.Char(
string='AT EFZ-Status (Krankenstand)', compute='_compute_efz_info',
help='Phasenstatus des Krankenstands im Abrechnungszeitraum: '
'volle/halbe Entgeltfortzahlung, ÖGK-Krankengeld-Phase, '
'Kontingente und Stufe (AngG § 8 / EFZG § 2).')
def _compute_efz_info(self):
for slip in self:
efz = slip._at_efz_status()
if not efz:
slip.l10n_at_efz_info = ''
continue
slip.l10n_at_efz_info = (
'Stufe %d (%d+%d Wo) · voll %d AT · halb %d AT · '
'ÖGK %d AT' % (
efz['stufe_wochen'], efz['stufe_wochen'],
efz['halb_wochen'], len(efz['voll_arbeitstage']),
len(efz['halb_arbeitstage']),
len(efz['oegk_arbeitstage'])))
# ------------------------------------------------------------------
# EFZ-Phasenbestimmung (Phasen-Karte: hr.version, AP8-Refaktur)
# ------------------------------------------------------------------
def _at_efz_status(self):
"""EFZ-Phasen je Abrechnungsperiode: {'stufe_wochen',
'halb_wochen', 'voll_arbeitstage', 'halb_arbeitstage',
'oegk_arbeitstage', 'oegk_kalendertage', 'episode_beginn'}
None ohne Krankenstand/AU im Arbeitsjahr.
Die Phasen-Karte (Kalendertage, Stufen je Tag, Krank-/AU-Topf
getrennt, § 8 Abs 2a AngG / § 2 Abs 5 EFZG) liefert
``hr.version._l10n_at_efz_phasen`` gemeinsame Basis mit der
Arbeits- und Entgeltbestätigung (AP8)."""
self.ensure_one()
version = self.version_id
aj_start = version._l10n_at_efz_arbeitsjahr_start(self.date_from)
if not aj_start:
return None
endtag = max(self.date_to, aj_start)
kontext = version._l10n_at_efz_phasen(aj_start, endtag,
stichtag=self.date_from)
phasen = kontext['phasen']
if not phasen:
return None
halb_wochen = kontext['halb_wochen']
# Arbeitstage je Phase im Abrechnungszeitraum (Work Entries)
eintraege = self.env['hr.work.entry'].search([
('employee_id', '=', self.employee_id.id),
('date', '>=', self.date_from),
('date', '<=', self.date_to),
('state', '!=', 'cancelled'),
('work_entry_type_id.code', 'in', (_KRANK_WET, _AU_WET)),
])
voll, halb, oegk = [], [], []
for eintrag in eintraege:
phase = phasen.get(eintrag.date)
if phase == 'voll':
voll.append(eintrag.date)
elif phase == 'halb':
halb.append(eintrag.date)
elif phase == 'oegk':
oegk.append(eintrag.date)
# Episode-Beginn für die fiktive BV (§ 7 Abs 3 BMSVG):
# frühester zusammenhängender Verhinderungsblock (Lücke = neue
# Episode), dessen Phase die Halb-/ÖGK-Phase erreicht.
episode_beginn = None
for tag in sorted(phasen):
if phasen[tag] in ('halb', 'oegk'):
block = tag
while (block - timedelta(days=1)) in phasen:
block = block - timedelta(days=1)
episode_beginn = block
break
return {
'stufe_wochen': version._l10n_at_efz_stufe_am(self.date_from),
'halb_wochen': halb_wochen,
'voll_arbeitstage': sorted(set(voll)),
'halb_arbeitstage': sorted(set(halb)),
'oegk_arbeitstage': sorted(set(oegk)),
# Kalendertage der Phasen IM ABRECHNUNGSZEITRAUM — Basis
# der fiktiven-BV-Aliquote (monatsscharf).
'oegk_kalendertage': sorted(
tag for tag in phasen
if self.date_from <= tag <= self.date_to
and phasen[tag] == 'oegk'),
'halb_kalendertage': sorted(
tag for tag in phasen
if self.date_from <= tag <= self.date_to
and phasen[tag] == 'halb'),
'episode_beginn': episode_beginn,
}
def _at_efz_tagesatz(self, result_rules=None):
"""Voller Tages-Entgeltsatz des Monats (Entgeltausfalls-
prinzip): laufende Monatsbasis ohne die eigenen
Teilentgelt-Reduktionen ÷ Arbeitstage des Kalendermonats.
Geteilter Helper von EFZG (TEIL_ENTGELT_HALB,
KRANKENTGELT_PHASE) und Mutterschutz (MSCH_ENTGELT, AP7)."""
self.ensure_one()
basis = self._l10n_at_sv_basis_roh(result_rules)
for code in ('TEIL_ENTGELT_HALB', 'KRANKENTGELT_PHASE',
'MSCH_ENTGELT', 'KARENZ_ENTGELT'):
if result_rules is not None and code in result_rules \
and result_rules[code]['total']:
basis -= result_rules[code]['total']
arbeitstage = self._at_arbeitstage(self.date_from, self.date_to)
return basis / max(arbeitstage, 1)
def _at_bv_fiktiv_krankengeld(self):
"""Fiktive BV-Grundlage während des Krankengeldbezugs
(§ 7 Abs 3 BMSVG, lb-krs-07): 50 % des Entgelts des
Kalendermonats vor dem Versicherungsfall (SZ ausgenommen),
aliquot nach Kalendertagen des Bezugs (Halb- + ÖGK-Phase,
30-Tage-Konvention). Bezug = letzter validierter Lauf vor
Episode-Beginn; ohne solchen: der laufende Monat (kurze
Dienstverhältnisse, lb-krs-02)."""
self.ensure_one()
efz = self._at_efz_status()
if not efz or not (efz['halb_kalendertage']
or efz['oegk_kalendertage']):
return 0.0
tage = len(efz['halb_kalendertage']) \
+ len(efz['oegk_kalendertage'])
vormonat = None
if efz['episode_beginn']:
vormonat = self.env['hr.payslip'].search([
('employee_id', '=', self.employee_id.id),
('state', 'in', ('validated', 'paid')),
('date_to', '<', efz['episode_beginn']),
], order='date_from desc', limit=1)
basis_monat = vormonat._l10n_at_sv_basis_roh() if vormonat \
else self._l10n_at_sv_basis_roh()
return 0.5 * basis_monat * tage / 30.0
@@ -0,0 +1,656 @@
# Part of the odoo-at-payroll project. License: LGPL-3.
"""AP14-A: Endabrechnungs-Zyklus — Urlaubsersatzleistung (UEL),
Abgangsentschädigung (§ 67 Abs 8 lit b), Aufrechnung offener Vorschüsse
und Zeitguthaben/-schulden (§ 19e AZG).
AP15-C: Vergleichssummen (§ 67 Abs 8 lit a).
Am Originalwortlaut verifiziert (.ris/, Stand 2026-09-14):
- § 11 Abs 2 ASVG: die Pflichtversicherung besteht für die Zeit des
Bezuges einer Ersatzleistung für Urlaubsentgelt (und einer
Kündigungsentschädigung) weiter; gebühren beide, ist zur Bestimmung
des Zeitraumes ZUERST die Kündigungsentschädigung und im Anschluss
die Ersatzleistung heranzuziehen (sequentielle Verlängerungsfenster).
Die 6-WT/5-AT-Tage-Umrechnung ist Verwaltungspraxis (lb-url-15),
nicht Gesetzeswortlaut.
- § 67 Abs 8 lit d EStG: UEL soweit sie laufenden Arbeitslohn
betreffen, als laufender Arbeitslohn, soweit sie sonstige Bezüge
betreffen, als sonstiger Bezug im Kalendermonat der Zahlung" — die
Zweikomponenten-Abbildung läuft nativ über l10n_at_bemessung.
- § 67 Abs 8 lit b EStG (Kündigungs-/Abgangsentschädigung): nach
Abzug der §-62-Beiträge ist ein Fünftel steuerfrei zu belassen,
höchstens ein Fünftel des Neunfachen der monatlichen HBG; der Rest
läuft tarifisch im Zuflussmonat (Abs 10), Abs 2 (Sechstel) ist
NICHT anzuwenden. LNK-pflichtig (FLAG § 41 Abs 4 nennt nur
§ 67 Abs 3 und 6) und SV-pflichtig (keine §-49-Abs-3-Z-7-Befreiung)
mit Umlegung auf den Kündigungsfrist-Zeitraum.
- § 19e Abs 2 AZG: Guthaben an Normalarbeitszeit + 50 % Zuschlag
(Entfall bei unberechtigtem vorzeitigem Austritt; KV kann
abweichend regeln).
- § 67 Abs 8 lit a EStG (AP15-C, Stand 2026-09-15): Vergleichssummen
sind, SOWEIT sie nicht nach Abs 3, 6 oder dem letzten Satz mit dem
festen Steuersatz zu versteuern sind, nach Abs 10 im Kalendermonat
der Zahlung zu erfassen; nach Abzug der §-62-Z-3/4/5-Beiträge ist
ein Fünftel steuerfrei (Deckel ein Fünftel des Neunfachen der
monatlichen HBG), Abs 2 (Sechstel) ist nicht anzuwenden. Erkennbare
Abfertigungsanteile laufen getrennt über Abs 3 (VwGH
2013/13/0001 = ARD 6490/20/2016: ausdrücklich vereinbarte
gesetzliche Abfertigung gesamt Abs 3) bzw. Abs 6. Fallen die
Vergleichssummen bei oder nach Beendigung an UND betreffen sie
Zeiträume mit BV-Kassen-Anwartschaft, sind sie bis 7 500 mit 6 %
zu versteuern (parametrisiert, §-33a-Indexkette); der übersteigende
Teil fällt in die Fünftelregelung zurück. SV-Aufrollung aktiver
Ansprüche je Beitragsmonat bzw. PV-Verlängerung: lb-end-22
(E-MVB 011-02-00-001), Kern-Mechanismus l10n_at_beitragsmonat.
"""
from datetime import timedelta
from dateutil.relativedelta import relativedelta
from odoo import fields, models
class HrSalaryRule(models.Model):
_inherit = 'hr.salary.rule'
l10n_at_sv_verlaengerung = fields.Selection(
[('keine', 'Keine SV-Verlängerung (§ 11 Abs 2 ASVG)'),
('urlaubsersatz', 'UEL-Tage-Schema (Urlaubsersatzleistung)'),
('abgang', 'Kündigungsfrist (Abgangsentschädigung)')],
string='AT SV-Verlängerung', default='keine',
help='Verteilt die laufende SV-Basis dieser Regel auf die '
'Pflichtversicherungs-Verlängerungsmonate nach § 11 Abs 2 '
'ASVG statt auf den Abrechnungsmonat: Abgangsentschädigung '
'zuerst (Kündigungsfrist), UEL anschließend (Tage-Schema '
'lb-url-15).')
l10n_at_s67_fuenftel = fields.Boolean(
string='AT § 67 Abs 8 lit a/b (Fünftelregelung)',
help='LSt-Pfad der Vergleichs-/Kündigungsentschädigungen: nach '
'Abzug der §-62-Beiträge ist ein Fünftel steuerfrei '
'(Deckel: ein Fünftel des Neunfachen der monatlichen HBG), '
'der Rest läuft tarifisch im Zuflussmonat — ohne '
'Jahressechstel-Erhöhung (Abs 2 nicht anzuwenden).')
l10n_at_vergleich_bv = fields.Boolean(
string='AT § 67 Abs 8 lit a — BV-Anwartschafts-Teil',
help='Teil einer Vergleichssumme, der bei oder nach Beendigung '
'für Zeiträume mit BV-Kassen-Anwartschaft ausbezahlt wird: '
'bis zum Deckel (at_lst_67_abs8_vergleich_bv, 2026: 7 500 €) '
'mit 6 %, der übersteigende Teil fällt in die '
'Fünftelregelung zurück. SV-pflichtig und LNK-pflichtig.')
l10n_at_s26_z4_reise = fields.Boolean(
string='AT § 26 Z 4 Reisevergütung',
help='§-26-Z-4-Reisevergütungen zählen nicht in die '
'12-Monats-Basis der §-67-Abs-6-Begünstigung (lb-end-13); '
'Sonderzahlungen sind ohnehin nicht bemessung „laufend“.')
class HrPayslip(models.Model):
_inherit = 'hr.payslip'
l10n_at_abf_alt_anspruchsart = fields.Selection(
[('normal', 'Normalanspruch (§ 23 AngG)'),
('eltern', 'Elternaustritt (§ 23a Abs 3/4 AngG)'),
('tod', 'Tod (§ 23 Abs 6 AngG)'),
('pension', 'Pension/Invalidität (§ 23a Abs 1 AngG)'),
('eingefroren', 'Eingefrorener Altanspruch (§ 47 BMSVG)'),
('manuell', 'Sonstiger nachgewiesener Anspruch')],
string='AT Abfertigung Alt — Anspruchsart', copy=False)
l10n_at_abf_alt_dienstzeit_monate = fields.Integer(
string='AT Abfertigung Alt — Dienstzeitmonate', copy=False)
l10n_at_abf_alt_monatsentgelt = fields.Float(
string='AT Abfertigung Alt — Monatsentgelt', copy=False,
help='Bestätigte arbeitsrechtliche Bemessungsgrundlage inklusive '
'regelmäßiger Bezugsbestandteile und aliquoter Sonderzahlungen.')
l10n_at_abf_alt_laufender_arbeitslohn = fields.Float(
string='AT Abfertigung Alt — laufender Arbeitslohn', copy=False,
help='Laufender Monatsarbeitslohn für den steuerlichen '
'Vervielfacher nach § 67 Abs 3 EStG.')
l10n_at_abf_alt_monatslohnsteuer = fields.Float(
string='AT Abfertigung Alt — tarifmäßige Monatslohnsteuer',
copy=False,
help='Auf den maßgeblichen laufenden Monatsarbeitslohn entfallende '
'tarifmäßige Lohnsteuer nach den persönlichen Verhältnissen.')
l10n_at_abf_alt_mehrfach = fields.Float(
string='AT Abfertigung Alt — Anspruchsfaktor', copy=False)
l10n_at_abf_alt_nachweis = fields.Text(
string='AT Abfertigung Alt — Nachweis/Begründung', copy=False)
l10n_at_abf_frei_basis_12 = fields.Float(
string='AT Freiwillige Abfertigung — 12-Monats-Basis', copy=False,
help='Laufende Bezüge der letzten zwölf Monate (§ 67 Abs 6 Z 1/2 '
'EStG): vom Wizard bestätigt — alle steuerbaren Bezugsteile '
'inklusive steuerfreier Zuschläge und Sachbezüge, ohne '
'Sonderzahlungen und ohne §-26-Z-4-Reisevergütungen; keine '
'Bezüge anderer Arbeitgeber, keine Hochrechnung kurzer DV.')
l10n_at_abf_frei_dienstzeit_monate = fields.Integer(
string='AT Freiwillige Abfertigung — nachgewiesene Dienstzeit '
'(Monate)', copy=False)
l10n_at_abf_frei_erhaltene_abfertigungen = fields.Float(
string='AT Freiwillige Abfertigung — erhaltene Abfertigungen',
copy=False,
help='Während der nachgewiesenen Dienstzeit bereits erhaltene '
'Abfertigungen nach § 67 Abs 3 oder Abs 6 EStG — kürzen das '
'Z-2-Ausmaß (§ 67 Abs 6 Z 3).')
l10n_at_abf_frei_gesetzlicher_anspruch = fields.Float(
string='AT Freiwillige Abfertigung — bestehender gesetzlicher '
'Anspruch', copy=False,
help='Bestehender Anspruch auf eine gesetzliche Abfertigung am '
'aktuellen Dienstverhältnis-Ende (§ 67 Abs 6 Z 3 EStG) — '
'kürzt das Z-2-Ausmaß.')
l10n_at_abf_frei_nachweis = fields.Text(
string='AT Freiwillige Abfertigung — Nachweis/Begründung',
copy=False)
l10n_at_vergleich_nachweis = fields.Text(
string='AT Vergleich — Nachweis/Begründung', copy=False,
help='Aufgliederung der Vergleichsvereinbarung (lb-end-21): '
'strittige Ansprüche, erkennbare Abfertigungsanteile, '
'BV-Anwartschafts-Zeiträume; beim Pauschalvergleich der '
'Verhältnisfaktor Forderungsbetrag : Vergleichssumme '
'(E-MVB 011-02-00-001).')
l10n_at_pram_nachweis = fields.Text(
string='AT Prämie bei Beendigung — Nachweis/Begründung', copy=False,
help='Anspruchsgrundlage (KV-/Vertragsvereinbarung, '
'Anspruchszeitraum), §-16-Aliquotierung, '
'Leistungsverknüpfung und Fälligkeitsvereinbarung '
'dokumentieren (lb-end-18).')
l10n_at_pram_beitragsmonat = fields.Date(
string='AT Prämie (§ 67 Abs 10) — SV-Beitragsmonat der Rollung',
copy=False,
help='Beitragsmonat, dem die später fällige anteilige Prämie '
'sv-rechtlich zuzuordnen ist — der Zeitraum, in dem die '
'Leistungen erbracht wurden (E-MVB 044-01-00-006, lb-end-18). '
'Leer = Zuflussmonat.')
# ------------------------------------------------------------------
# SV-Tage-Umrechnung und Verlängerungsmonate (§ 11 Abs 2 ASVG)
# ------------------------------------------------------------------
def _at_end_arbeitsrechtliches_ende(self):
"""Arbeitsrechtliches Ende = letzter Arbeitstag; das SV-Ende
(Ende des Entgeltanspruches") liegt darüber hinaus, wenn ein
Entgeltanspruch über das Ende hinaus läuft."""
self.ensure_one()
return self.version_id.contract_date_end or self.date_to
def _at_end_arbeitstage_woche(self):
"""Arbeitstage pro Woche des Vertragskalenders (Schema-Grundlage
der SV-Tage-Umrechnung: 5-AT-Woche, 6-WT-Woche, Teilzeitwoche)."""
self.ensure_one()
calendar = self.version_id.resource_calendar_id
days = {int(att.dayofweek) for att in calendar.attendance_ids} \
if calendar else set()
return len(days) or 5
def _at_end_sv_tage(self, tage):
"""SV-Tage aus abzurechnenden Urlaubstagen (Verwaltungspraxis
lb-url-15): je volle Woche à N Arbeitstage werden 7N
Kalendertage hinzugerechnet (5-AT-Woche +2, 6-WT-Woche +1,
2-Tage-Teilzeitwoche +5) nur ganze Wochenblöcke."""
self.ensure_one()
at_woche = self._at_end_arbeitstage_woche()
blocks = int(tage // at_woche)
return tage + blocks * (7 - at_woche)
def _at_end_verlaengerungs_monate(self, sv_tage, start):
"""[(Monatsanfang, Tage im Monat)] eines Verlängerungsfensters
ab dem übergebenen Starttag (§ 11 Abs 2 ASVG)."""
self.ensure_one()
ergebnis = {}
rest = int(sv_tage)
tag = start
while rest > 0:
monatsende = (tag.replace(day=28) + timedelta(days=4)) \
.replace(day=1) - timedelta(days=1)
tage_dabei = min((monatsende - tag).days + 1, rest)
monat = tag.replace(day=1)
ergebnis[monat] = ergebnis.get(monat, 0) + tage_dabei
tag = monatsende + timedelta(days=1)
rest -= tage_dabei
return sorted(ergebnis.items())
def _at_end_rule_verlaengerung(self, rule, result_rules=None):
"""[(Monatsanfang, Tage, Anteil)] der Regel-Basis nach § 11
Abs 2: die Abgangsentschädigung belegt ZUERST die (fiktive)
Kündigungsfrist, die UEL SCHLIESSLICH die Urlaubstage beide
Fenster laufen ab dem Tag nach dem arbeitsrechtlichen Ende
hintereinander (Gesetzeswortlaut: zunächst die
Kündigungsentschädigung und im Anschluss die Ersatzleistung")."""
self.ensure_one()
start = self._at_end_arbeitsrechtliches_ende() + timedelta(days=1)
if rule.l10n_at_sv_verlaengerung == 'urlaubsersatz':
ffrist = self._at_input_amount('ATP_ABGANG_FFRIST_TAGE')
if ffrist:
start += timedelta(days=int(ffrist))
tage = self._at_end_sv_tage(self._at_uel_tage())
else:
ffrist = self._at_input_amount('ATP_ABGANG_FFRIST_TAGE')
tage = int(ffrist) if ffrist else 0
monate = self._at_end_verlaengerungs_monate(tage, start)
if not monate:
return []
gesamt = sum(tage for _monat, tage in monate)
return [(monat, tage, tage / float(gesamt))
for monat, tage in monate]
# ------------------------------------------------------------------
# Eingabe-Helfer
# ------------------------------------------------------------------
def _at_input_amount(self, code):
self.ensure_one()
linie = self.input_line_ids.filtered(
lambda i: i.input_type_id.code == code)
return linie[:1].amount or 0.0
# ------------------------------------------------------------------
# Abfertigung Alt — §§ 23/23a AngG, § 67 Abs 3 EStG
# ------------------------------------------------------------------
def _at_abf_alt_staffel(self, dienstzeit_monate=None):
"""Arbeitsrechtlicher Grundfaktor nach § 23 Abs 1 AngG."""
self.ensure_one()
monate = dienstzeit_monate
if monate is None:
monate = self.l10n_at_abf_alt_dienstzeit_monate
staffel = self._l10n_at_param('at_endabrechnung').get(
'abfertigung_alt_staffel', [])
mehrfach = 0.0
for grenze, faktor in staffel:
if monate >= int(grenze):
mehrfach = float(faktor)
return mehrfach
def _at_abf_alt_mehrfach_vorschlag(self, dienstzeit_monate=None,
anspruchsart=None):
"""§-23-Faktor mit Halb-/Deckelregeln für Elternaustritt/Tod."""
self.ensure_one()
art = anspruchsart or self.l10n_at_abf_alt_anspruchsart
monate = dienstzeit_monate
if monate is None:
monate = self.l10n_at_abf_alt_dienstzeit_monate
mehrfach = self._at_abf_alt_staffel(monate)
if art == 'eltern':
if monate < 60:
return 0.0
return min(mehrfach / 2.0, 3.0) if mehrfach else 0.0
if art == 'tod':
return mehrfach / 2.0
return mehrfach
def _at_abf_alt_betrag(self, result_rules=None):
"""Gesetzliche Alt-Abfertigung als eigene Bezugszeile."""
self.ensure_one()
return self._at_input_amount('ATP_ABF_ALT')
def _at_abf_alt_steuer_6(self, result_rules=None):
self.ensure_one()
satz = float(self._l10n_at_param(
'at_endabrechnung')['abfertigung_steuersatz'])
return max(self._at_abf_alt_betrag(result_rules), 0.0) * satz / 100.0
def _at_abf_alt_steuer_vervielfachung(self, result_rules=None):
"""§ 67 Abs 3: Monats-Tarif-LSt × steuerlicher Vervielfacher.
Der steuerliche Vervielfacher ist Abfertigungsbetrag dividiert
durch den laufenden Arbeitslohn. Er kann vom arbeitsrechtlichen
Staffel-Faktor abweichen, weil die Abfertigungsbasis etwa aliquote
Sonderzahlungen enthält (lb-end-01, RF 21 Dienstjahre: 10,5 statt 9).
"""
self.ensure_one()
betrag = self._at_abf_alt_betrag(result_rules)
laufend = self.l10n_at_abf_alt_laufender_arbeitslohn
if betrag <= 0 or laufend <= 0:
return self._at_abf_alt_steuer_6(result_rules)
vervielfacher = betrag / laufend
return self.l10n_at_abf_alt_monatslohnsteuer * vervielfacher
def _at_abf_alt_lohnsteuer(self, result_rules=None):
"""Zwingend günstigere Steuer aus Vervielfachung und 6 %."""
self.ensure_one()
if self._at_abf_alt_betrag(result_rules) <= 0:
return 0.0
return min(self._at_abf_alt_steuer_vervielfachung(result_rules),
self._at_abf_alt_steuer_6(result_rules))
# ------------------------------------------------------------------
# Freiwillige Abfertigung — § 67 Abs 6 EStG (Viertel-/Zwölftelregelung)
# ------------------------------------------------------------------
def _at_bezuege_12_monate(self):
"""Laufende Bezüge der letzten zwölf Monate (§ 67 Abs 6 Z 1/2):
validierte Vormonats-Läufe plus eigene Zeilen ohne den als
Abs-6-Überhang gekennzeichneten Betrag (das ist ein sonstiger
Bezug, kein laufender) und ohne §-26-Z-4-Reisevergütungen;
Sonderzahlungen sind ohnehin nicht bemessung laufend
(lb-end-13)."""
self.ensure_one()
start = (self.date_from - relativedelta(months=11)).replace(day=1)
vor_slips = self.env['hr.payslip'].search([
('employee_id', '=', self.employee_id.id),
('id', '!=', self.id),
('date_from', '>=', start),
('date_from', '<', self.date_from.replace(day=1)),
]).filtered(lambda p: p.state not in ('draft', 'cancel'))
total = 0.0
for slip in list(vor_slips) + [self]:
for line in slip.line_ids:
rule = line.salary_rule_id
if rule.l10n_at_bemessung != 'laufend':
continue
if (rule.l10n_at_s67_abs6_ueberhang
or rule.l10n_at_s26_z4_reise):
continue
total += line.total
return total
def _at_abf_frei_zwoelftel(self):
"""Zwölftel-Anzahl nach § 67 Abs 6 Z 2: die Dienstzeitstaffel
entspricht den §-23-Grenzen (362/12 30012/12)."""
self.ensure_one()
return self._at_abf_alt_staffel(
self.l10n_at_abf_frei_dienstzeit_monate)
def _at_abf_frei_split(self, result_rules=None):
"""§ 67 Abs 6: begünstigter Teil (Viertel- und Zwölftelregelung,
gedeckelt 9 × bzw. n × 3 × monatliche HBG, Z-3-Kürzung um
erhaltene Abfertigungen und den bestehenden gesetzlichen
Anspruch) und der tarifische Überhang (Z 6)."""
self.ensure_one()
brutto = max(self._at_input_amount('ATP_ABF_FREI'), 0.0)
basis = self.l10n_at_abf_frei_basis_12
if basis <= 0:
basis = self._at_bezuege_12_monate()
basis = max(basis, 0.0)
hbg = float(self._l10n_at_param('at_sv_hbg_monatlich'))
z1 = min(basis / 4.0, 9.0 * hbg)
zwoelftel = self._at_abf_frei_zwoelftel()
z2 = min(basis * zwoelftel / 12.0, zwoelftel * 3.0 * hbg)
kuerzung = max(
self.l10n_at_abf_frei_erhaltene_abfertigungen
+ self.l10n_at_abf_frei_gesetzlicher_anspruch, 0.0)
z2 = max(z2 - kuerzung, 0.0)
beguenstigt = min(brutto, z1 + z2)
return {
'basis': basis,
'z1': z1,
'z2': z2,
'kuerzung': kuerzung,
'beguenstigt': beguenstigt,
'ueberhang': brutto - beguenstigt,
}
def _at_abf_frei_beguenstigt(self, result_rules=None):
self.ensure_one()
return self._at_abf_frei_split(result_rules)['beguenstigt']
def _at_abf_frei_ueberhang(self, result_rules=None):
self.ensure_one()
return self._at_abf_frei_split(result_rules)['ueberhang']
def _at_abf_frei_lst_6(self, result_rules=None):
"""6 % auf den begünstigten Teil (§ 67 Abs 6, fester Steuersatz)."""
self.ensure_one()
satz = float(self._l10n_at_param(
'at_endabrechnung')['abfertigung_steuersatz'])
return self._at_abf_frei_beguenstigt(result_rules) * satz / 100.0
# ------------------------------------------------------------------
# Urlaubsersatzleistung — § 67 Abs 8 lit d, lb-url-15/16
# ------------------------------------------------------------------
def _at_uel_tage(self):
"""Offene Urlaubstage (Arbeitstage; keine Rundung, lb-url-16)."""
self.ensure_one()
return self._at_input_amount('ATP_UEL_TAGE')
def _at_uel_divisor(self):
"""Tagewert-Divisor: /26 bei Werktags-, /22 bei 5-AT-Kalender
(lb-url-16)."""
self.ensure_one()
params = self._l10n_at_param('at_endabrechnung')
if self._at_end_arbeitstage_woche() >= 6:
return float(params['divisor_werktage'])
return float(params['divisor_arbeitstage'])
def _at_uel_lfd(self):
"""UEL-laufender Anteil: Tage × Monatsentgelt / Divisor —
läuft als laufender Bezug durch den Tarif des Austrittsmonats
und erhöht das Jahressechstel (nativ über bemessung='laufend')."""
self.ensure_one()
return self._at_uel_tage() * self._at_kv_gehalt_basis() \
/ self._at_uel_divisor()
def _at_uel_sz_tageswert(self):
"""SZ-Anteil je Urlaubstag: 2 Sonderzahlungen à Monatsentgelt
pro Jahr (KV § 11) je Monat 2 × SZ-Basis/12, als Tageswert
(auch bei bereits überaliquot bezahlten SZ, lb-url-16)."""
self.ensure_one()
sz_monat = 2.0 * self._at_sz_basis(self.date_to) / 12.0
return sz_monat / self._at_uel_divisor()
def _at_uel_sz(self):
"""UEL-Sonderzahlungsanteil: Tage × SZ-Tageswert."""
self.ensure_one()
return self._at_uel_tage() * self._at_uel_sz_tageswert()
# ------------------------------------------------------------------
# Abgangsentschädigung — § 67 Abs 8 lit b (Fünftelregelung)
# ------------------------------------------------------------------
def _at_fuenftel_regeln(self):
self.ensure_one()
return self.struct_id.rule_ids.filtered(
lambda rule: rule.l10n_at_s67_fuenftel)
def _at_fuenftel_total(self, result_rules=None):
self.ensure_one()
total = 0.0
for rule in self._at_fuenftel_regeln():
if result_rules is not None:
if rule.code in result_rules:
total += result_rules[rule.code]['total']
else:
linie = self.line_ids.filtered(lambda l: l.code == rule.code)
total += sum(linie.mapped('total'))
# § 67 Abs 8 lit a: der über den BV-6-%-Deckel hinausgehende
# Teil der Vergleichssumme fällt in die Fünftelregelung zurück.
total += self._at_vergleich_bv_ueberhang(result_rules)
return total
def _at_fuenftel_tarif_basis(self, result_rules=None):
"""Tarifischer Teil (4/5) der Fünftel-Bezüge: Bemessung nach
Abzug der darauf entfallenden §-62-Beiträge (approximiert über
den effektiven DN-Satz der laufenden Basis; die HBG-Caps der
Verlängerungsmonate werden im SV-Pfad separat geführt); ein
Fünftel ist steuerfrei, gedeckelt mit einem Fünftel des
Neunfachen der monatlichen HBG (lb-end-21: 2026 6.930 × 9/5
= 12.474)."""
self.ensure_one()
total = self._at_fuenftel_total(result_rules)
if total <= 0:
return 0.0
satz = sum(self._l10n_at_sv_satz(
'dn', komponente,
staffel_basis=self._l10n_at_sv_basis_roh(result_rules))
for komponente in ('kv', 'pv', 'av', 'ak', 'wf'))
bemessung = max(total * (1.0 - satz / 100.0), 0.0)
deckel = float(self._l10n_at_param('at_sv_hbg_monatlich')) \
* 9.0 / 5.0
frei = min(bemessung / 5.0, deckel)
return bemessung - frei
# ------------------------------------------------------------------
# Vergleichssumme — § 67 Abs 8 lit a (Rest, BV-Anwartschafts-Teil)
# ------------------------------------------------------------------
def _at_vergleich_bv_total(self, result_rules=None):
"""Summe der als BV-Anwartschafts-Teil gekennzeichneten
Vergleichs-Zeilen (Zeiträume mit BV-Kassen-Anwartschaft,
§ 67 Abs 8 lit a letzter Satz)."""
self.ensure_one()
total = 0.0
for rule in self.struct_id.rule_ids:
if not rule.l10n_at_vergleich_bv:
continue
if result_rules is not None:
if rule.code in result_rules:
total += result_rules[rule.code]['total']
else:
total += sum(self.line_ids.filtered(
lambda l, c=rule.code: l.code == c).mapped('total'))
return total
def _at_vergleich_bv_beguenstigt(self, result_rules=None):
"""Bis zum Deckel (2026: 7 500 €) begünstigter Teil — 6 %-Fahrspur
(parametrisiert: § 67 Abs 8 unterliegt der Inflationsanpassung
§ 33a/§ 124b EStG)."""
self.ensure_one()
deckel = float(self._l10n_at_param('at_lst_67_abs8_vergleich_bv'))
return min(self._at_vergleich_bv_total(result_rules), deckel)
def _at_vergleich_bv_ueberhang(self, result_rules=None):
"""Über den BV-Deckel hinausgehender Teil — fällt in die
Fünftelregelung zurück (erster Satz des lit a)."""
self.ensure_one()
deckel = float(self._l10n_at_param('at_lst_67_abs8_vergleich_bv'))
return max(self._at_vergleich_bv_total(result_rules) - deckel, 0.0)
def _at_vergleich_bv_lst_6(self, result_rules=None):
"""6 % auf den begünstigten BV-Teil nach anteiligem §-62-Abzug
(LStR Rz 1102b: die SV-Beiträge sind den Teilbeträgen
anteilsmäßig zuzuordnen approximiert über den effektiven
DN-Satz der laufenden Basis, wie im Fünftelpfad)."""
self.ensure_one()
beguenstigt = self._at_vergleich_bv_beguenstigt(result_rules)
if beguenstigt <= 0:
return 0.0
satz = sum(self._l10n_at_sv_satz(
'dn', komponente,
staffel_basis=self._l10n_at_sv_basis_roh(result_rules))
for komponente in ('kv', 'pv', 'av', 'ak', 'wf'))
bemessung = beguenstigt * (1.0 - satz / 100.0)
steuersatz = float(self._l10n_at_param(
'at_endabrechnung')['abfertigung_steuersatz'])
return bemessung * steuersatz / 100.0
def _at_fuenftel_frei_betr(self, result_rules=None):
"""Steuerfreies Fünftel der Fünftel-Bezüge (inkl. BV-Überhang),
gedeckelt mit einem Fünftel des Neunfachen der monatlichen HBG
(lb-end-21: 2026 6.930 × 9/5 = 12.474) getrennt vom
tarifischen Rest für die Wizard-Vorschau."""
self.ensure_one()
total = self._at_fuenftel_total(result_rules)
if total <= 0:
return 0.0
satz = sum(self._l10n_at_sv_satz(
'dn', komponente,
staffel_basis=self._l10n_at_sv_basis_roh(result_rules))
for komponente in ('kv', 'pv', 'av', 'ak', 'wf'))
bemessung = max(total * (1.0 - satz / 100.0), 0.0)
deckel = float(self._l10n_at_param('at_sv_hbg_monatlich')) \
* 9.0 / 5.0
return min(bemessung / 5.0, deckel)
# ------------------------------------------------------------------
# Engine-Anbindung: Tarif-Teil ohne Sechstel, LNK-Pflicht, SV-Umlegung
# ------------------------------------------------------------------
def _l10n_at_bemessung_laufen(self, result_rules=None):
basis = super()._l10n_at_bemessung_laufen(result_rules)
# § 67 Abs 8 lit b: 4/5 tarifisch im Zuflussmonat; die Regel
# selbst ist neutral und erhöht brutto('laufend') nicht → kein
# Sechstel-Effekt (Abs 2 nicht anzuwenden).
return basis + self._at_fuenftel_tarif_basis(result_rules)
def _at_lnk_basis(self, steuer, result_rules=None):
"""Upstream-Überschreibung (Muster account_posting_private;
Vorlage payslip_private._at_lnk_basis bei Änderungen dort
gegen diesen Pin neu abgleichen): Kündigungsentschädigungen und
Vergleichssummen sind Arbeitslöhne iSd § 41 Abs 3 FLAG und
unterliegen NICHT den Abs-4-Ausschlüssen (lit b nennt nur
§ 67 Abs 3 und 6 der lit-a-BV-Teil ist ebenfalls nicht
ausgenommen) sie sind VOR der Freibetragsstaffel in die
LNK-Basis aufzunehmen."""
self.ensure_one()
basis = (self._l10n_at_brutto('laufend', result_rules)
+ self._l10n_at_brutto('sonstig', result_rules)
- self._l10n_at_st_frei_freie_laufen(result_rules))
for rule in self.struct_id.rule_ids:
if (rule.l10n_at_bemessung in ('laufend', 'sonstig')
and rule.l10n_at_s67_abs in ('abs3', 'abs6')):
if result_rules is not None and rule.code in result_rules:
basis -= result_rules[rule.code]['total']
elif result_rules is None:
linie = self.line_ids.filtered(
lambda l: l.code == rule.code)
basis -= sum(linie.mapped('total'))
basis += self._at_fuenftel_total(result_rules)
# BV-Anwartschafts-Teil: begünstigter ≤-Deckel-Teil hier, der
# Überhang läuft bereits über _at_fuenftel_total — zusammen
# die volle Vergleichssumme, LNK-pflichtig (lb-end-20).
basis += self._at_vergleich_bv_beguenstigt(result_rules)
basis = max(basis, 0.0)
if steuer in ('flaf', 'dz'):
version = self.version_id
if (version.l10n_at_lnkp_frei_ab
and self.date_from >= version.l10n_at_lnkp_frei_ab):
return 0.0
freibetrag = self.env['hr.rule.parameter']._get_parameter_from_code(
'at_lnk_freibetrag', self.date_to, raise_if_not_found=False) or {}
grenze = float(freibetrag.get('grenze', 0.0))
if basis <= 0.0:
return 0.0
if basis <= grenze:
basis -= float(freibetrag.get('freibetrag', 0.0))
return max(basis, 0.0)
def _l10n_at_sv_basis_laufen_monate(self, result_rules=None):
"""Upstream-Überschreibung (Muster account_posting_private;
Vorlage l10n_at_hr_payroll.sozialversicherung bei Odoo- oder
Kern-Upgrades gegen diesen Pin neu abgleichen): identischer
Ablauf wie der Kern, zusätzlich werden Regeln mit AT-SV-
Verlängerung tageproportional auf die Verlängerungsmonate
gelegt. Der Standardanteil jedes Monats bleibt auf die volle
monatliche HBG gedeckelt, der Verlängerungsanteil auf den
Tageswert (monatliche HBG × Verlängerungstage/30, lb-url-15);
die Fenster laufen sequenziell (Abgang zuerst, UEL danach).
AP14-B: die später fällige Prämie (PRAM_END_TARIF) rollt über
l10n_at_pram_beitragsmonat in den Beitragsmonat des
Austrittsjahres (E-MVB 044, lb-end-18); die LSt bleibt im
Zuflussmonat (§ 67 Abs 10)."""
self.ensure_one()
gruppen = {}
verlaengerung = {}
tage_pro_monat = {}
for rule in self.struct_id.rule_ids:
if self._l10n_at_sv_art(rule) != 'laufend':
continue
werte = None
if result_rules is not None:
if rule.code in result_rules:
werte = result_rules[rule.code]
else:
linie = self.line_ids.filtered(
lambda l: l.code == rule.code)
if linie:
werte = {'total': sum(linie.mapped('total'))}
if not werte:
continue
if rule.l10n_at_sv_verlaengerung != 'keine':
for monat, tage, anteil in \
self._at_end_rule_verlaengerung(rule, result_rules):
verlaengerung[monat] = verlaengerung.get(monat, 0.0) \
+ werte['total'] * anteil
tage_pro_monat[monat] = \
tage_pro_monat.get(monat, 0) + tage
continue
if rule.code == 'PRAM_END_TARIF' \
and self.l10n_at_pram_beitragsmonat:
monat = self.l10n_at_pram_beitragsmonat.replace(day=1)
else:
monat = (rule.l10n_at_beitragsmonat or self.date_from) \
.replace(day=1)
gruppen[monat] = gruppen.get(monat, 0.0) + werte['total']
standard = self.date_from.replace(day=1)
if standard in gruppen:
gruppen[standard] -= self._l10n_at_sv49_freie_laufen(
result_rules)
ergebnis = []
for monat in sorted(set(gruppen) | set(verlaengerung)):
hbg = float(self._l10n_at_param('at_sv_hbg_monatlich', monat))
basis = min(max(gruppen.get(monat, 0.0), 0.0), hbg)
if monat in verlaengerung:
tageswert = hbg * tage_pro_monat[monat] / 30.0
basis += min(max(verlaengerung[monat], 0.0), tageswert)
ergebnis.append((monat, basis))
return ergebnis
@@ -0,0 +1,48 @@
# Part of the odoo-at-payroll project. License: LGPL-3.
"""GP8-Privat: Verlinkung des Fristenkalenders mit den
privatwirtschaftlichen GP7-Deklarationen (L16/mBGM-Sprungziele)."""
from odoo import fields, models
class L10nAtPayrollFrist(models.Model):
_inherit = 'l10n.at.payroll.frist'
ref_mbgm_id = fields.Many2one(
'l10n.at.payroll.mbgm', string='mBGM-Deklaration',
help='Sprungziel für SV-Fristen (Privatmodul — das '
'Kernmodell kennt mBGM nicht).')
def action_zum_objekt(self):
"""Sprung zur GP7-Deklaration (L16 oder mBGM)."""
self.ensure_one()
result = super().action_zum_objekt()
if result is not True:
return result
if self.ref_mbgm_id:
return {
'type': 'ir.actions.act_window',
'res_model': 'l10n.at.payroll.mbgm',
'res_id': self.ref_mbgm_id.id,
'view_mode': 'form',
}
return True
def _l10n_at_verlinke_deklarationen(self):
"""Sprungziele für offene L16- und mBGM-Fristen setzen: neueste
Deklaration des Bezugsjahres verlinken, wenn vorhanden."""
for frist in self:
if frist.fristart in ('l16', 'arbeitsstaette'):
l16 = self.env['l10n.at.payroll.l16'].search([
('company_id', '=', frist.company_id.id),
('jahr', '=', frist.jahr),
], order='id desc', limit=1)
frist.ref_l16_id = l16 or False
elif frist.fristart == 'sv_mbgm':
mbgm = self.env['l10n.at.payroll.mbgm'].search([
('company_id', '=', frist.company_id.id),
('beitragsmonat', '>=',
'%d-%02d-01' % (frist.jahr, frist.monat or 1)),
('beitragsmonat', '<=',
'%d-%02d-01' % (frist.jahr, frist.monat or 12)),
], order='id desc', limit=1)
frist.ref_mbgm_id = mbgm or False
@@ -3,11 +3,20 @@
hr.employee Muster ``hr_version_bgld.py`` des GemBG-Moduls. hr.employee Muster ``hr_version_bgld.py`` des GemBG-Moduls.
Die Stufenlogik der Referenz-KVs läuft über Verwendungsgruppenjahre Die Stufenlogik der Referenz-KVs läuft über Verwendungsgruppenjahre
(VWGJ, Biennal-/Triennalsprünge, § 17 der Rahmen-KVs); die Vorrückung (VWGJ, Biennal-/Triennalsprünge, § 17 der Rahmen-KVs verifiziert
selbst ist GP2-Thema (Wizard analog GemBG), hier werden die SI-2203: Abs 3 Einreihung, Abs 4 Anzahl der angerechneten VWGJ durch
Vertragsfelder und der Katalog-Lookup bereitgestellt. die Firmenleitung mit Dienstzettel-Mitteilung, Abs 5 Vorrückung am
Ersten des Erreichungsmonats). AP9 ergänzt die Vordienstzeiten:
Die Firma erkennt Vordienstzeiten in Monaten an (Dienstzettel),
daraus leitet der Einstufungs-Vorschlag die Erfahrungsstufe ab
(``vwgj_ab`` der Stufenzeilen) und den nächsten Vorrückungstermin.
Freigabe bewusst manuell (action_einstufung_uebernehmen).
""" """
from odoo import fields, models from datetime import date
from dateutil.relativedelta import relativedelta
from odoo import api, fields, models
class HrVersion(models.Model): class HrVersion(models.Model):
@@ -31,6 +40,35 @@ class HrVersion(models.Model):
help="Individuell vereinbartes Monatsgehalt; überschreibt das " help="Individuell vereinbartes Monatsgehalt; überschreibt das "
"Kataloggehalt der Gruppe/Stufe (Günstigkeitsvergleich, " "Kataloggehalt der Gruppe/Stufe (Günstigkeitsvergleich, "
"z. B. § 18 Anrechnung der Rahmen-KVs).") "z. B. § 18 Anrechnung der Rahmen-KVs).")
l10n_at_vordienstzeit_monate = fields.Integer(
string='AT Angerechnete Vordienstzeiten (Monate)',
groups="hr_payroll.group_hr_payroll_user", tracking=True,
help="Von der Firmenleitung anerkannte Vordienstzeiten in "
"Monaten (§ 17 Abs 4 Rahmen-KV: Anzahl der angerechneten "
"Verwendungsgruppenjahre — Dienstzettel-Mitteilungspflicht). "
"Fließen in den Einstufungs-Vorschlag und den nächsten "
"Vorrückungstermin ein. AP9.")
l10n_at_vwgj_monate = fields.Integer(
string='AT VWGJ gesamt (Monate)',
compute='_compute_l10n_at_vwgj',
groups="hr_payroll.group_hr_payroll_user",
help="Verwendungsgruppenjahre-Stand heute: Dienstzeit-Monate "
"seit Vertragsbeginn zuzüglich angerechnener "
"Vordienstzeiten.")
l10n_at_vwgj_stufe_vorschlag = fields.Integer(
string='AT Erfahrungsstufen-Vorschlag (VWGJ)',
compute='_compute_l10n_at_vwgj',
groups="hr_payroll.group_hr_payroll_user",
help="Vorgeschlagene Erfahrungsstufe aus den vwgj_ab-Schwellen "
"der Gruppen-Tabelle (nach N VWGJ); 0 = Schwellen "
"nicht gepflegt → manuelle Einstufung maßgeblich.")
l10n_at_naechste_vorrueckung = fields.Date(
string='AT Nächste VWGJ-Vorrückung',
compute='_compute_l10n_at_vwgj',
groups="hr_payroll.group_hr_payroll_user",
help="§ 17 Abs 5 Rahmen-KV: Erster des Monats, in dem die "
"nächste VWGJ-Schwelle erreicht wird (Vertragsbeginn + "
"Schwelle Vordienstzeiten).")
currency_id = fields.Many2one( currency_id = fields.Many2one(
'res.currency', related='company_id.currency_id') 'res.currency', related='company_id.currency_id')
l10n_at_personalgruppe = fields.Selection( l10n_at_personalgruppe = fields.Selection(
@@ -50,10 +88,97 @@ class HrVersion(models.Model):
self.ensure_one() self.ensure_one()
return self.kv_gruppe_id._get_gehalt(self.erfahrungsstufe, datum) return self.kv_gruppe_id._get_gehalt(self.erfahrungsstufe, datum)
# ------------------------------------------------------------------
# AP9: Vordienstzeiten → VWGJ-Einstufungsvorschlag (§ 17 Rahmen-KV)
# ------------------------------------------------------------------
@api.depends('kv_gruppe_id', 'contract_date_start',
'l10n_at_vordienstzeit_monate', 'erfahrungsstufe')
def _compute_l10n_at_vwgj(self):
"""VWGJ-Stand heute (Dienstzeit + Vordienstzeiten), Stufen-
Vorschlag aus den vwgj_ab-Schwellen der Gruppe und der nächste
Vorrückungstermin (Erster des Erreichungsmonats, § 17 Abs 5)."""
heute = fields.Date.context_today(self)
for version in self:
version.l10n_at_vwgj_monate = 0
version.l10n_at_vwgj_stufe_vorschlag = 0
version.l10n_at_naechste_vorrueckung = False
if not (version.kv_gruppe_id
and version.contract_date_start):
continue
delta = relativedelta(heute, version.contract_date_start)
dienstzeit_monate = delta.years * 12 + delta.months
version.l10n_at_vwgj_monate = max(
dienstzeit_monate
+ (version.l10n_at_vordienstzeit_monate or 0), 0)
jahre = version.l10n_at_vwgj_monate // 12
version.l10n_at_vwgj_stufe_vorschlag = \
version.kv_gruppe_id._get_stufe_fuer_vwgj(jahre, heute)
naechste = version.kv_gruppe_id._get_naechste_vwgj(
jahre, heute)
if naechste:
erreichung = version.contract_date_start \
+ relativedelta(
months=naechste * 12
- (version.l10n_at_vordienstzeit_monate or 0))
# § 17 Abs 5: Erster des Monats, in dem die erhöhte
# Anzahl der VWGJ erreicht wird.
version.l10n_at_naechste_vorrueckung = erreichung \
.replace(day=1)
def action_einstufung_uebernehmen(self):
"""Vorschlag als Erfahrungsstufe übernehmen — bewusst ein
expliziter Verwaltungsakt (§ 17 Abs 4: Firmenleitung, mit
Dienstzettel bekanntzugeben); kein automatisches Schreiben."""
for version in self:
if version.l10n_at_vwgj_stufe_vorschlag:
version.erfahrungsstufe = \
version.l10n_at_vwgj_stufe_vorschlag
l10n_at_status = fields.Selection(
[('an', 'Arbeitnehmer'),
('organ_minder', 'Organ ≤ 25 %% — SV-pflichtig'),
('organ_mehr', 'Organ ≥ 25 %% — kein SV-Verhältnis')],
string='AT Status (Organ/Geschäftsführung)',
default='an', required=True,
groups="hr_payroll.group_hr_payroll_user",
help='AP19 (lb-vst-03, lb-gsf-02/03/05): vertragsrechtlich freie '
'Dienstverhältnisse von Organnen. Organ ≥ 25 % Beteiligung: '
'kein SV-Verhältnis (§ 4 Abs 2 ASVG-Ausnahme — kein SVDN/SVDG, '
'kein mBGM, LSt normal); Organ ≤ 25 %: pflichtversichert wie '
'Dienstnehmer (§ 4 Abs 2 dritter Satz ASVG). '
'Mandanten-Entscheid je Fall; Rechtsberatung vor Umstellung.')
l10n_at_ferialpraktikant = fields.Boolean(
string='AT Ferialpraktikant/Praktikum',
groups="hr_payroll.group_hr_payroll_user",
help='AP19 (lb-pra-01…04): Ferialpraktikum/Volontariat. '
'SV/LSt laufen über die normale Entgelt-Logik (§ 4 Abs 2 '
'ASVG: unter GFG nur UV; Volontariat ohne Taschengeld: '
'UV-Teilversicherung § 8 Abs 1 Z 3 lit c — Meldewesen, '
'nicht Entgeltrechnung). BMSVG: § 6 nennt keinen '
'Ferialpraktikanten-Ausschluss — Beitrag wie sonstig '
'(Erstfenster).')
class HrEmployee(models.Model): class HrEmployee(models.Model):
_inherit = 'hr.employee' _inherit = 'hr.employee'
l10n_at_vordienstzeit_monate = fields.Integer(
related="version_id.l10n_at_vordienstzeit_monate", inherited=True,
readonly=False, groups="hr_payroll.group_hr_payroll_user")
l10n_at_vwgj_monate = fields.Integer(
related="version_id.l10n_at_vwgj_monate", inherited=True,
groups="hr_payroll.group_hr_payroll_user")
l10n_at_vwgj_stufe_vorschlag = fields.Integer(
related="version_id.l10n_at_vwgj_stufe_vorschlag", inherited=True,
groups="hr_payroll.group_hr_payroll_user")
l10n_at_naechste_vorrueckung = fields.Date(
related="version_id.l10n_at_naechste_vorrueckung", inherited=True,
groups="hr_payroll.group_hr_payroll_user")
def action_einstufung_uebernehmen(self):
"""Delegation an die Version (Button am Mitarbeiterformular)."""
self.mapped('version_id').action_einstufung_uebernehmen()
kv_id = fields.Many2one( kv_id = fields.Many2one(
related="version_id.kv_id", inherited=True, related="version_id.kv_id", inherited=True,
readonly=False, groups="hr_payroll.group_hr_payroll_user") readonly=False, groups="hr_payroll.group_hr_payroll_user")
@@ -0,0 +1,135 @@
# Part of the odoo-at-payroll project. License: LGPL-3.
"""AP16: Karenz-/Familien-Freistellungen — unbezahlte Karenz-Arten
(Elternkarenz generisch, Bildungs-, Familienhospiz-, Kinderrehabilita-
tions- und Pflegekarenz, Papa-Monat) mit Entgelt-Stopp, eSV-An- und
Abmeldeereignissen und der BMSVG-§-7-Brücke.
Am Originalwortlaut verifiziert (.ris/, 8.13 freigegeben 2026-09-15):
- Alle Voll-Freistellungen sind entgeltlos (AVRAG §§ 11, 14a, 14b,
14e, 14c; VKG § 1a) Entgelt-Stopp je Arbeitstag über die
negative Regel ``KARENZ_ENTGELT`` (Muster MSCH_ENTGELT, AP7).
- eSV: Abmeldung zum Ende des Entgeltanspruchs (Tag vor dem
Karenzbeginn), Anmeldung zum ersten Arbeitstag nach dem Ende;
Papamonat/Familienzeit: Abmeldung am Tag vor Beginn (§ 11 Abs 3
lit a ASVG, lb-pap-01). Karenzart-Strings der Sonderformate
Familienhospizkarenz/Pflegekarenz An-/Abmeldung" (lb-kzs-05)
laufen als Protokoll-Info erweiterte eSV-Sonderformate = GP7-Rest.
- BV: Elternkarenz/KBG zu Lasten FLAF (§ 7 Abs 5 BMSVG), Familien-
hospiz-/Pflegekarenz zu Lasten des Bundes (§ 7 Abs 6 BMSVG,
Fiktivgrundlage KBG § 5b Abs 1 KBGG idF vor BGBl I 53/2016);
Pflegeteilzeit: AG-Beitrag auf Vor-Reduzierungs-Entgelt (§ 6 Abs 4
BMSVG, Feld l10n_at_bv_vor_entgelt).
- Dienstzeit: Papamonat ist ANZURECHNEN (§ 1a Abs 7 VKG iVm § 15f
MSchG), Bildungs-/Pflegekarenz grundsätzlich NICHT (§ 11 Abs 2,
§ 14c/14d AVRAG) Wizard-bestätigte Dienstzeiten, dokumentiert.
"""
from datetime import timedelta
from odoo import fields, models
# Unbezahlte Karenz-WETs (generisch + AP16-Spezies)
_KARENZ_WET = 'ATP_KARENZ'
_BILDUNGSKARENZ_WET = 'ATP_BILDUNGSKARENZ'
_HOSPIZKARENZ_WET = 'ATP_HOSPIZKARENZ'
_KINDER_KRANK_WET = 'ATP_KINDERKRANK'
_KINDER_REHAB_WET = 'ATP_KINDERREHAB'
_PFLEGEKARENZ_WET = 'ATP_PFLEGEKARENZ'
_PAPAMONAT_WET = 'ATP_PAPAMONAT'
KARENZ_ARTEN = {
_KARENZ_WET: 'Elternkarenz (MSchG/VKG)',
_BILDUNGSKARENZ_WET: 'Bildungskarenz (§ 11 AVRAG)',
_HOSPIZKARENZ_WET: 'Familienhospizkarenz — Sterbebegleitung '
'(§ 14a AVRAG)',
_KINDER_KRANK_WET: 'Familienhospizkarenz — Begleitung schwerst '
'erkrankter Kinder (§ 14b AVRAG)',
_KINDER_REHAB_WET: 'Freistellung Kinderrehabilitation '
'(§ 14e AVRAG)',
_PFLEGEKARENZ_WET: 'Pflegekarenz (§ 14c AVRAG)',
_PAPAMONAT_WET: 'Papa-Monat/Familienzeit (§ 1a VKG / FamZeitbG)',
}
class HrPayslip(models.Model):
_inherit = 'hr.payslip'
def _at_karenz_wet_codes(self):
"""Codes der unbezahlten Karenz-Freistellungen (inkl. generisch
und AP16-Spezies)."""
self.ensure_one()
return list(KARENZ_ARTEN)
def _at_karenz_leaves(self):
"""Validierte Karenz-Freistellungen: [(von, bis, wett_code)]
chronologisch."""
self.ensure_one()
typen = self.env['hr.leave.type'].search([
('work_entry_type_id.code', 'in', self._at_karenz_wet_codes())
])
leaves = self.env['hr.leave'].search([
('employee_id', '=', self.employee_id.id),
('holiday_status_id', 'in', typen.ids),
('state', 'in', ('validate', 'validate1')),
])
ergebnis = []
for leave in leaves:
wett = leave.holiday_status_id.work_entry_type_id.code
ergebnis.append((leave.request_date_from,
leave.request_date_to, wett))
return [(von, bis, wett) for von, bis, wett in ergebnis
if von and bis]
def _at_karenz_status(self):
"""Karenz-Tage je Abrechnungsperiode über alle unbezahlten
Karenz-Arten: {'kalendertage', 'arbeitstage', 'arten'} None
ohne Freistellung im Zeitraum (Muster _at_msch_status)."""
self.ensure_one()
codes = set(self._at_karenz_wet_codes())
tage = {}
tag_art = {}
for von, bis, wett in self._at_karenz_leaves():
tag = fields.Date.to_date(von)
ende = fields.Date.to_date(bis)
while tag <= ende:
tage[tag] = True
tag_art[tag] = wett
tag += timedelta(days=1)
in_periode = sorted(
tag for tag in tage
if self.date_from <= tag <= self.date_to)
if not in_periode:
return None
eintraege = self.env['hr.work.entry'].search([
('employee_id', '=', self.employee_id.id),
('date', '>=', self.date_from),
('date', '<=', self.date_to),
('state', '!=', 'cancelled'),
('work_entry_type_id.code', 'in', list(codes)),
])
arten_aktuell = {tag_art[tag] for tag in in_periode}
return {
'kalendertage': in_periode,
'arbeitstage': sorted({e.date for e in eintraege}),
'arten': sorted(KARENZ_ARTEN[c] for c in arten_aktuell),
}
def _at_karenz_entgelt_tage(self, result_rules=None):
"""Arbeitstage der unbezahlten Karenz im Abrechnungszeitraum
(Entgelt-Stopp-Basis der Regel KARENZ_ENTGELT)."""
self.ensure_one()
status = self._at_karenz_status()
if not status:
return 0
return len(status['arbeitstage'])
class HrVersion(models.Model):
_inherit = 'hr.version'
l10n_at_bv_vor_entgelt = fields.Monetary(
string='BV-Basis vor Arbeitszeit-Reduktion (§ 6 Abs 4 BMSVG)',
currency_field='currency_id',
help='Monatsentgelt vor der Reduzierung (z. B. Pflegeteilzeit '
'§ 14d AVRAG): der BV-Beitrag läuft während der Maßnahme '
'auf Grundlage der Arbeitszeit vor Herabsetzung '
'(§ 6 Abs 4 BMSVG). Leer = laufende Basis.')
@@ -176,6 +176,42 @@ class L10nAtPayrollKvGruppe(models.Model):
('gueltig_ab', '<=', datum)], ('gueltig_ab', '<=', datum)],
order='gueltig_ab desc', limit=1) order='gueltig_ab desc', limit=1)
def _get_stufe_fuer_vwgj(self, vwgj_jahre, datum=None):
"""Erfahrungsstufe für vollendete Verwendungsgruppenjahre
(VWGJ) zum Stichtag AP9. Basis: vwgj_ab der Stufenzeilen
(nach N VWGJ"; 0 = ab Beginn). Gibt die höchste Stufe mit
vwgj_ab <= vwgj_jahre zurück; 0, wenn die Gruppe keine
gepflegten Schwellen hat (manuelle Einstufung maßgeblich).
Rahmen-KV § 17 Abs 3-5 (Biennal-/Triennalsprünge, Vorrückung
am Ersten des Erreichungsmonats)."""
self.ensure_one()
datum = datum or fields.Date.context_today(self)
# -1 = Schwelle nicht gepflegt (ORM liest Integer-NULL als 0 —
# deshalb Konvention -1 statt NULL; Domain > -1 filtert beides).
zeilen = self.env['l10n.at.payroll.kv.gruppe.stufe'].search(
[('gruppe_id', '=', self.id),
('gueltig_ab', '<=', datum),
('vwgj_ab', '>', -1)])
beste = zeilen.filtered(lambda z: z.vwgj_ab <= vwgj_jahre)
if not beste:
return 0
return max(beste.mapped('stufe'))
def _get_naechste_vwgj(self, vwgj_jahre, datum=None):
"""Nächste VWGJ-Schwelle oberhalb der erreichten Jahre (für
den Vorrückungstermin, § 17 Abs 5) None an der Höchststufe."""
self.ensure_one()
datum = datum or fields.Date.context_today(self)
zeilen = self.env['l10n.at.payroll.kv.gruppe.stufe'].search(
[('gruppe_id', '=', self.id),
('gueltig_ab', '<=', datum),
('vwgj_ab', '>', -1)])
schwellen = sorted(set(zeilen.mapped('vwgj_ab')))
for schwelle in schwellen:
if schwelle > vwgj_jahre:
return schwelle
return None
def _max_stufe(self, datum=None): def _max_stufe(self, datum=None):
"""Höchste Stufe mit gültigem Monatsgehalt zum Stichtag.""" """Höchste Stufe mit gültigem Monatsgehalt zum Stichtag."""
self.ensure_one() self.ensure_one()
@@ -202,6 +238,13 @@ class L10nAtPayrollKvGruppeStufe(models.Model):
bezeichnung = fields.Char( bezeichnung = fields.Char(
help="Stufen-Label der KV-Tabelle, z. B. 'im 1. u 2. VWGJ' " help="Stufen-Label der KV-Tabelle, z. B. 'im 1. u 2. VWGJ' "
"oder 'nach 15 VWGJ'.") "oder 'nach 15 VWGJ'.")
vwgj_ab = fields.Integer(
string='Ab (vollendete) VWGJ', index=True, default=-1,
help="Vollendete Verwendungsgruppenjahre, ab denen die Stufe "
"gebührt — aus dem Stufen-Label (§ 17 Abs 3-5 "
"Rahmen-KV: Biennal-/Triennalsprünge). 0 = ab Beginn; "
"-1 = Schwelle nicht gepflegt → kein Einstufungs-"
"Vorschlag (manuelle Einstufung maßgeblich). AP9.")
gehalt = fields.Monetary(currency_field='currency_id') gehalt = fields.Monetary(currency_field='currency_id')
currency_id = fields.Many2one( currency_id = fields.Many2one(
'res.currency', default=lambda self: self.env.company.currency_id) 'res.currency', default=lambda self: self.env.company.currency_id)
@@ -0,0 +1,710 @@
# 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 _l10n_at_gfb_positionen(self, slip):
"""AP17: Verrechnungspositionen für geringfügig Beschäftigte
(BSGR B010/B030): T01 = § 53a-UV-Pauschalbeitrag (Engine:
``_l10n_at_svdg_uv_geringfuegig`` 2026 6,50 /Monat, abgeleitet
aus aBG 591,29 × 1,1 %; § 51 Abs 2 ASVG), V01 = BMSVG-Beitrag
1,53 % (Abfertigungsbeitrag § 5 Abs 2 BMSVG, Engine-Position
BV). Keine AV-/IE-Anteile, keine Abschläge (A01A03/A09/A10/
A15), kein T02 GfB-SZ sind beitragsfrei (§ 49 Abs 3;
lb-bes-06)."""
self.ensure_one()
uv_pauschale = slip._l10n_at_svdg_uv_geringfuegig()
positionen = []
if uv_pauschale:
positionen.append(('T01', 0.0, uv_pauschale, 'AB'))
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 _l10n_at_bv_jahresabrechnung_gfb(self, jahr):
"""Jährliche Abrechnung GfB (§ 58 Abs 8 ASVG iVm § 6 Abs 2a
BMSVG): Summe der JAGB-mBGM-Beiträge des Kalenderjahres und
der 2,5 vH-Zuschlag (Z04 BV-Zuschlag bei jährlicher Zahlung,
D.60) Fälligkeit bis 15. Jänner des Folgejahres."""
self.ensure_one()
deklarationen = self.search([
('company_id', '=', self.company_id.id),
('beitragsmonat', '>=', date(jahr, 1, 1)),
('beitragsmonat', '<=', date(jahr, 12, 31)),
('art', '=', 'MB'),
('state', '=', 'done'),
])
summe = sum(
line.vsum
for deklaration in deklarationen
for line in deklaration.line_ids.filtered('jagb'))
zuschlag = round(summe * 0.025, 2)
return {'summe': summe, 'zuschlag': zuschlag,
'gesamt': round(summe + zuschlag, 2)}
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()
# AP17: mBGM-Pakete — 'normal' (JAGB 'N') und — bei
# jährlicher Abrechnung GfB (§ 58 Abs 8 ASVG iVm § 6
# Abs 2a BMSVG) — ein separates JAGB-Paket (E.32.2.2.1/D.52)
# für die GfB-Beitragsgrundlagen je Kalendermonat.
paket_gruppen = {
'normal': {'saetze': [], 'summe': 0.0, 'anzahl': 0},
'jagb': {'saetze': [], 'summe': 0.0, 'anzahl': 0},
}
jagb_flag = company.l10n_at_gfb_jaehrliche_abrechnung
if deklaration.art == 'ST':
jagb_flag = False
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 getattr(version, 'l10n_at_status', 'an') \
== 'organ_mehr':
warnungen.append(
'Mitarbeiter %s: Organ ≥ 25 %% (kein SV-'
'Verhältnis, AP19) — keine mBGM; LSt/Lohnzettel '
'unverändert.' % 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)
paket_gruppen['normal']['saetze'].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,
})
paket_gruppen['normal']['summe'] += original.vsum
paket_gruppen['normal']['anzahl'] += 1
continue
bsgr = deklaration._l10n_at_bsgr(version, slip)
if slip.l10n_at_geringfuegig:
# GfB (AP17): eigenes Beitragsset — § 53a-UV-
# Pauschale (T01) + BMSVG 1,53 % (V01), keine
# AV-/IE-/Abschlagspositionen, kein T02 (GfB-SZ
# beitragsfrei, § 49 Abs 3).
positionen = deklaration._l10n_at_gfb_positionen(
slip)
if not positionen:
fehler.append(
'Mitarbeiter %s: GfB-Beitragssätze fehlen '
'(§-53a-Parameter am Beitragsmonat).'
% employee.name)
continue
else:
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
positionen = deklaration._l10n_at_positionen(
slip, tarif)
if version.l10n_at_fallweise:
fehler.append(
'Mitarbeiter %s: fallweise mBGM (G3/G4 mit '
'tagesgenauen Tarifblöcken T2/T5) ist ein '
'dokumentierter Rest (BSGR %s).'
% (employee.name, bsgr))
continue
vsum = sum(rsum for _c, _s, rsum, _b in positionen)
# Konsistenz gegen die GP4-Payslip-Summen.
if slip.l10n_at_geringfuegig:
ist = (slip._l10n_at_svdg_uv_geringfuegig()
+ slip._at_bv_beitrag())
else:
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))
gruppe = (
'jagb' if (slip.l10n_at_geringfuegig and jagb_flag)
else 'normal')
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,
'jagb': gruppe == 'jagb',
})
paket_gruppen[gruppe]['saetze'].extend(
deklaration._l10n_at_mbgm_datensaetze(
employee, version, bsgr, positionen, vsum))
paket_gruppen[gruppe]['summe'] += vsum
paket_gruppen[gruppe]['anzahl'] += 1
datensaetze = []
for schluessel in ('normal', 'jagb'):
gruppe = paket_gruppen[schluessel]
if not gruppe['anzahl'] and not (
deklaration.art == 'FZ'
and schluessel == 'normal'):
continue
paket = L10nAtEldaSatz(
L10N_AT_ELDA_IDTEIL + MBGM_PAKET, 326,
bezeichnung='mBGM-Paket %s' % schluessel)
paket.setze('SART', 'PS')
paket.setze('VSTR', vstr)
paket.setze('REFP', 'MBGM-%d-%s%s' % (
company.id,
deklaration.beitragsmonat.strftime('%Y%m'),
'-JAGB' if schluessel == 'jagb' else ''))
paket.setze('BKNR', bknr)
paket.setze('DGNA', company.name[:70])
paket.setze('JAGB', 'J' if schluessel == 'jagb' else 'N')
paket.setze('BZRM', deklaration.beitragsmonat.strftime(
'%m%Y'))
paket.setze('GSVZ',
'+' if gruppe['summe'] >= 0 else '-')
paket.setze('GSUM', abs(gruppe['summe']), cent=True)
paket.setze('ANZM', gruppe['anzahl'])
datensaetze.append(paket)
datensaetze.extend(gruppe['saetze'])
if jagb_flag and paket_gruppen['jagb']['summe']:
warnungen.append(
'JAGB-Paket: jährliche Abrechnung der GfB-Beiträge '
'(§ 58 Abs 8 ASVG iVm § 6 Abs 2a BMSVG) — Fälligkeit '
'bis 15. Jänner des Folgejahres; der jährliche BV-'
'Beitragsbetrag ist um 2,5 %% Zuschlag zu erhöhen '
'(Summe mBGM-Jahr: siehe Protokoll der '
'Dezember-Abrechnung).')
if jagb_flag and deklaration.beitragsmonat.month == 12 \
and paket_gruppen['jagb']['summe']:
jahresabrechnung = deklaration\
._l10n_at_bv_jahresabrechnung_gfb(
deklaration.beitragsmonat.year)
warnungen.append(
'GfB-Jahresabrechnung %d: Beiträge %.2f € + '
'2,5 %% Zuschlag %.2f € (Z04) = %.2f €, fällig '
'bis 15. 1. %d.' % (
deklaration.beitragsmonat.year,
jahresabrechnung['summe'],
jahresabrechnung['zuschlag'],
jahresabrechnung['gesamt'],
deklaration.beitragsmonat.year + 1))
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))
if datensaetze:
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'),),
})
deklaration.write({
'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_gfb_jaehrliche_abrechnung = fields.Boolean(
string='AT GfB: jährliche Abrechnung der Beiträge (JAGB)',
help='Vereinbarung nach § 58 Abs 8 ASVG iVm § 6 Abs 2a BMSVG: '
'die Abfertigungs-/BV-Beiträge aus geringfügigen '
'Beschäftigungsverhältnissen werden jährlich verrechnet. '
'mBGM-seitig je Kalendermonat ein eigenes Paket mit '
'JAGB = „J“ (DM-ORG D.52/E.32.2.2.1); Fälligkeit bis '
'15. Jänner des Folgejahres (unterjähriges AV-Ende: '
'bis 15. des Folgemonats). Bei jährlicher Zahlungsweise '
'sind zusätzlich 2,5 vH vom Beitrag zu leisten. '
'Änderung der Zahlungsweise nur zum Jahresende und dem '
'KV-Träger vor dem Beitragszeitraum zu melden.')
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')
jagb = fields.Boolean(
string='JAGB-Paket (jährliche Abrechnung GfB)',
help='Zeile gehört zum mBGM-Paket für die jährliche Abrechnung '
'geringfügig Beschäftigter (DM-ORG D.52).')
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')
@@ -0,0 +1,345 @@
# Part of the odoo-at-payroll project. License: LGPL-3.
"""AP7: Mutterschutz/Wochengeld (MSchG §§ 3/5, ASVG § 162, BMSVG
§ 7 Abs 4 verifiziert 2026-09-12 gegen RIS bzw. Wissensbasis
lb-msf-03, Stand 2026-07).
**Klargestellte Rollenverteilung** (lb-msf-03, § 162 Abs 1 ASVG): Das
Wochengeld ist eine **Leistung aus der Krankenversicherung** die ÖGK
zahlt es direkt an die Arbeitnehmerin. Der Dienstgeber
1. schuldet für die Beschäftigungsverbots-Tage **kein Entgelt**
(Regel ``MSCH_ENTGELT`` stoppt die arbeitsseitigen Bezüge),
2. liefert die **Bemessungsdaten** (13-Wochen-/3-Kalendermonats-
Basis; Arbeits- und Entgeltbestätigung folgt mit AP8) hier als
vollständige Wochengeld-Berechnung nach § 162 Abs 3 ASVG:
Ø-Arbeitsverdienst der letzten drei Kalendermonate (Geld- und
Sachbezüge, Entstehungsprinzip), vermindert um Lohnsteuer,
SV-Anteil, Kammerumlage, Wohnbauförderungsbeitrag, Aufwandersätze
und Schlechtwetterentschädigungsbeitrag (= Netto der
Referenzläufe), erhöht um den satzungsmäßigen Sonderzahlungs-
Prozentsatz (14/17/21 %, ``at_msch_sz_prozentsatz`` mandanten-
seitig zu pflegen), je Kalendertag,
3. zahlt die **Abfertigung Neu auf fiktiver Grundlage** weiter
(§ 7 Abs 4 BMSVG): fiktive BG = Monatsentgelt, berechnet nach dem
in den letzten drei Kalendermonaten vor dem Versicherungsfall der
Mutterschaft gebührenden Entgelt **einschließlich anteiliger
Sonderzahlungen** (SZ-Fortzahlung während des Bezugs = vereinbarte
Ausnahme, dokumentierte Option), aliquot nach Kalendertagen
(30-Tage-Konvention).
Schutzfristen: § 3 MSchG Achtwochenfrist vor der voraussichtlichen
Entbindung; § 5 MSchG acht Wochen Wochenbett (mindestens zwölf bei
Früh-/Mehrlingsgeburten oder Kaiserschnitt; Verlängerung um
vorzeitig verkürzte Achtwochenfrist, max. 16 Wochen) als
Urlaubsart Mutterschutz-Frist" (WET ``ATP_MSCH``) zu erfassen; die
Fristberechnung aus dem ärztlichen Zeugnis bleibt beim Mandanten.
Urlaubsanspruch erwächst während des Wochengeldbezugs (lb-msf-03);
Sonderwochengeld (§ 163 ASVG, Günstigkeitsvergleich § 162 Abs 3b) =
dokumentierte Option.
AP16 (8.13): Karenz-Brücke nach § 7 Abs 4 Z 13 BMSVG folgt das
neuerliche Beschäftigungsverbot unmittelbar auf eine Karenz (Z 1) oder
nach einer kürzeren als dreimonatigen Beschäftigung (Z 2), ist die
Bemessungsgrundlage die des Kalendermonats vor dem der Karenz
unmittelbar vorangegangenen Beschäftigungsverbots; Z 3 (neues DV nach
Beendigung des karenzierten DV, < 3 Monate): der letzte volle
Kalendermonat vor dem neuerlichen Verbot. BV während des KBG-Bezugs
läuft zu Lasten des FLAF (§ 7 Abs 5), in der Familienhospiz-/Pflege-
karenz zu Lasten des Bundes (§ 7 Abs 6) keine AG-Regeln; das Ruhen
des Wochengeldes (§ 166 ASVG) und das Sonderwochengeld während
einer Karenz (§ 163 ASVG) werden als Info/Warnung ausgewiesen.
"""
from datetime import date, timedelta
from dateutil.relativedelta import relativedelta
from odoo import fields, models
from .karenz import KARENZ_ARTEN
_MSCH_WET = 'ATP_MSCH'
class HrPayslip(models.Model):
_inherit = 'hr.payslip'
l10n_at_msch_info = fields.Char(
string='AT Mutterschutz/Wochengeld-Status',
compute='_compute_msch_info',
help='Wochengeld je Kalendertag (§ 162 Abs 3 ASVG), '
'Bezugstage und fiktive BV-Grundlage (§ 7 Abs 4 BMSVG) '
'im Abrechnungszeitraum.')
def _compute_msch_info(self):
for slip in self:
msch = slip._at_msch_status()
if not msch:
slip.l10n_at_msch_info = ''
continue
wochengeld = slip._at_wochengeld()
info = (
'%d Kalendertage Bezug · WG %.2f/Kalendertag '
'(Ø-Netto %.2f + %.0f %%)' % (
len(msch['kalendertage']),
wochengeld['tagesatz'],
wochengeld['netto_drei_monate'],
wochengeld['sz_prozent']))
if slip._at_msch_sonderwochengeld():
info += (' · Sonderwochengeld (§ 163 ASVG) statt '
'Wochengeld — ÖGK, erhöhtes Krankengeld '
'(§ 141 Abs 2, § 125 iVm § 108f)')
warnungen = slip._at_msch_ruhen_warnungen()
if warnungen:
info += ' · Ruhen: ' + '; '.join(warnungen)
slip.l10n_at_msch_info = info
def _at_msch_leaves(self):
"""Validierte Mutterschutz-Frist-Abwesenheiten (ATP_MSCH)."""
self.ensure_one()
typen = self.env['hr.leave.type'].search([
('work_entry_type_id.code', '=', _MSCH_WET)])
leaves = self.env['hr.leave'].search([
('employee_id', '=', self.employee_id.id),
('holiday_status_id', 'in', typen.ids),
('state', 'in', ('validate', 'validate1')),
])
return [
(leave.request_date_from, leave.request_date_to)
for leave in leaves
if leave.request_date_from and leave.request_date_to]
def _at_msch_status(self):
"""Mutterschutz-Tage je Abrechnungsperiode: {'kalendertage',
'arbeitstage', 'episode_beginn'} None ohne Frist im Zeitraum.
Episode-Beginn = Anfang des zusammenhängenden Fristenblocks,
der in die Abrechnungsperiode reicht (Versicherungsfall der
Mutterschaft, § 120 Abs 1 Z 3 ASVG) Bemessungsstichtag für
Wochengeld (§ 162 Abs 3) und fiktive BV (§ 7 Abs 4 BMSVG)."""
self.ensure_one()
tage = {}
for von, bis in self._at_msch_leaves():
tag = von
while tag <= bis:
tage[tag] = True
tag += timedelta(days=1)
in_periode = sorted(
tag for tag in tage
if self.date_from <= tag <= self.date_to)
if not in_periode:
return None
block = in_periode[0]
while (block - timedelta(days=1)) in tage:
block = block - timedelta(days=1)
eintraege = self.env['hr.work.entry'].search([
('employee_id', '=', self.employee_id.id),
('date', '>=', self.date_from),
('date', '<=', self.date_to),
('state', '!=', 'cancelled'),
('work_entry_type_id.code', '=', _MSCH_WET),
])
return {
'kalendertage': in_periode,
'arbeitstage': sorted({e.date for e in eintraege}),
'episode_beginn': block,
}
def _at_msch_sonderwochengeld(self):
"""Sonderwochengeld statt Wochengeld (§ 163 ASVG, verifiziert):
die Mutterschutz-Frist fällt in eine Karenz nach dem MSchG
kein Anspruch auf Wochengeld (§ 162), das Sonderwochengeld
zahlt der Krankenversicherungsträger (erhöhtes Krankengeld
§ 141 Abs 2, Bemessungsgrundlage § 125 aus dem Arbeits-
verdienst vor dem Ende des letzten Entgeltanspruchs, mit
§ 108f-Anpassung)."""
self.ensure_one()
msch = self._at_msch_status()
if not msch:
return False
for von, bis, _wett in self._at_karenz_leaves():
if any(von <= tag <= bis for tag in msch['kalendertage']):
return True
return False
def _at_msch_ruhen_warnungen(self):
"""Ruhen des Wochengeldes (§ 166 ASVG, verifiziert) —
Warnungen zu Fortbezug während der Frist:
Z 2 Entgeltfortzahlung 100 % Ruhen zur Gänze, 50 %
hälftig (Urlaubs-/UEL-/KE-Zeiten gelten nicht als weiterge-
leistete Bezüge); Z 3 Erwerbstätigkeit und Z 4 Pflegekarenz-
geld sind Bezugszeitraum-Daten (AEB/ÖGK-Seite, Hinweis)."""
self.ensure_one()
msch = self._at_msch_status()
if not msch:
return []
tage = set(msch['kalendertage'])
efz = self._at_efz_status()
warnungen = []
if efz:
if any(tag in tage for tag in efz.get('voll_arbeitstage', [])):
warnungen.append('Fortbezug > 50 % (Entgeltfortzahlung) '
'— Ruhen zur Gänze (§ 166 Abs 1 Z 2)')
elif any(tag in tage for tag in efz.get('halb_arbeitstage', [])):
warnungen.append('Fortbezug = 50 % — Ruhen hälftig '
'(§ 166 Abs 1 Z 2)')
warnungen.append('Erwerbstätigkeit/Pflegekarenzgeld während der '
'Frist prüfen (§ 166 Abs 1 Z 3/4)')
return warnungen
def _at_msch_referenz_stichtag_und_anzahl(self, episode_beginn):
"""(Stichtag, Referenz-Monatsanzahl) nach § 7 Abs 4 BMSVG mit
Karenz-Brücke Z 13 (AP16):
- Z 1/Z 2: endete eine Karenz innerhalb von drei Monaten vor
dem neuerlichen Beschäftigungsverbot, gilt das der Karenz
unmittelbar vorangegangene Beschäftigungsverbot als
Bemessungsstichtag (Referenzmonate davor);
- Z 3: neues DV binnen drei Monaten nach Beendigung des
karenzierten DV (mit Karenz) der letzte volle Kalendermonat
vor dem neuerlichen Verbot als EINZELNER Referenzmonat
(volles Monatsentgelt");
- sonst: Versicherungsfall selbst, drei Monate."""
self.ensure_one()
stichtag = episode_beginn or self.date_from
grenze = stichtag - relativedelta(months=3)
karenz_kandidat = None
for von, bis, _wett in sorted(self._at_karenz_leaves(),
key=lambda k: k[1], reverse=True):
if grenze <= bis < stichtag:
karenz_kandidat = (von, bis)
break
if karenz_kandidat:
karenz_von = karenz_kandidat[0]
frist = max(
(m for m in self._at_msch_leaves()
if m[1] <= karenz_von),
key=lambda m: m[1], default=None)
if frist:
return frist[0], 3
version = self.version_id
vorher = self.env['hr.version'].search([
('employee_id', '=', self.employee_id.id),
('contract_date_end', '!=', False),
('id', '!=', version.id),
], order='contract_date_end desc', limit=1)
if (vorher and vorher.contract_date_end
and version.contract_date_start
and vorher.contract_date_end
<= version.contract_date_start
and (version.contract_date_start
- vorher.contract_date_end).days < 92
and any(k[1] <= vorher.contract_date_end
for k in self._at_karenz_leaves())):
erster = date(stichtag.year, stichtag.month, 1)
erster = (erster - timedelta(days=1)).replace(day=1)
return erster, 1
return stichtag, 3
def _at_msch_referenz_monate(self, episode_beginn):
"""Referenzkalendermonate der fiktiven BV (§ 7 Abs 4 BMSVG:
letzte drei Monate; Karenz-Brücke Z 13, AP16 s.
_at_msch_referenz_stichtag_und_anzahl; § 162 Abs 3: letzte
13 Wochen). Versicherungsfall im Monat X Monate X-3, X-2,
X-1 (aufsteigend)."""
self.ensure_one()
stichtag, anzahl = self._at_msch_referenz_stichtag_und_anzahl(
episode_beginn)
erster = date(stichtag.year, stichtag.month, 1)
monate = []
hilf = erster
for _ in range(anzahl):
hilf = (hilf - timedelta(days=1)).replace(day=1)
monate.append(hilf)
return list(reversed(monate))
def _at_msch_referenz_slip(self, monats_beginn):
"""Vollständiger validierter Referenzlauf des Monats (oder
leer)."""
self.ensure_one()
monats_ende = (monats_beginn.replace(day=28)
+ timedelta(days=4)).replace(day=1) \
- timedelta(days=1)
return self.env['hr.payslip'].search([
('employee_id', '=', self.employee_id.id),
('state', 'in', ('validated', 'paid')),
('date_from', '<=', monats_beginn),
('date_to', '>=', monats_ende),
], order='date_from desc', limit=1)
def _at_wochengeld(self):
"""Wochengeld-Berechnung nach § 162 Abs 3 ASVG (volle
Berechnung, AP7 Ausweis/AEB-Daten; Zahlung durch die ÖGK):
Ø-Arbeitsverdienst der drei Referenzmonate, vermindert um
Lohnsteuer, SV-Anteil, Kammerumlage, Wohnbauförderungsbeitrag,
Aufwandersätze und Schlechtwetterentschädigungsbeitrag (=
Netto der Referenzläufe), erhöht um den satzungsmäßigen
SZ-Prozentsatz, je Kalendertag der drei Monate. Kurzes
Dienstverhältnis: Ø über die vorhandenen Monate (lb-msf-03);
ohne jede Historie der laufende Monat."""
self.ensure_one()
sz_prozent = float(self.env['hr.rule.parameter']
._get_parameter_from_code(
'at_msch_sz_prozentsatz', self.date_from,
raise_if_not_found=False) or 0.0)
ergebnis = {
'netto_drei_monate': 0.0, 'sz_prozent': sz_prozent,
'kalendertage_basis': 0, 'tagesatz': 0.0,
'referenz_monate': [], 'episode_beginn': None,
}
msch = self._at_msch_status()
if not msch:
return ergebnis
ergebnis['episode_beginn'] = msch['episode_beginn']
netto, kalendertage, gefundene = 0.0, 0, []
for monat in self._at_msch_referenz_monate(
msch['episode_beginn']):
ergebnis['referenz_monate'].append(monat)
monats_ende = (monat.replace(day=28)
+ timedelta(days=4)).replace(day=1) \
- timedelta(days=1)
kalendertage += monats_ende.day
slip = self._at_msch_referenz_slip(monat)
if slip:
gefundene.append(monat)
netto += sum(slip.line_ids.filtered(
lambda l: l.code == 'NET').mapped('total'))
if not gefundene:
# Ohne jede Historie: Netto des laufenden Monats
# (tatsächlicher Arbeitszeitraum, lb-msf-03).
netto = sum(self.line_ids.filtered(
lambda l: l.code == 'NET').mapped('total'))
kalendertage = 30
ergebnis['netto_drei_monate'] = netto
ergebnis['kalendertage_basis'] = kalendertage
ergebnis['tagesatz'] = netto * (1.0 + sz_prozent / 100.0) \
/ max(kalendertage, 1)
return ergebnis
def _at_bv_fiktiv_wochengeld(self):
"""Fiktive BV-Grundlage während des Wochengeldbezugs
(§ 7 Abs 4 BMSVG, verifiziert): Monatsentgelt-Ø aus den
letzten drei Kalendermonaten vor dem Versicherungsfall der
Mutterschaft **einschließlich anteiliger Sonderzahlungen**
(hier: laufende + sonstige Bezüge der Referenzläufe;
SZ-Fortzahlung während des Bezugs = vereinbarte Ausnahme,
Option), aliquot nach Kalendertagen des Bezugs
(30-Tage-Konvention)."""
self.ensure_one()
msch = self._at_msch_status()
if not msch or not msch['kalendertage']:
return 0.0
summe, monate = 0.0, 0
for monat in self._at_msch_referenz_monate(
msch['episode_beginn']):
slip = self._at_msch_referenz_slip(monat)
if not slip:
continue
summe += (slip._l10n_at_brutto('laufend')
+ slip._l10n_at_brutto('sonstig'))
monate += 1
if not monate:
# Ohne Historie: laufender Monat (kurzes DV, lb-msf-03).
summe = (self._l10n_at_brutto('laufend')
+ self._l10n_at_brutto('sonstig'))
monate = 1
return summe / monate * len(msch['kalendertage']) / 30.0

Some files were not shown because too many files have changed in this diff Show More