Files
odoo-at-payroll/addons/l10n_at_hr_payroll_private/models/payslip_private.py
T
fegger 27654211fb [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.
2026-09-12 12:58:53 +02:00

438 lines
20 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Part of the odoo-at-payroll project. License: LGPL-3.
from datetime import date, timedelta
from odoo import api, fields, models
class HrWorkEntryType(models.Model):
_inherit = 'hr.work.entry.type'
l10n_at_nacht_sv = fields.Boolean(
string='AT Nacht-Sondervergütung (226)',
help='Stunden dieses Typs fallen ins KV-Nachtfenster 226 Uhr '
'und gebühren der Nacht-Sondervergütung nach KV § 6 '
'(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'
# All-in / Überstundenpauschale § 2g AVRAG (Deckungsrechnung GP2):
# zwingender Grundlohnausweis im Dienstzettel; Pauschale gilt bei
# Unterdeckung nur als Akontozahlung (Deckungsprüfbericht).
l10n_at_ue_pauschale_monatlich = fields.Monetary(
string='ÜStd-Pauschale/Monat (All-in § 2g AVRAG)',
currency_field='currency_id',
help='Monatliche Überstundenpauschale lt. Arbeitsvertrag; '
'0 = kein All-in.')
l10n_at_grundlohn_ausweis = fields.Monetary(
string='Grundlohn lt. Dienstzettel (§ 2g AVRAG)',
currency_field='currency_id',
help='Zwingend ausgewiesener Grundlohn (ohne ÜStd-Anteil) im '
'Arbeitsvertrag/Dienstzettel.')
class HrPayslip(models.Model):
_inherit = 'hr.payslip'
# ------------------------------------------------------------------
# Arbeitstage (resource_calendar) — § 17 Abs 2 KV, user convention:
# Werktage des KV werden als Arbeitstage aus dem Ressourcenkalender
# gelesen (MoFr-Kalender: Faktor = AT Dienstspanne / AT Zeitraum;
# für MoSa-Kalender identisch mit ÷26 × Werktage).
# ------------------------------------------------------------------
def _at_arbeitstage(self, von, bis):
"""Arbeitstage (Kalender-Arbeitstage) im Intervall [von, bis]."""
self.ensure_one()
calendar = self.version_id.resource_calendar_id
if not calendar or not von or not bis or von > bis:
return 0
days = {
int(att.dayofweek)
for att in calendar.attendance_ids
}
if not days:
return 0
count = 0
tag = von
while tag <= bis:
if tag.weekday() in days:
count += 1
tag += timedelta(days=1)
return count
def _at_arbeitstage_faktor(self):
"""Prorate-Faktor Ein-/Austritt: Arbeitstage der Beschäftigungs-
spanne (contract_date_*) im Abrechnungszeitraum ÷ Arbeitstage des
vollen Zeitraums."""
self.ensure_one()
version = self.version_id
von = max(self.date_from, version.contract_date_start) \
if version.contract_date_start else self.date_from
bis = min(self.date_to, version.contract_date_end) \
if version.contract_date_end else self.date_to
if von > bis:
return 0.0
total = self._at_arbeitstage(self.date_from, self.date_to)
if not total:
return 1.0
return self._at_arbeitstage(von, bis) / float(total)
# ------------------------------------------------------------------
# KV-Basis / Grundstunden (Teilungsfaktoren § 5 Abs 6 KV)
# ------------------------------------------------------------------
def _at_kv_gehalt_basis(self, datum=None):
"""Volles Monatsgehalt: Überzahlung überschreibt Katalog;
Teilzeit über work_time_rate (Enterprise-Fracht: Bruchteil 0..1,
compute aus Versionskalender ÷ Firmenzalender — D4-verifiziert)."""
self.ensure_one()
version = self.version_id
datum = datum or self.date_to
if version.ueberzahlung:
gehalt = version.ueberzahlung
else:
pos = self.employee_id.kv_gruppe_id._get_gehalt(
self.employee_id.erfahrungsstufe, datum)
gehalt = pos.gehalt if pos else 0.0
return gehalt * (version.work_time_rate or 1.0)
def _at_grundstunde_ue(self):
"""ÜStd-Grundvergütung (und Zuschlagsbasis) = Monatsgehalt ÷
Teilungsfaktor ÜStd (KV § 5 Abs 6: 2203 ÷ 150, 2748 ÷ 143)."""
self.ensure_one()
teiler = self.employee_id.kv_id._wert(
'teilungsfaktor_ue', self.date_to, raise_if_not_found=False)
if not teiler:
return 0.0
return self._at_kv_gehalt_basis() / float(teiler)
def _at_grundstunde_norm(self):
"""Normalarbeitsstunde/Mehrarbeit = Monatsgehalt ÷
Teilungsfaktor Normalstunde (2748: ÷ 167) bzw. Fallback
÷ (4,33 × KV-Wochenstunden) ohne KV-Teilungsfaktor."""
self.ensure_one()
kv = self.employee_id.kv_id
teiler = kv._wert(
'teilungsfaktor_norm', self.date_to, raise_if_not_found=False)
basis = self._at_kv_gehalt_basis()
if teiler:
return basis / float(teiler)
naz = kv._wert('naz', self.date_to, raise_if_not_found=False) or 40
return basis / (4.33 * float(naz))
def _at_nacht_sv_stunden(self):
"""Stunden der Nacht-Sondervergütung: Work-Entry-Types mit
Nacht-Flag (226-Fenster), unabhängig vom Zuschlagsstatus."""
self.ensure_one()
return sum(
(wd.number_of_hours or 0.0)
for wd in self.worked_days_line_ids
if wd.work_entry_type_id.l10n_at_nacht_sv)
# ------------------------------------------------------------------
# 13./14. Bezug (§ 11 KV) — Basis, Fälligkeit, Aliquotierung
# ------------------------------------------------------------------
def _at_sz_basis_datum(self, code):
"""Stichtag der B13-/B14-Basis: 'november' → 30. 11. des
Jahres (2748 B13), sonst Auszahlungsmonat = payslip-Stichtag."""
self.ensure_one()
methode = self.employee_id.kv_id._wert(
code, self.date_to, raise_if_not_found=False) or 'auszahlung'
if methode == 'november':
return date(self.date_to.year, 11, 30)
return self.date_to
def _at_jahres_versionen(self):
"""Versionen des Mitarbeiters mit Überlappung im Kalenderjahr
der Abrechnung (Mischberechnung Beschäftigungsausmaß, 2203
§ 11 Abs 2)."""
self.ensure_one()
jahr = self.date_to.year
von, bis = date(jahr, 1, 1), date(jahr, 12, 31)
versionen = []
for version in self.env['hr.version'].search([
('employee_id', '=', self.employee_id.id),
]):
v_von = version.date_start or von
v_bis = version.date_end or bis
if max(v_von, von) <= min(v_bis, bis):
versionen.append((version, max(v_von, von), min(v_bis, bis)))
return versionen
def _at_sz_basis(self, datum):
"""B13-/B14-Basis zum Stichtag: einheitliches Beschäftigungs-
ausmaß = aktuelles Monatsgehalt; Änderungen des Ausmaßes im
Kalenderjahr → Kalendertag-gewichtete Mischung über die
Jahres-Versionen (Konvention GP2, s. docs/rechenfaelle/b13b14.md)."""
self.ensure_one()
versionen = self._at_jahres_versionen()
if len(versionen) <= 1:
return self._at_kv_gehalt_basis(datum)
if len({(v.work_time_rate or 100.0) for v, _, _ in versionen}) == 1:
return self._at_kv_gehalt_basis(datum)
# Mischberechnung: Kalendertag-Anteile je Version im Jahr
jahr_von, jahr_bis = date(self.date_to.year, 1, 1), date(self.date_to.year, 12, 31)
tage_jahr = (jahr_bis - jahr_von).days + 1
summe_anteile, summe_gehaelter = 0.0, 0.0
for version, v_von, v_bis in versionen:
anteil = ((v_bis - v_von).days + 1) / float(tage_jahr)
if version.ueberzahlung:
gehalt = version.ueberzahlung
else:
pos = version.kv_gruppe_id._get_gehalt(
version.erfahrungsstufe, v_bis)
gehalt = pos.gehalt if pos else 0.0
summe_anteile += anteil
summe_gehaelter += gehalt * anteil \
* (version.work_time_rate or 1.0)
if not summe_anteile:
return 0.0
return summe_gehaelter / summe_anteile
def _at_sz_dienstzeit_faktor(self):
"""Aliquote Ein-/Austritt (§ 11 Abs 4 KV): Methode je
`sz_aliquote_methode` — 'kalendertage' (Default, user decision:
Gehalt/365 × Kalendertage Dienstzeit) oder 'monate' (volle
Monate ÷ 12, anteilige Monate taggenau)."""
self.ensure_one()
jahr = self.date_to.year
jahr_von, jahr_bis = date(jahr, 1, 1), date(jahr, 12, 31)
methode = self.employee_id.kv_id._wert(
'sz_aliquote_methode', self.date_to,
raise_if_not_found=False) or 'kalendertage'
kt_gesamt, monate_gesamt = 0, 0.0
for version, v_von, v_bis in self._at_jahres_versionen():
kt_gesamt += (v_bis - v_von).days + 1
tag = date(jahr, 1, 1)
while tag <= date(jahr, 12, 31):
if v_von <= tag <= v_bis:
monate_gesamt += 1.0 / ((tag.replace(day=28) + timedelta(days=4)).day)
tag += timedelta(days=1)
if methode == 'monate':
return min(monate_gesamt / 12.0, 1.0)
return min(kt_gesamt / 365.0, 1.0)
def _at_sz_bereits_bezahlt(self, code):
"""Summe bereits bezahlter B13-/B14-Linien im Kalenderjahr
(andere, bestätigte Läufe)."""
self.ensure_one()
jahr = self.date_to.year
linien = self.env['hr.payslip.line'].search([
('employee_id', '=', self.employee_id.id),
('code', '=', code),
('date_from', '>=', date(jahr, 1, 1)),
('date_from', '<=', date(jahr, 12, 31)),
])
linien = linien.filtered(
lambda l: l.slip_id.state not in ('draft', 'cancel')
and l.slip_id.id != self.id)
return sum(linien.mapped('total'))
def _at_urlaubstage_jahr(self):
"""Urlaubs-Work-Entries (is_leave) des Kalenderjahres."""
self.ensure_one()
jahr = self.date_to.year
eintraege = self.env['hr.work.entry'].search([
('version_id', 'in', self.employee_id.version_ids.ids),
('work_entry_type_id.is_leave', '=', True),
])
return sum(
1 for we in eintraege
if we.date.year == jahr)
def _at_b14_faellig(self):
"""B14-Fälligkeit § 11 Abs 3 KV: 'urlaub' → bei Urlaubsantritt
(längster Teil — R1: Auszahlung im erstfälligen Urlaubsmonat,
sofern nicht bereits bezahlt); ohne angetretenen Urlaub im
Dezember (`sz14_no_urlaub_monat`); bei Austritt aliquote
Endabrechnung. Fallback ohne KV-Code: Juni-Abrechnung
(user decision)."""
self.ensure_one()
kv = self.employee_id.kv_id
faelligkeit = kv._wert(
'sz14_faelligkeit', self.date_to, raise_if_not_found=False)
if faelligkeit != 'urlaub':
return self.date_to.month == 6 # Fallback Juni-Abrechnung
if self._at_sz_bereits_bezahlt('B14'):
return False
austritt = (
self.version_id.contract_date_end
and self.date_from <= self.version_id.contract_date_end <= self.date_to)
urlaub_im_zeitraum = any(
wd.work_entry_type_id.is_leave
for wd in self.worked_days_line_ids)
if urlaub_im_zeitraum or austritt:
return True
no_urlaub_monat = int(kv._wert(
'sz14_no_urlaub_monat', self.date_to,
raise_if_not_found=False) or 12)
return (self.date_to.month == no_urlaub_monat
and not self._at_urlaubstage_jahr())
def _at_b13_faellig(self):
"""B13-Fälligkeit: Auszahlmonat je KV (`sz13_monat`) bzw.
bei Austritt aliquote Endabrechnung (§ 11 Abs 4 KV)."""
self.ensure_one()
if self._at_sz_bereits_bezahlt('B13'):
return False
monat = int(self.employee_id.kv_id._wert(
'sz13_monat', self.date_to, raise_if_not_found=False) or 12)
austritt = (
self.version_id.contract_date_end
and self.date_from <= self.version_id.contract_date_end <= self.date_to)
return self.date_to.month == monat or austritt
# ------------------------------------------------------------------
# Urlaubsentgelt — regelmäßige Überstunden (§ 2 Abs 2 GKV;
# 7-von-12-Regel: KV § 5 Abs 11 (2203) / Abs 10 (2748))
# ------------------------------------------------------------------
def _at_urlaub_ue_avg(self):
"""(regelmaessig, durchschnittlicher monatlicher ÜStd-Betrag
der letzten 12 Monate) — nur ÜStd-Regeln, ohne Mehrarbeit.
Achtung: Lohnzettel-Lines tragen Regel-Codes (UE_*), nicht
WET-Codes (ATP_*)."""
self.ensure_one()
codes = ('UE_STD', 'UE_NACHT', 'UE_NACHT_FRUEH',
'UE_SOFEI', 'UE_SOFEI_NACHT')
von = fields.Date.subtract(self.date_from, months=12)
linien = self.env['hr.payslip.line'].search([
('employee_id', '=', self.employee_id.id),
('code', 'in', list(codes)),
('date_from', '>=', von),
]).filtered(lambda l: l.slip_id.state not in ('draft', 'cancel'))
if not linien:
return False, 0.0
monate = {l.slip_id.id for l in linien}
return len(monate) >= 7, sum(l.total for l in linien) / 12.0
# ------------------------------------------------------------------
# Lohnnebenkosten FLAF-DG-Beitrag / KommSt / DZ (GP4; § 41 FLAG,
# § 5+§ 9 KommStG, §§ 122/126 WKG — Werte/Staffeln im Kern)
# ------------------------------------------------------------------
def _at_lnk_basis(self, steuer, result_rules=None):
"""LNK-Bemessungsgrundlage nach Freibetragsstaffel: monatlich
gezahlte Arbeitslöhne (laufend + im Monat gezahlte §-67-Bezüge,
Zuflussprinzip; kein SV-Abzug), ohne §-67-Abs-3/6-Bezüge
(Abfertigung/Abfindung). 'flaf'/'dz' zusätzlich ohne lit-f-AN
(Arbeitslöhne ab Folgemonat nach Vollendung des 60. Lebensjahres,
§ 41 Abs 4 lit f FLAG — die Kommunalsteuer hat KEINE lit-f-
Befreiung). Freibetrag: ≤ 1.095 € keine Abgabe, bis 1.460 € →
Freibetrag 1.095 €, darüber volle BG (at_lnk_freibetrag)."""
self.ensure_one()
basis = (self._l10n_at_brutto('laufend', result_rules)
+ self._l10n_at_brutto('sonstig', result_rules))
for rule in self.struct_id.rule_ids:
if rule.l10n_at_s67_abs in ('abs3', 'abs6'):
if result_rules is not None and rule.code in result_rules:
basis -= result_rules[rule.code]['total']
elif result_rules is None:
linie = self.line_ids.filtered(
lambda l: l.code == rule.code)
basis -= sum(linie.mapped('total'))
basis = max(basis, 0.0)
if steuer in ('flaf', 'dz'):
version = self.version_id
if (version.l10n_at_lnkp_frei_ab
and self.date_from >= version.l10n_at_lnkp_frei_ab):
return 0.0
freibetrag = self.env['hr.rule.parameter']._get_parameter_from_code(
'at_lnk_freibetrag', self.date_to, raise_if_not_found=False) or {}
grenze = float(freibetrag.get('grenze', 0.0))
if basis <= 0.0:
return 0.0
if basis <= grenze:
basis -= float(freibetrag.get('freibetrag', 0.0))
return max(basis, 0.0)
def _at_flaf_db(self, result_rules=None):
"""FLAF-Dienstgeberbeitrag § 41 FLAG (3,7 % ab 2025, 2,7 % ab
2028 — at_flaf_db; BG s. _at_lnk_basis)."""
self.ensure_one()
basis = self._at_lnk_basis('flaf', result_rules)
satz = float(self.env['hr.rule.parameter']._get_parameter_from_code(
'at_flaf_db', self.date_to, raise_if_not_found=False) or 0.0)
return basis * satz / 100.0
def _at_kommst(self, result_rules=None):
"""Kommunalsteuer § 9 KommStG 1993 (3 % — at_kommst_satz; BG
§ 5 Abs 2: ohne §-67-Abs-3/6-Bezüge, ohne lit-f-Befreiung)."""
self.ensure_one()
basis = self._at_lnk_basis('kommst', result_rules)
satz = float(self.env['hr.rule.parameter']._get_parameter_from_code(
'at_kommst_satz', self.date_to, raise_if_not_found=False) or 0.0)
return basis * satz / 100.0
def _at_dz(self, result_rules=None):
"""Zuschlag zum Dienstgeberbeitrag (§§ 122/126 WKG) für WKO-
Mitgliedsbetriebe: Landeskammersatz je Betriebsstätten-Bundesland
inkl. Bundeskammeranteil (at_dz_vektor; ⚠ WKO-Verifikation 8.11);
BG identisch FLAF-DB inkl. lit f (lb-lnk-07)."""
self.ensure_one()
if not self.company_id.l10n_at_wk_mitglied:
return 0.0
basis = self._at_lnk_basis('dz', result_rules)
vektor = self.env['hr.rule.parameter']._get_parameter_from_code(
'at_dz_vektor', self.date_to, raise_if_not_found=False) or {}
satz = float((vektor.get('laender') or {}).get(
self.company_id.l10n_at_bundesland or '', 0.0))
return basis * satz / 100.0
# ------------------------------------------------------------------
# All-in-Deckungsrechnung (§ 2g AVRAG) — minimale GP2-Umsetzung:
# Jahresvergleich geschuldete ÜStd-Vergütung vs. erhaltene Pauschale
# ------------------------------------------------------------------
def _at_allin_deckung(self):
"""Deckungsrechnung für Verträge mit ÜStd-Pauschale:
{'pauschale': x, 'geschuldet': y, 'unterdeckung': z}."""
self.ensure_one()
version = self.version_id
pauschale = version.l10n_at_ue_pauschale_monatlich or 0.0
if not pauschale:
return {'pauschale': 0.0, 'geschuldet': 0.0, 'unterdeckung': 0.0}
jahr = self.date_to.year
codes = ('UE_STD', 'UE_NACHT', 'UE_NACHT_FRUEH',
'UE_SOFEI', 'UE_SOFEI_NACHT')
linien = self.env['hr.payslip.line'].search([
('employee_id', '=', self.employee_id.id),
('code', 'in', list(codes)),
('date_from', '>=', date(jahr, 1, 1)),
('date_from', '<=', date(jahr, 12, 31)),
]).filtered(lambda l: l.slip_id.state not in ('draft', 'cancel'))
geschuldet = sum(linien.mapped('total'))
monate = 12 \
if not version.contract_date_end \
or version.contract_date_end.year > jahr \
else version.contract_date_end.month
erhaltene = pauschale * monate
unterdeckung = max(geschuldet - erhaltene, 0.0)
return {
'pauschale': erhaltene,
'geschuldet': geschuldet,
'unterdeckung': unterdeckung,
}