[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:
@@ -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 — 600–619 Löhne, 620–639 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 370–389): wird angelegt und auf reconcile
|
||||
gesetzt — l10n_at-Standardchart hat es nicht.
|
||||
- **EKR-Debit-Split Arbeiter/Angestellte** (KFS/BW 6: 600–619 Löhne,
|
||||
620–639 Gehälter, 650–655/656–659 Sozialaufwand, 660–665/666–669
|
||||
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).
|
||||
@@ -5,3 +5,5 @@ from . import payslip_private
|
||||
from . import bv_kasse
|
||||
from . import abfertigung_private
|
||||
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 (620–639), Sozialaufwand Angestellte (656–659) und
|
||||
gehaltsabhängige Abgaben (666–669); Arbeiter über das Debit-Override-
|
||||
Feld ``l10n_at_account_debit_arbeiter`` auf Löhne (600–619),
|
||||
Sozialaufwand Arbeiter (650–655) und lohnabhängige Abgaben (660–665)
|
||||
— 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
|
||||
370–389 ü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: 600–619 Löhne · 620–639 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 (650–655 SV Arbeiter · 656–659 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 600–619, Sozialaufwand Arbeiter '
|
||||
'650–655, lohnabhängige Abgaben 660–665). 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 370–389): 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 '
|
||||
'(600–619 Löhne · 620–639 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."""
|
||||
@@ -56,3 +67,6 @@ class HrEmployee(models.Model):
|
||||
related="version_id.ueberzahlung", inherited=True,
|
||||
readonly=False, groups="hr_payroll.group_hr_payroll_user",
|
||||
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'
|
||||
|
||||
|
||||
@@ -4,3 +4,4 @@ from . import test_entgelt_private
|
||||
from . import test_lst_private
|
||||
from . import test_sv_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)
|
||||
Reference in New Issue
Block a user