mirror of
http://100.103.83.12:3003/fegger/odoo-at-payroll.git
synced 2026-09-17 16:56:42 +00:00
Compare commits
72 Commits
529ac30cdb
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| a3ef14ec40 | |||
| 9ba625f059 | |||
| 8d9ce2f2d4 | |||
| 67330a241f | |||
| 09f25533cb | |||
| f027e93666 | |||
| 9f120a4075 | |||
| a848c07e96 | |||
| 26a20e83b2 | |||
| f233cb706c | |||
| 30a25a4601 | |||
| e1a6e9e0b2 | |||
| 5090f07880 | |||
| af8bfcaa6a | |||
| b42c326d7f | |||
| 4f5107a46a | |||
| c8b55fbd38 | |||
| aa637270b7 | |||
| e4726e6cfd | |||
| ec33dfacf2 | |||
| 7e1d466ab7 | |||
| 1df5fb19c4 | |||
| efe76e6b3d | |||
| 31ed539941 | |||
| 27f3b4b7f9 | |||
| 16f6591e36 | |||
| 39d80d371e | |||
| 209a64dc53 | |||
| d53e1e64a1 | |||
| 1f4b4ce933 | |||
| 850e99df20 | |||
| 472b19a4b2 | |||
| 4b86bcb790 | |||
| 23a5ef74dc | |||
| 9add58fe83 | |||
| aa7adfd464 | |||
| 4687cd4792 | |||
| 7fb569b7cf | |||
| 97aa3c1c82 | |||
| b3e8e47cdd | |||
| 9e1d2fd2b0 | |||
| 263bbd2cac | |||
| 4fa98b7e82 | |||
| 50e62509e6 | |||
| cf16426f5f | |||
| b5ca81abab | |||
| 9d141eed9a | |||
| ee94f4664f | |||
| 9ff977e0d4 | |||
| 3f307a0688 | |||
| 91ac9e5fda | |||
| 8efea77b6c | |||
| 018554b215 | |||
| c3b9d6eb8e | |||
| 802f2ef04c | |||
| e20de9d543 | |||
| e444c1b2c6 | |||
| 11954f41d7 | |||
| 290b432b73 | |||
| 0abc1383d1 | |||
| 9b4221fcef | |||
| 34e6750fe3 | |||
| 3c85cc6790 | |||
| 3c5cc3aded | |||
| 8b9dbf7612 | |||
| 447a880063 | |||
| 2f55bfb21a | |||
| 57522b5be3 | |||
| c843701d1a | |||
| ea14c48cca | |||
| 14940ac31a | |||
| c474a39466 |
+1063
-7
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||||
|
M1–M4, 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; 8–12 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`: 30–50 questions with expected
|
||||||
|
IDs, including conflict cases (ATZ quotas) and 3–5 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.
|
||||||
@@ -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/
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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
|
||||||
@@ -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.1–E.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. 175–177, 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-06…08):
|
||||||
|
- 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='1–12; 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-01…04, 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')
|
||||||
@@ -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. 196–209: 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 (1–12), 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 (1–12) des vollen Familienbonus Plus (L16 '
|
||||||
|
'KEGFP).')
|
||||||
|
fab_halb_monat_von = fields.Integer(
|
||||||
|
string='Halber FB+ ab Monat (KBHFP)',
|
||||||
|
help='Monat (1–12), 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 (1–12) 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))
|
||||||
@@ -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. 219–220; 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. 223–233; 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 3–8
|
||||||
|
(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 196–209): je Kind 123 Stellen ab Pos. 1501
|
||||||
|
# (Block 1 = 1501–1623 … Block 15 = 3223–3345; 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='0–8 (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 5–10). 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 5–10).'
|
||||||
|
% 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 1–26 (Z 1 Schicht-/Wechseldienst in Nachtarbeit 22–6 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änner–Ende 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 502–761)
|
||||||
|
(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 1–26 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 1–26.' % 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:
|
||||||
|
|||||||
@@ -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 & 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="{"widget": "monetary", "display_currency": j['run'].company_id.currency_id}"/></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td colspan="3"><strong>Summe Bezüge & Abzüge</strong></td>
|
||||||
|
<td class="text-end"><strong><span t-out="j['summe_arten']" t-options="{"widget": "monetary", "display_currency": 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="{"widget": "monetary", "display_currency": 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="{"widget": "monetary", "display_currency": 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="{"widget": "monetary", "display_currency": run.company_id.currency_id}"/></td>
|
||||||
|
<td class="text-end"><span t-out="z['svdg']" t-options="{"widget": "monetary", "display_currency": run.company_id.currency_id}"/></td>
|
||||||
|
<td class="text-end"><span t-out="z['spalten']['FLAFDB']" t-options="{"widget": "monetary", "display_currency": run.company_id.currency_id}"/></td>
|
||||||
|
<td class="text-end"><span t-out="z['spalten']['KOMMST']" t-options="{"widget": "monetary", "display_currency": run.company_id.currency_id}"/></td>
|
||||||
|
<td class="text-end"><span t-out="z['spalten']['DZ']" t-options="{"widget": "monetary", "display_currency": run.company_id.currency_id}"/></td>
|
||||||
|
<td class="text-end"><span t-out="z['spalten']['BVG']" t-options="{"widget": "monetary", "display_currency": run.company_id.currency_id}"/></td>
|
||||||
|
<td class="text-end"><span t-out="z['spalten']['WIEN_DAG']" t-options="{"widget": "monetary", "display_currency": run.company_id.currency_id}"/></td>
|
||||||
|
<td class="text-end"><span t-out="z['weitere']" t-options="{"widget": "monetary", "display_currency": run.company_id.currency_id}"/></td>
|
||||||
|
<td class="text-end"><span t-out="z['dg_summe']" t-options="{"widget": "monetary", "display_currency": run.company_id.currency_id}"/></td>
|
||||||
|
<td class="text-end"><strong><span t-out="z['gesamtkosten']" t-options="{"widget": "monetary", "display_currency": 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="{"widget": "monetary", "display_currency": run.company_id.currency_id}"/></strong></td>
|
||||||
|
<td class="text-end"><strong><span t-out="summen['svdg']" t-options="{"widget": "monetary", "display_currency": run.company_id.currency_id}"/></strong></td>
|
||||||
|
<td class="text-end"><strong><span t-out="summen['spalten']['FLAFDB']" t-options="{"widget": "monetary", "display_currency": run.company_id.currency_id}"/></strong></td>
|
||||||
|
<td class="text-end"><strong><span t-out="summen['spalten']['KOMMST']" t-options="{"widget": "monetary", "display_currency": run.company_id.currency_id}"/></strong></td>
|
||||||
|
<td class="text-end"><strong><span t-out="summen['spalten']['DZ']" t-options="{"widget": "monetary", "display_currency": run.company_id.currency_id}"/></strong></td>
|
||||||
|
<td class="text-end"><strong><span t-out="summen['spalten']['BVG']" t-options="{"widget": "monetary", "display_currency": run.company_id.currency_id}"/></strong></td>
|
||||||
|
<td class="text-end"><strong><span t-out="summen['spalten']['WIEN_DAG']" t-options="{"widget": "monetary", "display_currency": run.company_id.currency_id}"/></strong></td>
|
||||||
|
<td class="text-end"><strong><span t-out="summen['weitere']" t-options="{"widget": "monetary", "display_currency": run.company_id.currency_id}"/></strong></td>
|
||||||
|
<td class="text-end"><strong><span t-out="summen['dg_summe']" t-options="{"widget": "monetary", "display_currency": run.company_id.currency_id}"/></strong></td>
|
||||||
|
<td class="text-end"><strong><span t-out="summen['gesamtkosten']" t-options="{"widget": "monetary", "display_currency": 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
|
||||||
|
|||||||
|
@@ -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>
|
||||||
@@ -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 pos−1).
|
||||||
|
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)
|
||||||
@@ -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 & 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) & 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>
|
||||||
@@ -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 01–04, 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.
|
||||||
|
**GP1–GP8 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
|
||||||
|
|||||||
|
@@ -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 (Wert−8)×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 60–65) und LSt (seq 70–71), -->
|
||||||
|
<!-- 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) > 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) > 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) > 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 (Wert−8)×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 1–42 (1. 9.–12. 10.) → September ohne Reduktion;
|
||||||
|
Oktober Arbeitstage 13.–31. 10. (14 AT) → Halbphase.
|
||||||
|
- Halb: Kalendertage 43–70 (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 41–42 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 + 7−N 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: 2–12 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, 1–3 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 1–3 BMSVG)
|
||||||
|
|
||||||
|
Mutterschutz-Frist 1.–15. 11. 2026 (Verbot 1) → Elternkarenz
|
||||||
|
16. 11. 2026–28. 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 2026–Feb 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.1–5.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.1–E.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 01–04/
|
||||||
|
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 1–3, 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 Aug–Okt
|
||||||
|
(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 1–3 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: 4–7 Fahrten → 1/3, 8–10 → 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.220−1.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.000−5.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.000−8) = **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, 365−0) = **300 €** → voll frei.
|
||||||
|
- Februar: YTD 300 → frei min(100, 365−300) = **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
|
||||||
|
186−100 = **86 €**, Überhang **14 €** (test_04).
|
||||||
|
- **Rabatt**: Jänner 700 € (frei, über 20 %), Februar 500 € → frei
|
||||||
|
1.000−700 = **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 100−70 = 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 35–45 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 (650–655 SV Arbeiter · 656–659 SV Angestellte)
|
# DG-Beiträge (650–655 SV Arbeiter · 656–659 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 (Montag–Sonntag)."""
|
||||||
|
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 7−N
|
||||||
|
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 (36→2/12 … 300→12/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 (A01–A03/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 1–3 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 1–3 (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 1–3, 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
Reference in New Issue
Block a user