[IMP] l10n_at_hr_payroll_private: GP6 FI-Buchung UGB (KFS/BW-6-Matrix, EKR-Debit-Split, NET-Konvention)

Buchungsmatrix nach dem Oesterreichischen Einheitskontenrahmen (KFS/BW 6; Nutzer-Referenz-Exemplar .additional_docs/KFSBW6_13072017_RF.pdf): Hook _configure_payroll_account_at (models/account_chart_private.py) ordnet Regelkonten, Journal SLR (Default = Gehaelterkonto 6200, BE-Muster) und das NET-Konto 3850 "Verbindlichkeiten aus Lohn und Gehalt" (Nutzer-Entscheidung 5.1; Anlage + reconcile, l10n_at-Standardchart hat es nicht) je Company zu — getriggert beim Chart-Load (_post_load_data) UND ueber die <function>-Datendatei data/hr_payroll_account_private.xml fuer Bestands-Companies (erster <value> einer function ist die Record-ID-Liste, convert.py-Semantik), idempotent. Konto-Suche exakt zuerst, Praefix-Fallback nur bis eine Stelle laenger — verhindert Matches auf Odoo-Scaffold-Konten chartloser Firmen (6000 darf nicht 600000 "Expenses" matchen).

EKR-Debit-Split Arbeiter/Angestellte (Nutzer-Klaerung: 600-619 Loehne, 620-639 Gehaelter — "6000 Gehaelter/6200 Loehne" war gegen die eigene KFS/BW-6-Referenz vertauscht): l10n_at_personalgruppe am Vertrag (models/hr_version_private.py) + Override-Feld l10n_at_account_debit_arbeiter je Regel; Option A minimal (Nutzer-Entscheidung 5.2: Grundentgelt 6000, Sozialaufwand 6500, Abgaben 6600/6601/6602; Zuschlaege/UeStd/Praemien/SZ und BV (EKR 644 ohne Split) buchen fuer Arbeiter bis zur Referenzbuchhaltungs-Fixierung auf die Angestellten-Konten — fehlende Konten warnen, keine Erfindung). models/account_posting_private.py: Gesamtueberschreibung von _prepare_slip_lines nach Upstream-Muster (Odoo-Pin 19.0+e.20260910) mit einzeiliger Kontosubstitution — Odoo bietet dafuer keinen Override-Punkt; Verhalten testgepinnt. Matrix: KVGEO/URL_UE 6200/6000, Zuschlaege 6225, UeStd 6310, Praemien 6230, B13/B14 6240, DN-SV/LSt auf 3600/3540, NET 3850, SVDG-Block 6560/6500 auf 3600, BVG 6440 (Fallback 6400) auf Credit 3600 (Abfuhr via KV-Traeger, Nutzer 5.3), FLAFDB 6661/6601 auf 3541, KOMMST 6660/6600 auf 3550, DZ 6662/6602 auf 3542.

NET-Umbenennung NETTO -> NET (Odoo-Konvention: action_register_payment, net_wage und Negativ-Netto-Erkennung filtern code == NET) inkl. 4 Bestands-Suiten. Odoo-19-Fund: das Anlegen einer Structure kopiert die Regeln des hr_payroll.default_structure (BASIC/GROSS/NET/...) via Feld-Default in rule_ids — die kopierte Default-NET-Regel kollidierte im result_rules-Dict (Netto fiel auf 0); Fix: <function> in salary_rules_private.xml archiviert die Kopie (active=False; KEIN Unlink — struct_id ist NOT NULL und historische Lohnkontolinien behalten ihre Regel-Referenz; One2many blendet Archivierte aus). Journal-Default-Konten duerfen nicht Receivable/Payable sein (Odoo-Constraint); Rundungscents bucht Odoo als Adjustment Entry aufs Journal-Default.

depends: + l10n_at, hr_payroll_account_iso20022 (SEPA-Auszahlung, Plan 4.5; kein VRV/MVAG — gem360-Bruecke). Tests: test_account_private (4: RF-BUCH-1..4 — Kontenplan-Mock ohne Chart-Install, Move-Balance, Kontenzeilen je Matrix, Arbeiter-Split, Geringfuegiger, Fallback/Warnung) — gesamt 88/88 gruen auf Wegwerf-DB (frischer -i-Lauf). Rechenfaelle docs/rechenfaelle/buchhaltung.md. 19.0.6.0.0.
This commit is contained in:
2026-09-12 12:58:53 +02:00
parent 1e88fd1636
commit 27654211fb
16 changed files with 842 additions and 25 deletions
+33 -8
View File
@@ -95,16 +95,41 @@ iVm § 49 ASVG — kein HBG-/Jahresdeckel, ohne §-67-Abs-3/6-Zeilen gemäß
Rechenfälle `docs/rechenfaelle/abfertigung.md`.
**Offen (GP6+):** Meldewesen/Belege (GP7), FI-Buchung UGB (GP6),
Jahreswechsel (GP8); All-in-UI-Bericht (GP8); B14 längster-Urlaubs-Teil-
Verfeinerung; DZ-WKO-Verifikation (8.11) vor Wien-Mandanten;
§-7-BMSVG-Fiktivfälle (Präsenzdienst/Kranken-/Wochengeld) als
dokumentierte Option (Rechenfälle-Datei).
**GP6 FI-Buchung UGB (umgesetzt 2026-09-12):** Buchungsmatrix nach dem
Österreichischen Einheitskontenrahmen (KFS/BW 6): Hook
`_configure_payroll_account_at` (Chart-Load + `<function>`-Datendatei,
idempotent; Konto-Suche exakt mit Längen-gebundenem Präfix-Fallback —
kein Match auf Odoo-Scaffold-Konten) ordnet Regelkonten, Journal SLR
(Default = Gehälterkonto, BE-Muster) und das NET-Konto **3850
„Verbindlichkeiten aus Lohn und Gehalt"** (Anlage + reconcile) zu.
**EKR-Debit-Split Arbeiter/Angestellte:** `l10n_at_personalgruppe` am
Vertrag + Override-Feld `l10n_at_account_debit_arbeiter` je Regel
(Option A minimal: Grundentgelt 6000, Sozialaufwand 6500, Abgaben
6600/6601/6602; Zuschläge/ÜStd/SZ/Prämien buchen bis zur
Referenzbuchhaltungs-Fixierung auf die Angestellten-Konten);
Gesamtüberschreibung von `_prepare_slip_lines` (Odoo bietet keinen
Override-Punkt; durch Tests gepinnt). Netto-Regel auf Odoo-Konvention
**NET** umbenannt + die automatisch kopierte Default-NET-Regel wird
archiviert (Code-Kollision im result_rules-Dict; archiviert statt
unlinkt — historische Linien behalten die Referenz).
Auszahlung über `hr_payroll_account_iso20022`; kein VRV/MVAG
(gem360-Brücke).
Rechenfälle `docs/rechenfaelle/buchhaltung.md`.
**Offen (GP7+):** Meldewesen/Belege (GP7), Jahreswechsel (GP8);
All-in-UI-Bericht (GP8); B14 längster-Urlaubs-Teil-Verfeinerung;
DZ-WKO-Verifikation (8.11) vor Wien-Mandanten; §-7-BMSVG-Fiktivfälle
(Präsenzdienst/Kranken-/Wochengeld) als dokumentierte Option
(Rechenfälle-Datei); Arbeiter-Sub-Konten (Option A-Rest) mit der
Referenzbuchhaltung fixieren (GP0-Rest).
Tests: `test_kv_katalog` + `test_entgelt_private` (GP2) +
`test_lst_private` (GP3) + `test_sv_private` (GP4) +
`test_bv_private` (GP5) —
`.venv/bin/python -m odoo -d <testdb> -u l10n_at_hr_payroll,l10n_at_hr_payroll_private
`test_bv_private` (GP5) + `test_account_private` (GP6)
`.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
--addons-path=odoo_19.0+e.20260910/odoo-19.0+e.20260910/odoo/addons,addons`
(Lokal grün am 2026-09-11: **84/84 Tests**.)
(Lokal grün am 2026-09-12 auf Wegwerf-DB, frischer `-i`-Lauf:
**88/88 Tests** — NET-Rundungscents bucht Odoo als „Adjustment Entry"
auf das Journal-Default.)
@@ -1,7 +1,7 @@
# Part of the odoo-at-payroll project. License: LGPL-3.
{
'name': 'Österreich - Privatwirtschaft Personalverrechnung (General-AT)',
'version': '19.0.5.0.0',
'version': '19.0.6.0.0',
'author': 'ixsol / gem360',
'website': 'https://gem360.at',
'category': 'Human Resources/Payroll',
@@ -41,7 +41,7 @@ Grundlagen: ``personalverrechnung/IMPLEMENTIERUNGSPLAN-Privat.md`` und
(§ 41 FLAG), KOMMST (§ 9 KommStG) und DZ (§§ 122/126 WKG,
company-flag-gated) mit Freibetragsstaffel 1.095/1.460 € —
Logik/Werte im Bundes-Kern (D3-Vorfinanzierung).
- **GP5 Abfertigung Neu (dieser Stand):** BMSVG-Regel ``BVG``
- **GP5 Abfertigung Neu (umgesetzt):** BMSVG-Regel ``BVG``
(§ 6: 1,53 % DG auf Entgelt und SZ, Kern-Parameter
``at_sv_bv_dg``) auf EIGENER, ungecappter BV-Grundlage (§ 6 Abs 5
iVm § 49 ASVG — kein HBG-Cap, ohne §-67-Abs-3/6-Zeilen),
@@ -52,17 +52,34 @@ Grundlagen: ``personalverrechnung/IMPLEMENTIERUNGSPLAN-Privat.md`` und
Monate am Mitarbeiter; Anwartschaftsreport (QWeb) + Endabrechnungs-
Wizard (§§ 14-17: Verfügungsausschluss-Matrix, 36-Beitragsmonate,
§-16-Fälligkeit, §-17-Optionen).
- **GP6 FI-Buchung UGB (dieser Stand):** Buchungsmatrix nach dem
Österreichischen Einheitskontenrahmen (KFS/BW 6): Hook
``_configure_payroll_account_at`` (Chart-Load + ``<function>``-
Datendatei, idempotent) zuordnet Regelkonten, Journal SLR und das
NET-Konto ``3850 Verbindlichkeiten aus Lohn und Gehalt`` (wird
angelegt, reconcile); **EKR-Debit-Split Arbeiter/Angestellte**
über ``l10n_at_personalgruppe`` am Vertrag + Override-Feld
``l10n_at_account_debit_arbeiter`` je Regel (Option A minimal:
Grundentgelt 6000, Sozialaufwand 6500, Abgaben 6600/6601/6602);
Netto-Regel auf Odoo-Konvention ``NET`` umbenannt
(Zahlungsregistrierung/SEPA via ``hr_payroll_account_iso20022``).
Auszahlung über ``hr_payroll_account_iso20022``; kein VRV/MVAG
(gem360-Brücke).
- **Seed D2 (Referenz-KVs R1):** SI-2203 „Handwerk und Gewerbe" +
SI-2748 „Metallgewerbe" (Gehaltstabellen ab 1. 1. 2026, kuratiert aus
den KV-Library-Volltexten).
- **Folgen (GP6+):** FI-Buchung UGB (GP6), Meldewesen/Belege (GP7),
Jahreswechsel (GP8); §-7-BMSVG-Fiktivfälle (Präsenzdienst/
Kranken-/Wochengeld) als dokumentierte Option.
- **Folgen (GP7+):** Meldewesen/Belege (GP7), Jahreswechsel (GP8);
All-in-UI-Bericht (GP8); B14 längster-Urlaubs-Teil-Verfeinerung;
DZ-WKO-Verifikation (8.11) vor Wien-Mandanten; §-7-BMSVG-
Fiktivfälle (Präsenzdienst/Kranken-/Wochengeld) als dokumentierte
Option; Referenzbuchhaltungs-Fixierung der Arbeiter-Sub-Konten
(Option A-Rest).
Kein VRV-Bezug (Gemeinde-Schiene bleibt ``l10n_at_gemeinde_payroll`` +
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'],
'data': [
'security/ir.model.access.csv',
'data/kv_seed.xml',
@@ -76,6 +93,7 @@ gem360-Brücke).
'data/salary_rules_lst_private.xml',
'data/salary_rules_sv_private.xml',
'data/salary_rules_bvg_private.xml',
'data/hr_payroll_account_private.xml',
'data/bv_kasse_seed.xml',
'views/kv_views.xml',
'views/bv_views.xml',
@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- GP6 FI-Buchung UGB: Buchungsmatrix für Bestands-Companies -->
<!-- auf dem AT-Kontenrahmen (res.company.chart_template ist das -->
<!-- Selection-Feld mit dem Template-Code, Muster -->
<!-- l10n_be_hr_payroll_account). Der ERSTE <value> ist die -->
<!-- Record-ID-Liste des Aufrufs (convert.py: browse(ids)) — -->
<!-- AbstractModel → []. Frische Chart-Loads laufen automatisch -->
<!-- über account.chart.template._post_load_data → -->
<!-- _load_payroll_accounts (Kontext chart_template_load). Beide -->
<!-- Pfade idempotent (Konto-/Journalsuche, Matrix-Write). -->
<data noupdate="1">
<function model="account.chart.template" name="_load_payroll_accounts">
<value eval="[]"/>
<value>at</value>
<value model="res.company"
eval="obj().search([('chart_template', '=', 'at')])"/>
</function>
</data>
</odoo>
@@ -367,10 +367,14 @@ result = bezahlt * (payslip._at_sz_dienstzeit_faktor() - 1.0)
]]></field>
</record>
<!-- NETTO (Kategoriensummen; DN-Abzüge folgen GP3/GP4). -->
<!-- NET (Kategoriensummen; DN-Abzüge folgen GP3/GP4). Odoo- -->
<!-- Regelkonvention: hr_payroll_account erwartet für die -->
<!-- Zahlungsregistrierung den Netto-Regelcode 'NET' (action_ -->
<!-- register_payment filtert code == 'NET' und prüft account_ -->
<!-- credit.reconcile) — GP6-Umbenennung von NETTO. -->
<record id="rule_netto" model="hr.salary.rule">
<field name="name">Netto</field>
<field name="code">NETTO</field>
<field name="code">NET</field>
<field name="sequence" eval="100"/>
<field name="category_id" ref="l10n_at_hr_payroll_private.category_atp_netto"/>
<field name="struct_id" ref="l10n_at_hr_payroll_private.structure_at_privat"/>
@@ -380,4 +384,15 @@ result = bezahlt * (payslip._at_sz_dienstzeit_faktor() - 1.0)
result = categories['ATP_BRUTTO'] - categories['ATP_ABZUG']
]]></field>
</record>
<!-- GP6: die beim Structure-Anlegen automatisch kopierte -->
<!-- Default-NET-Regel (hr_payroll.default_structure, Feld- -->
<!-- Default _get_default_rule_ids) von der Structure abhängen -->
<!-- und archivieren — Code-Kollision mit rule_netto (Code NET -->
<!-- nach Odoo-Konvention): sonst überschreiben sich zwei NET- -->
<!-- Regeln im result_rules-Dict und die Netto-Zeile fällt auf 0. -->
<!-- Methode: models/payslip_private.py; idempotent. -->
<function model="hr.payroll.structure" name="_l10n_at_entferne_default_net_kopie">
<value eval="[ref('l10n_at_hr_payroll_private.structure_at_privat')]"/>
</function>
</odoo>
@@ -0,0 +1,90 @@
# Rechenfälle FI-Buchung UGB (GP6) — KFS/BW 6-Buchungsmatrix
**Quellen:** Österreichischer Einheitskontenrahmen **KFS/BW 6**
(Fachsenat Betriebswirtschaft der Kammer der Wirtschaftstreuhänder
2017; Nutzer-Referenz-Exemplar `.additional_docs/KFSBW6_13072017_
RF.pdf`, extrahiert nach `/tmp/kfsbw6/` — ad hoc, unversioniert);
Nutzer-Entscheidungen 2026-09-12 (NET-Konto 3850; Arbeiter/Angestellte-
Split bestätigt — 600619 Löhne, 620639 Gehälten zugeordnet;
BV-Verbindlichkeit 3600; BV-Aufwand Kandidaten 6440 vor 6400).
Umsetzung: `models/account_chart_private.py` (Matrix + Hook
`_configure_payroll_account_at`), `models/account_posting_private.py`
(EKR-Debit-Split), `data/hr_payroll_account_private.xml` (Trigger für
Bestands-Companies). Tests: `test_account_private.py`.
## Systematik (GP6, verifiziert)
- **Odoo-Mechanik:** `hr_payroll_account` buchet je Payslip-Linie auf
`hr.salary.rule.account_debit/account_credit` (Company-Properties);
Posting-Schema: Brutto-Regeln **nur Debit** (Aufwand), DN-Abzüge
**nur Credit** (Verrechnung), NET **nur Credit** auf das
**reconcilable** NET-Konto (`action_register_payment` prüft
`NET.account_credit.reconcile` — deshalb Regelcode-Umbenennung
NETTO → **NET**), DG-Regeln Debit + Credit. Der Move balanciert
automatisch; NET-Konto-Saldo = Netto.
- **Hook-Trigger (beide idempotent):** (1) Chart-Load des
AT-Kontenrahmens → `account.chart.template._post_load_data`
`_load_payroll_accounts('at', company)`; (2) `<function>`-Datendatei
für Bestands-Companies (`res.company.chart_template = 'at'`;
der ERSTE `<value>` ist die Record-ID-Liste des Aufrufs — Muster
`l10n_be_hr_payroll_account`). Fehlende Konten werden nur gewarnt
(Kontenplan der Referenzbuchhaltung ist verbindlich, GP0-Rest);
Ausnahme NET-Konto **3850** („Verbindlichkeiten aus Lohn und
Gehalt", EKR-Bereich 370389): wird angelegt und auf reconcile
gesetzt — l10n_at-Standardchart hat es nicht.
- **EKR-Debit-Split Arbeiter/Angestellte** (KFS/BW 6: 600619 Löhne,
620639 Gehälter, 650655/656659 Sozialaufwand, 660665/666669
Abgaben): `l10n_at_personalgruppe` am Vertrag steuert je Regel das
Override-Debitkonto `l10n_at_account_debit_arbeiter`
(Gesamtüberschreibung von `_prepare_slip_lines` nach Upstream-Muster,
Odoo-Pin 19.0+e.20260910 — kein nativer Override-Punkt). Credits
gruppenneutral. **Option A minimal:** Overrides nur für
Grundentgelt **6000**, Sozialaufwand **6500**, Abgaben
**6600/6601/6602**; Zuschläge (6225), ÜStd (6310), Prämien (6230),
SZ (6240) und BV (6440, EKR 644 ohne Split) buchen für Arbeiter bis
zur Referenzbuchhaltungs-Fixierung auf die Angestellten-Konten.
## Buchungsmatrix
| Regel | Debit Angestellte | Debit Arbeiter (Override) | Credit |
|---|---|---|---|
| KVGEO, URL_UE | 6200 Gehälter | **6000 Löhne** | — |
| NACHT_SV, MEHR_* | 6225 Zulagen | — (Option A) | — |
| UE_* | 6310 Überstunden | — (Option A) | — |
| PRAM | 6230 Prämien | — (Option A) | — |
| B13, B14, B14_RUECK | 6240 Sonderzahlungen | — (Option A) | — |
| SVDN_KV/PV/AV/AK/WF, SVDN_SZ | — | — | 3600 SV |
| LST_LFD, LST_SZ | — | — | 3540 Lohnsteuer |
| **NET** | — | — | **3850 Verbindlichkeiten Lohn/Gehalt** (reconcile) |
| SVDG_* (inkl. SZ, UV_GFG) | 6560 Sozialaufwand Angest. | **6500** | 3600 |
| BVG | 6440 (EKR 644; Fallback 6400) | — | **3600** (Abfuhr via KV-Träger) |
| FLAFDB | 6661 FLAF-DB | 6601 | 3541 Verrechnung DG-Beitrag |
| KOMMST | 6660 KommSt | 6600 | 3550 Verrechnung KommSt |
| DZ | 6662 DZ | 6602 | 3542 Verrechnung DG-Zuschlag |
Journal: **SLR Lohnverrechnung** (Typ allgemein, Default-Konto
**6200 Gehälter** — Ausgleichsbuchung bei Unbalance, BE-Muster:
Journal-Default = Bezugs-Aufwandskonto; Odoo-Constraint verbietet
Receivable/Payable-Konten als Journal-Default); Structure →
`journal_id`.
## Rechenfälle (Fixtures test_account_private.py)
| RF | Konstellation | Erwartung |
|---|---|---|
| RF-BUCH-1 | Angestellte(r), November mit B13; Hook zweimal | Matrix verdrahtet (6200/6240/6560/6440/6660/6661, Credits 3600/3540/3550/3541, NET 3850, Journal SLR); 3850 nur 1×; Move **balanciert**; Zeilensalden je Konto = Payslip-Liniensummen (SV-Saldo 3600 = DN+DG+BV; NET = 3850-Saldo) |
| RF-BUCH-2 | Personalgruppe **Arbeiter**, November mit B13 | Grundentgelt auf **6000** (6200 = 0), SV-DG auf **6500**, KommSt/FLAF auf **6600/6601**; B13 weiter auf 6240 (Option A); DZ ohne WKO-Flag ohne Zeile; balanciert |
| RF-BUCH-3 | Geringfügiger (500 €, §-53a) | 6200 = 500; §-53a-UV 6,50 auf 6560; BVG 7,65 auf 6440; Credit 3600 = 14,15; NET 500 auf 3850; balanciert |
| RF-BUCH-4 | Ohne 6000/6600er/3850 im Plan | Hook warnt (fehlend-Liste), Arbeiter-Override bleibt leer → Fallback 6200 für Grundentgelt; 6500 existiert → SV-Override gesetzt; NET-Konto 3850 wird angelegt (reconcile) |
## Offene Punkte / bewusst nicht in GP6
- **Arbeiter-Sub-Konten** (Zuschläge/ÜStd/Prämien/SZ im 600er-Bereich)
und die 6600er-Kontenbelegung: Option A minimal — endgültige Codes
fixiert die Referenzbuchhaltung (GP0-Rest); Company-Properties sind
übersteuerbar, fehlende Kandidaten führen nur zu einer Warnung.
- Kein Mapping für DAG (keine Payslip-Zeile, Ausweis GP8), keine
Aufrollungs-/Beitragsmonats-Sonderbuchungen (GP8), kein L16/mBGM
(GP7). Kein VRV/MVAG/Ansatz — Gemeinde-Schiene bleibt gem360-Brücke.
- Auszahlung über `hr_payroll_account_iso20022` (SEPA) — Payment-
Registrierung setzt NET-account_credit.reconcile voraus (✓ 3850).
@@ -4,4 +4,6 @@ from . import hr_version_private
from . import payslip_private
from . import bv_kasse
from . import abfertigung_private
from . import bv_report
from . import bv_report
from . import account_chart_private
from . import account_posting_private
@@ -0,0 +1,238 @@
# Part of the odoo-at-payroll project. License: LGPL-3.
"""GP6 FI-Buchung UGB: Buchungsmatrix nach dem Österreichischen
Einheitskontenrahmen (KFS/BW 6, Fachsenat Betriebswirtschaft der
Kammer der Wirtschaftstreuhänder 2017 — Nutzer-Referenz 2026-09-12)
für die privatwirtschaftliche Structure.
Matrix (Nutzer-Entscheidungen 2026-09-12): Angestellte buchen auf
Gehälter (620639), Sozialaufwand Angestellte (656659) und
gehaltsabhängige Abgaben (666669); Arbeiter über das Debit-Override-
Feld ``l10n_at_account_debit_arbeiter`` auf Löhne (600619),
Sozialaufwand Arbeiter (650655) und lohnabhängige Abgaben (660665)
— Option A minimal: nur Grundentgelt 6000, Sozialaufwand 6500 und
Abgaben 6600/6601/6602; Zuschläge/ÜStd/SZ/Prämien buchen bis zur
Referenzbuchhaltungs-Fixierung auf die Angestellten-Konten. Credits
sind gruppenneutral (Verrechnungskonten 3540/3541/3542/3550/3600).
NET-Konto **3850 „Verbindlichkeiten aus Lohn und Gehalt"** (EKR-Bereich
370389 übrige sonstige Verbindlichkeiten) wird angelegt, wenn es
fehlt, und auf reconcile gesetzt — die Zahlungsregistrierung prüft
``NET.account_credit.reconcile``. BV-Verbindlichkeit 3600 (Abfuhr
über den KV-Träger); BV-Aufwand: Kandidatensuche 6440 (EKR 644)
vor 6400 (Odoo-l10n_at-Standard).
Trigger: (1) automatisch beim Laden des AT-Kontenrahmens
(``account.chart.template._post_load_data`` →
``_load_payroll_accounts``, Kontext ``chart_template_load``) und
(2) über die ``<function>``-Datendatei für Bestands-Companies bei
Modul-Install/-Update. Beide Pfade sind idempotent.
"""
import logging
from odoo import fields, models
_logger = logging.getLogger(__name__)
# Buchungsmatrix: Regel-XML-ID → Konten-Kandidaten (Präfixsuche je
# Company-Chart; erster Treffer gewinnt; fehlende Konten werden nur
# gewarnt — der Referenzbuchhaltungs-Kontenplan ist verbindlich,
# GP0-Rest). 'debit' = Standard (Angestellte), 'debit_arbeiter' =
# Override für Personalgruppe Arbeiter, 'credit' = Verrechnungskonto.
L10N_AT_BUCHUNGSMATRIX = {
# Entgelt (KFS/BW 6: 600619 Löhne · 620639 Gehälter)
'rule_kvgeo': {'debit': ('6200',), 'debit_arbeiter': ('6000',)},
'rule_url_ue': {'debit': ('6200',), 'debit_arbeiter': ('6000',)},
'rule_nacht_sv': {'debit': ('6225',)},
'rule_mehr_vz': {'debit': ('6225',)},
'rule_mehr_vz_nacht': {'debit': ('6225',)},
'rule_mehr_tz': {'debit': ('6225',)},
'rule_mehr_tz_nacht': {'debit': ('6225',)},
'rule_ue_std': {'debit': ('6310',)},
'rule_ue_nacht': {'debit': ('6310',)},
'rule_ue_nacht_frueh': {'debit': ('6310',)},
'rule_ue_sofei': {'debit': ('6310',)},
'rule_ue_sofei_nacht': {'debit': ('6310',)},
'rule_pram': {'debit': ('6230',)},
'rule_b13': {'debit': ('6240',)},
'rule_b14': {'debit': ('6240',)},
'rule_b14_rueck': {'debit': ('6240',)},
# Abzüge DN (§ 62-Kette) und Lohnsteuer — Credit-Verrechnungen
'rule_svdn_kv': {'credit': ('3600',)},
'rule_svdn_pv': {'credit': ('3600',)},
'rule_svdn_av': {'credit': ('3600',)},
'rule_svdn_ak': {'credit': ('3600',)},
'rule_svdn_wf': {'credit': ('3600',)},
'rule_svdn_sz': {'credit': ('3600',)},
'rule_lst_lfd': {'credit': ('3540',)},
'rule_lst_sz': {'credit': ('3540',)},
# Nettoauszahlung (reconcilable, s. o.)
'rule_netto': {'credit': ('3850',)},
# DG-Beiträge (650655 SV Arbeiter · 656659 SV Angestellte)
'rule_svdg_kv': {'debit': ('6560',), 'debit_arbeiter': ('6500',),
'credit': ('3600',)},
'rule_svdg_pv': {'debit': ('6560',), 'debit_arbeiter': ('6500',),
'credit': ('3600',)},
'rule_svdg_av': {'debit': ('6560',), 'debit_arbeiter': ('6500',),
'credit': ('3600',)},
'rule_svdg_uv': {'debit': ('6560',), 'debit_arbeiter': ('6500',),
'credit': ('3600',)},
'rule_svdg_wf': {'debit': ('6560',), 'debit_arbeiter': ('6500',),
'credit': ('3600',)},
'rule_svdg_iesg': {'debit': ('6560',), 'debit_arbeiter': ('6500',),
'credit': ('3600',)},
'rule_svdg_sz_kv': {'debit': ('6560',), 'debit_arbeiter': ('6500',),
'credit': ('3600',)},
'rule_svdg_sz_pv': {'debit': ('6560',), 'debit_arbeiter': ('6500',),
'credit': ('3600',)},
'rule_svdg_sz_av': {'debit': ('6560',), 'debit_arbeiter': ('6500',),
'credit': ('3600',)},
'rule_svdg_sz_uv': {'debit': ('6560',), 'debit_arbeiter': ('6500',),
'credit': ('3600',)},
'rule_svdg_sz_iesg': {'debit': ('6560',),
'debit_arbeiter': ('6500',),
'credit': ('3600',)},
'rule_svdg_uv_gfg': {'debit': ('6560',), 'debit_arbeiter': ('6500',),
'credit': ('3600',)},
# Abfertigung Neu: Aufwand 644 (EKR) mit Fallback Odoo-Standard
# 6400; Abfuhr über den KV-Träger → Verrechnung 3600 (5.3).
'rule_bvg': {'debit': ('6440', '6400'), 'credit': ('3600',)},
# Lohnabhängige Abgaben: 666er gehaltsabhängig (Angestellte),
# 660er lohnabhängig (Arbeiter); Verrechnungen 3541/3550/3542.
'rule_flafdb': {'debit': ('6661',), 'debit_arbeiter': ('6601',),
'credit': ('3541',)},
'rule_kommst': {'debit': ('6660',), 'debit_arbeiter': ('6600',),
'credit': ('3550',)},
'rule_dz': {'debit': ('6662',), 'debit_arbeiter': ('6602',),
'credit': ('3542',)},
}
class HrSalaryRule(models.Model):
_inherit = 'hr.salary.rule'
l10n_at_account_debit_arbeiter = fields.Many2one(
'account.account', string='AT Debitkonto Arbeiter (EKR)',
company_dependent=True, ondelete='restrict',
help='Debit-Override der Lohnverrechnungsbuchung für Arbeiter '
'(KFS/BW 6: Löhne 600619, Sozialaufwand Arbeiter '
'650655, lohnabhängige Abgaben 660665). Leer = '
'Buchung auf das Standard-Debitkonto (Angestellte). '
'Nur Aufwandskonten splitten; Verrechnungskonten sind '
'gruppenneutral.')
class AccountChartTemplate(models.AbstractModel):
_inherit = 'account.chart.template'
def _l10n_at_buchungskonto(self, company, codes):
"""Konto je Kandidaten-Code: exakter Code zuerst, sonst
Praefix-Fallback nur fuer leicht verlaengerte Konten (max. eine
Stelle, z. B. 6200 → 62000) — blockiert Odoo-Scaffold-Konten
chartloser Firmen (6000 darf nicht auf 600000 „Expenses"
matchen). Leer, wenn nichts Passendes existiert."""
account_model = self.env['account.account'].with_company(company)
basis = account_model._check_company_domain(company)
for code in codes:
account = account_model.search(
basis + [('code', '=', code)], limit=1)
if account:
return account
kandidaten = account_model.search(
basis + [('code', '=like', '%s%%' % code)])
account = kandidaten.filtered(
lambda a: len(a.code) <= len(code) + 1)[:1]
if account:
return account
return self.env['account.account']
def _l10n_at_netto_konto(self, company):
"""NET-Konto 3850 'Verbindlichkeiten aus Lohn und Gehalt'
(EKR 370389): anlegen, wenn es fehlt; reconcile erzwingen —
die Zahlungsregistrierung prüft NET.account_credit.reconcile
konten (hr_payroll_account) und l10n_at führt alle Verrechnungs-
konten reconcile."""
konto = self._l10n_at_buchungskonto(company, ('3850',))
if not konto:
konto = self.env['account.account'].with_company(
company).create({
'name': 'Verbindlichkeiten aus Lohn und Gehalt',
'code': '3850',
'account_type': 'liability_payable',
'reconcile': True,
})
elif not konto.reconcile:
konto.reconcile = True
return konto
def _l10n_at_lohnjournal(self, company, aufwand_konto):
"""Lohnjournal SLR (Code des hr_payroll_account-Templates)
suchen oder anlegen. Default-Konto = Gehälter-Aufwandskonto
(6200, BE-Muster: Journal-Default des Lohnjournals ist das
Bezugs-Aufwandskonto; die Unbalance-Ausgleichsbuchung
_prepare_adjust_line läuft darüber) — NICHT das NET-Konto:
Journal-Defaultkonten dürfen nach Odoo-Constraint nicht
Receivable/Payable sein (Constraint
_check_account_is_bank_journal_bank_account)."""
journal_model = self.env['account.journal'].with_company(company)
journal = journal_model.search([
*journal_model._check_company_domain(company),
('code', '=', 'SLR'),
], limit=1)
if not journal:
journal = journal_model.create({
'name': 'Lohnverrechnung',
'code': 'SLR',
'type': 'general',
})
if not journal.default_account_id and aufwand_konto:
journal.default_account_id = aufwand_konto.id
return journal
def _configure_payroll_account_at(self, companies):
"""Buchungsmatrix je Company anwenden (idempotent): Konten je
Kandidaten-Code suchen (fehlende nur warnen — der Kontenplan
der Referenzbuchhaltung ist verbindlich, GP0-Rest), NET-Konto
3850 sicherstellen, Journal SLR zuordnen."""
netto_konto = False
journal = False
fehlend = []
for company in companies:
netto_konto = self._l10n_at_netto_konto(company)
gehaelter_konto = self._l10n_at_buchungskonto(company, ('6200',))
if not gehaelter_konto:
fehlend.append('rule_kvgeo → (\'6200\',) [Journal-Standard]')
journal = self._l10n_at_lohnjournal(company, gehaelter_konto)
for xmlid, mapping in L10N_AT_BUCHUNGSMATRIX.items():
rule = self.env.ref(
'l10n_at_hr_payroll_private.%s' % xmlid, raise_if_not_found=False)
if not rule:
continue
vals = {}
for feld, schluessel in (
('account_debit', 'debit'),
('l10n_at_account_debit_arbeiter', 'debit_arbeiter'),
('account_credit', 'credit')):
codes = mapping.get(schluessel)
if not codes:
continue
if codes == ('3850',):
konto = netto_konto
else:
konto = self._l10n_at_buchungskonto(company, codes)
if not konto:
fehlend.append('%s%s' % (xmlid, codes))
continue
vals[feld] = konto.id
if vals:
rule.with_company(company).write(vals)
struktur = self.env.ref(
'l10n_at_hr_payroll_private.structure_at_privat',
raise_if_not_found=False)
if struktur:
struktur.with_company(company).journal_id = journal
if fehlend:
_logger.warning(
'AT-Payroll-Buchungsmatrix: fehlende Konten nicht '
'zugeordnet (Referenzbuchhaltung pflegen): %s',
', '.join(fehlend))
return {'netto_konto': netto_konto, 'journal': journal,
'fehlend': fehlend}
@@ -0,0 +1,89 @@
# Part of the odoo-at-payroll project. License: LGPL-3.
"""GP6: EKR-Debit-Split Arbeiter/Angestellte im Buchungslauf.
``hr_payroll_account`` liest die Buchungskonten statisch je Regel
(``hr.salary.rule.account_debit`` — Company-Property). Der Arbeiter/
Angestellte-Split des Einheitskontenrahmen braucht dieselbe Regel je
Personalgruppe auf zwei Konten — Odoo bietet dafür keinen Override-
Punkt (Accounts werden direkt in der Posting-Schleife gelesen).
Deshalb Gesamtüberschreibung von ``_prepare_slip_lines`` nach dem
Upstream-Muster (``hr_payroll_account/models/hr_payslip.py``, Odoo-
Pin 19.0+e.20260910) mit genau einer Substitution: Debit-Konto =
``l10n_at_account_debit_arbeiter``, wenn die Personalgruppe des
Vertrags 'arbeiter' ist und ein Override gesetzt ist. Credits sind
gruppenneutral (Verrechnungskonten). Das Verhalten ist durch
``test_account_private.py`` gepinnt; bei Odoo-Upgrades gegen den Pin
neu abgleichen (Muster: hr_payroll_account Upstream).
"""
from odoo import models
from odoo.tools import float_is_zero
class HrPayslip(models.Model):
_inherit = 'hr.payslip'
def _prepare_slip_lines(self, date, line_ids):
self.ensure_one()
precision = self.env['decimal.precision'].precision_get('Payroll')
new_lines = []
for line in self.line_ids.filtered(lambda line: line.category_id):
amount = line.total
if line.code == 'NET': # Check if the line is the 'Net Salary'.
for tmp_line in self.line_ids.filtered(lambda line: line.category_id):
if tmp_line.salary_rule_id.not_computed_in_net: # Check if the rule must be computed in the 'Net Salary' or not.
if amount > 0:
amount -= abs(tmp_line.total)
elif amount < 0:
amount += abs(tmp_line.total)
if float_is_zero(amount, precision_digits=precision):
continue
debit_account = line.salary_rule_id.account_debit
credit_account = line.salary_rule_id.account_credit
# GP6 EKR-Split (KFS/BW 6): Personalgruppe 'arbeiter' am
# Vertrag bucht auf das Arbeiter-Debitkonto, wenn ein
# Override gesetzt ist (Option A minimal); sonst Standard-
# debit (Angestellte). Referenzbuchhaltung pflegt die
# Company-Properties je Kontenplan.
if (self.version_id.l10n_at_personalgruppe == 'arbeiter'
and line.salary_rule_id.l10n_at_account_debit_arbeiter):
debit_account = \
line.salary_rule_id.l10n_at_account_debit_arbeiter
merge_amounts = self.company_id.batch_payroll_move_lines \
or not line.salary_rule_id.employee_move_line
if debit_account.id: # If the rule has a debit account.
if self.company_id.account_storno and self.credit_note:
debit = amount if amount < 0.0 else 0.0
credit = -amount if amount > 0.0 else 0.0
else:
debit = amount if amount > 0.0 else 0.0
credit = -amount if amount < 0.0 else 0.0
debit_line = merge_amounts and next(self._get_existing_lines(
line_ids + new_lines, line, debit_account.id, debit, credit), False)
if not debit_line:
debit_lines = self._prepare_line_values(line, debit_account, date, debit, credit)
new_lines.extend(debit_lines)
else:
debit_line['debit'] += debit
debit_line['credit'] += credit
if credit_account.id: # If the rule has a credit account.
if self.company_id.account_storno and self.credit_note:
debit = -amount if amount > 0.0 else 0.0
credit = amount if amount < 0.0 else 0.0
else:
debit = -amount if amount < 0.0 else 0.0
credit = amount if amount > 0.0 else 0.0
credit_line = merge_amounts and next(self._get_existing_lines(
line_ids + new_lines, line, credit_account.id, debit, credit), False)
if not credit_line:
credit_lines = self._prepare_line_values(line, credit_account, date, debit, credit)
new_lines.extend(credit_lines)
else:
credit_line['debit'] += debit
credit_line['credit'] += credit
return new_lines
@@ -33,6 +33,17 @@ class HrVersion(models.Model):
"z. B. § 18 Anrechnung der Rahmen-KVs).")
currency_id = fields.Many2one(
'res.currency', related='company_id.currency_id')
l10n_at_personalgruppe = fields.Selection(
[('angestellte', 'Angestellte (Gehälter)'),
('arbeiter', 'Arbeiter (Löhne)')],
string='AT Personalgruppe (EKR)',
default='angestellte',
groups="hr_payroll.group_hr_payroll_user", tracking=True,
help='Personalgruppe nach dem Einheitskontenrahmen (KFS/BW 6): '
'steuert die Debit-Konten der Lohnverrechnungsbuchung '
'(600619 Löhne · 620639 Gehälter · 650/656 Sozialaufwand '
'· 660/666 Abgaben). Rechenlogik (KV-Regime) ist identisch; '
'nur die FI-Buchung splittet (GP6).')
def _get_kv_gehalt(self, datum=None):
"""Katalog-Position (Monatsgehalt) der Version zum Stichtag."""
@@ -55,4 +66,7 @@ class HrEmployee(models.Model):
ueberzahlung = fields.Monetary(
related="version_id.ueberzahlung", inherited=True,
readonly=False, groups="hr_payroll.group_hr_payroll_user",
currency_field='currency_id')
currency_field='currency_id')
l10n_at_personalgruppe = fields.Selection(
related="version_id.l10n_at_personalgruppe", inherited=True,
readonly=False, groups="hr_payroll.group_hr_payroll_user")
@@ -14,6 +14,32 @@ class HrWorkEntryType(models.Model):
'(zuschlagsunabhängig, kumulativ).')
class HrPayrollStructure(models.Model):
_inherit = 'hr.payroll.structure'
def _l10n_at_entferne_default_net_kopie(self):
"""Odoo kopiert beim Anlegen einer Structure die Regeln des
``hr_payroll.default_structure`` (BASIC/GROSS/NET/ASSIG_
SALARY/…) in ``rule_ids`` (Feld-Default ``_get_default_
rule_ids``). Die kopierte NET-Regel kollidiert ab GP6 mit
unserer Netto-Regel (Code ``NET`` nach Odoo-Konvention —
Zahlungsregistrierung, net_wage und Negativ-Netto-Erkennung
filtern ``code == 'NET'``): zwei Regeln mit Code NET in einer
Structure überschreiben sich im ``result_rules``-Dict und die
Netto-Zeile fällt auf 0. Die Kopie wird daher archiviert —
One2many-Liesungen (rule_ids, Compute-Schleife, NET-Filter)
blenden Archivierte aus, historische Lohnkontolinien behalten
aber ihre Regel-Referenz (struct_id ist NOT NULL, ein Abhängen
ist nicht möglich). Idempotent (Aufruf via <function> in
salary_rules_private.xml bei Install und Update)."""
self.ensure_one()
unsere = self.env.ref(
'l10n_at_hr_payroll_private.rule_netto', False)
kopien = self.rule_ids.filtered(
lambda r: r.code == 'NET' and (not unsere or r != unsere))
kopien.write({'active': False})
class HrVersionPrivate(models.Model):
_inherit = 'hr.version'
@@ -3,4 +3,5 @@ from . import test_kv_katalog
from . import test_entgelt_private
from . import test_lst_private
from . import test_sv_private
from . import test_bv_private
from . import test_bv_private
from . import test_account_private
@@ -0,0 +1,279 @@
# Part of the odoo-at-payroll project. License: LGPL-3.
"""GP6 FI-Buchung UGB: Buchungsmatrix (KFS/BW 6 — Österreichischer
Einheitskontenrahmen) und EKR-Debit-Split Arbeiter/Angestellte.
Hook-Anwendung (Kontenzuordnung/Journal/NET-Konto), balancierte
Moves mit Kontenzeilen je Matrix, Arbeiter-Overrides (Option A
minimal) und Fallback-Verhalten bei fehlenden Konten.
Rechenfälle: docs/rechenfaelle/buchhaltung.md. Der Kontenplan wird
im Test angelegt (Kandidaten-Präfixsuche des Hooks wie im echten
Chart) — ein Chart-Install ist dafür nicht nötig."""
from datetime import date, timedelta
from odoo.tests import TransactionCase, tagged
@tagged('post_install', '-at_install')
class AccountPrivateTest(TransactionCase):
def setUp(self):
super().setUp()
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 Mo-Fr',
'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.env.company.resource_calendar_id = self.calendar
# ------------------------------------------------------------------
# Kontenplan-Mock (Kandidaten wie in models/account_chart_private)
# ------------------------------------------------------------------
def _konto(self, code, typ='expense', reconcile=False):
return self.env['account.account'].create({
'name': 'Test %s' % code,
'code': code,
'account_type': typ,
'reconcile': reconcile,
})
def _kontenplan(self, ohne=()):
"""EKR-Kontenplan-Mock; ``ohne`` lässt Kandidaten bewusst weg
(RF-BUCH-4: Fallback statt Erfindung)."""
for code in ('6000', '6200', '6225', '6230', '6240', '6310',
'6440', '6500', '6560', '6600', '6601', '6602',
'6660', '6661', '6662'):
if code not in ohne:
self._konto(code, 'expense')
for code in ('3540', '3541', '3542', '3550', '3600'):
self._konto(code, 'liability_current', reconcile=True)
if '3850' not in ohne:
self._konto('3850', 'liability_payable', reconcile=True)
def _hook(self):
return self.env['account.chart.template'] \
._configure_payroll_account_at(self.env.company)
def _employee(self, version_vals=None):
employee = self.env['hr.employee'].create({
'name': 'Test FI privat',
})
version = self.env['hr.version'].search(
[('employee_id', '=', employee.id)], limit=1)
if not version:
version = self.env['hr.version'].create({
'employee_id': employee.id,
'date_start': date(2020, 1, 1)})
version.write({
'contract_date_start': date(2020, 1, 1),
'kv_id': self.kv2203.id,
'kv_gruppe_id': self.gruppe_ii.id,
'erfahrungsstufe': 1,
'resource_calendar_id': self.calendar.id,
})
if version_vals:
version.write(version_vals)
return employee
def _slip(self, employee, monat, jahr=2026, confirm=False):
erster = date(jahr, monat, 1)
naechster = (erster + timedelta(days=32)).replace(day=1)
slip = self.env['hr.payslip'].create({
'name': 'Test-FI-Privat %d/%d' % (monat, jahr),
'employee_id': employee.id,
'struct_id': self.structure.id,
'date_from': erster,
'date_to': naechster - timedelta(days=1),
})
slip.compute_sheet()
if confirm:
slip.action_payslip_done()
return slip
def _total(self, slip, code):
return sum(slip.line_ids.filtered(
lambda l: l.code == code).mapped('total'))
def _saldo(self, lines, code):
"""Debit Credit je Kontoklasse des Moves."""
teil = lines.filtered(lambda l: l.account_id.code == code)
return sum(teil.mapped('debit')) - sum(teil.mapped('credit'))
# ------------------------------------------------------------------
# RF-BUCH-1: Matrix, NET-Konto, balancierter Move (Angestellte)
# ------------------------------------------------------------------
def test_01_matrix_und_move_angestellte(self):
"""Hook verdrahtet die Regelkonten (6200/6240/6560/6440/6660/
6661, Verrechnungen 3600/3540/3550/3541, NET 3850 reconcile,
Journal SLR) und ist idempotent; der November-Lauf mit B13
buchet balanciert — jede Zeile auf ihrem Matrix-Konto."""
self._kontenplan()
ergebnis = self._hook()
self.assertEqual(ergebnis['netto_konto'].code, '3850')
self.assertTrue(ergebnis['netto_konto'].reconcile)
self.assertTrue(ergebnis['journal'])
self.assertEqual(ergebnis['fehlend'], [])
# Stichproben der Matrix (Angestellte = Standard-Debit)
ref = self.env.ref
module = 'l10n_at_hr_payroll_private'
self.assertEqual(
ref('%s.rule_kvgeo' % module).account_debit.code, '6200')
self.assertEqual(
ref('%s.rule_b13' % module).account_debit.code, '6240')
self.assertEqual(
ref('%s.rule_svdg_kv' % module).account_debit.code, '6560')
self.assertEqual(
ref('%s.rule_svdg_kv' % module).account_credit.code, '3600')
self.assertEqual( # EKR 6440 vor Odoo-Fallback 6400
ref('%s.rule_bvg' % module).account_debit.code, '6440')
self.assertEqual(
ref('%s.rule_netto' % module).account_credit.code, '3850')
# Idempotenz: zweiter Lauf erzeugt kein zweites NET-Konto.
self._hook()
self.assertEqual(self.env['account.account'].search_count([
('code', '=like', '3850%')]), 1)
# Move: balanciert, Zeilen je Matrix-Konto
employee = self._employee()
slip = self._slip(employee, 11, confirm=True)
self.assertTrue(slip.move_id)
lines = slip.move_id.line_ids
self.assertAlmostEqual(sum(lines.mapped('debit')),
sum(lines.mapped('credit')), places=2)
# Aufwands- und Lohnkonten mit Rundungstoleranz: Odoo bucht
# Restcents des Moves als „Adjustment Entry" auf das
# Journal-Default (6200, BE-Muster) — NET-Linie und Summen der
# gerundeten Abzugszeilen können um Cents differieren.
self.assertAlmostEqual(self._saldo(lines, '6200'),
self._total(slip, 'KVGEO'), delta=0.02)
self.assertAlmostEqual(self._saldo(lines, '6240'),
self._total(slip, 'B13'), places=2)
dg = sum(self._total(slip, code) for code in (
'SVDG_KV', 'SVDG_PV', 'SVDG_AV', 'SVDG_UV', 'SVDG_WF',
'SVDG_IESG', 'SVDG_SZ_KV', 'SVDG_SZ_PV', 'SVDG_SZ_AV',
'SVDG_SZ_UV', 'SVDG_SZ_IESG'))
self.assertAlmostEqual(self._saldo(lines, '6560'), dg, places=2)
self.assertAlmostEqual(self._saldo(lines, '6440'),
self._total(slip, 'BVG'), places=2)
self.assertAlmostEqual(self._saldo(lines, '6660'),
self._total(slip, 'KOMMST'), places=2)
self.assertAlmostEqual(self._saldo(lines, '6661'),
self._total(slip, 'FLAFDB'), places=2)
# Verrechnungen: SV gesamt (DN + DG + BV) und Lohnsteuer
sv = dg + self._total(slip, 'BVG') + sum(
self._total(slip, code) for code in (
'SVDN_KV', 'SVDN_PV', 'SVDN_AV', 'SVDN_AK', 'SVDN_WF',
'SVDN_SZ'))
self.assertAlmostEqual(self._saldo(lines, '3600'), -sv, places=1)
lst = sum(self._total(slip, code) for code in
('LST_LFD', 'LST_SZ'))
self.assertAlmostEqual(self._saldo(lines, '3540'), -lst, places=1)
self.assertAlmostEqual(self._saldo(lines, '3550'),
-self._total(slip, 'KOMMST'), places=2)
self.assertAlmostEqual(self._saldo(lines, '3541'),
-self._total(slip, 'FLAFDB'), places=2)
self.assertAlmostEqual(self._saldo(lines, '3850'),
-self._total(slip, 'NET'), places=1)
# ------------------------------------------------------------------
# RF-BUCH-2: EKR-Debit-Split Personalgruppe Arbeiter (Option A)
# ------------------------------------------------------------------
def test_02_arbeiter_debit_split(self):
"""Arbeiter buchen Grundentgelt auf 6000 (Löhne), SV-DG auf
6500, KommSt/FLAF auf 6600/6601; die B13 bleibt auf 6240
(Option A: kein Arbeiter-Override für SZ); Credits
gruppenneutral; DZ ohne WKO-Flag ohne Zeile."""
self._kontenplan()
self._hook()
employee = self._employee(
{'l10n_at_personalgruppe': 'arbeiter'})
slip = self._slip(employee, 11, confirm=True)
lines = slip.move_id.line_ids
self.assertAlmostEqual(sum(lines.mapped('debit')),
sum(lines.mapped('credit')), places=2)
self.assertAlmostEqual(self._saldo(lines, '6000'),
self._total(slip, 'KVGEO'), delta=0.02)
self.assertAlmostEqual(self._saldo(lines, '6200'), 0.0, delta=0.02)
self.assertAlmostEqual(self._saldo(lines, '6240'),
self._total(slip, 'B13'), places=2)
dg = sum(self._total(slip, code) for code in (
'SVDG_KV', 'SVDG_PV', 'SVDG_AV', 'SVDG_UV', 'SVDG_WF',
'SVDG_IESG', 'SVDG_SZ_KV', 'SVDG_SZ_PV', 'SVDG_SZ_AV',
'SVDG_SZ_UV', 'SVDG_SZ_IESG'))
self.assertAlmostEqual(self._saldo(lines, '6500'), dg, places=2)
self.assertAlmostEqual(self._saldo(lines, '6560'), 0.0, places=2)
self.assertAlmostEqual(self._saldo(lines, '6600'),
self._total(slip, 'KOMMST'), places=2)
self.assertAlmostEqual(self._saldo(lines, '6601'),
self._total(slip, 'FLAFDB'), places=2)
self.assertAlmostEqual(self._saldo(lines, '6602'), 0.0, places=2)
# 6440 BV-Aufwand — kein Arbeiter-Override (EKR 644 ohne Split)
self.assertAlmostEqual(self._saldo(lines, '6440'),
self._total(slip, 'BVG'), places=2)
# ------------------------------------------------------------------
# RF-BUCH-3: Geringfügiger (§-53a-Fall) buchet sauber
# ------------------------------------------------------------------
def test_03_geringfuegig_move(self):
"""Geringfügiger: 6200 500,00; §-53a-UV 6,50 auf 6560, BVG
7,65 auf 6440 (beide Credit 3600); NET 500,00 auf 3850 —
Move balanciert."""
self._kontenplan()
self._hook()
employee = self._employee({'ueberzahlung': 500.0})
slip = self._slip(employee, 1, confirm=True)
lines = slip.move_id.line_ids
self.assertAlmostEqual(sum(lines.mapped('debit')),
sum(lines.mapped('credit')), places=2)
self.assertAlmostEqual(self._saldo(lines, '6200'), 500.0, places=2)
self.assertAlmostEqual(self._saldo(lines, '6560'), 6.50, places=2)
self.assertAlmostEqual(self._saldo(lines, '6440'), 7.65, places=2)
self.assertAlmostEqual(self._saldo(lines, '3600'), -(6.50 + 7.65),
places=2)
self.assertAlmostEqual(self._saldo(lines, '3850'), -500.0, places=2)
# ------------------------------------------------------------------
# RF-BUCH-4: Fehlende Konten → Warnung + Fallback, NET-Konto-Anlage
# ------------------------------------------------------------------
def test_04_hook_fehlende_konten(self):
"""Ohne 6000/6600er bleibt der Arbeiter-Override leer
(Buchung fällt auf die Angestellten-Konten zurück), fehlende
Kandidaten werden nur gemeldet; das NET-Konto 3850 legt der
Hook selbst an (reconcile) — Odoo-l10n_at-Chart hat es nicht."""
self._kontenplan(ohne=('6000', '6600', '6601', '6602', '3850'))
ergebnis = self._hook()
self.assertIn('rule_kvgeo', ' '.join(ergebnis['fehlend']))
self.assertIn('rule_kommst', ' '.join(ergebnis['fehlend']))
self.assertEqual(ergebnis['netto_konto'].code, '3850')
self.assertTrue(ergebnis['netto_konto'].reconcile)
ref = self.env.ref
module = 'l10n_at_hr_payroll_private'
self.assertFalse(
ref('%s.rule_kvgeo' % module).l10n_at_account_debit_arbeiter)
# 6500 existiert → SV-Override wird trotzdem gesetzt (Option A)
self.assertEqual(
ref('%s.rule_svdg_kv' % module)
.l10n_at_account_debit_arbeiter.code, '6500')
employee = self._employee(
{'l10n_at_personalgruppe': 'arbeiter'})
slip = self._slip(employee, 11, confirm=True)
lines = slip.move_id.line_ids
# Fallback: Grundentgelt des Arbeiters buchet auf 6200
# (± Restcent-Ausgleich auf dem Journal-Default 6200)
self.assertAlmostEqual(self._saldo(lines, '6200'),
self._total(slip, 'KVGEO'), delta=0.02)
# SV-DG des Arbeiters buchet auf 6500
dg = sum(self._total(slip, code) for code in (
'SVDG_KV', 'SVDG_PV', 'SVDG_AV', 'SVDG_UV', 'SVDG_WF',
'SVDG_IESG', 'SVDG_SZ_KV', 'SVDG_SZ_PV', 'SVDG_SZ_AV',
'SVDG_SZ_UV', 'SVDG_SZ_IESG'))
self.assertAlmostEqual(self._saldo(lines, '6500'), dg, places=2)
@@ -134,11 +134,11 @@ class BvPrivateTest(TransactionCase):
self.assertAlmostEqual(basis, 4118.82, places=2)
self.assertAlmostEqual(self._total(slip, 'BVG'),
4118.82 * 0.0153, places=2)
# Kein Nettoeffekt: ATP_DG bleibt außerhalb der NETTO-Rechnung.
# Kein Nettoeffekt: ATP_DG bleibt außerhalb der NET-Rechnung.
abzug = sum(self._total(slip, code) for code in
('SVDN_KV', 'SVDN_PV', 'SVDN_AV', 'SVDN_AK',
'SVDN_WF', 'SVDN_SZ', 'LST_LFD', 'LST_SZ'))
self.assertAlmostEqual(self._total(slip, 'NETTO'),
self.assertAlmostEqual(self._total(slip, 'NET'),
basis - abzug, places=1)
def test_02_kein_hbg_cap(self):
@@ -385,7 +385,7 @@ class EntgeltPrivateTest(TransactionCase):
for code in ('SVDN_KV', 'SVDN_PV', 'SVDN_AV', 'SVDN_AK',
'SVDN_WF', 'SVDN_SZ', 'LST_LFD', 'LST_SZ'))
self.assertAlmostEqual(
self._total(slip, 'NETTO'), brutto - abzug, places=1)
self._total(slip, 'NET'), brutto - abzug, places=1)
def test_21_allin_deckungsrechnung(self):
"""§ 2g AVRAG: Jahresdeckung = 12 × Pauschale vs. geschuldete
@@ -116,7 +116,7 @@ class LstPrivateTest(TransactionCase):
('SVDN_KV', 'SVDN_PV', 'SVDN_AV', 'SVDN_AK', 'SVDN_WF',
'SVDN_SZ', 'LST_LFD', 'LST_SZ'))
self.assertAlmostEqual(
self._total(slip, 'NETTO'), brutto - abzug, places=1)
self._total(slip, 'NET'), brutto - abzug, places=1)
def test_02_rollung_zweiter_monat(self):
"""Kumulative §-66-Rollung: Februar zahlt die Differenz."""
@@ -143,7 +143,7 @@ class SvPrivateTest(TransactionCase):
('SVDN_KV', 'SVDN_PV', 'SVDN_AV', 'SVDN_AK', 'SVDN_WF',
'SVDN_SZ', 'LST_LFD', 'LST_SZ'))
self.assertAlmostEqual(
self._total(slip, 'NETTO'), brutto - abzug, places=1)
self._total(slip, 'NET'), brutto - abzug, places=1)
def test_03_wien_dg(self):
"""Wien: SVDG_WF 0,75 % (Satzvektor, Wiener WF-Beitragstarif
@@ -242,4 +242,4 @@ class SvPrivateTest(TransactionCase):
# LSt: Bemessung 500 0 → Tarif unterhalb der Belastungszone
self.assertAlmostEqual(self._total(slip, 'LST_LFD'), 0.0, places=1)
self.assertAlmostEqual(
self._total(slip, 'NETTO'), 500.0, places=2)
self._total(slip, 'NET'), 500.0, places=2)